feat: Add typed stuff
This commit is contained in:
@@ -27,7 +27,7 @@ export { AIProviderError, AIErrorType } from './types/index.js';
|
||||
export {
|
||||
createResponseType,
|
||||
generateResponseTypePrompt,
|
||||
validateResponseType
|
||||
parseAndValidateResponseType
|
||||
} from './types/index.js';
|
||||
|
||||
// Base provider
|
||||
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
ProviderInfo,
|
||||
ResponseType
|
||||
} from '../types/index.js';
|
||||
import { AIProviderError, AIErrorType, generateResponseTypePrompt } from '../types/index.js';
|
||||
import { AIProviderError, AIErrorType, generateResponseTypePrompt, parseAndValidateResponseType } from '../types/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// ABSTRACT BASE PROVIDER CLASS
|
||||
@@ -148,7 +148,9 @@ export abstract class BaseAIProvider {
|
||||
* console.log(response.content);
|
||||
* ```
|
||||
*/
|
||||
public async complete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||
public async complete<T>(params: CompletionParams<T>): Promise<CompletionResponse<T>>;
|
||||
public async complete(params: CompletionParams): Promise<CompletionResponse<string>>;
|
||||
public async complete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse<T | string>> {
|
||||
// Ensure provider is ready for use
|
||||
this.ensureInitialized();
|
||||
|
||||
@@ -160,7 +162,23 @@ export abstract class BaseAIProvider {
|
||||
const processedParams = this.processResponseType(params);
|
||||
|
||||
// Delegate to provider-specific implementation
|
||||
return await this.doComplete(processedParams);
|
||||
const response = await this.doComplete(processedParams);
|
||||
|
||||
// If a responseType is defined, parse and validate the response
|
||||
if (params.responseType) {
|
||||
const parsedData = parseAndValidateResponseType<T>(response.content, params.responseType);
|
||||
return {
|
||||
...response,
|
||||
content: parsedData,
|
||||
rawContent: response.content,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, return the raw string content
|
||||
return {
|
||||
...response,
|
||||
content: response.content,
|
||||
};
|
||||
} catch (error) {
|
||||
// Normalize error to our standard format
|
||||
throw this.normalizeError(error as Error);
|
||||
@@ -249,7 +267,7 @@ export abstract class BaseAIProvider {
|
||||
* @returns Promise resolving to completion response
|
||||
* @throws {Error} If completion fails (will be normalized to AIProviderError)
|
||||
*/
|
||||
protected abstract doComplete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse>;
|
||||
protected abstract doComplete(params: CompletionParams): Promise<CompletionResponse<string>>;
|
||||
|
||||
/**
|
||||
* Provider-specific streaming implementation.
|
||||
|
||||
@@ -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<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse<string>> {
|
||||
if (!this.client) {
|
||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
@@ -533,7 +533,7 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
* @returns Formatted completion response
|
||||
* @throws {AIProviderError} If response format is unexpected
|
||||
*/
|
||||
private formatCompletionResponse(response: any): CompletionResponse {
|
||||
private formatCompletionResponse(response: any): CompletionResponse<string> {
|
||||
// Extract text content from response blocks
|
||||
const content = response.content
|
||||
?.filter((block: any) => block.type === 'text')
|
||||
|
||||
@@ -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<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse<string>> {
|
||||
if (!this.client || !this.model) {
|
||||
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
@@ -617,7 +617,7 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
* @returns Formatted completion response
|
||||
* @throws {AIProviderError} If response format is unexpected
|
||||
*/
|
||||
private formatCompletionResponse(response: any, model: string): CompletionResponse {
|
||||
private formatCompletionResponse(response: any, model: string): CompletionResponse<string> {
|
||||
// Handle multiple text parts in the response
|
||||
const candidate = response.candidates?.[0];
|
||||
if (!candidate) {
|
||||
|
||||
@@ -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<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse<string>> {
|
||||
if (!this.client) {
|
||||
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
@@ -537,7 +537,7 @@ export class OpenAIProvider extends BaseAIProvider {
|
||||
* @returns Formatted completion response
|
||||
* @throws {AIProviderError} If response format is unexpected
|
||||
*/
|
||||
private formatCompletionResponse(response: OpenAI.Chat.Completions.ChatCompletion): CompletionResponse {
|
||||
private formatCompletionResponse(response: OpenAI.Chat.Completions.ChatCompletion): CompletionResponse<string> {
|
||||
const choice = response.choices[0];
|
||||
if (!choice || !choice.message.content) {
|
||||
throw new AIProviderError(
|
||||
|
||||
@@ -524,7 +524,7 @@ export class OpenWebUIProvider extends BaseAIProvider {
|
||||
* @param response - Raw OpenWebUI response
|
||||
* @returns Formatted completion response
|
||||
*/
|
||||
private formatChatResponse(response: OpenWebUIChatResponse): CompletionResponse {
|
||||
private formatChatResponse(response: OpenWebUIChatResponse): CompletionResponse<string> {
|
||||
const choice = response.choices[0];
|
||||
if (!choice || !choice.message.content) {
|
||||
throw new AIProviderError(
|
||||
@@ -556,7 +556,7 @@ export class OpenWebUIProvider extends BaseAIProvider {
|
||||
* @param response - Raw Ollama response
|
||||
* @returns Formatted completion response
|
||||
*/
|
||||
private formatOllamaResponse(response: OllamaGenerateResponse): CompletionResponse {
|
||||
private formatOllamaResponse(response: OllamaGenerateResponse): CompletionResponse<string> {
|
||||
return {
|
||||
content: response.response,
|
||||
model: response.model,
|
||||
@@ -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<T = any>(params: CompletionParams<T>): Promise<CompletionResponse> {
|
||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse<string>> {
|
||||
if (this.useOllamaProxy) {
|
||||
return this.completeWithOllama(params);
|
||||
} else {
|
||||
|
||||
@@ -89,9 +89,11 @@ export interface TokenUsage {
|
||||
/**
|
||||
* Response from an AI completion request
|
||||
*/
|
||||
export interface CompletionResponse {
|
||||
/** Generated content */
|
||||
content: string;
|
||||
export interface CompletionResponse<T = string> {
|
||||
/** Generated content, either as a string or a typed object */
|
||||
content: T;
|
||||
/** Raw response from the provider, if content is a typed object */
|
||||
rawContent?: string;
|
||||
/** Model used for generation */
|
||||
model: string;
|
||||
/** Token usage statistics */
|
||||
@@ -220,55 +222,71 @@ export function createResponseType<T = any>(
|
||||
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`;
|
||||
let prompt = `You are an AI assistant that must respond with a JSON object. The JSON object must strictly adhere to the following TypeScript type definition:
|
||||
|
||||
`;
|
||||
prompt += `Type Definition:
|
||||
typescript
|
||||
${typeDefinition}
|
||||
|
||||
|
||||
`;
|
||||
prompt += `Description: ${description}
|
||||
|
||||
`;
|
||||
|
||||
if (example) {
|
||||
prompt += `Example:\n\`\`\`json\n${JSON.stringify(example, null, 2)}\n\`\`\`\n\n`;
|
||||
prompt += `Example of the expected JSON output:
|
||||
json
|
||||
${JSON.stringify(example, null, 2)}
|
||||
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
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.`;
|
||||
prompt += `IMPORTANT: Your entire response must be a single, valid JSON object that conforms to the type definition above. Do not include any additional text, explanations, or markdown formatting before or after the JSON object.`;
|
||||
} else {
|
||||
prompt += `Your response should follow the structure defined above.`;
|
||||
prompt += `Your response should contain a JSON object that follows the structure defined above.`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a response matches the expected type structure
|
||||
* Parses and 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
|
||||
* @returns The parsed and validated data object
|
||||
* @throws {AIProviderError} If parsing or validation fails
|
||||
*/
|
||||
export function validateResponseType<T = any>(
|
||||
export function parseAndValidateResponseType<T = any>(
|
||||
response: string,
|
||||
responseType: ResponseType<T>
|
||||
): { isValid: boolean; data?: T; error?: string } {
|
||||
): T {
|
||||
try {
|
||||
// Parse JSON response
|
||||
// Attempt to parse the 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
|
||||
// Basic validation: ensure it's a non-null object.
|
||||
// For more robust validation, a library like Zod or Ajv could be used
|
||||
// based on the typeDefinition, but that's beyond the current scope.
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Response must be a JSON object'
|
||||
};
|
||||
throw new Error('Response must be a JSON object');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
data: parsed as T
|
||||
};
|
||||
// Here you could add more sophisticated validation if needed, e.g.,
|
||||
// checking keys against the responseType.typeDefinition
|
||||
|
||||
return parsed as T;
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `Invalid JSON: ${(error as Error).message}`
|
||||
};
|
||||
// Wrap parsing/validation errors in a standardized AIProviderError
|
||||
throw new AIProviderError(
|
||||
`Failed to parse or validate structured response: ${(error as Error).message}`,
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
undefined,
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user