package routing import ( "context" "fmt" ) // LLMProvider defines interface for LLM services type LLMProvider interface { // Name returns provider name (e.g., "openai", "claude", "local") Name() string // Chat sends a message and returns response Chat(ctx context.Context, systemPrompt, userPrompt string) (string, error) // IsAvailable checks if provider is configured and reachable IsAvailable(ctx context.Context) error } // ProviderRegistry manages available LLM providers type ProviderRegistry struct { providers map[string]LLMProvider default_ string } // NewProviderRegistry creates a new registry func NewProviderRegistry() *ProviderRegistry { return &ProviderRegistry{ providers: make(map[string]LLMProvider), } } // Register adds a provider func (pr *ProviderRegistry) Register(provider LLMProvider) error { if provider.Name() == "" { return fmt.Errorf("provider name cannot be empty") } pr.providers[provider.Name()] = provider return nil } // SetDefault sets the default provider func (pr *ProviderRegistry) SetDefault(name string) error { if _, exists := pr.providers[name]; !exists { return fmt.Errorf("provider %s not registered", name) } pr.default_ = name return nil } // Get retrieves a provider by name func (pr *ProviderRegistry) Get(name string) (LLMProvider, error) { if name == "" { name = pr.default_ } provider, exists := pr.providers[name] if !exists { return nil, fmt.Errorf("provider %s not found", name) } return provider, nil } // GetDefault returns the default provider func (pr *ProviderRegistry) GetDefault() (LLMProvider, error) { if pr.default_ == "" { return nil, fmt.Errorf("no default provider set") } return pr.Get(pr.default_) } // RoutingProviderLLM routes between multiple LLM providers with fallback type RoutingProviderLLM struct { registry *ProviderRegistry fallbackOrder []string } // NewRoutingProviderLLM creates a routing LLM func NewRoutingProviderLLM(registry *ProviderRegistry, order ...string) *RoutingProviderLLM { return &RoutingProviderLLM{ registry: registry, fallbackOrder: order, } } // Chat tries providers in order func (rp *RoutingProviderLLM) Chat(ctx context.Context, systemPrompt, userPrompt string) (string, error) { for _, providerName := range rp.fallbackOrder { provider, err := rp.registry.Get(providerName) if err != nil { continue } if err := provider.IsAvailable(ctx); err != nil { continue } response, err := provider.Chat(ctx, systemPrompt, userPrompt) if err == nil { return response, nil } } return "", fmt.Errorf("all LLM providers failed") } // CachingLLMProvider wraps a provider with caching type CachingLLMProvider struct { provider LLMProvider cache map[string]string } // NewCachingLLMProvider creates a cached provider func NewCachingLLMProvider(provider LLMProvider) *CachingLLMProvider { return &CachingLLMProvider{ provider: provider, cache: make(map[string]string), } } // Chat returns cached response if available func (clp *CachingLLMProvider) Chat(ctx context.Context, systemPrompt, userPrompt string) (string, error) { key := systemPrompt + "|" + userPrompt if cached, exists := clp.cache[key]; exists { return cached, nil } response, err := clp.provider.Chat(ctx, systemPrompt, userPrompt) if err != nil { return "", err } clp.cache[key] = response return response, nil } // IsAvailable delegates to wrapped provider func (clp *CachingLLMProvider) IsAvailable(ctx context.Context) error { return clp.provider.IsAvailable(ctx) } // Name delegates to wrapped provider func (clp *CachingLLMProvider) Name() string { return clp.provider.Name() + "-cached" } // RetryingLLMProvider wraps a provider with retry logic type RetryingLLMProvider struct { provider LLMProvider maxRetries int backoffFunc func(attempt int) interface{} } // NewRetryingLLMProvider creates a retrying provider func NewRetryingLLMProvider(provider LLMProvider, maxRetries int) *RetryingLLMProvider { return &RetryingLLMProvider{ provider: provider, maxRetries: maxRetries, backoffFunc: func(attempt int) interface{} { // Exponential backoff: 1s, 2s, 4s... return 1 << uint(attempt) }, } } // Chat retries on failure func (rlp *RetryingLLMProvider) Chat(ctx context.Context, systemPrompt, userPrompt string) (string, error) { var lastErr error for attempt := 0; attempt <= rlp.maxRetries; attempt++ { response, err := rlp.provider.Chat(ctx, systemPrompt, userPrompt) if err == nil { return response, nil } lastErr = err } return "", fmt.Errorf("failed after %d retries: %w", rlp.maxRetries, lastErr) } // IsAvailable delegates to wrapped provider func (rlp *RetryingLLMProvider) IsAvailable(ctx context.Context) error { return rlp.provider.IsAvailable(ctx) } // Name delegates to wrapped provider func (rlp *RetryingLLMProvider) Name() string { return rlp.provider.Name() + "-retrying" } // PromptTemplate defines a reusable prompt structure type PromptTemplate interface { // Render creates a prompt from values Render(values map[string]interface{}) (string, error) } // SimplePromptTemplate uses Go text/template syntax type SimplePromptTemplate struct { template string } // NewSimplePromptTemplate creates a simple template func NewSimplePromptTemplate(template string) *SimplePromptTemplate { return &SimplePromptTemplate{template: template} } // Render renders the template (placeholder implementation) func (spt *SimplePromptTemplate) Render(values map[string]interface{}) (string, error) { // In real implementation, use text/template return spt.template, nil } // PromptBuilder builds prompts from components type PromptBuilder struct { system string sections []string } // NewPromptBuilder creates a new builder func NewPromptBuilder() *PromptBuilder { return &PromptBuilder{ sections: []string{}, } } // System sets the system prompt func (pb *PromptBuilder) System(prompt string) *PromptBuilder { pb.system = prompt return pb } // AddSection adds a prompt section func (pb *PromptBuilder) AddSection(title, content string) *PromptBuilder { if title != "" { pb.sections = append(pb.sections, fmt.Sprintf("## %s\n%s", title, content)) } else { pb.sections = append(pb.sections, content) } return pb } // Build returns the complete prompt func (pb *PromptBuilder) Build() (system, user string) { user = "" for i, section := range pb.sections { if i > 0 { user += "\n\n" } user += section } return pb.system, user }