import json import logging from odoo import models, api, _ from odoo.exceptions import UserError _logger = logging.getLogger(__name__) try: import anthropic as anthropic_sdk except ImportError: anthropic_sdk = None class FusionAccountingAdapterClaude(models.AbstractModel): _name = 'fusion.accounting.adapter.claude' _description = 'Claude AI Adapter' def _get_client(self): if anthropic_sdk is None: raise UserError(_("The 'anthropic' Python package is not installed.")) try: key = self.env['fusion.api.service'].get_api_key( provider_type='anthropic', consumer='fusion_accounting', feature='chat_with_tools', ) except Exception: ICP = self.env['ir.config_parameter'].sudo() key = ICP.get_param('fusion_accounting.anthropic_api_key', '') if not key: raise UserError(_("No Anthropic API key configured.")) return anthropic_sdk.Anthropic(api_key=key) def _get_model_name(self): ICP = self.env['ir.config_parameter'].sudo() return ICP.get_param('fusion_accounting.claude_model', 'claude-sonnet-4-6') def _format_tools(self, tools): formatted = [] for tool in tools: t = { 'name': tool['name'], 'description': tool['description'], 'input_schema': tool.get('parameters', {'type': 'object', 'properties': {}}), } formatted.append(t) return formatted def _supports_extended_thinking(self, model): return '4-6' in model or '4-5' in model or '4-1' in model or '4-0' in model 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() api_messages = [] for msg in messages: if msg['role'] in ('user', 'assistant'): api_messages.append(msg) kwargs = { 'model': model, 'max_tokens': 16384, 'system': system_prompt, 'messages': api_messages, } if tools: kwargs['tools'] = self._format_tools(tools) if self._supports_extended_thinking(model) and tools: kwargs['thinking'] = { 'type': 'enabled', 'budget_tokens': 8192, } try: response = client.messages.create(**kwargs) except anthropic_sdk.BadRequestError as e: if 'thinking' in str(e).lower(): kwargs.pop('thinking', None) response = client.messages.create(**kwargs) else: raise UserError(_("Claude API error: %s", str(e))) except Exception as e: _logger.error("Claude API error: %s", e) raise UserError(_("Claude API error: %s", str(e))) text_parts = [] tool_calls = [] thinking_text = [] for block in response.content: if block.type == 'text': text_parts.append(block.text) elif block.type == 'tool_use': tool_calls.append({ 'id': block.id, 'name': block.name, 'arguments': block.input, }) elif block.type == 'thinking': thinking_text.append(block.thinking) if thinking_text: _logger.debug("Claude thinking: %s", thinking_text[0][:500]) return { 'text': '\n'.join(text_parts), 'tool_calls': tool_calls if tool_calls else None, 'tokens_in': response.usage.input_tokens, 'tokens_out': response.usage.output_tokens, 'stop_reason': response.stop_reason, 'raw_content': response.content, } def append_tool_results(self, messages, ai_response, tool_results): assistant_content = [] for block in ai_response.get('raw_content', []): if hasattr(block, 'type'): if block.type == 'text': assistant_content.append({'type': 'text', 'text': block.text}) elif block.type == 'tool_use': assistant_content.append({ 'type': 'tool_use', 'id': block.id, 'name': block.name, 'input': block.input, }) messages.append({'role': 'assistant', 'content': assistant_content}) tool_result_content = [] for tr in tool_results: tool_result_content.append({ 'type': 'tool_result', 'tool_use_id': tr['tool_call_id'], 'content': tr['result'], }) messages.append({'role': 'user', 'content': tool_result_content}) return messages