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

@@ -8,6 +8,8 @@ import (
type Module string
type Section string
const (
ModuleOverview Module = "overview"
ModulePayments Module = "payments"
@@ -17,10 +19,28 @@ const (
ModuleGrowth Module = "growth"
)
const (
SectionFull Section = "full"
SectionPast Section = "past"
SectionLatestObserved Section = "latest-observed"
SectionFuture7Days Section = "future-7-days"
SectionFuture30Days Section = "future-30-days"
SectionFuture90Days Section = "future-90-days"
SectionFindings Section = "findings"
SectionOptions Section = "options"
)
var ErrNotFound = errors.New("ai insight: not found")
var ErrUnknownTerminal = errors.New("ai insight: unknown terminal")
var ErrRefreshLimited = errors.New("ai insight: refresh limited")
var ErrRefreshCooldown = errors.New("ai insight: refresh cooldown")
var ErrIdempotencyConflict = errors.New("ai insight: idempotency conflict")
var ErrOnboardingRequired = errors.New("ai insight: onboarding required")
type RefreshCooldownError struct{ NextRefreshAt time.Time }
func (e *RefreshCooldownError) Error() string { return "ai insight: refresh cooldown" }
func (e *RefreshCooldownError) Unwrap() error { return ErrRefreshCooldown }
func ParseModule(value string) (Module, bool) {
module := Module(value)
@@ -32,6 +52,18 @@ func ParseModule(value string) (Module, bool) {
}
}
func ParseSection(module Module, value string) (Section, bool) {
section := Section(value)
switch section {
case SectionPast, SectionLatestObserved, SectionFuture7Days, SectionFuture30Days, SectionFindings, SectionOptions:
return section, true
case SectionFuture90Days:
return section, module == ModuleGrowth
default:
return "", false
}
}
type Confidence string
const (
@@ -83,8 +115,44 @@ type Copy struct {
Options []Option `json:"options"`
}
func MergeSection(base, generated Copy, section Section) (Copy, error) {
switch section {
case SectionFull:
return generated, nil
case SectionPast:
base.Past = generated.Past
case SectionLatestObserved:
base.LatestObserved = generated.LatestObserved
case SectionFuture7Days:
base.Future7Days = generated.Future7Days
case SectionFuture30Days:
base.Future30Days = generated.Future30Days
case SectionFuture90Days:
if generated.Future90Days == nil {
return Copy{}, errors.New("ai insight: future 90 day section unavailable")
}
base.Future90Days = generated.Future90Days
case SectionFindings:
base.Findings = generated.Findings
case SectionOptions:
base.Options = generated.Options
default:
return Copy{}, errors.New("ai insight: invalid section")
}
return base, nil
}
type Scope struct {
TerminalID *string `json:"terminalId"`
PeriodID *string `json:"periodId,omitempty"`
}
type PeriodScope struct {
PeriodID string
PeriodType string
StartAt time.Time
EndAt time.Time
JalaliLabelFA string
}
type RefreshState struct {
@@ -95,27 +163,30 @@ type RefreshState struct {
}
type Snapshot struct {
InsightID string `json:"insightId"`
Module Module `json:"module"`
Revision int `json:"revision"`
Status string `json:"status"`
Scope Scope `json:"scope"`
AsOfDate string `json:"asOfDate"`
DataThroughDate string `json:"dataThroughDate"`
ForecastStart string `json:"forecastStartDate"`
Freshness Freshness `json:"freshness"`
DataLagDays int `json:"dataLagDays"`
Confidence Confidence `json:"confidence"`
Copy Copy `json:"copy"`
Evidence json.RawMessage `json:"evidence"`
CalendarContext []CalendarContext `json:"calendarContext"`
RefreshState RefreshState `json:"refreshState"`
GeneratedAt time.Time `json:"generatedAt"`
InsightID string `json:"insightId"`
Module Module `json:"module"`
Revision int `json:"revision"`
Status string `json:"status"`
Scope Scope `json:"scope"`
AsOfDate string `json:"asOfDate"`
DataThroughDate string `json:"dataThroughDate"`
ForecastStart string `json:"forecastStartDate"`
Freshness Freshness `json:"freshness"`
DataLagDays int `json:"dataLagDays"`
Confidence Confidence `json:"confidence"`
Copy Copy `json:"copy"`
Evidence json.RawMessage `json:"evidence"`
CalendarContext []CalendarContext `json:"calendarContext"`
RefreshState RefreshState `json:"refreshState"`
SectionRefresh map[Section]RefreshState `json:"sectionRefreshState"`
GeneratedAt time.Time `json:"generatedAt"`
}
type Job struct {
JobID string `json:"jobId"`
SnapshotID string `json:"snapshotId"`
Module Module `json:"module"`
Section Section `json:"sectionId"`
State string `json:"state"`
EventsURL string `json:"eventsUrl"`
CreatedAt time.Time `json:"createdAt"`
@@ -133,6 +204,7 @@ type CreateRequest struct {
MerchantID int64
TerminalID string
Module Module
Section Section
TriggerReason string
ManualRefresh bool
IdempotencyKey string
@@ -142,4 +214,23 @@ type CreateRequest struct {
Confidence Confidence
CatalogVersion string
Evidence json.RawMessage
Period PeriodScope
}
type ProviderUsage struct {
InputTokens int64
OutputTokens int64
ProviderRequests int64
LatencyMS int64
ProviderRequestID string
EstimatedCost string
}
type Completion struct {
Analysis Analysis
AICopy *Copy
Status string
FailureCode string
Model string
Usage ProviderUsage
}