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
394 lines
13 KiB
Go
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)
|
|
}
|
|
}
|