Skip to content

Commit 5a61c21

Browse files
committed
fix: Add response transformer to handle AI schema mismatch in Generate feature
1 parent bc50772 commit 5a61c21

3 files changed

Lines changed: 138 additions & 22 deletions

File tree

‎src/lib/ai/client.ts‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,86 @@ async function callSupabaseAI(
9191
return data.output;
9292
}
9393

94+
/**
95+
* Transforms AI generate response from various formats to the expected GenerateOutput schema.
96+
* This handles cases where the AI returns a different structure than requested.
97+
*/
98+
function transformGenerateResponse(response: Record<string, unknown>): GenerateOutput {
99+
// If already in correct format, return as-is
100+
if (response.caseStructure && response.contentions && response.predictedOpponentCase && response.counters && response.metadata) {
101+
return response as unknown as GenerateOutput;
102+
}
103+
104+
// Transform from wrong format to correct format
105+
const wrongFormatContentions = response.contentions as Array<{
106+
title?: string;
107+
claim?: string;
108+
points?: string[];
109+
evidence?: string[] | string;
110+
analysis?: string;
111+
reasoning?: string;
112+
impact?: string;
113+
}> || [];
114+
115+
const transformedContentions = wrongFormatContentions.map((c, i) => ({
116+
id: String(i + 1),
117+
claim: c.claim || c.title || 'Argument',
118+
reasoning: c.reasoning || c.analysis || (Array.isArray(c.points) ? c.points.join(' ') : ''),
119+
evidence: Array.isArray(c.evidence) ? c.evidence.join(' ') : (c.evidence || ''),
120+
needsSource: false,
121+
sourceQuery: '',
122+
impact: c.impact || c.analysis || '',
123+
}));
124+
125+
// Transform rebuttals to counters
126+
const rebuttals = (response.rebuttals as Array<{ attack?: string; response?: string }>) || [];
127+
const counters = rebuttals.map(r => ({
128+
targetArgument: r.attack || '',
129+
counterpoint: r.response || '',
130+
rebuttal: r.response || '',
131+
}));
132+
133+
// Transform potentialWeaknesses to predictedOpponentCase
134+
const weaknesses = (response.potentialWeaknesses as string[]) || [];
135+
136+
return {
137+
caseStructure: {
138+
thesis: (response.thesis as string) || (response.title as string) || '',
139+
framework: 'General framework',
140+
keyDefinitions: [],
141+
burdenOfProof: 'We must prove that ' + ((response.thesis as string) || '').slice(0, 50),
142+
},
143+
contentions: transformedContentions.length > 0 ? transformedContentions : [{
144+
id: '1',
145+
claim: 'Main argument',
146+
reasoning: 'Reasoning here',
147+
evidence: 'Evidence here',
148+
needsSource: false,
149+
sourceQuery: '',
150+
impact: 'Impact here',
151+
}],
152+
predictedOpponentCase: {
153+
likelyArguments: weaknesses.slice(0, 3),
154+
strategicWeaknesses: weaknesses,
155+
keyClashes: ['Main clash point'],
156+
},
157+
counters: counters.length > 0 ? counters : [{
158+
targetArgument: 'Opponent argument',
159+
counterpoint: 'Our counter',
160+
rebuttal: 'Rebuttal text',
161+
}],
162+
metadata: {
163+
side: (response.side as 'pro' | 'con') || 'pro',
164+
format: 'custom',
165+
generatedAt: new Date().toISOString(),
166+
},
167+
};
168+
}
169+
94170
export async function generateCase(input: GenerateInput, config: ProviderConfig): Promise<GenerateOutput> {
95171
if (config.provider === 'mock') return mockGenerateCase(input.motion, input.side);
96-
return callSupabaseAI('generate', input as Record<string, unknown>, config);
172+
const rawResponse = await callSupabaseAI('generate', input as Record<string, unknown>, config);
173+
return transformGenerateResponse(rawResponse as Record<string, unknown>);
97174
}
98175

