Files
poimen-workflows/internal/routing/spec_builder.go
T
Test 0d70da4f31
ci / test (push) Failing after 3m24s
refactor: make routing system extensible with provider/builder interfaces
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: ...})
2026-09-03 09:17:38 -07:00

281 lines
6.8 KiB
Go

package routing
import (
"fmt"
)
// SpecBuilder defines interface for building workflow specs
type SpecBuilder interface {
// FromIntent builds spec from an analyzed intent
FromIntent(intent *Intent, metadata *BuildMetadata) (*WorkflowSpec, error)
// Validate checks if builder can build this intent
Validate(intent *Intent) error
}
// BuildMetadata contains metadata for spec building
type BuildMetadata struct {
KnowledgeBase *KnowledgeBase
Context map[string]interface{}
Validators []WorkflowValidator
}
// DefaultSpecBuilder implements basic spec building
type DefaultSpecBuilder struct {
kb *KnowledgeBase
}
// NewDefaultSpecBuilder creates a builder
func NewDefaultSpecBuilder(kb *KnowledgeBase) *DefaultSpecBuilder {
return &DefaultSpecBuilder{kb: kb}
}
// Validate checks if intent is buildable
func (dsb *DefaultSpecBuilder) Validate(intent *Intent) error {
if len(intent.Activities) == 0 {
return fmt.Errorf("intent has no activities")
}
for _, actName := range intent.Activities {
if !dsb.kb.HasActivity(actName) {
return fmt.Errorf("activity %s not found", actName)
}
}
return nil
}
// FromIntent builds spec from intent
func (dsb *DefaultSpecBuilder) FromIntent(intent *Intent, metadata *BuildMetadata) (*WorkflowSpec, error) {
if err := dsb.Validate(intent); err != nil {
return nil, err
}
states := make([]State, 0, len(intent.Activities)+1)
// Build states for each activity
for i, actName := range intent.Activities {
act := dsb.kb.GetActivity(actName)
state := State{
Name: actName,
Type: StateTypeTask,
Resource: actName,
Parameters: dsb.buildParameters(act, intent, i),
Timeout: act.Constraints.DefaultTimeout,
Retry: dsb.buildRetryPolicy(act, intent),
}
// Set next state
if i < len(intent.Activities)-1 {
state.Next = intent.Activities[i+1]
} else {
state.End = true
}
// Add catch for flaky activities
if act.Constraints.IsFlaky && intent.ErrorHandling != "fail-fast" {
state.Catch = []CatchClause{
{
ErrorEquals: []string{"ActivityError", "TimeoutError"},
Next: "HandleError",
},
}
}
states = append(states, state)
}
// Add error handler if needed
if dsb.hasFlaky(intent) && intent.ErrorHandling != "fail-fast" {
states = append(states, State{
Name: "HandleError",
Type: StateTypeFail,
Error: "ActivityFailed",
Cause: "One or more activities failed",
})
}
// Build input map
inputMap := make(map[string]interface{})
for k, v := range intent.Parameters {
inputMap[k] = v
}
if metadata != nil && metadata.Context != nil {
for k, v := range metadata.Context {
if _, exists := inputMap[k]; !exists {
inputMap[k] = v
}
}
}
spec := &WorkflowSpec{
Name: intent.WorkflowName,
Input: inputMap,
States: states,
}
// Validate if validators provided
if metadata != nil && len(metadata.Validators) > 0 {
for _, v := range metadata.Validators {
if err := v.Validate(spec); err != nil {
return nil, err
}
}
}
return spec, nil
}
// buildParameters creates parameters for activity
func (dsb *DefaultSpecBuilder) buildParameters(act *ActivityMetadata, intent *Intent, stateIndex int) map[string]interface{} {
params := make(map[string]interface{})
resolver := &paramResolver{
intent: intent,
kb: dsb.kb,
}
if stateIndex > 0 {
resolver.prevState = intent.Activities[stateIndex-1]
}
for name, def := range act.Inputs {
if val := resolver.resolve(name, def); val != nil {
params[name] = val
}
}
return params
}
// buildRetryPolicy creates retry policy
func (dsb *DefaultSpecBuilder) buildRetryPolicy(act *ActivityMetadata, intent *Intent) *RetryPolicy {
if intent.ErrorHandling == "fail-fast" {
return &RetryPolicy{
MaxAttempts: 1,
BackoffRate: 1.0,
InitialInterval: "1s",
}
}
return &RetryPolicy{
MaxAttempts: int32(act.Constraints.RecommendedRetries),
BackoffRate: act.Constraints.RetryBackoff,
InitialInterval: "1s",
MaxInterval: "30s",
}
}
// hasFlaky checks if any activity is flaky
func (dsb *DefaultSpecBuilder) hasFlaky(intent *Intent) bool {
for _, actName := range intent.Activities {
act := dsb.kb.GetActivity(actName)
if act != nil && act.Constraints.IsFlaky {
return true
}
}
return false
}
// CronSpecBuilder builds cron workflow specs
type CronSpecBuilder struct {
regularBuilder SpecBuilder
}
// NewCronSpecBuilder creates a cron builder
func NewCronSpecBuilder(regularBuilder SpecBuilder) *CronSpecBuilder {
return &CronSpecBuilder{
regularBuilder: regularBuilder,
}
}
// Validate checks if intent is valid for cron
func (csb *CronSpecBuilder) Validate(intent *Intent) error {
if !intent.IsCron {
return fmt.Errorf("intent is not marked as cron")
}
if intent.CronSchedule == "" {
return fmt.Errorf("cron schedule is empty")
}
return csb.regularBuilder.Validate(intent)
}
// FromIntent builds cron spec
func (csb *CronSpecBuilder) FromIntent(intent *Intent, metadata *BuildMetadata) (*WorkflowSpec, error) {
spec, err := csb.regularBuilder.FromIntent(intent, metadata)
if err != nil {
return nil, err
}
// In production, wrap with cron metadata
// For now, just return the regular spec
return spec, nil
}
// SpecBuilderFactory creates appropriate spec builders
type SpecBuilderFactory struct {
kb *KnowledgeBase
}
// NewSpecBuilderFactory creates a factory
func NewSpecBuilderFactory(kb *KnowledgeBase) *SpecBuilderFactory {
return &SpecBuilderFactory{kb: kb}
}
// CreateBuilder creates appropriate builder for intent
func (sbf *SpecBuilderFactory) CreateBuilder(intent *Intent) (SpecBuilder, error) {
if intent.IsCron {
return NewCronSpecBuilder(NewDefaultSpecBuilder(sbf.kb)), nil
}
return NewDefaultSpecBuilder(sbf.kb), nil
}
// CompositeSpecBuilder combines multiple builders with fallback
type CompositeSpecBuilder struct {
builders []SpecBuilder
}
// NewCompositeSpecBuilder creates a composite builder
func NewCompositeSpecBuilder(builders ...SpecBuilder) *CompositeSpecBuilder {
return &CompositeSpecBuilder{builders: builders}
}
// Validate tries each builder
func (csb *CompositeSpecBuilder) Validate(intent *Intent) error {
var lastErr error
for _, builder := range csb.builders {
if err := builder.Validate(intent); err == nil {
return nil
} else {
lastErr = err
}
}
if lastErr != nil {
return lastErr
}
return fmt.Errorf("no builder could validate intent")
}
// FromIntent tries each builder
func (csb *CompositeSpecBuilder) FromIntent(intent *Intent, metadata *BuildMetadata) (*WorkflowSpec, error) {
var lastErr error
for _, builder := range csb.builders {
if spec, err := builder.FromIntent(intent, metadata); err == nil {
return spec, nil
} else {
lastErr = err
}
}
if lastErr != nil {
return nil, lastErr
}
return nil, fmt.Errorf("no builder could create spec")
}