From a83aebe70228ce5ce6f934834688e93ba9ac5811 Mon Sep 17 00:00:00 2001 From: Anton Dziatkovskii Date: Wed, 30 Sep 2026 12:36:54 +0100 Subject: [PATCH] fix(docs): harness docs document an async client API that does not exist (#5843) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) 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) * 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) * 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) Co-authored-by: Willem Jiang --- .../content/en/harness/integration-guide.mdx | 82 +++++++++---------- .../tutorials/create-your-first-harness.mdx | 43 +++++----- .../content/zh/harness/integration-guide.mdx | 80 ++++++++---------- .../tutorials/create-your-first-harness.mdx | 46 +++++++---- 4 files changed, 122 insertions(+), 129 deletions(-) diff --git a/frontend/src/content/en/harness/integration-guide.mdx b/frontend/src/content/en/harness/integration-guide.mdx index f5ab0c0020..5451668159 100644 --- a/frontend/src/content/en/harness/integration-guide.mdx +++ b/frontend/src/content/en/harness/integration-guide.mdx @@ -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 diff --git a/frontend/src/content/en/tutorials/create-your-first-harness.mdx b/frontend/src/content/en/tutorials/create-your-first-harness.mdx index 3e94f984ca..ca5dd7b577 100644 --- a/frontend/src/content/en/tutorials/create-your-first-harness.mdx +++ b/frontend/src/content/en/tutorials/create-your-first-harness.mdx @@ -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 diff --git a/frontend/src/content/zh/harness/integration-guide.mdx b/frontend/src/content/zh/harness/integration-guide.mdx index 3addf1bdef..b86d3bda01 100644 --- a/frontend/src/content/zh/harness/integration-guide.mdx +++ b/frontend/src/content/zh/harness/integration-guide.mdx @@ -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`,并在后续调用期间保留该设置,因为客户端运行时会再次解析配置路径。 diff --git a/frontend/src/content/zh/tutorials/create-your-first-harness.mdx b/frontend/src/content/zh/tutorials/create-your-first-harness.mdx index 622fd65535..2f17ad2a0d 100644 --- a/frontend/src/content/zh/tutorials/create-your-first-harness.mdx +++ b/frontend/src/content/zh/tutorials/create-your-first-harness.mdx @@ -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)