core.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  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. tool_call_budgets: dict[str, tuple[set[str], int]] | None = None,
  53. tool_repeat_requires_change: dict[str, set[str]] | None = None,
  54. ) -> None:
  55. self.name = name
  56. self.settings = settings or get_settings()
  57. self.logger = logger or AgentLogger(
  58. self.settings.logs_dir,
  59. enabled=self.settings.log_enabled,
  60. )
  61. self.llm = LLMClient(
  62. self.settings,
  63. logger=self.logger,
  64. reasoning_effort=reasoning_effort,
  65. )
  66. self.tools = tools or ToolRegistry()
  67. self.skills = skills or SkillRegistry(self.settings.skills_dir)
  68. self.system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT
  69. self.max_iterations = max_iterations or self.settings.agent_max_iterations
  70. self.temperature = temperature
  71. self.completion_guard = completion_guard
  72. self.tool_call_budgets = tool_call_budgets or {}
  73. self.tool_repeat_requires_change = tool_repeat_requires_change or {}
  74. if model:
  75. self.llm.set_model(model)
  76. self._active_skills: list[str] = []
  77. self._setup_builtin_tools()
  78. @property
  79. def model(self) -> str:
  80. return self.llm.model
  81. @model.setter
  82. def model(self, value: str) -> None:
  83. self.llm.set_model(value)
  84. def _setup_builtin_tools(self) -> None:
  85. """Register built-in tools for skill loading."""
  86. def load_skill(name: str) -> str:
  87. """Load a skill by name to get specialized instructions for a task."""
  88. context = self.skills.get_skill_context(name)
  89. if context is None:
  90. available = ", ".join(self.skills.list_skills()) or "none"
  91. return f'{{"error": "Skill \'{name}\' not found. Available: {available}"}}'
  92. if name not in self._active_skills:
  93. self._active_skills.append(name)
  94. return context
  95. load_skill.__doc__ = "Load a skill by name to get specialized instructions for a task."
  96. from supply_agent.tools.base import tool as tool_decorator
  97. decorated = tool_decorator(name="load_skill")(load_skill)
  98. self.tools.register(decorated, name="load_skill")
  99. def _build_system_message(self) -> Message:
  100. parts = [self.system_prompt]
  101. catalog = self.skills.get_catalog()
  102. if catalog:
  103. parts.append(catalog)
  104. for skill_name in self._active_skills:
  105. ctx = self.skills.get_skill_context(skill_name)
  106. if ctx:
  107. parts.append(ctx)
  108. return Message(role=Role.SYSTEM, content="\n\n".join(parts))
  109. def _create_loop(self, messages: list[Message]) -> AgentLoop:
  110. return AgentLoop(
  111. llm=self.llm,
  112. tools=self.tools,
  113. system_message=self._build_system_message(),
  114. system_message_builder=self._build_system_message,
  115. messages=messages,
  116. max_iterations=self.max_iterations,
  117. temperature=self.temperature,
  118. logger=self.logger,
  119. active_skills=self._active_skills,
  120. completion_guard=self.completion_guard,
  121. tool_call_budgets=self.tool_call_budgets,
  122. tool_repeat_requires_change=self.tool_repeat_requires_change,
  123. )
  124. def _finish_run(self, result: AgentResult) -> None:
  125. """Close run logs and publish visualization artifacts."""
  126. self.logger.end_run(result)
  127. publish_run_artifacts(self.logger)
  128. def run(self, user_input: str, *, history: list[Message] | None = None) -> AgentResult:
  129. """Run the agent synchronously with a user message."""
  130. self.logger.start_run(user_input, model=self.model, agent_name=self.name)
  131. messages = list(history or [])
  132. messages.append(Message(role=Role.USER, content=user_input))
  133. loop = self._create_loop(messages)
  134. result = loop.run()
  135. self._finish_run(result)
  136. return result
  137. async def arun(
  138. self, user_input: str, *, history: list[Message] | None = None
  139. ) -> AgentResult:
  140. """Run the agent asynchronously."""
  141. self.logger.start_run(user_input, model=self.model, agent_name=self.name)
  142. messages = list(history or [])
  143. messages.append(Message(role=Role.USER, content=user_input))
  144. loop = self._create_loop(messages)
  145. result = await loop.arun()
  146. self._finish_run(result)
  147. return result
  148. def stream(
  149. self, user_input: str, *, history: list[Message] | None = None
  150. ) -> Iterator[AgentEvent]:
  151. """Stream agent events during execution."""
  152. self.logger.start_run(user_input, model=self.model, agent_name=self.name)
  153. messages = list(history or [])
  154. messages.append(Message(role=Role.USER, content=user_input))
  155. loop = self._create_loop(messages)
  156. final_result: AgentResult | None = None
  157. for event in loop.stream():
  158. if event.type == AgentEventType.DONE:
  159. data = event.data
  160. final_result = AgentResult(
  161. content=data.get("content", ""),
  162. messages=loop.messages,
  163. iterations=data.get("iterations", 0),
  164. tool_calls_made=data.get("tool_calls_made", 0),
  165. skills_used=list(self._active_skills),
  166. )
  167. yield event
  168. if final_result:
  169. self._finish_run(final_result)
  170. async def astream(
  171. self, user_input: str, *, history: list[Message] | None = None
  172. ) -> AsyncIterator[AgentEvent]:
  173. """Async stream agent events."""
  174. self.logger.start_run(user_input, model=self.model, agent_name=self.name)
  175. messages = list(history or [])
  176. messages.append(Message(role=Role.USER, content=user_input))
  177. loop = self._create_loop(messages)
  178. final_result: AgentResult | None = None
  179. async for event in loop.astream():
  180. if event.type == AgentEventType.DONE:
  181. data = event.data
  182. final_result = AgentResult(
  183. content=data.get("content", ""),
  184. messages=loop.messages,
  185. iterations=data.get("iterations", 0),
  186. tool_calls_made=data.get("tool_calls_made", 0),
  187. skills_used=list(self._active_skills),
  188. )
  189. yield event
  190. if final_result:
  191. self._finish_run(final_result)
  192. def reset(self) -> None:
  193. """Reset active skills state."""
  194. self._active_skills.clear()