feat: rank growth events with grounded evidence

This commit is contained in:
2026-08-22 18:44:59 +08:00
parent 7f9c16be4b
commit 1a8a61cfe7
20 changed files with 2555 additions and 132 deletions

View File

@@ -7,7 +7,7 @@
## Objective
Give an authenticated merchant a trustworthy 90-day Persian growth outlook: historical event wins, upcoming similar occasions, likely low-performance windows, privacy-safe peer context, and compact campaign briefs. PostgreSQL and deterministic Go calculations own every number; AvalAI may only prioritize approved evidence and produce qualitative copy.
Give an authenticated merchant a trustworthy 90-day Persian growth outlook: historical event wins, upcoming similar occasions, likely low-performance windows, privacy-safe peer context, and compact campaign briefs. PostgreSQL and deterministic Go calculations own every financial or performance number; AvalAI may only prioritize approved evidence, allocate relative planning attention, and produce qualitative copy.
## Deterministic analytics contract
@@ -15,17 +15,29 @@ Give an authenticated merchant a trustworthy 90-day Persian growth outlook: hist
- Forecast begins the day after the latest complete imported dataset date and covers 90 days. Count and amount are modeled independently from weekday-aware rolling baselines, bounded trend adjustment, catalogued event uplift, and empirical residual P10/P50/P90 values.
- Rolling-origin backtests classify output as `HIGH`, `MEDIUM`, or `LOW`. Limited-history and poorly validated forecasts remain visible as `LOW` with wider uncertainty; only a scope with no historical observations returns `INSUFFICIENT_DATA`.
- Historical event effect compares matching weekdays from the preceding eight non-event weeks. At least four observations and 100 baseline sessions are required. A material win/decline is at least 20%.
- Upcoming similarity prefers an exact event family, then catalog tag similarity of at least 0.60. Return at most five opportunities and three risks. A risk requires a forecast at least 20% below a baseline with at least 100 sessions.
- Upcoming evidence first matches the most recent completed catalog occurrence with the exact same `occasion_key` and `event_variant`. A demand/campaign window must never use an operational/closure window as its own prior experience, even when both relate to the same holiday. Similar-family or tag matches are not presented as exact prior experience.
- Exact historical evidence is usable only when the existing event-effect completeness, observation, and baseline-volume thresholds pass. When it is absent or suppressed, reasoning falls back to current onboarding context: business category, business description/niches, and sales channels matched against reviewed catalog categories, channels, themes, and tags.
- Upcoming similarity beyond the exact-evidence decision may use catalog tag similarity of at least 0.60. Return at most five opportunities and three risks. A risk requires a forecast at least 20% below a baseline with at least 100 sessions.
- Merchant scope may include peer context only under the existing threshold of 10 other same-category merchants and 1,000 combined peer sessions, with one equally weighted value per merchant. Terminal scope always suppresses peers.
- The versioned catalog stores Persian title, Gregorian date range, family, tags, source, supported merchant categories, and official/commercial classification. The initial reviewed catalog covers the imported history and the following 90 days.
- The versioned catalog stores Persian title, Gregorian date range, explicit year-free `occasion_key`, `event_variant`, reviewed Jalali occurrence year, tags, source, supported merchant categories, and official/commercial classification. Coverage declarations must contain at least one dated event for every declared Jalali year. Repeated occurrences use the same canonical key, and one canonical key/variant may occur at most once per Jalali year. Campaign and operational windows for one holiday share the key and differ by variant.
- Product-defined analytical windows must cite their reviewed internal window policy and describe the derivation; an external source that establishes only the occasion must not be presented as evidence for a custom date range. A catalog cannot be approved or activated while its source checksum is a review placeholder.
## Ranked event-priority contract
- Return exactly three recommendations when at least three reviewed candidates exist. Each has rank order, `priorityPercent`, and stance `PROMOTE`, `PREPARE`, or `CAUTION`.
- `priorityPercent` is a positive integer share of the merchant's suggested planning attention. It is not a forecast, probability, predicted uplift, expected sales share, or guarantee. The three shares descend with rank and sum to exactly 100.
- Eligible exact historical evidence takes precedence over profile reasoning. Its deterministic prior window, observation count, paid-session uplift BPS, paid-amount uplift BPS, and material direction are displayed separately from AI prose.
- A negative historical result may still receive high planning priority because it warrants preparation or caution. Rank therefore measures attention, while stance communicates whether the recommended response is promotion, preparation, or caution.
- Without eligible exact history, show a visible `BUSINESS_PROFILE` basis and an insufficient-history explanation. Do not imply that profile relevance is the merchant's prior performance.
## AI boundary
- AvalAI is backend-only and receives five sanitized, chronologically upcoming events from the active reviewed calendar plus non-identifying merchant category/niche/channel context. It never receives raw transactions, payer keys, credentials, merchant ids, SQL, or peer rows.
- AvalAI is backend-only and receives five sanitized, chronologically upcoming events from the active reviewed calendar, minimized deterministic evidence summaries, and non-identifying merchant category/niche/channel context. It never receives raw transactions, payer keys, credentials, merchant ids, SQL, or peer rows.
- The consumer-owned `GrowthAnalyticsTools` boundary exposes only `get_growth_context`, `get_event_evidence`, `get_peer_context`, and `get_campaign_playbook`. Scope is injected from the authenticated job, not accepted from model arguments.
- Use `POST /v1/responses`, `store:false`, strict function schemas, strict structured output, `parallel_tool_calls:false`, no more than four tool calls and two model turns, 1,600 output tokens, a 30-second deadline, and one transient retry.
- Treat model output as untrusted. Accept only known evidence ids and qualitative Persian fields. Reject numeric claims, unsafe language, unknown references, refusal/incomplete output, or schema drift and use deterministic Persian fallback copy.
- The model selects exactly three distinct candidate evidence ids and returns one short qualitative Persian relevance reason per event; it cannot introduce dates, quantities, or event facts. Invalid output or provider failure produces `PARTIAL` with the first three chronological reviewed events and deterministic reasons.
- Treat model output as untrusted. Accept only known evidence ids, the bounded stance enum, integer planning-priority fields, and qualitative Persian reasons. Reject unsafe language, unknown references, refusal/incomplete output, schema drift, evidence-basis mismatch, or numeric performance claims in prose.
- The model selects exactly three distinct candidate evidence ids and returns one short qualitative Persian relevance reason, stance, and positive integer `priorityPercent` per event. The collection is valid only when shares descend with rank and sum to 100. The structured percentage field is planning preference, not a model-authored financial metric.
- Invalid output or provider failure produces `PARTIAL` using deterministic ranking: eligible historical evidence first, then reviewed catalog importance/relevance/confidence and profile matches. Largest-remainder normalization yields positive integer shares totaling 100; equal scores resolve to `34/33/33`. Deterministic evidence supplies stance and reason fallback, and verified historical metrics remain unchanged.
## Persistence, caching, jobs, and limits
@@ -43,13 +55,13 @@ Give an authenticated merchant a trustworthy 90-day Persian growth outlook: hist
- `GET /api/v1/analytics/growth-reports/latest?terminalId=...` returns the latest authorized scope report.
- `GET /api/v1/analytics/growth-reports/{reportId}` returns an owned report; absent and cross-tenant ids are indistinguishable.
- `GET /api/v1/analytics/growth-reports/{reportId}/events` is authenticated SSE with stable ids, heartbeat, replay/snapshot recovery, and terminal status.
- Responses include the five deterministic calendar candidates, exactly three validated AI-ranked event recommendations (or deterministic fallback), cache metadata, and next refresh time.
- Responses include the five deterministic calendar candidates, exactly three validated AI-ranked event recommendations (or deterministic fallback), their evidence basis and planning-priority shares, cache metadata, and next refresh time.
## Boundaries and acceptance
- Always derive merchant identity from the server session, validate terminal ownership, use parameterized SQL/context deadlines, keep numbers deterministic, and preserve useful prior output on failure.
- Never add chat, memory, notifications, automated campaigns/actions, runtime web search, raw-data tools, or model-authored numerical predictions.
- Tests cover event/baseline/forecast/backtest/risk/peer logic, fingerprints, Tehran refresh boundaries, authorization, persistence, locks/deduplication, budgets, AI schemas/rejections/timeouts, fallback, SSE, and sensitive-field absence.
- Tests cover exact occasion/variant matching, historical thresholds and profile fallback, event/baseline/forecast/backtest/risk/peer logic, fingerprints, Tehran refresh boundaries, authorization, persistence, locks/deduplication, budgets, AI percentage sum/order/uniqueness validation, evidence-basis rejection, deterministic fallback, SSE, and sensitive-field absence.
- Feature is disabled by default behind `AI_GROWTH_ENABLED`. Production enablement requires explicit AvalAI key/model/request budget/token budget configuration.
## Sources

