diff --git a/bot.db b/bot.db
index baa2994..25fb841 100644
Binary files a/bot.db and b/bot.db differ
diff --git a/handlers/dashboard.go b/handlers/dashboard.go
index f9f4947..f5d1cb2 100644
--- a/handlers/dashboard.go
+++ b/handlers/dashboard.go
@@ -60,29 +60,6 @@ func ShowDashboard(c *gin.Context) {
})
}
-// Test OpenRouter via the Dashboard
-func TestAIHandler(c *gin.Context) {
- var body struct {
- Prompt string `json:"prompt"`
- }
- if err := c.BindJSON(&body); err != nil {
- c.JSON(400, gin.H{"response": "Invalid request, dummy."})
- return
- }
-
- // Calling the service we wrote earlier
- response, err := services.GetAIResponse([]services.Message{
- {Role: "user", Content: body.Prompt},
- })
-
- if err != nil {
- c.JSON(500, gin.H{"response": "AI Error: " + err.Error()})
- return
- }
-
- c.JSON(200, gin.H{"response": response})
-}
-
// Add this to handlers/dashboard.go
func CreateAppointmentHandler(c *gin.Context) {
var body struct {
@@ -172,25 +149,38 @@ func PostMessageHandler(c *gin.Context) {
var body struct {
Content string `json:"content"`
}
- c.BindJSON(&body)
+ if err := c.BindJSON(&body); err != nil {
+ c.Status(400)
+ return
+ }
- // 1. Save User Message
+ // 1. Save User Message to DB first
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)", chatId, body.Content)
- // 2. Fetch history for AI
+ // 2. Fetch history
rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", chatId)
var history []services.Message
+ defer rows.Close()
for rows.Next() {
var m services.Message
rows.Scan(&m.Role, &m.Content)
history = append(history, m)
}
- // 3. Get AI Response
- aiResp, _ := services.GetAIResponse(history)
+ // 3. Set Headers for Streaming
+ c.Writer.Header().Set("Content-Type", "text/event-stream")
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ c.Writer.Header().Set("Connection", "keep-alive")
+ c.Writer.Header().Set("Transfer-Encoding", "chunked")
- // 4. Save Assistant Message
- db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatId, aiResp)
+ // 4. Call the Stream Service
+ // We pass a function that writes chunks directly to the HTTP response
+ fullResponse, _ := services.StreamAIResponse(history, func(chunk string) {
+ c.Writer.Write([]byte(chunk))
+ c.Writer.Flush() // Important: Send it NOW, don't buffer
+ })
- c.JSON(200, gin.H{"response": aiResp})
+ // 5. Save the FULL Assistant Message to DB for history
+ // We do this AFTER the stream finishes so the next load has the full text
+ db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatId, fullResponse)
}
diff --git a/main.go b/main.go
index 0819269..6718443 100644
--- a/main.go
+++ b/main.go
@@ -28,7 +28,7 @@ func main() {
r.GET("/webhook", handlers.VerifyWebhook)
r.POST("/webhook", handlers.HandleMessage)
r.GET("/dashboard", handlers.ShowDashboard)
- r.POST("/admin/test-ai", handlers.TestAIHandler)
+
r.DELETE("/admin/appointment/:id", handlers.DeleteAppointmentHandler)
r.POST("/admin/appointment", handlers.CreateAppointmentHandler)
r.PUT("/admin/appointment/:id", handlers.UpdateAppointmentHandler)
diff --git a/services/openrouter.go b/services/openrouter.go
index a6aa78f..485955b 100644
--- a/services/openrouter.go
+++ b/services/openrouter.go
@@ -1,12 +1,14 @@
package services
import (
- bytes "bytes"
- json "encoding/json"
- io "io"
- http "net/http"
- os "os"
- "whatsapp-bot/db" // Ensure this matches your module name
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "whatsapp-bot/db"
)
type Message struct {
@@ -14,9 +16,10 @@ type Message struct {
Content string `json:"content"`
}
-type OpenRouterResponse struct {
+// Structs to parse the incoming stream from OpenRouter
+type StreamResponse struct {
Choices []struct {
- Message struct {
+ Delta struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
@@ -26,37 +29,40 @@ type OpenRouterResponse struct {
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
- } `json:"message"`
+ } `json:"delta"`
+ FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
-func GetAIResponse(chatHistory []Message) (string, error) {
+// StreamAIResponse handles the streaming connection
+// onToken: a function that gets called every time we get text (we use this to push to the browser)
+// Returns: the full final string (so we can save it to the DB)
+func StreamAIResponse(chatHistory []Message, onToken func(string)) (string, error) {
apiKey := os.Getenv("OPENROUTER_API_KEY")
url := "https://openrouter.ai/api/v1/chat/completions"
- // 1. Build the full message list with a System Prompt
fullMessages := append([]Message{
{
Role: "system",
- Content: "You are a helpful Chilean business assistant. You can book appointments. If a user wants to schedule something, use the create_appointment tool. Always be concise and polite.",
+ Content: "You are a helpful business assistant. You can book appointments. If the user mentions scheduling an appointment always ask for his phone number and the time and date of the appointment, if the user wants to schedule something before doing it, it is requiered to have a phone number and a date, use the create_appointment tool. Be concise and polite.",
},
}, chatHistory...)
- // 2. Define the tools the AI can use
payload := map[string]interface{}{
- "model": "stepfun/step-3.5-flash:free",
+ "model": "arcee-ai/trinity-large-preview:free", // arcee-ai/trinity-large-preview:free, stepfun/step-3.5-flash:free
"messages": fullMessages,
+ "stream": true, // <--- THIS IS KEY
"tools": []map[string]interface{}{
{
"type": "function",
"function": map[string]interface{}{
"name": "create_appointment",
- "description": "Schedules a new appointment in the database",
+ "description": "Schedules a new appointment",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
- "customer_phone": map[string]string{"type": "string", "description": "The user's phone number"},
- "date": map[string]string{"type": "string", "description": "The date and time in YYYY-MM-DD HH:MM format"},
+ "customer_phone": map[string]string{"type": "string"},
+ "date": map[string]string{"type": "string"},
},
"required": []string{"customer_phone", "date"},
},
@@ -70,50 +76,82 @@ func GetAIResponse(chatHistory []Message) (string, error) {
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
- resp, err := http.DefaultClient.Do(req)
+ client := &http.Client{}
+ resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
- bodyBytes, _ := io.ReadAll(resp.Body)
- if resp.StatusCode != http.StatusOK {
- return "Error from OpenRouter: " + string(bodyBytes), nil
- }
+ // Prepare to read the stream line by line
+ scanner := bufio.NewScanner(resp.Body)
- var result OpenRouterResponse
- if err := json.Unmarshal(bodyBytes, &result); err != nil {
- return "", err
- }
+ var fullContentBuffer strings.Builder
+ var toolCallBuffer strings.Builder
+ var toolName string
+ isToolCall := false
- if len(result.Choices) > 0 {
- aiMsg := result.Choices[0].Message
+ for scanner.Scan() {
+ line := scanner.Text()
- // 3. Handle Tool Calls (The "Logic" part)
- if len(aiMsg.ToolCalls) > 0 {
- tc := aiMsg.ToolCalls[0].Function
- if tc.Name == "create_appointment" {
- var args struct {
- Phone string `json:"customer_phone"`
- Date string `json:"date"`
+ // OpenRouter sends "data: {JSON}" lines.
+ // Use specific string trimming to handle the format.
+ if !strings.HasPrefix(line, "data: ") {
+ continue
+ }
+ jsonStr := strings.TrimPrefix(line, "data: ")
+
+ // The stream ends with "data: [DONE]"
+ if jsonStr == "[DONE]" {
+ break
+ }
+
+ var chunk StreamResponse
+ if err := json.Unmarshal([]byte(jsonStr), &chunk); err != nil {
+ continue
+ }
+
+ if len(chunk.Choices) > 0 {
+ delta := chunk.Choices[0].Delta
+
+ // 1. Handle Text Content
+ if delta.Content != "" {
+ fullContentBuffer.WriteString(delta.Content)
+ // Send this chunk to the frontend immediately
+ onToken(delta.Content)
+ }
+
+ // 2. Handle Tool Calls (Accumulate them, don't stream execution yet)
+ if len(delta.ToolCalls) > 0 {
+ isToolCall = true
+ if delta.ToolCalls[0].Function.Name != "" {
+ toolName = delta.ToolCalls[0].Function.Name
}
- // Unmarshal the AI-generated arguments
- json.Unmarshal([]byte(tc.Arguments), &args)
-
- // Save to DB using your helper
- err := db.SaveAppointment(args.Phone, args.Date)
- if err != nil {
- return "I tried to book it, but the database hates me: " + err.Error(), nil
- }
- return "ā
[SYSTEM] Appointment automatically booked for " + args.Phone + " at " + args.Date, nil
+ toolCallBuffer.WriteString(delta.ToolCalls[0].Function.Arguments)
}
}
+ }
- // 4. Return plain text if no tool was called
- if aiMsg.Content != "" {
- return aiMsg.Content, nil
+ // If it was a tool call, execute it now that the stream is finished
+ if isToolCall && toolName == "create_appointment" {
+ var args struct {
+ Phone string `json:"customer_phone"`
+ Date string `json:"date"`
+ }
+ // Try to parse the accumulated JSON arguments
+ if err := json.Unmarshal([]byte(toolCallBuffer.String()), &args); err == nil {
+ err := db.SaveAppointment(args.Phone, args.Date)
+ resultMsg := ""
+ if err != nil {
+ resultMsg = "\n[System: Failed to book appointment.]"
+ } else {
+ resultMsg = fmt.Sprintf("\nā
Booked for %s at %s", args.Phone, args.Date)
+ }
+ // Send the tool result to the frontend
+ onToken(resultMsg)
+ fullContentBuffer.WriteString(resultMsg)
}
}
- return "The AI is giving me the silent treatment, seki.", nil
+ return fullContentBuffer.String(), nil
}
diff --git a/templates/dashboard.html b/templates/dashboard.html
index 598c037..14a21aa 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -2,98 +2,243 @@
- SekiBot | Conversations
+ SekiBot | Dashboard
+