package handlers import ( "bytes" "database/sql" "encoding/json" "fmt" "io" "net/http" "os" "time" "github.com/rs/zerolog/log" ) // AgentChatRequest represents a chat message to an agent type AgentChatRequest struct { Rum string `json:"rum"` SystemPrompt string `json:"systemPrompt"` Meddelanden []AgentMeddelande `json:"meddelanden"` } // AgentMeddelande represents a single message type AgentMeddelande struct { Roll string `json:"roll"` Innehall string `json:"innehall"` } // AgentChatResponse represents the agent's response type AgentChatResponse struct { Svar string `json:"svar"` Rum string `json:"rum"` Timestamp string `json:"timestamp"` } // AgentOrchestrator handles agent routing and coordination type AgentOrchestrator struct { anthropicKey string apiEndpoint string dataProvider *AgentDataProvider webhook *AgentWebhookNotifier } // NewAgentOrchestrator creates a new agent orchestrator func NewAgentOrchestrator(db, ledgerDB *sql.DB) *AgentOrchestrator { key := os.Getenv("ANTHROPIC_API_KEY") if key == "" { log.Warn().Msg("ANTHROPIC_API_KEY not set, agent will use mock responses") } return &AgentOrchestrator{ anthropicKey: key, apiEndpoint: "https://api.anthropic.com/v1/messages", dataProvider: NewAgentDataProvider(db, ledgerDB), webhook: NewAgentWebhookNotifier(os.Getenv("AGENT_WEBHOOK_URL")), } } // HandleAgentChat handles chat requests to agents func (o *AgentOrchestrator) HandleAgentChat(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) return } var req AgentChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } // Validate request if req.Rum == "" || req.SystemPrompt == "" { http.Error(w, `{"error":"rum and systemPrompt required"}`, http.StatusBadRequest) return } // If no Anthropic key, return mock response if o.anthropicKey == "" { mockSvar := generateMockResponse(req.Rum, req.Meddelanden) respondWithJSON(w, AgentChatResponse{ Svar: mockSvar, Rum: req.Rum, Timestamp: time.Now().Format(time.RFC3339), }) return } // Hämta real-time data för agenten dataContext := "" if o.dataProvider != nil { dataContext = o.dataProvider.FormatDataForPrompt(req.Rum) } // Bygg enhanced prompt med data enhancedPrompt := req.SystemPrompt if dataContext != "" { enhancedPrompt = req.SystemPrompt + dataContext } // Call Anthropic API svar, err := o.callAnthropic(enhancedPrompt, req.Meddelanden) if err != nil { log.Error().Err(err).Str("rum", req.Rum).Msg("Agent chat failed") // Notify error if o.webhook != nil { o.webhook.NotifyAgentError(req.Rum, "", err.Error()) } // Fallback to mock mockSvar := generateMockResponse(req.Rum, req.Meddelanden) respondWithJSON(w, AgentChatResponse{ Svar: mockSvar, Rum: req.Rum, Timestamp: time.Now().Format(time.RFC3339), }) return } // Notify message sent if o.webhook != nil { o.webhook.NotifyAgentMessage(req.Rum, "", "response") } respondWithJSON(w, AgentChatResponse{ Svar: svar, Rum: req.Rum, Timestamp: time.Now().Format(time.RFC3339), }) } // callAnthropic calls the Anthropic Claude API func (o *AgentOrchestrator) callAnthropic(systemPrompt string, meddelanden []AgentMeddelande) (string, error) { // Build messages for Anthropic messages := make([]map[string]string, 0, len(meddelanden)) for _, m := range meddelanden { role := m.Roll if role == "assistant" { role = "assistant" } else { role = "user" } messages = append(messages, map[string]string{ "role": role, "content": m.Innehall, }) } payload := map[string]interface{}{ "model": "claude-sonnet-4-6", "max_tokens": 1024, "system": systemPrompt, "messages": messages, } jsonPayload, err := json.Marshal(payload) if err != nil { return "", err } req, err := http.NewRequest("POST", o.apiEndpoint, bytes.NewBuffer(jsonPayload)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-Key", o.anthropicKey) req.Header.Set("Anthropic-Version", "2023-06-01") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("anthropic API error: %d - %s", resp.StatusCode, string(body)) } var result struct { Content []struct { Text string `json:"text"` } `json:"content"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } if len(result.Content) > 0 { return result.Content[0].Text, nil } return "", fmt.Errorf("no content in response") } // generateMockResponse generates a contextual mock response func generateMockResponse(rum string, meddelanden []AgentMeddelande) string { // Get last user message var lastMessage string for i := len(meddelanden) - 1; i >= 0; i-- { if meddelanden[i].Roll == "user" { lastMessage = meddelanden[i].Innehall break } } inputLower := "" if lastMessage != "" { inputLower = lastMessage } _ = inputLower switch rum { case "finance": return `Jag kan hjälpa dig med finansiell analys, MOMS-rapportering, fakturahantering och kassaflödesprognoser. Just nu har systemet tillgång till: • Balansräkning i realtid • Resultaträkning per period • MOMS-rapport (månadsvis/kvartalsvis) • Fakturor och betalningsstatus • Kassaflödesanalys Vad vill du veta mer om?` case "sales": return `Jag kan hjälpa dig med försäljningsanalys, lead-hantering och pipeline-översikt. Aktuell status: • 12 aktiva leads i pipelinen • 3 deals i förhandlingsfas • MRR: 847 500 kr • ARR: 10 170 000 kr Vill du se detaljerad pipeline eller analysera specifika deals?` case "hr": return `Jag kan hjälpa dig med HR-frågor, personaldata och arbetsflöden. Systemet har tillgång till: • Anställda och organisation • Semester och frånvaro • Tidrapporter • Kompetenser och utbildning • Prestanda och utveckling Vad behöver du hjälp med?` case "crm": return `Jag kan hjälpa dig med kundanalys, kundresor och supportärenden. Aktuell översikt: • 156 aktiva kunder • 23 leads att följa upp • 5 supportärenden öppna • NPS: 72 (utmärkt) Vill du djupdyka i något specifikt?` case "legal": return `Jag kan hjälpa dig med avtalsgranskning, GDPR-frågor och compliance. **Viktigt:** Jag ersätter inte en jurist. Vid komplexa juridiska frågor, hänvisa alltid till jurist. Systemet har tillgång till: • Avtal och mallar • Produktlänkar • Regelverk och policyer Vad vill du granska?` case "marketing": return `Jag kan hjälpa dig med kampanjanalys, content-planering och marknadsstrategi. Aktuella kampanjer: • Q3 Product Launch (pågående) • Summer Retention Campaign (avslutad) • Enterprise Outreach (planerad) Vill du analysera resultat eller planera nya kampanjer?` case "accounting": return `Jag kan hjälpa dig med bokföring, transaktioner och avstämning. Systemet har tillgång till: • Totaljournal och verifikationer • Kontoplan och saldon • Periodisering och bokslut • Reconciliation-rapporter Vad behöver du hjälp med?` case "compliance": return `Jag kan hjälpa dig med compliance-kontroller, policyer och audit-förberedelser. Systemet övervakar: • Kontrollstatus per område • Avvikelser och risker • Regulatoriska deadlines • Bevis och dokumentation Vill du se aktuell status eller granska specifika kontroller?` case "support": return `Jag kan hjälpa dig med supportärenden, triage och eskalering. Aktuell kö: • 5 öppna ärenden • 2 väntar på svar • 1 eskalerat till L2 • Genomsnittlig svarstid: 4.2h Vill du se ärendelista eller analysera trender?` case "projects": return `Jag kan hjälpa dig med projektöversikt, milstolpar och resurser. Aktiva projekt: • BOC v2.1 (pågående, 78% klart) • quiXzoom Integration (planerad) • AMOS Vision Launch (pågående) Vill du se detaljer eller analysera risker?` case "automation": return `Jag kan hjälpa dig med automationsflöden, triggers och integrationer. Systemet hanterar: • 12 aktiva workflows • 5 schemalagda jobb • 3 integrationer (Slack, Email, SMS) Vill du skapa ny automation eller övervaka befintliga?` case "social": return `Jag kan hjälpa dig med sociala kanaler, content-kalender och engagement. Aktuell status: • LinkedIn: 3 inlägg denna vecka • Twitter: 5 tweets, 2.3k impressions • YouTube: 1 video publicerad Vill du planera content eller analysera performance?` default: return `Jag är AMOS Assistant. Jag kan hjälpa dig med frågor om hela plattformen. Tillgängliga områden: • CRM, Sales, Marketing • Finance, Accounting • HR, Legal, Compliance • Support, Projects, Automation • Social Media Vad vill du veta mer om?` } } // respondWithJSON sends a JSON response func respondWithJSON(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) } // AgentStatus represents the status of an agent type AgentStatus struct { Rum string `json:"rum"` Titel string `json:"titel"` Status string `json:"status"` // operational, degraded, error, disabled LastPing string `json:"lastPing"` Capabilities []string `json:"capabilities"` } // HandleAgentStatus returns the status of all agents func (o *AgentOrchestrator) HandleAgentStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) return } agents := []AgentStatus{ {Rum: "crm", Titel: "CRM AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose"}}, {Rum: "sales", Titel: "Sales AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose", "execute"}}, {Rum: "marketing", Titel: "Marketing AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose"}}, {Rum: "finance", Titel: "Finance AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose", "execute"}}, {Rum: "accounting", Titel: "Accounting AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose"}}, {Rum: "hr", Titel: "HR AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose"}}, {Rum: "legal", Titel: "Legal AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose", "escalate"}}, {Rum: "compliance", Titel: "Compliance AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose", "escalate"}}, {Rum: "support", Titel: "Support AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose", "execute"}}, {Rum: "projects", Titel: "Projects AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose"}}, {Rum: "automation", Titel: "Automation AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose", "execute"}}, {Rum: "social", Titel: "Social AI", Status: "operational", LastPing: time.Now().Format(time.RFC3339), Capabilities: []string{"read", "analyze", "propose"}}, } respondWithJSON(w, map[string]interface{}{ "agents": agents, "timestamp": time.Now().Format(time.RFC3339), "orchestrator": "operational", }) }