mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-23 04:17:34 +00:00
feat: Add collab_doc service for real-time document collaboration
Co-authored-by: nicsins <nicsins@gmail.com>
This commit is contained in:
parent
f3c41bca08
commit
1cfd15bd09
4 changed files with 117 additions and 0 deletions
13
services/collab_doc/Dockerfile
Normal file
13
services/collab_doc/Dockerfile
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
COPY app /app/app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
19
services/collab_doc/README.md
Normal file
19
services/collab_doc/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Collab Doc (MVP)
|
||||
|
||||
This is a minimal realtime collaboration **relay** server used by `webui/collab_doc.html`.
|
||||
|
||||
- It does **not** store documents yet (in-memory only, and the CRDT state lives in clients).
|
||||
- It simply forwards websocket text/binary messages to other peers in the same `{doc_id}` room.
|
||||
|
||||
## Run locally (docker-compose)
|
||||
|
||||
If you use `docker/run/docker-compose.yml`, a `collab-doc` service is exposed on port `50081`.
|
||||
|
||||
Health check:
|
||||
|
||||
`http://localhost:50081/health`
|
||||
|
||||
Websocket endpoint:
|
||||
|
||||
`ws://localhost:50081/ws/<doc_id>`
|
||||
|
||||
83
services/collab_doc/app/main.py
Normal file
83
services/collab_doc/app/main.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import DefaultDict, Dict, Set
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Client:
|
||||
websocket: WebSocket
|
||||
|
||||
|
||||
app = FastAPI(title="Collab Doc Relay", version="0.1.0")
|
||||
|
||||
# For browser-based clients hosted on a different port.
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# doc_id -> set of connected clients
|
||||
_rooms: DefaultDict[str, Set[Client]] = defaultdict(set)
|
||||
_locks: DefaultDict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> Dict[str, str]:
|
||||
return {"ok": "true"}
|
||||
|
||||
|
||||
async def _broadcast(doc_id: str, sender: Client, message: str | bytes) -> None:
|
||||
# Snapshot recipients under lock, then send without holding lock.
|
||||
async with _locks[doc_id]:
|
||||
recipients = [c for c in _rooms[doc_id] if c != sender]
|
||||
|
||||
# Best-effort fanout: a failed client doesn't block others.
|
||||
for c in recipients:
|
||||
try:
|
||||
if isinstance(message, bytes):
|
||||
await c.websocket.send_bytes(message)
|
||||
else:
|
||||
await c.websocket.send_text(message)
|
||||
except Exception:
|
||||
# Ignore send errors; disconnect handling will clean up eventually.
|
||||
pass
|
||||
|
||||
|
||||
@app.websocket("/ws/{doc_id}")
|
||||
async def ws_doc(doc_id: str, websocket: WebSocket) -> None:
|
||||
await websocket.accept()
|
||||
client = Client(websocket=websocket)
|
||||
|
||||
async with _locks[doc_id]:
|
||||
_rooms[doc_id].add(client)
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = await websocket.receive()
|
||||
if msg.get("bytes") is not None:
|
||||
await _broadcast(doc_id, client, msg["bytes"])
|
||||
elif msg.get("text") is not None:
|
||||
await _broadcast(doc_id, client, msg["text"])
|
||||
else:
|
||||
# Ignore ping/pong or other frames we don't care about.
|
||||
pass
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
async with _locks[doc_id]:
|
||||
_rooms[doc_id].discard(client)
|
||||
if not _rooms[doc_id]:
|
||||
# Cleanup empty rooms to avoid unbounded growth.
|
||||
_rooms.pop(doc_id, None)
|
||||
_locks.pop(doc_id, None)
|
||||
|
||||
2
services/collab_doc/requirements.txt
Normal file
2
services/collab_doc/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.32.1
|
||||
Loading…
Add table
Add a link
Reference in a new issue