f259610f8c
- 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
426 lines
11 KiB
Go
426 lines
11 KiB
Go
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))
|
|
}
|