From 18769c134d2a07aadccdd64db9f4c9c654bf1515 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Thu, 4 Sep 2025 14:49:01 +0200 Subject: [PATCH] feat(docs): add structured output example and details --- README.md | 64 +++++ examples/structured-response-types.ts | 377 ++++++++++++++++++++++++++ package.json | 7 +- src/index.ts | 12 +- src/providers/base.ts | 64 ++++- src/providers/claude.ts | 4 +- src/providers/gemini.ts | 4 +- src/providers/openai.ts | 4 +- src/providers/openwebui.ts | 4 +- src/types/index.ts | 129 ++++++++- 10 files changed, 647 insertions(+), 22 deletions(-) create mode 100644 examples/structured-response-types.ts diff --git a/README.md b/README.md index 98bc58d..93d12c6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A professional, type-safe TypeScript package that provides a unified interface f - šŸ”§ **Configurable**: Extensive configuration options for each provider - šŸ“¦ **Zero Dependencies**: Lightweight with minimal external dependencies - 🌐 **Local Support**: OpenWebUI integration for local/private AI models +- šŸŽØ **Structured Output**: Define custom response types for type-safe AI outputs ## šŸš€ Quick Start @@ -83,6 +84,69 @@ const openwebui = createOpenWebUIProvider({ apiKey: 'your-key', baseUrl: 'http:/ const provider = createProvider('claude', { apiKey: 'your-key' }); ``` +## šŸŽØ Structured Response Types + +Define custom response types for type-safe, structured AI outputs: + +```typescript +import { createResponseType, validateResponseType } from 'simple-ai-provider'; + +// Define your response type +interface UserProfile { + name: string; + age: number; + email: string; + preferences: { + theme: 'light' | 'dark'; + notifications: boolean; + }; +} + +const userProfileType = createResponseType( + `{ + name: string; + age: number; + email: string; + preferences: { + theme: 'light' | 'dark'; + notifications: boolean; + }; + }`, + 'A user profile with personal information and preferences', + { + name: 'John Doe', + age: 30, + email: 'john@example.com', + preferences: { theme: 'dark', notifications: true } + } +); + +// Use with any provider +const response = await claude.complete({ + messages: [ + { role: 'user', content: 'Generate a user profile for a software developer' } + ], + responseType: userProfileType, + maxTokens: 500 +}); + +// Validate and get typed response +const validation = validateResponseType(response.content, userProfileType); +if (validation.isValid) { + const userProfile = validation.data as UserProfile; + console.log(`Name: ${userProfile.name}`); + console.log(`Theme: ${userProfile.preferences.theme}`); +} +``` + +### Key Benefits + +- **Type Safety**: Get fully typed responses from AI providers +- **Automatic Prompting**: System prompts are automatically generated +- **Validation**: Built-in response validation and parsing +- **Consistency**: Ensures AI outputs match your expected format +- **Developer Experience**: IntelliSense and compile-time type checking + ## šŸ“ Environment Variables Set up your API keys: diff --git a/examples/structured-response-types.ts b/examples/structured-response-types.ts new file mode 100644 index 0000000..3c2430d --- /dev/null +++ b/examples/structured-response-types.ts @@ -0,0 +1,377 @@ +/** + * Structured Response Types Example + * + * This example demonstrates how to use custom response types to get + * structured, type-safe outputs from AI providers. The library automatically + * generates system prompts that instruct the AI on the expected output format. + */ + +import { + createClaudeProvider, + createResponseType, + validateResponseType, + AIProviderError, + AIErrorType +} from '../src/index.js'; + +// ============================================================================ +// DEFINE CUSTOM RESPONSE TYPES +// ============================================================================ + +// Example 1: User Profile Type +interface UserProfile { + name: string; + age: number; + email: string; + preferences: { + theme: 'light' | 'dark'; + notifications: boolean; + }; + skills: string[]; +} + +const userProfileType = createResponseType( + `{ + name: string; + age: number; + email: string; + preferences: { + theme: 'light' | 'dark'; + notifications: boolean; + }; + skills: string[]; + }`, + 'A user profile with personal information, preferences, and skills', + { + name: 'John Doe', + age: 30, + email: 'john@example.com', + preferences: { theme: 'dark', notifications: true }, + skills: ['TypeScript', 'React', 'Node.js'] + } +); + +// Example 2: Product Analysis Type +interface ProductAnalysis { + productName: string; + category: string; + priceRange: 'budget' | 'mid-range' | 'premium'; + pros: string[]; + cons: string[]; + overallRating: number; // 1-10 scale + recommendation: 'buy' | 'consider' | 'avoid'; + reasoning: string; +} + +const productAnalysisType = createResponseType( + `{ + productName: string; + category: string; + priceRange: 'budget' | 'mid-range' | 'premium'; + pros: string[]; + cons: string[]; + overallRating: number; + recommendation: 'buy' | 'consider' | 'avoid'; + reasoning: string; + }`, + 'A comprehensive product analysis with pros, cons, rating, and recommendation', + { + productName: 'Example Product', + category: 'Electronics', + priceRange: 'mid-range', + pros: ['Good build quality', 'Reasonable price'], + cons: ['Limited features', 'Short warranty'], + overallRating: 7, + recommendation: 'consider', + reasoning: 'Good value for money but has some limitations' + } +); + +// Example 3: Code Review Type +interface CodeReview { + overallScore: number; // 1-10 scale + issues: Array<{ + type: 'error' | 'warning' | 'suggestion'; + line?: number; + message: string; + severity: 'low' | 'medium' | 'high'; + }>; + strengths: string[]; + improvements: string[]; + summary: string; +} + +const codeReviewType = createResponseType( + `{ + overallScore: number; + issues: Array<{ + type: 'error' | 'warning' | 'suggestion'; + line?: number; + message: string; + severity: 'low' | 'medium' | 'high'; + }>; + strengths: string[]; + improvements: string[]; + summary: string; + }`, + 'A comprehensive code review with scoring, issues, and recommendations', + { + overallScore: 8, + issues: [ + { type: 'warning', line: 15, message: 'Consider using const instead of let', severity: 'low' } + ], + strengths: ['Clear variable names', 'Good error handling'], + improvements: ['Add more comments', 'Consider refactoring large functions'], + summary: 'Well-written code with minor improvements needed' + } +); + +// ============================================================================ +// EXAMPLE FUNCTIONS +// ============================================================================ + +async function demonstrateUserProfileGeneration() { + console.log('\n=== User Profile Generation Example ==='); + + const claude = createClaudeProvider(process.env.ANTHROPIC_API_KEY || 'your-api-key-here'); + await claude.initialize(); + + try { + const response = await claude.complete({ + messages: [ + { + role: 'user', + content: 'Generate a realistic user profile for a software developer who loves open source projects and prefers dark themes.' + } + ], + responseType: userProfileType, + maxTokens: 500, + temperature: 0.7 + }); + + console.log('Raw response:', response.content); + + // Validate and parse the response + const validation = validateResponseType(response.content, userProfileType); + + if (validation.isValid) { + const userProfile = validation.data as UserProfile; + console.log('\nParsed User Profile:'); + console.log(`Name: ${userProfile.name}`); + console.log(`Age: ${userProfile.age}`); + console.log(`Email: ${userProfile.email}`); + console.log(`Theme Preference: ${userProfile.preferences.theme}`); + console.log(`Notifications: ${userProfile.preferences.notifications}`); + console.log(`Skills: ${userProfile.skills.join(', ')}`); + } else { + console.error('Validation failed:', validation.error); + } + + } catch (error) { + if (error instanceof AIProviderError) { + console.error(`AI Provider Error (${error.type}):`, error.message); + } else { + console.error('Unexpected error:', error); + } + } +} + +async function demonstrateProductAnalysis() { + console.log('\n=== Product Analysis Example ==='); + + const claude = createClaudeProvider(process.env.ANTHROPIC_API_KEY || 'your-api-key-here'); + await claude.initialize(); + + try { + const response = await claude.complete({ + messages: [ + { + role: 'user', + content: 'Analyze the iPhone 15 Pro from a consumer perspective. Consider its features, price point, and market position.' + } + ], + responseType: productAnalysisType, + maxTokens: 800, + temperature: 0.5 + }); + + console.log('Raw response:', response.content); + + const validation = validateResponseType(response.content, productAnalysisType); + + if (validation.isValid) { + const analysis = validation.data as ProductAnalysis; + console.log('\nProduct Analysis:'); + console.log(`Product: ${analysis.productName}`); + console.log(`Category: ${analysis.category}`); + console.log(`Price Range: ${analysis.priceRange}`); + console.log(`Overall Rating: ${analysis.overallRating}/10`); + console.log(`Recommendation: ${analysis.recommendation}`); + console.log(`Pros: ${analysis.pros.join(', ')}`); + console.log(`Cons: ${analysis.cons.join(', ')}`); + console.log(`Reasoning: ${analysis.reasoning}`); + } else { + console.error('Validation failed:', validation.error); + } + + } catch (error) { + if (error instanceof AIProviderError) { + console.error(`AI Provider Error (${error.type}):`, error.message); + } else { + console.error('Unexpected error:', error); + } + } +} + +async function demonstrateCodeReview() { + console.log('\n=== Code Review Example ==='); + + const claude = createClaudeProvider(process.env.ANTHROPIC_API_KEY || 'your-api-key-here'); + await claude.initialize(); + + const sampleCode = ` +function calculateTotal(items) { + let total = 0; + for (let i = 0; i < items.length; i++) { + total += items[i].price; + } + return total; +} + +function processPayment(amount, cardNumber) { + if (amount <= 0) { + throw new Error("Invalid amount"); + } + // Process payment logic here + return { success: true, transactionId: "12345" }; +} + `; + + try { + const response = await claude.complete({ + messages: [ + { + role: 'user', + content: `Please review this JavaScript code and provide a comprehensive analysis:\n\n\`\`\`javascript\n${sampleCode}\n\`\`\`` + } + ], + responseType: codeReviewType, + maxTokens: 1000, + temperature: 0.3 + }); + + console.log('Raw response:', response.content); + + const validation = validateResponseType(response.content, codeReviewType); + + if (validation.isValid) { + const review = validation.data as CodeReview; + console.log('\nCode Review:'); + console.log(`Overall Score: ${review.overallScore}/10`); + console.log(`Summary: ${review.summary}`); + console.log(`\nStrengths:`); + review.strengths.forEach(strength => console.log(` • ${strength}`)); + console.log(`\nImprovements:`); + review.improvements.forEach(improvement => console.log(` • ${improvement}`)); + console.log(`\nIssues:`); + review.issues.forEach(issue => { + const lineInfo = issue.line ? ` (line ${issue.line})` : ''; + console.log(` • [${issue.severity.toUpperCase()}] ${issue.type}: ${issue.message}${lineInfo}`); + }); + } else { + console.error('Validation failed:', validation.error); + } + + } catch (error) { + if (error instanceof AIProviderError) { + console.error(`AI Provider Error (${error.type}):`, error.message); + } else { + console.error('Unexpected error:', error); + } + } +} + +async function demonstrateStreamingWithResponseType() { + console.log('\n=== Streaming with Response Type Example ==='); + + const claude = createClaudeProvider(process.env.ANTHROPIC_API_KEY || 'your-api-key-here'); + await claude.initialize(); + + try { + console.log('Streaming response for product analysis...\n'); + + let fullResponse = ''; + for await (const chunk of claude.stream({ + messages: [ + { + role: 'user', + content: 'Analyze the Tesla Model 3 as a consumer product.' + } + ], + responseType: productAnalysisType, + maxTokens: 600, + temperature: 0.5 + })) { + if (!chunk.isComplete) { + process.stdout.write(chunk.content); + fullResponse += chunk.content; + } else { + console.log('\n\nStream completed. Usage:', chunk.usage); + + // Validate the complete streamed response + const validation = validateResponseType(fullResponse, productAnalysisType); + if (validation.isValid) { + console.log('\nStreamed response validation: SUCCESS'); + } else { + console.log('\nStreamed response validation: FAILED -', validation.error); + } + } + } + + } catch (error) { + if (error instanceof AIProviderError) { + console.error(`AI Provider Error (${error.type}):`, error.message); + } else { + console.error('Unexpected error:', error); + } + } +} + +// ============================================================================ +// MAIN EXECUTION +// ============================================================================ + +async function main() { + console.log('šŸš€ Structured Response Types Example'); + console.log('====================================='); + + // Check for API key + if (!process.env.ANTHROPIC_API_KEY) { + console.error('āŒ Please set ANTHROPIC_API_KEY environment variable'); + console.log('You can get an API key from: https://console.anthropic.com/'); + return; + } + + try { + await demonstrateUserProfileGeneration(); + await demonstrateProductAnalysis(); + await demonstrateCodeReview(); + await demonstrateStreamingWithResponseType(); + + console.log('\nāœ… All examples completed successfully!'); + console.log('\nKey Benefits of Response Types:'); + console.log('• Type-safe AI responses'); + console.log('• Automatic system prompt generation'); + console.log('• Built-in response validation'); + console.log('• Consistent output format'); + console.log('• Better error handling'); + + } catch (error) { + console.error('āŒ Example failed:', error); + } +} + +// Run the example +if (import.meta.main) { + main().catch(console.error); +} diff --git a/package.json b/package.json index 6395665..cb2a720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-ai-provider", - "version": "1.1.2", + "version": "1.2.0", "description": "A simple and extensible AI provider package for easy integration of multiple AI services", "main": "dist/index.js", "module": "dist/index.mjs", @@ -40,7 +40,10 @@ "nodejs", "openwebui", "llm", - "unified-api" + "unified-api", + "structured-output", + "response-types", + "type-safe" ], "author": "Jan-Marlon Leibl", "license": "MIT", diff --git a/src/index.ts b/src/index.ts index 64a5370..4124249 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,12 +16,20 @@ export type { CompletionResponse, CompletionChunk, ProviderInfo, - TokenUsage + TokenUsage, + ResponseType } from './types/index.js'; // Error types export { AIProviderError, AIErrorType } from './types/index.js'; +// Response type utilities +export { + createResponseType, + generateResponseTypePrompt, + validateResponseType +} from './types/index.js'; + // Base provider export { BaseAIProvider } from './providers/base.js'; @@ -50,4 +58,4 @@ export const SUPPORTED_PROVIDERS = ['claude', 'openai', 'gemini', 'openwebui'] a /** * Package version */ -export const VERSION = '1.0.0'; \ No newline at end of file +export const VERSION = '1.2.0'; \ No newline at end of file diff --git a/src/providers/base.ts b/src/providers/base.ts index e9bc6f3..4a7bf9d 100644 --- a/src/providers/base.ts +++ b/src/providers/base.ts @@ -19,9 +19,10 @@ import type { CompletionParams, CompletionResponse, CompletionChunk, - ProviderInfo + ProviderInfo, + ResponseType } from '../types/index.js'; -import { AIProviderError, AIErrorType } from '../types/index.js'; +import { AIProviderError, AIErrorType, generateResponseTypePrompt } from '../types/index.js'; // ============================================================================ // ABSTRACT BASE PROVIDER CLASS @@ -147,7 +148,7 @@ export abstract class BaseAIProvider { * console.log(response.content); * ``` */ - public async complete(params: CompletionParams): Promise { + public async complete(params: CompletionParams): Promise { // Ensure provider is ready for use this.ensureInitialized(); @@ -155,8 +156,11 @@ export abstract class BaseAIProvider { this.validateCompletionParams(params); try { + // Process response type instructions if specified + const processedParams = this.processResponseType(params); + // Delegate to provider-specific implementation - return await this.doComplete(params); + return await this.doComplete(processedParams); } catch (error) { // Normalize error to our standard format throw this.normalizeError(error as Error); @@ -184,7 +188,7 @@ export abstract class BaseAIProvider { * } * ``` */ - public async *stream(params: CompletionParams): AsyncIterable { + public async *stream(params: CompletionParams): AsyncIterable { // Ensure provider is ready for use this.ensureInitialized(); @@ -192,8 +196,11 @@ export abstract class BaseAIProvider { this.validateCompletionParams(params); try { + // Process response type instructions if specified + const processedParams = this.processResponseType(params); + // Delegate to provider-specific implementation - yield* this.doStream(params); + yield* this.doStream(processedParams); } catch (error) { // Normalize error to our standard format throw this.normalizeError(error as Error); @@ -242,7 +249,7 @@ export abstract class BaseAIProvider { * @returns Promise resolving to completion response * @throws {Error} If completion fails (will be normalized to AIProviderError) */ - protected abstract doComplete(params: CompletionParams): Promise; + protected abstract doComplete(params: CompletionParams): Promise; /** * Provider-specific streaming implementation. @@ -256,7 +263,7 @@ export abstract class BaseAIProvider { * @returns AsyncIterable yielding completion chunks * @throws {Error} If streaming fails (will be normalized to AIProviderError) */ - protected abstract doStream(params: CompletionParams): AsyncIterable; + protected abstract doStream(params: CompletionParams): AsyncIterable; // ======================================================================== // PROTECTED UTILITY METHODS @@ -370,7 +377,7 @@ export abstract class BaseAIProvider { * @param params - Parameters to validate * @throws {AIProviderError} If any parameter is invalid */ - protected validateCompletionParams(params: CompletionParams): void { + protected validateCompletionParams(params: CompletionParams): void { if (!params || typeof params !== 'object') { throw new AIProviderError( 'Completion parameters object is required', @@ -512,6 +519,45 @@ export abstract class BaseAIProvider { } } + /** + * Processes completion parameters to include response type instructions. + * + * This method automatically adds system prompt instructions when a response + * type is specified, ensuring the AI understands the expected output format. + * + * @protected + * @param params - Original completion parameters + * @returns Processed parameters with response type instructions + */ + protected processResponseType(params: CompletionParams): CompletionParams { + if (!params.responseType) { + return params; + } + + // Create a copy of the parameters to avoid mutation + const processedParams = { ...params }; + + // Generate the response type instruction + const responseTypePrompt = generateResponseTypePrompt(params.responseType); + + // Find existing system messages or create new ones + const systemMessages = params.messages.filter(msg => msg.role === 'system'); + const nonSystemMessages = params.messages.filter(msg => msg.role !== 'system'); + + // Combine existing system messages with response type instruction + const combinedSystemContent = systemMessages.length > 0 + ? systemMessages.map(msg => msg.content).join('\n\n') + '\n\n' + responseTypePrompt + : responseTypePrompt; + + // Create new messages array with combined system message + processedParams.messages = [ + { role: 'system', content: combinedSystemContent }, + ...nonSystemMessages + ]; + + return processedParams; + } + /** * Normalizes any error into a standardized AIProviderError. * diff --git a/src/providers/claude.ts b/src/providers/claude.ts index 2c80e72..ba0ba77 100644 --- a/src/providers/claude.ts +++ b/src/providers/claude.ts @@ -220,7 +220,7 @@ export class ClaudeProvider extends BaseAIProvider { * @returns Promise resolving to formatted completion response * @throws {Error} If API request fails */ - protected async doComplete(params: CompletionParams): Promise { + protected async doComplete(params: CompletionParams): Promise { if (!this.client) { throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST); } @@ -257,7 +257,7 @@ export class ClaudeProvider extends BaseAIProvider { * @returns AsyncIterable yielding completion chunks * @throws {Error} If streaming request fails */ - protected async *doStream(params: CompletionParams): AsyncIterable { + protected async *doStream(params: CompletionParams): AsyncIterable { if (!this.client) { throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST); } diff --git a/src/providers/gemini.ts b/src/providers/gemini.ts index 2e21a30..ec55a58 100644 --- a/src/providers/gemini.ts +++ b/src/providers/gemini.ts @@ -268,7 +268,7 @@ export class GeminiProvider extends BaseAIProvider { * @returns Promise resolving to formatted completion response * @throws {Error} If API request fails */ - protected async doComplete(params: CompletionParams): Promise { + protected async doComplete(params: CompletionParams): Promise { if (!this.client || !this.model) { throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST); } @@ -324,7 +324,7 @@ export class GeminiProvider extends BaseAIProvider { * @returns AsyncIterable yielding completion chunks * @throws {Error} If streaming request fails */ - protected async *doStream(params: CompletionParams): AsyncIterable { + protected async *doStream(params: CompletionParams): AsyncIterable { if (!this.client || !this.model) { throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST); } diff --git a/src/providers/openai.ts b/src/providers/openai.ts index 96a2f43..4bf8c9f 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -244,7 +244,7 @@ export class OpenAIProvider extends BaseAIProvider { * @returns Promise resolving to formatted completion response * @throws {Error} If API request fails */ - protected async doComplete(params: CompletionParams): Promise { + protected async doComplete(params: CompletionParams): Promise { if (!this.client) { throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST); } @@ -278,7 +278,7 @@ export class OpenAIProvider extends BaseAIProvider { * @returns AsyncIterable yielding completion chunks * @throws {Error} If streaming request fails */ - protected async *doStream(params: CompletionParams): AsyncIterable { + protected async *doStream(params: CompletionParams): AsyncIterable { if (!this.client) { throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST); } diff --git a/src/providers/openwebui.ts b/src/providers/openwebui.ts index 7bc2a4b..aa2322e 100644 --- a/src/providers/openwebui.ts +++ b/src/providers/openwebui.ts @@ -962,7 +962,7 @@ export class OpenWebUIProvider extends BaseAIProvider { * @returns Promise resolving to formatted completion response * @throws {Error} If API request fails */ - protected async doComplete(params: CompletionParams): Promise { + protected async doComplete(params: CompletionParams): Promise { if (this.useOllamaProxy) { return this.completeWithOllama(params); } else { @@ -978,7 +978,7 @@ export class OpenWebUIProvider extends BaseAIProvider { * @returns AsyncIterable yielding completion chunks * @throws {Error} If streaming request fails */ - protected async *doStream(params: CompletionParams): AsyncIterable { + protected async *doStream(params: CompletionParams): AsyncIterable { if (this.useOllamaProxy) { yield* this.streamWithOllama(params); } else { diff --git a/src/types/index.ts b/src/types/index.ts index 1c0cbf3..0a882e1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -36,10 +36,24 @@ export interface AIMessage { metadata?: Record; } +/** + * Response type definition for structured AI outputs + */ +export interface ResponseType { + /** The TypeScript type definition as a string */ + typeDefinition: string; + /** Human-readable description of the expected response format */ + description: string; + /** Example of the expected response structure */ + example?: T; + /** Whether to enforce strict JSON formatting */ + strictJson?: boolean; +} + /** * Parameters for AI completion requests */ -export interface CompletionParams { +export interface CompletionParams { /** Array of messages forming the conversation */ messages: AIMessage[]; /** Model to use for completion */ @@ -54,6 +68,8 @@ export interface CompletionParams { stopSequences?: string[]; /** Whether to stream the response */ stream?: boolean; + /** Expected response type for structured outputs */ + responseType?: ResponseType; /** Additional provider-specific parameters */ [key: string]: any; } @@ -144,4 +160,115 @@ export interface ProviderInfo { supportsStreaming: boolean; /** Additional capabilities */ capabilities?: Record; +} + +// ============================================================================ +// RESPONSE TYPE UTILITIES +// ============================================================================ + +/** + * Creates a response type definition for structured AI outputs + * + * @param typeDefinition - TypeScript type definition as a string + * @param description - Human-readable description of the expected format + * @param example - Optional example of the expected response structure + * @param strictJson - Whether to enforce strict JSON formatting (default: true) + * @returns ResponseType object for use in completion requests + * + * @example + * ```typescript + * const userType = createResponseType( + * `{ + * name: string; + * age: number; + * email: string; + * preferences: { + * theme: 'light' | 'dark'; + * notifications: boolean; + * }; + * }`, + * 'A user profile with personal information and preferences', + * { + * name: 'John Doe', + * age: 30, + * email: 'john@example.com', + * preferences: { theme: 'dark', notifications: true } + * } + * ); + * ``` + */ +export function createResponseType( + typeDefinition: string, + description: string, + example?: T, + strictJson: boolean = true +): ResponseType { + return { + typeDefinition: typeDefinition.trim(), + description, + example, + strictJson + }; +} + +/** + * Generates a system prompt instruction for structured output based on response type + * + * @param responseType - The response type definition + * @returns Formatted system prompt instruction + */ +export function generateResponseTypePrompt(responseType: ResponseType): string { + const { typeDefinition, description, example, strictJson } = responseType; + + let prompt = `You must respond with a JSON object that matches the following TypeScript type definition:\n\n`; + prompt += `Type Definition:\n\`\`\`typescript\n${typeDefinition}\n\`\`\`\n\n`; + prompt += `Description: ${description}\n\n`; + + if (example) { + prompt += `Example:\n\`\`\`json\n${JSON.stringify(example, null, 2)}\n\`\`\`\n\n`; + } + + if (strictJson) { + prompt += `IMPORTANT: Your response must be valid JSON only. Do not include any text before or after the JSON object. Do not use markdown formatting.`; + } else { + prompt += `Your response should follow the structure defined above.`; + } + + return prompt; +} + +/** + * Validates that a response matches the expected type structure + * + * @param response - The response content to validate + * @param responseType - The expected response type + * @returns Object with validation result and parsed data + */ +export function validateResponseType( + response: string, + responseType: ResponseType +): { isValid: boolean; data?: T; error?: string } { + try { + // Parse JSON response + const parsed = JSON.parse(response); + + // Basic validation - in a real implementation, you might want to use + // a schema validation library like zod or ajv for more thorough validation + if (typeof parsed !== 'object' || parsed === null) { + return { + isValid: false, + error: 'Response must be a JSON object' + }; + } + + return { + isValid: true, + data: parsed as T + }; + } catch (error) { + return { + isValid: false, + error: `Invalid JSON: ${(error as Error).message}` + }; + } } \ No newline at end of file