Compare commits

..

125 Commits

Author SHA1 Message Date
Copilot 0b41c71681 feat(health): add RegistryCheck for registry connectivity health checks (#2957)
goreleaser / goreleaser (push) Waiting to run
* Initial plan

* feat(health): add RegistryCheck for registry connectivity health checks

Add a RegistryCheck function to the health package that creates a health
check verifying connectivity to the service registry. This enables
Kubernetes readiness probes to detect when a service loses its connection
to the registry (e.g. etcd).

Usage:
  health.Register("registry", health.RegistryCheck(reg))

* fix(health): simplify error assertion in registry check test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-10 08:25:23 +01:00
Asim Aslam 550033dcce Optimize image formats and fix lease re-registration issue (#2959)
* perf: convert generated PNGs to optimized JPEGs (12MB -> 1.5MB)

The landing and docs loaded 18 AI-generated PNGs at 0.5-1MB each. They're
1200x800 RGB illustrations with no transparency, so they recompress ~8x
as progressive JPEG (quality 82) with no visible loss. Convert all,
update every reference (.png -> .jpg), and drop the originals (including
the unused hero.png). Generated images: 12.3MB -> 1.5MB.

* fix(registry/etcd): re-register when a lease silently expires (#2956)

The keepalive rework (long-lived KeepAlive instead of KeepAliveOnce)
moved lease renewal entirely onto the keepalive goroutine; the 30s
periodic Register now skips on the 'unchanged' check. The goroutine only
reacted to the keepalive channel closing, so a lease that expired
server-side without a prompt channel close (e.g. a partition that
outlasted the 90s TTL) left the node de-registered from etcd while the
cache still believed it was registered — and nothing re-registered it.
That is the hidden-failure mode reported in #2956.

React to a non-positive TTL keepalive response the same as a channel
close: drop the cached lease/hash so the next Register performs a full
re-registration. Extract the loop into keepAliveLoop and unit-test the
TTL-expired, channel-closed, and healthy paths (no etcd required).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-10 08:24:49 +01:00
Asim Aslam 584a9f2132 feat(health): add RegistryCheck for registry connectivity (#2956) (#2958)
A go-micro service can keep running while it has silently lost its
connection to the registry (etcd, Consul, …) — the process looks healthy
but other services can no longer discover it, and Kubernetes sees the
pod as fine. health.RegistryCheck(reg) probes connectivity via
ListServices and, registered as a critical check, makes /health/ready
report not-ready so a readiness probe can pull the pod from rotation.

- Works with any registry implementation (no interface change).
- Honors the check timeout: an unreachable/hung registry is reported
  down rather than blocking the probe.
- Tests cover healthy, down, timeout, nil, and the not-ready integration.
- Documented in the health guide with the Kubernetes readiness example.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-10 08:18:00 +01:00
Asim Aslam 6488d8402d Organize README features, enhance testing, and update docs (#2955)
* docs: group README features by section (AI / Framework / DX)

The features table repeated 'AI' down the Category column. Split into
three grouped tables — AI, Framework, Developer experience & deployment —
dropping the repetitive column. Adds a Guardrails row (MaxSteps,
ApproveTool).

* test: flow-to-agent end-to-end in the harness

Proves 'Flow triggers, Agent reasons': a workflow with FlowAgent hands an
event to the registered conductor agent over RPC, which plans, creates
tasks, and delegates to comms — the whole chain over real RPC with only
the LLM mocked. Deterministic (shared in-memory registry, no sleeps),
passes under -race.

* blog: 'The Evolution of Microservices' (#19)

A technical history of distributed-systems eras — the monolith's
coordination cost, the distributed-systems tax, containers and
declarative orchestration, the service mesh, and the modular-monolith
correction — establishing the durable unit (named, typed, discoverable,
independently deployable) that every runtime wave required. Then the
technical argument for agents: an LLM tool call needs exactly a service
interface, so the caller shifts from deterministic code to a reasoner
that composes typed capabilities from intent, with the honest caveats
(non-determinism, cost, guardrails). Not a product pitch.

* docs: bump install version to v5.27.0

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-08 10:16:05 +01:00
Asim Aslam d3be610367 Refactor README features and add end-to-end flow testing (#2954)
goreleaser / goreleaser (push) Waiting to run
* docs: group README features by section (AI / Framework / DX)

The features table repeated 'AI' down the Category column. Split into
three grouped tables — AI, Framework, Developer experience & deployment —
dropping the repetitive column. Adds a Guardrails row (MaxSteps,
ApproveTool).

* test: flow-to-agent end-to-end in the harness

Proves 'Flow triggers, Agent reasons': a workflow with FlowAgent hands an
event to the registered conductor agent over RPC, which plans, creates
tasks, and delegates to comms — the whole chain over real RPC with only
the LLM mocked. Deterministic (shared in-memory registry, no sleeps),
passes under -race.

* blog: 'The Evolution of Microservices' (#19)

A technical history of distributed-systems eras — the monolith's
coordination cost, the distributed-systems tax, containers and
declarative orchestration, the service mesh, and the modular-monolith
correction — establishing the durable unit (named, typed, discoverable,
independently deployable) that every runtime wave required. Then the
technical argument for agents: an LLM tool call needs exactly a service
interface, so the caller shifts from deterministic code to a reasoner
that composes typed capabilities from intent, with the honest caveats
(non-determinism, cost, guardrails). Not a product pitch.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-08 09:09:48 +01:00
Asim Aslam 35bc58e5d2 Enhance onboarding experience and clarify workflows vs agents (#2953)
* blog: 'Not Everything Should Be an Agent' (#18) on workflows

The workflow counterpart to the plan/delegate post: when the path is
known, use a deterministic Flow, not an autonomous agent. Frames flow vs
agent as two modes of the same building blocks, covers flow-triggers-
agent dispatch and the agent guardrails, and gives the simplest-first
guidance (single call -> workflow -> agent). Continues the arc from
blog 14/16/17; references Building Effective Agents in passing.

* docs: fix new-user onboarding friction

- README: lead Quick Start with a no-key 30-second path (micro new ->
  micro run -> curl), then the AI --prompt path with an explicit
  'export ANTHROPIC_API_KEY' so the headline command no longer fails
  silently for users without a key.
- Unify all install versions to v5.26.0 (README + docs were split across
  v5.16.0 / v5.25.0).
- Refresh the docs landing overview from the old 'microservices
  framework' framing to 'services and agents', matching the README.
- getting-started: add Prerequisites (Go 1.21+, and that a provider key
  is only needed for AI features).
- README features: Flows -> Workflows wording.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-08 08:52:11 +01:00
Asim Aslam e416ea4a75 Enhance agent workflows with guardrails and documentation updates (#2952)
* docs: map go-micro onto Anthropic's workflows-vs-agents taxonomy

- new guide 'Agents and Workflows': adopts Anthropic's Building Effective
  Agents vocabulary — workflow (predefined path) = flow, agent (dynamic
  self-direction) = agent — maps the augmented-LLM building block and the
  five workflow patterns onto go-micro, and shows routing (chat router)
  and orchestrator-workers (conductor + plan/delegate) are already native.
- flow package doc reframed as a workflow (predefined path) per the same
  taxonomy, with guidance on flow vs agent.
- nav + README link the new guide.

* feat: agent guardrails — step limit and tool approval hook

Anthropic's Building Effective Agents stresses stopping conditions and
human-in-the-loop checkpoints for autonomous agents. Add both as plain
options enforced at the tool-handler choke point — no provider changes,
no new abstraction:

- MaxSteps(n): bound tool executions per Ask; beyond the limit, actions
  are refused and the model is told to stop and summarize.
- ApproveTool(fn): gate each action before it runs; returning false
  blocks it and surfaces the reason to the model. The internal plan tool
  is never gated.

Exposed at the micro package (AgentMaxSteps, AgentApproveTool, ApproveFunc).
Tests cover the limit, blocking, and that plan is not gated. Guardrails
section of the agents-and-workflows guide updated from 'active work' to
documented options.

* feat: flow can dispatch to an agent (flow triggers, agent reasons)

Unify the engine without collapsing the workflow/agent distinction. A
Flow with Agent set hands each event's rendered prompt to a named
registered agent over RPC (Agent.Chat) instead of running its own LLM
step — so the workflow stays the deterministic trigger and the agent is
the reasoning engine, with its plan, delegate, memory, and guardrails.
A plain flow is unchanged (single augmented-LLM step).

- flow.Agent(name) / micro.FlowAgent(name); flow stores the client and
  skips model setup when dispatching.
- test: dispatch routes to comms.Agent.Chat with the rendered prompt and
  records the reply.
- guide: 'Flow triggers, Agent reasons' section.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-08 08:32:32 +01:00
Asim Aslam 830c8d84c2 Claude/loving meitner 3 etoi (#2951)
* rename coordinator agent to conductor in example and docs

* add plan & delegate integration harness

Runs the real go-micro stack end to end — services, registry, RPC, the
agent loop, store, and delegate-first routing — with only the LLM mocked
by a deterministic provider. Proves discovery, tool execution, plan
persistence, and agent-to-agent delegation over RPC work without an API
key; swap the provider to run the same flow against a live model.

* test: deterministic CI integration test + provider flag for harness

- main_test.go: TestPlanDelegateEndToEnd drives the full real stack
  (services, RPC, agent loop, store, delegate-first routing) over a
  shared in-memory registry — no mDNS, no sleeps. Asserts 3 tasks
  created via RPC, plan persisted to the store, and delegation reaching
  the comms agent (notify called once). Passes under -race, ~0.03s.
- main.go: add -provider flag (defaults to mock) and key detection so the
  same harness runs against a live model with no code change.

* chore: gitignore built harness/example binaries

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-07 19:28:24 +01:00
Asim Aslam 6a73608e9c rename coordinator agent to conductor in example and docs (#2950)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-07 18:33:04 +01:00
Asim Aslam cb33decd97 Add built-in plan and delegate tools for agents with examples (#2949)
* feat: add plan and delegate as built-in agent tools

Give agents two self-capabilities, expressed as plain tools wired into
the existing tool handler — no harness or graph, consistent with
"services are the only abstraction":

- plan: record/update an ordered plan, persisted to store-backed memory
  and surfaced in the system prompt on later turns (externalized
  planning).
- delegate: hand a self-contained subtask to another agent.
  Delegate-first — if the target names a registered agent it is called
  via RPC; otherwise a focused ephemeral sub-agent is created with
  agent.New + Ask in a fresh, isolated context (loads/persists no
  history, no built-in tools, so it cannot re-delegate).

Both are added automatically to any non-ephemeral agent, so existing
micro.NewAgent services and micro chat routing get them for free.
Tests are hermetic (memory store + memory registry).

* feat: add agent-plan-delegate example and document plan/delegate

- examples/agent-plan-delegate: coordinator that plans multi-step work,
  creates tasks with its own tools, and delegates notification to a
  separate registered comms agent over RPC.
- integration tests driving the full Ask loop through a fake provider:
  plan tool exposure + persistence, ephemeral delegation with isolated
  context, delegate-first RPC routing to a registered agent.
- docs: README (Building Agents + features + examples), AGENT_DESIGN
  (Built-in Capabilities), agent-patterns guide (Pattern 9), CLAUDE.md.

* docs: blog post and guide for plan & delegate

- blog/17: "Plan & Delegate: Deep Agents in Go" — what the feature is,
  how plan and delegate work, and a runnable getting-started path.
- guides/plan-delegate: reference guide with the smallest-agent snippet,
  plan/delegate semantics, and the multi-agent example; linked in nav.
- example: auto-detect provider/key from common env vars (ANTHROPIC_API_KEY,
  OPENAI_API_KEY, ...) so 'export KEY && go run main.go' just works.
- onboarding: getting-started paths now include go mod init / go get and a
  clone-and-run path, so a reader can actually run it from a cold start.

* refactor: reframe plan/delegate blog and clean up sub-agent construction

- blog/17 retitled "Agents That Plan and Delegate" and reframed around
  intent (plan = state intent, delegate = direct it), positioned as the
  next beat after blog 15/16 and tied to the existing store + agent RPC
  rather than re-announcing them. "Deep agents" now a single in-passing
  nod, matching how blog 14 references LangChain.
- agent: add unexported newEphemeral constructor for sub-agents instead
  of type-asserting the public Agent interface to set an internal field;
  matches the options-only construction idiom used elsewhere.

* feat: expose plan & delegate in the micro chat fallback

Add agent.Builtins(opts...) — returns the built-in tools plus a handler,
so the plan/delegate capabilities can be wired into a tool loop that
isn't a running Agent. micro chat's direct-service fallback now reuses
it (single source of truth, no duplicated handler logic), so planning
and delegation are available there too, not just for registered agents.
Adds a test for the accessor; notes CLI availability in the guide.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-07 10:55:34 +01:00
Asim Aslam 9bed04ced0 Enhance micro run with interactive console and image compression (#2948)
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
goreleaser / goreleaser (push) Waiting to run
* perf: compress hero image — 1.4MB to 80KB

Resized from 1536px to 1200px, converted to JPEG at quality 80.
80KB loads instantly vs 1.4MB stalling on slower connections.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: micro run drops into interactive console

One command does everything — generate, start, and chat. No
separate micro chat step. The landing page shows micro run
dropping straight into the > prompt.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: interactive console in micro run, -d for detached mode

micro run now drops into an interactive chat console after services
start. The console discovers services, exposes them as tools, and
lets you talk to them through an LLM — same as micro chat but
built into the run experience.

- Detects MICRO_AI_PROVIDER and MICRO_AI_API_KEY from environment
- Falls back to provider-specific env vars (ANTHROPIC_API_KEY, etc.)
- If no API key, prints hint and blocks on Ctrl-C (no console)
- -d / --detach flag skips the console (background mode)
- Ctrl-C always shuts everything down

Removed adopters section from README.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 20:49:25 +01:00
Asim Aslam 2e89961386 perf: compress hero image — 1.4MB to 80KB (#2947)
Resized from 1536px to 1200px, converted to JPEG at quality 80.
80KB loads instantly vs 1.4MB stalling on slower connections.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 14:38:02 +01:00
Asim Aslam 39e8dc7311 Reposition hero section and optimize hero image for landing page (#2946)
* docs: reposition hero — framework for services and agents

Landing page: "Build Services and Agents in Go" — positions as a
framework, not a code generator. Tagline: "A framework for
microservices that AI agents can discover, use, and manage."

Hero command reverts to go get (the framework) instead of
micro run --prompt (a feature).

README matches: "framework for building services and agents in Go."

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* perf: compress hero image — 1.4MB to 80KB

Resized from 1536px to 1200px, converted to JPEG at quality 80.
80KB loads instantly vs 1.4MB stalling on slower connections.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 14:32:35 +01:00
Asim Aslam c93bcdf4d3 Update landing page documentation and fix flow CLI import (#2945)
* docs: remove code references from landing page feature grid

Landing page describes concepts, not APIs. Removed micro.NewAgent()
and micro.NewFlow() code references. Features described in plain
language — what they do, not how to code them.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* fix: update micro flow CLI to import from go-micro.dev/v5/flow

The flow package moved to top-level but the CLI still imported
from the old ai/flow path. Fixed to use go-micro.dev/v5/flow.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 11:20:40 +01:00
Asim Aslam cd760a29f1 Update landing page content and fix CLI import path (#2944)
* docs: remove code references from landing page feature grid

Landing page describes concepts, not APIs. Removed micro.NewAgent()
and micro.NewFlow() code references. Features described in plain
language — what they do, not how to code them.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* fix: update micro flow CLI to import from go-micro.dev/v5/flow

The flow package moved to top-level but the CLI still imported
from the old ai/flow path. Fixed to use go-micro.dev/v5/flow.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 11:12:00 +01:00
Asim Aslam e45d3df0ad docs: remove code references from landing page feature grid (#2943)
Landing page describes concepts, not APIs. Removed micro.NewAgent()
and micro.NewFlow() code references. Features described in plain
language — what they do, not how to code them.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 11:08:17 +01:00
Asim Aslam 6731ee2f0c Update documentation and blogs for RPC-based agent architecture (#2942)
* docs: update blog posts 15 and 16 to reflect RPC-based agents

Blog 15: replaced broker-based agent communication with RPC —
agents are services, they communicate via standard RPC, no pub/sub
hacks. Updated the framework mapping section.

Blog 16: added proto definition, micro call example, and explanation
that agents are real services with proto-defined endpoints. Updated
Ask() method name.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: update agent design doc and blog posts for RPC-based agents

Rewrote AGENT_DESIGN.md — agents are services with proto-defined
Agent.Chat endpoints, communicate via RPC, no broker dependency.
Includes proto definition, CLI examples, generation output.

Blog 15: replaced broker references with RPC.
Blog 16: added proto definition and micro call example.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: purge all stale broker-based agent references

Updated across all surfaces:
- README: agents described as services with RPC, Ask() not Chat(),
  micro call example instead of micro agent chat
- Blog 15: replaced broker communication with RPC description
- Blog 16: replaced "coordinate through the broker" with RPC
- Getting started: agent is a service with proto endpoint, Ask()
  not Chat(), added micro flow CLI commands (run/exec), expanded
  CLI workflow table

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 11:02:45 +01:00
Asim Aslam f7c042ef26 docs: update blog posts 15 and 16 to reflect RPC-based agents (#2941)
Blog 15: replaced broker-based agent communication with RPC —
agents are services, they communicate via standard RPC, no pub/sub
hacks. Updated the framework mapping section.

Blog 16: added proto definition, micro call example, and explanation
that agents are real services with proto-defined endpoints. Updated
Ask() method name.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 10:31:02 +01:00
Asim Aslam 1bc886fa82 Introduce Agent abstraction and integrate with chat router (#2939)
* docs: Agent interface design sketch

Proposes Agent as a top-level abstraction alongside Service in the
micro package. Agent manages services — scoped tools, system prompt,
conversation memory, registry-discoverable.

Design only, no implementation.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: Agent as a first-class abstraction

Introduce micro.NewAgent() alongside micro.New() — Agent is to
intelligence what Service is to capability.

Agent interface:
- Chat(ctx, message) (*Response, error) — core interaction method
- Run() — registers in registry, subscribes to broker, blocks
- Stop() — graceful shutdown
- Scoped tools — only sees endpoints of its assigned services
- Persistent memory — conversation history stored in store
- Agent-to-agent — communication via broker topics

Top-level API:
  agent := micro.NewAgent("task-mgr",
      micro.AgentServices("task"),
      micro.AgentPrompt("You manage tasks."),
      micro.AgentProvider("anthropic"),
  )
  agent.Run()

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: wire agents into chat router, add micro agent CLI, expose Flow

Three top-level abstractions:
  micro.New("task")              — Service (capability)
  micro.NewAgent("task-mgr")     — Agent (intelligence)
  micro.NewFlow("onboard-user")  — Flow (event-driven orchestration)

micro chat as router:
- Discovers agents from registry on startup
- Single agent: routes directly
- Multiple agents: LLM classifies intent, dispatches to right agent
  via route_to_agent tool
- No agents: falls back to current direct-service behaviour
- Banner shows discovered agents

micro agent CLI:
- micro agent list — shows registered agents and their services
- micro agent describe <name> — shows agent details from registry

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: move flow to top level, update docs for three abstractions

Package structure now consistent:
  service/   — Service (capability)
  agent/     — Agent (intelligence)
  flow/      — Flow (event-driven orchestration)

ai/flow/ kept as backward-compatible re-export.

Updated across all surfaces:
- CLAUDE.md: added agent/ and flow/ to project structure
- README: added "Building Agents" section with NewAgent() examples,
  updated features table (Agents, Flows, Chat router), CLI table
  (agent list, agent describe), docs links
- Website: features grid shows Services, Agents, Flows as the three
  pillars alongside generation, MCP, and pluggable architecture
- micro.go: Flow imported from top-level flow/ package

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: rewrite getting-started, fix ai-integration import paths

Getting started now covers all three abstractions:
- Service (write handlers, micro run, templates)
- Agent (micro.NewAgent, scoped tools, memory, CLI)
- Flow (event-driven LLM orchestration)
Leads with prompt-based generation, then manual service creation.

ai-integration.md: fixed flow import path from go-micro.dev/v5/ai/flow
to go-micro.dev/v5/flow, updated stack diagram to show agent/flow/chat.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* blog: Introducing micro.NewAgent()

Post 16 — announces Agent as a first-class abstraction. Shows the
API (NewAgent, AgentServices, AgentPrompt, AgentProvider), scoped
tools, persistent memory, multi-service agents, multi-agent systems,
and the three-abstraction comparison table (Service/Agent/Flow).

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: fix agent registration, blog post 16

Agent registration:
- Add node with address so mDNS can discover agents
- Store type and services in node metadata (mDNS requirement)
- Connect broker before subscribing, non-fatal if broker unavailable
- Print registration confirmation on Run()

Agent/chat discovery:
- Check both service-level and node-level metadata for type=agent
  (mDNS stores metadata on nodes, not services)

Blog post 16: "Introducing micro.NewAgent()" — announces the Agent
abstraction with code examples, comparison table, multi-agent patterns.

Tested end-to-end: micro run → micro agent list discovers the agent →
micro chat routes to it → agent calls service endpoints.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: agents are proper services with RPC Chat endpoint

Refactored agent to use server.Server instead of fake registry entries.
An agent now:
- Creates a real RPC server with server.Name(agentName)
- Registers an Agent.Chat handler callable via standard RPC
- Sets server metadata type=agent, services=x,y for discovery
- No more fake addresses or broker hacks

micro chat calls agents via RPC (client.Call) instead of creating
local agent instances. The registry stays clean — agents are real
services with real endpoints.

Removed broker dependency from agent options. Agent-to-agent
communication is just RPC like everything else.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: agent uses proto-defined RPC interface

Added agent/proto/agent.proto with Agent service definition:
  rpc Chat(ChatRequest) returns (ChatResponse)

Agent now implements the generated AgentHandler interface and
registers via pb.RegisterAgentHandler. The Chat endpoint is a
standard proto-based RPC callable by any go-micro client.

Renamed the programmatic API from Chat() to Ask() to avoid
collision with the proto handler method name.

micro chat calls agents via standard RPC with JSON-encoded
request/response — no special types needed on the caller side.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: generate agent alongside services, update all docs

micro run --prompt now generates an agent binary that manages all
the generated services. The agent reads MICRO_AI_PROVIDER and
MICRO_AI_API_KEY from the environment. micro run propagates these
when started with --prompt.

Run banner shows services and agents separately.

Updated README, getting-started guide, and landing page to show
the complete flow: generate → services + agent start → micro chat
routes to agent → agent orchestrates services.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 10:25:14 +01:00
Asim Aslam 844ac5bc52 Revamp landing hero and introduce agent-based microservices model (#2938)
* docs: update landing hero — describe it, run it, talk to it

Lead with the AI-first experience instead of "write services in Go."
The entry point is now describing what you need, not writing code.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* blog: Agents for Services — a new model for microservices

Post 15 — explores the concept of distributed agents managing
services. Each service has an agent assigned to it (not embedded
in it). Agents are the intelligence layer; services are the
capability layer. Multi-service agents span domain boundaries.
Agent-to-agent communication through the broker.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 20:05:45 +01:00
Asim Aslam bc570d674f docs: update landing hero — describe it, run it, talk to it (#2937)
Lead with the AI-first experience instead of "write services in Go."
The entry point is now describing what you need, not writing code.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 18:51:41 +01:00
Asim Aslam e59d056f97 Update Discord invite link and enhance landing hero description (#2936)
* chore: update Discord invite link everywhere

Replace discord.gg/jwTYuUVAGh and discord.gg/go-micro with
discord.gg/WeMU5AGxD across all docs, blog posts, issue templates,
security policy, and contrib READMEs.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: update landing hero — describe it, run it, talk to it

Lead with the AI-first experience instead of "write services in Go."
The entry point is now describing what you need, not writing code.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 17:50:11 +01:00
Asim Aslam e13ef7eed5 chore: update Discord invite link everywhere (#2935)
Replace discord.gg/jwTYuUVAGh and discord.gg/go-micro with
discord.gg/WeMU5AGxD across all docs, blog posts, issue templates,
security policy, and contrib READMEs.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 13:46:30 +01:00
Asim Aslam 23f682181c Update section title from 'The Bet' to 'The next step' 2026-06-04 11:53:31 +01:00
Asim Aslam bc1092b18a Restructure README for AI experience and framework clarity (#2934)
* docs: restructure README — AI story completes before manual code

Quick Start now flows through the full AI experience: generate →
review → run → chat → grow (mid-conversation service generation).
The reader sees the complete prompt-to-production story without
interruption.

"Writing Services" is a separate section below for developers who
want to understand the framework underneath. Shows Go code, doc
comments, @example tags, micro run, and scaffolding templates.

Features table and CLI table reordered: AI first, then framework.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* blog: Going All In on AI

Post 14 — the strategic case for making AI the primary direction.
Covers the evolution from microservices framework to AI-native
platform, why the timing is right (tool calling works, MCP is real,
sponsors align), and what's not changing (framework still works,
no agent framework complexity).

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 11:52:48 +01:00
Asim Aslam 16918669ca docs: restructure README — AI story completes before manual code (#2933)
Quick Start now flows through the full AI experience: generate →
review → run → chat → grow (mid-conversation service generation).
The reader sees the complete prompt-to-production story without
interruption.

"Writing Services" is a separate section below for developers who
want to understand the framework underneath. Shows Go code, doc
comments, @example tags, micro run, and scaffolding templates.

Features table and CLI table reordered: AI first, then framework.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 11:35:57 +01:00
Asim Aslam a357d1870d Update README and landing page with new AI-native hero image (#2932)
* docs: add binary install option to README quick start

Show curl install.sh first (no Go required), go install second.
Uses the existing install script at go-micro.dev/install.sh which
downloads pre-built binaries from GitHub releases.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: regenerate hero image for new AI-native positioning

New hero shows terminal running micro run with service generation,
an AI agent orchestrating, and task/shipping/category service nodes.
Matches the "Microservices That AI Agents Can Use" headline.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* fix: replace video with hero image on landing page

The old video autoplayed and covered the new hero image. Replace
the video element with a static img tag showing the new AI-native
hero graphic.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 11:27:21 +01:00
Asim Aslam 91a545c6f2 Enhance README with binary install option and update hero image (#2931)
* docs: add binary install option to README quick start

Show curl install.sh first (no Go required), go install second.
Uses the existing install script at go-micro.dev/install.sh which
downloads pre-built binaries from GitHub releases.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: regenerate hero image for new AI-native positioning

New hero shows terminal running micro run with service generation,
an AI agent orchestrating, and task/shipping/category service nodes.
Matches the "Microservices That AI Agents Can Use" headline.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 11:21:47 +01:00
Asim Aslam 00392aa1f2 docs: add binary install option to README quick start (#2930)
Show curl install.sh first (no Go required), go install second.
Uses the existing install script at go-micro.dev/install.sh which
downloads pre-built binaries from GitHub releases.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 11:15:40 +01:00
Asim Aslam b54507710d Revise README with new install command and examples
Updated installation command and added usage examples.
2026-06-04 11:14:40 +01:00
Asim Aslam d541625e32 docs: unify README and website around one story (#2929)
Both surfaces now lead with the same line: "Go Micro is a framework
for building microservices that AI agents can use."

README:
- Leads with prompt generation + chat (the differentiator)
- Features as a compact table instead of paragraph-per-feature
- CLI workflow table
- Removed redundant sections, tightened to ~150 lines

Website:
- Hero: "Microservices That AI Agents Can Use"
- Hero command: micro run --prompt instead of go get
- Features grid reordered: AI tools, orchestration, generation first
- First two-col section: describe/generate/run/chat story
- Architecture and DX sections follow

Both tell the same story in the same order:
1. What it is (microservices framework)
2. What makes it different (every service is an AI tool)
3. How you use it (prompt → run → chat)
4. What's underneath (registry, RPC, store — all pluggable)

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 11:13:03 +01:00
Asim Aslam 7662b26b07 syntax highlighting (#2928)
* docs: add blog post 13 to blog index

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: add syntax highlighting to blog post 13 code blocks

Add language tags (bash, go, text) to all fenced code blocks so
Rouge highlights them correctly.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 08:19:52 +01:00
Asim Aslam 397010a82a docs: add blog post 13 to blog index (#2927)
https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 22:16:06 +01:00
Asim Aslam d3c4981326 Enhance AI service generation with prompt-based architecture and logic (#2926)
goreleaser / goreleaser (push) Waiting to run
* feat: add micro new --prompt and micro run --prompt

Add AI-powered service generation: describe a system in natural
language and get real go-micro services with proto definitions,
handlers, doc comments, and MCP support.

micro new --prompt "a contact book with notes and tags" \
  --provider anthropic

Generates:
  contacts/ — CRUD service with name, email, phone fields
  notes/    — notes linked to contacts
  tags/     — tagging system

Each service gets:
  proto/{name}.proto   — domain model + CRUD endpoints
  handler/{name}.go    — in-memory store, @example tags for MCP
  main.go              — MCP-enabled, proper imports
  go.mod + Makefile     — compiles with go mod tidy + make proto

micro run --prompt does the same then starts all services.

The LLM designs the architecture (service names, fields, endpoints,
descriptions) and returns structured JSON. Code generation uses
the existing template patterns — the output is standard go-micro
code that compiles, runs, and is immediately callable via MCP
and micro chat. No AI dependency at runtime.

* feat: LLM generates real business logic with compile-fix loop

Rebuild the generate package so the LLM writes actual handler
code with business logic, not just CRUD scaffolding.

The flow is now:
1. LLM designs architecture (service names, fields, endpoints)
   → returns structured JSON
2. Proto, main.go, go.mod, Makefile generated deterministically
   from the design (guaranteed to be correct)
3. go mod tidy + make proto compiles the protos
4. LLM generates handler code with REAL business logic
   → given the proto, endpoint descriptions, and go-micro patterns
5. go build — does it compile?
6. If no: feed errors back to LLM, get fixed code (up to 3 attempts)
7. If yes: service is ready

The handler prompt instructs the LLM to:
- Use sync.RWMutex for thread-safe in-memory state
- Include validation, edge cases, meaningful errors
- Write doc comments with @example tags for MCP
- Implement actual domain logic, not just map operations

Proto generation still uses deterministic templates (CRUD +
custom endpoints from the design spec) to guarantee correctness.
The compile-fix loop catches LLM mistakes automatically.

Both micro new --prompt and micro run --prompt use this flow.

* fix: handle edge cases in prompt-based generation

- Fix PATH for protoc-gen-micro in child processes
- Handle existing directories: skip structural files (main.go,
  go.mod, Makefile) if dir exists, always regenerate proto,
  only write placeholder handler if none exists
- Allow re-running micro new --prompt on same directory to
  iterate on business logic without clobbering user edits

Tested end-to-end: "a simple todo list with tasks and categories"
generates 2 services (task-service, category-service) with real
business logic (validation, toggle complete, etc.), compiles
after 1 fix iteration, and runs with 6 MCP tools discovered.

* feat: auto-detect modified handlers on regeneration

Instead of requiring a --keep-handlers flag, the generate package now
tracks a SHA-256 hash of each generated handler in a .micro metadata
file. On re-run, if the user has edited the handler since generation,
it's left untouched. Unmodified handlers are regenerated normally.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: add tests, fix go.mod, gitignore, proto tracking, spinner

- Add 12 tests covering helpers, proto generation, hash tracking
- Fix go.mod: write minimal module file, let go mod tidy resolve deps
- Add .gitignore to prompt-generated services
- Protect user-edited proto files (same hash tracking as handlers)
- Add spinner during LLM calls so it doesn't look hung

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: signal handling, existing service discovery, help text

- Ctrl+C during generation now cancels LLM calls immediately via
  signal-aware context; re-run picks up where it left off
- Design() scans for existing services in the working directory and
  includes their proto definitions in the prompt, so the LLM extends
  the system rather than redesigning from scratch
- Updated --prompt help text with usage examples on both new and run
- Listed all supported providers in flag descriptions
- Added discoverExisting test

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: show endpoints in run --prompt output, add micro chat hint

Print endpoint names and descriptions when designing services so users
see what was built. Add a micro chat hint to the run banner so users
know how to interact with their services after startup.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* fix: generated go.mod uses go 1.24 with explicit go-micro require

go 1.22 with no explicit require caused Go to resolve sub-packages
(gateway/mcp, client, server) as separate modules, hitting stale v1.18
tags. Pin to go 1.24 + require go-micro.dev/v5 v5.24.0 so go mod tidy
resolves all sub-packages from the root module correctly.

Tested end-to-end: 4 services generated and compiled successfully.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* fix: skip handler regeneration when proto unchanged

Compare proto hash before and after structure generation. If the proto
didn't change and the handler wasn't edited by the user, skip go mod
tidy, make proto, LLM handler generation, and compile-fix entirely.
Prints "(unchanged)" instead.

Reduces re-run of 4-service project from ~2 minutes to ~10 seconds.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: confirm design before generating code

Show the service design (names, endpoints) and prompt "Generate? [Y/n]"
before spending LLM time on handler generation. Applies to both
micro new --prompt and micro run --prompt. Default is yes (enter).

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* fix: use port :0 for MCP in generated multi-service projects

Each generated service had mcp.WithMCP(":3001") hardcoded, causing
port conflicts when running multiple services. Use :0 to auto-assign
a free port. micro run's central gateway handles unified MCP access.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: truncation detection, tool result display in chat

- Detect truncated LLM responses (unbalanced braces, doesn't end
  with '}') and retry with a conciseness hint before falling through
  to compile-fix
- Show tool call results in micro chat output (← for success, ✗ for
  errors) so users can see what the LLM did
- Add Result/Error fields to ToolCall, populated by Anthropic provider
  after tool execution
- Add isTruncated tests

Tested end-to-end with Anthropic: services generate, compile, start,
register, respond to RPC calls, and micro chat discovers and calls
tools correctly.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* fix: Anthropic tool loop, service naming, chat tool results

Anthropic provider:
- Fix tool execution loop to properly iterate (was re-processing all
  tool calls instead of only new ones each round)
- Clean assistant content blocks before sending back (strip 'id' from
  text blocks that Anthropic rejects on input)
- Include tools in follow-up requests so model can make additional calls
- Loop up to 10 rounds until model responds with text only

Service naming:
- Strip '-service' suffix from micro.New() name so services register
  as 'task', 'category' instead of 'taskservice', 'categoryservice'

Chat:
- Show tool results (← for success) and errors (✗) in chat output

Tested end-to-end: create task → list tasks works as multi-step
orchestration through micro chat with Anthropic Claude.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: blog post 13 — from prompt to production

Covers the full micro run --prompt flow: design, generate, compile-fix,
run, and chat orchestration. Positions agent-as-orchestrator as the
answer to service coordination.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* fix: timeouts, max_tokens, TTY detection, smaller services

- Add 60s timeout on design, 90s on handler generation, 60s on
  compile-fix LLM calls so hung providers don't block forever
- Bump Anthropic max_tokens from 4096 to 8192 to reduce truncation
- Add TTY detection: spinner prints static message in non-TTY (CI/pipes)
  instead of ANSI escape codes
- Tighten prompts: max 200 lines per handler, 2-4 services, 5-8 fields,
  explicit "services don't call each other" rule

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: chat suggests creating services when capabilities are missing

Update system prompt with the list of available services. When the user
asks for something no existing service can handle, the agent explains
what's available and suggests the exact micro new --prompt command to
create the missing service.

This is the natural evolution path: start with a few services, talk to
them via chat, and when the domain grows, the agent tells you what to
add. Each service stays small and focused.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: chat generates and starts services inline, drop -service suffix

Chat agent now has a micro_generate_service tool. When the user asks
for a capability that doesn't exist, the agent generates the service,
compiles it, starts it as a background process, waits for registration,
re-discovers tools, and uses the new endpoints immediately — all within
the conversation.

Service naming: design prompt now instructs LLM to return names without
'-service' suffix (e.g. 'task' not 'task-service'). buildMain keeps
TrimSuffix as safety net for backward compatibility.

Spawned processes are cleaned up when chat exits.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* docs: rewrite blog post 13 with inline service generation

Updated to reflect the full UX: services generate and start within
the chat conversation. Added the shipping example showing the agent
creating a service mid-conversation. Removed -service suffix from
all examples. Tightened the narrative around agent-as-orchestrator.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

* feat: persistent storage, README quickstart, auto-detect new services

Storage: generated handlers now use go-micro's store package instead
of in-memory maps. Data persists across restarts. The handler prompt
includes store API examples so the LLM generates correct store usage.

README: added "Generate From a Prompt" section with micro run --prompt
and micro chat examples, linking to blog post 13.

Watcher: micro run now scans for new service directories every 5s. When
micro chat generates a service, micro run detects the new directory,
builds it, starts it, and adds it to the watcher — fully automatic.
Added AddDir/Dirs methods to the watcher.

Blog: updated post 13 with persistent storage example and watcher note.

https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 20:31:01 +01:00
Asim Aslam 96280678ee Expose framework primitives via API gateway and MCP with auth control (#2925)
* feat: expose framework primitives via API gateway and MCP

Add registry, store, and broker as both HTTP routes and MCP tools
so AI agents and HTTP clients can inspect and operate the framework.

API gateway (/micro/* namespace):
  GET  /micro/registry         List registered services
  GET  /micro/registry/{name}  Describe a service
  GET  /micro/store            List store keys
  GET  /micro/store/{key}      Read a record
  POST /micro/store/{key}      Write a record
  POST /micro/broker/{topic}   Publish a message

MCP gateway (micro_* tool prefix):
  micro_registry_list    List services
  micro_registry_get     Describe a service
  micro_store_list       List keys
  micro_store_read       Read a record
  micro_store_write      Write a record
  micro_broker_publish   Publish a message

Framework tools use a Handler field on the MCP Tool struct for
direct dispatch (no RPC). Service tools continue to use RPC.
Rate limiters and circuit breakers are applied to framework
tools the same as service tools.

* fix: make framework internals opt-in on API and MCP gateways

Framework primitives (registry, broker, store) are now only
exposed when explicitly enabled:

API gateway:  micro api --internal
MCP gateway:  Options{Internal: true}

Off by default — user services are always exposed, framework
internals require the flag. Banner output only shows framework
routes when enabled.

* fix: always expose framework internals, gate by auth in production

Revert the --internal flag approach. Framework primitives (registry,
broker, store) are now always exposed:

- micro api: /micro/* routes always available (dev tool)
- MCP gateway: micro_* tools always registered. When Auth is
  configured (production), they require micro:admin scope.
  Without Auth (dev), they're open — same as all other tools.

This follows the existing pattern: micro run/api = dev (open),
micro server = production (auth + scopes). Framework internals
follow the same security model as user services.

Remove the Internal option from MCP Options. Remove --internal
flag from micro api.

Note: scope persistence depends on the store backend. The default
in-memory store does not survive restarts. Use MICRO_STORE=file
for persistent scopes in production.

* fix: correct DefaultStore comment — it's file-backed, not memory

* fix(server): don't recreate deleted admin user on restart

When the default admin account is deleted via the dashboard, set
a marker key (auth/.admin-deleted) in the store. On startup, skip
admin creation if the marker exists. This prevents the default
admin/micro credentials from reappearing after restart when the
user has intentionally removed them.

* fix: improve agent playground first-run UX and fix doc 404s

Agent playground:
- Add setup hint in empty state explaining how to get started
  (click Settings, enter API key, type a prompt)
- Hide hint automatically when API key is already configured
- Add all 7 providers to dropdown (was only OpenAI + Anthropic)
- Include CLI fallback suggestion (micro chat)

Docs:
- Fix .md links to .html across all doc pages — Jekyll serves
  .html files, not .md. Fixes 404s including the micro run guide.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 11:05:51 +01:00
Asim Aslam 69fc228c73 Enhance CLI color output and expose framework primitives via API (#2924)
* feat: expose framework primitives via API gateway and MCP

Add registry, store, and broker as both HTTP routes and MCP tools
so AI agents and HTTP clients can inspect and operate the framework.

API gateway (/micro/* namespace):
  GET  /micro/registry         List registered services
  GET  /micro/registry/{name}  Describe a service
  GET  /micro/store            List store keys
  GET  /micro/store/{key}      Read a record
  POST /micro/store/{key}      Write a record
  POST /micro/broker/{topic}   Publish a message

MCP gateway (micro_* tool prefix):
  micro_registry_list    List services
  micro_registry_get     Describe a service
  micro_store_list       List keys
  micro_store_read       Read a record
  micro_store_write      Write a record
  micro_broker_publish   Publish a message

Framework tools use a Handler field on the MCP Tool struct for
direct dispatch (no RPC). Service tools continue to use RPC.
Rate limiters and circuit breakers are applied to framework
tools the same as service tools.

* fix: make framework internals opt-in on API and MCP gateways

Framework primitives (registry, broker, store) are now only
exposed when explicitly enabled:

API gateway:  micro api --internal
MCP gateway:  Options{Internal: true}

Off by default — user services are always exposed, framework
internals require the flag. Banner output only shows framework
routes when enabled.

* fix: always expose framework internals, gate by auth in production

Revert the --internal flag approach. Framework primitives (registry,
broker, store) are now always exposed:

- micro api: /micro/* routes always available (dev tool)
- MCP gateway: micro_* tools always registered. When Auth is
  configured (production), they require micro:admin scope.
  Without Auth (dev), they're open — same as all other tools.

This follows the existing pattern: micro run/api = dev (open),
micro server = production (auth + scopes). Framework internals
follow the same security model as user services.

Remove the Internal option from MCP Options. Remove --internal
flag from micro api.

Note: scope persistence depends on the store backend. The default
in-memory store does not survive restarts. Use MICRO_STORE=file
for persistent scopes in production.

* fix: correct DefaultStore comment — it's file-backed, not memory

* fix(server): don't recreate deleted admin user on restart

When the default admin account is deleted via the dashboard, set
a marker key (auth/.admin-deleted) in the store. On startup, skip
admin creation if the marker exists. This prevents the default
admin/micro credentials from reappearing after restart when the
user has intentionally removed them.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 08:26:47 +01:00
Asim Aslam 5d7609027b Enhance CLI with color output and add teardown blog post (#2923)
* feat(cli): add color output to micro chat and micro api

micro chat:
- Startup banner matching micro run style: bold header, cyan
  provider/model, green dots for each discovered tool endpoint
- Cyan bold prompt (> ) instead of plain
- Yellow arrow (→) with dimmed tool name for tool calls
- Red "error:" prefix for errors
- Dimmed "(history cleared)" for reset

micro api:
- Startup banner matching micro run style: bold header, cyan
  address, colored HTTP methods (green GET, yellow POST)

Brings the CLI UX closer to what the generated terminal
screenshot depicts — color-coded, professional, readable.

* feat(cli): adopt consistent color output across all commands

Apply the same banner/output style across the remaining commands:

micro new:    bold header, cyan service name, green ✓, cyan URLs
micro build:  green ✓ checkmarks, cyan file paths
micro deploy: bold header, cyan target
micro mcp:    bold header, green dots per tool, dimmed count
micro flow:   bold header, cyan flow/topic/provider

All commands now follow the micro run/chat/api pattern:
bold header, cyan values, green status indicators, dimmed hints.

* docs: add "Tools as Services" blog post

Write blog/12 — connects the AI story back to Go Micro's original
design: services were always self-describing, named, and uniformly
callable. The path from API gateway to MCP to LLM tools is the
same pattern — read the registry, present services in a format
the consumer understands, route calls back.

Covers the access layer pattern (HTTP, web, CLI, MCP, chat),
why doc comments became functional in the AI era, and how the
framework primitives (registry, broker, store) could all become
tools using the same mechanism.

Add to blog index, link forward from blog/11.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-02 22:15:18 +01:00
Asim Aslam 5601be009a docs: add "Build Your Own AI Agent CLI" teardown blog post (#2921)
Write blog/11 — a teardown of micro chat showing how to build an
LLM tool-calling agent in ~150 lines. Walks through the four pieces:
discover tools, create the model, track conversation, run the loop.
Uses the actual chat.go source. Ends with extension ideas and a
"make it yours" framing.

Add to blog index, link forward from blog/10.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-02 12:06:22 +01:00
Asim Aslam 888dbbca4a Refactor AI tool handling and enhance CLI command documentation (#2920)
goreleaser / goreleaser (push) Waiting to run
* refactor(ai): rename ToolSet to Tools, simplify wiring with WithTools

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.

* feat(cli): add per-interface commands (registry, broker, store, config)

Map go-micro's core interfaces onto the CLI so the framework's
building blocks are inspectable and manipulable from the terminal:

  micro registry list/get/watch       service discovery
  micro broker publish/subscribe      pub/sub messaging
  micro store read/write/delete/list  persistence
  micro config get/dump               dynamic config (from env)

Structured pluggably in cmd/micro/resource: each interface is one
file exposing a Command() func, all wired through a commandFuncs
slice in resource.go. Adding a new resource command is a single
file plus one slice entry. Shared printJSON/fail helpers keep
output and errors consistent across commands.

Each command's verbs mirror the interface methods. Output is JSON
for structured data, raw for single values. Update README and
getting-started with an "inspecting the framework" section.

* docs: update CLI README with all new commands

Add documentation for commands that were missing from the CLI README:
- micro new --template (crud, pubsub, api)
- micro api (standalone HTTP gateway)
- micro registry list/get/watch
- micro broker publish/subscribe
- micro store read/write/delete/list
- micro config get/dump
- micro chat (interactive LLM agent)
- micro flow run/exec (event-driven orchestration)
- micro mcp serve/list/test

Organized into sections: API Gateway, Inspecting the Framework
(registry, broker, store, config), and AI & Agents (chat, flow, mcp).

* refactor(ai): move History from caller to Request field

History is now pure state (no Generate method). Instead, pass it
via Request.History and call ai.Generate(ctx, model, req):

Before:
  hist := ai.NewHistory("system prompt", 50)
  resp, _ := hist.Generate(ctx, model, prompt, tools)

After:
  hist := ai.NewHistory(50)
  resp, _ := ai.Generate(ctx, model, &ai.Request{
      Prompt:       prompt,
      SystemPrompt: "system prompt",
      Tools:        tools,
      History:      hist,
  })

The model is always the thing you call. History is context you
pass in. ai.Generate() handles the bookkeeping: prepends
accumulated messages before the call, records the exchange after.

NewHistory no longer takes a system prompt (it belongs on the
Request, where it always did).

Update micro chat, ai/flow, and all blog posts/docs.

* refactor(ai): make History a plain message accumulator

History no longer has Generate or touches the model. It's just
Add/Messages/Reset/Len with truncation — a helper for building
Request.Messages across turns.

Before:
  hist := ai.NewHistory(50)
  resp, _ := ai.Generate(ctx, m, &ai.Request{History: hist, ...})

After:
  hist := ai.NewHistory(50)
  hist.Add("user", prompt)
  resp, _ := m.Generate(ctx, &ai.Request{Messages: hist.Messages(), ...})
  hist.Add("assistant", resp.Reply)

Remove History field from Request. Remove package-level
ai.Generate(ctx, model, req) wrapper — users call m.Generate()
directly, which is the interface method. History is a convenience
for accumulating messages, not a participant in generation.

Update micro chat, ai/flow, blog posts 9 and 10.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-30 16:20:38 +01:00
Asim Aslam f48d81c760 Cli commands (#2919)
* refactor(ai): rename ToolSet to Tools, simplify wiring with WithTools

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.

* feat(cli): add per-interface commands (registry, broker, store, config)

Map go-micro's core interfaces onto the CLI so the framework's
building blocks are inspectable and manipulable from the terminal:

  micro registry list/get/watch       service discovery
  micro broker publish/subscribe      pub/sub messaging
  micro store read/write/delete/list  persistence
  micro config get/dump               dynamic config (from env)

Structured pluggably in cmd/micro/resource: each interface is one
file exposing a Command() func, all wired through a commandFuncs
slice in resource.go. Adding a new resource command is a single
file plus one slice entry. Shared printJSON/fail helpers keep
output and errors consistent across commands.

Each command's verbs mirror the interface methods. Output is JSON
for structured data, raw for single values. Update README and
getting-started with an "inspecting the framework" section.

* docs: update CLI README with all new commands

Add documentation for commands that were missing from the CLI README:
- micro new --template (crud, pubsub, api)
- micro api (standalone HTTP gateway)
- micro registry list/get/watch
- micro broker publish/subscribe
- micro store read/write/delete/list
- micro config get/dump
- micro chat (interactive LLM agent)
- micro flow run/exec (event-driven orchestration)
- micro mcp serve/list/test

Organized into sections: API Gateway, Inspecting the Framework
(registry, broker, store, config), and AI & Agents (chat, flow, mcp).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-30 14:52:22 +01:00
Asim Aslam 3acf74a29a Refactor tool management and add CLI commands for interfaces (#2918)
* refactor(ai): rename ToolSet to Tools, simplify wiring with WithTools

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.

* feat(cli): add per-interface commands (registry, broker, store, config)

Map go-micro's core interfaces onto the CLI so the framework's
building blocks are inspectable and manipulable from the terminal:

  micro registry list/get/watch       service discovery
  micro broker publish/subscribe      pub/sub messaging
  micro store read/write/delete/list  persistence
  micro config get/dump               dynamic config (from env)

Structured pluggably in cmd/micro/resource: each interface is one
file exposing a Command() func, all wired through a commandFuncs
slice in resource.go. Adding a new resource command is a single
file plus one slice entry. Shared printJSON/fail helpers keep
output and errors consistent across commands.

Each command's verbs mirror the interface methods. Output is JSON
for structured data, raw for single values. Update README and
getting-started with an "inspecting the framework" section.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-30 14:43:09 +01:00
Asim Aslam c4b4cbef25 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>
2026-05-30 14:21:35 +01:00
Asim Aslam a9421e5b7e Update logo design, add AI integration documentation and blog post (#2916)
* feat: update Go Micro logo to interconnected nodes design

Replace the text-on-blue-square logo with a modern icon: three
teal nodes connected in a triangle, representing distributed
systems. Generated via Atlas Cloud. Clean at all sizes — works
as GitHub avatar, favicon, and nav bar icon.

* feat: new logo, AI integration architecture doc, and landing page CTA

Update logo to triangle-nodes icon + "Go Micro" text wordmark.
Save icon-only variant for favicon/avatar use.

Add docs/ai-integration.md — a single page that explains how the
AI stack fits together: services → registry → MCP gateway →
ai/tools → ai.Model → micro chat. Layer-by-layer with code
examples, provider table, and "what you don't need" section.

Add AI Integration to docs sidebar navigation (after Getting
Started). Update the landing page AI section with a direct CTA
button linking to the new doc.

* fix: restore original logo and add border-radius to all renders

Revert logo to original. Add border-radius: 8px to the logo img
in the landing page nav, docs layout nav, and blog layout nav
so the square logo renders with rounded corners everywhere.
Remove unused icon.png.

* docs: add micro chat blog post

Write blog/10 — a dedicated post for micro chat covering:
- What it does (interactive LLM agent for services)
- How it works (ai/tools → ai.History → ai.Model stack)
- Multi-turn conversation examples
- All provider options and env vars
- Single prompt mode for scripting
- Why it works (registry metadata + doc comments = tool descriptions)
- Programmatic usage with the same building blocks
- Link to micro flow as the event-driven counterpart

Add to blog index. Update blog 9 nav to link forward.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-30 13:46:05 +01:00
Asim Aslam 03f912f68a Add AtlasCloud sponsor logo to README 2026-05-30 09:58:44 +01:00
Asim Aslam 594ee107b7 Clean up README.md formatting
Removed unnecessary whitespace and line breaks in README.
2026-05-30 09:58:08 +01:00
Asim Aslam 2f164595f2 Add Atlas Cloud logo link to README
Added an image link for Atlas Cloud to the README.
2026-05-30 09:49:55 +01:00
Asim Aslam 740980a9cc Clean up whitespace in README.md
Removed extra whitespace before the second image link.
2026-05-30 09:49:33 +01:00
Asim Aslam 3d2d9acd68 Fix formatting issues in README.md 2026-05-30 09:46:28 +01:00
Asim Aslam 88aa0cc666 Update logo, add AI integration docs, and enhance CLI features (#2915)
* feat: update Go Micro logo to interconnected nodes design

Replace the text-on-blue-square logo with a modern icon: three
teal nodes connected in a triangle, representing distributed
systems. Generated via Atlas Cloud. Clean at all sizes — works
as GitHub avatar, favicon, and nav bar icon.

* feat: new logo, AI integration architecture doc, and landing page CTA

Update logo to triangle-nodes icon + "Go Micro" text wordmark.
Save icon-only variant for favicon/avatar use.

Add docs/ai-integration.md — a single page that explains how the
AI stack fits together: services → registry → MCP gateway →
ai/tools → ai.Model → micro chat. Layer-by-layer with code
examples, provider table, and "what you don't need" section.

Add AI Integration to docs sidebar navigation (after Getting
Started). Update the landing page AI section with a direct CTA
button linking to the new doc.

* fix: restore original logo and add border-radius to all renders

Revert logo to original. Add border-radius: 8px to the logo img
in the landing page nav, docs layout nav, and blog layout nav
so the square logo renders with rounded corners everywhere.
Remove unused icon.png.

* feat(ai): add ai/flow package and micro flow CLI

Add ai/flow — event-driven LLM orchestration for go-micro. A Flow
subscribes to a broker topic, discovers services as tools, and
feeds each event into an LLM that decides which RPCs to call.

Key types:
- flow.New(name, opts...) creates a flow with trigger topic,
  prompt template, provider config
- flow.Register(registry, broker, client) wires it into a service
- flow.Execute(ctx, data) runs the flow once (for testing/CLI)
- flow.Results() returns execution history

Add micro flow CLI with two subcommands:
- micro flow run: subscribe to a topic and react to events
- micro flow exec: one-shot execution with inline data

Both output JSON results with flow name, prompt, tool calls,
reply, answer, duration, and errors.

Example:
  micro flow run --trigger events.user.created \
    --prompt "New user: {{.Data}}. Send welcome email." \
    --provider anthropic

  micro flow exec --prompt "List all users" --provider anthropic

* docs: update flows blog post with ai/flow package and CLI examples

Add "Update: We Built It" section to blog/9 showing the ai/flow
package API, CLI usage for both event-driven and one-shot modes,
and what it does/doesn't do. Links the conceptual discussion to
the shipped implementation.

* feat(cli): add micro api gateway command, clarify run vs server

Add 'micro api' — a standalone lightweight HTTP-to-RPC gateway:
- POST /{service}/{endpoint} proxies to RPC calls
- GET / lists all services and endpoints
- GET /{service} describes a service
- GET /health returns ok
- Supports Micro-Endpoint header for endpoint routing
- No dashboard, no auth, no hot reload — just the proxy

Update help text to clarify the three gateway modes:
- micro api: bare HTTP-to-RPC proxy
- micro run: development mode (hot reload + gateway + agent playground)
- micro server: production mode (dashboard + auth + JWT)

* docs: update README, getting started, and AI integration for all new features

Update the development workflow table in both README and getting
started to include all CLI commands: micro new --template,
micro api, micro chat, micro flow, micro call.

Getting started:
- Add CRUD template example to quick start
- Update workflow table with 8 stages
- Add AI Integration, MCP, and gRPC Interop to Next Steps

README:
- Add template flag to quick start example
- Update workflow table
- Reorder User Guides with AI Integration prominent

AI Integration doc:
- Update stack diagram to include micro api and ai/flow
- Add micro flow section with Go API and CLI examples
- Add micro api section
- Renumber layers (now 8 instead of 7)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 22:49:37 +01:00
Asim Aslam 601c67675f Update logo, add AI integration docs, and enhance CLI features (#2914)
* feat: update Go Micro logo to interconnected nodes design

Replace the text-on-blue-square logo with a modern icon: three
teal nodes connected in a triangle, representing distributed
systems. Generated via Atlas Cloud. Clean at all sizes — works
as GitHub avatar, favicon, and nav bar icon.

* feat: new logo, AI integration architecture doc, and landing page CTA

Update logo to triangle-nodes icon + "Go Micro" text wordmark.
Save icon-only variant for favicon/avatar use.

Add docs/ai-integration.md — a single page that explains how the
AI stack fits together: services → registry → MCP gateway →
ai/tools → ai.Model → micro chat. Layer-by-layer with code
examples, provider table, and "what you don't need" section.

Add AI Integration to docs sidebar navigation (after Getting
Started). Update the landing page AI section with a direct CTA
button linking to the new doc.

* fix: restore original logo and add border-radius to all renders

Revert logo to original. Add border-radius: 8px to the logo img
in the landing page nav, docs layout nav, and blog layout nav
so the square logo renders with rounded corners everywhere.
Remove unused icon.png.

* feat(ai): add ai/flow package and micro flow CLI

Add ai/flow — event-driven LLM orchestration for go-micro. A Flow
subscribes to a broker topic, discovers services as tools, and
feeds each event into an LLM that decides which RPCs to call.

Key types:
- flow.New(name, opts...) creates a flow with trigger topic,
  prompt template, provider config
- flow.Register(registry, broker, client) wires it into a service
- flow.Execute(ctx, data) runs the flow once (for testing/CLI)
- flow.Results() returns execution history

Add micro flow CLI with two subcommands:
- micro flow run: subscribe to a topic and react to events
- micro flow exec: one-shot execution with inline data

Both output JSON results with flow name, prompt, tool calls,
reply, answer, duration, and errors.

Example:
  micro flow run --trigger events.user.created \
    --prompt "New user: {{.Data}}. Send welcome email." \
    --provider anthropic

  micro flow exec --prompt "List all users" --provider anthropic

* docs: update flows blog post with ai/flow package and CLI examples

Add "Update: We Built It" section to blog/9 showing the ai/flow
package API, CLI usage for both event-driven and one-shot modes,
and what it does/doesn't do. Links the conceptual discussion to
the shipped implementation.

* feat(cli): add micro api gateway command, clarify run vs server

Add 'micro api' — a standalone lightweight HTTP-to-RPC gateway:
- POST /{service}/{endpoint} proxies to RPC calls
- GET / lists all services and endpoints
- GET /{service} describes a service
- GET /health returns ok
- Supports Micro-Endpoint header for endpoint routing
- No dashboard, no auth, no hot reload — just the proxy

Update help text to clarify the three gateway modes:
- micro api: bare HTTP-to-RPC proxy
- micro run: development mode (hot reload + gateway + agent playground)
- micro server: production mode (dashboard + auth + JWT)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 18:13:06 +01:00
Asim Aslam b97f45106d Update logo, add AI integration docs, and implement ai/flow package (#2913)
* feat: update Go Micro logo to interconnected nodes design

Replace the text-on-blue-square logo with a modern icon: three
teal nodes connected in a triangle, representing distributed
systems. Generated via Atlas Cloud. Clean at all sizes — works
as GitHub avatar, favicon, and nav bar icon.

* feat: new logo, AI integration architecture doc, and landing page CTA

Update logo to triangle-nodes icon + "Go Micro" text wordmark.
Save icon-only variant for favicon/avatar use.

Add docs/ai-integration.md — a single page that explains how the
AI stack fits together: services → registry → MCP gateway →
ai/tools → ai.Model → micro chat. Layer-by-layer with code
examples, provider table, and "what you don't need" section.

Add AI Integration to docs sidebar navigation (after Getting
Started). Update the landing page AI section with a direct CTA
button linking to the new doc.

* fix: restore original logo and add border-radius to all renders

Revert logo to original. Add border-radius: 8px to the logo img
in the landing page nav, docs layout nav, and blog layout nav
so the square logo renders with rounded corners everywhere.
Remove unused icon.png.

* feat(ai): add ai/flow package and micro flow CLI

Add ai/flow — event-driven LLM orchestration for go-micro. A Flow
subscribes to a broker topic, discovers services as tools, and
feeds each event into an LLM that decides which RPCs to call.

Key types:
- flow.New(name, opts...) creates a flow with trigger topic,
  prompt template, provider config
- flow.Register(registry, broker, client) wires it into a service
- flow.Execute(ctx, data) runs the flow once (for testing/CLI)
- flow.Results() returns execution history

Add micro flow CLI with two subcommands:
- micro flow run: subscribe to a topic and react to events
- micro flow exec: one-shot execution with inline data

Both output JSON results with flow name, prompt, tool calls,
reply, answer, duration, and errors.

Example:
  micro flow run --trigger events.user.created \
    --prompt "New user: {{.Data}}. Send welcome email." \
    --provider anthropic

  micro flow exec --prompt "List all users" --provider anthropic

* docs: update flows blog post with ai/flow package and CLI examples

Add "Update: We Built It" section to blog/9 showing the ai/flow
package API, CLI usage for both event-driven and one-shot modes,
and what it does/doesn't do. Links the conceptual discussion to
the shipped implementation.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 17:13:16 +01:00
Asim Aslam 28fa44b2ca Update logo design, enhance AI integration docs, and simplify navigation (#2912)
* feat: update Go Micro logo to interconnected nodes design

Replace the text-on-blue-square logo with a modern icon: three
teal nodes connected in a triangle, representing distributed
systems. Generated via Atlas Cloud. Clean at all sizes — works
as GitHub avatar, favicon, and nav bar icon.

* feat: new logo, AI integration architecture doc, and landing page CTA

Update logo to triangle-nodes icon + "Go Micro" text wordmark.
Save icon-only variant for favicon/avatar use.

Add docs/ai-integration.md — a single page that explains how the
AI stack fits together: services → registry → MCP gateway →
ai/tools → ai.Model → micro chat. Layer-by-layer with code
examples, provider table, and "what you don't need" section.

Add AI Integration to docs sidebar navigation (after Getting
Started). Update the landing page AI section with a direct CTA
button linking to the new doc.

* fix: restore original logo and add border-radius to all renders

Revert logo to original. Add border-radius: 8px to the logo img
in the landing page nav, docs layout nav, and blog layout nav
so the square logo renders with rounded corners everywhere.
Remove unused icon.png.

* fix: trim nav to 3 links across all layouts

Remove Reference and Home links from nav across landing page,
docs layout, and blog layout. Keep only Docs, Blog, GitHub —
the three things people actually need. Fixes crowded nav on
mobile where 5 links plus a menu button didn't fit.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 16:55:36 +01:00
Asim Aslam 985621f4b4 AI integration updates (#2911)
* feat: update Go Micro logo to interconnected nodes design

Replace the text-on-blue-square logo with a modern icon: three
teal nodes connected in a triangle, representing distributed
systems. Generated via Atlas Cloud. Clean at all sizes — works
as GitHub avatar, favicon, and nav bar icon.

* feat: new logo, AI integration architecture doc, and landing page CTA

Update logo to triangle-nodes icon + "Go Micro" text wordmark.
Save icon-only variant for favicon/avatar use.

Add docs/ai-integration.md — a single page that explains how the
AI stack fits together: services → registry → MCP gateway →
ai/tools → ai.Model → micro chat. Layer-by-layer with code
examples, provider table, and "what you don't need" section.

Add AI Integration to docs sidebar navigation (after Getting
Started). Update the landing page AI section with a direct CTA
button linking to the new doc.

* fix: restore original logo and add border-radius to all renders

Revert logo to original. Add border-radius: 8px to the logo img
in the landing page nav, docs layout nav, and blog layout nav
so the square logo renders with rounded corners everywhere.
Remove unused icon.png.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 16:30:23 +01:00
Asim Aslam 7a1ab14847 docs: rewrite Anthropic blog post with updated content and new header image (#2910)
goreleaser / goreleaser (push) Waiting to run
Rewrite blog/3 to reflect current state of the project:
- Update numbers (7 providers, image/video support, micro chat)
- Add "What Came After" section covering everything shipped since
- Tighten prose, remove stale roadmap percentages
- Replace generic MCP image with Claude-themed header generated
  via Atlas Cloud (orange AI orb connecting to service nodes)
- Streamline code examples
- Update star count and Try It section

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 15:57:39 +01:00
Asim Aslam c26d74a8a1 Add CRUD, pub/sub, and API gateway templates; update AI features (#2909)
* feat(website): redesign docs and blog layouts, add blog header images

Redesign both layouts to match the new landing page:
- Consistent nav bar with logo, Docs, Blog, GitHub, Reference, Home
- Consistent footer with copyright and links
- CSS custom properties for theming
- Updated typography, spacing, and code block styling
- Active sidebar link highlighting in docs
- Dark mode support preserved

Generate 4 blog header images via Atlas Cloud:
- blog-deploy.png for post 1 (micro deploy)
- blog-mcp.png for posts 2, 3, 7 (MCP-related)
- blog-agents-demo.png for post 4 (agents demo)
- blog-dx.png for post 5 (DX cleanup)
- Reuse data-model.png for post 6 (model package)

All 7 existing blog posts now have header images.

* fix(website): prevent horizontal scroll on mobile landing page

Add overflow-x: hidden on html and body. Set max-width: 100% and
height: auto on all section and two-col images. Add overflow:
hidden to .two-col grid. Constrain hero pre with max-width and
overflow-x. Reduce font sizes and padding at mobile breakpoint.

* feat(website): add images to remaining core doc pages

Generate 5 more images via Atlas Cloud for docs:
- registry.png: service discovery diagram
- broker.png: pub/sub message broker pattern
- transport.png: multi-transport layers (HTTP, gRPC, NATS)
- config.png: dynamic configuration from multiple sources
- observability.png: monitoring dashboard with metrics/traces

Add images to registry.md, broker.md, transport.md, config.md,
observability.md, and architecture.md. All 11 main doc pages
now have header images.

* feat: add sponsor logos to landing page, README images, and flows blog post

Add Anthropic and Atlas Cloud sponsor logos to the landing page
with links to their respective blog posts. Logos display at 0.7
opacity with hover effect.

Add architecture and MCP agent images to the GitHub README for
the Overview and MCP sections.

Write blog post 9: "From Chat to Flows" — explores the concept
of LLM-powered service orchestration. Compares micro chat's
interactive model with persistent event-driven flows, shows how
the existing building blocks (ai/tools, History, broker) could
compose into a flow engine, discusses tradeoffs vs traditional
orchestration (Step Functions, Temporal), and includes a working
15-line code example. Explicitly positions it as a concept for
community feedback, not an announcement.

* feat(website): add animated hero video to landing page

Generate a 6-second hero video via Atlas Cloud's image-to-video
API (gemini-omni-flash). Shows the microservices network diagram
animating with data flowing between nodes.

Replace the static hero image with an autoplay muted looping
video element. Falls back to the static image via poster
attribute and img fallback for browsers without video support.

* feat(ai): add VideoModel interface with Atlas Cloud provider

Add ai.VideoModel interface for video generation alongside Model
and ImageModel. Supports text-to-video and image-to-video via
VideoRequest with prompt, reference images, duration, aspect
ratio, and resolution fields.

Implement GenerateVideo for Atlas Cloud using their async API:
POST /api/v1/model/generateVideo → poll /api/v1/model/prediction.
Default model is gemini-omni-flash image-to-video. Polls every
5 seconds until completion or context cancellation.

Register Atlas Cloud as a video provider via ai.RegisterVideo.
Add 3 tests: registration, no-key error, compile-time interface
check. Update ai/README.md with VideoModel docs.

The ai package now covers all three modalities:
- Model (text) — 7 providers
- ImageModel (image) — 2 providers (Atlas Cloud, OpenAI)
- VideoModel (video) — 1 provider (Atlas Cloud)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 15:52:35 +01:00
Asim Aslam 669481224c Enhance AI features with ImageModel, History, and website updates (#2907)
* feat(cli): add CRUD, pub/sub, and API gateway templates for micro new

Add --template flag to 'micro new' with three preset templates:

- crud: CRUD service with Create/Read/Update/Delete/List, in-memory
  store with sync.RWMutex, UUID generation, pagination, and doc
  comments with @example tags for MCP tool discovery.

- pubsub: Event-driven service with Publish/Stats RPCs and a
  Subscribe method that hooks into the broker. Includes event
  types with ID, type, source, data, and timestamp.

- api: API gateway service with Health and Endpoint RPCs, an
  internal HTTP route table, and a response recorder for
  proxying requests through RPC.

All templates include MCP-ready doc comments and work with
--no-mcp. The default template (no flag) is unchanged.

Usage:
  micro new myservice --template crud
  micro new myservice --template pubsub
  micro new myservice --template api

* fix(ai): update Atlas Cloud provider to use actual API formats

Fix the Atlas Cloud image generation to use their real async API:
POST /api/v1/model/generateImage → poll /api/v1/model/prediction/{id}
instead of the OpenAI-compatible endpoint which doesn't exist.

Add Quality and OutputFormat fields to ai.ImageRequest for
provider-specific image parameters.

Update default text model from llama-3.3-70b (doesn't exist) to
deepseek-ai/DeepSeek-V3-0324 (their flagship model). Update
default image model to openai/gpt-image-2/text-to-image.

* feat(website): add AI-generated images to landing page, docs, and blog

Generate 5 images via Atlas Cloud's image API (gpt-image-2) to
elevate the website experience:

- hero.png: microservices network graph for landing page
- architecture.png: registry + broker architecture diagram
- mcp-agent.png: AI agent calling services via MCP
- developer-experience.png: terminal showing micro run/chat
- blog-atlas.png: Atlas Cloud unified API illustration

Add visual sections to the landing page with architecture,
MCP integration, and developer experience showcases. Add
images to docs index, MCP docs, and Atlas Cloud blog post.

All images resized to 1200px wide and optimized for web.
Generated using Atlas Cloud sponsor credits.

* feat(website): redesign landing page and add images to docs

Redesign the landing page from a centered card layout to a
full-width modern site with:
- Top navigation bar
- Hero section with gradient background and CTA buttons
- Full-width image showcase sections
- Two-column layout for architecture, MCP, and DX sections
- Feature grid with 6 capabilities
- Footer with links
- Responsive breakpoints for mobile

Generate 3 more images via Atlas Cloud for docs:
- getting-started.png for the getting started guide
- deployment.png for the deployment guide
- data-model.png for the data model docs

Add images to getting-started.md, model.md, and deployment.md.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 15:16:05 +01:00
Asim Aslam 86782e6d77 Add ImageModel interface and multi-turn conversation support (#2906)
* feat(ai): add ImageModel interface with Atlas Cloud and OpenAI support

Add ai.ImageModel interface for text-to-image generation alongside
the existing ai.Model for text. Uses the same options pattern
(WithAPIKey, WithBaseURL) and the same provider registration
system (RegisterImage/NewImage).

Implement GenerateImage for Atlas Cloud and OpenAI providers via
the OpenAI-compatible /v1/images/generations endpoint. Default
image model is gpt-image-1. Responses return images as URL,
base64, or both depending on the provider.

Update Atlas Cloud blog post and integration guide with image
generation examples. Update ai/README.md with ImageModel docs.

* fix(website): widen docs content by reducing layout max-width to 1100px

Remove the 800px max-width on .content (which left empty space on
the right) and reduce the overall .layout and footer from 1400px
to 1100px. With the 230px sidebar this gives ~830px of content
width — readable and fills the page properly on desktop.

* feat(ai): add History for multi-turn conversation state

Add ai.History — a lightweight message accumulator that tracks
user prompts, assistant replies, and tool call/result pairs
across turns. FIFO truncation when message count exceeds the
configured limit. System prompt is passed through on every
Generate call.

Wire History into micro chat so conversations are multi-turn by
default (limit 50 messages). Add 'reset' command to clear
history mid-session.

5 unit tests covering accumulation, truncation, reset, snapshot
isolation, and tool call recording.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 14:44:46 +01:00
Asim Aslam 1e2901ad0e feat(ai): add ImageModel interface with Atlas Cloud and OpenAI support (#2905)
goreleaser / goreleaser (push) Waiting to run
Add ai.ImageModel interface for text-to-image generation alongside
the existing ai.Model for text. Uses the same options pattern
(WithAPIKey, WithBaseURL) and the same provider registration
system (RegisterImage/NewImage).

Implement GenerateImage for Atlas Cloud and OpenAI providers via
the OpenAI-compatible /v1/images/generations endpoint. Default
image model is gpt-image-1. Responses return images as URL,
base64, or both depending on the provider.

Update Atlas Cloud blog post and integration guide with image
generation examples. Update ai/README.md with ImageModel docs.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 12:21:46 +01:00
Asim Aslam 426d7e4e6c fix: align sponsor logos with fixed height and clean SVG sources (#2903)
goreleaser / goreleaser (push) Waiting to run
Use height="26" on both logos for consistent alignment. Switch
Anthropic to the Wikimedia wordmark SVG (no padding) instead of
the logo.wine version which had excessive whitespace.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 11:07:38 +01:00
Asim Aslam 5968fce2d6 docs: add Atlas Cloud sponsorship blog post and integration guide (#2902)
Add blog/8 announcing Atlas Cloud as an official Go Micro sponsor.
Covers the sponsorship, Atlas Cloud's platform (300+ models, OpenAI
compatibility, enterprise compliance), and how the integration
works with the ai package, ai/tools, micro chat, and micro run.

Add guides/atlascloud-integration.md with full setup instructions:
quick start, configuration options, environment variables, model
selection, tool calling with services, and provider swapping.

Add Atlas Cloud and AI Provider guides to the docs sidebar
navigation. Add sponsorship link to README header.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 11:04:04 +01:00
Asim Aslam 415b0a76c2 Fix README header formatting
Removed a redundant pipe character from the README header.
2026-05-28 10:55:54 +01:00
Asim Aslam 7ef9a60441 Update README with documentation link and cleanup
Added documentation link to the README header and removed redundant documentation line.
2026-05-28 10:53:53 +01:00
Asim Aslam be156ecb95 Adjust logo width in README
Reduce the width of the Anthropic logo in sponsors section.
2026-05-28 10:53:01 +01:00
Asim Aslam 713dabbb5a Update Anthropic logo URL in README 2026-05-28 10:52:34 +01:00
Asim Aslam bbea48da78 Add sponsors section to README
Added sponsors section with Anthropic logo to README.
2026-05-28 10:52:03 +01:00
Asim Aslam a0ad9ee566 Claude/fix issue 2893 x3rpd (#2901)
* docs: add AI provider integration guide and Supported AI Providers section

Add a step-by-step guide for AI infrastructure companies to implement
ai.Model and contribute a provider to go-micro. Covers the full
lifecycle: skeleton, tool call handling, tests, registration, and PR
checklist.

Add a "Supported AI Providers" section to the project README that lists
current providers (Anthropic, OpenAI) in a table and links to the
integration guide with a call-to-action for new providers and sponsors.

Streamline the "Adding a New Provider" section in ai/README.md to point
to the new guide instead of duplicating a full code listing.

* fix: remove nonexistent Discord link from README

* fix(website): set content container width to 800px on desktop

Move the 800px max-width from .markdown-body up to .content so
the entire content pane (not just the inner body) is sized
correctly. The container now fills up to 800px beside the sidebar.

* feat(ai): wire Atlas Cloud into server and auto-detection

Import atlascloud provider in the micro server so it is available
when running micro run / micro server. Add atlascloud to
AutoDetectProvider so --ai_base_url with an atlascloud domain
selects the right provider automatically.

* feat(ai): add Google Gemini provider

Add ai/gemini implementing ai.Model for Google's Gemini API. Uses
the native generateContent endpoint with system_instruction,
contents/parts, and functionDeclarations — not an OpenAI shim.
Default model gemini-2.5-flash, auth via x-goog-api-key header.

Wire into micro server imports and AutoDetectProvider (matches
googleapis.com and google in base URL).

Update README.md and ai/README.md with provider listing.

* feat(ai): add Groq, Mistral, and Together AI providers

Add three new OpenAI-compatible providers:

- ai/groq: ultra-fast inference, default model llama-3.3-70b-versatile
- ai/mistral: Mistral AI, default model mistral-large-latest
- ai/together: Together AI, default model Llama-3.3-70B-Instruct-Turbo

All three are wired into the micro server imports and
AutoDetectProvider. README and ai/README updated with the full
provider table.

* feat(ai): add ai/tools helper and 'micro chat' interactive agent

Extract the registry-discovery + RPC-execution loop from the web
agent playground into a reusable ai/tools package:

- tools.New(reg) creates a Set bound to a registry
- Set.Discover() walks the registry and returns []ai.Tool with
  LLM-safe (underscored) names, remembering the mapping back to
  the original dotted form
- Set.Handler(client) returns an ai.ToolHandler that resolves
  the safe name and issues the RPC

Add cmd/micro/chat — an interactive 'micro chat' REPL that uses
ai/tools to let users talk to their services through any
registered AI provider. Supports --prompt for single-shot use,
auto-detects the provider from --base_url, and falls back to the
provider's conventional env var (ANTHROPIC_API_KEY, etc).

Update README with the new command and the programmatic example.

* feat(examples): add gRPC interop example

Add examples/grpc-interop showing that any standard gRPC client can
call a go-micro service — no go-micro SDK required on the client
side. Includes:

- proto/greeter.proto with generated Go, gRPC, and micro stubs
- server/ using go-micro gRPC transport
- client/ using stock google.golang.org/grpc (no go-micro imports)
- README with Python example and explanation of how routing works

Addresses the confusion from issue #2818 where users didn't know
that go-micro gRPC services are callable by any gRPC client.

* fix: strip /api prefix from MCP routes

Change /api/mcp/tools and /api/mcp/call to /mcp/tools and
/mcp/call. MCP is a first-class feature, not a sub-path of the
API proxy. Update server routes, playground template, scopes
template, run.go output, README, CLI README, and all docs.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-26 12:19:26 +01:00
Asim Aslam f5e33317b3 Remove Micro app platform promotion from README
Removed the promotion for the Micro app platform from the README.
2026-05-24 18:17:31 +01:00
Asim Aslam 081e375f29 Add AI provider integration guide and new providers support (#2900)
* docs: add AI provider integration guide and Supported AI Providers section

Add a step-by-step guide for AI infrastructure companies to implement
ai.Model and contribute a provider to go-micro. Covers the full
lifecycle: skeleton, tool call handling, tests, registration, and PR
checklist.

Add a "Supported AI Providers" section to the project README that lists
current providers (Anthropic, OpenAI) in a table and links to the
integration guide with a call-to-action for new providers and sponsors.

Streamline the "Adding a New Provider" section in ai/README.md to point
to the new guide instead of duplicating a full code listing.

* fix: remove nonexistent Discord link from README

* fix(website): set content container width to 800px on desktop

Move the 800px max-width from .markdown-body up to .content so
the entire content pane (not just the inner body) is sized
correctly. The container now fills up to 800px beside the sidebar.

* feat(ai): wire Atlas Cloud into server and auto-detection

Import atlascloud provider in the micro server so it is available
when running micro run / micro server. Add atlascloud to
AutoDetectProvider so --ai_base_url with an atlascloud domain
selects the right provider automatically.

* feat(ai): add Google Gemini provider

Add ai/gemini implementing ai.Model for Google's Gemini API. Uses
the native generateContent endpoint with system_instruction,
contents/parts, and functionDeclarations — not an OpenAI shim.
Default model gemini-2.5-flash, auth via x-goog-api-key header.

Wire into micro server imports and AutoDetectProvider (matches
googleapis.com and google in base URL).

Update README.md and ai/README.md with provider listing.

* feat(ai): add Groq, Mistral, and Together AI providers

Add three new OpenAI-compatible providers:

- ai/groq: ultra-fast inference, default model llama-3.3-70b-versatile
- ai/mistral: Mistral AI, default model mistral-large-latest
- ai/together: Together AI, default model Llama-3.3-70B-Instruct-Turbo

All three are wired into the micro server imports and
AutoDetectProvider. README and ai/README updated with the full
provider table.

* feat(ai): add ai/tools helper and 'micro chat' interactive agent

Extract the registry-discovery + RPC-execution loop from the web
agent playground into a reusable ai/tools package:

- tools.New(reg) creates a Set bound to a registry
- Set.Discover() walks the registry and returns []ai.Tool with
  LLM-safe (underscored) names, remembering the mapping back to
  the original dotted form
- Set.Handler(client) returns an ai.ToolHandler that resolves
  the safe name and issues the RPC

Add cmd/micro/chat — an interactive 'micro chat' REPL that uses
ai/tools to let users talk to their services through any
registered AI provider. Supports --prompt for single-shot use,
auto-detects the provider from --base_url, and falls back to the
provider's conventional env var (ANTHROPIC_API_KEY, etc).

Update README with the new command and the programmatic example.

* feat(examples): add gRPC interop example

Add examples/grpc-interop showing that any standard gRPC client can
call a go-micro service — no go-micro SDK required on the client
side. Includes:

- proto/greeter.proto with generated Go, gRPC, and micro stubs
- server/ using go-micro gRPC transport
- client/ using stock google.golang.org/grpc (no go-micro imports)
- README with Python example and explanation of how routing works

Addresses the confusion from issue #2818 where users didn't know
that go-micro gRPC services are callable by any gRPC client.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-24 18:17:04 +01:00
Asim Aslam 2f78103fed Claude/fix issue 2893 x3rpd (#2899)
* docs: add AI provider integration guide and Supported AI Providers section

Add a step-by-step guide for AI infrastructure companies to implement
ai.Model and contribute a provider to go-micro. Covers the full
lifecycle: skeleton, tool call handling, tests, registration, and PR
checklist.

Add a "Supported AI Providers" section to the project README that lists
current providers (Anthropic, OpenAI) in a table and links to the
integration guide with a call-to-action for new providers and sponsors.

Streamline the "Adding a New Provider" section in ai/README.md to point
to the new guide instead of duplicating a full code listing.

* feat(ai): add Atlas Cloud provider

Add ai/atlascloud implementing ai.Model for Atlas Cloud's
OpenAI-compatible chat completions API. Registers as "atlascloud"
with default model llama-3.3-70b and base URL
https://api.atlascloud.ai. Supports tool calling via ToolHandler.

Includes 7 unit tests covering registration, defaults, init,
generate-without-key, and stream-not-implemented.

Update the Supported AI Providers table in README.md and the
Supported Providers section in ai/README.md.

* fix: remove nonexistent Discord link from README

* fix(website): remove Micro app banner and widen content to 800px

Remove the "Try Micro" promotional banner from the homepage. Set
the docs content max-width to 800px for better readability on
desktop.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-24 13:49:32 +01:00
Asim Aslam 60527e1fe8 Add AI provider integration guide and Atlas Cloud support (#2898)
* docs: add AI provider integration guide and Supported AI Providers section

Add a step-by-step guide for AI infrastructure companies to implement
ai.Model and contribute a provider to go-micro. Covers the full
lifecycle: skeleton, tool call handling, tests, registration, and PR
checklist.

Add a "Supported AI Providers" section to the project README that lists
current providers (Anthropic, OpenAI) in a table and links to the
integration guide with a call-to-action for new providers and sponsors.

Streamline the "Adding a New Provider" section in ai/README.md to point
to the new guide instead of duplicating a full code listing.

* feat(ai): add Atlas Cloud provider

Add ai/atlascloud implementing ai.Model for Atlas Cloud's
OpenAI-compatible chat completions API. Registers as "atlascloud"
with default model llama-3.3-70b and base URL
https://api.atlascloud.ai. Supports tool calling via ToolHandler.

Includes 7 unit tests covering registration, defaults, init,
generate-without-key, and stream-not-implemented.

Update the Supported AI Providers table in README.md and the
Supported Providers section in ai/README.md.

* fix: remove nonexistent Discord link from README

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-24 13:31:44 +01:00
Asim Aslam defb2786ef update AI provider documentation (#2897)
goreleaser / goreleaser (push) Waiting to run
* feat: add prometheus monitoring wrapper

Reintroduces the Prometheus metrics wrapper previously available in the
plugins repository, updated for go-micro v5. Exposes request count and
latency histograms for handlers, subscribers, and outgoing client calls
via NewHandlerWrapper, NewSubscriberWrapper, NewCallWrapper and
NewClientWrapper, labelled with service/endpoint/status.

Options cover namespace, subsystem, const labels, histogram buckets and
a custom registerer; duplicate collectors (e.g. from multiple wrappers
sharing the same config) are reused transparently via a cached
metrics bundle.

Fixes #2893

* fix(registry/etcd): clear lease/register caches on KeepAlive channel closure

When the etcd client's long-lived KeepAlive channel closes (e.g. because
the lease expired on the server side during a network partition), the
previous cleanup only removed the channel bookkeeping. The stale entries
in `leases` and `register` caused the next registerNode() heartbeat to
hit the "unchanged hash" short-circuit and skip re-registration entirely,
so the service permanently disappeared from etcd.

Extract the cleanup into handleKeepAliveClosed and also drop the cached
lease id and hash so the next heartbeat performs a full Grant+Put and
the service recovers within one RegisterInterval.

Regression introduced by #2822; fix is symmetric with the existing
synchronous KeepAliveOnce recovery path that propagates
rpctypes.ErrLeaseNotFound.

* docs: add AI provider integration guide and Supported AI Providers section

Add a step-by-step guide for AI infrastructure companies to implement
ai.Model and contribute a provider to go-micro. Covers the full
lifecycle: skeleton, tool call handling, tests, registration, and PR
checklist.

Add a "Supported AI Providers" section to the project README that lists
current providers (Anthropic, OpenAI) in a table and links to the
integration guide with a call-to-action for new providers and sponsors.

Streamline the "Adding a New Provider" section in ai/README.md to point
to the new guide instead of duplicating a full code listing.

* Update contribution guidelines in README.md

Removed Discord contact information for platform contributions.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 17:14:04 +01:00
Asim Aslam 15cc876848 Emphasize 'Micro' in README link 2026-05-10 17:01:53 +01:00
Asim Aslam d3cfb3a99a Update app platform name from 'Mu' to 'Micro' 2026-05-10 17:01:23 +01:00
Asim Aslam b1680ca2da Update app platform link from Mu.xyz to Micro 2026-05-10 16:21:41 +01:00
Asim Aslam 90f7617fac Claude/fix issue 2893 x3rpd (#2895)
* feat: add prometheus monitoring wrapper

Reintroduces the Prometheus metrics wrapper previously available in the
plugins repository, updated for go-micro v5. Exposes request count and
latency histograms for handlers, subscribers, and outgoing client calls
via NewHandlerWrapper, NewSubscriberWrapper, NewCallWrapper and
NewClientWrapper, labelled with service/endpoint/status.

Options cover namespace, subsystem, const labels, histogram buckets and
a custom registerer; duplicate collectors (e.g. from multiple wrappers
sharing the same config) are reused transparently via a cached
metrics bundle.

Fixes #2893

* fix(registry/etcd): clear lease/register caches on KeepAlive channel closure

When the etcd client's long-lived KeepAlive channel closes (e.g. because
the lease expired on the server side during a network partition), the
previous cleanup only removed the channel bookkeeping. The stale entries
in `leases` and `register` caused the next registerNode() heartbeat to
hit the "unchanged hash" short-circuit and skip re-registration entirely,
so the service permanently disappeared from etcd.

Extract the cleanup into handleKeepAliveClosed and also drop the cached
lease id and hash so the next heartbeat performs a full Grant+Put and
the service recovers within one RegisterInterval.

Regression introduced by #2822; fix is symmetric with the existing
synchronous KeepAliveOnce recovery path that propagates
rpctypes.ErrLeaseNotFound.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 14:55:01 +01:00
Asim Aslam 79722e0c27 feat: add prometheus monitoring wrapper (#2894)
Reintroduces the Prometheus metrics wrapper previously available in the
plugins repository, updated for go-micro v5. Exposes request count and
latency histograms for handlers, subscribers, and outgoing client calls
via NewHandlerWrapper, NewSubscriberWrapper, NewCallWrapper and
NewClientWrapper, labelled with service/endpoint/status.

Options cover namespace, subsystem, const labels, histogram buckets and
a custom registerer; duplicate collectors (e.g. from multiple wrappers
sharing the same config) are reused transparently via a cached
metrics bundle.

Fixes #2893

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-15 09:24:43 +01:00
jejefferson 49070afa8c server/grpc: improve graceful stop behavior (#2892)
* server/grpc: improve graceful stop behavior

* server/grpc: add graceful stop example and test

* examples/graceful-stop: document verification steps
2026-04-10 08:10:10 +01:00
Asim Aslam 03f4759474 Fix inline style attribute in index.html 2026-04-08 07:42:12 +01:00
Asim Aslam f6fb541ace Enhance Mu.xyz promotion with styled div
Updated the promotional text for Mu.xyz with enhanced styling.
2026-04-08 07:40:51 +01:00
Asim Aslam 1426c8f349 Update README to use emoji for Mu.xyz link 2026-04-08 07:34:54 +01:00
Asim Aslam 5aedb601ba Add Mu.xyz promotional link to README
Added a promotional link for Mu.xyz app platform.
2026-04-08 07:34:25 +01:00
Asim Aslam bd13975acf Update container width and add promotional text
Increased the maximum width of the container and added a promotional paragraph for Mu.xyz.
2026-04-08 07:33:47 +01:00
Asim Aslam 213a09276e Update default.html 2026-03-26 10:11:00 +00:00
Asim Aslam f4291957e8 Remove Blog link from index.html
Removed the Blog link from the website index.
2026-03-26 10:09:12 +00:00
Asim Aslam 07411b1ef6 Remove article on chat app from blog index
Removed an article about building a chat app from the blog.
2026-03-26 10:08:54 +00:00
Asim Aslam 544496eec6 Delete internal/website/blog/8.md 2026-03-26 10:08:26 +00:00
Asim Aslam 2e95c4c610 Remove showcase section from index.html
Removed the showcase section for Go Micro projects.
2026-03-26 10:08:01 +00:00
BombartSimon 7b785df302 feat: add connection timeout call option (#2891) 2026-03-16 13:34:24 +00:00
Asim Aslam d96f12f57b Claude/update docs roadmap f zd2 j (#2889)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: consolidate top-level directories, reduce framework bloat

Move internal/non-public packages behind internal/ or into their
parent packages where they belong:

- deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway)
- profile/ → service/profile/ (preset plugin profiles are a service concern)
- scripts/ → internal/scripts/ (install script is not public API)
- test/ → internal/test/ (test harness is not public API)
- util/ → internal/util/ (internal helpers shouldn't be imported externally)

Also fixes CLAUDE.md merge conflict markers and updates project
structure documentation.

All import paths updated. Build and tests pass.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: redesign model package to match framework conventions

Rename model.Database interface to model.Model (consistent with
client.Client, server.Server, store.Store). Remove generics in
favor of interface{}-based API with reflection.

Key changes:
- model.Model interface: Register once, CRUD infers table from type
- DefaultModel + NewModel() + package-level convenience functions
- Schema registered via Register(&User{}), no per-call schema passing
- Memory implementation as default (in model package, like store)
- memory/sqlite/postgres backends updated for new interface
- protoc-gen-micro generates RegisterXModel() instead of generic factory
- All docs, blog, and README updated

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: clarify blog post 7 uses modular monolith, not multi-service

Blog post 7 demonstrated all handlers in a single process but framed
it as microservices without acknowledging the architectural difference.

- Add "A Note on Architecture" section explaining this is a modular
  monolith demo and pointing to micro/blog for multi-service
- Clarify that handlers can be broken out into separate services later
- Fix "service registry" language to match single-process reality
- Restructure "Adding MCP to Existing Services" to distinguish the
  in-process approach from registry-based gateway options
- Update closing to acknowledge both paradigms
- Fix README type names (&CommentService{} -> &Comments{}, etc.)

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add Micro Chat to website showcase

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: github artifact release CI (#2886)

* 👷feat(ci): add artifact and docker releases

* 💚fix(ci): build issues

* 💚fix(ci): add permissions

* 💚fix(ci): multiple artifacts

* 💚fix(ci): split archives

* 💚fix(ci): cross platform list

* 🚧chore(ci): package name

* 🐛fix(script): install script extract arch

* 👷fix(ci): docker origin go-micro

* Update image reference in goreleaser configuration (#2887)

Fix wrong order `user/repo`

* Add blog post on building a chat app with Go Micro

Added a blog post detailing the development of a full chat app using Go Micro, outlining features, architecture, and lessons learned.

* docs: add blog post 8 to index, put Blog before Docs on homepage

- Add "We Built a Full Chat App in a Day" (blog/8) to blog index
- Reorder homepage links: Blog first (primary), Docs second
- Rename "Documentation" to "Docs"

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alexander Serheyev <74361701+alex-dna-tech@users.noreply.github.com>
2026-03-07 13:00:22 +00:00
Asim Aslam 8608c15fbb Add blog post on building a chat app with Go Micro
Added a blog post detailing the development of a full chat app using Go Micro, outlining features, architecture, and lessons learned.
2026-03-07 12:49:39 +00:00
Asim Aslam 9823f6df5c Add platform showcase, blog post, and refactor project structure (#2888)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: consolidate top-level directories, reduce framework bloat

Move internal/non-public packages behind internal/ or into their
parent packages where they belong:

- deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway)
- profile/ → service/profile/ (preset plugin profiles are a service concern)
- scripts/ → internal/scripts/ (install script is not public API)
- test/ → internal/test/ (test harness is not public API)
- util/ → internal/util/ (internal helpers shouldn't be imported externally)

Also fixes CLAUDE.md merge conflict markers and updates project
structure documentation.

All import paths updated. Build and tests pass.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: redesign model package to match framework conventions

Rename model.Database interface to model.Model (consistent with
client.Client, server.Server, store.Store). Remove generics in
favor of interface{}-based API with reflection.

Key changes:
- model.Model interface: Register once, CRUD infers table from type
- DefaultModel + NewModel() + package-level convenience functions
- Schema registered via Register(&User{}), no per-call schema passing
- Memory implementation as default (in model package, like store)
- memory/sqlite/postgres backends updated for new interface
- protoc-gen-micro generates RegisterXModel() instead of generic factory
- All docs, blog, and README updated

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: clarify blog post 7 uses modular monolith, not multi-service

Blog post 7 demonstrated all handlers in a single process but framed
it as microservices without acknowledging the architectural difference.

- Add "A Note on Architecture" section explaining this is a modular
  monolith demo and pointing to micro/blog for multi-service
- Clarify that handlers can be broken out into separate services later
- Fix "service registry" language to match single-process reality
- Restructure "Adding MCP to Existing Services" to distinguish the
  in-process approach from registry-based gateway options
- Update closing to acknowledge both paradigms
- Fix README type names (&CommentService{} -> &Comments{}, etc.)

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add Micro Chat to website showcase

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-07 09:33:18 +00:00
Alexander Serheyev 28f7f53143 Update image reference in goreleaser configuration (#2887)
goreleaser / goreleaser (push) Waiting to run
Fix wrong order `user/repo`
2026-03-06 15:30:53 +00:00
Alexander Serheyev 0842431050 feat: github artifact release CI (#2886)
goreleaser / goreleaser (push) Waiting to run
* 👷feat(ci): add artifact and docker releases

* 💚fix(ci): build issues

* 💚fix(ci): add permissions

* 💚fix(ci): multiple artifacts

* 💚fix(ci): split archives

* 💚fix(ci): cross platform list

* 🚧chore(ci): package name

* 🐛fix(script): install script extract arch

* 👷fix(ci): docker origin go-micro
2026-03-06 14:49:18 +00:00
Asim Aslam 7a5d86a2a4 Claude/update docs roadmap f zd2 j (#2885)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: consolidate top-level directories, reduce framework bloat

Move internal/non-public packages behind internal/ or into their
parent packages where they belong:

- deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway)
- profile/ → service/profile/ (preset plugin profiles are a service concern)
- scripts/ → internal/scripts/ (install script is not public API)
- test/ → internal/test/ (test harness is not public API)
- util/ → internal/util/ (internal helpers shouldn't be imported externally)

Also fixes CLAUDE.md merge conflict markers and updates project
structure documentation.

All import paths updated. Build and tests pass.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: redesign model package to match framework conventions

Rename model.Database interface to model.Model (consistent with
client.Client, server.Server, store.Store). Remove generics in
favor of interface{}-based API with reflection.

Key changes:
- model.Model interface: Register once, CRUD infers table from type
- DefaultModel + NewModel() + package-level convenience functions
- Schema registered via Register(&User{}), no per-call schema passing
- Memory implementation as default (in model package, like store)
- memory/sqlite/postgres backends updated for new interface
- protoc-gen-micro generates RegisterXModel() instead of generic factory
- All docs, blog, and README updated

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: clarify blog post 7 uses modular monolith, not multi-service

Blog post 7 demonstrated all handlers in a single process but framed
it as microservices without acknowledging the architectural difference.

- Add "A Note on Architecture" section explaining this is a modular
  monolith demo and pointing to micro/blog for multi-service
- Clarify that handlers can be broken out into separate services later
- Fix "service registry" language to match single-process reality
- Restructure "Adding MCP to Existing Services" to distinguish the
  in-process approach from registry-based gateway options
- Update closing to acknowledge both paradigms
- Fix README type names (&CommentService{} -> &Comments{}, etc.)

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 13:13:31 +00:00
Asim Aslam 8ecdf07e6b Update description of the blogging platform example 2026-03-05 12:03:26 +00:00
Asim Aslam 1bb25d6e7f Add agent platform showcase and refactor project structure (#2884)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: consolidate top-level directories, reduce framework bloat

Move internal/non-public packages behind internal/ or into their
parent packages where they belong:

- deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway)
- profile/ → service/profile/ (preset plugin profiles are a service concern)
- scripts/ → internal/scripts/ (install script is not public API)
- test/ → internal/test/ (test harness is not public API)
- util/ → internal/util/ (internal helpers shouldn't be imported externally)

Also fixes CLAUDE.md merge conflict markers and updates project
structure documentation.

All import paths updated. Build and tests pass.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: redesign model package to match framework conventions

Rename model.Database interface to model.Model (consistent with
client.Client, server.Server, store.Store). Remove generics in
favor of interface{}-based API with reflection.

Key changes:
- model.Model interface: Register once, CRUD infers table from type
- DefaultModel + NewModel() + package-level convenience functions
- Schema registered via Register(&User{}), no per-call schema passing
- Memory implementation as default (in model package, like store)
- memory/sqlite/postgres backends updated for new interface
- protoc-gen-micro generates RegisterXModel() instead of generic factory
- All docs, blog, and README updated

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 11:21:41 +00:00
Asim Aslam b8fc0902d7 Claude/update docs roadmap f zd2 j (#2883)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 08:56:24 +00:00
Asim Aslam 524e16296b Update documentation, add agent demo, and enhance service API (#2882)
* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: enable multiple services in a single binary

Remove global state mutations from service and cmd option functions so
that configuring one service no longer overwrites another's settings.

Key changes:
- service/options.go: remove all DefaultXxx global writes from option
  functions; newOptions() now creates fresh Server, Client, Store, and
  Cache per service while sharing Registry, Broker, and Transport
- cmd/cmd.go: newCmd() uses local copies instead of pointers to package
  globals; Before() no longer mutates DefaultXxx vars
- cmd/options.go: remove global mutations from all option functions
- service/service.go: export ServiceImpl type for cross-package use
- service/group.go: new Group type for multi-service lifecycle
- micro.go: add Start/Stop to Service interface, expose Group and
  NewGroup convenience function
- examples/multi-service: working example with two services

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: highlight multi-service binary support

Add multi-service section to README with code example, update features
list, add to examples index, and note in status summary.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: make micro new generate MCP-enabled services by default

- main.go template includes mcp.WithMCP(":3001") by default
- Handler template has agent-friendly doc comments with @example tags
- Proto template has descriptive field comments
- README includes MCP usage, Claude Code config, and tool description tips
- Makefile adds mcp-tools, mcp-test, mcp-serve targets
- go.mod updated to Go 1.22
- Added --no-mcp flag to opt out of MCP integration
- Post-create output shows MCP endpoint URLs

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add MCP migration guide and troubleshooting guide

- Migration guide: 3 approaches to add MCP to existing services
  (WithMCP one-liner, standalone gateway, CLI)
- Troubleshooting guide: common issues with agents, WebSocket,
  Claude Code, auth, rate limiting, and performance

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename model/ package to ai/ for AI model providers

The model/ package name conflicted with the conventional use of "model"
for data models. Renamed to ai/ which better describes the package's
purpose (AI provider abstraction for Anthropic, OpenAI, etc.) and frees
up model/ for future data model layer use.

- Rename model/ → ai/ with package name change
- Update all Go imports from go-micro.dev/v5/model to go-micro.dev/v5/ai
- Update cmd/micro/server/server.go references (model.X → ai.X)
- Update all documentation and roadmap references
- All tests pass, CLI builds successfully

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model package for typed data access with CRUD and queries

New model/ package provides a typed data model layer using Go generics.
Supports structured CRUD operations, WHERE filters, ordering, pagination,
and automatic schema creation from struct tags.

Three backends:
- memory: in-memory for development and testing
- sqlite: embedded SQL for dev and single-node production
- postgres: full PostgreSQL for production deployments

Key features:
- Generic Model[T] with Create/Read/Update/Delete/List/Count
- Query builder: Where(), WhereOp(), OrderAsc/Desc(), Limit(), Offset()
- Struct tags: model:"key" for primary key, model:"index" for indexes
- Auto table creation from struct schema
- 19 tests passing across memory and sqlite backends

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model code generation to protoc-gen-micro

Extend the micro plugin to generate model structs from proto messages
annotated with // @model. Generated alongside client/server code in
the same .pb.micro.go file.

For a proto message like:
  // @model
  message User { string id = 1; string name = 2; }

Generates:
- UserModel struct with model:"key" and json tags
- NewUserModel(db) factory returning *model.Model[UserModel]
- UserModelFromProto(*User) *UserModel converter
- (*UserModel).ToProto() *User converter

Supports @model(table=custom_table, key=custom_field) options.
Adds GetComments() to generator for plugin comment inspection.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Model() to Service interface for Client/Server/Model trifecta

Every service now exposes Client(), Server(), and Model() — call services,
handle requests, and save/query data from the same interface. Includes
README docs, blog post, and a full model guide on the docs site.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Helm chart for MCP gateway Kubernetes deployment

Adds official Helm chart at deploy/helm/mcp-gateway/ with:
- Deployment, Service, ServiceAccount templates
- HPA for auto-scaling based on CPU/memory
- Ingress with TLS support
- Configurable registry (consul, etcd, mdns), rate limiting,
  JWT auth, audit logging, and per-tool scopes
- Security context (non-root, read-only rootfs, drop all caps)
- NOTES.txt with post-install connection instructions

Updates roadmap and status docs to reflect Helm Charts as delivered.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add Helm chart entry to changelog

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add per-tool circuit breakers to MCP gateway

Protects downstream services from cascading failures. When a tool's
RPC calls fail repeatedly, the circuit opens and rejects requests
immediately until the service recovers (half-open probe pattern).

- CircuitBreakerConfig with MaxFailures, Timeout, MaxHalfOpen
- Per-tool breakers created during service discovery
- Integrated into HTTP call path with 503 response when open
- Records success/failure after each RPC call
- --circuit-breaker and --circuit-breaker-timeout CLI flags
- 8 unit tests covering all state transitions

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 08:32:59 +00:00
Asim Aslam d91812b476 Update documentation and add multi-service support with examples (#2881)
* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: enable multiple services in a single binary

Remove global state mutations from service and cmd option functions so
that configuring one service no longer overwrites another's settings.

Key changes:
- service/options.go: remove all DefaultXxx global writes from option
  functions; newOptions() now creates fresh Server, Client, Store, and
  Cache per service while sharing Registry, Broker, and Transport
- cmd/cmd.go: newCmd() uses local copies instead of pointers to package
  globals; Before() no longer mutates DefaultXxx vars
- cmd/options.go: remove global mutations from all option functions
- service/service.go: export ServiceImpl type for cross-package use
- service/group.go: new Group type for multi-service lifecycle
- micro.go: add Start/Stop to Service interface, expose Group and
  NewGroup convenience function
- examples/multi-service: working example with two services

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: highlight multi-service binary support

Add multi-service section to README with code example, update features
list, add to examples index, and note in status summary.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: make micro new generate MCP-enabled services by default

- main.go template includes mcp.WithMCP(":3001") by default
- Handler template has agent-friendly doc comments with @example tags
- Proto template has descriptive field comments
- README includes MCP usage, Claude Code config, and tool description tips
- Makefile adds mcp-tools, mcp-test, mcp-serve targets
- go.mod updated to Go 1.22
- Added --no-mcp flag to opt out of MCP integration
- Post-create output shows MCP endpoint URLs

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add MCP migration guide and troubleshooting guide

- Migration guide: 3 approaches to add MCP to existing services
  (WithMCP one-liner, standalone gateway, CLI)
- Troubleshooting guide: common issues with agents, WebSocket,
  Claude Code, auth, rate limiting, and performance

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename model/ package to ai/ for AI model providers

The model/ package name conflicted with the conventional use of "model"
for data models. Renamed to ai/ which better describes the package's
purpose (AI provider abstraction for Anthropic, OpenAI, etc.) and frees
up model/ for future data model layer use.

- Rename model/ → ai/ with package name change
- Update all Go imports from go-micro.dev/v5/model to go-micro.dev/v5/ai
- Update cmd/micro/server/server.go references (model.X → ai.X)
- Update all documentation and roadmap references
- All tests pass, CLI builds successfully

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model package for typed data access with CRUD and queries

New model/ package provides a typed data model layer using Go generics.
Supports structured CRUD operations, WHERE filters, ordering, pagination,
and automatic schema creation from struct tags.

Three backends:
- memory: in-memory for development and testing
- sqlite: embedded SQL for dev and single-node production
- postgres: full PostgreSQL for production deployments

Key features:
- Generic Model[T] with Create/Read/Update/Delete/List/Count
- Query builder: Where(), WhereOp(), OrderAsc/Desc(), Limit(), Offset()
- Struct tags: model:"key" for primary key, model:"index" for indexes
- Auto table creation from struct schema
- 19 tests passing across memory and sqlite backends

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model code generation to protoc-gen-micro

Extend the micro plugin to generate model structs from proto messages
annotated with // @model. Generated alongside client/server code in
the same .pb.micro.go file.

For a proto message like:
  // @model
  message User { string id = 1; string name = 2; }

Generates:
- UserModel struct with model:"key" and json tags
- NewUserModel(db) factory returning *model.Model[UserModel]
- UserModelFromProto(*User) *UserModel converter
- (*UserModel).ToProto() *User converter

Supports @model(table=custom_table, key=custom_field) options.
Adds GetComments() to generator for plugin comment inspection.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Model() to Service interface for Client/Server/Model trifecta

Every service now exposes Client(), Server(), and Model() — call services,
handle requests, and save/query data from the same interface. Includes
README docs, blog post, and a full model guide on the docs site.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Helm chart for MCP gateway Kubernetes deployment

Adds official Helm chart at deploy/helm/mcp-gateway/ with:
- Deployment, Service, ServiceAccount templates
- HPA for auto-scaling based on CPU/memory
- Ingress with TLS support
- Configurable registry (consul, etcd, mdns), rate limiting,
  JWT auth, audit logging, and per-tool scopes
- Security context (non-root, read-only rootfs, drop all caps)
- NOTES.txt with post-install connection instructions

Updates roadmap and status docs to reflect Helm Charts as delivered.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 07:28:30 +00:00
Asim Aslam 76bfeae456 Claude/update docs roadmap f zd2 j (#2880)
* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: enable multiple services in a single binary

Remove global state mutations from service and cmd option functions so
that configuring one service no longer overwrites another's settings.

Key changes:
- service/options.go: remove all DefaultXxx global writes from option
  functions; newOptions() now creates fresh Server, Client, Store, and
  Cache per service while sharing Registry, Broker, and Transport
- cmd/cmd.go: newCmd() uses local copies instead of pointers to package
  globals; Before() no longer mutates DefaultXxx vars
- cmd/options.go: remove global mutations from all option functions
- service/service.go: export ServiceImpl type for cross-package use
- service/group.go: new Group type for multi-service lifecycle
- micro.go: add Start/Stop to Service interface, expose Group and
  NewGroup convenience function
- examples/multi-service: working example with two services

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: highlight multi-service binary support

Add multi-service section to README with code example, update features
list, add to examples index, and note in status summary.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: make micro new generate MCP-enabled services by default

- main.go template includes mcp.WithMCP(":3001") by default
- Handler template has agent-friendly doc comments with @example tags
- Proto template has descriptive field comments
- README includes MCP usage, Claude Code config, and tool description tips
- Makefile adds mcp-tools, mcp-test, mcp-serve targets
- go.mod updated to Go 1.22
- Added --no-mcp flag to opt out of MCP integration
- Post-create output shows MCP endpoint URLs

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add MCP migration guide and troubleshooting guide

- Migration guide: 3 approaches to add MCP to existing services
  (WithMCP one-liner, standalone gateway, CLI)
- Troubleshooting guide: common issues with agents, WebSocket,
  Claude Code, auth, rate limiting, and performance

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename model/ package to ai/ for AI model providers

The model/ package name conflicted with the conventional use of "model"
for data models. Renamed to ai/ which better describes the package's
purpose (AI provider abstraction for Anthropic, OpenAI, etc.) and frees
up model/ for future data model layer use.

- Rename model/ → ai/ with package name change
- Update all Go imports from go-micro.dev/v5/model to go-micro.dev/v5/ai
- Update cmd/micro/server/server.go references (model.X → ai.X)
- Update all documentation and roadmap references
- All tests pass, CLI builds successfully

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model package for typed data access with CRUD and queries

New model/ package provides a typed data model layer using Go generics.
Supports structured CRUD operations, WHERE filters, ordering, pagination,
and automatic schema creation from struct tags.

Three backends:
- memory: in-memory for development and testing
- sqlite: embedded SQL for dev and single-node production
- postgres: full PostgreSQL for production deployments

Key features:
- Generic Model[T] with Create/Read/Update/Delete/List/Count
- Query builder: Where(), WhereOp(), OrderAsc/Desc(), Limit(), Offset()
- Struct tags: model:"key" for primary key, model:"index" for indexes
- Auto table creation from struct schema
- 19 tests passing across memory and sqlite backends

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model code generation to protoc-gen-micro

Extend the micro plugin to generate model structs from proto messages
annotated with // @model. Generated alongside client/server code in
the same .pb.micro.go file.

For a proto message like:
  // @model
  message User { string id = 1; string name = 2; }

Generates:
- UserModel struct with model:"key" and json tags
- NewUserModel(db) factory returning *model.Model[UserModel]
- UserModelFromProto(*User) *UserModel converter
- (*UserModel).ToProto() *User converter

Supports @model(table=custom_table, key=custom_field) options.
Adds GetComments() to generator for plugin comment inspection.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Model() to Service interface for Client/Server/Model trifecta

Every service now exposes Client(), Server(), and Model() — call services,
handle requests, and save/query data from the same interface. Includes
README docs, blog post, and a full model guide on the docs site.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 13:13:34 +00:00
Asim Aslam f07a49e0d9 docs: add CHANGELOG.md with structured release history (#2878)
Keep a Changelog format covering 2026.01 through current unreleased
work. Includes all MCP gateway features, CLI commands, agent SDKs,
developer experience improvements, and documentation additions.

This replaces ad-hoc blog posts as the canonical "what changed" reference.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 12:05:06 +00:00
Asim Aslam 6247b1d065 chore: move internal status docs out of top level (#2879)
Move Claude-context working documents to internal/docs/:
- CURRENT_STATUS_SUMMARY.md
- PROJECT_STATUS_2026.md
- ROADMAP_2026.md
- IMPLEMENTATION_SUMMARY.md

These are session-tracking docs, not useful to most contributors.
Top level now only has standard files: README, CHANGELOG,
CONTRIBUTING, SECURITY, CLAUDE.md, and ROADMAP.

Updated all cross-references in CLAUDE.md, ROADMAP.md, and
website docs.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:59:52 +00:00
Asim Aslam c800dd3729 feat: add deployment example, workflow example, and MCP benchmarks (#2877)
Docker Compose deployment example (examples/deployment/):
- docker-compose.yml with Consul, MCP gateway, Jaeger tracing
- Dockerfile and Dockerfile.gateway for multi-stage builds
- README with architecture diagram and customization guide

Cross-service workflow example (examples/mcp/workflow/):
- Inventory, Orders, Notifications services
- Shows agents orchestrating multi-step workflows from natural language
- Stock check → reserve → order → notify in a single agent conversation

MCP gateway benchmark suite (gateway/mcp/benchmark_test.go):
- ListTools: ~20μs (10 tools), ~48μs (100 tools)
- Tool lookup: ~19ns (zero-alloc, scales to 500+ tools)
- Auth inspect: ~7ns, scope check: ~16ns
- Rate limiter: ~111ns per check
- JSON encode/decode: ~1.5-2μs per tool

Updated examples README with new examples index.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:40:10 +00:00
Asim Aslam 870b540922 Claude/mcp dx improvements f zd2 j (#2876)
* docs: add MCP migration guide and troubleshooting guide

- Migration guide: 3 approaches to add MCP to existing services
  (WithMCP one-liner, standalone gateway, CLI)
- Troubleshooting guide: common issues with agents, WebSocket,
  Claude Code, auth, rate limiting, and performance

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add CRUD example, error handling guide, and status updates

- CRUD contact book example (examples/mcp/crud/) with 6 operations,
  rich doc comments, @example tags, and description struct tags
- Error handling guide for writing agent-friendly error responses
  using typed errors, actionable messages, and idempotency patterns
- Updated MCP examples index, CLAUDE.md, and status summary

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:32:27 +00:00
Asim Aslam fad2fd7af4 Claude/update docs roadmap f zd2 j (#2875)
* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: enable multiple services in a single binary

Remove global state mutations from service and cmd option functions so
that configuring one service no longer overwrites another's settings.

Key changes:
- service/options.go: remove all DefaultXxx global writes from option
  functions; newOptions() now creates fresh Server, Client, Store, and
  Cache per service while sharing Registry, Broker, and Transport
- cmd/cmd.go: newCmd() uses local copies instead of pointers to package
  globals; Before() no longer mutates DefaultXxx vars
- cmd/options.go: remove global mutations from all option functions
- service/service.go: export ServiceImpl type for cross-package use
- service/group.go: new Group type for multi-service lifecycle
- micro.go: add Start/Stop to Service interface, expose Group and
  NewGroup convenience function
- examples/multi-service: working example with two services

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: highlight multi-service binary support

Add multi-service section to README with code example, update features
list, add to examples index, and note in status summary.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: make micro new generate MCP-enabled services by default

- main.go template includes mcp.WithMCP(":3001") by default
- Handler template has agent-friendly doc comments with @example tags
- Proto template has descriptive field comments
- README includes MCP usage, Claude Code config, and tool description tips
- Makefile adds mcp-tools, mcp-test, mcp-serve targets
- go.mod updated to Go 1.22
- Added --no-mcp flag to opt out of MCP integration
- Post-create output shows MCP endpoint URLs

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:24:33 +00:00
Asim Aslam 19892f2c67 Claude/update docs roadmap f zd2 j (#2874)
* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: enable multiple services in a single binary

Remove global state mutations from service and cmd option functions so
that configuring one service no longer overwrites another's settings.

Key changes:
- service/options.go: remove all DefaultXxx global writes from option
  functions; newOptions() now creates fresh Server, Client, Store, and
  Cache per service while sharing Registry, Broker, and Transport
- cmd/cmd.go: newCmd() uses local copies instead of pointers to package
  globals; Before() no longer mutates DefaultXxx vars
- cmd/options.go: remove global mutations from all option functions
- service/service.go: export ServiceImpl type for cross-package use
- service/group.go: new Group type for multi-service lifecycle
- micro.go: add Start/Stop to Service interface, expose Group and
  NewGroup convenience function
- examples/multi-service: working example with two services

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: highlight multi-service binary support

Add multi-service section to README with code example, update features
list, add to examples index, and note in status summary.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:19:43 +00:00
Asim Aslam d2036b880d Claude/update docs roadmap f zd2 j (#2873)
* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: enable multiple services in a single binary

Remove global state mutations from service and cmd option functions so
that configuring one service no longer overwrites another's settings.

Key changes:
- service/options.go: remove all DefaultXxx global writes from option
  functions; newOptions() now creates fresh Server, Client, Store, and
  Cache per service while sharing Registry, Broker, and Transport
- cmd/cmd.go: newCmd() uses local copies instead of pointers to package
  globals; Before() no longer mutates DefaultXxx vars
- cmd/options.go: remove global mutations from all option functions
- service/service.go: export ServiceImpl type for cross-package use
- service/group.go: new Group type for multi-service lifecycle
- micro.go: add Start/Stop to Service interface, expose Group and
  NewGroup convenience function
- examples/multi-service: working example with two services

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: highlight multi-service binary support

Add multi-service section to README with code example, update features
list, add to examples index, and note in status summary.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:13:27 +00:00
Asim Aslam cad0ff1e49 Claude/update docs roadmap f zd2 j (#2872)
* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: enable multiple services in a single binary

Remove global state mutations from service and cmd option functions so
that configuring one service no longer overwrites another's settings.

Key changes:
- service/options.go: remove all DefaultXxx global writes from option
  functions; newOptions() now creates fresh Server, Client, Store, and
  Cache per service while sharing Registry, Broker, and Transport
- cmd/cmd.go: newCmd() uses local copies instead of pointers to package
  globals; Before() no longer mutates DefaultXxx vars
- cmd/options.go: remove global mutations from all option functions
- service/service.go: export ServiceImpl type for cross-package use
- service/group.go: new Group type for multi-service lifecycle
- micro.go: add Start/Stop to Service interface, expose Group and
  NewGroup convenience function
- examples/multi-service: working example with two services

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: highlight multi-service binary support

Add multi-service section to README with code example, update features
list, add to examples index, and note in status summary.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 10:50:00 +00:00
Asim Aslam ffe43e0e6d Claude/update docs roadmap f zd2 j (#2871)
* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 10:16:17 +00:00
Asim Aslam ec9473f86f Update sponsorship information in README.md 2026-03-04 10:10:46 +00:00
Asim Aslam 4c673f61b1 Fix formatting in README.md links section 2026-03-04 10:10:18 +00:00
Asim Aslam bed94bfa95 Update sponsorship information in README
Updated sponsorship link and removed sponsor text.
2026-03-04 10:10:04 +00:00
Asim Aslam d02ff2ecfa feat: add standalone MCP gateway binary (#2870)
Add cmd/micro-mcp-gateway for production MCP gateway deployment
independent of micro run. Supports:

- Registry selection: mdns, consul, etcd (via --registry flag)
- Rate limiting per tool (--rate-limit, --rate-burst)
- JWT authentication (--auth)
- Per-tool scope requirements (--scope tool=scope1,scope2)
- Audit logging to stdout (--audit)
- Environment variable configuration for all flags
- Dockerfile for containerized deployment

Usage:
  micro-mcp-gateway --address :3000 --registry consul --registry-address consul:8500

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 10:08:09 +00:00
Asim Aslam 6d9645adce feat: polish agent playground UI (#2869)
Redesign the /agent playground with improved UX:
- Chat-focused layout with full-height message area and sticky input
- Collapsible tool call cards showing name, input, result, and timing
- Thinking indicator while waiting for agent response
- Settings panel collapsed by default (auto-opens if no API key)
- Empty state with available tools preview
- Clear chat button
- Better visual hierarchy with distinct message styles

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 09:38:04 +00:00
Asim Aslam beeaad748e Claude/update docs roadmap f zd2 j (#2868)
* feat: add LlamaIndex SDK for Go Micro services

Add LlamaIndex integration package that enables LlamaIndex agents to
discover and call Go Micro microservices through the MCP gateway.
Follows the same pattern as the existing LangChain SDK.

- GoMicroToolkit with from_gateway() factory and tool filtering
- FunctionTool integration via llama_index.core.tools
- Auth support, error handling, and retry configuration
- Examples for basic agent and RAG + microservices workflows
- Unit tests with mocked gateway responses

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: update status for OTel, WebSocket, and LlamaIndex SDK completion

Reflect recently completed work in roadmap and status documents:
- Q2 progress: 85% -> 95% (WebSocket, LlamaIndex SDK done)
- Q3 progress: 40% -> 50% (OpenTelemetry integration done)
- Transports: 2 -> 3 (added WebSocket)
- Agent SDKs: 1 -> 2 (added LlamaIndex)
- Test coverage: 568 -> 1,000+ lines

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add WithMCP convenience option, improve startup banner, and blog post

- Add mcp.WithMCP(":3000") service option for one-line MCP setup
- Improve `micro run` startup banner to show Agent playground, MCP
  tools, and WebSocket endpoints prominently
- Add blog post: "Building the AI-Native Future of Go Micro with Claude"
  covering WebSocket transport, OTel integration, LlamaIndex SDK, and
  Anthropic's Claude Max sponsorship
- Update blog index and navigation links

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 09:28:07 +00:00
Asim Aslam 076b7c37be feat: add WebSocket transport for MCP gateway (#2867)
Add bidirectional WebSocket transport at /mcp/ws using JSON-RPC 2.0
protocol (same as stdio). This enables persistent connections for
real-time AI agents that need streaming tool interactions.

New files:
- gateway/mcp/websocket.go: WebSocketTransport with connection-level
  auth, per-message auth fallback, write serialization, OTel tracing
- gateway/mcp/websocket_test.go: 14 tests covering initialize,
  tools/list, tool calls, auth (header + param), scopes, rate
  limiting, audit, concurrent requests, multiple connections,
  error handling, and connection persistence

Changes:
- gateway/mcp/mcp.go: Register /mcp/ws handler in serveHTTP
- go.mod: Added github.com/gorilla/websocket v1.5.3

Auth supports two modes:
- Connection-level: Bearer token in WebSocket upgrade request headers
- Per-message: _token field in JSON-RPC params (same as stdio)

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 08:26:09 +00:00
Asim Aslam ab6f027741 Claude/update docs roadmap f zd2 j (#2866)
* Update docs and roadmap to March 2026 with focus priorities

- ROADMAP.md: Updated from Nov 2025 to reflect Q1 completions and current state
- ROADMAP_2026.md: Updated status to March 2026, added model package as delivered
- CURRENT_STATUS_SUMMARY.md: Rewrote with March 2026 status and clear next priorities
- PROJECT_STATUS_2026.md: Added model package section, updated recommendations
- Website roadmap: Updated Q3 security status and timestamps

Key focus areas identified: documentation guides, multi-protocol MCP,
LlamaIndex SDK, and OpenTelemetry integration.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* Add CLAUDE.md and four documentation guides to fill doc gaps

- CLAUDE.md: Project guide with structure, build commands, and priorities
- ai-native-services.md: End-to-end tutorial building an MCP-enabled task service
- mcp-security.md: Production security guide (auth, scopes, rate limiting, audit)
- tool-descriptions.md: Best practices for writing Go comments that help agents
- agent-patterns.md: Six integration patterns from single-agent to event-driven
- Updated docs index with new "AI & Agents" section linking all four guides

These were the highest priority gaps identified in the roadmap analysis:
the framework has solid features that were under-documented.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add OpenTelemetry tracing to MCP gateway

Integrate OpenTelemetry spans into the MCP gateway for both HTTP and
stdio transports. Each tool call now creates a server span with rich
attributes (tool name, account ID, auth outcome, transport type).
Trace context is propagated to downstream RPC calls via metadata,
enabling end-to-end distributed tracing through Jaeger, Grafana, etc.

New files:
- gateway/mcp/otel.go: Span creation, attribute constants, metadata carrier
- gateway/mcp/otel_test.go: 8 tests covering span creation, auth denied/allowed,
  rate limiting, trace propagation, noop provider, and missing token

Changes:
- Options.TraceProvider: Optional trace.TracerProvider field
- handleCallTool (HTTP): Creates OTel spans with auth/rate-limit attributes
- handleToolsCall (stdio): Same instrumentation for stdio transport
- go.mod: Added go.opentelemetry.io/otel/sdk v1.35.0 (test dependency)

The existing MCP trace ID (UUID) is preserved for backward compatibility
and recorded as a span attribute alongside the W3C trace context.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 08:16:06 +00:00
Asim Aslam 14ec9b955f Update status docs: March 2026 progress and roadmap refinement (#2865)
* Update docs and roadmap to March 2026 with focus priorities

- ROADMAP.md: Updated from Nov 2025 to reflect Q1 completions and current state
- ROADMAP_2026.md: Updated status to March 2026, added model package as delivered
- CURRENT_STATUS_SUMMARY.md: Rewrote with March 2026 status and clear next priorities
- PROJECT_STATUS_2026.md: Added model package section, updated recommendations
- Website roadmap: Updated Q3 security status and timestamps

Key focus areas identified: documentation guides, multi-protocol MCP,
LlamaIndex SDK, and OpenTelemetry integration.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* Add CLAUDE.md and four documentation guides to fill doc gaps

- CLAUDE.md: Project guide with structure, build commands, and priorities
- ai-native-services.md: End-to-end tutorial building an MCP-enabled task service
- mcp-security.md: Production security guide (auth, scopes, rate limiting, audit)
- tool-descriptions.md: Best practices for writing Go comments that help agents
- agent-patterns.md: Six integration patterns from single-agent to event-driven
- Updated docs index with new "AI & Agents" section linking all four guides

These were the highest priority gaps identified in the roadmap analysis:
the framework has solid features that were under-documented.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 07:33:19 +00:00
Asim Aslam a789c9e94b Update Go Micro version in README 2026-02-21 05:22:24 +00:00
Copilot e110ccb5ff Refactor model interface to high-level idiomatic Go API (#2863)
* Initial plan

* Add model package with provider abstraction interface

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add unit tests for model providers

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Refactor server to use model package abstraction

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Use strings.Contains instead of custom substring search

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add comprehensive documentation for model package

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Refactor model interface to be more idiomatic Go

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Simplify server code to use new high-level Generate API

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Update documentation for new high-level model API

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-14 18:15:07 +00:00
Copilot e3337efd81 Update 2026 roadmap to reflect Q2 completions (85% → docs only) (#2862)
* Initial plan

* Update 2026 roadmap to reflect Q2 completions (CLI exports and LangChain SDK)

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Update PROJECT_STATUS and roadmap docs to reflect Q2 85% completion

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Fix CLI integration status table to show all commands as complete

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Address code review feedback: fix duplicates and add missing metrics

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 17:36:25 +00:00
Copilot 13d1116dee Implement Q2 2026 roadmap: MCP CLI export commands and LangChain SDK (#2861)
* Initial plan

* Implement micro mcp docs and export commands (Q2 2026 roadmap)

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add comprehensive CLI examples and documentation for new MCP commands

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add LangChain Python SDK for Go Micro (Q2 2026 roadmap)

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Update PROJECT_STATUS to reflect LangChain SDK completion

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add implementation summary for Roadmap 2026 session

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:39:48 +00:00
Copilot 1db7903010 [WIP] Implement missing features from documentation (#2859)
* Initial plan

* Add --header and --metadata flags to micro call command

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Apply code formatting with gofmt

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add clarifying comments for dual metadata handling paths

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:20:34 +00:00
Copilot 5e1042e5ae Implement micro mcp test command (#2857)
* Initial plan

* Implement micro mcp test command

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add test for parseTool function

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Address code review feedback - simplify parseTool

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:19:41 +00:00
Copilot 0f6453488e Implement missing --service flag for micro deploy command (#2858)
* Initial plan

* Implement --service flag for micro deploy command

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Address code review feedback - optimize validation and add comments

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:19:03 +00:00
361 changed files with 34824 additions and 2150 deletions
+1 -1
View File
@@ -48,4 +48,4 @@ Add any other context about the problem here.
- [Troubleshooting Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/getting-started.md)
- [Examples](https://github.com/micro/go-micro/tree/master/examples)
- [API Reference](https://pkg.go.dev/go-micro.dev/v5)
- [Discord Community](https://discord.gg/jwTYuUVAGh)
- [Discord Community](https://discord.gg/WeMU5AGxD)
+1 -1
View File
@@ -39,4 +39,4 @@ Add any other context, code examples, or screenshots about the feature request h
- [Roadmap](https://github.com/micro/go-micro/blob/master/ROADMAP.md)
- [Contributing Guide](https://github.com/micro/go-micro/blob/master/CONTRIBUTING.md)
- [Architecture Docs](https://github.com/micro/go-micro/tree/master/internal/website/docs/architecture.md)
- [Discord Community](https://discord.gg/jwTYuUVAGh)
- [Discord Community](https://discord.gg/WeMU5AGxD)
+51
View File
@@ -0,0 +1,51 @@
name: goreleaser
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: write
id-token: write
packages: write
attestations: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4
View File
@@ -57,3 +57,7 @@ examples/mcp/hello/hello
# IDE-specific files
.DS_Store
/micro
# Built example/harness binaries (go build ./path/... drops these at repo root)
/plan-delegate
/agent-plan-delegate
+136
View File
@@ -0,0 +1,136 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
builds:
- main: ./cmd/micro
id: micro
binary: micro
env:
- CGO_ENABLED=0
- >-
{{- if eq .Os "darwin" }}
{{- if eq .Arch "amd64"}}CC=o64-clang{{- end }}
{{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }}
{{- end }}
{{- if eq .Os "windows" }}
{{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }}
{{- end }}
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
goarm:
- 7
ignore:
- goos: windows
goarch: arm
- main: ./cmd/protoc-gen-micro
id: protoc-gen-micro
binary: protoc-gen-micro
env:
- CGO_ENABLED=0
- >-
{{- if eq .Os "darwin" }}
{{- if eq .Arch "amd64"}}CC=o64-clang{{- end }}
{{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }}
{{- end }}
{{- if eq .Os "windows" }}
{{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }}
{{- end }}
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
goarm:
- 7
ignore:
- goos: windows
goarch: arm
archives:
- id: micro
ids:
- micro
formats: [tar.gz]
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- none*
format_overrides:
- goos: windows
formats: [zip]
- id: protoc-gen-micro
ids:
- protoc-gen-micro
formats: [tar.gz]
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- none*
format_overrides:
- goos: windows
formats: [zip]
report_sizes: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
dockers_v2:
-
ids:
- micro
- protoc-gen-micro
images:
- "micro/micro"
- "ghcr.io/micro/go-micro"
tags:
- "v{{ .Version }}"
- "{{ if .IsNightly }}nightly{{ end }}"
- "{{ if not .IsNightly }}latest{{ end }}"
labels:
"io.artifacthub.package.readme-url": "https://raw.githubusercontent.com/micro/go-micro/refs/heads/master/README.md"
"io.artifacthub.package.logo-url": "https://www.gravatar.com/avatar/09d1da3ea9ee61753219a19016d6a672?s=120&r=g&d=404"
"org.opencontainers.image.description": "A Go Platform built for Developers"
"org.opencontainers.image.created": "{{.Date}}"
"org.opencontainers.image.title": "{{.ProjectName}}"
"org.opencontainers.image.revision": "{{.FullCommit}}"
"org.opencontainers.image.version": "{{.Version}}"
"org.opencontainers.image.source": "{{.GitURL}}"
"org.opencontainers.image.url": "{{.GitURL}}"
"org.opencontainers.image.licenses": "MIT"
platforms:
- linux/amd64
- linux/arm64
retry:
attempts: 5
delay: 5s
max_delay: 2m
+105
View File
@@ -0,0 +1,105 @@
# Changelog
All notable changes to Go Micro are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/). Go Micro uses
calendar-based versions (YYYY.MM) for the AI-native era.
---
## [Unreleased]
### Added
- **Agent platform showcase** — full platform example (Users, Posts, Comments, Mail) mirroring [micro/blog](https://github.com/micro/blog), demonstrating how existing microservices become agent-accessible with zero code changes (`examples/mcp/platform/`).
- **Blog post: "Your Microservices Are Already an AI Platform"** — walkthrough of agent-service interaction patterns using real-world services (`internal/website/blog/7.md`).
- **Circuit breakers for MCP gateway** — per-tool circuit breakers protect downstream services from cascading failures. Configurable max failures, open-state timeout, and half-open probing. Available via `Options.CircuitBreaker` and `--circuit-breaker` CLI flag (`gateway/mcp/circuitbreaker.go`).
- **Helm chart for MCP gateway** — official Helm chart at `deploy/helm/mcp-gateway/` with Deployment, Service, ServiceAccount, HPA, and Ingress templates. Supports Consul/etcd/mDNS registries, JWT auth, rate limiting, audit logging, per-tool scopes, TLS ingress, and auto-scaling.
- **MCP gateway benchmarks** — comprehensive benchmark suite for tool listing, lookup, auth, rate limiting, and JSON serialization (`gateway/mcp/benchmark_test.go`)
- **Workflow example** — cross-service orchestration demo with Inventory, Orders, and Notifications services showing agents chaining multi-step workflows from natural language (`examples/mcp/workflow/`)
- **Docker Compose deployment** — production-like setup with Consul registry, standalone MCP gateway, and Jaeger tracing in one `docker-compose up` (`examples/deployment/`)
---
## [2026.03] - March 2026
### Added
#### Developer Experience
- **`micro new` MCP templates** — `micro new myservice` generates MCP-enabled services with doc comments, `@example` tags, and `WithMCP()` wired in. Use `--no-mcp` to opt out.
- **`micro.New("name")` unified API** — single way to create services: `micro.New("greeter")` or `micro.New("greeter", micro.Address(":8080"))`. Replaces `micro.NewService()` + `service.New()` dual API.
- **`service.Handle()` simplified registration** — register handlers with `service.Handle(new(Greeter))` instead of manual `server.NewHandler` + `server.Handle`.
- **`micro.NewGroup()` modular monoliths** — run multiple services in one binary with shared lifecycle: `micro.NewGroup(users, orders).Run()`.
- **`mcp.WithMCP()` one-liner** — add MCP to any service with a single option: `micro.New("name", mcp.WithMCP(":3001"))`.
- **CRUD example** — contact book service with 6 operations, rich agent docs, and validation patterns (`examples/mcp/crud/`).
#### MCP Gateway
- **WebSocket transport** — bidirectional JSON-RPC 2.0 streaming over WebSocket for real-time agent communication (`gateway/mcp/websocket.go`).
- **OpenTelemetry integration** — full span instrumentation across HTTP, stdio, and WebSocket transports with W3C trace context propagation (`gateway/mcp/otel.go`).
- **Standalone gateway binary** — `micro-mcp-gateway` with Docker support for running the MCP gateway independently of services.
- **Per-tool auth scopes** — service-level (`server.WithEndpointScopes()`) and gateway-level (`Options.Scopes`) scope enforcement with bearer token auth.
- **Rate limiting** — per-tool token bucket rate limiting (`Options.RateLimit`).
- **Audit logging** — immutable audit records per tool call with trace ID, account, scopes, duration, and errors (`Options.AuditFunc`).
#### AI Model Package
- **`model.Model` interface** — unified AI provider abstraction with `Generate()` and `Stream()` methods.
- **Anthropic Claude provider** — `model/anthropic` with tool execution and auto-calling.
- **OpenAI GPT provider** — `model/openai` with provider auto-detection from base URL.
#### Agent SDKs
- **LangChain SDK** — `contrib/langchain-go-micro/` Python package with auto-discovery, tool generation, and multi-agent workflow examples.
- **LlamaIndex SDK** — `contrib/go-micro-llamaindex/` Python package with RAG integration examples.
#### Documentation
- **AI-native services guide** — building services for AI agents from scratch
- **MCP security guide** — auth, scopes, and audit logging
- **Tool descriptions guide** — writing doc comments that improve agent performance
- **Agent patterns guide** — architecture patterns for agent integration
- **Error handling guide** — writing agent-friendly error responses with typed errors
- **Troubleshooting guide** — common MCP issues and solutions
- **Migration guide** — add MCP to existing services in 5 minutes
#### CLI
- **`micro mcp serve`** — start MCP server (stdio for Claude Code, HTTP for web agents)
- **`micro mcp list`** — list available tools (human-readable or JSON)
- **`micro mcp test`** — test tools with JSON input
- **`micro mcp docs`** — generate tool documentation
- **`micro mcp export`** — export to LangChain, OpenAPI, or JSON formats
#### Agent Playground
- **Chat-focused UI** — redesigned playground with collapsible tool calls, real-time status, and thinking indicators
- **Provider settings** — configurable OpenAI/Anthropic provider, model, and API key
### Changed
- Service interface moved to `service.Service` with `micro.Service` as a type alias for backward compatibility.
- `service.New()` returns `service.Service` interface (was `*ServiceImpl`).
- `service.NewGroup()` accepts `service.Service` interface (was `*ServiceImpl`).
- `go.mod` template in `micro new` updated to Go 1.22.
### Fixed
- Handler `Handle()` method accepts variadic `server.HandlerOption` for scopes and metadata.
- Store initialization uses service name as table automatically.
- Service `Stop()` properly aggregates errors from lifecycle hooks.
---
## [2026.02] - February 2026
### Added
- **MCP gateway library** — `gateway/mcp/` with HTTP/SSE and stdio transports, service discovery, tool generation, and JSON schema generation from Go types (2,500+ lines).
- **CLI integration** — `micro run --mcp-address` flag to start MCP alongside services.
- **Documentation extraction** — auto-extract tool descriptions from Go doc comments with `@example` tag and struct tag parsing.
- **Blog post** — "Making Microservices AI-Native with MCP"
- **MCP examples** — `examples/mcp/hello/` and `examples/mcp/documented/`
---
## [2026.01] - January 2026
### Added
- **`micro deploy`** — deploy services to any Linux server via SSH + systemd with `micro deploy user@server`.
- **`micro build`** — build Go binaries and Docker images with `micro build --docker`.
- **Blog post** — "Introducing micro deploy"
---
_For earlier changes, see the [git log](https://github.com/micro/go-micro/commits/master)._
+148
View File
@@ -0,0 +1,148 @@
# CLAUDE.md - Go Micro Project Guide
## Project Overview
Go Micro is a framework for distributed systems development in Go. It provides pluggable abstractions for service discovery, RPC, pub/sub, config, auth, storage, and more.
The framework is evolving into an **AI-native platform** where every microservice is automatically accessible to AI agents via the Model Context Protocol (MCP).
## Build & Test
```bash
# Run all tests
make test
# Run tests for a specific package
go test ./gateway/mcp/...
go test ./ai/...
go test ./model/...
# Lint
make lint
# Format
make fmt
# Build CLI
go build -o micro ./cmd/micro
# Run locally with hot reload
micro run
```
## Project Structure
```
go-micro/
├── agent/ # Agent abstraction (intelligent service management)
├── ai/ # AI model providers (Anthropic, OpenAI, Gemini, etc.)
├── auth/ # Authentication (JWT, no-op)
├── broker/ # Message broker (NATS, RabbitMQ)
├── cache/ # Caching (Redis)
├── client/ # RPC client (gRPC)
├── cmd/micro/ # CLI tool (run, deploy, mcp, build, server)
├── codec/ # Message codecs (JSON, Proto)
├── config/ # Dynamic config (env, file, etcd, NATS)
├── errors/ # Error handling
├── events/ # Event system (NATS JetStream)
├── flow/ # Event-driven LLM orchestration
├── gateway/
│ ├── api/ # REST API gateway
│ └── mcp/ # MCP gateway (core AI integration)
│ └── deploy/ # Helm charts for MCP gateway
├── health/ # Health checking
├── logger/ # Logging
├── metadata/ # Context metadata
├── model/ # Typed data models (CRUD, queries, schemas)
├── registry/ # Service discovery (mDNS, Consul, etcd)
├── selector/ # Client-side load balancing
├── server/ # RPC server
├── service/ # Service interface + profiles
├── store/ # Data persistence (Postgres, NATS KV)
├── transport/ # Network transport
├── wrapper/ # Middleware (auth, trace, metrics)
├── examples/ # Working examples
└── internal/ # Non-public: docs, utils, test harness
```
## Key Architectural Decisions
- **Plugin architecture**: All abstractions use Go interfaces. Defaults work out of the box, everything is swappable.
- **Progressive complexity**: Zero-config for development, full control for production.
- **AI-native by default**: Every service is automatically an MCP tool. No extra code needed.
- **In-repo plugins**: Plugins live in the main repo to avoid version compatibility issues.
- **Reflection-based registration**: Handlers are registered via reflection for minimal boilerplate.
## Code Conventions
- Standard Go conventions (gofmt, golint)
- Functional options pattern for configuration (`WithX()` functions)
- Interface-first design: define the interface, then implement
- Tests alongside code (not in separate test directories)
- Commit messages: imperative mood, concise summary line
## Current Focus & Priorities (March 2026)
### Status
- **Q1 2026 (MCP Foundation):** COMPLETE
- **Q2 2026 (Agent DX):** COMPLETE (100%)
- **Q3 2026 (Production):** 50% complete (ahead of schedule)
### Priority 1: Agent Showcase & Examples
Build compelling demos showing agents interacting with go-micro services in realistic scenarios.
### Priority 2: Additional Protocol Support
- gRPC reflection-based MCP
- HTTP/3 support
### Priority 3: Kubernetes & Deployment
- Helm Charts for MCP gateway
- Kubernetes Operator with CRDs
### Recently Completed
- **Agent Plan & Delegate** - Two built-in agent tools: `plan` (ordered plan persisted to store-backed memory, surfaced in the prompt) and `delegate` (hand a subtask to another agent — RPC to a registered agent, else an ephemeral sub-agent with isolated context). Added automatically to every agent; no harness or graph. (`agent/builtin.go`, `examples/agent-plan-delegate/`)
- **`micro new` MCP Templates** - Scaffolds MCP-enabled services with doc comments, `@example` tags, `WithMCP()`. `--no-mcp` to opt out.
- **CRUD Example** - Contact book service with 6 operations, rich agent docs (`examples/mcp/crud/`)
- **Migration Guide** - "Add MCP to Existing Services" guide with 3 approaches
- **Troubleshooting Guide** - Common MCP issues and solutions
- **Error Handling Guide** - Patterns for agent-friendly error responses
- **Documentation Guides** - Six guides: AI-native services, MCP security, tool descriptions, agent patterns, error handling, troubleshooting
- **WithMCP Option** - One-line MCP setup (`gateway/mcp/option.go`)
- **Agent Playground Redesign** - Chat-focused UI with collapsible tool calls
- **Standalone Gateway Binary** - `micro-mcp-gateway` with Docker support
- **WebSocket Transport** - Bidirectional JSON-RPC 2.0 streaming (`gateway/mcp/websocket.go`)
- **OpenTelemetry Integration** - Full span instrumentation with W3C trace context (`gateway/mcp/otel.go`)
- **LlamaIndex SDK** - Python package with RAG examples (`contrib/go-micro-llamaindex/`)
## Key Files
| Purpose | File |
|---------|------|
| MCP Gateway | `gateway/mcp/mcp.go` |
| MCP Docs | `gateway/mcp/DOCUMENTATION.md` |
| AI Interface | `ai/model.go` |
| Model Layer | `model/model.go` |
| CLI Entry | `cmd/micro/main.go` |
| MCP CLI | `cmd/micro/mcp/` |
| Server (run/server) | `cmd/micro/server/server.go` |
| Roadmap | `internal/docs/ROADMAP_2026.md` |
| Status | `internal/docs/CURRENT_STATUS_SUMMARY.md` |
| Changelog | `CHANGELOG.md` |
| Docs Site | `internal/website/docs/` |
## Roadmap & Status Documents
- **[ROADMAP.md](ROADMAP.md)** - General framework roadmap
- **[internal/docs/ROADMAP_2026.md](internal/docs/ROADMAP_2026.md)** - AI-native era roadmap with business model
- **[internal/docs/CURRENT_STATUS_SUMMARY.md](internal/docs/CURRENT_STATUS_SUMMARY.md)** - Quick status overview
- **[internal/docs/PROJECT_STATUS_2026.md](internal/docs/PROJECT_STATUS_2026.md)** - Detailed technical status
- **[internal/docs/IMPLEMENTATION_SUMMARY.md](internal/docs/IMPLEMENTATION_SUMMARY.md)** - Implementation notes
- **[CHANGELOG.md](CHANGELOG.md)** - What changed and when
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines. Key points:
- Open an issue before large changes
- Include tests for new features
- Run `make test` and `make lint` before submitting
- Follow commit message format: `type: description` (e.g., `feat: add WebSocket transport`)
-282
View File
@@ -1,282 +0,0 @@
# Go Micro - Current Status Summary
**Updated:** February 11, 2026
## 🎯 Executive Summary
**Go Micro's MCP integration is 3-4 months ahead of schedule**, with Q1 2026 goals complete and significant Q2/Q3 2026 features already delivered.
### Quick Status
-**Q1 2026 (MCP Foundation):** 100% COMPLETE
- 🟢 **Q2 2026 (Agent DX):** 60% COMPLETE (ahead of schedule)
- 🟢 **Q3 2026 (Production):** 40% COMPLETE (ahead of schedule)
- 🟡 **Q4 2026 (Ecosystem):** 0% COMPLETE (on track)
---
## 📊 What's Been Built
### ✅ Core MCP Integration (Q1 - COMPLETE)
- **MCP Gateway Library** (`gateway/mcp/`) - 2,083 lines
- HTTP/SSE transport
- Stdio JSON-RPC 2.0 transport
- Service discovery & tool generation
- Schema generation from Go types
- **CLI Commands** (`micro mcp`)
- `micro mcp serve` - Start MCP server (stdio or HTTP)
- `micro mcp list` - List available tools
- `micro mcp test` - Test tools (placeholder)
- **Documentation**
- Complete API documentation
- 2 working examples (hello, documented)
- Blog post: "Making Microservices AI-Native with MCP"
### ✅ Advanced Features (Q2/Q3 - DELIVERED EARLY)
#### 🔒 Security & Auth
- **Per-Tool Scopes**
- Service-level: `server.WithEndpointScopes("Blog.Create", "blog:write")`
- Gateway-level: `Options.Scopes` map for overrides
- Bearer token authentication
- Scope enforcement before RPC execution
#### 📊 Observability
- **Tracing**
- UUID trace IDs per tool call
- Metadata propagation (`Mcp-Trace-Id`, `Mcp-Tool-Name`, `Mcp-Account-Id`)
- Full call chain tracking
- **Audit Logging**
- Immutable audit records per tool call
- Captures: tool, account, scopes, allowed/denied, duration, errors
- Callback function: `Options.AuditFunc`
#### 🚦 Rate Limiting
- Per-tool rate limiters
- Configurable requests/second and burst
- Token bucket algorithm
#### 📝 Documentation Extraction
- Auto-extract from Go doc comments
- `@example` tag support for JSON examples
- Struct tag parsing for parameter descriptions
- Manual override via `WithEndpointDocs()`
---
## 🚀 What Works Today
### For Claude Code Users
```bash
# Start MCP server for Claude Code
micro mcp serve
# Add to ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"my-services": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
```
### For Library Users
```go
package main
import (
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
func main() {
service := micro.NewService(micro.Name("myservice"))
service.Init()
// Add MCP gateway (3 lines!)
go mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
Auth: authProvider, // Optional: auth.Auth
Scopes: map[string][]string{ // Optional: per-tool scopes
"myservice.Handler.Create": {"write"},
},
RateLimit: &mcp.RateLimitConfig{ // Optional
RequestsPerSecond: 10,
Burst: 20,
},
AuditFunc: func(r mcp.AuditRecord) { // Optional
log.Printf("[audit] %+v", r)
},
})
service.Run()
}
```
### For Service Developers
```go
// Just add Go comments - docs extracted automatically!
// GetUser retrieves a user by ID. Returns full profile with email and preferences.
//
// @example {"id": "user-123"}
func (s *UserService) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
// implementation
}
// Register with scopes
handler := service.Server().NewHandler(
new(UserService),
server.WithEndpointScopes("UserService.Delete", "users:admin"),
)
```
---
## 📈 Test Coverage
**568 lines** of comprehensive tests covering:
- ✅ Scope validation & enforcement
- ✅ Auth provider integration
- ✅ Trace ID generation & propagation
- ✅ Audit record creation
- ✅ Rate limiting
- ✅ HTTP & Stdio transports
- ✅ Tool discovery & schema generation
---
## 🎯 What's Next (Recommended Priorities)
### Immediate (Next 2 Weeks)
1. **Complete `micro mcp test` command** (~1 day)
- Implement actual tool testing with JSON input/output
2. **LangChain SDK** (~1 week)
- Python package: `go-micro-langchain`
- Auto-generate LangChain tools from registry
- Example multi-agent workflow
- **Impact:** Largest agent framework integration
3. **Interactive Playground** (~1 week)
- Web UI for testing services with AI
- Real-time tool call visualization
- **Impact:** Critical for demos and sales
### Short-Term (Next Month)
4. **WebSocket Transport** (~3 days)
- Bidirectional streaming for long-running operations
5. **LlamaIndex SDK** (~1 week)
- Python package for RAG integration
6. **Case Studies** (ongoing)
- Document real-world usage
---
## 📊 By The Numbers
| Metric | Value |
|--------|-------|
| **Production Code** | 2,083 lines |
| **Test Code** | 568 lines |
| **Documentation Files** | 4+ |
| **Working Examples** | 2 |
| **CLI Commands** | 3 |
| **Transports** | 2 (HTTP/SSE, Stdio) |
| **Q1 Completion** | 100% |
| **Ahead of Schedule** | 3-4 months |
---
## 🔍 Where We Are on the Roadmap
### Q1 2026: MCP Foundation
**Status:** ✅ COMPLETE (100%)
- All 6 planned deliverables complete
- Production-ready implementation
- Comprehensive documentation
### Q2 2026: Agent Developer Experience
**Status:** 🟢 IN PROGRESS (60% complete)
**COMPLETED (ahead of schedule):**
- ✅ Stdio transport for Claude Code
-`micro mcp serve` and `list` commands
- ✅ Tool descriptions from comments
-`@example` tag support
- ✅ Schema generation from struct tags
- ✅ HTTP/SSE with auth
**NOT YET STARTED:**
-`micro mcp test` (full implementation)
-`micro mcp docs` and `export` commands
- ❌ Agent SDKs (LangChain, LlamaIndex, AutoGPT)
- ❌ Interactive Agent Playground
- ❌ Multi-protocol (WebSocket, gRPC, HTTP/3)
### Q3 2026: Production & Scale
**Status:** 🟢 IN PROGRESS (40% complete)
**COMPLETED (ahead of schedule):**
- ✅ Per-tool authentication & scopes
- ✅ Agent call tracing
- ✅ Rate limiting
- ✅ Audit logging
- ✅ Bearer token auth
**NOT YET STARTED:**
- ❌ Standalone MCP Gateway binary
- ❌ Kubernetes Operator
- ❌ Helm Charts
- ❌ OpenTelemetry integration
- ❌ Full observability dashboards
### Q4 2026: Ecosystem & Monetization
**Status:** 🟡 PLANNING (0% complete)
- All features planned for Q4 2026
- On track to start in Q4
---
## 📖 Key Documents
1. **[PROJECT_STATUS_2026.md](./PROJECT_STATUS_2026.md)** - Comprehensive 20-page status report
2. **[ROADMAP_2026.md](./ROADMAP_2026.md)** - Updated roadmap with completion markers
3. **[/gateway/mcp/DOCUMENTATION.md](./gateway/mcp/DOCUMENTATION.md)** - Complete MCP documentation
4. **[/examples/mcp/README.md](./examples/mcp/README.md)** - Examples and usage guide
5. **[/internal/website/blog/2.md](./internal/website/blog/2.md)** - Launch blog post
---
## 🎉 Key Achievements
1. **✅ Production-Ready in Q1** - Ahead of schedule
2. **✅ Security-First** - Auth, scopes, audit from day one
3. **✅ Developer-Friendly** - 3 lines of code to enable MCP
4. **✅ Claude Code Ready** - Works with Anthropic's flagship IDE
5. **✅ Comprehensive Testing** - 90%+ test coverage
6. **✅ Well-Documented** - Multiple docs + examples + blog post
---
## 💡 Bottom Line
**Go Micro is production-ready for AI agent integration TODAY.**
The Q1 2026 foundation is solid, with advanced Q2/Q3 features already delivered. The framework is:
- ✅ Ready for production use
- ✅ Secure by default
- ✅ Easy to use (3 lines of code)
- ✅ Well-tested and documented
- ✅ Compatible with Claude Code and other AI tools
**Next focus:** Agent SDKs and developer tools to drive adoption.
---
**For detailed technical analysis, see [PROJECT_STATUS_2026.md](./PROJECT_STATUS_2026.md)**
+26
View File
@@ -0,0 +1,26 @@
FROM alpine:latest
ARG TARGETPLATFORM
ENV USER=micro
ENV GROUPNAME=$USER
ARG UID=1001
ARG GID=1001
RUN addgroup --gid "$GID" "$GROUPNAME" \
&& adduser \
--disabled-password \
--gecos "" \
--home "/micro" \
--ingroup "$GROUPNAME" \
--no-create-home \
--uid "$UID" "$USER"
ENV PATH=/usr/local/go/bin:$PATH
RUN apk --no-cache add git make curl
COPY --from=golang:1.26.0-alpine /usr/local/go /usr/local/go
COPY $TARGETPLATFORM/micro /usr/local/go/bin/
COPY $TARGETPLATFORM/protoc-gen-micro /usr/local/go/bin/
WORKDIR /micro
EXPOSE 8080
ENTRYPOINT ["/usr/local/go/bin/micro"]
CMD ["server"]
+25 -1
View File
@@ -1,4 +1,14 @@
.PHONY: test test-race test-coverage lint fmt install-tools proto clean help
NAME = micro
GIT_COMMIT = $(shell git rev-parse --short HEAD)
GIT_TAG = $(shell git describe --abbrev=0 --tags --always --match "v*")
GIT_IMPORT = go-micro.dev/v5/cmd/micro
BUILD_DATE = $(shell date +%s)
LDFLAGS = -X $(GIT_IMPORT).BuildDate=$(BUILD_DATE) -X $(GIT_IMPORT).GitCommit=$(GIT_COMMIT) -X $(GIT_IMPORT).GitTag=$(GIT_TAG)
# GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser-cross:v1.25.7
GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser:latest
.PHONY: test test-race test-coverage lint fmt install-tools proto clean help gorelease-dry-run gorelease-dry-run-docker
# Default target
help:
@@ -13,6 +23,9 @@ help:
@echo " make proto - Generate protobuf code"
@echo " make clean - Clean build artifacts"
$(NAME):
CGO_ENABLED=0 go build -ldflags "-s -w ${LDFLAGS}" -o $(NAME) cmd/micro/main.go
# Run tests
test:
go test -v ./...
@@ -56,3 +69,14 @@ clean:
find . -name "*.test" -type f -delete
go clean -cache -testcache
# Try binary release
gorelease-dry-run:
docker run \
--rm \
-e CGO_ENABLED=0 \
-v $(CURDIR):/$(NAME) \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /$(NAME) \
$(GORELEASER_DOCKER_IMAGE) \
--clean --verbose --skip=publish,validate --snapshot
+249 -213
View File
@@ -1,292 +1,328 @@
# Go Micro [![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/go-micro.dev/v5?tab=doc) [![Go Report Card](https://goreportcard.com/badge/github.com/go-micro/go-micro)](https://goreportcard.com/report/github.com/go-micro/go-micro)
# Go Micro [![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/go-micro.dev/v5?tab=doc) [![Go Report Card](https://goreportcard.com/badge/github.com/go-micro/go-micro)](https://goreportcard.com/report/github.com/go-micro/go-micro)
Go Micro is a framework for distributed systems development.
Go Micro is a framework for building services and agents in Go.
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsor the project](https://github.com/sponsors/micro) | [Discord](https://discord.gg/jwTYuUVAGh)
Write services — they register, discover each other, and communicate via RPC and events. Every endpoint is automatically an AI-callable tool via [MCP](https://modelcontextprotocol.io/). Build agents to manage them intelligently. Both are Go code, both use the same primitives, both deploy the same way.
## Overview
## Sponsors
Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
The Go Micro philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
but everything can be easily swapped out.
<a href="https://go-micro.dev/blog/3"><img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" height="26" /></a>
&nbsp;&nbsp;
<a href="https://go-micro.dev/blog/8"><img src="https://www.atlascloud.ai/logo.svg" height="26" /></a>
## Features
## Quick Start
Go Micro abstracts away the details of distributed systems. Here are the main features.
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
zero trust networking by providing every service an identity and certificates. This additionally includes rule
based access control.
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
level config from any source such as env vars, file, etcd. You can merge the sources and even define fallbacks.
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for many storage backends
in the plugins repo. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
multicast DNS (mdns), a zeroconf system.
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
across the services and retry a different node if there's a problem.
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
and server handle this by default. This includes protobuf and json by default.
- **RPC Client/Server** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous
communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker.
- **MCP Integration** - An MCP gateway you can integrate as a library, server or CLI command which automatically exposes services
as tools for agents or other AI applications. Every service/endpoint get's converted into a callable tool.
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology.
## Getting Started
To make use of Go Micro
Install the CLI:
```bash
go get go-micro.dev/v5@latest
# Binary (no Go required)
curl -fsSL https://go-micro.dev/install.sh | sh
# Or with Go
go install go-micro.dev/v5/cmd/micro@v5.27.0
```
Create a service and register a handler
### Fastest start — no API key
Scaffold a service, run it, call it:
```bash
micro new helloworld
cd helloworld
micro run
```
Then in another terminal:
```bash
curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
-H 'Content-Type: application/json' -d '{"name":"World"}'
```
### Generate from a prompt — with an LLM key
Set a provider key, describe what you want, and the AI designs services, writes handlers, compiles, and starts them:
```bash
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
micro run --prompt "a task management system with categories" --provider anthropic
```
The AI designs the architecture, you review it, then it generates handlers with real business logic, compiles them, and starts them:
```
Services:
● task — Task management with status tracking
● project — Project organization
Generate? [Y/n]
Micro
Services:
● task
● project
Agents:
◆ agent
```
Then talk to your services from the console:
```
> Create a project called Launch, then add three tasks to it
→ project_Project_Create({"name":"Launch"})
← {"record":{"id":"p1..."},"success":true}
→ task_Task_Create({"title":"Design specs","project_id":"p1..."})
→ task_Task_Create({"title":"Write code","project_id":"p1..."})
→ task_Task_Create({"title":"Ship it","project_id":"p1..."})
Created Work category and added 'Finish report' task to it.
```
When you need a capability that doesn't exist, the agent generates a new service mid-conversation:
```
> I need to track shipping. Create a shipment for order 123 to London.
⚡ generating shipping service...
✓ shipping
→ shipping_Shipping_Create({"order_id":"123","destination":"London"})
← {"record":{"id":"xyz...","status":"pending"}}
Created shipment for order 123 going to London.
```
Edit the generated code by hand at any time — re-running preserves your changes. [Read more](https://go-micro.dev/blog/13).
## Writing Services
Under the hood, a service is a struct with methods. Doc comments and `@example` tags become tool descriptions for AI agents automatically.
```go
package main
import (
"go-micro.dev/v5"
"go-micro.dev/v5"
)
type Request struct {
Name string `json:"name"`
Name string `json:"name"`
}
type Response struct {
Message string `json:"message"`
Message string `json:"message"`
}
type Say struct{}
func (h *Say) Hello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
func main() {
// create the service
service := micro.New("helloworld")
// register handler
service.Handle(new(Say))
// run the service
service.Run()
}
```
Set a fixed address
```go
service := micro.NewService(
micro.Name("helloworld"),
micro.Address(":8080"),
)
```
Call it via curl
```bash
curl -XPOST \
-H 'Content-Type: application/json' \
-H 'Micro-Endpoint: Say.Hello' \
-d '{"name": "alice"}' \
http://localhost:8080
```
## MCP & AI Agents
Go Micro is designed for an **agent-first** workflow. Every service you build automatically becomes a tool that AI agents can discover and use via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/).
- **[🤖 Agent Playground](https://go-micro.dev/docs/mcp.html)** — Chat with your services through an interactive AI agent at `/agent`
- **[🔧 MCP Tools Registry](https://go-micro.dev/docs/mcp.html)** — Browse all services exposed as AI-callable tools at `/api/mcp/tools`
- **[📖 MCP Documentation](https://go-micro.dev/docs/mcp.html)** — Full guide to MCP integration, auth, and scopes
### Services as Tools
Write a normal Go Micro service and it's instantly available as an MCP tool:
```go
// SayHello greets a person by name.
// Hello greets a person by name.
// @example {"name": "Alice"}
func (g *GreeterService) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
func (h *Say) Hello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
func main() {
service := micro.New("greeter")
service.Handle(new(Say))
service.Run()
}
```
Run with `micro run` and the agent playground and MCP tools registry are ready:
Run it and everything is accessible — REST, gRPC, MCP, agent playground:
```bash
micro run
# Agent Playground: http://localhost:8080/agent
# MCP Tools: http://localhost:8080/api/mcp/tools
# Dashboard: http://localhost:8080
# API: http://localhost:8080/api/{service}/{method}
# Agent: http://localhost:8080/agent
# MCP Tools: http://localhost:8080/mcp/tools
```
Use `micro mcp serve` for local AI tools like Claude Code, or connect any MCP-compatible agent to the HTTP endpoint.
See the [MCP guide](https://go-micro.dev/docs/mcp.html) for authentication, scopes, and advanced usage.
## Examples
Check out [/examples](examples/) for runnable code:
- [hello-world](examples/hello-world/) - Basic RPC service
- [web-service](examples/web-service/) - HTTP REST API
- [mcp](examples/mcp/) - MCP integration with AI agents
See [all examples](examples/README.md) for more.
## Protobuf
Install the code generator and see usage in the docs:
You can also scaffold a service from a template:
```bash
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.16.0
micro new helloworld
micro new contacts --template crud
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
## Building Agents
Docs: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
An Agent is a service with an LLM inside it. It has a proto-defined `Agent.Chat` RPC endpoint, registers in the registry, and is callable like any service:
## Command Line
Install the CLI:
```
go install go-micro.dev/v5/cmd/micro@v5.16.0
```go
agent := micro.NewAgent("task-mgr",
micro.AgentServices("task", "project"),
micro.AgentPrompt("You manage tasks and projects. You understand deadlines and priorities."),
micro.AgentProvider("anthropic"),
)
agent.Run()
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
The agent discovers its services from the registry, scopes its tools to their endpoints, and maintains conversation memory in the store. It registers itself so `micro chat` and other agents can find it.
### Quick Start
```go
// Programmatic interaction
resp, _ := agent.Ask(ctx, "What tasks are overdue?")
fmt.Println(resp.Reply)
```
Multiple agents coordinate via RPC — each is a service with an `Agent.Chat` endpoint. `micro chat` routes to the right one.
```bash
micro new helloworld # Create a new service
cd helloworld
micro run # Run with API gateway and hot reload
micro agent list # list registered agents
micro call task-mgr Agent.Chat '{"message": "What tasks are overdue?"}'
```
Then open http://localhost:8080 to see your service and call it from the browser.
### Plan & Delegate
### Development Workflow
Every agent gets two built-in capabilities, exposed as tools — no extra setup, no harness:
| Stage | Command | Purpose |
|-------|---------|---------|
| **Develop** | `micro run` | Local dev with hot reload and API gateway |
| **Build** | `micro build` | Compile production binaries |
| **Deploy** | `micro deploy` | Push to a remote Linux server via SSH + systemd |
| **Dashboard** | `micro server` | Optional production web UI with JWT auth |
- **`plan`** — for multi-step work, the agent records an ordered plan in its store-backed memory and stays oriented across turns.
- **`delegate`** — the agent hands a self-contained subtask to another agent. If a registered agent already owns the relevant services, the hand-off goes over RPC to that agent; otherwise a focused, short-lived sub-agent is created for the subtask with its own isolated context.
### micro run
This keeps intelligence distributed: an agent doesn't need to know *how* to do everything, only *who* does. See [examples/agent-plan-delegate](examples/agent-plan-delegate/).
`micro run` starts your services with:
- **Web Dashboard** - Browse and call services at `/`
- **Agent Playground** - AI chat with MCP tools at `/agent`
- **API Explorer** - Browse endpoints and schemas at `/api`
- **API Gateway** - HTTP to RPC proxy at `/api/{service}/{method}` (no auth in dev mode)
- **MCP Tools** - Services as AI tools at `/api/mcp/tools`
- **Health Checks** - Aggregated health at `/health`
- **Hot Reload** - Auto-rebuild on file changes
> **Note:** `micro run` and `micro server` use a unified gateway architecture. See [Gateway Architecture](cmd/micro/README.md#gateway-architecture) for details.
```bash
micro run # Gateway on :8080
micro run --address :3000 # Custom gateway port
micro run --no-gateway # Services only
micro run --env production # Use production environment
```go
// A sub-agent is just an agent — created with New, talked to with Ask.
// delegate-first: reuse a registered agent, or spin up a focused one.
resp, _ := agent.Ask(ctx, "Plan the launch, create the tasks, and have comms notify the owner.")
```
### Configuration
## Features
For multi-service projects, create a `micro.mu` file:
### AI
| Feature | Details |
|---------|---------|
| Agents | `micro.NewAgent()` — intelligent layer that manages services |
| Plan & delegate | Built-in agent tools — plan multi-step work, delegate subtasks to other agents |
| Guardrails | `MaxSteps` (stopping condition) and `ApproveTool` (human-in-the-loop) on every agent |
| Workflows | `micro.NewFlow()` — event-driven; runs a step or triggers an agent |
| MCP gateway | Every endpoint is an AI tool automatically |
| 7 LLM providers | Anthropic, OpenAI, Gemini, Groq, Mistral, Together, Atlas Cloud |
| Interactive console | `micro run` includes a chat console for talking to services |
| Service generation | `micro run --prompt` — describe a system, get running services |
### Framework
| Feature | Details |
|---------|---------|
| Service registry | mDNS (default), Consul, etcd |
| RPC client/server | gRPC transport, load balancing, streaming |
| Pub/sub events | NATS, RabbitMQ, HTTP broker |
| Key-value store | File (bbolt), Postgres, NATS KV |
| Typed model layer | CRUD + queries, SQLite/Postgres backends |
| Everything swappable | All abstractions are Go interfaces |
### Developer experience & deployment
| Feature | Details |
|---------|---------|
| Hot reload | `micro run` watches files, rebuilds on change |
| Templates | `micro new --template crud/pubsub/api` |
| One-command deploy | `micro deploy user@server` — SSH + systemd, no Docker |
## CLI
| Command | Purpose |
|---------|---------|
| `micro run --prompt "..."` | Generate services + agent, start with interactive console |
| `micro run` | Dev mode: hot reload, gateway, interactive console |
| `micro run -d` | Detached mode (no console) |
| `micro chat` | Standalone chat (when not using micro run) |
| `micro agent list` | List registered agents |
| `micro new myservice` | Scaffold a service |
| `micro call service endpoint '{}'` | Call a service or agent from the CLI |
| `micro build` | Compile production binaries |
| `micro deploy user@server` | Deploy via SSH + systemd |
## Multi-Service Projects
Run multiple services together:
```go
users := micro.New("users", micro.Address(":9001"))
orders := micro.New("orders", micro.Address(":9002"))
users.Handle(new(Users))
orders.Handle(new(Orders))
g := micro.NewGroup(users, orders)
g.Run()
```
Or use a `micro.mu` config file:
```
service users
path ./users
port 8081
service posts
path ./posts
port 8082
service orders
path ./orders
depends users
env development
DATABASE_URL sqlite://./dev.db
```
The gateway runs on :8080 by default, so services should use other ports.
## Data Model
### Deployment
Typed persistence with CRUD and queries:
Deploy to any Linux server with systemd:
```go
type User struct {
ID string `json:"id" model:"key"`
Name string `json:"name"`
Email string `json:"email" model:"index"`
}
```bash
# On your server (one-time setup)
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
db := service.Model()
db.Register(&User{})
db.Create(ctx, &User{ID: "1", Name: "Alice", Email: "alice@example.com"})
# From your laptop
micro deploy user@your-server
var results []*User
db.List(ctx, &results, model.Where("email", "alice@example.com"))
```
The deploy command:
1. Builds binaries for Linux
2. Copies via SSH to the server
3. Sets up systemd services
4. Verifies services are healthy
Backends: memory (default), SQLite, Postgres.
Optionally run `micro server` on the deployed machine for a production web dashboard with JWT auth, user management, and API explorer.
## AI Providers
Manage deployed services:
```bash
micro status --remote user@server # Check status
micro logs --remote user@server # View logs
micro logs myservice --remote user@server -f # Follow specific service
Swap providers with a single import — same interface everywhere:
| Provider | Default Model |
|----------|---------------|
| Anthropic | `claude-sonnet-4-20250514` |
| OpenAI | `gpt-4o` |
| Google Gemini | `gemini-2.5-flash` |
| Groq | `llama-3.3-70b-versatile` |
| Mistral | `mistral-large-latest` |
| Together AI | `Llama-3.3-70B-Instruct-Turbo` |
| Atlas Cloud | `llama-3.3-70b` |
```go
m := ai.New("anthropic", ai.WithAPIKey(key))
resp, _ := m.Generate(ctx, &ai.Request{Prompt: "hello"})
```
No Docker required. No Kubernetes. Just systemd.
## Examples
See [internal/website/docs/deployment.md](internal/website/docs/deployment.md) for full deployment guide.
- [hello-world](examples/hello-world/) — Basic RPC service
- [multi-service](examples/multi-service/) — Multiple services in one binary
- [mcp](examples/mcp/) — MCP integration with AI agents
- [agent-plan-delegate](examples/agent-plan-delegate/) — Agent planning and multi-agent delegation
- [grpc-interop](examples/grpc-interop/) — Call go-micro from any gRPC client
See [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
See [all examples](examples/README.md).
Docs: [`internal/website/docs`](internal/website/docs)
## Docs
- [Getting Started](internal/website/docs/getting-started.md)
- [AI Integration](internal/website/docs/ai-integration.md)
- [Agents and Workflows](internal/website/docs/guides/agents-and-workflows.md)
- [Agent Design](internal/docs/AGENT_DESIGN.md)
- [Plan & Delegate](internal/website/docs/guides/plan-delegate.md)
- [MCP & AI Agents](internal/website/docs/mcp.md)
- [Data Model](internal/website/docs/model.md)
- [Deployment](internal/website/docs/deployment.md)
- [Plugins](internal/website/docs/plugins.md)
Package reference: https://pkg.go.dev/go-micro.dev/v5
**User Guides:**
- [Getting Started](internal/website/docs/getting-started.md)
- [MCP & AI Agents](internal/website/docs/mcp.md)
- [Plugins Overview](internal/website/docs/plugins.md)
- [Learn by Example](internal/website/docs/examples/index.md)
- [Deployment Guide](internal/website/docs/deployment.md)
**Architecture & Performance:**
- [Performance Considerations](internal/website/docs/performance.md)
- [Reflection Usage & Philosophy](internal/website/docs/REFLECTION-EVALUATION-SUMMARY.md)
**Security:**
- [TLS Security Migration](internal/website/docs/TLS_SECURITY_UPDATE.md)
- [Security Migration Guide](internal/website/docs/SECURITY_MIGRATION.md)
## Adopters
- [Sourse](https://sourse.eu) - Work in the field of earth observation, including embedded Kubernetes running onboard aircraft, and weve built a mission management SaaS platform using Go Micro.
+31 -14
View File
@@ -2,18 +2,27 @@
This roadmap outlines the planned features and improvements for Go Micro. Community feedback and contributions are welcome!
> **🚀 NEW:** See [ROADMAP_2026.md](ROADMAP_2026.md) for the **AI-Native Era roadmap** focused on MCP integration, agent-first development, and business sustainability. This document covers general framework improvements.
> **See [internal/docs/ROADMAP_2026.md](internal/docs/ROADMAP_2026.md) for the AI-Native Era roadmap** focused on MCP integration, agent-first development, and business sustainability. This document covers general framework improvements.
## Current Focus (Q1 2026)
## Current Focus (Q1 2026) - COMPLETE
### Documentation & Developer Experience
- [x] Modernize documentation structure
- [x] Add learn-by-example guides
- [x] Update issue templates
- [x] MCP integration documentation
- [x] Agent playground and MCP tools registry
- [ ] Create video tutorials
- [ ] Interactive documentation site
- [ ] Plugin discovery dashboard
### AI & Model Integration
- [x] AI package with provider abstraction (`ai.Model` interface)
- [x] Anthropic Claude provider (`ai/anthropic`)
- [x] OpenAI GPT provider (`ai/openai`)
- [x] Tool execution with auto-calling support
- [x] Streaming support via `ai.Stream`
### Observability
- [ ] OpenTelemetry native support
- [ ] Auto-instrumentation for handlers
@@ -22,7 +31,10 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
- [ ] Integration with popular observability platforms
### Developer Tools
- [ ] `micro dev` with hot reload
- [x] `micro run` with hot reload and unified gateway
- [x] `micro deploy` with SSH + systemd deployment
- [x] `micro mcp` command suite (serve, list, test, docs, export)
- [ ] `micro dev` with enhanced hot reload
- [ ] Service templates (`micro new --template`)
- [ ] Better error messages with suggestions
- [ ] Debug tooling improvements
@@ -31,8 +43,8 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
## Q2 2026
### Production Readiness
- [ ] Health check standardization
- [ ] Graceful shutdown improvements
- [x] Health check standardization
- [x] Graceful shutdown improvements
- [ ] Resource cleanup best practices
- [ ] Load testing framework integration
- [ ] Performance benchmarking suite
@@ -45,6 +57,10 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
- [ ] Multi-cluster patterns
### Security
- [x] Bearer token authentication for MCP
- [x] Per-tool scope enforcement
- [x] Audit logging
- [x] Rate limiting
- [ ] mTLS by default option
- [ ] Secret management integration (Vault, AWS Secrets Manager)
- [ ] RBAC improvements
@@ -62,7 +78,7 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
### Streaming & Async
- [ ] Improved streaming support
- [ ] Server-sent events (SSE) support
- [x] Server-sent events (SSE) support (via MCP gateway)
- [ ] WebSocket plugin
- [ ] Event sourcing patterns
- [ ] CQRS examples
@@ -116,6 +132,7 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
### Differentiation
- **Batteries included, fully swappable** - Start simple, scale complex
- **Zero-config local development** - No infrastructure required to start
- **AI-native by default** - Every service is an MCP tool automatically
- **Plugin ecosystem in-repo** - No version compatibility hell
- **Progressive complexity** - Learn as you grow
- **Cloud-native first** - Built for Kubernetes and containers
@@ -125,11 +142,11 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
We welcome contributions to any roadmap items! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
### High Priority Areas
1. Documentation improvements
2. Real-world examples
3. Plugin development
4. Performance optimizations
5. Testing infrastructure
1. Documentation improvements (guides, tutorials)
2. Multi-protocol MCP support (WebSocket, gRPC)
3. Agent SDK integrations (LlamaIndex, AutoGPT)
4. OpenTelemetry integration
5. Kubernetes operator and Helm charts
### How to Contribute
- Pick an item from the roadmap
@@ -139,7 +156,7 @@ We welcome contributions to any roadmap items! See [CONTRIBUTING.md](CONTRIBUTIN
## Feedback
Have suggestions for the roadmap?
Have suggestions for the roadmap?
- Open a [feature request](.github/ISSUE_TEMPLATE/feature_request.md)
- Start a discussion in GitHub Discussions
@@ -160,6 +177,6 @@ We follow semantic versioning:
---
Last updated: November 2025
Last updated: March 2026
This roadmap is subject to change based on community needs and priorities. Star the repo to stay updated! ⭐
This roadmap is subject to change based on community needs and priorities.
+1 -1
View File
@@ -174,6 +174,6 @@ We currently do not offer a bug bounty program, but we greatly appreciate respon
For security questions that are not vulnerabilities, please:
- Open a discussion: https://github.com/micro/go-micro/discussions
- Join Discord: https://discord.gg/jwTYuUVAGh
- Join Discord: https://discord.gg/WeMU5AGxD
- Email: support@go-micro.dev
+318
View File
@@ -0,0 +1,318 @@
// Package agent provides the Agent abstraction for Go Micro.
//
// An Agent is a service with an LLM inside it. It registers a Chat
// RPC endpoint, discovers its assigned services' tools, and
// orchestrates them intelligently.
//
// agent := micro.NewAgent("task-mgr",
// micro.AgentServices("task"),
// micro.AgentPrompt("You manage tasks."),
// micro.AgentProvider("anthropic"),
// )
// agent.Run()
package agent
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
pb "go-micro.dev/v5/agent/proto"
"go-micro.dev/v5/ai"
"go-micro.dev/v5/server"
"go-micro.dev/v5/store"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/atlascloud"
_ "go-micro.dev/v5/ai/gemini"
_ "go-micro.dev/v5/ai/groq"
_ "go-micro.dev/v5/ai/mistral"
_ "go-micro.dev/v5/ai/openai"
_ "go-micro.dev/v5/ai/together"
)
// Agent is the interface for an AI agent that manages services.
type Agent interface {
Name() string
Init(...Option)
Options() Options
Ask(ctx context.Context, message string) (*Response, error)
Run() error
Stop() error
String() string
}
// Response is what an agent returns from Chat.
type Response struct {
Reply string
ToolCalls []ai.ToolCall
Agent string
}
type agentImpl struct {
opts Options
model ai.Model
tools *ai.Tools
hist *ai.History
server server.Server
mu sync.Mutex
// ephemeral marks a short-lived sub-agent created by delegation.
// Ephemeral agents run with an isolated context: they load and
// persist no history, and have no built-in tools (so they cannot
// plan or re-delegate).
ephemeral bool
// steps counts tool executions in the current Ask, for MaxSteps.
steps int
}
// New creates a new Agent.
func New(opts ...Option) Agent {
return &agentImpl{
opts: newOptions(opts...),
}
}
// newEphemeral creates a short-lived sub-agent for a delegated subtask.
// It shares the parent's provider, model, and infrastructure but runs
// with an isolated context: it loads and persists no history and has no
// built-in tools (so it can neither plan nor re-delegate). Returns the
// concrete type because ephemeral is an internal construction detail,
// not a public option.
func newEphemeral(opts ...Option) *agentImpl {
return &agentImpl{
opts: newOptions(opts...),
ephemeral: true,
}
}
func (a *agentImpl) Name() string {
return a.opts.Name
}
func (a *agentImpl) Init(opts ...Option) {
for _, o := range opts {
o(&a.opts)
}
a.setup()
}
func (a *agentImpl) Options() Options {
return a.opts
}
func (a *agentImpl) String() string {
return "agent"
}
func (a *agentImpl) setup() {
var modelOpts []ai.Option
modelOpts = append(modelOpts, ai.WithAPIKey(a.opts.APIKey))
if a.opts.Model != "" {
modelOpts = append(modelOpts, ai.WithModel(a.opts.Model))
}
a.tools = ai.NewTools(a.opts.Registry, ai.ToolClient(a.opts.Client))
modelOpts = append(modelOpts, ai.WithToolHandler(a.toolHandler()))
a.model = ai.New(a.opts.Provider, modelOpts...)
a.hist = ai.NewHistory(a.opts.HistoryLimit)
if !a.ephemeral {
a.loadHistory()
}
}
// Ask sends a message and returns the agent's response.
// This is the programmatic API for direct use.
func (a *agentImpl) Ask(ctx context.Context, message string) (*Response, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.model == nil {
a.setup()
}
toolList, err := a.discoverTools()
if err != nil {
return nil, fmt.Errorf("discover tools: %w", err)
}
a.hist.Add("user", message)
a.steps = 0
resp, err := a.model.Generate(ctx, &ai.Request{
Prompt: message,
SystemPrompt: a.buildPrompt(),
Tools: toolList,
Messages: a.hist.Messages(),
})
if err != nil {
return nil, err
}
if resp.Reply != "" {
a.hist.Add("assistant", resp.Reply)
}
if resp.Answer != "" {
a.hist.Add("assistant", resp.Answer)
}
a.saveHistory()
reply := resp.Reply
if resp.Answer != "" {
if reply != "" {
reply += "\n\n"
}
reply += resp.Answer
}
return &Response{
Reply: reply,
ToolCalls: resp.ToolCalls,
Agent: a.opts.Name,
}, nil
}
// Chat implements the proto AgentHandler interface for RPC.
// @example {"message": "What tasks are overdue?"}
func (a *agentImpl) Chat(ctx context.Context, req *pb.ChatRequest, rsp *pb.ChatResponse) error {
resp, err := a.Ask(ctx, req.Message)
if err != nil {
return err
}
rsp.Reply = resp.Reply
rsp.Agent = resp.Agent
for _, tc := range resp.ToolCalls {
input, _ := json.Marshal(tc.Input)
rsp.ToolCalls = append(rsp.ToolCalls, &pb.ToolCall{
Id: tc.ID,
Name: tc.Name,
Input: string(input),
Result: tc.Result,
})
}
return nil
}
// Run starts the agent as a service with a Chat RPC endpoint.
func (a *agentImpl) Run() error {
if a.model == nil {
a.setup()
}
a.server = server.NewServer(
server.Name(a.opts.Name),
server.Registry(a.opts.Registry),
server.Metadata(map[string]string{
"type": "agent",
"services": strings.Join(a.opts.Services, ","),
}),
)
pb.RegisterAgentHandler(a.server, a)
if err := a.server.Start(); err != nil {
return fmt.Errorf("failed to start agent: %w", err)
}
fmt.Printf("Agent %s registered (manages: %s)\n", a.opts.Name, strings.Join(a.opts.Services, ", "))
ch := make(chan struct{})
<-ch
return nil
}
func (a *agentImpl) Stop() error {
if a.server != nil {
return a.server.Stop()
}
return nil
}
func (a *agentImpl) discoverTools() ([]ai.Tool, error) {
all, err := a.tools.Discover()
if err != nil {
return nil, err
}
var scoped []ai.Tool
for _, t := range all {
if strings.HasPrefix(t.OriginalName, a.opts.Name+".") {
continue
}
if len(a.opts.Services) == 0 {
scoped = append(scoped, t)
continue
}
for _, svc := range a.opts.Services {
if strings.HasPrefix(t.OriginalName, svc+".") {
scoped = append(scoped, t)
break
}
}
}
// Expose the agent's own capabilities (plan, delegate) as tools.
// Ephemeral sub-agents don't get them.
if !a.ephemeral {
scoped = append(scoped, builtinTools()...)
}
return scoped, nil
}
func (a *agentImpl) buildPrompt() string {
var base string
switch {
case a.opts.Prompt != "":
base = a.opts.Prompt
case len(a.opts.Services) > 0:
base = fmt.Sprintf("You are the %s agent. You manage these services: %s. Use the available tools to fulfill requests.",
a.opts.Name, strings.Join(a.opts.Services, ", "))
default:
base = fmt.Sprintf("You are the %s agent. Use the available tools to fulfill requests.", a.opts.Name)
}
// Keep the agent oriented: surface its saved plan, if any.
if !a.ephemeral {
if plan := a.loadPlan(); plan != "" {
base += "\n\nYour current plan (update it with the plan tool as you make progress):\n" + plan
}
}
return base
}
func (a *agentImpl) historyKey() string {
return "agent/" + a.opts.Name + "/history"
}
func (a *agentImpl) loadHistory() {
recs, err := a.opts.Store.Read(a.historyKey())
if err != nil || len(recs) == 0 {
return
}
var messages []ai.Message
if err := json.Unmarshal(recs[0].Value, &messages); err != nil {
return
}
for _, m := range messages {
a.hist.Add(m.Role, m.Content)
}
}
func (a *agentImpl) saveHistory() {
if a.ephemeral {
return
}
data, err := json.Marshal(a.hist.Messages())
if err != nil {
return
}
a.opts.Store.Write(&store.Record{
Key: a.historyKey(),
Value: data,
})
}
+88
View File
@@ -0,0 +1,88 @@
package agent
import (
"testing"
)
func TestNew(t *testing.T) {
a := New(
Name("test-agent"),
Services("task", "project"),
Prompt("You manage tasks."),
Provider("anthropic"),
)
if a.Name() != "test-agent" {
t.Errorf("Name() = %q, want %q", a.Name(), "test-agent")
}
opts := a.Options()
if opts.Provider != "anthropic" {
t.Errorf("Provider = %q, want %q", opts.Provider, "anthropic")
}
if len(opts.Services) != 2 {
t.Fatalf("Services = %v, want 2 items", opts.Services)
}
if opts.Services[0] != "task" || opts.Services[1] != "project" {
t.Errorf("Services = %v, want [task project]", opts.Services)
}
if opts.Prompt != "You manage tasks." {
t.Errorf("Prompt = %q, want %q", opts.Prompt, "You manage tasks.")
}
if opts.HistoryLimit != 50 {
t.Errorf("HistoryLimit = %d, want 50", opts.HistoryLimit)
}
}
func TestBuildPrompt(t *testing.T) {
// Custom prompt
a := New(Name("test"), Prompt("custom prompt")).(*agentImpl)
if got := a.buildPrompt(); got != "custom prompt" {
t.Errorf("buildPrompt() = %q, want %q", got, "custom prompt")
}
// Auto-generated prompt with services
a = New(Name("test"), Services("task", "project")).(*agentImpl)
got := a.buildPrompt()
if got == "" {
t.Error("buildPrompt() returned empty")
}
if !contains(got, "task") || !contains(got, "project") {
t.Errorf("buildPrompt() = %q, should mention services", got)
}
// Auto-generated prompt without services
a = New(Name("test")).(*agentImpl)
got = a.buildPrompt()
if !contains(got, "test") {
t.Errorf("buildPrompt() = %q, should mention agent name", got)
}
}
func TestDefaults(t *testing.T) {
a := New(Name("test"))
opts := a.Options()
if opts.Registry == nil {
t.Error("Registry should default to DefaultRegistry")
}
if opts.Client == nil {
t.Error("Client should default to DefaultClient")
}
if opts.Store == nil {
t.Error("Store should default to DefaultStore")
}
}
func contains(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || len(s) > 0 && containsStr(s, sub))
}
func containsStr(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
+253
View File
@@ -0,0 +1,253 @@
package agent
import (
"context"
"encoding/json"
"fmt"
"go-micro.dev/v5/ai"
codecBytes "go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/store"
)
// Built-in agent tools. These are not service endpoints — they are
// capabilities the agent has over itself: maintaining a plan in its
// memory, and delegating a subtask to another agent.
//
// They are plain tools, wired into the agent's tool handler alongside
// the discovered service tools. There is no separate harness or graph:
// the LLM calls them like any other tool.
const (
toolPlan = "plan"
toolDelegate = "delegate"
)
// builtinTools returns the tool definitions exposed to the model in
// addition to the agent's scoped service tools.
func builtinTools() []ai.Tool {
return []ai.Tool{
{
Name: toolPlan,
OriginalName: toolPlan,
Description: "Record or update your plan as an ordered list of steps before doing multi-step work. " +
"Call this whenever the plan changes. The plan is saved to your memory and shown back to you on later turns.",
Properties: map[string]any{
"steps": map[string]any{
"type": "array",
"description": "Ordered plan steps. Each step has a 'task' (string) and a " +
"'status' (one of: pending, in_progress, done).",
},
},
},
{
Name: toolDelegate,
OriginalName: toolDelegate,
Description: "Delegate a self-contained subtask to another agent. If 'to' names an agent that already " +
"manages the relevant services, that agent handles it; otherwise a focused sub-agent is created for the " +
"subtask. The sub-agent works in an isolated context and returns only its result. Use this to keep your " +
"own context focused and to let domain experts handle their own services.",
Properties: map[string]any{
"task": map[string]any{
"type": "string",
"description": "The subtask to delegate, described completely and self-contained.",
},
"to": map[string]any{
"type": "string",
"description": "Optional. The agent or service name best suited to the subtask.",
},
},
},
}
}
// Builtins returns the built-in agent tools (plan, delegate) together
// with a handler for them, so the same capabilities can be wired into a
// tool loop that isn't a running Agent — for example the `micro chat`
// fallback. The handler's third return value is false when the name is
// not a built-in, so callers can fall through to their own tools.
//
// Configure it with the same options as an Agent (Name, Provider,
// WithStore, WithRegistry, WithClient, ...); these back plan's memory
// and delegate's RPC/sub-agent behaviour.
func Builtins(opts ...Option) (tools []ai.Tool, handle func(name string, input map[string]any) (result any, content string, ok bool)) {
a := &agentImpl{opts: newOptions(opts...)}
handle = func(name string, input map[string]any) (any, string, bool) {
switch name {
case toolPlan:
r, c := a.handlePlan(input)
return r, c, true
case toolDelegate:
r, c := a.handleDelegate(input)
return r, c, true
}
return nil, "", false
}
return builtinTools(), handle
}
// toolHandler returns the agent's tool-call handler. It intercepts the
// built-in tools and falls through to RPC service execution for the
// rest. Ephemeral sub-agents get the bare service handler so they can
// neither plan nor re-delegate (which prevents runaway recursion).
func (a *agentImpl) toolHandler() ai.ToolHandler {
base := a.tools.Handler()
if a.ephemeral {
return base
}
return func(name string, input map[string]any) (any, string) {
// plan is internal bookkeeping, not an action — never gated.
if name == toolPlan {
return a.handlePlan(input)
}
// Stopping condition: bound the number of actions per Ask.
if a.opts.MaxSteps > 0 {
a.steps++
if a.steps > a.opts.MaxSteps {
return errResult(fmt.Sprintf(
"step limit reached (%d). Do not call any more tools; stop and summarize what you have so far.",
a.opts.MaxSteps))
}
}
// Human-in-the-loop / policy: gate the action before it runs.
if a.opts.Approve != nil {
if ok, reason := a.opts.Approve(name, input); !ok {
msg := "tool call was not approved"
if reason != "" {
msg += ": " + reason
}
return errResult(msg)
}
}
if name == toolDelegate {
return a.handleDelegate(input)
}
return base(name, input)
}
}
// handlePlan persists the supplied plan to the agent's memory and
// echoes it back so the model can see the stored state.
func (a *agentImpl) handlePlan(input map[string]any) (any, string) {
data, err := json.Marshal(input)
if err != nil {
return errResult("invalid plan: " + err.Error())
}
if a.opts.Store != nil {
a.opts.Store.Write(&store.Record{Key: a.planKey(), Value: data})
}
return input, string(data)
}
// handleDelegate hands a subtask to another agent. Delegate-first:
// if 'to' names a registered agent, it is called via RPC. Otherwise an
// ephemeral sub-agent is created with a fresh, isolated context, asked
// the subtask, and its reply returned.
func (a *agentImpl) handleDelegate(input map[string]any) (any, string) {
task, _ := input["task"].(string)
if task == "" {
return errResult("task is required")
}
to, _ := input["to"].(string)
// Delegate-first: an existing agent that owns the domain handles it.
if to != "" && a.isAgent(to) {
reply, err := a.callAgentRPC(context.Background(), to, task)
if err != nil {
return errResult("delegate to agent " + to + ": " + err.Error())
}
out := map[string]any{"agent": to, "reply": reply}
b, _ := json.Marshal(out)
return out, string(b)
}
// Otherwise create a focused, ephemeral sub-agent. Fresh context:
// it loads no history and persists none.
var svcs []string
if to != "" {
svcs = []string{to}
}
sub := newEphemeral(
Name(a.opts.Name+".sub"),
Services(svcs...),
Prompt("You are a sub-agent handling a single delegated subtask. "+
"Complete it using the available tools and report the result concisely."),
Provider(a.opts.Provider),
Model(a.opts.Model),
APIKey(a.opts.APIKey),
WithRegistry(a.opts.Registry),
WithClient(a.opts.Client),
WithStore(a.opts.Store),
)
resp, err := sub.Ask(context.Background(), task)
if err != nil {
return errResult("sub-agent: " + err.Error())
}
out := map[string]any{"reply": resp.Reply}
b, _ := json.Marshal(out)
return out, string(b)
}
// isAgent reports whether name resolves to a registered agent (a
// service advertising type=agent in its metadata).
func (a *agentImpl) isAgent(name string) bool {
if a.opts.Registry == nil {
return false
}
recs, err := a.opts.Registry.GetService(name)
if err != nil || len(recs) == 0 {
return false
}
if recs[0].Metadata != nil && recs[0].Metadata["type"] == "agent" {
return true
}
for _, n := range recs[0].Nodes {
if n.Metadata != nil && n.Metadata["type"] == "agent" {
return true
}
}
return false
}
// callAgentRPC calls another agent's Agent.Chat endpoint and returns
// its reply.
func (a *agentImpl) callAgentRPC(ctx context.Context, name, msg string) (string, error) {
body, _ := json.Marshal(map[string]string{"message": msg})
req := a.opts.Client.NewRequest(name, "Agent.Chat", &codecBytes.Frame{Data: body})
var rsp codecBytes.Frame
if err := a.opts.Client.Call(ctx, req, &rsp); err != nil {
return "", err
}
var out struct {
Reply string `json:"reply"`
}
if err := json.Unmarshal(rsp.Data, &out); err != nil {
return "", err
}
return out.Reply, nil
}
func (a *agentImpl) planKey() string {
return "agent/" + a.opts.Name + "/plan"
}
// loadPlan returns the stored plan as a JSON string, or "" if none.
func (a *agentImpl) loadPlan() string {
if a.opts.Store == nil {
return ""
}
recs, err := a.opts.Store.Read(a.planKey())
if err != nil || len(recs) == 0 {
return ""
}
return string(recs[0].Value)
}
func errResult(msg string) (any, string) {
m := map[string]string{"error": msg}
b, _ := json.Marshal(m)
return m, string(b)
}
+165
View File
@@ -0,0 +1,165 @@
package agent
import (
"encoding/json"
"testing"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
)
func TestBuiltinTools(t *testing.T) {
tools := builtinTools()
if len(tools) != 2 {
t.Fatalf("builtinTools() = %d tools, want 2", len(tools))
}
names := map[string]bool{}
for _, tl := range tools {
names[tl.Name] = true
}
if !names[toolPlan] || !names[toolDelegate] {
t.Errorf("builtin tools = %v, want plan and delegate", names)
}
}
func TestHandlePlanPersists(t *testing.T) {
mem := store.NewMemoryStore()
a := New(Name("planner"), WithStore(mem)).(*agentImpl)
steps := map[string]any{
"steps": []any{
map[string]any{"task": "gather requirements", "status": "done"},
map[string]any{"task": "write code", "status": "in_progress"},
},
}
_, content := a.handlePlan(steps)
if content == "" {
t.Fatal("handlePlan returned empty content")
}
// The plan must be retrievable from memory.
got := a.loadPlan()
if got == "" {
t.Fatal("loadPlan() returned empty after handlePlan")
}
var decoded map[string]any
if err := json.Unmarshal([]byte(got), &decoded); err != nil {
t.Fatalf("stored plan is not valid JSON: %v", err)
}
if _, ok := decoded["steps"]; !ok {
t.Errorf("stored plan missing steps: %s", got)
}
}
func TestPlanShowsInPrompt(t *testing.T) {
mem := store.NewMemoryStore()
a := New(Name("planner"), Prompt("base prompt"), WithStore(mem)).(*agentImpl)
if got := a.buildPrompt(); got != "base prompt" {
t.Errorf("buildPrompt() with no plan = %q, want %q", got, "base prompt")
}
a.handlePlan(map[string]any{"steps": []any{map[string]any{"task": "do it", "status": "pending"}}})
got := a.buildPrompt()
if got == "base prompt" {
t.Error("buildPrompt() should include the plan once one is saved")
}
if !containsStr(got, "do it") {
t.Errorf("buildPrompt() = %q, should contain the saved plan", got)
}
}
func TestDiscoverToolsIncludesBuiltins(t *testing.T) {
reg := registry.NewMemoryRegistry()
a := New(Name("a"), WithRegistry(reg), WithStore(store.NewMemoryStore())).(*agentImpl)
a.setup()
tools, err := a.discoverTools()
if err != nil {
t.Fatalf("discoverTools: %v", err)
}
// No services registered, so the only tools should be the builtins.
if len(tools) != len(builtinTools()) {
t.Fatalf("discoverTools() = %d tools, want %d builtins", len(tools), len(builtinTools()))
}
}
func TestEphemeralAgentHasNoBuiltins(t *testing.T) {
reg := registry.NewMemoryRegistry()
a := New(Name("a.sub"), WithRegistry(reg), WithStore(store.NewMemoryStore())).(*agentImpl)
a.ephemeral = true
a.setup()
tools, err := a.discoverTools()
if err != nil {
t.Fatalf("discoverTools: %v", err)
}
if len(tools) != 0 {
t.Errorf("ephemeral agent discoverTools() = %d tools, want 0", len(tools))
}
}
func TestBuiltinsAccessor(t *testing.T) {
mem := store.NewMemoryStore()
tools, handle := Builtins(
Name("chat"),
WithStore(mem),
WithRegistry(registry.NewMemoryRegistry()),
)
if len(tools) != 2 {
t.Fatalf("Builtins() returned %d tools, want 2", len(tools))
}
// A name that isn't a built-in falls through (ok == false).
if _, _, ok := handle("not_a_builtin", nil); ok {
t.Error("handle(non-builtin) ok = true, want false")
}
// plan is handled and persisted under the configured name.
_, content, ok := handle(toolPlan, map[string]any{
"steps": []any{map[string]any{"task": "x", "status": "pending"}},
})
if !ok {
t.Fatal("handle(plan) ok = false, want true")
}
if content == "" {
t.Fatal("handle(plan) returned empty content")
}
if recs, err := mem.Read("agent/chat/plan"); err != nil || len(recs) == 0 {
t.Errorf("plan not persisted under agent/chat/plan: err=%v recs=%d", err, len(recs))
}
}
func TestIsAgent(t *testing.T) {
reg := registry.NewMemoryRegistry()
// A plain service.
if err := reg.Register(&registry.Service{
Name: "task",
Nodes: []*registry.Node{{Id: "task-1", Address: "127.0.0.1:0"}},
}); err != nil {
t.Fatalf("register service: %v", err)
}
// An agent (advertises type=agent).
if err := reg.Register(&registry.Service{
Name: "task-mgr",
Metadata: map[string]string{"type": "agent"},
Nodes: []*registry.Node{{Id: "task-mgr-1", Address: "127.0.0.1:0"}},
}); err != nil {
t.Fatalf("register agent: %v", err)
}
a := New(Name("root"), WithRegistry(reg)).(*agentImpl)
if a.isAgent("task") {
t.Error("isAgent(task) = true, want false (plain service)")
}
if !a.isAgent("task-mgr") {
t.Error("isAgent(task-mgr) = false, want true (agent)")
}
if a.isAgent("nonexistent") {
t.Error("isAgent(nonexistent) = true, want false")
}
}
+84
View File
@@ -0,0 +1,84 @@
package agent
import (
"strings"
"testing"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
)
// MaxSteps refuses tool calls once the per-Ask limit is exceeded; plan
// is bookkeeping and is never counted.
func TestMaxStepsStopsActions(t *testing.T) {
a := newTestAgent(Name("limited"), MaxSteps(2))
h := a.toolHandler()
// plan must not consume a step.
a.steps = 0
h(toolPlan, map[string]any{"steps": []any{}})
if a.steps != 0 {
t.Fatalf("plan consumed a step: steps=%d", a.steps)
}
// First two actions are allowed (they fall through to RPC, which
// fails harmlessly — we only care they weren't refused by the limit).
for i := 1; i <= 2; i++ {
_, content := h("demo_Svc_Do", map[string]any{})
if strings.Contains(content, "step limit") {
t.Fatalf("action %d wrongly hit the step limit", i)
}
}
// Third action exceeds MaxSteps(2) and must be refused.
_, content := h("demo_Svc_Do", map[string]any{})
if !strings.Contains(content, "step limit") {
t.Errorf("third action should hit the step limit; got %q", content)
}
}
// ApproveTool blocks an action when the hook denies it, and the denial
// reason is surfaced to the model.
func TestApproveToolBlocks(t *testing.T) {
var sawTool string
a := newTestAgent(Name("gated"),
ApproveTool(func(tool string, input map[string]any) (bool, string) {
sawTool = tool
return false, "needs sign-off"
}),
)
_, content := a.toolHandler()("demo_Svc_Do", map[string]any{})
if sawTool != "demo_Svc_Do" {
t.Errorf("approver saw %q, want demo_Svc_Do", sawTool)
}
if !strings.Contains(content, "not approved") || !strings.Contains(content, "needs sign-off") {
t.Errorf("blocked call should surface the reason; got %q", content)
}
}
// A denying approver must not gate the internal plan tool.
func TestApproveToolDoesNotGatePlan(t *testing.T) {
mem := store.NewMemoryStore()
a := New(
Name("gated"),
Provider("fake"),
WithRegistry(registry.NewMemoryRegistry()),
WithStore(mem),
ApproveTool(func(tool string, input map[string]any) (bool, string) {
return false, "deny everything"
}),
).(*agentImpl)
a.setup()
_, content := a.toolHandler()(toolPlan, map[string]any{
"steps": []any{map[string]any{"task": "x", "status": "pending"}},
})
if strings.Contains(content, "not approved") {
t.Errorf("plan must not be gated by ApproveTool; got %q", content)
}
if recs, _ := mem.Read("agent/gated/plan"); len(recs) == 0 {
t.Error("plan should have been persisted despite the denying approver")
}
}
+178
View File
@@ -0,0 +1,178 @@
package agent
import (
"context"
"strings"
"testing"
"go-micro.dev/v5/ai"
"go-micro.dev/v5/client"
codecBytes "go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
)
// fakeGen drives the fake provider's Generate. Tests set it and reset
// it with a deferred cleanup. Tests in this package are not parallel,
// so a package-level hook is safe.
var fakeGen func(opts ai.Options, req *ai.Request) (*ai.Response, error)
type fakeModel struct{ opts ai.Options }
func (m *fakeModel) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *fakeModel) Options() ai.Options { return m.opts }
func (m *fakeModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
if fakeGen != nil {
return fakeGen(m.opts, req)
}
return &ai.Response{Reply: "ok"}, nil
}
func (m *fakeModel) Stream(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (ai.Stream, error) {
return nil, nil
}
func (m *fakeModel) String() string { return "fake" }
func init() {
ai.Register("fake", func(opts ...ai.Option) ai.Model {
m := &fakeModel{}
_ = m.Init(opts...)
return m
})
}
// fakeClient embeds the default client (so NewRequest works) and
// overrides Call with a test-supplied function.
type fakeClient struct {
client.Client
callFn func(ctx context.Context, req client.Request, rsp interface{}) error
}
func (c *fakeClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
return c.callFn(ctx, req, rsp)
}
func newTestAgent(opts ...Option) *agentImpl {
base := []Option{
Provider("fake"),
WithRegistry(registry.NewMemoryRegistry()),
WithStore(store.NewMemoryStore()),
}
a := New(append(base, opts...)...).(*agentImpl)
a.setup()
return a
}
// The model is offered the plan and delegate tools, and calling the
// plan tool persists the plan to memory.
func TestAskExposesAndRunsPlan(t *testing.T) {
var sawPlan, sawDelegate bool
fakeGen = func(opts ai.Options, req *ai.Request) (*ai.Response, error) {
for _, tl := range req.Tools {
switch tl.Name {
case toolPlan:
sawPlan = true
case toolDelegate:
sawDelegate = true
}
}
// Simulate the model recording a plan.
if opts.ToolHandler != nil {
opts.ToolHandler(toolPlan, map[string]any{
"steps": []any{map[string]any{"task": "step one", "status": "pending"}},
})
}
return &ai.Response{Answer: "done"}, nil
}
defer func() { fakeGen = nil }()
a := newTestAgent(Name("worker"))
resp, err := a.Ask(context.Background(), "do some multi-step work")
if err != nil {
t.Fatalf("Ask: %v", err)
}
if !sawPlan || !sawDelegate {
t.Errorf("model should be offered plan and delegate tools: plan=%v delegate=%v", sawPlan, sawDelegate)
}
if resp.Reply == "" {
t.Error("Ask returned empty reply")
}
if plan := a.loadPlan(); !strings.Contains(plan, "step one") {
t.Errorf("plan tool result not persisted; loadPlan() = %q", plan)
}
}
// Delegating with no matching agent creates an ephemeral sub-agent with
// a fresh, isolated context (no builtin tools) and returns its reply.
func TestDelegateEphemeral(t *testing.T) {
fakeGen = func(opts ai.Options, req *ai.Request) (*ai.Response, error) {
if strings.Contains(req.SystemPrompt, "sub-agent") {
for _, tl := range req.Tools {
if tl.Name == toolPlan || tl.Name == toolDelegate {
t.Errorf("ephemeral sub-agent must not have builtin tool %q", tl.Name)
}
}
return &ai.Response{Reply: "subtask complete"}, nil
}
return &ai.Response{Reply: "parent"}, nil
}
defer func() { fakeGen = nil }()
a := newTestAgent(Name("root"))
_, content := a.handleDelegate(map[string]any{"task": "summarize the report"})
if !strings.Contains(content, "subtask complete") {
t.Errorf("delegate should return the sub-agent's reply; got %q", content)
}
}
// Delegating to a name that resolves to a registered agent goes over
// RPC to that agent rather than spawning a sub-agent.
func TestDelegateToRegisteredAgent(t *testing.T) {
reg := registry.NewMemoryRegistry()
if err := reg.Register(&registry.Service{
Name: "comms",
Metadata: map[string]string{"type": "agent"},
Nodes: []*registry.Node{{Id: "comms-1", Address: "127.0.0.1:0"}},
}); err != nil {
t.Fatalf("register agent: %v", err)
}
var calledService, calledEndpoint string
fc := &fakeClient{Client: client.DefaultClient}
fc.callFn = func(ctx context.Context, req client.Request, rsp interface{}) error {
calledService, calledEndpoint = req.Service(), req.Endpoint()
frame := rsp.(*codecBytes.Frame)
frame.Data = []byte(`{"reply":"notified alice","agent":"comms"}`)
return nil
}
// fakeGen guards against the ephemeral path being taken by mistake.
fakeGen = func(opts ai.Options, req *ai.Request) (*ai.Response, error) {
t.Error("delegate to a registered agent must not spawn a sub-agent")
return &ai.Response{}, nil
}
defer func() { fakeGen = nil }()
a := newTestAgent(Name("root"), WithRegistry(reg), WithClient(fc))
_, content := a.handleDelegate(map[string]any{"task": "notify alice", "to": "comms"})
if calledService != "comms" || calledEndpoint != "Agent.Chat" {
t.Errorf("expected RPC to comms Agent.Chat, got %s %s", calledService, calledEndpoint)
}
if !strings.Contains(content, "notified alice") {
t.Errorf("delegate-first result missing agent reply; got %q", content)
}
}
// Delegate requires a task.
func TestDelegateRequiresTask(t *testing.T) {
a := newTestAgent(Name("root"))
_, content := a.handleDelegate(map[string]any{})
if !strings.Contains(content, "error") {
t.Errorf("delegate with no task should error; got %q", content)
}
}
+114
View File
@@ -0,0 +1,114 @@
package agent
import (
"go-micro.dev/v5/client"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
)
// Option configures an Agent.
type Option func(*Options)
// ApproveFunc decides whether an agent may execute a tool call before it
// runs. Returning false blocks the call; the reason is shown to the
// model so it can adapt. Use it for human-in-the-loop approval or policy
// checks. It is called for actions (service tools and delegate), not for
// the internal plan tool.
type ApproveFunc func(tool string, input map[string]any) (approved bool, reason string)
// Options holds agent configuration.
type Options struct {
Name string
Services []string
Prompt string
Provider string
Model string
APIKey string
Registry registry.Registry
Client client.Client
Store store.Store
HistoryLimit int
// MaxSteps bounds the number of tool executions per Ask (0 =
// unbounded). Once exceeded, further tool calls are refused and the
// model is told to stop and summarize. A stopping condition.
MaxSteps int
// Approve gates each action before it runs. Nil = allow all.
Approve ApproveFunc
}
func newOptions(opts ...Option) Options {
o := Options{
Registry: registry.DefaultRegistry,
Client: client.DefaultClient,
Store: store.DefaultStore,
HistoryLimit: 50,
}
for _, opt := range opts {
opt(&o)
}
return o
}
// Name sets the agent name.
func Name(n string) Option {
return func(o *Options) { o.Name = n }
}
// Services sets which services this agent manages.
func Services(names ...string) Option {
return func(o *Options) { o.Services = names }
}
// Prompt sets the system prompt.
func Prompt(p string) Option {
return func(o *Options) { o.Prompt = p }
}
// Provider sets the LLM provider.
func Provider(p string) Option {
return func(o *Options) { o.Provider = p }
}
// Model sets the LLM model name.
func Model(m string) Option {
return func(o *Options) { o.Model = m }
}
// APIKey sets the API key for the LLM provider.
func APIKey(k string) Option {
return func(o *Options) { o.APIKey = k }
}
// WithRegistry sets the service registry.
func WithRegistry(r registry.Registry) Option {
return func(o *Options) { o.Registry = r }
}
// WithClient sets the RPC client.
func WithClient(c client.Client) Option {
return func(o *Options) { o.Client = c }
}
// WithStore sets the store for agent memory.
func WithStore(s store.Store) Option {
return func(o *Options) { o.Store = s }
}
// HistoryLimit sets the max conversation messages to retain.
func HistoryLimit(n int) Option {
return func(o *Options) { o.HistoryLimit = n }
}
// MaxSteps bounds tool executions per Ask (0 = unbounded). A stopping
// condition: beyond the limit, tool calls are refused and the model is
// told to stop and summarize.
func MaxSteps(n int) Option {
return func(o *Options) { o.MaxSteps = n }
}
// ApproveTool sets a human-in-the-loop / policy hook called before each
// action (service tools and delegate). Returning false blocks the call.
func ApproveTool(fn ApproveFunc) Option {
return func(o *Options) { o.Approve = fn }
}
+267
View File
@@ -0,0 +1,267 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v3.21.12
// source: proto/agent.proto
package agent
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ChatRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ChatRequest) Reset() {
*x = ChatRequest{}
mi := &file_proto_agent_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ChatRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ChatRequest) ProtoMessage() {}
func (x *ChatRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_agent_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ChatRequest.ProtoReflect.Descriptor instead.
func (*ChatRequest) Descriptor() ([]byte, []int) {
return file_proto_agent_proto_rawDescGZIP(), []int{0}
}
func (x *ChatRequest) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type ChatResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Reply string `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"`
Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"`
ToolCalls []*ToolCall `protobuf:"bytes,3,rep,name=tool_calls,json=toolCalls,proto3" json:"tool_calls,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ChatResponse) Reset() {
*x = ChatResponse{}
mi := &file_proto_agent_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ChatResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ChatResponse) ProtoMessage() {}
func (x *ChatResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_agent_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ChatResponse.ProtoReflect.Descriptor instead.
func (*ChatResponse) Descriptor() ([]byte, []int) {
return file_proto_agent_proto_rawDescGZIP(), []int{1}
}
func (x *ChatResponse) GetReply() string {
if x != nil {
return x.Reply
}
return ""
}
func (x *ChatResponse) GetAgent() string {
if x != nil {
return x.Agent
}
return ""
}
func (x *ChatResponse) GetToolCalls() []*ToolCall {
if x != nil {
return x.ToolCalls
}
return nil
}
type ToolCall struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"`
Result string `protobuf:"bytes,4,opt,name=result,proto3" json:"result,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ToolCall) Reset() {
*x = ToolCall{}
mi := &file_proto_agent_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ToolCall) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ToolCall) ProtoMessage() {}
func (x *ToolCall) ProtoReflect() protoreflect.Message {
mi := &file_proto_agent_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ToolCall.ProtoReflect.Descriptor instead.
func (*ToolCall) Descriptor() ([]byte, []int) {
return file_proto_agent_proto_rawDescGZIP(), []int{2}
}
func (x *ToolCall) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *ToolCall) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ToolCall) GetInput() string {
if x != nil {
return x.Input
}
return ""
}
func (x *ToolCall) GetResult() string {
if x != nil {
return x.Result
}
return ""
}
var File_proto_agent_proto protoreflect.FileDescriptor
const file_proto_agent_proto_rawDesc = "" +
"\n" +
"\x11proto/agent.proto\x12\x05agent\"'\n" +
"\vChatRequest\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\"j\n" +
"\fChatResponse\x12\x14\n" +
"\x05reply\x18\x01 \x01(\tR\x05reply\x12\x14\n" +
"\x05agent\x18\x02 \x01(\tR\x05agent\x12.\n" +
"\n" +
"tool_calls\x18\x03 \x03(\v2\x0f.agent.ToolCallR\ttoolCalls\"\\\n" +
"\bToolCall\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
"\x05input\x18\x03 \x01(\tR\x05input\x12\x16\n" +
"\x06result\x18\x04 \x01(\tR\x06result2:\n" +
"\x05Agent\x121\n" +
"\x04Chat\x12\x12.agent.ChatRequest\x1a\x13.agent.ChatResponse\"\x00B\x0fZ\r./proto;agentb\x06proto3"
var (
file_proto_agent_proto_rawDescOnce sync.Once
file_proto_agent_proto_rawDescData []byte
)
func file_proto_agent_proto_rawDescGZIP() []byte {
file_proto_agent_proto_rawDescOnce.Do(func() {
file_proto_agent_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_agent_proto_rawDesc), len(file_proto_agent_proto_rawDesc)))
})
return file_proto_agent_proto_rawDescData
}
var file_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_proto_agent_proto_goTypes = []any{
(*ChatRequest)(nil), // 0: agent.ChatRequest
(*ChatResponse)(nil), // 1: agent.ChatResponse
(*ToolCall)(nil), // 2: agent.ToolCall
}
var file_proto_agent_proto_depIdxs = []int32{
2, // 0: agent.ChatResponse.tool_calls:type_name -> agent.ToolCall
0, // 1: agent.Agent.Chat:input_type -> agent.ChatRequest
1, // 2: agent.Agent.Chat:output_type -> agent.ChatResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_proto_agent_proto_init() }
func file_proto_agent_proto_init() {
if File_proto_agent_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_agent_proto_rawDesc), len(file_proto_agent_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_agent_proto_goTypes,
DependencyIndexes: file_proto_agent_proto_depIdxs,
MessageInfos: file_proto_agent_proto_msgTypes,
}.Build()
File_proto_agent_proto = out.File
file_proto_agent_proto_goTypes = nil
file_proto_agent_proto_depIdxs = nil
}
+79
View File
@@ -0,0 +1,79 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/agent.proto
package agent
import (
fmt "fmt"
proto "google.golang.org/protobuf/proto"
math "math"
)
import (
context "context"
client "go-micro.dev/v5/client"
server "go-micro.dev/v5/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ client.Option
var _ server.Option
// Client API for Agent service
type AgentService interface {
Chat(ctx context.Context, in *ChatRequest, opts ...client.CallOption) (*ChatResponse, error)
}
type agentService struct {
c client.Client
name string
}
func NewAgentService(name string, c client.Client) AgentService {
return &agentService{
c: c,
name: name,
}
}
func (c *agentService) Chat(ctx context.Context, in *ChatRequest, opts ...client.CallOption) (*ChatResponse, error) {
req := c.c.NewRequest(c.name, "Agent.Chat", in)
out := new(ChatResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Agent service
type AgentHandler interface {
Chat(context.Context, *ChatRequest, *ChatResponse) error
}
func RegisterAgentHandler(s server.Server, hdlr AgentHandler, opts ...server.HandlerOption) error {
type agent interface {
Chat(ctx context.Context, in *ChatRequest, out *ChatResponse) error
}
type Agent struct {
agent
}
h := &agentHandler{hdlr}
return s.Handle(s.NewHandler(&Agent{h}, opts...))
}
type agentHandler struct {
AgentHandler
}
func (h *agentHandler) Chat(ctx context.Context, in *ChatRequest, out *ChatResponse) error {
return h.AgentHandler.Chat(ctx, in, out)
}
+27
View File
@@ -0,0 +1,27 @@
syntax = "proto3";
package agent;
option go_package = "./proto;agent";
// Agent is the RPC interface for an AI agent.
service Agent {
rpc Chat(ChatRequest) returns (ChatResponse) {}
}
message ChatRequest {
string message = 1;
}
message ChatResponse {
string reply = 1;
string agent = 2;
repeated ToolCall tool_calls = 3;
}
message ToolCall {
string id = 1;
string name = 2;
string input = 3;
string result = 4;
}
+350
View File
@@ -0,0 +1,350 @@
# AI Package
The `ai` package provides simple, high-level interfaces for AI model providers. It supports text generation (`Model`), image generation (`ImageModel`), and video generation (`VideoModel`).
## Interfaces
### Text Generation (Model)
The Model interface follows the same patterns as other go-micro packages (Registry, Client, Broker):
```go
type Model interface {
Init(...Option) error
Options() Options
Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error)
Stream(ctx context.Context, req *Request, opts ...GenerateOption) (Stream, error)
String() string
}
```
## Quick Start
```go
import (
"context"
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/openai"
)
// Create a model
m := ai.New("openai",
ai.WithAPIKey("your-api-key"),
ai.WithModel("gpt-4o"),
)
// Generate a response
req := &ai.Request{
Prompt: "What is Go?",
SystemPrompt: "You are a helpful programming assistant",
}
resp, err := m.Generate(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Reply)
```
### Image Generation (ImageModel)
```go
type ImageModel interface {
GenerateImage(ctx context.Context, req *ImageRequest, opts ...GenerateOption) (*ImageResponse, error)
String() string
}
```
```go
import (
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/atlascloud"
)
ig := ai.NewImage("atlascloud",
ai.WithAPIKey("your-api-key"),
)
resp, err := ig.GenerateImage(context.Background(), &ai.ImageRequest{
Prompt: "A Go gopher in space",
Size: "1024x1024",
})
fmt.Println(resp.Images[0].URL)
```
Providers that support image generation: **Atlas Cloud**, **OpenAI**.
### Video Generation (VideoModel)
```go
type VideoModel interface {
GenerateVideo(ctx context.Context, req *VideoRequest, opts ...GenerateOption) (*VideoResponse, error)
String() string
}
```
```go
import (
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/atlascloud"
)
vg := ai.NewVideo("atlascloud",
ai.WithAPIKey("your-api-key"),
)
resp, err := vg.GenerateVideo(context.Background(), &ai.VideoRequest{
Prompt: "Microservices nodes animating with data flowing between them",
Images: []string{"https://example.com/diagram.png"}, // optional: image-to-video
Duration: 6,
})
fmt.Println(resp.URL)
```
Providers that support video generation: **Atlas Cloud**.
## Options
Configure the model using functional options:
```go
m := ai.New("anthropic",
ai.WithAPIKey("your-key"), // Required
ai.WithModel("claude-sonnet-4-20250514"), // Optional, uses provider default
ai.WithBaseURL("https://api.anthropic.com"), // Optional, uses provider default
)
```
You can also update options after creation:
```go
m.Init(
ai.WithModel("gpt-4o-mini"),
ai.WithAPIKey("new-key"),
)
```
## Using Tools
The model can automatically execute tool calls when provided with a tool handler:
```go
// Define a tool handler
toolHandler := func(name string, input map[string]any) (result any, content string) {
// Execute the tool and return results
switch name {
case "get_weather":
return map[string]string{"temp": "72F"}, `{"temp": "72F"}`
default:
return nil, `{"error": "unknown tool"}`
}
}
// Create model with tool handler
m := ai.New("openai",
ai.WithAPIKey("your-key"),
ai.WithToolHandler(toolHandler),
)
// Provide tools in the request
req := &ai.Request{
Prompt: "What's the weather?",
SystemPrompt: "You are a helpful assistant",
Tools: []ai.Tool{
{
Name: "get_weather",
Description: "Get current weather",
Properties: map[string]any{
"location": map[string]any{
"type": "string",
"description": "City name",
},
},
},
},
}
// Generate will automatically call tools and return final answer
resp, err := m.Generate(context.Background(), req)
fmt.Println(resp.Answer) // Final answer after tool execution
```
## Response Structure
```go
type Response struct {
Reply string // Initial reply from model
ToolCalls []ToolCall // Tools the model wants to call
Answer string // Final answer (after tool execution if handler provided)
}
```
- `Reply`: The model's first response
- `ToolCalls`: List of tools the model requested (if any)
- `Answer`: The final answer after tools are executed (only set if ToolHandler is provided)
## Supported Providers
### Anthropic Claude
```go
m := ai.New("anthropic",
ai.WithAPIKey("sk-ant-..."),
ai.WithModel("claude-sonnet-4-20250514"), // default
)
```
Default model: `claude-sonnet-4-20250514`
Default base URL: `https://api.anthropic.com`
### OpenAI GPT
```go
m := ai.New("openai",
ai.WithAPIKey("sk-..."),
ai.WithModel("gpt-4o"), // default
)
```
Default model: `gpt-4o`
Default base URL: `https://api.openai.com`
### Google Gemini
```go
m := ai.New("gemini",
ai.WithAPIKey("your-key"),
ai.WithModel("gemini-2.5-flash"), // default
)
```
Default model: `gemini-2.5-flash`
Default base URL: `https://generativelanguage.googleapis.com`
Google Gemini uses its own API format with `system_instruction`, `contents` (not `messages`), and `functionDeclarations` for tool calling. The provider handles the translation automatically.
### Groq
```go
m := ai.New("groq",
ai.WithAPIKey("your-key"),
ai.WithModel("llama-3.3-70b-versatile"), // default
)
```
Default model: `llama-3.3-70b-versatile`
Default base URL: `https://api.groq.com/openai`
Groq provides ultra-fast inference for open-weight models via an OpenAI-compatible endpoint.
### Mistral
```go
m := ai.New("mistral",
ai.WithAPIKey("your-key"),
ai.WithModel("mistral-large-latest"), // default
)
```
Default model: `mistral-large-latest`
Default base URL: `https://api.mistral.ai`
Mistral AI is a European AI company offering high-performance models via an OpenAI-compatible endpoint.
### Together AI
```go
m := ai.New("together",
ai.WithAPIKey("your-key"),
ai.WithModel("meta-llama/Llama-3.3-70B-Instruct-Turbo"), // default
)
```
Default model: `meta-llama/Llama-3.3-70B-Instruct-Turbo`
Default base URL: `https://api.together.xyz`
Together AI provides fast inference for open-weight models via an OpenAI-compatible endpoint.
### Atlas Cloud
```go
m := ai.New("atlascloud",
ai.WithAPIKey("your-key"),
ai.WithModel("llama-3.3-70b"), // default
)
```
Default model: `llama-3.3-70b`
Default base URL: `https://api.atlascloud.ai`
Atlas Cloud is an enterprise AI infrastructure platform offering high-performance LLM APIs. It exposes an OpenAI-compatible chat completions endpoint with tool calling support.
## Auto-Detection
Use `AutoDetectProvider()` to detect the provider from a base URL:
```go
provider := ai.AutoDetectProvider("https://api.anthropic.com")
// Returns "anthropic"
m := ai.New(provider, ai.WithAPIKey("..."))
```
## Adding a New Provider
See the full **[AI Provider Integration Guide](../internal/website/docs/guides/ai-provider-guide.md)** for a step-by-step walkthrough, checklist, and design notes.
Quick summary:
1. Create `ai/yourprovider/yourprovider.go` implementing `ai.Model`.
2. Call `ai.Register("yourprovider", ...)` in `init()`.
3. Add tests in `ai/yourprovider/yourprovider_test.go`.
4. Users enable the provider with a blank import:
```go
import _ "go-micro.dev/v5/ai/yourprovider"
```
We welcome contributions and sponsorships from AI infrastructure companies — see the guide for details.
## Comparison with Other Packages
The ai package follows the same patterns as other go-micro packages:
**Registry:**
```go
r := registry.NewRegistry(registry.Addrs("..."))
r.Register(service)
```
**Client:**
```go
c := client.NewClient(client.Retries(3))
c.Call(ctx, req, rsp)
```
**AI:**
```go
m := ai.New("openai", ai.WithAPIKey("..."))
m.Generate(ctx, req)
```
All use:
- `Init()` to update options
- `Options()` to get current options
- `String()` to get the implementation name
- Functional options pattern
## Testing
```bash
go test ./ai/...
```
## Examples
See the [server implementation](../cmd/micro/server/server.go) for a complete example of using the ai package with tool execution.
+272
View File
@@ -0,0 +1,272 @@
// Package anthropic implements the Anthropic Claude model provider
package anthropic
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("anthropic", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for Anthropic Claude
type Provider struct {
opts ai.Options
}
// NewProvider creates a new Anthropic provider
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
// Set defaults if not provided
if options.Model == "" {
options.Model = "claude-sonnet-4-20250514"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.anthropic.com"
}
return &Provider{
opts: options,
}
}
// Init initializes the provider with options
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
// Options returns the provider options
func (p *Provider) Options() ai.Options {
return p.opts
}
// String returns the provider name
func (p *Provider) String() string {
return "anthropic"
}
// Generate generates a response from the model
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
// Build tools for Anthropic format
var anthropicTools []map[string]any
for _, t := range req.Tools {
anthropicTools = append(anthropicTools, map[string]any{
"name": t.Name,
"description": t.Description,
"input_schema": map[string]any{
"type": "object",
"properties": t.Properties,
},
})
}
// Build initial request
apiReq := map[string]any{
"model": p.opts.Model,
"max_tokens": 8192,
"system": req.SystemPrompt,
"messages": []map[string]any{
{"role": "user", "content": req.Prompt},
},
}
if len(anthropicTools) > 0 {
apiReq["tools"] = anthropicTools
}
// Make API call
resp, rawContent, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
// If no tool calls or no handler, return as-is
if len(resp.ToolCalls) == 0 || p.opts.ToolHandler == nil {
return resp, nil
}
// Tool execution loop: execute tools, send results back, repeat
// until the model responds with text only (no more tool calls)
messages := []map[string]any{
{"role": "user", "content": req.Prompt},
{"role": "assistant", "content": cleanContent(rawContent)},
}
pendingCalls := resp.ToolCalls
for rounds := 0; rounds < 10; rounds++ {
var toolResultBlocks []map[string]any
for i := range pendingCalls {
_, content := p.opts.ToolHandler(pendingCalls[i].Name, pendingCalls[i].Input)
pendingCalls[i].Result = content
toolResultBlocks = append(toolResultBlocks, map[string]any{
"type": "tool_result",
"tool_use_id": pendingCalls[i].ID,
"content": content,
})
}
messages = append(messages, map[string]any{
"role": "user",
"content": toolResultBlocks,
})
followUpReq := map[string]any{
"model": p.opts.Model,
"max_tokens": 8192,
"system": req.SystemPrompt,
"messages": messages,
}
if len(anthropicTools) > 0 {
followUpReq["tools"] = anthropicTools
}
followUpResp, followUpRaw, err := p.callAPI(ctx, followUpReq)
if err != nil {
break
}
if len(followUpResp.ToolCalls) > 0 {
resp.ToolCalls = append(resp.ToolCalls, followUpResp.ToolCalls...)
pendingCalls = followUpResp.ToolCalls
messages = append(messages, map[string]any{
"role": "assistant",
"content": cleanContent(followUpRaw),
})
continue
}
if followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
break
}
return resp, nil
}
// Stream generates a streaming response (not yet implemented)
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for anthropic provider")
}
// callAPI makes an HTTP request to the Anthropic API
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, any, error) {
// Marshal request
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Build HTTP request
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/messages"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("x-api-key", p.opts.APIKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
// Make request
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
// Read response
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
// Parse response
var anthropicResp struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
} `json:"content"`
StopReason string `json:"stop_reason"`
}
if err := json.Unmarshal(respBody, &anthropicResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
response := &ai.Response{}
// Extract text reply
var replyParts []string
for _, block := range anthropicResp.Content {
if block.Type == "text" && block.Text != "" {
replyParts = append(replyParts, block.Text)
}
}
if len(replyParts) > 0 {
response.Reply = strings.Join(replyParts, "\n")
}
// Extract tool calls
for _, block := range anthropicResp.Content {
if block.Type == "tool_use" {
var input map[string]any
if err := json.Unmarshal(block.Input, &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: block.ID,
Name: block.Name,
Input: input,
})
}
}
return response, anthropicResp.Content, nil
}
// cleanContent strips fields from response content blocks that Anthropic
// rejects when sent back as assistant message content (e.g. "id" on text blocks).
func cleanContent(raw any) any {
blocks, ok := raw.([]struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
})
if !ok {
return raw
}
var cleaned []map[string]any
for _, b := range blocks {
switch b.Type {
case "text":
cleaned = append(cleaned, map[string]any{"type": "text", "text": b.Text})
case "tool_use":
var input any
json.Unmarshal(b.Input, &input)
cleaned = append(cleaned, map[string]any{"type": "tool_use", "id": b.ID, "name": b.Name, "input": input})
}
}
return cleaned
}
+94
View File
@@ -0,0 +1,94 @@
package anthropic
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "anthropic" {
t.Errorf("Expected provider name 'anthropic', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("test-model"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "test-model" {
t.Errorf("Expected model 'test-model', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "claude-sonnet-4-20250514" {
t.Errorf("Expected default model 'claude-sonnet-4-20250514', got '%s'", opts.Model)
}
if opts.BaseURL != "https://api.anthropic.com" {
t.Errorf("Expected default base URL 'https://api.anthropic.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
+489
View File
@@ -0,0 +1,489 @@
// Package atlascloud implements the Atlas Cloud model provider.
//
// Atlas Cloud is an enterprise AI infrastructure platform offering
// high-performance LLM, image, and video APIs. It exposes
// OpenAI-compatible endpoints for chat completions and image
// generation.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/atlascloud"
//
// m := ai.New("atlascloud",
// ai.WithAPIKey("your-api-key"),
// )
//
// // Image generation
// ig := ai.NewImage("atlascloud",
// ai.WithAPIKey("your-api-key"),
// )
package atlascloud
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("atlascloud", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
ai.RegisterImage("atlascloud", func(opts ...ai.Option) ai.ImageModel {
return NewProvider(opts...)
})
ai.RegisterVideo("atlascloud", func(opts ...ai.Option) ai.VideoModel {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for Atlas Cloud.
type Provider struct {
opts ai.Options
}
// NewProvider creates a new Atlas Cloud provider.
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "deepseek-ai/DeepSeek-V3-0324"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.atlascloud.ai"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "atlascloud" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(tools) > 0 {
apiReq["tools"] = tools
}
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpReq := map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
}
followUpResp, _, err := p.callAPI(ctx, followUpReq)
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for atlascloud provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{
Reply: choice.Message.Content,
}
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
const defaultImageModel = "openai/gpt-image-2/text-to-image"
// GenerateImage creates an image using Atlas Cloud's async image API.
// It submits the job and polls until completion or context cancellation.
func (p *Provider) GenerateImage(ctx context.Context, req *ai.ImageRequest, opts ...ai.GenerateOption) (*ai.ImageResponse, error) {
model := req.Model
if model == "" {
model = defaultImageModel
}
quality := req.Quality
if quality == "" {
quality = "medium"
}
outputFmt := req.OutputFormat
if outputFmt == "" {
outputFmt = "png"
}
size := req.Size
if size == "" {
size = "1024x1024"
}
apiReq := map[string]any{
"model": model,
"prompt": req.Prompt,
"quality": quality,
"output_format": outputFmt,
"size": size,
"enable_sync_mode": false,
"enable_base64_output": false,
"moderation": "low",
}
reqBody, err := json.Marshal(apiReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/api/v1/model/generateImage"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var submitResp struct {
Code int `json:"code"`
Msg string `json:"message"`
Data struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &submitResp); err != nil {
return nil, fmt.Errorf("failed to parse submit response: %w", err)
}
if submitResp.Code != 200 {
return nil, fmt.Errorf("API error: %s", submitResp.Msg)
}
predictionID := submitResp.Data.ID
pollURL := strings.TrimRight(p.opts.BaseURL, "/") + "/api/v1/model/prediction/" + predictionID
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
result, err := p.pollPrediction(ctx, pollURL)
if err != nil {
return nil, err
}
if result != nil {
return result, nil
}
}
}
}
func (p *Provider) pollPrediction(ctx context.Context, url string) (*ai.ImageResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("poll request failed: %w", err)
}
defer httpResp.Body.Close()
body, _ := io.ReadAll(httpResp.Body)
var pollResp struct {
Data struct {
Status string `json:"status"`
Outputs []string `json:"outputs"`
Error string `json:"error"`
} `json:"data"`
}
if err := json.Unmarshal(body, &pollResp); err != nil {
return nil, fmt.Errorf("failed to parse poll response: %w", err)
}
switch pollResp.Data.Status {
case "completed":
resp := &ai.ImageResponse{}
for _, output := range pollResp.Data.Outputs {
resp.Images = append(resp.Images, ai.Image{URL: output})
}
return resp, nil
case "failed":
return nil, fmt.Errorf("image generation failed: %s", pollResp.Data.Error)
default:
return nil, nil
}
}
const defaultVideoModel = "google/gemini-omni-flash/image-to-video-developer"
// GenerateVideo creates a video using Atlas Cloud's async video API.
// Supports text-to-video and image-to-video depending on whether
// Images are provided in the request.
func (p *Provider) GenerateVideo(ctx context.Context, req *ai.VideoRequest, opts ...ai.GenerateOption) (*ai.VideoResponse, error) {
model := req.Model
if model == "" {
model = defaultVideoModel
}
duration := req.Duration
if duration <= 0 {
duration = 6
}
aspect := req.AspectRatio
if aspect == "" {
aspect = "16:9"
}
resolution := req.Resolution
if resolution == "" {
resolution = "720p"
}
apiReq := map[string]any{
"model": model,
"prompt": req.Prompt,
"duration": duration,
"aspect_ratio": aspect,
"resolution": resolution,
"seed": -1,
}
if len(req.Images) > 0 {
apiReq["images"] = req.Images
}
reqBody, err := json.Marshal(apiReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/api/v1/model/generateVideo"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var submitResp struct {
Code int `json:"code"`
Msg string `json:"message"`
Data struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &submitResp); err != nil {
return nil, fmt.Errorf("failed to parse submit response: %w", err)
}
if submitResp.Code != 200 {
return nil, fmt.Errorf("API error: %s", submitResp.Msg)
}
pollURL := strings.TrimRight(p.opts.BaseURL, "/") + "/api/v1/model/prediction/" + submitResp.Data.ID
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
result, err := p.pollVideo(ctx, pollURL)
if err != nil {
return nil, err
}
if result != nil {
return result, nil
}
}
}
}
func (p *Provider) pollVideo(ctx context.Context, url string) (*ai.VideoResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("poll request failed: %w", err)
}
defer httpResp.Body.Close()
body, _ := io.ReadAll(httpResp.Body)
var pollResp struct {
Data struct {
Status string `json:"status"`
Outputs []string `json:"outputs"`
Error string `json:"error"`
} `json:"data"`
}
if err := json.Unmarshal(body, &pollResp); err != nil {
return nil, fmt.Errorf("failed to parse poll response: %w", err)
}
switch pollResp.Data.Status {
case "completed", "succeeded":
if len(pollResp.Data.Outputs) == 0 {
return nil, fmt.Errorf("video completed but no outputs returned")
}
return &ai.VideoResponse{URL: pollResp.Data.Outputs[0]}, nil
case "failed":
return nil, fmt.Errorf("video generation failed: %s", pollResp.Data.Error)
default:
return nil, nil
}
}
+148
View File
@@ -0,0 +1,148 @@
package atlascloud
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "atlascloud" {
t.Errorf("Expected provider name 'atlascloud', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("test-model"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "test-model" {
t.Errorf("Expected model 'test-model', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "deepseek-ai/DeepSeek-V3-0324" {
t.Errorf("Expected default model 'deepseek-ai/DeepSeek-V3-0324', got '%s'", opts.Model)
}
if opts.BaseURL != "https://api.atlascloud.ai" {
t.Errorf("Expected default base URL 'https://api.atlascloud.ai', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("atlascloud", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("ai.New('atlascloud') returned nil — provider not registered")
}
if m.String() != "atlascloud" {
t.Errorf("Expected 'atlascloud', got '%s'", m.String())
}
}
func TestProvider_ImageRegistration(t *testing.T) {
ig := ai.NewImage("atlascloud", ai.WithAPIKey("test"))
if ig == nil {
t.Fatal("ai.NewImage('atlascloud') returned nil — image provider not registered")
}
if ig.String() != "atlascloud" {
t.Errorf("Expected 'atlascloud', got '%s'", ig.String())
}
}
func TestProvider_GenerateImage_NoAPIKey(t *testing.T) {
p := NewProvider()
_, err := p.GenerateImage(context.Background(), &ai.ImageRequest{Prompt: "a cat"})
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_ImplementsImageModel(t *testing.T) {
var _ ai.ImageModel = (*Provider)(nil)
}
func TestProvider_VideoRegistration(t *testing.T) {
vg := ai.NewVideo("atlascloud", ai.WithAPIKey("test"))
if vg == nil {
t.Fatal("ai.NewVideo('atlascloud') returned nil — video provider not registered")
}
if vg.String() != "atlascloud" {
t.Errorf("Expected 'atlascloud', got '%s'", vg.String())
}
}
func TestProvider_GenerateVideo_NoAPIKey(t *testing.T) {
p := NewProvider()
_, err := p.GenerateVideo(context.Background(), &ai.VideoRequest{Prompt: "a cat"})
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_ImplementsVideoModel(t *testing.T) {
var _ ai.VideoModel = (*Provider)(nil)
}
+22
View File
@@ -0,0 +1,22 @@
// Package flow is maintained for backward compatibility.
// The canonical import is go-micro.dev/v5/flow.
package flow
import "go-micro.dev/v5/flow"
// Re-export types for backward compatibility.
type Flow = flow.Flow
type Options = flow.Options
type Option = flow.Option
type Result = flow.Result
var New = flow.New
var Trigger = flow.Trigger
var Prompt = flow.Prompt
var SystemPrompt = flow.SystemPrompt
var Provider = flow.Provider
var APIKey = flow.APIKey
var Model = flow.Model
var BaseURL = flow.BaseURL
var HistoryLimit = flow.HistoryLimit
var OnResult = flow.OnResult
+226
View File
@@ -0,0 +1,226 @@
// Package gemini implements the Google Gemini model provider.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/gemini"
//
// m := ai.New("gemini",
// ai.WithAPIKey("your-api-key"),
// )
package gemini
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("gemini", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for Google Gemini.
type Provider struct {
opts ai.Options
}
// NewProvider creates a new Gemini provider.
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "gemini-2.5-flash"
}
if options.BaseURL == "" {
options.BaseURL = "https://generativelanguage.googleapis.com"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "gemini" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
})
}
contents := []map[string]any{
{"role": "user", "parts": []map[string]any{{"text": req.Prompt}}},
}
apiReq := map[string]any{
"contents": contents,
}
if req.SystemPrompt != "" {
apiReq["system_instruction"] = map[string]any{
"parts": []map[string]any{{"text": req.SystemPrompt}},
}
}
if len(tools) > 0 {
apiReq["tools"] = []map[string]any{
{"functionDeclarations": tools},
}
}
resp, rawParts, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
var resultParts []map[string]any
for _, tc := range resp.ToolCalls {
result, _ := p.opts.ToolHandler(tc.Name, tc.Input)
resultParts = append(resultParts, map[string]any{
"functionResponse": map[string]any{
"name": tc.Name,
"id": tc.ID,
"response": result,
},
})
}
followUpContents := append(contents,
map[string]any{"role": "model", "parts": rawParts},
map[string]any{"role": "user", "parts": resultParts},
)
followUpReq := map[string]any{
"contents": followUpContents,
}
if req.SystemPrompt != "" {
followUpReq["system_instruction"] = map[string]any{
"parts": []map[string]any{{"text": req.SystemPrompt}},
}
}
followUpResp, _, err := p.callAPI(ctx, followUpReq)
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for gemini provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, []map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") +
"/v1beta/models/" + p.opts.Model + ":generateContent"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("x-goog-api-key", p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var geminiResp struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text"`
FunctionCall *functionCallPB `json:"functionCall"`
} `json:"parts"`
} `json:"content"`
} `json:"candidates"`
}
if err := json.Unmarshal(respBody, &geminiResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(geminiResp.Candidates) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
parts := geminiResp.Candidates[0].Content.Parts
response := &ai.Response{}
var replyParts []string
var rawParts []map[string]any
for _, part := range parts {
if part.Text != "" {
replyParts = append(replyParts, part.Text)
rawParts = append(rawParts, map[string]any{"text": part.Text})
}
if part.FunctionCall != nil {
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: part.FunctionCall.ID,
Name: part.FunctionCall.Name,
Input: part.FunctionCall.Args,
})
rawParts = append(rawParts, map[string]any{
"functionCall": map[string]any{
"id": part.FunctionCall.ID,
"name": part.FunctionCall.Name,
"args": part.FunctionCall.Args,
},
})
}
}
if len(replyParts) > 0 {
response.Reply = strings.Join(replyParts, "\n")
}
return response, rawParts, nil
}
type functionCallPB struct {
ID string `json:"id"`
Name string `json:"name"`
Args map[string]any `json:"args"`
}
+104
View File
@@ -0,0 +1,104 @@
package gemini
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "gemini" {
t.Errorf("Expected provider name 'gemini', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("gemini-2.0-flash"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "gemini-2.0-flash" {
t.Errorf("Expected model 'gemini-2.0-flash', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "gemini-2.5-flash" {
t.Errorf("Expected default model 'gemini-2.5-flash', got '%s'", opts.Model)
}
if opts.BaseURL != "https://generativelanguage.googleapis.com" {
t.Errorf("Expected default base URL 'https://generativelanguage.googleapis.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("gemini", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("ai.New('gemini') returned nil — provider not registered")
}
if m.String() != "gemini" {
t.Errorf("Expected 'gemini', got '%s'", m.String())
}
}
+194
View File
@@ -0,0 +1,194 @@
// Package groq implements the Groq model provider.
//
// Groq provides ultra-fast inference for open-weight models via an
// OpenAI-compatible chat completions endpoint.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/groq"
//
// m := ai.New("groq",
// ai.WithAPIKey("your-api-key"),
// )
package groq
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("groq", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
type Provider struct {
opts ai.Options
}
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "llama-3.3-70b-versatile"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.groq.com/openai"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "groq" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(tools) > 0 {
apiReq["tools"] = tools
}
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpResp, _, err := p.callAPI(ctx, map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
})
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for groq provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{Reply: choice.Message.Content}
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
+56
View File
@@ -0,0 +1,56 @@
package groq
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
if NewProvider().String() != "groq" {
t.Errorf("got %q", NewProvider().String())
}
}
func TestProvider_Defaults(t *testing.T) {
opts := NewProvider().Options()
if opts.Model != "llama-3.3-70b-versatile" {
t.Errorf("default model = %q", opts.Model)
}
if opts.BaseURL != "https://api.groq.com/openai" {
t.Errorf("default base URL = %q", opts.BaseURL)
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
if err := p.Init(ai.WithModel("m"), ai.WithAPIKey("k")); err != nil {
t.Fatal(err)
}
if p.Options().Model != "m" || p.Options().APIKey != "k" {
t.Error("Init did not apply options")
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
if _, err := NewProvider().Generate(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error without API key")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
if _, err := NewProvider().Stream(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("groq", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("provider not registered")
}
if m.String() != "groq" {
t.Errorf("got %q", m.String())
}
}
+45
View File
@@ -0,0 +1,45 @@
package ai
// History is a convenience for accumulating conversation messages
// with automatic truncation. Use it to build Request.Messages for
// multi-turn conversations.
//
// hist := ai.NewHistory(50)
// hist.Add("user", "hello")
// resp, _ := m.Generate(ctx, &ai.Request{Messages: hist.Messages(), Prompt: "next"})
// hist.Add("assistant", resp.Reply)
type History struct {
messages []Message
limit int
}
// NewHistory creates an empty History. limit controls the maximum
// number of messages retained (0 = unlimited).
func NewHistory(limit int) *History {
return &History{limit: limit}
}
// Add appends a message and truncates if over limit.
func (h *History) Add(role string, content any) {
h.messages = append(h.messages, Message{Role: role, Content: content})
if h.limit > 0 && len(h.messages) > h.limit {
h.messages = h.messages[len(h.messages)-h.limit:]
}
}
// Messages returns a copy of the accumulated messages.
func (h *History) Messages() []Message {
out := make([]Message, len(h.messages))
copy(out, h.messages)
return out
}
// Len returns the number of messages.
func (h *History) Len() int {
return len(h.messages)
}
// Reset clears all messages.
func (h *History) Reset() {
h.messages = nil
}
+62
View File
@@ -0,0 +1,62 @@
package ai
import "testing"
func TestHistory_Add(t *testing.T) {
h := NewHistory(0)
h.Add("user", "hello")
h.Add("assistant", "hi")
if h.Len() != 2 {
t.Errorf("len = %d, want 2", h.Len())
}
msgs := h.Messages()
if msgs[0].Role != "user" || msgs[0].Content != "hello" {
t.Errorf("first = %+v", msgs[0])
}
if msgs[1].Role != "assistant" || msgs[1].Content != "hi" {
t.Errorf("second = %+v", msgs[1])
}
}
func TestHistory_Truncation(t *testing.T) {
h := NewHistory(3)
for _, m := range []string{"a", "b", "c", "d", "e"} {
h.Add("user", m)
}
if h.Len() != 3 {
t.Errorf("len = %d, want 3", h.Len())
}
if h.Messages()[0].Content != "c" {
t.Errorf("first retained = %+v", h.Messages()[0])
}
}
func TestHistory_Reset(t *testing.T) {
h := NewHistory(0)
h.Add("user", "hello")
h.Reset()
if h.Len() != 0 {
t.Errorf("len after reset = %d", h.Len())
}
}
func TestHistory_SnapshotIsCopy(t *testing.T) {
h := NewHistory(0)
h.Add("user", "hello")
msgs := h.Messages()
msgs[0].Content = "mutated"
if h.Messages()[0].Content == "mutated" {
t.Error("snapshot returned reference, not copy")
}
}
func TestHistory_Unlimited(t *testing.T) {
h := NewHistory(0)
for i := 0; i < 100; i++ {
h.Add("user", "msg")
}
if h.Len() != 100 {
t.Errorf("len = %d, want 100", h.Len())
}
}
+65
View File
@@ -0,0 +1,65 @@
package ai
import "context"
// ImageModel provides an interface for image generation providers.
// Providers that support image generation implement this alongside
// or instead of Model. Use NewImage to construct, or type-assert
// a provider that implements both:
//
// p := atlascloud.NewProvider(ai.WithAPIKey(key))
// if ig, ok := p.(ai.ImageModel); ok {
// resp, _ := ig.GenerateImage(ctx, req)
// }
type ImageModel interface {
GenerateImage(ctx context.Context, req *ImageRequest, opts ...GenerateOption) (*ImageResponse, error)
String() string
}
// ImageRequest describes what image to generate.
type ImageRequest struct {
// Prompt is the text description of the image to generate.
Prompt string
// Model overrides the provider's default image model.
Model string
// Size of the generated image (e.g. "1024x1024"). Provider-specific.
Size string
// N is the number of images to generate. Defaults to 1.
N int
// Quality controls generation quality. Provider-specific (e.g. "low", "medium", "high").
Quality string
// OutputFormat sets the image format (e.g. "png", "jpeg"). Provider-specific.
OutputFormat string
}
// ImageResponse holds the generated images.
type ImageResponse struct {
Images []Image
}
// Image is a single generated image, returned as a URL, base64 data, or both
// depending on the provider and request options.
type Image struct {
// URL is a remote URL where the image can be fetched.
URL string
// Base64 is the base64-encoded image data.
Base64 string
}
// NewImageFunc creates a new ImageModel instance.
type NewImageFunc func(...Option) ImageModel
var imageProviders = make(map[string]NewImageFunc)
// RegisterImage registers an image generation provider.
func RegisterImage(name string, fn NewImageFunc) {
imageProviders[name] = fn
}
// NewImage creates a new ImageModel instance based on the provider name.
func NewImage(provider string, opts ...Option) ImageModel {
if fn, ok := imageProviders[provider]; ok {
return fn(opts...)
}
return nil
}
+194
View File
@@ -0,0 +1,194 @@
// Package mistral implements the Mistral AI model provider.
//
// Mistral AI is a European AI company offering high-performance models
// via an OpenAI-compatible chat completions endpoint.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/mistral"
//
// m := ai.New("mistral",
// ai.WithAPIKey("your-api-key"),
// )
package mistral
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("mistral", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
type Provider struct {
opts ai.Options
}
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "mistral-large-latest"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.mistral.ai"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "mistral" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(tools) > 0 {
apiReq["tools"] = tools
}
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpResp, _, err := p.callAPI(ctx, map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
})
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for mistral provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{Reply: choice.Message.Content}
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
+56
View File
@@ -0,0 +1,56 @@
package mistral
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
if NewProvider().String() != "mistral" {
t.Errorf("got %q", NewProvider().String())
}
}
func TestProvider_Defaults(t *testing.T) {
opts := NewProvider().Options()
if opts.Model != "mistral-large-latest" {
t.Errorf("default model = %q", opts.Model)
}
if opts.BaseURL != "https://api.mistral.ai" {
t.Errorf("default base URL = %q", opts.BaseURL)
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
if err := p.Init(ai.WithModel("m"), ai.WithAPIKey("k")); err != nil {
t.Fatal(err)
}
if p.Options().Model != "m" || p.Options().APIKey != "k" {
t.Error("Init did not apply options")
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
if _, err := NewProvider().Generate(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error without API key")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
if _, err := NewProvider().Stream(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("mistral", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("provider not registered")
}
if m.String() != "mistral" {
t.Errorf("got %q", m.String())
}
}
+144
View File
@@ -0,0 +1,144 @@
// Package ai provides abstraction for AI model providers
package ai
import (
"context"
"strings"
)
// Model provides an interface for interacting with AI model providers
type Model interface {
// Init initializes the model with options
Init(...Option) error
// Options returns the model options
Options() Options
// Generate generates a response from the model
Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error)
// Stream generates a streaming response (for future implementation)
Stream(ctx context.Context, req *Request, opts ...GenerateOption) (Stream, error)
// String returns the name of the provider
String() string
}
// Tool represents a tool/function that can be called by the model
type Tool struct {
Name string // LLM-safe name (e.g., "greeter_Greeter_Hello")
OriginalName string // Original name (e.g., "greeter.Greeter.Hello")
Description string
Properties map[string]any // JSON schema for tool parameters
}
// Request represents a request to generate content from a model
type Request struct {
// Prompt is the user's message/prompt
Prompt string
// SystemPrompt is the system instruction for the model
SystemPrompt string
// Tools available for the model to use
Tools []Tool
// Messages for continuing a conversation (optional).
// Use ai.History to accumulate these across turns.
Messages []Message
}
// Message represents a conversation message
type Message struct {
Role string // "user", "assistant", "system", "tool"
Content any // Can be string or structured content
}
// Response represents the response from a model
type Response struct {
// Reply is the text response from the model
Reply string
// ToolCalls are tool calls requested by the model
ToolCalls []ToolCall
// Answer is the final answer after tool execution (if tools were used)
Answer string
}
// ToolCall represents a request to call a tool and its result
type ToolCall struct {
ID string // Tool call ID (for correlation)
Name string // Tool name
Input map[string]any // Tool input arguments
Result string // Tool execution result (populated after execution)
Error string // Tool execution error (populated after execution)
}
// ToolResult represents the result of a tool execution
type ToolResult struct {
ID string // Tool call ID (for correlation)
Content string // Tool execution result (JSON string)
}
// Stream is the interface for streaming responses (future implementation)
type Stream interface {
// Recv receives the next chunk of the response
Recv() (*Response, error)
// Close closes the stream
Close() error
}
// ToolHandler is a function that handles tool calls
type ToolHandler func(name string, input map[string]any) (result any, content string)
// NewFunc creates a new Model instance
type NewFunc func(...Option) Model
var providers = make(map[string]NewFunc)
// Register registers a model provider
func Register(name string, fn NewFunc) {
providers[name] = fn
}
// New creates a new Model instance based on the provider name
func New(provider string, opts ...Option) Model {
if fn, ok := providers[provider]; ok {
return fn(opts...)
}
// Default to first registered provider
if len(providers) > 0 {
for _, fn := range providers {
return fn(opts...)
}
}
return nil
}
// AutoDetectProvider attempts to detect the provider from the base URL
func AutoDetectProvider(baseURL string) string {
if baseURL == "" {
return "openai"
}
switch {
case strings.Contains(baseURL, "anthropic"):
return "anthropic"
case strings.Contains(baseURL, "atlascloud"):
return "atlascloud"
case strings.Contains(baseURL, "googleapis.com"), strings.Contains(baseURL, "google"):
return "gemini"
case strings.Contains(baseURL, "groq"):
return "groq"
case strings.Contains(baseURL, "mistral"):
return "mistral"
case strings.Contains(baseURL, "together"):
return "together"
default:
return "openai"
}
}
// DefaultModel is a default model instance
var DefaultModel Model
// Generate generates a response using the default model.
func Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error) {
if DefaultModel == nil {
return nil, nil
}
return DefaultModel.Generate(ctx, req, opts...)
}
+297
View File
@@ -0,0 +1,297 @@
// Package openai implements the OpenAI model provider
package openai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("openai", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
ai.RegisterImage("openai", func(opts ...ai.Option) ai.ImageModel {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for OpenAI
type Provider struct {
opts ai.Options
}
// NewProvider creates a new OpenAI provider
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
// Set defaults if not provided
if options.Model == "" {
options.Model = "gpt-4o"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.openai.com"
}
return &Provider{
opts: options,
}
}
// Init initializes the provider with options
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
// Options returns the provider options
func (p *Provider) Options() ai.Options {
return p.opts
}
// String returns the provider name
func (p *Provider) String() string {
return "openai"
}
// Generate generates a response from the model
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
// Build tools for OpenAI format
var openaiTools []map[string]any
for _, t := range req.Tools {
openaiTools = append(openaiTools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
// Build messages
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
// Build initial request
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(openaiTools) > 0 {
apiReq["tools"] = openaiTools
}
// Make API call
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
// If no tool calls, return response
if len(resp.ToolCalls) == 0 {
return resp, nil
}
// If tool handler is provided, execute tools and get final answer
if p.opts.ToolHandler != nil {
// Build follow-up messages
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpReq := map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
}
// Make follow-up API call
followUpResp, _, err := p.callAPI(ctx, followUpReq)
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
// Stream generates a streaming response (not yet implemented)
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for openai provider")
}
// callAPI makes an HTTP request to the OpenAI API
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
// Marshal request
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Build HTTP request
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
// Make request
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
// Read response
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
// Parse response
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{
Reply: choice.Message.Content,
}
// Extract tool calls
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
// Return raw message for potential follow-up
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
const defaultImageModel = "gpt-image-1"
func (p *Provider) GenerateImage(ctx context.Context, req *ai.ImageRequest, opts ...ai.GenerateOption) (*ai.ImageResponse, error) {
model := req.Model
if model == "" {
model = defaultImageModel
}
n := req.N
if n <= 0 {
n = 1
}
apiReq := map[string]any{
"model": model,
"prompt": req.Prompt,
"n": n,
}
if req.Size != "" {
apiReq["size"] = req.Size
}
reqBody, err := json.Marshal(apiReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/images/generations"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var imgResp struct {
Data []struct {
URL string `json:"url"`
B64JSON string `json:"b64_json"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &imgResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
response := &ai.ImageResponse{}
for _, d := range imgResp.Data {
response.Images = append(response.Images, ai.Image{
URL: d.URL,
Base64: d.B64JSON,
})
}
return response, nil
}
+116
View File
@@ -0,0 +1,116 @@
package openai
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "openai" {
t.Errorf("Expected provider name 'openai', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("test-model"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "test-model" {
t.Errorf("Expected model 'test-model', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "gpt-4o" {
t.Errorf("Expected default model 'gpt-4o', got '%s'", opts.Model)
}
if opts.BaseURL != "https://api.openai.com" {
t.Errorf("Expected default base URL 'https://api.openai.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
func TestProvider_ImageRegistration(t *testing.T) {
ig := ai.NewImage("openai", ai.WithAPIKey("test"))
if ig == nil {
t.Fatal("ai.NewImage('openai') returned nil — image provider not registered")
}
if ig.String() != "openai" {
t.Errorf("Expected 'openai', got '%s'", ig.String())
}
}
func TestProvider_GenerateImage_NoAPIKey(t *testing.T) {
p := NewProvider()
_, err := p.GenerateImage(context.Background(), &ai.ImageRequest{Prompt: "a cat"})
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_ImplementsImageModel(t *testing.T) {
var _ ai.ImageModel = (*Provider)(nil)
}
+93
View File
@@ -0,0 +1,93 @@
package ai
import (
"context"
)
// Options for model configuration
type Options struct {
// Context for the model
Context context.Context
// Model name (e.g., "gpt-4o", "claude-sonnet-4-20250514")
Model string
// APIKey for authentication
APIKey string
// BaseURL for the API endpoint
BaseURL string
// ToolHandler handles tool calls (optional, for automatic tool execution)
ToolHandler ToolHandler
}
// GenerateOptions for generate call
type GenerateOptions struct {
// Context for this specific generate call
Context context.Context
}
// Option is a function that modifies Options
type Option func(*Options)
// GenerateOption is a function that modifies GenerateOptions
type GenerateOption func(*GenerateOptions)
// NewOptions creates new Options with defaults
func NewOptions(opts ...Option) Options {
options := Options{
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// WithModel sets the model name
func WithModel(m string) Option {
return func(o *Options) {
o.Model = m
}
}
// WithAPIKey sets the API key
func WithAPIKey(key string) Option {
return func(o *Options) {
o.APIKey = key
}
}
// WithBaseURL sets the base URL
func WithBaseURL(url string) Option {
return func(o *Options) {
o.BaseURL = url
}
}
// WithContext sets the context
func WithContext(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// WithToolHandler sets the tool handler
func WithToolHandler(handler ToolHandler) Option {
return func(o *Options) {
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()
}
}
}
+194
View File
@@ -0,0 +1,194 @@
// Package together implements the Together AI model provider.
//
// Together AI provides fast inference for open-weight models via an
// OpenAI-compatible chat completions endpoint.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/together"
//
// m := ai.New("together",
// ai.WithAPIKey("your-api-key"),
// )
package together
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("together", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
type Provider struct {
opts ai.Options
}
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.together.xyz"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "together" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(tools) > 0 {
apiReq["tools"] = tools
}
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpResp, _, err := p.callAPI(ctx, map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
})
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for together provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{Reply: choice.Message.Content}
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
+56
View File
@@ -0,0 +1,56 @@
package together
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
if NewProvider().String() != "together" {
t.Errorf("got %q", NewProvider().String())
}
}
func TestProvider_Defaults(t *testing.T) {
opts := NewProvider().Options()
if opts.Model != "meta-llama/Llama-3.3-70B-Instruct-Turbo" {
t.Errorf("default model = %q", opts.Model)
}
if opts.BaseURL != "https://api.together.xyz" {
t.Errorf("default base URL = %q", opts.BaseURL)
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
if err := p.Init(ai.WithModel("m"), ai.WithAPIKey("k")); err != nil {
t.Fatal(err)
}
if p.Options().Model != "m" || p.Options().APIKey != "k" {
t.Error("Init did not apply options")
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
if _, err := NewProvider().Generate(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error without API key")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
if _, err := NewProvider().Stream(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("together", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("provider not registered")
}
if m.String() != "together" {
t.Errorf("got %q", m.String())
}
}
+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"
}
}
+115
View File
@@ -0,0 +1,115 @@
package ai
import (
"testing"
"go-micro.dev/v5/registry"
)
func TestToolJSONType(t *testing.T) {
cases := map[string]string{
"string": "string",
"int": "integer",
"int64": "integer",
"float64": "number",
"bool": "boolean",
"User": "object",
"": "object",
}
for in, want := range cases {
if got := toolJSONType(in); got != want {
t.Errorf("toolJSONType(%q) = %q, want %q", in, got, want)
}
}
}
func TestDiscoverTools_Empty(t *testing.T) {
reg := registry.NewMemoryRegistry()
tools, err := DiscoverTools(reg)
if err != nil {
t.Fatalf("DiscoverTools: %v", err)
}
if len(tools) != 0 {
t.Errorf("expected 0 tools, got %d", len(tools))
}
}
func TestDiscoverTools_DiscoversEndpoints(t *testing.T) {
reg := registry.NewMemoryRegistry()
svc := &registry.Service{
Name: "users",
Version: "1.0.0",
Nodes: []*registry.Node{
{Id: "users-1", Address: "127.0.0.1:9000"},
},
Endpoints: []*registry.Endpoint{
{
Name: "Users.Get",
Metadata: map[string]string{
"description": "Fetch a user by ID",
},
Request: &registry.Value{
Name: "GetRequest",
Type: "GetRequest",
Values: []*registry.Value{
{Name: "id", Type: "string"},
{Name: "expand", Type: "bool"},
},
},
},
},
}
if err := reg.Register(svc); err != nil {
t.Fatalf("Register: %v", err)
}
tools, err := DiscoverTools(reg)
if err != nil {
t.Fatalf("DiscoverTools: %v", err)
}
if len(tools) != 1 {
t.Fatalf("expected 1 tool, got %d", len(tools))
}
tool := tools[0]
if tool.Name != "users_Users_Get" {
t.Errorf("safe name = %q", tool.Name)
}
if tool.OriginalName != "users.Users.Get" {
t.Errorf("original = %q", tool.OriginalName)
}
if tool.Description != "Fetch a user by ID" {
t.Errorf("description = %q", tool.Description)
}
}
func TestTools_HandlerResolvesSafeName(t *testing.T) {
tools := NewTools(registry.NewMemoryRegistry())
tools.names.put("users_Users_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 TestTools_HandlerInvalidName(t *testing.T) {
tools := NewTools(registry.NewMemoryRegistry())
h := tools.Handler()
result, content := h("foo", map[string]any{})
if result == nil {
t.Fatal("expected error result")
}
if content == "" {
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")
}
}
+51
View File
@@ -0,0 +1,51 @@
package ai
import "context"
// VideoModel provides an interface for video generation providers.
// Providers that support video generation implement this alongside
// Model and/or ImageModel.
type VideoModel interface {
GenerateVideo(ctx context.Context, req *VideoRequest, opts ...GenerateOption) (*VideoResponse, error)
String() string
}
// VideoRequest describes what video to generate.
type VideoRequest struct {
// Prompt is the text description or instructions for the video.
Prompt string
// Model overrides the provider's default video model.
Model string
// Images are reference image URLs for image-to-video generation.
Images []string
// Duration in seconds. Provider-specific defaults apply.
Duration int
// AspectRatio (e.g. "16:9", "9:16"). Provider-specific.
AspectRatio string
// Resolution (e.g. "720p", "1080p"). Provider-specific.
Resolution string
}
// VideoResponse holds the generated video.
type VideoResponse struct {
// URL is the remote URL where the video can be fetched.
URL string
}
// NewVideoFunc creates a new VideoModel instance.
type NewVideoFunc func(...Option) VideoModel
var videoProviders = make(map[string]NewVideoFunc)
// RegisterVideo registers a video generation provider.
func RegisterVideo(name string, fn NewVideoFunc) {
videoProviders[name] = fn
}
// NewVideo creates a new VideoModel instance based on the provider name.
func NewVideo(provider string, opts ...Option) VideoModel {
if fn, ok := videoProviders[provider]; ok {
return fn(opts...)
}
return nil
}
+3 -3
View File
@@ -20,9 +20,9 @@ import (
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/cache"
"go-micro.dev/v5/transport/headers"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
mls "go-micro.dev/v5/util/tls"
maddr "go-micro.dev/v5/internal/util/addr"
mnet "go-micro.dev/v5/internal/util/net"
mls "go-micro.dev/v5/internal/util/tls"
"golang.org/x/net/http2"
)
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/google/uuid"
log "go-micro.dev/v5/logger"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
maddr "go-micro.dev/v5/internal/util/addr"
mnet "go-micro.dev/v5/internal/util/net"
)
type memoryBroker struct {
+1 -1
View File
@@ -12,7 +12,7 @@ import (
amqp "github.com/rabbitmq/amqp091-go"
"go-micro.dev/v5/logger"
mtls "go-micro.dev/v5/util/tls"
mtls "go-micro.dev/v5/internal/util/tls"
)
type MQExchangeType string
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"go-micro.dev/v5/util/backoff"
"go-micro.dev/v5/internal/util/backoff"
)
type BackoffFunc func(ctx context.Context, req Request, attempts int) (time.Duration, error)
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"go-micro.dev/v5/metadata"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/selector"
pnet "go-micro.dev/v5/util/net"
pnet "go-micro.dev/v5/internal/util/net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding"
+6
View File
@@ -401,6 +401,12 @@ func WithMessageContentType(ct string) MessageOption {
}
}
func WithConnectionTimeout(d time.Duration) CallOption {
return func(o *CallOptions) {
o.ConnectionTimeout = d
}
}
// Request Options
func WithContentType(ct string) RequestOption {
+3 -3
View File
@@ -20,9 +20,9 @@ import (
"go-micro.dev/v5/selector"
"go-micro.dev/v5/transport"
"go-micro.dev/v5/transport/headers"
"go-micro.dev/v5/util/buf"
"go-micro.dev/v5/util/net"
"go-micro.dev/v5/util/pool"
"go-micro.dev/v5/internal/util/buf"
"go-micro.dev/v5/internal/util/net"
"go-micro.dev/v5/internal/util/pool"
)
const (
+31 -31
View File
@@ -23,7 +23,7 @@ import (
"go-micro.dev/v5/debug/trace"
"go-micro.dev/v5/events"
"go-micro.dev/v5/logger"
mprofile "go-micro.dev/v5/profile"
mprofile "go-micro.dev/v5/service/profile"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/consul"
"go-micro.dev/v5/registry/etcd"
@@ -299,20 +299,37 @@ func init() {
}
func newCmd(opts ...Option) Cmd {
// Create local copies so each cmd instance is isolated.
// This allows multiple services in a single binary without
// conflicting through shared global pointers.
localAuth := auth.DefaultAuth
localBroker := broker.DefaultBroker
localClient := client.DefaultClient
localRegistry := registry.DefaultRegistry
localServer := server.DefaultServer
localSelector := selector.DefaultSelector
localTransport := transport.DefaultTransport
localStore := store.DefaultStore
localTracer := trace.DefaultTracer
localProfile := profile.DefaultProfile
localConfig := config.DefaultConfig
localCache := cache.DefaultCache
localStream := events.DefaultStream
options := Options{
Auth: &auth.DefaultAuth,
Broker: &broker.DefaultBroker,
Client: &client.DefaultClient,
Registry: &registry.DefaultRegistry,
Server: &server.DefaultServer,
Selector: &selector.DefaultSelector,
Transport: &transport.DefaultTransport,
Store: &store.DefaultStore,
Tracer: &trace.DefaultTracer,
DebugProfile: &profile.DefaultProfile,
Config: &config.DefaultConfig,
Cache: &cache.DefaultCache,
Stream: &events.DefaultStream,
Auth: &localAuth,
Broker: &localBroker,
Client: &localClient,
Registry: &localRegistry,
Server: &localServer,
Selector: &localSelector,
Transport: &localTransport,
Store: &localStore,
Tracer: &localTracer,
DebugProfile: &localProfile,
Config: &localConfig,
Cache: &localCache,
Stream: &localStream,
Brokers: DefaultBrokers,
Clients: DefaultClients,
@@ -381,13 +398,9 @@ func (c *cmd) Before(ctx *cli.Context) error {
return fmt.Errorf("failed to load local profile: %v", ierr)
}
*c.opts.Registry = imported.Registry
registry.DefaultRegistry = imported.Registry
*c.opts.Broker = imported.Broker
broker.DefaultBroker = imported.Broker
*c.opts.Store = imported.Store
store.DefaultStore = imported.Store
*c.opts.Transport = imported.Transport
transport.DefaultTransport = imported.Transport
case "nats":
imported, ierr := mprofile.NatsProfile()
if ierr != nil {
@@ -428,7 +441,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
// only change if we have the client and type differs
if cl, ok := c.opts.Clients[name]; ok && (*c.opts.Client).String() != name {
*c.opts.Client = cl()
client.DefaultClient = *c.opts.Client
}
}
@@ -437,7 +449,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
// only change if we have the server and type differs
if s, ok := c.opts.Servers[name]; ok && (*c.opts.Server).String() != name {
*c.opts.Server = s()
server.DefaultServer = *c.opts.Server
}
}
@@ -449,7 +460,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Store = s(store.WithClient(*c.opts.Client))
store.DefaultStore = *c.opts.Store
}
// Set the tracer
@@ -460,7 +470,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Tracer = r()
trace.DefaultTracer = *c.opts.Tracer
}
// Setup auth
@@ -487,7 +496,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Auth = r(authOpts...)
auth.DefaultAuth = *c.opts.Auth
}
// Set the registry
@@ -509,7 +517,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
return fmt.Errorf("unsupported profile: %s", name)
}
*c.opts.DebugProfile = p()
profile.DefaultProfile = *c.opts.DebugProfile
}
// Set the broker
@@ -534,7 +541,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
// No server option here. Should there be?
clientOpts = append(clientOpts, client.Selector(*c.opts.Selector))
selector.DefaultSelector = *c.opts.Selector
}
// Set the transport
@@ -687,7 +693,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
logger.Fatalf("Error configuring config: %v", err)
}
*c.opts.Config = rc
config.DefaultConfig = *c.opts.Config
}
}
return nil
@@ -709,7 +714,6 @@ func (c *cmd) setRegistry(r registry.Registry) ([]server.Option, []client.Option
if err := (*c.opts.Broker).Init(broker.Registry(*c.opts.Registry)); err != nil {
logger.Fatalf("Error configuring broker: %v", err)
}
registry.DefaultRegistry = *c.opts.Registry
return serverOpts, clientOpts
}
func (c *cmd) setStream(s events.Stream) ([]server.Option, []client.Option) {
@@ -720,7 +724,6 @@ func (c *cmd) setStream(s events.Stream) ([]server.Option, []client.Option) {
// serverOpts = append(serverOpts, server.Registry(*c.opts.Registry))
// clientOpts = append(clientOpts, client.Registry(*c.opts.Registry))
events.DefaultStream = *c.opts.Stream
return serverOpts, clientOpts
}
@@ -730,7 +733,6 @@ func (c *cmd) setBroker(b broker.Broker) ([]server.Option, []client.Option) {
*c.opts.Broker = b
serverOpts = append(serverOpts, server.Broker(*c.opts.Broker))
clientOpts = append(clientOpts, client.Broker(*c.opts.Broker))
broker.DefaultBroker = *c.opts.Broker
return serverOpts, clientOpts
}
@@ -738,7 +740,6 @@ func (c *cmd) setStore(s store.Store) ([]server.Option, []client.Option) {
var serverOpts []server.Option
var clientOpts []client.Option
*c.opts.Store = s
store.DefaultStore = *c.opts.Store
return serverOpts, clientOpts
}
@@ -748,7 +749,6 @@ func (c *cmd) setTransport(t transport.Transport) ([]server.Option, []client.Opt
*c.opts.Transport = t
serverOpts = append(serverOpts, server.Transport(*c.opts.Transport))
clientOpts = append(clientOpts, client.Transport(*c.opts.Transport))
transport.DefaultTransport = *c.opts.Transport
return serverOpts, clientOpts
}
+19
View File
@@ -0,0 +1,19 @@
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /micro-mcp-gateway ./cmd/micro-mcp-gateway
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=builder /micro-mcp-gateway /usr/local/bin/micro-mcp-gateway
EXPOSE 3000
ENTRYPOINT ["micro-mcp-gateway"]
CMD ["--address", ":3000"]
+242
View File
@@ -0,0 +1,242 @@
// Command micro-mcp-gateway runs a standalone MCP gateway that discovers
// go-micro services via a registry and exposes them as AI-accessible tools
// through the Model Context Protocol.
//
// This is the production deployment binary for the MCP gateway, intended
// to run independently of your services.
//
// Usage:
//
// # mDNS (development default)
// micro-mcp-gateway --address :3000
//
// # Consul
// micro-mcp-gateway --address :3000 --registry consul --registry-address consul:8500
//
// # etcd
// micro-mcp-gateway --address :3000 --registry etcd --registry-address etcd:2379
//
// # With auth and rate limiting
// micro-mcp-gateway --address :3000 --registry consul \
// --rate-limit 100 --rate-burst 200 --audit
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/auth/jwt"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/consul"
"go-micro.dev/v5/registry/etcd"
"github.com/urfave/cli/v2"
)
var version = "0.1.0"
func main() {
app := &cli.App{
Name: "micro-mcp-gateway",
Usage: "Standalone MCP gateway for go-micro services",
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Usage: "Address to listen on",
Value: ":3000",
EnvVars: []string{"MCP_ADDRESS"},
},
&cli.StringFlag{
Name: "registry",
Usage: "Service registry (mdns, consul, etcd)",
Value: "mdns",
EnvVars: []string{"MICRO_REGISTRY"},
},
&cli.StringFlag{
Name: "registry-address",
Usage: "Registry address (e.g., consul:8500, etcd:2379)",
EnvVars: []string{"MICRO_REGISTRY_ADDRESS"},
},
&cli.Float64Flag{
Name: "rate-limit",
Usage: "Requests per second per tool (0 = unlimited)",
EnvVars: []string{"MCP_RATE_LIMIT"},
},
&cli.IntFlag{
Name: "rate-burst",
Usage: "Rate limit burst size",
Value: 20,
EnvVars: []string{"MCP_RATE_BURST"},
},
&cli.BoolFlag{
Name: "auth",
Usage: "Enable JWT authentication",
EnvVars: []string{"MCP_AUTH"},
},
&cli.BoolFlag{
Name: "audit",
Usage: "Enable audit logging to stdout",
EnvVars: []string{"MCP_AUDIT"},
},
&cli.StringSliceFlag{
Name: "scope",
Usage: "Tool scope requirement (format: tool=scope1,scope2)",
},
&cli.IntFlag{
Name: "circuit-breaker",
Usage: "Circuit breaker max failures before opening (0 = disabled)",
EnvVars: []string{"MCP_CIRCUIT_BREAKER"},
},
&cli.DurationFlag{
Name: "circuit-breaker-timeout",
Usage: "Circuit breaker open-state timeout before half-open probe",
Value: 30 * time.Second,
EnvVars: []string{"MCP_CIRCUIT_BREAKER_TIMEOUT"},
},
},
Action: run,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func run(c *cli.Context) error {
logger := log.New(os.Stdout, "[mcp-gateway] ", log.LstdFlags)
// Configure registry
reg, err := newRegistry(c.String("registry"), c.String("registry-address"))
if err != nil {
return fmt.Errorf("registry: %w", err)
}
// Build MCP options
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
opts := mcp.Options{
Registry: reg,
Address: c.String("address"),
Context: ctx,
Logger: logger,
}
// Rate limiting
if rps := c.Float64("rate-limit"); rps > 0 {
opts.RateLimit = &mcp.RateLimitConfig{
RequestsPerSecond: rps,
Burst: c.Int("rate-burst"),
}
logger.Printf("Rate limit: %.0f req/s, burst %d", rps, c.Int("rate-burst"))
}
// Auth
if c.Bool("auth") {
opts.Auth = jwt.NewAuth()
logger.Printf("JWT authentication enabled")
}
// Scopes
if scopes := c.StringSlice("scope"); len(scopes) > 0 {
opts.Scopes = parseScopes(scopes)
for tool, s := range opts.Scopes {
logger.Printf("Scope: %s requires [%s]", tool, strings.Join(s, ", "))
}
}
// Circuit breaker
if maxFail := c.Int("circuit-breaker"); maxFail > 0 {
opts.CircuitBreaker = &mcp.CircuitBreakerConfig{
MaxFailures: maxFail,
Timeout: c.Duration("circuit-breaker-timeout"),
}
logger.Printf("Circuit breaker: max %d failures, timeout %s", maxFail, c.Duration("circuit-breaker-timeout"))
}
// Audit
if c.Bool("audit") {
opts.AuditFunc = func(r mcp.AuditRecord) {
status := "ALLOWED"
if !r.Allowed {
status = "DENIED:" + r.DeniedReason
}
logger.Printf("[audit] %s tool=%s account=%s status=%s duration=%s",
r.TraceID, r.Tool, r.AccountID, status, r.Duration)
}
logger.Printf("Audit logging enabled")
}
// Print startup info
logger.Printf("Starting MCP gateway on %s", c.String("address"))
logger.Printf("Registry: %s", c.String("registry"))
if addr := c.String("registry-address"); addr != "" {
logger.Printf("Registry address: %s", addr)
}
// Start gateway in background
errCh := make(chan error, 1)
go func() {
errCh <- mcp.ListenAndServe(opts.Address, opts)
}()
// Wait for signal or error
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
select {
case sig := <-sigCh:
logger.Printf("Received %s, shutting down...", sig)
cancel()
return nil
case err := <-errCh:
return fmt.Errorf("gateway error: %w", err)
}
}
func newRegistry(name, address string) (registry.Registry, error) {
var opts []registry.Option
if address != "" {
opts = append(opts, registry.Addrs(strings.Split(address, ",")...))
}
switch name {
case "mdns", "":
return registry.NewMDNSRegistry(opts...), nil
case "consul":
return consul.NewConsulRegistry(opts...), nil
case "etcd":
return etcd.NewEtcdRegistry(opts...), nil
default:
return nil, fmt.Errorf("unknown registry %q (supported: mdns, consul, etcd)", name)
}
}
func parseScopes(raw []string) map[string][]string {
scopes := make(map[string][]string)
for _, s := range raw {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
continue
}
tool := strings.TrimSpace(parts[0])
scopeList := strings.Split(parts[1], ",")
for i := range scopeList {
scopeList[i] = strings.TrimSpace(scopeList[i])
}
scopes[tool] = scopeList
}
return scopes
}
// Ensure auth.Auth interface is satisfied at compile time.
var _ auth.Auth = jwt.NewAuth()
+108 -3
View File
@@ -19,6 +19,14 @@ Create your service (all setup is now automatic!):
micro new helloworld
```
Or use a template for common service patterns:
```
micro new contacts --template crud # CRUD with Create/Read/Update/Delete/List
micro new events --template pubsub # Pub/sub with broker integration
micro new gateway --template api # API gateway with health check
```
This will:
- Create a new service in the `helloworld` directory
- Automatically run `go mod tidy` and `make proto` for you
@@ -38,7 +46,7 @@ This starts:
- **Web Dashboard** at http://localhost:8080
- **Agent Playground** at http://localhost:8080/agent
- **API Explorer** at http://localhost:8080/api
- **MCP Tools** at http://localhost:8080/api/mcp/tools
- **MCP Tools** at http://localhost:8080/mcp/tools
- **Hot Reload** watching for file changes
- **Services** in dependency order
@@ -343,6 +351,103 @@ micro stop myservice --remote user@server
See [internal/website/docs/deployment.md](../../internal/website/docs/deployment.md) for the full deployment guide.
## API Gateway
Run a standalone HTTP-to-RPC gateway (no dashboard, no auth, no hot reload):
```bash
micro api # listen on :8080
micro api --address :3000 # custom port
```
Routes:
- `POST /{service}/{endpoint}` — proxies to an RPC call
- `GET /` — lists all services and endpoints
- `GET /{service}` — describes a service
- `GET /health` — health check
```bash
curl -XPOST -d '{"name":"Alice"}' http://localhost:8080/greeter/Greeter.Hello
```
## Inspecting the Framework
Every core interface has a matching CLI command:
### Registry
```bash
micro registry list # list all registered services (JSON)
micro registry get <name> # show nodes and endpoints for a service
micro registry watch # stream registration events
```
### Broker
```bash
micro broker publish <topic> <message> # publish a message
micro broker subscribe <topic> # stream messages from a topic
```
### Store
```bash
micro store list [prefix] # list keys (optionally by prefix)
micro store read <key> # read a record
micro store write <key> <value> # write a record
micro store delete <key> # delete a record
```
### Config
```bash
micro config get <key> # read a config value (dot notation → env var)
micro config dump # print all configuration
```
Keys use dot notation: `database.host` reads from `DATABASE_HOST`.
## AI & Agents
### micro chat
Interactive LLM agent that discovers services and orchestrates them through natural language:
```bash
ANTHROPIC_API_KEY=sk-ant-... micro chat --provider anthropic
> list all users
> send a welcome email to Alice
```
Supports: `--provider` (anthropic, openai, gemini, atlascloud, groq, mistral, together), `--prompt` for single-shot mode, `--model` and `--base_url` for overrides.
Environment variables: `MICRO_AI_PROVIDER`, `MICRO_AI_API_KEY`, or provider-specific keys like `ANTHROPIC_API_KEY`.
### micro flow
Event-driven LLM orchestration:
```bash
# Subscribe to events and react
micro flow run --trigger events.user.created \
--prompt "New user: {{.Data}}. Send welcome email." \
--provider anthropic
# One-shot execution
micro flow exec --prompt "List all users" --provider anthropic
```
### micro mcp
Expose services as MCP tools for AI agents:
```bash
micro mcp serve # stdio transport (for Claude Code)
micro mcp serve --address :3000 # HTTP/SSE transport
micro mcp list # list available tools
micro mcp test <tool> # test a tool
```
## Protobuf
Use protobuf for code generation with [protoc-gen-micro](https://github.com/micro/go-micro/tree/master/cmd/protoc-gen-micro)
@@ -426,7 +531,7 @@ Both commands provide:
- **Hot Service Updates**: Gateway automatically picks up new service registrations
- **JWT Authentication**: Tokens, user management, login at `/auth/login`, `/auth/tokens`, `/auth/users`
- **Endpoint Scopes**: Restrict which tokens can call which endpoints via `/auth/scopes`
- **MCP Integration**: AI tools at `/api/mcp/tools`, agent playground at `/agent`
- **MCP Integration**: AI tools at `/mcp/tools`, agent playground at `/agent`
### Authentication & Scopes
@@ -437,7 +542,7 @@ Both `micro run` and `micro server` use the same `auth.Account` type from the go
| Path | Description |
|------|-------------|
| `POST /api/{service}/{endpoint}` | HTTP API calls |
| `POST /api/mcp/call` | MCP tool invocations |
| `POST /mcp/call` | MCP tool invocations |
| Agent playground | Tool calls made by the AI agent |
Scopes are configured via the web UI at `/auth/scopes`. Each endpoint can require one or more scopes. A token must carry at least one matching scope to call a protected endpoint. The `*` scope on a token bypasses all checks. Endpoints with no scopes set are open to any authenticated token.
+325
View File
@@ -0,0 +1,325 @@
// Package api implements the 'micro api' command — a lightweight
// HTTP-to-RPC gateway that proxies JSON requests to go-micro services.
//
// Usage:
//
// micro api # listen on :8080
// micro api --address :3000 # custom port
//
// Requests:
//
// POST /service/endpoint → RPC call to service.endpoint
// GET /health → {"status":"ok"}
//
// The request body is forwarded as-is (JSON). The Micro-Endpoint
// header can also be used to specify the endpoint.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"sort"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/broker"
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
codecBytes "go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
)
func init() {
cmd.Register(&cli.Command{
Name: "api",
Usage: "Run a lightweight HTTP-to-RPC API gateway",
Description: `Start an HTTP gateway that proxies JSON requests to go-micro services.
Requests are routed by URL path:
POST /service/endpoint → calls service.endpoint via RPC
GET / → lists available services and endpoints
Examples:
# Start on default port
micro api
# Custom port
micro api --address :3000
# Call a service through the gateway
curl -XPOST -d '{"name":"Alice"}' http://localhost:8080/greeter/Greeter.Hello
# Or use the Micro-Endpoint header
curl -XPOST -H 'Micro-Endpoint: Greeter.Hello' \
-d '{"name":"Alice"}' http://localhost:8080/greeter`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Usage: "Address to listen on",
Value: ":8080",
EnvVars: []string{"MICRO_API_ADDRESS"},
},
},
Action: run,
})
}
func run(c *cli.Context) error {
addr := c.String("address")
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// Framework primitives under /micro/
registerFrameworkRoutes(mux)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
path := strings.TrimPrefix(r.URL.Path, "/")
path = strings.TrimSuffix(path, "/")
// Root: list services
if path == "" {
listServices(w)
return
}
// Parse service/endpoint from path
parts := strings.SplitN(path, "/", 2)
serviceName := parts[0]
endpoint := ""
if len(parts) > 1 {
endpoint = parts[1]
}
// Allow Micro-Endpoint header to override
if h := r.Header.Get("Micro-Endpoint"); h != "" {
endpoint = h
}
if endpoint == "" {
describeService(w, serviceName)
return
}
// Proxy RPC call
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "failed to read body: "+err.Error())
return
}
if len(body) == 0 {
body = []byte("{}")
}
req := client.DefaultClient.NewRequest(serviceName, endpoint, &codecBytes.Frame{Data: body})
var rsp codecBytes.Frame
if err := client.DefaultClient.Call(r.Context(), req, &rsp); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(rsp.Data)
})
fmt.Println()
fmt.Println(" \033[1mmicro api\033[0m")
fmt.Println()
fmt.Printf(" Listening \033[36m%s\033[0m\n", addr)
fmt.Println()
fmt.Println(" Routes:")
fmt.Println(" \033[32mGET\033[0m / List services")
fmt.Println(" \033[32mGET\033[0m /{service} Describe a service")
fmt.Println(" \033[33mPOST\033[0m /{service}/{endpoint} Call an endpoint")
fmt.Println(" \033[32mGET\033[0m /health Health check")
fmt.Println()
fmt.Println(" Framework:")
fmt.Println(" \033[32mGET\033[0m /micro/registry List registered services")
fmt.Println(" \033[32mGET\033[0m /micro/registry/{name} Describe a service")
fmt.Println(" \033[32mGET\033[0m /micro/store List store keys")
fmt.Println(" \033[32mGET\033[0m /micro/store/{key} Read a record")
fmt.Println(" \033[33mPOST\033[0m /micro/store/{key} Write a record")
fmt.Println(" \033[33mPOST\033[0m /micro/broker/{topic} Publish a message")
fmt.Println()
server := &http.Server{Addr: addr, Handler: mux}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "server error: %v\n", err)
os.Exit(1)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
fmt.Println("\nShutting down...")
return server.Close()
}
func listServices(w http.ResponseWriter) {
services, err := registry.ListServices()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
sort.Slice(services, func(i, j int) bool {
return services[i].Name < services[j].Name
})
type svcInfo struct {
Name string `json:"name"`
Endpoints []string `json:"endpoints,omitempty"`
}
var result []svcInfo
for _, svc := range services {
info := svcInfo{Name: svc.Name}
full, err := registry.GetService(svc.Name)
if err == nil && len(full) > 0 {
for _, ep := range full[0].Endpoints {
info.Endpoints = append(info.Endpoints, ep.Name)
}
}
result = append(result, info)
}
json.NewEncoder(w).Encode(result)
}
func describeService(w http.ResponseWriter, name string) {
services, err := registry.GetService(name)
if err != nil || len(services) == 0 {
writeError(w, http.StatusNotFound, "service not found: "+name)
return
}
type epInfo struct {
Name string `json:"name"`
Metadata map[string]string `json:"metadata,omitempty"`
}
svc := services[0]
var endpoints []epInfo
for _, ep := range svc.Endpoints {
endpoints = append(endpoints, epInfo{
Name: ep.Name,
Metadata: ep.Metadata,
})
}
json.NewEncoder(w).Encode(map[string]any{
"name": svc.Name,
"version": svc.Version,
"endpoints": endpoints,
"nodes": len(svc.Nodes),
})
}
func writeError(w http.ResponseWriter, code int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
// registerFrameworkRoutes adds /micro/* routes for registry, broker, and store.
func registerFrameworkRoutes(mux *http.ServeMux) {
// Registry
mux.HandleFunc("/micro/registry", func(w http.ResponseWriter, r *http.Request) {
listServices(w)
})
mux.HandleFunc("/micro/registry/", func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/micro/registry/")
if name == "" {
listServices(w)
return
}
describeService(w, name)
})
// Store
mux.HandleFunc("/micro/store", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
keys, err := store.List()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
json.NewEncoder(w).Encode(keys)
})
mux.HandleFunc("/micro/store/", func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(r.URL.Path, "/micro/store/")
if key == "" {
w.Header().Set("Content-Type", "application/json")
keys, _ := store.List()
json.NewEncoder(w).Encode(keys)
return
}
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
records, err := store.Read(key)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if len(records) == 0 {
writeError(w, http.StatusNotFound, "key not found")
return
}
w.Write(records[0].Value)
case http.MethodPost:
body, _ := io.ReadAll(r.Body)
if err := store.Write(&store.Record{Key: key, Value: body}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "key": key})
default:
writeError(w, http.StatusMethodNotAllowed, "use GET or POST")
}
})
// Broker
mux.HandleFunc("/micro/broker/", func(w http.ResponseWriter, r *http.Request) {
topic := strings.TrimPrefix(r.URL.Path, "/micro/broker/")
if topic == "" {
writeError(w, http.StatusBadRequest, "topic required: /micro/broker/{topic}")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "use POST to publish")
return
}
body, _ := io.ReadAll(r.Body)
b := broker.DefaultBroker
if err := b.Connect(); err != nil {
writeError(w, http.StatusInternalServerError, "broker connect: "+err.Error())
return
}
if err := b.Publish(topic, &broker.Message{Body: body}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "topic": topic})
})
}
+626
View File
@@ -0,0 +1,626 @@
// Package chat implements the 'micro chat' interactive agent command.
//
// micro chat opens a terminal REPL where you can talk to your services
// through an LLM. It discovers all services from the registry, exposes
// each endpoint as a tool, and lets the model orchestrate calls in
// response to natural-language prompts.
package chat
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/agent"
"go-micro.dev/v5/ai"
clt "go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/cli/generate"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/registry"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/atlascloud"
_ "go-micro.dev/v5/ai/gemini"
_ "go-micro.dev/v5/ai/groq"
_ "go-micro.dev/v5/ai/mistral"
_ "go-micro.dev/v5/ai/openai"
_ "go-micro.dev/v5/ai/together"
)
const systemPromptTmpl = `You are an agent that orchestrates microservices. Use the available tools to fulfill user requests. When you call a tool, explain what you are doing.
Available services: %s
If a user asks for something that no existing service can handle, use the micro_generate_service tool to create it. Pass a short description of what the service should do. After it's created, the new service's endpoints will be available as tools and you can use them immediately.
Do NOT make up capabilities. Only use the tools that are available. If generation fails, tell the user.`
var generateTool = ai.Tool{
Name: "micro_generate_service",
OriginalName: "micro.generate_service",
Description: "Generate a new microservice from a description. Use when the user needs a capability that no existing service provides. The service will be created, compiled, and started automatically.",
Properties: map[string]any{
"description": map[string]any{
"type": "string",
"description": "What the service should do, e.g. 'a shipping service that tracks parcels and calculates rates'",
},
},
}
func init() {
cmd.Register(&cli.Command{
Name: "chat",
Usage: "Interactive AI chat that orchestrates your services",
Description: `Start an interactive chat session that uses an LLM to call your services.
micro chat discovers every service in the registry, exposes each endpoint as a
tool, and lets you ask natural-language questions like "list all users" or
"create an order for product 42". The model decides which tool to call and
issues RPCs to the right service.
If you ask for something no existing service handles, the agent will generate
a new service automatically and start using it.
Examples:
ANTHROPIC_API_KEY=sk-ant-... micro chat --provider anthropic
micro chat --provider openai --prompt "list all users"`,
Flags: []cli.Flag{
&cli.StringFlag{Name: "provider", Usage: "AI provider (anthropic, openai, gemini, groq, mistral, together, atlascloud)", EnvVars: []string{"MICRO_AI_PROVIDER"}},
&cli.StringFlag{Name: "api_key", Usage: "API key for the provider", EnvVars: []string{"MICRO_AI_API_KEY"}},
&cli.StringFlag{Name: "model", Usage: "Model name (uses provider default if unset)", EnvVars: []string{"MICRO_AI_MODEL"}},
&cli.StringFlag{Name: "base_url", Usage: "Override the provider's base URL", EnvVars: []string{"MICRO_AI_BASE_URL"}},
&cli.StringFlag{Name: "prompt", Usage: "Send a single prompt and exit (non-interactive)"},
},
Action: run,
})
}
// agentInfo holds metadata about a discovered agent.
type agentInfo struct {
Name string
Services []string
}
type session struct {
provider string
apiKey string
model ai.Model
tools *ai.Tools
reg registry.Registry
cl clt.Client
hist *ai.History
toolList []ai.Tool
sysPrompt string
procs []*exec.Cmd
agents map[string]agentInfo
// Built-in agent capabilities (plan, delegate), shared with the
// agent package so the direct-service fallback has the same tools a
// real agent would.
builtinTools []ai.Tool
builtinHandle func(name string, input map[string]any) (any, string, bool)
}
// discoverAgents finds agents registered in the registry.
func (s *session) discoverAgents() bool {
svcs, err := s.reg.ListServices()
if err != nil {
return false
}
s.agents = make(map[string]agentInfo)
for _, svc := range svcs {
records, err := s.reg.GetService(svc.Name)
if err != nil || len(records) == 0 {
continue
}
meta := records[0].Metadata
if meta == nil || meta["type"] != "agent" {
if len(records[0].Nodes) > 0 {
meta = records[0].Nodes[0].Metadata
}
if meta == nil || meta["type"] != "agent" {
continue
}
}
var services []string
if svcsStr := meta["services"]; svcsStr != "" {
services = strings.Split(svcsStr, ",")
}
s.agents[svc.Name] = agentInfo{Name: svc.Name, Services: services}
}
return len(s.agents) > 0
}
// callAgent calls an agent's Chat endpoint via RPC.
func (s *session) callAgent(ctx context.Context, name, message string) (*agent.Response, error) {
reqBody, _ := json.Marshal(map[string]string{"message": message})
req := s.cl.NewRequest(name, "Agent.Chat", &bytes.Frame{Data: reqBody})
var rsp bytes.Frame
if err := s.cl.Call(ctx, req, &rsp); err != nil {
return nil, err
}
var resp struct {
Reply string `json:"reply"`
Agent string `json:"agent"`
ToolCalls []struct {
ID string `json:"id"`
Name string `json:"name"`
Input string `json:"input"`
Result string `json:"result"`
} `json:"tool_calls"`
}
if err := json.Unmarshal(rsp.Data, &resp); err != nil {
return nil, err
}
r := &agent.Response{
Reply: resp.Reply,
Agent: resp.Agent,
}
for _, tc := range resp.ToolCalls {
var input map[string]any
json.Unmarshal([]byte(tc.Input), &input)
r.ToolCalls = append(r.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Name,
Input: input,
Result: tc.Result,
})
}
return r, nil
}
// buildRouterPrompt creates a system prompt for the router that
// knows about all available agents and can dispatch to them.
func (s *session) buildRouterPrompt() string {
var agentDescs []string
for name, info := range s.agents {
svcs := strings.Join(info.Services, ", ")
agentDescs = append(agentDescs, fmt.Sprintf("- %s (manages: %s)", name, svcs))
}
sort.Strings(agentDescs)
return fmt.Sprintf(`You are a router that dispatches user requests to the right agent.
Available agents:
%s
For each user message, decide which agent should handle it and call the route_to_agent tool with the agent name and the message. If the request spans multiple agents, call route_to_agent multiple times.
If no agent can handle the request, say so.`, strings.Join(agentDescs, "\n"))
}
func (s *session) refreshTools() {
discovered, err := s.tools.Discover()
if err != nil {
return
}
s.toolList = append(discovered, generateTool)
s.toolList = append(s.toolList, s.builtinTools...)
serviceNames := make(map[string]bool)
for _, t := range discovered {
parts := strings.SplitN(t.OriginalName, ".", 2)
if len(parts) == 2 {
serviceNames[parts[0]] = true
}
}
var svcList []string
for name := range serviceNames {
svcList = append(svcList, name)
}
if len(svcList) == 0 {
s.sysPrompt = fmt.Sprintf(systemPromptTmpl, "(none yet)")
} else {
s.sysPrompt = fmt.Sprintf(systemPromptTmpl, strings.Join(svcList, ", "))
}
}
func (s *session) handleGenerate(input map[string]any) (any, string) {
desc, _ := input["description"].(string)
if desc == "" {
return map[string]string{"error": "description is required"}, `{"error":"description is required"}`
}
fmt.Printf("\n \033[36m⚡\033[0m generating service: %s\n", desc)
design, err := generate.Design(context.Background(), s.provider, s.apiKey, "", ".", desc)
if err != nil {
msg := fmt.Sprintf(`{"error":"design failed: %s"}`, err)
return map[string]string{"error": err.Error()}, msg
}
if err := generate.Generate(context.Background(), ".", design, s.provider, s.apiKey, ""); err != nil {
msg := fmt.Sprintf(`{"error":"generate failed: %s"}`, err)
return map[string]string{"error": err.Error()}, msg
}
// Find which services are new (not already in registry)
existing := make(map[string]bool)
if svcs, err := s.reg.ListServices(); err == nil {
for _, svc := range svcs {
existing[svc.Name] = true
}
}
var created []string
for _, svc := range design.Services {
name := strings.TrimSuffix(svc.Name, "-service")
if existing[name] {
continue
}
created = append(created, svc.Name)
// Build and start the new service
svcDir, _ := filepath.Abs(svc.Name)
fmt.Printf(" \033[36m⚡\033[0m starting %s...\n", svc.Name)
buildCmd := exec.Command("go", "build", "-o", svc.Name, ".")
buildCmd.Dir = svcDir
if out, err := buildCmd.CombinedOutput(); err != nil {
fmt.Printf(" \033[33m⚠\033[0m build failed: %s\n", string(out))
continue
}
runCmd := exec.Command(filepath.Join(svcDir, svc.Name))
runCmd.Dir = svcDir
if err := runCmd.Start(); err != nil {
fmt.Printf(" \033[33m⚠\033[0m start failed: %v\n", err)
continue
}
s.procs = append(s.procs, runCmd)
}
if len(created) == 0 {
result := map[string]any{"message": "No new services needed — all already exist."}
b, _ := json.Marshal(result)
return result, string(b)
}
// Wait for services to register
fmt.Printf(" \033[36m⚡\033[0m waiting for services to register...\n")
time.Sleep(5 * time.Second)
s.refreshTools()
fmt.Printf(" \033[32m✓\033[0m %d tools available\n\n", len(s.toolList)-1)
result := map[string]any{
"created": created,
"message": fmt.Sprintf("Created and started: %s. Their endpoints are now available as tools.", strings.Join(created, ", ")),
}
b, _ := json.Marshal(result)
return result, string(b)
}
func run(c *cli.Context) error {
provider := c.String("provider")
apiKey := c.String("api_key")
modelName := c.String("model")
baseURL := c.String("base_url")
singlePrompt := c.String("prompt")
if provider == "" {
provider = ai.AutoDetectProvider(baseURL)
}
if apiKey == "" {
apiKey = fallbackAPIKey(provider)
}
if apiKey == "" {
return fmt.Errorf("no API key configured; set --api_key or %s", envVarForProvider(provider))
}
reg := registry.DefaultRegistry
cl := clt.DefaultClient
tools := ai.NewTools(reg, ai.ToolClient(cl))
// Built-in agent capabilities (plan, delegate), reused from the
// agent package so the direct-service fallback matches a real agent.
builtinTools, builtinHandle := agent.Builtins(
agent.Name("chat"),
agent.WithRegistry(reg),
agent.WithClient(cl),
agent.Provider(provider),
agent.Model(modelName),
agent.APIKey(apiKey),
)
s := &session{
provider: provider,
apiKey: apiKey,
tools: tools,
reg: reg,
cl: cl,
hist: ai.NewHistory(50),
builtinTools: builtinTools,
builtinHandle: builtinHandle,
}
s.refreshTools()
// Wrap the tool handler to intercept generate calls
baseHandler := tools.Handler()
wrappedHandler := func(name string, input map[string]any) (any, string) {
if name == "micro_generate_service" {
return s.handleGenerate(input)
}
if result, content, ok := s.builtinHandle(name, input); ok {
return result, content
}
return baseHandler(name, input)
}
opts := []ai.Option{
ai.WithAPIKey(apiKey),
ai.WithToolHandler(wrappedHandler),
}
if modelName != "" {
opts = append(opts, ai.WithModel(modelName))
}
if baseURL != "" {
opts = append(opts, ai.WithBaseURL(baseURL))
}
s.model = ai.New(provider, opts...)
if s.model == nil {
return fmt.Errorf("unknown provider: %s", provider)
}
defer s.cleanup()
// Discover registered agents
hasAgents := s.discoverAgents()
if singlePrompt != "" {
return s.ask(c.Context, singlePrompt)
}
fmt.Println()
fmt.Println(" \033[1mmicro chat\033[0m")
fmt.Println()
fmt.Printf(" Provider \033[36m%s\033[0m\n", provider)
fmt.Printf(" Model \033[36m%s\033[0m\n", s.model.Options().Model)
fmt.Println()
if hasAgents {
fmt.Println(" Agents:")
for name, info := range s.agents {
fmt.Printf(" \033[35m◆\033[0m %s \033[2m(%s)\033[0m\n", name, strings.Join(info.Services, ", "))
}
fmt.Println()
}
fmt.Println(" Tools:")
for _, t := range s.toolList {
fmt.Printf(" \033[32m●\033[0m %s\n", t.OriginalName)
}
if len(s.toolList) == 0 && !hasAgents {
fmt.Println(" \033[33m(no services found)\033[0m")
}
fmt.Println()
fmt.Println(" Type a prompt and press enter. \033[2mCtrl-D or 'exit' to quit.\033[0m")
fmt.Println()
scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 0, 4096), 1024*1024)
for {
fmt.Print("\033[1;36m>\033[0m ")
if !scanner.Scan() {
fmt.Println()
return nil
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if line == "exit" || line == "quit" {
return nil
}
if line == "reset" {
s.hist.Reset()
fmt.Println("\033[2m(history cleared)\033[0m")
fmt.Println()
continue
}
if err := s.ask(c.Context, line); err != nil {
fmt.Printf("\033[31merror:\033[0m %v\n", err)
}
fmt.Println()
}
}
func (s *session) ask(ctx context.Context, prompt string) error {
// If agents are registered, route to them
if len(s.agents) > 0 {
return s.routeToAgent(ctx, prompt)
}
// Fallback: direct service access (no agents)
s.hist.Add("user", prompt)
resp, err := s.model.Generate(ctx, &ai.Request{
Prompt: prompt,
SystemPrompt: s.sysPrompt,
Tools: s.toolList,
Messages: s.hist.Messages(),
})
if err != nil {
return err
}
if resp.Reply != "" {
s.hist.Add("assistant", resp.Reply)
}
if resp.Answer != "" {
s.hist.Add("assistant", resp.Answer)
}
if resp.Reply != "" {
fmt.Println(resp.Reply)
}
for _, tc := range resp.ToolCalls {
if tc.Name == "micro_generate_service" {
continue
}
args, _ := json.Marshal(tc.Input)
fmt.Printf(" \033[33m→\033[0m \033[2m%s\033[0m(%s)\n", tc.Name, args)
if tc.Result != "" {
fmt.Printf(" \033[32m←\033[0m \033[2m%s\033[0m\n", truncateResult(tc.Result))
}
if tc.Error != "" {
fmt.Printf(" \033[31m✗\033[0m %s\n", tc.Error)
}
}
if resp.Answer != "" {
fmt.Println()
fmt.Println(resp.Answer)
}
return nil
}
// routeToAgent dispatches a message to the right agent.
// If there's only one agent, sends directly. Otherwise uses the
// LLM to classify intent and route.
func (s *session) routeToAgent(ctx context.Context, prompt string) error {
// Single agent — call directly via RPC
if len(s.agents) == 1 {
for name := range s.agents {
fmt.Printf(" \033[35m◆\033[0m \033[2m%s\033[0m\n", name)
resp, err := s.callAgent(ctx, name, prompt)
if err != nil {
return err
}
s.printAgentResponse(resp)
return nil
}
}
// Multiple agents — use LLM to route
routeTool := ai.Tool{
Name: "route_to_agent",
OriginalName: "route_to_agent",
Description: "Route a message to a specific agent for handling.",
Properties: map[string]any{
"agent": map[string]any{
"type": "string",
"description": "The agent name to route to",
},
"message": map[string]any{
"type": "string",
"description": "The message to send to the agent",
},
},
}
routerHandler := func(name string, input map[string]any) (any, string) {
agentName, _ := input["agent"].(string)
message, _ := input["message"].(string)
if message == "" {
message = prompt
}
if _, ok := s.agents[agentName]; !ok {
return map[string]string{"error": "unknown agent: " + agentName}, `{"error":"unknown agent"}`
}
fmt.Printf(" \033[35m◆\033[0m \033[2m%s\033[0m\n", agentName)
resp, err := s.callAgent(ctx, agentName, message)
if err != nil {
return map[string]string{"error": err.Error()}, `{"error":"` + err.Error() + `"}`
}
s.printAgentResponse(resp)
result := map[string]any{"agent": agentName, "reply": resp.Reply}
b, _ := json.Marshal(result)
return result, string(b)
}
routerModel := ai.New(s.provider,
ai.WithAPIKey(s.apiKey),
ai.WithToolHandler(routerHandler),
)
resp, err := routerModel.Generate(ctx, &ai.Request{
Prompt: prompt,
SystemPrompt: s.buildRouterPrompt(),
Tools: []ai.Tool{routeTool},
})
if err != nil {
return err
}
if resp.Answer != "" {
fmt.Println()
fmt.Println(resp.Answer)
}
return nil
}
func (s *session) printAgentResponse(resp *agent.Response) {
for _, tc := range resp.ToolCalls {
args, _ := json.Marshal(tc.Input)
fmt.Printf(" \033[33m→\033[0m \033[2m%s\033[0m(%s)\n", tc.Name, args)
if tc.Result != "" {
fmt.Printf(" \033[32m←\033[0m \033[2m%s\033[0m\n", truncateResult(tc.Result))
}
}
if resp.Reply != "" {
fmt.Println()
fmt.Println(resp.Reply)
}
}
func (s *session) cleanup() {
for _, p := range s.procs {
if p.Process != nil {
p.Process.Kill()
}
}
}
func fallbackAPIKey(provider string) string {
if v := os.Getenv(envVarForProvider(provider)); v != "" {
return v
}
return ""
}
func envVarForProvider(provider string) string {
switch provider {
case "anthropic":
return "ANTHROPIC_API_KEY"
case "openai":
return "OPENAI_API_KEY"
case "gemini":
return "GEMINI_API_KEY"
case "groq":
return "GROQ_API_KEY"
case "mistral":
return "MISTRAL_API_KEY"
case "together":
return "TOGETHER_API_KEY"
case "atlascloud":
return "ATLASCLOUD_API_KEY"
default:
return "MICRO_AI_API_KEY"
}
}
func truncateResult(s string) string {
if len(s) <= 200 {
return s
}
return s[:200] + "..."
}
+80
View File
@@ -0,0 +1,80 @@
// Package agent registers the 'micro agent' CLI commands.
package agent
import (
"encoding/json"
"fmt"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/registry"
)
func init() {
cmd.Register(&cli.Command{
Name: "agent",
Usage: "Manage AI agents",
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List registered agents",
Action: func(c *cli.Context) error {
svcs, err := registry.ListServices()
if err != nil {
return err
}
found := false
for _, svc := range svcs {
records, err := registry.GetService(svc.Name)
if err != nil || len(records) == 0 {
continue
}
meta := records[0].Metadata
if meta == nil || meta["type"] != "agent" {
if len(records[0].Nodes) > 0 {
meta = records[0].Nodes[0].Metadata
}
if meta == nil || meta["type"] != "agent" {
continue
}
}
found = true
services := meta["services"]
if services == "" {
services = "(all)"
}
fmt.Printf(" \033[35m◆\033[0m %-20s manages: %s\n", svc.Name, services)
}
if !found {
fmt.Println(" No agents registered.")
fmt.Println()
fmt.Println(" Start an agent with:")
fmt.Println(" micro run (if agents are part of your project)")
}
return nil
},
},
{
Name: "describe",
Usage: "Describe an agent",
ArgsUsage: "[name]",
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
return fmt.Errorf("usage: micro agent describe [name]")
}
records, err := registry.GetService(name)
if err != nil {
return err
}
if len(records) == 0 {
return fmt.Errorf("agent %s not found", name)
}
b, _ := json.MarshalIndent(records[0], "", " ")
fmt.Println(string(b))
return nil
},
},
},
})
}
+7 -7
View File
@@ -72,7 +72,7 @@ func Build(c *cli.Context) error {
}
}
fmt.Printf("\n✓ Built to %s\n", outDir)
fmt.Printf("\n \033[32m✓\033[0m Built to \033[36m%s\033[0m\n", outDir)
return nil
}
@@ -83,7 +83,7 @@ func buildService(name, dir, outDir, targetOS, targetArch string) error {
}
outPath := filepath.Join(outDir, binName)
fmt.Printf("Building %s (%s/%s)...\n", name, targetOS, targetArch)
fmt.Printf(" Building \033[36m%s (%s/%s)...\n", name, targetOS, targetArch)
// Build command
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
@@ -100,7 +100,7 @@ func buildService(name, dir, outDir, targetOS, targetArch string) error {
return fmt.Errorf("go build failed: %w", err)
}
fmt.Printf(" %s\n", outPath)
fmt.Printf(" \033[32m✓\033[0m %s\n", outPath)
return nil
}
@@ -179,7 +179,7 @@ func buildDockerImage(name, dir string, port int, tag, registry string, push boo
imageName = registry + "/" + imageName
}
fmt.Printf("Building %s...\n", imageName)
fmt.Printf(" Building \033[36m%s...\n", imageName)
buildCmd := exec.Command("docker", "build", "-t", imageName, dir)
buildCmd.Stdout = os.Stdout
@@ -188,7 +188,7 @@ func buildDockerImage(name, dir string, port int, tag, registry string, push boo
return fmt.Errorf("docker build failed: %w", err)
}
fmt.Printf(" Built %s\n", imageName)
fmt.Printf(" \033[32m✓\033[0m Built %s\n", imageName)
if push {
fmt.Printf("Pushing %s...\n", imageName)
@@ -198,7 +198,7 @@ func buildDockerImage(name, dir string, port int, tag, registry string, push boo
if err := pushCmd.Run(); err != nil {
return fmt.Errorf("docker push failed: %w", err)
}
fmt.Printf(" Pushed %s\n", imageName)
fmt.Printf(" \033[32m✓\033[0m Pushed %s\n", imageName)
}
return nil
@@ -268,7 +268,7 @@ func Compose(c *cli.Context) error {
return fmt.Errorf("failed to write docker-compose.yml: %w", err)
}
fmt.Printf(" Generated %s\n", output)
fmt.Printf(" \033[32m✓\033[0m Generated %s\n", output)
return nil
}
+52 -3
View File
@@ -17,6 +17,7 @@ import (
"go-micro.dev/v5/cmd/micro/cli/util"
// Import packages that register commands via init()
_ "go-micro.dev/v5/cmd/micro/cli/agent"
_ "go-micro.dev/v5/cmd/micro/cli/build"
_ "go-micro.dev/v5/cmd/micro/cli/deploy"
_ "go-micro.dev/v5/cmd/micro/cli/init"
@@ -39,9 +40,38 @@ func genProtoHandler(c *cli.Context) error {
func init() {
cmd.Register([]*cli.Command{
{
Name: "new",
Usage: "Create a new service",
Name: "new",
Usage: "Create a new service",
ArgsUsage: "[name]",
UsageText: ` micro new helloworld # scaffold a single service
micro new --prompt "a todo list with tasks" # AI-design multiple services
micro new --prompt "add tags to the task service" # extend existing services`,
Action: new.Run,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "no-mcp",
Usage: "Disable MCP gateway integration in generated code",
},
&cli.StringFlag{
Name: "template",
Usage: "Service template: default, crud, pubsub, api",
},
&cli.StringFlag{
Name: "prompt",
Usage: "Describe the system to generate (uses AI to design & build services with real business logic)",
EnvVars: []string{"MICRO_NEW_PROMPT"},
},
&cli.StringFlag{
Name: "provider",
Usage: "AI provider for --prompt (anthropic, openai, gemini, atlascloud, groq, mistral, together)",
EnvVars: []string{"MICRO_AI_PROVIDER"},
},
&cli.StringFlag{
Name: "api_key",
Usage: "API key for --prompt (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)",
EnvVars: []string{"MICRO_AI_API_KEY"},
},
},
},
{
Name: "gen",
@@ -71,6 +101,18 @@ func init() {
{
Name: "call",
Usage: "Call a service",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "header",
Aliases: []string{"H"},
Usage: "Set request headers (can be used multiple times): --header 'Key:Value'",
},
&cli.StringSliceFlag{
Name: "metadata",
Aliases: []string{"m"},
Usage: "Set request metadata (can be used multiple times): --metadata 'Key:Value'",
},
},
Action: func(ctx *cli.Context) error {
args := ctx.Args()
@@ -86,9 +128,16 @@ func init() {
request = args.Get(2)
}
// Create context with metadata if provided
// Note: This is for the direct 'micro call' command.
// Dynamic service calls (e.g., 'micro helloworld call') are handled in CallService.
callCtx := context.TODO()
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("metadata"))
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("header"))
req := client.NewRequest(service, endpoint, &bytes.Frame{Data: []byte(request)})
var rsp bytes.Frame
err := client.Call(context.TODO(), req, &rsp)
err := client.Call(callCtx, req, &rsp)
if err != nil {
return err
}
+4 -1
View File
@@ -103,7 +103,10 @@ func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
remotePath = defaultRemotePath
}
fmt.Printf("Deploying to %s...\n\n", target)
fmt.Println()
fmt.Println(" \033[1mmicro deploy\033[0m")
fmt.Println()
fmt.Printf(" Target \033[36m%s\033[0m\n\n", target)
// Early validation: Check if the requested service exists before SSH checks
filterService := c.String("service")
+856
View File
@@ -0,0 +1,856 @@
// Package generate implements AI-powered service generation for go-micro.
// It uses an LLM to design service architecture and generate handler code
// with real business logic, then compiles and fixes errors iteratively.
package generate
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/atlascloud"
_ "go-micro.dev/v5/ai/gemini"
_ "go-micro.dev/v5/ai/groq"
_ "go-micro.dev/v5/ai/mistral"
_ "go-micro.dev/v5/ai/openai"
_ "go-micro.dev/v5/ai/together"
)
const designPrompt = `You are a Go microservices architect using the go-micro framework.
Given a system description, design the services needed.
Return ONLY valid JSON:
{
"services": [
{
"name": "service-name",
"description": "What this service does",
"fields": [
{"name": "field_name", "type": "string", "description": "What this field is"}
],
"endpoints": [
{"name": "EndpointName", "description": "What this endpoint does", "example": "{\"key\": \"value\"}"}
]
}
]
}
Rules:
- Service names are lowercase, hyphenated, WITHOUT a "-service" suffix (e.g. "task" not "task-service", "shipping" not "shipping-service")
- Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List
- Add 1-3 custom endpoints for real business logic (e.g. PlaceOrder, CheckInventory)
- Field types: string, int64, bool, float64
- Every service needs id (string), created (int64), updated (int64) fields
- Endpoint names are PascalCase
- Examples should be realistic JSON
- 2-4 services max, focused on the domain
- Keep services small and focused — one concern per service, max 5-8 fields
- Services don't call each other; an AI agent orchestrates across them`
const designPromptWithExisting = `You are a Go microservices architect using the go-micro framework.
The user has an EXISTING system with services already running. They want to extend or modify it.
Existing services:
%s
Given the user's request, return the COMPLETE set of services (existing + new/modified).
For existing services the user hasn't asked to change, return them as-is.
For new or modified services, include the full specification.
Return ONLY valid JSON:
{
"services": [
{
"name": "service-name",
"description": "What this service does",
"fields": [
{"name": "field_name", "type": "string", "description": "What this field is"}
],
"endpoints": [
{"name": "EndpointName", "description": "What this endpoint does", "example": "{\"key\": \"value\"}"}
]
}
]
}
Rules:
- Service names are lowercase, hyphenated, WITHOUT a "-service" suffix (e.g. "task" not "task-service", "shipping" not "shipping-service")
- Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List
- Add custom endpoints for real business logic
- Field types: string, int64, bool, float64
- Every service needs id (string), created (int64), updated (int64) fields
- Endpoint names are PascalCase
- Examples should be realistic JSON
- Keep existing services unless the user explicitly asks to change them`
const handlerPrompt = `You are a Go developer writing a handler for a go-micro service.
Generate a COMPLETE, COMPILABLE Go handler file.
The handler must:
1. Use package "handler"
2. Import the proto package as: pb "%s/proto"
3. Import go-micro logger as: log "go-micro.dev/v5/logger"
4. Import "github.com/google/uuid" for ID generation
5. Use "go-micro.dev/v5/store" for persistent storage (NOT in-memory maps)
6. Include REAL business logic — not just CRUD store operations
7. Every exported method must have a doc comment explaining what it does
8. Every method must have an @example tag with realistic JSON input
9. Handle edge cases, validation, and return meaningful errors
10. Keep the file under 200 lines — be concise, no boilerplate
For storage, use the go-micro store package:
import "go-micro.dev/v5/store"
import "encoding/json"
// In the struct:
store store.Store
// In the constructor:
func New() *%s { return &%s{store: store.DefaultStore} }
// Write a record:
data, _ := json.Marshal(record)
store.Write(&store.Record{Key: "prefix/" + id, Value: data})
// Read a record:
recs, err := store.Read("prefix/" + id)
json.Unmarshal(recs[0].Value, &record)
// List keys:
keys, _ := store.List(store.ListPrefix("prefix/"))
// Delete:
store.Delete("prefix/" + id)
Do NOT use sync.Mutex or in-memory maps. Use store for all data.
The struct name is %s.
The constructor is func New() *%s.
Here is the proto definition:
%s
Here is what each endpoint should do:
%s
Return ONLY the Go code. No markdown, no explanation. Just the .go file content starting with "package handler".`
// ServiceDesign is the LLM's output.
type ServiceDesign struct {
Services []ServiceSpec `json:"services"`
}
type ServiceSpec struct {
Name string `json:"name"`
Description string `json:"description"`
Fields []FieldSpec `json:"fields"`
Endpoints []EndpointSpec `json:"endpoints"`
}
type FieldSpec struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
}
type EndpointSpec struct {
Name string `json:"name"`
Description string `json:"description"`
Example string `json:"example"`
}
// Design calls an LLM to design services from a prompt.
// If baseDir contains existing services, they are included as context
// so the LLM extends the system rather than redesigning from scratch.
func Design(ctx context.Context, provider, apiKey, model, baseDir, prompt string) (*ServiceDesign, error) {
m := newModel(provider, apiKey, model)
if m == nil {
return nil, fmt.Errorf("unknown provider: %s", provider)
}
existing := discoverExisting(baseDir)
var sysPrompt, userPrompt string
if len(existing) > 0 {
sysPrompt = fmt.Sprintf(designPromptWithExisting, existing)
userPrompt = fmt.Sprintf("Extend or modify the system: %s", prompt)
} else {
sysPrompt = designPrompt
userPrompt = fmt.Sprintf("Design a microservices system for: %s", prompt)
}
sp := startSpinner("designing services...")
designCtx, designCancel := context.WithTimeout(ctx, 60*time.Second)
defer designCancel()
resp, err := m.Generate(designCtx, &ai.Request{
Prompt: userPrompt,
SystemPrompt: sysPrompt,
})
sp.Stop()
if err != nil {
return nil, fmt.Errorf("design failed: %w", err)
}
reply := firstNonEmpty(resp.Answer, resp.Reply)
reply = extractJSON(reply)
var design ServiceDesign
if err := json.Unmarshal([]byte(reply), &design); err != nil {
return nil, fmt.Errorf("failed to parse design: %w\nResponse: %s", err, reply)
}
if len(design.Services) == 0 {
return nil, fmt.Errorf("no services designed")
}
return &design, nil
}
// discoverExisting scans a directory for existing go-micro services
// and returns a summary string for inclusion in the design prompt.
func discoverExisting(baseDir string) string {
entries, err := os.ReadDir(baseDir)
if err != nil {
return ""
}
var summaries []string
for _, e := range entries {
if !e.IsDir() {
continue
}
svcDir := filepath.Join(baseDir, e.Name())
// Look for proto files as indicator of a go-micro service
protoDir := filepath.Join(svcDir, "proto")
protos, err := filepath.Glob(filepath.Join(protoDir, "*.proto"))
if err != nil || len(protos) == 0 {
continue
}
proto := readFile(protos[0])
if proto == "" {
continue
}
summaries = append(summaries, fmt.Sprintf("### %s\nProto:\n```\n%s\n```", e.Name(), proto))
}
return strings.Join(summaries, "\n\n")
}
// Generate creates go-micro service directories from a design.
// If a service directory already exists, it skips structure generation
// but regenerates the handler (allowing iterative improvement).
func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provider, apiKey, model string) error {
m := newModel(provider, apiKey, model)
for i, svc := range design.Services {
if ctx.Err() != nil {
return ctx.Err()
}
svcDir := filepath.Join(baseDir, svc.Name)
handlerFile := filepath.Join(svcDir, "handler", svc.Name+".go")
protoFile := filepath.Join(svcDir, "proto", svc.Name+".proto")
// Snapshot proto hash before structure generation
protoBefore := fileHash(protoFile)
fmt.Printf(" \033[2m[%d/%d]\033[0m generating \033[36m%s\033[0m...\n", i+1, len(design.Services), svc.Name)
// Step 1: Generate proto (deterministic — from design spec)
if err := generateStructure(svcDir, svc); err != nil {
return fmt.Errorf("structure %s: %w", svc.Name, err)
}
protoAfter := fileHash(protoFile)
protoChanged := protoBefore != protoAfter
// If proto unchanged and handler unmodified, nothing to do
if !protoChanged && protoBefore != "" && !handlerModified(svcDir, handlerFile) {
fmt.Printf(" \033[32m✓\033[0m %s \033[2m(unchanged)\033[0m\n", svc.Name)
continue
}
// Step 2: Run go mod tidy + make proto to get compiled proto
runIn(svcDir, "go", "mod", "tidy")
runIn(svcDir, "make", "proto")
// Step 3: Generate handler with business logic (LLM)
proto := readFile(protoFile)
if err := generateHandler(ctx, m, svcDir, svc, proto); err != nil {
return fmt.Errorf("handler %s: %w", svc.Name, err)
}
// Step 4: Compile-fix loop
if err := compileFix(ctx, m, svcDir, svc.Name, 3); err != nil {
fmt.Printf(" \033[33m⚠\033[0m %s has compile errors (may need manual fix)\n", svc.Name)
} else {
fmt.Printf(" \033[32m✓\033[0m %s\n", svc.Name)
}
// Record final handler hash (after any compile fixes)
recordHandlerHash(svcDir, handlerFile)
}
// Generate an agent that manages all the services
var svcNames []string
for _, svc := range design.Services {
svcNames = append(svcNames, svc.Name)
}
if err := generateAgent(baseDir, design, svcNames); err != nil {
fmt.Printf(" \033[33m⚠\033[0m agent generation failed: %v\n", err)
}
return nil
}
// generateStructure creates the proto, main.go, go.mod, Makefile.
// If the directory already exists, only regenerates the proto
// (handler will be regenerated separately by the LLM).
func generateStructure(dir string, svc ServiceSpec) error {
exists := false
if _, err := os.Stat(dir); err == nil {
exists = true
}
os.MkdirAll(filepath.Join(dir, "handler"), 0755)
os.MkdirAll(filepath.Join(dir, "proto"), 0755)
name := svc.Name
titleName := toTitle(name)
dehyphen := strings.ReplaceAll(name, "-", "")
// Regenerate proto unless user has modified it
protoPath := filepath.Join(dir, "proto", name+".proto")
if !fileModified(dir, "proto_hash", protoPath) {
writeFile(protoPath, buildProto(dehyphen, titleName, svc))
recordFileHash(dir, "proto_hash", protoPath)
} else {
fmt.Printf(" \033[2mkeeping %s proto (modified)\033[0m\n", name)
}
// Only write structural files if directory is new
if !exists {
writeFile(filepath.Join(dir, "main.go"), buildMain(name, titleName))
writeFile(filepath.Join(dir, "Makefile"),
"GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n")
writeFile(filepath.Join(dir, "go.mod"),
fmt.Sprintf("module %s\n\ngo 1.24\n\nrequire go-micro.dev/v5 v5.24.0\n", name))
writeFile(filepath.Join(dir, ".gitignore"),
fmt.Sprintf("%s\n.micro\n", name))
}
// Placeholder handler so go mod tidy works (will be overwritten by LLM)
handlerPath := filepath.Join(dir, "handler", name+".go")
if _, err := os.Stat(handlerPath); os.IsNotExist(err) {
writeFile(handlerPath,
fmt.Sprintf("package handler\n\ntype %s struct{}\n\nfunc New() *%s { return &%s{} }\n", titleName, titleName, titleName))
recordHandlerHash(dir, handlerPath)
}
return nil
}
// generateHandler asks the LLM to write the handler with business logic.
// If the handler exists and the user has modified it since generation,
// it is left untouched.
func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpec, proto string) error {
if m == nil {
return nil // no LLM — keep the placeholder
}
handlerFile := filepath.Join(dir, "handler", svc.Name+".go")
if handlerModified(dir, handlerFile) {
fmt.Printf(" \033[2mkeeping %s handler (modified)\033[0m\n", svc.Name)
return nil
}
titleName := toTitle(svc.Name)
// Build endpoint descriptions
var epDescs []string
for _, ep := range svc.Endpoints {
epDescs = append(epDescs, fmt.Sprintf("- %s: %s (example input: %s)", ep.Name, ep.Description, ep.Example))
}
prompt := fmt.Sprintf(handlerPrompt,
svc.Name, titleName, titleName, titleName, titleName, proto, strings.Join(epDescs, "\n"))
sp := startSpinner(fmt.Sprintf("writing %s handler...", svc.Name))
genCtx, genCancel := context.WithTimeout(ctx, 90*time.Second)
defer genCancel()
resp, err := m.Generate(genCtx, &ai.Request{
Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic.", svc.Name),
SystemPrompt: prompt,
})
sp.Stop()
if err != nil {
return err
}
code := firstNonEmpty(resp.Answer, resp.Reply)
code = extractCode(code)
if !strings.HasPrefix(strings.TrimSpace(code), "package") {
return fmt.Errorf("LLM did not return valid Go code")
}
if isTruncated(code) {
fmt.Printf(" \033[33m→\033[0m response truncated, retrying...\n")
sp = startSpinner(fmt.Sprintf("rewriting %s handler...", svc.Name))
retryCtx, retryCancel := context.WithTimeout(ctx, 90*time.Second)
defer retryCancel()
resp, err = m.Generate(retryCtx, &ai.Request{
Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic. Keep it concise — no more than 200 lines.", svc.Name),
SystemPrompt: prompt,
})
sp.Stop()
if err != nil {
return err
}
code = firstNonEmpty(resp.Answer, resp.Reply)
code = extractCode(code)
}
if !strings.HasPrefix(strings.TrimSpace(code), "package") {
return fmt.Errorf("LLM did not return valid Go code")
}
writeFile(handlerFile, code)
recordHandlerHash(dir, handlerFile)
return nil
}
// compileFix tries to compile, and if it fails, sends the error to
// the LLM to fix. Up to maxAttempts iterations.
func compileFix(ctx context.Context, m ai.Model, dir, name string, maxAttempts int) error {
for attempt := 0; attempt < maxAttempts; attempt++ {
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err == nil {
return nil // compiles!
}
if m == nil {
return fmt.Errorf("compile failed: %s", string(out))
}
// Read current handler
handlerPath := filepath.Join(dir, "handler", name+".go")
currentCode := readFile(handlerPath)
sp := startSpinner(fmt.Sprintf("fixing compile errors (attempt %d/%d)...", attempt+1, maxAttempts))
fixCtx, fixCancel := context.WithTimeout(ctx, 60*time.Second)
resp, fixErr := m.Generate(fixCtx, &ai.Request{
Prompt: fmt.Sprintf("This Go code has compile errors. Fix ALL of them and return the COMPLETE corrected file.\n\nErrors:\n%s\n\nCode:\n%s",
string(out), currentCode),
SystemPrompt: "You are a Go expert. Return ONLY the corrected Go code. No markdown, no explanation. Start with 'package handler'.",
})
fixCancel()
sp.Stop()
if fixErr != nil {
return fmt.Errorf("fix attempt failed: %w", fixErr)
}
fixed := firstNonEmpty(resp.Answer, resp.Reply)
fixed = extractCode(fixed)
if strings.HasPrefix(strings.TrimSpace(fixed), "package") && !isTruncated(fixed) {
writeFile(handlerPath, fixed)
}
}
// Final check
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("still fails after %d attempts: %s", maxAttempts, string(out))
}
return nil
}
func newModel(provider, apiKey, model string) ai.Model {
if provider == "" {
provider = ai.AutoDetectProvider("")
}
var opts []ai.Option
opts = append(opts, ai.WithAPIKey(apiKey))
if model != "" {
opts = append(opts, ai.WithModel(model))
}
return ai.New(provider, opts...)
}
func buildProto(dehyphen, titleName string, svc ServiceSpec) string {
var b strings.Builder
b.WriteString(fmt.Sprintf("syntax = \"proto3\";\n\npackage %s;\n\noption go_package = \"./proto;%s\";\n\n", dehyphen, dehyphen))
b.WriteString(fmt.Sprintf("service %s {\n", titleName))
for _, ep := range svc.Endpoints {
b.WriteString(fmt.Sprintf("\trpc %s(%sRequest) returns (%sResponse) {}\n", ep.Name, ep.Name, ep.Name))
}
b.WriteString("}\n\n")
// Record message
b.WriteString(fmt.Sprintf("message %sRecord {\n", titleName))
for i, f := range svc.Fields {
b.WriteString(fmt.Sprintf("\t%s %s = %d; // %s\n", protoType(f.Type), f.Name, i+1, f.Description))
}
b.WriteString("}\n\n")
// Request/response for each endpoint
for _, ep := range svc.Endpoints {
switch ep.Name {
case "Create":
b.WriteString(fmt.Sprintf("message CreateRequest {\n"))
n := 1
for _, f := range svc.Fields {
if f.Name == "id" || f.Name == "created" || f.Name == "updated" {
continue
}
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
n++
}
b.WriteString(fmt.Sprintf("}\n\nmessage CreateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
case "Read":
b.WriteString(fmt.Sprintf("message ReadRequest {\n\tstring id = 1;\n}\n\nmessage ReadResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
case "Update":
b.WriteString("message UpdateRequest {\n\tstring id = 1;\n")
n := 2
for _, f := range svc.Fields {
if f.Name == "id" || f.Name == "created" || f.Name == "updated" {
continue
}
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
n++
}
b.WriteString(fmt.Sprintf("}\n\nmessage UpdateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
case "Delete":
b.WriteString(fmt.Sprintf("message DeleteRequest {\n\tstring id = 1;\n}\n\nmessage DeleteResponse {\n\tbool deleted = 1;\n}\n\n"))
case "List":
b.WriteString(fmt.Sprintf("message ListRequest {\n\tint64 limit = 1;\n\tint64 offset = 2;\n\tstring query = 3;\n}\n\nmessage ListResponse {\n\trepeated %sRecord records = 1;\n\tint64 total = 2;\n}\n\n", titleName))
default:
// Custom endpoint — use all fields as input, record as output
b.WriteString(fmt.Sprintf("message %sRequest {\n", ep.Name))
n := 1
for _, f := range svc.Fields {
if f.Name == "created" || f.Name == "updated" {
continue
}
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
n++
}
b.WriteString(fmt.Sprintf("}\n\nmessage %sResponse {\n\t%sRecord record = 1;\n\tstring message = 2;\n\tbool success = 3;\n}\n\n", ep.Name, titleName))
}
}
return b.String()
}
func buildMain(name, titleName string) string {
svcName := strings.TrimSuffix(name, "-service")
return fmt.Sprintf(`package main
import (
"%s/handler"
pb "%s/proto"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
func main() {
service := micro.New("%s",
mcp.WithMCP(":0"),
)
service.Init()
pb.Register%sHandler(service.Server(), handler.New())
service.Run()
}
`, name, name, svcName, titleName)
}
func generateAgent(baseDir string, design *ServiceDesign, svcNames []string) error {
agentName := "agent"
agentDir := filepath.Join(baseDir, agentName)
if _, err := os.Stat(agentDir); err == nil {
return nil // already exists
}
os.MkdirAll(agentDir, 0755)
// Build a description of all services for the agent prompt
var svcDescs []string
for _, svc := range design.Services {
var eps []string
for _, ep := range svc.Endpoints {
eps = append(eps, ep.Name)
}
svcDescs = append(svcDescs, fmt.Sprintf("- %s: %s (%s)", svc.Name, svc.Description, strings.Join(eps, ", ")))
}
prompt := fmt.Sprintf("You manage these services:\\n%s\\nUse the available tools to fulfill requests. Be helpful and concise.", strings.Join(svcDescs, "\\n"))
quoted := strings.Join(svcNames, `", "`)
writeFile(filepath.Join(agentDir, "main.go"), fmt.Sprintf(`package main
import (
"os"
"go-micro.dev/v5"
)
func main() {
agent := micro.NewAgent("agent",
micro.AgentServices("%s"),
micro.AgentPrompt("%s"),
micro.AgentProvider(os.Getenv("MICRO_AI_PROVIDER")),
micro.AgentAPIKey(os.Getenv("MICRO_AI_API_KEY")),
)
agent.Init()
agent.Run()
}
`, quoted, prompt))
writeFile(filepath.Join(agentDir, "go.mod"),
fmt.Sprintf("module %s\n\ngo 1.24\n\nrequire go-micro.dev/v5 v5.25.0\n", agentName))
runIn(agentDir, "go", "mod", "tidy")
fmt.Printf(" \033[35m◆\033[0m agent \033[2m(manages: %s)\033[0m\n", strings.Join(svcNames, ", "))
return nil
}
func extractJSON(s string) string {
if i := strings.Index(s, "```json"); i >= 0 {
s = s[i+7:]
if j := strings.Index(s, "```"); j >= 0 {
return strings.TrimSpace(s[:j])
}
}
if i := strings.Index(s, "```"); i >= 0 {
s = s[i+3:]
if j := strings.Index(s, "```"); j >= 0 {
return strings.TrimSpace(s[:j])
}
}
if i := strings.Index(s, "{"); i >= 0 {
depth := 0
for j := i; j < len(s); j++ {
switch s[j] {
case '{':
depth++
case '}':
depth--
if depth == 0 {
return s[i : j+1]
}
}
}
}
return s
}
func extractCode(s string) string {
if i := strings.Index(s, "```go"); i >= 0 {
s = s[i+5:]
if j := strings.Index(s, "```"); j >= 0 {
return strings.TrimSpace(s[:j])
}
}
if i := strings.Index(s, "```"); i >= 0 {
s = s[i+3:]
if j := strings.Index(s, "```"); j >= 0 {
return strings.TrimSpace(s[:j])
}
}
// Try to find raw package declaration
if i := strings.Index(s, "package "); i >= 0 {
return strings.TrimSpace(s[i:])
}
return strings.TrimSpace(s)
}
func isTruncated(code string) bool {
trimmed := strings.TrimSpace(code)
if len(trimmed) == 0 {
return true
}
// Valid Go files end with a closing brace
if trimmed[len(trimmed)-1] != '}' {
return true
}
// Check balanced braces
depth := 0
for _, c := range trimmed {
switch c {
case '{':
depth++
case '}':
depth--
}
}
return depth != 0
}
func protoType(t string) string {
switch t {
case "int64":
return "int64"
case "int32":
return "int32"
case "bool":
return "bool"
case "float64":
return "double"
default:
return "string"
}
}
func toTitle(s string) string {
words := strings.FieldsFunc(s, func(r rune) bool { return r == '-' || r == '_' || r == ' ' })
for i, w := range words {
if len(w) > 0 {
words[i] = strings.ToUpper(w[:1]) + w[1:]
}
}
return strings.Join(words, "")
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
return v
}
}
return ""
}
func readFile(path string) string {
b, _ := os.ReadFile(path)
return string(b)
}
func writeFile(path, content string) {
os.WriteFile(path, []byte(content), 0644)
}
func runIn(dir string, name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "PATH="+os.Getenv("PATH")+":"+os.Getenv("GOPATH")+"/bin:"+os.Getenv("HOME")+"/go/bin")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
type spinner struct {
msg string
stop chan struct{}
done sync.WaitGroup
}
func isTTY() bool {
fi, err := os.Stdout.Stat()
if err != nil {
return false
}
return fi.Mode()&os.ModeCharDevice != 0
}
func startSpinner(msg string) *spinner {
s := &spinner{msg: msg, stop: make(chan struct{})}
if !isTTY() {
fmt.Printf(" %s\n", msg)
return s
}
s.done.Add(1)
go func() {
defer s.done.Done()
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
i := 0
t := time.NewTicker(100 * time.Millisecond)
defer t.Stop()
for {
select {
case <-s.stop:
fmt.Printf("\r\033[K")
return
case <-t.C:
fmt.Printf("\r %s %s", frames[i%len(frames)], msg)
i++
}
}
}()
return s
}
func (s *spinner) Stop() {
close(s.stop)
s.done.Wait()
}
func fileHash(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
func metaPath(svcDir string) string {
return filepath.Join(svcDir, ".micro")
}
func readMeta(svcDir string) map[string]string {
m := make(map[string]string)
b, err := os.ReadFile(metaPath(svcDir))
if err != nil {
return m
}
json.Unmarshal(b, &m)
return m
}
func writeMeta(svcDir string, m map[string]string) {
b, _ := json.MarshalIndent(m, "", " ")
os.WriteFile(metaPath(svcDir), b, 0644)
}
func fileModified(svcDir, key, path string) bool {
meta := readMeta(svcDir)
savedHash, ok := meta[key]
if !ok {
return false
}
return fileHash(path) != savedHash
}
func recordFileHash(svcDir, key, path string) {
meta := readMeta(svcDir)
meta[key] = fileHash(path)
writeMeta(svcDir, meta)
}
func handlerModified(svcDir, handlerFile string) bool {
return fileModified(svcDir, "handler_hash", handlerFile)
}
func recordHandlerHash(svcDir, handlerFile string) {
recordFileHash(svcDir, "handler_hash", handlerFile)
}
+421
View File
@@ -0,0 +1,421 @@
package generate
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestToTitle(t *testing.T) {
tests := []struct {
in, want string
}{
{"order-service", "OrderService"},
{"task", "Task"},
{"inventory_item", "InventoryItem"},
{"hello world", "HelloWorld"},
{"a-b-c", "ABC"},
{"already", "Already"},
}
for _, tt := range tests {
if got := toTitle(tt.in); got != tt.want {
t.Errorf("toTitle(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestProtoType(t *testing.T) {
tests := []struct {
in, want string
}{
{"string", "string"},
{"int64", "int64"},
{"int32", "int32"},
{"bool", "bool"},
{"float64", "double"},
{"unknown", "string"},
{"", "string"},
}
for _, tt := range tests {
if got := protoType(tt.in); got != tt.want {
t.Errorf("protoType(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestFirstNonEmpty(t *testing.T) {
if got := firstNonEmpty("", "", "c"); got != "c" {
t.Errorf("got %q, want %q", got, "c")
}
if got := firstNonEmpty("a", "b"); got != "a" {
t.Errorf("got %q, want %q", got, "a")
}
if got := firstNonEmpty("", ""); got != "" {
t.Errorf("got %q, want %q", got, "")
}
}
func TestExtractJSON(t *testing.T) {
tests := []struct {
name, in, want string
}{
{
"fenced json",
"Here's the design:\n```json\n{\"services\": []}\n```\nDone.",
`{"services": []}`,
},
{
"fenced no lang",
"```\n{\"a\": 1}\n```",
`{"a": 1}`,
},
{
"raw json",
`some text {"key": "val"} trailing`,
`{"key": "val"}`,
},
{
"nested braces",
`{"a": {"b": 1}}`,
`{"a": {"b": 1}}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractJSON(tt.in)
if got != tt.want {
t.Errorf("extractJSON() = %q, want %q", got, tt.want)
}
})
}
}
func TestExtractCode(t *testing.T) {
tests := []struct {
name, in string
wantPrefix string
}{
{
"go fence",
"Here:\n```go\npackage handler\n\nfunc Foo() {}\n```\nDone.",
"package handler",
},
{
"generic fence",
"```\npackage main\n```",
"package main",
},
{
"raw code",
"Sure, here's the code:\npackage handler\n\ntype X struct{}",
"package handler",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractCode(tt.in)
if !strings.HasPrefix(got, tt.wantPrefix) {
t.Errorf("extractCode() = %q, want prefix %q", got, tt.wantPrefix)
}
})
}
}
func TestBuildProto(t *testing.T) {
svc := ServiceSpec{
Name: "task-service",
Description: "Manages tasks",
Fields: []FieldSpec{
{Name: "id", Type: "string", Description: "Task ID"},
{Name: "title", Type: "string", Description: "Task title"},
{Name: "done", Type: "bool", Description: "Completion status"},
{Name: "created", Type: "int64", Description: "Created timestamp"},
{Name: "updated", Type: "int64", Description: "Updated timestamp"},
},
Endpoints: []EndpointSpec{
{Name: "Create", Description: "Create a task"},
{Name: "Read", Description: "Get a task"},
{Name: "Update", Description: "Update a task"},
{Name: "Delete", Description: "Delete a task"},
{Name: "List", Description: "List tasks"},
{Name: "ToggleComplete", Description: "Toggle completion"},
},
}
proto := buildProto("taskservice", "TaskService", svc)
checks := []string{
`syntax = "proto3"`,
`package taskservice`,
`service TaskService`,
`rpc Create(CreateRequest) returns (CreateResponse)`,
`rpc ToggleComplete(ToggleCompleteRequest) returns (ToggleCompleteResponse)`,
`message TaskServiceRecord`,
`string title = 2`,
`bool done = 3`,
`message CreateRequest`,
`message ReadRequest`,
`message DeleteRequest`,
`message ListRequest`,
`message ToggleCompleteRequest`,
}
for _, c := range checks {
if !strings.Contains(proto, c) {
t.Errorf("buildProto() missing %q", c)
}
}
// Create should not include id, created, updated
createIdx := strings.Index(proto, "message CreateRequest")
createEnd := strings.Index(proto[createIdx:], "}")
createBlock := proto[createIdx : createIdx+createEnd]
for _, skip := range []string{"string id", "int64 created", "int64 updated"} {
if strings.Contains(createBlock, skip) {
t.Errorf("CreateRequest should not contain %q", skip)
}
}
}
func TestBuildMain(t *testing.T) {
// New naming: no -service suffix
main := buildMain("order", "Order")
checks := []string{
`"order/handler"`,
`pb "order/proto"`,
`micro.New("order"`,
`pb.RegisterOrderHandler`,
`handler.New()`,
}
for _, c := range checks {
if !strings.Contains(main, c) {
t.Errorf("buildMain(order) missing %q", c)
}
}
// Legacy naming: -service suffix stripped
main = buildMain("order-service", "OrderService")
checks = []string{
`"order-service/handler"`,
`pb "order-service/proto"`,
`micro.New("order"`,
`pb.RegisterOrderServiceHandler`,
`handler.New()`,
}
for _, c := range checks {
if !strings.Contains(main, c) {
t.Errorf("buildMain() missing %q", c)
}
}
}
func TestHandlerModifiedTracking(t *testing.T) {
dir := t.TempDir()
handlerDir := filepath.Join(dir, "handler")
os.MkdirAll(handlerDir, 0755)
handlerFile := filepath.Join(handlerDir, "test.go")
// No .micro file → not modified
os.WriteFile(handlerFile, []byte("package handler\n"), 0644)
if handlerModified(dir, handlerFile) {
t.Error("expected not modified when no .micro exists")
}
// Record hash → not modified
recordHandlerHash(dir, handlerFile)
if handlerModified(dir, handlerFile) {
t.Error("expected not modified after recording hash")
}
// Edit the file → modified
os.WriteFile(handlerFile, []byte("package handler\n\nfunc Foo() {}\n"), 0644)
if !handlerModified(dir, handlerFile) {
t.Error("expected modified after editing file")
}
// Re-record → not modified again
recordHandlerHash(dir, handlerFile)
if handlerModified(dir, handlerFile) {
t.Error("expected not modified after re-recording hash")
}
}
func TestMetaReadWrite(t *testing.T) {
dir := t.TempDir()
m := readMeta(dir)
if len(m) != 0 {
t.Error("expected empty meta for new dir")
}
m["handler_hash"] = "abc123"
m["version"] = "1"
writeMeta(dir, m)
m2 := readMeta(dir)
if m2["handler_hash"] != "abc123" || m2["version"] != "1" {
t.Errorf("readMeta() = %v, want handler_hash=abc123, version=1", m2)
}
}
func TestGenerateStructure(t *testing.T) {
dir := t.TempDir()
svcDir := filepath.Join(dir, "test-svc")
svc := ServiceSpec{
Name: "test-svc",
Description: "Test service",
Fields: []FieldSpec{
{Name: "id", Type: "string"},
{Name: "name", Type: "string"},
},
Endpoints: []EndpointSpec{
{Name: "Create"},
{Name: "Read"},
},
}
if err := generateStructure(svcDir, svc); err != nil {
t.Fatal(err)
}
// Check files exist
for _, f := range []string{
"proto/test-svc.proto",
"handler/test-svc.go",
"main.go",
"go.mod",
"Makefile",
".gitignore",
} {
if _, err := os.Stat(filepath.Join(svcDir, f)); err != nil {
t.Errorf("missing %s: %v", f, err)
}
}
// Check .micro was created with handler hash
meta := readMeta(svcDir)
if meta["handler_hash"] == "" {
t.Error("expected handler_hash in .micro after generateStructure")
}
// Run again — should not overwrite main.go
mainBefore, _ := os.ReadFile(filepath.Join(svcDir, "main.go"))
os.WriteFile(filepath.Join(svcDir, "main.go"), []byte("// user edited\n"), 0644)
if err := generateStructure(svcDir, svc); err != nil {
t.Fatal(err)
}
mainAfter, _ := os.ReadFile(filepath.Join(svcDir, "main.go"))
if string(mainAfter) == string(mainBefore) {
t.Error("expected main.go to keep user edit on re-run")
}
// Proto should be protected if user modified it
protoFile := filepath.Join(svcDir, "proto", "test-svc.proto")
protoBefore, _ := os.ReadFile(protoFile)
os.WriteFile(protoFile, []byte("// user-edited proto\n"), 0644)
if err := generateStructure(svcDir, svc); err != nil {
t.Fatal(err)
}
protoAfter, _ := os.ReadFile(protoFile)
if string(protoAfter) != "// user-edited proto\n" {
t.Error("expected proto to be preserved after user edit")
}
// Proto should regenerate if NOT modified
recordFileHash(svcDir, "proto_hash", protoFile)
if err := generateStructure(svcDir, svc); err != nil {
t.Fatal(err)
}
protoAfter2, _ := os.ReadFile(protoFile)
if string(protoAfter2) == string(protoBefore) {
// ok — regenerated from spec
}
}
func TestFileModified(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "test.txt")
os.WriteFile(f, []byte("original"), 0644)
// No hash → not modified
if fileModified(dir, "test_hash", f) {
t.Error("expected not modified with no saved hash")
}
recordFileHash(dir, "test_hash", f)
// Same content → not modified
if fileModified(dir, "test_hash", f) {
t.Error("expected not modified with matching hash")
}
// Changed content → modified
os.WriteFile(f, []byte("changed"), 0644)
if !fileModified(dir, "test_hash", f) {
t.Error("expected modified after content change")
}
}
func TestDiscoverExisting(t *testing.T) {
dir := t.TempDir()
// Empty directory → empty string
if got := discoverExisting(dir); got != "" {
t.Errorf("expected empty for empty dir, got %q", got)
}
// Non-service directory (no proto) → empty
os.MkdirAll(filepath.Join(dir, "not-a-service"), 0755)
if got := discoverExisting(dir); got != "" {
t.Errorf("expected empty for dir without proto, got %q", got)
}
// Create a real service directory with proto
svcDir := filepath.Join(dir, "order-service")
os.MkdirAll(filepath.Join(svcDir, "proto"), 0755)
os.WriteFile(filepath.Join(svcDir, "proto", "order-service.proto"),
[]byte("syntax = \"proto3\";\nservice OrderService {}"), 0644)
got := discoverExisting(dir)
if !strings.Contains(got, "order-service") {
t.Errorf("expected to find order-service, got %q", got)
}
if !strings.Contains(got, "OrderService") {
t.Errorf("expected to find proto content, got %q", got)
}
// Add a second service
svc2Dir := filepath.Join(dir, "user-service")
os.MkdirAll(filepath.Join(svc2Dir, "proto"), 0755)
os.WriteFile(filepath.Join(svc2Dir, "proto", "user-service.proto"),
[]byte("syntax = \"proto3\";\nservice UserService {}"), 0644)
got = discoverExisting(dir)
if !strings.Contains(got, "order-service") || !strings.Contains(got, "user-service") {
t.Errorf("expected both services, got %q", got)
}
}
func TestIsTruncated(t *testing.T) {
tests := []struct {
name string
code string
want bool
}{
{"complete", "package handler\n\nfunc New() *H { return &H{} }\n", false},
{"empty", "", true},
{"no closing brace", "package handler\n\nfunc Foo() {", true},
{"unbalanced", "package handler\n\nfunc Foo() {\n\tif true {", true},
{"balanced", "package handler\n\nfunc Foo() {\n\tif true {\n\t}\n}", false},
{"trailing whitespace ok", "package handler\n\ntype X struct{}\n\n", false},
{"mid-expression", "package handler\n\nfunc F() {\n\tx := 1 +", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isTruncated(tt.code); got != tt.want {
t.Errorf("isTruncated() = %v, want %v", got, tt.want)
}
})
}
}
+142 -6
View File
@@ -2,14 +2,20 @@
package new
import (
"bufio"
"context"
"fmt"
"go/build"
"os"
"os/exec"
"os/signal"
"path"
"path/filepath"
"runtime"
"strings"
"syscall"
"go-micro.dev/v5/cmd/micro/cli/generate"
"text/template"
"time"
@@ -84,7 +90,10 @@ func create(c config) error {
return fmt.Errorf("%s already exists", c.Dir)
}
fmt.Printf("Creating service %s\n\n", c.Alias)
fmt.Println()
fmt.Println(" \033[1mmicro new\033[0m")
fmt.Println()
fmt.Printf(" Creating \033[36m%s\033[0m\n\n", c.Alias)
t := treeprint.New()
@@ -135,6 +144,11 @@ func addFileToTree(root treeprint.Tree, file string) {
}
func Run(ctx *cli.Context) error {
// Handle --prompt: design services with AI, then generate each one
if prompt := ctx.String("prompt"); prompt != "" {
return runPrompt(ctx, prompt)
}
dir := ctx.Args().First()
if len(dir) == 0 {
fmt.Println("specify service name")
@@ -174,17 +188,23 @@ func Run(ctx *cli.Context) error {
}
goDir = filepath.Join(goPath, "src", path.Clean(dir))
noMCP := ctx.Bool("no-mcp")
templateName := ctx.String("template")
// Select templates based on --template flag
mainTmpl, handlerTmpl, protoTmpl := selectTemplates(templateName, noMCP)
c := config{
Alias: dir,
Comments: nil, // Remove redundant protoComments
Comments: nil,
Dir: dir,
GoDir: goDir,
GoPath: goPath,
UseGoPath: false,
Files: []file{
{"main.go", tmpl.MainSRV},
{"handler/" + dir + ".go", tmpl.HandlerSRV},
{"proto/" + dir + ".proto", tmpl.ProtoSRV},
{"main.go", mainTmpl},
{"handler/" + dir + ".go", handlerTmpl},
{"proto/" + dir + ".proto", protoTmpl},
{"Makefile", tmpl.Makefile},
{"README.md", tmpl.Readme},
{".gitignore", tmpl.GitIgnore},
@@ -214,10 +234,53 @@ func Run(ctx *cli.Context) error {
fmt.Println("\nProject structure after 'make proto':")
printTree(dir)
fmt.Println("\nService created successfully! Start coding in your new service directory.")
fmt.Println()
fmt.Printf(" \033[32m✓\033[0m Service \033[36m%s\033[0m created\n\n", dir)
fmt.Println(" Next steps:")
fmt.Printf(" cd %s\n", dir)
fmt.Println(" go run .")
if !noMCP {
fmt.Println()
fmt.Printf(" MCP tools \033[36mhttp://localhost:3001/mcp/tools\033[0m\n")
fmt.Println(" Claude Code \033[2mmicro mcp serve\033[0m")
}
fmt.Println()
return nil
}
func selectTemplates(name string, noMCP bool) (mainTmpl, handlerTmpl, protoTmpl string) {
switch name {
case "crud":
if noMCP {
mainTmpl = tmpl.MainSRVNoMCP
} else {
mainTmpl = tmpl.MainSRV
}
return mainTmpl, tmpl.CrudHandlerSRV, tmpl.CrudProtoSRV
case "pubsub":
if noMCP {
mainTmpl = tmpl.PubsubMainSRVNoMCP
} else {
mainTmpl = tmpl.PubsubMainSRV
}
return mainTmpl, tmpl.PubsubHandlerSRV, tmpl.PubsubProtoSRV
case "api":
if noMCP {
mainTmpl = tmpl.MainSRVNoMCP
} else {
mainTmpl = tmpl.MainSRV
}
return mainTmpl, tmpl.ApiHandlerSRV, tmpl.ApiProtoSRV
default:
if noMCP {
mainTmpl = tmpl.MainSRVNoMCP
} else {
mainTmpl = tmpl.MainSRV
}
return mainTmpl, tmpl.HandlerSRV, tmpl.ProtoSRV
}
}
func runInDir(dir, cmd string) error {
parts := strings.Fields(cmd)
c := exec.Command(parts[0], parts[1:]...)
@@ -255,3 +318,76 @@ func printTree(dir string) {
filepath.Walk(dir, walk)
fmt.Println(t.String())
}
func runPrompt(cliCtx *cli.Context, prompt string) error {
provider := cliCtx.String("provider")
apiKey := cliCtx.String("api_key")
if apiKey == "" {
// Try provider-specific env vars
for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY",
"ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY", "MICRO_AI_API_KEY"} {
if v := os.Getenv(env); v != "" {
apiKey = v
break
}
}
}
if apiKey == "" {
return fmt.Errorf("--api_key or a provider API key env var is required for --prompt")
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
fmt.Println()
fmt.Println(" \033[1mmicro new --prompt\033[0m")
fmt.Println()
fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt)
design, err := generate.Design(ctx, provider, apiKey, "", ".", prompt)
if err != nil {
return fmt.Errorf("design failed: %w", err)
}
fmt.Println(" Services:")
for _, svc := range design.Services {
fmt.Printf(" \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description)
for _, ep := range svc.Endpoints {
fmt.Printf(" %s: %s\n", ep.Name, ep.Description)
}
}
fmt.Println()
if !confirmGenerate() {
fmt.Println(" Cancelled.")
return nil
}
fmt.Println(" Generating code...")
if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil {
return fmt.Errorf("generate failed: %w", err)
}
for _, svc := range design.Services {
fmt.Printf(" \033[32m✓\033[0m %s/\n", svc.Name)
}
fmt.Println()
fmt.Println(" \033[32m✓\033[0m All services generated")
fmt.Println()
fmt.Println(" Next steps:")
fmt.Println(" micro run \033[2m# start all services\033[0m")
fmt.Println(" micro chat --provider anthropic \033[2m# talk to them\033[0m")
fmt.Println()
return nil
}
func confirmGenerate() bool {
fmt.Print(" Generate? [Y/n] ")
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return false
}
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
return answer == "" || answer == "y" || answer == "yes"
}
+122
View File
@@ -0,0 +1,122 @@
package template
var (
ApiProtoSRV = `syntax = "proto3";
package {{dehyphen .Alias}};
option go_package = "./proto;{{dehyphen .Alias}}";
service {{title .Alias}} {
rpc Health(HealthRequest) returns (HealthResponse) {}
rpc Endpoint(EndpointRequest) returns (EndpointResponse) {}
}
message HealthRequest {}
message HealthResponse {
string status = 1;
int64 uptime = 2;
}
message EndpointRequest {
string method = 1;
string path = 2;
string body = 3;
map<string, string> headers = 4;
}
message EndpointResponse {
int32 status_code = 1;
string body = 2;
map<string, string> headers = 3;
}
`
ApiHandlerSRV = `package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
log "go-micro.dev/v5/logger"
pb "{{.Dir}}/proto"
)
type {{title .Alias}} struct {
started time.Time
routes map[string]http.HandlerFunc
}
func New() *{{title .Alias}} {
h := &{{title .Alias}}{
started: time.Now(),
routes: make(map[string]http.HandlerFunc),
}
h.registerRoutes()
return h
}
func (h *{{title .Alias}}) registerRoutes() {
h.routes["GET /hello"] = func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
json.NewEncoder(w).Encode(map[string]string{
"message": fmt.Sprintf("Hello %s", name),
})
}
}
// Health returns the service health status and uptime.
//
// @example {}
func (h *{{title .Alias}}) Health(ctx context.Context, req *pb.HealthRequest, rsp *pb.HealthResponse) error {
rsp.Status = "ok"
rsp.Uptime = int64(time.Since(h.started).Seconds())
return nil
}
// Endpoint handles proxied HTTP requests. The method and path fields
// select the route; body and headers are forwarded.
//
// @example {"method": "GET", "path": "/hello", "body": "", "headers": {}}
func (h *{{title .Alias}}) Endpoint(ctx context.Context, req *pb.EndpointRequest, rsp *pb.EndpointResponse) error {
key := fmt.Sprintf("%s %s", req.Method, req.Path)
handler, ok := h.routes[key]
if !ok {
log.Infof("Route not found: %s", key)
rsp.StatusCode = 404
rsp.Body = ` + "`" + `{"error":"not found"}` + "`" + `
return nil
}
rec := &responseRecorder{headers: make(map[string]string), statusCode: 200}
fakeReq, _ := http.NewRequestWithContext(ctx, req.Method, req.Path, nil)
handler(rec, fakeReq)
rsp.StatusCode = int32(rec.statusCode)
rsp.Body = rec.body
rsp.Headers = rec.headers
return nil
}
type responseRecorder struct {
headers map[string]string
body string
statusCode int
}
func (r *responseRecorder) Header() http.Header { return http.Header{} }
func (r *responseRecorder) WriteHeader(statusCode int) { r.statusCode = statusCode }
func (r *responseRecorder) Write(b []byte) (int, error) {
r.body = string(b)
return len(b), nil
}
`
)
+225
View File
@@ -0,0 +1,225 @@
package template
var (
CrudProtoSRV = `syntax = "proto3";
package {{dehyphen .Alias}};
option go_package = "./proto;{{dehyphen .Alias}}";
service {{title .Alias}} {
rpc Create(CreateRequest) returns (CreateResponse) {}
rpc Read(ReadRequest) returns (ReadResponse) {}
rpc Update(UpdateRequest) returns (UpdateResponse) {}
rpc Delete(DeleteRequest) returns (DeleteResponse) {}
rpc List(ListRequest) returns (ListResponse) {}
}
message {{title .Alias}}Record {
string id = 1;
string name = 2;
string email = 3;
string phone = 4;
string company = 5;
int64 created = 6;
int64 updated = 7;
}
message CreateRequest {
string name = 1;
string email = 2;
string phone = 3;
string company = 4;
}
message CreateResponse {
{{title .Alias}}Record record = 1;
}
message ReadRequest {
string id = 1;
}
message ReadResponse {
{{title .Alias}}Record record = 1;
}
message UpdateRequest {
string id = 1;
string name = 2;
string email = 3;
string phone = 4;
string company = 5;
}
message UpdateResponse {
{{title .Alias}}Record record = 1;
}
message DeleteRequest {
string id = 1;
}
message DeleteResponse {
bool deleted = 1;
}
message ListRequest {
int64 limit = 1;
int64 offset = 2;
}
message ListResponse {
repeated {{title .Alias}}Record records = 1;
int64 total = 2;
}
`
CrudHandlerSRV = `package handler
import (
"context"
"fmt"
"sort"
"sync"
"time"
"github.com/google/uuid"
log "go-micro.dev/v5/logger"
pb "{{.Dir}}/proto"
)
type {{title .Alias}} struct {
mu sync.RWMutex
records map[string]*pb.{{title .Alias}}Record
}
func New() *{{title .Alias}} {
return &{{title .Alias}}{
records: make(map[string]*pb.{{title .Alias}}Record),
}
}
// Create adds a new record and returns it with a generated ID.
//
// @example {"name": "Alice Smith", "email": "alice@example.com", "phone": "+1-555-0100", "company": "Acme Inc"}
func (h *{{title .Alias}}) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
log.Infof("Creating record: %s", req.Name)
now := time.Now().Unix()
record := &pb.{{title .Alias}}Record{
Id: uuid.New().String(),
Name: req.Name,
Email: req.Email,
Phone: req.Phone,
Company: req.Company,
Created: now,
Updated: now,
}
h.mu.Lock()
h.records[record.Id] = record
h.mu.Unlock()
rsp.Record = record
return nil
}
// Read retrieves a record by ID.
//
// @example {"id": "some-uuid"}
func (h *{{title .Alias}}) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
h.mu.RLock()
record, ok := h.records[req.Id]
h.mu.RUnlock()
if !ok {
return fmt.Errorf("record %s not found", req.Id)
}
rsp.Record = record
return nil
}
// Update modifies an existing record. Only non-empty fields are updated.
//
// @example {"id": "some-uuid", "name": "Alice Johnson", "email": "alice.j@example.com"}
func (h *{{title .Alias}}) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
h.mu.Lock()
defer h.mu.Unlock()
record, ok := h.records[req.Id]
if !ok {
return fmt.Errorf("record %s not found", req.Id)
}
if req.Name != "" {
record.Name = req.Name
}
if req.Email != "" {
record.Email = req.Email
}
if req.Phone != "" {
record.Phone = req.Phone
}
if req.Company != "" {
record.Company = req.Company
}
record.Updated = time.Now().Unix()
rsp.Record = record
return nil
}
// Delete removes a record by ID.
//
// @example {"id": "some-uuid"}
func (h *{{title .Alias}}) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
h.mu.Lock()
_, ok := h.records[req.Id]
if ok {
delete(h.records, req.Id)
}
h.mu.Unlock()
rsp.Deleted = ok
return nil
}
// List returns all records with optional pagination.
//
// @example {"limit": 10, "offset": 0}
func (h *{{title .Alias}}) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
h.mu.RLock()
defer h.mu.RUnlock()
all := make([]*pb.{{title .Alias}}Record, 0, len(h.records))
for _, r := range h.records {
all = append(all, r)
}
sort.Slice(all, func(i, j int) bool {
return all[i].Created > all[j].Created
})
rsp.Total = int64(len(all))
offset := int(req.Offset)
if offset > len(all) {
offset = len(all)
}
limit := int(req.Limit)
if limit <= 0 {
limit = 20
}
end := offset + limit
if end > len(all) {
end = len(all)
}
rsp.Records = all[offset:end]
return nil
}
`
)
+8 -3
View File
@@ -13,19 +13,24 @@ import (
type {{title .Alias}} struct{}
// Return a new handler
// Return a new handler.
func New() *{{title .Alias}} {
return &{{title .Alias}}{}
}
// Call is a single request handler called via client.Call or the generated client code
// Call greets a person by name and returns a welcome message.
//
// @example {"name": "Alice"}
func (e *{{title .Alias}}) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
log.Info("Received {{title .Alias}}.Call request")
rsp.Msg = "Hello " + req.Name
return nil
}
// Stream is a server side stream handler called via client.Stream or the generated client code
// Stream sends a sequence of numbered responses back to the caller.
// Use this for streaming large result sets or real-time updates.
//
// @example {"count": 5}
func (e *{{title .Alias}}) Stream(ctx context.Context, req *pb.StreamingRequest, stream pb.{{title .Alias}}_StreamStream) error {
log.Infof("Received {{title .Alias}}.Stream request with count: %d", req.Count)
+1
View File
@@ -3,5 +3,6 @@ package template
var (
GitIgnore = `
{{.Alias}}
.micro
`
)
+27
View File
@@ -3,6 +3,33 @@ package template
var (
MainSRV = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
func main() {
// Create service
service := micro.New("{{lower .Alias}}",
mcp.WithMCP(":3001"),
)
// Initialize service
service.Init()
// Register handler
pb.Register{{title .Alias}}Handler(service.Server(), handler.New())
// Run service
service.Run()
}
`
MainSRVNoMCP = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
+12 -8
View File
@@ -27,6 +27,18 @@ test-coverage:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# List MCP tools exposed by this service
mcp-tools:
micro mcp list
# Test an MCP tool interactively
mcp-test:
micro mcp test
# Start MCP server for Claude Code
mcp-serve:
micro mcp serve
# Clean build artifacts
clean:
rm -rf bin/ coverage.out coverage.html
@@ -35,14 +47,6 @@ clean:
docker:
docker build -t {{.Alias}}:latest .
# Run with Docker Compose
docker-up:
docker-compose up -d
# Stop Docker Compose
docker-down:
docker-compose down
# Lint code
lint:
golangci-lint run ./...
+1 -1
View File
@@ -3,7 +3,7 @@ package template
var (
Module = `module {{.Dir}}
go 1.18
go 1.22
require (
go-micro.dev/v5 latest
+4
View File
@@ -17,18 +17,22 @@ message Message {
}
message Request {
// Name of the person to greet
string name = 1;
}
message Response {
// Greeting message
string msg = 1;
}
message StreamingRequest {
// Number of responses to stream back
int64 count = 1;
}
message StreamingResponse {
// Current sequence number in the stream
int64 count = 1;
}
`
+184
View File
@@ -0,0 +1,184 @@
package template
var (
PubsubProtoSRV = `syntax = "proto3";
package {{dehyphen .Alias}};
option go_package = "./proto;{{dehyphen .Alias}}";
service {{title .Alias}} {
rpc Publish(PublishRequest) returns (PublishResponse) {}
rpc Stats(StatsRequest) returns (StatsResponse) {}
}
message Event {
string id = 1;
string type = 2;
string source = 3;
string data = 4;
int64 timestamp = 5;
}
message PublishRequest {
string type = 1;
string data = 2;
}
message PublishResponse {
string id = 1;
}
message StatsRequest {}
message StatsResponse {
int64 published = 1;
int64 received = 2;
}
`
PubsubHandlerSRV = `package handler
import (
"context"
"encoding/json"
"sync/atomic"
"time"
"github.com/google/uuid"
"go-micro.dev/v5/broker"
log "go-micro.dev/v5/logger"
pb "{{.Dir}}/proto"
)
const Topic = "{{lower .Alias}}.events"
type {{title .Alias}} struct {
broker broker.Broker
published atomic.Int64
received atomic.Int64
}
func New(b broker.Broker) *{{title .Alias}} {
return &{{title .Alias}}{broker: b}
}
// Publish sends an event to the message broker.
//
// @example {"type": "user.created", "data": "{\"id\": \"123\", \"name\": \"Alice\"}"}
func (h *{{title .Alias}}) Publish(ctx context.Context, req *pb.PublishRequest, rsp *pb.PublishResponse) error {
event := &pb.Event{
Id: uuid.New().String(),
Type: req.Type,
Source: "{{lower .Alias}}",
Data: req.Data,
Timestamp: time.Now().Unix(),
}
body, err := json.Marshal(event)
if err != nil {
return err
}
if err := h.broker.Publish(Topic, &broker.Message{Body: body}); err != nil {
return err
}
h.published.Add(1)
log.Infof("Published event %s type=%s", event.Id, event.Type)
rsp.Id = event.Id
return nil
}
// Stats returns the number of events published and received.
//
// @example {}
func (h *{{title .Alias}}) Stats(ctx context.Context, req *pb.StatsRequest, rsp *pb.StatsResponse) error {
rsp.Published = h.published.Load()
rsp.Received = h.received.Load()
return nil
}
// Subscribe sets up a subscription to the event topic. Call this
// after the service has started.
func (h *{{title .Alias}}) Subscribe() error {
_, err := h.broker.Subscribe(Topic, func(p broker.Event) error {
h.received.Add(1)
var event pb.Event
if err := json.Unmarshal(p.Message().Body, &event); err != nil {
log.Errorf("Failed to unmarshal event: %v", err)
return nil
}
log.Infof("Received event %s type=%s data=%s", event.Id, event.Type, event.Data)
return nil
})
return err
}
`
PubsubMainSRV = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
log "go-micro.dev/v5/logger"
)
func main() {
service := micro.New("{{lower .Alias}}",
mcp.WithMCP(":3001"),
)
service.Init()
h := handler.New(service.Options().Broker)
pb.Register{{title .Alias}}Handler(service.Server(), h)
// Subscribe to events after service starts
go func() {
if err := h.Subscribe(); err != nil {
log.Fatalf("Failed to subscribe: %v", err)
}
log.Info("Subscribed to ", handler.Topic)
}()
service.Run()
}
`
PubsubMainSRVNoMCP = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
"go-micro.dev/v5"
log "go-micro.dev/v5/logger"
)
func main() {
service := micro.New("{{lower .Alias}}")
service.Init()
h := handler.New(service.Options().Broker)
pb.Register{{title .Alias}}Handler(service.Server(), h)
go func() {
if err := h.Subscribe(); err != nil {
log.Fatalf("Failed to subscribe: %v", err)
}
log.Info("Subscribed to ", handler.Topic)
}()
service.Run()
}
`
)
+73 -13
View File
@@ -3,28 +3,88 @@ package template
var (
Readme = `# {{title .Alias}} Service
This is the {{title .Alias}} service
Generated with
` + "```" +
`
` + "```" + `
micro new {{.Alias}}
` + "```" + `
## Usage
## Getting Started
Generate the proto code
Generate the proto code:
` + "```" +
`
` + "```bash" + `
make proto
` + "```" + `
Run the service
Run the service:
` + "```" +
`
micro run .
` + "```"
` + "```bash" + `
go run .
` + "```" + `
## MCP & AI Agents
This service is MCP-enabled by default. When running, AI agents can discover
and call your service endpoints automatically.
**MCP tools endpoint:** http://localhost:3001/mcp/tools
### Test with curl
` + "```bash" + `
# List available tools
curl http://localhost:3001/mcp/tools | jq
# Call the service via MCP
curl -X POST http://localhost:3001/mcp/call \
-H 'Content-Type: application/json' \
-d '{"tool": "{{lower .Alias}}.{{title .Alias}}.Call", "arguments": {"name": "Alice"}}'
` + "```" + `
### Use with Claude Code
` + "```bash" + `
# Start MCP server for Claude Code
micro mcp serve
` + "```" + `
Or add to your Claude Code config:
` + "```json" + `
{
"mcpServers": {
"{{lower .Alias}}": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
` + "```" + `
### Writing Good Tool Descriptions
AI agents work best when your handler methods have clear doc comments:
` + "```go" + `
// CreateUser registers a new user account with the given email and name.
// Returns the created user with their assigned ID.
//
// @example {"email": "alice@example.com", "name": "Alice Smith"}
func (s *Users) CreateUser(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
// ...
}
` + "```" + `
See the [tool descriptions guide](https://go-micro.dev/docs/guides/tool-descriptions) for more tips.
## Development
` + "```bash" + `
make proto # Regenerate proto code
make build # Build binary
make test # Run tests
make dev # Run with hot reload (requires air)
` + "```" + `
`
)
+38 -7
View File
@@ -15,9 +15,30 @@ import (
"github.com/stretchr/objx"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/client"
"go-micro.dev/v5/metadata"
"go-micro.dev/v5/registry"
)
// AddMetadataToContext parses metadata strings in the format "Key:Value" and adds them to the context
func AddMetadataToContext(ctx context.Context, metadataStrings []string) context.Context {
if len(metadataStrings) == 0 {
return ctx
}
md := make(metadata.Metadata)
for _, m := range metadataStrings {
parts := strings.SplitN(m, ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
md[key] = value
}
return metadata.MergeContext(ctx, md, true)
}
// LookupService queries the service for a service with the given alias. If
// no services are found for a given alias, the registry will return nil and
// the error will also be nil. An error is only returned if there was an issue
@@ -132,17 +153,27 @@ func CallService(srv *registry.Service, args []string) error {
return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name)
}
// parse the flags
// create a context for the call
callCtx := context.TODO()
// parse out --header or --metadata flags before parsing request body
// Note: This is for dynamic service calls (e.g., 'micro helloworld call --header X:Y').
// Direct 'micro call' commands are handled in cli.go.
if headerFlags, ok := flags["header"]; ok {
callCtx = AddMetadataToContext(callCtx, headerFlags)
delete(flags, "header")
}
if metadataFlags, ok := flags["metadata"]; ok {
callCtx = AddMetadataToContext(callCtx, metadataFlags)
delete(flags, "metadata")
}
// parse the flags into request body
body, err := FlagsToRequest(flags, ep.Request)
if err != nil {
return err
}
// create a context for the call based on the cli context
callCtx := context.TODO()
// TODO: parse out --header or --metadata
// construct and execute the request using the json content type
req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json"))
var rsp json.RawMessage
@@ -387,7 +418,7 @@ func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]
// so we do that here
if strings.Contains(key, "-") {
parts := strings.Split(key, "-")
for i, _ := range parts {
for i := range parts {
pToCreate := strings.Join(parts[0:i], ".")
if i > 0 && i < len(parts) && !result.Has(pToCreate) {
result.Set(pToCreate, map[string]interface{}{})
+74
View File
@@ -1,11 +1,13 @@
package util
import (
"context"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"go-micro.dev/v5/metadata"
goregistry "go-micro.dev/v5/registry"
)
@@ -377,3 +379,75 @@ func TestDynamicFlagParsing(t *testing.T) {
}
}
func TestAddMetadataToContext(t *testing.T) {
tests := []struct {
name string
metadataStrs []string
expectedKeys []string
expectedValues []string
}{
{
name: "Single metadata",
metadataStrs: []string{"Key1:Value1"},
expectedKeys: []string{"Key1"},
expectedValues: []string{"Value1"},
},
{
name: "Multiple metadata",
metadataStrs: []string{"Key1:Value1", "Key2:Value2"},
expectedKeys: []string{"Key1", "Key2"},
expectedValues: []string{"Value1", "Value2"},
},
{
name: "Metadata with spaces",
metadataStrs: []string{"Key1: Value1 ", " Key2 : Value2"},
expectedKeys: []string{"Key1", "Key2"},
expectedValues: []string{"Value1", "Value2"},
},
{
name: "Metadata with colon in value",
metadataStrs: []string{"Authorization:Bearer token:123"},
expectedKeys: []string{"Authorization"},
expectedValues: []string{"Bearer token:123"},
},
{
name: "Empty metadata",
metadataStrs: []string{},
expectedKeys: []string{},
expectedValues: []string{},
},
{
name: "Invalid metadata format",
metadataStrs: []string{"InvalidFormat"},
expectedKeys: []string{},
expectedValues: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
ctx = AddMetadataToContext(ctx, tt.metadataStrs)
md, ok := metadata.FromContext(ctx)
if len(tt.expectedKeys) == 0 && !ok {
return // Expected no metadata
}
if !ok && len(tt.expectedKeys) > 0 {
t.Fatal("Expected metadata in context but got none")
}
for i, key := range tt.expectedKeys {
value, found := md.Get(key)
if !found {
t.Fatalf("Expected key %s not found in metadata", key)
}
if value != tt.expectedValues[i] {
t.Fatalf("Expected value %s for key %s, got %s", tt.expectedValues[i], key, value)
}
}
})
}
}
+170
View File
@@ -0,0 +1,170 @@
// Package flow implements the 'micro flow' command for event-driven
// LLM orchestration of microservices.
package flow
import (
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/urfave/cli/v2"
aiflow "go-micro.dev/v5/flow"
"go-micro.dev/v5/broker"
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/registry"
)
func init() {
cmd.Register(&cli.Command{
Name: "flow",
Usage: "Event-driven LLM orchestration",
Description: `Run flows that subscribe to broker events and use an LLM to
orchestrate service calls in response.
Examples:
# Run a flow that reacts to user creation events
micro flow run --trigger events.user.created \
--prompt "New user: {{.Data}}. Send welcome email." \
--provider anthropic
# Run a one-shot flow with inline data
micro flow exec --prompt "List all users and count them" \
--provider anthropic
# Run a flow with a specific model
micro flow exec --prompt "Create a test user" \
--provider atlascloud --model deepseek-ai/DeepSeek-V3-0324`,
Subcommands: []*cli.Command{
{
Name: "run",
Usage: "Start a flow that listens to broker events",
Flags: flowFlags(),
Action: func(c *cli.Context) error {
return runFlow(c, false)
},
},
{
Name: "exec",
Usage: "Execute a flow once with inline data",
Flags: append(flowFlags(), &cli.StringFlag{
Name: "data",
Usage: "Input data for the flow (default: reads from --prompt only)",
}),
Action: func(c *cli.Context) error {
return runFlow(c, true)
},
},
},
})
}
func flowFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{Name: "trigger", Usage: "Broker topic to subscribe to", EnvVars: []string{"MICRO_FLOW_TRIGGER"}},
&cli.StringFlag{Name: "prompt", Usage: "Prompt template (use {{.Data}} for event data)", EnvVars: []string{"MICRO_FLOW_PROMPT"}},
&cli.StringFlag{Name: "provider", Usage: "AI provider", Value: "openai", EnvVars: []string{"MICRO_AI_PROVIDER"}},
&cli.StringFlag{Name: "api_key", Usage: "API key", EnvVars: []string{"MICRO_AI_API_KEY"}},
&cli.StringFlag{Name: "model", Usage: "Model name", EnvVars: []string{"MICRO_AI_MODEL"}},
&cli.StringFlag{Name: "base_url", Usage: "Provider base URL", EnvVars: []string{"MICRO_AI_BASE_URL"}},
&cli.StringFlag{Name: "name", Usage: "Flow name", Value: "default"},
}
}
func runFlow(c *cli.Context, oneShot bool) error {
prompt := c.String("prompt")
if prompt == "" {
return fmt.Errorf("--prompt is required")
}
provider := c.String("provider")
apiKey := c.String("api_key")
if apiKey == "" {
apiKey = fallbackKey(provider)
}
if apiKey == "" {
return fmt.Errorf("no API key; set --api_key or the provider's env var")
}
opts := []aiflow.Option{
aiflow.Prompt(prompt),
aiflow.Provider(provider),
aiflow.APIKey(apiKey),
}
if v := c.String("trigger"); v != "" {
opts = append(opts, aiflow.Trigger(v))
}
if v := c.String("model"); v != "" {
opts = append(opts, aiflow.Model(v))
}
if v := c.String("base_url"); v != "" {
opts = append(opts, aiflow.BaseURL(v))
}
opts = append(opts, aiflow.OnResult(func(r aiflow.Result) {
out, _ := json.MarshalIndent(r, "", " ")
fmt.Println(string(out))
}))
f := aiflow.New(c.String("name"), opts...)
reg := registry.DefaultRegistry
br := broker.DefaultBroker
cl := client.DefaultClient
if err := br.Connect(); err != nil {
return fmt.Errorf("broker connect: %w", err)
}
if err := f.Register(reg, br, cl); err != nil {
return err
}
if oneShot {
data := c.String("data")
if data == "" {
data = prompt
}
return f.Execute(context.Background(), data)
}
if c.String("trigger") == "" {
return fmt.Errorf("--trigger is required for 'flow run' (use 'flow exec' for one-shot)")
}
fmt.Println()
fmt.Println(" \033[1mmicro flow\033[0m")
fmt.Println()
fmt.Printf(" Flow \033[36m%s\033[0m\n", f.Name())
fmt.Printf(" Topic \033[36m%s\033[0m\n", c.String("trigger"))
fmt.Printf(" Provider \033[36m%s\033[0m\n", provider)
fmt.Println()
fmt.Println(" \033[2mListening for events. Ctrl-C to stop.\033[0m")
fmt.Println()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
fmt.Printf("\nStopped. %d executions recorded.\n", len(f.Results()))
return nil
}
func fallbackKey(provider string) string {
envMap := map[string]string{
"anthropic": "ANTHROPIC_API_KEY",
"openai": "OPENAI_API_KEY",
"gemini": "GEMINI_API_KEY",
"groq": "GROQ_API_KEY",
"mistral": "MISTRAL_API_KEY",
"together": "TOGETHER_API_KEY",
"atlascloud": "ATLASCLOUD_API_KEY",
}
if env, ok := envMap[provider]; ok {
return os.Getenv(env)
}
return ""
}
+4
View File
@@ -4,10 +4,14 @@ import (
"embed"
"go-micro.dev/v5/cmd"
_ "go-micro.dev/v5/cmd/micro/api"
_ "go-micro.dev/v5/cmd/micro/chat"
_ "go-micro.dev/v5/cmd/micro/cli"
_ "go-micro.dev/v5/cmd/micro/cli/build"
_ "go-micro.dev/v5/cmd/micro/cli/deploy"
_ "go-micro.dev/v5/cmd/micro/flow"
_ "go-micro.dev/v5/cmd/micro/mcp"
_ "go-micro.dev/v5/cmd/micro/resource"
_ "go-micro.dev/v5/cmd/micro/run"
"go-micro.dev/v5/cmd/micro/server"
)
+453
View File
@@ -0,0 +1,453 @@
# MCP CLI Command Examples
This document provides examples of using the `micro mcp` commands for AI agent integration.
## Table of Contents
- [List Available Tools](#list-available-tools)
- [Test a Tool](#test-a-tool)
- [Generate Documentation](#generate-documentation)
- [Export to Different Formats](#export-to-different-formats)
## Prerequisites
You need at least one microservice running with the go-micro framework. The service will automatically be discovered via the registry (mdns by default).
Example service:
```bash
cd examples/mcp/hello
go run main.go
```
## List Available Tools
### Human-readable list
```bash
micro mcp list
```
Output:
```
Available MCP Tools:
Service: greeter
• greeter.Greeter.SayHello
Total: 1 tools
```
### JSON output
```bash
micro mcp list --json
```
Output:
```json
{
"count": 1,
"tools": [
{
"description": "Call SayHello on greeter service",
"endpoint": "Greeter.SayHello",
"name": "greeter.Greeter.SayHello",
"service": "greeter"
}
]
}
```
## Test a Tool
### Basic test
```bash
micro mcp test greeter.Greeter.SayHello '{"name": "Alice"}'
```
Output:
```
Testing tool: greeter.Greeter.SayHello
Service: greeter
Endpoint: Greeter.SayHello
Input: {"name": "Alice"}
✅ Call successful!
Response:
{
"message": "Hello Alice!"
}
```
### Test with default empty input
```bash
micro mcp test greeter.Greeter.SayHello
```
This will call the tool with an empty JSON object `{}`.
## Generate Documentation
### Markdown documentation (stdout)
```bash
micro mcp docs
```
Output:
```markdown
# MCP Tools Documentation
Generated: 2026-02-13 14:30:00
Total Tools: 1
## Service: greeter
### greeter.Greeter.SayHello
**Description:** Greets a person by name. Returns a friendly greeting message.
**Example Input:**
\`\`\`json
{"name": "Alice"}
\`\`\`
```
### Markdown documentation (save to file)
```bash
micro mcp docs --output mcp-tools.md
```
This creates a `mcp-tools.md` file with the documentation.
### JSON documentation
```bash
micro mcp docs --format json
```
Output:
```json
{
"count": 1,
"tools": [
{
"description": "Greets a person by name. Returns a friendly greeting message.",
"endpoint": "Greeter.SayHello",
"example": "{\"name\": \"Alice\"}",
"metadata": {
"description": "Greets a person by name. Returns a friendly greeting message.",
"example": "{\"name\": \"Alice\"}"
},
"name": "greeter.Greeter.SayHello",
"scopes": null,
"service": "greeter"
}
]
}
```
### JSON documentation (save to file)
```bash
micro mcp docs --format json --output tools.json
```
## Export to Different Formats
### Export to LangChain (Python)
Generate Python code with LangChain tool definitions:
```bash
micro mcp export langchain
```
Output:
```python
# LangChain Tools for Go Micro Services
# Auto-generated from MCP service discovery
from langchain.tools import Tool
import requests
import json
# Configure your MCP gateway endpoint
MCP_GATEWAY_URL = 'http://localhost:3000/mcp'
def call_mcp_tool(tool_name, arguments):
"""Call an MCP tool via HTTP gateway"""
response = requests.post(
f'{MCP_GATEWAY_URL}/call',
json={'name': tool_name, 'arguments': arguments}
)
response.raise_for_status()
return response.json()
# Define tools
tools = []
def greeter_Greeter_SayHello(arguments: str) -> str:
"""Greets a person by name. Returns a friendly greeting message."""
args = json.loads(arguments) if isinstance(arguments, str) else arguments
return json.dumps(call_mcp_tool('greeter.Greeter.SayHello', args))
tools.append(Tool(
name='greeter.Greeter.SayHello',
func=greeter_Greeter_SayHello,
description='Greets a person by name. Returns a friendly greeting message.'
))
# Example usage:
# from langchain.agents import initialize_agent, AgentType
# from langchain.llms import OpenAI
#
# llm = OpenAI(temperature=0)
# agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)
# agent.run('Your query here')
```
Save to file:
```bash
micro mcp export langchain --output langchain_tools.py
```
### Export to OpenAPI 3.0
Generate an OpenAPI specification:
```bash
micro mcp export openapi
```
Output:
```json
{
"components": {
"securitySchemes": {
"bearerAuth": {
"scheme": "bearer",
"type": "http"
}
}
},
"info": {
"description": "Auto-generated OpenAPI spec from MCP service discovery",
"title": "Go Micro MCP Services",
"version": "1.0.0"
},
"openapi": "3.0.0",
"paths": {
"/mcp/call/greeter/Greeter/SayHello": {
"post": {
"description": "Greets a person by name. Returns a friendly greeting message.",
"operationId": "greeter_Greeter_SayHello",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": "Successful response"
}
},
"summary": "greeter.Greeter.SayHello"
}
}
},
"servers": [
{
"description": "MCP Gateway",
"url": "http://localhost:3000"
}
]
}
```
Save to file:
```bash
micro mcp export openapi --output openapi.json
```
### Export to raw JSON
Export raw tool definitions:
```bash
micro mcp export json
```
This is similar to `micro mcp docs --format json` but specifically for export purposes.
Save to file:
```bash
micro mcp export json --output tools.json
```
## Using with Different Registries
By default, the commands use mdns registry. You can specify a different registry:
```bash
# Using consul
micro mcp list --registry consul --registry_address consul:8500
# Using etcd
micro mcp list --registry etcd --registry_address etcd:2379
```
## Integration Examples
### Using LangChain Export with Claude
1. Export your tools to LangChain format:
```bash
micro mcp export langchain --output my_tools.py
```
2. Use in your Python agent:
```python
from my_tools import tools
from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatAnthropic
llm = ChatAnthropic(model="claude-3-sonnet-20240229")
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)
result = agent.run("Greet Alice")
print(result)
```
### Using OpenAPI Export with GPT
1. Export to OpenAPI:
```bash
micro mcp export openapi --output openapi.json
```
2. Upload to ChatGPT as a custom GPT action or use with OpenAI Assistants API.
### Documentation for AI Agents
Generate documentation that AI agents can read to understand your services:
```bash
micro mcp docs --format json --output service-catalog.json
```
This JSON file can be fed to AI agents for service discovery and understanding.
## Advanced Usage
### Piping and Processing
You can pipe the output to other tools:
```bash
# Count tools per service
micro mcp list --json | jq '.tools | group_by(.service) | map({service: .[0].service, count: length})'
# Extract all tool names
micro mcp list --json | jq -r '.tools[].name'
# Filter tools by service
micro mcp list --json | jq '.tools[] | select(.service == "greeter")'
```
### Monitoring and CI/CD
Use these commands in your CI/CD pipeline:
```bash
# Validate all services are discoverable
SERVICE_COUNT=$(micro mcp list --json | jq '.count')
if [ "$SERVICE_COUNT" -lt 5 ]; then
echo "Error: Expected at least 5 services, found $SERVICE_COUNT"
exit 1
fi
# Generate documentation on each deployment
micro mcp docs --output docs/mcp-services.md
git add docs/mcp-services.md
git commit -m "Update MCP service documentation"
```
### Testing in Development
Create a script to test all your tools:
```bash
#!/bin/bash
# test-all-tools.sh
TOOLS=$(micro mcp list --json | jq -r '.tools[].name')
for tool in $TOOLS; do
echo "Testing $tool..."
micro mcp test "$tool" "{}" || echo "Failed: $tool"
done
```
## Troubleshooting
### No tools found
If `micro mcp list` shows 0 tools:
1. Verify services are running:
```bash
ps aux | grep "your-service"
```
2. Check registry (mdns might need time to discover):
```bash
# Wait a few seconds and try again
sleep 3
micro mcp list
```
3. Use a different registry if mdns is unreliable:
```bash
# Start services with consul
micro --registry consul server
# List with consul
micro mcp list --registry consul
```
### Service not responding in tests
If `micro mcp test` fails:
1. Verify the tool name is correct:
```bash
micro mcp list
```
2. Check the JSON input format:
```bash
# Invalid
micro mcp test service.Handler.Method '{invalid}'
# Valid
micro mcp test service.Handler.Method '{"key": "value"}'
```
3. Check service logs for errors.
## Next Steps
- Read the [MCP Documentation](../../gateway/mcp/DOCUMENTATION.md)
- Try the [MCP Examples](../../examples/mcp/README.md)
- Learn about [Tool Scopes and Security](../../gateway/mcp/DOCUMENTATION.md#authentication-and-scopes)
- Explore [Agent SDKs](#) (coming soon)
+560 -7
View File
@@ -8,10 +8,14 @@ import (
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/registry"
)
@@ -129,6 +133,83 @@ Example:
},
Action: testAction,
},
{
Name: "docs",
Usage: "Generate MCP documentation",
Description: `Generate documentation for all available MCP tools.
The documentation includes tool names, descriptions, parameters, and examples
extracted from service metadata and Go comments.
Examples:
# Generate markdown documentation
micro mcp docs
# Generate JSON documentation
micro mcp docs --format json
# Save to file
micro mcp docs --output mcp-tools.md`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "registry",
Usage: "Registry for service discovery",
Value: "mdns",
},
&cli.StringFlag{
Name: "registry_address",
Usage: "Registry address",
},
&cli.StringFlag{
Name: "format",
Usage: "Output format (markdown, json)",
Value: "markdown",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output file (default: stdout)",
},
},
Action: docsAction,
},
{
Name: "export",
Usage: "Export tools to different formats",
Description: `Export MCP tools to various agent framework formats.
Supported formats:
- langchain: LangChain tool definitions (Python)
- openapi: OpenAPI 3.0 specification
- json: Raw JSON tool definitions
Examples:
# Export to LangChain format
micro mcp export langchain
# Export to OpenAPI
micro mcp export openapi --output openapi.yaml
# Export raw JSON
micro mcp export json`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "registry",
Usage: "Registry for service discovery",
Value: "mdns",
},
&cli.StringFlag{
Name: "registry_address",
Usage: "Registry address",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output file (default: stdout)",
},
},
Action: exportAction,
},
},
})
}
@@ -214,7 +295,9 @@ func listAction(ctx *cli.Context) error {
}
// Human-readable output
fmt.Printf("Available MCP Tools:\n\n")
fmt.Println()
fmt.Println(" \033[1mmicro mcp tools\033[0m")
fmt.Println()
toolCount := 0
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
@@ -222,16 +305,16 @@ func listAction(ctx *cli.Context) error {
continue
}
fmt.Printf("Service: %s\n", svc.Name)
fmt.Printf(" \033[1m%s\033[0m\n", svc.Name)
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
fmt.Printf(" %s\n", toolName)
fmt.Printf(" \033[32m●\033[0m %s\n", toolName)
toolCount++
}
fmt.Println()
}
fmt.Printf("Total: %d tools\n", toolCount)
fmt.Printf(" \033[2m%d tools\033[0m\n\n", toolCount)
return nil
}
@@ -247,10 +330,480 @@ func testAction(ctx *cli.Context) error {
inputJSON = ctx.Args().Get(1)
}
// Validate input JSON
var inputData map[string]interface{}
if err := json.Unmarshal([]byte(inputJSON), &inputData); err != nil {
return fmt.Errorf("invalid JSON input: %w", err)
}
// Get registry
reg := registry.DefaultRegistry
if regName := ctx.String("registry"); regName != "" {
if regName != "mdns" {
return fmt.Errorf("registry %s not yet supported, use mdns", regName)
}
}
// Create MCP options
opts := mcp.Options{
Registry: reg,
Context: context.Background(),
Logger: log.New(os.Stderr, "", 0),
}
// Parse tool name (format: "service.endpoint" or "service.Handler.Method")
parts := parseTool(toolName)
if len(parts) < 2 {
return fmt.Errorf("invalid tool name format. Expected: service.endpoint or service.Handler.Method")
}
serviceName := parts[0]
endpointName := parts[1]
// If tool name has 3 parts, combine last two for endpoint (e.g., Handler.Method)
if len(parts) == 3 {
endpointName = parts[1] + "." + parts[2]
}
// Discover the tool from registry
services, err := opts.Registry.GetService(serviceName)
if err != nil || len(services) == 0 {
return fmt.Errorf("service %s not found: %w", serviceName, err)
}
// Find the endpoint
var endpoint *registry.Endpoint
for _, ep := range services[0].Endpoints {
if ep.Name == endpointName {
endpoint = ep
break
}
}
if endpoint == nil {
return fmt.Errorf("endpoint %s not found in service %s", endpointName, serviceName)
}
// Display test info
fmt.Printf("Testing tool: %s\n", toolName)
fmt.Printf("Input: %s\n", inputJSON)
fmt.Println("\nResult:")
fmt.Println("(Not yet implemented - coming soon)")
fmt.Printf("Service: %s\n", serviceName)
fmt.Printf("Endpoint: %s\n", endpointName)
fmt.Printf("Input: %s\n\n", inputJSON)
// Convert input to JSON bytes for RPC call
inputBytes, err := json.Marshal(inputData)
if err != nil {
return fmt.Errorf("failed to marshal input: %w", err)
}
// Make RPC call using bytes codec
c := opts.Client
if c == nil {
c = client.DefaultClient
}
// Create request with bytes frame
req := c.NewRequest(serviceName, endpointName, &bytes.Frame{Data: inputBytes})
// Make the call
var rsp bytes.Frame
if err := c.Call(opts.Context, req, &rsp); err != nil {
fmt.Printf("❌ Call failed: %v\n", err)
return err
}
// Parse and display response
fmt.Println("✅ Call successful!")
fmt.Println("\nResponse:")
// Try to pretty-print JSON response
var result interface{}
if err := json.Unmarshal(rsp.Data, &result); err == nil {
prettyJSON, err := json.MarshalIndent(result, "", " ")
if err == nil {
fmt.Println(string(prettyJSON))
} else {
fmt.Println(string(rsp.Data))
}
} else {
// Not JSON, print raw
fmt.Println(string(rsp.Data))
}
return nil
}
// parseTool splits a tool name into service and endpoint parts
func parseTool(toolName string) []string {
return strings.Split(toolName, ".")
}
// docsAction generates documentation for MCP tools
func docsAction(ctx *cli.Context) error {
// Get registry
reg := registry.DefaultRegistry
// Create temporary MCP server to discover tools
opts := mcp.Options{
Registry: reg,
Context: context.Background(),
Logger: log.New(os.Stderr, "", 0),
}
// Discover services
services, err := opts.Registry.ListServices()
if err != nil {
return fmt.Errorf("failed to list services: %w", err)
}
format := ctx.String("format")
outputFile := ctx.String("output")
// Prepare output writer
writer := os.Stdout
if outputFile != "" {
f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
writer = f
}
// Collect all tools with metadata
type ToolDoc struct {
Name string `json:"name"`
Service string `json:"service"`
Endpoint string `json:"endpoint"`
Description string `json:"description"`
Example string `json:"example,omitempty"`
Scopes []string `json:"scopes,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
var tools []ToolDoc
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
toolDoc := ToolDoc{
Name: fmt.Sprintf("%s.%s", svc.Name, ep.Name),
Service: svc.Name,
Endpoint: ep.Name,
Description: fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name),
Metadata: ep.Metadata,
}
// Extract description from metadata if available
if desc, ok := ep.Metadata["description"]; ok {
toolDoc.Description = desc
}
// Extract example from metadata if available
if example, ok := ep.Metadata["example"]; ok {
toolDoc.Example = example
}
// Extract scopes from metadata if available
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
toolDoc.Scopes = strings.Split(scopesStr, ",")
}
tools = append(tools, toolDoc)
}
}
// Generate output based on format
switch format {
case "json":
enc := json.NewEncoder(writer)
enc.SetIndent("", " ")
return enc.Encode(map[string]interface{}{
"tools": tools,
"count": len(tools),
})
case "markdown":
fmt.Fprintf(writer, "# MCP Tools Documentation\n\n")
fmt.Fprintf(writer, "Generated: %s\n\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Fprintf(writer, "Total Tools: %d\n\n", len(tools))
// Group by service
serviceMap := make(map[string][]ToolDoc)
for _, tool := range tools {
serviceMap[tool.Service] = append(serviceMap[tool.Service], tool)
}
for service, serviceTools := range serviceMap {
fmt.Fprintf(writer, "## Service: %s\n\n", service)
for _, tool := range serviceTools {
fmt.Fprintf(writer, "### %s\n\n", tool.Name)
fmt.Fprintf(writer, "**Description:** %s\n\n", tool.Description)
if len(tool.Scopes) > 0 {
fmt.Fprintf(writer, "**Required Scopes:** %s\n\n", strings.Join(tool.Scopes, ", "))
}
if tool.Example != "" {
fmt.Fprintf(writer, "**Example Input:**\n```json\n%s\n```\n\n", tool.Example)
}
}
}
return nil
default:
return fmt.Errorf("unsupported format: %s (supported: markdown, json)", format)
}
}
// exportAction exports tools to different formats
func exportAction(ctx *cli.Context) error {
if ctx.Args().Len() < 1 {
return fmt.Errorf("usage: micro mcp export <format>\nSupported formats: langchain, openapi, json")
}
exportFormat := ctx.Args().First()
// Get registry
reg := registry.DefaultRegistry
// Create temporary MCP server to discover tools
opts := mcp.Options{
Registry: reg,
Context: context.Background(),
Logger: log.New(os.Stderr, "", 0),
}
// Discover services
services, err := opts.Registry.ListServices()
if err != nil {
return fmt.Errorf("failed to list services: %w", err)
}
outputFile := ctx.String("output")
// Prepare output writer
writer := os.Stdout
if outputFile != "" {
f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
writer = f
}
switch exportFormat {
case "langchain":
return exportLangChain(writer, services, opts)
case "openapi":
return exportOpenAPI(writer, services, opts)
case "json":
return exportJSON(writer, services, opts)
default:
return fmt.Errorf("unsupported export format: %s\nSupported: langchain, openapi, json", exportFormat)
}
}
// exportLangChain exports tools in LangChain format (Python)
func exportLangChain(writer *os.File, services []*registry.Service, opts mcp.Options) error {
fmt.Fprintf(writer, "# LangChain Tools for Go Micro Services\n")
fmt.Fprintf(writer, "# Auto-generated from MCP service discovery\n\n")
fmt.Fprintf(writer, "from langchain.tools import Tool\n")
fmt.Fprintf(writer, "import requests\nimport json\n\n")
fmt.Fprintf(writer, "# Configure your MCP gateway endpoint\n")
fmt.Fprintf(writer, "MCP_GATEWAY_URL = 'http://localhost:3000/mcp'\n\n")
fmt.Fprintf(writer, "def call_mcp_tool(tool_name, arguments):\n")
fmt.Fprintf(writer, " \"\"\"Call an MCP tool via HTTP gateway\"\"\"\n")
fmt.Fprintf(writer, " response = requests.post(\n")
fmt.Fprintf(writer, " f'{MCP_GATEWAY_URL}/call',\n")
fmt.Fprintf(writer, " json={'name': tool_name, 'arguments': arguments}\n")
fmt.Fprintf(writer, " )\n")
fmt.Fprintf(writer, " response.raise_for_status()\n")
fmt.Fprintf(writer, " return response.json()\n\n")
fmt.Fprintf(writer, "# Define tools\n")
fmt.Fprintf(writer, "tools = []\n\n")
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
if desc, ok := ep.Metadata["description"]; ok {
description = desc
}
// Generate Python function name (replace dots with underscores)
funcName := strings.ReplaceAll(toolName, ".", "_")
fmt.Fprintf(writer, "def %s(arguments: str) -> str:\n", funcName)
fmt.Fprintf(writer, " \"\"\"% s\"\"\"\n", description)
fmt.Fprintf(writer, " args = json.loads(arguments) if isinstance(arguments, str) else arguments\n")
fmt.Fprintf(writer, " return json.dumps(call_mcp_tool('%s', args))\n\n", toolName)
fmt.Fprintf(writer, "tools.append(Tool(\n")
fmt.Fprintf(writer, " name='%s',\n", toolName)
fmt.Fprintf(writer, " func=%s,\n", funcName)
fmt.Fprintf(writer, " description='%s'\n", strings.ReplaceAll(description, "'", "\\'"))
fmt.Fprintf(writer, "))\n\n")
}
}
fmt.Fprintf(writer, "# Example usage:\n")
fmt.Fprintf(writer, "# from langchain.agents import initialize_agent, AgentType\n")
fmt.Fprintf(writer, "# from langchain.llms import OpenAI\n")
fmt.Fprintf(writer, "#\n")
fmt.Fprintf(writer, "# llm = OpenAI(temperature=0)\n")
fmt.Fprintf(writer, "# agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)\n")
fmt.Fprintf(writer, "# agent.run('Your query here')\n")
return nil
}
// exportOpenAPI exports tools in OpenAPI 3.0 format
func exportOpenAPI(writer *os.File, services []*registry.Service, opts mcp.Options) error {
spec := map[string]interface{}{
"openapi": "3.0.0",
"info": map[string]interface{}{
"title": "Go Micro MCP Services",
"description": "Auto-generated OpenAPI spec from MCP service discovery",
"version": "1.0.0",
},
"servers": []map[string]interface{}{
{
"url": "http://localhost:3000",
"description": "MCP Gateway",
},
},
"paths": make(map[string]interface{}),
}
paths := spec["paths"].(map[string]interface{})
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
path := fmt.Sprintf("/mcp/call/%s", strings.ReplaceAll(toolName, ".", "/"))
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
if desc, ok := ep.Metadata["description"]; ok {
description = desc
}
operation := map[string]interface{}{
"summary": toolName,
"description": description,
"operationId": strings.ReplaceAll(toolName, ".", "_"),
"requestBody": map[string]interface{}{
"required": true,
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
},
},
},
},
"responses": map[string]interface{}{
"200": map[string]interface{}{
"description": "Successful response",
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
},
},
},
},
},
}
// Add scope security if available
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
operation["security"] = []map[string]interface{}{
{
"bearerAuth": strings.Split(scopesStr, ","),
},
}
}
paths[path] = map[string]interface{}{
"post": operation,
}
}
}
// Add security schemes
spec["components"] = map[string]interface{}{
"securitySchemes": map[string]interface{}{
"bearerAuth": map[string]interface{}{
"type": "http",
"scheme": "bearer",
},
},
}
enc := json.NewEncoder(writer)
enc.SetIndent("", " ")
return enc.Encode(spec)
}
// exportJSON exports raw tool definitions as JSON
func exportJSON(writer *os.File, services []*registry.Service, opts mcp.Options) error {
var tools []map[string]interface{}
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
tool := map[string]interface{}{
"name": fmt.Sprintf("%s.%s", svc.Name, ep.Name),
"service": svc.Name,
"endpoint": ep.Name,
"metadata": ep.Metadata,
}
if desc, ok := ep.Metadata["description"]; ok {
tool["description"] = desc
}
if example, ok := ep.Metadata["example"]; ok {
tool["example"] = example
}
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
tool["scopes"] = strings.Split(scopesStr, ",")
}
tools = append(tools, tool)
}
}
enc := json.NewEncoder(writer)
enc.SetIndent("", " ")
return enc.Encode(map[string]interface{}{
"tools": tools,
"count": len(tools),
})
}
+79
View File
@@ -0,0 +1,79 @@
package mcp
import (
"reflect"
"testing"
)
func TestParseTool(t *testing.T) {
tests := []struct {
name string
toolName string
want []string
}{
{
name: "simple two-part tool",
toolName: "service.endpoint",
want: []string{"service", "endpoint"},
},
{
name: "three-part tool (service.Handler.Method)",
toolName: "greeter.Greeter.Hello",
want: []string{"greeter", "Greeter", "Hello"},
},
{
name: "single part (invalid)",
toolName: "service",
want: []string{"service"},
},
{
name: "four-part tool",
toolName: "users.Users.Get.All",
want: []string{"users", "Users", "Get", "All"},
},
{
name: "empty string",
toolName: "",
want: []string{""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseTool(tt.toolName)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseTool(%q) = %v, want %v", tt.toolName, got, tt.want)
}
})
}
}
func TestExportFormats(t *testing.T) {
// Test that export formats are recognized
formats := []string{"langchain", "openapi", "json"}
for _, format := range formats {
t.Run(format, func(t *testing.T) {
// This is a basic test to ensure the format strings are defined
// The actual export functions are tested through integration tests
if format == "" {
t.Error("export format should not be empty")
}
})
}
}
func TestDocsFormats(t *testing.T) {
// Test that docs formats are recognized
formats := []string{"markdown", "json"}
for _, format := range formats {
t.Run(format, func(t *testing.T) {
// This is a basic test to ensure the format strings are defined
// The actual docs functions are tested through integration tests
if format == "" {
t.Error("docs format should not be empty")
}
})
}
}
+84
View File
@@ -0,0 +1,84 @@
package resource
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/broker"
)
// brokerCommand exposes the broker interface: publish, subscribe.
func brokerCommand() *cli.Command {
return &cli.Command{
Name: "broker",
Usage: "Publish and subscribe to broker topics",
Description: `Interact with the message broker.
micro broker publish <topic> <message> Publish a message to a topic
micro broker subscribe <topic> Stream messages from a topic`,
Subcommands: []*cli.Command{
{
Name: "publish",
Usage: "Publish a message to a topic",
ArgsUsage: "<topic> <message>",
Action: brokerPublish,
},
{
Name: "subscribe",
Usage: "Stream messages from a topic",
ArgsUsage: "<topic>",
Action: brokerSubscribe,
},
},
}
}
func brokerPublish(c *cli.Context) error {
topic := c.Args().Get(0)
msg := c.Args().Get(1)
if topic == "" || msg == "" {
return fail("usage: micro broker publish <topic> <message>")
}
b := broker.DefaultBroker
if err := b.Connect(); err != nil {
return fail("broker connect: %v", err)
}
if err := b.Publish(topic, &broker.Message{Body: []byte(msg)}); err != nil {
return fail("publish: %v", err)
}
fmt.Printf("Published to %q\n", topic)
return nil
}
func brokerSubscribe(c *cli.Context) error {
topic := c.Args().First()
if topic == "" {
return fail("usage: micro broker subscribe <topic>")
}
b := broker.DefaultBroker
if err := b.Connect(); err != nil {
return fail("broker connect: %v", err)
}
sub, err := b.Subscribe(topic, func(e broker.Event) error {
fmt.Printf("%s\n", string(e.Message().Body))
return nil
})
if err != nil {
return fail("subscribe: %v", err)
}
defer sub.Unsubscribe()
fmt.Printf("Subscribed to %q (Ctrl-C to stop)...\n", topic)
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
return nil
}
+79
View File
@@ -0,0 +1,79 @@
package resource
import (
"fmt"
"strings"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/config"
"go-micro.dev/v5/config/source/env"
)
// configCommand exposes the config interface: get, dump.
//
// The CLI loads configuration from environment variables (the source
// that makes sense without a running service). Keys use dot notation,
// e.g. "database.host" reads from DATABASE_HOST.
func configCommand() *cli.Command {
return &cli.Command{
Name: "config",
Usage: "Read dynamic configuration (from environment)",
Description: `Read dynamic configuration loaded from environment variables.
Keys use dot notation: "database.host" maps to DATABASE_HOST.
micro config get <key> Read a config value
micro config dump Print the full config as JSON`,
Subcommands: []*cli.Command{
{
Name: "get",
Usage: "Read a config value",
ArgsUsage: "<key>",
Action: configGet,
},
{
Name: "dump",
Usage: "Print the full config",
Action: configDump,
},
},
}
}
func loadConfig() (config.Config, error) {
conf, err := config.NewConfig()
if err != nil {
return nil, err
}
if err := conf.Load(env.NewSource()); err != nil {
return nil, err
}
return conf, nil
}
func configGet(c *cli.Context) error {
key := c.Args().First()
if key == "" {
return fail("usage: micro config get <key>")
}
conf, err := loadConfig()
if err != nil {
return fail("load config: %v", err)
}
path := strings.Split(key, ".")
val, err := conf.Get(path...)
if err != nil {
return fail("get %q: %v", key, err)
}
fmt.Println(string(val.Bytes()))
return nil
}
func configDump(c *cli.Context) error {
conf, err := loadConfig()
if err != nil {
return fail("load config: %v", err)
}
fmt.Println(string(conf.Bytes()))
return nil
}
+92
View File
@@ -0,0 +1,92 @@
package resource
import (
"fmt"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/registry"
)
// registryCommand exposes the registry interface: list, get, watch.
func registryCommand() *cli.Command {
return &cli.Command{
Name: "registry",
Usage: "Inspect the service registry",
Description: `Interact with the service registry.
micro registry list List all registered services
micro registry get <name> Show nodes and endpoints for a service
micro registry watch Stream registration events`,
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List all registered services",
Action: registryList,
},
{
Name: "get",
Usage: "Show details for a service",
ArgsUsage: "<name>",
Action: registryGet,
},
{
Name: "watch",
Usage: "Stream registration events",
Action: registryWatch,
},
},
}
}
func registryList(c *cli.Context) error {
services, err := registry.ListServices()
if err != nil {
return fail("list services: %v", err)
}
out := make([]map[string]any, 0, len(services))
for _, s := range services {
out = append(out, map[string]any{
"name": s.Name,
"version": s.Version,
})
}
return printJSON(out)
}
func registryGet(c *cli.Context) error {
name := c.Args().First()
if name == "" {
return fail("usage: micro registry get <name>")
}
services, err := registry.GetService(name)
if err != nil {
return fail("get service %q: %v", name, err)
}
if len(services) == 0 {
return fail("service %q not found", name)
}
return printJSON(services)
}
func registryWatch(c *cli.Context) error {
w, err := registry.Watch()
if err != nil {
return fail("watch registry: %v", err)
}
defer w.Stop()
fmt.Println("Watching registry for changes (Ctrl-C to stop)...")
for {
res, err := w.Next()
if err != nil {
return fail("watch: %v", err)
}
name := ""
version := ""
if res.Service != nil {
name = res.Service.Name
version = res.Service.Version
}
fmt.Printf("%-10s %s %s\n", res.Action, name, version)
}
}
+52
View File
@@ -0,0 +1,52 @@
// Package resource provides CLI commands that map directly onto
// go-micro's core interfaces — registry, broker, store, and config.
//
// Each interface gets its own top-level command with verbs that mirror
// the interface methods, so the framework's building blocks are
// inspectable and manipulable from the terminal:
//
// micro registry list
// micro broker publish <topic> <message>
// micro store read <key>
// micro config get <key>
//
// New resource commands are registered by appending to the commands
// slice in init — see registry.go, broker.go, store.go, config.go for
// the per-interface implementations.
package resource
import (
"encoding/json"
"fmt"
"os"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
)
// commandFunc returns a cli.Command for a single core interface. Add a
// new one here to expose another package on the CLI.
var commandFuncs = []func() *cli.Command{
registryCommand,
brokerCommand,
storeCommand,
configCommand,
}
func init() {
for _, fn := range commandFuncs {
cmd.Register(fn())
}
}
// printJSON writes v as indented JSON to stdout.
func printJSON(v any) error {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(v)
}
// fail returns a cli error with a consistent prefix.
func fail(format string, args ...any) error {
return cli.Exit(fmt.Sprintf(format, args...), 1)
}
+29
View File
@@ -0,0 +1,29 @@
package resource
import "testing"
func TestCommandsRegistered(t *testing.T) {
// Each command func must return a command with a name and at least
// one subcommand, so the resource surface stays consistent.
for _, fn := range commandFuncs {
c := fn()
if c.Name == "" {
t.Error("command with empty name")
}
if len(c.Subcommands) == 0 {
t.Errorf("command %q has no subcommands", c.Name)
}
}
}
func TestExpectedCommands(t *testing.T) {
names := map[string]bool{}
for _, fn := range commandFuncs {
names[fn().Name] = true
}
for _, want := range []string{"registry", "broker", "store", "config"} {
if !names[want] {
t.Errorf("missing %q command", want)
}
}
}
+106
View File
@@ -0,0 +1,106 @@
package resource
import (
"fmt"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/store"
)
// storeCommand exposes the store interface: read, write, delete, list.
func storeCommand() *cli.Command {
return &cli.Command{
Name: "store",
Usage: "Read and write records in the store",
Description: `Interact with the data store.
micro store list [prefix] List keys (optionally by prefix)
micro store read <key> Read a record
micro store write <key> <value> Write a record
micro store delete <key> Delete a record`,
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List keys",
ArgsUsage: "[prefix]",
Action: storeList,
},
{
Name: "read",
Usage: "Read a record",
ArgsUsage: "<key>",
Action: storeRead,
},
{
Name: "write",
Usage: "Write a record",
ArgsUsage: "<key> <value>",
Action: storeWrite,
},
{
Name: "delete",
Usage: "Delete a record",
ArgsUsage: "<key>",
Action: storeDelete,
},
},
}
}
func storeList(c *cli.Context) error {
var opts []store.ListOption
if prefix := c.Args().First(); prefix != "" {
opts = append(opts, store.ListPrefix(prefix))
}
keys, err := store.DefaultStore.List(opts...)
if err != nil {
return fail("list: %v", err)
}
return printJSON(keys)
}
func storeRead(c *cli.Context) error {
key := c.Args().First()
if key == "" {
return fail("usage: micro store read <key>")
}
records, err := store.DefaultStore.Read(key)
if err != nil {
return fail("read %q: %v", key, err)
}
if len(records) == 0 {
return fail("key %q not found", key)
}
// Print the raw value for a single record, JSON for multiple.
if len(records) == 1 {
fmt.Println(string(records[0].Value))
return nil
}
return printJSON(records)
}
func storeWrite(c *cli.Context) error {
key := c.Args().Get(0)
value := c.Args().Get(1)
if key == "" {
return fail("usage: micro store write <key> <value>")
}
rec := &store.Record{Key: key, Value: []byte(value)}
if err := store.DefaultStore.Write(rec); err != nil {
return fail("write %q: %v", key, err)
}
fmt.Printf("Wrote %q\n", key)
return nil
}
func storeDelete(c *cli.Context) error {
key := c.Args().First()
if key == "" {
return fail("usage: micro store delete <key>")
}
if err := store.DefaultStore.Delete(key); err != nil {
return fail("delete %q: %v", key, err)
}
fmt.Printf("Deleted %q\n", key)
return nil
}
+348 -39
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"crypto/md5"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -18,10 +19,22 @@ import (
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/ai"
clt "go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/cli/generate"
"go-micro.dev/v5/cmd/micro/run/config"
"go-micro.dev/v5/cmd/micro/run/watcher"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/cmd/micro/server"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/atlascloud"
_ "go-micro.dev/v5/ai/gemini"
_ "go-micro.dev/v5/ai/groq"
_ "go-micro.dev/v5/ai/mistral"
_ "go-micro.dev/v5/ai/openai"
_ "go-micro.dev/v5/ai/together"
)
// Color codes for log output
@@ -175,6 +188,11 @@ func waitForHealth(port int, timeout time.Duration) bool {
}
func Run(c *cli.Context) error {
// Handle --prompt: generate services first, then run them
if prompt := c.String("prompt"); prompt != "" {
return runWithPrompt(c, prompt)
}
dir := c.Args().Get(0)
if dir == "" {
dir = "."
@@ -358,7 +376,7 @@ func Run(c *cli.Context) error {
}
// Print startup banner
printBanner(services, gw, !c.Bool("no-watch"))
printBanner(services, gw, !c.Bool("no-watch"), c.String("mcp-address"))
// Setup signal handling
sigCh := make(chan os.Signal, 1)
@@ -387,10 +405,38 @@ func Run(c *cli.Context) error {
}
}
}()
// Scan for new services added by micro chat or micro new
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-sigCh:
return
case <-ticker.C:
newSvcs := discoverNewServices(absDir, servicesByDir, binDir, runDir, logsDir, envVars, len(services))
for _, sp := range newSvcs {
services = append(services, sp)
servicesByDir[sp.dir] = sp
watch.AddDir(sp.dir)
if err := sp.start(logsDir); err != nil {
fmt.Fprintf(os.Stderr, "[%s] %v\n", sp.name, err)
continue
}
fmt.Printf("\n \033[32m●\033[0m %s \033[2m(new)\033[0m\n", sp.name)
}
}
}
}()
}
// Wait for signal
<-sigCh
// Interactive console or wait for signal
if c.Bool("detach") {
<-sigCh
} else {
runConsole(sigCh)
}
fmt.Println("\nShutting down...")
if watch != nil {
@@ -427,67 +473,232 @@ func processRunning(pidStr string) bool {
return proc.Signal(syscall.Signal(0)) == nil
}
func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool) {
func discoverNewServices(baseDir string, known map[string]*serviceProcess, binDir, runDir, logsDir string, envVars []string, colorOffset int) []*serviceProcess {
var newSvcs []*serviceProcess
entries, err := os.ReadDir(baseDir)
if err != nil {
return nil
}
for _, e := range entries {
if !e.IsDir() {
continue
}
svcDir := filepath.Join(baseDir, e.Name())
absSvcDir, _ := filepath.Abs(svcDir)
if _, exists := known[absSvcDir]; exists {
continue
}
mainFile := filepath.Join(svcDir, "main.go")
if _, err := os.Stat(mainFile); err != nil {
continue
}
name := e.Name()
hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8]
sp := &serviceProcess{
name: name,
dir: absSvcDir,
binPath: filepath.Join(binDir, name+"-"+hash),
pidFile: filepath.Join(runDir, name+"-"+hash+".pid"),
logFile: filepath.Join(logsDir, name+"-"+hash+".log"),
color: colorFor(colorOffset + len(newSvcs)),
env: envVars,
}
newSvcs = append(newSvcs, sp)
}
return newSvcs
}
func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool, mcpAddr string) {
fmt.Println()
fmt.Println(" \033[1mMicro\033[0m")
fmt.Println()
fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
fmt.Println(" │ │")
fmt.Println(" │ \033[1mMicro\033[0m │")
fmt.Println(" │ │")
if gw != nil {
fmt.Printf(" │ Web: \033[36mhttp://localhost%s\033[0m\n", gw.Addr())
fmt.Printf(" │ API: \033[36mhttp://localhost%s/api/{service}/{method}\033[0m\n", gw.Addr())
fmt.Printf(" │ Health: \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr())
fmt.Printf(" Dashboard \033[36mhttp://localhost%s\033[0m\n", gw.Addr())
fmt.Printf(" API \033[36mhttp://localhost%s/api/{service}/{method}\033[0m\n", gw.Addr())
fmt.Printf(" Agent \033[36mhttp://localhost%s/agent\033[0m\n", gw.Addr())
fmt.Printf(" Health \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr())
if mcpAddr != "" {
fmt.Printf(" MCP \033[36mhttp://localhost%s\033[0m\n", mcpAddr)
fmt.Printf(" MCP Tools \033[36mhttp://localhost%s/mcp/tools\033[0m\n", mcpAddr)
fmt.Printf(" WebSocket \033[36mws://localhost%s/mcp/ws\033[0m\n", mcpAddr)
}
}
fmt.Println(" │ │")
fmt.Println(" │ Services: │")
var agents, svcs []*serviceProcess
for _, s := range services {
if s.name == "agent" {
agents = append(agents, s)
} else {
svcs = append(svcs, s)
}
}
for _, svc := range services {
status := "\033[32m●\033[0m" // green dot
fmt.Println()
fmt.Println(" Services:")
for _, svc := range svcs {
status := "\033[32m●\033[0m"
if !svc.running {
status = "\033[31m●\033[0m" // red dot
status = "\033[31m●\033[0m"
}
name := svc.name
if len(name) > 20 {
name = name[:17] + "..."
}
fmt.Printf(" │ %s %-20s │\n", status, name)
fmt.Printf(" %s %s\n", status, svc.name)
}
fmt.Println(" │ │")
if len(agents) > 0 {
fmt.Println()
fmt.Println(" Agents:")
for _, a := range agents {
status := "\033[35m◆\033[0m"
if !a.running {
status = "\033[31m◆\033[0m"
}
fmt.Printf(" %s %s\n", status, a.name)
}
}
fmt.Println()
fmt.Println(" Auth: \033[32menabled\033[0m (admin / micro)")
if watching {
fmt.Println(" \033[33mWatching for changes...\033[0m")
fmt.Println(" │ │")
fmt.Println(" \033[33mWatching for changes...\033[0m")
}
fmt.Println(" │ Auth: \033[32menabled\033[0m (admin / micro) │")
fmt.Println(" │ │")
if gw != nil && len(services) > 0 {
svc := services[0]
fmt.Println(" │ Try: │")
fmt.Printf(" │ \033[90mcurl -X POST http://localhost%s/api/%s/...\033[0m │\n", gw.Addr(), svc.name)
fmt.Println(" │ │")
}
fmt.Println(" └─────────────────────────────────────────────────────────────┘")
fmt.Println()
}
func runConsole(sigCh chan os.Signal) {
// Detect provider and API key from environment
provider := os.Getenv("MICRO_AI_PROVIDER")
apiKey := os.Getenv("MICRO_AI_API_KEY")
if apiKey == "" {
for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY",
"ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY"} {
if v := os.Getenv(env); v != "" {
apiKey = v
break
}
}
}
if provider == "" {
provider = ai.AutoDetectProvider("")
}
if apiKey == "" {
fmt.Println(" \033[2mSet MICRO_AI_API_KEY to enable the interactive console.\033[0m")
fmt.Println(" \033[2mCtrl-C to stop.\033[0m")
fmt.Println()
<-sigCh
return
}
// Wait a moment for services to register
time.Sleep(2 * time.Second)
// Set up tools and model
reg := registry.DefaultRegistry
cl := clt.DefaultClient
tools := ai.NewTools(reg, ai.ToolClient(cl))
var modelOpts []ai.Option
modelOpts = append(modelOpts, ai.WithAPIKey(apiKey))
modelOpts = append(modelOpts, ai.WithToolHandler(tools.Handler()))
m := ai.New(provider, modelOpts...)
hist := ai.NewHistory(50)
// Build system prompt with service list
discovered, _ := tools.Discover()
serviceNames := make(map[string]bool)
for _, t := range discovered {
parts := strings.SplitN(t.OriginalName, ".", 2)
if len(parts) == 2 {
serviceNames[parts[0]] = true
}
}
var svcList []string
for name := range serviceNames {
svcList = append(svcList, name)
}
sysPrompt := fmt.Sprintf("You are an agent that orchestrates microservices. Available services: %s. "+
"Use the available tools to fulfill requests. When you call a tool, explain what you are doing. "+
"If a capability doesn't exist, say so.",
strings.Join(svcList, ", "))
fmt.Printf(" \033[2m%d tools from %d services. Type a message or Ctrl-C to stop.\033[0m\n\n",
len(discovered), len(serviceNames))
// Interactive REPL
scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 0, 4096), 1024*1024)
done := make(chan struct{})
go func() {
for {
fmt.Print("\033[1;36m>\033[0m ")
if !scanner.Scan() {
close(done)
return
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
hist.Add("user", line)
resp, err := m.Generate(context.Background(), &ai.Request{
Prompt: line,
SystemPrompt: sysPrompt,
Tools: discovered,
Messages: hist.Messages(),
})
if err != nil {
fmt.Printf("\033[31merror:\033[0m %v\n\n", err)
continue
}
if resp.Reply != "" {
hist.Add("assistant", resp.Reply)
fmt.Println(resp.Reply)
}
for _, tc := range resp.ToolCalls {
args, _ := json.Marshal(tc.Input)
fmt.Printf(" \033[33m→\033[0m \033[2m%s\033[0m(%s)\n", tc.Name, args)
if tc.Result != "" {
result := tc.Result
if len(result) > 200 {
result = result[:200] + "..."
}
fmt.Printf(" \033[32m←\033[0m \033[2m%s\033[0m\n", result)
}
}
if resp.Answer != "" {
hist.Add("assistant", resp.Answer)
fmt.Println()
fmt.Println(resp.Answer)
}
fmt.Println()
}
}()
select {
case <-sigCh:
case <-done:
}
}
func init() {
cmd.Register(&cli.Command{
Name: "run",
Usage: "Run services with API gateway and hot reload",
Description: `Run discovers and runs services in a directory.
Usage: "Development mode: run services with hot reload and API gateway",
Description: `Run discovers and runs services in a directory (development mode).
Starts an HTTP gateway on :8080 providing:
- Web dashboard at /
- Agent playground at /agent (AI chat with MCP tools)
- API explorer at /api
- API proxy at /api/{service}/{endpoint}
- MCP tools at /api/mcp/tools
- MCP tools at /mcp/tools
- Health checks at /health
With a micro.mu or micro.json config file, services start in dependency order.
@@ -499,7 +710,8 @@ Examples:
micro run --no-gateway # Services only, no HTTP gateway
micro run --no-watch # Disable hot reload
micro run --env production # Use production environment
micro run --mcp-address :3000 # Enable MCP protocol gateway`,
micro run --mcp-address :3000 # Enable MCP protocol gateway
micro run --prompt "an order system for dropshipping" # Generate and run`,
Action: Run,
Flags: []cli.Flag{
&cli.StringFlag{
@@ -516,6 +728,11 @@ Examples:
Name: "no-watch",
Usage: "Disable hot reload (file watching)",
},
&cli.BoolFlag{
Name: "detach",
Aliases: []string{"d"},
Usage: "Run without interactive console (background mode)",
},
&cli.StringFlag{
Name: "env",
Aliases: []string{"e"},
@@ -527,6 +744,98 @@ Examples:
Usage: "MCP gateway address (e.g., :3000). Enables MCP protocol for AI tools.",
EnvVars: []string{"MICRO_MCP_ADDRESS"},
},
&cli.StringFlag{
Name: "prompt",
Usage: "Describe a system to generate and run (AI designs, builds, and starts services)",
EnvVars: []string{"MICRO_RUN_PROMPT"},
},
&cli.StringFlag{
Name: "provider",
Usage: "AI provider for --prompt (anthropic, openai, gemini, atlascloud, groq, mistral, together)",
EnvVars: []string{"MICRO_AI_PROVIDER"},
},
&cli.StringFlag{
Name: "api_key",
Usage: "API key for --prompt (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)",
EnvVars: []string{"MICRO_AI_API_KEY"},
},
},
})
}
func runWithPrompt(c *cli.Context, prompt string) error {
provider := c.String("provider")
apiKey := c.String("api_key")
if apiKey == "" {
for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY",
"ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY", "MICRO_AI_API_KEY"} {
if v := os.Getenv(env); v != "" {
apiKey = v
break
}
}
}
if apiKey == "" {
return fmt.Errorf("--api_key or a provider API key env var is required for --prompt")
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
fmt.Println()
fmt.Println(" \033[1mmicro run --prompt\033[0m")
fmt.Println()
fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt)
design, err := generate.Design(ctx, provider, apiKey, "", ".", prompt)
if err != nil {
return fmt.Errorf("design failed: %w", err)
}
fmt.Println(" Services:")
for _, svc := range design.Services {
fmt.Printf(" \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description)
for _, ep := range svc.Endpoints {
fmt.Printf(" %s: %s\n", ep.Name, ep.Description)
}
}
fmt.Println()
if !confirmGenerate() {
fmt.Println(" Cancelled.")
return nil
}
fmt.Println(" Generating code...")
if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil {
return fmt.Errorf("generate failed: %w", err)
}
for _, svc := range design.Services {
fmt.Printf(" \033[32m✓\033[0m %s/\n", svc.Name)
}
fmt.Println()
// Set env vars so the agent process can pick them up
if provider != "" {
os.Setenv("MICRO_AI_PROVIDER", provider)
}
os.Setenv("MICRO_AI_API_KEY", apiKey)
// Now run normally — micro run discovers the generated services + agent
fmt.Println(" Starting services...")
fmt.Println()
cancel()
c.Set("prompt", "")
return Run(c)
}
func confirmGenerate() bool {
fmt.Print(" Generate? [Y/n] ")
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return false
}
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
return answer == "" || answer == "y" || answer == "yes"
}
+19
View File
@@ -75,6 +75,25 @@ func (w *Watcher) Start() {
go w.watch()
}
// AddDir adds a new directory to watch
func (w *Watcher) AddDir(dir string) {
w.mu.Lock()
defer w.mu.Unlock()
for _, d := range w.dirs {
if d == dir {
return
}
}
w.dirs = append(w.dirs, dir)
}
// Dirs returns the currently watched directories
func (w *Watcher) Dirs() []string {
w.mu.Lock()
defer w.mu.Unlock()
return append([]string{}, w.dirs...)
}
// Stop stops the watcher
func (w *Watcher) Stop() {
close(w.done)
+76 -315
View File
@@ -28,6 +28,14 @@ import (
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
codecBytes "go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/atlascloud"
_ "go-micro.dev/v5/ai/gemini"
_ "go-micro.dev/v5/ai/groq"
_ "go-micro.dev/v5/ai/mistral"
_ "go-micro.dev/v5/ai/openai"
_ "go-micro.dev/v5/ai/together"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
"golang.org/x/crypto/bcrypt"
@@ -58,7 +66,7 @@ type templates struct {
authLogin *template.Template
authUsers *template.Template
playground *template.Template
scopes *template.Template
scopes *template.Template
}
type TemplateUser struct {
ID string
@@ -82,7 +90,7 @@ func parseTemplates() *templates {
authLogin: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")),
authUsers: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_users.html")),
playground: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/playground.html")),
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
}
}
@@ -425,7 +433,7 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
})
// MCP API endpoints - list tools and call tools through the web server
mux.HandleFunc("/api/mcp/tools", wrap(func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/mcp/tools", wrap(func(w http.ResponseWriter, r *http.Request) {
services, err := registry.ListServices()
if err != nil {
w.Header().Set("Content-Type", "application/json")
@@ -485,7 +493,7 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
json.NewEncoder(w).Encode(map[string]any{"tools": tools})
}))
mux.HandleFunc("/api/mcp/call", wrap(func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/mcp/call", wrap(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
@@ -606,7 +614,7 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
}
}
apiKey := ""
model := ""
modelName := ""
baseURL := ""
provider := ""
if settings != nil {
@@ -614,7 +622,7 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
apiKey = v
}
if v := settings["model"]; v != "" {
model = v
modelName = v
}
if v := settings["base_url"]; v != "" {
baseURL = v
@@ -630,39 +638,12 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
// Auto-detect provider if not explicitly set
if provider == "" {
if strings.Contains(baseURL, "anthropic") {
provider = "anthropic"
} else {
provider = "openai"
}
}
// Set defaults based on provider
if provider == "anthropic" {
if model == "" {
model = "claude-sonnet-4-20250514"
}
if baseURL == "" {
baseURL = "https://api.anthropic.com"
}
} else {
if model == "" {
model = "gpt-4o"
}
if baseURL == "" {
baseURL = "https://api.openai.com"
}
provider = ai.AutoDetectProvider(baseURL)
}
// Discover tools from registry
services, _ := registry.ListServices()
type toolInfo struct {
Name string // original dotted name (e.g. "greeter.Greeter.Hello")
SafeName string // LLM-safe name (dots replaced with underscores)
Description string
Properties map[string]any
}
var discoveredTools []toolInfo
var discoveredTools []ai.Tool
// safeNameMap maps LLM-safe names back to original dotted names
safeNameMap := map[string]string{}
for _, svc := range services {
@@ -689,11 +670,11 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
}
}
}
discoveredTools = append(discoveredTools, toolInfo{
Name: tName,
SafeName: safeName,
Description: desc,
Properties: props,
discoveredTools = append(discoveredTools, ai.Tool{
Name: safeName,
OriginalName: tName,
Description: desc,
Properties: props,
})
}
}
@@ -776,282 +757,54 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
return rpcResult, string(rsp.Data)
}
// callLLMAPI makes an HTTP request to the LLM provider
callLLMAPI := func(url string, body []byte) ([]byte, error) {
httpReq, err := http.NewRequestWithContext(r.Context(), "POST", url, bytes.NewReader(body))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
if provider == "anthropic" {
httpReq.Header.Set("x-api-key", apiKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
} else {
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("LLM API request failed: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("LLM API error (%s): %s", resp.Status, string(respBody))
}
return respBody, nil
// Create model with options
var modelOpts []ai.Option
modelOpts = append(modelOpts, ai.WithAPIKey(apiKey))
if modelName != "" {
modelOpts = append(modelOpts, ai.WithModel(modelName))
}
if baseURL != "" {
modelOpts = append(modelOpts, ai.WithBaseURL(baseURL))
}
modelOpts = append(modelOpts, ai.WithToolHandler(executeToolCall))
m := ai.New(provider, modelOpts...)
if m == nil {
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to create model provider"})
return
}
// Build request
modelReq := &ai.Request{
Prompt: req.Prompt,
SystemPrompt: agentSystemPrompt,
Tools: discoveredTools,
}
// Generate response
response, err := m.Generate(r.Context(), modelReq)
if err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Build result
result := map[string]any{}
if provider == "anthropic" {
// --- Anthropic Messages API ---
var anthropicTools []map[string]any
for _, t := range discoveredTools {
anthropicTools = append(anthropicTools, map[string]any{
"name": t.SafeName,
"description": t.Description,
"input_schema": map[string]any{
"type": "object",
"properties": t.Properties,
},
if response.Reply != "" {
result["reply"] = response.Reply
}
if len(response.ToolCalls) > 0 {
var toolCalls []map[string]any
for _, tc := range response.ToolCalls {
toolCalls = append(toolCalls, map[string]any{
"tool": tc.Name,
"input": tc.Input,
})
}
anthropicReq := map[string]any{
"model": model,
"max_tokens": 4096,
"system": agentSystemPrompt,
"messages": []map[string]any{
{"role": "user", "content": req.Prompt},
},
}
if len(anthropicTools) > 0 {
anthropicReq["tools"] = anthropicTools
}
chatBody, _ := json.Marshal(anthropicReq)
apiURL := strings.TrimRight(baseURL, "/") + "/v1/messages"
respBody, err := callLLMAPI(apiURL, chatBody)
if err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Parse Anthropic response
var anthropicResp struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
} `json:"content"`
StopReason string `json:"stop_reason"`
}
if err := json.Unmarshal(respBody, &anthropicResp); err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to parse LLM response: " + err.Error()})
return
}
// Extract text reply
var replyParts []string
for _, block := range anthropicResp.Content {
if block.Type == "text" && block.Text != "" {
replyParts = append(replyParts, block.Text)
}
}
if len(replyParts) > 0 {
result["reply"] = strings.Join(replyParts, "\n")
}
// Execute tool uses
var toolUseBlocks []struct {
ID string
Name string
Input map[string]any
}
for _, block := range anthropicResp.Content {
if block.Type == "tool_use" {
var input map[string]any
if err := json.Unmarshal(block.Input, &input); err != nil {
log.Printf("[agent] failed to parse tool input: %v", err)
input = map[string]any{}
}
toolUseBlocks = append(toolUseBlocks, struct {
ID string
Name string
Input map[string]any
}{ID: block.ID, Name: block.Name, Input: input})
}
}
if len(toolUseBlocks) > 0 {
var toolCalls []map[string]any
var toolResultBlocks []map[string]any
for _, tu := range toolUseBlocks {
rpcResult, rpcContent := executeToolCall(tu.Name, tu.Input)
toolCalls = append(toolCalls, map[string]any{
"tool": tu.Name,
"input": tu.Input,
"result": rpcResult,
})
toolResultBlocks = append(toolResultBlocks, map[string]any{
"type": "tool_result",
"tool_use_id": tu.ID,
"content": rpcContent,
})
}
result["tool_calls"] = toolCalls
// Follow-up: send tool results back to Anthropic
followUpReq := map[string]any{
"model": model,
"max_tokens": 4096,
"system": agentSystemPrompt,
"messages": []map[string]any{
{"role": "user", "content": req.Prompt},
{"role": "assistant", "content": anthropicResp.Content},
{"role": "user", "content": toolResultBlocks},
},
}
followUpBody, _ := json.Marshal(followUpReq)
if followUpRespBody, err := callLLMAPI(apiURL, followUpBody); err == nil {
var followUpResp struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
}
if json.Unmarshal(followUpRespBody, &followUpResp) == nil {
var answerParts []string
for _, block := range followUpResp.Content {
if block.Type == "text" && block.Text != "" {
answerParts = append(answerParts, block.Text)
}
}
if len(answerParts) > 0 {
result["answer"] = strings.Join(answerParts, "\n")
}
}
}
}
} else {
// --- OpenAI Chat Completions API ---
var openaiTools []map[string]any
for _, t := range discoveredTools {
openaiTools = append(openaiTools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.SafeName,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": agentSystemPrompt},
{"role": "user", "content": req.Prompt},
}
chatReq := map[string]any{
"model": model,
"messages": messages,
}
if len(openaiTools) > 0 {
chatReq["tools"] = openaiTools
}
chatBody, _ := json.Marshal(chatReq)
apiURL := strings.TrimRight(baseURL, "/") + "/v1/chat/completions"
respBody, err := callLLMAPI(apiURL, chatBody)
if err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to parse LLM response: " + err.Error()})
return
}
if len(chatResp.Choices) == 0 {
json.NewEncoder(w).Encode(map[string]string{"error": "No response from LLM"})
return
}
choice := chatResp.Choices[0]
if choice.Message.Content != "" {
result["reply"] = choice.Message.Content
}
// Execute any tool calls
if len(choice.Message.ToolCalls) > 0 {
var toolCalls []map[string]any
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
})
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
log.Printf("[agent] failed to parse tool arguments: %v", err)
}
if input == nil {
input = map[string]any{}
}
rpcResult, rpcContent := executeToolCall(tc.Function.Name, input)
toolCalls = append(toolCalls, map[string]any{
"tool": tc.Function.Name,
"input": input,
"result": rpcResult,
})
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": rpcContent,
})
}
result["tool_calls"] = toolCalls
// Follow-up: send tool results back to LLM for a final answer
followUpReq := map[string]any{
"model": model,
"messages": followUpMessages,
}
followUpBody, _ := json.Marshal(followUpReq)
if followUpRespBody, err := callLLMAPI(apiURL, followUpBody); err == nil {
var followUpChat struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if json.Unmarshal(followUpRespBody, &followUpChat) == nil && len(followUpChat.Choices) > 0 {
result["answer"] = followUpChat.Choices[0].Message.Content
}
}
}
result["tool_calls"] = toolCalls
}
if response.Answer != "" {
result["answer"] = response.Answer
}
json.NewEncoder(w).Encode(result)
@@ -1660,7 +1413,11 @@ You can generate tokens on the <a href='/auth/tokens'>Tokens page</a>.
if del := r.FormValue("delete"); del != "" {
// Delete user
storeInst.Delete("auth/" + del)
deleteUserTokens(storeInst, del) // Delete all JWT tokens for this user
deleteUserTokens(storeInst, del)
// Mark default admin as deleted so it won't be recreated on restart
if del == "admin" {
storeInst.Write(&store.Record{Key: "auth/.admin-deleted", Value: []byte("true")})
}
http.Redirect(w, r, "/auth/users", http.StatusSeeOther)
return
}
@@ -1874,11 +1631,15 @@ func initAuth() error {
_, _ = os.ReadFile(privPath)
_, _ = os.ReadFile(pubPath)
storeInst := store.DefaultStore
// --- Ensure default admin account exists ---
// --- Ensure default admin account exists on first run ---
// If the admin was explicitly deleted (marker key exists), don't recreate.
adminID := "admin"
adminPass := "micro"
adminKey := "auth/" + adminID
if recs, _ := storeInst.Read(adminKey); len(recs) == 0 {
adminDeletedKey := "auth/.admin-deleted"
if recs, _ := storeInst.Read(adminDeletedKey); len(recs) > 0 {
// Admin was explicitly deleted — don't recreate
} else if recs, _ := storeInst.Read(adminKey); len(recs) == 0 {
// Hash the admin password with bcrypt
hash, err := bcrypt.GenerateFromPassword([]byte(adminPass), bcrypt.DefaultCost)
if err != nil {
@@ -1903,7 +1664,7 @@ func parseStartTime(s string) (time.Time, error) {
func init() {
cmd.Register(&cli.Command{
Name: "server",
Usage: "Run the micro server",
Usage: "Production mode: run the micro server with dashboard and auth",
Action: Run,
Flags: []cli.Flag{
&cli.StringFlag{
+238 -67
View File
@@ -1,39 +1,152 @@
{{define "content"}}
<h2>Agent</h2>
<p>Chat with your microservices using AI. Configure a model API key in settings, then use the prompt to interact with your services.</p>
<style>
#agent-chat {
display: flex;
flex-direction: column;
height: calc(100vh - 160px);
max-height: 800px;
}
#agent-messages {
flex: 1;
overflow-y: auto;
padding: 1em 0;
scroll-behavior: smooth;
}
.msg { padding: 0.7em 1em; border-radius: 8px; margin-bottom: 0.6em; line-height: 1.6; }
.msg-user { background: #f0f4ff; border: 1px solid #d0d8f0; }
.msg-user b { color: #336; }
.msg-assistant { background: #fff; border: 1px solid #ddd; }
.msg-assistant b { color: #333; }
.msg-error { background: #fff5f5; border: 1px solid #e88; color: #a00; }
.msg-thinking { background: #fafafa; border: 1px solid #e5e5e5; color: #888; font-style: italic; }
.tool-call {
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px;
margin-bottom: 0.6em; font-size: 0.93em; overflow: hidden;
}
.tool-header {
padding: 0.5em 1em; cursor: pointer; display: flex; align-items: center;
gap: 0.5em; user-select: none; font-weight: 600; color: #555;
}
.tool-header:hover { background: #f0f0f0; }
.tool-header .arrow { transition: transform 0.2s; display: inline-block; }
.tool-header .arrow.open { transform: rotate(90deg); }
.tool-body { padding: 0 1em 0.8em 1em; display: none; }
.tool-body.open { display: block; }
.tool-body pre {
background: #f5f5f5; padding: 0.6em; border-radius: 4px;
overflow-x: auto; font-size: 0.9em; margin: 0.4em 0;
}
.tool-body .tool-label { font-weight: 600; color: #666; font-size: 0.85em; margin-top: 0.5em; }
.tool-status { font-size: 0.8em; font-weight: normal; margin-left: auto; }
.tool-status.ok { color: #2a2; }
.tool-status.err { color: #c00; }
#prompt-bar {
display: flex; gap: 0.5em; align-items: center;
padding: 0.8em 0 0 0; border-top: 1px solid #eee;
}
#prompt-bar input {
flex: 1; margin-bottom: 0; padding: 0.6em 0.8em;
border: 1px solid #ccc; border-radius: 6px; font-size: 1em;
}
#prompt-bar button { padding: 0.6em 1.5em; border-radius: 6px; font-size: 1em; white-space: nowrap; }
#prompt-bar button:disabled { opacity: 0.5; cursor: not-allowed; }
.tools-bar {
display: flex; gap: 0.5em; align-items: center;
padding: 0.5em 0; font-size: 0.85em; color: #888;
}
.tools-bar .tool-count { font-weight: 600; color: #555; }
.settings-toggle {
background: none; border: 1px solid #ddd; border-radius: 6px;
padding: 0.3em 0.8em; font-size: 0.85em; cursor: pointer; color: #555;
}
.settings-toggle:hover { background: #f5f5f5; }
#settings-panel {
display: none; background: #fafafa; border: 1px solid #e5e5e5;
border-radius: 8px; padding: 1em 1.2em; margin-bottom: 1em;
}
#settings-panel.open { display: block; }
#settings-panel label { display: block; font-weight: 600; margin-top: 0.6em; font-size: 0.9em; }
#settings-panel input, #settings-panel select {
width: 100%; padding: 0.4em 0.6em; font-size: 0.9em;
border: 1px solid #ddd; border-radius: 4px; margin-top: 0.2em;
}
#settings-panel .settings-row { display: flex; gap: 1em; }
#settings-panel .settings-row > div { flex: 1; }
.clear-btn {
background: none; border: none; color: #999; cursor: pointer;
font-size: 0.85em; padding: 0.3em 0.5em;
}
.clear-btn:hover { color: #c00; }
.empty-state {
display: flex; flex-direction: column; align-items: center;
justify-content: center; height: 100%; color: #aaa; text-align: center;
}
.empty-state .icon { font-size: 3em; margin-bottom: 0.3em; }
.empty-state p { margin: 0.3em 0; max-width: 400px; }
</style>
<h3>Prompt</h3>
<div id="agent-messages"></div>
<form onsubmit="return false;" style="display:flex; gap:0.5em; align-items:flex-end;">
<input type="text" id="prompt-input" placeholder="e.g. List all users, Create a blog post..." style="flex:1; margin-bottom:0;">
<button id="prompt-btn" onclick="sendPrompt()">Send</button>
</form>
<div id="agent-chat">
<div class="tools-bar">
<span>Tools: <span class="tool-count" id="tool-count">...</span></span>
<button class="settings-toggle" onclick="toggleSettings()">Settings</button>
<button class="clear-btn" onclick="clearChat()">Clear chat</button>
</div>
<h3>Settings</h3>
<form id="settings-form" onsubmit="return false;">
<label style="display:block; font-weight:600;">Provider</label>
<select id="provider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
</select>
<label style="display:block; font-weight:600;">Model API Key</label>
<input type="password" id="api-key" placeholder="sk-... or API key for your model provider">
<label style="display:block; font-weight:600;">Model (optional)</label>
<input type="text" id="model-name" placeholder="e.g. gpt-4o or claude-sonnet-4-20250514">
<label style="display:block; font-weight:600;">Base URL (optional)</label>
<input type="text" id="base-url" placeholder="Leave blank for default">
<button onclick="saveSettings()">Save Settings</button>
<span id="settings-status" style="margin-left:0.5em; color:#888;"></span>
</form>
<div id="settings-panel">
<div class="settings-row">
<div>
<label>Provider</label>
<select id="provider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="gemini">Google Gemini</option>
<option value="atlascloud">Atlas Cloud</option>
<option value="groq">Groq</option>
<option value="mistral">Mistral</option>
<option value="together">Together AI</option>
</select>
</div>
<div>
<label>Model (optional)</label>
<input type="text" id="model-name" placeholder="e.g. gpt-4o, claude-sonnet-4-20250514">
</div>
</div>
<label>API Key</label>
<input type="password" id="api-key" placeholder="sk-... or API key">
<label>Base URL (optional)</label>
<input type="text" id="base-url" placeholder="Leave blank for default">
<div style="margin-top:0.8em; display:flex; gap:0.5em; align-items:center;">
<button onclick="saveSettings()" style="padding:0.4em 1em; font-size:0.9em;">Save</button>
<span id="settings-status" style="color:#888; font-size:0.85em;"></span>
</div>
</div>
<h3>Available Tools</h3>
<div id="tools-list">
<p style="color:#888;">Loading tools...</p>
<div id="agent-messages">
<div class="empty-state" id="empty-state">
<div class="icon">&#129302;</div>
<p><b>Chat with your services</b></p>
<p>Ask the agent to interact with your microservices. It will discover and call the right tools automatically.</p>
<p id="empty-tools" style="font-size:0.85em; color:#bbb; margin-top:0.8em;"></p>
<div id="setup-hint" style="margin-top:1.2em; padding:1em; background:#f8f9fa; border-radius:8px; border:1px solid #e5e5e5; text-align:left; font-size:0.88em; max-width:400px; margin-left:auto; margin-right:auto;">
<p style="margin:0 0 0.5em; font-weight:600;">Getting started:</p>
<p style="margin:0 0 0.3em;">1. Click <b>Settings</b> above</p>
<p style="margin:0 0 0.3em;">2. Choose a provider and enter your API key</p>
<p style="margin:0 0 0.3em;">3. Type a prompt like <em>"list all services"</em></p>
<p style="margin:0.6em 0 0; color:#888; font-size:0.9em;">Or use the CLI: <code style="background:#e9ecef; padding:2px 5px; border-radius:3px;">micro chat --provider anthropic</code></p>
</div>
</div>
</div>
<div id="prompt-bar">
<input type="text" id="prompt-input" placeholder="Ask the agent to call your services..." autofocus>
<button id="prompt-btn" onclick="sendPrompt()">Send</button>
</div>
</div>
<script>
(function() {
var tools = [];
var toolCount = document.getElementById('tool-count');
loadSettings();
loadTools();
@@ -46,10 +159,25 @@
if (data.api_key) document.getElementById('api-key').value = data.api_key;
if (data.model) document.getElementById('model-name').value = data.model;
if (data.base_url) document.getElementById('base-url').value = data.base_url;
// Hide setup hint if API key is configured
if (data.api_key) {
var hint = document.getElementById('setup-hint');
if (hint) hint.style.display = 'none';
}
// Auto-show settings if no API key configured
if (!data.api_key) {
document.getElementById('settings-panel').classList.add('open');
}
})
.catch(function() {});
.catch(function() {
document.getElementById('settings-panel').classList.add('open');
});
}
window.toggleSettings = function() {
document.getElementById('settings-panel').classList.toggle('open');
};
window.saveSettings = function() {
var status = document.getElementById('settings-status');
status.textContent = 'Saving...';
@@ -64,37 +192,42 @@
})
})
.then(function(r) { return r.json(); })
.then(function() { status.textContent = 'Saved'; })
.then(function() {
status.textContent = 'Saved';
setTimeout(function() { status.textContent = ''; }, 2000);
})
.catch(function(err) { status.textContent = 'Error: ' + err; });
};
function loadTools() {
fetch('/api/mcp/tools')
fetch('/mcp/tools')
.then(function(r) { return r.json(); })
.then(function(data) {
tools = data.tools || [];
renderTools();
toolCount.textContent = tools.length;
var emptyTools = document.getElementById('empty-tools');
if (tools.length > 0) {
var names = tools.slice(0, 5).map(function(t) { return t.name; });
var suffix = tools.length > 5 ? ' and ' + (tools.length - 5) + ' more' : '';
emptyTools.textContent = 'Available: ' + names.join(', ') + suffix;
} else {
emptyTools.textContent = 'No tools found. Start some services first.';
}
})
.catch(function(err) {
document.getElementById('tools-list').innerHTML = '<p style="color:#c00;">Failed to load tools: ' + err + '</p>';
.catch(function() {
toolCount.textContent = '0';
});
}
function renderTools() {
var el = document.getElementById('tools-list');
if (tools.length === 0) {
el.innerHTML = '<p style="color:#888;">No tools available. Start some services and they will appear here.</p>';
return;
}
var html = '<table><thead><tr><th>Tool</th><th>Description</th></tr></thead><tbody>';
for (var i = 0; i < tools.length; i++) {
var t = tools[i];
html += '<tr><td><code>' + escapeHtml(t.name) + '</code></td>';
html += '<td>' + escapeHtml(t.description || '') + '</td></tr>';
}
html += '</tbody></table>';
el.innerHTML = html;
}
window.clearChat = function() {
var container = document.getElementById('agent-messages');
container.innerHTML = '';
var emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.id = 'empty-state';
emptyState.innerHTML = '<div class="icon">&#129302;</div><p><b>Chat with your services</b></p><p>Ask the agent to interact with your microservices.</p>';
container.appendChild(emptyState);
};
window.sendPrompt = function() {
var input = document.getElementById('prompt-input');
@@ -102,11 +235,21 @@
if (!text) return;
input.value = '';
// Remove empty state on first message
var emptyState = document.getElementById('empty-state');
if (emptyState) emptyState.remove();
addMessage('user', escapeHtml(text));
// Show thinking indicator
var thinkingId = 'thinking-' + Date.now();
addMessage('thinking', 'Agent is thinking...', thinkingId);
var btn = document.getElementById('prompt-btn');
btn.disabled = true;
btn.textContent = '...';
btn.textContent = 'Sending...';
var startTime = Date.now();
fetch('/api/agent/prompt', {
method: 'POST',
@@ -115,8 +258,15 @@
})
.then(function(r) { return r.json(); })
.then(function(data) {
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
btn.disabled = false;
btn.textContent = 'Send';
input.focus();
// Remove thinking indicator
var thinking = document.getElementById(thinkingId);
if (thinking) thinking.remove();
if (data.error) {
addMessage('error', escapeHtml(data.error));
return;
@@ -127,9 +277,7 @@
if (data.tool_calls && data.tool_calls.length > 0) {
for (var i = 0; i < data.tool_calls.length; i++) {
var tc = data.tool_calls[i];
addMessage('tool', '<b>Tool:</b> <code>' + escapeHtml(tc.tool) + '</code>' +
'<pre>' + escapeHtml(JSON.stringify(tc.input, null, 2)) + '</pre>' +
'<b>Result:</b><pre>' + escapeHtml(JSON.stringify(tc.result, null, 2)) + '</pre>');
addToolCall(tc, elapsed);
}
}
if (data.answer) {
@@ -139,35 +287,58 @@
.catch(function(err) {
btn.disabled = false;
btn.textContent = 'Send';
input.focus();
var thinking = document.getElementById(thinkingId);
if (thinking) thinking.remove();
addMessage('error', 'Error: ' + escapeHtml(String(err)));
});
};
function addToolCall(tc, elapsed) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.className = 'tool-call';
var hasError = tc.result && tc.result.error;
var statusClass = hasError ? 'err' : 'ok';
var statusText = hasError ? 'error' : elapsed + 's';
var inputJson = JSON.stringify(tc.input, null, 2);
var resultJson = JSON.stringify(tc.result, null, 2);
div.innerHTML =
'<div class="tool-header" onclick="this.querySelector(\'.arrow\').classList.toggle(\'open\'); this.nextElementSibling.classList.toggle(\'open\');">' +
'<span class="arrow">&#9654;</span> ' +
'<code>' + escapeHtml(tc.tool) + '</code>' +
'<span class="tool-status ' + statusClass + '">' + statusText + '</span>' +
'</div>' +
'<div class="tool-body">' +
'<div class="tool-label">Input</div>' +
'<pre>' + escapeHtml(inputJson) + '</pre>' +
'<div class="tool-label">Result</div>' +
'<pre>' + escapeHtml(resultJson) + '</pre>' +
'</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
document.getElementById('prompt-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); window.sendPrompt(); }
});
function addMessage(type, html) {
function addMessage(type, html, id) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.style.cssText = 'padding:0.8em 1em; border-radius:7px; margin-bottom:0.8em; line-height:1.6;';
div.className = 'msg msg-' + type;
if (id) div.id = id;
if (type === 'user') {
div.style.background = '#f7f7f7';
div.style.border = '1px solid #eee';
div.innerHTML = '<b>You:</b> ' + html;
div.innerHTML = '<b>You</b><br>' + html;
} else if (type === 'assistant' || type === 'answer') {
div.style.background = '#fff';
div.style.border = '1px solid #ddd';
div.innerHTML = '<b>Agent:</b> ' + html;
} else if (type === 'tool') {
div.style.background = '#fafafa';
div.style.border = '1px solid #e0e0e0';
div.style.fontSize = '0.95em';
div.innerHTML = '<b>Agent</b><br>' + html;
} else if (type === 'thinking') {
div.innerHTML = html;
} else if (type === 'error') {
div.style.background = '#fff';
div.style.border = '1px solid #c00';
div.style.color = '#c00';
div.innerHTML = html;
}
container.appendChild(div);
+1 -1
View File
@@ -79,7 +79,7 @@
<thead><tr><th>Access method</th><th>How auth works</th></tr></thead>
<tbody>
<tr><td>API (<code>/api/service/endpoint</code>)</td><td><code>Authorization: Bearer &lt;token&gt;</code> header</td></tr>
<tr><td>MCP tools (<code>/api/mcp/call</code>)</td><td><code>Authorization: Bearer &lt;token&gt;</code> header</td></tr>
<tr><td>MCP tools (<code>/mcp/call</code>)</td><td><code>Authorization: Bearer &lt;token&gt;</code> header</td></tr>
<tr><td>Agent playground</td><td>Uses your logged-in session and its scopes</td></tr>
</tbody>
</table>
-13
View File
@@ -84,91 +84,78 @@ func Version(v string) Option {
func Broker(b *broker.Broker) Option {
return func(o *Options) {
o.Broker = b
broker.DefaultBroker = *b
}
}
func Cache(c *cache.Cache) Option {
return func(o *Options) {
o.Cache = c
cache.DefaultCache = *c
}
}
func Config(c *config.Config) Option {
return func(o *Options) {
o.Config = c
config.DefaultConfig = *c
}
}
func Selector(s *selector.Selector) Option {
return func(o *Options) {
o.Selector = s
selector.DefaultSelector = *s
}
}
func Registry(r *registry.Registry) Option {
return func(o *Options) {
o.Registry = r
registry.DefaultRegistry = *r
}
}
func Transport(t *transport.Transport) Option {
return func(o *Options) {
o.Transport = t
transport.DefaultTransport = *t
}
}
func Client(c *client.Client) Option {
return func(o *Options) {
o.Client = c
client.DefaultClient = *c
}
}
func Server(s *server.Server) Option {
return func(o *Options) {
o.Server = s
server.DefaultServer = *s
}
}
func Store(s *store.Store) Option {
return func(o *Options) {
o.Store = s
store.DefaultStore = *s
}
}
func Stream(s *events.Stream) Option {
return func(o *Options) {
o.Stream = s
events.DefaultStream = *s
}
}
func Tracer(t *trace.Tracer) Option {
return func(o *Options) {
o.Tracer = t
trace.DefaultTracer = *t
}
}
func Auth(a *auth.Auth) Option {
return func(o *Options) {
o.Auth = a
auth.DefaultAuth = *a
}
}
func Profile(p *profile.Profile) Option {
return func(o *Options) {
o.DebugProfile = p
profile.DefaultProfile = *p
}
}
@@ -0,0 +1,158 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: user.proto
package user
import (
fmt "fmt"
proto "google.golang.org/protobuf/proto"
math "math"
)
import (
context "context"
client "go-micro.dev/v5/client"
server "go-micro.dev/v5/server"
model "go-micro.dev/v5/model"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ client.Option
var _ server.Option
var _ model.Model
// Client API for UserService service
type UserServiceService interface {
Create(ctx context.Context, in *CreateUserRequest, opts ...client.CallOption) (*CreateUserResponse, error)
Get(ctx context.Context, in *GetUserRequest, opts ...client.CallOption) (*GetUserResponse, error)
Delete(ctx context.Context, in *DeleteUserRequest, opts ...client.CallOption) (*DeleteUserResponse, error)
}
type userServiceService struct {
c client.Client
name string
}
func NewUserServiceService(name string, c client.Client) UserServiceService {
return &userServiceService{
c: c,
name: name,
}
}
func (c *userServiceService) Create(ctx context.Context, in *CreateUserRequest, opts ...client.CallOption) (*CreateUserResponse, error) {
req := c.c.NewRequest(c.name, "UserService.Create", in)
out := new(CreateUserResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceService) Get(ctx context.Context, in *GetUserRequest, opts ...client.CallOption) (*GetUserResponse, error) {
req := c.c.NewRequest(c.name, "UserService.Get", in)
out := new(GetUserResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceService) Delete(ctx context.Context, in *DeleteUserRequest, opts ...client.CallOption) (*DeleteUserResponse, error) {
req := c.c.NewRequest(c.name, "UserService.Delete", in)
out := new(DeleteUserResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for UserService service
type UserServiceHandler interface {
Create(context.Context, *CreateUserRequest, *CreateUserResponse) error
Get(context.Context, *GetUserRequest, *GetUserResponse) error
Delete(context.Context, *DeleteUserRequest, *DeleteUserResponse) error
}
func RegisterUserServiceHandler(s server.Server, hdlr UserServiceHandler, opts ...server.HandlerOption) error {
type userService interface {
Create(ctx context.Context, in *CreateUserRequest, out *CreateUserResponse) error
Get(ctx context.Context, in *GetUserRequest, out *GetUserResponse) error
Delete(ctx context.Context, in *DeleteUserRequest, out *DeleteUserResponse) error
}
type UserService struct {
userService
}
h := &userServiceHandler{hdlr}
return s.Handle(s.NewHandler(&UserService{h}, opts...))
}
type userServiceHandler struct {
UserServiceHandler
}
func (h *userServiceHandler) Create(ctx context.Context, in *CreateUserRequest, out *CreateUserResponse) error {
return h.UserServiceHandler.Create(ctx, in, out)
}
func (h *userServiceHandler) Get(ctx context.Context, in *GetUserRequest, out *GetUserResponse) error {
return h.UserServiceHandler.Get(ctx, in, out)
}
func (h *userServiceHandler) Delete(ctx context.Context, in *DeleteUserRequest, out *DeleteUserResponse) error {
return h.UserServiceHandler.Delete(ctx, in, out)
}
// UserModel is a model struct generated from User.
// Use NewUserModel to create a typed table backed by any model.Model.
type UserModel struct {
Id string `json:"id" model:"key"`
Name string `json:"name"`
Email string `json:"email"`
Age int32 `json:"age"`
Status string `json:"status"`
}
// RegisterUserModel registers the UserModel table with the given model backend.
func RegisterUserModel(db model.Model) error {
return db.Register(&UserModel{}, model.WithTable("users"))
}
// UserModelFromProto converts a User proto message to a UserModel.
func UserModelFromProto(p *User) *UserModel {
if p == nil {
return nil
}
return &UserModel{
Id: p.GetId(),
Name: p.GetName(),
Email: p.GetEmail(),
Age: p.GetAge(),
Status: p.GetStatus(),
}
}
// ToProto converts a UserModel to a User proto message.
func (m *UserModel) ToProto() *User {
if m == nil {
return nil
}
return &User{
Id: m.Id,
Name: m.Name,
Email: m.Email,
Age: m.Age,
Status: m.Status,
}
}
@@ -0,0 +1,41 @@
syntax = "proto3";
option go_package = "../user";
// UserService manages user accounts.
service UserService {
rpc Create(CreateUserRequest) returns (CreateUserResponse) {}
rpc Get(GetUserRequest) returns (GetUserResponse) {}
rpc Delete(DeleteUserRequest) returns (DeleteUserResponse) {}
}
// @model
message User {
string id = 1;
string name = 2;
string email = 3;
int32 age = 4;
string status = 5;
}
message CreateUserRequest {
User user = 1;
}
message CreateUserResponse {
User user = 1;
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
User user = 1;
}
message DeleteUserRequest {
string id = 1;
}
message DeleteUserResponse {}
@@ -1193,6 +1193,15 @@ func (g *Generator) PrintComments(path string) bool {
return false
}
// GetComments returns the raw leading comment text for the given path, if any.
func (g *Generator) GetComments(path string) (string, bool) {
loc, ok := g.file.comments[path]
if !ok {
return "", false
}
return loc.GetLeadingComments(), true
}
// makeComments generates the comment string for the field, no "\n" at the end
func (g *Generator) makeComments(path string) (string, bool) {
loc, ok := g.file.comments[path]
+230 -6
View File
@@ -18,6 +18,7 @@ const (
contextPkgPath = "context"
clientPkgPath = "go-micro.dev/v5/client"
serverPkgPath = "go-micro.dev/v5/server"
modelPkgPath = "go-micro.dev/v5/model"
)
func init() {
@@ -42,6 +43,7 @@ var (
contextPkg string
clientPkg string
serverPkg string
modelPkg string
pkgImports map[generator.GoPackageName]bool
)
@@ -51,6 +53,7 @@ func (g *micro) Init(gen *generator.Generator) {
contextPkg = generator.RegisterUniquePackageName("context", nil)
clientPkg = generator.RegisterUniquePackageName("client", nil)
serverPkg = generator.RegisterUniquePackageName("server", nil)
modelPkg = generator.RegisterUniquePackageName("model", nil)
}
// Given a type name defined in a .proto, return its object.
@@ -70,29 +73,66 @@ func (g *micro) P(args ...interface{}) { g.gen.P(args...) }
// Generate generates code for the services in the given file.
func (g *micro) Generate(file *generator.FileDescriptor) {
if len(file.FileDescriptorProto.Service) == 0 {
// Check if any messages have @model annotation
hasModels := false
for i := range file.FileDescriptorProto.MessageType {
if g.isModelMessage(i) {
hasModels = true
break
}
}
if len(file.FileDescriptorProto.Service) == 0 && !hasModels {
return
}
g.P("// Reference imports to suppress errors if they are not otherwise used.")
g.P("var _ ", contextPkg, ".Context")
g.P("var _ ", clientPkg, ".Option")
g.P("var _ ", serverPkg, ".Option")
if len(file.FileDescriptorProto.Service) > 0 {
g.P("var _ ", clientPkg, ".Option")
g.P("var _ ", serverPkg, ".Option")
}
if hasModels {
g.P("var _ ", modelPkg, ".Database")
}
g.P()
for i, service := range file.FileDescriptorProto.Service {
g.generateService(file, service, i)
}
// Generate model structs for @model annotated messages
for i, msg := range file.FileDescriptorProto.MessageType {
if g.isModelMessage(i) {
g.generateModel(msg, i)
}
}
}
// GenerateImports generates the import declaration for this file.
func (g *micro) GenerateImports(file *generator.FileDescriptor, imports map[generator.GoImportPath]generator.GoPackageName) {
if len(file.FileDescriptorProto.Service) == 0 {
hasServices := len(file.FileDescriptorProto.Service) > 0
hasModels := false
for i := range file.FileDescriptorProto.MessageType {
if g.isModelMessage(i) {
hasModels = true
break
}
}
if !hasServices && !hasModels {
return
}
g.P("import (")
g.P(contextPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, contextPkgPath)))
g.P(clientPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, clientPkgPath)))
g.P(serverPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, serverPkgPath)))
if hasServices {
g.P(clientPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, clientPkgPath)))
g.P(serverPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, serverPkgPath)))
}
if hasModels {
g.P(modelPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, modelPkgPath)))
}
g.P(")")
g.P()
@@ -529,3 +569,187 @@ func (g *micro) generateServerMethod(servName string, method *pb.MethodDescripto
return hname
}
// isModelMessage checks if the message at the given index has a // @model annotation.
// Path "4,<index>" refers to message_type[index] in FileDescriptorProto.
func (g *micro) isModelMessage(msgIndex int) bool {
commentPath := fmt.Sprintf("4,%d", msgIndex)
comment, ok := g.gen.GetComments(commentPath)
if !ok {
return false
}
return strings.Contains(comment, "@model")
}
// parseModelOptions extracts options from the @model annotation comment.
// Supports: @model, @model(table=my_table), @model(key=custom_id)
func parseModelOptions(comment string) (table string, key string) {
idx := strings.Index(comment, "@model")
if idx < 0 {
return "", ""
}
rest := comment[idx+len("@model"):]
rest = strings.TrimSpace(rest)
if !strings.HasPrefix(rest, "(") {
return "", ""
}
end := strings.Index(rest, ")")
if end < 0 {
return "", ""
}
opts := rest[1:end]
for _, part := range strings.Split(opts, ",") {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 {
continue
}
switch strings.TrimSpace(kv[0]) {
case "table":
table = strings.TrimSpace(kv[1])
case "key":
key = strings.TrimSpace(kv[1])
}
}
return table, key
}
// protoFieldGoType returns the Go type string for a proto field for use in model structs.
// Only supports scalar types (no nested messages or enums in model structs).
func protoFieldGoType(field *pb.FieldDescriptorProto) string {
switch field.GetType() {
case pb.FieldDescriptorProto_TYPE_DOUBLE:
return "float64"
case pb.FieldDescriptorProto_TYPE_FLOAT:
return "float32"
case pb.FieldDescriptorProto_TYPE_INT64, pb.FieldDescriptorProto_TYPE_SINT64, pb.FieldDescriptorProto_TYPE_SFIXED64:
return "int64"
case pb.FieldDescriptorProto_TYPE_UINT64, pb.FieldDescriptorProto_TYPE_FIXED64:
return "uint64"
case pb.FieldDescriptorProto_TYPE_INT32, pb.FieldDescriptorProto_TYPE_SINT32, pb.FieldDescriptorProto_TYPE_SFIXED32:
return "int32"
case pb.FieldDescriptorProto_TYPE_UINT32, pb.FieldDescriptorProto_TYPE_FIXED32:
return "uint32"
case pb.FieldDescriptorProto_TYPE_BOOL:
return "bool"
case pb.FieldDescriptorProto_TYPE_STRING:
return "string"
case pb.FieldDescriptorProto_TYPE_BYTES:
return "[]byte"
default:
return "string"
}
}
// generateModel generates the model struct, factory, and proto conversion for a message.
func (g *micro) generateModel(msg *pb.DescriptorProto, msgIndex int) {
msgName := generator.CamelCase(msg.GetName())
modelName := msgName + "Model"
// Parse options from comment
commentPath := fmt.Sprintf("4,%d", msgIndex)
comment, _ := g.gen.GetComments(commentPath)
tableName, keyField := parseModelOptions(comment)
// Default table: lowercase message name + "s"
if tableName == "" {
tableName = strings.ToLower(msg.GetName()) + "s"
}
// Default key: first field, or "id" if a field named "id" exists
if keyField == "" {
for _, field := range msg.Field {
if field.GetName() == "id" {
keyField = "id"
break
}
}
if keyField == "" && len(msg.Field) > 0 {
keyField = msg.Field[0].GetName()
}
}
// Filter to scalar fields only (skip nested messages, maps, oneofs)
type modelField struct {
goName string
jsonName string
goType string
isKey bool
proto *pb.FieldDescriptorProto
}
var fields []modelField
for _, field := range msg.Field {
ft := field.GetType()
// Skip message and enum types (not directly storable as scalars)
if ft == pb.FieldDescriptorProto_TYPE_MESSAGE || ft == pb.FieldDescriptorProto_TYPE_GROUP {
continue
}
// Skip repeated fields (slices aren't directly storable)
if field.GetLabel() == pb.FieldDescriptorProto_LABEL_REPEATED {
continue
}
goName := generator.CamelCase(field.GetName())
jsonName := field.GetJsonName()
if jsonName == "" {
jsonName = field.GetName()
}
fields = append(fields, modelField{
goName: goName,
jsonName: jsonName,
goType: protoFieldGoType(field),
isKey: field.GetName() == keyField,
proto: field,
})
}
if len(fields) == 0 {
return
}
// Generate model struct
g.P()
g.P("// ", modelName, " is a model struct generated from ", msgName, ".")
g.P("// Use New", modelName, " to create a typed table backed by any model.Model.")
g.P("type ", modelName, " struct {")
for _, f := range fields {
tags := fmt.Sprintf("`json:%q", f.jsonName)
if f.isKey {
tags += ` model:"key"`
}
tags += "`"
g.P(f.goName, " ", f.goType, " ", tags)
}
g.P("}")
g.P()
// Generate Register helper: RegisterXModel(db) registers the model with the given backend.
g.P("// Register", modelName, " registers the ", modelName, " table with the given model backend.")
g.P("func Register", modelName, "(db ", modelPkg, ".Model) error {")
g.P("return db.Register(&", modelName, "{}, ", modelPkg, `.WithTable("`, tableName, `"))`)
g.P("}")
g.P()
// Generate FromProto: XModelFromProto(*X) *XModel
g.P("// ", modelName, "FromProto converts a ", msgName, " proto message to a ", modelName, ".")
g.P("func ", modelName, "FromProto(p *", msgName, ") *", modelName, " {")
g.P("if p == nil { return nil }")
g.P("return &", modelName, "{")
for _, f := range fields {
getter := "Get" + f.goName
g.P(f.goName, ": p.", getter, "(),")
}
g.P("}")
g.P("}")
g.P()
// Generate ToProto: (*XModel).ToProto() *X
g.P("// ToProto converts a ", modelName, " to a ", msgName, " proto message.")
g.P("func (m *", modelName, ") ToProto() *", msgName, " {")
g.P("if m == nil { return nil }")
g.P("return &", msgName, "{")
for _, f := range fields {
g.P(f.goName, ": m.", f.goName, ",")
}
g.P("}")
g.P("}")
g.P()
}

Some files were not shown because too many files have changed in this diff Show More