import json import logging from odoo import models, api, _ from odoo.exceptions import UserError _logger = logging.getLogger(__name__) try: from openai import OpenAI except ImportError: OpenAI = None class FusionAccountingAdapterOpenAI(models.AbstractModel): _name = 'fusion.accounting.adapter.openai' _description = 'OpenAI AI Adapter' def _get_client(self): if OpenAI is None: raise UserError(_("The 'openai' Python package is not installed.")) try: key = self.env['fusion.api.service'].get_api_key( provider_type='openai', consumer='fusion_accounting', feature='chat_with_tools', ) except Exception: ICP = self.env['ir.config_parameter'].sudo() key = ICP.get_param('fusion_accounting.openai_api_key', '') if not key: raise UserError(_("No OpenAI API key configured.")) return OpenAI(api_key=key) def _get_model_name(self): ICP = self.env['ir.config_parameter'].sudo() return ICP.get_param('fusion_accounting.openai_model', 'gpt-5.4-mini') def _format_tools(self, tools): formatted = [] for tool in tools: formatted.append({ 'type': 'function', 'function': { 'name': tool['name'], 'description': tool['description'], 'parameters': tool.get('parameters', {'type': 'object', 'properties': {}}), }, }) return formatted def _is_reasoning_model(self, model): return model.startswith('o1') or model.startswith('o3') or model.startswith('o4') def call_with_tools(self, system_prompt, messages, tools=None, model_override=None): client = self._get_client() model = model_override or self._get_model_name() is_reasoning = self._is_reasoning_model(model) if is_reasoning: api_messages = [{'role': 'developer', 'content': system_prompt}] else: api_messages = [{'role': 'system', 'content': system_prompt}] for msg in messages: if msg['role'] in ('user', 'assistant', 'tool'): api_messages.append(msg) kwargs = { 'model': model, 'messages': api_messages, } if is_reasoning: kwargs['max_completion_tokens'] = 16384 kwargs['reasoning_effort'] = 'medium' else: kwargs['max_tokens'] = 4096 if tools: kwargs['tools'] = self._format_tools(tools) try: response = client.chat.completions.create(**kwargs) except Exception as e: _logger.error("OpenAI API error: %s", e) raise UserError(_("OpenAI API error: %s", str(e))) choice = response.choices[0] message = choice.message tool_calls = [] if message.tool_calls: for tc in message.tool_calls: try: args = json.loads(tc.function.arguments) except (json.JSONDecodeError, TypeError): _logger.warning("Malformed tool args from OpenAI: %s", tc.function.arguments) args = {} tool_calls.append({ 'id': tc.id, 'name': tc.function.name, 'arguments': args, }) return { 'text': message.content or '', 'tool_calls': tool_calls if tool_calls else None, 'tokens_in': response.usage.prompt_tokens, 'tokens_out': response.usage.completion_tokens, 'stop_reason': choice.finish_reason, 'raw_message': message, } def append_tool_results(self, messages, ai_response, tool_results): raw_msg = ai_response.get('raw_message') assistant_msg = {'role': 'assistant', 'content': raw_msg.content or ''} if raw_msg.tool_calls: assistant_msg['tool_calls'] = [ { 'id': tc.id, 'type': 'function', 'function': { 'name': tc.function.name, 'arguments': tc.function.arguments, }, } for tc in raw_msg.tool_calls ] messages.append(assistant_msg) for tr in tool_results: messages.append({ 'role': 'tool', 'tool_call_id': tr['tool_call_id'], 'content': tr['result'], }) return messages