feat: RoutingWorkflow + LLM Router + Memory Activity
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
This commit is contained in:
Test
2026-09-02 19:21:53 -07:00
parent 5a465b145c
commit a0e64224a7
74 changed files with 3950 additions and 12421 deletions
+143 -3
View File
@@ -2,9 +2,11 @@ package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"strings"
"time"
@@ -13,20 +15,27 @@ import (
"github.com/rockliang/poimen/workflows/internal/config"
"github.com/rockliang/poimen/workflows/internal/health"
"github.com/rockliang/poimen/workflows/internal/logging"
"github.com/rockliang/poimen/workflows/internal/routing"
"github.com/rockliang/poimen/workflows/statemachine"
)
func main() {
var (
// Orchestrator flags
repoPath = flag.String("repo", "", "target repo path")
remoteURL = flag.String("remote", "", "remote URL")
milestone = flag.String("milestone", "T0", "milestone ID")
dryRun = flag.Bool("dry-run", false, "disable git push/merge")
dryRun = flag.Bool("dry-run", false, "disable git push/merge (orchestrator) or skip submit (routing)")
plannerModel = flag.String("planner-model", "reasoning", "planner model ID (local-llm)")
judgeModel = flag.String("judge-model", "reasoning", "judge model ID (local-llm)")
implementerModel = flag.String("implementer-model", "ornith:35b", "implementer model ID (local-llm ornith)")
piProvider = flag.String("pi-provider", "local-llm", "pi provider name for skills (local-llm)")
healthCheck = flag.Bool("health", false, "check health and exit")
// Routing workflow flags
routeMsg = flag.String("route", "", "natural language message for LLM routing")
specFile = flag.String("spec", "", "JSON workflow spec file (direct submit, skip LLM)")
cronSpec = flag.Bool("cron", false, "treat spec as CronWorkflowSpec")
)
flag.Parse()
@@ -66,9 +75,15 @@ func main() {
return
}
// Validate required flags for workflow start
// Handle routing workflow mode
if *routeMsg != "" || *specFile != "" {
runRoutingWorkflow(c, *routeMsg, *specFile, *cronSpec, *dryRun)
return
}
// Validate required flags for orchestrator workflow
if *repoPath == "" || *remoteURL == "" {
logging.Fatal("--repo and --remote flags are required")
logging.Fatal("--repo and --remote flags are required (or use --route/--spec for routing workflow)")
}
@@ -157,3 +172,128 @@ func main() {
fmt.Printf("\nWorkflow completed: %+v\n", result)
}
}
// runRoutingWorkflow handles --route and --spec flags
func runRoutingWorkflow(c client.Client, routeMsg, specFile string, isCron, dryRun bool) {
ctx := context.Background()
var spec *routing.WorkflowSpec
var cronSpec *routing.CronWorkflowSpec
if specFile != "" {
// Load spec from file
data, err := os.ReadFile(specFile)
if err != nil {
logging.Fatal("failed to read spec file", logging.Err(err))
}
validator := routing.NewValidator(nil) // nil KB = skip activity validation
if isCron {
cronSpec = &routing.CronWorkflowSpec{}
if err := json.Unmarshal(data, cronSpec); err != nil {
logging.Fatal("failed to parse cron spec", logging.Err(err))
}
// Validate
result := validator.ValidateCronWorkflowSpec(cronSpec)
if !result.Valid {
logging.Fatal("invalid cron spec", logging.String("errors", result.String()))
}
} else {
spec = &routing.WorkflowSpec{}
if err := json.Unmarshal(data, spec); err != nil {
logging.Fatal("failed to parse spec", logging.Err(err))
}
// Validate
result := validator.ValidateWorkflowSpec(spec)
if !result.Valid {
logging.Fatal("invalid spec", logging.String("errors", result.String()))
}
}
} else {
// Use LLM router
logging.Info("routing message via LLM", logging.String("message", routeMsg))
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
if err != nil {
logging.Fatal("failed to load knowledge base", logging.Err(err))
}
router, err := routing.NewLLMRouter(kb)
if err != nil {
logging.Fatal("failed to create LLM router", logging.Err(err))
}
output, err := router.Route(ctx, routing.LLMRouterInput{Message: routeMsg})
if err != nil {
logging.Fatal("LLM routing failed", logging.Err(err))
}
if output.IsCron {
cronSpec = output.CronSpec
fmt.Printf("\n=== Generated Cron Spec ===\n")
fmt.Printf("Name: %s\n", cronSpec.Name)
fmt.Printf("Schedule: %s\n", cronSpec.Schedule)
fmt.Printf("States: %d\n", len(cronSpec.States))
} else {
spec = output.Spec
fmt.Printf("\n=== Generated Workflow Spec ===\n")
fmt.Printf("Name: %s\n", spec.Name)
fmt.Printf("States: %d\n", len(spec.States))
}
}
if dryRun {
fmt.Printf("\n[dry-run] Spec generated but not submitted\n")
if spec != nil {
data, _ := json.MarshalIndent(spec, "", " ")
fmt.Printf("%s\n", data)
} else if cronSpec != nil {
data, _ := json.MarshalIndent(cronSpec, "", " ")
fmt.Printf("%s\n", data)
}
return
}
// Submit to Temporal
if cronSpec != nil {
// For cron, we'd use Temporal's schedule feature
// For now, just start as regular workflow (cron scheduling TBD)
spec = &routing.WorkflowSpec{
Name: cronSpec.Name,
Input: cronSpec.Input,
States: cronSpec.States,
}
logging.Warn("cron scheduling not yet implemented, running as one-shot workflow")
}
workflowID := "routing-" + spec.Name + "-" + time.Now().Format("20060102-150405")
input := statemachine.RoutingWorkflowInput{Spec: spec}
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: workflowID,
TaskQueue: "poimen-taskqueue",
}, statemachine.RoutingWorkflow, input)
if err != nil {
logging.Fatal("failed to start routing workflow", logging.Err(err))
}
fmt.Printf("\n=== Routing Workflow Started ===\n")
fmt.Printf("Workflow ID: %s\n", workflowID)
fmt.Printf("Run ID: %s\n", run.GetRunID())
// Wait briefly for result
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var result statemachine.RoutingWorkflowOutput
if err := run.Get(waitCtx, &result); err != nil {
fmt.Printf("\nWorkflow running (check Temporal UI for status)\n")
} else {
fmt.Printf("\nWorkflow completed: %s\n", result.Status)
if len(result.StepResults) > 0 {
for step, res := range result.StepResults {
fmt.Printf(" %s: %v\n", step, res)
}
}
}
}
+20
View File
@@ -51,6 +51,7 @@ func main() {
w.RegisterWorkflow(statemachine.OrchestratorWorkflow)
w.RegisterWorkflow(statemachine.TaskUnitWorkflow)
w.RegisterWorkflow(statemachine.TestWorkflow)
w.RegisterWorkflow(statemachine.RoutingWorkflow)
// Register all activities
w.RegisterActivity(action.CloneRepoActivity)
@@ -68,6 +69,25 @@ func main() {
// w.RegisterActivity(action.UpdateLessonsActivity)
// w.RegisterActivity(action.ReadLessonsActivity)
// Routing workflow activities
w.RegisterActivity(action.LLMRouterActivity)
w.RegisterActivity(action.ValidateWorkflowSpecActivity)
w.RegisterActivity(action.ValidateCronWorkflowSpecActivity)
// Analysis activities
w.RegisterActivity(action.AnalyzeCodeActivity)
w.RegisterActivity(action.SecurityScanActivity)
w.RegisterActivity(action.GenerateReportActivity)
// Notification and utility activities
w.RegisterActivity(action.NotifyStatusActivity)
w.RegisterActivity(action.ArchiveResultsActivity)
w.RegisterActivity(action.DeploymentPreCheckActivity)
w.RegisterActivity(action.ApproveWorkflowActivity)
// Memory activities
w.RegisterActivity(action.RetrieveMemoryActivity)
// Initialize health checker
healthChecker := health.NewChecker(c)
healthHandler := health.NewHandler(healthChecker)