enhanced_llm.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. """增强版 HelloAgentsLLM - 支持流式工具调用"""
  2. from dataclasses import dataclass, field
  3. from enum import Enum
  4. from typing import Optional, List, Dict, Union, Any, AsyncIterator
  5. from hello_agents.core.llm import HelloAgentsLLM
  6. from hello_agents.core.exceptions import HelloAgentsException
  7. # ==================== 流式工具调用数据结构 ====================
  8. class StreamToolEventType(Enum):
  9. """流式工具调用事件类型"""
  10. CONTENT = "content" # 文本内容增量
  11. TOOL_CALL_START = "tool_call_start" # 工具调用开始(收到ID和名称)
  12. TOOL_CALL_DELTA = "tool_call_delta" # 工具调用参数增量
  13. FINISH = "finish" # 流结束
  14. @dataclass
  15. class StreamToolEvent:
  16. """流式工具调用事件
  17. 封装流式响应中的不同类型数据,统一处理文本内容和工具调用。
  18. """
  19. event_type: StreamToolEventType
  20. # 文本内容
  21. content: Optional[str] = None
  22. # 工具调用
  23. tool_call_index: Optional[int] = None # 工具调用索引(用于增量累积)
  24. tool_call_id: Optional[str] = None # 工具调用ID
  25. tool_name: Optional[str] = None # 工具名称
  26. tool_arguments_delta: Optional[str] = None # 参数增量
  27. # 结束信息
  28. finish_reason: Optional[str] = None
  29. @property
  30. def is_content(self) -> bool:
  31. """是否为文本内容事件"""
  32. return self.event_type == StreamToolEventType.CONTENT
  33. @property
  34. def is_tool_call(self) -> bool:
  35. """是否为工具调用事件"""
  36. return self.event_type in (
  37. StreamToolEventType.TOOL_CALL_START,
  38. StreamToolEventType.TOOL_CALL_DELTA
  39. )
  40. @property
  41. def is_finish(self) -> bool:
  42. """是否为结束事件"""
  43. return self.event_type == StreamToolEventType.FINISH
  44. @dataclass
  45. class StreamToolCallResult:
  46. """流式工具调用完成后的结果
  47. 包含累积的文本内容和工具调用列表。
  48. """
  49. content: str = ""
  50. tool_calls: List[Dict[str, Any]] = field(default_factory=list)
  51. finish_reason: Optional[str] = None
  52. def add_content(self, delta: str):
  53. """添加文本内容"""
  54. self.content += delta
  55. def add_tool_call_start(self, index: int, tool_id: str, tool_name: str):
  56. """添加工具调用开始"""
  57. # 确保列表足够长
  58. while len(self.tool_calls) <= index:
  59. self.tool_calls.append({"id": "", "name": "", "arguments": ""})
  60. self.tool_calls[index]["id"] = tool_id
  61. self.tool_calls[index]["name"] = tool_name
  62. def add_tool_call_delta(self, index: int, arguments_delta: str):
  63. """添加工具调用参数增量"""
  64. while len(self.tool_calls) <= index:
  65. self.tool_calls.append({"id": "", "name": "", "arguments": ""})
  66. self.tool_calls[index]["arguments"] += arguments_delta
  67. def get_complete_tool_calls(self) -> List[Dict[str, Any]]:
  68. """获取完整的工具调用列表(过滤不完整的)"""
  69. return [
  70. tc for tc in self.tool_calls
  71. if tc["id"] and tc["name"]
  72. ]
  73. def to_assistant_message(self) -> Dict[str, Any]:
  74. """转换为助手消息格式(用于追加到消息历史)"""
  75. message: Dict[str, Any] = {"role": "assistant", "content": self.content or None}
  76. if self.tool_calls:
  77. message["tool_calls"] = [
  78. {
  79. "id": tc["id"],
  80. "type": "function",
  81. "function": {
  82. "name": tc["name"],
  83. "arguments": tc["arguments"]
  84. }
  85. }
  86. for tc in self.get_complete_tool_calls()
  87. ]
  88. return message
  89. # ==================== 增强版 LLM 类 ====================
  90. class EnhancedHelloAgentsLLM(HelloAgentsLLM):
  91. """
  92. 增强版 HelloAgentsLLM - 添加流式工具调用支持
  93. 继承自 HelloAgentsLLM,新增以下方法:
  94. - astream_invoke_with_tools: 异步流式工具调用
  95. - get_last_stream_tool_result: 获取最后一次流式工具调用的累积结果
  96. """
  97. def __init__(self, *args, **kwargs):
  98. super().__init__(*args, **kwargs)
  99. self._last_stream_tool_result: Optional[StreamToolCallResult] = None
  100. async def astream_invoke_with_tools(
  101. self,
  102. messages: List[Dict],
  103. tools: List[Dict],
  104. tool_choice: Union[str, Dict] = "auto",
  105. **kwargs
  106. ) -> AsyncIterator[StreamToolEvent]:
  107. """
  108. 异步流式调用 LLM 并支持工具调用(Function Calling)
  109. 这是最优雅的流式工具调用方法,封装了所有流式处理的复杂逻辑。
  110. Args:
  111. messages: 消息列表
  112. tools: 工具 schema 列表
  113. tool_choice: 工具选择策略
  114. **kwargs: 其他参数(temperature, max_tokens 等)
  115. Yields:
  116. StreamToolEvent: 流式事件,可能是文本内容或工具调用增量
  117. Example:
  118. async for event in llm.astream_invoke_with_tools(messages, tools):
  119. if event.is_content:
  120. print(event.content, end="")
  121. elif event.event_type == StreamToolEventType.TOOL_CALL_START:
  122. print(f"\\n调用工具: {event.tool_name}")
  123. # 获取累积结果
  124. result = llm.get_last_stream_tool_result()
  125. """
  126. from openai import AsyncOpenAI
  127. # 创建异步客户端
  128. client = AsyncOpenAI(
  129. api_key=self.api_key,
  130. base_url=self.base_url,
  131. timeout=self.timeout
  132. )
  133. # 构建请求参数
  134. request_params: Dict[str, Any] = {
  135. "model": self.model,
  136. "messages": messages,
  137. "tools": tools,
  138. "tool_choice": tool_choice,
  139. "stream": True,
  140. }
  141. if kwargs.get("temperature") is not None:
  142. request_params["temperature"] = kwargs["temperature"]
  143. if self.max_tokens:
  144. request_params["max_tokens"] = self.max_tokens
  145. # 初始化累积结果
  146. result = StreamToolCallResult()
  147. try:
  148. response = await client.chat.completions.create(**request_params)
  149. async for chunk in response:
  150. if not chunk.choices:
  151. continue
  152. choice = chunk.choices[0]
  153. delta = choice.delta
  154. # 处理文本内容
  155. if delta.content:
  156. result.add_content(delta.content)
  157. yield StreamToolEvent(
  158. event_type=StreamToolEventType.CONTENT,
  159. content=delta.content
  160. )
  161. # 处理工具调用增量
  162. if delta.tool_calls:
  163. for tc_delta in delta.tool_calls:
  164. idx = tc_delta.index
  165. # 工具调用开始(收到 ID 或名称)
  166. if tc_delta.id or (tc_delta.function and tc_delta.function.name):
  167. tool_id = tc_delta.id or ""
  168. tool_name = tc_delta.function.name if tc_delta.function else ""
  169. if tool_id or tool_name:
  170. result.add_tool_call_start(idx, tool_id, tool_name)
  171. yield StreamToolEvent(
  172. event_type=StreamToolEventType.TOOL_CALL_START,
  173. tool_call_index=idx,
  174. tool_call_id=tool_id,
  175. tool_name=tool_name
  176. )
  177. # 工具调用参数增量
  178. if tc_delta.function and tc_delta.function.arguments:
  179. args_delta = tc_delta.function.arguments
  180. result.add_tool_call_delta(idx, args_delta)
  181. yield StreamToolEvent(
  182. event_type=StreamToolEventType.TOOL_CALL_DELTA,
  183. tool_call_index=idx,
  184. tool_arguments_delta=args_delta
  185. )
  186. # 处理结束原因
  187. if choice.finish_reason:
  188. result.finish_reason = choice.finish_reason
  189. yield StreamToolEvent(
  190. event_type=StreamToolEventType.FINISH,
  191. finish_reason=choice.finish_reason
  192. )
  193. except Exception as e:
  194. raise HelloAgentsException(f"流式工具调用失败: {str(e)}")
  195. # 保存累积结果供后续使用
  196. self._last_stream_tool_result = result
  197. def get_last_stream_tool_result(self) -> Optional[StreamToolCallResult]:
  198. """
  199. 获取最后一次流式工具调用的累积结果
  200. Returns:
  201. StreamToolCallResult 或 None
  202. """
  203. return self._last_stream_tool_result