11 — AI-Powered Features
Chapter 11 — AI-Powered Features
Section titled “Chapter 11 — AI-Powered Features”Add a streaming AI chat to the TaskBoard that can read tasks and answer questions about them. Users type a question, the AI streams the response word-by-word, and it can look up real task data using tool-calling.
What we’ll build
Section titled “What we’ll build”- A
/chatpage with a streaming chat interface - An AI provider registered via DI
- A tRPC
chatmutation that streams (returns full response for now) - An MCP-style tool so the AI can query actual tasks
- Token usage tracked per org
Register the AI module
Section titled “Register the AI module”Add AIContainerModule to your app:
import { createCruzApp } from '@cruzjs/core';import { AIContainerModule } from '@cruzjs/ai';import { TasksModule } from './modules/tasks';
export default createCruzApp({ modules: [TasksModule, /* ... */], containerModules: [AIContainerModule],});Set your API key in .dev.vars:
CRUZJS_AI_PROVIDER=openaiOPENAI_API_KEY=sk-...Register a provider at startup
Section titled “Register a provider at startup”import { OpenAIProvider, AI_PROVIDER_REGISTRY } from '@cruzjs/ai';
export async function onStartup(container: CruzContainer) { const registry = container.get(AI_PROVIDER_REGISTRY); registry.register(new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY!, defaultModel: 'gpt-4o-mini', }));}Add a chat tRPC procedure
Section titled “Add a chat tRPC procedure”Create apps/web/src/modules/tasks/tasks-ai.trpc.ts:
import { injectable, inject } from 'inversify';import { Router, Route, TrpcRouter } from '@cruzjs/core';import { orgProcedure } from '@cruzjs/core/trpc/context';import { AIProviderRegistry, AI_PROVIDER_REGISTRY, McpBridge } from '@cruzjs/ai';import { z } from 'zod';import { TasksService } from './tasks.service';
@injectable()@Router()export class TasksAITrpc implements TrpcRouter { constructor( @inject(AI_PROVIDER_REGISTRY) private registry: AIProviderRegistry, @inject(TasksService) private tasksService: TasksService, ) {}
@Route() chat = orgProcedure .input(z.object({ messages: z.array(z.object({ role: z.enum(['user', 'assistant']), content: z.string() })), })) .mutation(async ({ input, ctx }) => { const ai = this.registry.resolve(); const orgId = ctx.org.orgId;
const result = await McpBridge.runWithTools(ai, [ { role: 'system', content: 'You are a helpful project management assistant. Use the list_tasks tool to look up task data when asked.', }, ...input.messages, ], { tools: [{ name: 'list_tasks', description: 'List tasks for the current organization', parameters: { status: { type: 'string', enum: ['pending', 'in_progress', 'done'], description: 'Filter by status' }, }, }], executor: async (toolCall) => { if (toolCall.name === 'list_tasks') { const tasks = await this.tasksService.list(orgId, { status: toolCall.arguments.status as string | undefined, }); return JSON.stringify(tasks.map(t => ({ id: t.id, title: t.title, status: t.status }))); } return '[]'; }, maxRounds: 3, });
return { content: result.content }; });}Register it in TasksModule:
import { TasksAITrpc } from './tasks-ai.trpc';
@Module({ providers: [TasksService, TasksAITrpc], trpcRouters: [{ name: 'tasks', router: TasksAITrpc }],})export class TasksModule {}Build the chat UI
Section titled “Build the chat UI”Create apps/web/src/routes/chat.tsx:
import { useState } from 'react';import { getTRPC } from '@cruzjs/core/trpc/client';
type Message = { role: 'user' | 'assistant'; content: string };
export default function ChatPage() { const trpc = getTRPC(); const chatMutation = trpc.tasks.chat.useMutation(); const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState('');
const send = async () => { if (!input.trim()) return; const userMessage: Message = { role: 'user', content: input }; setInput(''); setMessages(prev => [...prev, userMessage]);
const result = await chatMutation.mutateAsync({ messages: [...messages, userMessage] }); setMessages(prev => [...prev, { role: 'assistant', content: result.content }]); };
return ( <div className="max-w-2xl mx-auto p-6 space-y-4"> <h1 className="text-2xl font-semibold">Task Assistant</h1>
<div className="border rounded-lg p-4 h-96 overflow-y-auto space-y-3"> {messages.length === 0 && ( <p className="text-text-muted text-sm">Ask about your tasks. Try: "What tasks are in progress?"</p> )} {messages.map((m, i) => ( <div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}> <div className={`max-w-xs rounded-lg px-4 py-2 text-sm ${ m.role === 'user' ? 'bg-primary text-white' : 'bg-surface border border-surface-border' }`}> {m.content} </div> </div> ))} {chatMutation.isPending && ( <div className="flex justify-start"> <div className="bg-surface border border-surface-border rounded-lg px-4 py-2 text-sm text-text-muted"> Thinking… </div> </div> )} </div>
<div className="flex gap-2"> <input value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && !e.shiftKey && send()} placeholder="Ask about your tasks…" className="flex-1 rounded-lg border border-surface-border bg-surface px-4 py-2 text-sm outline-none focus:border-primary" /> <button onClick={send} disabled={chatMutation.isPending || !input.trim()} className="rounded-lg bg-primary px-4 py-2 text-sm text-white disabled:opacity-60" > Send </button> </div> </div> );}Track token usage
Section titled “Track token usage”After each AI call, record usage:
import { AIUsageTracker, AI_USAGE_TRACKER } from '@cruzjs/ai';
// In tasks-ai.trpc.ts, inject the tracker:@inject(AI_USAGE_TRACKER) private tracker: AIUsageTracker,
// After McpBridge.runWithTools():this.tracker.record({ orgId, provider: 'openai', model: 'gpt-4o-mini', inputTokens: 0, // add when provider returns token counts outputTokens: 0, durationMs: Date.now() - start, timestamp: new Date(),});Try it
Section titled “Try it”cruz devVisit http://localhost:5173/chat. Ask:
- “What tasks are pending?”
- “How many tasks are in progress?”
- “Summarize all done tasks”
The AI will call list_tasks automatically, read the real data, and reply in natural language.
What we built
Section titled “What we built”- Registered
@cruzjs/aiwith OpenAI provider - Created a tRPC mutation that runs the AI with tool-calling
- AI can query real task data via
McpBridge - Chat UI with message history
- Token usage tracked per org
Explore more @cruzjs/ai features in the AI Integration docs:
useStreamReact hook for token-by-token streaminguseChatfor multi-turn conversations with history- Org-scoped API keys per tenant
- Anthropic, OpenRouter, Cloudflare Gateway providers