From 89b9f7919a960997b3eee81572c64ec7b7dcd113 Mon Sep 17 00:00:00 2001 From: snaily Date: Sat, 22 Mar 2025 02:48:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9OpenAI?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E5=8A=9F=E8=83=BD=E7=9A=84?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改进消息转换器以处理OpenAI的tool_calls格式 添加JSON解析以正确转换函数调用参数 优化消息处理逻辑,增加更多空值检查 在流式响应中添加工具调用检测和处理 根据工具调用状态设置适当的finish_reason --- app/handler/message_converter.py | 55 ++++++++++++++----------- app/service/chat/openai_chat_service.py | 12 ++++-- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/app/handler/message_converter.py b/app/handler/message_converter.py index 0cb3163..ca000ed 100644 --- a/app/handler/message_converter.py +++ b/app/handler/message_converter.py @@ -1,6 +1,7 @@ # app/services/chat/message_converter.py from abc import ABC, abstractmethod +import json import re from typing import Any, Dict, List, Optional import requests @@ -114,6 +115,36 @@ class OpenAIMessageConverter(MessageConverter): for idx, msg in enumerate(messages): 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 == "tool": role = "user" @@ -123,30 +154,6 @@ class OpenAIMessageConverter(MessageConverter): role = "user" else: 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 role == "system": system_instruction_parts.extend(parts) diff --git a/app/service/chat/openai_chat_service.py b/app/service/chat/openai_chat_service.py index df095c5..67fed05 100644 --- a/app/service/chat/openai_chat_service.py +++ b/app/service/chat/openai_chat_service.py @@ -78,7 +78,7 @@ def _build_tools( tool.pop("googleSearch", None) tool.pop("codeExecution", None) - return [tool] + return [tool] if tool else [] def _get_safety_settings(model: str) -> List[Dict[str, str]]: @@ -201,10 +201,11 @@ class OpenAIChatService: max_retries = 3 while retries < max_retries: try: + tool_call_flag = False async for line in self.api_client.stream_generate_content( payload, model, api_key ): - # print(line) + print(line) if line.startswith("data:"): chunk = json.loads(line[6:]) openai_chunk = self.response_handler.handle_response( @@ -227,8 +228,13 @@ class OpenAIChatService: yield optimized_chunk 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(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" logger.info("Streaming completed successfully") break # 成功后退出循环