feat(agent): connect agents to real BOC data
BOC CI/CD / Test (push) Failing after 17s
BOC CI/CD / Security Scan (push) Has been skipped
BOC CI/CD / Build & Push (push) Has been skipped
BOC CI/CD / Deploy to Staging (push) Has been skipped
BOC CI/CD / Deploy to Production (push) Has been skipped

- Add AgentDataProvider with real DB queries for finance, CRM, sales, HR
- Inject live data into Anthropic prompts for context-aware responses
- Reuse existing ledgerDB connection for agent data access
- Agents now respond with actual numbers from the system
This commit is contained in:
Bernt
2026-08-12 09:24:47 +00:00
parent 0124fc0186
commit f259610f8c
5 changed files with 445 additions and 5 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+17 -2
View File
@@ -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
+425
View File
@@ -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))
}
+3 -3
View File
@@ -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")