core.py 8.4 KB

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