package services import ( "bufio" "bytes" "encoding/json" "fmt" "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"` } // 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. 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...) 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, "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.", "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", }, }, "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 }