This commit is contained in:
Siddharth Ganesan
2025-07-08 19:14:57 -07:00
parent d1fe209d29
commit 02c41127c2
9 changed files with 144 additions and 121 deletions

View File

@@ -1,14 +1,14 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import {
createChat,
generateChatTitle,
generateDocsResponse,
getChat,
createChat,
updateChat,
generateChatTitle,
} from '@/lib/copilot/service'
import { createLogger } from '@/lib/logs/console-logger'
const logger = createLogger('CopilotDocsAPI')
@@ -254,4 +254,4 @@ export async function POST(req: NextRequest) {
logger.error(`[${requestId}] Copilot docs error:`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
}

View File

@@ -1,16 +1,8 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createChat, deleteChat, getChat, listChats, sendMessage } from '@/lib/copilot/service'
import { createLogger } from '@/lib/logs/console-logger'
import {
sendMessage,
createChat,
getChat,
listChats,
deleteChat,
generateDocsResponse,
type CopilotMessage,
} from '@/lib/copilot/service'
const logger = createLogger('CopilotAPI')

View File

@@ -2,13 +2,13 @@ import { and, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { getCopilotConfig, getCopilotModel } from '@/lib/copilot/config'
import { createLogger } from '@/lib/logs/console-logger'
import { generateEmbeddings } from '@/app/api/knowledge/utils'
import { db } from '@/db'
import { copilotChats, docsEmbeddings } from '@/db/schema'
import { executeProviderRequest } from '@/providers'
import { getApiKey } from '@/providers/utils'
import { getCopilotConfig, getCopilotModel } from '@/lib/copilot/config'
const logger = createLogger('DocsRAG')
@@ -33,7 +33,7 @@ async function generateChatTitle(userMessage: string): Promise<string> {
let apiKey: string
try {
// Use rotating key directly for hosted providers
if ((provider === 'openai' || provider === 'anthropic')) {
if (provider === 'openai' || provider === 'anthropic') {
const { getRotatingApiKey } = require('@/lib/utils')
apiKey = getRotatingApiKey(provider)
} else {
@@ -120,7 +120,7 @@ async function generateResponse(
conversationHistory: any[] = []
): Promise<string | ReadableStream> {
const config = getCopilotConfig()
// Determine which provider and model to use - allow overrides
const selectedProvider = provider || config.rag.defaultProvider
const selectedModel = model || config.rag.defaultModel
@@ -129,7 +129,7 @@ async function generateResponse(
let apiKey: string
try {
// Use rotating key directly for hosted providers
if ((selectedProvider === 'openai' || selectedProvider === 'anthropic')) {
if (selectedProvider === 'openai' || selectedProvider === 'anthropic') {
const { getRotatingApiKey } = require('@/lib/utils')
apiKey = getRotatingApiKey(selectedProvider)
} else {
@@ -137,7 +137,9 @@ async function generateResponse(
}
} catch (error) {
logger.error(`Failed to get API key for ${selectedProvider} ${selectedModel}:`, error)
throw new Error(`API key not configured for ${selectedProvider}. Please set up API keys for this provider or use a different one.`)
throw new Error(
`API key not configured for ${selectedProvider}. Please set up API keys for this provider or use a different one.`
)
}
// Format chunks as context with numbered sources

View File

@@ -21,9 +21,9 @@ import {
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { createLogger } from '@/lib/logs/console-logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useCopilotStore } from '@/stores/copilot/store'
import type { CopilotMessage } from '@/stores/copilot/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { CopilotModal } from './components/copilot-modal/copilot-modal'
const logger = createLogger('Copilot')
@@ -56,7 +56,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(
const scrollAreaRef = useRef<HTMLDivElement>(null)
const { activeWorkflowId } = useWorkflowRegistry()
// Use the new copilot store
const {
currentChat,
@@ -128,8 +128,8 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(
const handleSubmit = useCallback(
async (e: React.FormEvent, message?: string) => {
e.preventDefault()
const query = message || (inputRef.current?.value?.trim() || '')
const query = message || inputRef.current?.value?.trim() || ''
if (!query || isSendingMessage || !activeWorkflowId) return
// Clear input if using the form input
@@ -408,12 +408,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(
className='flex-1'
autoComplete='off'
/>
<Button
type='submit'
size='icon'
disabled={isSendingMessage}
className='h-10 w-10'
>
<Button type='submit' size='icon' disabled={isSendingMessage} className='h-10 w-10'>
{isSendingMessage ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (

View File

@@ -72,7 +72,7 @@ When you reference information from documentation sources, use this format:
- Each source should only be cited once in your response
- Continue your full response after adding citations - don't stop mid-answer
IMPORTANT: Always provide complete, helpful responses. If you add citations, continue writing your full answer. Do not stop your response after adding a citation.`
IMPORTANT: Always provide complete, helpful responses. If you add citations, continue writing your full answer. Do not stop your response after adding a citation.`,
},
rag: {
defaultProvider: 'anthropic',
@@ -81,13 +81,13 @@ IMPORTANT: Always provide complete, helpful responses. If you add citations, con
maxTokens: 2000,
embeddingModel: 'text-embedding-3-small',
maxSources: 5,
similarityThreshold: 0.7
similarityThreshold: 0.7,
},
general: {
streamingEnabled: true,
maxConversationHistory: 10,
titleGenerationModel: 'claude-3-haiku-20240307' // Faster model for titles
}
titleGenerationModel: 'claude-3-haiku-20240307', // Faster model for titles
},
}
/**
@@ -129,7 +129,9 @@ export function getCopilotConfig(): CopilotConfig {
config.rag.maxSources = Number.parseInt(process.env.COPILOT_RAG_MAX_SOURCES)
}
if (process.env.COPILOT_RAG_SIMILARITY_THRESHOLD) {
config.rag.similarityThreshold = Number.parseFloat(process.env.COPILOT_RAG_SIMILARITY_THRESHOLD)
config.rag.similarityThreshold = Number.parseFloat(
process.env.COPILOT_RAG_SIMILARITY_THRESHOLD
)
}
// General configuration overrides
@@ -137,7 +139,9 @@ export function getCopilotConfig(): CopilotConfig {
config.general.streamingEnabled = process.env.COPILOT_STREAMING_ENABLED === 'true'
}
if (process.env.COPILOT_MAX_CONVERSATION_HISTORY) {
config.general.maxConversationHistory = Number.parseInt(process.env.COPILOT_MAX_CONVERSATION_HISTORY)
config.general.maxConversationHistory = Number.parseInt(
process.env.COPILOT_MAX_CONVERSATION_HISTORY
)
}
logger.info('Copilot configuration loaded', {
@@ -145,7 +149,7 @@ export function getCopilotConfig(): CopilotConfig {
chatModel: config.chat.defaultModel,
ragProvider: config.rag.defaultProvider,
ragModel: config.rag.defaultModel,
streamingEnabled: config.general.streamingEnabled
streamingEnabled: config.general.streamingEnabled,
})
} catch (error) {
logger.warn('Error applying environment variable overrides, using defaults', { error })
@@ -157,24 +161,27 @@ export function getCopilotConfig(): CopilotConfig {
/**
* Get the model to use for a specific copilot function
*/
export function getCopilotModel(type: 'chat' | 'rag' | 'title'): { provider: ProviderId; model: string } {
export function getCopilotModel(type: 'chat' | 'rag' | 'title'): {
provider: ProviderId
model: string
} {
const config = getCopilotConfig()
switch (type) {
case 'chat':
return {
provider: config.chat.defaultProvider,
model: config.chat.defaultModel
model: config.chat.defaultModel,
}
case 'rag':
return {
provider: config.rag.defaultProvider,
model: config.rag.defaultModel
model: config.rag.defaultModel,
}
case 'title':
return {
provider: config.chat.defaultProvider, // Use same provider as chat
model: config.general.titleGenerationModel
model: config.general.titleGenerationModel,
}
default:
throw new Error(`Unknown copilot model type: ${type}`)
@@ -184,7 +191,10 @@ export function getCopilotModel(type: 'chat' | 'rag' | 'title'): { provider: Pro
/**
* Validate that a provider/model combination is available
*/
export function validateCopilotConfig(config: CopilotConfig): { isValid: boolean; errors: string[] } {
export function validateCopilotConfig(config: CopilotConfig): {
isValid: boolean
errors: string[]
} {
const errors: string[] = []
// Validate chat provider/model
@@ -232,6 +242,6 @@ export function validateCopilotConfig(config: CopilotConfig): { isValid: boolean
return {
isValid: errors.length === 0,
errors
errors,
}
}
}

View File

@@ -1,10 +1,10 @@
import { and, eq } from 'drizzle-orm'
import { createLogger } from '@/lib/logs/console-logger'
import { getApiKey } from '@/providers/utils'
import { executeProviderRequest } from '@/providers'
import { db } from '@/db'
import { copilotChats, embedding, knowledgeBase, document } from '@/db/schema'
import type { ProviderId, ProviderToolConfig } from '@/providers/types'
import { copilotChats } from '@/db/schema'
import { executeProviderRequest } from '@/providers'
import type { ProviderToolConfig } from '@/providers/types'
import { getApiKey } from '@/providers/utils'
import { getCopilotConfig, getCopilotModel } from './config'
const logger = createLogger('CopilotService')
@@ -74,7 +74,7 @@ export async function generateChatTitle(userMessage: string): Promise<string> {
let apiKey: string
try {
// Use rotating key directly for hosted providers
if ((provider === 'openai' || provider === 'anthropic')) {
if (provider === 'openai' || provider === 'anthropic') {
const { getRotatingApiKey } = require('@/lib/utils')
apiKey = getRotatingApiKey(provider)
} else {
@@ -87,7 +87,8 @@ export async function generateChatTitle(userMessage: string): Promise<string> {
const response = await executeProviderRequest(provider, {
model,
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}"\n\nReturn only the title text, nothing else.`,
temperature: 0.3,
maxTokens: 50,
@@ -115,27 +116,29 @@ export async function searchDocumentation(
topK?: number
threshold?: number
} = {}
): Promise<Array<{
id: number
title: string
url: string
content: string
similarity: number
}>> {
): Promise<
Array<{
id: number
title: string
url: string
content: string
similarity: number
}>
> {
const { generateEmbeddings } = require('@/app/api/knowledge/utils')
const { docsEmbeddings } = require('@/db/schema')
const { sql } = require('drizzle-orm')
const config = getCopilotConfig()
const { topK = config.rag.maxSources, threshold = config.rag.similarityThreshold } = options
try {
logger.info('Documentation search requested', { query, topK, threshold })
// Generate embedding for the query
const embeddings = await generateEmbeddings([query])
const queryEmbedding = embeddings[0]
if (!queryEmbedding || queryEmbedding.length === 0) {
logger.warn('Failed to generate query embedding')
return []
@@ -157,12 +160,12 @@ export async function searchDocumentation(
.limit(topK)
// Filter by similarity threshold
const filteredResults = results.filter(result => result.similarity >= threshold)
const filteredResults = results.filter((result) => result.similarity >= threshold)
logger.info(`Found ${filteredResults.length} relevant documentation chunks`, {
totalResults: results.length,
afterFiltering: filteredResults.length,
threshold
threshold,
})
return filteredResults.map((result, index) => ({
@@ -170,7 +173,7 @@ export async function searchDocumentation(
title: String(result.headerText || 'Untitled Section'),
url: String(result.sourceLink || '#'),
content: String(result.chunkText || ''),
similarity: result.similarity
similarity: result.similarity,
}))
} catch (error) {
logger.error('Failed to search documentation:', error)
@@ -203,11 +206,11 @@ export async function generateDocsResponse(
}> {
const config = getCopilotConfig()
const { provider, model } = getCopilotModel('rag')
const {
const {
stream = config.general.streamingEnabled,
topK = config.rag.maxSources,
provider: overrideProvider,
model: overrideModel
model: overrideModel,
} = options
const selectedProvider = overrideProvider || provider
@@ -217,25 +220,31 @@ export async function generateDocsResponse(
let apiKey: string
try {
// Use rotating key directly for hosted providers
if ((selectedProvider === 'openai' || selectedProvider === 'anthropic')) {
if (selectedProvider === 'openai' || selectedProvider === 'anthropic') {
const { getRotatingApiKey } = require('@/lib/utils')
apiKey = getRotatingApiKey(selectedProvider)
} else {
apiKey = getApiKey(selectedProvider, selectedModel)
}
} catch (error) {
logger.error(`Failed to get API key for docs response (${selectedProvider} ${selectedModel}):`, error)
throw new Error(`API key not configured for ${selectedProvider}. Please set up API keys for this provider or use a different one.`)
logger.error(
`Failed to get API key for docs response (${selectedProvider} ${selectedModel}):`,
error
)
throw new Error(
`API key not configured for ${selectedProvider}. Please set up API keys for this provider or use a different one.`
)
}
// Search documentation
const searchResults = await searchDocumentation(query, { topK })
if (searchResults.length === 0) {
const fallbackResponse = "I couldn't find any relevant documentation for your question. Please try rephrasing your query or check if you're asking about a feature that exists in Sim Studio."
const fallbackResponse =
"I couldn't find any relevant documentation for your question. Please try rephrasing your query or check if you're asking about a feature that exists in Sim Studio."
return {
response: fallbackResponse,
sources: []
sources: [],
}
}
@@ -289,7 +298,9 @@ The sources are numbered [1] through [${searchResults.length}] in the context be
Documentation Context:
${context}`
logger.info(`Generating docs response using provider: ${selectedProvider}, model: ${selectedModel}`)
logger.info(
`Generating docs response using provider: ${selectedProvider}, model: ${selectedModel}`
)
const response = await executeProviderRequest(selectedProvider, {
model: selectedModel,
@@ -333,11 +344,13 @@ ${context}`
return {
response: cleanedContent,
sources
sources,
}
} catch (error) {
logger.error('Failed to generate docs response:', error)
throw new Error(`Failed to generate docs response: ${error instanceof Error ? error.message : 'Unknown error'}`)
throw new Error(
`Failed to generate docs response: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
@@ -361,7 +374,7 @@ export async function generateChatResponse(
let apiKey: string
try {
// Use rotating key directly for hosted providers
if ((provider === 'openai' || provider === 'anthropic')) {
if (provider === 'openai' || provider === 'anthropic') {
const { getRotatingApiKey } = require('@/lib/utils')
apiKey = getRotatingApiKey(provider)
} else {
@@ -369,7 +382,9 @@ export async function generateChatResponse(
}
} catch (error) {
logger.error(`Failed to get API key for chat (${provider} ${model}):`, error)
throw new Error(`API key not configured for ${provider}. Please set up API keys for this provider or use a different one.`)
throw new Error(
`API key not configured for ${provider}. Please set up API keys for this provider or use a different one.`
)
}
// Build conversation context
@@ -378,7 +393,7 @@ export async function generateChatResponse(
// Add conversation history (limited by config)
const historyLimit = config.general.maxConversationHistory
const recentHistory = conversationHistory.slice(-historyLimit)
for (const msg of recentHistory) {
messages.push({
role: msg.role as 'user' | 'assistant' | 'system',
@@ -397,7 +412,8 @@ export async function generateChatResponse(
{
id: 'docs_search_internal',
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: {},
parameters: {
type: 'object',
@@ -441,7 +457,9 @@ export async function generateChatResponse(
return 'Sorry, I could not generate a response.'
} catch (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'}`
)
}
}
@@ -502,7 +520,9 @@ export async function createChat(
}
} catch (error) {
logger.error('Failed to create chat:', error)
throw new Error(`Failed to create chat: ${error instanceof Error ? error.message : 'Unknown error'}`)
throw new Error(
`Failed to create chat: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
@@ -558,7 +578,7 @@ export async function listChats(
.limit(limit)
.offset(offset)
return chats.map(chat => ({
return chats.map((chat) => ({
id: chat.id,
title: chat.title,
model: chat.model,
@@ -713,4 +733,4 @@ export async function sendMessage(request: SendMessageRequest): Promise<{
logger.error('Failed to send message:', error)
throw error
}
}
}

View File

@@ -1,8 +1,8 @@
export { useCopilotStore } from './store'
export type {
CopilotMessage,
CopilotChat,
CopilotState,
CopilotActions,
CopilotChat,
CopilotMessage,
CopilotState,
CopilotStore,
} from './types'
} from './types'

View File

@@ -1,16 +1,16 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console-logger'
import {
type CopilotChat,
type CopilotMessage,
createChat,
deleteChat as deleteApiChat,
getChat,
listChats,
sendStreamingMessage,
sendStreamingDocsMessage,
type CopilotChat,
type CopilotMessage,
sendStreamingMessage,
} from '@/lib/copilot-api'
import { createLogger } from '@/lib/logs/console-logger'
import type { CopilotStore } from './types'
const logger = createLogger('CopilotStore')
@@ -48,7 +48,7 @@ export const useCopilotStore = create<CopilotStore>()(
messages: [],
error: null,
})
// Load chats for the new workflow
if (workflowId) {
get().loadChats()
@@ -68,27 +68,27 @@ export const useCopilotStore = create<CopilotStore>()(
try {
const result = await listChats(workflowId)
if (result.success) {
set({
set({
chats: result.chats,
isLoadingChats: false,
})
// If no current chat and we have chats, optionally select the most recent one
const { currentChat } = get()
if (!currentChat && result.chats.length > 0) {
// Auto-select most recent chat
await get().selectChat(result.chats[0])
}
logger.info(`Loaded ${result.chats.length} chats for workflow ${workflowId}`)
} else {
throw new Error(result.error || 'Failed to load chats')
}
} catch (error) {
logger.error('Failed to load chats:', error)
set({
set({
error: error instanceof Error ? error.message : 'Failed to load chats',
isLoadingChats: false,
})
@@ -101,21 +101,21 @@ export const useCopilotStore = create<CopilotStore>()(
try {
const result = await getChat(chat.id)
if (result.success && result.chat) {
set({
currentChat: result.chat,
messages: result.chat.messages,
isLoading: false,
})
logger.info(`Selected chat: ${result.chat.title || 'Untitled'}`)
} else {
throw new Error(result.error || 'Failed to load chat')
}
} catch (error) {
logger.error('Failed to select chat:', error)
set({
set({
error: error instanceof Error ? error.message : 'Failed to load chat',
isLoading: false,
})
@@ -134,24 +134,24 @@ export const useCopilotStore = create<CopilotStore>()(
try {
const result = await createChat(workflowId, options)
if (result.success && result.chat) {
set({
currentChat: result.chat,
messages: result.chat.messages,
isLoading: false,
})
// Reload chats to include the new one
await get().loadChats()
logger.info(`Created new chat: ${result.chat.id}`)
} else {
throw new Error(result.error || 'Failed to create chat')
}
} catch (error) {
logger.error('Failed to create new chat:', error)
set({
set({
error: error instanceof Error ? error.message : 'Failed to create chat',
isLoading: false,
})
@@ -162,15 +162,15 @@ export const useCopilotStore = create<CopilotStore>()(
deleteChat: async (chatId: string) => {
try {
const result = await deleteApiChat(chatId)
if (result.success) {
const { currentChat } = get()
// Remove from chats list
set((state) => ({
chats: state.chats.filter((chat) => chat.id !== chatId),
}))
// If this was the current chat, clear it
if (currentChat?.id === chatId) {
set({
@@ -178,14 +178,14 @@ export const useCopilotStore = create<CopilotStore>()(
messages: [],
})
}
logger.info(`Deleted chat: ${chatId}`)
} else {
throw new Error(result.error || 'Failed to delete chat')
}
} catch (error) {
logger.error('Failed to delete chat:', error)
set({
set({
error: error instanceof Error ? error.message : 'Failed to delete chat',
})
}
@@ -239,12 +239,13 @@ export const useCopilotStore = create<CopilotStore>()(
}
} catch (error) {
logger.error('Failed to send message:', error)
// Replace streaming message with error
const errorMessage: CopilotMessage = {
id: streamingMessage.id,
role: 'assistant',
content: 'Sorry, I encountered an error while processing your message. Please try again.',
content:
'Sorry, I encountered an error while processing your message. Please try again.',
timestamp: new Date().toISOString(),
}
@@ -307,12 +308,13 @@ export const useCopilotStore = create<CopilotStore>()(
}
} catch (error) {
logger.error('Failed to send docs message:', error)
// Replace streaming message with error
const errorMessage: CopilotMessage = {
id: streamingMessage.id,
role: 'assistant',
content: 'Sorry, I encountered an error while searching the documentation. Please try again.',
content:
'Sorry, I encountered an error while searching the documentation. Please try again.',
timestamp: new Date().toISOString(),
}
@@ -374,7 +376,8 @@ export const useCopilotStore = create<CopilotStore>()(
? {
...msg,
content: accumulatedContent,
citations: responseCitations.length > 0 ? responseCitations : undefined,
citations:
responseCitations.length > 0 ? responseCitations : undefined,
}
: msg
),
@@ -387,7 +390,8 @@ export const useCopilotStore = create<CopilotStore>()(
? {
...msg,
content: accumulatedContent,
citations: responseCitations.length > 0 ? responseCitations : undefined,
citations:
responseCitations.length > 0 ? responseCitations : undefined,
}
: msg
),

View File

@@ -33,21 +33,21 @@ export interface CopilotChat {
export interface CopilotState {
// Current active chat
currentChat: CopilotChat | null
// List of available chats for current workflow
chats: CopilotChat[]
// Current messages (from active chat)
messages: CopilotMessage[]
// Loading states
isLoading: boolean
isLoadingChats: boolean
isSendingMessage: boolean
// Error state
error: string | null
// Current workflow ID (for chat context)
workflowId: string | null
}
@@ -62,16 +62,16 @@ export interface CopilotActions {
selectChat: (chat: CopilotChat) => Promise<void>
createNewChat: (options?: { title?: string; initialMessage?: string }) => Promise<void>
deleteChat: (chatId: string) => Promise<void>
// Message handling
sendMessage: (message: string, options?: { stream?: boolean }) => Promise<void>
sendDocsMessage: (query: string, options?: { stream?: boolean; topK?: number }) => Promise<void>
// Utility actions
clearMessages: () => void
clearError: () => void
reset: () => void
// Internal helper (not exposed publicly)
handleStreamingResponse: (stream: ReadableStream, messageId: string) => Promise<void>
}