diff --git a/README.md b/README.md index 57b5ce9f..ba52d13e 100644 --- a/README.md +++ b/README.md @@ -166,17 +166,16 @@ ANTHROPIC_API_KEY=sk-ant-... micro chat --provider anthropic > create an order for product 42 ``` -`micro chat` discovers every service in the registry, exposes each endpoint as a tool, and lets the model orchestrate calls. The same building blocks (`ai/tools`) work from your own services: +`micro chat` discovers every service in the registry, exposes each endpoint as a tool, and lets the model orchestrate calls. The same building blocks (`ai.Tools`) work from your own services: ```go -import "go-micro.dev/v5/ai/tools" -set := tools.New(service.Registry()) -discovered, _ := set.Discover() +tools := ai.NewTools(service.Registry()) +discovered, _ := tools.Discover() m := ai.New("anthropic", ai.WithAPIKey(key), - ai.WithToolHandler(set.Handler(service.Client())), + ai.WithTools(tools), ) resp, _ := m.Generate(ctx, &ai.Request{ Prompt: userInput, diff --git a/ai/flow/flow.go b/ai/flow/flow.go index 96622034..9f361fa4 100644 --- a/ai/flow/flow.go +++ b/ai/flow/flow.go @@ -25,7 +25,6 @@ import ( "time" "go-micro.dev/v5/ai" - "go-micro.dev/v5/ai/tools" "go-micro.dev/v5/broker" "go-micro.dev/v5/client" "go-micro.dev/v5/logger" @@ -48,7 +47,7 @@ type Flow struct { name string opts Options model ai.Model - toolSet *tools.Set + toolSet *ai.Tools tmpl *template.Template log logger.Logger mu sync.Mutex @@ -100,7 +99,7 @@ func New(name string, opts ...Option) *Flow { // 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.toolSet = tools.New(reg) + f.toolSet = ai.NewTools(reg, ai.ToolClient(cl)) var modelOpts []ai.Option if f.opts.APIKey != "" { @@ -112,7 +111,7 @@ func (f *Flow) Register(reg registry.Registry, br broker.Broker, cl client.Clien if f.opts.BaseURL != "" { modelOpts = append(modelOpts, ai.WithBaseURL(f.opts.BaseURL)) } - modelOpts = append(modelOpts, ai.WithToolHandler(f.toolSet.Handler(cl))) + modelOpts = append(modelOpts, ai.WithTools(f.toolSet)) f.model = ai.New(f.opts.Provider, modelOpts...) if f.model == nil { diff --git a/ai/options.go b/ai/options.go index 5a1d2b9a..75650d5c 100644 --- a/ai/options.go +++ b/ai/options.go @@ -75,3 +75,19 @@ func WithToolHandler(handler ToolHandler) Option { o.ToolHandler = handler } } + +// WithTools wires a Tools instance into the model, setting the tool +// handler so the model can execute discovered service endpoints. The +// tool list itself is passed per-request via Request.Tools. +// +// tools := ai.NewTools(service.Registry()) +// list, _ := tools.Discover() +// m := ai.New("anthropic", ai.WithAPIKey(key), ai.WithTools(tools)) +// resp, _ := m.Generate(ctx, &ai.Request{Prompt: input, Tools: list}) +func WithTools(t *Tools) Option { + return func(o *Options) { + if t != nil { + o.ToolHandler = t.Handler() + } + } +} diff --git a/ai/tools.go b/ai/tools.go new file mode 100644 index 00000000..ada218ba --- /dev/null +++ b/ai/tools.go @@ -0,0 +1,184 @@ +package ai + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + + "go-micro.dev/v5/client" + codecBytes "go-micro.dev/v5/codec/bytes" + "go-micro.dev/v5/registry" +) + +type toolNameMap struct { + mu sync.RWMutex + m map[string]string +} + +func (n *toolNameMap) put(safe, original string) { + n.mu.Lock() + n.m[safe] = original + n.mu.Unlock() +} + +func (n *toolNameMap) get(safe string) (string, bool) { + n.mu.RLock() + v, ok := n.m[safe] + n.mu.RUnlock() + return v, ok +} + +// Tools discovers go-micro services from a registry and converts their +// endpoints into Tool definitions. It also executes tool calls via RPC. +// +// Create with NewTools, discover the tool list with Discover, and wire +// execution into a model with WithTools: +// +// tools := ai.NewTools(service.Registry()) +// list, _ := tools.Discover() +// m := ai.New("anthropic", ai.WithAPIKey(key), ai.WithTools(tools)) +// resp, _ := m.Generate(ctx, &ai.Request{Prompt: input, Tools: list}) +type Tools struct { + registry registry.Registry + client client.Client + names *toolNameMap +} + +// ToolOption configures a Tools instance. +type ToolOption func(*Tools) + +// ToolClient sets the client used to execute tool calls. Defaults to +// client.DefaultClient. +func ToolClient(c client.Client) ToolOption { + return func(t *Tools) { + if c != nil { + t.client = c + } + } +} + +// NewTools creates a Tools bound to the given registry. +func NewTools(reg registry.Registry, opts ...ToolOption) *Tools { + t := &Tools{ + registry: reg, + client: client.DefaultClient, + names: &toolNameMap{m: map[string]string{}}, + } + for _, o := range opts { + o(t) + } + return t +} + +// Discover walks the registry and returns one Tool per service +// endpoint. Tool names are LLM-safe (dots replaced with underscores). +func (t *Tools) Discover() ([]Tool, error) { + services, err := t.registry.ListServices() + if err != nil { + return nil, err + } + + var out []Tool + for _, svc := range services { + full, err := t.registry.GetService(svc.Name) + if err != nil || len(full) == 0 { + continue + } + for _, ep := range full[0].Endpoints { + original := fmt.Sprintf("%s.%s", svc.Name, ep.Name) + safe := strings.ReplaceAll(original, ".", "_") + t.names.put(safe, original) + + desc := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name) + if ep.Metadata != nil { + if d, ok := ep.Metadata["description"]; ok && d != "" { + desc = d + } + } + + props := map[string]any{} + if ep.Request != nil { + for _, field := range ep.Request.Values { + props[field.Name] = map[string]any{ + "type": toolJSONType(field.Type), + "description": fmt.Sprintf("%s (%s)", field.Name, field.Type), + } + } + } + + out = append(out, Tool{ + Name: safe, + OriginalName: original, + Description: desc, + Properties: props, + }) + } + } + + return out, nil +} + +// Handler returns a ToolHandler that executes tool calls via RPC using +// the configured client. Tool names may be LLM-safe (underscored) or +// original (dotted). WithTools uses this internally. +func (t *Tools) Handler() ToolHandler { + c := t.client + if c == nil { + c = client.DefaultClient + } + return func(name string, input map[string]any) (any, string) { + if orig, ok := t.names.get(name); ok { + name = orig + } + parts := strings.SplitN(name, ".", 2) + if len(parts) != 2 { + return toolErrResult("invalid tool name: " + name) + } + + inputBytes, err := json.Marshal(input) + if err != nil { + return toolErrResult("failed to marshal input: " + err.Error()) + } + + req := c.NewRequest(parts[0], parts[1], &codecBytes.Frame{Data: inputBytes}) + var rsp codecBytes.Frame + if err := c.Call(context.Background(), req, &rsp); err != nil { + return toolErrResult(err.Error()) + } + + var result any + if err := json.Unmarshal(rsp.Data, &result); err != nil { + result = string(rsp.Data) + } + return result, string(rsp.Data) + } +} + +// DiscoverTools is a convenience that discovers tools from a registry +// without creating a Tools instance. For paired discovery + execution, +// create a Tools with NewTools instead. +func DiscoverTools(reg registry.Registry) ([]Tool, error) { + return NewTools(reg).Discover() +} + +func toolErrResult(msg string) (any, string) { + encoded, _ := json.Marshal(map[string]string{"error": msg}) + return map[string]string{"error": msg}, string(encoded) +} + +func toolJSONType(goType string) string { + switch goType { + case "string": + return "string" + case "int", "int32", "int64", "uint", "uint32", "uint64": + return "integer" + case "float32", "float64": + return "number" + case "bool": + return "boolean" + default: + return "object" + } +} diff --git a/ai/tools/tools.go b/ai/tools/tools.go deleted file mode 100644 index fadcfbea..00000000 --- a/ai/tools/tools.go +++ /dev/null @@ -1,197 +0,0 @@ -// Package tools turns go-micro services into ai.Tool definitions and -// provides an ai.ToolHandler that executes tool calls by issuing RPCs -// to the corresponding service. -// -// This is the building block that lets any go-micro service reason -// about and call other services through an LLM: -// -// m := ai.New("anthropic", -// ai.WithAPIKey(key), -// ai.WithToolHandler(tools.Handler(service.Client())), -// ) -// resp, _ := m.Generate(ctx, &ai.Request{ -// Prompt: userInput, -// Tools: tools.FromRegistry(service.Registry()), -// }) -package tools - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "sync" - - "go-micro.dev/v5/ai" - "go-micro.dev/v5/client" - codecBytes "go-micro.dev/v5/codec/bytes" - "go-micro.dev/v5/registry" -) - -// nameMap holds the mapping between LLM-safe tool names (no dots) and the -// original `service.Endpoint` names used by the registry/client. Many -// providers reject dots in tool names, so we substitute underscores when -// presenting the tool and restore the original when executing. -type nameMap struct { - mu sync.RWMutex - m map[string]string -} - -func (n *nameMap) put(safe, original string) { - n.mu.Lock() - n.m[safe] = original - n.mu.Unlock() -} - -func (n *nameMap) get(safe string) (string, bool) { - n.mu.RLock() - v, ok := n.m[safe] - n.mu.RUnlock() - return v, ok -} - -// Set is the shared discovery state between FromRegistry and Handler. -// Use New, then Discover + Handler together when you want the handler -// to recognise LLM-safe tool names that were emitted by Discover. -// -// FromRegistry+Handler are convenience wrappers that create their own -// internal Set; Set is exposed for callers that want to control the -// lifecycle (e.g. cache the tool list and reuse it across turns). -type Set struct { - registry registry.Registry - names *nameMap -} - -// New creates an empty Set bound to the given registry. Call Discover -// to populate it. The registry is only used by Discover; Handler does -// not need it. -func New(reg registry.Registry) *Set { - return &Set{ - registry: reg, - names: &nameMap{m: map[string]string{}}, - } -} - -// Discover walks the registry and returns one ai.Tool per service -// endpoint. The returned tools have LLM-safe names (dots replaced with -// underscores); the Set remembers the mapping so Handler can route -// calls back to the right service. -func (s *Set) Discover() ([]ai.Tool, error) { - services, err := s.registry.ListServices() - if err != nil { - return nil, err - } - - var tools []ai.Tool - for _, svc := range services { - full, err := s.registry.GetService(svc.Name) - if err != nil || len(full) == 0 { - continue - } - for _, ep := range full[0].Endpoints { - original := fmt.Sprintf("%s.%s", svc.Name, ep.Name) - safe := strings.ReplaceAll(original, ".", "_") - s.names.put(safe, original) - - desc := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name) - if ep.Metadata != nil { - if d, ok := ep.Metadata["description"]; ok && d != "" { - desc = d - } - } - - props := map[string]any{} - if ep.Request != nil { - for _, field := range ep.Request.Values { - props[field.Name] = map[string]any{ - "type": jsonType(field.Type), - "description": fmt.Sprintf("%s (%s)", field.Name, field.Type), - } - } - } - - tools = append(tools, ai.Tool{ - Name: safe, - OriginalName: original, - Description: desc, - Properties: props, - }) - } - } - - return tools, nil -} - -// Handler returns an ai.ToolHandler that executes tool calls against -// the given client. Tool names may be the LLM-safe form (with -// underscores) emitted by Discover or the original dotted form; both -// resolve to the same RPC. -func (s *Set) Handler(c client.Client) ai.ToolHandler { - if c == nil { - c = client.DefaultClient - } - return func(name string, input map[string]any) (any, string) { - if orig, ok := s.names.get(name); ok { - name = orig - } - parts := strings.SplitN(name, ".", 2) - if len(parts) != 2 { - return errResult("invalid tool name: " + name) - } - - inputBytes, err := json.Marshal(input) - if err != nil { - return errResult("failed to marshal input: " + err.Error()) - } - - req := c.NewRequest(parts[0], parts[1], &codecBytes.Frame{Data: inputBytes}) - var rsp codecBytes.Frame - if err := c.Call(context.Background(), req, &rsp); err != nil { - return errResult(err.Error()) - } - - var result any - if err := json.Unmarshal(rsp.Data, &result); err != nil { - result = string(rsp.Data) - } - return result, string(rsp.Data) - } -} - -// FromRegistry is a convenience that builds a one-shot Set, discovers -// tools, and returns just the tool list. Use NewSet directly if you -// need to also wire up Handler against the same name mapping. -func FromRegistry(reg registry.Registry) ([]ai.Tool, error) { - return New(reg).Discover() -} - -// Handler is a convenience that returns an ai.ToolHandler bound to the -// given client. It only resolves dotted "service.Endpoint" names — it -// has no awareness of any LLM-safe name mapping. For full round-tripping -// of underscored names emitted by FromRegistry, construct a Set with -// New and call Set.Handler. -func Handler(c client.Client) ai.ToolHandler { - return (&Set{names: &nameMap{m: map[string]string{}}}).Handler(c) -} - -func errResult(msg string) (any, string) { - encoded, _ := json.Marshal(map[string]string{"error": msg}) - return map[string]string{"error": msg}, string(encoded) -} - -// jsonType maps Go types to JSON schema types. Anything that isn't a -// recognised primitive becomes "object". -func jsonType(goType string) string { - switch goType { - case "string": - return "string" - case "int", "int32", "int64", "uint", "uint32", "uint64": - return "integer" - case "float32", "float64": - return "number" - case "bool": - return "boolean" - default: - return "object" - } -} diff --git a/ai/tools/tools_test.go b/ai/tools_test.go similarity index 58% rename from ai/tools/tools_test.go rename to ai/tools_test.go index a3f37d9c..2341c86a 100644 --- a/ai/tools/tools_test.go +++ b/ai/tools_test.go @@ -1,4 +1,4 @@ -package tools +package ai import ( "testing" @@ -6,7 +6,7 @@ import ( "go-micro.dev/v5/registry" ) -func TestJSONType(t *testing.T) { +func TestToolJSONType(t *testing.T) { cases := map[string]string{ "string": "string", "int": "integer", @@ -17,24 +17,24 @@ func TestJSONType(t *testing.T) { "": "object", } for in, want := range cases { - if got := jsonType(in); got != want { - t.Errorf("jsonType(%q) = %q, want %q", in, got, want) + if got := toolJSONType(in); got != want { + t.Errorf("toolJSONType(%q) = %q, want %q", in, got, want) } } } -func TestFromRegistry_Empty(t *testing.T) { +func TestDiscoverTools_Empty(t *testing.T) { reg := registry.NewMemoryRegistry() - tools, err := FromRegistry(reg) + tools, err := DiscoverTools(reg) if err != nil { - t.Fatalf("FromRegistry: %v", err) + t.Fatalf("DiscoverTools: %v", err) } if len(tools) != 0 { t.Errorf("expected 0 tools, got %d", len(tools)) } } -func TestFromRegistry_DiscoversEndpoints(t *testing.T) { +func TestDiscoverTools_DiscoversEndpoints(t *testing.T) { reg := registry.NewMemoryRegistry() svc := ®istry.Service{ Name: "users", @@ -63,9 +63,9 @@ func TestFromRegistry_DiscoversEndpoints(t *testing.T) { t.Fatalf("Register: %v", err) } - tools, err := FromRegistry(reg) + tools, err := DiscoverTools(reg) if err != nil { - t.Fatalf("FromRegistry: %v", err) + t.Fatalf("DiscoverTools: %v", err) } if len(tools) != 1 { t.Fatalf("expected 1 tool, got %d", len(tools)) @@ -73,7 +73,7 @@ func TestFromRegistry_DiscoversEndpoints(t *testing.T) { tool := tools[0] if tool.Name != "users_Users_Get" { - t.Errorf("safe name = %q, want users_Users_Get", tool.Name) + t.Errorf("safe name = %q", tool.Name) } if tool.OriginalName != "users.Users.Get" { t.Errorf("original = %q", tool.OriginalName) @@ -81,38 +81,22 @@ func TestFromRegistry_DiscoversEndpoints(t *testing.T) { if tool.Description != "Fetch a user by ID" { t.Errorf("description = %q", tool.Description) } - - id, ok := tool.Properties["id"].(map[string]any) - if !ok { - t.Fatal("missing id property") - } - if id["type"] != "string" { - t.Errorf("id type = %v", id["type"]) - } - expand, ok := tool.Properties["expand"].(map[string]any) - if !ok { - t.Fatal("missing expand property") - } - if expand["type"] != "boolean" { - t.Errorf("expand type = %v", expand["type"]) - } } -func TestSet_HandlerResolvesSafeName(t *testing.T) { - s := New(registry.NewMemoryRegistry()) - s.names.put("users_Users_Get", "users.Users.Get") +func TestTools_HandlerResolvesSafeName(t *testing.T) { + tools := NewTools(registry.NewMemoryRegistry()) + tools.names.put("users_Users_Get", "users.Users.Get") - resolved, ok := s.names.get("users_Users_Get") + resolved, ok := tools.names.get("users_Users_Get") if !ok || resolved != "users.Users.Get" { t.Errorf("name map lookup = (%q, %v)", resolved, ok) } } -func TestSet_HandlerInvalidName(t *testing.T) { - s := New(registry.NewMemoryRegistry()) - h := s.Handler(nil) +func TestTools_HandlerInvalidName(t *testing.T) { + tools := NewTools(registry.NewMemoryRegistry()) + h := tools.Handler() - // "foo" has no dot, no mapping entry — should error cleanly. result, content := h("foo", map[string]any{}) if result == nil { t.Fatal("expected error result") @@ -121,3 +105,11 @@ func TestSet_HandlerInvalidName(t *testing.T) { t.Error("expected non-empty content") } } + +func TestWithTools(t *testing.T) { + tools := NewTools(registry.NewMemoryRegistry()) + opts := NewOptions(WithTools(tools)) + if opts.ToolHandler == nil { + t.Error("WithTools did not set a ToolHandler") + } +} diff --git a/cmd/micro/chat/chat.go b/cmd/micro/chat/chat.go index 3fd4b668..22ae8c4c 100644 --- a/cmd/micro/chat/chat.go +++ b/cmd/micro/chat/chat.go @@ -16,7 +16,6 @@ import ( "github.com/urfave/cli/v2" "go-micro.dev/v5/ai" - "go-micro.dev/v5/ai/tools" "go-micro.dev/v5/client" "go-micro.dev/v5/cmd" "go-micro.dev/v5/registry" @@ -96,15 +95,15 @@ func run(c *cli.Context) error { reg := registry.DefaultRegistry cli := client.DefaultClient - toolSet := tools.New(reg) - discovered, err := toolSet.Discover() + tools := ai.NewTools(reg, ai.ToolClient(cli)) + discovered, err := tools.Discover() if err != nil { return fmt.Errorf("discover tools: %w", err) } opts := []ai.Option{ ai.WithAPIKey(apiKey), - ai.WithToolHandler(toolSet.Handler(cli)), + ai.WithTools(tools), } if model != "" { opts = append(opts, ai.WithModel(model)) diff --git a/internal/website/blog/10.md b/internal/website/blog/10.md index b2cec1c0..f4e0706e 100644 --- a/internal/website/blog/10.md +++ b/internal/website/blog/10.md @@ -35,11 +35,11 @@ No glue code. No API wrappers. No tool definitions. You write normal Go services Three building blocks, stacked: -**1. `ai/tools`** discovers services from the registry and creates typed tool definitions: +**1. `ai.Tools`** discovers services from the registry and creates typed tool definitions: ```go -set := tools.New(service.Registry()) -discovered, _ := set.Discover() +tools := ai.NewTools(service.Registry()) +discovered, _ := tools.Discover() // discovered = []ai.Tool with name, description, parameters for each endpoint ``` @@ -138,7 +138,7 @@ func (h *Users) CreateUser(ctx context.Context, req *pb.CreateRequest, rsp *pb.C } ``` -The `ai/tools` package reads all of this from the registry and translates it into the tool format that LLMs understand. The better your doc comments, the better the LLM uses your services. +The `ai.Tools` package reads all of this from the registry and translates it into the tool format that LLMs understand. The better your doc comments, the better the LLM uses your services. ## Using It Programmatically @@ -147,16 +147,16 @@ The `ai/tools` package reads all of this from the registry and translates it int ```go import ( "go-micro.dev/v5/ai" - "go-micro.dev/v5/ai/tools" + _ "go-micro.dev/v5/ai/anthropic" ) -set := tools.New(service.Registry()) -discovered, _ := set.Discover() +tools := ai.NewTools(service.Registry()) +discovered, _ := tools.Discover() m := ai.New("anthropic", ai.WithAPIKey(key), - ai.WithToolHandler(set.Handler(service.Client())), + ai.WithTools(tools), ) hist := ai.NewHistory("You are a helpful assistant.", 50) diff --git a/internal/website/blog/3.md b/internal/website/blog/3.md index 217e45c1..14948b0e 100644 --- a/internal/website/blog/3.md +++ b/internal/website/blog/3.md @@ -80,7 +80,7 @@ The Claude sponsorship set a direction that kept going. Since then: **`micro chat`** — an interactive CLI that discovers your services, exposes them as tools, and lets you orchestrate them through natural language. Multi-turn conversation with history. -**`ai/tools`** — a reusable package that turns registry discovery + client RPC into an `ai.ToolHandler`. Any service can reason about and call other services through an LLM. +**`ai.Tools`** — a reusable package that turns registry discovery + client RPC into an `ai.ToolHandler`. Any service can reason about and call other services through an LLM. **Service templates** — `micro new --template crud` scaffolds a full CRUD service with typed proto, in-memory store, pagination, and MCP-ready doc comments. diff --git a/internal/website/blog/8.md b/internal/website/blog/8.md index 1fcf223e..086c95a7 100644 --- a/internal/website/blog/8.md +++ b/internal/website/blog/8.md @@ -85,14 +85,13 @@ Atlas Cloud supports models like `gpt-image-1`, `flux-2`, and more from their 30 Atlas Cloud supports OpenAI-compatible function calling, which means it works with Go Micro's tool execution flow. Services registered in the registry become tools that the model can call: ```go -import "go-micro.dev/v5/ai/tools" -set := tools.New(service.Registry()) -discovered, _ := set.Discover() +tools := ai.NewTools(service.Registry()) +discovered, _ := tools.Discover() m := ai.New("atlascloud", ai.WithAPIKey(key), - ai.WithToolHandler(set.Handler(service.Client())), + ai.WithTools(tools), ) resp, _ := m.Generate(ctx, &ai.Request{ @@ -139,7 +138,7 @@ With Atlas Cloud, Go Micro now supports seven AI providers: | **Mistral** | OpenAI-compatible | `mistral-large-latest` | | **Together AI** | OpenAI-compatible | `Llama-3.3-70B-Instruct-Turbo` | -All providers implement the same `ai.Model` interface and work with `ai/tools`, `micro chat`, and the agent playground. +All providers implement the same `ai.Model` interface and work with `ai.Tools`, `micro chat`, and the agent playground. ## Getting Started diff --git a/internal/website/blog/9.md b/internal/website/blog/9.md index a9b1f218..ec36f29c 100644 --- a/internal/website/blog/9.md +++ b/internal/website/blog/9.md @@ -22,7 +22,7 @@ This post explores that idea. We're not shipping anything yet — we're thinking Go Micro already has the building blocks: - **Services as tools**: every endpoint is discoverable via MCP with typed schemas -- **`ai/tools`**: programmatic discovery and execution — `tools.FromRegistry(reg)` gives you the tool list, `tools.Handler(client)` executes RPCs +- **`ai.Tools`**: programmatic discovery and execution — `ai.DiscoverTools(reg)` gives you the tool list, `ai.NewTools(reg).Handler()` executes RPCs - **`ai.History`**: multi-turn conversation state so the LLM has context across steps - **`micro chat`**: the interactive agent loop that ties it together - **Broker/events**: pub/sub for async communication between services @@ -96,7 +96,7 @@ Under the hood, `Flow` would: 3. Call `history.Generate()` in a loop until the LLM stops requesting tool calls 4. Log the full conversation for audit -The building blocks already exist. `ai.History` manages the conversation. `ai/tools` discovers and executes services. The broker delivers events. A `Flow` just connects them. +The building blocks already exist. `ai.History` manages the conversation. `ai.Tools` discovers and executes services. The broker delivers events. A `Flow` just connects them. ## Why We Haven't Built It Yet @@ -122,15 +122,15 @@ Despite those caveats, there are use cases where this is genuinely better than t ## What You Can Do Today -You don't need a flow engine to get most of this value. The `ai/tools` package already gives you programmatic access: +You don't need a flow engine to get most of this value. The `ai.Tools` package already gives you programmatic access: ```go -set := tools.New(service.Registry()) -discovered, _ := set.Discover() +tools := ai.NewTools(service.Registry()) +discovered, _ := tools.Discover() m := ai.New("atlascloud", ai.WithAPIKey(key), - ai.WithToolHandler(set.Handler(service.Client())), + ai.WithTools(tools), ) hist := ai.NewHistory("You are a service orchestrator.", 50) @@ -188,9 +188,9 @@ The questions from the original post still stand. We'd love feedback on what gua ## The Bigger Picture -The thesis behind Go Micro's AI-native direction is that **services should be composable by agents, not just by code.** MCP made services discoverable. `ai/tools` made them callable. `micro chat` made them interactive. Flows would make them orchestratable. +The thesis behind Go Micro's AI-native direction is that **services should be composable by agents, not just by code.** MCP made services discoverable. `ai.Tools` made them callable. `micro chat` made them interactive. Flows would make them orchestratable. -Each layer builds on the previous one. And at each layer, the question is the same: does this belong in the framework, or is it better left to the user? So far, we've been conservative — `ai/tools` is 150 lines, `History` is 80, `micro chat` is 170. Small, composable building blocks rather than a big orchestration framework. +Each layer builds on the previous one. And at each layer, the question is the same: does this belong in the framework, or is it better left to the user? So far, we've been conservative — `ai.Tools` is 150 lines, `History` is 80, `micro chat` is 170. Small, composable building blocks rather than a big orchestration framework. We think that's the right approach. But we're watching to see if the community says otherwise. diff --git a/internal/website/docs/ai-integration.md b/internal/website/docs/ai-integration.md index 57bf15bd..b3533527 100644 --- a/internal/website/docs/ai-integration.md +++ b/internal/website/docs/ai-integration.md @@ -18,7 +18,7 @@ Registry → automatic service discovery (mDNS, Consul, etcd) ↓ Gateways → micro api (HTTP→RPC) / micro mcp (MCP tools) ↓ -ai/tools → discovers services + executes RPCs programmatically +ai.Tools → discovers services + executes RPCs programmatically ↓ ai.Model → calls LLMs (Anthropic, OpenAI, Gemini, Atlas Cloud, ...) ↓ @@ -73,16 +73,16 @@ micro mcp serve --address :3000 # HTTP for web agents Any MCP-compatible agent (Claude Code, ChatGPT, custom agents) can discover and call your services. -### 4. ai/tools (discover + execute) +### 4. ai.Tools (discover + execute) -The `ai/tools` package extracts the MCP gateway's logic into a reusable building block: +`ai.Tools` turns registered services into LLM-callable tools — discovery plus RPC execution in one type: ```go -import "go-micro.dev/v5/ai/tools" +tools := ai.NewTools(service.Registry()) +discovered, _ := tools.Discover() // []ai.Tool from all registered services -set := tools.New(service.Registry()) -discovered, _ := set.Discover() // []ai.Tool from all registered services -handler := set.Handler(service.Client()) // executes tool calls via RPC +// Wire execution into a model with one option: +m := ai.New("anthropic", ai.WithAPIKey(key), ai.WithTools(tools)) ``` This is what powers `micro chat` and the agent playground. You can use it directly in your own services to build agentic workflows. @@ -100,7 +100,7 @@ import ( m := ai.New("anthropic", ai.WithAPIKey(key)) resp, _ := m.Generate(ctx, &ai.Request{ Prompt: "What users are in the system?", - Tools: discovered, // from ai/tools + Tools: discovered, // from ai.Tools }) ``` diff --git a/internal/website/docs/guides/atlascloud-integration.md b/internal/website/docs/guides/atlascloud-integration.md index 608aa778..6e348a50 100644 --- a/internal/website/docs/guides/atlascloud-integration.md +++ b/internal/website/docs/guides/atlascloud-integration.md @@ -160,7 +160,7 @@ The `ai.ImageModel` interface is also implemented by the OpenAI provider, so swi ## Using with Services (Tool Calling) -Atlas Cloud supports OpenAI-compatible function calling. Combined with Go Micro's `ai/tools` package, your services become tools that the model can call: +Atlas Cloud supports OpenAI-compatible function calling. Combined with Go Micro's `ai.Tools`, your services become tools that the model can call: ```go package main @@ -172,7 +172,7 @@ import ( "go-micro.dev/v5" "go-micro.dev/v5/ai" - "go-micro.dev/v5/ai/tools" + _ "go-micro.dev/v5/ai/atlascloud" ) @@ -181,8 +181,8 @@ func main() { service.Init() // Discover all services as tools - set := tools.New(service.Registry()) - discovered, err := set.Discover() + tools := ai.NewTools(service.Registry()) + discovered, err := tools.Discover() if err != nil { log.Fatal(err) } @@ -190,7 +190,7 @@ func main() { // Create a model with tool execution m := ai.New("atlascloud", ai.WithAPIKey("your-key"), - ai.WithToolHandler(set.Handler(service.Client())), + ai.WithTools(tools), ) // The model can now call your services @@ -209,10 +209,10 @@ func main() { ### How it works -1. `tools.New(registry)` creates a tool set bound to the service registry -2. `set.Discover()` walks the registry and returns every endpoint as an `ai.Tool` -3. `set.Handler(client)` returns an `ai.ToolHandler` that executes tool calls via RPC -4. When the model decides to call a tool, the handler routes to the correct service +1. `ai.NewTools(registry)` creates a tool set bound to the service registry +2. `tools.Discover()` walks the registry and returns every endpoint as an `ai.Tool` +3. `ai.WithTools(tools)` wires execution into the model — tool calls are routed via RPC +4. When the model decides to call a tool, it routes to the correct service This works identically across all providers. Swap `"atlascloud"` for `"anthropic"` or `"openai"` and the same services, tools, and handlers work without changes. @@ -299,5 +299,5 @@ The dedicated `atlascloud` provider simply sets these defaults for you. - [Atlas Cloud](https://www.atlascloud.ai/) — Sign up and get an API key - [AI Provider Integration Guide](/docs/guides/ai-provider-guide) — How providers are built -- [ai/tools package](https://pkg.go.dev/go-micro.dev/v5/ai/tools) — Service-to-tool discovery +- [ai.Tools](https://pkg.go.dev/go-micro.dev/v5/ai.Tools) — Service-to-tool discovery - [Blog: Atlas Cloud Sponsors Go Micro](/blog/8) — Announcement post