Appearance
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:
- API URL: Endpoint for non-streaming requests
- Stream URL: Endpoint for streaming (SSE) requests
- Polling URL: Base URL for polling mode requests
File: app/agent-settings.tsx
Storage: URLs are stored in the local database:
custom_agent_api_urlcustom_agent_api_url_streamcustom_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
streamUrlis not provided, it falls back toapiUrl - If
pollingUrlis 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
| Header | Description | Required |
|---|---|---|
Content-Type | application/json | Yes |
x-api-key | API key for authentication | Conditional* |
x-prosusai-ifood-access-token | iFood OAuth access token | Conditional** |
x-prosusai-ifood-account-id | iFood account ID | Conditional** |
x-prosusai-user-first-name | User's first name | Optional |
x-prosusai-user-last-name | User's last name | Optional |
x-prosusai-user-email | User's email address | Optional |
* 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
- Content Chunks:
{"content": "text chunk", "type": "text"} - Search Items:
{"type": "search_items", "message": "...", "data": {"search_items": [...]}} - 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:
API URL: For non-streaming requests
- Placeholder:
https://api.example.com/agent - Required for non-streaming mode
- Placeholder:
Stream URL: For streaming requests
- Placeholder:
https://api.example.com/agent/stream - Optional (falls back to API URL if not provided)
- Placeholder:
Polling URL: Base URL for polling requests
- Placeholder:
https://api.example.com/polling/ - Optional (polling mode unavailable if not provided)
- Placeholder:
Environment Variables
| Variable | Description | Source |
|---|---|---|
EXPO_PUBLIC_INTERNAL_API_KEY | API key for authentication (if your API requires it) | From .env file |
EXPO_PUBLIC_IFOOD_CLIENT_ID | iFood OAuth Client ID (if using iFood integration) | From .env file |
EXPO_PUBLIC_IFOOD_CLIENT_SECRET | iFood 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:
- First request → API may return new
session_id - Subsequent requests → Include
session_idin request body - 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:
- Request Format: Accept the standard request body format
- Response Format: Return responses in the expected format (JSON or SSE)
- Headers: Handle optional headers (user info, iFood auth, API key)
- Session Management: Support
session_idin requests and responses (optional)
Recommended Features
- Error Handling: Return appropriate HTTP status codes
- CORS: Enable CORS for web platform (if applicable)
- Authentication: Support API key authentication via
x-api-keyheader - Rate Limiting: Implement rate limiting to prevent abuse
Use Cases
Common Custom Agent Scenarios
- Self-Hosted Agent: Host your own agent service with custom integrations
- Third-Party Integration: Connect to external agent services
- Local Development: Test agent implementations locally
- Specialized Agents: Create agents for specific domains or use cases
- Hybrid Approach: Combine multiple agent services
Limitations
- User Configuration Required: URLs must be manually configured
- API Compatibility: API must match expected request/response formats
- Network Dependency: Requires internet connection (or local network for localhost)
- Error Handling: Depends on API's error handling implementation
- Security: User is responsible for API security and authentication
Best Practices
API Design
- Consistent Response Format: Use consistent JSON structure
- Error Messages: Provide clear, actionable error messages
- Status Codes: Use appropriate HTTP status codes
- Documentation: Document your API endpoints and formats
- Versioning: Consider API versioning for future changes
Security
- Authentication: Implement proper authentication (API keys, OAuth, etc.)
- HTTPS: Use HTTPS for production endpoints
- Input Validation: Validate and sanitize all inputs
- Rate Limiting: Implement rate limiting to prevent abuse
- CORS: Configure CORS appropriately for web platform
Development
- Local Testing: Test with localhost before deploying
- Error Handling: Implement comprehensive error handling
- Logging: Add logging for debugging and monitoring
- Testing: Write tests for your API endpoints
- 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
Related Documentation
- Native Agent - Native Agent documentation
- Multi-Agent - Multi-Agent documentation
- Browser Agent - Browser Agent documentation
- Agent Settings UI:
app/agent-settings.tsxin the main application - Custom Agent Handler:
hooks/use-chat-custom-agent.tsin the main application - Polling Agent Handler:
hooks/use-chat-polling-agent.tsin the main application