View File

@@ -5,15 +5,30 @@
## Context
Growth forecasting, event uplift, low-period detection, and peer comparisons must be reproducible, testable, privacy-safe, and available when an AI provider is unavailable. AvalAI adds value for semantic prioritization and natural Persian campaign guidance, but must not become the source of financial analytics.
Growth forecasting, event uplift, low-period detection, and peer comparisons must be reproducible, testable, privacy-safe, and available when an AI provider is unavailable. Merchants also need a ranked view of three upcoming occasions and a personalized explanation grounded in their own comparable history when it exists. AvalAI adds value for semantic prioritization and natural Persian campaign guidance, but must not become the source of financial analytics or present general business-profile relevance as prior experience.
## Decision
Use PostgreSQL plus deterministic Go analytics as the sole authority for numbers and evidence. Run a bounded backend-only AvalAI Responses workflow over sanitized aggregates, validate its structured qualitative output, and persist report snapshots by a versioned data fingerprint. Use Redis only for hot caching, coordination, budgets, and rate limits. Defer MCP; keep a narrow internal tool interface that an MCP adapter may wrap only when multiple independent agent clients exist.
For event recommendations, use this evidence precedence:
1. Select the most recent completed catalog occurrence with the exact same stable `occasion_key` and `event_variant` as the upcoming event.
2. Accept its deterministic effect only when the established completeness, observation-count, and baseline-session thresholds pass.
3. Otherwise use current onboarding category, description/niches, and sales-channel matches as visibly labeled business-profile evidence.
Do not substitute an operational holiday window for a demand/campaign window, or a similar-family event for an exact occurrence. Deterministic code owns prior-window dates, counts, money, uplift BPS, material direction, and all other financial/performance metrics.
AvalAI may rank three known candidate evidence ids and return an integer `priorityPercent`, stance, and qualitative Persian reason. The percentages represent relative planning attention only, never probability, predicted growth, uplift, expected revenue, or a guarantee. Validation requires three distinct known ids, positive shares descending with rank, and a sum of exactly 100. AI prose remains prohibited from introducing numeric performance claims and cannot claim profile evidence when eligible historical evidence exists.
On invalid output, provider failure, or budget exhaustion, deterministic ranking prefers eligible historical evidence and then reviewed catalog importance/relevance/confidence plus profile matches. Largest-remainder normalization guarantees a 100-percent collection (`34/33/33` for equal scores); deterministic evidence also supplies stance and Persian fallback reasoning.
## Consequences
- The growth page remains useful with AI disabled, unavailable, rate-limited, or rejected.
- Repeated page visits do not create paid calls while the fingerprint is unchanged.
- Algorithm, catalog, prompt, schema, model, or import changes intentionally invalidate the report.
- The backend owns more deterministic forecasting and validation code, but the result is auditable and tenant-safe.
- A high planning share may represent either an opportunity or a risk requiring preparation; stance carries that distinction.
- Reviewed event catalogs must use year-free canonical occasion keys, distinct variants for campaign and operational windows, and honest per-year coverage so historical matching remains auditable.
- Campaign and operational variants of the same holiday share one canonical occasion key. Product-defined date windows cite a reviewed internal derivation policy rather than attributing the custom range to a source that only documents the occasion, and review-placeholder checksums block approval and activation.

