MikelWL commited on
Commit
735f8ed
·
1 Parent(s): d7e3980

Add care experience breakdown pie chart

Browse files
README.md CHANGED
@@ -98,6 +98,7 @@ Backend listens on `http://localhost:8000`, Gradio on `http://localhost:7860`.
98
  After the conversation completes, the app runs post-conversation analysis and populates:
99
  - Bottom-up findings (emergent themes) with evidence
100
  - Top-down coding (care experience rubric + codebook categories) with evidence
 
101
 
102
  ### Analysis frameworks
103
 
 
98
  After the conversation completes, the app runs post-conversation analysis and populates:
99
  - Bottom-up findings (emergent themes) with evidence
100
  - Top-down coding (care experience rubric + codebook categories) with evidence
101
+ - Care experience includes a model-rated positive/mixed/negative split (for pie chart summary)
102
 
103
  ### Analysis frameworks
104
 
backend/api/conversation_service.py CHANGED
@@ -144,7 +144,7 @@ async def run_resource_agent_analysis(
144
  )
145
 
146
  schema_version = "9"
147
- analysis_prompt_version = "v4-3pass"
148
 
149
  evidence_catalog: Dict[str, Dict[str, Any]] = {}
150
  for message in transcript:
@@ -243,6 +243,11 @@ async def run_resource_agent_analysis(
243
  "{\n"
244
  f" \"schema_version\": \"{schema_version}\",\n"
245
  f" \"analysis_prompt_version\": \"{analysis_prompt_version}\",\n"
 
 
 
 
 
246
  " \"care_experience\": {\n"
247
  " \"positive\": {\n"
248
  " \"summary\": string,\n"
@@ -348,6 +353,26 @@ async def run_resource_agent_analysis(
348
  await _phase("rubric", "running")
349
  rubric_raw = await client.generate(prompt=_rubric_prompt(), system_prompt=rubric_system_prompt, temperature=0.2)
350
  rubric = json.loads(rubric_raw) if isinstance(rubric_raw, str) else {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  care_experience = rubric.get("care_experience") or {}
352
  for key in ("positive", "mixed", "negative", "neutral"):
353
  box = care_experience.get(key)
@@ -356,6 +381,7 @@ async def run_resource_agent_analysis(
356
  if normalized is not None:
357
  box["confidence"] = normalized
358
  await _partial({
 
359
  "care_experience": care_experience,
360
  })
361
  await _phase("rubric", "complete")
@@ -397,6 +423,7 @@ async def run_resource_agent_analysis(
397
  "analysis_prompt_version": analysis_prompt_version,
398
  "evidence_catalog": evidence_catalog,
399
  "themes": themes,
 
400
  "care_experience": care_experience,
401
  "top_down_codebook": td_book or {
402
  "template_id": top_down_template_id or "",
 
144
  )
145
 
146
  schema_version = "9"
147
+ analysis_prompt_version = "v5-3pass"
148
 
149
  evidence_catalog: Dict[str, Dict[str, Any]] = {}
150
  for message in transcript:
 
243
  "{\n"
244
  f" \"schema_version\": \"{schema_version}\",\n"
245
  f" \"analysis_prompt_version\": \"{analysis_prompt_version}\",\n"
246
+ " \"care_experience_breakdown\": {\n"
247
+ " \"positive\": number, // 0-100, sums to 100 with mixed + negative\n"
248
+ " \"mixed\": number,\n"
249
+ " \"negative\": number\n"
250
+ " },\n"
251
  " \"care_experience\": {\n"
252
  " \"positive\": {\n"
253
  " \"summary\": string,\n"
 
353
  await _phase("rubric", "running")
354
  rubric_raw = await client.generate(prompt=_rubric_prompt(), system_prompt=rubric_system_prompt, temperature=0.2)
355
  rubric = json.loads(rubric_raw) if isinstance(rubric_raw, str) else {}
356
+ breakdown = rubric.get("care_experience_breakdown") or {}
357
+ breakdown_normalized = {}
358
+ if isinstance(breakdown, dict):
359
+ for key in ("positive", "mixed", "negative"):
360
+ value = breakdown.get(key)
361
+ try:
362
+ num = float(value)
363
+ except (TypeError, ValueError):
364
+ num = None
365
+ if num is not None:
366
+ if num <= 1.0:
367
+ num = num * 100.0
368
+ breakdown_normalized[key] = max(0.0, min(100.0, num))
369
+ if breakdown_normalized:
370
+ total = sum(breakdown_normalized.values())
371
+ if total > 0:
372
+ for key in breakdown_normalized:
373
+ breakdown_normalized[key] = round((breakdown_normalized[key] / total) * 100.0, 1)
374
+ else:
375
+ breakdown_normalized = None
376
  care_experience = rubric.get("care_experience") or {}
377
  for key in ("positive", "mixed", "negative", "neutral"):
378
  box = care_experience.get(key)
 
381
  if normalized is not None:
382
  box["confidence"] = normalized
383
  await _partial({
384
+ "care_experience_breakdown": breakdown_normalized,
385
  "care_experience": care_experience,
386
  })
387
  await _phase("rubric", "complete")
 
423
  "analysis_prompt_version": analysis_prompt_version,
424
  "evidence_catalog": evidence_catalog,
425
  "themes": themes,
426
+ "care_experience_breakdown": breakdown_normalized,
427
  "care_experience": care_experience,
428
  "top_down_codebook": td_book or {
429
  "template_id": top_down_template_id or "",
docs/capabilities.md CHANGED
@@ -88,6 +88,7 @@ Analysis runs post-conversation (or on uploaded text) and produces evidence-back
88
 
89
  - **Bottom-up findings** (emergent themes)
90
  - **Care experience rubric** (fixed buckets: positive / mixed / negative / neutral)
 
91
  - **Top-down codebook categories** (template-driven)
92
 
93
  ### Analysis execution (3-pass)
 
88
 
89
  - **Bottom-up findings** (emergent themes)
90
  - **Care experience rubric** (fixed buckets: positive / mixed / negative / neutral)
91
+ - Includes a model-rated split across positive/mixed/negative (for a pie chart summary)
92
  - **Top-down codebook categories** (template-driven)
93
 
94
  ### Analysis execution (3-pass)
frontend/pages/shared_views.py CHANGED
@@ -443,6 +443,34 @@ def get_shared_views_js() -> str:
443
  } = props;
444
 
445
  const care = activeResources?.care_experience || null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  if (!care || typeof care !== 'object') {
447
  return (
448
  <p className="text-slate-400 text-center py-8 text-sm">
@@ -459,6 +487,47 @@ def get_shared_views_js() -> str:
459
  const neutral = care.neutral || null;
460
  return (
461
  <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  {renderCareBox('Positive', 'positive', positive, 'No positive insights detected.')}
463
  {renderCareBox('Mixed / Tradeoffs', 'mixed', mixed, 'No mixed/tradeoff insights detected.')}
464
  {renderCareBox('Negative', 'negative', negative, 'No negative insights detected.')}
 
443
  } = props;
444
 
445
  const care = activeResources?.care_experience || null;
446
+ const breakdownRaw = activeResources?.care_experience_breakdown || null;
447
+ const normalizeBreakdown = (raw) => {
448
+ if (!raw || typeof raw !== 'object') return null;
449
+ const toNum = (v) => {
450
+ if (typeof v === 'number') return v;
451
+ const parsed = parseFloat(String(v));
452
+ return Number.isFinite(parsed) ? parsed : null;
453
+ };
454
+ const out = {
455
+ positive: toNum(raw.positive),
456
+ mixed: toNum(raw.mixed),
457
+ negative: toNum(raw.negative)
458
+ };
459
+ if (out.positive == null && out.mixed == null && out.negative == null) return null;
460
+ Object.keys(out).forEach((k) => {
461
+ const v = out[k];
462
+ if (v == null) return;
463
+ out[k] = v <= 1 ? v * 100 : v;
464
+ });
465
+ const total = (out.positive || 0) + (out.mixed || 0) + (out.negative || 0);
466
+ if (!total) return null;
467
+ return {
468
+ positive: Math.round(((out.positive || 0) / total) * 1000) / 10,
469
+ mixed: Math.round(((out.mixed || 0) / total) * 1000) / 10,
470
+ negative: Math.round(((out.negative || 0) / total) * 1000) / 10
471
+ };
472
+ };
473
+ const breakdown = normalizeBreakdown(breakdownRaw);
474
  if (!care || typeof care !== 'object') {
475
  return (
476
  <p className="text-slate-400 text-center py-8 text-sm">
 
487
  const neutral = care.neutral || null;
488
  return (
489
  <>
490
+ {breakdown && (
491
+ <div className="bg-slate-50 border border-slate-200 rounded-lg p-3">
492
+ <div className="flex items-center justify-between gap-2 mb-2">
493
+ <div className="text-xs font-semibold text-slate-600">Care experience split</div>
494
+ <div className="relative group">
495
+ <div className="h-5 w-5 rounded-full border border-slate-300 text-slate-500 flex items-center justify-center text-[11px] font-semibold bg-white cursor-help">
496
+ i
497
+ </div>
498
+ <div className="pointer-events-none absolute right-0 top-6 z-10 w-64 rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs text-slate-700 shadow-lg opacity-0 transition-opacity duration-150 group-hover:opacity-100">
499
+ This pie chart reflects the model’s own judgment of how strongly the conversation supports positive, mixed, or negative experiences. It is a weighted perception (not a simple count), so one strong negative can outweigh many mild positives.
500
+ </div>
501
+ </div>
502
+ </div>
503
+ <div className="flex items-center gap-4">
504
+ <div
505
+ className="h-20 w-20 rounded-full border border-slate-200"
506
+ style={{
507
+ background: `conic-gradient(#16a34a 0% ${breakdown.positive}%, #f59e0b ${breakdown.positive}% ${breakdown.positive + breakdown.mixed}%, #ef4444 ${breakdown.positive + breakdown.mixed}% 100%)`
508
+ }}
509
+ title={`Positive ${breakdown.positive}%, Mixed ${breakdown.mixed}%, Negative ${breakdown.negative}%`}
510
+ />
511
+ <div className="space-y-1 text-xs text-slate-700">
512
+ <div className="flex items-center gap-2">
513
+ <span className="inline-block h-2.5 w-2.5 rounded-full bg-green-600" />
514
+ <span>Positive</span>
515
+ <span className="ml-auto font-semibold">{breakdown.positive}%</span>
516
+ </div>
517
+ <div className="flex items-center gap-2">
518
+ <span className="inline-block h-2.5 w-2.5 rounded-full bg-amber-500" />
519
+ <span>Mixed</span>
520
+ <span className="ml-auto font-semibold">{breakdown.mixed}%</span>
521
+ </div>
522
+ <div className="flex items-center gap-2">
523
+ <span className="inline-block h-2.5 w-2.5 rounded-full bg-red-500" />
524
+ <span>Negative</span>
525
+ <span className="ml-auto font-semibold">{breakdown.negative}%</span>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ )}
531
  {renderCareBox('Positive', 'positive', positive, 'No positive insights detected.')}
532
  {renderCareBox('Mixed / Tradeoffs', 'mixed', mixed, 'No mixed/tradeoff insights detected.')}
533
  {renderCareBox('Negative', 'negative', negative, 'No negative insights detected.')}