feat: Function Calling 兼容

新增 xcli_core/fc_tools.py:
- MCP 定义 → OpenAI FC 格式转换
- FC 响应解析 (tool_calls 提取)
- 自动收集所有插件 FC 定义
- 400 错误自动降级为普通模式

引擎层改动 (openai_engine + ollama_engine):
- generate_response 新增 tools 参数
- 请求体自动注入 tools/tool_choice
- 响应检测 tool_calls 并返回 FC 标记
- 对话历史支持 tool 角色消息和 assistant.tool_calls
- _build_messages_with_history 兼容 FC 消息格式

cli_core.py 集成:
- _handle_fc_response: FC 响应 → 工具执行 → 结果回传
- _execute_tool_by_name: FC 调用路径的安全检查+工具分发
- process_conversation 循环自动处理 FC 工具调用
- TUI/CLI 双模式 FC 支持
This commit is contained in:
shuiyu1123 committed 2026-04-27 00:29:34 +08:00
1 parent 15e35ca8ad
commit 6de5c6f202
5 files changed
+559 -57

No files matched your search

+104 -42
View File
@@ -107,74 +107,136 @@ Ollama AI引擎插件帮助信息
# 如果出现异常,说明服务未运行
return False
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成AI响应 - 使用Ollama API"""
def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None):
"""
生成AI响应 - 使用Ollama API
Args:
user_input: 用户输入
tool_results: 工具执行结果
system_prompt: 系统提示词
tools: OpenAI FC 工具定义列表 (可选)
"""
try:
# 检查Ollama服务是否运行
if not self.is_service_running:
return "错误:Ollama服务未运行,请启动Ollama服务后再使用此引擎."
# 如果有工具结果,将其作为上下文返回给AI处理
if tool_results:
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
# 构建包含历史记录的消息列表
messages = self._build_messages_with_history(system_prompt, tool_result_text)
# 使用Ollama Python库调用API
response = ollama_client.chat(
model=self.model,
messages=messages,
stream=False
)
# 返回AI的回复
return response["message"]["content"]
# 构建包含历史记录的消息列表
messages = self._build_messages_with_history(system_prompt, user_input)
else:
messages = self._build_messages_with_history(system_prompt, user_input)
# 构建调用参数
kwargs = {
"model": self.model,
"messages": messages,
"stream": False
}
# ── Function Calling: 注入工具定义 ──
if tools:
kwargs["tools"] = tools
# 使用Ollama Python库调用API
response = ollama_client.chat(
model=self.model,
messages=messages,
stream=False
)
# 返回AI的回复
content = response["message"]["content"]
response = ollama_client.chat(**kwargs)
# ── Function Calling: 检查 tool_calls ──
message = response.get("message", {})
tool_calls_raw = message.get("tool_calls")
if tool_calls_raw:
# Ollama 的 tool_calls 格式: [{"function": {"name": ..., "arguments": {...}}}]
fc_calls = []
for i, tc in enumerate(tool_calls_raw):
func = tc.get("function", {})
name = func.get("name", "")
args = func.get("arguments", {})
call_id = f"call_{name}_{i}"
# 统一为小狸内部格式
if "args" in args and isinstance(args["args"], str):
final_args = args["args"]
elif "args" in args:
final_args = json.dumps(args["args"], ensure_ascii=False)
else:
parts = []
for k, v in args.items():
parts.append(str(v) if isinstance(v, str) else json.dumps(v, ensure_ascii=False))
final_args = " ".join(parts) if parts else ""
fc_calls.append({
"id": call_id,
"name": name,
"arguments": args,
"action": "use_tool",
"tool": name,
"args": final_args,
})
if fc_calls:
return json.dumps({
"_fc": True,
"tool_calls": fc_calls,
}, ensure_ascii=False)
# 普通文本响应
content = message.get("content", "")
# 清理无效的 UTF-8 代理字符
content = content.encode('utf-8', 'replace').decode('utf-8')
return content
except Exception as e:
# 如果API调用失败,返回错误信息
return f"Ollama API调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表"""
"""构建包含历史记录的消息列表(支持 FC tool 消息)"""
messages = []
# 添加系统提示词
if system_prompt:
# 清理代理字符
clean_prompt = system_prompt.encode('utf-8', 'replace').decode('utf-8')
messages.append({
"role": "system",
"content": clean_prompt
})
# 添加历史对话记录(使用共享历史)
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
# 确保历史记录格式正确
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
# 清理内容中的代理字符
clean_content = msg["content"].encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": msg["role"], "content": clean_content})
else:
# 如果格式不正确,跳过该条记录
if not isinstance(msg, dict) or "role" not in msg:
continue
role = msg["role"]
# tool 角色消息(FC 工具结果)
if role == "tool":
tool_msg = {"role": "tool", "content": msg.get("content", "")}
if "tool_call_id" in msg:
tool_msg["tool_call_id"] = msg["tool_call_id"]
if "name" in msg:
tool_msg["name"] = msg["name"]
messages.append(tool_msg)
continue
# assistant 消息可能包含 tool_calls
if role == "assistant" and "tool_calls" in msg:
assistant_msg = {
"role": "assistant",
"content": msg.get("content"),
"tool_calls": msg["tool_calls"]
}
messages.append(assistant_msg)
continue
# 普通 user/assistant 消息
if "content" in msg:
clean_content = msg["content"]
if isinstance(clean_content, str):
clean_content = clean_content.encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": role, "content": clean_content})
# 清理用户输入
if user_input:
clean_input = user_input.encode('utf-8', 'replace').decode('utf-8')
@@ -182,7 +244,7 @@ Ollama AI引擎插件帮助信息
"role": "user",
"content": clean_input
})
return messages
+83 -13
View File
@@ -125,8 +125,16 @@ OpenAI 兼容格式引擎帮助信息
url = url.rstrip('/') + '/chat/completions'
return url
def generate_response(self, user_input, tool_results=None, system_prompt=None):
"""生成 AI 响应"""
def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None):
"""
生成 AI 响应
Args:
user_input: 用户输入
tool_results: 工具执行结果
system_prompt: 系统提示词
tools: OpenAI FC 工具定义列表 (可选)
"""
try:
if not self.base_url:
return "错误: openai 引擎的 base_url 未配置,请在 config.json 中设置"
@@ -145,6 +153,11 @@ OpenAI 兼容格式引擎帮助信息
"stream": False
}
# ── Function Calling: 注入工具定义 ──
if tools:
body["tools"] = tools
body["tool_choice"] = "auto"
# 发送请求
api_url = self._get_api_url()
response = requests.post(
@@ -157,18 +170,49 @@ OpenAI 兼容格式引擎帮助信息
if response.status_code == 200:
response_data = response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
ai_response = response_data["choices"][0]["message"]["content"]
# 清理无效的 UTF-8 代理字符
ai_response = ai_response.encode('utf-8', 'replace').decode('utf-8')
# 处理思考内容
ai_response = self._process_thinking_content(ai_response)
return ai_response
message = response_data["choices"][0].get("message", {})
# ── Function Calling: 检查 tool_calls ──
tool_calls_raw = message.get("tool_calls")
if tool_calls_raw:
# 返回 FC 结果(由调用方处理工具执行)
from xcli_core.fc_tools import parse_fc_response
fc_calls = parse_fc_response(response_data)
if fc_calls:
# 返回一个特殊的 JSON 字符串,标记为 FC 调用
return json.dumps({
"_fc": True,
"tool_calls": fc_calls,
"raw_message": message
}, ensure_ascii=False)
# 普通文本响应
ai_response = message.get("content", "")
if ai_response:
# 清理无效的 UTF-8 代理字符
ai_response = ai_response.encode('utf-8', 'replace').decode('utf-8')
# 处理思考内容
ai_response = self._process_thinking_content(ai_response)
return ai_response or ""
else:
return f"API 返回格式错误: {response_data}"
elif response.status_code == 401:
return "API 认证失败,请检查 api_key 是否正确"
elif response.status_code == 404:
return f"API 端点不存在,请检查 base_url 是否正确: {api_url}"
elif response.status_code == 400:
# 工具定义可能不兼容,降级为无工具重试
if tools and "tools" in str(response.text).lower():
print(f"{Fore.YELLOW}FC 不支持,降级为普通模式{Style.RESET_ALL}")
body.pop("tools", None)
body.pop("tool_choice", None)
retry = requests.post(url=api_url, json=body, headers=self._get_headers(), timeout=None)
if retry.status_code == 200:
rd = retry.json()
if "choices" in rd and rd["choices"]:
ai_response = rd["choices"][0].get("message", {}).get("content", "")
return ai_response.encode('utf-8', 'replace').decode('utf-8') if ai_response else ""
return f"API 调用失败: {response.status_code} - {response.text}"
else:
return f"API 调用失败: {response.status_code} - {response.text}"
@@ -178,7 +222,7 @@ OpenAI 兼容格式引擎帮助信息
return f"API 调用失败: {str(e)}"
def _build_messages_with_history(self, system_prompt, user_input):
"""构建包含历史记录的消息列表"""
"""构建包含历史记录的消息列表(支持 FC tool 消息)"""
messages = []
# 添加系统提示词
@@ -192,12 +236,38 @@ OpenAI 兼容格式引擎帮助信息
# 添加历史对话记录(使用共享历史)
if self.shared_conversation_history is not None:
for msg in self.shared_conversation_history[-self.max_history:]:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
clean_content = msg["content"].encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": msg["role"], "content": clean_content})
else:
if not isinstance(msg, dict) or "role" not in msg:
continue
role = msg["role"]
# tool 角色消息(FC 工具结果)
if role == "tool":
tool_msg = {"role": "tool", "content": msg.get("content", "")}
if "tool_call_id" in msg:
tool_msg["tool_call_id"] = msg["tool_call_id"]
if "name" in msg:
tool_msg["name"] = msg["name"]
messages.append(tool_msg)
continue
# assistant 消息可能包含 tool_calls
if role == "assistant" and "tool_calls" in msg:
assistant_msg = {
"role": "assistant",
"content": msg.get("content"),
"tool_calls": msg["tool_calls"]
}
messages.append(assistant_msg)
continue
# 普通 user/assistant 消息
if "content" in msg:
clean_content = msg["content"]
if isinstance(clean_content, str):
clean_content = clean_content.encode('utf-8', 'replace').decode('utf-8')
messages.append({"role": role, "content": clean_content})
# 添加当前用户输入
if user_input:
clean_input = user_input.encode('utf-8', 'replace').decode('utf-8')