File size: 2,668 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
import { BaseOutputParser, OutputParserException } from './base-parser.js';

/**

 * Parser that extracts JSON from LLM output

 * Handles markdown code blocks and extra text

 *

 * Example:

 *   const parser = new JsonOutputParser();

 *   const result = await parser.parse('```json\n{"name": "Alice"}\n```');

 *   // { name: "Alice" }

 */
export class JsonOutputParser extends BaseOutputParser {
  constructor(options = {}) {
    super();
    this.schema = options.schema;
  }

  /**

   * Parse JSON from text

   */
  async parse(text) {
    try {
      // Try to extract JSON from the text
      const jsonText = this._extractJson(text);
      const parsed = JSON.parse(jsonText);

      // Validate against schema if provided
      if (this.schema) {
        this._validateSchema(parsed);
      }

      return parsed;
    } catch (error) {
      throw new OutputParserException(
          `Failed to parse JSON: ${error.message}`,
          text,
          error
      );
    }
  }

  /**

   * Extract JSON from text (handles markdown, extra text)

   */
  _extractJson(text) {
    // Try direct parse first
    try {
      JSON.parse(text.trim());
      return text.trim();
    } catch {
      // Not direct JSON, try to find it
    }

    // Look for JSON in markdown code blocks
    const markdownMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
    if (markdownMatch) {
      return markdownMatch[1].trim();
    }

    // Look for JSON object/array patterns
    const jsonObjectMatch = text.match(/\{[\s\S]*\}/);
    if (jsonObjectMatch) {
      return jsonObjectMatch[0];
    }

    const jsonArrayMatch = text.match(/\[[\s\S]*\]/);
    if (jsonArrayMatch) {
      return jsonArrayMatch[0];
    }

    // Give up, return original
    return text.trim();
  }

  /**

   * Validate parsed JSON against schema

   */
  _validateSchema(parsed) {
    if (!this.schema) return;

    for (const [key, type] of Object.entries(this.schema)) {
      if (!(key in parsed)) {
        throw new Error(`Missing required field: ${key}`);
      }

      const actualType = typeof parsed[key];
      if (actualType !== type) {
        throw new Error(
            `Field ${key} should be ${type}, got ${actualType}`
        );
      }
    }
  }

  getFormatInstructions() {
    let instructions = 'Respond with valid JSON.';

    if (this.schema) {
      const schemaDesc = Object.entries(this.schema)
          .map(([key, type]) => `"${key}": ${type}`)
          .join(', ');
      instructions += ` Schema: { ${schemaDesc} }`;
    }

    return instructions;
  }
}