mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-10 22:55:16 -05:00
Clean up code
This commit is contained in:
@@ -8,7 +8,7 @@ import { getSession } from '@/lib/auth'
|
||||
import { buildConversationHistory } from '@/lib/copilot/chat-context'
|
||||
import { resolveOrCreateChat } from '@/lib/copilot/chat-lifecycle'
|
||||
import { buildCopilotRequestPayload } from '@/lib/copilot/chat-payload'
|
||||
import { generateChatTitle } from '@/lib/copilot/chat-title'
|
||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
||||
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
|
||||
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
|
||||
import {
|
||||
@@ -23,10 +23,54 @@ import {
|
||||
createRequestTracker,
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/request-helpers'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { resolveWorkflowIdForUser } from '@/lib/workflows/utils'
|
||||
|
||||
const logger = createLogger('CopilotChatAPI')
|
||||
|
||||
async function requestChatTitleFromCopilot(params: {
|
||||
message: string
|
||||
model: string
|
||||
provider?: string
|
||||
}): Promise<string | null> {
|
||||
const { message, model, provider } = params
|
||||
if (!message || !model) return null
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (env.COPILOT_API_KEY) {
|
||||
headers['x-api-key'] = env.COPILOT_API_KEY
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${SIM_AGENT_API_URL}/api/generate-chat-title`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
model,
|
||||
...(provider ? { provider } : {}),
|
||||
}),
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
logger.warn('Failed to generate chat title via copilot backend', {
|
||||
status: response.status,
|
||||
error: payload,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const title = typeof payload?.title === 'string' ? payload.title.trim() : ''
|
||||
return title || null
|
||||
} catch (error) {
|
||||
logger.error('Error generating chat title:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const FileAttachmentSchema = z.object({
|
||||
id: z.string(),
|
||||
key: z.string(),
|
||||
@@ -280,7 +324,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
if (actualChatId && !currentChat?.title && conversationHistory.length === 0) {
|
||||
generateChatTitle({ message, model: selectedModel, provider })
|
||||
requestChatTitleFromCopilot({ message, model: selectedModel, provider })
|
||||
.then(async (title) => {
|
||||
if (title) {
|
||||
await db
|
||||
@@ -407,7 +451,7 @@ export async function POST(req: NextRequest) {
|
||||
// Start title generation in parallel if this is first message (non-streaming)
|
||||
if (actualChatId && !currentChat.title && conversationHistory.length === 0) {
|
||||
logger.info(`[${tracker.requestId}] Starting title generation for non-streaming response`)
|
||||
generateChatTitle({ message, model: selectedModel, provider })
|
||||
requestChatTitleFromCopilot({ message, model: selectedModel, provider })
|
||||
.then(async (title) => {
|
||||
if (title) {
|
||||
await db
|
||||
|
||||
@@ -624,7 +624,7 @@ async function handleBuildToolCall(
|
||||
): Promise<CallToolResult> {
|
||||
try {
|
||||
const requestText = (args.request as string) || JSON.stringify(args)
|
||||
const { model } = getCopilotModel('chat')
|
||||
const { model } = getCopilotModel()
|
||||
const workflowId = args.workflowId as string | undefined
|
||||
|
||||
const resolved = workflowId ? { workflowId } : await resolveWorkflowIdForUser(userId)
|
||||
@@ -721,7 +721,7 @@ async function handleSubagentToolCall(
|
||||
context.plan = args.plan
|
||||
}
|
||||
|
||||
const { model } = getCopilotModel('chat')
|
||||
const { model } = getCopilotModel()
|
||||
|
||||
const result = await orchestrateSubagentStream(
|
||||
toolDef.agentId,
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const parsed = RequestSchema.parse(body)
|
||||
const defaults = getCopilotModel('chat')
|
||||
const defaults = getCopilotModel()
|
||||
const selectedModel = parsed.model || defaults.model
|
||||
|
||||
// Resolve workflow ID
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
|
||||
const logger = createLogger('SimAgentUtils')
|
||||
|
||||
interface GenerateChatTitleParams {
|
||||
message: string
|
||||
model: string
|
||||
provider?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a short title for a chat based on the first message
|
||||
* using the Copilot backend's server-side provider configuration.
|
||||
*/
|
||||
export async function generateChatTitle({
|
||||
message,
|
||||
model,
|
||||
provider,
|
||||
}: GenerateChatTitleParams): Promise<string | null> {
|
||||
if (!message || !model) {
|
||||
return null
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (env.COPILOT_API_KEY) {
|
||||
headers['x-api-key'] = env.COPILOT_API_KEY
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${SIM_AGENT_API_URL}/api/generate-chat-title`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
model,
|
||||
...(provider ? { provider } : {}),
|
||||
}),
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
logger.warn('Failed to generate chat title via copilot backend', {
|
||||
status: response.status,
|
||||
error: payload,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const title = typeof payload?.title === 'string' ? payload.title.trim() : ''
|
||||
return title || null
|
||||
} catch (error) {
|
||||
logger.error('Error generating chat title:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { AGENT_MODE_SYSTEM_PROMPT } from '@/lib/copilot/prompts'
|
||||
import { getProviderDefaultModel } from '@/providers/models'
|
||||
import type { ProviderId } from '@/providers/types'
|
||||
|
||||
const logger = createLogger('CopilotConfig')
|
||||
|
||||
/**
|
||||
* Configuration validation constraints
|
||||
*/
|
||||
const VALIDATION_CONSTRAINTS = {
|
||||
temperature: { min: 0, max: 2 },
|
||||
maxTokens: { min: 1, max: 100000 },
|
||||
maxSources: { min: 1, max: 20 },
|
||||
similarityThreshold: { min: 0, max: 1 },
|
||||
maxConversationHistory: { min: 1, max: 50 },
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Copilot model types
|
||||
*/
|
||||
export type CopilotModelType = 'chat' | 'rag' | 'title'
|
||||
|
||||
/**
|
||||
* Configuration validation result
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
isValid: boolean
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Copilot configuration interface
|
||||
*/
|
||||
export interface CopilotConfig {
|
||||
// Chat LLM configuration
|
||||
chat: {
|
||||
defaultProvider: ProviderId
|
||||
defaultModel: string
|
||||
temperature: number
|
||||
maxTokens: number
|
||||
systemPrompt: string
|
||||
}
|
||||
// RAG (documentation search) LLM configuration
|
||||
rag: {
|
||||
defaultProvider: ProviderId
|
||||
defaultModel: string
|
||||
temperature: number
|
||||
maxTokens: number
|
||||
embeddingModel: string
|
||||
maxSources: number
|
||||
similarityThreshold: number
|
||||
}
|
||||
// General configuration
|
||||
general: {
|
||||
streamingEnabled: boolean
|
||||
maxConversationHistory: number
|
||||
titleGenerationModel: string
|
||||
}
|
||||
}
|
||||
|
||||
function parseFloatEnv(value: string | undefined, name: string): number | null {
|
||||
if (!value) return null
|
||||
const parsed = Number.parseFloat(value)
|
||||
if (Number.isNaN(parsed)) {
|
||||
logger.warn(`Invalid ${name}: ${value}. Expected a valid number.`)
|
||||
return null
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
function parseIntEnv(value: string | undefined, name: string): number | null {
|
||||
if (!value) return null
|
||||
const parsed = Number.parseInt(value, 10)
|
||||
if (Number.isNaN(parsed)) {
|
||||
logger.warn(`Invalid ${name}: ${value}. Expected a valid integer.`)
|
||||
return null
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
function parseBooleanEnv(value: string | undefined): boolean | null {
|
||||
if (!value) return null
|
||||
return value.toLowerCase() === 'true'
|
||||
}
|
||||
|
||||
export const DEFAULT_COPILOT_CONFIG: CopilotConfig = {
|
||||
chat: {
|
||||
defaultProvider: 'anthropic',
|
||||
defaultModel: 'claude-4.6-opus',
|
||||
temperature: 0.1,
|
||||
maxTokens: 8192,
|
||||
systemPrompt: AGENT_MODE_SYSTEM_PROMPT,
|
||||
},
|
||||
rag: {
|
||||
defaultProvider: 'anthropic',
|
||||
defaultModel: 'claude-4.6-opus',
|
||||
temperature: 0.1,
|
||||
maxTokens: 2000,
|
||||
embeddingModel: 'text-embedding-3-small',
|
||||
maxSources: 10,
|
||||
similarityThreshold: 0.3,
|
||||
},
|
||||
general: {
|
||||
streamingEnabled: true,
|
||||
maxConversationHistory: 10,
|
||||
titleGenerationModel: 'claude-3-haiku-20240307',
|
||||
},
|
||||
}
|
||||
|
||||
function applyEnvironmentOverrides(config: CopilotConfig): void {
|
||||
const chatTemperature = parseFloatEnv(
|
||||
process.env.COPILOT_CHAT_TEMPERATURE,
|
||||
'COPILOT_CHAT_TEMPERATURE'
|
||||
)
|
||||
if (chatTemperature !== null) {
|
||||
config.chat.temperature = chatTemperature
|
||||
}
|
||||
|
||||
const chatMaxTokens = parseIntEnv(process.env.COPILOT_CHAT_MAX_TOKENS, 'COPILOT_CHAT_MAX_TOKENS')
|
||||
if (chatMaxTokens !== null) {
|
||||
config.chat.maxTokens = chatMaxTokens
|
||||
}
|
||||
|
||||
const ragTemperature = parseFloatEnv(
|
||||
process.env.COPILOT_RAG_TEMPERATURE,
|
||||
'COPILOT_RAG_TEMPERATURE'
|
||||
)
|
||||
if (ragTemperature !== null) {
|
||||
config.rag.temperature = ragTemperature
|
||||
}
|
||||
|
||||
const ragMaxTokens = parseIntEnv(process.env.COPILOT_RAG_MAX_TOKENS, 'COPILOT_RAG_MAX_TOKENS')
|
||||
if (ragMaxTokens !== null) {
|
||||
config.rag.maxTokens = ragMaxTokens
|
||||
}
|
||||
|
||||
const ragMaxSources = parseIntEnv(process.env.COPILOT_RAG_MAX_SOURCES, 'COPILOT_RAG_MAX_SOURCES')
|
||||
if (ragMaxSources !== null) {
|
||||
config.rag.maxSources = ragMaxSources
|
||||
}
|
||||
|
||||
const ragSimilarityThreshold = parseFloatEnv(
|
||||
process.env.COPILOT_RAG_SIMILARITY_THRESHOLD,
|
||||
'COPILOT_RAG_SIMILARITY_THRESHOLD'
|
||||
)
|
||||
if (ragSimilarityThreshold !== null) {
|
||||
config.rag.similarityThreshold = ragSimilarityThreshold
|
||||
}
|
||||
|
||||
const streamingEnabled = parseBooleanEnv(process.env.COPILOT_STREAMING_ENABLED)
|
||||
if (streamingEnabled !== null) {
|
||||
config.general.streamingEnabled = streamingEnabled
|
||||
}
|
||||
|
||||
const maxConversationHistory = parseIntEnv(
|
||||
process.env.COPILOT_MAX_CONVERSATION_HISTORY,
|
||||
'COPILOT_MAX_CONVERSATION_HISTORY'
|
||||
)
|
||||
if (maxConversationHistory !== null) {
|
||||
config.general.maxConversationHistory = maxConversationHistory
|
||||
}
|
||||
|
||||
if (process.env.COPILOT_TITLE_GENERATION_MODEL) {
|
||||
config.general.titleGenerationModel = process.env.COPILOT_TITLE_GENERATION_MODEL
|
||||
}
|
||||
}
|
||||
|
||||
export function getCopilotConfig(): CopilotConfig {
|
||||
const config = structuredClone(DEFAULT_COPILOT_CONFIG)
|
||||
|
||||
try {
|
||||
applyEnvironmentOverrides(config)
|
||||
} catch (error) {
|
||||
logger.warn('Error applying environment variable overrides, using defaults', { error })
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export function getCopilotModel(type: CopilotModelType): {
|
||||
provider: ProviderId
|
||||
model: string
|
||||
} {
|
||||
const config = getCopilotConfig()
|
||||
|
||||
switch (type) {
|
||||
case 'chat':
|
||||
return {
|
||||
provider: config.chat.defaultProvider,
|
||||
model: config.chat.defaultModel,
|
||||
}
|
||||
case 'rag':
|
||||
return {
|
||||
provider: config.rag.defaultProvider,
|
||||
model: config.rag.defaultModel,
|
||||
}
|
||||
case 'title':
|
||||
return {
|
||||
provider: config.chat.defaultProvider,
|
||||
model: config.general.titleGenerationModel,
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown copilot model type: ${type}`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumericValue(
|
||||
value: number,
|
||||
constraint: { min: number; max: number },
|
||||
name: string
|
||||
): string | null {
|
||||
if (value < constraint.min || value > constraint.max) {
|
||||
return `${name} must be between ${constraint.min} and ${constraint.max}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function validateCopilotConfig(config: CopilotConfig): ValidationResult {
|
||||
const errors: string[] = []
|
||||
|
||||
try {
|
||||
const chatDefaultModel = getProviderDefaultModel(config.chat.defaultProvider)
|
||||
if (!chatDefaultModel) {
|
||||
errors.push(`Chat provider '${config.chat.defaultProvider}' not found`)
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Invalid chat provider: ${config.chat.defaultProvider}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const ragDefaultModel = getProviderDefaultModel(config.rag.defaultProvider)
|
||||
if (!ragDefaultModel) {
|
||||
errors.push(`RAG provider '${config.rag.defaultProvider}' not found`)
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Invalid RAG provider: ${config.rag.defaultProvider}`)
|
||||
}
|
||||
|
||||
const validationChecks = [
|
||||
{
|
||||
value: config.chat.temperature,
|
||||
constraint: VALIDATION_CONSTRAINTS.temperature,
|
||||
name: 'Chat temperature',
|
||||
},
|
||||
{
|
||||
value: config.rag.temperature,
|
||||
constraint: VALIDATION_CONSTRAINTS.temperature,
|
||||
name: 'RAG temperature',
|
||||
},
|
||||
{
|
||||
value: config.chat.maxTokens,
|
||||
constraint: VALIDATION_CONSTRAINTS.maxTokens,
|
||||
name: 'Chat maxTokens',
|
||||
},
|
||||
{
|
||||
value: config.rag.maxTokens,
|
||||
constraint: VALIDATION_CONSTRAINTS.maxTokens,
|
||||
name: 'RAG maxTokens',
|
||||
},
|
||||
{
|
||||
value: config.rag.maxSources,
|
||||
constraint: VALIDATION_CONSTRAINTS.maxSources,
|
||||
name: 'RAG maxSources',
|
||||
},
|
||||
{
|
||||
value: config.rag.similarityThreshold,
|
||||
constraint: VALIDATION_CONSTRAINTS.similarityThreshold,
|
||||
name: 'RAG similarityThreshold',
|
||||
},
|
||||
{
|
||||
value: config.general.maxConversationHistory,
|
||||
constraint: VALIDATION_CONSTRAINTS.maxConversationHistory,
|
||||
name: 'General maxConversationHistory',
|
||||
},
|
||||
]
|
||||
|
||||
for (const check of validationChecks) {
|
||||
const error = validateNumericValue(check.value, check.constraint, check.name)
|
||||
if (error) {
|
||||
errors.push(error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ interface DocsSearchParams {
|
||||
threshold?: number
|
||||
}
|
||||
|
||||
const DEFAULT_DOCS_SIMILARITY_THRESHOLD = 0.3
|
||||
|
||||
export const searchDocumentationServerTool: BaseServerTool<DocsSearchParams, any> = {
|
||||
name: 'search_documentation',
|
||||
async execute(params: DocsSearchParams): Promise<any> {
|
||||
@@ -19,9 +21,7 @@ export const searchDocumentationServerTool: BaseServerTool<DocsSearchParams, any
|
||||
|
||||
logger.info('Executing docs search', { query, topK })
|
||||
|
||||
const { getCopilotConfig } = await import('@/lib/copilot/config')
|
||||
const config = getCopilotConfig()
|
||||
const similarityThreshold = threshold ?? config.rag.similarityThreshold
|
||||
const similarityThreshold = threshold ?? DEFAULT_DOCS_SIMILARITY_THRESHOLD
|
||||
|
||||
const { generateSearchEmbedding } = await import('@/lib/knowledge/embeddings')
|
||||
const queryEmbedding = await generateSearchEmbedding(query)
|
||||
|
||||
Reference in New Issue
Block a user