Merge branch 'arc53:main' into main

This commit is contained in:
Manish Madan authored and GitHub committed 2026-09-15 04:18:40 +05:30
commit 2afc711b57
60 files changed
+4696 -1690

No files matched your search

+2 -1
View File
@@ -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:**
+3 -3
View File
@@ -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
+9
View File
@@ -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
+30 -31
View File
@@ -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