ci / test (push) Failing after 3m24s
BREAKING: LLMRouter now requires explicit LLMProvider
New Abstractions:
- LLMProvider interface: swap providers (OpenAI, Claude, local, etc)
- SpecBuilder interface: custom spec generation strategies
- ParameterBinder interface: flexible parameter resolution
- ActivityExecutor interface: pluggable activity execution
- WorkflowValidator interface: composable validation
Provider System:
- ProviderRegistry: manage multiple LLM providers
- RoutingProviderLLM: fallback across providers
- CachingLLMProvider: caching wrapper
- RetryingLLMProvider: retry wrapper
Spec Building:
- DefaultSpecBuilder: basic spec generation
- CronSpecBuilder: cron workflow specialization
- SpecBuilderFactory: builder selection
- CompositeSpecBuilder: multi-strategy fallback
- BuildMetadata: context for builders
Validators:
- StateGraphValidator: DAG structure
- ActivityAvailabilityValidator: activity existence
- TimeoutValidator: timeout format
- CompositeValidator: multiple validators
- TransitionValidator: state transitions
Refactored Components:
- LLMRouter: config-driven, provider-agnostic
- LLMClient: now implements LLMProvider
- llm_router.go: 97 fewer lines (delegated to builders)
Migration Path:
OLD: NewLLMRouter(kb)
NEW: NewLLMRouter(LLMRouterConfig{Provider: ..., KB: ...})
253 lines
6.4 KiB
Go
253 lines
6.4 KiB
Go
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
|
|
}
|