core.py 7.7 KB

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