From abd0c7a585d8cfb32d590489dc616d79683bd10c Mon Sep 17 00:00:00 2001 From: yym36991 <157363180+yym36991@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:33:57 +0800 Subject: [PATCH] docs(api): document stateless /api/runs/stream endpoint (#4177) * docs(api): document stateless /api/runs/stream endpoint Record that clients can start a conversation without pre-creating a thread, and that Gateway returns thread_id and run_id via the Content-Location header. Co-authored-by: yym36991@gmail.com * docs(api): show on_run_created for stateless stream continuation The Python SDK example now captures thread_id/run_id via on_run_created before documenting the follow-up call with config.configurable.thread_id, matching the TS/cURL examples and langgraph-sdk 0.3.x behavior. Co-authored-by: yym36991@gmail.com --- backend/docs/API.md | 214 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 202 insertions(+), 12 deletions(-) diff --git a/backend/docs/API.md b/backend/docs/API.md index d574897d7..2e9c4e9a6 100644 --- a/backend/docs/API.md +++ b/backend/docs/API.md @@ -11,6 +11,11 @@ DeerFlow backend exposes two sets of APIs: All APIs are accessed through the Nginx reverse proxy at port 2026. +For agent conversations, clients can either pre-create a thread +(`POST /api/langgraph/threads`) or start immediately with the stateless stream +endpoint (`POST /api/langgraph/runs/stream`). The latter auto-creates a thread +and returns `thread_id` and `run_id` in the response `Content-Location` header. + ## LangGraph-compatible API Base URL: `/api/langgraph` @@ -161,6 +166,78 @@ Content-Type: application/json Same request body as Create Run. Returns SSE stream. +#### Stateless Stream Run + +Start a conversation without creating a thread first. Gateway auto-creates a +thread when `config.configurable.thread_id` is omitted, and returns both +identifiers in the response `Content-Location` header. + +```http +POST /api/langgraph/runs/stream +Content-Type: application/json +Accept: text/event-stream +``` + +Through Nginx, `/api/langgraph/runs/stream` is rewritten to the native Gateway +path `POST /api/runs/stream`. + +**Request Body:** Same as [Create Run](#create-run). Omit `thread_id` to start a +new conversation; include it to continue an existing one: + +```json +{ + "input": { + "messages": [ + { + "role": "user", + "content": "Hello, can you help me?" + } + ] + }, + "config": { + "recursion_limit": 100, + "configurable": { + "model_name": "gpt-4", + "thinking_enabled": false, + "is_plan_mode": false + } + }, + "stream_mode": ["values", "messages-tuple", "custom"] +} +``` + +**Response:** Server-Sent Events (SSE) stream with a `Content-Location` header: + +```http +Content-Location: /api/threads/{thread_id}/runs/{run_id} +``` + +Clients should parse `thread_id` and `run_id` from this header (the path ends +with `/runs/{run_id}`). Persist `thread_id` and send it back on the next turn +via `config.configurable.thread_id` to keep conversation history. + +**Continuing a conversation:** + +```json +{ + "input": { + "messages": [ + { + "role": "user", + "content": "What did I just ask?" + } + ] + }, + "config": { + "configurable": { + "thread_id": "abc123", + "model_name": "gpt-4" + } + }, + "stream_mode": ["values", "messages-tuple", "custom"] +} +``` + --- ## Gateway API @@ -611,13 +688,26 @@ location /api/ { ## Streaming Support -Gateway's LangGraph-compatible API streams run events with Server-Sent Events (SSE): +Gateway's LangGraph-compatible API streams run events with Server-Sent Events (SSE). + +**Thread-scoped streaming** (thread must exist): ```http POST /api/langgraph/threads/{thread_id}/runs/stream Accept: text/event-stream ``` +**Stateless streaming** (no pre-created thread; Gateway auto-creates one): + +```http +POST /api/langgraph/runs/stream +Accept: text/event-stream +``` + +Both endpoints return `Content-Location: /api/threads/{thread_id}/runs/{run_id}`. +The DeerFlow web UI and LangGraph SDK clients rely on this header to discover the +assigned `thread_id` and `run_id` on the first message of a new chat. + --- ## SDK Usage @@ -628,17 +718,50 @@ Accept: text/event-stream from langgraph_sdk import get_client client = get_client(url="http://localhost:2026/api/langgraph") +run_meta: dict[str, str] = {} -# Create thread + +def on_run_created(meta) -> None: + # langgraph-sdk 0.3.x parses Content-Location only when this callback is set. + if meta.thread_id: + run_meta["thread_id"] = meta.thread_id + run_meta["run_id"] = meta.run_id + + +# Option A: stateless stream — no thread pre-creation +# Gateway auto-creates a thread and returns thread_id/run_id in Content-Location. +async for event in client.runs.stream( + None, + "lead_agent", + input={"messages": [{"role": "user", "content": "Hello"}]}, + config={"configurable": {"model_name": "gpt-4"}}, + stream_mode=["values", "messages-tuple", "custom"], + on_run_created=on_run_created, +): + print(event) + +thread_id = run_meta["thread_id"] # persist before the next turn + +# Option A (continued): same thread on the next turn +async for event in client.runs.stream( + None, + "lead_agent", + input={"messages": [{"role": "user", "content": "What did I just ask?"}]}, + config={"configurable": {"thread_id": thread_id, "model_name": "gpt-4"}}, + stream_mode=["values", "messages-tuple", "custom"], + on_run_created=on_run_created, +): + print(event) + +# Option B: thread-scoped stream — create thread first, then stream thread = await client.threads.create() - -# Run agent async for event in client.runs.stream( thread["thread_id"], "lead_agent", input={"messages": [{"role": "user", "content": "Hello"}]}, config={"configurable": {"model_name": "gpt-4"}}, stream_mode=["values", "messages-tuple", "custom"], + on_run_created=on_run_created, ): print(event) ``` @@ -651,7 +774,46 @@ const response = await fetch('/api/models'); const data = await response.json(); console.log(data.models); -// Create a run and stream SSE events +function parseRunLocation(contentLocation: string | null) { + if (!contentLocation) return null; + const match = /\/threads\/([^/]+)\/runs\/([^/]+)/.exec(contentLocation); + if (!match) return null; + return { threadId: match[1], runId: match[2] }; +} + +// Option A: stateless stream — no thread pre-creation +let threadId: string | undefined; +const firstResponse = await fetch("/api/langgraph/runs/stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + input: { messages: [{ role: "user", content: "Hello" }] }, + stream_mode: ["values", "messages-tuple", "custom"], + }), +}); + +const created = parseRunLocation(firstResponse.headers.get("Content-Location")); +threadId = created?.threadId; +console.log("thread_id:", created?.threadId, "run_id:", created?.runId); + +// Option B: continue the same thread on the next turn +const followUpResponse = await fetch("/api/langgraph/runs/stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + input: { messages: [{ role: "user", content: "What did I just ask?" }] }, + config: { configurable: { thread_id: threadId } }, + stream_mode: ["values", "messages-tuple", "custom"], + }), +}); + +// Option C: thread-scoped stream when you already have a thread_id const streamResponse = await fetch(`/api/langgraph/threads/${threadId}/runs/stream`, { method: "POST", headers: { @@ -684,19 +846,47 @@ curl -X POST http://localhost:2026/api/threads/abc123/uploads \ # Enable skill curl -X POST http://localhost:2026/api/skills/pdf-processing/enable -# Create thread and run agent -curl -X POST http://localhost:2026/api/langgraph/threads \ - -H "Content-Type: application/json" \ - -d '{}' - -curl -X POST http://localhost:2026/api/langgraph/threads/abc123/runs \ +# Stateless stream — no thread pre-creation +curl -s -D - -N -X POST http://localhost:2026/api/langgraph/runs/stream \ -H "Content-Type: application/json" \ + -H "Accept: text/event-stream" \ -d '{ "input": {"messages": [{"role": "user", "content": "Hello"}]}, "config": { "recursion_limit": 100, "configurable": {"model_name": "gpt-4"} - } + }, + "stream_mode": ["values", "messages-tuple", "custom"] + }' +# Read Content-Location: /api/threads/{thread_id}/runs/{run_id} from the headers. + +# Continue the same thread on the next turn +curl -s -N -X POST http://localhost:2026/api/langgraph/runs/stream \ + -H "Content-Type: application/json" \ + -H "Accept: text/event-stream" \ + -d '{ + "input": {"messages": [{"role": "user", "content": "What did I just ask?"}]}, + "config": { + "configurable": {"thread_id": "abc123", "model_name": "gpt-4"} + }, + "stream_mode": ["values", "messages-tuple", "custom"] + }' + +# Thread-scoped flow — create thread first, then stream +curl -X POST http://localhost:2026/api/langgraph/threads \ + -H "Content-Type: application/json" \ + -d '{}' + +curl -X POST http://localhost:2026/api/langgraph/threads/abc123/runs/stream \ + -H "Content-Type: application/json" \ + -H "Accept: text/event-stream" \ + -d '{ + "input": {"messages": [{"role": "user", "content": "Hello"}]}, + "config": { + "recursion_limit": 100, + "configurable": {"model_name": "gpt-4"} + }, + "stream_mode": ["values", "messages-tuple", "custom"] }' ```