Initial commit
This commit is contained in:
350
fusion_claims/models/client_chat.py
Normal file
350
fusion_claims/models/client_chat.py
Normal file
@@ -0,0 +1,350 @@
|
||||
# -*- 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,
|
||||
)
|
||||
Reference in New Issue
Block a user