-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: migrate from ai/rsc to ai/ui (useChat) #579
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,388 @@ | ||||||||||||||||||||||||||
| import { CoreMessage, ToolResultPart, streamText, LanguageModel } from 'ai' | ||||||||||||||||||||||||||
| import { nanoid } from '@/lib/utils' | ||||||||||||||||||||||||||
| import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user' | ||||||||||||||||||||||||||
| import { taskManager, inquire, querySuggestor } from '@/lib/agents' | ||||||||||||||||||||||||||
| import { researcher } from '@/lib/agents/researcher' | ||||||||||||||||||||||||||
| import { writer } from '@/lib/agents/writer' | ||||||||||||||||||||||||||
| import { resolutionSearch, type DrawnFeature } from '@/lib/agents/resolution-search' | ||||||||||||||||||||||||||
| import { getModel } from '@/lib/utils' | ||||||||||||||||||||||||||
| import { getSystemPrompt, saveChat } from '@/lib/actions/chat' | ||||||||||||||||||||||||||
| import type { Chat, AIMessage } from '@/lib/types' | ||||||||||||||||||||||||||
| import type { MapProvider } from '@/lib/store/settings' | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const maxDuration = 60 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const streamHeaders = { | ||||||||||||||||||||||||||
| 'Content-Type': 'text/plain; charset=utf-8', | ||||||||||||||||||||||||||
| 'x-vercel-ai-data-stream': 'v1', | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export async function POST(request: Request) { | ||||||||||||||||||||||||||
| const userId = await getCurrentUserIdOnServer() | ||||||||||||||||||||||||||
| if (!userId) { | ||||||||||||||||||||||||||
| return new Response(JSON.stringify({ error: 'Unauthorized' }), { | ||||||||||||||||||||||||||
| status: 401, | ||||||||||||||||||||||||||
| headers: { 'Content-Type': 'application/json' } | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const body = await request.json() | ||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||
| messages: clientMessages, | ||||||||||||||||||||||||||
| chatId = nanoid(), | ||||||||||||||||||||||||||
| action, | ||||||||||||||||||||||||||
| mapProvider = 'mapbox', | ||||||||||||||||||||||||||
| drawnFeatures: drawnFeaturesRaw, | ||||||||||||||||||||||||||
| timezone, | ||||||||||||||||||||||||||
| latitude, | ||||||||||||||||||||||||||
| longitude, | ||||||||||||||||||||||||||
| fileData, | ||||||||||||||||||||||||||
| mapboxImageData, | ||||||||||||||||||||||||||
| googleImageData, | ||||||||||||||||||||||||||
| } = body | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const drawnFeatures: DrawnFeature[] = drawnFeaturesRaw || [] | ||||||||||||||||||||||||||
| const location = (latitude !== undefined && longitude !== undefined) | ||||||||||||||||||||||||||
| ? { lat: parseFloat(latitude), lng: parseFloat(longitude) } | ||||||||||||||||||||||||||
| : undefined | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const currentSystemPrompt = (await getSystemPrompt(userId)) || '' | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RAG context is still not being passed to the researcher.
Based on learnings: "there's a bug on line 433 of 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| const useSpecificAPI = process.env.USE_SPECIFIC_API_FOR_WRITER === 'true' | ||||||||||||||||||||||||||
| const maxMsgs = useSpecificAPI ? 5 : 10 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Build core messages from client messages | ||||||||||||||||||||||||||
| const messages: CoreMessage[] = (clientMessages || []) | ||||||||||||||||||||||||||
| .filter((m: any) => m.role === 'user' || m.role === 'assistant') | ||||||||||||||||||||||||||
| .map((m: any) => ({ | ||||||||||||||||||||||||||
| role: m.role, | ||||||||||||||||||||||||||
| content: m.content | ||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Trim to max messages | ||||||||||||||||||||||||||
| if (messages.length > maxMsgs) { | ||||||||||||||||||||||||||
| messages.splice(0, messages.length - maxMsgs) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+53
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trimming via Two issues co-located here:
- // Build core messages from client messages
- const messages: CoreMessage[] = (clientMessages || [])
- .filter((m: any) => m.role === 'user' || m.role === 'assistant')
- .map((m: any) => ({
- role: m.role,
- content: m.content
- }))
-
- // Trim to max messages
- if (messages.length > maxMsgs) {
- messages.splice(0, messages.length - maxMsgs)
- }
+ // Capture the original first user message before trimming so chat title stays stable.
+ const originalFirstUserMessage = (clientMessages || []).find((m: any) => m.role === 'user')
+
+ const messages: CoreMessage[] = (clientMessages || [])
+ .filter((m: any) => m.role === 'user' || m.role === 'assistant' || m.role === 'tool')
+ .map((m: any) => ({
+ role: m.role,
+ content: m.content,
+ // preserve assistant tool calls so the model doesn't re-issue them
+ ...(m.toolInvocations ? { toolInvocations: m.toolInvocations } : {}),
+ }))
+
+ if (messages.length > maxMsgs) {
+ messages.splice(0, messages.length - maxMsgs)
+ }…and pass 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Resolution search action | ||||||||||||||||||||||||||
| if (action === 'resolution_search' && fileData) { | ||||||||||||||||||||||||||
| return handleResolutionSearch({ | ||||||||||||||||||||||||||
| messages, chatId, userId, fileData, mapboxImageData, googleImageData, | ||||||||||||||||||||||||||
| timezone, drawnFeatures, location | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Hardcoded responses | ||||||||||||||||||||||||||
| const lastMsg = messages[messages.length - 1] | ||||||||||||||||||||||||||
| const lastText = typeof lastMsg?.content === 'string' ? lastMsg.content.trim().toLowerCase() : '' | ||||||||||||||||||||||||||
| if (lastText === 'what is a planet computer?' || lastText === 'what is qcx-terra?') { | ||||||||||||||||||||||||||
| const definition = lastText === 'what is a planet computer?' | ||||||||||||||||||||||||||
| ? `A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet. Available for our Pro and Enterprise customers. [QCX Pricing](https://www.queue.cx/#pricing)` | ||||||||||||||||||||||||||
| : `QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery. Available for our Pro and Enterprise customers. [QCX Pricing](https://www.queue.cx/#pricing)`; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const encoder = new TextEncoder() | ||||||||||||||||||||||||||
| const stream = new ReadableStream({ | ||||||||||||||||||||||||||
| start(controller) { | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`0:${JSON.stringify(definition)}\n`)) | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`2:[{"relatedQueries":{"items":[]},"type":"related"}]\n`)) | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`e:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`)) | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`)) | ||||||||||||||||||||||||||
| controller.close() | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| saveChatAsync(chatId, userId, messages, definition) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return new Response(stream, { | ||||||||||||||||||||||||||
| headers: streamHeaders | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+77
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded replies: content drift vs. system prompt, and fragile string matching.
🤖 Prompt for AI Agents
Comment on lines
+75
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded reply branch is fragile and disagrees with the system prompt source-of-truth.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Task manager: decide inquire vs proceed | ||||||||||||||||||||||||||
| let nextAction = 'proceed' | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| const taskResult = await taskManager(messages) | ||||||||||||||||||||||||||
| if (taskResult?.object?.next === 'inquire') { | ||||||||||||||||||||||||||
| nextAction = 'inquire' | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||
| console.error('Task manager error:', e) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Inquiry path | ||||||||||||||||||||||||||
| if (nextAction === 'inquire') { | ||||||||||||||||||||||||||
| const inquiryResult = await inquire(messages) | ||||||||||||||||||||||||||
| const encoder = new TextEncoder() | ||||||||||||||||||||||||||
| const stream = new ReadableStream({ | ||||||||||||||||||||||||||
| start(controller) { | ||||||||||||||||||||||||||
| // Send inquiry data as a data annotation | ||||||||||||||||||||||||||
| const annotation = { type: 'inquiry', data: inquiryResult } | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`2:[${JSON.stringify(annotation)}]\n`)) | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`e:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`)) | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`)) | ||||||||||||||||||||||||||
| controller.close() | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| return new Response(stream, { | ||||||||||||||||||||||||||
| headers: streamHeaders | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Proceed path: researcher -> optionally writer -> query suggestor | ||||||||||||||||||||||||||
| let answer = '' | ||||||||||||||||||||||||||
| let toolOutputs: ToolResultPart[] = [] | ||||||||||||||||||||||||||
| let errorOccurred = false | ||||||||||||||||||||||||||
| const allToolOutputs: ToolResultPart[] = [] | ||||||||||||||||||||||||||
| const maxAttempts = 3 | ||||||||||||||||||||||||||
| let attempts = 0 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| while ( | ||||||||||||||||||||||||||
| attempts < maxAttempts && | ||||||||||||||||||||||||||
| (useSpecificAPI | ||||||||||||||||||||||||||
| ? answer.length === 0 | ||||||||||||||||||||||||||
| : answer.length === 0 && !errorOccurred) | ||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||
| attempts++ | ||||||||||||||||||||||||||
| const { fullResponse, hasError, toolResponses, newSegments } = await researcher( | ||||||||||||||||||||||||||
| currentSystemPrompt, | ||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||
| mapProvider as MapProvider, | ||||||||||||||||||||||||||
| useSpecificAPI, | ||||||||||||||||||||||||||
| drawnFeatures | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| answer = fullResponse | ||||||||||||||||||||||||||
| toolOutputs = toolResponses | ||||||||||||||||||||||||||
| errorOccurred = hasError | ||||||||||||||||||||||||||
| allToolOutputs.push(...toolResponses) | ||||||||||||||||||||||||||
| // Only append segments to messages on success or final attempt | ||||||||||||||||||||||||||
| if (answer.length > 0 || errorOccurred || attempts >= maxAttempts) { | ||||||||||||||||||||||||||
| messages.push(...newSegments) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (useSpecificAPI && answer.length === 0) { | ||||||||||||||||||||||||||
| const latestMessages = messages.slice(maxMsgs * -1) | ||||||||||||||||||||||||||
| answer = await writer(currentSystemPrompt, latestMessages) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Get related queries (sanitize to remove image parts) | ||||||||||||||||||||||||||
| let relatedQueries = {} | ||||||||||||||||||||||||||
| if (!errorOccurred) { | ||||||||||||||||||||||||||
| const sanitizedMessages: CoreMessage[] = messages.map((m: any) => { | ||||||||||||||||||||||||||
| if (Array.isArray(m.content)) { | ||||||||||||||||||||||||||
| return { ...m, content: m.content.filter((part: any) => part.type !== 'image') } as CoreMessage | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return m | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| relatedQueries = await querySuggestor(sanitizedMessages) | ||||||||||||||||||||||||||
|
Comment on lines
+168
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm querySuggestor's return shape vs the consumer's expectation.
rg -nP -C5 "export\s+async\s+function\s+querySuggestor" lib/agents/query-suggestor.tsx
rg -nP -C5 "relatedQueries|PartialRelated" lib/schema/related.ts
rg -nP -C2 "related\.relatedQueries|relatedQueries\.items" components/Repository: QueueLab/QCX Length of output: 1244 🏁 Script executed: # Find where PartialRelated is defined (broader search)
rg -n "PartialRelated" --type ts --type tsxRepository: QueueLab/QCX Length of output: 84 🏁 Script executed: # Get complete querySuggestor function implementation
rg -n -A20 "export\s+async\s+function\s+querySuggestor" --type ts --type tsxRepository: QueueLab/QCX Length of output: 84 🏁 Script executed: # Check the route.ts file around lines 168-200 to see how annotation is emitted
fd "route.ts" | xargs head -n 210 | tail -n 50Repository: QueueLab/QCX Length of output: 1650 🏁 Script executed: # Find PartialRelated definition
rg -n "PartialRelated" -t ts -t jsRepository: QueueLab/QCX Length of output: 1868 🏁 Script executed: # Find the chat stream route specifically
fd "stream/route" -e ts -e jsRepository: QueueLab/QCX Length of output: 38 🏁 Script executed: # Get complete querySuggestor function
rg -n -A30 "export async function querySuggestor" -t ts -t jsRepository: QueueLab/QCX Length of output: 2651 🏁 Script executed: # Get the relatedSchema definition to understand the structure
rg -n -A15 "export.*relatedSchema" lib/schema/related.tsxRepository: QueueLab/QCX Length of output: 341 🏁 Script executed: # Get the chat stream route to see annotation emission around lines 168-200
cat -n app/api/chat/stream/route.ts | sed -n '160,205p'Repository: QueueLab/QCX Length of output: 2049 Remove the shape mismatch concern; the structure is correct.
However, the bandwidth concern is valid: 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Build streaming response | ||||||||||||||||||||||||||
| const encoder = new TextEncoder() | ||||||||||||||||||||||||||
| const stream = new ReadableStream({ | ||||||||||||||||||||||||||
| start(controller) { | ||||||||||||||||||||||||||
| // Send tool results as annotations | ||||||||||||||||||||||||||
| for (const toolResult of allToolOutputs) { | ||||||||||||||||||||||||||
| const annotation = { | ||||||||||||||||||||||||||
| type: 'tool_result', | ||||||||||||||||||||||||||
| toolName: toolResult.toolName, | ||||||||||||||||||||||||||
| result: toolResult.result | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`2:[${JSON.stringify(annotation)}]\n`)) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Stream the text response | ||||||||||||||||||||||||||
| if (answer) { | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`0:${JSON.stringify(answer)}\n`)) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Send related queries as annotation | ||||||||||||||||||||||||||
| const relatedAnnotation = { type: 'related', relatedQueries } | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`2:[${JSON.stringify(relatedAnnotation)}]\n`)) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Finish | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`e:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`)) | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`)) | ||||||||||||||||||||||||||
| controller.close() | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Save chat asynchronously | ||||||||||||||||||||||||||
| saveChatAsync(chatId, userId, messages, answer, allToolOutputs, relatedQueries) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return new Response(stream, { | ||||||||||||||||||||||||||
| headers: streamHeaders | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
Comment on lines
+209
to
+214
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, check if the file exists and examine the relevant sections
if [ -f "app/api/chat/stream/route.ts" ]; then
echo "=== File exists ==="
wc -l app/api/chat/stream/route.ts
echo ""
echo "=== Lines 185-195 (around first occurrence) ==="
sed -n '185,195p' app/api/chat/stream/route.ts
echo ""
echo "=== Lines 260-270 (around second occurrence) ==="
sed -n '260,270p' app/api/chat/stream/route.ts
echo ""
echo "=== Check imports at top of file ==="
head -30 app/api/chat/stream/route.ts
else
echo "File not found. Searching for similar files..."
find . -name "*route.ts" -o -name "*stream*" | grep -i chat
fiRepository: QueueLab/QCX Length of output: 1968 🏁 Script executed: # Check if saveChatAsync is defined and how it's used throughout the file
rg "saveChatAsync" --context 3Repository: QueueLab/QCX Length of output: 1547 🏁 Script executed: # Check the package.json to see Next.js version
if [ -f "package.json" ]; then
echo "=== Next.js version ==="
rg '"next"' package.json
fiRepository: QueueLab/QCX Length of output: 102 🏁 Script executed: # Get the saveChatAsync function definition
rg "async function saveChatAsync" -A 20Repository: QueueLab/QCX Length of output: 1288 🏁 Script executed: # Get exact line numbers for all saveChatAsync calls (non-definition)
rg -n "saveChatAsync\(" app/api/chat/stream/route.ts | grep -v "async function"Repository: QueueLab/QCX Length of output: 279 🏁 Script executed: # Check if saveChat is used elsewhere and what it does
rg "saveChat" app/lib/actions/chat.ts -A 10 | head -50Repository: QueueLab/QCX Length of output: 120 🏁 Script executed: # Find the chat actions file
find . -name "chat.ts" -o -name "chat.js" | grep -i actionsRepository: QueueLab/QCX Length of output: 78 🏁 Script executed: # Check the saveChatAsync function to confirm it does DB operations
sed -n '320,359p' app/api/chat/stream/route.tsRepository: QueueLab/QCX Length of output: 836 🏁 Script executed: # Check if `after` from next/server is imported anywhere
rg "from 'next/server'" app/api/chat/stream/route.tsRepository: QueueLab/QCX Length of output: 38 🏁 Script executed: # Check what saveChat does in lib/actions/chat.ts
cat -n lib/actions/chat.ts | head -80Repository: QueueLab/QCX Length of output: 2940 🏁 Script executed: # Look for the full saveChat function
rg "export.*function saveChat|export const saveChat" lib/actions/chat.ts -A 15Repository: QueueLab/QCX Length of output: 597 🏁 Script executed: # Confirm all imports in the route.ts file to see what is imported
head -20 app/api/chat/stream/route.tsRepository: QueueLab/QCX Length of output: 935 Fix fire-and-forget
🔧 Proposed fiximport { CoreMessage, ToolResultPart, streamText, LanguageModel } from 'ai'
+import { after } from 'next/server'Then wrap each -saveChatAsync(chatId, userId, messages, definition)
+after(() => saveChatAsync(chatId, userId, messages, definition))📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async function handleResolutionSearch({ | ||||||||||||||||||||||||||
| messages, chatId, userId, fileData, mapboxImageData, googleImageData, | ||||||||||||||||||||||||||
| timezone, drawnFeatures, location | ||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||
| messages: CoreMessage[] | ||||||||||||||||||||||||||
| chatId: string | ||||||||||||||||||||||||||
| userId: string | ||||||||||||||||||||||||||
| fileData: string | ||||||||||||||||||||||||||
| mapboxImageData?: string | ||||||||||||||||||||||||||
| googleImageData?: string | ||||||||||||||||||||||||||
| timezone?: string | ||||||||||||||||||||||||||
| drawnFeatures: DrawnFeature[] | ||||||||||||||||||||||||||
| location?: { lat: number; lng: number } | ||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||
| const content: CoreMessage['content'] = [ | ||||||||||||||||||||||||||
| { type: 'text', text: 'Analyze this map view.' }, | ||||||||||||||||||||||||||
| { type: 'image', image: fileData, mimeType: 'image/png' } | ||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||
| messages.push({ role: 'user', content }) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
Comment on lines
+231
to
+236
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Base64 images saved to db Resolution-search pushes fileData (a base64 data URL) into CoreMessage.content and saveChatAsync persists it verbatim; saveChat then JSON-stringifies it into the messages.content text column, risking extreme DB bloat and slow history loads. Agent Prompt
|
||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| const streamResult = await resolutionSearch( | ||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||
| timezone || 'UTC', | ||||||||||||||||||||||||||
| drawnFeatures, | ||||||||||||||||||||||||||
| location | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const analysisResult = await streamResult.object | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Get related queries | ||||||||||||||||||||||||||
| const sanitizedMessages: CoreMessage[] = messages.map((m: any) => { | ||||||||||||||||||||||||||
| if (Array.isArray(m.content)) { | ||||||||||||||||||||||||||
| return { ...m, content: m.content.filter((part: any) => part.type !== 'image') } as CoreMessage | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return m | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| const relatedQueries = await querySuggestor(sanitizedMessages) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const encoder = new TextEncoder() | ||||||||||||||||||||||||||
| const stream = new ReadableStream({ | ||||||||||||||||||||||||||
| start(controller) { | ||||||||||||||||||||||||||
| // Send resolution result as annotation | ||||||||||||||||||||||||||
| const resAnnotation = { | ||||||||||||||||||||||||||
| type: 'resolution_search_result', | ||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||
| ...analysisResult, | ||||||||||||||||||||||||||
| image: fileData, | ||||||||||||||||||||||||||
| mapboxImage: mapboxImageData, | ||||||||||||||||||||||||||
| googleImage: googleImageData | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`2:[${JSON.stringify(resAnnotation)}]\n`)) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Stream summary text | ||||||||||||||||||||||||||
| if (analysisResult.summary) { | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`0:${JSON.stringify(analysisResult.summary)}\n`)) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Related queries | ||||||||||||||||||||||||||
| const relatedAnnotation = { type: 'related', relatedQueries } | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`2:[${JSON.stringify(relatedAnnotation)}]\n`)) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`e:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`)) | ||||||||||||||||||||||||||
| controller.enqueue(encoder.encode(`d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`)) | ||||||||||||||||||||||||||
| controller.close() | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| saveChatAsync(chatId, userId, messages, analysisResult.summary || '') | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return new Response(stream, { | ||||||||||||||||||||||||||
| headers: streamHeaders | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||
| console.error('Resolution search error:', error) | ||||||||||||||||||||||||||
| return new Response(JSON.stringify({ error: 'Resolution search failed' }), { | ||||||||||||||||||||||||||
| status: 500, | ||||||||||||||||||||||||||
| headers: { 'Content-Type': 'application/json' } | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+217
to
+298
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Two issues remaining after the recent fixes:
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async function saveChatAsync( | ||||||||||||||||||||||||||
| chatId: string, | ||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||
| messages: CoreMessage[], | ||||||||||||||||||||||||||
| answer: string, | ||||||||||||||||||||||||||
| toolOutputs?: ToolResultPart[], | ||||||||||||||||||||||||||
| relatedQueries?: any | ||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| let title = 'Untitled Chat' | ||||||||||||||||||||||||||
| const firstMsg = messages[0] | ||||||||||||||||||||||||||
| if (firstMsg) { | ||||||||||||||||||||||||||
| if (typeof firstMsg.content === 'string') { | ||||||||||||||||||||||||||
| title = firstMsg.content.substring(0, 100) | ||||||||||||||||||||||||||
| } else if (Array.isArray(firstMsg.content)) { | ||||||||||||||||||||||||||
| const textPart = (firstMsg.content as any[]).find(p => p.type === 'text') | ||||||||||||||||||||||||||
| title = textPart?.text?.substring(0, 100) || 'Image Message' | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const aiMessages: AIMessage[] = [] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for (const msg of messages) { | ||||||||||||||||||||||||||
| let content: CoreMessage['content'] = msg.content | ||||||||||||||||||||||||||
| if (Array.isArray(content)) { | ||||||||||||||||||||||||||
| content = (content as any[]).filter((part: any) => part.type !== 'image') as CoreMessage['content'] | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| aiMessages.push({ | ||||||||||||||||||||||||||
| id: (msg as any).id || nanoid(), | ||||||||||||||||||||||||||
| role: msg.role as AIMessage['role'], | ||||||||||||||||||||||||||
| content, | ||||||||||||||||||||||||||
| type: msg.role === 'user' ? 'input' : undefined | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Add tool outputs | ||||||||||||||||||||||||||
| if (toolOutputs) { | ||||||||||||||||||||||||||
| for (const tool of toolOutputs) { | ||||||||||||||||||||||||||
| aiMessages.push({ | ||||||||||||||||||||||||||
| id: nanoid(), | ||||||||||||||||||||||||||
| role: 'tool', | ||||||||||||||||||||||||||
| content: JSON.stringify(tool.result), | ||||||||||||||||||||||||||
| name: tool.toolName, | ||||||||||||||||||||||||||
| type: 'tool' | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Add response | ||||||||||||||||||||||||||
| if (answer) { | ||||||||||||||||||||||||||
| aiMessages.push({ | ||||||||||||||||||||||||||
| id: nanoid(), | ||||||||||||||||||||||||||
| role: 'assistant', | ||||||||||||||||||||||||||
| content: answer, | ||||||||||||||||||||||||||
| type: 'response' | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Add related queries | ||||||||||||||||||||||||||
| if (relatedQueries) { | ||||||||||||||||||||||||||
| aiMessages.push({ | ||||||||||||||||||||||||||
| id: nanoid(), | ||||||||||||||||||||||||||
| role: 'assistant', | ||||||||||||||||||||||||||
| content: JSON.stringify(relatedQueries), | ||||||||||||||||||||||||||
| type: 'related' | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Add end marker | ||||||||||||||||||||||||||
| aiMessages.push({ | ||||||||||||||||||||||||||
| id: nanoid(), | ||||||||||||||||||||||||||
| role: 'assistant', | ||||||||||||||||||||||||||
| content: 'end', | ||||||||||||||||||||||||||
| type: 'end' | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
Comment on lines
+320
to
+374
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Synthetic The user/assistant messages now correctly preserve Use a deterministic id derived from 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const chat: Chat = { | ||||||||||||||||||||||||||
| id: chatId, | ||||||||||||||||||||||||||
| createdAt: new Date(), | ||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||
| path: `/search/${chatId}`, | ||||||||||||||||||||||||||
| title, | ||||||||||||||||||||||||||
| messages: aiMessages | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| await saveChat(chat, userId) | ||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||
| console.error('Error saving chat:', error) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.