Skip to main content

Companion data-analyst skill — render the full Impact-Dashboard chart set

Date: 2026-07-02 Branch: worktree-cheeky-greeting-origami (off main) Status: approved-by-default (Aziz away; recommended options chosen — overridable)

Goal

Give the OpenCX companion (the in-product AI agent) the ability to render the same charts that live on the Impact Dashboard (/reports/impact-dashboard), inline in chat, with the same companion feature set it already has for the Sankey: fetch → render inline spec → deep-link to the dashboard → period-over-period comparison. Collapse the existing conversation-flow-analyst skill and the new chart capabilities into one skill named data-analyst.

Non-goals (explicit)

  • No per-segment ticket drill-down for the new charts. The Sankey keeps its own /sankey/tickets drill-down; pie/bar/metric charts do not get one.
  • No maximize/zoom, no AI-mode filter. Those are Impact-Dashboard page features (and not even merged to main); companion renders charts inline as specs and never uses them.
  • Do not touch automation-strategist / handoff-analyst skills.

Impact-Dashboard chart inventory (target set)

#ChartKindData today
1Total tickets solved by AIMetric cardget_impact_report totals ✅
2Automation percentageMetric cardget_impact_report totals ✅
3Conversation flowSankeyget_conversation_sankey ✅ (skill exists)
4Resolutions by typeDonut/Pieneeds data ❌
5Resolutions by dayStacked barneeds data ❌
6Sentiments by typeDonut/Pieneeds data ❌
7Sentiments by dayStacked barneeds data ❌
Charts 4–7 are derivable from ReportsService.getResolutionsReportForTimeZone (backend/src/reports/reports.service.ts:57), which already returns per-day resolutions and handoffSentiments arrays (resolutionsReportResSchema, backend/src/reports/dto/resolutions-report-res.dto.ts). That service is behind the dashboard-only POST /reports/resolutions (dashboardAuth) — not exposed to the public API that companion’s MCP tools call.

Architecture / how companion renders charts (context)

  • Companion skills = SKILL.md (YAML frontmatter + markdown) under backend/src/companion/skills/.claude/skills/<name>/, baked into the Daytona sandbox by daytona/build-snapshot.ts and listed in companion.prompt.ts.
  • Charts are rendered via the json-render spec protocol: the agent emits inline JSON like { "type": "Sankey", "props": { … } }; the dashboard renders it via a component catalog (dashboard/apps/dashboard/app/_lib/json-render/catalog.ts) → registry (registry.tsx) → the real React chart component.
  • The catalog is the single source of truth; it generates the backend system-prompt string backend/src/companion/ui-prompt.generated.ts via pnpm -C dashboard/apps/dashboard gen:companion-prompt (dashboard/apps/dashboard/scripts/generate-companion-ui-prompt.ts). CI gates on git diff --exit-code for that generated file.
  • Companion’s MCP tools live in dashboard/packages/mcp-server/src/tools/ and call the public API via the generated OpenAPI client.
  • Existing spec components: Sankey, Metric, Table, Card, Badge, Heading, Text, Stack, List, CoworkerArtifactCard, PhoneAgentCard. Missing: Pie/Donut and Stacked Bar.

Work items

1. Backend — extend the public impact-report (ADDITIVE only)

File: backend/src/public-api/controllers/impact-report.public.controller.ts (GET /impact-report, currently returns ImpactReportResponseSchema). Add these new optional/additional fields to the response (do not change or remove existing fields — public API is contract-stable):
  • resolutions_daily: [{ day, resolved, assumed_resolved, handed_off, in_progress }]
  • sentiments_daily: [{ day, happy, neutral, angry, in_progress }]
  • sentiment_totals: { happy, neutral, angry, in_progress }
