- 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:
@@ -0,0 +1,234 @@
|
||||
// Example: External service integrating with Poimen RoutingWorkflow
|
||||
// Shows how to submit a task and wait for completion
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/sdk/client"
|
||||
)
|
||||
|
||||
// ---- Types (mirror internal/routing/types.go) ----
|
||||
|
||||
type WorkflowSpec struct {
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input,omitempty"`
|
||||
States []State `json:"states"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // Task, Pass, Fail
|
||||
Resource string `json:"resource,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
Next string `json:"next,omitempty"`
|
||||
End bool `json:"end,omitempty"`
|
||||
}
|
||||
|
||||
type RoutingWorkflowInput struct {
|
||||
Spec *WorkflowSpec `json:"spec"`
|
||||
}
|
||||
|
||||
type RoutingWorkflowOutput struct {
|
||||
Status string `json:"status"`
|
||||
StepResults map[string]map[string]interface{} `json:"stepResults"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ---- Example Service ----
|
||||
|
||||
type TaskService struct {
|
||||
temporalClient client.Client
|
||||
taskQueue string
|
||||
}
|
||||
|
||||
func NewTaskService(temporalHost, namespace, taskQueue string) (*TaskService, error) {
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: temporalHost,
|
||||
Namespace: namespace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Temporal: %w", err)
|
||||
}
|
||||
|
||||
return &TaskService{
|
||||
temporalClient: c,
|
||||
taskQueue: taskQueue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *TaskService) Close() {
|
||||
s.temporalClient.Close()
|
||||
}
|
||||
|
||||
// SubmitAndWait submits a workflow spec and waits for completion
|
||||
func (s *TaskService) SubmitAndWait(ctx context.Context, spec *WorkflowSpec, timeout time.Duration) (*RoutingWorkflowOutput, error) {
|
||||
workflowID := fmt.Sprintf("%s-%d", spec.Name, time.Now().UnixNano())
|
||||
|
||||
// Start workflow
|
||||
run, err := s.temporalClient.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: s.taskQueue,
|
||||
}, "RoutingWorkflow", RoutingWorkflowInput{Spec: spec})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start workflow: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Workflow started: ID=%s, RunID=%s", run.GetID(), run.GetRunID())
|
||||
|
||||
// Wait for completion with timeout
|
||||
waitCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
var result RoutingWorkflowOutput
|
||||
if err := run.Get(waitCtx, &result); err != nil {
|
||||
return nil, fmt.Errorf("workflow failed: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SubmitAsync submits workflow and returns immediately (fire-and-forget)
|
||||
func (s *TaskService) SubmitAsync(ctx context.Context, spec *WorkflowSpec) (workflowID string, runID string, err error) {
|
||||
workflowID = fmt.Sprintf("%s-%d", spec.Name, time.Now().UnixNano())
|
||||
|
||||
run, err := s.temporalClient.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: s.taskQueue,
|
||||
}, "RoutingWorkflow", RoutingWorkflowInput{Spec: spec})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to start workflow: %w", err)
|
||||
}
|
||||
|
||||
return run.GetID(), run.GetRunID(), nil
|
||||
}
|
||||
|
||||
// WaitForCompletion waits for an existing workflow to complete
|
||||
func (s *TaskService) WaitForCompletion(ctx context.Context, workflowID string, timeout time.Duration) (*RoutingWorkflowOutput, error) {
|
||||
run := s.temporalClient.GetWorkflow(ctx, workflowID, "")
|
||||
|
||||
waitCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
var result RoutingWorkflowOutput
|
||||
if err := run.Get(waitCtx, &result); err != nil {
|
||||
return nil, fmt.Errorf("workflow failed: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetStatus gets current workflow status without waiting
|
||||
func (s *TaskService) GetStatus(ctx context.Context, workflowID string) (string, error) {
|
||||
desc, err := s.temporalClient.DescribeWorkflowExecution(ctx, workflowID, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return desc.WorkflowExecutionInfo.Status.String(), nil
|
||||
}
|
||||
|
||||
// ---- Example Usage ----
|
||||
|
||||
func main() {
|
||||
// Connect to Temporal
|
||||
svc, err := NewTaskService(
|
||||
"temporal-frontend.temporal:7233",
|
||||
"poimen-harness",
|
||||
"poimen-taskqueue",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer svc.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Example 1: Implement task T0.3 with code analysis
|
||||
fmt.Println("=== Example 1: Submit and Wait ===")
|
||||
spec := &WorkflowSpec{
|
||||
Name: "implement-T0.3",
|
||||
Input: map[string]interface{}{
|
||||
"taskId": "T0.3",
|
||||
"description": "Implement git worktree management",
|
||||
"repo": "https://github.com/rockliang/poimen",
|
||||
},
|
||||
States: []State{
|
||||
{
|
||||
Name: "Clone",
|
||||
Type: "Task",
|
||||
Resource: "CloneRepoActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"repo": "${workflow.input.repo}",
|
||||
"branch": "main",
|
||||
},
|
||||
Timeout: "5m",
|
||||
Next: "Analyze",
|
||||
},
|
||||
{
|
||||
Name: "Analyze",
|
||||
Type: "Task",
|
||||
Resource: "AnalyzeCodeActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"path": "${Clone.output.path}",
|
||||
"depth": 3,
|
||||
},
|
||||
Timeout: "10m",
|
||||
Next: "Report",
|
||||
},
|
||||
{
|
||||
Name: "Report",
|
||||
Type: "Task",
|
||||
Resource: "GenerateReportActivity",
|
||||
Parameters: map[string]interface{}{
|
||||
"analysisResult": "${Analyze.output}",
|
||||
"format": "markdown",
|
||||
},
|
||||
Timeout: "2m",
|
||||
End: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := svc.SubmitAndWait(ctx, spec, 30*time.Minute)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Status: %s\n", result.Status)
|
||||
for step, output := range result.StepResults {
|
||||
fmt.Printf(" %s: %v\n", step, output)
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2: Fire and forget, then poll
|
||||
fmt.Println("\n=== Example 2: Async Submit + Poll ===")
|
||||
workflowID, runID, err := svc.SubmitAsync(ctx, spec)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Submitted: workflowID=%s, runID=%s\n", workflowID, runID)
|
||||
|
||||
// Poll status
|
||||
for i := 0; i < 5; i++ {
|
||||
status, _ := svc.GetStatus(ctx, workflowID)
|
||||
fmt.Printf(" Poll %d: status=%s\n", i+1, status)
|
||||
if status == "WORKFLOW_EXECUTION_STATUS_COMPLETED" {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
// Get final result
|
||||
result, err = svc.WaitForCompletion(ctx, workflowID, 30*time.Minute)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
} else {
|
||||
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
||||
fmt.Printf("Final result:\n%s\n", resultJSON)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Example: WaitForTaskComplete pattern
|
||||
// Use case: External service submits implementation task, waits for result
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/sdk/client"
|
||||
)
|
||||
|
||||
// WaitForTaskComplete - the core pattern
|
||||
//
|
||||
// 1. Build workflow spec for the task
|
||||
// 2. Submit to Temporal
|
||||
// 3. Block until completion or timeout
|
||||
// 4. Return result
|
||||
func WaitForTaskComplete(
|
||||
c client.Client,
|
||||
taskID string,
|
||||
repo string,
|
||||
timeout time.Duration,
|
||||
) (map[string]interface{}, error) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Build spec for implementation task
|
||||
spec := map[string]interface{}{
|
||||
"name": fmt.Sprintf("implement-%s", taskID),
|
||||
"input": map[string]interface{}{
|
||||
"taskId": taskID,
|
||||
"repo": repo,
|
||||
},
|
||||
"states": []map[string]interface{}{
|
||||
{
|
||||
"name": "Clone",
|
||||
"type": "Task",
|
||||
"resource": "CloneRepoActivity",
|
||||
"parameters": map[string]interface{}{
|
||||
"repo": repo,
|
||||
},
|
||||
"next": "Analyze",
|
||||
},
|
||||
{
|
||||
"name": "Analyze",
|
||||
"type": "Task",
|
||||
"resource": "AnalyzeCodeActivity",
|
||||
"parameters": map[string]interface{}{
|
||||
"path": "${Clone.output.path}",
|
||||
},
|
||||
"next": "SecurityScan",
|
||||
},
|
||||
{
|
||||
"name": "SecurityScan",
|
||||
"type": "Task",
|
||||
"resource": "SecurityScanActivity",
|
||||
"parameters": map[string]interface{}{
|
||||
"path": "${Clone.output.path}",
|
||||
},
|
||||
"next": "Report",
|
||||
},
|
||||
{
|
||||
"name": "Report",
|
||||
"type": "Task",
|
||||
"resource": "GenerateReportActivity",
|
||||
"parameters": map[string]interface{}{
|
||||
"analysisResult": "${Analyze.output}",
|
||||
"securityResult": "${SecurityScan.output}",
|
||||
},
|
||||
"end": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Submit workflow
|
||||
workflowID := fmt.Sprintf("%s-%d", taskID, time.Now().UnixNano())
|
||||
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||
ID: workflowID,
|
||||
TaskQueue: "poimen-taskqueue",
|
||||
}, "RoutingWorkflow", map[string]interface{}{"spec": spec})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("submit failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Workflow started: %s\n", taskID, workflowID)
|
||||
|
||||
// Wait for completion
|
||||
waitCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := run.Get(waitCtx, &result); err != nil {
|
||||
return nil, fmt.Errorf("workflow failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Workflow completed: %s\n", taskID, result["status"])
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Example usage in another service:
|
||||
//
|
||||
// func (s *MyService) ImplementTask(taskID string) error {
|
||||
// c, _ := client.Dial(client.Options{
|
||||
// HostPort: "temporal-frontend.temporal:7233",
|
||||
// Namespace: "poimen-harness",
|
||||
// })
|
||||
// defer c.Close()
|
||||
//
|
||||
// result, err := WaitForTaskComplete(c, taskID, "https://github.com/...", 30*time.Minute)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// // Process result
|
||||
// if result["status"] == "COMPLETED" {
|
||||
// report := result["stepResults"].(map[string]interface{})["Report"]
|
||||
// // Use report...
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
Reference in New Issue
Block a user