mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 04:15:35 +00:00
Merge branch 'arc53:main' into main
This commit is contained in:
60 files changed
+4696
-1690
No files matched your search
@@ -70,7 +70,7 @@ GET /api/messages/<message_id>/events
|
||||
This replays the message's events past your last-seen sequence number and tails the rest live. It is backed by the Postgres `message_events` journal (retained for `MESSAGE_EVENTS_RETENTION_DAYS`, default 14).
|
||||
|
||||
<Callout type="warning" emoji="⚠️">
|
||||
The chat reconnect endpoint is a native-async route served by the ASGI entrypoint. Under a plain `flask run` dev server it returns `404`; run the backend via the ASGI app (`uvicorn docsgpt.asgi:asgi_app`) or the production gunicorn uvicorn worker to use it. See the [Development Environment](/Deploying/Development-Environment) guide.
|
||||
Both channels are native-async routes served by the ASGI entrypoint. Under a plain `flask run` dev server they return `404`; run the backend via the ASGI app (`uvicorn docsgpt.asgi:asgi_app`) or the production gunicorn uvicorn worker to use them. See the [Development Environment](/Deploying/Development-Environment) guide.
|
||||
</Callout>
|
||||
|
||||
## Settings
|
||||
@@ -81,6 +81,7 @@ The chat reconnect endpoint is a native-async route served by the ASGI entrypoin
|
||||
| `EVENTS_STREAM_MAXLEN` | `1000` | Per-user backlog cap (approximate). |
|
||||
| `SSE_KEEPALIVE_SECONDS` | `15` | Keepalive comment-frame cadence (keep below your proxy's idle timeout). |
|
||||
| `SSE_MAX_CONCURRENT_PER_USER` | `8` | Max simultaneous SSE connections per user (`0` disables the cap). |
|
||||
| `ASYNC_REDIS_MAX_CONNECTIONS` | `2000` | Redis connection pool per API worker. Each open SSE connection holds one, so this caps concurrent streams per worker. |
|
||||
| `EVENTS_REPLAY_MAX_PER_REQUEST` | `200` | Max backlog entries per replay request. |
|
||||
| `EVENTS_REPLAY_BUDGET_REQUESTS_PER_WINDOW` | `30` | Per-user replay requests per window (`0` disables). |
|
||||
| `EVENTS_REPLAY_BUDGET_WINDOW_SECONDS` | `60` | Replay budget window length. |
|
||||
|
||||
@@ -133,7 +133,7 @@ To run the DocsGPT backend locally, you'll need to set up a Python environment a
|
||||
flask --app docsgpt/app.py run --host=0.0.0.0 --port=7091
|
||||
```
|
||||
|
||||
But it serves **only** the WSGI Flask app and omits the routes mounted on the ASGI shell in `docsgpt/asgi.py`: the `/mcp` FastMCP endpoint and the native-async SSE reconnect reader `GET /api/messages/<id>/events`. Under `flask run` those paths return 404 — chat still works (`POST /stream` is a Flask route), but a stream interrupted by a disconnect won't auto-resume on reconnect. Use `flask run` only when you don't need those routes.
|
||||
But it serves **only** the WSGI Flask app and omits the native-async routes mounted on the ASGI shell in `docsgpt/asgi.py`: the `/mcp` FastMCP endpoint, the chat reconnect reader `GET /api/messages/<id>/events`, the notification stream `GET /api/events`, the remote-device command stream `GET /api/devices/sessions/<id>/events`, and artifact downloads `GET /api/artifacts/<id>/download`. Under `flask run` those paths return 404 — chat still works (`POST /stream` is a Flask route), but live notifications, stream auto-resume, paired devices and artifact downloads don't. Use `flask run` only when you don't need them.
|
||||
|
||||
6. **Start the Celery Worker:**
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ flowchart TB
|
||||
Clients["Web UI, widgets and API clients"] --> API
|
||||
|
||||
subgraph Runtime["DocsGPT processes"]
|
||||
API["API process<br/>Flask routes and agent execution<br/>ASGI shell: MCP and chat reconnect"]
|
||||
API["API process<br/>Flask routes and agent execution<br/>ASGI shell: MCP and streaming routes"]
|
||||
Redis["Redis<br/>Task broker, results and realtime delivery"]
|
||||
Worker["Celery workers<br/>Ingestion, parsing, embeddings<br/>and background agent runs"]
|
||||
API -->|"Enqueue work"| Redis
|
||||
@@ -63,7 +63,7 @@ The storage boxes describe responsibilities, not a required number of servers. F
|
||||
|
||||
### API entry points
|
||||
|
||||
[The ASGI application](https://github.com/arc53/DocsGPT/blob/main/docsgpt/asgi.py) mounts Flask through a WSGI adapter, the FastMCP server, and native asynchronous chat-reconnect readers in one process.
|
||||
[The ASGI application](https://github.com/arc53/DocsGPT/blob/main/docsgpt/asgi.py) mounts Flask through a WSGI adapter, the FastMCP server, and native asynchronous routes in one process. The asynchronous routes are the ones that hold a response open: the notification stream, chat reconnect, the remote-device command stream, and artifact downloads. They run on the event loop, so an idle connection doesn't occupy a Flask worker thread.
|
||||
|
||||
| Interface | Purpose |
|
||||
| --- | --- |
|
||||
@@ -76,7 +76,7 @@ The storage boxes describe responsibilities, not a required number of servers. F
|
||||
DocsGPT also acts as an **MCP client**: agents can call external MCP servers through the [MCP tool integration](/Guides/Integrations/mcp-tool-integration). This is a separate direction from exposing DocsGPT search at `/mcp`.
|
||||
|
||||
<Callout type="info">
|
||||
Use the ASGI entry point for the complete application. A plain `flask run` server omits `/mcp` and the native async chat-reconnect routes. See [Development Environment](/Deploying/Development-Environment).
|
||||
Use the ASGI entry point for the complete application. A plain `flask run` server omits `/mcp` and the native asynchronous routes: notifications, chat reconnect, the remote-device command stream, and artifact downloads. See [Development Environment](/Deploying/Development-Environment).
|
||||
</Callout>
|
||||
|
||||
### Agents and retrieval
|
||||
|
||||
@@ -150,6 +150,15 @@ takes over as the fallback parser. If that fallback also extracts almost
|
||||
nothing — OCR off, or no engine available — the upload fails with a clear
|
||||
message instead of silently indexing an empty document.
|
||||
|
||||
Chat attachments are the exception: a scanned PDF attached from the message
|
||||
box is kept with no text (`extraction.status: no_text`), because the file
|
||||
itself is what a model reads. Models that take PDFs get the document; models
|
||||
that take only images get its pages as images; a text-only model is told it
|
||||
cannot read the file, and the message box warns before sending. Images work
|
||||
the same way with OCR off, and TIFF and BMP attachments are stored as PNG,
|
||||
since model providers do not accept those formats. A TIFF or BMP larger than
|
||||
40 million pixels is refused instead of converted.
|
||||
|
||||
Mixed documents — text pages with scanned pages among them — convert
|
||||
through anydoc, which reads the text pages and skips the scanned ones. With
|
||||
OCR on, DocsGPT probes every page's text layer, OCRs the pages that have
|
||||
|
||||
@@ -39,10 +39,10 @@ redis-cli -n 2 XLEN user:<user_id>:stream
|
||||
Worker (publish_user_event) Frontend tab
|
||||
│ ▲
|
||||
▼ │ GET /api/events SSE
|
||||
Redis Streams: XADD Flask route
|
||||
Redis Streams: XADD async route (event loop)
|
||||
user:<id>:stream ──────────────► replay_backlog (snapshot)
|
||||
│ +
|
||||
▼ Topic.subscribe (live tail)
|
||||
▼ AsyncTopic.subscribe (live tail)
|
||||
Redis pub/sub: PUBLISH │
|
||||
user:<id> ────────────────────────────────┘
|
||||
```
|
||||
@@ -158,14 +158,16 @@ null clear propagates into the next reconnect.**
|
||||
|
||||
The per-user concurrent-connection cap (`SSE_MAX_CONCURRENT_PER_USER`,
|
||||
default 8) refused the connection. User has too many tabs open or a
|
||||
runaway reconnect loop. `redis-cli -n 2 GET user:<id>:sse_count`
|
||||
shows the live counter; the TTL is 1h from the last connection
|
||||
attempt (rolling — every INCR re-seeds it), so the key only ages
|
||||
out after the user stops reconnecting for a full hour.
|
||||
runaway reconnect loop. The cap covers `/api/events` and chat reconnects
|
||||
together.
|
||||
|
||||
If the count is wedged high without explanation, the
|
||||
counter-DECR-in-finally path didn't run (worker SIGKILL, OOM). Wait
|
||||
for the TTL or `redis-cli -n 2 DEL user:<id>:sse_count` to reset.
|
||||
Each open stream holds a lease in a sorted set scored by its last
|
||||
refresh; `redis-cli -n 2 ZRANGE user:<id>:sse_leases 0 -1 WITHSCORES`
|
||||
lists them. A live stream refreshes its lease as it sends frames. A
|
||||
lease whose stream died without cleanup (worker SIGKILL, OOM) stops
|
||||
counting once it is older than the lease TTL: 60s, or 4 ×
|
||||
`SSE_KEEPALIVE_SECONDS` if that is larger. The cap heals on its own;
|
||||
`redis-cli -n 2 DEL user:<id>:sse_leases` clears it immediately.
|
||||
|
||||
### G. "Replay snapshot stops at 200 events"
|
||||
|
||||
@@ -204,11 +206,13 @@ the snapshot would re-serve from the start, the cap would re-trip).
|
||||
logs to confirm the publisher is still firing.
|
||||
- Pull `event.connect user=<id>` from the API logs to confirm the
|
||||
client is reconnecting.
|
||||
- Check the gunicorn worker count and `WSGIMiddleware(workers=32)` —
|
||||
if the deploy reduced worker count, the per-user cap is still 8
|
||||
but total concurrent SSE connections are bounded by `gunicorn
|
||||
workers × 32`. A capacity miss looks like users randomly getting
|
||||
429'd.
|
||||
- Check the async Redis pool. Every open `/api/events` stream holds
|
||||
one connection from it, so concurrent streams per worker are bounded
|
||||
by `ASYNC_REDIS_MAX_CONNECTIONS` (default 2000). When the pool is
|
||||
full the API logs `async Redis pool exhausted subscribing to
|
||||
user:<id>` and the stream closes at once, so clients reconnect in a
|
||||
loop. Raise the setting (keeping the total across workers under the
|
||||
Redis server's `maxclients`) or add workers.
|
||||
|
||||
---
|
||||
|
||||
@@ -303,8 +307,8 @@ redis-cli -n 2 PUBSUB NUMSUB $(redis-cli -n 2 PUBSUB CHANNELS 'user:*')
|
||||
# subscribers; OK after explaining to the user)
|
||||
redis-cli -n 2 XTRIM user:<id>:stream MAXLEN 0
|
||||
|
||||
# Clear a wedged concurrent-connection counter
|
||||
redis-cli -n 2 DEL user:<id>:sse_count
|
||||
# Clear a user's SSE connection leases (they also age out on their own)
|
||||
redis-cli -n 2 DEL user:<id>:sse_leases
|
||||
|
||||
# Force-flip every client to re-snapshot (drop the stream key entirely
|
||||
# — destroys the backlog; clients reconnect with their last id and
|
||||
@@ -324,6 +328,7 @@ Everything in `docsgpt/core/settings.py`:
|
||||
| `EVENTS_STREAM_MAXLEN` | `1000` | Per-user backlog cap. Approximate via `XADD MAXLEN ~`. |
|
||||
| `SSE_KEEPALIVE_SECONDS` | `15` | Comment-frame cadence. Must sit under reverse-proxy idle close. |
|
||||
| `SSE_MAX_CONCURRENT_PER_USER` | `8` | Cap on simultaneous SSE connections per user. 0 = disabled. |
|
||||
| `ASYNC_REDIS_MAX_CONNECTIONS` | `2000` | Async Redis pool per API worker; each open stream holds one connection. |
|
||||
| `EVENTS_REPLAY_MAX_PER_REQUEST` | `200` | Hard cap on snapshot rows per request. |
|
||||
| `EVENTS_REPLAY_BUDGET_REQUESTS_PER_WINDOW` | `30` | Per-user replays per window. 0 = disabled. |
|
||||
| `EVENTS_REPLAY_BUDGET_WINDOW_SECONDS` | `60` | Window length. |
|
||||
@@ -360,22 +365,16 @@ running task and the terminal SSE arrives later, the toast pops
|
||||
back. Intentional ("notify the user it's done"); revisit if the
|
||||
re-surface UX is too aggressive for v2.
|
||||
|
||||
### Reconnect reader needs the ASGI entrypoint
|
||||
### Both SSE channels need the ASGI entrypoint
|
||||
|
||||
The chat-stream reconnect reader `GET /api/messages/<id>/events`
|
||||
is a native-async Starlette route mounted in
|
||||
`docsgpt/asgi.py`, not a Flask route. Plain `flask run`
|
||||
serves only the WSGI Flask app, so under it that endpoint 404s
|
||||
and reconnect-after-disconnect can't resume. Run the backend via
|
||||
`uvicorn docsgpt.asgi:asgi_app --reload` (or the production
|
||||
gunicorn uvicorn-worker) to exercise it.
|
||||
|
||||
### Werkzeug doesn't auto-reload route files
|
||||
|
||||
The dev server (`flask run`) doesn't watch
|
||||
`docsgpt/api/events/routes.py` for changes by default.
|
||||
After editing the route, restart Flask manually — `--reload`
|
||||
isn't on. (Production gunicorn reloads via deploy.)
|
||||
`GET /api/events` and the chat-stream reconnect reader
|
||||
`GET /api/messages/<id>/events` are native-async Starlette routes
|
||||
mounted in `docsgpt/asgi.py`, not Flask routes. Plain `flask run`
|
||||
serves only the WSGI Flask app, so under it both endpoints 404:
|
||||
no live notifications, and reconnect-after-disconnect can't resume.
|
||||
Run the backend via `uvicorn docsgpt.asgi:asgi_app --reload` (or the
|
||||
production gunicorn uvicorn-worker) to exercise them; `--reload`
|
||||
picks up edits to `docsgpt/api/events/routes.py`.
|
||||
|
||||
### MCP OAuth completion can fall outside the user stream's MAXLEN window
|
||||
|
||||
|
||||
Reference in new issue
Block a user