AI Chat Integration
Technical details of the Vercel AI Gateway integration, AI tool schemas, and dashboard AI chat
This page provides technical details for the AI Assistant integration, including model registry configuration, prompt system architecture, telemetry tracking, and message persistence.
Model Configuration
The AI model configuration is centralized in packages/ai/lib/models.ts. General language calls use Vercel AI Gateway; general selects DeepSeek V4.1 Flash and fast selects GLM 5.3 Flash. The Luna curator uses GPT-6 Luna. Additional registry slots and image/speech factories do not demonstrate an active product feature. Embeddings (voyageai/voyage-4) and reranking (voyageai/rerank-3) use the separate OpenRouter boundary (OPENROUTER_API_KEY). The maintained workload routing reference distinguishes agents, workflows, bounded generators, retrieval, external voice inference and forecasting.
import "server-only";
import { createGateway } from "@ai-sdk/gateway";
// Abridged: getGateway() wraps createGateway() for lazy key resolution
// (packages/ai/lib/models.ts holds the full registry).
export const MODEL_REGISTRY: Record<string, ModelDefinition> = {
general: {
key: "general",
modelId: "deepseek/deepseek-v4.1-flash",
displayName: "DeepSeek V4.1 Flash",
provider: "DeepSeek",
maxOutputTokens: 4096,
supportsTools: true,
},
};Environment variable required:
AI_GATEWAY_API_KEY— API key for Vercel AI Gateway access; the shared factory resolves it lazily and rejects a missing key before constructing the provider.
AI Route Handler
The assistant route handler lives at apps/app/src/app/api/assistant/chat/route.ts and streams through the Vercel AI SDK message pipeline via @repo/ai; canonical execution runs through the Mastra runtime (apps/api/lib/assistant/mastra.ts), enabled by default. Agent instructions live with the runtime: tier and knowledge rules in apps/api/lib/assistant/mastra.ts, and per-mode and draft-purpose guidance in apps/api/lib/assistant/mode-instructions.ts, selected from the conversation's persisted mode and the forwarded draft purpose.
In Actions mode, managers also get three drafting tools from apps/api/lib/assistant/operational-draft-tools.ts:
draftShiftHandoffbuilds a handoff from the focused location's logged incidents and shift task records.draftGuestRecoverytriages a guest concern and drafts a reply with contact details redacted and no unauthorized compensation.draftTrainingMaterialdrafts a lesson or quiz from verified company SOPs only.
Each tool takes identity, location, and evidence from the server, and Ask Danvas renders the result with the matching artifact card for review. None of them sends or assigns anything.
System Prompt Architecture
The system prompt injects:
- User Identity & Role: Role (
admin/manager/member),teamId, and effective location scope. An admin with no explicit location assignments has team-wide location scope; other callers receive their authorized location IDs. - Assistant Mode:
general: Operational, SOP, and policy navigation.actions: Draft-first assistance for announcements, handover notes, and incident summaries.data: Metric definitions and explanation of data contracts.
- UI Context: Current page surface (
surface: "ask"or specific dashboard view) and activelocationSlug/locationId.
Message Persistence Schema
Dashboard chat threads, messages, and telemetry are stored in PostgreSQL (packages/database/src/schema/chat.ts):
export const chatThreads = pgTable("chat_threads", {
id: text("id").primaryKey(),
teamId: text("team_id").notNull(),
userId: text("user_id").notNull(),
title: text("title"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const chatMessages = pgTable("chat_messages", {
id: text("id").primaryKey(),
threadId: text("thread_id").notNull().references(() => chatThreads.id),
role: text("role", { enum: ["user", "assistant", "system"] }).notNull(),
parts: jsonb("parts").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
export const chatGenerations = pgTable("chat_generations", {
id: text("id").primaryKey(),
messageId: text("message_id").references(() => chatMessages.id),
modelId: text("model_id").notNull(),
promptTokens: integer("prompt_tokens"),
completionTokens: integer("completion_tokens"),
estimatedCost: numeric("estimated_cost", { precision: 10, scale: 6 }),
createdAt: timestamp("created_at").notNull().defaultNow(),
});