Merge pull request 'fix(workbench): tolerera platt/inkapslad JSON från lokal modell' (#442) from fix/model-output-tolerance into main
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / security (push) Successful in 20s
Security Scan (SIAX Cloud) / security (push) Successful in 26s
CI (SIAX Cloud) / integration (push) Successful in 40s
CI (SIAX Cloud) / quality (push) Successful in 43s
CI (SIAX Cloud) / sonar (push) Successful in 1m5s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m20s
Deploy to Coolify / deploy (push) Successful in 1m38s
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / security (push) Successful in 20s
Security Scan (SIAX Cloud) / security (push) Successful in 26s
CI (SIAX Cloud) / integration (push) Successful in 40s
CI (SIAX Cloud) / quality (push) Successful in 43s
CI (SIAX Cloud) / sonar (push) Successful in 1m5s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m20s
Deploy to Coolify / deploy (push) Successful in 1m38s
This commit was merged in pull request #442.
This commit is contained in:
@@ -133,31 +133,54 @@ export async function structureDocument(input: {
|
|||||||
try {
|
try {
|
||||||
parsed = JSON.parse(content);
|
parsed = JSON.parse(content);
|
||||||
} catch {
|
} 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)) {
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
return { status: 'needs_review', issues: [{ field: '', code: 'invalid_model_json' }], model };
|
return { status: 'needs_review', issues: [{ field: '', code: 'invalid_model_json' }], model };
|
||||||
}
|
}
|
||||||
const candidate = parsed as { value?: unknown; evidence?: unknown };
|
const candidate = parsed as Record<string, unknown>;
|
||||||
const asObject =
|
const fieldNames = new Set(snapshot.fields.map((field) => field.name));
|
||||||
candidate.value && typeof candidate.value === 'object' && !Array.isArray(candidate.value)
|
// Accept the documented {value, evidence} envelope, a flat object of the
|
||||||
? (candidate.value as Record<string, unknown>)
|
// requested fields, or {value: {...}} with extra prose keys.
|
||||||
: undefined;
|
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 };
|
if (!asObject) return { status: 'needs_review', issues: [{ field: '', code: 'invalid_output' }], model };
|
||||||
|
|
||||||
// 1. Trust model-supplied evidence only if it validates as-is.
|
// 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({
|
const supplied = validateStructuredOutput({
|
||||||
fields: snapshot.fields,
|
fields: snapshot.fields,
|
||||||
sources: snapshot.sources,
|
sources: snapshot.sources,
|
||||||
value: asObject,
|
value: asObject,
|
||||||
evidence: candidate.evidence as FieldEvidence[],
|
evidence: (candidate as { evidence?: unknown }).evidence as FieldEvidence[],
|
||||||
});
|
});
|
||||||
if (supplied.status === 'mechanically_validated') {
|
if (supplied.status === 'mechanically_validated') {
|
||||||
return {
|
return {
|
||||||
status: 'mechanically_validated',
|
status: 'mechanically_validated',
|
||||||
value: asObject,
|
value: asObject,
|
||||||
evidence: candidate.evidence as FieldEvidence[],
|
evidence: (candidate as { evidence?: unknown }).evidence as FieldEvidence[],
|
||||||
model,
|
model,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -169,7 +192,9 @@ export async function structureDocument(input: {
|
|||||||
fields: snapshot.fields,
|
fields: snapshot.fields,
|
||||||
sources: snapshot.sources,
|
sources: snapshot.sources,
|
||||||
value: asObject,
|
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 };
|
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' },
|
{ 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 () => {
|
it('refuses ambiguous or absent values when computing evidence itself', async () => {
|
||||||
const twice = await structureDocument({
|
const twice = await structureDocument({
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
Reference in New Issue
Block a user