diff --git a/backend/bin/boc b/backend/bin/boc index f82ab6d7c..c92bd5daa 100755 Binary files a/backend/bin/boc and b/backend/bin/boc differ diff --git a/backend/boc-api b/backend/boc-api index f82ab6d7c..c92bd5daa 100755 Binary files a/backend/boc-api and b/backend/boc-api differ diff --git a/backend/handlers/agent.go b/backend/handlers/agent.go index f4a3dd71e..b6780f98a 100644 --- a/backend/handlers/agent.go +++ b/backend/handlers/agent.go @@ -38,6 +38,7 @@ type AgentOrchestrator struct { anthropicKey string apiEndpoint string dataProvider *AgentDataProvider + webhook *AgentWebhookNotifier } // NewAgentOrchestrator creates a new agent orchestrator @@ -51,6 +52,7 @@ func NewAgentOrchestrator(db, ledgerDB *sql.DB) *AgentOrchestrator { anthropicKey: key, apiEndpoint: "https://api.anthropic.com/v1/messages", dataProvider: NewAgentDataProvider(db, ledgerDB), + webhook: NewAgentWebhookNotifier(os.Getenv("AGENT_WEBHOOK_URL")), } } @@ -100,6 +102,10 @@ func (o *AgentOrchestrator) HandleAgentChat(w http.ResponseWriter, r *http.Reque 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{ @@ -110,6 +116,11 @@ func (o *AgentOrchestrator) HandleAgentChat(w http.ResponseWriter, r *http.Reque return } + // Notify message sent + if o.webhook != nil { + o.webhook.NotifyAgentMessage(req.Rum, "", "response") + } + respondWithJSON(w, AgentChatResponse{ Svar: svar, Rum: req.Rum, diff --git a/backend/handlers/agent_webhook.go b/backend/handlers/agent_webhook.go new file mode 100644 index 000000000..0982adb90 --- /dev/null +++ b/backend/handlers/agent_webhook.go @@ -0,0 +1,118 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "time" + + "github.com/rs/zerolog/log" +) + +// AgentWebhookEvent representerar en agent-händelse +type AgentWebhookEvent struct { + EventType string `json:"event_type"` + AgentRum string `json:"agent_rum"` + Timestamp string `json:"timestamp"` + UserID string `json:"user_id,omitempty"` + Payload map[string]interface{} `json:"payload"` +} + +// AgentWebhookNotifier skickar agent-händelser till konfigurerade webhooks +type AgentWebhookNotifier struct { + webhookURL string + client *http.Client + enabled bool +} + +// NewAgentWebhookNotifier skapar en ny notifier +func NewAgentWebhookNotifier(webhookURL string) *AgentWebhookNotifier { + return &AgentWebhookNotifier{ + webhookURL: webhookURL, + client: &http.Client{Timeout: 10 * time.Second}, + enabled: webhookURL != "", + } +} + +// Notify skickar en händelse till webhook +func (n *AgentWebhookNotifier) Notify(event AgentWebhookEvent) { + if !n.enabled { + return + } + + event.Timestamp = time.Now().Format(time.RFC3339) + + jsonData, err := json.Marshal(event) + if err != nil { + log.Error().Err(err).Str("event", event.EventType).Msg("Failed to marshal webhook event") + return + } + + go func() { + resp, err := n.client.Post(n.webhookURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + log.Warn().Err(err).Str("url", n.webhookURL).Msg("Webhook delivery failed") + return + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + log.Warn().Int("status", resp.StatusCode).Str("url", n.webhookURL).Msg("Webhook returned error") + } + }() +} + +// NotifyAgentActivated skickas när en agent aktiveras +func (n *AgentWebhookNotifier) NotifyAgentActivated(rum, userID string) { + n.Notify(AgentWebhookEvent{ + EventType: "agent.activated", + AgentRum: rum, + UserID: userID, + Payload: map[string]interface{}{ + "rum": rum, + "user_id": userID, + }, + }) +} + +// NotifyAgentMessage skickas när en agent skickar ett meddelande +func (n *AgentWebhookNotifier) NotifyAgentMessage(rum, userID, messageType string) { + n.Notify(AgentWebhookEvent{ + EventType: "agent.message", + AgentRum: rum, + UserID: userID, + Payload: map[string]interface{}{ + "rum": rum, + "user_id": userID, + "message_type": messageType, + }, + }) +} + +// NotifyAgentEscalation skickas när en agent eskalerar +func (n *AgentWebhookNotifier) NotifyAgentEscalation(rum, userID, reason string) { + n.Notify(AgentWebhookEvent{ + EventType: "agent.escalation", + AgentRum: rum, + UserID: userID, + Payload: map[string]interface{}{ + "rum": rum, + "user_id": userID, + "reason": reason, + }, + }) +} + +// NotifyAgentError skickas när en agent stöter på ett fel +func (n *AgentWebhookNotifier) NotifyAgentError(rum, userID, errorMsg string) { + n.Notify(AgentWebhookEvent{ + EventType: "agent.error", + AgentRum: rum, + UserID: userID, + Payload: map[string]interface{}{ + "rum": rum, + "user_id": userID, + "error": errorMsg, + }, + }) +} diff --git a/web-v2/src/pages/CompliancePage.tsx b/web-v2/src/pages/CompliancePage.tsx index 2bf3dad90..6aba70982 100644 --- a/web-v2/src/pages/CompliancePage.tsx +++ b/web-v2/src/pages/CompliancePage.tsx @@ -14,6 +14,7 @@ import { Clock, TrendingUp, } from 'lucide-react' +import { AgentFAB } from '@/components/agent/AgentFAB' export function CompliancePage() { const [activeTab, setActiveTab] = useState('iso') @@ -514,6 +515,9 @@ export function CompliancePage() { )} + + {/* Compliance Agent */} + ) } diff --git a/web-v2/src/pages/ProjectsPage.tsx b/web-v2/src/pages/ProjectsPage.tsx index 218490c94..24a8701a6 100644 --- a/web-v2/src/pages/ProjectsPage.tsx +++ b/web-v2/src/pages/ProjectsPage.tsx @@ -16,6 +16,7 @@ import { X, GripVertical, } from 'lucide-react' +import { AgentFAB } from '@/components/agent/AgentFAB' interface Task { id: string @@ -562,6 +563,9 @@ export function ProjectsPage() { )} + + {/* Projects Agent */} + ) }