fix(workbench): tolerera platt/inkapslad JSON från lokal modell #442
@@ -133,31 +133,54 @@ export async function structureDocument(input: {
|
||||
try {
|
||||
parsed = JSON.parse(content);
|
||||
} catch {
|
||||
return { status: 'needs_review', issues: [{ field: '', code: 'invalid_model_json' }], model };
|
||||
// Small models often wrap JSON in a markdown fence or add prose.
|
||||
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(content);
|
||||
let inner = fenced ? fenced[1]!.trim() : '';
|
||||
if (!inner) {
|
||||
const start = content.indexOf('{');
|
||||
const end = content.lastIndexOf('}');
|
||||
inner = start >= 0 && end > start ? content.slice(start, end + 1) : '';
|
||||
}
|
||||
try {
|
||||
parsed = inner ? JSON.parse(inner) : undefined;
|
||||
} catch {
|
||||
parsed = undefined;
|
||||
}
|
||||
if (parsed === undefined) {
|
||||
return { status: 'needs_review', issues: [{ field: '', code: 'invalid_model_json' }], model };
|
||||
}
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return { status: 'needs_review', issues: [{ field: '', code: 'invalid_model_json' }], model };
|
||||
}
|
||||
const candidate = parsed as { value?: unknown; evidence?: unknown };
|
||||
const asObject =
|
||||
candidate.value && typeof candidate.value === 'object' && !Array.isArray(candidate.value)
|
||||
? (candidate.value as Record<string, unknown>)
|
||||
: undefined;
|
||||
const candidate = parsed as Record<string, unknown>;
|
||||
const fieldNames = new Set(snapshot.fields.map((field) => field.name));
|
||||
// Accept the documented {value, evidence} envelope, a flat object of the
|
||||
// requested fields, or {value: {...}} with extra prose keys.
|
||||
let asObject: Record<string, unknown> | undefined;
|
||||
if (candidate.value && typeof candidate.value === 'object' && !Array.isArray(candidate.value)) {
|
||||
asObject = candidate.value as Record<string, unknown>;
|
||||
} else if (
|
||||
Object.keys(candidate).length > 0 &&
|
||||
Object.keys(candidate).every((key) => fieldNames.has(key))
|
||||
) {
|
||||
asObject = candidate;
|
||||
}
|
||||
if (!asObject) return { status: 'needs_review', issues: [{ field: '', code: 'invalid_output' }], model };
|
||||
|
||||
// 1. Trust model-supplied evidence only if it validates as-is.
|
||||
if (Array.isArray(candidate.evidence)) {
|
||||
if (Array.isArray((candidate as { evidence?: unknown }).evidence)) {
|
||||
const supplied = validateStructuredOutput({
|
||||
fields: snapshot.fields,
|
||||
sources: snapshot.sources,
|
||||
value: asObject,
|
||||
evidence: candidate.evidence as FieldEvidence[],
|
||||
evidence: (candidate as { evidence?: unknown }).evidence as FieldEvidence[],
|
||||
});
|
||||
if (supplied.status === 'mechanically_validated') {
|
||||
return {
|
||||
status: 'mechanically_validated',
|
||||
value: asObject,
|
||||
evidence: candidate.evidence as FieldEvidence[],
|
||||
evidence: (candidate as { evidence?: unknown }).evidence as FieldEvidence[],
|
||||
model,
|
||||
};
|
||||
}
|
||||
@@ -169,7 +192,9 @@ export async function structureDocument(input: {
|
||||
fields: snapshot.fields,
|
||||
sources: snapshot.sources,
|
||||
value: asObject,
|
||||
evidence: Array.isArray(candidate.evidence) ? (candidate.evidence as FieldEvidence[]) : [],
|
||||
evidence: Array.isArray((candidate as { evidence?: unknown }).evidence)
|
||||
? ((candidate as { evidence?: unknown }).evidence as FieldEvidence[])
|
||||
: [],
|
||||
});
|
||||
return { status: 'needs_review', issues: checked.issues, model };
|
||||
}
|
||||
|
||||
@@ -122,6 +122,20 @@ describe('structure document workflow', () => {
|
||||
{ field: 'name', sourceId: 'page1', start: 6, end: 10, transform: 'exact' },
|
||||
]);
|
||||
});
|
||||
it('accepts a flat object or a fenced JSON object from a small local model', async () => {
|
||||
const flat = await structureDocument({
|
||||
...base,
|
||||
gateway: { complete: vi.fn(async () => ({ ...response, content: JSON.stringify({ name: 'Acme' }) })) },
|
||||
});
|
||||
expect(flat).toMatchObject({ status: 'mechanically_validated', value: { name: 'Acme' } });
|
||||
const fenced = await structureDocument({
|
||||
...base,
|
||||
gateway: {
|
||||
complete: vi.fn(async () => ({ ...response, content: '```json\n{"value":{"name":"Acme"}}\n```' })),
|
||||
},
|
||||
});
|
||||
expect(fenced).toMatchObject({ status: 'mechanically_validated', value: { name: 'Acme' } });
|
||||
});
|
||||
it('refuses ambiguous or absent values when computing evidence itself', async () => {
|
||||
const twice = await structureDocument({
|
||||
...base,
|
||||
|
||||
Reference in New Issue
Block a user