Skip to content
Open
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
60 changes: 60 additions & 0 deletions examples/weather/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Weather Agent Example

This example demonstrates a simple, non-streaming A2A (Agent-to-Agent) agent that provides real-time weather information.

It consists of two parts:
1. `server`: The A2A agent that listens for requests.
2. `client`: A command-line client to interact with the agent.

The agent fetches weather data from the public API at [wttr.in](https://wttr.in/).

## How to Run

You will need two separate terminal windows to run the server and the client.

### 1. Run the Server

In your first terminal, navigate to the server directory and run the `main.go` file:

```bash
cd examples/weather/server
go run .
```

The server will start and listen on `http://localhost:8080`. You should see the following output:

```
INFO[YYYY-MM-DD HH:MM:SS] Starting Weather Agent server on localhost:8080...
```

### 2. Run the Client

In your second terminal, navigate to the client directory. You can run the client to get the weather for a specific city.

**To get the weather for the default city (shenzhen):**

```bash
cd examples/weather/client
go run .
```

**To get the weather for a different city (e.g., London):**

Use the `-city` command-line flag:

```bash
cd examples/weather/client
go run . -city=London
```

### Expected Output

If successful, the client will print output similar to this:

```
INFO[YYYY-MM-DD HH:MM:SS] Connecting to agent: http://localhost:8080/ (Timeout: 30s)
INFO[YYYY-MM-DD HH:MM:SS] Requesting weather for city: London
INFO[YYYY-MM-DD HH:MM:SS] Message sent successfully!
INFO[YYYY-MM-DD HH:MM:SS] Received weather report:
INFO[YYYY-MM-DD HH:MM:SS] >> London: ⛅️ +18°C
```
103 changes: 103 additions & 0 deletions examples/weather/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Package main implements the weather client.
// This is the client-side counterpart to the Weather Agent server.
// It demonstrates how to use the trpc-a2a-go client library to interact with an A2A-compliant agent.
package main

import (
"context"
"flag"
"time"

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

func main() {
// --- 1. Parse Command-Line Arguments ---
// The 'flag' package provides a simple way to create command-line tools.
// We define flags for the agent's URL, a request timeout, and the city to query.
agentURL := flag.String("agent", "http://localhost:8080/", "Target A2A agent URL")
timeout := flag.Duration("timeout", 30*time.Second, "Request timeout (e.g., 30s, 1m)")
city := flag.String("city", "shenzhen", "City name to get weather for")
// flag.Parse() must be called to process the command-line arguments.
flag.Parse()

// --- 2. Create A2A Client ---
// NewA2AClient is the factory function for creating a client instance.
// We pass the agent's URL and configure a timeout using the WithTimeout option.
a2aClient, err := client.NewA2AClient(*agentURL, client.WithTimeout(*timeout))
if err != nil {
log.Fatalf("Failed to create A2A client: %v", err)
}

log.Infof("Connecting to agent: %s (Timeout: %v)", *agentURL, *timeout)

// --- 3. Construct the Message to Send ---
// According to the A2A protocol, a message consists of a role and a list of parts.
// Here, we send the city name as a single text part from the 'user'.
userMessage := protocol.NewMessage(
protocol.MessageRoleUser,
[]protocol.Part{protocol.NewTextPart(*city)},
)

// --- 4. Construct Request Parameters ---
// SendMessageParams bundles the message and its configuration.
params := protocol.SendMessageParams{
Message: userMessage,
Configuration: &protocol.SendMessageConfiguration{
// 'Blocking: true' means this is a standard, synchronous request.
// The client will wait for the final result from the agent.
Blocking: boolPtr(true),
// 'AcceptedOutputModes' tells the agent what kind of content we can handle.
// For this simple client, we only expect plain text.
AcceptedOutputModes: []string{"text"},
},
}

log.Infof("Requesting weather for city: %s", *city)

// --- 5. Create a Context with Timeout ---
// Context is crucial for controlling request lifecycle, especially for cancellation and deadlines.
// context.WithTimeout creates a context that will be automatically cancelled after the timeout duration.
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
// 'defer cancel()' is a critical pattern in Go. It ensures that the context's resources
// are released when the function returns, preventing resource leaks.
defer cancel()

// --- 6. Send the Message ---
// a2aClient.SendMessage performs the actual JSON-RPC call to the agent.
// It blocks until a response is received, an error occurs, or the context is cancelled.
messageResult, err := a2aClient.SendMessage(ctx, params)
if err != nil {
log.Fatalf("Failed to send message: %v", err)
}

log.Infof("Message sent successfully!")

// --- 7. Process and Print the Result ---
// The result from a non-blocking call can be of various types (e.g., Message, Task).
// We use a type switch to safely check what kind of result we received.
switch result := messageResult.Result.(type) {
case *protocol.Message:
// This is the expected case for our simple weather agent.
log.Infof("Received weather report:")
// A message can have multiple parts, so we iterate through them.
for _, part := range result.Parts {
// We use a type assertion to check if the part is a TextPart.
if textPart, ok := part.(*protocol.TextPart); ok {
log.Infof(">> %s", textPart.Text)
}
}
default:
// If the agent returns something unexpected, we log its type.
log.Infof("Received unexpected result type: %T", result)
}
}

// boolPtr is a helper function to get a pointer to a boolean value.
// This is needed because many fields in the protocol structs are pointers
// to distinguish between a value being false and a value not being set at all.
func boolPtr(b bool) *bool {
return &b
}
197 changes: 197 additions & 0 deletions examples/weather/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Package main implements a Weather Agent server.
// This example demonstrates how to build a simple, non-streaming A2A agent
// that provides weather information based on a city name.
package main

import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/signal"
"syscall"

"trpc.group/trpc-go/trpc-a2a-go/log"
"trpc.group/trpc-go/trpc-a2a-go/protocol"
"trpc.group/trpc-go/trpc-a2a-go/server"
"trpc.group/trpc-go/trpc-a2a-go/taskmanager"
)

// weatherProcessor implements the taskmanager.MessageProcessor interface.
// This struct is where the core business logic of our agent resides.
type weatherProcessor struct{}

// ProcessMessage is the single required method for the MessageProcessor interface.
// It gets called by the task manager whenever a new message arrives.
func (p *weatherProcessor) ProcessMessage(
ctx context.Context,
message protocol.Message,
options taskmanager.ProcessOptions,
handler taskmanager.TaskHandler,
) (*taskmanager.MessageProcessingResult, error) {
// For this simple agent, we only support non-streaming (blocking) requests.
if options.Streaming {
errMsg := "This weather agent does not support streaming."
log.Errorf(errMsg)
errorMessage := protocol.NewMessage(
protocol.MessageRoleAgent,
[]protocol.Part{protocol.NewTextPart(errMsg)},
)
return &taskmanager.MessageProcessingResult{Result: &errorMessage}, nil
}

// Extract the city name from the incoming message.
location := extractText(message)
if location == "" {
errMsg := "Please provide a city name to get the weather."
log.Errorf("Message processing failed: %s", errMsg)
errorMessage := protocol.NewMessage(
protocol.MessageRoleAgent,
[]protocol.Part{protocol.NewTextPart(errMsg)},
)
return &taskmanager.MessageProcessingResult{Result: &errorMessage}, nil
}

log.Infof("Processing message with input location: %s", location)

// Call our business logic function to get the weather.
weatherInfo, err := getWeather(location, "https://wttr.in/")
if err != nil {
errMsg := fmt.Sprintf("Failed to get weather for %s: %v", location, err)
log.Errorf("getWeather failed: %s", errMsg)
errorMessage := protocol.NewMessage(
protocol.MessageRoleAgent,
[]protocol.Part{protocol.NewTextPart(errMsg)},
)
return &taskmanager.MessageProcessingResult{Result: &errorMessage}, nil
}

// If successful, create a message with the weather info.
successMessage := protocol.NewMessage(
protocol.MessageRoleAgent,
[]protocol.Part{protocol.NewTextPart(weatherInfo)},
)

// Wrap the success message in the result struct.
return &taskmanager.MessageProcessingResult{
Result: &successMessage,
}, nil
}

// extractText is a helper function to get the first text part from a message.
func extractText(message protocol.Message) string {
for _, part := range message.Parts {
if textPart, ok := part.(*protocol.TextPart); ok {
return textPart.Text
}
}
return ""
}

// getWeather fetches weather information from the external wttr.in API.
func getWeather(location string, baseURL string) (string, error) {
// URL-encode the location to handle spaces or special characters safely.
encodedLocation := url.QueryEscape(location)
// We use the simple format=3 for a concise weather report.
fullURL := fmt.Sprintf("%s/%s?format=3", baseURL, encodedLocation)

resp, err := http.Get(fullURL)
if err != nil {
// Wrap the error with context using %w, preserving the original error.
return "", fmt.Errorf("failed to request weather API: %w", err)
}
// It's crucial to close the response body to prevent resource leaks.
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("weather API returned non-200 status: %s", resp.Status)
}

bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read weather API response: %w", err)
}

