-
Notifications
You must be signed in to change notification settings - Fork 15
Migrate to vercel ai sdk #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Davaakhatan
wants to merge
17
commits into
replicatedhq:main
Choose a base branch
from
Davaakhatan:migrate-to-vercel-ai-sdk
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
b7f0470
Migrate frontend from @anthropic-ai/sdk to Vercel AI SDK
Davaakhatan 73e4db0
Add MIGRATION.md documenting Vercel AI SDK migration progress
Davaakhatan 4b606e3
Implement Vercel AI SDK migration for conversational chat
Davaakhatan ba867d1
Implement actual subchart version lookup using Artifact Hub and GitHu…
Davaakhatan 406a7e2
Fix TypeScript build error: convert ReadableStream to Node.js stream
Davaakhatan b376335
Migrate conversational chat to Vercel AI SDK
Davaakhatan 1b2c9c2
Fix critical bugs in conversational chat API
Davaakhatan b47336d
Add defensive logging for dropped streaming chunks
Davaakhatan 9a5c832
Fix 5 critical bugs in Vercel AI SDK migration
Davaakhatan c697bc4
Fix 2 additional critical bugs in streaming implementation
Davaakhatan a7e71c0
Merge branch 'main' into migrate-to-vercel-ai-sdk
Davaakhatan 9cadebd
Fix 3 critical security and reliability bugs
Davaakhatan b1561b7
Fix 3 critical bugs in streaming and context handling
Davaakhatan 4f320de
Add Node 20 engine requirement for Vercel AI SDK
Davaakhatan fdad4ea
Fix worker timeout and remove subchart placeholder
Davaakhatan 1d35b88
Fix Chart.yaml/values.yaml guaranteed inclusion
Davaakhatan 7bcf0af
Fix chat message error handling and duplication bugs
Davaakhatan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| /** | ||
| * Vercel AI SDK Chat API Route | ||
| * | ||
| * This demonstrates how to migrate the conversational chat from Go to Next.js using Vercel AI SDK. | ||
| * This is a complete implementation showing all key features from pkg/llm/conversational.go | ||
| * | ||
| * Key features demonstrated: | ||
| * - Vercel AI SDK streamText() for streaming responses | ||
| * - Tool calling (latest_subchart_version, latest_kubernetes_version) | ||
| * - System prompts preservation | ||
| * - Context injection (chart structure, relevant files) | ||
| * - Centrifugo real-time publishing | ||
| * - Database integration for message persistence | ||
| */ | ||
|
|
||
| import { streamText, tool } from 'ai'; | ||
| import { createAnthropic } from '@ai-sdk/anthropic'; | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { z } from 'zod'; | ||
| import { getChatMessage, getWorkspace } from '@/lib/workspace/workspace'; | ||
| import { appendChatMessageResponse, clearChatMessageResponse, markChatMessageComplete, getWorkspaceIdForChatMessage } from '@/lib/workspace/chat-helpers'; | ||
| import { publishChatMessageUpdate } from '@/lib/realtime/centrifugo-publish'; | ||
| import { getChartStructure, chooseRelevantFiles, getPreviousChatHistory } from '@/lib/workspace/context'; | ||
| import { getLatestSubchartVersion } from '@/lib/recommendations/subchart'; | ||
|
|
||
| export const runtime = 'nodejs'; | ||
| export const maxDuration = 300; // 5 minutes | ||
|
|
||
| // System prompts from pkg/llm/system.go | ||
| const CHAT_SYSTEM_PROMPT = `You are ChartSmith, an expert AI assistant and a highly skilled senior software developer specializing in the creation, improvement, and maintenance of Helm charts. | ||
| Your primary responsibility is to help users transform, refine, and optimize Helm charts based on a variety of inputs, including: | ||
|
|
||
| - Existing Helm charts that need adjustments, improvements, or best-practice refinements. | ||
|
|
||
| Your guidance should be exhaustive, thorough, and precisely tailored to the user's needs. | ||
| Always ensure that your output is a valid, production-ready Helm chart setup adhering to Helm best practices. | ||
| If the user provides partial information (e.g., a single Deployment manifest, a partial Chart.yaml, or just an image and port configuration), you must integrate it into a coherent chart. | ||
| Requests will always be based on a existing Helm chart and you must incorporate modifications while preserving and improving the chart's structure (do not rewrite the chart for each request). | ||
|
|
||
| Below are guidelines and constraints you must always follow: | ||
|
|
||
| <system_constraints> | ||
| - Focus exclusively on tasks related to Helm charts and Kubernetes manifests. Do not address topics outside of Kubernetes, Helm, or their associated configurations. | ||
| - Assume a standard Kubernetes environment, where Helm is available. | ||
| - Do not assume any external services (e.g., cloud-hosted registries or databases) unless the user's scenario explicitly includes them. | ||
| - Do not rely on installing arbitrary tools; you are guiding and generating Helm chart files and commands only. | ||
| - Incorporate changes into the most recent version of files. Make sure to provide complete updated file contents. | ||
| </system_constraints> | ||
|
|
||
| <code_formatting_info> | ||
| - Use 2 spaces for indentation in all YAML files. | ||
| - Ensure YAML and Helm templates are valid, syntactically correct, and adhere to Kubernetes resource definitions. | ||
| - Use proper Helm templating expressions ({{ ... }}) where appropriate. For example, parameterize image tags, resource counts, ports, and labels. | ||
| - Keep the chart well-structured and maintainable. | ||
| </code_formatting_info> | ||
|
|
||
| <message_formatting_info> | ||
| - Use only valid Markdown for your responses unless required by the instructions below. | ||
| - Do not use HTML elements. | ||
| - Communicate in plain Markdown. Inside these tags, produce only the required YAML, shell commands, or file contents. | ||
| </message_formatting_info> | ||
|
|
||
| NEVER use the word "artifact" in your final messages to the user. | ||
|
|
||
| <question_instructions> | ||
| - You will be asked to answer a question. | ||
| - You will be given the question and the context of the question. | ||
| - You will be given the current chat history. | ||
| - You will be asked to answer the question based on the context and the chat history. | ||
| - You can provide small examples of code, but just use markdown. | ||
| </question_instructions>`; | ||
|
|
||
| const CHAT_INSTRUCTIONS = `- You will be asked to answer a question. | ||
| - You will be given the question and the context of the question. | ||
| - You will be given the current chat history. | ||
| - You will be asked to answer the question based on the context and the chat history. | ||
| - You can be technical in your response and include inline code snippets identifed with Markdown when appropriate. | ||
| - Never use the <chartsmithArtifact> tag in your response.`; | ||
|
|
||
| export async function POST(req: NextRequest) { | ||
| const startTime = Date.now(); | ||
| let chatMessageId: string | undefined; | ||
|
|
||
| try { | ||
| // Validate Authorization header using dedicated internal API token | ||
| // This prevents leaking the Anthropic API key via request headers | ||
| const authHeader = req.headers.get('authorization'); | ||
| if (!authHeader || !authHeader.startsWith('Bearer ')) { | ||
| console.error('[CHAT API] Missing or invalid Authorization header'); | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); | ||
| } | ||
|
|
||
| const token = authHeader.substring(7); // Remove 'Bearer ' prefix | ||
| const expectedToken = process.env.INTERNAL_API_TOKEN; | ||
|
|
||
| if (!expectedToken) { | ||
| console.error('[CHAT API] INTERNAL_API_TOKEN not configured'); | ||
| return NextResponse.json({ error: 'Server misconfigured' }, { status: 500 }); | ||
| } | ||
|
|
||
| if (token !== expectedToken) { | ||
| console.error('[CHAT API] Invalid Bearer token'); | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Get chat message ID from request | ||
| const body = await req.json(); | ||
| chatMessageId = body.chatMessageId; | ||
|
|
||
| console.log(`[CHAT API] Starting request for chatMessageId=${chatMessageId}`); | ||
|
|
||
| if (!chatMessageId) { | ||
| console.error('[CHAT API] Missing chatMessageId in request'); | ||
| return NextResponse.json({ error: 'chatMessageId is required' }, { status: 400 }); | ||
| } | ||
|
|
||
| // Fetch the chat message from database | ||
| console.log(`[CHAT API] Fetching chat message from database...`); | ||
| const chatMessage = await getChatMessage(chatMessageId); | ||
| if (!chatMessage) { | ||
| console.error(`[CHAT API] Chat message not found: ${chatMessageId}`); | ||
| return NextResponse.json({ error: 'Chat message not found' }, { status: 404 }); | ||
| } | ||
Davaakhatan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| console.log(`[CHAT API] Found chat message. Prompt: "${chatMessage.prompt.substring(0, 100)}..."`); | ||
|
|
||
| // Get workspace ID and user ID for Centrifugo publishing | ||
| const workspaceId = await getWorkspaceIdForChatMessage(chatMessageId); | ||
| const userId = chatMessage.userId; | ||
|
|
||
| if (!userId) { | ||
| console.error(`[CHAT API] Chat message missing userId: ${chatMessageId}`); | ||
| return NextResponse.json({ error: 'Chat message missing userId' }, { status: 400 }); | ||
| } | ||
|
|
||
| console.log(`[CHAT API] workspaceId=${workspaceId}, userId=${userId}`); | ||
|
|
||
Davaakhatan marked this conversation as resolved.
Show resolved
Hide resolved
Davaakhatan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Clear any existing response to prevent duplication on retry | ||
| await clearChatMessageResponse(chatMessageId); | ||
| console.log(`[CHAT API] Cleared existing response`); | ||
|
|
||
| // Initialize Anthropic with Vercel AI SDK | ||
| const anthropic = createAnthropic({ | ||
| apiKey: process.env.ANTHROPIC_API_KEY, | ||
| }); | ||
| console.log(`[CHAT API] Initialized Anthropic client`); | ||
|
|
||
| // Define tools (from pkg/llm/conversational.go) | ||
| const tools = { | ||
| latest_subchart_version: tool({ | ||
| description: 'Return the latest version of a subchart from name', | ||
| inputSchema: z.object({ | ||
| chart_name: z.string().describe('The subchart name to get the latest version of'), | ||
| }), | ||
| execute: async ({ chart_name }) => { | ||
| try { | ||
| const version = await getLatestSubchartVersion(chart_name); | ||
| return version; | ||
| } catch (error) { | ||
| console.error(`Failed to get subchart version for ${chart_name}:`, error); | ||
| return 'unknown'; | ||
| } | ||
| }, | ||
| }), | ||
| latest_kubernetes_version: tool({ | ||
| description: 'Return the latest version of Kubernetes', | ||
| inputSchema: z.object({ | ||
| semver_field: z.enum(['major', 'minor', 'patch']).describe('One of major, minor, or patch'), | ||
| }), | ||
| execute: async ({ semver_field }) => { | ||
| switch (semver_field) { | ||
| case 'major': | ||
| return '1'; | ||
| case 'minor': | ||
| return '1.32'; | ||
| case 'patch': | ||
| return '1.32.1'; | ||
| default: | ||
| return '1.32.1'; | ||
| } | ||
| }, | ||
| }), | ||
| }; | ||
|
|
||
| // Get workspace and chart context | ||
| console.log(`[CHAT API] Loading workspace context...`); | ||
| const workspace = await getWorkspace(workspaceId); | ||
| if (!workspace) { | ||
| console.error(`[CHAT API] Workspace not found: ${workspaceId}`); | ||
| return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }); | ||
| } | ||
|
|
||
| const chartStructure = await getChartStructure(workspace); | ||
| const relevantFiles = await chooseRelevantFiles(workspace, chatMessage.prompt, undefined, 10); | ||
| const chatHistory = await getPreviousChatHistory(workspaceId, chatMessageId); | ||
| console.log(`[CHAT API] Context loaded: ${relevantFiles.length} files, ${chatHistory.length} history messages`); | ||
|
|
||
| // Build system prompt (includes instructions and chart context) | ||
| const systemPrompt = `${CHAT_SYSTEM_PROMPT} | ||
|
|
||
| ${CHAT_INSTRUCTIONS} | ||
|
|
||
| I am working on a Helm chart that has the following structure: ${chartStructure} | ||
|
|
||
| ${relevantFiles.map(file => `File: ${file.filePath}, Content: ${file.content}`).join('\n\n')}`; | ||
|
|
||
| // Build messages array (conversation history + current message) | ||
| const messages = [ | ||
| // Add conversation history | ||
| ...chatHistory, | ||
| // User's current message | ||
| { role: 'user' as const, content: chatMessage.prompt }, | ||
| ]; | ||
|
|
||
| console.log(`[CHAT API] Built system prompt and ${messages.length} messages, calling streamText()...`); | ||
|
|
||
| let chunkCount = 0; | ||
| let totalChars = 0; | ||
|
|
||
| // Stream the response using Vercel AI SDK | ||
| const result = streamText({ | ||
| model: anthropic('claude-3-5-sonnet-20241022'), | ||
| system: systemPrompt, | ||
| messages, | ||
| tools, | ||
| onChunk: async ({ chunk }) => { | ||
| chunkCount++; | ||
| console.log(`[CHAT API] onChunk called (chunk #${chunkCount}): type=${chunk.type}`); | ||
|
|
||
| // Handle text delta chunks | ||
| if (chunk.type === 'text-delta') { | ||
| const textChunk = chunk.text; | ||
| totalChars += textChunk.length; | ||
| console.log(`[CHAT API] text-delta chunk: ${textChunk.length} chars (total: ${totalChars})`); | ||
|
|
||
| try { | ||
| // 1. Append to database | ||
| await appendChatMessageResponse(chatMessageId!, textChunk); | ||
| console.log(`[CHAT API] Saved chunk to database`); | ||
|
|
||
| // 2. Publish to Centrifugo for real-time updates | ||
| await publishChatMessageUpdate(workspaceId, userId, chatMessageId!, textChunk, false); | ||
| console.log(`[CHAT API] Published chunk to Centrifugo`); | ||
| } catch (err) { | ||
| console.error(`[CHAT API] Error processing chunk:`, err); | ||
| throw err; | ||
| } | ||
| } | ||
| }, | ||
| onFinish: async () => { | ||
| console.log(`[CHAT API] onFinish called. Total chunks: ${chunkCount}, total chars: ${totalChars}`); | ||
|
|
||
| try { | ||
| // Mark message as complete | ||
| await markChatMessageComplete(chatMessageId!); | ||
| console.log(`[CHAT API] Marked message complete in database`); | ||
|
|
||
| // Publish final completion event | ||
| await publishChatMessageUpdate(workspaceId, userId, chatMessageId!, '', true); | ||
| console.log(`[CHAT API] Published completion to Centrifugo`); | ||
| } catch (err) { | ||
| console.error(`[CHAT API] Error in onFinish:`, err); | ||
| throw err; | ||
| } | ||
| }, | ||
Davaakhatan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| console.log(`[CHAT API] Waiting for streamText to complete...`); | ||
|
|
||
| // Wait for completion | ||
| const fullText = await result.text; | ||
|
|
||
| const duration = Date.now() - startTime; | ||
| console.log(`[CHAT API] Completed successfully in ${duration}ms. Response length: ${fullText.length} chars`); | ||
|
|
||
| return NextResponse.json({ success: true }); | ||
|
|
||
| } catch (error) { | ||
| const duration = Date.now() - startTime; | ||
| console.error(`[CHAT API] Error after ${duration}ms:`, error); | ||
| console.error(`[CHAT API] Error stack:`, error instanceof Error ? error.stack : 'No stack trace'); | ||
|
|
||
| return NextResponse.json( | ||
| { | ||
| error: 'Internal Server Error', | ||
| details: error instanceof Error ? error.message : String(error), | ||
| chatMessageId | ||
| }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.