package ai import ( "context" "log" "git.georgsen.dk/hwlab/internal/inventory" ) // Orchestrator manages the three-tier AI pipeline. // Tier1 is local oMLX (fast, low cost). Tier2 is OpenRouter (slower, better). // Tier3 (Lab Advisor) is out of scope for Phase 2. type Orchestrator struct { tier1 AIClient tier2 AIClient threshold float64 // confidence threshold for escalation; default 0.75 } // NewOrchestrator creates an Orchestrator. Both tier1 and tier2 must be non-nil. func NewOrchestrator(tier1, tier2 AIClient, threshold float64) *Orchestrator { if threshold <= 0 { threshold = 0.75 } return &Orchestrator{tier1: tier1, tier2: tier2, threshold: threshold} } // Analyze runs tier1 and escalates to tier2 if confidence is below threshold. // Never returns an error from individual tier failures — tier errors cause escalation. // Returns a non-nil IntakeResult in all cases (may be zero-value on total failure). // The returned CatalogStatus is either StatusIndexed or StatusNeedsResearch. func (o *Orchestrator) Analyze(ctx context.Context, req IntakeRequest) (*IntakeResult, inventory.CatalogStatus, error) { result, err := o.tier1.AnalyzePhotos(ctx, req) if err != nil { log.Printf("orchestrator: tier1 error (escalating to tier2): %v", err) result = nil } // Escalate if tier1 result is missing, nil, or low confidence if result == nil || result.Confidence < o.threshold { log.Printf("orchestrator: tier1 confidence=%.2f < threshold=%.2f — escalating to tier2", confidenceOf(result), o.threshold) result2, err2 := o.tier2.AnalyzePhotos(ctx, req) if err2 != nil { log.Printf("orchestrator: tier2 error: %v", err2) } else if result2 != nil { result = result2 } } // Map confidence to CatalogStatus if result == nil { return &IntakeResult{ AINotes: "all AI tiers failed", Confidence: 0.0, ConfidenceNote: "no result from any tier", }, inventory.StatusNeedsResearch, nil } status := inventory.StatusIndexed if result.Confidence < o.threshold { status = inventory.StatusNeedsResearch } return result, status, nil } // confidenceOf returns 0.0 for nil results, otherwise result.Confidence. func confidenceOf(r *IntakeResult) float64 { if r == nil { return 0.0 } return r.Confidence }