Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.23.0
toolchain go1.23.7

require (
github.com/go-chi/chi/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/lestrrat-go/jwx/v2 v2.1.4
github.com/redis/go-redis/v9 v9.10.0
Expand Down
2 changes: 2 additions & 0 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
Expand Down
219 changes: 219 additions & 0 deletions examples/multi_endpoint/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Multi-Agent Server Example

This example demonstrates how to run **multiple agents in a single process** using different URL paths to define separate endpoints for each agent.

## Architecture Overview

**Single Process, Multiple Agents**: This implementation runs all agents within one server process, using URL path routing to distinguish between different agents. Each agent gets its own endpoint path but shares the same server resources.

```
Single Server Process (localhost:8080)
├── /api/v1/agent/chatAgent/ → Chat Agent
└── /api/v1/agent/workerAgent/ → Worker Agent
```

## Key Features

- **Single Process Architecture**: All agents run in one server process
- **Path-Based Routing**: Each agent has a unique URL path endpoint
- **Dynamic Agent Discovery**: URL path parameter `{agentName}` determines which agent handles the request
- **Shared Resources**: All agents share the same server process, memory, and resources
- **Unified Protocol**: All agents follow the same A2A protocol interface

## Available Agents

### Chat Agent (`chatAgent`)
- **Function**: Provides chat conversation capabilities
- **Description**: "I am a chatbot"
- **Endpoints**:
- Agent Card: `GET /api/v1/agent/chatAgent/.well-known/agent.json`
- JSON-RPC: `POST /api/v1/agent/chatAgent/`

### Worker Agent (`workerAgent`)
- **Function**: Provides worker task processing
- **Description**: "I am a worker"
- **Endpoints**:
- Agent Card: `GET /api/v1/agent/workerAgent/.well-known/agent.json`
- JSON-RPC: `POST /api/v1/agent/workerAgent/`

## Running the Example

### 1. Start the Server
```bash
cd examples/multiagent/server
go run main.go
```

The server will start on `localhost:8080` and display available endpoints:
```
Starting A2A server listening on localhost:8080
Chat agent card url : http://localhost:8080/api/v1/agent/chatAgent/.well-known/agent.json:
Chat agent interfaces: http://localhost:8080/api/v1/agent/chatAgent/
Worker agent card url: http://localhost:8080/api/v1/agent/workerAgent/.well-known/agent.json
Worker agent interfaces: http://localhost:8080/api/v1/agent/workerAgent/
```

### 2. Run the Test Client
In another terminal:
```bash
cd examples/multiagent/client
go run main.go
```

Or with custom message:
```bash
go run main.go -message="How are you today?"
```

### 3. Manual Testing

#### Get Agent Cards
```bash
# Chat Agent
curl http://localhost:8080/api/v1/agent/chatAgent/.well-known/agent.json

# Worker Agent
curl http://localhost:8080/api/v1/agent/workerAgent/.well-known/agent.json
```

#### Send Messages to Agents
```bash
# Send message to Chat Agent
curl -X POST http://localhost:8080/api/v1/agent/chatAgent/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"message/send","params":{"message":{"role":"user","kind":"message","messageId":"test-123","parts":[{"kind":"text","text":"Hello!"}]}},"id":1}'

# Send message to Worker Agent
curl -X POST http://localhost:8080/api/v1/agent/workerAgent/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"message/send","params":{"message":{"role":"user","kind":"message","messageId":"test-456","parts":[{"kind":"text","text":"What can you do?"}]}},"id":1}'
```

## Core Implementation

### 1. HTTPRouter Interface
```go
type HTTPRouter interface {
Handle(pattern string, handler http.Handler)
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
```

### 2. Using Chi Router with Path Parameters
```go
// Create Chi router
router := chi.NewMux()

// Register routes with path parameters using Chi's Route method
router.Route("/api/v1/agent/{agentName}", func(r chi.Router) {
// Create A2A server for this route group
a2aServer, err := server.NewA2AServer(
agentCard,
taskManager,
server.WithMiddleWare(&middleWare{}),
server.WithHTTPRouter(r),
server.WithAgentCardHandler(&multiAgentCardHandler{}),
)
if err != nil {
log.Fatalf("Failed to create A2A server: %v", err)
}

// Mount the A2A server handler to the subrouter
r.Mount("/", a2aServer.Handler())
})
```

### 3. Middleware for Agent Name Extraction
```go
type middleWare struct{}

func (m *middleWare) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, ctxAgentNameKey, chi.URLParam(r, "agentName"))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```

### 4. Dynamic Agent Card Handler
```go
type multiAgentCardHandler struct{}

func (h *multiAgentCardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
agentName := chi.URLParam(r, "agentName")
var agentCard server.AgentCard

switch agentName {
case "chatAgent":
agentCard = server.AgentCard{
Name: "ChatAgent",
Description: "I am a chatbot",
// ... other fields
}
case "workerAgent":
agentCard = server.AgentCard{
Name: "WorkerAgent",
Description: "I am a worker",
// ... other fields
}
default:
w.WriteHeader(http.StatusNotFound)
return
}

w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(agentCard)
}
```

## Extension Example

You can easily add more agents by extending the switch cases in the handlers:

```go
// Add a new agent in multiAgentCardHandler
case "calculatorAgent":
agentCard = server.AgentCard{
Name: "CalculatorAgent",
Description: "I can perform mathematical calculations",
URL: fmt.Sprintf("http://%s/api/v1/agent/calculatorAgent/", *host),
Skills: []server.AgentSkill{
{
Name: "calculate",
Description: stringPtr("Perform mathematical operations"),
InputModes: []string{"text"},
OutputModes: []string{"text"},
Tags: []string{"math", "calculator"},
Examples: []string{"2 + 2", "10 * 5"},
},
},
// ... other fields
}

// Add corresponding case in multiAgentProcessor
case "calculatorAgent":
return u.calculatorAgentProcessMessage(ctx, message, options, taskHandler)
```

## Client Usage

The client automatically tests both agents and displays their responses:

```bash
=== Multi-Agent tRPC Client Demo ===
Server: http://localhost:8080
Testing both agents...

--- Conversation with chatAgent ---
Agent: ChatAgent - I am a chatbot
User: Hello, how are you?
Response: Hello from chat agent!

--- Conversation with workerAgent ---
Agent: WorkerAgent - I am a worker
User: Hello, how are you?
Response: Hello from worker agent!

=== Demo completed ===
```
145 changes: 145 additions & 0 deletions examples/multi_endpoint/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Package main implements a simple A2A client example.
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"time"

"trpc.group/trpc-go/trpc-a2a-go/client"
"trpc.group/trpc-go/trpc-a2a-go/protocol"
"trpc.group/trpc-go/trpc-a2a-go/server"
)

