agent / backend /backend_app /helloAgents /agents /function_call_agent.py
Toadied's picture
2312
8b383ad verified
Raw
History Blame Contribute Delete
19.2 kB
"""FunctionCallAgent - 使用OpenAI函数调用范式的Agent实现"""
from __future__ import annotations
import json
import logging
from typing import Iterator, Optional, Union, TYPE_CHECKING, Any, Dict, Callable
from ..core.agent import Agent
from ..core.config import Config
from ..core.llm import HelloAgentsLLM
from ..core.message import Message
if TYPE_CHECKING:
from ..tools.registry import ToolRegistry
logger = logging.getLogger(__name__)
def _map_parameter_type(param_type: str) -> str:
"""将工具参数类型映射为JSON Schema允许的类型"""
normalized = (param_type or "").lower()
if normalized in {"string", "number", "integer", "boolean", "array", "object"}:
return normalized
return "string"
class FunctionCallAgent(Agent):
"""基于OpenAI原生函数调用机制的Agent"""
def __init__(
self,
name: str,
llm: HelloAgentsLLM,
system_prompt: Optional[str] = None,
config: Optional[Config] = None,
tool_registry: Optional["ToolRegistry"] = None,
enable_tool_calling: bool = True,
default_tool_choice: Union[str, dict] = "auto",
max_tool_iterations: int = 3,
tool_call_listener: Optional[Callable[[dict[str, Any]], None]] = None,
):
super().__init__(name, llm, system_prompt, config)
self.tool_registry = tool_registry
self.enable_tool_calling = enable_tool_calling and tool_registry is not None
self.default_tool_choice = default_tool_choice
self.max_tool_iterations = max_tool_iterations
self._tool_call_listener = tool_call_listener
def _get_system_prompt(self) -> str:
"""构建系统提示词,注入工具描述"""
base_prompt = self.system_prompt or "你是一个可靠的AI助理,能够在需要时调用工具完成任务。"
if not self.enable_tool_calling or not self.tool_registry:
return base_prompt
tools_description = self.tool_registry.get_tools_description()
if not tools_description or tools_description == "暂无可用工具":
return base_prompt
prompt = base_prompt + "\n\n## 可用工具\n"
prompt += "当你判断需要外部信息或执行动作时,可以直接通过函数调用使用以下工具:\n"
prompt += tools_description + "\n"
prompt += "\n请主动决定是否调用工具,合理利用多次调用来获得完备答案。"
return prompt
def _build_tool_schemas(self) -> list[dict[str, Any]]:
if not self.enable_tool_calling or not self.tool_registry:
return []
schemas: list[dict[str, Any]] = []
# Tool对象
for tool in self.tool_registry.get_all_tools():
properties: Dict[str, Any] = {}
required: list[str] = []
try:
parameters = tool.get_parameters()
except Exception:
parameters = []
for param in parameters:
properties[param.name] = {
"type": _map_parameter_type(param.type),
"description": param.description or ""
}
if param.default is not None:
properties[param.name]["default"] = param.default
if getattr(param, "required", True):
required.append(param.name)
schema: dict[str, Any] = {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description or "",
"parameters": {
"type": "object",
"properties": properties
}
}
}
if required:
schema["function"]["parameters"]["required"] = required
schemas.append(schema)
# register_function 注册的工具(直接访问内部结构)
function_map = getattr(self.tool_registry, "_functions", {})
for name, info in function_map.items():
schemas.append(
{
"type": "function",
"function": {
"name": name,
"description": info.get("description", ""),
"parameters": {
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "输入文本"
}
},
"required": ["input"]
}
}
}
)
return schemas
@staticmethod
def _extract_message_content(raw_content: Any) -> str:
"""从OpenAI响应的message.content中安全提取文本"""
if raw_content is None:
return ""
if isinstance(raw_content, str):
return raw_content
if isinstance(raw_content, list):
parts: list[str] = []
for item in raw_content:
text = getattr(item, "text", None)
if text is None and isinstance(item, dict):
text = item.get("text")
if text:
parts.append(text)
return "".join(parts)
return str(raw_content)
@staticmethod
def _parse_function_call_arguments(arguments: Optional[str]) -> dict[str, Any]:
"""解析模型返回的JSON字符串参数"""
if not arguments:
return {}
try:
parsed = json.loads(arguments)
return parsed if isinstance(parsed, dict) else {}
except json.JSONDecodeError:
return {}
def _convert_parameter_types(self, tool_name: str, param_dict: dict[str, Any]) -> dict[str, Any]:
"""根据工具定义尽可能转换参数类型"""
if not self.tool_registry:
return param_dict
tool = self.tool_registry.get_tool(tool_name)
if not tool:
return param_dict
try:
tool_params = tool.get_parameters()
except Exception:
return param_dict
type_mapping = {param.name: param.type for param in tool_params}
converted: dict[str, Any] = {}
for key, value in param_dict.items():
param_type = type_mapping.get(key)
if not param_type:
converted[key] = value
continue
try:
normalized = param_type.lower()
if normalized in {"number", "float"}:
converted[key] = float(value)
elif normalized in {"integer", "int"}:
converted[key] = int(value)
elif normalized in {"boolean", "bool"}:
if isinstance(value, bool):
converted[key] = value
elif isinstance(value, (int, float)):
converted[key] = bool(value)
elif isinstance(value, str):
converted[key] = value.lower() in {"true", "1", "yes"}
else:
converted[key] = bool(value)
else:
converted[key] = value
except (TypeError, ValueError):
converted[key] = value
return converted
def _execute_tool_call(self, tool_name: str, arguments: dict[str, Any]) -> str:
"""执行工具调用并返回字符串结果"""
logger.info(f"[TOOL EXECUTION] 开始执行工具调用 - 工具名称: {tool_name}, 参数数量: {len(arguments)}")
if not self.tool_registry:
logger.error("[TOOL EXECUTION] 错误:未配置工具注册表")
return "❌ 错误:未配置工具注册表"
result = ""
parsed_arguments = arguments.copy()
success = False
try:
tool = self.tool_registry.get_tool(tool_name)
if tool:
logger.info(f"[TOOL EXECUTION] 找到Tool对象: {tool_name}")
typed_arguments = self._convert_parameter_types(tool_name, arguments)
logger.debug(f"[TOOL EXECUTION] 参数类型转换完成 - 原始参数: {arguments}, 转换后: {typed_arguments}")
result = tool.run(typed_arguments)
parsed_arguments = typed_arguments
success = "❌" not in result and "错误:" not in result and "失败" not in result
logger.info(f"[TOOL EXECUTION] Tool执行完成 - 成功: {success}, 结果长度: {len(result)}")
else:
func = self.tool_registry.get_function(tool_name)
if func:
logger.info(f"[TOOL EXECUTION] 找到函数工具: {tool_name}")
input_text = arguments.get("input", "")
result = func(input_text)
success = "❌" not in result and "错误:" not in result and "失败" not in result
logger.info(f"[TOOL EXECUTION] 函数工具执行完成 - 成功: {success}, 结果长度: {len(result)}")
else:
logger.error(f"[TOOL EXECUTION] 未找到工具: {tool_name}")
result = f"❌ 错误:未找到工具 '{tool_name}'"
except Exception as exc:
logger.error(f"[TOOL EXECUTION] 工具调用异常 - 工具: {tool_name}, 异常: {exc}")
result = f"❌ 工具调用失败:{exc}"
success = False
# 通知监听器
if self._tool_call_listener:
try:
logger.debug("[TOOL EXECUTION] 调用工具调用监听器")
self._tool_call_listener({
"agent_name": self.name,
"tool_name": tool_name,
"raw_arguments": arguments, # 原始参数字典
"parsed_arguments": parsed_arguments, # 转换后的参数
"result": result,
"success": success
})
except Exception as e:
logger.warning(f"工具调用监听器失败: {e}")
logger.info(f"[TOOL EXECUTION] 工具调用执行结束 - 工具: {tool_name}, 成功: {success}")
return result
def _invoke_with_tools(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]], tool_choice: Union[str, dict], **kwargs):
"""调用底层OpenAI客户端执行函数调用"""
client = getattr(self.llm, "_client", None)
if client is None:
raise RuntimeError("HelloAgentsLLM 未正确初始化客户端,无法执行函数调用。")
client_kwargs = dict(kwargs)
client_kwargs.setdefault("temperature", self.llm.temperature)
if self.llm.max_tokens is not None:
client_kwargs.setdefault("max_tokens", self.llm.max_tokens)
return client.chat.completions.create(
model=self.llm.model,
messages=messages,
tools=tools,
tool_choice=tool_choice,
**client_kwargs,
)
def run(
self,
input_text: str,
*,
max_tool_iterations: Optional[int] = None,
tool_choice: Optional[Union[str, dict]] = None,
**kwargs,
) -> str:
"""
执行函数调用范式的对话流程
"""
logger.info(f"[FUNCTION CALL AGENT] 开始执行 - 输入长度: {len(input_text)}")
messages: list[dict[str, Any]] = []
system_prompt = self._get_system_prompt()
messages.append({"role": "system", "content": system_prompt})
logger.info(f"[FUNCTION CALL AGENT] 系统提示词长度: {len(system_prompt)}")
for msg in self._history:
messages.append({"role": msg.role, "content": msg.content})
messages.append({"role": "user", "content": input_text})
tool_schemas = self._build_tool_schemas()
logger.info(f"[FUNCTION CALL AGENT] 构建工具模式 - 可用工具数量: {len(tool_schemas)}")
if not tool_schemas:
logger.info("[FUNCTION CALL AGENT] 无可用工具,直接调用LLM")
response_text = self.llm.invoke(messages, **kwargs)
self.add_message(Message(input_text, "user"))
self.add_message(Message(response_text, "assistant"))
logger.info(f"[FUNCTION CALL AGENT] 直接LLM响应长度: {len(response_text)}")
return response_text
iterations_limit = max_tool_iterations if max_tool_iterations is not None else self.max_tool_iterations
effective_tool_choice: Union[str, dict] = tool_choice if tool_choice is not None else self.default_tool_choice
logger.info(f"[FUNCTION CALL AGENT] 迭代设置 - 最大迭代次数: {iterations_limit}, 工具选择策略: {effective_tool_choice}")
current_iteration = 0
final_response = ""
logger.info(f"[FUNCTION CALL AGENT] 开始工具调用迭代循环 (当前迭代: {current_iteration}/{iterations_limit})")
while current_iteration < iterations_limit:
logger.info(f"[FUNCTION CALL AGENT] 迭代 {current_iteration + 1}/{iterations_limit} - 调用LLM进行工具选择")
response = self._invoke_with_tools(
messages,
tools=tool_schemas,
tool_choice=effective_tool_choice,
**kwargs,
)
choice = response.choices[0]
assistant_message = choice.message
content = self._extract_message_content(assistant_message.content)
tool_calls = list(assistant_message.tool_calls or [])
logger.info(f"[FUNCTION CALL AGENT] LLM响应 - 内容长度: {len(content)}, 工具调用数量: {len(tool_calls)}")
if tool_calls:
logger.info(f"[FUNCTION CALL AGENT] 检测到工具调用 - 将执行 {len(tool_calls)} 个工具调用")
assistant_payload: dict[str, Any] = {"role": "assistant", "content": content}
assistant_payload["tool_calls"] = []
for tool_call in tool_calls:
assistant_payload["tool_calls"].append(
{
"id": tool_call.id,
"type": tool_call.type,
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments,
},
}
)
messages.append(assistant_payload)
for i, tool_call in enumerate(tool_calls):
tool_name = tool_call.function.name
arguments = self._parse_function_call_arguments(tool_call.function.arguments)
logger.info(f"[FUNCTION CALL AGENT] 执行工具调用 {i+1}/{len(tool_calls)}: {tool_name}, 参数长度: {len(str(arguments))}")
result = self._execute_tool_call(tool_name, arguments)
logger.info(f"[FUNCTION CALL AGENT] 工具调用 {tool_name} 完成 - 结果长度: {len(result)}")
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_name,
"content": result,
}
)
current_iteration += 1
logger.info(f"[FUNCTION CALL AGENT] 迭代 {current_iteration}/{iterations_limit} 完成,继续下一轮迭代")
continue
final_response = content
logger.info(f"[FUNCTION CALL AGENT] 无工具调用,获取最终响应 - 长度: {len(final_response)}")
messages.append({"role": "assistant", "content": final_response})
break
if current_iteration >= iterations_limit and not final_response:
logger.info(f"[FUNCTION CALL AGENT] 达到最大迭代次数 ({iterations_limit}) 但无最终响应,强制调用LLM生成最终答案")
final_choice = self._invoke_with_tools(
messages,
tools=tool_schemas,
tool_choice="none",
**kwargs,
)
final_response = self._extract_message_content(final_choice.choices[0].message.content)
logger.info(f"[FUNCTION CALL AGENT] 强制生成的最终响应长度: {len(final_response)}")
messages.append({"role": "assistant", "content": final_response})
self.add_message(Message(input_text, "user"))
self.add_message(Message(final_response, "assistant"))
logger.info(f"[FUNCTION CALL AGENT] 执行完成 - 总迭代次数: {current_iteration}, 最终响应长度: {len(final_response)}")
return final_response
def add_tool(self, tool) -> None:
"""便捷方法:将工具注册到当前Agent"""
if not self.tool_registry:
from ..tools.registry import ToolRegistry
self.tool_registry = ToolRegistry()
self.enable_tool_calling = True
if hasattr(tool, "auto_expand") and getattr(tool, "auto_expand"):
expanded_tools = tool.get_expanded_tools()
if expanded_tools:
for expanded_tool in expanded_tools:
self.tool_registry.register_tool(expanded_tool)
print(f"✅ MCP工具 '{tool.name}' 已展开为 {len(expanded_tools)} 个独立工具")
return
self.tool_registry.register_tool(tool)
def remove_tool(self, tool_name: str) -> bool:
if self.tool_registry:
before = set(self.tool_registry.list_tools())
self.tool_registry.unregister(tool_name)
after = set(self.tool_registry.list_tools())
return tool_name in before and tool_name not in after
return False
def list_tools(self) -> list[str]:
if self.tool_registry:
return self.tool_registry.list_tools()
return []
def has_tools(self) -> bool:
return self.enable_tool_calling and self.tool_registry is not None
def stream_run(self, input_text: str, **kwargs) -> Iterator[str]:
"""流式调用暂未实现,直接回退到一次性调用"""
result = self.run(input_text, **kwargs)
yield result