ci / test (push) Successful in 2m12s
- Add RoutingWorkflow: generic state machine executor for WorkflowSpec - Add LLM Router: natural language → WorkflowSpec generation - Add RetrieveMemoryActivity: query poimen-memory for context - Add activities: AnalyzeCode, SecurityScan, GenerateReport, Notify, etc. - Add agent-prompts/router: LLM prompt documentation - Extend starter with --route flag for routing workflows - Remove orchestrator job (trigger via API/message instead) - Clean up: move docs to Desktop, add .gitignore for *.md
276 lines
7.9 KiB
Go
276 lines
7.9 KiB
Go
package routing
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
)
|
|
|
|
// KnowledgeBase represents the activity knowledge base
|
|
type KnowledgeBase struct {
|
|
Version string `json:"version"`
|
|
Activities []ActivityMetadata `json:"activities"`
|
|
Metadata KnowledgeBaseMetadata `json:"metadata"`
|
|
|
|
// Index for fast lookups
|
|
byName map[string]*ActivityMetadata
|
|
}
|
|
|
|
// KnowledgeBaseMetadata tracks KB metadata
|
|
type KnowledgeBaseMetadata struct {
|
|
TotalActivities int `json:"totalActivities"`
|
|
LastUpdated string `json:"lastUpdated"`
|
|
Categories map[string]int `json:"categories"`
|
|
}
|
|
|
|
// LoadKnowledgeBase loads the activity knowledge base from a JSON file
|
|
func LoadKnowledgeBase(filePath string) (*KnowledgeBase, error) {
|
|
// Read file
|
|
data, err := ioutil.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read knowledge base file: %w", err)
|
|
}
|
|
|
|
// Parse JSON
|
|
var kb KnowledgeBase
|
|
err = json.Unmarshal(data, &kb)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse knowledge base JSON: %w", err)
|
|
}
|
|
|
|
// Build index
|
|
kb.byName = make(map[string]*ActivityMetadata)
|
|
for i := range kb.Activities {
|
|
kb.byName[kb.Activities[i].Name] = &kb.Activities[i]
|
|
}
|
|
|
|
return &kb, nil
|
|
}
|
|
|
|
// LoadKnowledgeBaseFromDefaultPath loads KB from default location
|
|
// Looks for activity_knowledge_base.json in same directory as caller
|
|
func LoadKnowledgeBaseFromDefaultPath() (*KnowledgeBase, error) {
|
|
// Try to find from package directory
|
|
execDir, err := os.Executable()
|
|
if err == nil {
|
|
// Try in same directory as binary
|
|
path := filepath.Join(filepath.Dir(execDir), "activity_knowledge_base.json")
|
|
if _, err := os.Stat(path); err == nil {
|
|
return LoadKnowledgeBase(path)
|
|
}
|
|
}
|
|
|
|
// Try from current working directory
|
|
if _, err := os.Stat("activity_knowledge_base.json"); err == nil {
|
|
return LoadKnowledgeBase("activity_knowledge_base.json")
|
|
}
|
|
|
|
// Try from internal/routing directory relative to cwd
|
|
if _, err := os.Stat("internal/routing/activity_knowledge_base.json"); err == nil {
|
|
return LoadKnowledgeBase("internal/routing/activity_knowledge_base.json")
|
|
}
|
|
|
|
// Try from parent directory (for tests running from tests/ dir)
|
|
if _, err := os.Stat("../internal/routing/activity_knowledge_base.json"); err == nil {
|
|
return LoadKnowledgeBase("../internal/routing/activity_knowledge_base.json")
|
|
}
|
|
|
|
// Try using runtime to find package directory
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if ok {
|
|
pkgDir := filepath.Dir(filename)
|
|
path := filepath.Join(pkgDir, "activity_knowledge_base.json")
|
|
if _, err := os.Stat(path); err == nil {
|
|
return LoadKnowledgeBase(path)
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("activity_knowledge_base.json not found in any expected location")
|
|
}
|
|
|
|
// GetActivity returns metadata for a specific activity
|
|
func (kb *KnowledgeBase) GetActivity(name string) *ActivityMetadata {
|
|
return kb.byName[name]
|
|
}
|
|
|
|
// ListActivities returns all activities
|
|
func (kb *KnowledgeBase) ListActivities() []ActivityMetadata {
|
|
return kb.Activities
|
|
}
|
|
|
|
// ListActivitiesByCategory returns all activities in a category
|
|
func (kb *KnowledgeBase) ListActivitiesByCategory(category string) []ActivityMetadata {
|
|
var result []ActivityMetadata
|
|
for _, activity := range kb.Activities {
|
|
if activity.Category == category {
|
|
result = append(result, activity)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetActivityNames returns all activity names
|
|
func (kb *KnowledgeBase) GetActivityNames() []string {
|
|
names := make([]string, len(kb.Activities))
|
|
for i, activity := range kb.Activities {
|
|
names[i] = activity.Name
|
|
}
|
|
return names
|
|
}
|
|
|
|
// HasActivity checks if an activity exists
|
|
func (kb *KnowledgeBase) HasActivity(name string) bool {
|
|
_, exists := kb.byName[name]
|
|
return exists
|
|
}
|
|
|
|
// GetDependencies returns all dependencies for an activity
|
|
func (kb *KnowledgeBase) GetDependencies(activityName string) []string {
|
|
activity := kb.GetActivity(activityName)
|
|
if activity == nil {
|
|
return []string{}
|
|
}
|
|
return activity.Constraints.Dependencies
|
|
}
|
|
|
|
// GetTimeoutForActivity returns the timeout for an activity
|
|
func (kb *KnowledgeBase) GetTimeoutForActivity(activityName string) string {
|
|
activity := kb.GetActivity(activityName)
|
|
if activity == nil {
|
|
return "5m" // Default timeout
|
|
}
|
|
return activity.Constraints.DefaultTimeout
|
|
}
|
|
|
|
// GetRetryPolicyForActivity returns retry configuration for an activity
|
|
func (kb *KnowledgeBase) GetRetryPolicyForActivity(activityName string) *RetryPolicy {
|
|
activity := kb.GetActivity(activityName)
|
|
if activity == nil {
|
|
return &RetryPolicy{
|
|
MaxAttempts: 1,
|
|
BackoffRate: 1.0,
|
|
InitialInterval: "1s",
|
|
}
|
|
}
|
|
|
|
return &RetryPolicy{
|
|
MaxAttempts: int32(activity.Constraints.RecommendedRetries),
|
|
BackoffRate: activity.Constraints.RetryBackoff,
|
|
InitialInterval: "1s",
|
|
MaxInterval: "30s",
|
|
}
|
|
}
|
|
|
|
// IsFlaky returns whether an activity is marked as flaky
|
|
func (kb *KnowledgeBase) IsFlaky(activityName string) bool {
|
|
activity := kb.GetActivity(activityName)
|
|
if activity == nil {
|
|
return false
|
|
}
|
|
return activity.Constraints.IsFlaky
|
|
}
|
|
|
|
// GetNotes returns implementation notes for an activity
|
|
func (kb *KnowledgeBase) GetNotes(activityName string) string {
|
|
activity := kb.GetActivity(activityName)
|
|
if activity == nil {
|
|
return ""
|
|
}
|
|
return activity.Constraints.Notes
|
|
}
|
|
|
|
// Validate checks the knowledge base for consistency
|
|
func (kb *KnowledgeBase) Validate() error {
|
|
// Check for circular dependencies
|
|
visited := make(map[string]bool)
|
|
for _, activity := range kb.Activities {
|
|
if err := kb.checkDependencies(activity.Name, visited, []string{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check that all dependencies exist
|
|
for _, activity := range kb.Activities {
|
|
for _, dep := range activity.Constraints.Dependencies {
|
|
if !kb.HasActivity(dep) {
|
|
return fmt.Errorf("activity %s depends on non-existent activity %s", activity.Name, dep)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkDependencies validates activity dependencies for cycles
|
|
func (kb *KnowledgeBase) checkDependencies(activityName string, visited map[string]bool, path []string) error {
|
|
// Check for cycles
|
|
for _, p := range path {
|
|
if p == activityName {
|
|
cycleStr := ""
|
|
found := false
|
|
for _, n := range path {
|
|
if found {
|
|
cycleStr += " -> " + n
|
|
}
|
|
if n == activityName {
|
|
found = true
|
|
cycleStr += n
|
|
}
|
|
}
|
|
cycleStr += " -> " + activityName
|
|
return fmt.Errorf("circular dependency detected: %s", cycleStr)
|
|
}
|
|
}
|
|
|
|
if visited[activityName] {
|
|
return nil // Already checked this branch
|
|
}
|
|
|
|
visited[activityName] = true
|
|
newPath := append(path, activityName)
|
|
|
|
activity := kb.GetActivity(activityName)
|
|
if activity == nil {
|
|
return nil // Non-existent activity will be caught elsewhere
|
|
}
|
|
|
|
for _, dep := range activity.Constraints.Dependencies {
|
|
if err := kb.checkDependencies(dep, visited, newPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// String returns a human-readable description of the knowledge base
|
|
func (kb *KnowledgeBase) String() string {
|
|
return fmt.Sprintf("KnowledgeBase(v%s, %d activities)", kb.Version, kb.Metadata.TotalActivities)
|
|
}
|
|
|
|
// PrintSummary prints a summary of available activities
|
|
func (kb *KnowledgeBase) PrintSummary() string {
|
|
summary := fmt.Sprintf("=== Activity Knowledge Base ===\nVersion: %s\nTotal Activities: %d\n\n", kb.Version, kb.Metadata.TotalActivities)
|
|
|
|
summary += "Activities by Category:\n"
|
|
for category, count := range kb.Metadata.Categories {
|
|
summary += fmt.Sprintf(" %s: %d\n", category, count)
|
|
}
|
|
|
|
summary += "\nActivity Details:\n"
|
|
for _, activity := range kb.Activities {
|
|
summary += fmt.Sprintf("\n[%s] %s\n", activity.Name, activity.Description)
|
|
summary += fmt.Sprintf(" Category: %s\n", activity.Category)
|
|
summary += fmt.Sprintf(" Timeout: %s\n", activity.Constraints.DefaultTimeout)
|
|
summary += fmt.Sprintf(" Flaky: %v (Retries: %d)\n", activity.Constraints.IsFlaky, activity.Constraints.RecommendedRetries)
|
|
if len(activity.Constraints.Dependencies) > 0 {
|
|
summary += fmt.Sprintf(" Dependencies: %v\n", activity.Constraints.Dependencies)
|
|
}
|
|
}
|
|
|
|
return summary
|
|
}
|