mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 17:22:00 +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 个内置模板全部冒烟通过,含真实模型端到端验证。
83 lines
2.6 KiB
Python
83 lines
2.6 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from langgraph.config import get_stream_writer
|
|
|
|
from app.core.events import EventType
|
|
from app.engine.schema import GraphNode, GraphSpec
|
|
from app.engine.state import GraphState, template_context
|
|
from app.engine.expressions import render_deep, render_template
|
|
|
|
|
|
@dataclass
|
|
class RunContext:
|
|
"""一次运行的全局上下文,编译时闭包进每个节点。"""
|
|
|
|
run_id: str
|
|
thread_id: str
|
|
spec: GraphSpec
|
|
workflow_id: str | None = None
|
|
# 深度 > 0 表示当前在子图里,用来防止子图无限递归
|
|
depth: int = 0
|
|
memory_scope: str = "default"
|
|
collection: str = "default"
|
|
extra: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class NodeContext:
|
|
"""单个节点执行时能拿到的东西。"""
|
|
|
|
node: GraphNode
|
|
run: RunContext
|
|
|
|
@property
|
|
def config(self) -> dict[str, Any]:
|
|
return self.node.data.config
|
|
|
|
def emit(self, event_type: EventType | str, **data: Any) -> None:
|
|
"""往 LangGraph 的 custom 流里发一条事件。
|
|
|
|
runner 在另一端消费,转成落库的 RunEvent 并广播给前端 —— 画布上的节点高亮、
|
|
token 流、工具调用卡片全都来自这里。
|
|
"""
|
|
try:
|
|
writer = get_stream_writer()
|
|
except Exception: # noqa: BLE001 - 不在图执行上下文里(比如单元测试)
|
|
return
|
|
writer(
|
|
{
|
|
"__agentlab__": True,
|
|
"type": str(event_type),
|
|
"node_id": self.node.id,
|
|
"ts": time.time(),
|
|
"data": data,
|
|
}
|
|
)
|
|
|
|
def render(self, value: Any, state: GraphState) -> Any:
|
|
"""用当前状态渲染配置里的模板。"""
|
|
return render_deep(value, template_context(state))
|
|
|
|
def render_str(self, value: str | None, state: GraphState) -> str:
|
|
return render_template(value or "", template_context(state))
|
|
|
|
def cfg(self, key: str, default: Any = None) -> Any:
|
|
"""读节点配置,没有就回退到图级 defaults。"""
|
|
value = self.config.get(key)
|
|
if value in (None, ""):
|
|
return self.run.spec.defaults.get(key, default)
|
|
return value
|
|
|
|
|
|
class NodeError(RuntimeError):
|
|
"""节点执行失败。带上 node_id 方便前端定位到具体的卡片。"""
|
|
|
|
def __init__(self, node_id: str, message: str, *, retryable: bool = False) -> None:
|
|
super().__init__(message)
|
|
self.node_id = node_id
|
|
self.retryable = retryable
|