mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
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:
parent
0110845f66
commit
a83aebe702
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user