// Package flow implements the 'micro flow' command for event-driven // LLM orchestration of microservices. package flow import ( "context" "encoding/json" "fmt" "io" "os" "os/signal" "sort" "syscall" "github.com/urfave/cli/v2" "go-micro.dev/v6/broker" "go-micro.dev/v6/client" "go-micro.dev/v6/cmd" aiflow "go-micro.dev/v6/flow" "go-micro.dev/v6/registry" ) func init() { cmd.Register(&cli.Command{ Name: "flow", Usage: "Event-driven LLM orchestration", Description: `Run flows that subscribe to broker events and use an LLM to orchestrate service calls in response. This command runs a single-step flow from flags. For ordered, durable multi-step workflows (checkpointed steps that resume after a crash), define the flow in code with micro.FlowSteps and a Checkpoint — see examples/flow-durable. Examples: # Run a flow that reacts to user creation events micro flow run --trigger events.user.created \ --prompt "New user: {{.Data}}. Send welcome email." \ --provider anthropic # Run a one-shot flow with inline data micro flow exec --prompt "List all users and count them" \ --provider anthropic # Run a flow with a specific model micro flow exec --prompt "Create a test user" \ --provider atlascloud --model deepseek-ai/DeepSeek-V3-0324`, Subcommands: []*cli.Command{ { Name: "run", Usage: "Start a flow that listens to broker events", Flags: flowFlags(), Action: func(c *cli.Context) error { return runFlow(c, false) }, }, { Name: "exec", Usage: "Execute a flow once with inline data", Flags: append(flowFlags(), &cli.StringFlag{ Name: "data", Usage: "Input data for the flow (default: reads from --prompt only)", }), Action: func(c *cli.Context) error { return runFlow(c, true) }, }, { Name: "list", Usage: "List running flows (from the registry, type=flow)", Action: listFlows, }, { Name: "runs", Usage: "Show durable run history for a flow", ArgsUsage: "[name]", Flags: []cli.Flag{ &cli.BoolFlag{Name: "json", Usage: "Print durable run history as JSON for automation"}, &cli.BoolFlag{Name: "pending", Usage: "Only show runs that have not completed"}, &cli.StringFlag{Name: "status", Usage: "Only show runs with this status (running, done, failed)"}, &cli.IntFlag{Name: "limit", Usage: "Show the most recently updated N runs"}, &cli.StringFlag{Name: "stage", Usage: "Only show runs currently checkpointed at this stage"}, }, Action: flowRuns, }, }, }) } // listFlows shows flows currently registered in the registry — the live // view, mirroring `micro agent list`. func listFlows(c *cli.Context) error { svcs, err := registry.ListServices() if err != nil { return err } found := false for _, svc := range svcs { records, err := registry.GetService(svc.Name) if err != nil || len(records) == 0 { continue } meta := records[0].Metadata isFlow := meta != nil && meta["type"] == "flow" if !isFlow && len(records[0].Nodes) > 0 { nm := records[0].Nodes[0].Metadata isFlow = nm != nil && nm["type"] == "flow" } if !isFlow { continue } found = true trigger := "" if meta != nil { trigger = meta["trigger"] } fmt.Printf(" \033[36m⚡\033[0m %-20s trigger: %s\n", svc.Name, trigger) } if !found { fmt.Println(" No running flows.") fmt.Println(" Run one with: micro flow run --trigger --prompt <...>") } return nil } // flowRuns shows a flow's durable run history from the store — the // historic view, available whether or not the flow is currently running. func flowRuns(c *cli.Context) error { name := c.Args().First() if name == "" { return fmt.Errorf("flow name required: micro flow runs ") } runs, err := aiflow.StoreCheckpoint(nil, name).List(context.Background()) if err != nil { return err } opts, err := validateFlowRunOptions(flowRunOptions{Pending: c.Bool("pending"), Status: c.String("status"), Stage: c.String("stage"), Limit: c.Int("limit")}) if err != nil { return err } runs = filterFlowRuns(runs, opts) if len(runs) == 0 { if c.Bool("pending") { fmt.Printf(" No pending runs recorded for flow %q.\n", name) return nil } fmt.Printf(" No runs recorded for flow %q.\n", name) return nil } return writeFlowRuns(os.Stdout, runs, c.Bool("json")) } type flowRunOptions struct { Pending bool Status string Stage string Limit int } func validateFlowRunOptions(opts flowRunOptions) (flowRunOptions, error) { switch opts.Status { case "", "running", "done", "failed": default: return opts, fmt.Errorf("invalid run status %q: expected running, done, or failed", opts.Status) } if opts.Limit < 0 { return opts, fmt.Errorf("invalid limit %d: expected a non-negative value", opts.Limit) } return opts, nil } func filterFlowRuns(runs []aiflow.Run, opts flowRunOptions) []aiflow.Run { if len(runs) == 0 { return nil } filtered := make([]aiflow.Run, 0, len(runs)) for _, run := range runs { if opts.Pending && run.Status == "done" { continue } if opts.Status != "" && run.Status != opts.Status { continue } if opts.Stage != "" && run.State.Stage != opts.Stage { continue } filtered = append(filtered, run) } if opts.Limit > 0 && len(filtered) > opts.Limit { sort.SliceStable(filtered, func(i, j int) bool { if filtered[i].Updated.Equal(filtered[j].Updated) { return filtered[i].Started.Before(filtered[j].Started) } return filtered[i].Updated.Before(filtered[j].Updated) }) start := len(filtered) - opts.Limit limited := append([]aiflow.Run(nil), filtered[start:]...) return limited } return filtered } func pendingFlowRuns(runs []aiflow.Run) []aiflow.Run { return filterFlowRuns(runs, flowRunOptions{Pending: true}) } func writeFlowRuns(w io.Writer, runs []aiflow.Run, asJSON bool) error { if asJSON { enc := json.NewEncoder(w) enc.SetIndent("", " ") return enc.Encode(runs) } fmt.Fprintf(w, " %d run%s\n", len(runs), plural(len(runs))) for _, r := range runs { id := r.ID if len(id) > 8 { id = id[:8] } stage := r.State.Stage if stage == "" { stage = "-" } fmt.Fprintf(w, " %s %-8s stage=%-12s updated=%s (%d steps)\n", id, r.Status, stage, r.Updated.Format("2006-01-02T15:04:05Z07:00"), len(r.Steps)) for _, step := range r.Steps { fmt.Fprintf(w, " - %-12s %-11s attempts=%d", step.Name, step.Status, step.Attempts) if step.Error != "" { fmt.Fprintf(w, " error=%q", step.Error) } fmt.Fprintln(w) } } return nil } func plural(n int) string { if n == 1 { return "" } return "s" } func flowFlags() []cli.Flag { return []cli.Flag{ &cli.StringFlag{Name: "trigger", Usage: "Broker topic to subscribe to", EnvVars: []string{"MICRO_FLOW_TRIGGER"}}, &cli.StringFlag{Name: "prompt", Usage: "Prompt template (use {{.Data}} for event data)", EnvVars: []string{"MICRO_FLOW_PROMPT"}}, &cli.StringFlag{Name: "provider", Usage: "AI provider", Value: "openai", EnvVars: []string{"MICRO_AI_PROVIDER"}}, &cli.StringFlag{Name: "api_key", Usage: "API key", EnvVars: []string{"MICRO_AI_API_KEY"}}, &cli.StringFlag{Name: "model", Usage: "Model name", EnvVars: []string{"MICRO_AI_MODEL"}}, &cli.StringFlag{Name: "base_url", Usage: "Provider base URL", EnvVars: []string{"MICRO_AI_BASE_URL"}}, &cli.StringFlag{Name: "name", Usage: "Flow name", Value: "default"}, } } func runFlow(c *cli.Context, oneShot bool) error { prompt := c.String("prompt") if prompt == "" { return fmt.Errorf("--prompt is required") } provider := c.String("provider") apiKey := c.String("api_key") if apiKey == "" { apiKey = fallbackKey(provider) } if apiKey == "" { return fmt.Errorf("no API key; set --api_key or the provider's env var") } opts := []aiflow.Option{ aiflow.Prompt(prompt), aiflow.Provider(provider), aiflow.APIKey(apiKey), } if v := c.String("trigger"); v != "" { opts = append(opts, aiflow.Trigger(v)) } if v := c.String("model"); v != "" { opts = append(opts, aiflow.Model(v)) } if v := c.String("base_url"); v != "" { opts = append(opts, aiflow.BaseURL(v)) } opts = append(opts, aiflow.OnResult(func(r aiflow.Result) { out, _ := json.MarshalIndent(r, "", " ") fmt.Println(string(out)) })) f := aiflow.New(c.String("name"), opts...) reg := registry.DefaultRegistry br := broker.DefaultBroker cl := client.DefaultClient if err := br.Connect(); err != nil { return fmt.Errorf("broker connect: %w", err) } if err := f.Register(reg, br, cl); err != nil { return err } if oneShot { data := c.String("data") if data == "" { data = prompt } return f.Execute(context.Background(), data) } if c.String("trigger") == "" { return fmt.Errorf("--trigger is required for 'flow run' (use 'flow exec' for one-shot)") } fmt.Println() fmt.Println(" \033[1mmicro flow\033[0m") fmt.Println() fmt.Printf(" Flow \033[36m%s\033[0m\n", f.Name()) fmt.Printf(" Topic \033[36m%s\033[0m\n", c.String("trigger")) fmt.Printf(" Provider \033[36m%s\033[0m\n", provider) fmt.Println() fmt.Println(" \033[2mListening for events. Ctrl-C to stop.\033[0m") fmt.Println() sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig fmt.Printf("\nStopped. %d executions recorded.\n", len(f.Results())) return nil } func fallbackKey(provider string) string { envMap := map[string]string{ "anthropic": "ANTHROPIC_API_KEY", "openai": "OPENAI_API_KEY", "gemini": "GEMINI_API_KEY", "groq": "GROQ_API_KEY", "mistral": "MISTRAL_API_KEY", "together": "TOGETHER_API_KEY", "atlascloud": "ATLASCLOUD_API_KEY", } if env, ok := envMap[provider]; ok { return os.Getenv(env) } return "" }