modified: bot.db

modified:   handlers/dashboard.go
	modified:   main.go
	modified:   services/openrouter.go
	modified:   templates/dashboard.html
This commit is contained in:
2026-03-01 07:10:01 -03:00
parent d4d395356b
commit 6f7987c3fe
5 changed files with 328 additions and 151 deletions

View File

@@ -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
}