# -*- coding: utf-8 -*- # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import json import logging from odoo import api, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class FusionClientChatSession(models.Model): _name = 'fusion.client.chat.session' _description = 'Client Intelligence Chat Session' _order = 'create_date desc' name = fields.Char(string='Session Title', required=True, default=lambda self: f'Chat - {fields.Date.today()}') profile_id = fields.Many2one( 'fusion.client.profile', string='Client Profile', ondelete='set null', help='If set, chat is scoped to this specific client', ) user_id = fields.Many2one( 'res.users', string='User', default=lambda self: self.env.user, required=True, ) message_ids = fields.One2many( 'fusion.client.chat.message', 'session_id', string='Messages', ) state = fields.Selection([ ('active', 'Active'), ('archived', 'Archived'), ], default='active', string='State') # Input field for the form view user_input = fields.Text(string='Your Question') def action_send_message(self): """Process user message and generate AI response.""" self.ensure_one() if not self.user_input or not self.user_input.strip(): return question = self.user_input.strip() # Create user message self.env['fusion.client.chat.message'].create({ 'session_id': self.id, 'role': 'user', 'content': question, }) # Generate AI response try: response = self._generate_ai_response(question) except Exception as e: _logger.exception('AI chat error: %s', e) response = f'Sorry, I encountered an error processing your question. Error: {str(e)}' # Create assistant message msg = self.env['fusion.client.chat.message'].create({ 'session_id': self.id, 'role': 'assistant', 'content': response, }) # Clear input self.user_input = False return { 'type': 'ir.actions.act_window', 'res_model': 'fusion.client.chat.session', 'view_mode': 'form', 'res_id': self.id, 'target': 'current', } def _generate_ai_response(self, question): """Generate an AI-powered response to the user question. Uses OpenAI API to analyze the question, query relevant data, and formulate a response. """ ICP = self.env['ir.config_parameter'].sudo() api_key = ICP.get_param('fusion_claims.ai_api_key', '') if not api_key: return self._generate_local_response(question) ai_model = ICP.get_param('fusion_claims.ai_model', 'gpt-4o-mini') # Build context about available data context_data = self._build_data_context(question) # Build system prompt system_prompt = self._build_system_prompt() # Build messages messages = [{'role': 'system', 'content': system_prompt}] # Add conversation history (last 10 messages) history = self.message_ids.sorted('create_date')[-10:] for msg in history: messages.append({'role': msg.role, 'content': msg.content}) # Add current question with data context user_msg = question if context_data: user_msg += f'\n\n--- Retrieved Data ---\n{context_data}' messages.append({'role': 'user', 'content': user_msg}) # Call OpenAI API try: import requests response = requests.post( 'https://api.openai.com/v1/chat/completions', headers={ 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', }, json={ 'model': ai_model, 'messages': messages, 'max_tokens': 2000, 'temperature': 0.3, }, timeout=30, ) response.raise_for_status() result = response.json() return result['choices'][0]['message']['content'] except ImportError: return self._generate_local_response(question) except Exception as e: _logger.warning('OpenAI API error: %s', e) return self._generate_local_response(question) def _generate_local_response(self, question): """Generate a response without AI, using direct database queries. This is the fallback when no API key is configured. """ question_lower = question.lower() Profile = self.env['fusion.client.profile'] SaleOrder = self.env['sale.order'] # If scoped to a specific profile if self.profile_id: profile = self.profile_id orders = SaleOrder.search([ ('partner_id', '=', profile.partner_id.id), ('x_fc_sale_type', '!=', False), ]) if profile.partner_id else SaleOrder lines = [] lines.append(f'**Client: {profile.display_name}**') lines.append(f'- Health Card: {profile.health_card_number or "N/A"}') lines.append(f'- Date of Birth: {profile.date_of_birth or "N/A"}') lines.append(f'- City: {profile.city or "N/A"}') lines.append(f'- Medical Condition: {profile.medical_condition or "N/A"}') lines.append(f'- Mobility Status: {profile.mobility_status or "N/A"}') lines.append(f'- Total Claims: {len(orders)}') lines.append(f'- Total ADP Funded: ${profile.total_adp_funded:,.2f}') lines.append(f'- Total Client Portion: ${profile.total_client_portion:,.2f}') if orders: lines.append('\n**Claims History:**') for order in orders[:10]: status = dict(order._fields['x_fc_adp_application_status'].selection).get( order.x_fc_adp_application_status, order.x_fc_adp_application_status or 'N/A' ) lines.append( f'- {order.name}: {order.x_fc_sale_type or "N/A"} | ' f'Status: {status} | ' f'ADP: ${order.x_fc_adp_portion_total:,.2f} | ' f'Client: ${order.x_fc_client_portion_total:,.2f}' ) apps = profile.application_data_ids[:5] if apps: lines.append('\n**Application History:**') for app in apps: lines.append( f'- {app.application_date or "No date"}: ' f'{app.device_category or "N/A"} | ' f'Device: {app.base_device or "N/A"} | ' f'Reason: {app.reason_for_application or "N/A"}' ) return '\n'.join(lines) # Global queries total_profiles = Profile.search_count([]) total_orders = SaleOrder.search_count([('x_fc_sale_type', '!=', False)]) if 'how many' in question_lower or 'count' in question_lower: if 'client' in question_lower or 'profile' in question_lower: return f'There are currently **{total_profiles}** client profiles in the system.' if 'claim' in question_lower or 'order' in question_lower or 'case' in question_lower: return f'There are currently **{total_orders}** claims/orders in the system.' # Search for specific client if 'find' in question_lower or 'search' in question_lower or 'show' in question_lower: # Try to extract name from question words = question.split() profiles = Profile.search([], limit=20) for word in words: if len(word) > 2 and word[0].isupper(): found = Profile.search([ '|', ('first_name', 'ilike', word), ('last_name', 'ilike', word), ], limit=5) if found: profiles = found break if profiles: lines = [f'Found **{len(profiles)}** matching profile(s):'] for p in profiles[:10]: lines.append( f'- **{p.display_name}** | HC: {p.health_card_number or "N/A"} | ' f'City: {p.city or "N/A"} | Claims: {p.claim_count}' ) return '\n'.join(lines) return ( f'I have access to **{total_profiles}** client profiles and **{total_orders}** claims. ' f'You can ask me questions like:\n' f'- "How many clients are from Brampton?"\n' f'- "Find client Raymond Wellesley"\n' f'- "Show all clients with CVA diagnosis"\n\n' f'For more intelligent responses, configure an OpenAI API key in ' f'Fusion Claims > Configuration > Settings.' ) def _build_system_prompt(self): """Build the system prompt for the AI.""" profile_context = '' if self.profile_id: p = self.profile_id profile_context = f""" You are currently looking at a specific client profile: - Name: {p.display_name} - Health Card: {p.health_card_number or 'N/A'} - DOB: {p.date_of_birth or 'N/A'} - City: {p.city or 'N/A'} - Medical Condition: {p.medical_condition or 'N/A'} - Mobility Status: {p.mobility_status or 'N/A'} - Total Claims: {p.claim_count} - Total ADP Funded: ${p.total_adp_funded:,.2f} - Total Client Portion: ${p.total_client_portion:,.2f} """ return f"""You are a helpful AI assistant for Fusion Claims, a healthcare equipment claims management system. You help users find information about clients, their ADP (Assistive Devices Program) claims, funding history, medical conditions, and devices. Available data includes: - Client profiles with personal info, health card numbers, addresses, medical conditions - ADP application data parsed from XML submissions - Sale orders with funding type, status, ADP/client portions - Device information (wheelchairs, walkers, power bases, seating) Funding types: ADP, ODSP, WSIB, March of Dimes, Muscular Dystrophy, Insurance, Hardship Funding, Rentals, Direct/Private Client types: REG (75%/25%), ODS, OWP, ACS, LTC, SEN, CCA (100%/0%) {profile_context} Answer concisely and include specific data when available. Format monetary values with $ and commas.""" def _build_data_context(self, question): """Query relevant data based on the question to provide context to AI.""" question_lower = question.lower() context_parts = [] Profile = self.env['fusion.client.profile'] SaleOrder = self.env['sale.order'] if self.profile_id: # Scoped to specific client - load their data p = self.profile_id orders = SaleOrder.search([ ('partner_id', '=', p.partner_id.id), ('x_fc_sale_type', '!=', False), ], limit=20) if p.partner_id else SaleOrder if orders: order_data = [] for o in orders: order_data.append({ 'name': o.name, 'sale_type': o.x_fc_sale_type, 'status': o.x_fc_adp_application_status, 'adp_total': o.x_fc_adp_portion_total, 'client_total': o.x_fc_client_portion_total, 'amount_total': o.amount_total, 'date': str(o.date_order) if o.date_order else '', }) context_parts.append(f'Orders: {json.dumps(order_data)}') apps = p.application_data_ids[:10] if apps: app_data = [] for a in apps: app_data.append({ 'date': str(a.application_date) if a.application_date else '', 'device_category': a.device_category, 'base_device': a.base_device or '', 'condition': a.medical_condition or '', 'reason': a.reason_for_application or '', 'authorizer': f'{a.authorizer_first_name} {a.authorizer_last_name}'.strip(), }) context_parts.append(f'Applications: {json.dumps(app_data)}') else: # Global query - provide summary stats total_profiles = Profile.search_count([]) total_orders = SaleOrder.search_count([('x_fc_sale_type', '!=', False)]) # City distribution if 'city' in question_lower or 'cities' in question_lower or 'where' in question_lower: city_data = SaleOrder.read_group( [('x_fc_sale_type', '!=', False), ('partner_id.city', '!=', False)], ['partner_id'], ['partner_id'], limit=20, ) context_parts.append(f'Total profiles: {total_profiles}, Total orders: {total_orders}') context_parts.append(f'Summary: {total_profiles} profiles, {total_orders} orders') return '\n'.join(context_parts) if context_parts else '' class FusionClientChatMessage(models.Model): _name = 'fusion.client.chat.message' _description = 'Chat Message' _order = 'create_date asc' session_id = fields.Many2one( 'fusion.client.chat.session', string='Session', required=True, ondelete='cascade', ) role = fields.Selection([ ('user', 'User'), ('assistant', 'Assistant'), ], string='Role', required=True) content = fields.Text(string='Content', required=True) timestamp = fields.Datetime( string='Timestamp', default=fields.Datetime.now, )