Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions chartsmith-app/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,81 @@ This is a next.js project that is the front end for chartsmith.
- We aren't using Next.JS API routes, except when absolutely necessary.
- Front end should call server actions, which call lib/* functions.
- Database queries are not allowed in the server action. Server actions are just wrappers for which lib functions we expose.

## AI SDK Integration

We use the [Vercel AI SDK](https://ai-sdk.dev/docs) for LLM integration with a hybrid architecture:

### Architecture Overview

```
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
├──────────────────────────┬──────────────────────────────────┤
│ AIChatContainer │ ChatContainer │
│ (AI SDK useChat) │ (Centrifugo for plans/renders) │
└──────────────────────────┴──────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────┐
│ /api/chat │ │ Go Worker │
│ (AI SDK streamText) │ │ (Anthropic/Groq SDKs) │
└──────────────────────────┘ └──────────────────────────────┘
```

### Streaming Chat (AI SDK) - Conversational Q&A
- **API Route**: `/api/chat` uses AI SDK Core's `streamText` for real-time streaming
- **Frontend**: `AIChatContainer` component uses `useChat` hook from `@ai-sdk/react`
- **Provider**: Configurable via `LLM_PROVIDER` env var (anthropic, openai)
- **Tools**: `getLatestSubchartVersion`, `getLatestKubernetesVersion`
- **Auto-response**: Automatically responds to unanswered user messages (e.g., from workspace creation)

### Complex Workflows (Go + Centrifugo) - Plans, Renders, Conversions
- **Plans, Renders, Conversions**: Handled by Go backend worker
- **Realtime updates**: Streamed via Centrifugo pub/sub
- **Intent detection**: Uses Groq in Go backend for speed
- **File modifications**: Go worker handles all chart file changes

### Key Files
| File | Description |
|------|-------------|
| `lib/llm/provider.ts` | LLM provider configuration and model selection |
| `lib/llm/system-prompts.ts` | System prompts (matches Go `pkg/llm/system.go`) |
| `lib/llm/message-adapter.ts` | Converts between DB messages and AI SDK format |
| `app/api/chat/route.ts` | Streaming API endpoint with tool support |
| `components/AIChatContainer.tsx` | AI SDK chat UI with streaming |
| `hooks/useChartsmithChat.ts` | Custom hook wrapping `useChat` |
| `lib/workspace/actions/save-ai-chat-message.ts` | Persists AI chat messages to DB |

### Feature Flag
Set `USE_AI_SDK_CHAT=true` in `.env.local` to enable the new AI SDK chat in workspaces.
When disabled (default), workspaces use the existing Centrifugo-based `ChatContainer`.

### Provider Switching
```bash
# Anthropic (default)
LLM_PROVIDER=anthropic
LLM_MODEL=claude-3-5-sonnet-20241022

# OpenAI
LLM_PROVIDER=openai
LLM_MODEL=gpt-4o
OPENAI_API_KEY=sk-...
```

### Message Flow
1. User sends message via `AIChatContainer` input
2. `useChat` hook calls `/api/chat` with message history
3. API route builds system prompt based on role (auto/developer/operator)
4. `streamText` streams response from LLM
5. On completion, message is saved to DB via `saveAIChatMessageAction`
6. Tool calls (e.g., chart version lookup) are executed inline

### Testing
```bash
npm run test:unit # Jest unit tests (message-adapter, etc.)
npm run test:e2e # Playwright API tests
npm run build # TypeScript compilation check
```

See `docs/AI_SDK_MIGRATION.md` for full migration details.
167 changes: 167 additions & 0 deletions chartsmith-app/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* AI Chat Streaming API Route
*
* This route uses the Vercel AI SDK to stream chat responses from the LLM.
* It handles conversational messages for the Chartsmith chat interface.
*
* For plan execution, renders, and other complex workflows,
* the Go backend continues to handle those via Centrifugo.
*/

import { streamText, convertToCoreMessages, CoreMessage } from 'ai';
import type { Message as UIMessage } from '@ai-sdk/react';
import { z } from 'zod';
import { NextRequest } from 'next/server';
import { cookies } from 'next/headers';

