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/ticketsdrill-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-analystskills.
Impact-Dashboard chart inventory (target set)
| # | Chart | Kind | Data today |
|---|---|---|---|
| 1 | Total tickets solved by AI | Metric card | get_impact_report totals ✅ |
| 2 | Automation percentage | Metric card | get_impact_report totals ✅ |
| 3 | Conversation flow | Sankey | get_conversation_sankey ✅ (skill exists) |
| 4 | Resolutions by type | Donut/Pie | needs data ❌ |
| 5 | Resolutions by day | Stacked bar | needs data ❌ |
| 6 | Sentiments by type | Donut/Pie | needs data ❌ |
| 7 | Sentiments by day | Stacked bar | needs data ❌ |
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) underbackend/src/companion/skills/.claude/skills/<name>/, baked into the Daytona sandbox bydaytona/build-snapshot.tsand listed incompanion.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.tsviapnpm -C dashboard/apps/dashboard gen:companion-prompt(dashboard/apps/dashboard/scripts/generate-companion-ui-prompt.ts). CI gates ongit diff --exit-codefor 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 }
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) andBar(stacked) definitions tocatalog.ts, mapped in the catalog component map, each with a custom rule (mirroring theSankeyrule) 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(followsankeyPropsSchema). Pie props ≈{ data: [{ name, value }], … }; Bar props ≈{ data: [{ day, …series }], keys, … }. - Add registry entries in
registry.tsxthat render the existing reusable componentsReportPieChart/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-promptand commit the updatedbackend/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.mdthat:- Absorbs everything in
conversation-flow-analyst/SKILL.mdverbatim in behavior (Sankey:get_sankey_dimensions→get_conversation_sankey→get_sankey_tickets; deep-link; period compare; drill-down). - Adds sections for automation/resolutions and sentiments: call
get_impact_report, then renderMetriccards (charts 1,2),Pie(charts 4,6), andBar(charts 5,7). Passing tool output through to props. - Deep-links to
{dashboardBaseUrl}/reports/impact-dashboard. - Documents period-over-period (call the tool twice).
- Absorbs everything in
- Delete
backend/src/companion/skills/.claude/skills/conversation-flow-analyst/. - Update
backend/src/companion/companion.prompt.tsSkills section: replace theconversation-flow-analystline withdata-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 atdata-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_dailyper-day counts,sentiments_dailyper-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.mdis markdown — no test; verify it’s discoverable (present in the skills dir, referenced incompanion.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)
- Plumbing: extend
get_impact_report(one endpoint/tool) — chosen over new per-dataset routes. - Feature parity: all-but-drill-down for the new charts.
- Skill scope: merge flow-analyst + all ID charts into
data-analyst, deleteconversation-flow-analyst, leave the other analytics skills alone.