Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions sdks/typescript/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to the `@learning-commons/evaluators` TypeScript SDK will be documented in this file.

## [0.3.0] — 2026-03-20

### Added

- **Conventionality Evaluator** — evaluates how explicit, literal, and straightforward a text's meaning is versus how abstract, ironic, figurative, or archaic it is, relative to grades 3–12.
- **Conventionality added to TextComplexityEvaluator** — composite evaluator now runs vocabulary, sentence structure, SMK, and conventionality in parallel; result includes `conventionality` key.

---

## [0.2.0] — 2026-03-18

### Added
Expand Down
75 changes: 71 additions & 4 deletions sdks/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,71 @@ console.log(result._internal.identified_topics); // ["hydraulics", "propulsion",

---

### 4. Text Complexity Evaluator
### 4. Conventionality Evaluator

Composite evaluator that analyzes vocabulary, sentence structure, and subject matter knowledge complexity in parallel.
Evaluates how explicit, literal, and straightforward a text's meaning is versus how abstract, ironic, figurative, or archaic it is for the target grade level. Based on the Common Core Qualitative Text Complexity Rubric.

**Supported Grades:** 3-12

**Uses:** Google Gemini 3 Flash Preview

**Constructor:**
```typescript
const evaluator = new ConventionalityEvaluator({
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
logLevel?: LogLevel; // Optional - Logging verbosity (default: WARN)
});
```

**API:**
```typescript
await evaluator.evaluate(text: string, grade: string)
```

**Returns:**
```typescript
{
score: 'Slightly complex' | 'Moderately complex' | 'Very complex' | 'Exceedingly complex';
reasoning: string;
metadata: {
model: string;
processingTimeMs: number;
};
_internal: {
conventionality_features: string[];
grade_context: string;
instructional_insights: string;
complexity_score: 'Slightly complex' | 'Moderately complex' | 'Very complex' | 'Exceedingly complex';
reasoning: string;
};
}
```

**Example:**
```typescript
import { ConventionalityEvaluator } from '@learning-commons/evaluators';

const evaluator = new ConventionalityEvaluator({
googleApiKey: process.env.GOOGLE_API_KEY,
});

const result = await evaluator.evaluate(
"The author uses sustained irony to critique societal norms throughout the passage.",
"10"
);
console.log(result.score); // "Very complex"
console.log(result.reasoning);
console.log(result._internal.conventionality_features); // ["sustained irony", ...]
```

---

### 5. Text Complexity Evaluator

Composite evaluator that analyzes vocabulary, sentence structure, subject matter knowledge, and conventionality complexity in parallel.

**Supported Grades:** 3-12

