Files
micro--go-micro/examples/mcp/workflow/main.go
T
wehub-resource-sync e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

394 lines
13 KiB
Go

// Workflow example: cross-service orchestration via AI agents.
//
// This example runs three services (Inventory, Orders, Notifications) and
// demonstrates how an AI agent can orchestrate a multi-step workflow:
//
// 1. Check inventory for a product
// 2. Place an order if in stock
// 3. Send a confirmation notification
//
// The agent figures out the right sequence of calls on its own — no
// workflow engine, no glue code, just natural language.
//
// Run:
//
// go run .
//
// MCP tools: http://localhost:3001/mcp/tools
package main
import (
"context"
"fmt"
"log"
"strings"
"sync"
"time"
"go-micro.dev/v6"
"go-micro.dev/v6/gateway/mcp"
)
// ---------------------------------------------------------------------------
// Inventory service
// ---------------------------------------------------------------------------
type Product struct {
SKU string `json:"sku" description:"Stock keeping unit identifier"`
Name string `json:"name" description:"Product name"`
Price float64 `json:"price" description:"Unit price in USD"`
InStock int `json:"in_stock" description:"Number of units available"`
Category string `json:"category" description:"Product category"`
}
type CheckStockRequest struct {
SKU string `json:"sku" description:"Product SKU to check"`
}
type CheckStockResponse struct {
Product *Product `json:"product" description:"Product details with current stock level"`
}
type SearchProductsRequest struct {
Query string `json:"query" description:"Search term to match against product name or category"`
Category string `json:"category,omitempty" description:"Filter by category: electronics, clothing, books (optional)"`
}
type SearchProductsResponse struct {
Products []*Product `json:"products" description:"Products matching the search criteria"`
}
type ReserveStockRequest struct {
SKU string `json:"sku" description:"Product SKU to reserve"`
Quantity int `json:"quantity" description:"Number of units to reserve"`
}
type ReserveStockResponse struct {
Reserved bool `json:"reserved" description:"True if stock was successfully reserved"`
Remaining int `json:"remaining" description:"Units remaining after reservation"`
Message string `json:"message" description:"Human-readable result message"`
}
type InventoryService struct {
mu sync.RWMutex
products map[string]*Product
}
// CheckStock returns the current stock level for a product.
// Use this before placing an order to verify availability.
//
// @example {"sku": "LAPTOP-001"}
func (s *InventoryService) CheckStock(ctx context.Context, req *CheckStockRequest, rsp *CheckStockResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.products[req.SKU]
if !ok {
return fmt.Errorf("product %s not found", req.SKU)
}
rsp.Product = p
return nil
}
// Search finds products by name or category. Use this to help
// users find what they're looking for before checking stock.
//
// @example {"query": "laptop"}
func (s *InventoryService) Search(ctx context.Context, req *SearchProductsRequest, rsp *SearchProductsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
q := strings.ToLower(req.Query)
for _, p := range s.products {
if req.Category != "" && !strings.EqualFold(p.Category, req.Category) {
continue
}
if q == "" || strings.Contains(strings.ToLower(p.Name), q) || strings.Contains(strings.ToLower(p.Category), q) {
rsp.Products = append(rsp.Products, p)
}
}
return nil
}
// ReserveStock decrements inventory for a product. Call this after
// confirming stock is available. Returns an error if insufficient stock.
//
// @example {"sku": "LAPTOP-001", "quantity": 1}
func (s *InventoryService) ReserveStock(ctx context.Context, req *ReserveStockRequest, rsp *ReserveStockResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.products[req.SKU]
if !ok {
return fmt.Errorf("product %s not found", req.SKU)
}
if p.InStock < req.Quantity {
rsp.Reserved = false
rsp.Remaining = p.InStock
rsp.Message = fmt.Sprintf("insufficient stock: requested %d but only %d available", req.Quantity, p.InStock)
return nil
}
p.InStock -= req.Quantity
rsp.Reserved = true
rsp.Remaining = p.InStock
rsp.Message = fmt.Sprintf("reserved %d units of %s", req.Quantity, p.Name)
return nil
}
// ---------------------------------------------------------------------------
// Orders service
// ---------------------------------------------------------------------------
type Order struct {
ID string `json:"id" description:"Unique order identifier"`
Customer string `json:"customer" description:"Customer name or email"`
SKU string `json:"sku" description:"Product SKU ordered"`
Quantity int `json:"quantity" description:"Number of units"`
Total float64 `json:"total" description:"Total order amount in USD"`
Status string `json:"status" description:"Order status: pending, confirmed, shipped, delivered"`
CreatedAt time.Time `json:"created_at" description:"When the order was placed"`
}
type PlaceOrderRequest struct {
Customer string `json:"customer" description:"Customer name or email (required)"`
SKU string `json:"sku" description:"Product SKU to order (required)"`
Quantity int `json:"quantity" description:"Number of units (required, must be positive)"`
}
type PlaceOrderResponse struct {
Order *Order `json:"order" description:"The newly created order"`
}
type GetOrderRequest struct {
ID string `json:"id" description:"Order ID to look up"`
}
type GetOrderResponse struct {
Order *Order `json:"order" description:"The requested order"`
}
type ListOrdersRequest struct {
Customer string `json:"customer,omitempty" description:"Filter by customer (optional)"`
Status string `json:"status,omitempty" description:"Filter by status (optional)"`
}
type ListOrdersResponse struct {
Orders []*Order `json:"orders" description:"Matching orders"`
}
type OrderService struct {
mu sync.RWMutex
orders map[string]*Order
nextID int
// In a real app this would be a client to the inventory service
inventory *InventoryService
}
// PlaceOrder creates a new order. Stock must be reserved first via
// InventoryService.ReserveStock — this service does not check inventory.
//
// @example {"customer": "alice@example.com", "sku": "LAPTOP-001", "quantity": 1}
func (s *OrderService) PlaceOrder(ctx context.Context, req *PlaceOrderRequest, rsp *PlaceOrderResponse) error {
if req.Customer == "" {
return fmt.Errorf("customer is required")
}
if req.SKU == "" {
return fmt.Errorf("sku is required")
}
if req.Quantity <= 0 {
return fmt.Errorf("quantity must be positive")
}
// Look up price
s.inventory.mu.RLock()
p, ok := s.inventory.products[req.SKU]
s.inventory.mu.RUnlock()
if !ok {
return fmt.Errorf("product %s not found", req.SKU)
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
order := &Order{
ID: fmt.Sprintf("ORD-%04d", s.nextID),
Customer: req.Customer,
SKU: req.SKU,
Quantity: req.Quantity,
Total: p.Price * float64(req.Quantity),
Status: "confirmed",
CreatedAt: time.Now(),
}
s.orders[order.ID] = order
rsp.Order = order
return nil
}
// GetOrder retrieves an order by ID.
//
// @example {"id": "ORD-0001"}
func (s *OrderService) GetOrder(ctx context.Context, req *GetOrderRequest, rsp *GetOrderResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
o, ok := s.orders[req.ID]
if !ok {
return fmt.Errorf("order %s not found", req.ID)
}
rsp.Order = o
return nil
}
// ListOrders returns orders, optionally filtered by customer or status.
//
// @example {"customer": "alice@example.com"}
func (s *OrderService) ListOrders(ctx context.Context, req *ListOrdersRequest, rsp *ListOrdersResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, o := range s.orders {
if req.Customer != "" && o.Customer != req.Customer {
continue
}
if req.Status != "" && o.Status != req.Status {
continue
}
rsp.Orders = append(rsp.Orders, o)
}
return nil
}
// ---------------------------------------------------------------------------
// Notifications service
// ---------------------------------------------------------------------------
type Notification struct {
ID string `json:"id" description:"Notification identifier"`
Recipient string `json:"recipient" description:"Who received the notification"`
Subject string `json:"subject" description:"Notification subject line"`
Body string `json:"body" description:"Notification body text"`
Channel string `json:"channel" description:"Delivery channel: email, sms, or slack"`
SentAt time.Time `json:"sent_at" description:"When the notification was sent"`
}
type SendNotificationRequest struct {
Recipient string `json:"recipient" description:"Email address, phone number, or Slack handle"`
Subject string `json:"subject" description:"Subject line (required)"`
Body string `json:"body" description:"Message body (required)"`
Channel string `json:"channel,omitempty" description:"Channel: email (default), sms, or slack"`
}
type SendNotificationResponse struct {
Notification *Notification `json:"notification" description:"The sent notification with delivery details"`
}
type ListNotificationsRequest struct {
Recipient string `json:"recipient,omitempty" description:"Filter by recipient (optional)"`
}
type ListNotificationsResponse struct {
Notifications []*Notification `json:"notifications" description:"Sent notifications"`
}
type NotificationService struct {
mu sync.RWMutex
notifications []*Notification
nextID int
}
// Send delivers a notification to a recipient via the specified channel.
// Use this to confirm orders, alert users, or send updates.
// Defaults to email if no channel is specified.
//
// @example {"recipient": "alice@example.com", "subject": "Order Confirmed", "body": "Your order ORD-0001 has been confirmed.", "channel": "email"}
func (s *NotificationService) Send(ctx context.Context, req *SendNotificationRequest, rsp *SendNotificationResponse) error {
if req.Recipient == "" {
return fmt.Errorf("recipient is required")
}
if req.Subject == "" {
return fmt.Errorf("subject is required")
}
if req.Body == "" {
return fmt.Errorf("body is required")
}
channel := req.Channel
if channel == "" {
channel = "email"
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
n := &Notification{
ID: fmt.Sprintf("notif-%d", s.nextID),
Recipient: req.Recipient,
Subject: req.Subject,
Body: req.Body,
Channel: channel,
SentAt: time.Now(),
}
s.notifications = append(s.notifications, n)
rsp.Notification = n
return nil
}
// List returns sent notifications, optionally filtered by recipient.
//
// @example {"recipient": "alice@example.com"}
func (s *NotificationService) List(ctx context.Context, req *ListNotificationsRequest, rsp *ListNotificationsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, n := range s.notifications {
if req.Recipient != "" && n.Recipient != req.Recipient {
continue
}
rsp.Notifications = append(rsp.Notifications, n)
}
return nil
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
func main() {
service := micro.NewService("shop",
micro.Address(":9090"),
mcp.WithMCP(":3001"),
)
service.Init()
inventory := &InventoryService{products: map[string]*Product{
"LAPTOP-001": {SKU: "LAPTOP-001", Name: "ThinkPad X1 Carbon", Price: 1299.99, InStock: 15, Category: "electronics"},
"LAPTOP-002": {SKU: "LAPTOP-002", Name: "MacBook Air M3", Price: 1099.00, InStock: 8, Category: "electronics"},
"PHONE-001": {SKU: "PHONE-001", Name: "Pixel 8 Pro", Price: 899.00, InStock: 23, Category: "electronics"},
"BOOK-001": {SKU: "BOOK-001", Name: "Designing Data-Intensive Applications", Price: 45.99, InStock: 50, Category: "books"},
"BOOK-002": {SKU: "BOOK-002", Name: "The Go Programming Language", Price: 39.99, InStock: 0, Category: "books"},
"SHIRT-001": {SKU: "SHIRT-001", Name: "Go Gopher T-Shirt", Price: 24.99, InStock: 100, Category: "clothing"},
}}
orders := &OrderService{
orders: make(map[string]*Order),
inventory: inventory,
}
notifications := &NotificationService{}
service.Handle(inventory)
service.Handle(orders)
service.Handle(notifications)
fmt.Println()
fmt.Println(" Shop Workflow Demo")
fmt.Println()
fmt.Println(" MCP Tools: http://localhost:3001/mcp/tools")
fmt.Println()
fmt.Println(" Try asking an agent:")
fmt.Println()
fmt.Println(" \"What laptops do you have in stock?\"")
fmt.Println(" \"Order a ThinkPad for alice@example.com and send her a confirmation\"")
fmt.Println(" \"Check if 'The Go Programming Language' is available\"")
fmt.Println(" \"Show me all orders for alice@example.com\"")
fmt.Println(" \"Order 3 Go Gopher t-shirts for bob@example.com, reserve the stock, and notify him\"")
fmt.Println()
if err := service.Run(); err != nil {
log.Fatal(err)
}
}