194 lines
5.6 KiB
Go
194 lines
5.6 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"time"
|
|
|
|
"supply-intelligence/internal/domain"
|
|
"supply-intelligence/internal/integration"
|
|
)
|
|
|
|
// SchedulerTrigger defines how discovery is invoked
|
|
type SchedulerTrigger int
|
|
|
|
const (
|
|
TriggerManual SchedulerTrigger = iota
|
|
TriggerScheduled
|
|
TriggerNewAccount
|
|
)
|
|
|
|
// SupplierAdapterRegistry holds all registered platform adapters
|
|
type SupplierAdapterRegistry struct {
|
|
adapters map[string]integration.SupplierAdapter
|
|
}
|
|
|
|
func NewSupplierAdapterRegistry() *SupplierAdapterRegistry {
|
|
return &SupplierAdapterRegistry{adapters: make(map[string]integration.SupplierAdapter)}
|
|
}
|
|
|
|
func (r *SupplierAdapterRegistry) Register(adapter integration.SupplierAdapter) {
|
|
r.adapters[adapter.Platform()] = adapter
|
|
}
|
|
|
|
func (r *SupplierAdapterRegistry) Get(platform string) (integration.SupplierAdapter, bool) {
|
|
adapter, ok := r.adapters[platform]
|
|
return adapter, ok
|
|
}
|
|
|
|
func (r *SupplierAdapterRegistry) ListPlatforms() []string {
|
|
platforms := make([]string, 0, len(r.adapters))
|
|
for p := range r.adapters {
|
|
platforms = append(platforms, p)
|
|
}
|
|
return platforms
|
|
}
|
|
|
|
// ScanResult holds the outcome of a platform scan
|
|
type ScanResult struct {
|
|
Platform string
|
|
NewModels int
|
|
RemovedModels []string // models that were in candidates but not in supplier list
|
|
Errors []string
|
|
}
|
|
|
|
// DiscoveryScheduler orchestrates periodic and on-demand discovery scans
|
|
type DiscoveryScheduler struct {
|
|
service *Service
|
|
registry *SupplierAdapterRegistry
|
|
repo AccountLister
|
|
now func() time.Time
|
|
}
|
|
|
|
// AccountLister is implemented by repository.Repository
|
|
type AccountLister interface {
|
|
ListActiveAccounts(ctx context.Context) []domain.AccountRoutingState
|
|
ListSupplyAccountsByPlatform(ctx context.Context, platform string) []domain.SupplyAccount
|
|
}
|
|
|
|
func NewDiscoveryScheduler(service *Service, registry *SupplierAdapterRegistry, repo AccountLister) *DiscoveryScheduler {
|
|
return &DiscoveryScheduler{
|
|
service: service,
|
|
registry: registry,
|
|
repo: repo,
|
|
now: func() time.Time { return time.Now().UTC() },
|
|
}
|
|
}
|
|
|
|
// ScanAllPlatforms runs discovery across all registered platforms
|
|
func (s *DiscoveryScheduler) ScanAllPlatforms(ctx context.Context) ([]ScanResult, error) {
|
|
platforms := s.registry.ListPlatforms()
|
|
results := make([]ScanResult, 0, len(platforms))
|
|
|
|
for _, platform := range platforms {
|
|
result, err := s.ScanPlatform(ctx, platform)
|
|
if err != nil {
|
|
results = append(results, ScanResult{Platform: platform, Errors: []string{err.Error()}})
|
|
continue
|
|
}
|
|
results = append(results, *result)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// ScanPlatform runs discovery for a single platform
|
|
func (s *DiscoveryScheduler) ScanPlatform(ctx context.Context, platform string) (*ScanResult, error) {
|
|
adapter, ok := s.registry.Get(platform)
|
|
if !ok {
|
|
return nil, ErrPlatformNotSupported
|
|
}
|
|
|
|
result := &ScanResult{Platform: platform}
|
|
|
|
// Get models from the platform
|
|
// In production these accounts come from the database; here we accept a map for injection
|
|
accounts := s.loadAccountsForPlatform(ctx, platform)
|
|
if len(accounts) == 0 {
|
|
log.Printf("[discovery] no accounts registered for platform %s, skipping", platform)
|
|
return result, nil
|
|
}
|
|
|
|
// Use the first account as the source of models (in production would fan out)
|
|
account := accounts[0]
|
|
models, err := adapter.GetModels(ctx, account)
|
|
if err != nil {
|
|
result.Errors = append(result.Errors, "GetModels: "+err.Error())
|
|
return result, err
|
|
}
|
|
|
|
log.Printf("[discovery] platform=%s found %d models", platform, len(models))
|
|
|
|
// Record each model as a candidate
|
|
for _, model := range models {
|
|
candidateInput := RecordCandidateInput{
|
|
CandidateID: platform + "-" + model.ModelID,
|
|
AccountID: account.AccountID,
|
|
Platform: platform,
|
|
Model: model.ModelID,
|
|
Source: "official_api",
|
|
DiscoveredAt: s.now(),
|
|
}
|
|
out, err := s.service.RecordCandidate(ctx, candidateInput)
|
|
if err != nil {
|
|
result.Errors = append(result.Errors, "RecordCandidate: "+err.Error())
|
|
continue
|
|
}
|
|
if out.Created {
|
|
result.NewModels++
|
|
log.Printf("[discovery] new candidate: platform=%s model=%s", platform, model.ModelID)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// loadAccountsForPlatform returns supplier accounts for a platform
|
|
func (s *DiscoveryScheduler) loadAccountsForPlatform(ctx context.Context, platform string) []integration.SupplierAccount {
|
|
if s.repo == nil {
|
|
// Fallback: return a default account when repo is not configured
|
|
return []integration.SupplierAccount{
|
|
{AccountID: 1, Platform: platform, APIKey: "", BaseURL: defaultBaseURL(platform)},
|
|
}
|
|
}
|
|
// Prefer supply_accounts (has API key)
|
|
supplyAccounts := s.repo.ListSupplyAccountsByPlatform(ctx, platform)
|
|
if len(supplyAccounts) > 0 {
|
|
accounts := make([]integration.SupplierAccount, 0, len(supplyAccounts))
|
|
for _, acc := range supplyAccounts {
|
|
accounts = append(accounts, integration.SupplierAccount{
|
|
AccountID: acc.AccountID,
|
|
Platform: acc.Platform,
|
|
APIKey: acc.APIKey,
|
|
BaseURL: defaultBaseURL(platform),
|
|
})
|
|
}
|
|
return accounts
|
|
}
|
|
// Fallback: routing states (API key may be empty)
|
|
allAccounts := s.repo.ListActiveAccounts(ctx)
|
|
var accounts []integration.SupplierAccount
|
|
for _, acc := range allAccounts {
|
|
if acc.Platform == platform {
|
|
accounts = append(accounts, integration.SupplierAccount{
|
|
AccountID: acc.AccountID,
|
|
Platform: acc.Platform,
|
|
APIKey: acc.APIKey,
|
|
BaseURL: defaultBaseURL(platform),
|
|
})
|
|
}
|
|
}
|
|
return accounts
|
|
}
|
|
|
|
func defaultBaseURL(platform string) string {
|
|
switch platform {
|
|
case "openai":
|
|
return "https://api.openai.com"
|
|
case "anthropic":
|
|
return "https://api.anthropic.com"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|