Skip to content

Custom Agent Integration Documentation

Overview

The Custom Agent integration allows users to connect to their own external agent API endpoints. This provides maximum flexibility, enabling users to:

  • Use their own hosted agent services
  • Connect to local development servers
  • Integrate with third-party agent APIs
  • Customize agent behavior and capabilities

Unlike the Multi-Agent and Browser-Agent which use hardcoded Prosus-hosted endpoints, the Custom Agent uses user-configured URLs that can point to any compatible API service.

Architecture

High-Level Architecture

Key Characteristics

  • User-Configurable: URLs are set by users in Agent Settings
  • Flexible Endpoints: Supports any compatible API endpoint
  • Multiple Modes: Supports streaming, non-streaming, and polling modes
  • Local Development: Can connect to localhost for development
  • Session Management: Supports session ID for conversation continuity
  • iFood Integration: Can include iFood authentication headers when available

API Endpoint Configuration

URL Configuration

Users configure three optional URLs in the Agent Settings:

  1. API URL: Endpoint for non-streaming requests
  2. Stream URL: Endpoint for streaming (SSE) requests
  3. Polling URL: Base URL for polling mode requests

File: app/agent-settings.tsx

Storage: URLs are stored in the local database:

  • custom_agent_api_url
  • custom_agent_api_url_stream
  • custom_agent_polling_url

URL Resolution

File: lib/database/repositories/agent-config.repository.ts

typescript
export function getAgentUrls(
  agentType: AgentType,
  customApiUrl?: string | null,
  customStreamUrl?: string | null,
  customPollingUrl?: string | null
): { apiUrl: string; streamUrl: string; pollingUrl: string } | null {
  if (agentType === 'custom') {
    if (customApiUrl) {
      return {
        apiUrl: customApiUrl,
        streamUrl: customStreamUrl || customApiUrl,  // Fallback to apiUrl
        pollingUrl: customPollingUrl || '',
      };
    }
  }
  return null;
}

Fallback Behavior:

  • If streamUrl is not provided, it falls back to apiUrl
  • If pollingUrl is not provided, polling mode is unavailable

Communication Modes

The Custom Agent supports all three communication modes:

1. Streaming Mode

Endpoint: User-configured streamUrl (or apiUrl as fallback)

Behavior:

  • Returns Server-Sent Events (SSE) format
  • Content is streamed incrementally
  • Provides real-time feedback to users

Implementation: hooks/use-chat-custom-agent.ts - handleCustomAgentStreamingResponse()

2. Non-Streaming Mode

Endpoint: User-configured apiUrl

Behavior:

  • Returns complete response in single JSON object
  • Simpler implementation
  • Useful when streaming is disabled

Implementation: hooks/use-chat-custom-agent.ts - handleCustomAgentNonStreamingResponse()

3. Polling Mode

Endpoint: User-configured pollingUrl (base URL)

Behavior:

  • Initiates task and polls for status
  • Best for long-running tasks
  • Same pattern as Browser Agent polling

Implementation: hooks/use-chat-polling-agent.ts - handlePollingAgentResponse()

Request Format

HTTP Method

POST (for streaming and non-streaming) POST + GET (for polling - POST to initialize, GET to poll status)

Headers

HeaderDescriptionRequired
Content-Typeapplication/jsonYes
x-api-keyAPI key for authenticationConditional*
x-prosusai-ifood-access-tokeniFood OAuth access tokenConditional**
x-prosusai-ifood-account-idiFood account IDConditional**
x-prosusai-user-first-nameUser's first nameOptional
x-prosusai-user-last-nameUser's last nameOptional
x-prosusai-user-emailUser's email addressOptional

* Required if your API expects authentication via x-api-key header
** Included automatically when iFood authentication is available

Request Body

typescript
{
  message: string;           // User's message content
  history: HistoryMessage[]; // Chat history
  mode: "super_tool";       // Always "super_tool"
  session_id?: string;      // Optional: Session ID for conversation continuity
}

History Message Format

typescript
interface HistoryMessage {
  role: "user" | "assistant";
  content: string;
}

Example Request (Non-Streaming)

bash
curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-prosusai-user-first-name: John" \
  -H "x-prosusai-user-last-name: Doe" \
  -H "x-prosusai-user-email: john.doe@example.com" \
  -d '{
    "message": "Hello, how are you?",
    "history": [],
    "mode": "super_tool",
    "session_id": "abc123-session-id"
  }'

Example Request (Streaming)

bash
curl -X POST http://localhost:3000/api/chat/stream \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "message": "Tell me a story",
    "history": [
      {
        "role": "user",
        "content": "Hi"
      },
      {
        "role": "assistant",
        "content": "Hello! How can I help?"
      }
    ],
    "mode": "super_tool"
  }'