var (
host = flag.String("host", "localhost:8080", "server host")
message = flag.String("message", "Hello!", "message to send")
)

func main() {
flag.Parse()

fmt.Printf("=== Multi-Agent tRPC Client Demo ===\n")
fmt.Printf("Server: http://%s\n", *host)
fmt.Printf("Testing both agents...\n\n")

// Test both agents
agents := []string{"chatAgent", "workerAgent"}
for _, agentName := range agents {
fmt.Printf("--- Conversation with %s ---\n", agentName)

// Create agent URL
agentURL := fmt.Sprintf("http://%s/api/v1/agent/%s/", *host, agentName)

// Get agent card first
agentCard, err := getAgentCard(agentURL)
if err != nil {
log.Printf("Failed to get agent card for %s: %v", agentName, err)
continue
}
fmt.Printf("Agent: %s - %s\n", agentCard.Name, agentCard.Description)

// Create A2A client for this agent
a2aClient, err := client.NewA2AClient(agentURL, client.WithTimeout(30*time.Second))
if err != nil {
log.Printf("Failed to create A2A client for %s: %v", agentName, err)
continue
}

// Send message using A2A client
response, err := sendMessageToAgent(context.Background(), a2aClient, *message)
if err != nil {
log.Printf("Failed to send message to %s: %v", agentName, err)
continue
}
fmt.Printf("Response: %s\n\n", response)
}

fmt.Println("=== Demo completed ===")
}

// getAgentCard retrieves the agent card for the specified agent
func getAgentCard(agentURL string) (*server.AgentCard, error) {
cardURL := fmt.Sprintf("%s.well-known/agent.json", agentURL)

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(cardURL)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: failed to get agent card", resp.StatusCode)
}

var agentCard server.AgentCard
if err := json.NewDecoder(resp.Body).Decode(&agentCard); err != nil {
return nil, fmt.Errorf("failed to parse agent card: %w", err)
}

return &agentCard, nil
}

// sendMessageToAgent sends a message to the specified agent using A2A client
func sendMessageToAgent(ctx context.Context, a2aClient *client.A2AClient, messageText string) (string, error) {

fmt.Printf("User: %s\n", messageText)
// Create the message to send
userMessage := protocol.NewMessage(
protocol.MessageRoleUser,
[]protocol.Part{protocol.NewTextPart(messageText)},
)

// Create message parameters
params := protocol.SendMessageParams{
Message: userMessage,
Configuration: &protocol.SendMessageConfiguration{
Blocking: boolPtr(true), // Use blocking mode for simplicity
AcceptedOutputModes: []string{"text"},
},
}

// Send message using A2A client
messageResult, err := a2aClient.SendMessage(ctx, params)
if err != nil {
return "", fmt.Errorf("failed to send message: %w", err)
}

// Extract text from the response
switch result := messageResult.Result.(type) {
case *protocol.Message:
return extractTextFromMessage(result), nil
case *protocol.Task:
if result.Status.Message != nil {
return extractTextFromMessage(result.Status.Message), nil
}
return fmt.Sprintf("Task %s - State: %s", result.ID, result.Status.State), nil
default:
return fmt.Sprintf("Unknown result type: %T", result), nil
}
}

// extractTextFromMessage extracts text content from a message
func extractTextFromMessage(msg *protocol.Message) string {
if msg == nil {
return ""
}

for _, part := range msg.Parts {
if textPart, ok := part.(*protocol.TextPart); ok {
return textPart.Text
}
}

return "(no text content)"
}

// boolPtr returns a pointer to a boolean value
func boolPtr(b bool) *bool {
return &b
}
Loading
Loading