fix(docs): harness docs document an async client API that does not exist (#5843)

* docs: replace nonexistent async client API in harness docs

DeerFlowClient has no async methods: client.astream() and client.ainvoke()
do not exist, so the "Create Your First Harness" tutorial and the harness
integration guide fail with AttributeError on their first call. Overrides
were also shown as a nested config={"configurable": {...}} dict, which
stream()/chat() swallow via **kwargs and silently ignore.

Switch the examples to the shipping API (sync stream()/chat(), flat kwarg
overrides, agent_name as a constructor arg) in the EN and ZH copies of both
pages. backend/docs/STREAMING.md is left alone: its astream mentions are
design discussion and LangGraph internals, not user-facing examples.

Assisted-by: Claude Code / claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com>

* docs: serialize StreamEvent before StreamingResponse, use real event names

Review follow-up on the two points raised in #5843.

P2 — the streaming examples handed a generator of `StreamEvent` dataclasses
straight to Starlette. `StreamingResponse` calls `.encode()` on any chunk that
is not `str`/`bytes`, so the first event raised
`AttributeError: 'StreamEvent' object has no attribute 'encode'`. Both
occurrences (the `run_agent` snippet and the inline FastAPI route, which shipped
the dataclass `repr()` through an f-string) now serialize to SSE frames built
from `event.type` / `event.data`.

P3 — the tutorial showed mapping literals with `messages` and `thread_state`,
neither of which exists. The real contract is
`StreamEventType = Literal["values", "messages-tuple", "custom", "end"]`
(backend/packages/harness/deerflow/client.py:128) and the stream yields
dataclasses, so the section now shows `StreamEvent(...)` with the real names
plus a consuming loop.

The zh tutorial had no equivalent section at all; added for parity.

Assisted-by: Claude Code / claude-opus-5
Machine: A-Mac16-2019-PaloAlto
Account: tonydzi
Operator: robot:pr-reply-daily

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: drop the nonexistent load_config() from every harness snippet

Review follow-up on #5843 (@willem-bd): the rewritten tutorial still opened
with `from deerflow.config import load_config`, which raises ImportError
before `client.stream(...)` is ever reached — `deerflow/config/__init__.py`
exports `get_app_config` and friends, and the string `load_config` does not
appear anywhere under `backend/packages/harness/deerflow/` (the only near
match is `load_configured_extension_middlewares`).

The call was also unnecessary: `DeerFlowClient.__init__` already resolves
configuration itself (client.py:220-222 — `reload_app_config(config_path)`
when a path is given, otherwise `get_app_config()`, which auto-loads from
the default path or DEER_FLOW_CONFIG_PATH).

Fixed the class rather than the one snippet you flagged: the same dead
import appeared 10 times across 4 pages (EN + ZH tutorial and integration
guide). Every one is gone. The "configuration in embedded mode" section now
shows the API that exists — `DeerFlowClient(config_path="...")` — instead of
`load_config(config_path="...")`.

Also from the review:
- the `end` example showed only `total_tokens`; `stream()` always yields
  input/output/total (client.py:920), and the snippet right below prints the
  whole usage dict, so a reader saw keys the docs omitted. Both EN and ZH now
  show the full payload, and the `values` example visibly elides `artifacts`
  and `summary_text` instead of pretending they are absent.
- the two SSE comments on the ZH integration page stayed English while the
  example above them was translated; translated.

Verified statically, not executed: `grep` for the symbol across the harness
package (zero hits) and a read of `DeerFlowClient.__init__`. I did not stand
up a backend to run the tutorial end to end.

Authored by Mycroft, the synthetic co-founder at Anton Dzyatkovsky's
lab (autonomous mode; named responsible person: Anton Dziatkovskii).

Assisted-by: Claude Code / Opus 5 (review response, implementation)
Machine: MacBook-Anton
Account: a
Operator: mycroft (autonomous routine github-thread-watch)
Signed-off-by: tonydzi <194927794+tonydzi@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: fix native artifact SSE encoding and custom config advice

---------

Signed-off-by: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com>
Signed-off-by: tonydzi <194927794+tonydzi@users.noreply.github.com>
Co-authored-by: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Anton Dziatkovskii 2026-09-30 12:36:54 +01:00 committed by GitHub
parent 0110845f66
commit a83aebe702
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 122 additions and 129 deletions

View File

@ -21,36 +21,35 @@ The primary integration point is `DeerFlowClient`. It wraps the LangGraph runtim
```python
from deerflow.client import DeerFlowClient
from deerflow.config import load_config
# Load configuration (reads config.yaml or DEER_FLOW_CONFIG_PATH)
load_config()
# The client loads configuration on construction (reads config.yaml
# or DEER_FLOW_CONFIG_PATH)
client = DeerFlowClient()
```
The client is thread-safe and designed to be instantiated once and reused across requests.
## Async streaming
## Streaming
The recommended integration pattern is async streaming. This gives you real-time access to each token and event as the agent produces it:
The recommended integration pattern is streaming. `stream()` is a sync generator, so no asyncio is required — it gives you real-time access to each token and event as the agent produces it:
Tool artifacts can contain native Python objects. `default=str` represents objects that are not JSON serializable as strings so these events do not interrupt the stream.
```python
import asyncio
import json
async def run_agent(thread_id: str, user_message: str):
async for event in client.astream(
thread_id=thread_id,
def run_agent(thread_id: str, user_message: str):
for event in client.stream(
message=user_message,
config={
"configurable": {
"model_name": "gpt-4o",
"subagent_enabled": True,
}
},
thread_id=thread_id,
model_name="gpt-4o",
subagent_enabled=True,
):
# Process each streaming event
yield event
# `stream()` yields `StreamEvent` dataclasses, not strings. Serialise each
# one before it goes over the wire: Starlette calls `.encode()` on any
# chunk that is not already `str` / `bytes`, so yielding the dataclass
# raises `AttributeError` on the very first event.
yield f"event: {event.type}\ndata: {json.dumps(event.data, default=str)}\n\n"
# In a FastAPI handler:
# from fastapi.responses import StreamingResponse
@ -62,10 +61,10 @@ async def run_agent(thread_id: str, user_message: str):
For batch processing or when you only need the final result:
```python
async def run_agent_sync(thread_id: str, user_message: str) -> dict:
result = await client.ainvoke(
thread_id=thread_id,
def run_agent_sync(thread_id: str, user_message: str) -> str:
result = client.chat(
message=user_message,
thread_id=thread_id,
)
return result
```
@ -82,25 +81,22 @@ thread_id = str(uuid.uuid4())
# Continuing an existing conversation (same thread_id)
# The agent will see the full history if a checkpointer is configured
await client.ainvoke(thread_id=existing_thread_id, message="Follow up question")
client.chat(message="Follow up question", thread_id=existing_thread_id)
```
## Custom per-agent configuration
Build domain-specific agents by creating named agent configs and passing the `agent_name` at runtime:
Build domain-specific agents by creating named agent configs and passing the `agent_name` when constructing the client:
```python
# agents/research-assistant/config.yaml must exist with skills and tool config
result = await client.ainvoke(
thread_id=thread_id,
research_client = DeerFlowClient(agent_name="research-assistant")
result = research_client.chat(
message=user_message,
config={
"configurable": {
"agent_name": "research-assistant",
"model_name": "gpt-4o",
}
},
thread_id=thread_id,
model_name="gpt-4o",
)
```
@ -110,9 +106,6 @@ DeerFlow Gateway is itself a FastAPI application. You can mount it as a sub-appl
```python
from fastapi import FastAPI
from deerflow.config import load_config
load_config()
app = FastAPI()
@ -124,6 +117,8 @@ app.mount("/deerflow", gateway_app)
Or use `DeerFlowClient` directly in your own FastAPI routes with streaming:
```python
import json
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from deerflow.client import DeerFlowClient
@ -133,9 +128,11 @@ client = DeerFlowClient()
@app.post("/chat/{thread_id}")
async def chat(thread_id: str, body: dict):
async def generate():
async for event in client.astream(thread_id=thread_id, message=body["message"]):
yield f"data: {event}\n\n"
def generate():
for event in client.stream(message=body["message"], thread_id=thread_id):
# Serialise the `StreamEvent` dataclass; a raw f-string of the object
# would ship its `repr()`, which no SSE client can parse.
yield f"event: {event.type}\ndata: {json.dumps(event.data, default=str)}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
```
@ -159,16 +156,11 @@ When embedded in another application, set the config path explicitly to avoid am
import os
os.environ["DEER_FLOW_CONFIG_PATH"] = "/path/to/my-deerflow-config.yaml"
from deerflow.config import load_config
load_config()
from deerflow.client import DeerFlowClient
client = DeerFlowClient()
```
Or pass the path directly:
```python
from deerflow.config import load_config
load_config(config_path="/path/to/my-deerflow-config.yaml")
```
Set `DEER_FLOW_CONFIG_PATH` before constructing the client and keep it set for subsequent calls, since configuration is resolved again when the client runs.
## MCP server integration

View File

@ -53,31 +53,21 @@ tools:
Create `my_agent.py` in the `backend/` directory:
```python
import asyncio
import os
from deerflow.client import DeerFlowClient
from deerflow.config import load_config
os.environ["OPENAI_API_KEY"] = "sk-..."
# Load config.yaml
load_config()
# The client loads config.yaml itself. To select a specific file,
# set DEER_FLOW_CONFIG_PATH before constructing the client.
client = DeerFlowClient()
async def main():
async for event in client.astream(
thread_id="my-first-thread",
message="Write a Python fibonacci function with a docstring",
config={
"configurable": {
"model_name": "gpt-4o",
}
},
):
print(event)
asyncio.run(main())
for event in client.stream(
message="Write a Python fibonacci function with a docstring",
thread_id="my-first-thread",
model_name="gpt-4o",
):
print(event)
```
### Run it
@ -91,11 +81,22 @@ uv run python my_agent.py
## What the events look like
The stream yields events like:
`stream()` yields `StreamEvent` dataclasses, not dicts. Read them through
`event.type` and `event.data`. There are exactly four types —
`"values"`, `"messages-tuple"`, `"custom"` and `"end"`:
```python
{"type": "messages", "data": {"content": "def fibonacci..."}}
{"type": "thread_state", "data": {"title": "Python Fibonacci Function"}}
StreamEvent(type="messages-tuple", data={"type": "ai", "content": "def fibonacci...", "id": "m1"})
StreamEvent(type="values", data={"title": "Python Fibonacci Function", "messages": [...], "artifacts": [...], "summary_text": None})
StreamEvent(type="end", data={"usage": {"input_tokens": 1180, "output_tokens": 412, "total_tokens": 1592}})
```
```python
for event in client.stream(message="...", thread_id="my-first-thread"):
if event.type == "messages-tuple":
print(event.data.get("content", ""), end="")
elif event.type == "end":
print("\ndone:", event.data.get("usage"))
```
## Next steps

View File

@ -20,36 +20,33 @@ DeerFlow Harness 不仅仅是一个独立应用程序——它是一个可以导
```python
from deerflow.client import DeerFlowClient
from deerflow.config import load_config
# 加载配置(读取 config.yaml 或 DEER_FLOW_CONFIG_PATH)
load_config()
# 客户端在构造时加载配置(读取 config.yaml 或 DEER_FLOW_CONFIG_PATH)
client = DeerFlowClient()
```
客户端是线程安全的,设计为实例化一次并在请求之间复用。
## 异步流式传输
## 流式传输
推荐的集成模式是异步流式传输。这让你可以在 Agent 生成响应时实时访问每个 token 和事件:
推荐的集成模式是流式传输。`stream()` 是同步生成器,无需 asyncio,即可在 Agent 生成响应时实时访问每个 token 和事件:
工具产物可能包含原生 Python 对象。`default=str` 将不可 JSON 序列化的对象转换为字符串,避免中断事件流。
```python
import asyncio
import json
async def run_agent(thread_id: str, user_message: str):
async for event in client.astream(
thread_id=thread_id,
def run_agent(thread_id: str, user_message: str):
for event in client.stream(
message=user_message,
config={
"configurable": {
"model_name": "gpt-4o",
"subagent_enabled": True,
}
},
thread_id=thread_id,
model_name="gpt-4o",
subagent_enabled=True,
):
# 处理每个流式事件
yield event
# `stream()` 产出的是 `StreamEvent` 数据类实例,而不是字符串。发送前必须
# 先序列化:Starlette 会对非 `str` / `bytes` 的分块调用 `.encode()`,
# 直接产出数据类会在第一个事件上抛出 `AttributeError`。
yield f"event: {event.type}\ndata: {json.dumps(event.data, default=str)}\n\n"
# 在 FastAPI 处理器中:
# from fastapi.responses import StreamingResponse
@ -61,10 +58,10 @@ async def run_agent(thread_id: str, user_message: str):
对于批处理或只需要最终结果的场景:
```python
async def run_agent_sync(thread_id: str, user_message: str) -> dict:
result = await client.ainvoke(
thread_id=thread_id,
def run_agent_sync(thread_id: str, user_message: str) -> str:
result = client.chat(
message=user_message,
thread_id=thread_id,
)
return result
```
@ -81,25 +78,22 @@ thread_id = str(uuid.uuid4())
# 继续已有对话(相同 thread_id)
# 如果配置了检查点,Agent 将看到完整历史
await client.ainvoke(thread_id=existing_thread_id, message="后续问题")
client.chat(message="后续问题", thread_id=existing_thread_id)
```
## 自定义 Agent 配置
通过创建命名 Agent 配置并在运行时传入 `agent_name` 来构建领域特定 Agent:
通过创建命名 Agent 配置并在构造客户端时传入 `agent_name` 来构建领域特定 Agent:
```python
# agents/research-assistant/config.yaml 必须存在并包含技能和工具配置
result = await client.ainvoke(
thread_id=thread_id,
research_client = DeerFlowClient(agent_name="research-assistant")
result = research_client.chat(
message=user_message,
config={
"configurable": {
"agent_name": "research-assistant",
"model_name": "gpt-4o",
}
},
thread_id=thread_id,
model_name="gpt-4o",
)
```
@ -109,9 +103,6 @@ DeerFlow Gateway 本身是一个 FastAPI 应用程序。你可以将其作为子
```python
from fastapi import FastAPI
from deerflow.config import load_config
load_config()
app = FastAPI()
@ -123,6 +114,8 @@ app.mount("/deerflow", gateway_app)
或者在你自己的 FastAPI 路由中直接使用 `DeerFlowClient` 进行流式传输:
```python
import json
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from deerflow.client import DeerFlowClient
@ -132,9 +125,11 @@ client = DeerFlowClient()
@app.post("/chat/{thread_id}")
async def chat(thread_id: str, body: dict):
async def generate():
async for event in client.astream(thread_id=thread_id, message=body["message"]):
yield f"data: {event}\n\n"
def generate():
for event in client.stream(message=body["message"], thread_id=thread_id):
# 序列化 `StreamEvent` 数据类;直接对对象做 f-string 会发送它的
# `repr()`,SSE 客户端无法解析。
yield f"event: {event.type}\ndata: {json.dumps(event.data, default=str)}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
```
@ -146,16 +141,11 @@ async def chat(thread_id: str, body: dict):
import os
os.environ["DEER_FLOW_CONFIG_PATH"] = "/path/to/my-deerflow-config.yaml"
from deerflow.config import load_config
load_config()
from deerflow.client import DeerFlowClient
client = DeerFlowClient()
```
或直接传递路径:
```python
from deerflow.config import load_config
load_config(config_path="/path/to/my-deerflow-config.yaml")
```
在创建客户端前设置 `DEER_FLOW_CONFIG_PATH`,并在后续调用期间保留该设置,因为客户端运行时会再次解析配置路径。
<Cards num={2}>
<Cards.Card title="自定义与扩展" href="/docs/harness/customization" />

View File

@ -47,32 +47,22 @@ tools:
创建 `my_agent.py`:
```python
import asyncio
import os
from deerflow.client import DeerFlowClient
from deerflow.config import load_config
# 设置 API Key
os.environ["OPENAI_API_KEY"] = "sk-..."
# 加载配置
load_config()
# 客户端自行加载 config.yaml;如需指定文件,
# 请在创建客户端前设置 DEER_FLOW_CONFIG_PATH。
client = DeerFlowClient()
async def main():
async for event in client.astream(
thread_id="my-first-thread",
message="用 Python 写一个斐波那契数列函数,包含文档字符串",
config={
"configurable": {
"model_name": "gpt-4o",
}
},
):
print(event)
asyncio.run(main())
for event in client.stream(
message="用 Python 写一个斐波那契数列函数,包含文档字符串",
thread_id="my-first-thread",
model_name="gpt-4o",
):
print(event)
```
## 运行
@ -82,6 +72,26 @@ cd backend
uv run python my_agent.py
```
## 事件长什么样
`stream()` 产出的是 `StreamEvent` 数据类实例,而不是字典,需通过
`event.type` 和 `event.data` 读取。事件类型只有四种:
`"values"`、`"messages-tuple"`、`"custom"` 和 `"end"`:
```python
StreamEvent(type="messages-tuple", data={"type": "ai", "content": "def fibonacci...", "id": "m1"})
StreamEvent(type="values", data={"title": "Python Fibonacci Function", "messages": [...], "artifacts": [...], "summary_text": None})
StreamEvent(type="end", data={"usage": {"input_tokens": 1180, "output_tokens": 412, "total_tokens": 1592}})
```
```python
for event in client.stream(message="...", thread_id="my-first-thread"):
if event.type == "messages-tuple":
print(event.data.get("content", ""), end="")
elif event.type == "end":
print("\ndone:", event.data.get("usage"))
```
## 下一步
- [使用工具和技能](/docs/tutorials/use-tools-and-skills)