Example Request (Polling - Initialize)

bash
curl -X POST http://localhost:3000/api/polling/chat \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-prosusai-user-email: user@example.com" \
  -d '{
    "message": "Perform a long-running task",
    "history": []
  }'

Response:

json
{
  "taskId": "task-123",
  "status": "pending",
  "statusUrl": "task/status/task-123"
}

Example Request (Polling - Status Check)

bash
curl -X GET http://localhost:3000/api/polling/task/status/task-123 \
  -H "x-api-key: YOUR_API_KEY"

Response:

json
{
  "taskId": "task-123",
  "status": "complete",
  "content": "Task completed successfully",
  "products": []
}

Response Format

Non-Streaming Response

Returns a single JSON object:

typescript
{
  content: string;              // Agent's response text
  products?: Product[];         // Optional: Product items
  ifoodSearchItems?: IfoodSearchItem[]; // Optional: iFood search results
  session_id?: string;          // Optional: Session ID for future requests
  type?: "search_items" | "text"; // Response type
}

Streaming Response (SSE)

Returns Server-Sent Events (SSE) format:

data: {"content": "Hello", "type": "text"}
data: {"content": " there", "type": "text"}
data: {"content": "!", "type": "text"}
data: {"type": "search_items", "message": "Here are results:", "data": {"search_items": [...]}}
data: {"session_id": "abc123-session-id"}

Each line is a JSON object prefixed with data: .

SSE Event Types

  1. Content Chunks: {"content": "text chunk", "type": "text"}
  2. Search Items: {"type": "search_items", "message": "...", "data": {"search_items": [...]}}
  3. Session ID: {"session_id": "session-id-string"}

Polling Response

Task Initialization:

typescript
{
  taskId: string;
  status: "pending" | "running";
  message?: string;
  statusUrl: string;
}

Task Status:

typescript
{
  taskId: string;
  status: "pending" | "running" | "complete" | "failed";
  createdAt?: string;
  content?: string;      // Available when status is "complete"
  products?: Product[];  // Optional product items
}

Integration Flow

Streaming/Non-Streaming Flow

Polling Flow

Same as Browser Agent polling flow. See Browser Agent for details.

Code Examples

Example 1: Basic Request Flow

typescript
// In useChat hook
const sendMessage = async (content: string) => {
  // ... create assistant message
  
  // Get agent URLs from user configuration
  const agentUrls = getAgentUrls(
    'custom',
    config.customAgentApiUrl,
    config.customAgentApiUrlStream,
    config.customAgentPollingUrl
  );
  
  if (agentType === 'custom' && agentUrls) {
    // Refresh iFood token if needed
    await useIfoodAuthStore.getState().refreshTokenIfNeeded();
    
    // Get iFood auth (optional)
    const ifoodAuth = {
      accessToken: useIfoodAuthStore.getState().accessToken,
      accountId: useIfoodAuthStore.getState().accountId,
    };
    
    // Handle session ID callback
    const handleSessionIdReceived = async (newSessionId: string) => {
      setSessionId(newSessionId);
      await conversationsDb.updateSessionId(convId, newSessionId);
    };
    
    // Choose communication mode
    if (usePolling && agentUrls.pollingUrl) {
      await handlePollingAgentResponse(
        content,
        agentUrls.pollingUrl,
        abortSignal,
        assistantMessageId,
        convId,
        updateMessage,
        handleStreamComplete,
        messages
      );
    } else if (useStreaming) {
      await handleCustomAgentStreamingResponse(
        content,
        agentUrls.streamUrl,
        abortSignal,
        assistantMessageId,
        convId,
        updateMessage,
        handleStreamComplete,
        messages,
        ifoodAuth,
        sessionId,
        handleSessionIdReceived
      );
    } else {
      await handleCustomAgentNonStreamingResponse(
        content,
        agentUrls.apiUrl,
        abortSignal,
        assistantMessageId,
        convId,
        updateMessage,
        messages,
        ifoodAuth,
        sessionId,
        handleSessionIdReceived
      );
    }
  }
};

Example 2: Streaming Response Processing

typescript
// Inside handleCustomAgentStreamingResponse
const response = await fetch(apiUrl, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.EXPO_PUBLIC_INTERNAL_API_KEY || "",
    ...(ifoodAuth?.accessToken && {
      "x-prosusai-ifood-access-token": ifoodAuth.accessToken,
    }),
    ...(ifoodAuth?.accountId && {
      "x-prosusai-ifood-account-id": ifoodAuth.accountId,
    }),
    ...(userFirstName && { "x-prosusai-user-first-name": userFirstName }),
    ...(userLastName && { "x-prosusai-user-last-name": userLastName }),
    ...(userEmail && { "x-prosusai-user-email": userEmail }),
  },
  body: JSON.stringify({
    message: content,
    history: history,
    mode: "super_tool",
    ...(sessionId && { session_id: sessionId }),
  }),
  signal,
});