99176
export async function attackCase(input: AttackInput, config: ProviderConfig): Promise<AttackOutput> {

‎src/pages/GeneratePage.tsx‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export default function GeneratePage() {
2323
setGenerateLoading(true);
2424
try {
2525
const result = await generateCase(data, config);
26+
console.log('[DEBUG] Generate result:', JSON.stringify(result, null, 2));
27+
console.log('[DEBUG] Has caseStructure:', !!result?.caseStructure);
28+
console.log('[DEBUG] Has contentions:', !!result?.contentions);
29+
console.log('[DEBUG] Has predictedOpponentCase:', !!result?.predictedOpponentCase);
30+
console.log('[DEBUG] Has counters:', !!result?.counters);
2631
setGenerateResult(result);
2732
} catch (error) {
2833
console.error('Failed to generate case:', error);
@@ -41,7 +46,7 @@ export default function GeneratePage() {
4146
// TODO: Implement section-specific regeneration
4247
console.log('Regenerate section:', section);
4348
};
44-
49+
4550
const handleSave = () => {
4651
if (!generateResult) return;
4752
const title = window.prompt("Enter a title for this case:", generateResult.caseStructure.thesis.slice(0, 50));
@@ -53,33 +58,33 @@ export default function GeneratePage() {
5358

5459
const handleDiscuss = async () => {
5560
if (!generateResult) return;
56-
61+
5762
const summary = `I'd like to discuss the case we just generated.
5863
5964
**Thesis:** ${generateResult.caseStructure.thesis}
6065
6166
**Contentions:**
62-
${generateResult.contentions.map((c, i) => `${i+1}. ${c.claim}`).join('\n')}
67+
${generateResult.contentions.map((c, i) => `${i + 1}. ${c.claim}`).join('\n')}
6368
6469
Can we refine this?`;
6570

6671
addAskMessage({ role: 'user', content: summary });
6772
navigate('/ask');
68-
73+
6974
setAskLoading(true);
7075
try {
71-
const history = [...askMessages, { role: 'user', content: summary }].map(m => ({
72-
role: m.role as 'user' | 'assistant',
73-
content: m.content
76+
const history = [...askMessages, { role: 'user', content: summary }].map(m => ({
77+
role: m.role as 'user' | 'assistant',
78+
content: m.content
7479
}));
75-
80+
7681
const result = await askChat(summary, history, 'general', askConfig);
77-
78-
addAskMessage({
79-
role: 'assistant',
82+
83+
addAskMessage({
84+
role: 'assistant',
8085
content: result.response,
8186
// @ts-expect-error: form schema and UI fields diverge
82-
output: result
87+
output: result
8388
});
8489
} catch (error) {
8590
console.error('Failed to discuss result:', error);
@@ -115,8 +120,8 @@ export default function GeneratePage() {
115120
Save Case
116121
</Button>
117122
</div>
118-
<GenerateResults
119-
result={generateResult}
123+
<GenerateResults
124+
result={generateResult}
120125
onRegenerateSection={handleRegenerateSection}
121126
onDiscuss={handleDiscuss}
122127
/>

‎supabase/functions/ai/index.ts‎

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -466,14 +466,48 @@ CRITICAL: The debate format is strictly:
466466
- R2 (6 min): Builds further on R1's arguments but primarily focuses on refuting the opponent.
467467
- Conclusor (3 min): Summarizes the debate and explains why this team wins.
468468
469-
Return a valid JSON object matching this structure:
469+
CRITICAL - STRICT JSON SCHEMA REQUIREMENT:
470+
You MUST return a valid JSON object with EXACTLY these fields. Do NOT use different field names.
471+
Do NOT add extra fields. Do NOT omit any fields. Follow this structure EXACTLY:
472+
470473
{
471-
"caseStructure": { "thesis": "string", "framework": "string", "keyDefinitions": ["string"], "burdenOfProof": "string" },
472-
"contentions": [{ "id": "1", "claim": "string", "reasoning": "string", "evidence": "string", "needsSource": boolean, "sourceQuery": "string", "impact": "string" }],
473-
"predictedOpponentCase": { "likelyArguments": ["string"], "strategicWeaknesses": ["string"], "keyClashes": ["string"] },
474-
"counters": [{ "targetArgument": "string", "counterpoint": "string", "rebuttal": "string" }],
475-
"metadata": { "side": "pro/con", "format": "string", "generatedAt": "ISO string" }
476-
}`;
474+
"caseStructure": {
475+
"thesis": "The main thesis statement for the debate",
476+
"framework": "The framework or lens through which to view the debate",
477+
"keyDefinitions": ["definition1", "definition2"],
478+
"burdenOfProof": "What we must prove to win"
479+
},
480+
"contentions": [
481+
{
482+
"id": "1",
483+
"claim": "The main claim of this contention",
484+
"reasoning": "Why this claim is true",
485+
"evidence": "Supporting evidence",
486+
"needsSource": false,
487+
"sourceQuery": "",
488+
"impact": "Why this matters"
489+
}
490+
],
491+
"predictedOpponentCase": {
492+
"likelyArguments": ["Argument opponent will make"],
493+
"strategicWeaknesses": ["Weakness in our case"],
494+
"keyClashes": ["Main clash points"]
495+
},
496+
"counters": [
497+
{
498+
"targetArgument": "What opponent argument this counters",
499+
"counterpoint": "Our counter to that argument",
500+
"rebuttal": "How to phrase the rebuttal"
501+
}
502+
],
503+
"metadata": {
504+
"side": "pro",
505+
"format": "custom",
506+
"generatedAt": "CURRENT_ISO_TIMESTAMP"
507+
}
508+
}
509+
510+
IMPORTANT: The "side" in metadata must be exactly "pro" or "con". Always include the generatedAt timestamp.`;
477511

478512
const ATTACK_PROMPT = `You are an expert debate judge and coach. Analyze the given debate case and provide a critical breakdown in JSON format.
479513

0 commit comments

Comments
 (0)