fix: deliver validated AI insight responses

This commit is contained in:
2026-08-22 15:47:38 +08:00
parent ca55c2fb3d
commit 3fba4261ee
68 changed files with 6870 additions and 493 deletions

View File

@@ -50,14 +50,18 @@ func (s *Service) Create(ctx context.Context, userID, merchantID int64, request
if err != nil {
return Record{}, false, err
}
catalogVersion, err := s.repository.ActiveCatalogVersion(ctx)
if err != nil {
return Record{}, false, err
}
start := tehranToday(now)
fingerprint := s.fingerprint(merchantID, request.TerminalID, watermark, start)
fingerprint := s.fingerprint(merchantID, request.TerminalID, watermark, catalogVersion, start)
var refreshDay *time.Time
if request.Refresh {
day := tehranMidnight(now)
refreshDay = &day
}
record, cached, err := s.repository.CreateOrReuse(ctx, CreateRecord{ID: newReportID(), MerchantID: merchantID, TerminalID: request.TerminalID, Fingerprint: fingerprint, DatasetWatermark: watermark, ForecastStartDate: &start, CatalogVersion: growth.CatalogVersion, AnalyticsVersion: growth.AnalyticsVersion, PromptVersion: s.config.PromptVersion, SchemaVersion: s.config.SchemaVersion, Model: s.config.Model, ManualRefresh: request.Refresh, RefreshDay: refreshDay, IdempotencyKey: idempotencyKey, RequestHash: RequestHash(merchantID, request.TerminalID, request.Refresh)}, func() error {
record, cached, err := s.repository.CreateOrReuse(ctx, CreateRecord{ID: newReportID(), MerchantID: merchantID, TerminalID: request.TerminalID, Fingerprint: fingerprint, DatasetWatermark: watermark, ForecastStartDate: &start, CatalogVersion: catalogVersion, AnalyticsVersion: growth.AnalyticsVersion, PromptVersion: s.config.PromptVersion, SchemaVersion: s.config.SchemaVersion, Model: s.config.Model, ManualRefresh: request.Refresh, RefreshDay: refreshDay, IdempotencyKey: idempotencyKey, RequestHash: RequestHash(merchantID, request.TerminalID, request.Refresh)}, func() error {
if !hasAISummary {
return nil
}
@@ -97,15 +101,20 @@ func (s *Service) Latest(ctx context.Context, merchantID int64, terminalID strin
record.Stale = true
} else {
start := tehranToday(s.now())
record.Stale = record.Fingerprint != s.fingerprint(merchantID, terminalID, watermark, start)
catalogVersion, catalogErr := s.repository.ActiveCatalogVersion(ctx)
if catalogErr != nil {
record.Stale = true
} else {
record.Stale = record.Fingerprint != s.fingerprint(merchantID, terminalID, watermark, catalogVersion, start)
}
}
record.NextRefreshAt, _ = s.repository.NextRefreshAt(ctx, merchantID, terminalID, growth.AnalyticsVersion, s.now())
}
return record, err
}
func (s *Service) fingerprint(merchantID int64, terminalID, watermark string, start time.Time) string {
return growth.Fingerprint(growth.FingerprintInput{MerchantID: merchantID, TerminalID: terminalID, DatasetWatermark: watermark, ForecastStart: start, HorizonDays: 90, CatalogVersion: growth.CatalogVersion, AnalyticsVersion: growth.AnalyticsVersion, PromptVersion: s.config.PromptVersion, SchemaVersion: s.config.SchemaVersion, Model: s.config.Model, ModelConfigVersion: growth.DefaultModelConfigVersion})
func (s *Service) fingerprint(merchantID int64, terminalID, watermark, catalogVersion string, start time.Time) string {
return growth.Fingerprint(growth.FingerprintInput{MerchantID: merchantID, TerminalID: terminalID, DatasetWatermark: watermark, ForecastStart: start, HorizonDays: 90, CatalogVersion: catalogVersion, AnalyticsVersion: growth.AnalyticsVersion, PromptVersion: s.config.PromptVersion, SchemaVersion: s.config.SchemaVersion, Model: s.config.Model, ModelConfigVersion: growth.DefaultModelConfigVersion})
}
type Processor struct {
@@ -138,6 +147,11 @@ func (p *Processor) ProcessNext(ctx context.Context) error {
return err
}
copy := analysis.Fallback
if !analysis.ShouldCallAI {
status, failure := deterministicCompletion()
slog.Info("growth deterministic analysis complete", "report_id", record.ReportID, "reason", "NO_ACTIONABLE_SIGNALS")
return p.repository.CompleteClaimed(ctx, record.ReportID, record.ClaimToken, status, analysis.Encoded, &copy, Usage{}, failure)
}
status := StatusPartial
failure := "AI_DISABLED_OR_UNAVAILABLE"
if analysis.ShouldCallAI && p.aiEnabled && p.avalai != nil {
@@ -164,12 +178,16 @@ func (p *Processor) ProcessNext(ctx context.Context) error {
slog.Warn("growth AI skipped", "report_id", record.ReportID, "reason", failure)
}
}
if !analysis.ShouldCallAI || !p.aiEnabled {
if !p.aiEnabled {
slog.Info("growth deterministic fallback used", "report_id", record.ReportID, "reason", failure)
}
return p.repository.CompleteClaimed(ctx, record.ReportID, record.ClaimToken, status, analysis.Encoded, &copy, Usage{}, failure)
}
func deterministicCompletion() (Status, string) {
return StatusReady, ""
}
// EstimatedTokenReservation reserves for two complete attempts, each with two
// provider turns, before any network call. Bytes are deliberately counted as
// tokens and the fixed allowance covers tool schemas, instructions, tool