From 291c1cbd9851c740ce537d3e5d4014600aaac4fb Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:39:19 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20harden=20litigation=20AI=20pipeline=20?= =?UTF-8?q?=E2=80=94=20empty=20response=20guard,=20QC=20false-clear,=20sou?= =?UTF-8?q?rce=20tracing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: callAIGatewayFallback throws on empty AI response instead of returning empty string. QC endpoint validates parsed result is array and surfaces "scan incomplete" warning on parse failure. All synthesis/draft response paths now include source field for traceability. Frontend: LitigationAssistant surfaces QC warning field — shows "Scan Incomplete" with retry button instead of false "Clear" when AI output was unparseable. Co-Authored-By: Claude Opus 4.6 --- src/routes/litigation.ts | 38 +++++++++++++++++++--------- ui/src/pages/LitigationAssistant.tsx | 18 ++++++++++++- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/routes/litigation.ts b/src/routes/litigation.ts index 4964a9a..3889dcd 100644 --- a/src/routes/litigation.ts +++ b/src/routes/litigation.ts @@ -46,9 +46,9 @@ litigationRoutes.post('/synthesize', async (c) => { if (result) { if (!result.aiEnabled) { - return c.json({ synthesis: rawNotes, passthrough: true }); + return c.json({ synthesis: rawNotes, passthrough: true, source: 'passthrough' }); } - return c.json({ synthesis: result.result }); + return c.json({ synthesis: result.result, source: 'chittyconnect' }); } console.warn('[litigation/synthesize] ChittyConnect execute failed, falling back to direct AI'); } @@ -59,7 +59,7 @@ litigationRoutes.post('/synthesize', async (c) => { FALLBACK_SYNTHESIZE_PROMPT, `Raw notes:\n${rawNotes}${property ? `\nProperty: ${property}` : ''}${caseNumber ? `\nCase: ${caseNumber}` : ''}`, ); - return c.json({ synthesis: result }); + return c.json({ synthesis: result, source: 'ai-gateway-fallback' }); } catch (err) { console.error('[litigation/synthesize]', err instanceof Error ? err.message : err); return c.json({ error: 'AI synthesis failed. Please try again.' }, 502); @@ -182,9 +182,9 @@ litigationRoutes.post('/draft', async (c) => { if (result) { if (!result.aiEnabled) { - return c.json({ draft: synthesizedFacts, passthrough: true }); + return c.json({ draft: synthesizedFacts, passthrough: true, source: 'passthrough' }); } - return c.json({ draft: result.result }); + return c.json({ draft: result.result, source: 'chittyconnect' }); } console.warn('[litigation/draft] ChittyConnect execute failed, falling back to direct AI'); } @@ -194,7 +194,7 @@ litigationRoutes.post('/draft', async (c) => { FALLBACK_DRAFT_PROMPT.replace('{{recipient}}', recipient).replace('{{focus}}', focus), `Synthesized facts:\n${synthesizedFacts}`, ); - return c.json({ draft: result }); + return c.json({ draft: result, source: 'ai-gateway-fallback' }); } catch (err) { console.error('[litigation/draft]', err instanceof Error ? err.message : err); return c.json({ error: 'AI drafting failed. Please try again.' }, 502); @@ -223,13 +223,19 @@ litigationRoutes.post('/qc', async (c) => { if (result) { if (!result.aiEnabled) { - return c.json({ flags: [], passthrough: true }); + return c.json({ flags: [], passthrough: true, warning: 'AI not enabled — QC scan skipped' }); } try { const cleaned = result.result.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim(); - return c.json({ flags: JSON.parse(cleaned) }); - } catch { - return c.json({ flags: [], warning: 'QC analysis returned non-parseable results' }); + const flags = JSON.parse(cleaned); + if (!Array.isArray(flags)) { + console.warn('[litigation/qc] Parsed result is not an array:', typeof flags); + return c.json({ flags: [], warning: 'QC analysis returned unexpected format — scan incomplete' }); + } + return c.json({ flags }); + } catch (parseErr) { + console.error('[litigation/qc] Parse failed:', parseErr instanceof Error ? parseErr.message : parseErr); + return c.json({ flags: [], warning: 'QC analysis returned non-parseable results — scan incomplete' }); } } console.warn('[litigation/qc] ChittyConnect execute failed, falling back to direct AI'); @@ -242,11 +248,15 @@ litigationRoutes.post('/qc', async (c) => { ); const cleaned = result.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim(); const flags = JSON.parse(cleaned); + if (!Array.isArray(flags)) { + console.warn('[litigation/qc] Fallback parsed result is not an array:', typeof flags); + return c.json({ flags: [], warning: 'QC analysis returned unexpected format — scan incomplete' }); + } return c.json({ flags }); } catch (err) { console.error('[litigation/qc]', err instanceof Error ? err.message : err); if (err instanceof SyntaxError) { - return c.json({ flags: [], warning: 'QC analysis returned non-parseable results' }); + return c.json({ flags: [], warning: 'QC analysis returned non-parseable results — scan incomplete' }); } return c.json({ error: 'AI QC scan failed. Please try again.' }, 502); } @@ -294,7 +304,11 @@ async function callAIGatewayFallback( choices?: { message?: { content?: string } }[]; }; - return result.choices?.[0]?.message?.content || ''; + const content = result.choices?.[0]?.message?.content || ''; + if (!content.trim()) { + throw new Error('AI gateway returned empty response'); + } + return content; } // ── Fallback prompts (used until ChittyConnect prompt registry is seeded) ── diff --git a/ui/src/pages/LitigationAssistant.tsx b/ui/src/pages/LitigationAssistant.tsx index efaae1d..01e6a69 100644 --- a/ui/src/pages/LitigationAssistant.tsx +++ b/ui/src/pages/LitigationAssistant.tsx @@ -36,6 +36,7 @@ export function LitigationAssistant() { const [synthesis, setSynthesis] = useState(''); const [draft, setDraft] = useState(''); const [flags, setFlags] = useState([]); + const [qcWarning, setQcWarning] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); @@ -95,6 +96,7 @@ export function LitigationAssistant() { setSynthesis(''); setDraft(''); setFlags([]); + setQcWarning(null); try { const res = await api.litigationSynthesize({ rawNotes, property, caseNumber }); @@ -131,6 +133,7 @@ export function LitigationAssistant() { try { const res = await api.litigationQC({ rawNotes, draftEmail: draft }); setFlags(res.flags || []); + setQcWarning(res.warning || null); setStep('scanned'); } catch (err) { setError(err instanceof Error ? err.message : 'QC scan failed'); @@ -384,7 +387,20 @@ export function LitigationAssistant() { Step 4 {step === 'scanned' ? ( - flags.length === 0 ? ( + flags.length === 0 && qcWarning ? ( +
+ +

Scan Incomplete

+

{qcWarning}

+ +
+ ) : flags.length === 0 ? (

Clear