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

@@ -50,6 +50,20 @@ STATUS_NEXT_STEPS = {
'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):
"""Extend ai.agent with Fusion Claims tool methods."""
@@ -450,3 +464,207 @@ class AIAgentFusionClaims(models.Model):
},
'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)