feat: add Demographics & Analytics tool to Fusion Claims Intelligence

- Add Tool 6 for demographic analysis using direct SQL queries
- Age group breakdowns: clients, applications, avg apps/client, avg funding
- Device popularity by age bracket (under 45, 45-60, 61-75, 75+)
- City demographics with average age and funding per city
- Benefit type analysis (ODSP, OWP, ACSD, Regular)
- Top devices with average client age
- Overall funding summary (totals, averages, age range)
- Update AI topic and system prompt with Tool 6 routing examples
This commit is contained in:
2026-03-10 02:45:51 +00:00
parent 0053576cc2
commit 8761d0e7c7
2 changed files with 245 additions and 3 deletions

View File

@@ -69,26 +69,45 @@ ai['result'] = record._fc_tool_adp_billing_period(period)
<field name="ai_tool_schema">{"type": "object", "properties": {"period": {"type": "string", "description": "Which period to query: 'current' (default), 'previous', 'next', or a specific date in YYYY-MM-DD format"}}, "required": []}</field> <field name="ai_tool_schema">{"type": "object", "properties": {"period": {"type": "string", "description": "Which period to query: 'current' (default), 'previous', 'next', or a specific date in YYYY-MM-DD format"}}, "required": []}</field>
</record> </record>
<!-- Tool 6: Demographics & Analytics -->
<record id="ai_tool_demographics" model="ir.actions.server">
<field name="name">Fusion: Demographics &amp; Analytics</field>
<field name="state">code</field>
<field name="use_in_ai" eval="True"/>
<field name="model_id" ref="ai.model_ai_agent"/>
<field name="code">
ai['result'] = record._fc_tool_demographics(analysis_type, city_filter, sale_type_filter)
</field>
<field name="ai_tool_description">Run demographic and analytical queries on client data. Returns age group breakdowns, device popularity by age, city demographics with average age and funding, benefit type analysis, top devices with average client age, and overall funding summaries. Use for questions like "average applications by age group", "what devices do clients over 75 use", "demographics by city", "how old are our clients on average".</field>
<field name="ai_tool_schema">{"type": "object", "properties": {"analysis_type": {"type": "string", "description": "Type of analysis: 'full' (all reports), 'age_groups' (clients/apps by age), 'devices_by_age' (device popularity per age bracket), 'city_demographics' (per-city stats with avg age), 'benefits' (benefit type breakdown), 'top_devices' (most popular devices with avg client age), 'funding_summary' (overall totals and averages)"}, "city_filter": {"type": "string", "description": "Optional: filter city demographics to a specific city"}, "sale_type_filter": {"type": "string", "description": "Optional: filter by sale type (adp, odsp, wsib, etc.)"}}, "required": []}</field>
</record>
<!-- ================================================================= --> <!-- ================================================================= -->
<!-- AI TOPIC (references all tools) --> <!-- AI TOPIC (references all tools) -->
<!-- ================================================================= --> <!-- ================================================================= -->
<record id="ai_topic_client_intelligence" model="ai.topic"> <record id="ai_topic_client_intelligence" model="ai.topic">
<field name="name">Fusion Claims Client Intelligence</field> <field name="name">Fusion Claims Client Intelligence</field>
<field name="description">Query client profiles, ADP claims, funding history, billing periods, and device information.</field> <field name="description">Query client profiles, ADP claims, funding history, billing periods, demographics, and device information.</field>
<field name="instructions">You help users find information about ADP clients, claims, medical conditions, devices, funding history, and billing periods. Use the Fusion tools to query data. <field name="instructions">You help users find information about ADP clients, claims, medical conditions, devices, funding history, billing periods, and demographics. Use the Fusion tools to query data.
Common questions and which tool to use: Common questions and which tool to use:
- "What is the status of [name]?" -> Use Client Status Lookup (Tool 4) - "What is the status of [name]?" -> Use Client Status Lookup (Tool 4)
- "What is the ADP billing for this period?" -> Use ADP Billing Period (Tool 5) - "What is the ADP billing for this period?" -> Use ADP Billing Period (Tool 5)
- "Tell me about [name]'s funding history" -> Use Client Status Lookup first, then Client Details for more depth - "Tell me about [name]'s funding history" -> Use Client Status Lookup first, then Client Details for more depth
- "How many claims do we have?" -> Use Claims Statistics (Tool 3) - "How many claims do we have?" -> Use Claims Statistics (Tool 3)
- "Find clients in [city]" -> Use Search Client Profiles (Tool 1)</field> - "Find clients in [city]" -> Use Search Client Profiles (Tool 1)
- "Average applications by age group" -> Use Demographics (Tool 6) with analysis_type="age_groups"
- "What devices do seniors use?" -> Use Demographics (Tool 6) with analysis_type="devices_by_age"
- "What is the average age of our clients?" -> Use Demographics (Tool 6) with analysis_type="funding_summary"
- "Show demographics for Brampton" -> Use Demographics (Tool 6) with analysis_type="city_demographics" and city_filter="Brampton"
- "How many ODSP clients do we have?" -> Use Demographics (Tool 6) with analysis_type="benefits"</field>
<field name="tool_ids" eval="[(6, 0, [ <field name="tool_ids" eval="[(6, 0, [
ref('fusion_claims.ai_tool_search_clients'), ref('fusion_claims.ai_tool_search_clients'),
ref('fusion_claims.ai_tool_client_details'), ref('fusion_claims.ai_tool_client_details'),
ref('fusion_claims.ai_tool_client_stats'), ref('fusion_claims.ai_tool_client_stats'),
ref('fusion_claims.ai_tool_client_status'), ref('fusion_claims.ai_tool_client_status'),
ref('fusion_claims.ai_tool_adp_billing'), ref('fusion_claims.ai_tool_adp_billing'),
ref('fusion_claims.ai_tool_demographics'),
])]"/> ])]"/>
</record> </record>
@@ -146,12 +165,17 @@ Capabilities:
3. Provide aggregated statistics about claims, funding types, and demographics 3. Provide aggregated statistics about claims, funding types, and demographics
4. Look up a client's complete status by name -- including all orders, invoices, documents, and next steps 4. Look up a client's complete status by name -- including all orders, invoices, documents, and next steps
5. Query ADP billing period summaries -- total invoiced to ADP, paid vs unpaid, submission deadlines 5. Query ADP billing period summaries -- total invoiced to ADP, paid vs unpaid, submission deadlines
6. Run demographic analytics -- age group breakdowns, device popularity by age, city demographics, benefit analysis, funding summaries
How to handle common requests: How to handle common requests:
- "What is the status of [name]?" -> Use the Client Status Lookup tool with the client's name - "What is the status of [name]?" -> Use the Client Status Lookup tool with the client's name
- "What is the ADP billing this period?" -> Use the ADP Billing Period tool with period="current" - "What is the ADP billing this period?" -> Use the ADP Billing Period tool with period="current"
- "What was the ADP billing last period?" -> Use the ADP Billing Period tool with period="previous" - "What was the ADP billing last period?" -> Use the ADP Billing Period tool with period="previous"
- "Show me [name]'s funding history" -> Use Client Status Lookup, then Client Details for full history - "Show me [name]'s funding history" -> Use Client Status Lookup, then Client Details for full history
- "Average applications by age group" -> Use Demographics tool with analysis_type="age_groups"
- "What devices do clients over 75 use?" -> Use Demographics tool with analysis_type="devices_by_age"
- "What is the average age of our clients?" -> Use Demographics tool with analysis_type="funding_summary"
- "Demographics for [city]" -> Use Demographics tool with city_filter
- If a client is not found by profile, the system also searches by contact/partner name - If a client is not found by profile, the system also searches by contact/partner name
Response guidelines: Response guidelines:

