|
@@ -6,14 +6,20 @@ Agent Runner - Agent 执行引擎
|
|
|
2. 记录执行轨迹(Trace + Messages + GoalTree)
|
|
2. 记录执行轨迹(Trace + Messages + GoalTree)
|
|
|
3. 检索和注入记忆(Experience + Skill)
|
|
3. 检索和注入记忆(Experience + Skill)
|
|
|
4. 管理执行计划(GoalTree)
|
|
4. 管理执行计划(GoalTree)
|
|
|
-5. 收集反馈,提取经验
|
|
|
|
|
|
|
+5. 支持续跑(continue)和回溯重跑(rewind)
|
|
|
|
|
+
|
|
|
|
|
+参数分层:
|
|
|
|
|
+- Infrastructure: AgentRunner 构造时设置(trace_store, llm_call 等)
|
|
|
|
|
+- RunConfig: 每次 run 时指定(model, trace_id, insert_after 等)
|
|
|
|
|
+- Messages: OpenAI SDK 格式的任务消息
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
-from agent.tools.builtin.browser import browser_read_long_content
|
|
|
|
|
|
|
+import json
|
|
|
import logging
|
|
import logging
|
|
|
-from dataclasses import dataclass
|
|
|
|
|
|
|
+import uuid
|
|
|
|
|
+from dataclasses import dataclass, field
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
-from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal, Union
|
|
|
|
|
|
|
+from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal, Tuple, Union
|
|
|
|
|
|
|
|
from agent.trace.models import Trace, Message
|
|
from agent.trace.models import Trace, Message
|
|
|
from agent.trace.protocols import TraceStore
|
|
from agent.trace.protocols import TraceStore
|
|
@@ -26,24 +32,39 @@ from agent.tools import ToolRegistry, get_tool_registry
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ===== 运行配置 =====
|
|
|
|
|
+
|
|
|
@dataclass
|
|
@dataclass
|
|
|
-class AgentConfig:
|
|
|
|
|
- """Agent 配置"""
|
|
|
|
|
- agent_type: str = "default"
|
|
|
|
|
|
|
+class RunConfig:
|
|
|
|
|
+ """
|
|
|
|
|
+ 运行参数 — 控制 Agent 如何执行
|
|
|
|
|
+
|
|
|
|
|
+ 分为模型层参数(由上游 agent 或用户决定)和框架层参数(由系统注入)。
|
|
|
|
|
+ """
|
|
|
|
|
+ # --- 模型层参数 ---
|
|
|
|
|
+ model: str = "gpt-4o"
|
|
|
|
|
+ temperature: float = 0.3
|
|
|
max_iterations: int = 200
|
|
max_iterations: int = 200
|
|
|
|
|
+ tools: Optional[List[str]] = None # None = 全部内置工具
|
|
|
|
|
+
|
|
|
|
|
+ # --- 框架层参数 ---
|
|
|
|
|
+ agent_type: str = "default"
|
|
|
|
|
+ uid: Optional[str] = None
|
|
|
|
|
+ system_prompt: Optional[str] = None # None = 从 skills 自动构建
|
|
|
enable_memory: bool = True
|
|
enable_memory: bool = True
|
|
|
auto_execute_tools: bool = True
|
|
auto_execute_tools: bool = True
|
|
|
|
|
+ name: Optional[str] = None # 显示名称(空则由 utility_llm 自动生成)
|
|
|
|
|
|
|
|
|
|
+ # --- Trace 控制 ---
|
|
|
|
|
+ trace_id: Optional[str] = None # None = 新建
|
|
|
|
|
+ parent_trace_id: Optional[str] = None # 子 Agent 专用
|
|
|
|
|
+ parent_goal_id: Optional[str] = None
|
|
|
|
|
|
|
|
-@dataclass
|
|
|
|
|
-class CallResult:
|
|
|
|
|
- """单次调用结果"""
|
|
|
|
|
- reply: str
|
|
|
|
|
- tool_calls: Optional[List[Dict]] = None
|
|
|
|
|
- trace_id: Optional[str] = None
|
|
|
|
|
- step_id: Optional[str] = None
|
|
|
|
|
- tokens: Optional[Dict[str, int]] = None
|
|
|
|
|
- cost: float = 0.0
|
|
|
|
|
|
|
+ # --- 续跑控制 ---
|
|
|
|
|
+ insert_after: Optional[int] = None # 回溯插入点(message sequence)
|
|
|
|
|
+
|
|
|
|
|
+ # --- 额外 LLM 参数(传给 llm_call 的 **kwargs)---
|
|
|
|
|
+ extra_llm_params: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
# 内置工具列表(始终自动加载)
|
|
# 内置工具列表(始终自动加载)
|
|
@@ -98,22 +119,44 @@ BUILTIN_TOOLS = [
|
|
|
"browser_ensure_login_with_cookies",
|
|
"browser_ensure_login_with_cookies",
|
|
|
"browser_wait_for_user_action",
|
|
"browser_wait_for_user_action",
|
|
|
"browser_done",
|
|
"browser_done",
|
|
|
-
|
|
|
|
|
- # 飞书工具
|
|
|
|
|
- "feishu_get_chat_history",
|
|
|
|
|
- "feishu_get_contact_replies",
|
|
|
|
|
- "feishu_send_message_to_contact",
|
|
|
|
|
- "feishu_get_contact_list",
|
|
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ===== 向后兼容 =====
|
|
|
|
|
+
|
|
|
|
|
+@dataclass
|
|
|
|
|
+class AgentConfig:
|
|
|
|
|
+ """[向后兼容] Agent 配置,新代码请使用 RunConfig"""
|
|
|
|
|
+ agent_type: str = "default"
|
|
|
|
|
+ max_iterations: int = 200
|
|
|
|
|
+ enable_memory: bool = True
|
|
|
|
|
+ auto_execute_tools: bool = True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@dataclass
|
|
|
|
|
+class CallResult:
|
|
|
|
|
+ """单次调用结果"""
|
|
|
|
|
+ reply: str
|
|
|
|
|
+ tool_calls: Optional[List[Dict]] = None
|
|
|
|
|
+ trace_id: Optional[str] = None
|
|
|
|
|
+ step_id: Optional[str] = None
|
|
|
|
|
+ tokens: Optional[Dict[str, int]] = None
|
|
|
|
|
+ cost: float = 0.0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ===== 执行引擎 =====
|
|
|
|
|
+
|
|
|
|
|
+CONTEXT_INJECTION_INTERVAL = 10 # 每 N 轮注入一次 GoalTree + Collaborators
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class AgentRunner:
|
|
class AgentRunner:
|
|
|
"""
|
|
"""
|
|
|
Agent 执行引擎
|
|
Agent 执行引擎
|
|
|
|
|
|
|
|
- 支持两种模式:
|
|
|
|
|
- 1. call(): 单次 LLM 调用(简洁 API)
|
|
|
|
|
- 2. run(): Agent 模式(循环 + 记忆 + 追踪)
|
|
|
|
|
|
|
+ 支持三种运行模式(通过 RunConfig 区分):
|
|
|
|
|
+ 1. 新建:trace_id=None
|
|
|
|
|
+ 2. 续跑:trace_id=已有ID, insert_after=None
|
|
|
|
|
+ 3. 回溯:trace_id=已有ID, insert_after=N
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
def __init__(
|
|
@@ -123,6 +166,7 @@ class AgentRunner:
|
|
|
state_store: Optional[StateStore] = None,
|
|
state_store: Optional[StateStore] = None,
|
|
|
tool_registry: Optional[ToolRegistry] = None,
|
|
tool_registry: Optional[ToolRegistry] = None,
|
|
|
llm_call: Optional[Callable] = None,
|
|
llm_call: Optional[Callable] = None,
|
|
|
|
|
+ utility_llm_call: Optional[Callable] = None,
|
|
|
config: Optional[AgentConfig] = None,
|
|
config: Optional[AgentConfig] = None,
|
|
|
skills_dir: Optional[str] = None,
|
|
skills_dir: Optional[str] = None,
|
|
|
goal_tree: Optional[GoalTree] = None,
|
|
goal_tree: Optional[GoalTree] = None,
|
|
@@ -132,162 +176,99 @@ class AgentRunner:
|
|
|
初始化 AgentRunner
|
|
初始化 AgentRunner
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
- trace_store: Trace 存储(可选,不提供则不记录)
|
|
|
|
|
- memory_store: Memory 存储(可选,不提供则不使用记忆)
|
|
|
|
|
- state_store: State 存储(可选,用于任务状态)
|
|
|
|
|
- tool_registry: 工具注册表(可选,默认使用全局注册表)
|
|
|
|
|
- llm_call: LLM 调用函数(必须提供,用于实际调用 LLM)
|
|
|
|
|
- config: Agent 配置
|
|
|
|
|
- skills_dir: Skills 目录路径(可选,不提供则不加载 skills)
|
|
|
|
|
- goal_tree: 执行计划(可选,不提供则在运行时按需创建)
|
|
|
|
|
- debug: 保留参数(已废弃,请使用 API Server 可视化)
|
|
|
|
|
|
|
+ trace_store: Trace 存储
|
|
|
|
|
+ memory_store: Memory 存储(可选)
|
|
|
|
|
+ state_store: State 存储(可选)
|
|
|
|
|
+ tool_registry: 工具注册表(默认使用全局注册表)
|
|
|
|
|
+ llm_call: 主 LLM 调用函数
|
|
|
|
|
+ utility_llm_call: 轻量 LLM(用于生成任务标题等),可选
|
|
|
|
|
+ config: [向后兼容] AgentConfig
|
|
|
|
|
+ skills_dir: Skills 目录路径
|
|
|
|
|
+ goal_tree: 初始 GoalTree(可选)
|
|
|
|
|
+ debug: 保留参数(已废弃)
|
|
|
"""
|
|
"""
|
|
|
self.trace_store = trace_store
|
|
self.trace_store = trace_store
|
|
|
self.memory_store = memory_store
|
|
self.memory_store = memory_store
|
|
|
self.state_store = state_store
|
|
self.state_store = state_store
|
|
|
self.tools = tool_registry or get_tool_registry()
|
|
self.tools = tool_registry or get_tool_registry()
|
|
|
self.llm_call = llm_call
|
|
self.llm_call = llm_call
|
|
|
|
|
+ self.utility_llm_call = utility_llm_call
|
|
|
self.config = config or AgentConfig()
|
|
self.config = config or AgentConfig()
|
|
|
self.skills_dir = skills_dir
|
|
self.skills_dir = skills_dir
|
|
|
self.goal_tree = goal_tree
|
|
self.goal_tree = goal_tree
|
|
|
self.debug = debug
|
|
self.debug = debug
|
|
|
|
|
|
|
|
- def _generate_id(self) -> str:
|
|
|
|
|
- """生成唯一 ID"""
|
|
|
|
|
- import uuid
|
|
|
|
|
- return str(uuid.uuid4())
|
|
|
|
|
|
|
+ # ===== 核心公开方法 =====
|
|
|
|
|
|
|
|
- # ===== 单次调用 =====
|
|
|
|
|
-
|
|
|
|
|
- async def call(
|
|
|
|
|
|
|
+ async def run(
|
|
|
self,
|
|
self,
|
|
|
messages: List[Dict],
|
|
messages: List[Dict],
|
|
|
- model: str = "gpt-4o",
|
|
|
|
|
- tools: Optional[List[str]] = None,
|
|
|
|
|
- uid: Optional[str] = None,
|
|
|
|
|
- trace: bool = True,
|
|
|
|
|
- **kwargs
|
|
|
|
|
- ) -> CallResult:
|
|
|
|
|
|
|
+ config: Optional[RunConfig] = None,
|
|
|
|
|
+ ) -> AsyncIterator[Union[Trace, Message]]:
|
|
|
"""
|
|
"""
|
|
|
- 单次 LLM 调用
|
|
|
|
|
|
|
+ Agent 模式执行(核心方法)
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
- messages: 消息列表
|
|
|
|
|
- model: 模型名称
|
|
|
|
|
- tools: 工具名称列表
|
|
|
|
|
- uid: 用户 ID
|
|
|
|
|
- trace: 是否记录 Trace
|
|
|
|
|
- **kwargs: 其他参数传递给 LLM
|
|
|
|
|
|
|
+ messages: OpenAI SDK 格式的输入消息
|
|
|
|
|
+ 新建: 初始任务消息 [{"role": "user", "content": "..."}]
|
|
|
|
|
+ 续跑: 追加的新消息
|
|
|
|
|
+ 回溯: 在插入点之后追加的消息
|
|
|
|
|
+ config: 运行配置
|
|
|
|
|
|
|
|
- Returns:
|
|
|
|
|
- CallResult
|
|
|
|
|
|
|
+ Yields:
|
|
|
|
|
+ Union[Trace, Message]: Trace 对象(状态变化)或 Message 对象(执行过程)
|
|
|
"""
|
|
"""
|
|
|
if not self.llm_call:
|
|
if not self.llm_call:
|
|
|
raise ValueError("llm_call function not provided")
|
|
raise ValueError("llm_call function not provided")
|
|
|
|
|
|
|
|
- trace_id = None
|
|
|
|
|
- message_id = None
|
|
|
|
|
-
|
|
|
|
|
- # 准备工具 Schema
|
|
|
|
|
- tool_names = BUILTIN_TOOLS.copy()
|
|
|
|
|
- if tools:
|
|
|
|
|
- for tool in tools:
|
|
|
|
|
- if tool not in tool_names:
|
|
|
|
|
- tool_names.append(tool)
|
|
|
|
|
- tool_schemas = self.tools.get_schemas(tool_names)
|
|
|
|
|
|
|
+ config = config or RunConfig()
|
|
|
|
|
+ trace = None
|
|
|
|
|
|
|
|
- # 创建 Trace
|
|
|
|
|
- if trace and self.trace_store:
|
|
|
|
|
- trace_obj = Trace.create(
|
|
|
|
|
- mode="call",
|
|
|
|
|
- uid=uid,
|
|
|
|
|
- model=model,
|
|
|
|
|
- tools=tool_schemas, # 保存工具定义
|
|
|
|
|
- llm_params=kwargs, # 保存 LLM 参数
|
|
|
|
|
- )
|
|
|
|
|
- trace_id = await self.trace_store.create_trace(trace_obj)
|
|
|
|
|
-
|
|
|
|
|
- # 调用 LLM
|
|
|
|
|
- result = await self.llm_call(
|
|
|
|
|
- messages=messages,
|
|
|
|
|
- model=model,
|
|
|
|
|
- tools=tool_schemas,
|
|
|
|
|
- **kwargs
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- # 记录 Message(单次调用模式不使用 GoalTree)
|
|
|
|
|
- if trace and self.trace_store and trace_id:
|
|
|
|
|
- msg = Message.create(
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- role="assistant",
|
|
|
|
|
- sequence=1,
|
|
|
|
|
- goal_id=None, # 单次调用没有 goal
|
|
|
|
|
- content={"text": result.get("content", ""), "tool_calls": result.get("tool_calls")},
|
|
|
|
|
- prompt_tokens=result.get("prompt_tokens", 0),
|
|
|
|
|
- completion_tokens=result.get("completion_tokens", 0),
|
|
|
|
|
- finish_reason=result.get("finish_reason"),
|
|
|
|
|
- cost=result.get("cost", 0),
|
|
|
|
|
- )
|
|
|
|
|
- message_id = await self.trace_store.add_message(msg)
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Phase 1: PREPARE TRACE
|
|
|
|
|
+ trace, goal_tree, sequence = await self._prepare_trace(messages, config)
|
|
|
|
|
+ yield trace
|
|
|
|
|
|
|
|
- # 完成 Trace
|
|
|
|
|
- await self.trace_store.update_trace(
|
|
|
|
|
- trace_id,
|
|
|
|
|
- status="completed",
|
|
|
|
|
- completed_at=datetime.now(),
|
|
|
|
|
|
|
+ # Phase 2: BUILD HISTORY
|
|
|
|
|
+ history, sequence, created_messages = await self._build_history(
|
|
|
|
|
+ trace.trace_id, messages, goal_tree, config, sequence
|
|
|
)
|
|
)
|
|
|
|
|
+ for msg in created_messages:
|
|
|
|
|
+ yield msg
|
|
|
|
|
|
|
|
- return CallResult(
|
|
|
|
|
- reply=result.get("content", ""),
|
|
|
|
|
- tool_calls=result.get("tool_calls"),
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- step_id=message_id, # 兼容字段名
|
|
|
|
|
- tokens={
|
|
|
|
|
- "prompt": result.get("prompt_tokens", 0),
|
|
|
|
|
- "completion": result.get("completion_tokens", 0),
|
|
|
|
|
- },
|
|
|
|
|
- cost=result.get("cost", 0)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ # Phase 3: AGENT LOOP
|
|
|
|
|
+ async for event in self._agent_loop(trace, history, goal_tree, config, sequence):
|
|
|
|
|
+ yield event
|
|
|
|
|
|
|
|
- # ===== Agent 模式 =====
|
|
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Agent run failed: {e}")
|
|
|
|
|
+ tid = config.trace_id or (trace.trace_id if trace else None)
|
|
|
|
|
+ if self.trace_store and tid:
|
|
|
|
|
+ await self.trace_store.update_trace(
|
|
|
|
|
+ tid,
|
|
|
|
|
+ status="failed",
|
|
|
|
|
+ error_message=str(e),
|
|
|
|
|
+ completed_at=datetime.now()
|
|
|
|
|
+ )
|
|
|
|
|
+ trace_obj = await self.trace_store.get_trace(tid)
|
|
|
|
|
+ if trace_obj:
|
|
|
|
|
+ yield trace_obj
|
|
|
|
|
+ raise
|
|
|
|
|
|
|
|
async def run_result(
|
|
async def run_result(
|
|
|
self,
|
|
self,
|
|
|
- task: str,
|
|
|
|
|
- messages: Optional[List[Dict]] = None,
|
|
|
|
|
- system_prompt: Optional[str] = None,
|
|
|
|
|
- model: str = "gpt-4o",
|
|
|
|
|
- tools: Optional[List[str]] = None,
|
|
|
|
|
- agent_type: Optional[str] = None,
|
|
|
|
|
- uid: Optional[str] = None,
|
|
|
|
|
- max_iterations: Optional[int] = None,
|
|
|
|
|
- enable_memory: Optional[bool] = None,
|
|
|
|
|
- auto_execute_tools: Optional[bool] = None,
|
|
|
|
|
- trace_id: Optional[str] = None,
|
|
|
|
|
- **kwargs
|
|
|
|
|
|
|
+ messages: List[Dict],
|
|
|
|
|
+ config: Optional[RunConfig] = None,
|
|
|
) -> Dict[str, Any]:
|
|
) -> Dict[str, Any]:
|
|
|
"""
|
|
"""
|
|
|
- Agent 结果模式执行。
|
|
|
|
|
|
|
+ 结果模式 — 消费 run(),返回结构化结果。
|
|
|
|
|
|
|
|
- 消费 run() 的流式事件,返回结构化结果(最后一条有文本的 assistant + trace 统计)。
|
|
|
|
|
|
|
+ 主要用于 subagent 工具内部。
|
|
|
"""
|
|
"""
|
|
|
last_assistant_text = ""
|
|
last_assistant_text = ""
|
|
|
final_trace: Optional[Trace] = None
|
|
final_trace: Optional[Trace] = None
|
|
|
|
|
|
|
|
- async for item in self.run(
|
|
|
|
|
- task=task,
|
|
|
|
|
- messages=messages,
|
|
|
|
|
- system_prompt=system_prompt,
|
|
|
|
|
- model=model,
|
|
|
|
|
- tools=tools,
|
|
|
|
|
- agent_type=agent_type,
|
|
|
|
|
- uid=uid,
|
|
|
|
|
- max_iterations=max_iterations,
|
|
|
|
|
- enable_memory=enable_memory,
|
|
|
|
|
- auto_execute_tools=auto_execute_tools,
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- **kwargs
|
|
|
|
|
- ):
|
|
|
|
|
|
|
+ async for item in self.run(messages=messages, config=config):
|
|
|
if isinstance(item, Message) and item.role == "assistant":
|
|
if isinstance(item, Message) and item.role == "assistant":
|
|
|
content = item.content
|
|
content = item.content
|
|
|
text = ""
|
|
text = ""
|
|
@@ -300,8 +281,9 @@ class AgentRunner:
|
|
|
elif isinstance(item, Trace):
|
|
elif isinstance(item, Trace):
|
|
|
final_trace = item
|
|
final_trace = item
|
|
|
|
|
|
|
|
- if not final_trace and trace_id and self.trace_store:
|
|
|
|
|
- final_trace = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
|
|
+ config = config or RunConfig()
|
|
|
|
|
+ if not final_trace and config.trace_id and self.trace_store:
|
|
|
|
|
+ final_trace = await self.trace_store.get_trace(config.trace_id)
|
|
|
|
|
|
|
|
status = final_trace.status if final_trace else "unknown"
|
|
status = final_trace.status if final_trace else "unknown"
|
|
|
error = final_trace.error_message if final_trace else None
|
|
error = final_trace.error_message if final_trace else None
|
|
@@ -309,12 +291,12 @@ class AgentRunner:
|
|
|
|
|
|
|
|
if not summary:
|
|
if not summary:
|
|
|
status = "failed"
|
|
status = "failed"
|
|
|
- error = error or "Sub-Agent 没有产生 assistant 文本结果"
|
|
|
|
|
|
|
+ error = error or "Agent 没有产生 assistant 文本结果"
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
"status": status,
|
|
"status": status,
|
|
|
"summary": summary,
|
|
"summary": summary,
|
|
|
- "trace_id": final_trace.trace_id if final_trace else trace_id,
|
|
|
|
|
|
|
+ "trace_id": final_trace.trace_id if final_trace else config.trace_id,
|
|
|
"error": error,
|
|
"error": error,
|
|
|
"stats": {
|
|
"stats": {
|
|
|
"total_messages": final_trace.total_messages if final_trace else 0,
|
|
"total_messages": final_trace.total_messages if final_trace else 0,
|
|
@@ -323,51 +305,26 @@ class AgentRunner:
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async def run(
|
|
|
|
|
|
|
+ # ===== 单次调用(保留)=====
|
|
|
|
|
+
|
|
|
|
|
+ async def call(
|
|
|
self,
|
|
self,
|
|
|
- task: str,
|
|
|
|
|
- messages: Optional[List[Dict]] = None,
|
|
|
|
|
- system_prompt: Optional[str] = None,
|
|
|
|
|
|
|
+ messages: List[Dict],
|
|
|
model: str = "gpt-4o",
|
|
model: str = "gpt-4o",
|
|
|
tools: Optional[List[str]] = None,
|
|
tools: Optional[List[str]] = None,
|
|
|
- agent_type: Optional[str] = None,
|
|
|
|
|
uid: Optional[str] = None,
|
|
uid: Optional[str] = None,
|
|
|
- max_iterations: Optional[int] = None,
|
|
|
|
|
- enable_memory: Optional[bool] = None,
|
|
|
|
|
- auto_execute_tools: Optional[bool] = None,
|
|
|
|
|
- trace_id: Optional[str] = None,
|
|
|
|
|
|
|
+ trace: bool = True,
|
|
|
**kwargs
|
|
**kwargs
|
|
|
- ) -> AsyncIterator[Union[Trace, Message]]:
|
|
|
|
|
|
|
+ ) -> CallResult:
|
|
|
"""
|
|
"""
|
|
|
- Agent 模式执行
|
|
|
|
|
-
|
|
|
|
|
- Args:
|
|
|
|
|
- task: 任务描述
|
|
|
|
|
- messages: 初始消息(可选)
|
|
|
|
|
- system_prompt: 系统提示(可选)
|
|
|
|
|
- model: 模型名称
|
|
|
|
|
- tools: 工具名称列表
|
|
|
|
|
- agent_type: Agent 类型
|
|
|
|
|
- uid: 用户 ID
|
|
|
|
|
- max_iterations: 最大迭代次数
|
|
|
|
|
- enable_memory: 是否启用记忆
|
|
|
|
|
- auto_execute_tools: 是否自动执行工具
|
|
|
|
|
- trace_id: Trace ID(可选,传入时复用已有 Trace)
|
|
|
|
|
- **kwargs: 其他参数
|
|
|
|
|
-
|
|
|
|
|
- Yields:
|
|
|
|
|
- Union[Trace, Message]: Trace 对象(状态变化)或 Message 对象(执行过程)
|
|
|
|
|
|
|
+ 单次 LLM 调用(无 Agent Loop)
|
|
|
"""
|
|
"""
|
|
|
if not self.llm_call:
|
|
if not self.llm_call:
|
|
|
raise ValueError("llm_call function not provided")
|
|
raise ValueError("llm_call function not provided")
|
|
|
|
|
|
|
|
- # 使用配置默认值
|
|
|
|
|
- agent_type = agent_type or self.config.agent_type
|
|
|
|
|
- max_iterations = max_iterations or self.config.max_iterations
|
|
|
|
|
- enable_memory = enable_memory if enable_memory is not None else self.config.enable_memory
|
|
|
|
|
- auto_execute_tools = auto_execute_tools if auto_execute_tools is not None else self.config.auto_execute_tools
|
|
|
|
|
|
|
+ trace_id = None
|
|
|
|
|
+ message_id = None
|
|
|
|
|
|
|
|
- # 准备工具 Schema(提前准备,用于 Trace)
|
|
|
|
|
tool_names = BUILTIN_TOOLS.copy()
|
|
tool_names = BUILTIN_TOOLS.copy()
|
|
|
if tools:
|
|
if tools:
|
|
|
for tool in tools:
|
|
for tool in tools:
|
|
@@ -375,319 +332,554 @@ class AgentRunner:
|
|
|
tool_names.append(tool)
|
|
tool_names.append(tool)
|
|
|
tool_schemas = self.tools.get_schemas(tool_names)
|
|
tool_schemas = self.tools.get_schemas(tool_names)
|
|
|
|
|
|
|
|
- # 创建或复用 Trace
|
|
|
|
|
- if trace_id:
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- trace_obj = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
- if not trace_obj:
|
|
|
|
|
- raise ValueError(f"Trace not found: {trace_id}")
|
|
|
|
|
- else:
|
|
|
|
|
- trace_obj = Trace(
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- mode="agent",
|
|
|
|
|
- task=task,
|
|
|
|
|
- agent_type=agent_type,
|
|
|
|
|
- uid=uid,
|
|
|
|
|
- model=model,
|
|
|
|
|
- tools=tool_schemas,
|
|
|
|
|
- llm_params=kwargs,
|
|
|
|
|
- status="running"
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ if trace and self.trace_store:
|
|
|
|
|
+ trace_obj = Trace.create(mode="call", uid=uid, model=model, tools=tool_schemas, llm_params=kwargs)
|
|
|
|
|
+ trace_id = await self.trace_store.create_trace(trace_obj)
|
|
|
|
|
+
|
|
|
|
|
+ result = await self.llm_call(messages=messages, model=model, tools=tool_schemas, **kwargs)
|
|
|
|
|
+
|
|
|
|
|
+ if trace and self.trace_store and trace_id:
|
|
|
|
|
+ msg = Message.create(
|
|
|
|
|
+ trace_id=trace_id, role="assistant", sequence=1, goal_id=None,
|
|
|
|
|
+ content={"text": result.get("content", ""), "tool_calls": result.get("tool_calls")},
|
|
|
|
|
+ prompt_tokens=result.get("prompt_tokens", 0),
|
|
|
|
|
+ completion_tokens=result.get("completion_tokens", 0),
|
|
|
|
|
+ finish_reason=result.get("finish_reason"),
|
|
|
|
|
+ cost=result.get("cost", 0),
|
|
|
|
|
+ )
|
|
|
|
|
+ message_id = await self.trace_store.add_message(msg)
|
|
|
|
|
+ await self.trace_store.update_trace(trace_id, status="completed", completed_at=datetime.now())
|
|
|
|
|
+
|
|
|
|
|
+ return CallResult(
|
|
|
|
|
+ reply=result.get("content", ""),
|
|
|
|
|
+ tool_calls=result.get("tool_calls"),
|
|
|
|
|
+ trace_id=trace_id,
|
|
|
|
|
+ step_id=message_id,
|
|
|
|
|
+ tokens={"prompt": result.get("prompt_tokens", 0), "completion": result.get("completion_tokens", 0)},
|
|
|
|
|
+ cost=result.get("cost", 0)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # ===== Phase 1: PREPARE TRACE =====
|
|
|
|
|
+
|
|
|
|
|
+ async def _prepare_trace(
|
|
|
|
|
+ self,
|
|
|
|
|
+ messages: List[Dict],
|
|
|
|
|
+ config: RunConfig,
|
|
|
|
|
+ ) -> Tuple[Trace, Optional[GoalTree], int]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 准备 Trace:创建新的或加载已有的
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ (trace, goal_tree, next_sequence)
|
|
|
|
|
+ """
|
|
|
|
|
+ if config.trace_id:
|
|
|
|
|
+ return await self._prepare_existing_trace(config)
|
|
|
else:
|
|
else:
|
|
|
- trace_id = self._generate_id()
|
|
|
|
|
- trace_obj = Trace(
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- mode="agent",
|
|
|
|
|
- task=task,
|
|
|
|
|
- agent_type=agent_type,
|
|
|
|
|
- uid=uid,
|
|
|
|
|
- model=model,
|
|
|
|
|
- tools=tool_schemas, # 保存工具定义
|
|
|
|
|
- llm_params=kwargs, # 保存 LLM 参数
|
|
|
|
|
- status="running"
|
|
|
|
|
|
|
+ return await self._prepare_new_trace(messages, config)
|
|
|
|
|
+
|
|
|
|
|
+ async def _prepare_new_trace(
|
|
|
|
|
+ self,
|
|
|
|
|
+ messages: List[Dict],
|
|
|
|
|
+ config: RunConfig,
|
|
|
|
|
+ ) -> Tuple[Trace, Optional[GoalTree], int]:
|
|
|
|
|
+ """创建新 Trace"""
|
|
|
|
|
+ trace_id = str(uuid.uuid4())
|
|
|
|
|
+
|
|
|
|
|
+ # 生成任务名称
|
|
|
|
|
+ task_name = config.name or await self._generate_task_name(messages)
|
|
|
|
|
+
|
|
|
|
|
+ # 准备工具 Schema
|
|
|
|
|
+ tool_schemas = self._get_tool_schemas(config.tools)
|
|
|
|
|
+
|
|
|
|
|
+ trace_obj = Trace(
|
|
|
|
|
+ trace_id=trace_id,
|
|
|
|
|
+ mode="agent",
|
|
|
|
|
+ task=task_name,
|
|
|
|
|
+ agent_type=config.agent_type,
|
|
|
|
|
+ parent_trace_id=config.parent_trace_id,
|
|
|
|
|
+ parent_goal_id=config.parent_goal_id,
|
|
|
|
|
+ uid=config.uid,
|
|
|
|
|
+ model=config.model,
|
|
|
|
|
+ tools=tool_schemas,
|
|
|
|
|
+ llm_params={"temperature": config.temperature, **config.extra_llm_params},
|
|
|
|
|
+ status="running",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ goal_tree = self.goal_tree or GoalTree(mission=task_name)
|
|
|
|
|
+
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ await self.trace_store.create_trace(trace_obj)
|
|
|
|
|
+ await self.trace_store.update_goal_tree(trace_id, goal_tree)
|
|
|
|
|
+
|
|
|
|
|
+ return trace_obj, goal_tree, 1
|
|
|
|
|
+
|
|
|
|
|
+ async def _prepare_existing_trace(
|
|
|
|
|
+ self,
|
|
|
|
|
+ config: RunConfig,
|
|
|
|
|
+ ) -> Tuple[Trace, Optional[GoalTree], int]:
|
|
|
|
|
+ """加载已有 Trace(续跑或回溯)"""
|
|
|
|
|
+ if not self.trace_store:
|
|
|
|
|
+ raise ValueError("trace_store required for continue/rewind")
|
|
|
|
|
+
|
|
|
|
|
+ trace_obj = await self.trace_store.get_trace(config.trace_id)
|
|
|
|
|
+ if not trace_obj:
|
|
|
|
|
+ raise ValueError(f"Trace not found: {config.trace_id}")
|
|
|
|
|
+
|
|
|
|
|
+ goal_tree = await self.trace_store.get_goal_tree(config.trace_id)
|
|
|
|
|
+
|
|
|
|
|
+ if config.insert_after is not None:
|
|
|
|
|
+ # 回溯模式
|
|
|
|
|
+ sequence = await self._rewind(config.trace_id, config.insert_after, goal_tree)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 续跑模式:从最大 sequence + 1 开始
|
|
|
|
|
+ all_messages = await self.trace_store.get_trace_messages(
|
|
|
|
|
+ config.trace_id, include_abandoned=True
|
|
|
)
|
|
)
|
|
|
|
|
+ sequence = max((m.sequence for m in all_messages), default=0) + 1
|
|
|
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- await self.trace_store.create_trace(trace_obj)
|
|
|
|
|
|
|
+ # 状态置为 running
|
|
|
|
|
+ await self.trace_store.update_trace(
|
|
|
|
|
+ config.trace_id,
|
|
|
|
|
+ status="running",
|
|
|
|
|
+ completed_at=None,
|
|
|
|
|
+ )
|
|
|
|
|
+ trace_obj.status = "running"
|
|
|
|
|
|
|
|
- # 初始化 GoalTree
|
|
|
|
|
- goal_tree = self.goal_tree or GoalTree(mission=task)
|
|
|
|
|
- await self.trace_store.update_goal_tree(trace_id, goal_tree)
|
|
|
|
|
|
|
+ return trace_obj, goal_tree, sequence
|
|
|
|
|
|
|
|
- # 返回 Trace(表示开始)
|
|
|
|
|
- yield trace_obj
|
|
|
|
|
|
|
+ # ===== Phase 2: BUILD HISTORY =====
|
|
|
|
|
+
|
|
|
|
|
+ async def _build_history(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace_id: str,
|
|
|
|
|
+ new_messages: List[Dict],
|
|
|
|
|
+ goal_tree: Optional[GoalTree],
|
|
|
|
|
+ config: RunConfig,
|
|
|
|
|
+ sequence: int,
|
|
|
|
|
+ ) -> Tuple[List[Dict], int, List[Message]]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 构建完整的 LLM 消息历史
|
|
|
|
|
+
|
|
|
|
|
+ 1. 加载已有 active messages(续跑/回溯场景)
|
|
|
|
|
+ 2. 构建 system prompt(新建时注入 skills/experiences)
|
|
|
|
|
+ 3. 追加 input messages
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ (history, next_sequence, created_messages)
|
|
|
|
|
+ created_messages: 本次新创建并持久化的 Message 列表,供 run() yield 给调用方
|
|
|
|
|
+ """
|
|
|
|
|
+ history: List[Dict] = []
|
|
|
|
|
+ created_messages: List[Message] = []
|
|
|
|
|
+
|
|
|
|
|
+ # 1. 加载已有 messages
|
|
|
|
|
+ if config.trace_id and self.trace_store:
|
|
|
|
|
+ existing_messages = await self.trace_store.get_trace_messages(trace_id)
|
|
|
|
|
+ history = [msg.to_llm_dict() for msg in existing_messages]
|
|
|
|
|
+
|
|
|
|
|
+ # 2. 构建 system prompt(如果历史中没有 system message)
|
|
|
|
|
+ has_system = any(m.get("role") == "system" for m in history)
|
|
|
|
|
+ has_system_in_new = any(m.get("role") == "system" for m in new_messages)
|
|
|
|
|
+
|
|
|
|
|
+ if not has_system and not has_system_in_new:
|
|
|
|
|
+ system_prompt = await self._build_system_prompt(config)
|
|
|
|
|
+ if system_prompt:
|
|
|
|
|
+ history = [{"role": "system", "content": system_prompt}] + history
|
|
|
|
|
|
|
|
- try:
|
|
|
|
|
- # 加载记忆(Experience 和 Skill)
|
|
|
|
|
- experiences_text = ""
|
|
|
|
|
- skills_text = ""
|
|
|
|
|
-
|
|
|
|
|
- if enable_memory and self.memory_store:
|
|
|
|
|
- scope = f"agent:{agent_type}"
|
|
|
|
|
- experiences = await self.memory_store.search_experiences(scope, task)
|
|
|
|
|
- experiences_text = self._format_experiences(experiences)
|
|
|
|
|
- logger.info(f"加载 {len(experiences)} 条经验")
|
|
|
|
|
-
|
|
|
|
|
- # 加载 Skills(内置 + 用户自定义)
|
|
|
|
|
- skills = load_skills_from_dir(self.skills_dir)
|
|
|
|
|
- if skills:
|
|
|
|
|
- skills_text = self._format_skills(skills)
|
|
|
|
|
- if self.skills_dir:
|
|
|
|
|
- logger.info(f"加载 {len(skills)} 个 skills (内置 + 自定义: {self.skills_dir})")
|
|
|
|
|
- else:
|
|
|
|
|
- logger.info(f"加载 {len(skills)} 个内置 skills")
|
|
|
|
|
-
|
|
|
|
|
- # 构建初始消息
|
|
|
|
|
- sequence = 1
|
|
|
|
|
- if messages is None:
|
|
|
|
|
- if trace_id and self.trace_store:
|
|
|
|
|
- existing_messages = await self.trace_store.get_trace_messages(trace_id)
|
|
|
|
|
- messages = []
|
|
|
|
|
- for msg in existing_messages:
|
|
|
|
|
- msg_dict = {"role": msg.role}
|
|
|
|
|
- if isinstance(msg.content, dict):
|
|
|
|
|
- if msg.content.get("text"):
|
|
|
|
|
- msg_dict["content"] = msg.content["text"]
|
|
|
|
|
- if msg.content.get("tool_calls"):
|
|
|
|
|
- msg_dict["tool_calls"] = msg.content["tool_calls"]
|
|
|
|
|
- else:
|
|
|
|
|
- msg_dict["content"] = msg.content
|
|
|
|
|
-
|
|
|
|
|
- if msg.role == "tool" and msg.tool_call_id:
|
|
|
|
|
- msg_dict["tool_call_id"] = msg.tool_call_id
|
|
|
|
|
- msg_dict["name"] = msg.description or "unknown"
|
|
|
|
|
-
|
|
|
|
|
- messages.append(msg_dict)
|
|
|
|
|
-
|
|
|
|
|
- if existing_messages:
|
|
|
|
|
- sequence = existing_messages[-1].sequence + 1
|
|
|
|
|
- else:
|
|
|
|
|
- messages = []
|
|
|
|
|
- # 记录初始 system 和 user 消息到 trace
|
|
|
|
|
-
|
|
|
|
|
- if system_prompt and not any(m.get("role") == "system" for m in messages):
|
|
|
|
|
- # 注入记忆和 skills 到 system prompt
|
|
|
|
|
- full_system = system_prompt
|
|
|
|
|
- if skills_text:
|
|
|
|
|
- full_system += f"\n\n## Skills\n{skills_text}"
|
|
|
|
|
- if experiences_text:
|
|
|
|
|
- full_system += f"\n\n## 相关经验\n{experiences_text}"
|
|
|
|
|
-
|
|
|
|
|
- messages = [{"role": "system", "content": full_system}] + messages
|
|
|
|
|
-
|
|
|
|
|
- # 保存 system 消息
|
|
|
|
|
if self.trace_store:
|
|
if self.trace_store:
|
|
|
system_msg = Message.create(
|
|
system_msg = Message.create(
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- role="system",
|
|
|
|
|
- sequence=sequence,
|
|
|
|
|
- goal_id=None, # 初始消息没有 goal
|
|
|
|
|
- content=full_system,
|
|
|
|
|
|
|
+ trace_id=trace_id, role="system", sequence=sequence,
|
|
|
|
|
+ goal_id=None, content=system_prompt,
|
|
|
)
|
|
)
|
|
|
await self.trace_store.add_message(system_msg)
|
|
await self.trace_store.add_message(system_msg)
|
|
|
- yield system_msg
|
|
|
|
|
|
|
+ created_messages.append(system_msg)
|
|
|
sequence += 1
|
|
sequence += 1
|
|
|
|
|
|
|
|
- # 添加任务描述(支持 continue_from 场景再次追加)
|
|
|
|
|
- if task:
|
|
|
|
|
- messages.append({"role": "user", "content": task})
|
|
|
|
|
|
|
+ # 3. 追加新 messages
|
|
|
|
|
+ for msg_dict in new_messages:
|
|
|
|
|
+ history.append(msg_dict)
|
|
|
|
|
|
|
|
- # 保存 user 消息(任务描述)
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- user_msg = Message.create(
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- role="user",
|
|
|
|
|
- sequence=sequence,
|
|
|
|
|
- goal_id=None, # 初始消息没有 goal
|
|
|
|
|
- content=task,
|
|
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ stored_msg = Message.from_llm_dict(
|
|
|
|
|
+ msg_dict, trace_id=trace_id, sequence=sequence, goal_id=None
|
|
|
|
|
+ )
|
|
|
|
|
+ await self.trace_store.add_message(stored_msg)
|
|
|
|
|
+ created_messages.append(stored_msg)
|
|
|
|
|
+ sequence += 1
|
|
|
|
|
+
|
|
|
|
|
+ return history, sequence, created_messages
|
|
|
|
|
+
|
|
|
|
|
+ # ===== Phase 3: AGENT LOOP =====
|
|
|
|
|
+
|
|
|
|
|
+ async def _agent_loop(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace: Trace,
|
|
|
|
|
+ history: List[Dict],
|
|
|
|
|
+ goal_tree: Optional[GoalTree],
|
|
|
|
|
+ config: RunConfig,
|
|
|
|
|
+ sequence: int,
|
|
|
|
|
+ ) -> AsyncIterator[Union[Trace, Message]]:
|
|
|
|
|
+ """ReAct 循环"""
|
|
|
|
|
+ trace_id = trace.trace_id
|
|
|
|
|
+ tool_schemas = self._get_tool_schemas(config.tools)
|
|
|
|
|
+
|
|
|
|
|
+ # 设置 goal_tree 到 goal 工具
|
|
|
|
|
+ if goal_tree and self.trace_store:
|
|
|
|
|
+ from agent.trace.goal_tool import set_goal_tree
|
|
|
|
|
+ set_goal_tree(goal_tree)
|
|
|
|
|
+
|
|
|
|
|
+ for iteration in range(config.max_iterations):
|
|
|
|
|
+ # 构建 LLM messages(注入上下文)
|
|
|
|
|
+ llm_messages = list(history)
|
|
|
|
|
+
|
|
|
|
|
+ # 周期性注入 GoalTree + Collaborators
|
|
|
|
|
+ if iteration % CONTEXT_INJECTION_INTERVAL == 0:
|
|
|
|
|
+ context_injection = self._build_context_injection(trace, goal_tree)
|
|
|
|
|
+ if context_injection:
|
|
|
|
|
+ llm_messages.append({"role": "system", "content": context_injection})
|
|
|
|
|
+
|
|
|
|
|
+ # 调用 LLM
|
|
|
|
|
+ result = await self.llm_call(
|
|
|
|
|
+ messages=llm_messages,
|
|
|
|
|
+ model=config.model,
|
|
|
|
|
+ tools=tool_schemas,
|
|
|
|
|
+ temperature=config.temperature,
|
|
|
|
|
+ **config.extra_llm_params,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ response_content = result.get("content", "")
|
|
|
|
|
+ tool_calls = result.get("tool_calls")
|
|
|
|
|
+ finish_reason = result.get("finish_reason")
|
|
|
|
|
+ prompt_tokens = result.get("prompt_tokens", 0)
|
|
|
|
|
+ completion_tokens = result.get("completion_tokens", 0)
|
|
|
|
|
+ step_cost = result.get("cost", 0)
|
|
|
|
|
+
|
|
|
|
|
+ # 按需自动创建 root goal
|
|
|
|
|
+ if goal_tree and not goal_tree.goals and tool_calls:
|
|
|
|
|
+ has_goal_call = any(
|
|
|
|
|
+ tc.get("function", {}).get("name") == "goal"
|
|
|
|
|
+ for tc in tool_calls
|
|
|
|
|
+ )
|
|
|
|
|
+ if not has_goal_call:
|
|
|
|
|
+ mission = goal_tree.mission
|
|
|
|
|
+ root_desc = mission[:200] if len(mission) > 200 else mission
|
|
|
|
|
+ goal_tree.add_goals(
|
|
|
|
|
+ descriptions=[root_desc],
|
|
|
|
|
+ reasons=["系统自动创建:Agent 未显式创建目标"],
|
|
|
|
|
+ parent_id=None
|
|
|
)
|
|
)
|
|
|
- await self.trace_store.add_message(user_msg)
|
|
|
|
|
- yield user_msg
|
|
|
|
|
- sequence += 1
|
|
|
|
|
|
|
+ goal_tree.focus(goal_tree.goals[0].id)
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ await self.trace_store.update_goal_tree(trace_id, goal_tree)
|
|
|
|
|
+ await self.trace_store.add_goal(trace_id, goal_tree.goals[0])
|
|
|
|
|
+ logger.info(f"自动创建 root goal: {goal_tree.goals[0].id}")
|
|
|
|
|
+
|
|
|
|
|
+ # 获取当前 goal_id
|
|
|
|
|
+ current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
|
|
|
|
|
+
|
|
|
|
|
+ # 记录 assistant Message
|
|
|
|
|
+ assistant_msg = Message.create(
|
|
|
|
|
+ trace_id=trace_id,
|
|
|
|
|
+ role="assistant",
|
|
|
|
|
+ sequence=sequence,
|
|
|
|
|
+ goal_id=current_goal_id,
|
|
|
|
|
+ content={"text": response_content, "tool_calls": tool_calls},
|
|
|
|
|
+ prompt_tokens=prompt_tokens,
|
|
|
|
|
+ completion_tokens=completion_tokens,
|
|
|
|
|
+ finish_reason=finish_reason,
|
|
|
|
|
+ cost=step_cost,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- # 获取 GoalTree
|
|
|
|
|
- goal_tree = None
|
|
|
|
|
if self.trace_store:
|
|
if self.trace_store:
|
|
|
- goal_tree = await self.trace_store.get_goal_tree(trace_id)
|
|
|
|
|
-
|
|
|
|
|
- # 设置 goal_tree 到 goal 工具(供 LLM 调用)
|
|
|
|
|
- from agent.trace.goal_tool import set_goal_tree
|
|
|
|
|
- set_goal_tree(goal_tree)
|
|
|
|
|
-
|
|
|
|
|
- # 执行循环
|
|
|
|
|
- for iteration in range(max_iterations):
|
|
|
|
|
- # 注入当前计划到 messages(如果有 goals)
|
|
|
|
|
- llm_messages = list(messages)
|
|
|
|
|
- if goal_tree and goal_tree.goals:
|
|
|
|
|
- plan_text = f"\n## Current Plan\n\n{goal_tree.to_prompt()}"
|
|
|
|
|
- # 在最后一条 system 消息之后注入
|
|
|
|
|
- llm_messages.append({"role": "system", "content": plan_text})
|
|
|
|
|
-
|
|
|
|
|
- # 调用 LLM
|
|
|
|
|
- result = await self.llm_call(
|
|
|
|
|
- messages=llm_messages,
|
|
|
|
|
- model=model,
|
|
|
|
|
- tools=tool_schemas,
|
|
|
|
|
- **kwargs
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ await self.trace_store.add_message(assistant_msg)
|
|
|
|
|
+
|
|
|
|
|
+ yield assistant_msg
|
|
|
|
|
+ sequence += 1
|
|
|
|
|
+
|
|
|
|
|
+ # 处理工具调用
|
|
|
|
|
+ if tool_calls and config.auto_execute_tools:
|
|
|
|
|
+ history.append({
|
|
|
|
|
+ "role": "assistant",
|
|
|
|
|
+ "content": response_content,
|
|
|
|
|
+ "tool_calls": tool_calls,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ for tc in tool_calls:
|
|
|
|
|
+ current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
|
|
|
|
|
+
|
|
|
|
|
+ tool_name = tc["function"]["name"]
|
|
|
|
|
+ tool_args = tc["function"]["arguments"]
|
|
|
|
|
+
|
|
|
|
|
+ if isinstance(tool_args, str):
|
|
|
|
|
+ tool_args = json.loads(tool_args) if tool_args.strip() else {}
|
|
|
|
|
+ elif tool_args is None:
|
|
|
|
|
+ tool_args = {}
|
|
|
|
|
+
|
|
|
|
|
+ tool_result = await self.tools.execute(
|
|
|
|
|
+ tool_name,
|
|
|
|
|
+ tool_args,
|
|
|
|
|
+ uid=config.uid or "",
|
|
|
|
|
+ context={
|
|
|
|
|
+ "store": self.trace_store,
|
|
|
|
|
+ "trace_id": trace_id,
|
|
|
|
|
+ "goal_id": current_goal_id,
|
|
|
|
|
+ "runner": self,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- response_content = result.get("content", "")
|
|
|
|
|
- tool_calls = result.get("tool_calls")
|
|
|
|
|
- finish_reason = result.get("finish_reason")
|
|
|
|
|
- prompt_tokens = result.get("prompt_tokens", 0)
|
|
|
|
|
- completion_tokens = result.get("completion_tokens", 0)
|
|
|
|
|
- step_tokens = prompt_tokens + completion_tokens
|
|
|
|
|
- step_cost = result.get("cost", 0)
|
|
|
|
|
-
|
|
|
|
|
- # 按需自动创建 root goal:LLM 有 tool 调用但未主动创建目标时兜底
|
|
|
|
|
- if goal_tree and not goal_tree.goals and tool_calls:
|
|
|
|
|
- has_goal_call = any(
|
|
|
|
|
- tc.get("function", {}).get("name") == "goal"
|
|
|
|
|
- for tc in tool_calls
|
|
|
|
|
|
|
+ tool_msg = Message.create(
|
|
|
|
|
+ trace_id=trace_id,
|
|
|
|
|
+ role="tool",
|
|
|
|
|
+ sequence=sequence,
|
|
|
|
|
+ goal_id=current_goal_id,
|
|
|
|
|
+ tool_call_id=tc["id"],
|
|
|
|
|
+ content={"tool_name": tool_name, "result": tool_result},
|
|
|
)
|
|
)
|
|
|
- if not has_goal_call:
|
|
|
|
|
- root_desc = goal_tree.mission[:200] if len(goal_tree.mission) > 200 else goal_tree.mission
|
|
|
|
|
- goal_tree.add_goals(
|
|
|
|
|
- descriptions=[root_desc],
|
|
|
|
|
- reasons=["系统自动创建:Agent 未显式创建目标"],
|
|
|
|
|
- parent_id=None
|
|
|
|
|
- )
|
|
|
|
|
- goal_tree.focus(goal_tree.goals[0].id)
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- await self.trace_store.update_goal_tree(trace_id, goal_tree)
|
|
|
|
|
- await self.trace_store.add_goal(trace_id, goal_tree.goals[0])
|
|
|
|
|
- logger.info(f"自动创建 root goal: {goal_tree.goals[0].id}")
|
|
|
|
|
-
|
|
|
|
|
- # 获取当前 goal_id
|
|
|
|
|
- current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
|
|
|
|
|
-
|
|
|
|
|
- # 记录 assistant Message
|
|
|
|
|
- assistant_msg = Message.create(
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- role="assistant",
|
|
|
|
|
- sequence=sequence,
|
|
|
|
|
- goal_id=current_goal_id,
|
|
|
|
|
- content={"text": response_content, "tool_calls": tool_calls},
|
|
|
|
|
- prompt_tokens=prompt_tokens,
|
|
|
|
|
- completion_tokens=completion_tokens,
|
|
|
|
|
- finish_reason=finish_reason,
|
|
|
|
|
- cost=step_cost,
|
|
|
|
|
- )
|
|
|
|
|
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- await self.trace_store.add_message(assistant_msg)
|
|
|
|
|
- # WebSocket 广播由 add_message 内部的 append_event 触发
|
|
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ await self.trace_store.add_message(tool_msg)
|
|
|
|
|
|
|
|
- yield assistant_msg
|
|
|
|
|
- sequence += 1
|
|
|
|
|
|
|
+ yield tool_msg
|
|
|
|
|
+ sequence += 1
|
|
|
|
|
|
|
|
- # 处理工具调用
|
|
|
|
|
- if tool_calls and auto_execute_tools:
|
|
|
|
|
- # 添加 assistant 消息到对话历史
|
|
|
|
|
- messages.append({
|
|
|
|
|
- "role": "assistant",
|
|
|
|
|
- "content": response_content,
|
|
|
|
|
- "tool_calls": tool_calls,
|
|
|
|
|
|
|
+ history.append({
|
|
|
|
|
+ "role": "tool",
|
|
|
|
|
+ "tool_call_id": tc["id"],
|
|
|
|
|
+ "name": tool_name,
|
|
|
|
|
+ "content": str(tool_result),
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- for tc in tool_calls:
|
|
|
|
|
- # 每次工具执行前重新获取最新的 goal_id(处理并行 tool_calls 的情况)
|
|
|
|
|
- current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
|
|
|
|
|
-
|
|
|
|
|
- tool_name = tc["function"]["name"]
|
|
|
|
|
- tool_args = tc["function"]["arguments"]
|
|
|
|
|
-
|
|
|
|
|
- # 解析参数
|
|
|
|
|
- if isinstance(tool_args, str):
|
|
|
|
|
- if tool_args.strip(): # 非空字符串
|
|
|
|
|
- import json
|
|
|
|
|
- tool_args = json.loads(tool_args)
|
|
|
|
|
- else:
|
|
|
|
|
- tool_args = {} # 空字符串转换为空字典
|
|
|
|
|
- elif tool_args is None:
|
|
|
|
|
- tool_args = {} # None 转换为空字典
|
|
|
|
|
-
|
|
|
|
|
- # 执行工具(统一处理,传递 context)
|
|
|
|
|
- tool_result = await self.tools.execute(
|
|
|
|
|
- tool_name,
|
|
|
|
|
- tool_args,
|
|
|
|
|
- uid=uid or "",
|
|
|
|
|
- context={
|
|
|
|
|
- "store": self.trace_store,
|
|
|
|
|
- "trace_id": trace_id,
|
|
|
|
|
- "goal_id": current_goal_id,
|
|
|
|
|
- "runner": self,
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- # 记录 tool Message
|
|
|
|
|
- tool_msg = Message.create(
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- role="tool",
|
|
|
|
|
- sequence=sequence,
|
|
|
|
|
- goal_id=current_goal_id,
|
|
|
|
|
- tool_call_id=tc["id"],
|
|
|
|
|
- content={"tool_name": tool_name, "result": tool_result},
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- await self.trace_store.add_message(tool_msg)
|
|
|
|
|
-
|
|
|
|
|
- yield tool_msg
|
|
|
|
|
- sequence += 1
|
|
|
|
|
-
|
|
|
|
|
- # 添加到消息历史
|
|
|
|
|
- messages.append({
|
|
|
|
|
- "role": "tool",
|
|
|
|
|
- "tool_call_id": tc["id"],
|
|
|
|
|
- "name": tool_name,
|
|
|
|
|
- "content": str(tool_result),
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- continue # 继续循环
|
|
|
|
|
-
|
|
|
|
|
- # 无工具调用,任务完成
|
|
|
|
|
|
|
+ continue # 继续循环
|
|
|
|
|
+
|
|
|
|
|
+ # 无工具调用,任务完成
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ # 完成 Trace
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ await self.trace_store.update_trace(
|
|
|
|
|
+ trace_id,
|
|
|
|
|
+ status="completed",
|
|
|
|
|
+ completed_at=datetime.now(),
|
|
|
|
|
+ )
|
|
|
|
|
+ trace_obj = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
+ if trace_obj:
|
|
|
|
|
+ yield trace_obj
|
|
|
|
|
+
|
|
|
|
|
+ # ===== 回溯(Rewind)=====
|
|
|
|
|
+
|
|
|
|
|
+ async def _rewind(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace_id: str,
|
|
|
|
|
+ insert_after: int,
|
|
|
|
|
+ goal_tree: Optional[GoalTree],
|
|
|
|
|
+ ) -> int:
|
|
|
|
|
+ """
|
|
|
|
|
+ 执行回溯:标记 insert_after 之后的 messages 和 goals 为 abandoned
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 下一个可用的 sequence 号
|
|
|
|
|
+ """
|
|
|
|
|
+ if not self.trace_store:
|
|
|
|
|
+ raise ValueError("trace_store required for rewind")
|
|
|
|
|
+
|
|
|
|
|
+ # 1. 加载所有 messages(含已 abandoned 的)
|
|
|
|
|
+ all_messages = await self.trace_store.get_trace_messages(
|
|
|
|
|
+ trace_id, include_abandoned=True
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if not all_messages:
|
|
|
|
|
+ return 1
|
|
|
|
|
+
|
|
|
|
|
+ # 2. 找到安全截断点(确保不截断在 tool_call 和 tool response 之间)
|
|
|
|
|
+ cutoff = self._find_safe_cutoff(all_messages, insert_after)
|
|
|
|
|
+
|
|
|
|
|
+ # 3. 批量标记 messages 为 abandoned
|
|
|
|
|
+ abandoned_ids = await self.trace_store.abandon_messages_after(trace_id, cutoff)
|
|
|
|
|
+
|
|
|
|
|
+ # 4. 处理 Goals
|
|
|
|
|
+ if goal_tree:
|
|
|
|
|
+ active_messages = [m for m in all_messages if m.sequence <= cutoff]
|
|
|
|
|
+ active_goal_ids = {m.goal_id for m in active_messages if m.goal_id}
|
|
|
|
|
+
|
|
|
|
|
+ for goal in goal_tree.goals:
|
|
|
|
|
+ if goal.status == "abandoned":
|
|
|
|
|
+ continue # 已 abandoned,跳过
|
|
|
|
|
+ if goal.status == "completed" and goal.id in active_goal_ids:
|
|
|
|
|
+ continue # 已完成且有截断点之前的 messages → 保留
|
|
|
|
|
+ # 其余全部 abandon(含无 active messages 的 completed goal)
|
|
|
|
|
+ goal.status = "abandoned"
|
|
|
|
|
+ goal.summary = "回溯导致放弃"
|
|
|
|
|
+
|
|
|
|
|
+ # 重置 current_id
|
|
|
|
|
+ goal_tree._current_id = None
|
|
|
|
|
+
|
|
|
|
|
+ await self.trace_store.update_goal_tree(trace_id, goal_tree)
|
|
|
|
|
+
|
|
|
|
|
+ # 5. 记录 rewind 事件
|
|
|
|
|
+ abandoned_sequences = [
|
|
|
|
|
+ m.sequence for m in all_messages
|
|
|
|
|
+ if m.sequence > cutoff and m.status != "abandoned" # 本次新 abandon 的
|
|
|
|
|
+ ]
|
|
|
|
|
+ await self.trace_store.append_event(trace_id, "rewind", {
|
|
|
|
|
+ "insert_after_sequence": cutoff,
|
|
|
|
|
+ "abandoned_message_count": len(abandoned_ids),
|
|
|
|
|
+ "abandoned_sequences": abandoned_sequences[:20], # 只记前 20 条
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ # 6. 返回 next sequence
|
|
|
|
|
+ max_seq = max((m.sequence for m in all_messages), default=0)
|
|
|
|
|
+ return max_seq + 1
|
|
|
|
|
+
|
|
|
|
|
+ def _find_safe_cutoff(self, messages: List[Message], insert_after: int) -> int:
|
|
|
|
|
+ """
|
|
|
|
|
+ 找到安全的截断点。
|
|
|
|
|
+
|
|
|
|
|
+ 如果 insert_after 指向一条带 tool_calls 的 assistant message,
|
|
|
|
|
+ 则自动扩展到其所有对应的 tool response 之后。
|
|
|
|
|
+ """
|
|
|
|
|
+ cutoff = insert_after
|
|
|
|
|
+
|
|
|
|
|
+ # 找到 insert_after 对应的 message
|
|
|
|
|
+ target_msg = None
|
|
|
|
|
+ for msg in messages:
|
|
|
|
|
+ if msg.sequence == insert_after:
|
|
|
|
|
+ target_msg = msg
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
- # 完成 Trace
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- trace_obj = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
- if trace_obj:
|
|
|
|
|
- await self.trace_store.update_trace(
|
|
|
|
|
- trace_id,
|
|
|
|
|
- status="completed",
|
|
|
|
|
- completed_at=datetime.now(),
|
|
|
|
|
- )
|
|
|
|
|
- # 重新获取更新后的 Trace 并返回
|
|
|
|
|
- trace_obj = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
- if trace_obj:
|
|
|
|
|
- yield trace_obj
|
|
|
|
|
|
|
+ if not target_msg:
|
|
|
|
|
+ return cutoff
|
|
|
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.error(f"Agent run failed: {e}")
|
|
|
|
|
|
|
+ # 如果是 assistant 且有 tool_calls,找到所有对应的 tool responses
|
|
|
|
|
+ if target_msg.role == "assistant":
|
|
|
|
|
+ content = target_msg.content
|
|
|
|
|
+ if isinstance(content, dict) and content.get("tool_calls"):
|
|
|
|
|
+ tool_call_ids = set()
|
|
|
|
|
+ for tc in content["tool_calls"]:
|
|
|
|
|
+ if isinstance(tc, dict) and tc.get("id"):
|
|
|
|
|
+ tool_call_ids.add(tc["id"])
|
|
|
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- await self.trace_store.update_trace(
|
|
|
|
|
- trace_id,
|
|
|
|
|
- status="failed",
|
|
|
|
|
- error_message=str(e),
|
|
|
|
|
- completed_at=datetime.now()
|
|
|
|
|
- )
|
|
|
|
|
- trace_obj = await self.trace_store.get_trace(trace_id)
|
|
|
|
|
- if trace_obj:
|
|
|
|
|
- yield trace_obj
|
|
|
|
|
- raise
|
|
|
|
|
|
|
+ # 找到这些 tool_call 对应的 tool messages
|
|
|
|
|
+ for msg in messages:
|
|
|
|
|
+ if (msg.role == "tool" and msg.tool_call_id
|
|
|
|
|
+ and msg.tool_call_id in tool_call_ids):
|
|
|
|
|
+ cutoff = max(cutoff, msg.sequence)
|
|
|
|
|
+
|
|
|
|
|
+ return cutoff
|
|
|
|
|
+
|
|
|
|
|
+ # ===== 上下文注入 =====
|
|
|
|
|
+
|
|
|
|
|
+ def _build_context_injection(
|
|
|
|
|
+ self,
|
|
|
|
|
+ trace: Trace,
|
|
|
|
|
+ goal_tree: Optional[GoalTree],
|
|
|
|
|
+ ) -> str:
|
|
|
|
|
+ """构建周期性注入的上下文(GoalTree + Active Collaborators)"""
|
|
|
|
|
+ parts = []
|
|
|
|
|
+
|
|
|
|
|
+ # GoalTree
|
|
|
|
|
+ if goal_tree and goal_tree.goals:
|
|
|
|
|
+ parts.append(f"## Current Plan\n\n{goal_tree.to_prompt()}")
|
|
|
|
|
+
|
|
|
|
|
+ # Active Collaborators
|
|
|
|
|
+ collaborators = trace.context.get("collaborators", [])
|
|
|
|
|
+ if collaborators:
|
|
|
|
|
+ lines = ["## Active Collaborators"]
|
|
|
|
|
+ for c in collaborators:
|
|
|
|
|
+ status_str = c.get("status", "unknown")
|
|
|
|
|
+ ctype = c.get("type", "agent")
|
|
|
|
|
+ summary = c.get("summary", "")
|
|
|
|
|
+ name = c.get("name", "unnamed")
|
|
|
|
|
+ lines.append(f"- {name} [{ctype}, {status_str}]: {summary}")
|
|
|
|
|
+ parts.append("\n".join(lines))
|
|
|
|
|
+
|
|
|
|
|
+ return "\n\n".join(parts)
|
|
|
|
|
|
|
|
# ===== 辅助方法 =====
|
|
# ===== 辅助方法 =====
|
|
|
|
|
|
|
|
|
|
+ def _get_tool_schemas(self, tools: Optional[List[str]]) -> List[Dict]:
|
|
|
|
|
+ """获取工具 Schema"""
|
|
|
|
|
+ tool_names = BUILTIN_TOOLS.copy()
|
|
|
|
|
+ if tools:
|
|
|
|
|
+ for tool in tools:
|
|
|
|
|
+ if tool not in tool_names:
|
|
|
|
|
+ tool_names.append(tool)
|
|
|
|
|
+ return self.tools.get_schemas(tool_names)
|
|
|
|
|
+
|
|
|
|
|
+ async def _build_system_prompt(self, config: RunConfig) -> Optional[str]:
|
|
|
|
|
+ """构建 system prompt(注入 skills 和 experiences)"""
|
|
|
|
|
+ system_prompt = config.system_prompt
|
|
|
|
|
+
|
|
|
|
|
+ # 加载 Skills
|
|
|
|
|
+ skills_text = ""
|
|
|
|
|
+ skills = load_skills_from_dir(self.skills_dir)
|
|
|
|
|
+ if skills:
|
|
|
|
|
+ skills_text = self._format_skills(skills)
|
|
|
|
|
+
|
|
|
|
|
+ # 加载 Experiences
|
|
|
|
|
+ experiences_text = ""
|
|
|
|
|
+ if config.enable_memory and self.memory_store:
|
|
|
|
|
+ scope = f"agent:{config.agent_type}"
|
|
|
|
|
+ # 从 messages 提取文本作为查询
|
|
|
|
|
+ experiences = await self.memory_store.search_experiences(scope, system_prompt or "")
|
|
|
|
|
+ experiences_text = self._format_experiences(experiences)
|
|
|
|
|
+
|
|
|
|
|
+ # 拼装
|
|
|
|
|
+ if system_prompt:
|
|
|
|
|
+ if skills_text:
|
|
|
|
|
+ system_prompt += f"\n\n## Skills\n{skills_text}"
|
|
|
|
|
+ if experiences_text:
|
|
|
|
|
+ system_prompt += f"\n\n## 相关经验\n{experiences_text}"
|
|
|
|
|
+ elif skills_text or experiences_text:
|
|
|
|
|
+ parts = []
|
|
|
|
|
+ if skills_text:
|
|
|
|
|
+ parts.append(f"## Skills\n{skills_text}")
|
|
|
|
|
+ if experiences_text:
|
|
|
|
|
+ parts.append(f"## 相关经验\n{experiences_text}")
|
|
|
|
|
+ system_prompt = "\n\n".join(parts)
|
|
|
|
|
+
|
|
|
|
|
+ return system_prompt
|
|
|
|
|
+
|
|
|
|
|
+ async def _generate_task_name(self, messages: List[Dict]) -> str:
|
|
|
|
|
+ """生成任务名称:优先使用 utility_llm,fallback 到文本截取"""
|
|
|
|
|
+ # 提取 messages 中的文本内容
|
|
|
|
|
+ text_parts = []
|
|
|
|
|
+ for msg in messages:
|
|
|
|
|
+ content = msg.get("content", "")
|
|
|
|
|
+ if isinstance(content, str):
|
|
|
|
|
+ text_parts.append(content)
|
|
|
|
|
+ elif isinstance(content, list):
|
|
|
|
|
+ for part in content:
|
|
|
|
|
+ if isinstance(part, dict) and part.get("type") == "text":
|
|
|
|
|
+ text_parts.append(part.get("text", ""))
|
|
|
|
|
+ raw_text = " ".join(text_parts).strip()
|
|
|
|
|
+
|
|
|
|
|
+ if not raw_text:
|
|
|
|
|
+ return "未命名任务"
|
|
|
|
|
+
|
|
|
|
|
+ # 尝试使用 utility_llm 生成标题
|
|
|
|
|
+ if self.utility_llm_call:
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = await self.utility_llm_call(
|
|
|
|
|
+ messages=[
|
|
|
|
|
+ {"role": "system", "content": "用中文为以下任务生成一个简短标题(10-30字),只输出标题本身:"},
|
|
|
|
|
+ {"role": "user", "content": raw_text[:2000]},
|
|
|
|
|
+ ],
|
|
|
|
|
+ model="gpt-4o-mini", # 使用便宜模型
|
|
|
|
|
+ )
|
|
|
|
|
+ title = result.get("content", "").strip()
|
|
|
|
|
+ if title and len(title) < 100:
|
|
|
|
|
+ return title
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ # Fallback: 截取前 50 字符
|
|
|
|
|
+ return raw_text[:50] + ("..." if len(raw_text) > 50 else "")
|
|
|
|
|
+
|
|
|
def _format_skills(self, skills: List[Skill]) -> str:
|
|
def _format_skills(self, skills: List[Skill]) -> str:
|
|
|
- """格式化技能为 Prompt 文本"""
|
|
|
|
|
if not skills:
|
|
if not skills:
|
|
|
return ""
|
|
return ""
|
|
|
return "\n\n".join(s.to_prompt_text() for s in skills)
|
|
return "\n\n".join(s.to_prompt_text() for s in skills)
|
|
|
|
|
|
|
|
def _format_experiences(self, experiences: List[Experience]) -> str:
|
|
def _format_experiences(self, experiences: List[Experience]) -> str:
|
|
|
- """格式化经验为 Prompt 文本"""
|
|
|
|
|
if not experiences:
|
|
if not experiences:
|
|
|
return ""
|
|
return ""
|
|
|
return "\n".join(f"- {e.to_prompt_text()}" for e in experiences)
|
|
return "\n".join(f"- {e.to_prompt_text()}" for e in experiences)
|