Files
sim/apps/sim/lib/copilot/api.ts
Siddharth Ganesan f959d78741 Fix azure anthropic
2026-02-10 11:53:08 -08:00

237 lines
5.8 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { COPILOT_CHAT_API_PATH, COPILOT_CHAT_STREAM_API_PATH } from '@/lib/copilot/constants'
import type { CopilotMode, CopilotModelId, CopilotTransportMode } from '@/lib/copilot/models'
const logger = createLogger('CopilotAPI')
/**
* Citation interface for documentation references
*/
export interface Citation {
id: number
title: string
url: string
similarity?: number
}
/**
* Message interface for copilot conversations
*/
export interface CopilotMessage {
id: string
role: 'user' | 'assistant' | 'system'
content: string
timestamp: string
citations?: Citation[]
}
/**
* Chat config stored in database
*/
export interface CopilotChatConfig {
mode?: CopilotMode
model?: CopilotModelId
}
/**
* Chat interface for copilot conversations
*/
export interface CopilotChat {
id: string
title: string | null
model: string
messages: CopilotMessage[]
messageCount: number
planArtifact: string | null
config: CopilotChatConfig | null
createdAt: Date
updatedAt: Date
}
/**
* File attachment interface for message requests
*/
export interface MessageFileAttachment {
id: string
key: string
filename: string
media_type: string
size: number
}
/**
* Request interface for sending messages
*/
export interface SendMessageRequest {
message: string
userMessageId?: string // ID from frontend for the user message
chatId?: string
workflowId?: string
mode?: CopilotMode | CopilotTransportMode
model?: CopilotModelId
provider?: string
prefetch?: boolean
createNewChat?: boolean
stream?: boolean
implicitFeedback?: string
fileAttachments?: MessageFileAttachment[]
abortSignal?: AbortSignal
contexts?: Array<{
kind: string
label?: string
chatId?: string
workflowId?: string
executionId?: string
}>
commands?: string[]
resumeFromEventId?: number
}
/**
* Base API response interface
*/
export interface ApiResponse {
success: boolean
error?: string
status?: number
}
/**
* Streaming response interface
*/
export interface StreamingResponse extends ApiResponse {
stream?: ReadableStream
}
/**
* Handle API errors and return user-friendly error messages
*/
async function handleApiError(response: Response, defaultMessage: string): Promise<string> {
try {
const data = await response.json()
return (data && (data.error || data.message)) || defaultMessage
} catch {
return `${defaultMessage} (${response.status})`
}
}
/**
* Send a streaming message to the copilot chat API
* This is the main API endpoint that handles all chat operations
*/
export async function sendStreamingMessage(
request: SendMessageRequest
): Promise<StreamingResponse> {
try {
const { abortSignal, resumeFromEventId, ...requestBody } = request
try {
const preview = Array.isArray((requestBody as any).contexts)
? (requestBody as any).contexts.map((c: any) => ({
kind: c?.kind,
chatId: c?.chatId,
workflowId: c?.workflowId,
label: c?.label,
}))
: undefined
logger.info('Preparing to send streaming message', {
hasContexts: Array.isArray((requestBody as any).contexts),
contextsCount: Array.isArray((requestBody as any).contexts)
? (requestBody as any).contexts.length
: 0,
contextsPreview: preview,
resumeFromEventId,
})
} catch (error) {
logger.warn('Failed to log streaming message context preview', {
error: error instanceof Error ? error.message : String(error),
})
}
const streamId = request.userMessageId
if (typeof resumeFromEventId === 'number') {
if (!streamId) {
return {
success: false,
error: 'streamId is required to resume a stream',
status: 400,
}
}
const url = `${COPILOT_CHAT_STREAM_API_PATH}?streamId=${encodeURIComponent(
streamId
)}&from=${encodeURIComponent(String(resumeFromEventId))}`
const response = await fetch(url, {
method: 'GET',
signal: abortSignal,
credentials: 'include',
})
if (!response.ok) {
const errorMessage = await handleApiError(response, 'Failed to resume streaming message')
return {
success: false,
error: errorMessage,
status: response.status,
}
}
if (!response.body) {
return {
success: false,
error: 'No response body received',
status: 500,
}
}
return {
success: true,
stream: response.body,
}
}
const response = await fetch(COPILOT_CHAT_API_PATH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...requestBody, stream: true }),
signal: abortSignal,
credentials: 'include', // Include cookies for session authentication
})
if (!response.ok) {
const errorMessage = await handleApiError(response, 'Failed to send streaming message')
return {
success: false,
error: errorMessage,
status: response.status,
}
}
if (!response.body) {
return {
success: false,
error: 'No response body received',
status: 500,
}
}
return {
success: true,
stream: response.body,
}
} catch (error) {
// Handle AbortError gracefully - this is expected when user aborts
if (error instanceof Error && error.name === 'AbortError') {
logger.info('Streaming message was aborted by user')
return {
success: false,
error: 'Request was aborted',
}
}
logger.error('Failed to send streaming message:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}