refactor(ai): rename ToolSet to Tools, simplify wiring with WithTools (#2917)

Move tool discovery/execution fully into the ai package as ai.Tools
(formerly ai.ToolSet), and simplify the usage model:

- NewTools(reg, ai.ToolClient(c)) takes the execution client as an
  option instead of threading it through Handler(c) per call
- New ai.WithTools(tools) option wires the tool handler into a model
  in one call, replacing ai.WithToolHandler(set.Handler(c))
- ai.DiscoverTools(reg) for one-shot discovery

Before:
  set := ai.NewToolSet(reg)
  list, _ := set.Discover()
  m := ai.New(p, ai.WithToolHandler(set.Handler(client)))

After:
  tools := ai.NewTools(reg, ai.ToolClient(client))
  list, _ := tools.Discover()
  m := ai.New(p, ai.WithTools(tools))

Update ai/flow, micro chat, README, ai integration doc, Atlas Cloud
guide, and blog posts 3/8/9/10.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Asim Aslam
2026-05-30 14:21:35 +01:00
committed by GitHub
co-authored by Claude
parent a9421e5b7e
commit c4b4cbef25
13 changed files with 275 additions and 284 deletions
+4 -5
View File
@@ -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,
+3 -4
View File
@@ -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 {
+16
View File
@@ -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()
}
}
}
+184
View File
@@ -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"
}
}
-197
View File
@@ -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"
}
}
+26 -34
View File
@@ -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 := &registry.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")
}
}
+3 -4
View File
@@ -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))
+8 -8
View File
@@ -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)
+1 -1
View File
@@ -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.
+4 -5
View File
@@ -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
+8 -8
View File
@@ -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.
+8 -8
View File
@@ -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
})
```
@@ -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