From c4425280c6c6317f20a7b7e355bca4219f223fb3 Mon Sep 17 00:00:00 2001 From: Alessio Pelliccione Date: Sat, 20 Dec 2025 19:43:04 +0100 Subject: [PATCH 1/5] refactor: apply SOLID principles to AI package and extract ConfigLoader --- packages/ai/ai-agent.ts | 249 ++-------- packages/ai/config-loader.ts | 76 +++ packages/ai/constants.ts | 10 + packages/ai/index.ts | 4 +- packages/ai/providers/vercel-ai-provider.ts | 440 ------------------ packages/ai/providers/vercel/index.ts | 4 + packages/ai/providers/vercel/model-factory.ts | 116 +++++ .../ai/providers/vercel/options-builder.ts | 60 +++ packages/ai/providers/vercel/provider.ts | 194 ++++++++ packages/ai/providers/vercel/utils.ts | 47 ++ packages/ai/types.ts | 14 +- packages/cli/package.json | 1 + .../cli/src/services/generation-context.ts | 33 +- packages/cli/src/utils/logger.ts | 214 +-------- packages/shared/package.json | 15 + packages/shared/src/index.ts | 150 ++++++ packages/shared/tsconfig.json | 10 + pnpm-lock.yaml | 12 + 18 files changed, 747 insertions(+), 902 deletions(-) create mode 100644 packages/ai/config-loader.ts delete mode 100644 packages/ai/providers/vercel-ai-provider.ts create mode 100644 packages/ai/providers/vercel/index.ts create mode 100644 packages/ai/providers/vercel/model-factory.ts create mode 100644 packages/ai/providers/vercel/options-builder.ts create mode 100644 packages/ai/providers/vercel/provider.ts create mode 100644 packages/ai/providers/vercel/utils.ts create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/tsconfig.json diff --git a/packages/ai/ai-agent.ts b/packages/ai/ai-agent.ts index a9eb09e2..6c967fd1 100644 --- a/packages/ai/ai-agent.ts +++ b/packages/ai/ai-agent.ts @@ -10,14 +10,12 @@ import { GenerateTextOptions, IAIProvider, AIProviderError, - AIAgentRoleConfig, - AIProvider, // Import AIProvider type here + AIProvider, ILogger, ObservabilityMetadata, } from './types'; -import { VercelAIProvider } from './providers/vercel-ai-provider'; - -import { getDefaultModel } from './constants'; +import { VercelAIProvider } from './providers/vercel'; +import { ConfigLoader } from './config-loader'; /** * Collection of AI Agent instances for different roles. @@ -37,26 +35,16 @@ export class AIAgent { private config: AIAgentConfig; private retryConfig: { maxAttempts: number; delayMs: number }; private logger?: ILogger; - private role?: 'planner' | 'writer' | 'researcher' | 'reviewer'; + private role?: string; - constructor(config: AIAgentConfig, role?: 'planner' | 'writer' | 'researcher' | 'reviewer') { + constructor(config: AIAgentConfig, role?: string) { this.config = config; this.retryConfig = config.retry || { maxAttempts: 3, delayMs: 1000 }; this.logger = config.logger; this.role = role; // Initialize the appropriate provider - this.provider = this.createProvider(); - } - - /** - * Create provider based on configuration - */ - private createProvider(): IAIProvider { - const { model, debug } = this.config; - - // Use Vercel AI SDK for all supported providers - return new VercelAIProvider(model, debug, this.logger); + this.provider = new VercelAIProvider(config.model, config.debug, this.logger); } /** @@ -71,57 +59,9 @@ export class AIAgent { provider: this.provider.provider, }); - // TODO: Use options to customize generation (currently passed to provider) return this.executeWithRetry(() => this.provider.generateDocumentation(request)); } - async generateBatch( - items: Array<{ symbolName: string; signatureText: string }>, - prompt: string, - systemPrompt: string, - ): Promise> { - this.log(`Generating batch documentation for ${items.length} items`); - - // Try batch generation if provider supports it - if (this.provider.generateBatchDocumentation) { - try { - const batchResult = await this.executeWithRetry(() => - this.provider.generateBatchDocumentation!(items, prompt, systemPrompt), - ); - - // Start with successful generations from batch - const results = [...batchResult.success]; - - // Log batch statistics - if (batchResult.stats.failed > 0) { - this.log( - `Batch generated ${batchResult.stats.succeeded}/${batchResult.stats.total} successfully. ` + - `Retrying ${batchResult.stats.failed} failed items sequentially...`, - ); - - // For the sake of refactor, removing complex batch retry fallback that depends on single prompt logic - // as it's not directly relevant to the current Model Routing task and would require more complex changes - // to pass single prompts or dynamically generate them here. - this.log( - `Batch retry fallback currently not supported for ${batchResult.stats.failed} failed items.`, - ); - } else { - this.log( - `Batch completed successfully: ${batchResult.stats.succeeded}/${batchResult.stats.total}`, - ); - } - - return results; - } catch (error: unknown) { - // Use unknown for caught errors - this.log('Batch generation failed completely', error); - } - } - - this.log('Batch generation not supported by provider or failed, returning empty.'); - return []; - } - /** * Generate generic text using the AI provider */ @@ -135,10 +75,9 @@ export class AIAgent { model: this.config.model.modelId, }); - // Merge role metadata with provided metadata const mergedMetadata: ObservabilityMetadata = { ...metadata, - agentRole: this.role || metadata?.agentRole, + agentRole: (this.role as any) || metadata?.agentRole, tags: [...(metadata?.tags || []), ...(this.role ? [`agent-${this.role}`] : [])], }; @@ -148,9 +87,7 @@ export class AIAgent { ); } - throw new Error( - `Provider ${this.config.model.provider} with model ${this.config.model.modelId} does not support text generation`, - ); + throw new Error(`Provider ${this.config.model.provider} does not support text generation`); } /** @@ -162,7 +99,6 @@ export class AIAgent { try { return await this.provider.validateConnection(); } catch (error: unknown) { - // Use unknown for caught errors this.log('Connection validation failed:', error); return false; } @@ -175,192 +111,79 @@ export class AIAgent { try { return await fn(); } catch (error: unknown) { - // Use unknown for caught errors const isRetryable = this.isRetryableError(error); const hasAttemptsLeft = attempt < this.retryConfig.maxAttempts; if (isRetryable && hasAttemptsLeft) { - this.log( - `Attempt ${attempt} for model ${this.config.model.modelId} failed, retrying in ${this.retryConfig.delayMs}ms...`, - ); + this.log(`Attempt ${attempt} failed, retrying in ${this.retryConfig.delayMs}ms...`); - await this.delay(this.retryConfig.delayMs); + await new Promise((resolve) => setTimeout(resolve, this.retryConfig.delayMs)); return this.executeWithRetry(fn, attempt + 1); } - // No more retries or non-retryable error throw error; } } - /** - * Check if an error is retryable - */ private isRetryableError(error: unknown): boolean { - if (error instanceof AIProviderError && error.code) { - // Check if it's an instance of AIProviderError - const providerError = error as AIProviderError; - - // Retry on timeout, rate limit, and network errors - return ['TIMEOUT', 'RATE_LIMIT', 'NETWORK_ERROR'].includes(providerError.code); + if (error instanceof AIProviderError) { + return ['TIMEOUT', 'RATE_LIMIT', 'NETWORK_ERROR'].includes(error.code); } - return false; } - /** - * Delay utility for retry logic - */ - private delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - /** - * Log debug messages - */ - /** - * Log debug messages - */ private log(message: string, ...args: unknown[]): void { if (this.config.debug) { + const prefix = `[AIAgent${this.role ? `:${this.role}` : ''}]`; if (this.logger) { - this.logger.debug(`[AIAgent ${this.config.model.modelId}] ${message}`, ...args); + this.logger.debug(`${prefix} ${message}`, ...args); } else { - console.log(`[AIAgent ${this.config.model.modelId}]`, message, ...args); + console.log(prefix, message, ...args); } } } - /** - * Get the current provider name - */ getProvider(): AIProvider { - // Use AIProvider type return this.config.model.provider; } - /** - * Get the current model ID - */ getModelId(): string { return this.config.model.modelId; } - - /** - * Get a summary of configuration - */ - getConfig(): { provider: AIProvider; model: string; timeout: number } { - // Use AIProvider type - return { - provider: this.config.model.provider, - model: this.config.model.modelId, - timeout: this.config.timeout || 30000, - }; - } } /** - * Helper to normalize logging calls (fallback to console if no logger) + * Orchestrator factory to create agents from environment */ -function getLogger(logger?: ILogger) { - return { - debug: (msg: string, ...args: unknown[]) => - logger ? logger.debug(msg, ...args) : console.log(msg, ...args), - info: (msg: string, ...args: unknown[]) => - logger ? logger.info(msg, ...args) : console.log(msg, ...args), - warn: (msg: string, ...args: unknown[]) => - logger ? logger.warn(msg, ...args) : console.warn(msg, ...args), - error: (msg: string, ...args: unknown[]) => - logger ? logger.error(msg, ...args) : console.error(msg, ...args), - }; -} - -/** - * Internal helper to create a single AI Agent based on role configuration and ENV vars. - * @param roleOptions Specific options for this role, potentially overriding ENV. - * @param globalOptions Global options like debug flag. - */ -function _createSingleAgentFromEnv( - roleOptions: AIAgentRoleConfig, - globalOptions: { debug?: boolean; timeout?: number; logger?: ILogger }, -): AIAgentConfig { - // Require OpenAI API key - const openaiKey = process.env.OPENAI_API_KEY; - - if (!openaiKey) { - throw new Error( - `OPENAI_API_KEY is required. Please set it in your .env file. ` + - `Note: HELICONE_API_KEY can be used for observability tracking, but OPENAI_API_KEY is still required.`, - ); - } - - const modelId = roleOptions.modelId || getDefaultModel('openai') || 'gpt-4o'; - return { - model: { - provider: 'openai', - modelId: modelId, - apiKey: openaiKey, - maxTokens: roleOptions.maxTokens, - temperature: roleOptions.temperature, - }, - timeout: globalOptions.timeout, - debug: globalOptions.debug, - logger: globalOptions.logger, - }; -} - export function createAIAgentsFromEnv( - globalOptions: { + options: { debug?: boolean; timeout?: number; - maxTokens?: number; - temperature?: number; logger?: ILogger; } = {}, - roleConfigs: { - planner?: AIAgentRoleConfig; - writer?: AIAgentRoleConfig; - researcher?: AIAgentRoleConfig; - reviewer?: AIAgentRoleConfig; - } = {}, ): AIAgents { - const roles = ['planner', 'writer', 'researcher', 'reviewer'] as const; - - // Check if we have OpenAI API key - if (!process.env.OPENAI_API_KEY) { - throw new Error( - 'OPENAI_API_KEY is required. Please set it in your .env file.\n' + - 'Note: You can also set HELICONE_API_KEY for observability tracking.', - ); - } - - // OpenAI model defaults - const modelDefaults = { - planner: 'gpt-5-mini', - writer: 'gpt-5-mini', - researcher: 'gpt-5-mini', - reviewer: 'gpt-5-nano', - }; - - // Use Record type for better type safety instead of 'any' - const agents: Record = {}; - for (const role of roles) { - const roleConfig = roleConfigs?.[role]; - const options: AIAgentRoleConfig = { - modelId: roleConfig?.modelId || modelDefaults[role], - provider: 'openai', // Only OpenAI supported - maxTokens: roleConfig?.maxTokens, - temperature: roleConfig?.temperature, + const config = ConfigLoader.loadFromEnv(); + const agents: Partial = {}; + + for (const [role, roleConfig] of Object.entries(config.roles)) { + const agentConfig: AIAgentConfig = { + model: { + ...config.globalModel, + ...roleConfig, + }, + debug: options.debug, + timeout: options.timeout, + logger: options.logger, }; - const agent = new AIAgent(_createSingleAgentFromEnv(options, globalOptions), role); - agents[role] = agent; + (agents as any)[role] = new AIAgent(agentConfig, role); - const log = getLogger(globalOptions.logger); - log.debug( - `[AIAgentManager] ${role.charAt(0).toUpperCase() + role.slice(1)} initialized with ${agent.getModelId()} (${agent.getProvider()})`, - ); + if (options.logger) { + options.logger.debug( + `[AIAgentManager] ${role} initialized with ${roleConfig.modelId} (${roleConfig.provider || config.globalModel.provider})`, + ); + } } - return agents as unknown as AIAgents; + return agents as AIAgents; } diff --git a/packages/ai/config-loader.ts b/packages/ai/config-loader.ts new file mode 100644 index 00000000..30c90ad5 --- /dev/null +++ b/packages/ai/config-loader.ts @@ -0,0 +1,76 @@ +import { AIProvider, AIModel, AIAgentRoleConfig } from './types'; +import { DEFAULT_MODELS, DEFAULT_TEMPERATURES } from './constants'; + +export interface AIAgentsConfig { + roles: Record; + globalModel: AIModel; +} + +export class ConfigLoader { + /** + * Loads agent configurations from environment variables + */ + static loadFromEnv(): AIAgentsConfig { + const provider = (process.env.AI_PROVIDER as AIProvider) || 'openai'; + const apiKey = this.getApiKey(provider); + + const globalModel: AIModel = { + provider, + apiKey, + modelId: process.env.AI_MODEL_ID || DEFAULT_MODELS[provider], + maxTokens: parseInt(process.env.AI_MAX_TOKENS || '2000'), + temperature: parseFloat( + process.env.AI_TEMPERATURE || String(DEFAULT_TEMPERATURES.writer), + ), + endpoint: process.env.AI_ENDPOINT, + }; + + const roles = { + planner: this.loadRoleConfig('PLANNER', globalModel, DEFAULT_TEMPERATURES.planner), + researcher: this.loadRoleConfig( + 'RESEARCHER', + globalModel, + DEFAULT_TEMPERATURES.researcher, + ), + writer: this.loadRoleConfig('WRITER', globalModel, DEFAULT_TEMPERATURES.writer), + reviewer: this.loadRoleConfig('REVIEWER', globalModel, DEFAULT_TEMPERATURES.reviewer), + }; + + return { globalModel, roles }; + } + + private static loadRoleConfig( + prefix: string, + fallback: AIModel, + defaultTemp: number, + ): AIAgentRoleConfig { + const provider = (process.env[`${prefix}_PROVIDER`] as AIProvider) || fallback.provider; + return { + provider, + modelId: process.env[`${prefix}_MODEL_ID`] || DEFAULT_MODELS[provider], + temperature: process.env[`${prefix}_TEMPERATURE`] + ? parseFloat(process.env[`${prefix}_TEMPERATURE`]!) + : defaultTemp, + maxTokens: process.env[`${prefix}_MAX_TOKENS`] + ? parseInt(process.env[`${prefix}_MAX_TOKENS`]!) + : fallback.maxTokens, + }; + } + + private static getApiKey(provider: AIProvider): string { + const keys: Record = { + openai: process.env.OPENAI_API_KEY || process.env.LDR_OPENAI_API_KEY, + gemini: process.env.GEMINI_API_KEY || process.env.LDR_GEMINI_API_KEY, + anthropic: process.env.ANTHROPIC_API_KEY || process.env.LDR_ANTHROPIC_API_KEY, + mistral: process.env.MISTRAL_API_KEY || process.env.LDR_MISTRAL_API_KEY, + }; + + const key = keys[provider]; + if (!key) { + throw new Error( + `API Key for provider ${provider} is missing (tried ${provider.toUpperCase()}_API_KEY and LDR_${provider.toUpperCase()}_API_KEY)`, + ); + } + return key; + } +} diff --git a/packages/ai/constants.ts b/packages/ai/constants.ts index 187951e6..1cca3552 100644 --- a/packages/ai/constants.ts +++ b/packages/ai/constants.ts @@ -15,6 +15,16 @@ export const DEFAULT_MODELS: Record = { mistral: 'mistral-small-latest', } as const; +/** + * Default temperatures for different agent roles + */ +export const DEFAULT_TEMPERATURES = { + planner: 0.2, + researcher: 0.3, + writer: 0.7, + reviewer: 0.1, +} as const; + /** * Get the default model ID for a given provider */ diff --git a/packages/ai/index.ts b/packages/ai/index.ts index c0f7af5a..5a1799b7 100644 --- a/packages/ai/index.ts +++ b/packages/ai/index.ts @@ -5,8 +5,10 @@ */ export * from './types'; +export * from './config-loader'; export * from './ai-agent'; // Exports AIAgent, createOpenAIAgent, createAIAgentsFromEnv -export * from './providers/vercel-ai-provider'; + +export * from './providers/vercel'; export * from './constants'; export * from './structured-schema'; export * from './markdown-builder'; diff --git a/packages/ai/providers/vercel-ai-provider.ts b/packages/ai/providers/vercel-ai-provider.ts deleted file mode 100644 index 218f8196..00000000 --- a/packages/ai/providers/vercel-ai-provider.ts +++ /dev/null @@ -1,440 +0,0 @@ -import { - IAIProvider, - AIProvider, - DocumentationRequest, - DocumentationResponse, - AIModel, - AIProviderError, - BatchDocumentationResult, - ILogger, - ObservabilityMetadata, - GenerateTextOptions, -} from '../types'; -import { generateObject, generateText } from 'ai'; -import { createOpenAI } from '@ai-sdk/openai'; -import { createGoogleGenerativeAI } from '@ai-sdk/google'; -import { createAnthropic } from '@ai-sdk/anthropic'; -import { createMistral } from '@ai-sdk/mistral'; -import { createHelicone } from '@helicone/ai-sdk-provider'; -import { - BatchDocumentationStructureSchema, - DocumentationStructureSchema, - type DocumentationStructure, -} from '../structured-schema'; -import { buildMarkdownFromStructure } from '../markdown-builder'; -import { z } from 'zod'; - -export class VercelAIProvider implements IAIProvider { - readonly provider: AIProvider; - private modelConfig: AIModel; - private debug: boolean; - private logger?: ILogger; - - constructor(config: AIModel, debug: boolean = false, logger?: ILogger) { - this.provider = config.provider; - this.modelConfig = config; - this.debug = debug; - this.logger = logger; - } - - /** - * Get model with Helicone fallback - * Tries to use Helicone if configured, falls back to native provider on failure - */ - private getModel(metadata?: ObservabilityMetadata) { - const heliconeApiKey = process.env.HELICONE_API_KEY; - - // If Helicone is configured and we have metadata, try to use it as a proxy - if (heliconeApiKey && metadata) { - try { - const helicone = createHelicone({ apiKey: heliconeApiKey }); - - // Log when Helicone is successfully initialized - if (this.logger) { - this.logger.info('📊 Helicone observability enabled'); - } else if (this.debug) { - console.log('[VercelAIProvider] ✓ Using Helicone proxy for observability'); - } - - return helicone(this.modelConfig.modelId, { - extraBody: { - helicone: this.buildHeliconeConfig(metadata), - }, - }); - } catch (error) { - // Log warning and fall back to native provider - if (this.logger) { - this.logger.warn( - `[VercelAIProvider] Helicone initialization failed, falling back to native provider: ${error}`, - ); - } else { - console.warn( - `[VercelAIProvider] Helicone initialization failed, falling back to native provider:`, - error, - ); - } - // Fall through to native provider - } - } - - // Use the standard native provider (fallback or when Helicone not configured) - return this.getNativeModel(); - } - - /** - * Get native model without Helicone proxy - */ - private getNativeModel() { - switch (this.provider) { - case 'openai': { - const provider = createOpenAI({ - apiKey: this.modelConfig.apiKey, - baseURL: this.modelConfig.endpoint, - }); - return provider(this.modelConfig.modelId); - } - case 'gemini': { - const provider = createGoogleGenerativeAI({ - apiKey: this.modelConfig.apiKey, - }); - return provider(this.modelConfig.modelId); - } - case 'anthropic': { - const provider = createAnthropic({ - apiKey: this.modelConfig.apiKey, - }); - return provider(this.modelConfig.modelId); - } - case 'mistral': { - const provider = createMistral({ - apiKey: this.modelConfig.apiKey, - }); - return provider(this.modelConfig.modelId); - } - default: - throw new Error(`Unsupported provider: ${this.provider}`); - } - } - - /** - * Build Helicone configuration from observability metadata - */ - private buildHeliconeConfig(metadata?: ObservabilityMetadata): any { - if (!metadata) return {}; - - const heliconeConfig: any = {}; - - if (metadata.sessionId) { - heliconeConfig.sessionId = metadata.sessionId; - } - - if (metadata.sessionName) { - heliconeConfig.sessionName = metadata.sessionName; - } - - if (metadata.userId) { - heliconeConfig.userId = metadata.userId; - } - - if (metadata.tags || metadata.agentRole) { - heliconeConfig.tags = [ - ...(metadata.tags || []), - ...(metadata.agentRole ? [metadata.agentRole] : []), - ]; - } - - if (metadata.properties) { - heliconeConfig.properties = metadata.properties; - } - - return heliconeConfig; - } - - async generateDocumentation(request: DocumentationRequest): Promise { - const model = this.getModel(); - - // Use structured prompt from request - const prompt = request.prompt; - const systemPrompt = request.systemPrompt; - - try { - const options: any = { - model, - prompt, - system: systemPrompt, - schema: z.object({ - documentation: DocumentationStructureSchema, - }), - }; - - if (this.modelConfig.maxTokens) { - options.maxTokens = this.modelConfig.maxTokens; - } - - if (this.modelConfig.temperature !== undefined) { - options.temperature = this.modelConfig.temperature; - } - - const result: any = await generateObject(options); - const doc: DocumentationStructure = result.object.documentation; - const usageAny = result.usage as any; - - // Build Markdown from structure (Zod schema already validated the data) - const markdown = buildMarkdownFromStructure(doc); - - return { - content: markdown, - provider: this.provider, - modelId: this.modelConfig.modelId, - timestamp: Date.now(), - usage: result.usage - ? { - promptTokens: usageAny.promptTokens || 0, - completionTokens: usageAny.completionTokens || 0, - totalTokens: usageAny.totalTokens || 0, - } - : undefined, - }; - } catch (error) { - const err = error as any; - let code = 'GENERATION_FAILED'; - const message = err.message || 'Unknown error during generation'; - - // Map common error codes if possible - if (err.name === 'APICallError' && err.statusCode === 429) { - code = 'RATE_LIMIT'; - } - - throw new AIProviderError(code, message, this.provider, error); - } - } - - async generateBatchDocumentation( - items: Array<{ symbolName: string; signatureText: string }>, - prompt: string, - systemPrompt: string, - ): Promise { - const model = this.getModel(); - - try { - const options: any = { - model, - prompt, - system: systemPrompt, - schema: BatchDocumentationStructureSchema, - }; - - // Increase token limit for batches - if (this.modelConfig.maxTokens) { - options.maxTokens = this.modelConfig.maxTokens * items.length; - } else { - // Default generous limit for batches if not specified - options.maxTokens = 4096; - } - - if (this.modelConfig.temperature !== undefined) { - options.temperature = this.modelConfig.temperature; - } - - const result: any = await generateObject(options); - - // Build Markdown from each structured documentation entry - // (Zod schema already validated all data during parsing) - const success: Array<{ symbolName: string; content: string }> = - result.object.documentations.map((doc: DocumentationStructure) => { - const markdown = buildMarkdownFromStructure(doc); - - if (this.debug) { - if (this.logger) { - this.logger.debug( - `[VercelAIProvider] Generated markdown for ${doc.symbolName}`, - ); - } else { - console.log( - `[VercelAIProvider] Generated markdown for ${doc.symbolName}`, - ); - } - } - - return { - symbolName: doc.symbolName, - content: markdown, - }; - }); - - return { - success, - failures: [], - stats: { - total: items.length, - succeeded: success.length, - failed: 0, - }, - }; - } catch (error) { - const err = error as any; - // Complete batch failure (network, API error, etc.) - if (this.logger) { - this.logger.warn( - `[VercelAIProvider] Batch generation failed completely: ${err.message}`, - ); - } else { - console.warn( - `[VercelAIProvider] Batch generation failed completely: ${err.message}`, - ); - } - - // Return empty result with all items marked as failures - return { - success: [], - failures: items.map((item) => ({ - symbolName: item.symbolName, - errors: [`Batch generation error: ${err.message}`], - })), - stats: { - total: items.length, - succeeded: 0, - failed: items.length, - }, - }; - } - } - - async validateConnection(): Promise { - const model = this.getModel(); - // Check for reasoning models (o1-*, o3-*) which have special parameter restrictions - const isReasoningModel = /^o[13]-/.test(this.modelConfig.modelId); - - try { - // Simple ping to validate key - const options: any = { - model, - prompt: 'Hello', - }; - - // Reasoning models (o1, o3) do not support maxTokens in the same way - if (!isReasoningModel) { - options.maxTokens = 5; - } - - await generateText(options); - return true; - } catch (error) { - if (this.debug) { - if (this.logger) { - this.logger.error('Connection validation failed:', error); - } else { - console.error('Connection validation failed:', error); - } - } - return false; - } - } - - /** - * Generate plain text from a prompt (useful for non-documentation tasks) - */ - async generateText( - prompt: string, - options: GenerateTextOptions = {}, - metadata?: ObservabilityMetadata, - ): Promise { - const model = this.getModel(metadata); - // Check for reasoning models (o1-*, o3-*) which have special parameter restrictions - const isReasoningModel = /^o[13]-/.test(this.modelConfig.modelId); - - try { - const genOptions: any = { - model, - prompt, - }; - - // Reasoning models (o1, o3) have restrictions on temperature and other params - if (!isReasoningModel) { - genOptions.temperature = options.temperature ?? this.modelConfig.temperature; - genOptions.maxTokens = options.maxTokens ?? this.modelConfig.maxTokens ?? 1000; - } else { - // For reasoning models (o1/o3), we omit temperature/maxTokens - // They use maxCompletionTokens instead, handled by the AI SDK - } - - // Pass tools if provided - if (options.tools) { - genOptions.tools = options.tools; - } - - // Pass maxSteps if provided (default to 1 for standard generation, higher for agents) - if (options.maxSteps) { - genOptions.maxSteps = options.maxSteps; - } - - const result = await generateText(genOptions); - return result.text; - } catch (error) { - const err = error as any; - - // If Helicone was used and it failed, retry with native provider - const heliconeApiKey = process.env.HELICONE_API_KEY; - if (heliconeApiKey && metadata) { - if (this.logger) { - this.logger.warn( - `[VercelAIProvider] Helicone API call failed, retrying with native provider: ${err.message}`, - ); - } else { - console.warn( - `[VercelAIProvider] Helicone API call failed, retrying with native provider:`, - err.message, - ); - } - - try { - // Retry with native provider - const nativeModel = this.getNativeModel(); - const genOptions: any = { - model: nativeModel, - prompt, - }; - - // Redeclare for fallback scope - const isReasoningModel = /^o[13]-/.test(this.modelConfig.modelId); - - // Reasoning models don't support maxTokens parameter - if (!isReasoningModel) { - genOptions.temperature = - options.temperature ?? this.modelConfig.temperature; - genOptions.maxTokens = - options.maxTokens ?? this.modelConfig.maxTokens ?? 1000; - } - - // Pass tools if provided - if (options.tools) { - genOptions.tools = options.tools; - } - if (options.maxSteps) { - genOptions.maxSteps = options.maxSteps; - } - - const result = await generateText(genOptions); - return result.text; - } catch (fallbackError) { - // Both Helicone and native failed, throw the native error - const fallbackErr = fallbackError as any; - throw new AIProviderError( - 'GENERATION_FAILED', - fallbackErr.message || 'Text generation failed', - this.provider, - fallbackError, - ); - } - } - - // Either Helicone wasn't used, or it was the only attempt - throw new AIProviderError( - 'GENERATION_FAILED', - err.message || 'Text generation failed', - this.provider, - error, - ); - } - } -} diff --git a/packages/ai/providers/vercel/index.ts b/packages/ai/providers/vercel/index.ts new file mode 100644 index 00000000..b64c35db --- /dev/null +++ b/packages/ai/providers/vercel/index.ts @@ -0,0 +1,4 @@ +export * from './provider'; +export * from './model-factory'; +export * from './options-builder'; +export * from './utils'; diff --git a/packages/ai/providers/vercel/model-factory.ts b/packages/ai/providers/vercel/model-factory.ts new file mode 100644 index 00000000..4d87d978 --- /dev/null +++ b/packages/ai/providers/vercel/model-factory.ts @@ -0,0 +1,116 @@ +import { createOpenAI } from '@ai-sdk/openai'; +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { createAnthropic } from '@ai-sdk/anthropic'; +import { createMistral } from '@ai-sdk/mistral'; +import { createHelicone } from '@helicone/ai-sdk-provider'; +import { AIModel, ILogger, ObservabilityMetadata } from '../../types'; +import { log } from './utils'; + +export interface ModelFactoryConfig { + modelConfig: AIModel; + debug?: boolean; + logger?: ILogger; +} + +export class ModelFactory { + readonly #config: ModelFactoryConfig; + + constructor(config: ModelFactoryConfig) { + this.#config = config; + } + + getModel(metadata?: ObservabilityMetadata) { + if (metadata) { + const heliconeModel = this.tryCreateHeliconeModel(metadata); + if (heliconeModel) { + log( + this.#config.logger, + this.#config.debug || false, + 'info', + '📊 Helicone observability enabled', + ); + return heliconeModel; + } + } + + return this.getNativeModel(); + } + + getNativeModel() { + return this.createProviderModel()(this.#config.modelConfig.modelId); + } + + private createProviderModel(): (id: string) => any { + const factories = { + openai: createOpenAI, + gemini: createGoogleGenerativeAI, + anthropic: createAnthropic, + mistral: createMistral, + } as const; + + const factory = factories[this.#config.modelConfig.provider]; + if (!factory) { + throw new Error(`Unsupported provider: ${this.#config.modelConfig.provider}`); + } + + const instance = factory({ + apiKey: this.#config.modelConfig.apiKey, + baseURL: this.#config.modelConfig.endpoint, + } as any); + + return instance; + } + + private tryCreateHeliconeModel(metadata: ObservabilityMetadata) { + const heliconeApiKey = process.env.HELICONE_API_KEY; + if (!heliconeApiKey) return null; + + try { + const helicone = createHelicone({ apiKey: heliconeApiKey }); + + return helicone(this.#config.modelConfig.modelId, { + extraBody: { + helicone: this.buildHeliconeConfig(metadata), + }, + }); + } catch (error) { + log( + this.#config.logger, + this.#config.debug || false, + 'warn', + `[ModelFactory] Helicone initialization failed, falling back to native provider`, + error, + ); + return null; + } + } + + private buildHeliconeConfig(metadata: ObservabilityMetadata) { + const heliconeConfig: Record = {}; + + if (metadata.sessionId) { + heliconeConfig.sessionId = metadata.sessionId; + } + + if (metadata.sessionName) { + heliconeConfig.sessionName = metadata.sessionName; + } + + if (metadata.userId) { + heliconeConfig.userId = metadata.userId; + } + + if (metadata.tags || metadata.agentRole) { + heliconeConfig.tags = [ + ...(metadata.tags || []), + ...(metadata.agentRole ? [metadata.agentRole] : []), + ]; + } + + if (metadata.properties) { + heliconeConfig.properties = metadata.properties; + } + + return heliconeConfig; + } +} diff --git a/packages/ai/providers/vercel/options-builder.ts b/packages/ai/providers/vercel/options-builder.ts new file mode 100644 index 00000000..b6e204f3 --- /dev/null +++ b/packages/ai/providers/vercel/options-builder.ts @@ -0,0 +1,60 @@ +import { AIModel, GenerateTextOptions } from '../../types'; +import { isReasoningModel } from './utils'; + +export class OptionsBuilder { + readonly #config: AIModel; + + constructor(config: AIModel) { + this.#config = config; + } + + /** + * Build base options for AI SDK generation functions + */ + buildBaseOptions(model: any, prompt: string, overrides: Record = {}) { + const options: Record = { + model, + prompt, + ...overrides, + }; + + // Reasoning models (o1, o3) handle tokens/temperature differently + if (!isReasoningModel(this.#config.modelId)) { + if (this.#config.maxTokens && options.maxTokens === undefined) { + options.maxTokens = this.#config.maxTokens; + } + + if (this.#config.temperature !== undefined && options.temperature === undefined) { + options.temperature = this.#config.temperature; + } + } + + return options; + } + + /** + * Build options specifically for generateText + */ + buildTextOptions( + model: any, + prompt: string, + options: GenerateTextOptions, + ): Record { + const baseOptions = this.buildBaseOptions(model, prompt, {}); + + if (!isReasoningModel(this.#config.modelId)) { + baseOptions.temperature = options.temperature ?? this.#config.temperature; + baseOptions.maxTokens = options.maxTokens ?? this.#config.maxTokens ?? 1000; + } + + if (options.tools) { + baseOptions.tools = options.tools; + } + + if (options.maxSteps) { + baseOptions.maxSteps = options.maxSteps; + } + + return baseOptions; + } +} diff --git a/packages/ai/providers/vercel/provider.ts b/packages/ai/providers/vercel/provider.ts new file mode 100644 index 00000000..f08faf87 --- /dev/null +++ b/packages/ai/providers/vercel/provider.ts @@ -0,0 +1,194 @@ +import { + IAIProvider, + AIProvider, + DocumentationRequest, + DocumentationResponse, + AIModel, + BatchDocumentationResult, + ILogger, + ObservabilityMetadata, + GenerateTextOptions, +} from '../../types'; +import { generateObject, generateText } from 'ai'; +import { + BatchDocumentationStructureSchema, + DocumentationStructureSchema, + type DocumentationStructure, +} from '../../structured-schema'; +import { buildMarkdownFromStructure } from '../../markdown-builder'; +import { z } from 'zod'; +import { ModelFactory } from './model-factory'; +import { OptionsBuilder } from './options-builder'; +import { isReasoningModel, mapToProviderError, log } from './utils'; + +export class VercelAIProvider implements IAIProvider { + readonly provider: AIProvider; + readonly #modelConfig: AIModel; + readonly #debug: boolean; + readonly #logger?: ILogger; + readonly #modelFactory: ModelFactory; + readonly #optionsBuilder: OptionsBuilder; + + constructor(config: AIModel, debug: boolean = false, logger?: ILogger) { + this.provider = config.provider; + this.#modelConfig = config; + this.#debug = debug; + this.#logger = logger; + + this.#modelFactory = new ModelFactory({ + modelConfig: config, + debug, + logger, + }); + + this.#optionsBuilder = new OptionsBuilder(config); + } + + /** + * Executes a task with Helicone observability, falling back to native model on failure + */ + private async withHeliconeFallback( + metadata: ObservabilityMetadata | undefined, + task: (model: any) => Promise, + ): Promise { + try { + return await task(this.#modelFactory.getModel(metadata)); + } catch (error) { + if (metadata && process.env.HELICONE_API_KEY) { + log( + this.#logger, + this.#debug, + 'warn', + 'Helicone failed, retrying with native model', + error, + ); + return task(this.#modelFactory.getNativeModel()); + } + throw error; + } + } + + /** + * Maps AI SDK result to DocumentationResponse + */ + private toDocumentationResponse( + result: any, + doc: DocumentationStructure, + ): DocumentationResponse { + const usage = result.usage ?? {}; + return { + content: buildMarkdownFromStructure(doc), + provider: this.provider, + modelId: this.#modelConfig.modelId, + timestamp: Date.now(), + usage: { + promptTokens: usage.promptTokens ?? 0, + completionTokens: usage.completionTokens ?? 0, + totalTokens: usage.totalTokens ?? 0, + }, + }; + } + + async generateDocumentation(request: DocumentationRequest): Promise { + try { + return await this.withHeliconeFallback(undefined, async (model) => { + const options = this.#optionsBuilder.buildBaseOptions(model, request.prompt, { + system: request.systemPrompt, + schema: z.object({ + documentation: DocumentationStructureSchema, + }), + }); + + const result: any = await generateObject(options as any); + return this.toDocumentationResponse(result, result.object.documentation); + }); + } catch (error) { + throw mapToProviderError(error, this.provider); + } + } + + async generateBatchDocumentation( + items: Array<{ symbolName: string; signatureText: string }>, + prompt: string, + systemPrompt: string, + ): Promise { + const model = this.#modelFactory.getNativeModel(); // Batch usually doesn't need Helicone retry logic here + + try { + const options = this.#optionsBuilder.buildBaseOptions(model, prompt, { + system: systemPrompt, + schema: BatchDocumentationStructureSchema, + maxTokens: this.#modelConfig.maxTokens + ? this.#modelConfig.maxTokens * items.length + : 4096, + }); + + const result: any = await generateObject(options as any); + + const success = result.object.documentations.map((doc: DocumentationStructure) => ({ + symbolName: doc.symbolName, + content: buildMarkdownFromStructure(doc), + })); + + return { + success, + failures: [], + stats: { + total: items.length, + succeeded: success.length, + failed: 0, + }, + }; + } catch (error) { + const err = error as any; + log(this.#logger, this.#debug, 'warn', `Batch generation failed: ${err.message}`); + + return { + success: [], + failures: items.map((item) => ({ + symbolName: item.symbolName, + errors: [`Batch generation error: ${err.message}`], + })), + stats: { + total: items.length, + succeeded: 0, + failed: items.length, + }, + }; + } + } + + async validateConnection(): Promise { + const model = this.#modelFactory.getNativeModel(); + const reasoning = isReasoningModel(this.#modelConfig.modelId); + + try { + const options: Record = { model, prompt: 'Hello' }; + if (!reasoning) options.maxTokens = 5; + + await generateText(options as any); + return true; + } catch (error) { + if (this.#debug) { + log(this.#logger, this.#debug, 'error', 'Connection validation failed:', error); + } + return false; + } + } + + async generateText( + prompt: string, + options: GenerateTextOptions = {}, + metadata?: ObservabilityMetadata, + ): Promise { + try { + return await this.withHeliconeFallback(metadata, async (model) => { + const textOptions = this.#optionsBuilder.buildTextOptions(model, prompt, options); + const result = await generateText(textOptions as any); + return result.text; + }); + } catch (error) { + throw mapToProviderError(error, this.provider); + } + } +} diff --git a/packages/ai/providers/vercel/utils.ts b/packages/ai/providers/vercel/utils.ts new file mode 100644 index 00000000..1c682704 --- /dev/null +++ b/packages/ai/providers/vercel/utils.ts @@ -0,0 +1,47 @@ +import { AIProvider, AIProviderError, ILogger } from '../../types'; + +/** + * Check if the model is a reasoning model (like OpenAI o1/o3) + */ +export function isReasoningModel(modelId: string): boolean { + return /^o[13]-/.test(modelId); +} + +/** + * Maps common AI SDK errors to internal AIProviderError + */ +export function mapToProviderError( + error: any, + provider: AIProvider, + fallbackCode = 'GENERATION_FAILED', +) { + if (error?.name === 'APICallError' && error.statusCode === 429) { + return new AIProviderError('RATE_LIMIT', error.message, provider, error); + } + + return new AIProviderError(fallbackCode, error?.message ?? 'Unknown error', provider, error); +} + +/** + * Helper to log messages using the provided logger or falling back to console + */ +export function log( + logger: ILogger | undefined, + debug: boolean, + level: 'info' | 'warn' | 'debug' | 'error', + message: string, + error?: unknown, +) { + const loggerFn = logger?.[level]; + + if (loggerFn) { + error ? loggerFn.call(logger, message, error) : loggerFn.call(logger, message); + return; + } + + // Fallback if no logger provided or method missing + if (debug || level !== 'debug') { + const consoleFn = console[level === 'debug' ? 'log' : level] || console.log; + error ? consoleFn(message, error) : consoleFn(message); + } +} diff --git a/packages/ai/types.ts b/packages/ai/types.ts index b160d0df..37af2f49 100644 --- a/packages/ai/types.ts +++ b/packages/ai/types.ts @@ -3,6 +3,8 @@ */ import { CodeSignature } from '@sintesi/core'; +import { ILogger } from '@sintesi/shared'; +export { ILogger }; /** * Supported AI providers @@ -32,18 +34,6 @@ export interface AIModel { endpoint?: string; } -/** - * Interface for a logger to be injected into the AI module - */ -export interface ILogger { - debug(message: string, ...args: unknown[]): void; - info(message: string, ...args: unknown[]): void; - warn(message: string, ...args: unknown[]): void; - error(message: string, ...args: unknown[]): void; - success?(message: string, ...args: unknown[]): void; - log?(message: string, ...args: unknown[]): void; -} - /** * Configuration for a specific AI Agent role (e.g., planner, writer) */ diff --git a/packages/cli/package.json b/packages/cli/package.json index 3e389ca6..601043fd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,6 +50,7 @@ "@helicone/ai-sdk-provider": "^1.0.11", "@lancedb/lancedb": "^0.23.0", "@sintesi/core": "workspace:*", + "@sintesi/shared": "workspace:*", "@types/uuid": "^10.0.0", "ai": "^5.0.106", "cohere-ai": "^7.20.0", diff --git a/packages/cli/src/services/generation-context.ts b/packages/cli/src/services/generation-context.ts index d7327d75..dbb3ea7a 100644 --- a/packages/cli/src/services/generation-context.ts +++ b/packages/cli/src/services/generation-context.ts @@ -4,7 +4,7 @@ import { resolve, relative, dirname, join } from 'path'; import { readFileSync, existsSync } from 'fs'; import { execSync } from 'child_process'; import { ChangeAnalysisService } from './analysis-service'; -import { createAIAgentsFromEnv, AIAgents, AIAgentRoleConfig } from '../../../ai'; +import { createAIAgentsFromEnv, AIAgents } from '../../../ai'; import { getContextPrompt } from '../prompts/analysis'; import { RetrievalService } from './rag'; import { SkeletonizerService } from './skeletonizer'; @@ -79,34 +79,15 @@ export class GenerationContextService { /** * Initializes AI Agents from environment variables. */ - async getAIAgents( - verbose: boolean, - plannerModelId?: string, - writerModelId?: string, - ): Promise { + async getAIAgents(verbose: boolean): Promise { try { - const plannerConfig: AIAgentRoleConfig = { - modelId: plannerModelId || process.env.SINTESI_PLANNER_MODEL_ID || '', - provider: process.env.SINTESI_PLANNER_PROVIDER as any, - }; - const writerConfig: AIAgentRoleConfig = { - modelId: writerModelId || process.env.SINTESI_WRITER_MODEL_ID || '', - provider: process.env.SINTESI_WRITER_PROVIDER as any, - }; - - const aiAgents = createAIAgentsFromEnv( - { debug: verbose, logger: this.logger }, - { planner: plannerConfig, writer: writerConfig }, - ); + const aiAgents = createAIAgentsFromEnv({ + debug: verbose, + logger: this.logger, + }); const writerConnected = await aiAgents.writer.validateConnection(); - - // If planner is different from writer, check it too, otherwise we assume if writer works, planner likely works (or it's the same) - // But let's be safe if they are distinct instances - let plannerConnected = true; - if (aiAgents.planner !== aiAgents.writer) { - plannerConnected = await aiAgents.planner.validateConnection(); - } + const plannerConnected = await aiAgents.planner.validateConnection(); if (!writerConnected || !plannerConnected) { this.logger.error('AI provider connection failed. Please check your API key.'); diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index 7e8acb7c..92eaadbf 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -1,210 +1,4 @@ -/** - * Simple CLI logger with colored output - */ - -import type { ILogger } from '../../../ai'; - -export enum LogLevel { - ERROR = 'error', - WARN = 'warn', - INFO = 'info', - SUCCESS = 'success', - DEBUG = 'debug', -} - -/** - * ANSI color codes for terminal output - */ -const colors = { - reset: '\x1b[0m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m', - gray: '\x1b[90m', - bold: '\x1b[1m', -}; - -/** - * CLI Logger for formatted console output - * Implements ILogger interface for compatibility with AI module - */ -export class Logger implements ILogger { - private verbose: boolean; - - constructor(verbose: boolean = false) { - this.verbose = verbose; - } - - /** - * Log an error message - */ - public error(message: string, ...args: unknown[]): void { - console.error(`${colors.red}✗${colors.reset} ${message}`, ...args); - } - - /** - * Log a warning message - */ - public warn(message: string, ...args: unknown[]): void { - console.warn(`${colors.yellow}⚠${colors.reset} ${message}`, ...args); - } - - /** - * Log an info message - */ - public info(message: string, ...args: unknown[]): void { - console.log(`${colors.blue}ℹ${colors.reset} ${message}`, ...args); - } - - /** - * Log a success message - */ - public success(message: string, ...args: unknown[]): void { - console.log(`${colors.green}✓${colors.reset} ${message}`, ...args); - } - - /** - * Log a debug message (only in verbose mode) - */ - public debug(message: string, ...args: unknown[]): void { - if (this.verbose) { - console.log(`${colors.gray}[DEBUG]${colors.reset} ${message}`, ...args); - } - } - - /** - * Log a plain message without prefix - */ - public log(message: string, ...args: unknown[]): void { - console.log(message, ...args); - } - - /** - * Log a section header - */ - public header(message: string): void { - console.log(`\n${colors.bold}${colors.cyan}${message}${colors.reset}\n`); - } - - /** - * Log a divider line - */ - public divider(): void { - console.log(colors.gray + '─'.repeat(60) + colors.reset); - } - - /** - * Create a new line - */ - public newline(): void { - console.log(); - } - - /** - * Format text with color - */ - public static color(text: string, color: keyof typeof colors): string { - return `${colors[color]}${text}${colors.reset}`; - } - - /** - * Format text as bold - */ - public static bold(text: string): string { - return `${colors.bold}${text}${colors.reset}`; - } - - /** - * Format file path - */ - public static path(filePath: string): string { - return `${colors.cyan}${filePath}${colors.reset}`; - } - - /** - * Format symbol name - */ - public static symbol(symbolName: string): string { - return `${colors.magenta}${symbolName}${colors.reset}`; - } - - /** - * Format hash (shortened) - */ - public static hash(hash: string, length: number = 8): string { - return `${colors.gray}${hash.substring(0, length)}${colors.reset}`; - } - - /** - * Get verbose status - */ - public getVerbose(): boolean { - return this.verbose; - } - - /** - * Print a banner with text - */ - public banner(text: string): void { - const width = 60; - const padding = Math.max(0, Math.floor((width - text.length - 2) / 2)); - const line = '═'.repeat(width); - - console.log(`${colors.cyan}${colors.bold}`); - console.log(`╔${line}╗`); - console.log(`║${' '.repeat(padding)}${text}${' '.repeat(width - padding - text.length)}║`); - console.log(`╚${line}╝`); - console.log(colors.reset); - } - - /** - * Print a box around text - */ - public box(title: string, content: string[]): void { - const maxLength = Math.max( - title.length, - // eslint-disable-next-line no-control-regex - ...content.map((line) => line.replace(/\x1b\[[0-9;]*m/g, '').length), - ); - const width = Math.min(maxLength + 4, 70); - - console.log(`${colors.cyan}┌${'─'.repeat(width)}┐${colors.reset}`); - console.log( - `${colors.cyan}│${colors.reset} ${colors.bold}${title}${colors.reset}${' '.repeat(width - title.length - 1)}${colors.cyan}│${colors.reset}`, - ); - console.log(`${colors.cyan}├${'─'.repeat(width)}┤${colors.reset}`); - - content.forEach((line) => { - // eslint-disable-next-line no-control-regex - const stripped = line.replace(/\x1b\[[0-9;]*m/g, ''); - const padding = width - stripped.length - 1; - console.log( - `${colors.cyan}│${colors.reset} ${line}${' '.repeat(padding)}${colors.cyan}│${colors.reset}`, - ); - }); - - console.log(`${colors.cyan}└${'─'.repeat(width)}┘${colors.reset}`); - } - - /** - * Print a step header - */ - public step(stepNumber: number, totalSteps: number, title: string, emoji: string = '▶'): void { - console.log(); - console.log( - `${colors.bold}${colors.cyan}${emoji} Step ${stepNumber}/${totalSteps}: ${colors.reset}${colors.bold}${title}${colors.reset}`, - ); - console.log(); - } - - /** - * Print an item in a list - */ - public listItem(text: string, checked: boolean = false): void { - const icon = checked ? `${colors.green}✓${colors.reset}` : `${colors.gray}○${colors.reset}`; - console.log(` ${icon} ${text}`); - } -} +export * from '@sintesi/shared'; +import { Logger } from '@sintesi/shared'; +export { Logger }; +export default Logger; diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 00000000..a23d527f --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,15 @@ +{ + "name": "@sintesi/shared", + "version": "0.1.0", + "private": true, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "clean": "rm -rf dist" + }, + "devDependencies": { + "@types/node": "^20.10.6", + "typescript": "^5.3.3" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 00000000..f48c6f62 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,150 @@ +/** + * Interface for a logger + */ +export interface ILogger { + debug(message: string, ...args: unknown[]): void; + info(message: string, ...args: unknown[]): void; + warn(message: string, ...args: unknown[]): void; + error(message: string, ...args: unknown[]): void; + success?(message: string, ...args: unknown[]): void; + log?(message: string, ...args: unknown[]): void; +} + +/** + * ANSI color codes for terminal output + */ +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + gray: '\x1b[90m', + bold: '\x1b[1m', +}; + +/** + * CLI Logger for formatted console output + */ +export class Logger implements ILogger { + private verbose: boolean; + + constructor(verbose: boolean = false) { + this.verbose = verbose; + } + + public error(message: string, ...args: unknown[]): void { + console.error(`${colors.red}✗${colors.reset} ${message}`, ...args); + } + + public warn(message: string, ...args: unknown[]): void { + console.warn(`${colors.yellow}⚠${colors.reset} ${message}`, ...args); + } + + public info(message: string, ...args: unknown[]): void { + console.log(`${colors.blue}ℹ${colors.reset} ${message}`, ...args); + } + + public success(message: string, ...args: unknown[]): void { + console.log(`${colors.green}✓${colors.reset} ${message}`, ...args); + } + + public debug(message: string, ...args: unknown[]): void { + if (this.verbose) { + console.log(`${colors.gray}[DEBUG]${colors.reset} ${message}`, ...args); + } + } + + public log(message: string, ...args: unknown[]): void { + console.log(message, ...args); + } + + public header(message: string): void { + console.log(`\n${colors.bold}${colors.cyan}${message}${colors.reset}\n`); + } + + public divider(): void { + console.log(colors.gray + '─'.repeat(60) + colors.reset); + } + + public newline(): void { + console.log(); + } + + public static color(text: string, color: keyof typeof colors): string { + return `${colors[color]}${text}${colors.reset}`; + } + + public static bold(text: string): string { + return `${colors.bold}${text}${colors.reset}`; + } + + public static path(filePath: string): string { + return `${colors.cyan}${filePath}${colors.reset}`; + } + + public static symbol(symbolName: string): string { + return `${colors.magenta}${symbolName}${colors.reset}`; + } + + public static hash(hash: string, length: number = 8): string { + return `${colors.gray}${hash.substring(0, length)}${colors.reset}`; + } + + public getVerbose(): boolean { + return this.verbose; + } + + public banner(text: string): void { + const width = 60; + const padding = Math.max(0, Math.floor((width - text.length - 2) / 2)); + const line = '═'.repeat(width); + + console.log(`${colors.cyan}${colors.bold}`); + console.log(`╔${line}╗`); + console.log(`║${' '.repeat(padding)}${text}${' '.repeat(width - padding - text.length)}║`); + console.log(`╚${line}╝`); + console.log(colors.reset); + } + + public box(title: string, content: string[]): void { + const maxLength = Math.max( + title.length, + // eslint-disable-next-line no-control-regex + ...content.map((line) => line.replace(/\x1b\[[0-9;]*m/g, '').length), + ); + const width = Math.min(maxLength + 4, 70); + + console.log(`${colors.cyan}┌${'─'.repeat(width)}┐${colors.reset}`); + console.log( + `${colors.cyan}│${colors.reset} ${colors.bold}${title}${colors.reset}${' '.repeat(width - title.length - 1)}${colors.cyan}│${colors.reset}`, + ); + console.log(`${colors.cyan}├${'─'.repeat(width)}┤${colors.reset}`); + + content.forEach((line) => { + // eslint-disable-next-line no-control-regex + const stripped = line.replace(/\x1b\[[0-9;]*m/g, ''); + const padding = width - stripped.length - 1; + console.log( + `${colors.cyan}│${colors.reset} ${line}${' '.repeat(padding)}${colors.cyan}│${colors.reset}`, + ); + }); + + console.log(`${colors.cyan}└${'─'.repeat(width)}┘${colors.reset}`); + } + + public step(stepNumber: number, totalSteps: number, title: string, emoji: string = '▶'): void { + console.log(); + console.log( + `${colors.bold}${colors.cyan}${emoji} Step ${stepNumber}/${totalSteps}: ${colors.reset}${colors.bold}${title}${colors.reset}`, + ); + console.log(); + } + + public listItem(text: string, checked: boolean = false): void { + const icon = checked ? `${colors.green}✓${colors.reset}` : `${colors.gray}○${colors.reset}`; + console.log(` ${icon} ${text}`); + } +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 00000000..2b3a413d --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": ".", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c95d0ea7..26ad8fc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: '@sintesi/core': specifier: workspace:* version: link:../core + '@sintesi/shared': + specifier: workspace:* + version: link:../shared '@types/uuid': specifier: ^10.0.0 version: 10.0.0 @@ -189,6 +192,15 @@ importers: specifier: ^5.3.3 version: 5.9.3 + packages/shared: + devDependencies: + '@types/node': + specifier: ^20.10.6 + version: 20.19.27 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + packages: /@ai-sdk/anthropic@2.0.56(zod@4.2.1): From 0fe1f43c08ad51eb7e9c274852ae1aec599f4162 Mon Sep 17 00:00:00 2001 From: Alessio Pelliccione Date: Sat, 20 Dec 2025 19:45:17 +0100 Subject: [PATCH 2/5] refactor: final polish of AI package, centralize domain types in types.ts --- packages/ai/index.ts | 11 ++++- packages/ai/markdown-builder.ts | 2 +- packages/ai/providers/vercel/provider.ts | 2 +- packages/ai/structured-schema.ts | 40 ++++++++-------- packages/ai/types.ts | 59 ++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 22 deletions(-) diff --git a/packages/ai/index.ts b/packages/ai/index.ts index 5a1799b7..f09d277f 100644 --- a/packages/ai/index.ts +++ b/packages/ai/index.ts @@ -5,10 +5,19 @@ */ export * from './types'; + export * from './config-loader'; -export * from './ai-agent'; // Exports AIAgent, createOpenAIAgent, createAIAgentsFromEnv + +export * from './ai-agent'; + +export * from './constants'; + +export * from './structured-schema'; + +export * from './markdown-builder'; export * from './providers/vercel'; + export * from './constants'; export * from './structured-schema'; export * from './markdown-builder'; diff --git a/packages/ai/markdown-builder.ts b/packages/ai/markdown-builder.ts index aa216509..d5cfb1c4 100644 --- a/packages/ai/markdown-builder.ts +++ b/packages/ai/markdown-builder.ts @@ -6,7 +6,7 @@ * JSON artifacts and ensures all documentation follows the same format. */ -import type { DocumentationStructure, Parameter, ReturnTypeInfo } from './structured-schema'; +import type { DocumentationStructure, Parameter, ReturnTypeInfo } from './types'; /** * Build Markdown documentation from structured data diff --git a/packages/ai/providers/vercel/provider.ts b/packages/ai/providers/vercel/provider.ts index f08faf87..6a75c05a 100644 --- a/packages/ai/providers/vercel/provider.ts +++ b/packages/ai/providers/vercel/provider.ts @@ -13,8 +13,8 @@ import { generateObject, generateText } from 'ai'; import { BatchDocumentationStructureSchema, DocumentationStructureSchema, - type DocumentationStructure, } from '../../structured-schema'; +import { DocumentationStructure } from '../../types'; import { buildMarkdownFromStructure } from '../../markdown-builder'; import { z } from 'zod'; import { ModelFactory } from './model-factory'; diff --git a/packages/ai/structured-schema.ts b/packages/ai/structured-schema.ts index 384a7d80..29b74365 100644 --- a/packages/ai/structured-schema.ts +++ b/packages/ai/structured-schema.ts @@ -6,6 +6,14 @@ */ import { z } from 'zod'; +import type { + Parameter, + ReturnTypeInfo, + DocumentationStructure, + BatchDocumentationStructure, + Property, + ExtendedDocumentation, +} from './types'; /** * Schema for a single parameter @@ -24,9 +32,7 @@ export const ParameterSchema = z.object({ .describe('Brief description of what this parameter does'), optional: z.boolean().optional().describe('Whether this parameter is optional'), defaultValue: z.string().trim().optional().describe('Default value if any'), -}); - -export type Parameter = z.infer; +}) as z.ZodType; /** * Schema for return type information @@ -42,14 +48,12 @@ export const ReturnTypeSchema = z.object({ .trim() .min(1, 'Return type description cannot be empty') .describe('Description of what is returned'), -}); - -export type ReturnTypeInfo = z.infer; +}) as z.ZodType; /** - * Schema for a single documentation entry + * Internal Zod Object for DocumentationStructure to allow extension */ -export const DocumentationStructureSchema = z.object({ +const DocumentationStructureObject = z.object({ symbolName: z .string() .trim() @@ -78,16 +82,18 @@ export const DocumentationStructureSchema = z.object({ .describe('Additional notes, caveats, or important information'), }); -export type DocumentationStructure = z.infer; +/** + * Schema for a single documentation entry + */ +export const DocumentationStructureSchema = + DocumentationStructureObject as z.ZodType; /** * Schema for batch documentation generation */ export const BatchDocumentationStructureSchema = z.object({ documentations: z.array(DocumentationStructureSchema), -}); - -export type BatchDocumentationStructure = z.infer; +}) as z.ZodType; /** * Schema for properties (interfaces, classes) @@ -102,18 +108,14 @@ export const PropertySchema = z.object({ .describe('What this property represents'), optional: z.boolean().optional().describe('Whether this property is optional'), readonly: z.boolean().optional().describe('Whether this property is readonly'), -}); - -export type Property = z.infer; +}) as z.ZodType; /** * Extended schema for complex types (classes, interfaces) */ -export const ExtendedDocumentationSchema = DocumentationStructureSchema.extend({ +export const ExtendedDocumentationSchema = DocumentationStructureObject.extend({ properties: z.array(PropertySchema).optional().describe('Properties for classes/interfaces'), methods: z.array(DocumentationStructureSchema).optional().describe('Methods for classes'), extends: z.string().optional().describe('Parent class or extended interfaces'), implements: z.array(z.string()).optional().describe('Implemented interfaces'), -}); - -export type ExtendedDocumentation = z.infer; +}) as z.ZodType; diff --git a/packages/ai/types.ts b/packages/ai/types.ts index 37af2f49..bb9565fb 100644 --- a/packages/ai/types.ts +++ b/packages/ai/types.ts @@ -276,3 +276,62 @@ export interface GenerateOptions { /** Observability metadata for tracking */ metadata?: ObservabilityMetadata; } + +/** + * Structured data for a single parameter + */ +export interface Parameter { + name: string; + type: string; + description: string; + optional?: boolean; + defaultValue?: string; +} + +/** + * Structured data for return type information + */ +export interface ReturnTypeInfo { + type: string; + description: string; +} + +/** + * Structured data for properties (interfaces, classes) + */ +export interface Property { + name: string; + type: string; + description: string; + optional?: boolean; + readonly?: boolean; +} + +/** + * Core documentation structure + */ +export interface DocumentationStructure { + symbolName: string; + purpose: string; + parameters?: Parameter[]; + returnType?: ReturnTypeInfo; + usageExample?: string; + notes?: string[]; +} + +/** + * Extended documentation structure for complex types + */ +export interface ExtendedDocumentation extends DocumentationStructure { + properties?: Property[]; + methods?: DocumentationStructure[]; + extends?: string; + implements?: string[]; +} + +/** + * Batch documentation structure + */ +export interface BatchDocumentationStructure { + documentations: DocumentationStructure[]; +} From d4ac117b23cebc029ca4339869807f833abe0d8f Mon Sep 17 00:00:00 2001 From: Alessio Pelliccione Date: Sat, 20 Dec 2025 19:46:53 +0100 Subject: [PATCH 3/5] refactor: centralize environment access and improve error type safety in AI package --- packages/ai/config-loader.ts | 3 +++ packages/ai/index.ts | 12 +---------- packages/ai/providers/vercel/model-factory.ts | 2 +- packages/ai/providers/vercel/provider.ts | 2 +- packages/ai/providers/vercel/utils.ts | 4 ++-- packages/ai/types.ts | 20 +++++++++++++++++-- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/ai/config-loader.ts b/packages/ai/config-loader.ts index 30c90ad5..47c92826 100644 --- a/packages/ai/config-loader.ts +++ b/packages/ai/config-loader.ts @@ -23,6 +23,9 @@ export class ConfigLoader { process.env.AI_TEMPERATURE || String(DEFAULT_TEMPERATURES.writer), ), endpoint: process.env.AI_ENDPOINT, + observability: { + heliconeApiKey: process.env.HELICONE_API_KEY, + }, }; const roles = { diff --git a/packages/ai/index.ts b/packages/ai/index.ts index f09d277f..da01ee8e 100644 --- a/packages/ai/index.ts +++ b/packages/ai/index.ts @@ -5,19 +5,9 @@ */ export * from './types'; - +export * from './constants'; export * from './config-loader'; - export * from './ai-agent'; - -export * from './constants'; - export * from './structured-schema'; - export * from './markdown-builder'; - export * from './providers/vercel'; - -export * from './constants'; -export * from './structured-schema'; -export * from './markdown-builder'; diff --git a/packages/ai/providers/vercel/model-factory.ts b/packages/ai/providers/vercel/model-factory.ts index 4d87d978..575c3b80 100644 --- a/packages/ai/providers/vercel/model-factory.ts +++ b/packages/ai/providers/vercel/model-factory.ts @@ -62,7 +62,7 @@ export class ModelFactory { } private tryCreateHeliconeModel(metadata: ObservabilityMetadata) { - const heliconeApiKey = process.env.HELICONE_API_KEY; + const heliconeApiKey = this.#config.modelConfig.observability?.heliconeApiKey; if (!heliconeApiKey) return null; try { diff --git a/packages/ai/providers/vercel/provider.ts b/packages/ai/providers/vercel/provider.ts index 6a75c05a..bab43855 100644 --- a/packages/ai/providers/vercel/provider.ts +++ b/packages/ai/providers/vercel/provider.ts @@ -54,7 +54,7 @@ export class VercelAIProvider implements IAIProvider { try { return await task(this.#modelFactory.getModel(metadata)); } catch (error) { - if (metadata && process.env.HELICONE_API_KEY) { + if (metadata && this.#modelConfig.observability?.heliconeApiKey) { log( this.#logger, this.#debug, diff --git a/packages/ai/providers/vercel/utils.ts b/packages/ai/providers/vercel/utils.ts index 1c682704..7e2a2464 100644 --- a/packages/ai/providers/vercel/utils.ts +++ b/packages/ai/providers/vercel/utils.ts @@ -1,4 +1,4 @@ -import { AIProvider, AIProviderError, ILogger } from '../../types'; +import { AIProvider, AIProviderError, ILogger, AIErrorCode } from '../../types'; /** * Check if the model is a reasoning model (like OpenAI o1/o3) @@ -13,7 +13,7 @@ export function isReasoningModel(modelId: string): boolean { export function mapToProviderError( error: any, provider: AIProvider, - fallbackCode = 'GENERATION_FAILED', + fallbackCode: AIErrorCode = 'GENERATION_FAILED', ) { if (error?.name === 'APICallError' && error.statusCode === 429) { return new AIProviderError('RATE_LIMIT', error.message, provider, error); diff --git a/packages/ai/types.ts b/packages/ai/types.ts index bb9565fb..0a278ddd 100644 --- a/packages/ai/types.ts +++ b/packages/ai/types.ts @@ -32,6 +32,11 @@ export interface AIModel { /** API endpoint (optional, for custom endpoints) */ endpoint?: string; + + /** Observability configuration (e.g., Helicone) */ + observability?: { + heliconeApiKey?: string; + }; } /** @@ -107,12 +112,23 @@ export interface DocumentationResponse { timestamp: number; } +/** + * Common error codes for AI providers + */ +export type AIErrorCode = + | 'TIMEOUT' + | 'RATE_LIMIT' + | 'NETWORK_ERROR' + | 'INVALID_API_KEY' + | 'GENERATION_FAILED' + | 'MODEL_NOT_FOUND'; + /** * Error from AI provider */ export class AIProviderError extends Error { /** Error code */ - code: string; + code: AIErrorCode; /** Provider name */ provider: AIProvider; @@ -120,7 +136,7 @@ export class AIProviderError extends Error { /** Original error (if available) */ originalError?: unknown; - constructor(code: string, message: string, provider: AIProvider, originalError?: unknown) { + constructor(code: AIErrorCode, message: string, provider: AIProvider, originalError?: unknown) { super(message); this.name = 'AIProviderError'; this.code = code; From 2153de682dcb467a6cda23be0d399d586565d0c9 Mon Sep 17 00:00:00 2001 From: Alessio Pelliccione Date: Sat, 20 Dec 2025 19:48:27 +0100 Subject: [PATCH 4/5] refactor: support multi-provider configurations and role-specific environment overrides in AI package --- packages/ai/ai-agent.ts | 9 ++---- packages/ai/config-loader.ts | 60 ++++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/packages/ai/ai-agent.ts b/packages/ai/ai-agent.ts index 6c967fd1..208dc045 100644 --- a/packages/ai/ai-agent.ts +++ b/packages/ai/ai-agent.ts @@ -165,12 +165,9 @@ export function createAIAgentsFromEnv( const config = ConfigLoader.loadFromEnv(); const agents: Partial = {}; - for (const [role, roleConfig] of Object.entries(config.roles)) { + for (const [role, modelConfig] of Object.entries(config.roles)) { const agentConfig: AIAgentConfig = { - model: { - ...config.globalModel, - ...roleConfig, - }, + model: modelConfig, debug: options.debug, timeout: options.timeout, logger: options.logger, @@ -180,7 +177,7 @@ export function createAIAgentsFromEnv( if (options.logger) { options.logger.debug( - `[AIAgentManager] ${role} initialized with ${roleConfig.modelId} (${roleConfig.provider || config.globalModel.provider})`, + `[AIAgentManager] ${role} initialized with ${modelConfig.modelId} (${modelConfig.provider})`, ); } } diff --git a/packages/ai/config-loader.ts b/packages/ai/config-loader.ts index 47c92826..b8f713f5 100644 --- a/packages/ai/config-loader.ts +++ b/packages/ai/config-loader.ts @@ -1,9 +1,8 @@ -import { AIProvider, AIModel, AIAgentRoleConfig } from './types'; +import { AIProvider, AIModel } from './types'; import { DEFAULT_MODELS, DEFAULT_TEMPERATURES } from './constants'; export interface AIAgentsConfig { - roles: Record; - globalModel: AIModel; + roles: Record; } export class ConfigLoader { @@ -11,52 +10,67 @@ export class ConfigLoader { * Loads agent configurations from environment variables */ static loadFromEnv(): AIAgentsConfig { - const provider = (process.env.AI_PROVIDER as AIProvider) || 'openai'; - const apiKey = this.getApiKey(provider); + const globalProvider = (process.env.AI_PROVIDER as AIProvider) || 'openai'; - const globalModel: AIModel = { - provider, - apiKey, - modelId: process.env.AI_MODEL_ID || DEFAULT_MODELS[provider], + const globalDefaults = { + provider: globalProvider, + modelId: process.env.AI_MODEL_ID || DEFAULT_MODELS[globalProvider], maxTokens: parseInt(process.env.AI_MAX_TOKENS || '2000'), temperature: parseFloat( process.env.AI_TEMPERATURE || String(DEFAULT_TEMPERATURES.writer), ), endpoint: process.env.AI_ENDPOINT, - observability: { - heliconeApiKey: process.env.HELICONE_API_KEY, - }, + heliconeApiKey: process.env.HELICONE_API_KEY, }; const roles = { - planner: this.loadRoleConfig('PLANNER', globalModel, DEFAULT_TEMPERATURES.planner), - researcher: this.loadRoleConfig( + planner: this.loadRoleModel('PLANNER', globalDefaults, DEFAULT_TEMPERATURES.planner), + researcher: this.loadRoleModel( 'RESEARCHER', - globalModel, + globalDefaults, DEFAULT_TEMPERATURES.researcher, ), - writer: this.loadRoleConfig('WRITER', globalModel, DEFAULT_TEMPERATURES.writer), - reviewer: this.loadRoleConfig('REVIEWER', globalModel, DEFAULT_TEMPERATURES.reviewer), + writer: this.loadRoleModel('WRITER', globalDefaults, DEFAULT_TEMPERATURES.writer), + reviewer: this.loadRoleModel('REVIEWER', globalDefaults, DEFAULT_TEMPERATURES.reviewer), }; - return { globalModel, roles }; + return { roles }; } - private static loadRoleConfig( + private static loadRoleModel( prefix: string, - fallback: AIModel, + fallback: { + provider: AIProvider; + modelId: string; + maxTokens: number; + temperature: number; + endpoint?: string; + heliconeApiKey?: string; + }, defaultTemp: number, - ): AIAgentRoleConfig { + ): AIModel { const provider = (process.env[`${prefix}_PROVIDER`] as AIProvider) || fallback.provider; + const apiKey = this.getApiKey(provider); + return { provider, - modelId: process.env[`${prefix}_MODEL_ID`] || DEFAULT_MODELS[provider], + apiKey, + modelId: + process.env[`${prefix}_MODEL_ID`] || + (provider === fallback.provider ? fallback.modelId : DEFAULT_MODELS[provider]), temperature: process.env[`${prefix}_TEMPERATURE`] ? parseFloat(process.env[`${prefix}_TEMPERATURE`]!) - : defaultTemp, + : provider === fallback.provider + ? fallback.temperature + : defaultTemp, maxTokens: process.env[`${prefix}_MAX_TOKENS`] ? parseInt(process.env[`${prefix}_MAX_TOKENS`]!) : fallback.maxTokens, + endpoint: process.env[`${prefix}_ENDPOINT`] || fallback.endpoint, + observability: { + heliconeApiKey: + process.env[`${prefix}_HELICONE_API_KEY`] || fallback.heliconeApiKey, + }, }; } From eb02683465d9069e587c4d02ad06e33b5c67d6b5 Mon Sep 17 00:00:00 2001 From: Alessio Pelliccione Date: Sat, 20 Dec 2025 19:52:41 +0100 Subject: [PATCH 5/5] refactor: reorganize AI package into subdirectories for better modularity --- packages/ai/{ => agents}/ai-agent.ts | 6 +-- packages/ai/{ => config}/config-loader.ts | 4 +- packages/ai/{ => core}/constants.ts | 0 packages/ai/core/index.ts | 2 + packages/ai/{ => core}/types.ts | 0 .../{ => documentation}/markdown-builder.ts | 51 +++++++------------ .../{ => documentation}/structured-schema.ts | 2 +- packages/ai/index.ts | 17 ++----- packages/ai/providers/vercel/model-factory.ts | 2 +- .../ai/providers/vercel/options-builder.ts | 2 +- packages/ai/providers/vercel/provider.ts | 8 +-- packages/ai/providers/vercel/utils.ts | 2 +- 12 files changed, 39 insertions(+), 57 deletions(-) rename packages/ai/{ => agents}/ai-agent.ts (97%) rename packages/ai/{ => config}/config-loader.ts (96%) rename packages/ai/{ => core}/constants.ts (100%) create mode 100644 packages/ai/core/index.ts rename packages/ai/{ => core}/types.ts (100%) rename packages/ai/{ => documentation}/markdown-builder.ts (54%) rename packages/ai/{ => documentation}/structured-schema.ts (99%) diff --git a/packages/ai/ai-agent.ts b/packages/ai/agents/ai-agent.ts similarity index 97% rename from packages/ai/ai-agent.ts rename to packages/ai/agents/ai-agent.ts index 208dc045..c9378391 100644 --- a/packages/ai/ai-agent.ts +++ b/packages/ai/agents/ai-agent.ts @@ -13,9 +13,9 @@ import { AIProvider, ILogger, ObservabilityMetadata, -} from './types'; -import { VercelAIProvider } from './providers/vercel'; -import { ConfigLoader } from './config-loader'; +} from '../core/types'; +import { VercelAIProvider } from '../providers/vercel'; +import { ConfigLoader } from '../config/config-loader'; /** * Collection of AI Agent instances for different roles. diff --git a/packages/ai/config-loader.ts b/packages/ai/config/config-loader.ts similarity index 96% rename from packages/ai/config-loader.ts rename to packages/ai/config/config-loader.ts index b8f713f5..79514ac4 100644 --- a/packages/ai/config-loader.ts +++ b/packages/ai/config/config-loader.ts @@ -1,5 +1,5 @@ -import { AIProvider, AIModel } from './types'; -import { DEFAULT_MODELS, DEFAULT_TEMPERATURES } from './constants'; +import { AIProvider, AIModel } from '../core/types'; +import { DEFAULT_MODELS, DEFAULT_TEMPERATURES } from '../core/constants'; export interface AIAgentsConfig { roles: Record; diff --git a/packages/ai/constants.ts b/packages/ai/core/constants.ts similarity index 100% rename from packages/ai/constants.ts rename to packages/ai/core/constants.ts diff --git a/packages/ai/core/index.ts b/packages/ai/core/index.ts new file mode 100644 index 00000000..33c8572a --- /dev/null +++ b/packages/ai/core/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './constants'; diff --git a/packages/ai/types.ts b/packages/ai/core/types.ts similarity index 100% rename from packages/ai/types.ts rename to packages/ai/core/types.ts diff --git a/packages/ai/markdown-builder.ts b/packages/ai/documentation/markdown-builder.ts similarity index 54% rename from packages/ai/markdown-builder.ts rename to packages/ai/documentation/markdown-builder.ts index d5cfb1c4..53491505 100644 --- a/packages/ai/markdown-builder.ts +++ b/packages/ai/documentation/markdown-builder.ts @@ -1,41 +1,34 @@ /** * Deterministic Markdown builder from structured documentation - * - * This module takes structured JSON documentation and builds consistent, - * well-formatted Markdown output using templates. This approach eliminates - * JSON artifacts and ensures all documentation follows the same format. */ -import type { DocumentationStructure, Parameter, ReturnTypeInfo } from './types'; +import type { DocumentationStructure, Parameter, ReturnTypeInfo } from '../core/types'; /** * Build Markdown documentation from structured data - * - * This is the core function that converts JSON structure to Markdown. - * The template is deterministic - the same input always produces the same output. */ export function buildMarkdownFromStructure(doc: DocumentationStructure): string { const parts: string[] = []; - // 1. Purpose (always present) - parts.push(`**Purpose:** ${doc.purpose}`); + // 1. Purpose + parts.push('**Purpose:** ' + doc.purpose); - // 2. Parameters (if present) + // 2. Parameters if (doc.parameters && doc.parameters.length > 0) { parts.push(''); parts.push('**Parameters:**'); - doc.parameters.forEach((param) => { + for (const param of doc.parameters) { parts.push(formatParameter(param)); - }); + } } - // 3. Return Type (if present) + // 3. Return Type if (doc.returnType) { parts.push(''); parts.push(formatReturnType(doc.returnType)); } - // 4. Usage Example (if present) + // 4. Usage Example if (doc.usageExample) { parts.push(''); parts.push('**Usage Example:**'); @@ -44,13 +37,13 @@ export function buildMarkdownFromStructure(doc: DocumentationStructure): string parts.push('```'); } - // 5. Notes (if present) + // 5. Notes if (doc.notes && doc.notes.length > 0) { parts.push(''); parts.push('**Notes:**'); - doc.notes.forEach((note) => { - parts.push(`- ${note}`); - }); + for (const note of doc.notes) { + parts.push('- ' + note); + } } return parts.join('\n'); @@ -60,36 +53,30 @@ export function buildMarkdownFromStructure(doc: DocumentationStructure): string * Format a single parameter */ function formatParameter(param: Parameter): string { - const parts: string[] = [`- \`${param.name}\``]; + let line = '- `' + param.name + '`'; - // Add optional marker if (param.optional) { - parts.push('(optional)'); + line += ' (optional)'; } - // Add type - parts.push(`(\`${param.type}\`):`); + line += ' (`' + param.type + '`): ' + param.description; - // Add description - parts.push(param.description); - - // Add default value if present if (param.defaultValue) { - parts.push(`Default: \`${param.defaultValue}\``); + line += ' Default: `' + param.defaultValue + '`'; } - return parts.join(' '); + return line; } /** * Format return type information */ function formatReturnType(returnType: ReturnTypeInfo): string { - return `**Returns:** \`${returnType.type}\` - ${returnType.description}`; + return '**Returns:** `' + returnType.type + '` - ' + returnType.description; } /** - * Build markdown for multiple symbols (used in batch generation) + * Build markdown for multiple symbols */ export function buildBatchMarkdown( docs: DocumentationStructure[], diff --git a/packages/ai/structured-schema.ts b/packages/ai/documentation/structured-schema.ts similarity index 99% rename from packages/ai/structured-schema.ts rename to packages/ai/documentation/structured-schema.ts index 29b74365..5ccc10ee 100644 --- a/packages/ai/structured-schema.ts +++ b/packages/ai/documentation/structured-schema.ts @@ -13,7 +13,7 @@ import type { BatchDocumentationStructure, Property, ExtendedDocumentation, -} from './types'; +} from '../core/types'; /** * Schema for a single parameter diff --git a/packages/ai/index.ts b/packages/ai/index.ts index da01ee8e..aea85211 100644 --- a/packages/ai/index.ts +++ b/packages/ai/index.ts @@ -1,13 +1,6 @@ -/** - * AI Agent module - * - * Provides AI-powered documentation generation using OpenAI, Gemini, Anthropic or Mistral APIs - */ - -export * from './types'; -export * from './constants'; -export * from './config-loader'; -export * from './ai-agent'; -export * from './structured-schema'; -export * from './markdown-builder'; +export * from './core'; +export * from './config/config-loader'; +export * from './agents/ai-agent'; +export * from './documentation/structured-schema'; +export * from './documentation/markdown-builder'; export * from './providers/vercel'; diff --git a/packages/ai/providers/vercel/model-factory.ts b/packages/ai/providers/vercel/model-factory.ts index 575c3b80..b75ace8e 100644 --- a/packages/ai/providers/vercel/model-factory.ts +++ b/packages/ai/providers/vercel/model-factory.ts @@ -3,7 +3,7 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { createAnthropic } from '@ai-sdk/anthropic'; import { createMistral } from '@ai-sdk/mistral'; import { createHelicone } from '@helicone/ai-sdk-provider'; -import { AIModel, ILogger, ObservabilityMetadata } from '../../types'; +import { AIModel, ILogger, ObservabilityMetadata } from '../../core/types'; import { log } from './utils'; export interface ModelFactoryConfig { diff --git a/packages/ai/providers/vercel/options-builder.ts b/packages/ai/providers/vercel/options-builder.ts index b6e204f3..d351b096 100644 --- a/packages/ai/providers/vercel/options-builder.ts +++ b/packages/ai/providers/vercel/options-builder.ts @@ -1,4 +1,4 @@ -import { AIModel, GenerateTextOptions } from '../../types'; +import { AIModel, GenerateTextOptions } from '../../core/types'; import { isReasoningModel } from './utils'; export class OptionsBuilder { diff --git a/packages/ai/providers/vercel/provider.ts b/packages/ai/providers/vercel/provider.ts index bab43855..c78549aa 100644 --- a/packages/ai/providers/vercel/provider.ts +++ b/packages/ai/providers/vercel/provider.ts @@ -8,14 +8,14 @@ import { ILogger, ObservabilityMetadata, GenerateTextOptions, -} from '../../types'; + DocumentationStructure, +} from '../../core/types'; import { generateObject, generateText } from 'ai'; import { BatchDocumentationStructureSchema, DocumentationStructureSchema, -} from '../../structured-schema'; -import { DocumentationStructure } from '../../types'; -import { buildMarkdownFromStructure } from '../../markdown-builder'; +} from '../../documentation/structured-schema'; +import { buildMarkdownFromStructure } from '../../documentation/markdown-builder'; import { z } from 'zod'; import { ModelFactory } from './model-factory'; import { OptionsBuilder } from './options-builder'; diff --git a/packages/ai/providers/vercel/utils.ts b/packages/ai/providers/vercel/utils.ts index 7e2a2464..9c45e4ee 100644 --- a/packages/ai/providers/vercel/utils.ts +++ b/packages/ai/providers/vercel/utils.ts @@ -1,4 +1,4 @@ -import { AIProvider, AIProviderError, ILogger, AIErrorCode } from '../../types'; +import { AIProvider, AIProviderError, ILogger, AIErrorCode } from '../../core/types'; /** * Check if the model is a reasoning model (like OpenAI o1/o3)