Skip to main content

Normal Conversations

The main chat experience. User opens the chat panel and asks a question; Mia returns streamed text + interactive widgets + follow-up suggestions.

Where it lives

  • Frontend: src/routes/PivotAiAgentDashboard/
  • Backend entrypoint: functions/pivotAiAgent/index.ts (chatHandler, Express)
  • Intent: functions/pivotAiAgent/intent.ts
  • Chat path (fast): functions/pivotAiAgent/chat.ts
  • Data path (full pipeline): functions/pivotAiAgent/pipeline.ts

Request shape

POST /api/chat
{
messages: [...], // conversation history
companyId: string,
firebaseToken: string, // Bearer token (in body, since SSE EventSource can't send headers)
pageContext?: { // optional — current frontend route context
label?: string, // e.g., "Schedule", "Employees"
path?: string, // route path
selectedEmployeeName?: string,
selectedEmployeeId?: string,
},
sessionId: string,
forceData?: boolean, // skip intent routing, always run data path
personaName?: 'Mia',
uploadId?: string, // reference a pre-uploaded file (CSV / image / sheet)
userId?: string,
routedCategories?: string[],
}

When pageContext.label is set (and isn't the default "Pivot"), the planner system prompt gets a CURRENT PAGE line telling Mia to give context-aware answers (e.g. "show me this week's schedule" while on /schedule means the schedule the user is viewing). Source: functions/pivotAiAgent/pipeline.ts.

Two-path pipeline

Intent classification (always first)

Haiku (~200ms) classifies the message as chat or data. Keywords like late, absent, shift, hours, export, employee shove it toward data.

Chat path

If chat: a single Haiku call streams the answer character-by-character. No tools, no API reads. Total latency ~200-500ms.

Data path

If data (or forceData: true):

  1. Sonnet tool loop — Sonnet receives the full message history + all 51 main-chat tool definitions (see catalog). It emits tool_use blocks; the pipeline calls executeTool(name, input, ...) for each. Most tools HTTP-call internal Firebase Function endpoints with the caller's Bearer token (so they inherit the user's permissions). Tool results are appended to the conversation; Sonnet may emit more tool_use blocks on the next turn until it produces a final message.

  2. Build name map — replace Firebase IDs with human names so the final answer is readable.

  3. Parallel fan-out (Haiku × 3) — three independent Haiku calls fired in parallel; the answer streams to the UI first while the other two complete in the background:

    • Answer (512 tokens). 80-word summary, bold key numbers. System prompt: "respond as a manager would brief another manager." Streams character-by-character so the user sees text within ~1 s.

    • Widget config (4096 tokens). JSON describing one or more charts/cards rendered by the AI Dashboard. The system prompt (see buildWidgetSystem in pipeline.ts) lists the 30 pre-built components and their required props; the model picks the right type and bakes the data inline. Has an absolute rule: never invent rows that aren't in the collected data — empty dashboards are better than fake dashboards.

    • Suggestions (256 tokens). Exactly 3 follow-up questions, max 8 words each. System prompt (verbatim from pipeline.ts):

      "You generate short follow-up question suggestions for a restaurant manager using an AI dashboard. Output ONLY a JSON array of exactly 3 short questions (max 8 words each). No prose, no explanation. Example: [\"Show labor cost by employee\",\"Compare this week vs last week\",\"Which day had highest sales?\"]"

      The model gets a tiny context: company name, the user's question verbatim, and the keys of the collected data (not the values — just the names of the things that were fetched). That's enough for it to propose plausible drill-downs.

  4. Save session — write to AIZackboardSessions/{cid}/{uid}/{sessionId}.

Total latency: 5-30s depending on tool-call depth.

SSE response

The same SSE stream carries text deltas, widget JSON, and suggestions:

data: {"type":"status","text":"Analyzing data…"}

data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"L"}}

data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"a"}}

...

data: {"type":"widget_start"}

data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"{\"widgets\":[..."}}

data: {"type":"widget_complete"}

data: {"type":"suggestions","suggestions":["Compare to last week","Show by role","Export to PDF"]}

data: {"type":"done"}

The frontend re-hydrates partial JSON for widgets as bytes arrive, so the first chart can render before the full payload completes.

Session resume

Sessions live at AIZackboardSessions/{companyId}/{userId}/{sessionId} in RTDB. The frontend persists the session ID locally; on next load it re-fetches the session and replays the message history. Widgets and suggestions are stored alongside, so the resumed view matches what the user last saw.

Title for the session is generated by a separate Haiku call (≤ 8 words) so the session list shows readable labels.

Observability

Every chat call signals a pipelineTrackingWorkflow in Temporal — each pipeline phase becomes a named activity in the Temporal Web UI timeline. See Temporal: tracking workflow.

Why tool calls inherit user permissions

When Sonnet decides to call get_employees, the pipeline's executeTool('get_employees', input, token, ...) ends up issuing an HTTP request to the internal /employees/employees endpoint with Authorization: Bearer <user token>. That endpoint's companyScopeMiddleware rejects the request if the user can't access that company. So Mia cannot read or write data the caller doesn't have permission for — the AI agent inherits user-level access automatically. See Security Model.

Rules hierarchy

The system prompt layers four tiers in strict priority order — Pivot Absolute Rules → User Rules → Company Memory → Core operational rules — so manager-set rules can shape tone and preferences but never override safety constraints. Full details in Security Model: rules hierarchy.