core.py 8.6 KB

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