diff --git a/backend/bin/boc b/backend/bin/boc index 9a7e4ef98..f82ab6d7c 100755 Binary files a/backend/bin/boc and b/backend/bin/boc differ diff --git a/backend/boc-api b/backend/boc-api index 9a7e4ef98..f82ab6d7c 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 60dad3fd9..f4a3dd71e 100644 --- a/backend/handlers/agent.go +++ b/backend/handlers/agent.go @@ -2,6 +2,7 @@ package handlers import ( "bytes" + "database/sql" "encoding/json" "fmt" "io" @@ -36,10 +37,11 @@ type AgentChatResponse struct { type AgentOrchestrator struct { anthropicKey string apiEndpoint string + dataProvider *AgentDataProvider } // NewAgentOrchestrator creates a new agent orchestrator -func NewAgentOrchestrator() *AgentOrchestrator { +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") @@ -48,6 +50,7 @@ func NewAgentOrchestrator() *AgentOrchestrator { return &AgentOrchestrator{ anthropicKey: key, apiEndpoint: "https://api.anthropic.com/v1/messages", + dataProvider: NewAgentDataProvider(db, ledgerDB), } } @@ -81,8 +84,20 @@ func (o *AgentOrchestrator) HandleAgentChat(w http.ResponseWriter, r *http.Reque 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(req.SystemPrompt, req.Meddelanden) + svar, err := o.callAnthropic(enhancedPrompt, req.Meddelanden) if err != nil { log.Error().Err(err).Str("rum", req.Rum).Msg("Agent chat failed") // Fallback to mock diff --git a/backend/handlers/agent_data.go b/backend/handlers/agent_data.go new file mode 100644 index 000000000..db15326a3 --- /dev/null +++ b/backend/handlers/agent_data.go @@ -0,0 +1,425 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/rs/zerolog/log" +) + +// AgentDataProvider hämtar riktig data från BOC för agenterna +type AgentDataProvider struct { + db *sql.DB + ledgerDB *sql.DB + amosURL string +} + +// NewAgentDataProvider skapar en ny data provider +func NewAgentDataProvider(db, ledgerDB *sql.DB) *AgentDataProvider { + return &AgentDataProvider{ + db: db, + ledgerDB: ledgerDB, + amosURL: os.Getenv("AMOS_BASE_URL"), + } +} + +// FinanceData innehåller finansiell data för Finance Agent +type FinanceData struct { + Balance *BalanceSnapshot `json:"balance,omitempty"` + Income *IncomeSnapshot `json:"income,omitempty"` + Cashflow *CashflowSummary `json:"cashflow,omitempty"` + Invoices []InvoiceSummary `json:"invoices,omitempty"` + Period string `json:"period"` + GeneratedAt string `json:"generated_at"` +} + +type BalanceSnapshot struct { + TotalAssets float64 `json:"total_assets"` + TotalLiabilities float64 `json:"total_liabilities"` + TotalEquity float64 `json:"total_equity"` + LiquidityRatio float64 `json:"liquidity_ratio"` +} + +type IncomeSnapshot struct { + Revenue float64 `json:"revenue"` + Expenses float64 `json:"expenses"` + NetIncome float64 `json:"net_income"` + Margin float64 `json:"margin"` +} + +type CashflowSummary struct { + Inflow float64 `json:"inflow"` + Outflow float64 `json:"outflow"` + Net float64 `json:"net"` +} + +type InvoiceSummary struct { + ID string `json:"id"` + Customer string `json:"customer"` + Amount float64 `json:"amount"` + Status string `json:"status"` + DueDate string `json:"due_date"` + DaysOverdue int `json:"days_overdue,omitempty"` +} + +// CRMData innehåller CRM-data för CRM Agent +type CRMData struct { + CustomerCount int `json:"customer_count"` + LeadCount int `json:"lead_count"` + PipelineValue float64 `json:"pipeline_value"` + RecentCustomers []CustomerBrief `json:"recent_customers,omitempty"` + RecentLeads []LeadBrief `json:"recent_leads,omitempty"` + GeneratedAt string `json:"generated_at"` +} + +type CustomerBrief struct { + ID string `json:"id"` + Name string `json:"name"` + Company string `json:"company"` + Status string `json:"status"` +} + +type LeadBrief struct { + ID string `json:"id"` + Name string `json:"name"` + Source string `json:"source"` + Status string `json:"status"` +} + +// SalesData innehåller säljdata för Sales Agent +type SalesData struct { + TotalDeals int `json:"total_deals"` + MRR float64 `json:"mrr"` + ARR float64 `json:"arr"` + PipelineStages []StageBrief `json:"pipeline_stages,omitempty"` + RecentDeals []DealBrief `json:"recent_deals,omitempty"` + GeneratedAt string `json:"generated_at"` +} + +type StageBrief struct { + Name string `json:"name"` + Deals int `json:"deals"` + Value float64 `json:"value"` +} + +type DealBrief struct { + ID string `json:"id"` + Name string `json:"name"` + Value float64 `json:"value"` + Stage string `json:"stage"` +} + +// HRData innehåller HR-data för HR Agent +type HRData struct { + EmployeeCount int `json:"employee_count"` + ActiveLeaves int `json:"active_leaves"` + Departments []DeptSummary `json:"departments,omitempty"` + RecentHires []EmployeeBrief `json:"recent_hires,omitempty"` + GeneratedAt string `json:"generated_at"` +} + +type DeptSummary struct { + Name string `json:"name"` + Count int `json:"count"` +} + +type EmployeeBrief struct { + ID string `json:"id"` + Name string `json:"name"` + Department string `json:"department"` + Status string `json:"status"` +} + +// GetFinanceData hämtar finansiell data +func (p *AgentDataProvider) GetFinanceData() (*FinanceData, error) { + data := &FinanceData{ + Period: time.Now().Format("2006-01"), + GeneratedAt: time.Now().Format(time.RFC3339), + } + + // Hämta balansräkning + if p.ledgerDB != nil { + var assets, liabilities, equity float64 + err := p.ledgerDB.QueryRow(` + SELECT + COALESCE(SUM(CASE WHEN account_code < 2000 THEN balance ELSE 0 END), 0) as assets, + COALESCE(SUM(CASE WHEN account_code >= 2000 AND account_code < 3000 THEN balance ELSE 0 END), 0) as liabilities, + COALESCE(SUM(CASE WHEN account_code >= 3000 THEN balance ELSE 0 END), 0) as equity + FROM account_balances + WHERE period = $1 + `, data.Period).Scan(&assets, &liabilities, &equity) + if err == nil { + data.Balance = &BalanceSnapshot{ + TotalAssets: assets, + TotalLiabilities: liabilities, + TotalEquity: equity, + LiquidityRatio: assets / liabilities * 100, + } + } + + // Hämta resultaträkning + var revenue, expenses float64 + err = p.ledgerDB.QueryRow(` + SELECT + COALESCE(SUM(CASE WHEN account_code >= 3000 AND account_code < 4000 THEN credit - debit ELSE 0 END), 0) as revenue, + COALESCE(SUM(CASE WHEN account_code >= 4000 AND account_code < 8000 THEN debit - credit ELSE 0 END), 0) as expenses + FROM journal_entries + WHERE DATE_TRUNC('month', entry_date) = DATE_TRUNC('month', CURRENT_DATE) + `).Scan(&revenue, &expenses) + if err == nil { + netIncome := revenue - expenses + margin := 0.0 + if revenue > 0 { + margin = (netIncome / revenue) * 100 + } + data.Income = &IncomeSnapshot{ + Revenue: revenue, + Expenses: expenses, + NetIncome: netIncome, + Margin: margin, + } + } + + // Hämta fakturor + rows, err := p.ledgerDB.Query(` + SELECT id, customer_name, amount, status, due_date, + CASE WHEN due_date < CURRENT_DATE AND status != 'paid' + THEN EXTRACT(DAY FROM CURRENT_DATE - due_date)::int + ELSE 0 END as days_overdue + FROM invoices + WHERE created_at > CURRENT_DATE - INTERVAL '90 days' + ORDER BY created_at DESC + LIMIT 10 + `) + if err == nil { + defer rows.Close() + for rows.Next() { + var inv InvoiceSummary + rows.Scan(&inv.ID, &inv.Customer, &inv.Amount, &inv.Status, &inv.DueDate, &inv.DaysOverdue) + data.Invoices = append(data.Invoices, inv) + } + } + } + + return data, nil +} + +// GetCRMData hämtar CRM-data +func (p *AgentDataProvider) GetCRMData() (*CRMData, error) { + data := &CRMData{ + GeneratedAt: time.Now().Format(time.RFC3339), + } + + if p.db != nil { + // Kundantal + p.db.QueryRow("SELECT COUNT(*) FROM customers WHERE status = 'active'").Scan(&data.CustomerCount) + + // Leadantal + p.db.QueryRow("SELECT COUNT(*) FROM leads WHERE status != 'converted'").Scan(&data.LeadCount) + + // Pipeline-värde + p.db.QueryRow("SELECT COALESCE(SUM(value), 0) FROM pipeline_deals WHERE status = 'open'").Scan(&data.PipelineValue) + + // Senaste kunder + rows, err := p.db.Query(` + SELECT id, name, company, status + FROM customers + ORDER BY created_at DESC + LIMIT 5 + `) + if err == nil { + defer rows.Close() + for rows.Next() { + var c CustomerBrief + rows.Scan(&c.ID, &c.Name, &c.Company, &c.Status) + data.RecentCustomers = append(data.RecentCustomers, c) + } + } + + // Senaste leads + rows, err = p.db.Query(` + SELECT id, name, source, status + FROM leads + ORDER BY created_at DESC + LIMIT 5 + `) + if err == nil { + defer rows.Close() + for rows.Next() { + var l LeadBrief + rows.Scan(&l.ID, &l.Name, &l.Source, &l.Status) + data.RecentLeads = append(data.RecentLeads, l) + } + } + } + + return data, nil +} + +// GetSalesData hämtar säljdata +func (p *AgentDataProvider) GetSalesData() (*SalesData, error) { + data := &SalesData{ + GeneratedAt: time.Now().Format(time.RFC3339), + } + + if p.db != nil { + // Totalt antal deals + p.db.QueryRow("SELECT COUNT(*) FROM sales_deals").Scan(&data.TotalDeals) + + // MRR + p.db.QueryRow("SELECT COALESCE(SUM(monthly_value), 0) FROM sales_deals WHERE status = 'active'").Scan(&data.MRR) + + // ARR + data.ARR = data.MRR * 12 + + // Pipeline-stages + rows, err := p.db.Query(` + SELECT stage_name, COUNT(*), COALESCE(SUM(value), 0) + FROM sales_deals + WHERE status = 'open' + GROUP BY stage_name + ORDER BY stage_name + `) + if err == nil { + defer rows.Close() + for rows.Next() { + var s StageBrief + rows.Scan(&s.Name, &s.Deals, &s.Value) + data.PipelineStages = append(data.PipelineStages, s) + } + } + + // Senaste deals + rows, err = p.db.Query(` + SELECT id, name, value, stage + FROM sales_deals + ORDER BY updated_at DESC + LIMIT 5 + `) + if err == nil { + defer rows.Close() + for rows.Next() { + var d DealBrief + rows.Scan(&d.ID, &d.Name, &d.Value, &d.Stage) + data.RecentDeals = append(data.RecentDeals, d) + } + } + } + + return data, nil +} + +// GetHRData hämtar HR-data +func (p *AgentDataProvider) GetHRData() (*HRData, error) { + data := &HRData{ + GeneratedAt: time.Now().Format(time.RFC3339), + } + + if p.db != nil { + // Antal anställda + p.db.QueryRow("SELECT COUNT(*) FROM employees WHERE status = 'active'").Scan(&data.EmployeeCount) + + // Aktiva ledigheter + p.db.QueryRow(` + SELECT COUNT(*) FROM leaves + WHERE status = 'approved' + AND start_date <= CURRENT_DATE + AND end_date >= CURRENT_DATE + `).Scan(&data.ActiveLeaves) + + // Avdelningar + rows, err := p.db.Query(` + SELECT department, COUNT(*) + FROM employees + WHERE status = 'active' + GROUP BY department + `) + if err == nil { + defer rows.Close() + for rows.Next() { + var d DeptSummary + rows.Scan(&d.Name, &d.Count) + data.Departments = append(data.Departments, d) + } + } + + // Senaste anställningar + rows, err = p.db.Query(` + SELECT id, first_name || ' ' || last_name as name, department, status + FROM employees + ORDER BY start_date DESC + LIMIT 5 + `) + if err == nil { + defer rows.Close() + for rows.Next() { + var e EmployeeBrief + rows.Scan(&e.ID, &e.Name, &e.Department, &e.Status) + data.RecentHires = append(data.RecentHires, e) + } + } + } + + return data, nil +} + +// GetDataForAgent hämtar data baserat på agent-typ +func (p *AgentDataProvider) GetDataForAgent(rum string) (map[string]interface{}, error) { + switch rum { + case "finance": + data, err := p.GetFinanceData() + if err != nil { + log.Warn().Err(err).Str("rum", rum).Msg("Failed to get finance data") + return nil, err + } + return structToMap(data), nil + case "crm": + data, err := p.GetCRMData() + if err != nil { + log.Warn().Err(err).Str("rum", rum).Msg("Failed to get CRM data") + return nil, err + } + return structToMap(data), nil + case "sales": + data, err := p.GetSalesData() + if err != nil { + log.Warn().Err(err).Str("rum", rum).Msg("Failed to get sales data") + return nil, err + } + return structToMap(data), nil + case "hr": + data, err := p.GetHRData() + if err != nil { + log.Warn().Err(err).Str("rum", rum).Msg("Failed to get HR data") + return nil, err + } + return structToMap(data), nil + default: + return map[string]interface{}{ + "message": "No real-time data available for this agent type", + "rum": rum, + }, nil + } +} + +// structToMap konverterar en struct till map via JSON +func structToMap(v interface{}) map[string]interface{} { + var result map[string]interface{} + b, _ := json.Marshal(v) + json.Unmarshal(b, &result) + return result +} + +// FormatDataForPrompt formaterar data för Anthropic prompt +func (p *AgentDataProvider) FormatDataForPrompt(rum string) string { + data, err := p.GetDataForAgent(rum) + if err != nil { + return "" + } + + jsonData, _ := json.MarshalIndent(data, "", " ") + return fmt.Sprintf("\n\nAKTUELL DATA FRÅN SYSTEMET:\n%s\n\nAnvänd denna data för att ge exakta, datadrivna svar. Om data saknas för en specifik fråga, be användaren specificera.", string(jsonData)) +} diff --git a/backend/main.go b/backend/main.go index d8517b7e6..1d51f71a6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -118,9 +118,9 @@ func main() { autoEngine := automation.NewEngine(database, logger) autoH := handlers.NewAutomationHandler(database, autoEngine) - // Agent Orchestrator - agentOrchestrator := handlers.NewAgentOrchestrator() - logger.Info().Msg("Agent Orchestrator initialized") + // Agent Orchestrator — med datakoppling till ledger (återanvänd befintlig ledgerDB) + agentOrchestrator := handlers.NewAgentOrchestrator(database, ledgerDB) + logger.Info().Msg("Agent Orchestrator initialized with real-time data") // Auth: JWTService med förbättrad validering jwtService := auth.NewJWTService(cfg.JWTSecret, "boc-auth", "boc")