Imported from sramam/customer-service-agent-demo (
AGENTS.md). Install upstream withnpx skills add sramam/customer-service-agent-demo. Copyright stays with the author.
This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ before writing any code. Heed deprecation notices.
Agent architecture & design
This document describes how customer AI, employee AI, persistence, and UI fit together in this repo. For stack-specific API notes, see Project notes below.
Project notes (stack)
- AI SDK v6 uses
inputSchema(notparameters) for tool definitions. - AI SDK v6
useChatusestransport: new DefaultChatTransport({ api })(notapidirectly). - AI SDK v6
streamTextusesstopWhen: stepCountIs(n)(notmaxSteps). - Prisma 7 uses
prisma.config.tsfor datasource URL (noturlinschema.prisma). - Prisma 7 uses
prisma.config.tsforDATABASE_URL/DIRECT_URL(PostgreSQL / Neon). - Prisma 7
PrismaClientrequires a driver adapter (@prisma/adapter-neon+ pooledDATABASE_URL) or Accelerate.
High-level architecture
The app models two lanes:
- Customer AI — Public docs + account and invoice tools customers can only read. Cannot change subscriptions or billing; escalates to a human when intent is clear and a handoff is needed.
- Employee AI — Backs the human agent: public + internal docs, full account read/write tools, structured internal notes vs draft customer reply.
Persistence is PostgreSQL (Neon in dev/deploy) via Prisma: CustomerAccount, Invoice, Conversation, Message. Messages use audience (CUSTOMER_VISIBLE vs INTERNAL_ONLY) so employee↔AI chat does not appear in the customer thread.
Primary surfaces
| Route / area | Purpose |
|---|---|
src/app/page.tsx |
Split view: customer chat + agent panel |
src/app/customer/page.tsx |
Standalone customer chat (?email=) |
src/app/agent/page.tsx |
Agent dashboard: escalations list + thread + employee AI |
Customer AI (/api/chat)
- Model & system prompt:
src/lib/agents/customer.ts,CUSTOMER_SYSTEM+CUSTOMER_RESPONSE_FORMAT_INSTRUCTIONfromsrc/lib/types.ts. - Tools:
listPublicProductDocs,searchPublicDocs,getAccountInfo,listInvoices, and an inlinerequestEscalationtool defined insrc/app/api/chat/route.ts(usesescalation-handoff.ts). Keep schema changes inescalation-handoff.ts+ the route tool. - Streaming:
streamTextwithstopWhen: stepCountIs(8)to allow multi-step gather + tools. - Persistence: Creates conversation if needed; stores last user message;
onFinishpersists assistant text; escalation tool + backup scan inonFinishsetESCALATEDand system line.
Design choices (customer)
- Gather first, then escalate: The model is instructed to clarify what the customer wants and which products (from
getAccountInfo) before callingrequestEscalation. Deeper discovery (retention, contract detail) is deferred to post-escalation human/employee flow. - Structured handoff:
requestEscalationusessrc/lib/agents/tools/escalation-handoff.ts—changeSummary,productsInvolved[], optionalcontextForAgent.buildEscalationReason()flattens that intoescalationReason+ customer-visible system message. - Suggested chips (
suggestedQuestions): Treated as the customer’s next message when tapped. Must be customer voice (e.g. product names, “Upgrade”, “Downgrade”) — not agent questions (“Are you looking to…?”). For product pickers, chips are name-only; prose should not duplicate the same list (avoid triple redundancy: bullets + bullets + chips). - Response shape: A single JSON object with
text(GFM markdown),sources, andsuggestedQuestions. Parsed byparseCustomerResponseinsrc/lib/parse-response.tsfor display and chips. Server validation (validate-ai-response.ts) accepts JSON only.
Employee AI (/api/agent-chat)
- Model & system prompt:
src/lib/agents/employee.ts+EMPLOYEE_RESPONSE_FORMAT_INSTRUCTIONinsrc/lib/types.ts. - Tools:
listPublicProductDocs,searchPublicDocs,searchInternalDocs,getAccountInfo,listInvoices,updateAccount,createCreditMemo. - Context injection: Route loads conversation + customer account; appends customer profile, escalation reason, and customer-visible message history to the system prompt. Employee
useChatmessages are persisted asrole: employee,audience: INTERNAL_ONLY. - Streaming:
stopWhen: stepCountIs(8);onFinishstores full assistant text asemployee-ai/ internal-only for audit. - Client: When the customer posts a new message on the open thread,
useEmployeeAiRefreshOnCustomerMessagestops the Employee AI stream, clears the copilot, and re-sends the suggest-draft kickoff so internal notes/draft track the latest thread (/agentand split-view agent column).
Design choices (employee)
- Strict separation: JSON fields
internalNotes(agent-only markdown) vsdraftCustomerResponse(customer-safe markdown for the draft box).sourceslists docs/invoices; internal-doc rows inform internal notes; public/account/invoice rows support the draft. - Unstructured replies: If the model returns non-JSON text,
parseEmployeeResponsetreats the entire body as internal notes and leaves draft empty — so conversational answers to the employee never populate the customer draft by accident. - Prompt emphasis: Internal notes are incremental only — new tool/doc findings and deltas; avoid repeating escalation reason or thread already shown in the UI. Draft should be the best next customer message, ready to send.
Human-approved reply to customer
- POST
/api/conversations/[id]/approvewith{ content }— createsMessagewithrole: assistant,audience: CUSTOMER_VISIBLE. - UI:
ReviewControlsshows internal notes (markdown-rendered) + editable draft + Send. Internal notes useMarkdownContent; draft is a textarea (markdown sent as-is).
Escalation & status
- Conversation
statustransitions includeWITH_CUSTOMER_AI→ESCALATED(and optionally resolved elsewhere). - Customer-visible system row:
Escalated to human agent: …with built reason string. - Client: Escalation reason for banners is extracted from tool parts (
tool-requestEscalation) viasrc/lib/escalation-ui.ts, with prose fallback helper where needed.
Multi-conversation, realtime, and employee AI context
- Only customer flows create new
Conversationrows (ensureCustomerConversationinPOST /api/chat). Agents open existing threads only. - Customers can list their threads:
GET /api/conversations/customer?email=…, and load customer-visible messages withGET /api/conversations/[id]/customer-messages(burger menu on customer surfaces). - PartyKit after a customer-visible insert:
notifyPartyConversationMessagesends a minimal{ type: customer_thread_updated, conversationId, messageId }signal; the customer UI pulls from the GET route to stay consistent and avoid shipping full text on the wire. - Same-tab split demo: when the customer finishes sending a message on an escalated thread, the app dispatches
f5-conversation-messages-updatedso the agent pane refetches immediately (PartyKit/polling can lag). Full/agentpage still relies on PartyKit + polling when the customer is in another tab. - Employee AI (
POST /api/agent-chat):streamTextmessagesare assembled from the database viabuildEmployeeModelMessages(customer-visible + internal rows in order); the system prompt carries profile and escalation summary, not a second copy of the whole thread. - Agent dashboard / split agent column: escalations are chosen from a burger menu (
AgentEscalationsBurger) — no sidebar rail; the menu lists all escalated threads from the same data asGET /api/conversations.
End-to-end flow (diagram)
flowchart TD
subgraph customer_lane [Customer lane]
U[User] --> CH[Customer chat UI]
CH --> API_CHAT["POST /api/chat"]
API_CHAT --> CAI[Customer AI + tools]
CAI --> G{Intent and products clear?}
G -->|No| CAI
G -->|Yes| T["requestEscalation(changeSummary, products, context)"]
T --> DB1[(Prisma: status ESCALATED, escalationReason, system message)]
CAI --> DB2[(User + assistant messages)]
end
subgraph agent_lane [Agent lane]
DB1 --> AD[Agent dashboard]
AD --> API_AGENT["POST /api/agent-chat"]
API_AGENT --> EAI[Employee AI + tools]
EAI --> RC[ReviewControls: internal notes + draft]
RC --> APPROVE["POST .../approve { content }"]
APPROVE --> DB3[(Message: assistant, CUSTOMER_VISIBLE)]
DB3 --> CH2[Customer thread shows human reply]
end
sequenceDiagram
participant U as Customer
participant Chat as /api/chat
participant CAI as Customer AI
participant DB as Database
participant Ag as Human agent
participant Emp as /api/agent-chat
participant EAI as Employee AI
U->>Chat: user message
Chat->>CAI: streamText + tools
CAI->>DB: persist messages (onFinish)
CAI->>Chat: requestEscalation
Chat->>DB: ESCALATED + system row
Ag->>Emp: employee message (INTERNAL_ONLY)
Emp->>EAI: streamText + account context
EAI->>DB: employee-ai transcript (internal)
EAI->>Ag: structured notes + draft (parsed in UI)
Ag->>DB: approve → assistant message to customer
U->>Chat: sees approved reply in thread
API map (App Router)
| Endpoint | Role |
|---|---|
POST /api/chat |
Customer AI stream; sets X-Conversation-Id |
POST /api/agent-chat |
Employee AI stream (requires conversationId) |
GET /api/conversations |
List escalations + enriched customer snapshot |
POST /api/conversations/[id]/approve |
Post human-approved customer message |
GET /api/invoices/download?key=… |
Invoice PDF proxy |
GET /api/docs |
Doc listing you can only read / file content (scope, optional file) for dev/inspector UIs |
UI layout (agent workspace)
- Single scroll column: Customer thread (including escalation banner) and the employee workspace (Employee AI transcript, input,
ReviewControls) live in oneoverflow-y-autoregion so nothing is trapped in a 50%-height pane with independent scroll. - Thread labels:
getAssistantThreadKindinagent-message-body.tsxdistinguishes customer AI vs human agent messages after escalation for correct bubbles.
Key files (quick reference)
| Area | Files |
|---|---|
| Customer agent | src/lib/agents/customer.ts, src/lib/types.ts (customer format) |
| Employee agent | src/lib/agents/employee.ts, src/lib/types.ts (employee format) |
| Escalation schema | src/lib/agents/tools/escalation-handoff.ts |
| Public doc catalog | src/lib/public-doc-catalog.ts (product focus for searchPublicDocs) |
| Chat API | src/app/api/chat/route.ts |
| Agent chat API | src/app/api/agent-chat/route.ts |
| Parsing | src/lib/parse-response.ts |
| Escalation UI helpers | src/lib/escalation-ui.ts |
| Agent UI | src/components/agent-dashboard/review-controls.tsx, agent-message-body.tsx |
Conventions for future changes
- Keep customer and employee format instructions in
src/lib/types.tsaligned with prompts; avoid duplicating conflicting rules in multiple markdown files unless necessary. - Any new tool used in
/api/chatshould be reflected inCUSTOMER_SYSTEMand tested for streaming + persistence. - Employee-facing markdown in ReviewControls should remain GitHub-flavored and safe for
react-markdown+remark-gfm(src/components/markdown-content.tsx).