(Resolution totals for the Pie chart #4 are already present as total_resolved / total_assumed_resolved / total_handed_off / total_in_progress.) Source the daily arrays from ReportsService.getResolutionsReportForTimeZone (UTC / timeZoneOffset: '+00:00', matching the public /sankey route). Sum the per-day sentiment array to produce sentiment_totals. Prefer adding the logic to the service method the controller already calls (ReportsService.getImpactReport) or composing both service calls in the controller — whichever is the smaller, cleaner seam. Use snake_case for new public fields to match the existing response. DTO rules: response schema keeps .meta({ id }); no any/as/!; Zod .parse at boundaries. Regenerate the OpenAPI spec + SDK (pnpm gensdk from repo root) and the mcp-server client types so the new fields are typed end-to-end.

2. mcp-server — surface the richer data

File: dashboard/packages/mcp-server/src/tools/impact-report.ts. The tool already passes data through, so the new fields flow automatically once the client types regenerate. Update the tool description to state it now also returns daily resolutions, daily sentiments, and sentiment totals, so the model knows it can build the pie/bar charts from one call.

3. Dashboard json-render — add Pie + Bar spec components

  • Add Pie (donut) and Bar (stacked) definitions to catalog.ts, mapped in the catalog component map, each with a custom rule (mirroring the Sankey rule) that tells the model to pass the tool output straight through.
  • Add their props schemas to dashboard/apps/dashboard/app/_lib/json-render/json-render-props.ts (follow sankeyPropsSchema). Pie props ≈ { data: [{ name, value }], … }; Bar props ≈ { data: [{ day, …series }], keys, … }.
  • Add registry entries in registry.tsx that render the existing reusable components ReportPieChart / ReportStackedBarChart (.../impact-dashboard/_components/common/). Reuse them; only add a thin adapter if their props don’t map directly. Do not create new chart components if the existing ones suffice (ponytail; honor “no new shared helpers”).
  • Run pnpm -C dashboard/apps/dashboard gen:companion-prompt and commit the updated backend/src/companion/ui-prompt.generated.ts.

4. Companion skill — create data-analyst, delete conversation-flow-analyst

  • New backend/src/companion/skills/.claude/skills/data-analyst/SKILL.md that:
    • Absorbs everything in conversation-flow-analyst/SKILL.md verbatim in behavior (Sankey: get_sankey_dimensionsget_conversation_sankeyget_sankey_tickets; deep-link; period compare; drill-down).
    • Adds sections for automation/resolutions and sentiments: call get_impact_report, then render Metric cards (charts 1,2), Pie (charts 4,6), and Bar (charts 5,7). Passing tool output through to props.
    • Deep-links to {dashboardBaseUrl}/reports/impact-dashboard.
    • Documents period-over-period (call the tool twice).
  • Delete backend/src/companion/skills/.claude/skills/conversation-flow-analyst/.
  • Update backend/src/companion/companion.prompt.ts Skills section: replace the conversation-flow-analyst line with data-analyst (name + trigger blurb). Leave other skill lines untouched.
  • grep -rn "conversation-flow-analyst" across the repo and update any cross-skill references / handoffs to point at data-analyst. Also fix the stale “maximize button” claim if edited text touches it.

5. Tests (real end-to-end, exhaustive — the closing step)

  • Backend (primary surface): a real e2e spec for the extended GET /impact-report. Fresh org per test (TestUtils.*), seed chat sessions across multiple days with every resolution status (resolved / assumed-resolved / handed-off / in-progress) and every sentiment (happy / neutral / angry / in-progress via handoff). Assert: existing totals unchanged (regression), resolutions_daily per-day counts, sentiments_daily per-day counts, sentiment_totals = sum of daily, date-range filtering (rows outside the window excluded), empty range → zeroed/empty arrays, and multi-tenant isolation (another org’s sessions never counted). Reference seeding: backend/src/reports/__tests__/get-resolutions-report-for-timezone.spec.ts. Follow public-api auth/scope (impact-report:read).
  • json-render: rely on pnpm type-check + the CI prompt-drift gate for the catalog/registry wiring; add a focused test only if a pure function is introduced.
  • SKILL.md is markdown — no test; verify it’s discoverable (present in the skills dir, referenced in companion.prompt.ts).

Verification / pre-commit

  • cd backend && pnpm tsgo + pnpm lint <touched files>
  • cd dashboard && pnpm type-check + pnpm lint <touched files>
  • pnpm gensdk (repo root) after the DTO change; commit generated diffs.
  • Run the new backend spec (targeted filter) and show green output.

Open decisions (defaulted; Aziz can override)

  1. Plumbing: extend get_impact_report (one endpoint/tool) — chosen over new per-dataset routes.
  2. Feature parity: all-but-drill-down for the new charts.
  3. Skill scope: merge flow-analyst + all ID charts into data-analyst, delete conversation-flow-analyst, leave the other analytics skills alone.