ultisuite-backend/internal/ai/providers.go
R3D347HR4Y 0466a1c169
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
wow
2026-06-11 01:22:52 +02:00

188 lines
4.9 KiB
Go

package ai
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/ultisuite/ulti-backend/internal/llm"
)
const orgSettingsSingletonID = 1
type orgLLMPolicy struct {
DefaultProviderID string `json:"default_provider_id"`
Providers []llm.Provider `json:"providers"`
EnforceOrgProviders bool `json:"enforce_org_providers"`
AllowUserOverride bool `json:"allow_user_override"`
ContactDiscoveryModel string `json:"contact_discovery_model,omitempty"`
}
func LoadEffectiveLLMSettings(ctx context.Context, db *pgxpool.Pool, externalUserID string) (llm.Settings, error) {
if db == nil {
return llm.Settings{}, fmt.Errorf("database unavailable")
}
org, err := loadOrgLLMPolicy(ctx, db)
if err != nil {
return llm.Settings{}, err
}
user, err := loadUserLLMSettings(ctx, db, externalUserID)
if err != nil {
return llm.Settings{}, err
}
if org.EnforceOrgProviders && len(org.Providers) > 0 {
if !org.AllowUserOverride {
return orgToSettings(org), nil
}
merged := orgToSettings(org)
if strings.TrimSpace(user.DefaultProviderID) != "" {
merged.DefaultProviderID = user.DefaultProviderID
}
if strings.TrimSpace(user.ContactDiscoveryModel) != "" {
merged.ContactDiscoveryModel = user.ContactDiscoveryModel
}
if strings.TrimSpace(user.ContactDiscoveryProvider) != "" {
merged.ContactDiscoveryProvider = user.ContactDiscoveryProvider
}
return merged, nil
}
if len(user.Providers) > 0 {
return user, nil
}
if len(org.Providers) > 0 {
return orgToSettings(org), nil
}
return user, nil
}
func orgToSettings(org orgLLMPolicy) llm.Settings {
return llm.Settings{
DefaultProviderID: org.DefaultProviderID,
Providers: org.Providers,
ContactDiscoveryModel: org.ContactDiscoveryModel,
ContactDiscoveryProvider: org.DefaultProviderID,
}
}
func loadOrgLLMPolicy(ctx context.Context, db *pgxpool.Pool) (orgLLMPolicy, error) {
var raw []byte
err := db.QueryRow(ctx, `
SELECT settings->'llm' FROM org_settings WHERE id = $1
`, orgSettingsSingletonID).Scan(&raw)
if err != nil {
if err == pgx.ErrNoRows {
return orgLLMPolicy{}, nil
}
return orgLLMPolicy{}, err
}
if len(raw) == 0 || string(raw) == "null" {
return orgLLMPolicy{}, nil
}
var out orgLLMPolicy
if err := json.Unmarshal(raw, &out); err != nil {
return orgLLMPolicy{}, err
}
return out, nil
}
func loadUserLLMSettings(ctx context.Context, db *pgxpool.Pool, externalUserID string) (llm.Settings, error) {
var raw []byte
err := db.QueryRow(ctx, `
SELECT COALESCE(s.preferences->'llm', '{}'::jsonb)
FROM users u
LEFT JOIN settings s ON s.user_id = u.id
WHERE u.external_id = $1
`, externalUserID).Scan(&raw)
if err != nil {
if err == pgx.ErrNoRows {
return llm.Settings{}, nil
}
return llm.Settings{}, err
}
var out llm.Settings
if len(raw) > 0 {
if err := json.Unmarshal(raw, &out); err != nil {
return llm.Settings{}, err
}
}
return out, nil
}
func LoadAssistantPolicy(ctx context.Context, db *pgxpool.Pool) (AssistantPolicy, error) {
defaults := AssistantPolicy{
Enabled: false,
PublicPath: "/ai",
EmbedDefaultTemporary: true,
EnabledTools: []string{"mail", "drive", "contacts", "search"},
ChatSyncEnabled: true,
ChatNCPath: "/.ultimail/ai/chats",
}
if db == nil {
return defaults, nil
}
var raw []byte
err := db.QueryRow(ctx, `
SELECT settings->'ai_assistant' FROM org_settings WHERE id = $1
`, orgSettingsSingletonID).Scan(&raw)
if err != nil {
if err == pgx.ErrNoRows {
return defaults, nil
}
return defaults, err
}
if len(raw) == 0 || string(raw) == "null" {
return defaults, nil
}
var stored AssistantPolicy
if err := json.Unmarshal(raw, &stored); err != nil {
return defaults, err
}
if stored.PublicPath == "" {
stored.PublicPath = defaults.PublicPath
}
if stored.ChatNCPath == "" {
stored.ChatNCPath = defaults.ChatNCPath
}
if len(stored.EnabledTools) == 0 {
stored.EnabledTools = defaults.EnabledTools
}
return stored, nil
}
func LoadQuotaLimits(ctx context.Context, db *pgxpool.Pool) (QuotaLimits, error) {
defaults := QuotaLimits{RequestsPerDay: 100, TokensPerMonth: 500_000}
if db == nil {
return defaults, nil
}
var raw []byte
err := db.QueryRow(ctx, `
SELECT settings->'usage_quotas' FROM org_settings WHERE id = $1
`, orgSettingsSingletonID).Scan(&raw)
if err != nil {
if err == pgx.ErrNoRows {
return defaults, nil
}
return defaults, err
}
if len(raw) == 0 || string(raw) == "null" {
return defaults, nil
}
var stored map[string]any
if err := json.Unmarshal(raw, &stored); err != nil {
return defaults, err
}
if v, ok := stored["llm_requests_per_day"].(float64); ok && v > 0 {
defaults.RequestsPerDay = int(v)
}
if v, ok := stored["llm_tokens_per_month"].(float64); ok && v > 0 {
defaults.TokensPerMonth = int64(v)
}
return defaults, nil
}