mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-05 20:38:41 +00:00
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
This commit is contained in:
parent
c57cf221d3
commit
abd0c7a585
@ -11,6 +11,11 @@ DeerFlow backend exposes two sets of APIs:
|
|||||||
|
|
||||||
All APIs are accessed through the Nginx reverse proxy at port 2026.
|
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
|
## LangGraph-compatible API
|
||||||
|
|
||||||
Base URL: `/api/langgraph`
|
Base URL: `/api/langgraph`
|
||||||
@ -161,6 +166,78 @@ Content-Type: application/json
|
|||||||
|
|
||||||
Same request body as Create Run. Returns SSE stream.
|
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
|
## Gateway API
|
||||||
@ -611,13 +688,26 @@ location /api/ {
|
|||||||
|
|
||||||
## Streaming Support
|
## 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
|
```http
|
||||||
POST /api/langgraph/threads/{thread_id}/runs/stream
|
POST /api/langgraph/threads/{thread_id}/runs/stream
|
||||||
Accept: text/event-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
|
## SDK Usage
|
||||||
@ -628,17 +718,50 @@ Accept: text/event-stream
|
|||||||
from langgraph_sdk import get_client
|
from langgraph_sdk import get_client
|
||||||
|
|
||||||
client = get_client(url="http://localhost:2026/api/langgraph")
|
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()
|
thread = await client.threads.create()
|
||||||
|
|
||||||
# Run agent
|
|
||||||
async for event in client.runs.stream(
|
async for event in client.runs.stream(
|
||||||
thread["thread_id"],
|
thread["thread_id"],
|
||||||
"lead_agent",
|
"lead_agent",
|
||||||
input={"messages": [{"role": "user", "content": "Hello"}]},
|
input={"messages": [{"role": "user", "content": "Hello"}]},
|
||||||
config={"configurable": {"model_name": "gpt-4"}},
|
config={"configurable": {"model_name": "gpt-4"}},
|
||||||
stream_mode=["values", "messages-tuple", "custom"],
|
stream_mode=["values", "messages-tuple", "custom"],
|
||||||
|
on_run_created=on_run_created,
|
||||||
):
|
):
|
||||||
print(event)
|
print(event)
|
||||||
```
|
```
|
||||||
@ -651,7 +774,46 @@ const response = await fetch('/api/models');
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(data.models);
|
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`, {
|
const streamResponse = await fetch(`/api/langgraph/threads/${threadId}/runs/stream`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -684,19 +846,47 @@ curl -X POST http://localhost:2026/api/threads/abc123/uploads \
|
|||||||
# Enable skill
|
# Enable skill
|
||||||
curl -X POST http://localhost:2026/api/skills/pdf-processing/enable
|
curl -X POST http://localhost:2026/api/skills/pdf-processing/enable
|
||||||
|
|
||||||
# Create thread and run agent
|
# Stateless stream — no thread pre-creation
|
||||||
curl -X POST http://localhost:2026/api/langgraph/threads \
|
curl -s -D - -N -X POST http://localhost:2026/api/langgraph/runs/stream \
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{}'
|
|
||||||
|
|
||||||
curl -X POST http://localhost:2026/api/langgraph/threads/abc123/runs \
|
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: text/event-stream" \
|
||||||
-d '{
|
-d '{
|
||||||
"input": {"messages": [{"role": "user", "content": "Hello"}]},
|
"input": {"messages": [{"role": "user", "content": "Hello"}]},
|
||||||
"config": {
|
"config": {
|
||||||
"recursion_limit": 100,
|
"recursion_limit": 100,
|
||||||
"configurable": {"model_name": "gpt-4"}
|
"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"]
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user