8cb26cdb15
- Add AgentFAB to ProjectsPage and CompliancePage - Add AgentWebhookNotifier for agent events (activated, message, escalation, error) - Webhook URL configurable via AGENT_WEBHOOK_URL env var - Build fresh web-v2 dist
404 lines
12 KiB
Go
404 lines
12 KiB
Go
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",
|
|
})
|
|
}
|