Skip to content

Commit 956493b

Browse files
fix: chat adapter — match Dojo Gateway API format (message + session_id)
Root cause: ChatAdapter sent OpenAI-style { messages: [...] } but the Dojo Gateway expects { message: string, session_id: string, stream: bool }. Fixes: - _checkProxy: sends correct { message, session_id, stream: false } probe - _sendToGateway: sends user message text, not messages array - _proxyAvailable: only true on r.ok (was accepting 400 as "available") - chat.js page component: passes userMessage string, not history array - Session ID generated per page load for Gateway session continuity Verified: "What county has highest uninsured rate?" → "Menominee County, 16.5%" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent eff29d4 commit 956493b

2 files changed

Lines changed: 50 additions & 75 deletions

File tree

cmd/pdi/frontend/lib/chat.js

Lines changed: 47 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,100 @@
1-
// lib/chat.js — SSE streaming adapter for Dojo Gateway.
2-
// Falls back to a helpful placeholder while the proxy endpoint is being wired.
1+
// lib/chat.js — Chat adapter for the Dojo Gateway via /v1/chat proxy.
2+
// The Gateway expects: { message: string, session_id: string, stream: bool }
3+
// NOT the OpenAI-style { messages: [...] } format.
34
const ChatAdapter = {
5+
_sessionId: 'pdi-web-' + Date.now().toString(36),
6+
_proxyAvailable: null,
47

5-
// Placeholder responses shown before the Gateway proxy is connected.
68
_placeholders: [
7-
"The chat interface connects to the Dojo Gateway for AI-powered data analysis. The Gateway proxy endpoint is being wired up — check back soon.",
8-
"While the live gateway connection is being configured, you can explore the data using the Counties, Compare, Evidence, and Analysis tabs.",
9-
"The Policy Data Infrastructure platform tracks 42 indicator variables across 13 federal and state data sources covering all 72 Wisconsin counties and 1,652 census tracts.",
10-
"Try browsing Wisconsin counties at #/counties, or compare two counties side-by-side at #/compare to see detailed indicator differences.",
11-
"The Analysis section (#/analysis) surfaces statistical results including OLS regression coefficients, correlation matrices, and composite disadvantage indices computed across all tracts."
9+
"The chat interface connects to the Dojo Gateway for AI-powered data analysis. Try asking about Wisconsin counties, poverty rates, health outcomes, or policy positions.",
10+
"Try: 'What county has the highest poverty rate?' or 'Compare Dane and Milwaukee counties' or 'Tell me about Francesca Hong's housing policies'",
11+
"The platform tracks 42 indicator variables across 13 data sources covering 72 Wisconsin counties and 1,652 census tracts."
1212
],
1313

14-
// Detect whether the PDI chat proxy endpoint is available.
15-
_proxyAvailable: null,
16-
1714
async _checkProxy() {
1815
if (this._proxyAvailable !== null) return this._proxyAvailable;
1916
try {
2017
const r = await fetch('/v1/chat', {
2118
method: 'POST',
2219
headers: { 'Content-Type': 'application/json' },
23-
body: JSON.stringify({ messages: [{ role: 'user', content: 'ping' }], stream: false }),
24-
signal: AbortSignal.timeout(2000)
20+
body: JSON.stringify({ message: 'ping', session_id: this._sessionId, stream: false }),
21+
signal: AbortSignal.timeout(5000)
2522
});
26-
this._proxyAvailable = r.status !== 404 && r.status !== 501;
23+
// 200 = working, 400 with "message is required" = wrong format, 502 = gateway down
24+
this._proxyAvailable = r.ok;
2725
} catch (_) {
2826
this._proxyAvailable = false;
2927
}
3028
return this._proxyAvailable;
3129
},
3230

33-
// Send messages and stream the response.
34-
// onChunk(text) called with each chunk; onDone() called when complete.
35-
async send(messages, onChunk, onDone) {
31+
// Send a message and stream the response.
32+
// userMessage is the latest user text. conversationHistory is ignored for now
33+
// (the Gateway manages session state via session_id).
34+
async send(userMessage, onChunk, onDone) {
3635
const available = await this._checkProxy();
3736

3837
if (available) {
39-
await this._sendToGateway(messages, onChunk, onDone);
38+
await this._sendToGateway(userMessage, onChunk, onDone);
4039
} else {
41-
await this._sendPlaceholder(messages, onChunk, onDone);
40+
await this._sendPlaceholder(userMessage, onChunk, onDone);
4241
}
4342
},
4443

45-
// Real SSE streaming to the PDI chat proxy (→ Dojo Gateway → Anthropic).
46-
async _sendToGateway(messages, onChunk, onDone) {
44+
async _sendToGateway(userMessage, onChunk, onDone) {
4745
try {
4846
const r = await fetch('/v1/chat', {
4947
method: 'POST',
5048
headers: { 'Content-Type': 'application/json' },
51-
body: JSON.stringify({ messages, stream: true })
49+
body: JSON.stringify({
50+
message: userMessage,
51+
session_id: this._sessionId,
52+
stream: false // Non-streaming for now — gateway returns complete JSON
53+
})
5254
});
5355

5456
if (!r.ok) {
55-
onChunk(`Error: HTTP ${r.status} from chat endpoint.`);
57+
const errBody = await r.text();
58+
onChunk(`Error (${r.status}): ${errBody.substring(0, 200)}`);
5659
onDone();
5760
return;
5861
}
5962

60-
const reader = r.body.getReader();
61-
const decoder = new TextDecoder();
62-
let buffer = '';
63+
const data = await r.json();
64+
// Gateway response: { type: "complete", content: "...", usage: {...} }
65+
const content = data.content || data.message || JSON.stringify(data);
6366

64-
while (true) {
65-
const { done, value } = await reader.read();
66-
if (done) break;
67-
68-
buffer += decoder.decode(value, { stream: true });
69-
const lines = buffer.split('\n');
70-
buffer = lines.pop(); // keep incomplete last line
71-
72-
for (const line of lines) {
73-
if (line.startsWith('data: ')) {
74-
const data = line.slice(6).trim();
75-
if (data === '[DONE]') { onDone(); return; }
76-
try {
77-
const parsed = JSON.parse(data);
78-
// Anthropic SSE delta format
79-
const delta = parsed?.delta?.text
80-
?? parsed?.choices?.[0]?.delta?.content
81-
?? '';
82-
if (delta) onChunk(delta);
83-
} catch (_) {
84-
// non-JSON SSE line — ignore
85-
}
86-
}
87-
}
67+
// Simulate streaming for UX consistency
68+
for (let i = 0; i < content.length; i += 5) {
69+
onChunk(content.substring(i, Math.min(i + 5, content.length)));
70+
await new Promise(resolve => setTimeout(resolve, 10));
8871
}
89-
9072
onDone();
9173
} catch (err) {
9274
onChunk(`Connection error: ${err.message}`);
9375
onDone();
9476
}
9577
},
9678

97-
// Placeholder fallback — simulates streaming for UX consistency.
98-
async _sendPlaceholder(messages, onChunk, onDone) {
99-
const last = messages[messages.length - 1]?.content?.toLowerCase() || '';
79+
async _sendPlaceholder(userMessage, onChunk, onDone) {
80+
const q = (userMessage || '').toLowerCase();
10081
let response;
10182

102-
// Simple keyword routing for slightly smarter placeholder responses.
103-
if (last.includes('county') || last.includes('dane') || last.includes('milwaukee')) {
104-
response = "County-level data is available at #/counties. Each county card shows poverty rate, median household income, and uninsured rate. Click any county for a full profile with grouped indicators by health, housing, food access, and demographics.";
105-
} else if (last.includes('tract') || last.includes('census')) {
106-
response = "Census tract data is available for all 1,652 Wisconsin tracts. Navigate to a county profile and click 'View Tracts' to explore tract-level CDC PLACES health outcomes and USDA food access indicators.";
107-
} else if (last.includes('policy') || last.includes('candidate')) {
108-
response = "The platform tracks 85 policy positions from Wisconsin progressive candidates, crosswalked to equity dimensions. Visit #/candidates to filter by candidate or policy category.";
109-
} else if (last.includes('analysis') || last.includes('regression') || last.includes('correlation')) {
110-
response = "The Analysis section (#/analysis) surfaces computed statistical results: OLS regression models, pairwise correlation matrices, composite disadvantage indices, and tipping point analyses across all 1,652 tracts.";
83+
if (q.includes('county') || q.includes('dane') || q.includes('milwaukee')) {
84+
response = "County-level data is available at #/counties. Each county shows poverty rate, median household income, and uninsured rate from Census ACS 2023. Click any county for a full profile with indicators grouped by health, housing, food access, and demographics.";
85+
} else if (q.includes('tract') || q.includes('census')) {
86+
response = "Census tract data covers 1,652 Wisconsin tracts with CDC PLACES health outcomes (8 indicators) and USDA food access data (6 indicators). Navigate to a county profile to explore its tracts.";
87+
} else if (q.includes('policy') || q.includes('candidate') || q.includes('hong') || q.includes('mamdani')) {
88+
response = "The platform tracks 85 policy positions from Francesca Hong (WI Governor candidate, DSA) and Zohran Mamdani (NYC Mayor, DSA). Visit #/candidates to browse and filter.";
89+
} else if (q.includes('poverty') || q.includes('income') || q.includes('rate')) {
90+
response = "The average poverty rate across Wisconsin's 72 counties is 10.5% (Census ACS 2023). Menominee County has the highest rate. Browse all counties at #/counties or compare two at #/compare.";
11191
} else {
112-
const idx = Math.abs(last.length) % this._placeholders.length;
113-
response = this._placeholders[idx];
92+
response = this._placeholders[Math.floor(Math.random() * this._placeholders.length)];
11493
}
11594

116-
// Simulate streaming at ~50 chars/sec.
117-
for (let i = 0; i < response.length; i += 3) {
118-
onChunk(response.substring(i, Math.min(i + 3, response.length)));
119-
await new Promise(r => setTimeout(r, 20));
95+
for (let i = 0; i < response.length; i += 4) {
96+
onChunk(response.substring(i, Math.min(i + 4, response.length)));
97+
await new Promise(resolve => setTimeout(resolve, 15));
12098
}
12199
onDone();
122100
}

cmd/pdi/frontend/pages/chat.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,10 @@ document.addEventListener('alpine:init', () => {
2929
this.$nextTick(() => this._scrollToBottom());
3030

3131
try {
32-
// Build messages array for the adapter (exclude the empty placeholder).
33-
const history = this.messages
34-
.filter(m => m.id !== assistantId && m.id !== 'sys-welcome')
35-
.map(m => ({ role: m.role, content: m.content }));
36-
32+
// Send the latest user message text to the adapter.
33+
// The Gateway manages session state via session_id.
3734
await ChatAdapter.send(
38-
history,
35+
text,
3936
(chunk) => {
4037
const msg = this.messages.find(m => m.id === assistantId);
4138
if (msg) {

0 commit comments

Comments
 (0)