Files
ZY-Agent/apps/examples/http-chat/app.py
T
1445043649 7dfc9968fe feat(apps): 新增 DeerFlow 应用脚手架,并修复 store 的 database 回退
apps/:新增「基于 DeerFlow 的应用」目录,与 backend/、frontend/ 平级,
位于「app 消费 deerflow、不反向依赖」边界的正确侧。含两种集成示例:
  - examples/http-chat   —— HTTP Gateway (REST+SSE),含登录/CSRF/建线程/流式对话
  - examples/embedded-chat —— 进程内直接调 DeerFlowClient
README 说明边界规则、两种模式、鉴权流程及新建应用约定。

runtime/store:修复 make_store 缺失的 database 段回退。原先 store 工厂只读
legacy 的 checkpointer 段,导致仅配 database:postgres 时,checkpointer 走了
Postgres、但 store 仍回退 InMemoryStore(并打出误导性的「线程列表会丢失」告警,
实际线程在 threads_meta 表里、本就持久)。现对齐 checkpointer 工厂的优先级:
checkpointer 段 → database 段 → InMemoryStore;postgres 分支同样剥掉 +asyncpg
方言前缀,使一个 DATABASE_URL 同时满足 SQLAlchemy 与 LangGraph 的 psycopg store。
告警文案也修正为「跨线程 store 数据会丢失」。

tests:新增 test_store_provider.py(3 例,TDD)覆盖 database→postgres 回退、
无配置时的内存回退、以及 checkpointer 段优先级。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 22:02:44 +08:00

126 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
HTTP 模式示例:把 DeerFlow 当底层服务,通过 Gateway (REST + SSE) 对话。
运行:
pip install -r requirements.txt
python app.py # 前提:仓库根目录已 `make dev`
字段 / 事件名均已对照后端源码核对:
鉴权 backend/app/gateway/routers/auth.py + auth_middleware.py + csrf_middleware.py
线程/运行 backend/app/gateway/routers/threads.py + thread_runs.py
SSE 事件名 backend/packages/harness/deerflow/runtime/runs/worker.py
"""
import os
import json
import requests
# 默认走 nginx(:2026);只起了 Gateway 时用 BASE=http://localhost:8001 覆盖
BASE = os.environ.get("DF_BASE", "http://localhost:2026")
EMAIL = os.environ.get("DF_EMAIL", "admin@example.com")
PASSWORD = os.environ.get("DF_PASSWORD", "change-me-please-123") # 至少 8 位,避免弱口令
def authenticate(s: requests.Session) -> None:
"""首启则初始化管理员,否则登录。成功后 cookie 落在 session。"""
status = s.get(f"{BASE}/api/v1/auth/setup-status").json()
if status.get("needs_setup"):
print("→ 首次启动,创建管理员账号")
r = s.post(f"{BASE}/api/v1/auth/initialize",
json={"email": EMAIL, "password": PASSWORD})
else:
print("→ 已有账号,登录")
# login/local 是 OAuth2 表单:字段名 username(填邮箱)+ password
r = s.post(f"{BASE}/api/v1/auth/login/local",
data={"username": EMAIL, "password": PASSWORD})
r.raise_for_status()
print(" cookies:", list(s.cookies.keys()))
def _csrf(s: requests.Session) -> dict:
"""双提交 cookie 模式:csrf_token cookie 的值放进 X-CSRF-Token 头。"""
token = s.cookies.get("csrf_token")
if not token:
raise RuntimeError("缺少 csrf_token cookie —— 鉴权可能失败")
return {"X-CSRF-Token": token}
def create_thread(s: requests.Session) -> str:
r = s.post(f"{BASE}/api/threads", json={}, headers=_csrf(s))
r.raise_for_status()
tid = r.json()["thread_id"]
print(f"→ 线程已创建: {tid}")
return tid
_seen_text = "" # 简单状态,按需扩展为按 message-id 维护
def stream_chat(s: requests.Session, thread_id: str, message: str) -> None:
body = {
"assistant_id": "lead_agent", # 见 backend/langgraph.json
"input": {"messages": [{"role": "user", "content": message}]},
"stream_mode": ["messages-tuple", "values"], # 增量文本 + 全量状态
}
headers = {**_csrf(s), "Accept": "text/event-stream"}
with s.post(f"{BASE}/api/threads/{thread_id}/runs/stream",
json=body, headers=headers, stream=True) as resp:
resp.raise_for_status()
print(f"\n👤 {message}\n🤖 ", end="", flush=True)
event, buf = None, []
for raw in resp.iter_lines(decode_unicode=True):
if raw is None:
continue
line = raw.strip()
if line == "": # 一帧结束
if event:
_handle(event, "\n".join(buf))
event, buf = None, []
elif line.startswith(":"): # 心跳注释
continue
elif line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
buf.append(line[5:].strip())
print()
def _handle(event: str, data: str) -> None:
if event == "end" or not data:
return
try:
payload = json.loads(data)
except json.JSONDecodeError:
return
if event == "messages":
# 形如 [chunk_dict, metadata_dict]AI 文本是增量
chunk = payload[0] if isinstance(payload, list) and payload else {}
if chunk.get("type") in ("ai", "AIMessageChunk"):
content = chunk.get("content")
text = content if isinstance(content, str) else _flatten(content)
if text:
print(text, end="", flush=True)
# event == "metadata" → {run_id, thread_id}
# event == "values" → 全量状态快照(title / messages / artifacts ...
def _flatten(content) -> str:
if isinstance(content, list):
return "".join(b.get("text", "") for b in content if isinstance(b, dict))
return ""
def main() -> None:
s = requests.Session()
authenticate(s)
tid = create_thread(s)
stream_chat(s, tid, "用一句话介绍你自己,然后心算 17 * 23。")
stream_chat(s, tid, "刚才结果再乘以 2 是多少?") # 复用 thread_id 即多轮
if __name__ == "__main__":
main()