modified: handlers/dashboard.go modified: main.go modified: services/openrouter.go modified: templates/dashboard.html
158 lines
4.5 KiB
Go
158 lines
4.5 KiB
Go
package services
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"whatsapp-bot/db"
|
|
)
|
|
|
|
type Message struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
// Structs to parse the incoming stream from OpenRouter
|
|
type StreamResponse struct {
|
|
Choices []struct {
|
|
Delta struct {
|
|
Content string `json:"content"`
|
|
ToolCalls []struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Function struct {
|
|
Name string `json:"name"`
|
|
Arguments string `json:"arguments"`
|
|
} `json:"function"`
|
|
} `json:"tool_calls"`
|
|
} `json:"delta"`
|
|
FinishReason string `json:"finish_reason"`
|
|
} `json:"choices"`
|
|
}
|
|
|
|
// 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"
|
|
|
|
fullMessages := append([]Message{
|
|
{
|
|
Role: "system",
|
|
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...)
|
|
|
|
payload := map[string]interface{}{
|
|
"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",
|
|
"parameters": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"customer_phone": map[string]string{"type": "string"},
|
|
"date": map[string]string{"type": "string"},
|
|
},
|
|
"required": []string{"customer_phone", "date"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
jsonData, _ := json.Marshal(payload)
|
|
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Prepare to read the stream line by line
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
|
|
var fullContentBuffer strings.Builder
|
|
var toolCallBuffer strings.Builder
|
|
var toolName string
|
|
isToolCall := false
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
// 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
|
|
}
|
|
toolCallBuffer.WriteString(delta.ToolCalls[0].Function.Arguments)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 fullContentBuffer.String(), nil
|
|
}
|