mirror of
https://github.com/go-micro/go-micro.git
synced 2026-06-15 19:35:13 +02:00
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:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user