This commit is contained in:
gsinghpal
2026-04-02 23:40:34 -04:00
parent 1c560c6df2
commit 4cd7357aa0
73 changed files with 7076 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from . import claude
from . import openai_adapter

View File

@@ -0,0 +1,141 @@
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):
client = self._get_client()
model = 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

View File

@@ -0,0 +1,137 @@
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):
client = self._get_client()
model = 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