-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Expand file tree
/
Copy pathflow.go
More file actions
345 lines (310 loc) · 9.63 KB
/
Copy pathflow.go
File metadata and controls
345 lines (310 loc) · 9.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
// Package flow provides event-driven workflows for go-micro services.
//
// A Flow is a workflow in the sense of Anthropic's "Building Effective
// Agents": LLMs and tools orchestrated through a predefined path. It
// subscribes to a broker topic and, for each event, runs one augmented
// LLM step — the registered services as tools, a fixed prompt — and
// lets the model decide which RPCs to call. Use a Flow when the task is
// well-defined and you want a deterministic trigger; use an Agent (see
// the agent package) when the work needs to direct itself dynamically.
//
// Usage:
//
// f := flow.New("onboard-user",
// flow.Trigger("events.user.created"),
// flow.Prompt("New user created: {{.Data}}. Send welcome email and create workspace."),
// flow.Provider("anthropic"),
// flow.APIKey(key),
// )
// f.Register(service)
// service.Run()
package flow
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strconv"
"sync"
"text/template"
"time"
"github.com/google/uuid"
"go-micro.dev/v6/ai"
"go-micro.dev/v6/broker"
"go-micro.dev/v6/client"
codecbytes "go-micro.dev/v6/codec/bytes"
"go-micro.dev/v6/logger"
"go-micro.dev/v6/registry"
// Register default providers.
_ "go-micro.dev/v6/ai/anthropic"
_ "go-micro.dev/v6/ai/atlascloud"
_ "go-micro.dev/v6/ai/gemini"
_ "go-micro.dev/v6/ai/groq"
_ "go-micro.dev/v6/ai/mistral"
_ "go-micro.dev/v6/ai/openai"
_ "go-micro.dev/v6/ai/together"
)
// Flow is an event-driven LLM orchestration unit. It subscribes to
// a broker topic, discovers services as tools, and feeds each event
// into an LLM that decides which RPCs to call.
type Flow struct {
name string
opts Options
model ai.Model
toolSet *ai.Tools
client client.Client
tmpl *template.Template
log logger.Logger
checkpoint Checkpoint
reg registry.Registry
sub broker.Subscriber
registration *registry.Service
mu sync.Mutex
results []Result
}
// Result records one flow execution.
type Result struct {
FlowName string `json:"flow"`
Trigger string `json:"trigger"`
Prompt string `json:"prompt"`
Reply string `json:"reply,omitempty"`
Answer string `json:"answer,omitempty"`
ToolCalls []string `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
ErrorKind string `json:"error_kind,omitempty"`
Timestamp time.Time `json:"timestamp"`
Duration float64 `json:"duration_seconds"`
}
// New creates a Flow with the given name and options.
func New(name string, opts ...Option) *Flow {
o := Options{
Provider: "openai",
SystemPrompt: "You are a service orchestrator. Use the available tools to fulfill the request. Explain what you do.",
HistoryLimit: 20,
}
for _, opt := range opts {
opt(&o)
}
var tmpl *template.Template
if o.Prompt != "" {
var err error
tmpl, err = template.New(name).Parse(o.Prompt)
if err != nil {
tmpl = template.Must(template.New(name).Parse("{{.Data}}"))
}
}
return &Flow{
name: name,
opts: o,
tmpl: tmpl,
log: logger.DefaultLogger,
checkpoint: defaultCheckpoint(name, o),
}
}
// Register wires the flow into a running service. It sets up the
// model, discovers tools from the registry, and subscribes to the
// trigger topic on the broker. Call this before service.Run().
func (f *Flow) Register(reg registry.Registry, br broker.Broker, cl client.Client) error {
f.client = cl
f.reg = reg
f.toolSet = ai.NewTools(reg, ai.ToolClient(cl))
// A flow that dispatches to an agent doesn't run its own model — the
// agent is the engine. Otherwise, set up the augmented LLM.
if f.opts.Agent == "" {
var modelOpts []ai.Option
if f.opts.APIKey != "" {
modelOpts = append(modelOpts, ai.WithAPIKey(f.opts.APIKey))
}
if f.opts.Model != "" {
modelOpts = append(modelOpts, ai.WithModel(f.opts.Model))
}
if f.opts.BaseURL != "" {
modelOpts = append(modelOpts, ai.WithBaseURL(f.opts.BaseURL))
}
modelOpts = append(modelOpts, ai.WithTools(f.toolSet))
f.model = ai.New(f.opts.Provider, modelOpts...)
if f.model == nil {
return fmt.Errorf("unknown provider: %s", f.opts.Provider)
}
}
if f.opts.TriggerTopic != "" {
sub, err := br.Subscribe(f.opts.TriggerTopic, func(p broker.Event) error {
data := string(p.Message().Body)
ctx := ai.WithRunInfo(context.Background(), ai.RunInfo{Dispatch: "broker", Trigger: f.opts.TriggerTopic})
if err := f.Execute(ctx, data); err != nil {
f.log.Logf(logger.ErrorLevel, "Flow %s failed: %v", f.name, err)
}
return nil
})
if err != nil {
return fmt.Errorf("subscribe to %s: %w", f.opts.TriggerTopic, err)
}
f.sub = sub
f.log.Logf(logger.InfoLevel, "Flow %s subscribed to %s", f.name, f.opts.TriggerTopic)
// Announce the flow in the registry so it's discoverable like a
// service or agent (e.g. `micro flow list`). This is liveness only:
// Stop deregisters it. Durable run history lives in the store.
f.registration = ®istry.Service{
Name: f.name,
Version: "latest",
Metadata: map[string]string{
"type": "flow",
"trigger": f.opts.TriggerTopic,
"steps": strconv.Itoa(len(f.opts.Steps)),
},
Nodes: []*registry.Node{{
Id: f.name + "-" + uuid.New().String()[:8],
Address: "flow://" + f.name,
Metadata: map[string]string{"type": "flow"},
}},
}
if err := reg.Register(f.registration); err != nil {
f.log.Logf(logger.ErrorLevel, "Flow %s registry register: %v", f.name, err)
f.registration = nil
}
}
return nil
}
// Stop unsubscribes the flow from its trigger and deregisters it from the
// registry. In-flight and past runs remain in the store; Stop only ends
// the flow's liveness, mirroring how a service leaves the registry when
// it shuts down.
func (f *Flow) withTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
if ctx == nil {
ctx = context.Background()
}
if f.opts.Timeout <= 0 {
return ctx, func() {}
}
if _, ok := ctx.Deadline(); ok {
return ctx, func() {}
}
return context.WithTimeout(ctx, f.opts.Timeout)
}
func (f *Flow) Stop() error {
if f.sub != nil {
_ = f.sub.Unsubscribe()
f.sub = nil
}
if f.registration != nil && f.reg != nil {
err := f.reg.Deregister(f.registration)
f.registration = nil
return err
}
return nil
}
// Execute runs the flow once with the given input data. This is
// called automatically on each broker event, but can also be
// invoked directly for testing or one-shot use.
func (f *Flow) Execute(ctx context.Context, data string) error {
ctx, cancel := f.withTimeout(ctx)
defer cancel()
// Stepped flows run the ordered, checkpointed step loop.
if len(f.opts.Steps) > 0 {
_, err := f.startRun(ctx, data)
return err
}
runID := uuid.New().String()
info, _ := ai.RunInfoFrom(ctx)
info.RunID = runID
info.Flow = f.name
ctx = ai.WithRunInfo(ctx, info)
start := time.Now()
prompt := data
if f.tmpl != nil {
var buf bytes.Buffer
_ = f.tmpl.Execute(&buf, map[string]string{"Data": data})
prompt = buf.String()
}
result := Result{
FlowName: f.name,
Trigger: f.opts.TriggerTopic,
Prompt: prompt,
Timestamp: start,
}
// Flow triggers, Agent reasons: hand the event to the named agent.
if f.opts.Agent != "" {
reply, err := f.callAgent(ctx, f.opts.Agent, prompt)
result.Duration = time.Since(start).Seconds()
if err != nil {
result.Error = err.Error()
result.ErrorKind = string(ai.ClassifyError(err))
f.record(result)
return err
}
result.Reply = reply
f.record(result)
f.log.Logf(logger.InfoLevel, "Flow %s dispatched to agent %s in %.1fs",
f.name, f.opts.Agent, result.Duration)
return nil
}
// Otherwise run a single augmented-LLM step with the services as tools.
discovered, err := f.toolSet.Discover()
if err != nil {
result.Duration = time.Since(start).Seconds()
result.Error = err.Error()
result.ErrorKind = string(ai.ClassifyError(err))
f.record(result)
return fmt.Errorf("discover tools: %w", err)
}
resp, err := f.model.Generate(ctx, &ai.Request{
Prompt: prompt,
SystemPrompt: f.opts.SystemPrompt,
Tools: discovered,
})
result.Duration = time.Since(start).Seconds()
if err != nil {
result.Error = err.Error()
result.ErrorKind = string(ai.ClassifyError(err))
f.record(result)
return err
}
result.Reply = resp.Reply
result.Answer = resp.Answer
for _, tc := range resp.ToolCalls {
args, _ := json.Marshal(tc.Input)
result.ToolCalls = append(result.ToolCalls, fmt.Sprintf("%s(%s)", tc.Name, args))
}
f.record(result)
f.log.Logf(logger.InfoLevel, "Flow %s completed in %.1fs: %d tool calls",
f.name, result.Duration, len(result.ToolCalls))
return nil
}
// callAgent hands the rendered prompt to a registered agent's Agent.Chat
// endpoint over RPC and returns its reply.
func (f *Flow) callAgent(ctx context.Context, name, message string) (string, error) {
info, _ := ai.RunInfoFrom(ctx)
body, _ := json.Marshal(map[string]string{"message": message, "parent_id": info.RunID})
req := f.client.NewRequest(name, "Agent.Chat", &codecbytes.Frame{Data: body})
var rsp codecbytes.Frame
if err := f.client.Call(ctx, req, &rsp); err != nil {
return "", err
}
var out struct {
Reply string `json:"reply"`
}
if err := json.Unmarshal(rsp.Data, &out); err != nil {
return "", err
}
return out.Reply, nil
}
// Results returns a copy of all recorded execution results.
func (f *Flow) Results() []Result {
f.mu.Lock()
defer f.mu.Unlock()
out := make([]Result, len(f.results))
copy(out, f.results)
return out
}
// Name returns the flow name.
func (f *Flow) Name() string {
return f.name
}
func (f *Flow) record(r Result) {
f.mu.Lock()
f.results = append(f.results, r)
f.mu.Unlock()
if f.opts.OnResult != nil {
f.opts.OnResult(r)
}
}