feat(docs): add structured output example and details
This commit is contained in:
64
README.md
64
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
|
- 🔧 **Configurable**: Extensive configuration options for each provider
|
||||||
- 📦 **Zero Dependencies**: Lightweight with minimal external dependencies
|
- 📦 **Zero Dependencies**: Lightweight with minimal external dependencies
|
||||||
- 🌐 **Local Support**: OpenWebUI integration for local/private AI models
|
- 🌐 **Local Support**: OpenWebUI integration for local/private AI models
|
||||||
|
- 🎨 **Structured Output**: Define custom response types for type-safe AI outputs
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -83,6 +84,69 @@ const openwebui = createOpenWebUIProvider({ apiKey: 'your-key', baseUrl: 'http:/
|
|||||||
const provider = createProvider('claude', { apiKey: 'your-key' });
|
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<UserProfile>(
|
||||||
|
`{
|
||||||
|
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
|
## 📝 Environment Variables
|
||||||
|
|
||||||
Set up your API keys:
|
Set up your API keys:
|
||||||
|
|||||||
377
examples/structured-response-types.ts
Normal file
377
examples/structured-response-types.ts
Normal file
@@ -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<UserProfile>(
|
||||||
|
`{
|
||||||
|
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<ProductAnalysis>(
|
||||||
|
`{
|
||||||
|
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<CodeReview>(
|
||||||
|
`{
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-ai-provider",
|
"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",
|
"description": "A simple and extensible AI provider package for easy integration of multiple AI services",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
@@ -40,7 +40,10 @@
|
|||||||
"nodejs",
|
"nodejs",
|
||||||
"openwebui",
|
"openwebui",
|
||||||
"llm",
|
"llm",
|
||||||
"unified-api"
|
"unified-api",
|
||||||
|
"structured-output",
|
||||||
|
"response-types",
|
||||||
|
"type-safe"
|
||||||
],
|
],
|
||||||
"author": "Jan-Marlon Leibl",
|
"author": "Jan-Marlon Leibl",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
12
src/index.ts
12
src/index.ts
@@ -16,12 +16,20 @@ export type {
|
|||||||
CompletionResponse,
|
CompletionResponse,
|
||||||
CompletionChunk,
|
CompletionChunk,
|
||||||
ProviderInfo,
|
ProviderInfo,
|
||||||
TokenUsage
|
TokenUsage,
|
||||||
|
ResponseType
|
||||||
} from './types/index.js';
|
} from './types/index.js';
|
||||||
|
|
||||||
// Error types
|
// Error types
|
||||||
export { AIProviderError, AIErrorType } from './types/index.js';
|
export { AIProviderError, AIErrorType } from './types/index.js';
|
||||||
|
|
||||||
|
// Response type utilities
|
||||||
|
export {
|
||||||
|
createResponseType,
|
||||||
|
generateResponseTypePrompt,
|
||||||
|
validateResponseType
|
||||||
|
} from './types/index.js';
|
||||||
|
|
||||||
// Base provider
|
// Base provider
|
||||||
export { BaseAIProvider } from './providers/base.js';
|
export { BaseAIProvider } from './providers/base.js';
|
||||||
|
|
||||||
@@ -50,4 +58,4 @@ export const SUPPORTED_PROVIDERS = ['claude', 'openai', 'gemini', 'openwebui'] a
|
|||||||
/**
|
/**
|
||||||
* Package version
|
* Package version
|
||||||
*/
|
*/
|
||||||
export const VERSION = '1.0.0';
|
export const VERSION = '1.2.0';
|
||||||
@@ -19,9 +19,10 @@ import type {
|
|||||||
CompletionParams,
|
CompletionParams,
|
||||||
CompletionResponse,
|
CompletionResponse,
|
||||||
CompletionChunk,
|
CompletionChunk,
|
||||||
ProviderInfo
|
ProviderInfo,
|
||||||
|
ResponseType
|
||||||
} from '../types/index.js';
|
} from '../types/index.js';
|
||||||
import { AIProviderError, AIErrorType } from '../types/index.js';
|
import { AIProviderError, AIErrorType, generateResponseTypePrompt } from '../types/index.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ABSTRACT BASE PROVIDER CLASS
|
// ABSTRACT BASE PROVIDER CLASS
|
||||||
@@ -147,7 +148,7 @@ export abstract class BaseAIProvider {
|
|||||||
* console.log(response.content);
|
* console.log(response.content);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public async complete(params: CompletionParams): Promise<CompletionResponse> {
|
public async complete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||||
// Ensure provider is ready for use
|
// Ensure provider is ready for use
|
||||||
this.ensureInitialized();
|
this.ensureInitialized();
|
||||||
|
|
||||||
@@ -155,8 +156,11 @@ export abstract class BaseAIProvider {
|
|||||||
this.validateCompletionParams(params);
|
this.validateCompletionParams(params);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Process response type instructions if specified
|
||||||
|
const processedParams = this.processResponseType(params);
|
||||||
|
|
||||||
// Delegate to provider-specific implementation
|
// Delegate to provider-specific implementation
|
||||||
return await this.doComplete(params);
|
return await this.doComplete(processedParams);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Normalize error to our standard format
|
// Normalize error to our standard format
|
||||||
throw this.normalizeError(error as Error);
|
throw this.normalizeError(error as Error);
|
||||||
@@ -184,7 +188,7 @@ export abstract class BaseAIProvider {
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public async *stream(params: CompletionParams): AsyncIterable<CompletionChunk> {
|
public async *stream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
|
||||||
// Ensure provider is ready for use
|
// Ensure provider is ready for use
|
||||||
this.ensureInitialized();
|
this.ensureInitialized();
|
||||||
|
|
||||||
@@ -192,8 +196,11 @@ export abstract class BaseAIProvider {
|
|||||||
this.validateCompletionParams(params);
|
this.validateCompletionParams(params);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Process response type instructions if specified
|
||||||
|
const processedParams = this.processResponseType(params);
|
||||||
|
|
||||||
// Delegate to provider-specific implementation
|
// Delegate to provider-specific implementation
|
||||||
yield* this.doStream(params);
|
yield* this.doStream(processedParams);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Normalize error to our standard format
|
// Normalize error to our standard format
|
||||||
throw this.normalizeError(error as Error);
|
throw this.normalizeError(error as Error);
|
||||||
@@ -242,7 +249,7 @@ export abstract class BaseAIProvider {
|
|||||||
* @returns Promise resolving to completion response
|
* @returns Promise resolving to completion response
|
||||||
* @throws {Error} If completion fails (will be normalized to AIProviderError)
|
* @throws {Error} If completion fails (will be normalized to AIProviderError)
|
||||||
*/
|
*/
|
||||||
protected abstract doComplete(params: CompletionParams): Promise<CompletionResponse>;
|
protected abstract doComplete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider-specific streaming implementation.
|
* Provider-specific streaming implementation.
|
||||||
@@ -256,7 +263,7 @@ export abstract class BaseAIProvider {
|
|||||||
* @returns AsyncIterable yielding completion chunks
|
* @returns AsyncIterable yielding completion chunks
|
||||||
* @throws {Error} If streaming fails (will be normalized to AIProviderError)
|
* @throws {Error} If streaming fails (will be normalized to AIProviderError)
|
||||||
*/
|
*/
|
||||||
protected abstract doStream(params: CompletionParams): AsyncIterable<CompletionChunk>;
|
protected abstract doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk>;
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// PROTECTED UTILITY METHODS
|
// PROTECTED UTILITY METHODS
|
||||||
@@ -370,7 +377,7 @@ export abstract class BaseAIProvider {
|
|||||||
* @param params - Parameters to validate
|
* @param params - Parameters to validate
|
||||||
* @throws {AIProviderError} If any parameter is invalid
|
* @throws {AIProviderError} If any parameter is invalid
|
||||||
*/
|
*/
|
||||||
protected validateCompletionParams(params: CompletionParams): void {
|
protected validateCompletionParams<T = any>(params: CompletionParams<T>): void {
|
||||||
if (!params || typeof params !== 'object') {
|
if (!params || typeof params !== 'object') {
|
||||||
throw new AIProviderError(
|
throw new AIProviderError(
|
||||||
'Completion parameters object is required',
|
'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<T>(params: CompletionParams<T>): CompletionParams<T> {
|
||||||
|
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.
|
* Normalizes any error into a standardized AIProviderError.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export class ClaudeProvider extends BaseAIProvider {
|
|||||||
* @returns Promise resolving to formatted completion response
|
* @returns Promise resolving to formatted completion response
|
||||||
* @throws {Error} If API request fails
|
* @throws {Error} If API request fails
|
||||||
*/
|
*/
|
||||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
|
protected async doComplete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ export class ClaudeProvider extends BaseAIProvider {
|
|||||||
* @returns AsyncIterable yielding completion chunks
|
* @returns AsyncIterable yielding completion chunks
|
||||||
* @throws {Error} If streaming request fails
|
* @throws {Error} If streaming request fails
|
||||||
*/
|
*/
|
||||||
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
|
protected async *doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ export class GeminiProvider extends BaseAIProvider {
|
|||||||
* @returns Promise resolving to formatted completion response
|
* @returns Promise resolving to formatted completion response
|
||||||
* @throws {Error} If API request fails
|
* @throws {Error} If API request fails
|
||||||
*/
|
*/
|
||||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
|
protected async doComplete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||||
if (!this.client || !this.model) {
|
if (!this.client || !this.model) {
|
||||||
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,7 @@ export class GeminiProvider extends BaseAIProvider {
|
|||||||
* @returns AsyncIterable yielding completion chunks
|
* @returns AsyncIterable yielding completion chunks
|
||||||
* @throws {Error} If streaming request fails
|
* @throws {Error} If streaming request fails
|
||||||
*/
|
*/
|
||||||
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
|
protected async *doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
|
||||||
if (!this.client || !this.model) {
|
if (!this.client || !this.model) {
|
||||||
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export class OpenAIProvider extends BaseAIProvider {
|
|||||||
* @returns Promise resolving to formatted completion response
|
* @returns Promise resolving to formatted completion response
|
||||||
* @throws {Error} If API request fails
|
* @throws {Error} If API request fails
|
||||||
*/
|
*/
|
||||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
|
protected async doComplete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ export class OpenAIProvider extends BaseAIProvider {
|
|||||||
* @returns AsyncIterable yielding completion chunks
|
* @returns AsyncIterable yielding completion chunks
|
||||||
* @throws {Error} If streaming request fails
|
* @throws {Error} If streaming request fails
|
||||||
*/
|
*/
|
||||||
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
|
protected async *doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -962,7 +962,7 @@ export class OpenWebUIProvider extends BaseAIProvider {
|
|||||||
* @returns Promise resolving to formatted completion response
|
* @returns Promise resolving to formatted completion response
|
||||||
* @throws {Error} If API request fails
|
* @throws {Error} If API request fails
|
||||||
*/
|
*/
|
||||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
|
protected async doComplete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||||
if (this.useOllamaProxy) {
|
if (this.useOllamaProxy) {
|
||||||
return this.completeWithOllama(params);
|
return this.completeWithOllama(params);
|
||||||
} else {
|
} else {
|
||||||
@@ -978,7 +978,7 @@ export class OpenWebUIProvider extends BaseAIProvider {
|
|||||||
* @returns AsyncIterable yielding completion chunks
|
* @returns AsyncIterable yielding completion chunks
|
||||||
* @throws {Error} If streaming request fails
|
* @throws {Error} If streaming request fails
|
||||||
*/
|
*/
|
||||||
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
|
protected async *doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
|
||||||
if (this.useOllamaProxy) {
|
if (this.useOllamaProxy) {
|
||||||
yield* this.streamWithOllama(params);
|
yield* this.streamWithOllama(params);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -36,10 +36,24 @@ export interface AIMessage {
|
|||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response type definition for structured AI outputs
|
||||||
|
*/
|
||||||
|
export interface ResponseType<T = any> {
|
||||||
|
/** 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
|
* Parameters for AI completion requests
|
||||||
*/
|
*/
|
||||||
export interface CompletionParams {
|
export interface CompletionParams<T = any> {
|
||||||
/** Array of messages forming the conversation */
|
/** Array of messages forming the conversation */
|
||||||
messages: AIMessage[];
|
messages: AIMessage[];
|
||||||
/** Model to use for completion */
|
/** Model to use for completion */
|
||||||
@@ -54,6 +68,8 @@ export interface CompletionParams {
|
|||||||
stopSequences?: string[];
|
stopSequences?: string[];
|
||||||
/** Whether to stream the response */
|
/** Whether to stream the response */
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
|
/** Expected response type for structured outputs */
|
||||||
|
responseType?: ResponseType<T>;
|
||||||
/** Additional provider-specific parameters */
|
/** Additional provider-specific parameters */
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
@@ -144,4 +160,115 @@ export interface ProviderInfo {
|
|||||||
supportsStreaming: boolean;
|
supportsStreaming: boolean;
|
||||||
/** Additional capabilities */
|
/** Additional capabilities */
|
||||||
capabilities?: Record<string, any>;
|
capabilities?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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<T = any>(
|
||||||
|
typeDefinition: string,
|
||||||
|
description: string,
|
||||||
|
example?: T,
|
||||||
|
strictJson: boolean = true
|
||||||
|
): ResponseType<T> {
|
||||||
|
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<T = any>(
|
||||||
|
response: string,
|
||||||
|
responseType: ResponseType<T>
|
||||||
|
): { 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}`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user