This commit is contained in:
Siddharth Ganesan
2025-07-08 18:14:07 -07:00
parent caccb61362
commit 2354909ef9
5 changed files with 77 additions and 57 deletions
+53 -45
View File
@@ -1,14 +1,13 @@
import { eq, and } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server' import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod' import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger' import { createLogger } from '@/lib/logs/console-logger'
import { getRotatingApiKey } from '@/lib/utils' import { getRotatingApiKey } from '@/lib/utils'
import { getSession } from '@/lib/auth'
import { db } from '@/db' import { db } from '@/db'
import { copilotChats } from '@/db/schema' import { copilotChats } from '@/db/schema'
import { executeProviderRequest } from '@/providers' import { executeProviderRequest } from '@/providers'
import type { Message } from '@/providers/types' import type { Message } from '@/providers/types'
import { executeTool } from '@/tools'
const logger = createLogger('CopilotChat') const logger = createLogger('CopilotChat')
@@ -34,10 +33,11 @@ const CopilotChatSchema = z.object({
async function generateChatTitle(userMessage: string): Promise<string> { async function generateChatTitle(userMessage: string): Promise<string> {
try { try {
const apiKey = getRotatingApiKey('anthropic') const apiKey = getRotatingApiKey('anthropic')
const response = await executeProviderRequest('anthropic', { const response = await executeProviderRequest('anthropic', {
model: 'claude-3-haiku-20240307', model: 'claude-3-haiku-20240307',
systemPrompt: 'You are a helpful assistant that generates concise, descriptive titles for chat conversations. Create a title that captures the main topic or question being discussed. Keep it under 50 characters and make it specific and clear.', systemPrompt:
'You are a helpful assistant that generates concise, descriptive titles for chat conversations. Create a title that captures the main topic or question being discussed. Keep it under 50 characters and make it specific and clear.',
context: `Generate a concise title for a conversation that starts with this user message: "${userMessage}" context: `Generate a concise title for a conversation that starts with this user message: "${userMessage}"
Return only the title text, nothing else.`, Return only the title text, nothing else.`,
@@ -58,8 +58,6 @@ Return only the title text, nothing else.`,
} }
} }
/** /**
* Generate chat response with tool calling support * Generate chat response with tool calling support
*/ */
@@ -84,8 +82,8 @@ function extractCitationsFromResponse(response: any): Array<{
return [] return []
} }
const docsSearchResult = response.toolResults.find((result: any) => const docsSearchResult = response.toolResults.find(
result.sources && Array.isArray(result.sources) (result: any) => result.sources && Array.isArray(result.sources)
) )
if (!docsSearchResult || !docsSearchResult.sources) { if (!docsSearchResult || !docsSearchResult.sources) {
@@ -109,15 +107,16 @@ async function generateChatResponse(
// Build conversation context // Build conversation context
const messages: Message[] = [] const messages: Message[] = []
// Add conversation history // Add conversation history
for (const msg of conversationHistory.slice(-10)) { // Keep last 10 messages for (const msg of conversationHistory.slice(-10)) {
// Keep last 10 messages
messages.push({ messages.push({
role: msg.role as 'user' | 'assistant' | 'system', role: msg.role as 'user' | 'assistant' | 'system',
content: msg.content, content: msg.content,
}) })
} }
// Add current user message // Add current user message
messages.push({ messages.push({
role: 'user', role: 'user',
@@ -170,7 +169,8 @@ MAKE SURE YOU FULLY ANSWER THE USER'S QUESTION.
{ {
id: 'docs_search_internal', id: 'docs_search_internal',
name: 'Search Documentation', name: 'Search Documentation',
description: 'Search Sim Studio documentation for information about features, tools, workflows, and functionality', description:
'Search Sim Studio documentation for information about features, tools, workflows, and functionality',
params: {}, params: {},
parameters: { parameters: {
type: 'object', type: 'object',
@@ -204,43 +204,42 @@ MAKE SURE YOU FULLY ANSWER THE USER'S QUESTION.
stream: false, // Always start with non-streaming to handle tool calls stream: false, // Always start with non-streaming to handle tool calls
}) })
// If this is a streaming request and we got a regular response,
// If this is a streaming request and we got a regular response,
// we need to create a streaming response from the content // we need to create a streaming response from the content
if (stream && typeof response === 'object' && 'content' in response) { if (stream && typeof response === 'object' && 'content' in response) {
const content = response.content || 'Sorry, I could not generate a response.' const content = response.content || 'Sorry, I could not generate a response.'
// Extract citations from the provider response for later use // Extract citations from the provider response for later use
const responseCitations = extractCitationsFromResponse(response) const responseCitations = extractCitationsFromResponse(response)
// Create a ReadableStream that emits the content in character chunks // Create a ReadableStream that emits the content in character chunks
const streamResponse = new ReadableStream({ const streamResponse = new ReadableStream({
start(controller) { start(controller) {
// Use character-based streaming for more reliable transmission // Use character-based streaming for more reliable transmission
const chunkSize = 8 // Stream 8 characters at a time for smooth experience const chunkSize = 8 // Stream 8 characters at a time for smooth experience
let index = 0 let index = 0
const pushNext = () => { const pushNext = () => {
if (index < content.length) { if (index < content.length) {
const chunk = content.slice(index, index + chunkSize) const chunk = content.slice(index, index + chunkSize)
controller.enqueue(new TextEncoder().encode(chunk)) controller.enqueue(new TextEncoder().encode(chunk))
index += chunkSize index += chunkSize
// Add a small delay to simulate streaming // Add a small delay to simulate streaming
setTimeout(pushNext, 25) setTimeout(pushNext, 25)
} else { } else {
controller.close() controller.close()
} }
} }
pushNext() pushNext()
} },
}) })
// Store citations for later use in the main streaming handler // Store citations for later use in the main streaming handler
;(streamResponse as any)._citations = responseCitations ;(streamResponse as any)._citations = responseCitations
return streamResponse return streamResponse
} }
@@ -252,7 +251,9 @@ MAKE SURE YOU FULLY ANSWER THE USER'S QUESTION.
return 'Sorry, I could not generate a response.' return 'Sorry, I could not generate a response.'
} catch (error) { } catch (error) {
logger.error('Failed to generate chat response:', error) logger.error('Failed to generate chat response:', error)
throw new Error(`Failed to generate response: ${error instanceof Error ? error.message : 'Unknown error'}`) throw new Error(
`Failed to generate response: ${error instanceof Error ? error.message : 'Unknown error'}`
)
} }
} }
@@ -268,7 +269,7 @@ export async function POST(req: NextRequest) {
const { message, chatId, workflowId, createNewChat, stream } = CopilotChatSchema.parse(body) const { message, chatId, workflowId, createNewChat, stream } = CopilotChatSchema.parse(body)
const session = await getSession() const session = await getSession()
logger.info(`[${requestId}] Copilot chat message: "${message}"`, { logger.info(`[${requestId}] Copilot chat message: "${message}"`, {
chatId, chatId,
workflowId, workflowId,
@@ -285,12 +286,7 @@ export async function POST(req: NextRequest) {
const [existingChat] = await db const [existingChat] = await db
.select() .select()
.from(copilotChats) .from(copilotChats)
.where( .where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, session.user.id)))
and(
eq(copilotChats.id, chatId),
eq(copilotChats.userId, session.user.id)
)
)
.limit(1) .limit(1)
if (existingChat) { if (existingChat) {
@@ -326,11 +322,11 @@ export async function POST(req: NextRequest) {
const encoder = new TextEncoder() const encoder = new TextEncoder()
// Extract citations from the stream object if available // Extract citations from the stream object if available
const citations = (response as any)._citations || [] const citations = (response as any)._citations || []
return new Response( return new Response(
new ReadableStream({ new ReadableStream({
async start(controller) { async start(controller) {
const reader = response.getReader() const reader = response.getReader()
let accumulatedResponse = '' let accumulatedResponse = ''
// Send initial metadata // Send initial metadata
@@ -352,7 +348,7 @@ export async function POST(req: NextRequest) {
const chunkText = new TextDecoder().decode(value) const chunkText = new TextDecoder().decode(value)
accumulatedResponse += chunkText accumulatedResponse += chunkText
const contentChunk = { const contentChunk = {
type: 'content', type: 'content',
content: chunkText, content: chunkText,
@@ -430,14 +426,22 @@ export async function POST(req: NextRequest) {
} }
// Extract citations from response if available // Extract citations from response if available
const citations = typeof response === 'object' && 'citations' in response ? response.citations : const citations =
typeof response === 'object' && 'toolResults' in response ? extractCitationsFromResponse(response) : [] typeof response === 'object' && 'citations' in response
? response.citations
: typeof response === 'object' && 'toolResults' in response
? extractCitationsFromResponse(response)
: []
const assistantMessage = { const assistantMessage = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
role: 'assistant', role: 'assistant',
content: typeof response === 'string' ? response : content:
'content' in response ? response.content : '[Error generating response]', typeof response === 'string'
? response
: 'content' in response
? response.content
: '[Error generating response]',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
citations: citations.length > 0 ? citations : undefined, citations: citations.length > 0 ? citations : undefined,
} }
@@ -466,8 +470,12 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
response: typeof response === 'string' ? response : response:
'content' in response ? response.content : '[Error generating response]', typeof response === 'string'
? response
: 'content' in response
? response.content
: '[Error generating response]',
chatId: currentChat?.id, chatId: currentChat?.id,
metadata: { metadata: {
requestId, requestId,
@@ -485,4 +493,4 @@ export async function POST(req: NextRequest) {
logger.error(`[${requestId}] Copilot chat error:`, error) logger.error(`[${requestId}] Copilot chat error:`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
} }
} }
+13 -5
View File
@@ -93,10 +93,18 @@ export async function POST(req: NextRequest) {
// Step 3: Format the response with context and sources // Step 3: Format the response with context and sources
const context = chunks const context = chunks
.map((chunk, index) => { .map((chunk, index) => {
const headerText = typeof chunk.headerText === 'string' ? chunk.headerText : String(chunk.headerText || 'Untitled Section') const headerText =
const sourceDocument = typeof chunk.sourceDocument === 'string' ? chunk.sourceDocument : String(chunk.sourceDocument || 'Unknown Document') typeof chunk.headerText === 'string'
const sourceLink = typeof chunk.sourceLink === 'string' ? chunk.sourceLink : String(chunk.sourceLink || '#') ? chunk.headerText
const chunkText = typeof chunk.chunkText === 'string' ? chunk.chunkText : String(chunk.chunkText || '') : String(chunk.headerText || 'Untitled Section')
const sourceDocument =
typeof chunk.sourceDocument === 'string'
? chunk.sourceDocument
: String(chunk.sourceDocument || 'Unknown Document')
const sourceLink =
typeof chunk.sourceLink === 'string' ? chunk.sourceLink : String(chunk.sourceLink || '#')
const chunkText =
typeof chunk.chunkText === 'string' ? chunk.chunkText : String(chunk.chunkText || '')
return `[${index + 1}] ${headerText} return `[${index + 1}] ${headerText}
Document: ${sourceDocument} Document: ${sourceDocument}
@@ -138,4 +146,4 @@ Content: ${chunkText}`
logger.error(`[${requestId}] Docs search error:`, error) logger.error(`[${requestId}] Docs search error:`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
} }
} }
@@ -208,7 +208,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(
const decoder = new TextDecoder() const decoder = new TextDecoder()
let accumulatedContent = '' let accumulatedContent = ''
let newChatId: string | undefined let newChatId: string | undefined
let responseCitations: Array<{id: number, title: string, url: string}> = [] let responseCitations: Array<{ id: number; title: string; url: string }> = []
while (true) { while (true) {
const { done, value } = await reader.read() const { done, value } = await reader.read()
@@ -241,7 +241,8 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(
? { ? {
...msg, ...msg,
content: accumulatedContent, content: accumulatedContent,
citations: responseCitations.length > 0 ? responseCitations : undefined, citations:
responseCitations.length > 0 ? responseCitations : undefined,
} }
: msg : msg
) )
@@ -254,12 +255,13 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(
? { ? {
...msg, ...msg,
content: accumulatedContent, content: accumulatedContent,
citations: responseCitations.length > 0 ? responseCitations : undefined, citations:
responseCitations.length > 0 ? responseCitations : undefined,
} }
: msg : msg
) )
) )
// Update current chat state with the chatId from response // Update current chat state with the chatId from response
if (newChatId && !currentChat) { if (newChatId && !currentChat) {
// For new chats, create a temporary chat object and reload the full chat list // For new chats, create a temporary chat object and reload the full chat list
+4 -2
View File
@@ -68,6 +68,8 @@ export const docsSearchTool: ToolConfig<DocsSearchParams, DocsSearchResponse> =
}, },
transformError: (error) => { transformError: (error) => {
return error instanceof Error ? error.message : 'An error occurred while searching documentation' return error instanceof Error
? error.message
: 'An error occurred while searching documentation'
}, },
} }
+1 -1
View File
@@ -2,8 +2,8 @@ import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger' import { createLogger } from '@/lib/logs/console-logger'
import { useCustomToolsStore } from '@/stores/custom-tools/store' import { useCustomToolsStore } from '@/stores/custom-tools/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store' import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { tools } from './registry'
import { docsSearchTool } from './docs/search' import { docsSearchTool } from './docs/search'
import { tools } from './registry'
import type { TableRow, ToolConfig, ToolResponse } from './types' import type { TableRow, ToolConfig, ToolResponse } from './types'
const logger = createLogger('ToolsUtils') const logger = createLogger('ToolsUtils')