return string(bodyBytes), nil
}

// boolPtr is a helper function to get a pointer to a boolean value.
func boolPtr(b bool) *bool {
return &b
}

func main() {
host := "localhost"
port := 8080

// --- Define the Agent's Identity Card ---
// The AgentCard provides metadata about the agent to clients.
agentCard := server.AgentCard{
Name: "Weather Agent",
Description: "An agent that provides real-time weather for a given city.",
URL: fmt.Sprintf("http://%s:%d/", host, port),
Version: "1.0.0",
Provider: &server.AgentProvider{
Organization: "tRPC-A2A-Go Examples",
},
// Capabilities clearly state what this agent can and cannot do.
Capabilities: server.AgentCapabilities{
Streaming: boolPtr(false), // We explicitly say we don't support streaming.
PushNotifications: boolPtr(false),
StateTransitionHistory: boolPtr(false),
},
DefaultInputModes: []string{"text"},
DefaultOutputModes: []string{"text"},
// Skills describe the specific tasks the agent can perform.
Skills: []server.AgentSkill{
{
ID: "weather_query",
Name: "Weather Query",
Description: func(s string) *string { return &s }("Get real-time weather for a city by name."),
Tags: []string{"weather", "location"},
Examples: []string{"beijing", "London"},
InputModes: []string{"text"},
OutputModes: []string{"text"},
},
},
}

// --- Set up the Core Components ---
processor := &weatherProcessor{}
// The task manager is responsible for orchestrating message processing.
// We use a simple in-memory manager for this example.
taskManager, err := taskmanager.NewMemoryTaskManager(processor)
if err != nil {
log.Fatalf("Failed to create task manager: %v", err)
}

// The server ties the agent card and task manager together into an HTTP service.
srv, err := server.NewA2AServer(agentCard, taskManager)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}

// --- Graceful Shutdown Handling ---
// This setup ensures that if we press Ctrl+C, the server shuts down cleanly.
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

// Start the server in a new goroutine so it doesn't block the main thread.
go func() {
serverAddr := fmt.Sprintf("%s:%d", host, port)
log.Infof("Starting Weather Agent server on %s...", serverAddr)
// srv.Start is a blocking call.
if err := srv.Start(serverAddr); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed: %v", err)
}
}()

// Block the main thread here, waiting for a shutdown signal.
sig := <-sigChan
log.Infof("Received signal %v, shutting down...", sig)

// Implement graceful shutdown logic here in a real-world application.
// For this example, we simply exit.
}
Loading
Loading