core.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. from __future__ import annotations
  2. from collections.abc import AsyncIterator, Callable, Iterator
  3. from supply_agent.agent.loop import AgentLoop
  4. from supply_agent.config import Settings, get_settings
  5. from supply_agent.llm.client import LLMClient
  6. from supply_agent.logging.logger import AgentLogger
  7. from supply_agent.logging.publish import publish_run_artifacts
  8. from supply_agent.skills.registry import SkillRegistry
  9. from supply_agent.tools.registry import ToolRegistry
  10. from supply_agent.types import AgentEvent, AgentEventType, AgentResult, Message, Role
  11. DEFAULT_SYSTEM_PROMPT = """\
  12. 你是一个通用 AI 助手,根据用户的要求理解任务并完成目标。
  13. ## 工作方式
  14. 1. 先理解用户真正想要什么,必要时先澄清关键信息
  15. 2. 将复杂任务拆解为可执行的步骤,逐步推进
  16. 3. 需要外部信息或操作时,主动调用可用工具
  17. 4. 完成后给出清晰、可直接使用的结果
  18. ## 工具使用
  19. - 你有权访问一组工具,仅在确实需要时调用
  20. - 调用前想清楚:需要什么输入、期望得到什么输出
  21. - 工具返回后,结合结果继续推理,不要重复无效调用
  22. ## 技能(Skills)
  23. - 若系统提示中列出了可用 Skills,且任务与某项技能匹配,先调用 `load_skill` 加载对应技能
  24. - 技能包含专业流程和输出规范,加载后严格遵循,不要自行猜测格式
  25. ## 输出要求
  26. - 回答紧扣用户要求,避免无关内容
  27. - 结构清晰,重点突出,便于阅读和直接使用
  28. - 无法完成时,说明原因并给出可行的替代方案
  29. """
  30. class Agent:
  31. """
  32. Main Agent class — orchestrates LLM, tools, and skills.
  33. Usage:
  34. agent = Agent(model="anthropic/claude-sonnet-5")
  35. agent.tools.register(my_tool)
  36. result = agent.run("What is 2+2?")
  37. """
  38. def __init__(
  39. self,
  40. settings: Settings | None = None,
  41. *,
  42. name: str | None = None,
  43. model: str | None = None,
  44. system_prompt: str | None = None,
  45. tools: ToolRegistry | None = None,
  46. skills: SkillRegistry | None = None,
  47. max_iterations: int | None = None,
  48. temperature: float | None = None,
  49. reasoning_effort: str | None = None,
  50. logger: AgentLogger | None = None,
  51. completion_guard: Callable[[list[Message]], str | None] | None = None,
  52. completion_guard_blocks: bool = False,
  53. tool_call_budgets: dict[str, tuple[set[str], int]] | None = None,
  54. tool_repeat_requires_change: dict[str, set[str]] | None = None,
  55. ) -> None:
  56. self.name = name
  57. self.settings = settings or get_settings()
  58. self.logger = logger or AgentLogger(
  59. self.settings.logs_dir,
  60. enabled=self.settings.log_enabled,
  61. )
  62. self.llm = LLMClient(
  63. self.settings,
  64. logger=self.logger,
  65. reasoning_effort=reasoning_effort,
  66. )
  67. self.tools = tools or ToolRegistry()
  68. self.skills = skills or SkillRegistry(self.settings.skills_dir)
  69. self.system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT
  70. self.max_iterations = max_iterations or self.settings.agent_max_iterations
  71. self.temperature = temperature
  72. self.completion_guard = completion_guard
  73. self.completion_guard_blocks = completion_guard_blocks
  74. self.tool_call_budgets = tool_call_budgets or {}
  75. self.tool_repeat_requires_change = tool_repeat_requires_change or {}
  76. if model:
  77. self.llm.set_model(model)
  78. self._active_skills: list[str] = []
  79. self._setup_builtin_tools()
  80. @property
  81. def model(self) -> str:
  82. return self.llm.model
  83. @model.setter
  84. def model(self, value: str) -> None:
  85. self.llm.set_model(value)
  86. def _setup_builtin_tools(self) -> None:
  87. """Register built-in tools for skill loading."""
  88. def load_skill(name: str) -> str:
  89. """Load a skill by name to get specialized instructions for a task."""
  90. context = self.skills.get_skill_context(name)
  91. if context is None:
  92. available = ", ".join(self.skills.list_skills()) or "none"
  93. return f'{{"error": "Skill \'{name}\' not found. Available: {available}"}}'
  94. if name not in self._active_skills:
  95. self._active_skills.append(name)
  96. return context
  97. load_skill.__doc__ = "Load a skill by name to get specialized instructions for a task."
  98. from supply_agent.tools.base import tool as tool_decorator
  99. decorated = tool_decorator(name="load_skill")(load_skill)
  100. self.tools.register(decorated, name="load_skill")
  101. def _build_system_message(self) -> Message:
  102. parts = [self.system_prompt]
  103. catalog = self.skills.get_catalog()
  104. if catalog:
  105. parts.append(catalog)
  106. for skill_name in self._active_skills:
  107. ctx = self.skills.get_skill_context(skill_name)
  108. if ctx:
  109. parts.append(ctx)
  110. return Message(role=Role.SYSTEM, content="\n\n".join(parts))
  111. def _create_loop(self, messages: list[Message]) -> AgentLoop:
  112. return AgentLoop(
  113. llm=self.llm,
  114. tools=self.tools,
  115. system_message=self._build_system_message(),
  116. system_message_builder=self._build_system_message,
  117. messages=messages,
  118. max_iterations=self.max_iterations,
  119. temperature=self.temperature,
  120. logger=self.logger,
  121. active_skills=self._active_skills,
  122. completion_guard=self.completion_guard,
  123. completion_guard_blocks=self.completion_guard_blocks,
  124. tool_call_budgets=self.tool_call_budgets,
  125. tool_repeat_requires_change=self.tool_repeat_requires_change,
  126. )
  127. def _finish_run(self, result: AgentResult) -> None:
  128. """Close run logs and publish visualization artifacts."""
  129. self.logger.end_run(result)
  130. publish_run_artifacts(self.logger)
  131. def run(self, user_input: str, *, history: list[Message] | None = None) -> AgentResult:
  132. """Run the agent synchronously with a user message."""
  133. self.logger.start_run(user_input, model=self.model, agent_name=self.name)
  134. messages = list(history or [])
  135. messages.append(Message(role=Role.USER, content=user_input))
  136. loop = self._create_loop(messages)
  137. result = loop.run()
  138. self._finish_run(result)
  139. return result
  140. async def arun_core(
  141. self, user_input: str, *, history: list[Message] | None = None
  142. ) -> AgentResult:
  143. """Run the agent loop without closing logs or publishing artifacts."""
  144. self.logger.start_run(user_input, model=self.model, agent_name=self.name)
  145. messages = list(history or [])
  146. messages.append(Message(role=Role.USER, content=user_input))
  147. loop = self._create_loop(messages)
  148. return await loop.arun()
  149. async def arun(
  150. self, user_input: str, *, history: list[Message] | None = None
  151. ) -> AgentResult:
  152. """Run the agent asynchronously."""
  153. result = await self.arun_core(user_input, history=history)
  154. self._finish_run(result)
  155. return result
  156. def stream(
  157. self, user_input: str, *, history: list[Message] | None = None
  158. ) -> Iterator[AgentEvent]:
  159. """Stream agent events during execution."""
  160. self.logger.start_run(user_input, model=self.model, agent_name=self.name)
  161. messages = list(history or [])
  162. messages.append(Message(role=Role.USER, content=user_input))
  163. loop = self._create_loop(messages)
  164. final_result: AgentResult | None = None
  165. for event in loop.stream():
  166. if event.type == AgentEventType.DONE:
  167. data = event.data
  168. final_result = AgentResult(
  169. content=data.get("content", ""),
  170. messages=loop.messages,
  171. iterations=data.get("iterations", 0),
  172. tool_calls_made=data.get("tool_calls_made", 0),
  173. skills_used=list(self._active_skills),
  174. )
  175. yield event
  176. if final_result:
  177. self._finish_run(final_result)
  178. async def astream(
  179. self, user_input: str, *, history: list[Message] | None = None
  180. ) -> AsyncIterator[AgentEvent]:
  181. """Async stream agent events."""
  182. self.logger.start_run(user_input, model=self.model, agent_name=self.name)
  183. messages = list(history or [])
  184. messages.append(Message(role=Role.USER, content=user_input))
  185. loop = self._create_loop(messages)
  186. final_result: AgentResult | None = None
  187. async for event in loop.astream():
  188. if event.type == AgentEventType.DONE:
  189. data = event.data
  190. final_result = AgentResult(
  191. content=data.get("content", ""),
  192. messages=loop.messages,
  193. iterations=data.get("iterations", 0),
  194. tool_calls_made=data.get("tool_calls_made", 0),
  195. skills_used=list(self._active_skills),
  196. )
  197. yield event
  198. if final_result:
  199. self._finish_run(final_result)
  200. def reset(self) -> None:
  201. """Reset active skills state."""
  202. self._active_skills.clear()