/** * AI Client for Control Center * CRESyncFlow - Commercial Real Estate CRM * * Provides a unified interface for interacting with Claude and OpenAI APIs. * Supports text completions and tool use with both providers. */ import Anthropic from '@anthropic-ai/sdk'; import OpenAI from 'openai'; import { ToolDefinition, ToolCall } from './types'; // ============================================================================= // Types // ============================================================================= /** * Parameters for creating an AI completion */ export interface AICompletionParams { /** AI provider to use */ provider: 'claude' | 'openai'; /** API key for the provider */ apiKey: string; /** Conversation messages */ messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>; /** Optional tool definitions */ tools?: ToolDefinition[]; /** Optional system prompt (defaults to CRESyncFlow assistant) */ systemPrompt?: string; /** Optional model override */ model?: string; /** Optional max tokens (defaults to 4096) */ maxTokens?: number; } /** * Result from an AI completion */ export interface AICompletionResult { /** Text content of the response */ content: string; /** Tool calls made by the assistant (if any) */ toolCalls?: ToolCall[]; /** Model used for the completion */ model: string; /** Number of input tokens used (if available) */ inputTokens?: number; /** Number of output tokens generated (if available) */ outputTokens?: number; /** Reason the completion stopped */ stopReason: string; } // ============================================================================= // Tool Format Converters // ============================================================================= /** * Convert our tool definitions to Claude's tool format */ function toClaudeTools(tools: ToolDefinition[]): Anthropic.Tool[] { return tools.map((tool) => ({ name: tool.name, description: tool.description, input_schema: { type: 'object' as const, properties: tool.inputSchema.properties || {}, required: tool.inputSchema.required || [], }, })); } /** * Convert our tool definitions to OpenAI's tool format */ function toOpenAITools(tools: ToolDefinition[]): OpenAI.Chat.ChatCompletionTool[] { return tools.map((tool) => ({ type: 'function' as const, function: { name: tool.name, description: tool.description, parameters: { type: 'object', properties: tool.inputSchema.properties || {}, required: tool.inputSchema.required || [], }, }, })); } // ============================================================================= // Claude Implementation // ============================================================================= /** * Create a completion using the Claude API */ async function createClaudeCompletion( params: AICompletionParams ): Promise { const client = new Anthropic({ apiKey: params.apiKey }); // Filter out system messages as Claude handles system prompt separately const filteredMessages = params.messages.filter((m) => m.role !== 'system'); // Build the request parameters const requestParams: Anthropic.MessageCreateParams = { model: params.model || 'claude-sonnet-4-20250514', max_tokens: params.maxTokens || 4096, system: params.systemPrompt || 'You are a helpful assistant for CRESyncFlow CRM, a commercial real estate management platform.', messages: filteredMessages.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content, })), }; // Add tools if provided if (params.tools && params.tools.length > 0) { requestParams.tools = toClaudeTools(params.tools); } try { const response = await client.messages.create(requestParams); // Extract text content and tool calls from response let textContent = ''; const toolCalls: ToolCall[] = []; for (const block of response.content) { if (block.type === 'text') { textContent += block.text; } else if (block.type === 'tool_use') { toolCalls.push({ id: block.id, name: block.name, input: block.input as Record, }); } } return { content: textContent, toolCalls: toolCalls.length > 0 ? toolCalls : undefined, model: response.model, inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, stopReason: response.stop_reason || 'unknown', }; } catch (error) { if (error instanceof Anthropic.APIError) { throw new AIClientError( `Claude API error: ${error.message}`, error.status, 'claude', error ); } throw new AIClientError( `Claude client error: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, 'claude', error ); } } // ============================================================================= // OpenAI Implementation // ============================================================================= /** * Create a completion using the OpenAI API */ async function createOpenAICompletion( params: AICompletionParams ): Promise { const client = new OpenAI({ apiKey: params.apiKey }); // Build messages array with system prompt first const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ { role: 'system', content: params.systemPrompt || 'You are a helpful assistant for CRESyncFlow CRM, a commercial real estate management platform.', }, ...params.messages .filter((m) => m.role !== 'system') .map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content, })), ]; // Build the request parameters const requestParams: OpenAI.Chat.ChatCompletionCreateParams = { model: params.model || 'gpt-4o', messages, }; // Add tools if provided if (params.tools && params.tools.length > 0) { requestParams.tools = toOpenAITools(params.tools); } try { const response = await client.chat.completions.create(requestParams); const choice = response.choices[0]; if (!choice) { throw new AIClientError('No response from OpenAI', undefined, 'openai'); } // Extract text content const textContent = choice.message.content || ''; // Extract tool calls if present const toolCalls: ToolCall[] = []; if (choice.message.tool_calls) { for (const toolCall of choice.message.tool_calls) { if (toolCall.type === 'function') { let parsedArguments: Record = {}; try { parsedArguments = JSON.parse(toolCall.function.arguments); } catch { // If JSON parsing fails, use empty object console.warn( `Failed to parse tool call arguments: ${toolCall.function.arguments}` ); } toolCalls.push({ id: toolCall.id, name: toolCall.function.name, input: parsedArguments, }); } } } return { content: textContent, toolCalls: toolCalls.length > 0 ? toolCalls : undefined, model: response.model, inputTokens: response.usage?.prompt_tokens, outputTokens: response.usage?.completion_tokens, stopReason: choice.finish_reason || 'unknown', }; } catch (error) { if (error instanceof OpenAI.APIError) { throw new AIClientError( `OpenAI API error: ${error.message}`, error.status, 'openai', error ); } throw new AIClientError( `OpenAI client error: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, 'openai', error ); } } // ============================================================================= // Error Handling // ============================================================================= /** * Custom error class for AI client errors */ export class AIClientError extends Error { /** HTTP status code (if applicable) */ statusCode?: number; /** Provider that caused the error */ provider: 'claude' | 'openai'; /** Original error (if wrapped) */ originalError?: unknown; constructor( message: string, statusCode?: number, provider: 'claude' | 'openai' = 'claude', originalError?: unknown ) { super(message); this.name = 'AIClientError'; this.statusCode = statusCode; this.provider = provider; this.originalError = originalError; } } // ============================================================================= // Main Export // ============================================================================= /** * Create an AI completion using the specified provider * * @param params - Completion parameters including provider, messages, and tools * @returns Promise resolving to the completion result * @throws AIClientError if the API call fails * * @example * ```typescript * const result = await createAICompletion({ * provider: 'claude', * apiKey: process.env.ANTHROPIC_API_KEY!, * messages: [{ role: 'user', content: 'Hello!' }], * systemPrompt: 'You are a helpful CRM assistant.', * }); * console.log(result.content); * ``` */ export async function createAICompletion( params: AICompletionParams ): Promise { if (!params.apiKey) { throw new AIClientError( `API key is required for ${params.provider}`, 401, params.provider ); } if (!params.messages || params.messages.length === 0) { throw new AIClientError( 'At least one message is required', 400, params.provider ); } if (params.provider === 'claude') { return createClaudeCompletion(params); } else if (params.provider === 'openai') { return createOpenAICompletion(params); } else { throw new AIClientError( `Unknown provider: ${params.provider}`, 400, params.provider as 'claude' | 'openai' ); } } /** * Check if an error is an AIClientError */ export function isAIClientError(error: unknown): error is AIClientError { return error instanceof AIClientError; }