mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-02-15 16:25:52 +08:00
295 lines
11 KiB
Python
295 lines
11 KiB
Python
# encoding:utf-8
|
|
|
|
"""
|
|
OpenAI-Compatible Bot Base Class
|
|
|
|
Provides a common implementation for bots that are compatible with OpenAI's API format.
|
|
This includes: OpenAI, LinkAI, Azure OpenAI, and many third-party providers.
|
|
"""
|
|
|
|
import json
|
|
import openai
|
|
from common.log import logger
|
|
|
|
|
|
class OpenAICompatibleBot:
|
|
"""
|
|
Base class for OpenAI-compatible bots.
|
|
|
|
Provides common tool calling implementation that can be inherited by:
|
|
- ChatGPTBot
|
|
- LinkAIBot
|
|
- OpenAIBot
|
|
- AzureChatGPTBot
|
|
- Other OpenAI-compatible providers
|
|
|
|
Subclasses only need to override get_api_config() to provide their specific API settings.
|
|
"""
|
|
|
|
def get_api_config(self):
|
|
"""
|
|
Get API configuration for this bot.
|
|
|
|
Subclasses should override this to provide their specific config.
|
|
|
|
Returns:
|
|
dict: {
|
|
'api_key': str,
|
|
'api_base': str (optional),
|
|
'model': str,
|
|
'default_temperature': float,
|
|
'default_top_p': float,
|
|
'default_frequency_penalty': float,
|
|
'default_presence_penalty': float,
|
|
}
|
|
"""
|
|
raise NotImplementedError("Subclasses must implement get_api_config()")
|
|
|
|
def call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
|
"""
|
|
Call OpenAI-compatible API with tool support for agent integration
|
|
|
|
This method handles:
|
|
1. Format conversion (Claude format → OpenAI format)
|
|
2. System prompt injection
|
|
3. API calling with proper configuration
|
|
4. Error handling
|
|
|
|
Args:
|
|
messages: List of messages (may be in Claude format from agent)
|
|
tools: List of tool definitions (may be in Claude format from agent)
|
|
stream: Whether to use streaming
|
|
**kwargs: Additional parameters (max_tokens, temperature, system, etc.)
|
|
|
|
Returns:
|
|
Formatted response in OpenAI format or generator for streaming
|
|
"""
|
|
try:
|
|
# Get API configuration from subclass
|
|
api_config = self.get_api_config()
|
|
|
|
# Convert messages from Claude format to OpenAI format
|
|
messages = self._convert_messages_to_openai_format(messages)
|
|
|
|
# Convert tools from Claude format to OpenAI format
|
|
if tools:
|
|
tools = self._convert_tools_to_openai_format(tools)
|
|
|
|
# Handle system prompt (OpenAI uses system message, Claude uses separate parameter)
|
|
system_prompt = kwargs.get('system')
|
|
if system_prompt:
|
|
# Add system message at the beginning if not already present
|
|
if not messages or messages[0].get('role') != 'system':
|
|
messages = [{"role": "system", "content": system_prompt}] + messages
|
|
else:
|
|
# Replace existing system message
|
|
messages[0] = {"role": "system", "content": system_prompt}
|
|
|
|
# Build request parameters
|
|
request_params = {
|
|
"model": kwargs.get("model", api_config.get('model', 'gpt-3.5-turbo')),
|
|
"messages": messages,
|
|
"temperature": kwargs.get("temperature", api_config.get('default_temperature', 0.9)),
|
|
"top_p": kwargs.get("top_p", api_config.get('default_top_p', 1.0)),
|
|
"frequency_penalty": kwargs.get("frequency_penalty", api_config.get('default_frequency_penalty', 0.0)),
|
|
"presence_penalty": kwargs.get("presence_penalty", api_config.get('default_presence_penalty', 0.0)),
|
|
"stream": stream
|
|
}
|
|
|
|
# Add max_tokens if specified
|
|
if kwargs.get("max_tokens"):
|
|
request_params["max_tokens"] = kwargs["max_tokens"]
|
|
|
|
# Add tools if provided
|
|
if tools:
|
|
request_params["tools"] = tools
|
|
request_params["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
|
|
# Make API call with proper configuration
|
|
api_key = api_config.get('api_key')
|
|
api_base = api_config.get('api_base')
|
|
|
|
if stream:
|
|
return self._handle_stream_response(request_params, api_key, api_base)
|
|
else:
|
|
return self._handle_sync_response(request_params, api_key, api_base)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
logger.error(f"[{self.__class__.__name__}] call_with_tools error: {error_msg}")
|
|
if stream:
|
|
def error_generator():
|
|
yield {
|
|
"error": True,
|
|
"message": error_msg,
|
|
"status_code": 500
|
|
}
|
|
return error_generator()
|
|
else:
|
|
return {
|
|
"error": True,
|
|
"message": error_msg,
|
|
"status_code": 500
|
|
}
|
|
|
|
def _handle_sync_response(self, request_params, api_key, api_base):
|
|
"""Handle synchronous OpenAI API response"""
|
|
try:
|
|
# Build kwargs with explicit API configuration
|
|
kwargs = dict(request_params)
|
|
if api_key:
|
|
kwargs["api_key"] = api_key
|
|
if api_base:
|
|
kwargs["api_base"] = api_base
|
|
|
|
response = openai.ChatCompletion.create(**kwargs)
|
|
return response
|
|
|
|
except Exception as e:
|
|
logger.error(f"[{self.__class__.__name__}] sync response error: {e}")
|
|
return {
|
|
"error": True,
|
|
"message": str(e),
|
|
"status_code": 500
|
|
}
|
|
|
|
def _handle_stream_response(self, request_params, api_key, api_base):
|
|
"""Handle streaming OpenAI API response"""
|
|
try:
|
|
# Build kwargs with explicit API configuration
|
|
kwargs = dict(request_params)
|
|
if api_key:
|
|
kwargs["api_key"] = api_key
|
|
if api_base:
|
|
kwargs["api_base"] = api_base
|
|
|
|
stream = openai.ChatCompletion.create(**kwargs)
|
|
|
|
# Stream chunks to caller
|
|
for chunk in stream:
|
|
yield chunk
|
|
|
|
except Exception as e:
|
|
logger.error(f"[{self.__class__.__name__}] stream response error: {e}")
|
|
yield {
|
|
"error": True,
|
|
"message": str(e),
|
|
"status_code": 500
|
|
}
|
|
|
|
def _convert_tools_to_openai_format(self, tools):
|
|
"""
|
|
Convert tools from Claude format to OpenAI format
|
|
|
|
Claude format: {name, description, input_schema}
|
|
OpenAI format: {type: "function", function: {name, description, parameters}}
|
|
"""
|
|
if not tools:
|
|
return None
|
|
|
|
openai_tools = []
|
|
for tool in tools:
|
|
# Check if already in OpenAI format
|
|
if 'type' in tool and tool['type'] == 'function':
|
|
openai_tools.append(tool)
|
|
else:
|
|
# Convert from Claude format
|
|
openai_tools.append({
|
|
"type": "function",
|
|
"function": {
|
|
"name": tool.get("name"),
|
|
"description": tool.get("description"),
|
|
"parameters": tool.get("input_schema", {})
|
|
}
|
|
})
|
|
|
|
return openai_tools
|
|
|
|
def _convert_messages_to_openai_format(self, messages):
|
|
"""
|
|
Convert messages from Claude format to OpenAI format
|
|
|
|
Claude uses content blocks with types like 'tool_use', 'tool_result'
|
|
OpenAI uses 'tool_calls' in assistant messages and 'tool' role for results
|
|
"""
|
|
if not messages:
|
|
return []
|
|
|
|
openai_messages = []
|
|
|
|
for msg in messages:
|
|
role = msg.get("role")
|
|
content = msg.get("content")
|
|
|
|
# Handle string content (already in correct format)
|
|
if isinstance(content, str):
|
|
openai_messages.append(msg)
|
|
continue
|
|
|
|
# Handle list content (Claude format with content blocks)
|
|
if isinstance(content, list):
|
|
# Check if this is a tool result message (user role with tool_result blocks)
|
|
if role == "user" and any(block.get("type") == "tool_result" for block in content):
|
|
# Separate text content and tool_result blocks
|
|
text_parts = []
|
|
tool_results = []
|
|
|
|
for block in content:
|
|
if block.get("type") == "text":
|
|
text_parts.append(block.get("text", ""))
|
|
elif block.get("type") == "tool_result":
|
|
tool_results.append(block)
|
|
|
|
# First, add tool result messages (must come immediately after assistant with tool_calls)
|
|
for block in tool_results:
|
|
openai_messages.append({
|
|
"role": "tool",
|
|
"tool_call_id": block.get("tool_use_id"),
|
|
"content": block.get("content", "")
|
|
})
|
|
|
|
# Then, add text content as a separate user message if present
|
|
if text_parts:
|
|
openai_messages.append({
|
|
"role": "user",
|
|
"content": " ".join(text_parts)
|
|
})
|
|
|
|
# Check if this is an assistant message with tool_use blocks
|
|
elif role == "assistant":
|
|
# Separate text content and tool_use blocks
|
|
text_parts = []
|
|
tool_calls = []
|
|
|
|
for block in content:
|
|
if block.get("type") == "text":
|
|
text_parts.append(block.get("text", ""))
|
|
elif block.get("type") == "tool_use":
|
|
tool_calls.append({
|
|
"id": block.get("id"),
|
|
"type": "function",
|
|
"function": {
|
|
"name": block.get("name"),
|
|
"arguments": json.dumps(block.get("input", {}))
|
|
}
|
|
})
|
|
|
|
# Build OpenAI format assistant message
|
|
openai_msg = {
|
|
"role": "assistant",
|
|
"content": " ".join(text_parts) if text_parts else None
|
|
}
|
|
|
|
if tool_calls:
|
|
openai_msg["tool_calls"] = tool_calls
|
|
|
|
openai_messages.append(openai_msg)
|
|
else:
|
|
# Other list content, keep as is
|
|
openai_messages.append(msg)
|
|
else:
|
|
# Other formats, keep as is
|
|
openai_messages.append(msg)
|
|
|
|
return openai_messages
|