feat: add configurable AgentRouter AI provider
This commit is contained in:
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
var ErrDisabled = errors.New("growth report: disabled")
|
||||
|
||||
const MaxProviderRequests int64 = 4
|
||||
const MaxProviderRequests int64 = maxAvalAIAttempts * maxAvalAIProviderTurns
|
||||
|
||||
type ServiceConfig struct {
|
||||
Enabled bool
|
||||
@@ -120,14 +120,18 @@ func (s *Service) fingerprint(merchantID int64, terminalID, watermark, catalogVe
|
||||
type Processor struct {
|
||||
repository *Repository
|
||||
builder Builder
|
||||
avalai *AvalAIClient
|
||||
generator CopyGenerator
|
||||
coordinator *Coordinator
|
||||
aiEnabled bool
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewProcessor(repository *Repository, builder Builder, avalai *AvalAIClient, coordinator *Coordinator, aiEnabled bool) *Processor {
|
||||
return &Processor{repository: repository, builder: builder, avalai: avalai, coordinator: coordinator, aiEnabled: aiEnabled, now: time.Now}
|
||||
type CopyGenerator interface {
|
||||
GenerateWithTools(context.Context, json.RawMessage, KnownEvidence, GrowthAnalyticsTools) (AICopy, Usage, error)
|
||||
}
|
||||
|
||||
func NewProcessor(repository *Repository, builder Builder, generator CopyGenerator, coordinator *Coordinator, aiEnabled bool) *Processor {
|
||||
return &Processor{repository: repository, builder: builder, generator: generator, coordinator: coordinator, aiEnabled: aiEnabled, now: time.Now}
|
||||
}
|
||||
func (p *Processor) ProcessNext(ctx context.Context) error {
|
||||
record, err := p.repository.Claim(ctx)
|
||||
@@ -153,19 +157,19 @@ func (p *Processor) ProcessNext(ctx context.Context) error {
|
||||
}
|
||||
status := StatusPartial
|
||||
failure := "AI_DISABLED_OR_UNAVAILABLE"
|
||||
if analysis.ShouldCallAI && p.aiEnabled && p.avalai != nil {
|
||||
if analysis.ShouldCallAI && p.aiEnabled && p.generator != nil {
|
||||
reservedTokens := EstimatedTokenReservation(analysis.Encoded)
|
||||
release, enterErr := p.coordinator.Enter(ctx)
|
||||
if enterErr == nil {
|
||||
defer release()
|
||||
if err := p.coordinator.ReserveBudget(ctx, MaxProviderRequests, reservedTokens, p.now()); err == nil {
|
||||
generated, usage, aiErr := p.avalai.GenerateWithTools(ctx, analysis.Encoded, analysis.KnownEvidence, NewEvidenceTools(analysis))
|
||||
generated, usage, aiErr := p.generator.GenerateWithTools(ctx, analysis.Encoded, analysis.KnownEvidence, NewEvidenceTools(analysis))
|
||||
usage.ReservedTokens = reservedTokens
|
||||
if aiErr == nil {
|
||||
slog.Info("growth AI generation complete", "report_id", record.ReportID, "provider_requests", usage.ProviderRequests, "reserved_tokens", usage.ReservedTokens, "input_tokens", usage.InputTokens, "output_tokens", usage.OutputTokens, "latency_ms", usage.LatencyMS, "estimated_cost", usage.EstimatedCost, "provider_request_id", usage.ProviderRequestID)
|
||||
return p.repository.CompleteClaimed(ctx, record.ReportID, record.ClaimToken, StatusReady, analysis.Encoded, &generated, usage, "")
|
||||
}
|
||||
slog.Warn("growth AI output rejected", "report_id", record.ReportID, "reason", "AI_REJECTED", "provider_requests", usage.ProviderRequests, "reserved_tokens", usage.ReservedTokens, "input_tokens", usage.InputTokens, "output_tokens", usage.OutputTokens, "latency_ms", usage.LatencyMS, "estimated_cost", usage.EstimatedCost, "provider_request_id", usage.ProviderRequestID)
|
||||
slog.Warn("growth AI output rejected", "report_id", record.ReportID, "reason", "AI_REJECTED", "error", aiErr, "provider_requests", usage.ProviderRequests, "reserved_tokens", usage.ReservedTokens, "input_tokens", usage.InputTokens, "output_tokens", usage.OutputTokens, "latency_ms", usage.LatencyMS, "estimated_cost", usage.EstimatedCost, "provider_request_id", usage.ProviderRequestID)
|
||||
failure = "AI_REJECTED"
|
||||
return p.repository.CompleteClaimed(ctx, record.ReportID, record.ClaimToken, status, analysis.Encoded, analysis.Fallback, usage, failure)
|
||||
} else {
|
||||
@@ -190,12 +194,12 @@ func deterministicCompletion(hasRecommendations bool) (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
|
||||
// outputs, and every turn's 1,600-token output ceiling.
|
||||
// EstimatedTokenReservation reserves for two complete attempts of the bounded
|
||||
// tool workflow before any network call. Bytes are deliberately over-counted
|
||||
// and the fixed allowance covers schemas, instructions, accumulated tool
|
||||
// outputs, and every provider turn's configured output ceiling.
|
||||
func EstimatedTokenReservation(evidence json.RawMessage) int64 {
|
||||
return int64(12*len(evidence) + 32000)
|
||||
return int64(30*len(evidence) + 64000 + int(MaxProviderRequests)*maxAvalAIOutputTokens)
|
||||
}
|
||||
|
||||
func newReportID() string {
|
||||
|
||||
Reference in New Issue
Block a user