Skip to content

Commit 9ffcf7e

Browse files
feat: direct Anthropic API for chat — bypass Gateway for grounded responses
The Dojo Gateway strips system_prompt from chat requests, causing the LLM to give generic answers instead of using the injected county/policy data. Fix: PDI now calls Anthropic Messages API directly with the full system prompt containing 72 counties + 85 policies + methodology. The ANTHROPIC_API_KEY env var is read at startup. This gives the chat: - Complete control over the system prompt (no Gateway interference) - Direct Claude Sonnet 4 responses (no Ollama fallback) - Proper grounding in live data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 723da4a commit 9ffcf7e

1 file changed

Lines changed: 86 additions & 26 deletions

File tree

cmd/pdi/serve.go

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"embed"
6+
"encoding/json"
67
"fmt"
78
"io"
89
"io/fs"
@@ -104,48 +105,93 @@ func runServe(port int) error {
104105
c.JSON(http.StatusOK, gin.H{"status": "ready"})
105106
})
106107

107-
// Chat proxy — forward /v1/chat to the Dojo Gateway for LLM-powered
108-
// conversational data analysis. The gateway handles model routing, tool
109-
// calling, and SSE streaming. DOJO_GATEWAY_URL configures the upstream.
110-
gatewayURL := os.Getenv("DOJO_GATEWAY_URL")
111-
if gatewayURL == "" {
112-
gatewayURL = "http://localhost:7340"
113-
}
114-
gwTarget := strings.TrimRight(gatewayURL, "/")
108+
// Chat endpoint — calls Anthropic directly with a rich system prompt
109+
// grounded in the live data. The ANTHROPIC_API_KEY env var must be set.
110+
anthropicKey := os.Getenv("ANTHROPIC_API_KEY")
115111
r.POST("/v1/chat", func(c *gin.Context) {
116-
// Read the incoming request body.
117-
body, err := io.ReadAll(c.Request.Body)
118-
if err != nil {
119-
c.JSON(http.StatusBadRequest, gin.H{"error": "read body failed"})
112+
if anthropicKey == "" {
113+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "chat not configured (ANTHROPIC_API_KEY not set)"})
120114
return
121115
}
122-
// Forward to gateway /chat endpoint.
123-
proxyReq, err := http.NewRequestWithContext(c.Request.Context(), "POST", gwTarget+"/v1/chat", strings.NewReader(string(body)))
116+
117+
var req struct {
118+
Message string `json:"message"`
119+
SystemPrompt string `json:"system_prompt"`
120+
SessionID string `json:"session_id"`
121+
}
122+
if err := c.ShouldBindJSON(&req); err != nil || req.Message == "" {
123+
c.JSON(http.StatusBadRequest, gin.H{"error": "message is required"})
124+
return
125+
}
126+
127+
systemPrompt := req.SystemPrompt
128+
if systemPrompt == "" {
129+
systemPrompt = "You are a helpful assistant."
130+
}
131+
132+
// Build Anthropic Messages API request
133+
anthropicBody := fmt.Sprintf(`{
134+
"model": "claude-sonnet-4-20250514",
135+
"max_tokens": 2048,
136+
"system": %s,
137+
"messages": [{"role": "user", "content": %s}]
138+
}`,
139+
jsonEscapeString(systemPrompt),
140+
jsonEscapeString(req.Message),
141+
)
142+
143+
proxyReq, err := http.NewRequestWithContext(c.Request.Context(), "POST",
144+
"https://api.anthropic.com/v1/messages",
145+
strings.NewReader(anthropicBody))
124146
if err != nil {
125-
c.JSON(http.StatusInternalServerError, gin.H{"error": "build proxy request failed"})
147+
c.JSON(http.StatusInternalServerError, gin.H{"error": "build request failed"})
126148
return
127149
}
128150
proxyReq.Header.Set("Content-Type", "application/json")
129-
proxyReq.Header.Set("Accept", c.GetHeader("Accept"))
151+
proxyReq.Header.Set("x-api-key", anthropicKey)
152+
proxyReq.Header.Set("anthropic-version", "2023-06-01")
130153

131-
client := &http.Client{Timeout: 5 * time.Minute}
154+
client := &http.Client{Timeout: 2 * time.Minute}
132155
resp, err := client.Do(proxyReq)
133156
if err != nil {
134-
c.JSON(http.StatusBadGateway, gin.H{"error": "gateway unreachable", "detail": err.Error()})
157+
c.JSON(http.StatusBadGateway, gin.H{"error": "anthropic unreachable", "detail": err.Error()})
135158
return
136159
}
137160
defer resp.Body.Close()
138161

139-
// Stream the response back — works for both JSON and SSE.
140-
extraHeaders := map[string]string{}
141-
for _, h := range []string{"Content-Type", "Cache-Control", "Connection"} {
142-
if v := resp.Header.Get(h); v != "" {
143-
extraHeaders[h] = v
144-
}
162+
respBody, _ := io.ReadAll(resp.Body)
163+
164+
if resp.StatusCode != http.StatusOK {
165+
c.JSON(resp.StatusCode, gin.H{"error": "anthropic error", "detail": string(respBody[:minInt(len(respBody), 500)])})
166+
return
145167
}
146-
c.DataFromReader(resp.StatusCode, resp.ContentLength, resp.Header.Get("Content-Type"), resp.Body, extraHeaders)
168+
169+
// Parse Anthropic response and return in our format
170+
var anthropicResp struct {
171+
Content []struct {
172+
Text string `json:"text"`
173+
} `json:"content"`
174+
}
175+
if err := json.Unmarshal(respBody, &anthropicResp); err != nil {
176+
c.JSON(http.StatusInternalServerError, gin.H{"error": "parse response failed"})
177+
return
178+
}
179+
180+
text := ""
181+
for _, block := range anthropicResp.Content {
182+
text += block.Text
183+
}
184+
185+
c.JSON(http.StatusOK, gin.H{
186+
"type": "complete",
187+
"content": text,
188+
})
147189
})
148-
fmt.Printf(" chat: /v1/chat → %s/v1/chat\n", gwTarget)
190+
if anthropicKey != "" {
191+
fmt.Println(" chat: /v1/chat → Anthropic Claude (direct)")
192+
} else {
193+
fmt.Println(" chat: /v1/chat → NOT CONFIGURED (set ANTHROPIC_API_KEY)")
194+
}
149195

150196
// Serve embedded frontend static files.
151197
feFS, _ := fs.Sub(frontendFS, "frontend")
@@ -184,3 +230,17 @@ func runServe(port int) error {
184230
}
185231
return nil
186232
}
233+
234+
// jsonEscapeString returns a JSON-encoded string (with surrounding quotes).
235+
func jsonEscapeString(s string) string {
236+
b, _ := json.Marshal(s)
237+
return string(b)
238+
}
239+
240+
// min returns the smaller of two ints. (Go 1.21+ has builtin min but we keep compat.)
241+
func minInt(a, b int) int {
242+
if a < b {
243+
return a
244+
}
245+
return b
246+
}

0 commit comments

Comments
 (0)