View File

@@ -50,6 +50,20 @@ STATUS_NEXT_STEPS = {
'expired': 'Contact client about reapplication if still needed', 'expired': 'Contact client about reapplication if still needed',
} }
BASE_DEVICE_LABELS = {
'adultWalkertype1': 'Adult Walker Type 1',
'adultWalkertype2': 'Adult Walker Type 2',
'adultWalkertype3': 'Adult Walker Type 3',
'adultlightwtStdwheelchair': 'Lightweight Standard Wheelchair',
'adultlightwtPermwheelchair': 'Lightweight Permanent Wheelchair',
'adultTiltwheelchair': 'Adult Tilt Wheelchair',
'adultStdwheelchair': 'Adult Standard Wheelchair',
'adultHighperfwheelchair': 'Adult High Performance Wheelchair',
'adultType2': 'Adult Power Type 2',
'adultType3': 'Adult Power Type 3',
'powerScooter': 'Power Scooter',
}
class AIAgentFusionClaims(models.Model): class AIAgentFusionClaims(models.Model):
"""Extend ai.agent with Fusion Claims tool methods.""" """Extend ai.agent with Fusion Claims tool methods."""
@@ -450,3 +464,207 @@ class AIAgentFusionClaims(models.Model):
}, },
'invoices': invoice_details, 'invoices': invoice_details,
}) })
# ------------------------------------------------------------------
# Tool 6: Demographics & Analytics
# ------------------------------------------------------------------
def _fc_tool_demographics(self, analysis_type=None, city_filter=None, sale_type_filter=None):
"""AI Tool: Run demographic and analytical queries on client data."""
cr = self.env.cr
results = {}
if not analysis_type:
analysis_type = 'full'
if analysis_type in ('full', 'age_groups'):
cr.execute("""
SELECT
CASE
WHEN age < 18 THEN 'Under 18'
WHEN age BETWEEN 18 AND 30 THEN '18-30'
WHEN age BETWEEN 31 AND 45 THEN '31-45'
WHEN age BETWEEN 46 AND 60 THEN '46-60'
WHEN age BETWEEN 61 AND 75 THEN '61-75'
ELSE '75+'
END AS age_group,
COUNT(DISTINCT p.id) AS clients,
COUNT(app.id) AS applications,
ROUND(COUNT(app.id)::numeric / NULLIF(COUNT(DISTINCT p.id), 0), 2) AS avg_applications,
COALESCE(ROUND(SUM(p.total_adp_funded) / NULLIF(COUNT(DISTINCT p.id), 0), 2), 0) AS avg_adp_funded,
COALESCE(ROUND(SUM(p.total_amount) / NULLIF(COUNT(DISTINCT p.id), 0), 2), 0) AS avg_total
FROM fusion_client_profile p
CROSS JOIN LATERAL (
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
) a
LEFT JOIN fusion_adp_application_data app ON app.profile_id = p.id
WHERE p.date_of_birth IS NOT NULL
GROUP BY age_group
ORDER BY MIN(age)
""")
rows = cr.fetchall()
results['age_groups'] = [
{
'age_group': r[0], 'clients': r[1], 'applications': r[2],
'avg_applications': float(r[3]), 'avg_adp_funded': float(r[4]),
'avg_total': float(r[5]),
} for r in rows
]
if analysis_type in ('full', 'devices_by_age'):
cr.execute("""
SELECT
CASE
WHEN age < 45 THEN 'Under 45'
WHEN age BETWEEN 45 AND 60 THEN '45-60'
WHEN age BETWEEN 61 AND 75 THEN '61-75'
ELSE '75+'
END AS age_group,
app.base_device,
COUNT(*) AS count
FROM fusion_client_profile p
CROSS JOIN LATERAL (
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
) a
JOIN fusion_adp_application_data app ON app.profile_id = p.id
WHERE p.date_of_birth IS NOT NULL
AND app.base_device IS NOT NULL AND app.base_device != ''
GROUP BY age_group, app.base_device
ORDER BY age_group, count DESC
""")
rows = cr.fetchall()
devices_by_age = {}
for age_group, device, count in rows:
if age_group not in devices_by_age:
devices_by_age[age_group] = []
label = BASE_DEVICE_LABELS.get(device, device)
devices_by_age[age_group].append({'device': label, 'count': count})
results['devices_by_age'] = devices_by_age
if analysis_type in ('full', 'city_demographics'):
city_clause = ""
params = []
if city_filter:
city_clause = "AND LOWER(p.city) = LOWER(%s)"
params = [city_filter]
cr.execute(f"""
SELECT
p.city,
COUNT(DISTINCT p.id) AS clients,
COUNT(app.id) AS applications,
ROUND(AVG(a.age), 1) AS avg_age,
COALESCE(ROUND(SUM(p.total_adp_funded) / NULLIF(COUNT(DISTINCT p.id), 0), 2), 0) AS avg_adp_funded
FROM fusion_client_profile p
CROSS JOIN LATERAL (
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
) a
LEFT JOIN fusion_adp_application_data app ON app.profile_id = p.id
WHERE p.city IS NOT NULL AND p.city != ''
AND p.date_of_birth IS NOT NULL
{city_clause}
GROUP BY p.city
ORDER BY clients DESC
LIMIT 15
""", params)
rows = cr.fetchall()
results['city_demographics'] = [
{
'city': r[0], 'clients': r[1], 'applications': r[2],
'avg_age': float(r[3]), 'avg_adp_funded': float(r[4]),
} for r in rows
]
if analysis_type in ('full', 'benefits'):
cr.execute("""
SELECT
CASE
WHEN p.benefit_type = 'odsp' THEN 'ODSP'
WHEN p.benefit_type = 'owp' THEN 'Ontario Works'
WHEN p.benefit_type = 'acsd' THEN 'ACSD'
WHEN p.receives_social_assistance THEN 'Social Assistance (other)'
ELSE 'Regular (no assistance)'
END AS benefit_category,
COUNT(DISTINCT p.id) AS clients,
COUNT(app.id) AS applications,
ROUND(AVG(a.age), 1) AS avg_age,
COALESCE(ROUND(AVG(p.total_adp_funded), 2), 0) AS avg_adp_funded
FROM fusion_client_profile p
CROSS JOIN LATERAL (
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
) a
LEFT JOIN fusion_adp_application_data app ON app.profile_id = p.id
WHERE p.date_of_birth IS NOT NULL
GROUP BY benefit_category
ORDER BY clients DESC
""")
rows = cr.fetchall()
results['benefits_breakdown'] = [
{
'category': r[0], 'clients': r[1], 'applications': r[2],
'avg_age': float(r[3]), 'avg_adp_funded': float(r[4]),
} for r in rows
]
if analysis_type in ('full', 'top_devices'):
cr.execute("""
SELECT
app.base_device,
COUNT(*) AS count,
COUNT(DISTINCT app.profile_id) AS unique_clients,
ROUND(AVG(a.age), 1) AS avg_age
FROM fusion_adp_application_data app
JOIN fusion_client_profile p ON p.id = app.profile_id
CROSS JOIN LATERAL (
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
) a
WHERE app.base_device IS NOT NULL AND app.base_device != '' AND app.base_device != 'none'
AND p.date_of_birth IS NOT NULL
GROUP BY app.base_device
ORDER BY count DESC
LIMIT 15
""")
rows = cr.fetchall()
results['top_devices'] = [
{
'device': BASE_DEVICE_LABELS.get(r[0], r[0]),
'device_code': r[0],
'applications': r[1],
'unique_clients': r[2],
'avg_client_age': float(r[3]),
} for r in rows
]
if analysis_type in ('full', 'funding_summary'):
cr.execute("""
SELECT
COUNT(*) AS total_profiles,
ROUND(AVG(a.age), 1) AS avg_age,
ROUND(SUM(p.total_adp_funded), 2) AS total_adp_funded,
ROUND(SUM(p.total_client_portion), 2) AS total_client_portion,
ROUND(SUM(p.total_amount), 2) AS grand_total,
ROUND(AVG(p.total_adp_funded), 2) AS avg_adp_per_client,
ROUND(AVG(p.total_client_portion), 2) AS avg_client_per_client,
ROUND(AVG(p.claim_count), 2) AS avg_claims_per_client,
MIN(a.age) AS youngest,
MAX(a.age) AS oldest
FROM fusion_client_profile p
CROSS JOIN LATERAL (
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
) a
WHERE p.date_of_birth IS NOT NULL
""")
r = cr.fetchone()
results['funding_summary'] = {
'total_profiles': r[0],
'avg_age': float(r[1]),
'total_adp_funded': float(r[2]),
'total_client_portion': float(r[3]),
'grand_total': float(r[4]),
'avg_adp_per_client': float(r[5]),
'avg_client_per_client': float(r[6]),
'avg_claims_per_client': float(r[7]),
'youngest_client_age': r[8],
'oldest_client_age': r[9],
}
return json.dumps(results)