diff --git a/sdks/typescript/README.md b/sdks/typescript/README.md index c499ac2..d836164 100644 --- a/sdks/typescript/README.md +++ b/sdks/typescript/README.md @@ -37,8 +37,8 @@ Evaluates vocabulary complexity using the Qual Text Complexity rubric (SAP). **Constructor:** ```typescript const evaluator = new VocabularyEvaluator({ - googleApiKey: string; // Required - Google API key - openaiApiKey: string; // Required - OpenAI API key + googleApiKey?: string; // Google API key (required by this evaluator) + openaiApiKey?: string; // OpenAI API key (required by this evaluator) maxRetries?: number; // Optional - Max retry attempts (default: 2) telemetry?: boolean | TelemetryOptions; // Optional (default: true) logger?: Logger; // Optional - Custom logger @@ -72,14 +72,14 @@ await evaluator.evaluate(text: string, grade: string) Evaluates sentence structure complexity based on grammatical features. -**Supported Grades:** K-12 +**Supported Grades:** 3-12 **Uses:** OpenAI GPT-4o **Constructor:** ```typescript const evaluator = new SentenceStructureEvaluator({ - openaiApiKey: string; // Required - OpenAI API key + openaiApiKey?: string; // OpenAI API key (required by this evaluator) maxRetries?: number; // Optional - Max retry attempts (default: 2) telemetry?: boolean | TelemetryOptions; // Optional (default: true) logger?: Logger; // Optional - Custom logger @@ -108,7 +108,51 @@ await evaluator.evaluate(text: string, grade: string) --- -### 3. Grade Level Appropriateness Evaluator +### 3. Text Complexity Evaluator + +Composite evaluator that analyzes both vocabulary and sentence structure complexity in parallel. + +**Supported Grades:** 3-12 + +**Uses:** Google Gemini 2.5 Pro + OpenAI GPT-4o (composite) + +**Constructor:** +```typescript +const evaluator = new TextComplexityEvaluator({ + googleApiKey?: string; // Google API key (required by this evaluator) + openaiApiKey?: string; // OpenAI API key (required by this evaluator) + maxRetries?: number; // Optional - Max retry attempts (default: 2) + telemetry?: boolean | TelemetryOptions; // Optional (default: true) + logger?: Logger; // Optional - Custom logger + logLevel?: LogLevel; // Optional - Logging verbosity (default: WARN) +}); +``` + +**API:** +```typescript +await evaluator.evaluate(text: string, grade: string) +``` + +**Returns:** +```typescript +{ + score: { + overall: string; // Overall complexity (highest of the two) + vocabulary: string; // Vocabulary complexity score + sentenceStructure: string; // Sentence structure complexity score + }; + reasoning: string; // Combined reasoning from both evaluators + metadata: EvaluationMetadata; + _internal: { + vocabulary: EvaluationResult | { error: Error }; + sentenceStructure: EvaluationResult | { error: Error }; + }; +} +``` + +--- + +### 4. Grade Level Appropriateness Evaluator Determines appropriate grade level for text. @@ -119,7 +163,7 @@ Determines appropriate grade level for text. **Constructor:** ```typescript const evaluator = new GradeLevelAppropriatenessEvaluator({ - googleApiKey: string; // Required - Google API key + googleApiKey?: string; // Google API key (required by this evaluator) maxRetries?: number; // Optional - Max retry attempts (default: 2) telemetry?: boolean | TelemetryOptions; // Optional (default: true) logger?: Logger; // Optional - Custom logger @@ -229,10 +273,12 @@ See [docs/telemetry.md](./docs/telemetry.md) for telemetry configuration and pri ## Configuration Options -All evaluators support these common options: +All evaluators use the same `BaseEvaluatorConfig` interface: ```typescript interface BaseEvaluatorConfig { + googleApiKey?: string; // Google API key (required by some evaluators) + openaiApiKey?: string; // OpenAI API key (required by some evaluators) maxRetries?: number; // Max API retry attempts (default: 2) telemetry?: boolean | TelemetryOptions; // Telemetry config (default: true) logger?: Logger; // Custom logger (optional) @@ -241,6 +287,12 @@ interface BaseEvaluatorConfig { } ``` +**Note:** Which API keys are required depends on the evaluator. The SDK validates required keys at runtime based on the evaluator's metadata: +- **Vocabulary**: Requires both `googleApiKey` and `openaiApiKey` +- **Sentence Structure**: Requires `openaiApiKey` only +- **Text Complexity**: Requires both `googleApiKey` and `openaiApiKey` +- **Grade Level Appropriateness**: Requires `googleApiKey` only + --- ## License diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index 9d6e51c..4959d3c 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "compromise": "^14.13.0", + "p-limit": "^5.0.0", "syllable": "^5.0.1", "zod": "^3.22.4" }, @@ -2847,15 +2848,14 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2876,6 +2876,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3856,12 +3883,11 @@ "dev": true }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 9ec2bf0..53b4c36 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -63,6 +63,7 @@ }, "dependencies": { "compromise": "^14.13.0", + "p-limit": "^5.0.0", "syllable": "^5.0.1", "zod": "^3.22.4" }, diff --git a/sdks/typescript/src/evaluators/base.ts b/sdks/typescript/src/evaluators/base.ts index 65ea428..e15f083 100644 --- a/sdks/typescript/src/evaluators/base.ts +++ b/sdks/typescript/src/evaluators/base.ts @@ -80,6 +80,25 @@ export interface BaseEvaluatorConfig { logLevel?: LogLevel; } +/** + * Evaluator metadata interface + * Each evaluator must provide this metadata as static properties + */ +export interface EvaluatorMetadata { + /** Unique identifier for the evaluator (e.g., 'vocabulary', 'sentence-structure') */ + readonly id: string; + /** Human-readable name (e.g., 'Vocabulary', 'Sentence Structure') */ + readonly name: string; + /** Brief description of what the evaluator does */ + readonly description: string; + /** Supported grade levels (e.g., ['3', '4', '5', ...]) */ + readonly supportedGrades: readonly string[]; + /** Whether this evaluator requires a Google API key */ + readonly requiresGoogleKey: boolean; + /** Whether this evaluator requires an OpenAI API key */ + readonly requiresOpenAIKey: boolean; +} + /** * Abstract base class for all evaluators * @@ -88,6 +107,9 @@ export interface BaseEvaluatorConfig { * - Text validation * - Grade validation (with overridable default) * - Metadata creation + * + * Concrete evaluators must implement: + * - static metadata: Provide evaluator metadata (see EvaluatorMetadata interface) */ export abstract class BaseEvaluator { protected telemetryClient?: TelemetryClient; @@ -96,9 +118,34 @@ export abstract class BaseEvaluator { telemetry: Required; }; + /** + * Static metadata for the evaluator + * + * Concrete evaluators MUST define this property. + * + * @example + * ```typescript + * class MyEvaluator extends BaseEvaluator { + * static readonly metadata = { + * id: 'my-evaluator', + * name: 'My Evaluator', + * description: 'Does something useful', + * supportedGrades: ['3', '4', '5'], + * requiresGoogleKey: true, + * requiresOpenAIKey: false, + * }; + * } + * ``` + */ + static readonly metadata: EvaluatorMetadata; + constructor(config: BaseEvaluatorConfig) { // Initialize logger this.logger = createLogger(config.logger, config.logLevel ?? LogLevel.WARN); + + // Validate required API keys based on metadata + this.validateApiKeys(config); + // Normalize telemetry config const telemetryConfig = this.normalizeTelemetryConfig(config.telemetry); @@ -124,6 +171,31 @@ export abstract class BaseEvaluator { } } + /** + * Get metadata for this evaluator instance + */ + protected get metadata(): EvaluatorMetadata { + return (this.constructor as typeof BaseEvaluator).metadata; + } + + /** + * Validate that required API keys are provided based on metadata + * @throws {ValidationError} If required API keys are missing + */ + private validateApiKeys(config: BaseEvaluatorConfig): void { + if (this.metadata.requiresGoogleKey && !config.googleApiKey) { + throw new ValidationError( + `Google API key is required for ${this.metadata.name} evaluator. Pass googleApiKey in config.` + ); + } + + if (this.metadata.requiresOpenAIKey && !config.openaiApiKey) { + throw new ValidationError( + `OpenAI API key is required for ${this.metadata.name} evaluator. Pass openaiApiKey in config.` + ); + } + } + /** * Normalize telemetry config to standard format */ @@ -153,10 +225,12 @@ export abstract class BaseEvaluator { } /** - * Get the evaluator type identifier (e.g., "vocabulary", "sentence-structure") - * Must be implemented by concrete evaluators + * Get the evaluator type identifier from metadata + * @returns The evaluator type ID (e.g., "vocabulary", "sentence-structure") */ - protected abstract getEvaluatorType(): string; + protected getEvaluatorType(): string { + return this.metadata.id; + } /** * Validate text meets requirements diff --git a/sdks/typescript/src/evaluators/grade-level-appropriateness.ts b/sdks/typescript/src/evaluators/grade-level-appropriateness.ts index fa125c4..755fe73 100644 --- a/sdks/typescript/src/evaluators/grade-level-appropriateness.ts +++ b/sdks/typescript/src/evaluators/grade-level-appropriateness.ts @@ -9,14 +9,6 @@ import type { EvaluationResult } from '../schemas/index.js'; import { BaseEvaluator, type BaseEvaluatorConfig } from './base.js'; import { ValidationError, wrapProviderError } from '../errors.js'; -/** - * Configuration for GradeLevelAppropriatenessEvaluator - */ -export interface GradeLevelAppropriatenessEvaluatorConfig extends BaseEvaluatorConfig { - /** Google API key for grade level evaluation (uses Gemini 2.5 Pro) */ - googleApiKey: string; -} - /** * Grade Level Appropriateness Evaluator * @@ -45,20 +37,21 @@ export interface GradeLevelAppropriatenessEvaluatorConfig extends BaseEvaluatorC * ``` */ export class GradeLevelAppropriatenessEvaluator extends BaseEvaluator { + static readonly metadata = { + id: 'grade-level-appropriateness', + name: 'Grade Level Appropriateness', + description: 'Determines appropriate grade level for text with scaffolding recommendations', + supportedGrades: [] as const, // No grade parameter required - evaluates what grade the text is appropriate for + requiresGoogleKey: true, + requiresOpenAIKey: false, + }; + private provider: LLMProvider; - private evaluatorConfig: GradeLevelAppropriatenessEvaluatorConfig; - constructor(config: GradeLevelAppropriatenessEvaluatorConfig) { - // Call base constructor for common setup (telemetry, etc.) + constructor(config: BaseEvaluatorConfig) { + // Call base constructor for common setup (telemetry, API key validation, etc.) super(config); - // Validate required API keys - if (!config.googleApiKey) { - throw new ValidationError('Google API key is required. Pass googleApiKey in config.'); - } - - this.evaluatorConfig = config; - // Create Google Gemini provider this.provider = createProvider({ type: 'google', @@ -69,11 +62,6 @@ export class GradeLevelAppropriatenessEvaluator extends BaseEvaluator { }); } - // Implement abstract methods from BaseEvaluator - protected getEvaluatorType(): string { - return 'grade-level-appropriateness'; - } - /** * Evaluate grade level appropriateness for a given text * @@ -205,7 +193,7 @@ export class GradeLevelAppropriatenessEvaluator extends BaseEvaluator { */ export async function evaluateGradeLevelAppropriateness( text: string, - config: GradeLevelAppropriatenessEvaluatorConfig + config: BaseEvaluatorConfig ): Promise> { const evaluator = new GradeLevelAppropriatenessEvaluator(config); return evaluator.evaluate(text); diff --git a/sdks/typescript/src/evaluators/index.ts b/sdks/typescript/src/evaluators/index.ts index 12b55d6..3a42ba2 100644 --- a/sdks/typescript/src/evaluators/index.ts +++ b/sdks/typescript/src/evaluators/index.ts @@ -1,19 +1,28 @@ -export { BaseEvaluator, type BaseEvaluatorConfig, type TelemetryOptions } from './base.js'; +export { + BaseEvaluator, + type BaseEvaluatorConfig, + type TelemetryOptions, + type EvaluatorMetadata, +} from './base.js'; export { VocabularyEvaluator, evaluateVocabulary, - type VocabularyEvaluatorConfig, } from './vocabulary.js'; export { SentenceStructureEvaluator, evaluateSentenceStructure, - type SentenceStructureEvaluatorConfig, } from './sentence-structure.js'; export { GradeLevelAppropriatenessEvaluator, evaluateGradeLevelAppropriateness, - type GradeLevelAppropriatenessEvaluatorConfig, } from './grade-level-appropriateness.js'; + +export { + TextComplexityEvaluator, + evaluateTextComplexity, + type TextComplexityScore, + type TextComplexityInternal, +} from './text-complexity.js'; diff --git a/sdks/typescript/src/evaluators/sentence-structure.ts b/sdks/typescript/src/evaluators/sentence-structure.ts index 6726fd3..7226f97 100644 --- a/sdks/typescript/src/evaluators/sentence-structure.ts +++ b/sdks/typescript/src/evaluators/sentence-structure.ts @@ -19,11 +19,6 @@ import { BaseEvaluator, type BaseEvaluatorConfig } from './base.js'; import type { StageDetail } from '../telemetry/index.js'; import { ValidationError, wrapProviderError } from '../errors.js'; -/** - * Valid grade levels (K-12) - */ -const VALID_GRADES = new Set(['K', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']); - /** * Internal data structure for sentence structure evaluation */ @@ -54,14 +49,6 @@ function normalizeLabel(label: string | null | undefined): string | null { return mapping[normalized] || null; // Return null if no mapping found } -/** - * Configuration for SentenceStructureEvaluator - */ -export interface SentenceStructureEvaluatorConfig extends BaseEvaluatorConfig { - /** OpenAI API key for sentence analysis and complexity evaluation (uses GPT-4o) */ - openaiApiKey: string; -} - /** * Sentence Structure Evaluator * @@ -88,21 +75,22 @@ export interface SentenceStructureEvaluatorConfig extends BaseEvaluatorConfig { * ``` */ export class SentenceStructureEvaluator extends BaseEvaluator { + static readonly metadata = { + id: 'sentence-structure', + name: 'Sentence Structure', + description: 'Evaluates sentence structure complexity based on grammatical features', + supportedGrades: ['3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] as const, + requiresGoogleKey: false, + requiresOpenAIKey: true, + }; + private analysisProvider: LLMProvider; private complexityProvider: LLMProvider; - private evaluatorConfig: SentenceStructureEvaluatorConfig; - constructor(config: SentenceStructureEvaluatorConfig) { - // Call base constructor for common setup (telemetry, etc.) + constructor(config: BaseEvaluatorConfig) { + // Call base constructor for common setup (telemetry, API key validation, etc.) super(config); - // Validate required API keys - if (!config.openaiApiKey) { - throw new ValidationError('OpenAI API key is required. Pass openaiApiKey in config.'); - } - - this.evaluatorConfig = config; - // Create OpenAI GPT-4o provider for both stages this.analysisProvider = createProvider({ type: 'openai', @@ -119,11 +107,6 @@ export class SentenceStructureEvaluator extends BaseEvaluator { }); } - // Implement abstract methods from BaseEvaluator - protected getEvaluatorType(): string { - return 'sentence-structure'; - } - /** * Evaluate sentence structure complexity for a given text and grade level * @@ -145,7 +128,7 @@ export class SentenceStructureEvaluator extends BaseEvaluator { // Use inherited validation methods this.validateText(text); - this.validateGrade(grade, VALID_GRADES); + this.validateGrade(grade, new Set(SentenceStructureEvaluator.metadata.supportedGrades)); const startTime = Date.now(); const stageDetails: StageDetail[] = []; @@ -400,7 +383,7 @@ export class SentenceStructureEvaluator extends BaseEvaluator { export async function evaluateSentenceStructure( text: string, grade: string, - config: SentenceStructureEvaluatorConfig + config: BaseEvaluatorConfig ): Promise> { const evaluator = new SentenceStructureEvaluator(config); return evaluator.evaluate(text, grade); diff --git a/sdks/typescript/src/evaluators/text-complexity.ts b/sdks/typescript/src/evaluators/text-complexity.ts new file mode 100644 index 0000000..f93ee0c --- /dev/null +++ b/sdks/typescript/src/evaluators/text-complexity.ts @@ -0,0 +1,293 @@ +import pLimit from 'p-limit'; +import { VocabularyEvaluator } from './vocabulary.js'; +import { SentenceStructureEvaluator } from './sentence-structure.js'; +import type { BaseEvaluatorConfig } from './base.js'; +import { BaseEvaluator } from './base.js'; +import type { EvaluationResult } from '../schemas/index.js'; +import { ValidationError } from '../errors.js'; + +/** + * Internal data structure for text complexity evaluation + * Stores either successful evaluation results or errors from sub-evaluators + */ +export interface TextComplexityInternal { + vocabulary: EvaluationResult | { error: Error }; + sentenceStructure: EvaluationResult | { error: Error }; +} + +/** + * Composite score for text complexity + */ +export interface TextComplexityScore { + /** Overall complexity assessment */ + overall: string; + /** Vocabulary complexity score */ + vocabulary: string; + /** Sentence structure complexity score */ + sentenceStructure: string; +} + +/** + * Text Complexity Evaluator + * + * Composite evaluator that analyzes both vocabulary and sentence structure complexity. + * Runs both evaluations in parallel with concurrency control to avoid rate limiting. + * + * Uses: + * - VocabularyEvaluator (Google Gemini 2.5 Pro + OpenAI GPT-4o) + * - SentenceStructureEvaluator (OpenAI GPT-4o) + * + * @example + * ```typescript + * const evaluator = new TextComplexityEvaluator({ + * googleApiKey: process.env.GOOGLE_API_KEY, + * openaiApiKey: process.env.OPENAI_API_KEY + * }); + * + * const result = await evaluator.evaluate(text, "5"); + * console.log(result.score.overall); + * console.log(result.score.vocabulary); + * console.log(result.score.sentenceStructure); + * ``` + */ +export class TextComplexityEvaluator extends BaseEvaluator { + static readonly metadata = { + id: 'text-complexity', + name: 'Text Complexity', + description: 'Composite evaluator analyzing vocabulary and sentence structure complexity', + supportedGrades: ['3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] as const, + requiresGoogleKey: true, + requiresOpenAIKey: true, + }; + + private vocabularyEvaluator: VocabularyEvaluator; + private sentenceStructureEvaluator: SentenceStructureEvaluator; + private limit: ReturnType; + + constructor(config: BaseEvaluatorConfig) { + // Call base constructor for common setup (telemetry, API key validation, etc.) + super(config); + + // Create child evaluators with same config + this.vocabularyEvaluator = new VocabularyEvaluator(config); + this.sentenceStructureEvaluator = new SentenceStructureEvaluator(config); + + // Create concurrency limiter (max 3 concurrent operations) + this.limit = pLimit(3); + } + + /** + * Evaluate text complexity for a given text and grade level + * + * Runs vocabulary and sentence structure evaluations in parallel with concurrency control. + * + * @param text - The text to evaluate + * @param grade - The target grade level (3-12) + * @returns Evaluation result with composite complexity score + * @throws {Error} If text is empty or grade is invalid + */ + async evaluate( + text: string, + grade: string + ): Promise> { + this.logger.info('Starting text complexity evaluation', { + evaluator: 'text-complexity', + operation: 'evaluate', + grade, + textLength: text.length, + }); + + // Use inherited validation methods + this.validateText(text); + this.validateGrade(grade, new Set(TextComplexityEvaluator.metadata.supportedGrades)); + + const startTime = Date.now(); + + // Run both evaluators in parallel with concurrency control + const [vocabResult, sentenceResult] = await Promise.all([ + this.limit(() => this.runSubEvaluator(this.vocabularyEvaluator, text, grade)), + this.limit(() => this.runSubEvaluator(this.sentenceStructureEvaluator, text, grade)), + ]); + + const latencyMs = Date.now() - startTime; + + // Determine overall complexity + const overall = this.determineOverallComplexity(vocabResult, sentenceResult); + + // Build combined reasoning + const reasoning = this.buildCombinedReasoning(vocabResult, sentenceResult); + + // Check if any evaluations failed + const vocabFailed = 'error' in vocabResult; + const sentenceFailed = 'error' in sentenceResult; + const hasFailures = vocabFailed || sentenceFailed; + + if (hasFailures) { + const errors: string[] = []; + if (vocabFailed) { + errors.push(`Vocabulary evaluation failed: ${vocabResult.error.message}`); + } + if (sentenceFailed) { + errors.push(`Sentence structure evaluation failed: ${sentenceResult.error.message}`); + } + + this.logger.error('Text complexity evaluation completed with errors', { + evaluator: 'text-complexity', + operation: 'evaluate', + grade, + errors, + processingTimeMs: latencyMs, + }); + + // If both failed, throw error + if (vocabFailed && sentenceFailed) { + throw new Error( + `Text complexity evaluation failed: ${errors.join('; ')}` + ); + } + } + + const result = { + score: { + overall, + vocabulary: vocabFailed ? 'N/A' : vocabResult.score, + sentenceStructure: sentenceFailed ? 'N/A' : sentenceResult.score, + }, + reasoning, + metadata: { + promptVersion: '1.0', + model: 'composite:gemini-2.5-pro+gpt-4o', + timestamp: new Date(), + processingTimeMs: latencyMs, + }, + _internal: { + vocabulary: vocabResult, + sentenceStructure: sentenceResult, + }, + }; + + // Send telemetry (fire-and-forget) + this.sendTelemetry({ + status: hasFailures ? 'error' : 'success', + latencyMs, + textLength: text.length, + grade, + provider: 'composite:google+openai', + retryAttempts: -1, // Composite evaluator doesn't track retries + errorCode: hasFailures ? 'PartialFailure' : undefined, + inputText: text, + }).catch(() => { + // Ignore telemetry errors + }); + + this.logger.info('Text complexity evaluation completed', { + evaluator: 'text-complexity', + operation: 'evaluate', + grade, + overall: result.score.overall, + processingTimeMs: latencyMs, + hasFailures, + }); + + return result; + } + + /** + * Run a sub-evaluator with error handling + * Returns the evaluation result or an error object + */ + private async runSubEvaluator( + evaluator: { evaluate(text: string, grade: string): Promise> }, + text: string, + grade: string + ): Promise | { error: Error }> { + try { + return await evaluator.evaluate(text, grade); + } catch (error) { + return { + error: error instanceof Error ? error : new Error(String(error)), + }; + } + } + + /** + * Determine overall complexity from individual results + * + * Logic: Take the higher (more complex) of the two scores + * Order: Slightly < Moderately < Very < Exceedingly + */ + private determineOverallComplexity( + vocabResult: EvaluationResult | { error: Error }, + sentenceResult: EvaluationResult | { error: Error } + ): string { + // If either failed, use the successful one or return error + if ('error' in vocabResult) { + return 'error' in sentenceResult ? 'Error' : sentenceResult.score; + } + if ('error' in sentenceResult) { + return vocabResult.score; + } + + // Both succeeded - take the higher complexity + const complexityOrder = [ + 'slightly complex', + 'moderately complex', + 'very complex', + 'exceedingly complex', + ]; + + const vocabIndex = complexityOrder.indexOf(vocabResult.score.toLowerCase()); + const sentenceIndex = complexityOrder.indexOf(sentenceResult.score.toLowerCase()); + + // Return the higher complexity (or vocabulary if equal) + return vocabIndex >= sentenceIndex ? vocabResult.score : sentenceResult.score; + } + + /** + * Build combined reasoning from individual results + */ + private buildCombinedReasoning( + vocabResult: EvaluationResult | { error: Error }, + sentenceResult: EvaluationResult | { error: Error } + ): string { + const parts: string[] = []; + + if ('error' in vocabResult) { + parts.push(`**Vocabulary Complexity:** Evaluation failed - ${vocabResult.error.message}`); + } else { + parts.push(`**Vocabulary Complexity (${vocabResult.score}):**\n${vocabResult.reasoning}`); + } + + if ('error' in sentenceResult) { + parts.push(`**Sentence Structure Complexity:** Evaluation failed - ${sentenceResult.error.message}`); + } else { + parts.push(`**Sentence Structure Complexity (${sentenceResult.score}):**\n${sentenceResult.reasoning}`); + } + + return parts.join('\n\n'); + } +} + +/** + * Functional API for text complexity evaluation + * + * @example + * ```typescript + * const result = await evaluateTextComplexity( + * "The cat sat on the mat.", + * "5", + * { + * googleApiKey: process.env.GOOGLE_API_KEY, + * openaiApiKey: process.env.OPENAI_API_KEY + * } + * ); + * ``` + */ +export async function evaluateTextComplexity( + text: string, + grade: string, + config: BaseEvaluatorConfig +): Promise> { + const evaluator = new TextComplexityEvaluator(config); + return evaluator.evaluate(text, grade); +} diff --git a/sdks/typescript/src/evaluators/vocabulary.ts b/sdks/typescript/src/evaluators/vocabulary.ts index b9270bd..912376f 100644 --- a/sdks/typescript/src/evaluators/vocabulary.ts +++ b/sdks/typescript/src/evaluators/vocabulary.ts @@ -16,22 +16,6 @@ import { BaseEvaluator, type BaseEvaluatorConfig } from './base.js'; import type { StageDetail } from '../telemetry/index.js'; import { ValidationError, wrapProviderError } from '../errors.js'; -/** - * Valid grade levels (3-12) - */ -const VALID_GRADES = new Set(['3', '4', '5', '6', '7', '8', '9', '10', '11', '12']); - -/** - * Configuration for VocabularyEvaluator - */ -export interface VocabularyEvaluatorConfig extends BaseEvaluatorConfig { - /** Google API key for complexity evaluation (uses Gemini 2.5 Pro) */ - googleApiKey: string; - - /** OpenAI API key for background knowledge generation (uses GPT-4o) */ - openaiApiKey: string; -} - /** * Vocabulary Evaluator * @@ -59,25 +43,22 @@ export interface VocabularyEvaluatorConfig extends BaseEvaluatorConfig { * ``` */ export class VocabularyEvaluator extends BaseEvaluator { + static readonly metadata = { + id: 'vocabulary', + name: 'Vocabulary', + description: 'Evaluates vocabulary complexity using the Qual Text Complexity rubric (SAP)', + supportedGrades: ['3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] as const, + requiresGoogleKey: true, + requiresOpenAIKey: true, + }; + private complexityProvider: LLMProvider; private backgroundKnowledgeProvider: LLMProvider; - private evaluatorConfig: VocabularyEvaluatorConfig; - constructor(config: VocabularyEvaluatorConfig) { - // Call base constructor for common setup (telemetry, etc.) + constructor(config: BaseEvaluatorConfig) { + // Call base constructor for common setup (telemetry, API key validation, etc.) super(config); - // Validate required API keys - if (!config.googleApiKey) { - throw new ValidationError('Google API key is required. Pass googleApiKey in config.'); - } - - if (!config.openaiApiKey) { - throw new ValidationError('OpenAI API key is required. Pass openaiApiKey in config.'); - } - - this.evaluatorConfig = config; - // Create Google Gemini provider for complexity evaluation this.complexityProvider = createProvider({ type: 'google', @@ -95,11 +76,6 @@ export class VocabularyEvaluator extends BaseEvaluator { }); } - // Implement abstract methods from BaseEvaluator - protected getEvaluatorType(): string { - return 'vocabulary'; - } - /** * Evaluate vocabulary complexity for a given text and grade level * @@ -121,7 +97,7 @@ export class VocabularyEvaluator extends BaseEvaluator { // Use inherited validation methods this.validateText(text); - this.validateGrade(grade, VALID_GRADES); + this.validateGrade(grade, new Set(VocabularyEvaluator.metadata.supportedGrades)); const startTime = Date.now(); const stageDetails: StageDetail[] = []; @@ -351,7 +327,7 @@ export class VocabularyEvaluator extends BaseEvaluator { export async function evaluateVocabulary( text: string, grade: string, - config: VocabularyEvaluatorConfig + config: BaseEvaluatorConfig ): Promise> { const evaluator = new VocabularyEvaluator(config); return evaluator.evaluate(text, grade); diff --git a/sdks/typescript/src/index.ts b/sdks/typescript/src/index.ts index 2e576d1..3ea2aaa 100644 --- a/sdks/typescript/src/index.ts +++ b/sdks/typescript/src/index.ts @@ -66,15 +66,17 @@ export { GradeLevelAppropriatenessSchema } from './schemas/grade-level-appropria export { VocabularyEvaluator, evaluateVocabulary, - type VocabularyEvaluatorConfig, SentenceStructureEvaluator, evaluateSentenceStructure, - type SentenceStructureEvaluatorConfig, GradeLevelAppropriatenessEvaluator, evaluateGradeLevelAppropriateness, - type GradeLevelAppropriatenessEvaluatorConfig, + TextComplexityEvaluator, + evaluateTextComplexity, + type TextComplexityScore, + type TextComplexityInternal, type BaseEvaluatorConfig, type TelemetryOptions, + type EvaluatorMetadata, } from './evaluators/index.js'; // Features diff --git a/sdks/typescript/tests/integration/sentence-structure.integration.test.ts b/sdks/typescript/tests/integration/sentence-structure.integration.test.ts index 5361bae..3012f64 100644 --- a/sdks/typescript/tests/integration/sentence-structure.integration.test.ts +++ b/sdks/typescript/tests/integration/sentence-structure.integration.test.ts @@ -12,7 +12,7 @@ config(); /** * Sentence Structure Evaluator Integration Tests * - * Test cases cover grades 2-6 with varying complexity levels. + * Test cases cover grades 3-6 with varying complexity levels. * * Each test uses a retry mechanism (up to 3 attempts) to account for LLM non-determinism, * with short-circuiting on first expected match. If no expected match is found after all @@ -33,13 +33,13 @@ const describeIntegration = SKIP_INTEGRATION ? describe.skip : describe; const TEST_TIMEOUT_MS = 2 * 60 * 1000; const TEST_CASES: BaseTestCase[] = [ - { - id: 'SS2', - grade: '2', - text: "The Roman Empire was a powerful empire that lasted for hundreds of years. It started as a small village in Italy and grew into a huge empire that controlled much of Europe, Asia, and Africa. The Roman Empire had many strong leaders like Julius Caesar and Augustus. These leaders helped the empire grow and become very powerful.\n \n\n The Roman Empire had a period of peace and prosperity called the Pax Romana. This time was good for the empire, but it didn't last forever. The empire started to have problems. The army became weaker, and the economy had problems. The empire was also attacked by groups of people called barbarians.\n \n\n The Roman Empire was divided into two parts: the Western Roman Empire and the Eastern Roman Empire. The Western Roman Empire eventually fell apart in 476 AD. The Eastern Roman Empire, also known as the Byzantine Empire, lasted for many more years. The Roman Empire left behind many things that we still use today, like the Roman alphabet and the calendar.", - expected: 'moderately complex', - acceptable: ['slightly complex', 'very complex'], - }, + // { + // id: 'SS2', + // grade: '2', + // text: "The Roman Empire was a powerful empire that lasted for hundreds of years. It started as a small village in Italy and grew into a huge empire that controlled much of Europe, Asia, and Africa. The Roman Empire had many strong leaders like Julius Caesar and Augustus. These leaders helped the empire grow and become very powerful.\n \n\n The Roman Empire had a period of peace and prosperity called the Pax Romana. This time was good for the empire, but it didn't last forever. The empire started to have problems. The army became weaker, and the economy had problems. The empire was also attacked by groups of people called barbarians.\n \n\n The Roman Empire was divided into two parts: the Western Roman Empire and the Eastern Roman Empire. The Western Roman Empire eventually fell apart in 476 AD. The Eastern Roman Empire, also known as the Byzantine Empire, lasted for many more years. The Roman Empire left behind many things that we still use today, like the Roman alphabet and the calendar.", + // expected: 'moderately complex', + // acceptable: ['slightly complex', 'very complex'], + // }, { id: 'SS3', grade: '3', diff --git a/sdks/typescript/tests/unit/evaluators/grade-level-appropriateness.test.ts b/sdks/typescript/tests/unit/evaluators/grade-level-appropriateness.test.ts index 0d88be0..15fb72b 100644 --- a/sdks/typescript/tests/unit/evaluators/grade-level-appropriateness.test.ts +++ b/sdks/typescript/tests/unit/evaluators/grade-level-appropriateness.test.ts @@ -37,7 +37,7 @@ describe('GradeLevelAppropriatenessEvaluator - Constructor Validation', () => { it('should throw error when Google API key is missing', () => { expect(() => new GradeLevelAppropriatenessEvaluator({ googleApiKey: '', - })).toThrow('Google API key is required. Pass googleApiKey in config.'); + })).toThrow('Google API key is required for Grade Level Appropriateness evaluator. Pass googleApiKey in config.'); }); }); diff --git a/sdks/typescript/tests/unit/evaluators/sentence-structure.test.ts b/sdks/typescript/tests/unit/evaluators/sentence-structure.test.ts index 562e824..80417b8 100644 --- a/sdks/typescript/tests/unit/evaluators/sentence-structure.test.ts +++ b/sdks/typescript/tests/unit/evaluators/sentence-structure.test.ts @@ -78,7 +78,7 @@ describe('SentenceStructureEvaluator - Constructor Validation', () => { it('should throw error when OpenAI API key is missing', () => { expect(() => new SentenceStructureEvaluator({ openaiApiKey: '', - })).toThrow('OpenAI API key is required. Pass openaiApiKey in config.'); + })).toThrow('OpenAI API key is required for Sentence Structure evaluator. Pass openaiApiKey in config.'); }); }); @@ -111,7 +111,7 @@ describe('SentenceStructureEvaluator - Evaluation Flow', () => { describe('Successful Evaluation Flow', () => { it('should successfully evaluate text through both stages', async () => { const testText = 'The cat sat on the mat. It was sleeping peacefully.'; - const testGrade = 'K'; + const testGrade = '3'; // Mock sentence analysis response vi.mocked(mockAnalysisProvider.generateStructured).mockResolvedValue({ @@ -128,7 +128,7 @@ describe('SentenceStructureEvaluator - Evaluation Flow', () => { vi.mocked(mockComplexityProvider.generateStructured).mockResolvedValue({ data: { answer: 'Slightly Complex', - reasoning: 'The text uses simple sentence structures appropriate for kindergarten.', + reasoning: 'The text uses simple sentence structures appropriate for third grade.', }, model: 'gpt-4o', usage: { diff --git a/sdks/typescript/tests/unit/evaluators/text-complexity.test.ts b/sdks/typescript/tests/unit/evaluators/text-complexity.test.ts new file mode 100644 index 0000000..7949628 --- /dev/null +++ b/sdks/typescript/tests/unit/evaluators/text-complexity.test.ts @@ -0,0 +1,337 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { TextComplexityEvaluator } from '../../../src/evaluators/text-complexity.js'; +import { VocabularyEvaluator } from '../../../src/evaluators/vocabulary.js'; +import { SentenceStructureEvaluator } from '../../../src/evaluators/sentence-structure.js'; +import { ValidationError } from '../../../src/errors.js'; + +// Mock telemetry to avoid real HTTP calls +vi.mock('../../../src/telemetry/client.js', () => ({ + TelemetryClient: class MockTelemetryClient { + send = vi.fn().mockResolvedValue(undefined); + }, +})); + +// Mock providers to avoid real API calls +vi.mock('../../../src/providers/index.js', () => ({ + createProvider: vi.fn(() => ({ + generateStructured: vi.fn().mockResolvedValue({ + data: { + complexity_score: 'moderately complex', + reasoning: 'Test reasoning', + answer: 'Moderately Complex', + }, + usage: { inputTokens: 100, outputTokens: 50 }, + latencyMs: 100, + }), + generateText: vi.fn().mockResolvedValue({ + text: 'Test background knowledge', + usage: { inputTokens: 100, outputTokens: 50 }, + latencyMs: 100, + }), + })), +})); + +describe('TextComplexityEvaluator', () => { + describe('Metadata', () => { + it('should have correct metadata', () => { + expect(TextComplexityEvaluator.metadata.id).toBe('text-complexity'); + expect(TextComplexityEvaluator.metadata.name).toBe('Text Complexity'); + expect(TextComplexityEvaluator.metadata.requiresGoogleKey).toBe(true); + expect(TextComplexityEvaluator.metadata.requiresOpenAIKey).toBe(true); + expect(TextComplexityEvaluator.metadata.supportedGrades).toEqual([ + '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', + ]); + }); + }); + + describe('Constructor', () => { + it('should create evaluator with valid config', () => { + const evaluator = new TextComplexityEvaluator({ + googleApiKey: 'test-google-key', + openaiApiKey: 'test-openai-key', + telemetry: false, + }); + + expect(evaluator).toBeDefined(); + }); + + it('should throw error when Google API key is missing', () => { + expect(() => { + new TextComplexityEvaluator({ + openaiApiKey: 'test-openai-key', + telemetry: false, + }); + }).toThrow(ValidationError); + expect(() => { + new TextComplexityEvaluator({ + openaiApiKey: 'test-openai-key', + telemetry: false, + }); + }).toThrow('Google API key is required for Text Complexity evaluator'); + }); + + it('should throw error when OpenAI API key is missing', () => { + expect(() => { + new TextComplexityEvaluator({ + googleApiKey: 'test-google-key', + telemetry: false, + }); + }).toThrow(ValidationError); + expect(() => { + new TextComplexityEvaluator({ + googleApiKey: 'test-google-key', + telemetry: false, + }); + }).toThrow('OpenAI API key is required for Text Complexity evaluator'); + }); + + it('should throw error when both API keys are missing', () => { + expect(() => { + new TextComplexityEvaluator({ + telemetry: false, + }); + }).toThrow(ValidationError); + }); + }); + + describe('evaluate()', () => { + let evaluator: TextComplexityEvaluator; + let vocabSpy: any; + let sentenceSpy: any; + + beforeEach(() => { + evaluator = new TextComplexityEvaluator({ + googleApiKey: 'test-google-key', + openaiApiKey: 'test-openai-key', + telemetry: false, + }); + + // Mock the child evaluators' evaluate methods + vocabSpy = vi.spyOn((evaluator as any).vocabularyEvaluator, 'evaluate').mockResolvedValue({ + score: 'moderately complex', + reasoning: 'Vocabulary test reasoning', + metadata: { + promptVersion: '1.0', + model: 'gemini-2.5-pro + gpt-4o', + timestamp: new Date(), + processingTimeMs: 100, + }, + _internal: {}, + }); + + sentenceSpy = vi.spyOn((evaluator as any).sentenceStructureEvaluator, 'evaluate').mockResolvedValue({ + score: 'Moderately Complex', + reasoning: 'Sentence structure test reasoning', + metadata: { + promptVersion: '1.0', + model: 'gpt-4o', + timestamp: new Date(), + processingTimeMs: 100, + }, + _internal: {}, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should evaluate text successfully', async () => { + const text = 'The cat sat on the mat.'; + const grade = '5'; + + const result = await evaluator.evaluate(text, grade); + + expect(result).toBeDefined(); + expect(result.score).toBeDefined(); + expect(result.score.overall).toBeDefined(); + expect(result.score.vocabulary).toBeDefined(); + expect(result.score.sentenceStructure).toBeDefined(); + expect(result.reasoning).toBeDefined(); + expect(result.metadata).toBeDefined(); + expect(result.metadata.model).toBe('composite:gemini-2.5-pro+gpt-4o'); + expect(result._internal).toBeDefined(); + expect(result._internal.vocabulary).toBeDefined(); + expect(result._internal.sentenceStructure).toBeDefined(); + }); + + it('should validate text input', async () => { + await expect(evaluator.evaluate('', '5')).rejects.toThrow(ValidationError); + await expect(evaluator.evaluate(' ', '5')).rejects.toThrow( + 'Text cannot be empty or contain only whitespace' + ); + await expect(evaluator.evaluate('abc', '5')).rejects.toThrow( + 'Text is too short' + ); + }); + + it('should validate grade input', async () => { + const text = 'The cat sat on the mat.'; + + await expect(evaluator.evaluate(text, 'invalid')).rejects.toThrow( + ValidationError + ); + await expect(evaluator.evaluate(text, 'invalid')).rejects.toThrow( + 'Invalid grade "invalid"' + ); + + // Grades outside supported range (K, 1, 2 not supported) + await expect(evaluator.evaluate(text, 'K')).rejects.toThrow(ValidationError); + await expect(evaluator.evaluate(text, '1')).rejects.toThrow(ValidationError); + await expect(evaluator.evaluate(text, '2')).rejects.toThrow(ValidationError); + }); + + it('should accept all supported grades', async () => { + const text = 'The cat sat on the mat.'; + const supportedGrades = ['3', '4', '5', '6', '7', '8', '9', '10', '11', '12']; + + for (const grade of supportedGrades) { + const result = await evaluator.evaluate(text, grade); + expect(result).toBeDefined(); + } + }); + + it('should run both evaluators in parallel', async () => { + const text = 'The cat sat on the mat.'; + const grade = '5'; + + const startTime = Date.now(); + const result = await evaluator.evaluate(text, grade); + const duration = Date.now() - startTime; + + // With mocked providers that take ~100ms each, parallel execution should be faster than sequential + // Sequential would be ~200ms, parallel should be ~100ms + // Allow some overhead but should be significantly less than 200ms + expect(duration).toBeLessThan(200); + + expect('error' in result._internal.vocabulary).toBe(false); + expect('error' in result._internal.sentenceStructure).toBe(false); + }); + + it('should handle partial failures gracefully', async () => { + const text = 'The cat sat on the mat.'; + const grade = '5'; + + // Override the spy to make vocabulary fail but sentence structure succeed + vocabSpy.mockRejectedValue(new Error('Vocabulary evaluation failed')); + + const result = await evaluator.evaluate(text, grade); + + expect(result).toBeDefined(); + expect('error' in result._internal.vocabulary).toBe(true); + expect(result._internal.vocabulary.error).toBeDefined(); + expect('error' in result._internal.sentenceStructure).toBe(false); + expect(result.score.vocabulary).toBe('N/A'); + expect(result.score.sentenceStructure).not.toBe('N/A'); + }); + + it('should throw when both evaluators fail', async () => { + const text = 'The cat sat on the mat.'; + const grade = '5'; + + // Override both spies to fail + vocabSpy.mockRejectedValue(new Error('Vocabulary evaluation failed')); + sentenceSpy.mockRejectedValue(new Error('Sentence structure evaluation failed')); + + await expect(evaluator.evaluate(text, grade)).rejects.toThrow( + 'Text complexity evaluation failed' + ); + }); + + it('should determine overall complexity correctly', async () => { + const text = 'The cat sat on the mat.'; + const grade = '5'; + + // Override vocabulary to return "moderately complex" + vocabSpy.mockResolvedValue({ + score: 'moderately complex', + reasoning: 'Vocab reasoning', + metadata: { + promptVersion: '1.0', + model: 'gemini-2.5-pro', + timestamp: new Date(), + processingTimeMs: 100, + }, + _internal: {}, + }); + + // Override sentence structure to return "Slightly Complex" + sentenceSpy.mockResolvedValue({ + score: 'Slightly Complex', + reasoning: 'Sentence reasoning', + metadata: { + promptVersion: '1.0', + model: 'gpt-4o', + timestamp: new Date(), + processingTimeMs: 100, + }, + _internal: {}, + }); + + const result = await evaluator.evaluate(text, grade); + + // Should take the higher complexity (moderately complex) + expect(result.score.overall).toBe('moderately complex'); + expect(result.score.vocabulary).toBe('moderately complex'); + expect(result.score.sentenceStructure).toBe('Slightly Complex'); + }); + + it('should build combined reasoning from both evaluators', async () => { + const text = 'The cat sat on the mat.'; + const grade = '5'; + + // Override both evaluators with specific reasoning + vocabSpy.mockResolvedValue({ + score: 'moderately complex', + reasoning: 'This is the vocabulary reasoning.', + metadata: { + promptVersion: '1.0', + model: 'gemini-2.5-pro', + timestamp: new Date(), + processingTimeMs: 100, + }, + _internal: {}, + }); + + sentenceSpy.mockResolvedValue({ + score: 'Slightly Complex', + reasoning: 'This is the sentence structure reasoning.', + metadata: { + promptVersion: '1.0', + model: 'gpt-4o', + timestamp: new Date(), + processingTimeMs: 100, + }, + _internal: {}, + }); + + const result = await evaluator.evaluate(text, grade); + + expect(result.reasoning).toContain('Vocabulary Complexity'); + expect(result.reasoning).toContain('This is the vocabulary reasoning.'); + expect(result.reasoning).toContain('Sentence Structure Complexity'); + expect(result.reasoning).toContain('This is the sentence structure reasoning.'); + }); + }); + + describe('Concurrency Control', () => { + it('should use p-limit for concurrency control', async () => { + const evaluator = new TextComplexityEvaluator({ + googleApiKey: 'test-google-key', + openaiApiKey: 'test-openai-key', + telemetry: false, + }); + + // Check that limit is defined + expect((evaluator as any).limit).toBeDefined(); + + const text = 'The cat sat on the mat.'; + const grade = '5'; + + await evaluator.evaluate(text, grade); + + // The limit should have been used (both calls go through it) + expect((evaluator as any).limit).toBeDefined(); + }); + }); +}); diff --git a/sdks/typescript/tests/unit/evaluators/vocabulary.test.ts b/sdks/typescript/tests/unit/evaluators/vocabulary.test.ts index 6b75336..423f755 100644 --- a/sdks/typescript/tests/unit/evaluators/vocabulary.test.ts +++ b/sdks/typescript/tests/unit/evaluators/vocabulary.test.ts @@ -39,14 +39,14 @@ describe('VocabularyEvaluator - Constructor Validation', () => { expect(() => new VocabularyEvaluator({ googleApiKey: '', openaiApiKey: 'test-openai-key', - })).toThrow('Google API key is required. Pass googleApiKey in config.'); + })).toThrow('Google API key is required for Vocabulary evaluator. Pass googleApiKey in config.'); }); it('should throw error when OpenAI API key is missing', () => { expect(() => new VocabularyEvaluator({ googleApiKey: 'test-google-key', openaiApiKey: '', - })).toThrow('OpenAI API key is required. Pass openaiApiKey in config.'); + })).toThrow('OpenAI API key is required for Vocabulary evaluator. Pass openaiApiKey in config.'); }); });