import { getModel } from '@/lib/llm/provider';
import { buildSystemPrompt, ChatRole, ChartContext } from '@/lib/llm/system-prompts';
import { findSession } from '@/lib/auth/session';
import { searchArtifactHubCharts } from '@/lib/artifacthub/artifacthub';

// Allow streaming responses up to 60 seconds
export const maxDuration = 60;

/**
* Request body schema for the chat endpoint.
*/
interface ChatRequestBody {
messages: UIMessage[];
workspaceId?: string;
role?: ChatRole;
chartContext?: ChartContext;
}

/**
* Validates the user session from cookies.
* Returns the session if valid, null otherwise.
*/
async function getAuthenticatedSession() {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get('session');

if (!sessionCookie?.value) {
return null;
}

try {
const session = await findSession(sessionCookie.value);
return session;
} catch {
return null;
}
}

/**
* Searches for the latest version of a subchart from ArtifactHub.
*/
async function getLatestSubchartVersion(chartName: string): Promise<string> {
try {
const results = await searchArtifactHubCharts(chartName);
if (results.length === 0) {
return 'Not found on ArtifactHub';
}

// Extract version from URL or return the first result
// The searchArtifactHubCharts returns URLs like https://artifacthub.io/packages/helm/org/name
// We'd need to fetch the actual version from the package details
// For now, return that we found it (the full version lookup would require additional API call)
return `Found: ${results[0]}`;
} catch (error) {
console.error('Error searching ArtifactHub:', error);
return 'Error searching ArtifactHub';
}
}

/**
* POST handler for chat streaming.
*/
export async function POST(req: NextRequest) {
try {
// Authenticate the user
const session = await getAuthenticatedSession();

if (!session) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}

// Parse request body
const body: ChatRequestBody = await req.json();
const { messages, role = 'auto', chartContext } = body;

if (!messages || !Array.isArray(messages)) {
return new Response(JSON.stringify({ error: 'Messages array is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}

// Build the system prompt based on role and context
const systemPrompt = buildSystemPrompt(role, chartContext);

// Convert UI messages to core format for the model
const coreMessages: CoreMessage[] = convertToCoreMessages(messages);

// Stream the response using AI SDK
const result = streamText({
model: getModel(),
system: systemPrompt,
messages: coreMessages,
maxTokens: 8192,
tools: {
/**
* Tool to get the latest version of a subchart from ArtifactHub.
* The LLM can use this when users ask about specific chart versions.
*/
getLatestSubchartVersion: {
description: 'Get the latest version of a Helm subchart from ArtifactHub. Use this when the user asks about chart versions or dependencies.',
parameters: z.object({
chartName: z.string().describe('The name of the subchart to look up (e.g., "redis", "postgresql", "ingress-nginx")'),
}),
execute: async ({ chartName }) => {
return await getLatestSubchartVersion(chartName);
},
},

/**
* Tool to get the latest Kubernetes version information.
*/
getLatestKubernetesVersion: {
description: 'Get the latest version of Kubernetes. Use this when the user asks about Kubernetes version compatibility.',
parameters: z.object({
semverField: z.enum(['major', 'minor', 'patch']).describe('Which part of the version to return'),
}),
execute: async ({ semverField }) => {
// Current latest Kubernetes versions (as of early 2025)
const versions: Record<string, string> = {
major: '1',
minor: '1.32',
patch: '1.32.1',
};
return versions[semverField];
},
},
},
});

// Return the streaming response
return result.toDataStreamResponse();
} catch (error) {
console.error('Chat API error:', error);

return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}

4 changes: 4 additions & 0 deletions chartsmith-app/app/workspace/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export default async function WorkspacePage({
listWorkspaceConversionsAction(session, workspace.id)
])

// Check if AI SDK chat is enabled via environment variable
const useAISDKChat = process.env.USE_AI_SDK_CHAT === 'true';

// Pass the initial data as props
return (
<WorkspaceContent
Expand All @@ -47,6 +50,7 @@ export default async function WorkspacePage({
initialPlans={plans}
initialRenders={renders}
initialConversions={conversions}
useAISDKChat={useAISDKChat}
/>
);
}
Loading