feat: 添加对OpenAI工具调用功能的支持

改进消息转换器以处理OpenAI的tool_calls格式
添加JSON解析以正确转换函数调用参数
优化消息处理逻辑,增加更多空值检查
在流式响应中添加工具调用检测和处理
根据工具调用状态设置适当的finish_reason
This commit is contained in:
snaily
2025-03-22 02:48:25 +08:00
parent a8dc98ab6a
commit 89b9f7919a
2 changed files with 40 additions and 27 deletions
+31 -24
View File
@@ -1,6 +1,7 @@
# app/services/chat/message_converter.py # app/services/chat/message_converter.py
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import json
import re import re
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import requests import requests
@@ -114,6 +115,36 @@ class OpenAIMessageConverter(MessageConverter):
for idx, msg in enumerate(messages): for idx, msg in enumerate(messages):
role = msg.get("role", "") role = msg.get("role", "")
parts = []
# 特别处理最后一个assistant的消息,按\n\n分割
if "content" in msg and isinstance(msg["content"], str) and msg["content"] and role == "assistant" and idx == len(messages) - 2:
# 按\n\n分割消息
content_parts = msg["content"].split("\n\n")
for part in content_parts:
if not part.strip(): # 跳过空内容
continue
# 处理可能包含图片的文本
parts.extend(_process_text_with_image(part))
elif "content" in msg and isinstance(msg["content"], str) and msg["content"]:
# 请求 gemini 接口时如果包含 content 字段但内容为空时会返回 400 错误,所以需要判断是否为空并移除
parts.extend(_process_text_with_image(msg["content"]))
elif "content" in msg and isinstance(msg["content"], list):
for content in msg["content"]:
if isinstance(content, str) and content:
parts.append({"text": content})
elif isinstance(content, dict):
if content["type"] == "text" and content["text"]:
parts.append({"text": content["text"]})
elif content["type"] == "image_url":
parts.append(_convert_image(content["image_url"]["url"]))
elif "tool_calls" in msg and isinstance(msg["tool_calls"], list):
for tool_call in msg["tool_calls"]:
function_call = tool_call.get("function",{})
function_call["args"] = json.loads(function_call.get("arguments","{}"))
del function_call["arguments"]
parts.append({"functionCall": function_call})
if role not in SUPPORTED_ROLES: if role not in SUPPORTED_ROLES:
if role == "tool": if role == "tool":
role = "user" role = "user"
@@ -123,30 +154,6 @@ class OpenAIMessageConverter(MessageConverter):
role = "user" role = "user"
else: else:
role = "model" role = "model"
parts = []
# 特别处理最后一个assistant的消息,按\n\n分割
if role == "assistant" and idx == len(messages) - 2 and isinstance(msg["content"], str) and msg["content"]:
# 按\n\n分割消息
content_parts = msg["content"].split("\n\n")
for part in content_parts:
if not part.strip(): # 跳过空内容
continue
# 处理可能包含图片的文本
parts.extend(_process_text_with_image(part))
elif isinstance(msg["content"], str) and msg["content"]:
# 请求 gemini 接口时如果包含 content 字段但内容为空时会返回 400 错误,所以需要判断是否为空并移除
parts.extend(_process_text_with_image(msg["content"]))
elif isinstance(msg["content"], list):
for content in msg["content"]:
if isinstance(content, str) and content:
parts.append({"text": content})
elif isinstance(content, dict):
if content["type"] == "text" and content["text"]:
parts.append({"text": content["text"]})
elif content["type"] == "image_url":
parts.append(_convert_image(content["image_url"]["url"]))
if parts: if parts:
if role == "system": if role == "system":
system_instruction_parts.extend(parts) system_instruction_parts.extend(parts)
+9 -3
View File
@@ -78,7 +78,7 @@ def _build_tools(
tool.pop("googleSearch", None) tool.pop("googleSearch", None)
tool.pop("codeExecution", None) tool.pop("codeExecution", None)
return [tool] return [tool] if tool else []
def _get_safety_settings(model: str) -> List[Dict[str, str]]: def _get_safety_settings(model: str) -> List[Dict[str, str]]:
@@ -201,10 +201,11 @@ class OpenAIChatService:
max_retries = 3 max_retries = 3
while retries < max_retries: while retries < max_retries:
try: try:
tool_call_flag = False
async for line in self.api_client.stream_generate_content( async for line in self.api_client.stream_generate_content(
payload, model, api_key payload, model, api_key
): ):
# print(line) print(line)
if line.startswith("data:"): if line.startswith("data:"):
chunk = json.loads(line[6:]) chunk = json.loads(line[6:])
openai_chunk = self.response_handler.handle_response( openai_chunk = self.response_handler.handle_response(
@@ -227,8 +228,13 @@ class OpenAIChatService:
yield optimized_chunk yield optimized_chunk
else: else:
# 如果没有文本内容(如工具调用等),整块输出 # 如果没有文本内容(如工具调用等),整块输出
if "tool_calls" in json.dumps(openai_chunk):
tool_call_flag = True
yield f"data: {json.dumps(openai_chunk)}\n\n" yield f"data: {json.dumps(openai_chunk)}\n\n"
yield f"data: {json.dumps(self.response_handler.handle_response({}, model, stream=True, finish_reason='stop'))}\n\n" if tool_call_flag:
yield f"data: {json.dumps(self.response_handler.handle_response({}, model, stream=True, finish_reason='tool_calls'))}\n\n"
else:
yield f"data: {json.dumps(self.response_handler.handle_response({}, model, stream=True, finish_reason='stop'))}\n\n"
yield "data: [DONE]\n\n" yield "data: [DONE]\n\n"
logger.info("Streaming completed successfully") logger.info("Streaming completed successfully")
break # 成功后退出循环 break # 成功后退出循环