235 lines
6.3 KiB
Go
235 lines
6.3 KiB
Go
// 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)
|
||
|
|
}
|
||
|
|
}
|