// Read SSE response
const fullText = await response.text();
const lines = fullText.split("\n");

let accumulatedContent = "";
let finalProducts: any[] = [];
let ifoodSearchItems: IfoodSearchItem[] | undefined;

for (const line of lines) {
  if (!line.trim() || !line.startsWith("data: ")) continue;
  
  const jsonString = line.substring(6); // Remove "data: " prefix
  
  try {
    const data = JSON.parse(jsonString);
    
    // Handle content chunks
    if (data.content) {
      accumulatedContent += data.content;
      updateMessage(assistantMessageId, {
        content: accumulatedContent,
        isStreaming: true,
      }, true, convId);
      
      // Small delay for React re-render
      await sleep(50);
    }
    
    // Handle products
    if (data.products && data.products.length > 0) {
      finalProducts = data.products;
    }
    
    // Handle iFood search items
    if (data.type === "search_items") {
      accumulatedContent = data.message || "";
      ifoodSearchItems = data.data?.search_items || [];
      updateMessage(assistantMessageId, {
        content: accumulatedContent,
        isStreaming: true,
        ifoodSearchItems,
      }, true, convId);
    }
    
    // Handle session ID
    if (data.session_id && onSessionIdReceived) {
      onSessionIdReceived(data.session_id);
    }
  } catch {
    // Skip invalid JSON lines
    continue;
  }
}

Example 3: Non-Streaming Response Processing

typescript
// Inside handleCustomAgentNonStreamingResponse
const response = await fetch(apiUrl, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.EXPO_PUBLIC_INTERNAL_API_KEY || "",
    ...(ifoodAuth?.accessToken && {
      "x-prosusai-ifood-access-token": ifoodAuth.accessToken,
    }),
    ...(ifoodAuth?.accountId && {
      "x-prosusai-ifood-account-id": ifoodAuth.accountId,
    }),
    ...(userFirstName && { "x-prosusai-user-first-name": userFirstName }),
    ...(userLastName && { "x-prosusai-user-last-name": userLastName }),
    ...(userEmail && { "x-prosusai-user-email": userEmail }),
  },
  body: JSON.stringify({
    message: content,
    history: history,
    mode: "super_tool",
    ...(sessionId && { session_id: sessionId }),
  }),
  signal,
});

if (!response.ok) {
  throw new Error(`API request failed: ${response.statusText}`);
}

const data = await response.json();

// Handle session_id
if (data.session_id && onSessionIdReceived) {
  onSessionIdReceived(data.session_id);
}

// Handle response based on type
let finalContent: string;
let products: any[] = [];
let ifoodSearchItems: IfoodSearchItem[] | undefined;

if (data.type === "search_items") {
  finalContent = data.message || "";
  ifoodSearchItems = data.data?.search_items || [];
} else {
  finalContent = data.content || "";
  products = data.products || [];
}

// Update UI
updateMessage(assistantMessageId, {
  content: finalContent,
  isStreaming: false,
  products,
  ifoodSearchItems,
}, false);

Configuration

Agent Type Selection

The Custom Agent is selected in the Agent Settings screen:

File: app/agent-settings.tsx in the main application

Description: "Use your own custom API endpoints. Configure the URLs below to connect to your external agent service."

URL Configuration UI

Users can configure three URLs in the Agent Settings:

  1. API URL: For non-streaming requests

    • Placeholder: https://api.example.com/agent
    • Required for non-streaming mode
  2. Stream URL: For streaming requests

    • Placeholder: https://api.example.com/agent/stream
    • Optional (falls back to API URL if not provided)
  3. Polling URL: Base URL for polling requests

    • Placeholder: https://api.example.com/polling/
    • Optional (polling mode unavailable if not provided)

Environment Variables

VariableDescriptionSource
EXPO_PUBLIC_INTERNAL_API_KEYAPI key for authentication (if your API requires it)From .env file
EXPO_PUBLIC_IFOOD_CLIENT_IDiFood OAuth Client ID (if using iFood integration)From .env file
EXPO_PUBLIC_IFOOD_CLIENT_SECRETiFood OAuth Client Secret (if using iFood integration)From .env file

Important: Never commit API keys to version control. All keys should be stored in .env.local (which is git-ignored).

Local Development

Connecting to Localhost

For local development, you can configure URLs pointing to your local server:

Example URLs:

  • API URL: http://localhost:3000/api/chat
  • Stream URL: http://localhost:3000/api/chat/stream
  • Polling URL: http://localhost:3000/api/polling/

Note: For React Native apps running on physical devices or emulators:

  • Use your machine's IP address instead of localhost
  • Example: http://192.168.1.100:3000/api/chat

Testing Your API

You can test your custom agent API using curl:

bash
# Test non-streaming endpoint
curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Hello",
    "history": [],
    "mode": "super_tool"
  }'

# Test streaming endpoint
curl -X POST http://localhost:3000/api/chat/stream \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Tell me a story",
    "history": [],
    "mode": "super_tool"
  }'

Session Management

Session ID

The Custom Agent supports session management for conversation continuity:

Storage:

  • Stored in conversation record in local database
  • Field: session_id

Lifecycle:

  1. First request → API may return new session_id
  2. Subsequent requests → Include session_id in request body
  3. Session persists until conversation is deleted

Implementation:

typescript
// Callback to save session_id when received
const handleSessionIdReceived = async (newSessionId: string) => {
  setSessionId(newSessionId);
  await conversationsDb.updateSessionId(convId, newSessionId);
};

// Include in request if available
body: JSON.stringify({
  message: content,
  history: history,
  mode: "super_tool",
  ...(sessionId && { session_id: sessionId }),
})

Error Handling

HTTP Errors

  • 4xx Errors: Client errors (bad request, unauthorized, etc.)
  • 5xx Errors: Server errors (internal server error, service unavailable, etc.)

All errors are caught and displayed to the user with appropriate error messages.

Network Errors

  • Connection timeouts
  • Network failures
  • Abort signal support for cancellation

Implementation

typescript
try {
  const response = await fetch(apiUrl, { ... });
  
  if (!response.ok) {
    throw new Error(`API request failed: ${response.statusText}`);
  }
  
  // Process response
} catch (error) {
  console.error("Error fetching custom agent response:", error);
  throw error;
}

API Compatibility

Required Features

Your custom agent API should support:

  1. Request Format: Accept the standard request body format
  2. Response Format: Return responses in the expected format (JSON or SSE)
  3. Headers: Handle optional headers (user info, iFood auth, API key)
  4. Session Management: Support session_id in requests and responses (optional)
  1. Error Handling: Return appropriate HTTP status codes
  2. CORS: Enable CORS for web platform (if applicable)
  3. Authentication: Support API key authentication via x-api-key header
  4. Rate Limiting: Implement rate limiting to prevent abuse

Use Cases

Common Custom Agent Scenarios

  1. Self-Hosted Agent: Host your own agent service with custom integrations
  2. Third-Party Integration: Connect to external agent services
  3. Local Development: Test agent implementations locally
  4. Specialized Agents: Create agents for specific domains or use cases
  5. Hybrid Approach: Combine multiple agent services

Limitations

  1. User Configuration Required: URLs must be manually configured
  2. API Compatibility: API must match expected request/response formats
  3. Network Dependency: Requires internet connection (or local network for localhost)
  4. Error Handling: Depends on API's error handling implementation
  5. Security: User is responsible for API security and authentication

Best Practices

API Design

  1. Consistent Response Format: Use consistent JSON structure
  2. Error Messages: Provide clear, actionable error messages
  3. Status Codes: Use appropriate HTTP status codes
  4. Documentation: Document your API endpoints and formats
  5. Versioning: Consider API versioning for future changes

Security

  1. Authentication: Implement proper authentication (API keys, OAuth, etc.)
  2. HTTPS: Use HTTPS for production endpoints
  3. Input Validation: Validate and sanitize all inputs
  4. Rate Limiting: Implement rate limiting to prevent abuse
  5. CORS: Configure CORS appropriately for web platform

Development

  1. Local Testing: Test with localhost before deploying
  2. Error Handling: Implement comprehensive error handling
  3. Logging: Add logging for debugging and monitoring
  4. Testing: Write tests for your API endpoints
  5. Documentation: Document your API for other developers

Future Enhancements

Potential improvements:

  • API endpoint validation
  • Response format validation
  • Automatic API discovery
  • Support for additional authentication methods
  • Enhanced error messages and debugging
  • API health checks
  • Native Agent - Native Agent documentation
  • Multi-Agent - Multi-Agent documentation
  • Browser Agent - Browser Agent documentation
  • Agent Settings UI: app/agent-settings.tsx in the main application
  • Custom Agent Handler: hooks/use-chat-custom-agent.ts in the main application
  • Polling Agent Handler: hooks/use-chat-polling-agent.ts in the main application

Prosus AI App Documentation