BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

359 lines
10 KiB
TypeScript

/**
* 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<AICompletionResult> {
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<string, any>,
});
}
}
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<AICompletionResult> {
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<string, any> = {};
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<AICompletionResult> {
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;
}