modified: .env
new file: __debug_bin.exe modified: bot.db modified: db/db.go modified: go.mod new file: handlers/auth.go modified: handlers/dashboard.go new file: handlers/saas.go modified: handlers/webhook.go modified: main.go new file: saas_bot.db modified: services/openrouter.go new file: services/types.go modified: services/whatsapp.go new file: static/style.css modified: templates/dashboard.html new file: templates/landing.html new file: templates/login.html new file: templates/register.html deleted: types/types.go
This commit is contained in:
@@ -8,84 +8,36 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"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"`
|
||||
}
|
||||
// NOTA: struct Message ya NO está aquí, está en types.go
|
||||
|
||||
// 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) {
|
||||
func StreamAIResponse(userID int, userMessage string, systemPrompt string, onToken func(string)) (string, error) {
|
||||
apiKey := os.Getenv("OPENROUTER_API_KEY")
|
||||
url := "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
||||
// 1. Get Current Time for the LLM
|
||||
currentTime := time.Now().Format("Monday, 2006-01-02 15:04")
|
||||
|
||||
// 2. Strict System Prompt
|
||||
systemPrompt := fmt.Sprintf(`
|
||||
You are a helpful scheduler assistant for a business in Chile.
|
||||
Current Date/Time: %s
|
||||
|
||||
RULES FOR BOOKING:
|
||||
1. You MUST get three things from the user before booking:
|
||||
- The Date (day/month)
|
||||
- The Time (hour)
|
||||
- The Phone Number
|
||||
2. If any of these are missing, ASK for them. Do NOT assume or guess.
|
||||
3. If the user says "tomorrow" or "next Friday", calculate the date based on the Current Date/Time above.
|
||||
4. Only when you have all details, use the 'create_appointment' tool.
|
||||
`, currentTime)
|
||||
|
||||
// 3. Prepend System Prompt to History
|
||||
fullMessages := append([]Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
}, chatHistory...)
|
||||
messages := []map[string]string{
|
||||
{"role": "system", "content": systemPrompt},
|
||||
{"role": "user", "content": userMessage},
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"model": "arcee-ai/trinity-large-preview:free", // stepfun/step-3.5-flash:free, arcee-ai/trinity-large-preview:free
|
||||
"messages": fullMessages,
|
||||
"model": "arcee-ai/trinity-large-preview:free", //stepfun/step-3.5-flash:free
|
||||
"messages": messages,
|
||||
"stream": true,
|
||||
"tools": []map[string]interface{}{
|
||||
{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": "create_appointment",
|
||||
"description": "Schedules a new appointment. ONLY use this when you have a confirm date, time, and phone number.",
|
||||
"description": "Schedules a new appointment. ONLY use when you have date, time, and phone.",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"customer_phone": map[string]string{
|
||||
"type": "string",
|
||||
"description": "The user's phone number (e.g., +569...)",
|
||||
},
|
||||
"date": map[string]string{
|
||||
"type": "string",
|
||||
"description": "The full 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"},
|
||||
},
|
||||
@@ -106,9 +58,7 @@ RULES FOR BOOKING:
|
||||
}
|
||||
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
|
||||
@@ -116,35 +66,37 @@ RULES FOR BOOKING:
|
||||
|
||||
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
|
||||
var chunk struct {
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []struct {
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
} `json:"tool_calls"`
|
||||
} `json:"delta"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
json.Unmarshal([]byte(jsonStr), &chunk)
|
||||
|
||||
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)
|
||||
if onToken != nil {
|
||||
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 != "" {
|
||||
@@ -155,24 +107,24 @@ RULES FOR BOOKING:
|
||||
}
|
||||
}
|
||||
|
||||
// If it was a tool call, execute it now that the stream is finished
|
||||
// EXECUTE TOOL
|
||||
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)
|
||||
err := db.SaveAppointment(userID, args.Phone, args.Date)
|
||||
resultMsg := ""
|
||||
if err != nil {
|
||||
resultMsg = "\n[System: Failed to book appointment.]"
|
||||
resultMsg = "\n(System: Failed to book appointment)"
|
||||
} else {
|
||||
resultMsg = fmt.Sprintf("\n✅ Booked for %s at %s", args.Phone, args.Date)
|
||||
resultMsg = fmt.Sprintf("\n✅ Appointment Confirmed: %s", args.Date)
|
||||
}
|
||||
// Send the tool result to the frontend
|
||||
onToken(resultMsg)
|
||||
fullContentBuffer.WriteString(resultMsg)
|
||||
if onToken != nil {
|
||||
onToken(resultMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
services/types.go
Normal file
27
services/types.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package services
|
||||
|
||||
// Message: Usado para el historial del chat (OpenRouter) y dashboard
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// WebhookPayload: Usado para leer el JSON que envía WhatsApp
|
||||
type WebhookPayload struct {
|
||||
Entry []struct {
|
||||
Changes []struct {
|
||||
Value struct {
|
||||
Metadata struct {
|
||||
PhoneNumberID string `json:"phone_number_id"`
|
||||
} `json:"metadata"`
|
||||
Messages []struct {
|
||||
From string `json:"from"`
|
||||
Text struct {
|
||||
Body string `json:"body"`
|
||||
} `json:"text"`
|
||||
Type string `json:"type"`
|
||||
} `json:"messages"`
|
||||
} `json:"value"`
|
||||
} `json:"changes"`
|
||||
} `json:"entry"`
|
||||
}
|
||||
@@ -5,13 +5,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// SendWhatsAppMessage sends a text reply to a user
|
||||
func SendWhatsAppMessage(toPhone string, messageBody string) error {
|
||||
token := os.Getenv("WHATSAPP_TOKEN") // "EAA..."
|
||||
phoneID := os.Getenv("WHATSAPP_PHONE_ID") // "100..."
|
||||
// SendWhatsAppMessage sends a text reply using the specific Client's credentials
|
||||
func SendWhatsAppMessage(token, phoneID, toPhone, messageBody string) error {
|
||||
version := "v17.0"
|
||||
url := fmt.Sprintf("https://graph.facebook.com/%s/%s/messages", version, phoneID)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user