# 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): # Convert each tool_result block to a separate tool message for block in content: if block.get("type") == "tool_result": openai_messages.append({ "role": "tool", "tool_call_id": block.get("tool_use_id"), "content": block.get("content", "") }) # 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