Add SchemaLlmTool and TextLlmTool

This commit is contained in:
2026-08-12 20:54:10 +02:00
parent ee4aba83e8
commit 8d468903a4
4 changed files with 143 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
import type { ZodSchema } from 'zod';
import { LlmTool } from './LlmTool';
import type { RunContext } from './RunContext';
export class LlmOutputParseError extends Error {
constructor(
message: string,
readonly raw: string
) {
super(message);
this.name = 'LlmOutputParseError';
}
}
export abstract class SchemaLlmTool<TInput, TOutput> extends LlmTool<TInput, TOutput> {
protected abstract readonly outputSchema: ZodSchema<TOutput>;
protected parseOutput(raw: string, _ctx: RunContext): TOutput {
const json = extractJson(raw);
const result = this.outputSchema.safeParse(json);
if (!result.success) {
throw new LlmOutputParseError(`Output failed schema validation: ${result.error.message}`, raw);
}
return result.data;
}
}
export function extractJson(content: string): unknown {
let text = content.trim();
const fenceStart = text.indexOf('```');
if (fenceStart !== -1) {
text = text.slice(fenceStart);
text = text.replace(/^```json/, '').replace(/^```/, '');
const fenceEnd = text.indexOf('```');
if (fenceEnd !== -1) {
text = text.slice(0, fenceEnd);
}
text = text.trim();
}
const arrayStart = text.indexOf('[');
const objectStart = text.indexOf('{');
const starts = [arrayStart, objectStart].filter((i) => i !== -1);
if (starts.length === 0) {
throw new LlmOutputParseError(`No JSON found in LLM response: ${content}`, content);
}
const start = Math.min(...starts);
const isArray = start === arrayStart;
const end = isArray ? text.lastIndexOf(']') : text.lastIndexOf('}');
if (end === -1 || end <= start) {
throw new LlmOutputParseError(`No JSON found in LLM response: ${content}`, content);
}
try {
return JSON.parse(text.slice(start, end + 1));
} catch (err) {
throw new LlmOutputParseError(`Failed to parse JSON from LLM response: ${String(err)}`, content);
}
}

View File

@@ -0,0 +1,8 @@
import { LlmTool } from './LlmTool';
import type { RunContext } from './RunContext';
export abstract class TextLlmTool<TInput> extends LlmTool<TInput, string> {
protected parseOutput(raw: string, _ctx: RunContext): string {
return raw.trim();
}
}