mirror of
https://github.com/vee1e/workorder-desk.git
synced 2026-09-01 09:50:13 +00:00
fix(backend): parse triage proposals wrapped in prose or code fences
Free-model routers often answer with surrounding prose, so a strict whole- response JSON parse failed and every triage run ended in error. Extract the first JSON block before parsing, keeping the zod schema gate intact.
This commit is contained in:
parent
f117d78baf
commit
35069fe4ad
2 changed files with 29 additions and 5 deletions
|
|
@ -197,14 +197,26 @@ function buildUserContent(wo: WorkOrderDoc, lastError: string | null): string {
|
||||||
|
|
||||||
type ProposalParse = { ok: true; data: TriageProposal } | { ok: false; message: string };
|
type ProposalParse = { ok: true; data: TriageProposal } | { ok: false; message: string };
|
||||||
|
|
||||||
|
function extractJsonBlock(text: string): string | null {
|
||||||
|
const trimmed = text.trim();
|
||||||
|
const fenced = trimmed.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
|
||||||
|
for (const candidate of [fenced, trimmed]) {
|
||||||
|
const start = candidate.search(/[{[]/);
|
||||||
|
if (start === -1) continue;
|
||||||
|
const open = candidate[start] as '{' | '[';
|
||||||
|
const close = open === '{' ? '}' : ']';
|
||||||
|
const end = candidate.lastIndexOf(close);
|
||||||
|
if (end > start) return candidate.slice(start, end + 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function parseProposal(content: string): ProposalParse {
|
function parseProposal(content: string): ProposalParse {
|
||||||
const cleaned = content
|
const block = extractJsonBlock(content);
|
||||||
.trim()
|
if (!block) return { ok: false, message: 'the response did not contain a JSON object' };
|
||||||
.replace(/^```(?:json)?\s*/i, '')
|
|
||||||
.replace(/```\s*$/, '');
|
|
||||||
let obj: unknown;
|
let obj: unknown;
|
||||||
try {
|
try {
|
||||||
obj = JSON.parse(cleaned);
|
obj = JSON.parse(block);
|
||||||
} catch {
|
} catch {
|
||||||
return { ok: false, message: 'the response was not valid JSON' };
|
return { ok: false, message: 'the response was not valid JSON' };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,18 @@ describe('triage agent', () => {
|
||||||
expect(providerMock.chatComplete).not.toHaveBeenCalled();
|
expect(providerMock.chatComplete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses a proposal wrapped in prose and code fences', async () => {
|
||||||
|
const { wo } = await makeWorkOrder();
|
||||||
|
providerMock.chatComplete.mockResolvedValue(
|
||||||
|
providerResult({ content: `Sure, here you go:\n\`\`\`json\n${proposal()}\n\`\`\`\nHope that helps!` }),
|
||||||
|
);
|
||||||
|
const outcome = await runTriage(wo.id);
|
||||||
|
expect(outcome.outcome).toBe('done');
|
||||||
|
const suggestions = await TriageSuggestion.find({ workOrderId: wo.id });
|
||||||
|
expect(suggestions).toHaveLength(1);
|
||||||
|
expect(suggestions[0]).toMatchObject({ suggestedPriority: 'high' });
|
||||||
|
});
|
||||||
|
|
||||||
it('returns retry when the provider fails transiently and records AI_UNAVAILABLE', async () => {
|
it('returns retry when the provider fails transiently and records AI_UNAVAILABLE', async () => {
|
||||||
const { wo } = await makeWorkOrder();
|
const { wo } = await makeWorkOrder();
|
||||||
providerMock.chatComplete.mockRejectedValue(new providerMock.ProviderError('upstream down', 'network'));
|
providerMock.chatComplete.mockRejectedValue(new providerMock.ProviderError('upstream down', 'network'));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue