Skip to content

Latest commit

 

History

History
779 lines (638 loc) · 18 KB

File metadata and controls

779 lines (638 loc) · 18 KB
title Agent Client
description Learn how to interact with Agent APIs using the AgentClient class from torus-ts-sdk

import { CardGrid, LinkCard, Aside, Tabs, TabItem, } from "@astrojs/starlight/components";

The AgentClient class provides a simple way to interact with Agent APIs built with the AgentServer. It handles JWT authentication automatically and provides a clean interface for making API calls.

Overview

The AgentClient simplifies communication with Agent APIs by:

  • Automatic Authentication: Creates and manages JWT tokens using SR25519 signatures
  • Type-Safe Responses: Provides structured success/error responses
  • HTTP Method Support: Supports GET, POST, PUT, PATCH, and DELETE requests
  • Error Handling: Consistent error handling with detailed error information

Installation

npm install @@torus-network/sdk

Basic Usage

import { AgentClient, Keypair } from "@@torus-network/sdk";

// Create a keypair from your mnemonic
const keypair = new Keypair(
  "your twelve word mnemonic phrase goes here exactly like this"
);

// Create the client
const client = new AgentClient({
  keypair,
  baseUrl: "http://localhost:3000",
});

// Make a call to the agent
const response = await client.call({
  endpoint: "hello",
  data: { name: "Alice" },
});

if (response.success) {
  console.log("Response:", response.data);
} else {
  console.error("Error:", response.error);
}

Configuration

AgentClientOptions

interface AgentClientOptions {
  /** The keypair for creating JWT tokens */
  keypair: Keypair;
  /** The base URL of the agent server */
  baseUrl: string;
}

Creating a Keypair

The Keypair class handles SR25519 key generation and JWT token creation:

import { Keypair } from "@@torus-network/sdk";

// Create from mnemonic
const keypair = new Keypair(
  "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
);

// Get key information
const keyInfo = await keypair.getKeyInfo();
console.log("Address:", keyInfo.address);
console.log("Public Key:", keyInfo.publicKey);
Never hardcode mnemonics in your source code. Use environment variables or secure key management systems in production.

Making API Calls

Call Options

interface CallOptions {
  /** The endpoint path (without leading slash) */
  endpoint: string;
  /** The HTTP method to use */
  method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; // Default: "POST"
  /** The request body data */
  data?: unknown;
}

Response Structure

interface CallResponse<T = unknown> {
  /** Whether the request was successful */
  success: boolean;
  /** The response data (present if success is true) */
  data?: T;
  /** Error information (present if success is false) */
  error?: {
    message: string;
    code?: string;
    status: number;
  };
}

HTTP Methods

```ts // POST with data const response = await client.call({ endpoint: "store-memory", data: { content: "Remember this important information", tags: ["important", "meeting"] } });

if (response.success) { console.log("Memory stored with ID:", response.data.id); }

</TabItem>
<TabItem label="GET">
```ts
// GET request
const response = await client.call({
  endpoint: "get-memory",
  method: "GET",
  data: { id: "memory-123" }
});

if (response.success) {
  console.log("Memory content:", response.data.content);
}
```ts // PUT request for full replacement const response = await client.call({ endpoint: "item", method: "PUT", data: { id: "memory-123", content: "Complete new memory content", tags: ["updated", "important"], metadata: { type: "note", priority: "high" } // Full resource replacement } }); ``` ```ts // PATCH request for partial updates const response = await client.call({ endpoint: "item", method: "PATCH", data: { id: "memory-123", updates: { content: "Updated memory content", tags: ["updated", "important"] } // Only updating specific fields } }); ``` ```ts // DELETE request const response = await client.call({ endpoint: "delete-memory", method: "DELETE", data: { id: "memory-123" } });

if (response.success) { console.log("Memory deleted successfully"); }

</TabItem>
</Tabs>

## Error Handling

The AgentClient provides consistent error handling for various failure scenarios:

### Network Errors

```ts
const response = await client.call({
  endpoint: "test-endpoint",
  data: { test: "data" }
});

if (!response.success) {
  switch (response.error?.status) {
    case 0:
      console.error("Network error:", response.error.message);
      break;
    case 401:
      console.error("Authentication failed:", response.error.message);
      break;
    case 403:
      console.error("Access denied:", response.error.message);
      break;
    case 400:
      console.error("Bad request:", response.error.message);
      break;
    default:
      console.error("Unexpected error:", response.error.message);
  }
}

Structured Error Responses

const response = await client.call({
  endpoint: "risky-operation",
  data: { action: "dangerous" },
});

if (!response.success && response.error?.code) {
  switch (response.error.code) {
    case "FORBIDDEN_ACTION":
      console.error("This action is not allowed");
      break;
    case "NAMESPACE_ACCESS_DENIED":
      console.error("Insufficient permissions");
      break;
    default:
      console.error("Unknown error:", response.error.code);
  }
}

Type Safety

Use TypeScript generics to ensure type safety for your API responses:

interface MemoryResponse {
  id: string;
  content: string;
  timestamp: number;
  tags: string[];
}

interface ErrorResponse {
  error: string;
  code: string;
}

// Type-safe API call
const response = await client.call<MemoryResponse>({
  endpoint: "get-memory",
  method: "GET",
  data: { id: "memory-123" },
});

if (response.success) {
  // response.data is now typed as MemoryResponse
  console.log("Memory ID:", response.data.id);
  console.log("Content:", response.data.content);
  console.log("Tags:", response.data.tags);
}

Complete Examples

Memory Agent Client

import { AgentClient, Keypair } from "@@torus-network/sdk";

class MemoryAgentClient {
  private client: AgentClient;

  constructor(mnemonic: string, baseUrl: string) {
    const keypair = new Keypair(mnemonic);
    this.client = new AgentClient({ keypair, baseUrl });
  }

  async storeMemory(
    content: string,
    tags?: string[],
    metadata?: Record<string, any>
  ) {
    const response = await this.client.call({
      endpoint: "store",
      data: { content, tags, metadata },
    });

    if (response.success) {
      return {
        success: true,
        id: response.data.id,
        timestamp: response.data.timestamp,
      };
    } else {
      return {
        success: false,
        error: response.error?.message || "Failed to store memory",
      };
    }
  }

  async retrieveMemory(id: string) {
    const response = await this.client.call({
      endpoint: "retrieve",
      method: "GET",
      data: { id },
    });

    if (response.success) {
      return {
        success: true,
        memory: response.data,
      };
    } else {
      return {
        success: false,
        error: response.error?.message || "Failed to retrieve memory",
      };
    }
  }

  async searchMemories(query: string, tags?: string[], limit = 10) {
    const response = await this.client.call({
      endpoint: "search",
      data: { query, tags, limit },
    });

    if (response.success) {
      return {
        success: true,
        results: response.data.results,
        total: response.data.total,
      };
    } else {
      return {
        success: false,
        error: response.error?.message || "Search failed",
      };
    }
  }
}

// Usage
const memoryClient = new MemoryAgentClient(
  "your mnemonic phrase here",
  "http://localhost:3000"
);

// Store a memory
const storeResult = await memoryClient.storeMemory(
  "Important meeting notes from today",
  ["meeting", "important"],
  { date: "2024-01-15", attendees: ["Alice", "Bob"] }
);

if (storeResult.success) {
  console.log("Memory stored:", storeResult.id);

  // Retrieve the memory
  const retrieveResult = await memoryClient.retrieveMemory(storeResult.id);
  if (retrieveResult.success) {
    console.log("Retrieved memory:", retrieveResult.memory);
  }
}

// Search for memories
const searchResult = await memoryClient.searchMemories("meeting", [
  "important",
]);
if (searchResult.success) {
  console.log(`Found ${searchResult.total} memories`);
  searchResult.results.forEach((result) => {
    console.log(`- ${result.content} (score: ${result.score})`);
  });
}

Environment-based Configuration

import { AgentClient, Keypair } from "@@torus-network/sdk";

// Environment configuration
const config = {
  mnemonic: process.env.AGENT_MNEMONIC || "",
  baseUrl: process.env.AGENT_BASE_URL || "http://localhost:3000",
};

if (!config.mnemonic) {
  throw new Error("AGENT_MNEMONIC environment variable is required");
}

const keypair = new Keypair(config.mnemonic);
const client = new AgentClient({
  keypair,
  baseUrl: config.baseUrl,
});

// Helper function for making calls with retry logic
async function callWithRetry<T>(
  options: CallOptions,
  maxRetries = 3
): Promise<CallResponse<T>> {
  let lastError: any;

  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await client.call<T>(options);

      if (response.success) {
        return response;
      }

      // Don't retry on client errors (4xx)
      if (
        response.error?.status &&
        response.error.status >= 400 &&
        response.error.status < 500
      ) {
        return response;
      }

      lastError = response.error;

      // Wait before retry
      await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
    } catch (error) {
      lastError = error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
    }
  }

  return {
    success: false,
    error: {
      message: lastError?.message || "Max retries exceeded",
      status: lastError?.status || 0,
    },
  };
}

// Usage with retry
const response = await callWithRetry({
  endpoint: "important-operation",
  data: { critical: "data" },
});

Authentication Details

The AgentClient automatically handles JWT authentication:

  1. JWT Creation: Creates a JWT token using your keypair's SR25519 signature
  2. Token Management: Tokens are created fresh for each request
  3. Protocol Metadata: Includes protocol version information for compatibility
  4. Automatic Headers: Adds the Authorization: Bearer <token> header to all requests

JWT Token Structure

{
  "header": {
    "alg": "SR25519",
    "typ": "JWT"
  },
  "payload": {
    "sub": "5FgfC2DY4yreEWEughz46RZYQ8oBhHVqD9fVq6gV89E6z4Ea", // Your address
    "publicKey": "0x...", // Your public key
    "keyType": "sr25519", // Key type
    "addressInfo": {
      "addressType": "ss58", // Address encoding type
      "metadata": {
        "prefix": 42 // SS58 prefix for Substrate generic
      }
    },
    "iat": 1640995200, // Issued at timestamp
    "exp": 1640998800, // Expires at timestamp
    "nonce": "unique-uuid", // Prevents replay attacks
    "_protocol_metadata": {
      "version": "1.0.0" // Protocol version
    }
  }
}

Other Languages

If you're building an Agent Client in a language other than TypeScript, you'll need to implement the following protocol specifications:

JWT Token Generation

To authenticate with Agent Servers, you must generate JWT tokens with SR25519 signatures.

Required Components

  1. SR25519 Keypair: Generate from mnemonic seed phrase
  2. JWT Structure: Header + Payload + Signature
  3. Protocol Metadata: Version compatibility information

JWT Creation Process

```python import json import base64 import time import uuid from sr25519 import sign, keypair_from_mnemonic # Use appropriate SR25519 library

class AgentClient: def init(self, mnemonic: str, base_url: str): self.keypair = keypair_from_mnemonic(mnemonic) self.base_url = base_url.rstrip('/') self.public_key = self.keypair.public_key.hex() self.address = self.keypair.ss58_address

def create_jwt(self) -> str:
    now = int(time.time())

    # JWT Header
    header = {
        "alg": "SR25519",
        "typ": "JWT"
    }

    # JWT Payload
    payload = {
        "sub": self.address,
        "publicKey": f"0x{self.public_key}",
        "keyType": "sr25519",
        "addressInfo": {
            "addressType": "ss58",
            "metadata": {
                "prefix": 42
            }
        },
        "iat": now,
        "exp": now + 3600,  # 1 hour expiration
        "nonce": str(uuid.uuid4()),
        "_protocol_metadata": {
            "version": "1.0.0"
        }
    }

    # Encode header and payload
    header_encoded = base64.urlsafe_b64encode(
        json.dumps(header).encode()
    ).decode().rstrip('=')

    payload_encoded = base64.urlsafe_b64encode(
        json.dumps(payload).encode()
    ).decode().rstrip('=')

    # Create signing input
    signing_input = f"{header_encoded}.{payload_encoded}"

    # Sign with SR25519
    signature = sign(signing_input.encode(), self.keypair.secret_key)
    signature_encoded = base64.urlsafe_b64encode(signature).decode().rstrip('=')

    return f"{header_encoded}.{payload_encoded}.{signature_encoded}"

def call(self, endpoint: str, method: str = "POST", data: dict = None) -> dict:
    import requests

    jwt_token = self.create_jwt()
    url = f"{self.base_url}/{endpoint.lstrip('/')}"

    headers = {
        "Authorization": f"Bearer {jwt_token}",
        "Content-Type": "application/json"
    }

    try:
        if method.upper() == "GET":
            response = requests.get(url, headers=headers, params=data)
        else:
            response = requests.request(
                method.upper(), url, headers=headers, json=data
            )

        if response.status_code == 200:
            return {"success": True, "data": response.json()}
        else:
            error_data = response.json() if response.text else {}
            return {
                "success": False,
                "error": {
                    "message": error_data.get("message", f"HTTP {response.status_code}"),
                    "code": error_data.get("code"),
                    "status": response.status_code
                }
            }
    except Exception as e:
        return {
            "success": False,
            "error": {
                "message": str(e),
                "status": 0
            }
        }

Usage example

client = AgentClient("your mnemonic phrase here", "http://localhost:3000") result = client.call("hello", data={"name": "Alice"})

</TabItem>
</Tabs>

### HTTP Request Format

**Headers:**

Authorization: Bearer <JWT_TOKEN> Content-Type: application/json


**Request Body (POST/PUT/PATCH):**
```json
{
  "field1": "value1",
  "field2": "value2"
}

Response Handling

Success Response (200):

{
  "result": "success",
  "data": { ... }
}

Error Responses:

{
  "message": "Error description",
  "code": "ERROR_CODE"
}

SR25519 Key Generation

To generate keypairs from mnemonic phrases, you'll need SR25519 cryptographic libraries:

  • Python: sr25519-python or substrate-interface
  • Go: go-sr25519 or go-substrate-crypto
  • Rust: sr25519 or sp-core
  • JavaScript: @polkadot/util-crypto

Error Codes

Common error codes you should handle:

  • MISSING_AUTH_HEADERS: No Authorization header provided
  • INVALID_JWT: JWT token is malformed or invalid
  • JWT_EXPIRED: JWT token has expired
  • INVALID_SIGNATURE: SR25519 signature verification failed
  • NAMESPACE_ACCESS_DENIED: Insufficient permissions for endpoint
  • INVALID_INPUT: Request data validation failed

Best Practices

Security

  • Store mnemonics securely using environment variables
  • Use secure key management systems in production
  • Implement proper error handling to avoid information leakage
  • Validate responses before using the data

Performance

  • Reuse AgentClient instances when possible
  • Implement connection pooling for high-throughput scenarios
  • Use appropriate timeouts for network requests
  • Consider caching for frequently accessed data

Error Handling

  • Always check the success field before accessing data
  • Implement retry logic for transient failures
  • Log errors appropriately for debugging
  • Provide meaningful error messages to users
JWT tokens expire after 1 hour by default. The AgentClient creates a new token for each request, so you don't need to worry about token management.

Troubleshooting

Common Issues

Authentication Failures

// Check if your mnemonic is correct
const keyInfo = await keypair.getKeyInfo();
console.log("Using address:", keyInfo.address);

Network Errors

// Verify the base URL is correct
console.log("Connecting to:", client.baseUrl);

Permission Errors

// Check if you have the required capability permissions
if (response.error?.code === "NAMESPACE_ACCESS_DENIED") {
  console.log("You need permission for this namespace");
}