Files
Odoo-Modules/fusion_accounting/services/adapters/claude.py
gsinghpal 3cc93b8783 changes
2026-04-04 15:37:16 -04:00

142 lines
4.8 KiB
Python

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