View File

@@ -1528,25 +1528,80 @@ components:
expectedSessions: {$ref: '#/components/schemas/GrowthRange'}
expectedAmountRial: {$ref: '#/components/schemas/GrowthRange'}
historicalEvidenceId: {type: string}
GrowthUpcomingEvent:
GrowthHistoricalEventEvidence:
type: object
additionalProperties: false
required: [evidenceId, eventId, eventTitleFa, date, eventType, officialHoliday, merchantCategories, behavioralChannels]
required: [priorStartDate, priorEndDate, observationCount, paidSessionUpliftBps, amountUpliftBps, materialWin, materialDecline]
properties:
priorStartDate: {type: string, format: date-time}
priorEndDate: {type: string, format: date-time}
observationCount: {type: integer, minimum: 0, description: Number of eligible comparison windows used to establish the baseline.}
paidSessionUpliftBps: {type: integer, format: int64}
amountUpliftBps: {type: integer, format: int64}
materialWin: {type: boolean}
materialDecline: {type: boolean}
GrowthProfileEventEvidence:
type: object
additionalProperties: false
required: [matchedSignals]
properties:
matchedSignals:
type: array
items: {type: string, minLength: 1}
historyUnavailableReason: {$ref: '#/components/schemas/GrowthHistoryUnavailableReason'}
GrowthHistoryUnavailableReason:
type: string
enum: [INSUFFICIENT_BASELINE_OBSERVATIONS, INSUFFICIENT_BASELINE_SESSIONS, INSUFFICIENT_HISTORY, INCOMPLETE_EVENT_WINDOW, INSUFFICIENT_COHORT, TERMINAL_SCOPE_UNSUPPORTED]
GrowthUpcomingEventCommon:
type: object
required: [evidenceId, eventId, occasionKey, eventVariant, eventTitleFa, date, endDate, eventType, officialHoliday, merchantCategories, behavioralChannels]
properties:
evidenceId: {type: string}
eventId: {type: string}
occasionKey: {type: string, minLength: 1}
eventVariant: {type: string, enum: [PRIMARY, CAMPAIGN_WINDOW, OPERATIONAL_WINDOW]}
eventTitleFa: {type: string}
date: {type: string, format: date-time}
endDate: {type: string, format: date-time}
eventType: {type: string}
officialHoliday: {type: boolean}
merchantCategories: {type: array, items: {type: string}}
behavioralChannels: {type: array, items: {type: string}}
GrowthUpcomingHistoricalEvent:
allOf:
- {$ref: '#/components/schemas/GrowthUpcomingEventCommon'}
- type: object
required: [insightBasis, historicalEvidence]
properties:
insightBasis: {type: string, enum: [HISTORICAL]}
historicalEvidence: {$ref: '#/components/schemas/GrowthHistoricalEventEvidence'}
GrowthUpcomingProfileEvent:
allOf:
- {$ref: '#/components/schemas/GrowthUpcomingEventCommon'}
- type: object
required: [insightBasis, profileEvidence]
properties:
insightBasis: {type: string, enum: [BUSINESS_PROFILE]}
profileEvidence: {$ref: '#/components/schemas/GrowthProfileEventEvidence'}
GrowthUpcomingEvent:
oneOf:
- {$ref: '#/components/schemas/GrowthUpcomingHistoricalEvent'}
- {$ref: '#/components/schemas/GrowthUpcomingProfileEvent'}
discriminator:
propertyName: insightBasis
mapping:
HISTORICAL: '#/components/schemas/GrowthUpcomingHistoricalEvent'
BUSINESS_PROFILE: '#/components/schemas/GrowthUpcomingProfileEvent'
GrowthEventRecommendation:
type: object
additionalProperties: false
required: [evidenceId, relevanceReason]
description: Exactly three recommendations are returned in descending priority order. Their positive integer priorityPercent values sum to 100.
required: [evidenceId, priorityPercent, stance, insightBasis, relevanceReason]
properties:
evidenceId: {type: string}
priorityPercent: {type: integer, minimum: 1, maximum: 98}
stance: {type: string, enum: [PROMOTE, PREPARE, CAUTION]}
insightBasis: {type: string, enum: [HISTORICAL, BUSINESS_PROFILE]}
relevanceReason: {type: string, minLength: 1, maxLength: 320}
GrowthRisk:
type: object
@@ -1609,6 +1664,7 @@ components:
category: {type: string}
niches: {type: array, items: {type: string}}
salesChannels: {type: array, items: {type: string}}
businessDescriptions: {type: array, items: {type: string}}
GrowthReport:
type: object
additionalProperties: false
@@ -1628,7 +1684,12 @@ components:
additionalProperties: false
required: [eventRecommendations]
properties:
eventRecommendations: {type: array, minItems: 3, maxItems: 3, items: {$ref: '#/components/schemas/GrowthEventRecommendation'}}
eventRecommendations:
type: array
minItems: 3
maxItems: 3
description: Ranked in descending priorityPercent order; the three percentages sum to exactly 100.
items: {$ref: '#/components/schemas/GrowthEventRecommendation'}
failureCode: {type: string}
cached: {type: boolean}
stale: {type: boolean}