Expand Down Expand Up @@ -211,10 +273,11 @@ await evaluator.evaluate(text: string, grade: string)
vocabulary: EvaluationResult<TextComplexityLevel> | { error: Error };
sentenceStructure: EvaluationResult<TextComplexityLevel> | { error: Error };
subjectMatterKnowledge: EvaluationResult<TextComplexityLevel> | { error: Error };
conventionality: EvaluationResult<TextComplexityLevel> | { error: Error };
}
```

Each sub-evaluator result is either a full `EvaluationResult` or `{ error: Error }` if that evaluator failed. An error is only thrown if all three fail.
Each sub-evaluator result is either a full `EvaluationResult` or `{ error: Error }` if that evaluator failed. An error is only thrown if all four fail.

**Example:**
```typescript
Expand All @@ -236,11 +299,14 @@ if (!('error' in result.sentenceStructure)) {
if (!('error' in result.subjectMatterKnowledge)) {
console.log('Subject matter knowledge:', result.subjectMatterKnowledge.score);
}
if (!('error' in result.conventionality)) {
console.log('Conventionality:', result.conventionality.score);
}
```

---

### 5. Grade Level Appropriateness Evaluator
### 6. Grade Level Appropriateness Evaluator

Determines appropriate grade level for text.

Expand Down Expand Up @@ -388,6 +454,7 @@ interface BaseEvaluatorConfig {
- **Vocabulary**: Requires both `googleApiKey` and `openaiApiKey`
- **Sentence Structure**: Requires `openaiApiKey` only
- **Subject Matter Knowledge**: Requires `googleApiKey` only
- **Conventionality**: Requires `googleApiKey` only
- **Text Complexity**: Requires both `googleApiKey` and `openaiApiKey`
- **Grade Level Appropriateness**: Requires `googleApiKey` only

Expand Down
2 changes: 1 addition & 1 deletion sdks/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@learning-commons/evaluators",
"version": "0.1.0",
"version": "0.3.0",
"description": "TypeScript SDK for Learning Commons educational evaluators",
"type": "module",
"types": "./dist/index.d.ts",
Expand Down
229 changes: 229 additions & 0 deletions sdks/typescript/src/evaluators/conventionality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import type { LLMProvider } from '../providers/index.js';
import { createProvider } from '../providers/index.js';
import { ConventionalityOutputSchema, type ConventionalityInternal } from '../schemas/conventionality.js';
import { calculateFleschKincaidGrade } from '../features/index.js';
import { getSystemPrompt, getUserPrompt } from '../prompts/conventionality/index.js';
import type { EvaluationResult, TextComplexityLevel } from '../schemas/index.js';
import { BaseEvaluator, type BaseEvaluatorConfig } from './base.js';
import type { StageDetail } from '../telemetry/index.js';
import { ValidationError, wrapProviderError } from '../errors.js';

/**
* Conventionality Evaluator
*
* Evaluates how explicit, literal, and straightforward a text's meaning is versus
* how abstract, ironic, figurative, or archaic it is for the target grade level.
*
* Based on the Common Core Qualitative Text Complexity Rubric with 4 levels:
* - Slightly complex
* - Moderately complex
* - Very complex
* - Exceedingly complex
*
* @example
* ```typescript
* const evaluator = new ConventionalityEvaluator({
* googleApiKey: process.env.GOOGLE_API_KEY
* });
*
* const result = await evaluator.evaluate(text, "6");
* console.log(result.score); // "Moderately complex"
* console.log(result.reasoning);
* ```
*/
export class ConventionalityEvaluator extends BaseEvaluator {
static readonly metadata = {
id: 'conventionality',
name: 'Conventionality',
description: 'Evaluates how explicit, literal, and straightforward a text\'s meaning is relative to grade level',
supportedGrades: ['3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] as const,
requiresGoogleKey: true,
requiresOpenAIKey: false,
};

private provider: LLMProvider;

constructor(config: BaseEvaluatorConfig) {
super(config);

this.provider = createProvider({
type: 'google',
model: 'gemini-3-flash-preview',
apiKey: config.googleApiKey,
maxRetries: this.config.maxRetries,
});
}

/**
* Evaluate conventionality complexity for a given text and grade level
*
* @param text - The text to evaluate
* @param grade - The target grade level (3-12)
* @returns Evaluation result with complexity score and detailed analysis
* @throws {ValidationError} If text is empty, too short/long, or grade is invalid
* @throws {APIError} If LLM API calls fail (includes AuthenticationError, RateLimitError, NetworkError, TimeoutError)
*/
async evaluate(
text: string,
grade: string
): Promise<EvaluationResult<TextComplexityLevel, ConventionalityInternal>> {
this.logger.info('Starting Conventionality evaluation', {
evaluator: 'conventionality',
operation: 'evaluate',
grade,
textLength: text.length,
});

const startTime = Date.now();
const stageDetails: StageDetail[] = [];

try {
// Validate inputs — inside try so validation errors are telemetered.
this.validateText(text);
this.validateGrade(grade, new Set(ConventionalityEvaluator.metadata.supportedGrades));

this.logger.debug('Evaluating conventionality complexity', {
evaluator: 'conventionality',
operation: 'conventionality_evaluation',
});

const fkScore = calculateFleschKincaidGrade(text);
const response = await this.evaluateConventionality(text, grade, fkScore);

stageDetails.push({
stage: 'conventionality_evaluation',
provider: 'google:gemini-3-flash-preview',
latency_ms: response.latencyMs,
token_usage: {
input_tokens: response.usage.inputTokens,
output_tokens: response.usage.outputTokens,
},
});

const latencyMs = Date.now() - startTime;

// Aggregate token usage
const totalTokenUsage = {
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0),
};

const result = {
score: response.data.complexity_score,
reasoning: response.data.reasoning,
metadata: {
model: 'google:gemini-3-flash-preview',
processingTimeMs: latencyMs,
},
_internal: response.data,
};

// Send success telemetry (fire-and-forget)
this.sendTelemetry({
status: 'success',
latencyMs,
textLength: text.length,
grade,
provider: 'google:gemini-3-flash-preview',
tokenUsage: totalTokenUsage,
metadata: {
stage_details: stageDetails,
},
inputText: text,
}).catch(() => {
// Ignore telemetry errors
});

this.logger.info('Conventionality evaluation completed successfully', {
evaluator: 'conventionality',
operation: 'evaluate',
grade,
score: result.score,
processingTimeMs: latencyMs,
});

return result;
} catch (error) {
const latencyMs = Date.now() - startTime;

this.logger.error('Conventionality evaluation failed', {
evaluator: 'conventionality',
operation: 'evaluate',
grade,
error: error instanceof Error ? error : undefined,
processingTimeMs: latencyMs,
completedStages: stageDetails.length,
});

const totalTokenUsage = stageDetails.length > 0 ? {
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0),
} : undefined;

this.sendTelemetry({
status: 'error',
latencyMs,
textLength: text.length,
grade,
provider: 'google:gemini-3-flash-preview',
tokenUsage: totalTokenUsage,
errorCode: error instanceof Error ? error.name : 'UnknownError',
metadata: stageDetails.length > 0 ? { stage_details: stageDetails } : undefined,
inputText: text,
}).catch(() => {
// Ignore telemetry errors
});

if (error instanceof ValidationError) {
throw error;
}

throw wrapProviderError(error, 'Conventionality evaluation failed');
}
}

/**
* Run the Conventionality evaluation LLM call
*/
private async evaluateConventionality(
text: string,
grade: string,
fkScore: number
): Promise<{ data: ConventionalityInternal; usage: { inputTokens: number; outputTokens: number }; latencyMs: number }> {
const response = await this.provider.generateStructured({
messages: [
{ role: 'system', content: getSystemPrompt() },
{ role: 'user', content: getUserPrompt(text, grade, fkScore) },
],
schema: ConventionalityOutputSchema,
temperature: 0,
});

return {
data: response.data,
usage: response.usage,
latencyMs: response.latencyMs,
};
}
}

/**
* Functional API for Conventionality evaluation
*
* @example
* ```typescript
* const result = await evaluateConventionality(
* "The author uses sustained irony to critique societal norms.",
* "10",
* { googleApiKey: process.env.GOOGLE_API_KEY }
* );
* ```
*/
export async function evaluateConventionality(
text: string,
grade: string,
config: BaseEvaluatorConfig
): Promise<EvaluationResult<TextComplexityLevel, ConventionalityInternal>> {
const evaluator = new ConventionalityEvaluator(config);
return evaluator.evaluate(text, grade);
}
5 changes: 5 additions & 0 deletions sdks/typescript/src/evaluators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export {
evaluateSmk,
} from './smk.js';

export {
ConventionalityEvaluator,
evaluateConventionality,
} from './conventionality.js';

export {
TextComplexityEvaluator,
evaluateTextComplexity,
Expand Down
Loading
Loading