mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 06:31:58 +00:00
964eeaa829
在画布上拖拽编排 agent 工作流,并实时看着它执行。 后端 FastAPI + LangGraph:把画布 JSON 编译成 StateGraph,15 种节点 (模型/Agent/多Agent、工具/沙箱、分支/循环/子图、记忆/检索/整形、 人工介入/结构校验)。选 LangGraph 是因为可视化编排真正难的是暂停、 恢复和回放,而 checkpointer + interrupt() 正好把这几件事做在了底层。 前端 React + React Flow:节点元数据驱动属性面板与连接桩,运行时 节点高亮、边流动画、卡片内流式 token 全部由同一份事件流驱动。 关键实现: - Agent 内工具审批用 @task 缓存模型调用,interrupt 重放时不重复计费 - 沙箱保持只读根文件系统,代码经 exec+base64 写入(put_archive 会被 只读容器拒绝),断网、非 root、内存/CPU/超时限额 - 检索为向量与 BM25 混合,本地哈希 embedding 零配置可用 - 分支条件走 AST 白名单求值器;HTTP 工具逐 IP 拦截内网与元数据地址 - Mock provider 让无 API Key 也能跑通完整链路 7 个内置模板全部冒烟通过,含真实模型端到端验证。
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
from __future__ import annotations
|
||
|
||
import operator
|
||
from typing import Annotated, Any, TypedDict
|
||
|
||
from langchain_core.messages import AnyMessage
|
||
from langgraph.graph import add_messages
|
||
|
||
|
||
def merge_dict(left: dict[str, Any] | None, right: dict[str, Any] | None) -> dict[str, Any]:
|
||
"""浅合并。并行分支各自写不同的 key 时不会互相覆盖。"""
|
||
out = dict(left or {})
|
||
out.update(right or {})
|
||
return out
|
||
|
||
|
||
def merge_usage(left: dict[str, Any] | None, right: dict[str, Any] | None) -> dict[str, Any]:
|
||
"""累加 token / 成本,其余字段取新值。"""
|
||
out = dict(left or {})
|
||
for key, value in (right or {}).items():
|
||
if isinstance(value, (int, float)) and isinstance(out.get(key), (int, float)):
|
||
out[key] = out[key] + value
|
||
else:
|
||
out[key] = value
|
||
return out
|
||
|
||
|
||
class GraphState(TypedDict, total=False):
|
||
"""所有节点共享的状态。
|
||
|
||
- messages:对话历史,agent 节点之间靠它传递上下文
|
||
- nodes:node_id -> 该节点的结构化输出,模板里用 {{ nodes.xxx.text }} 引用
|
||
- vars:用户显式写入的变量池
|
||
- output:output 节点收集的最终成果
|
||
"""
|
||
|
||
messages: Annotated[list[AnyMessage], add_messages]
|
||
input: dict[str, Any]
|
||
vars: Annotated[dict[str, Any], merge_dict]
|
||
nodes: Annotated[dict[str, Any], merge_dict]
|
||
output: Annotated[dict[str, Any], merge_dict]
|
||
usage: Annotated[dict[str, Any], merge_usage]
|
||
trail: Annotated[list[dict[str, Any]], operator.add]
|
||
# 循环计数器:node_id -> 已迭代次数,防止无限循环
|
||
loops: Annotated[dict[str, Any], merge_dict]
|
||
|
||
|
||
def template_context(state: GraphState) -> dict[str, Any]:
|
||
"""把状态摊平成模板/表达式可以引用的命名空间。"""
|
||
return {
|
||
"input": state.get("input") or {},
|
||
"nodes": state.get("nodes") or {},
|
||
"vars": state.get("vars") or {},
|
||
"output": state.get("output") or {},
|
||
"loops": state.get("loops") or {},
|
||
"usage": state.get("usage") or {},
|
||
"messages": [
|
||
{"role": getattr(m, "type", "?"), "content": _text_of(m)}
|
||
for m in (state.get("messages") or [])
|
||
],
|
||
"last_message": _text_of((state.get("messages") or [None])[-1]),
|
||
}
|
||
|
||
|
||
def _text_of(message: Any) -> str:
|
||
"""把 LangChain 的多模态 content 压成纯文本。"""
|
||
if message is None:
|
||
return ""
|
||
content = getattr(message, "content", message)
|
||
if isinstance(content, str):
|
||
return content
|
||
if isinstance(content, list):
|
||
parts: list[str] = []
|
||
for block in content:
|
||
if isinstance(block, str):
|
||
parts.append(block)
|
||
elif isinstance(block, dict):
|
||
if block.get("type") == "text":
|
||
parts.append(block.get("text", ""))
|
||
elif "text" in block:
|
||
parts.append(str(block["text"]))
|
||
return "".join(parts)
|
||
return str(content)
|
||
|
||
|
||
def message_text(message: Any) -> str:
|
||
return _text_of(message)
|
||
|
||
|
||
def thinking_text(message: Any) -> str:
|
||
"""抽出思考块的内容。
|
||
|
||
开了 adaptive thinking 的模型(Claude 4.6+)返回的 content 里混着 thinking 块,
|
||
正文提取必须跳过它们,但它本身对"看清楚模型怎么想的"很有价值,所以单独取出来。
|
||
"""
|
||
content = getattr(message, "content", message)
|
||
if not isinstance(content, list):
|
||
return ""
|
||
parts: list[str] = []
|
||
for block in content:
|
||
if isinstance(block, dict) and block.get("type") == "thinking":
|
||
parts.append(str(block.get("thinking") or ""))
|
||
return "".join(parts)
|