File size: 4,063 Bytes
e706de2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
import { BaseOutputParser, OutputParserException } from './base-parser.js';
/**
* Parser with full schema validation
*
* Example:
* const parser = new StructuredOutputParser({
* responseSchemas: [
* {
* name: "sentiment",
* type: "string",
* description: "The sentiment (positive/negative/neutral)",
* enum: ["positive", "negative", "neutral"]
* },
* {
* name: "confidence",
* type: "number",
* description: "Confidence score between 0 and 1"
* }
* ]
* });
*/
export class StructuredOutputParser extends BaseOutputParser {
constructor(options = {}) {
super();
this.responseSchemas = options.responseSchemas || [];
}
/**
* Parse and validate against schema
*/
async parse(text) {
try {
// Extract JSON
const jsonText = this._extractJson(text);
const parsed = JSON.parse(jsonText);
// Validate against schema
this._validateAgainstSchema(parsed);
return parsed;
} catch (error) {
throw new OutputParserException(
`Failed to parse structured output: ${error.message}`,
text,
error
);
}
}
/**
* Extract JSON from text (same as JsonOutputParser)
*/
_extractJson(text) {
try {
JSON.parse(text.trim());
return text.trim();
} catch {}
const markdownMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
if (markdownMatch) return markdownMatch[1].trim();
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) return jsonMatch[0];
return text.trim();
}
/**
* Validate parsed data against schema
*/
_validateAgainstSchema(parsed) {
for (const schema of this.responseSchemas) {
const { name, type, enum: enumValues, required = true } = schema;
// Check required fields
if (required && !(name in parsed)) {
throw new Error(`Missing required field: ${name}`);
}
if (name in parsed) {
const value = parsed[name];
// Check type
if (!this._checkType(value, type)) {
throw new Error(
`Field ${name} should be ${type}, got ${typeof value}`
);
}
// Check enum values
if (enumValues && !enumValues.includes(value)) {
throw new Error(
`Field ${name} must be one of: ${enumValues.join(', ')}`
);
}
}
}
}
/**
* Check if value matches expected type
*/
_checkType(value, type) {
switch (type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'object':
return typeof value === 'object' && value !== null && !Array.isArray(value);
default:
return true;
}
}
/**
* Generate format instructions for LLM
*/
getFormatInstructions() {
const schemaDescriptions = this.responseSchemas.map(schema => {
let desc = `"${schema.name}": ${schema.type}`;
if (schema.description) {
desc += ` // ${schema.description}`;
}
if (schema.enum) {
desc += ` (one of: ${schema.enum.join(', ')})`;
}
return desc;
});
return `Respond with valid JSON matching this schema:
{
${schemaDescriptions.map(d => ' ' + d).join(',\n')}
}`;
}
/**
* Static helper to create from simple schema
*/
static fromNamesAndDescriptions(schemas) {
const responseSchemas = Object.entries(schemas).map(([name, description]) => ({
name,
description,
type: 'string' // Default type
}));
return new StructuredOutputParser({ responseSchemas });
}
} |