@@ -3,6 +3,7 @@ package main
33import (
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