OAuth Integration Deep Dive
Master multi-provider OAuth integration in Fiberwise. Learn real-world patterns from a production email agent app including OAuth configuration, credential services, and secure token management across Gmail, Outlook, and Yahoo.
What You'll Learn
This tutorial explores real OAuth implementation patterns from a production email agent application:
- OAuth Configuration: Multi-provider setup in app_manifest.yaml with proper scopes and parameters
- Credential Service: Frontend OAuth flow handling with CredentialService class
- Connection Management: Building UI components for OAuth connections
- Agent Integration: Using OAuth credentials in Python agents with the execute() pattern
- Multi-Provider Support: Unified abstraction for Gmail, Outlook, and Yahoo
- Error Handling: Production-ready error recovery and token refresh
Real Production Code
All code examples in this tutorial are from the working email-agent-app in the fiber-apps repository. You can explore the full implementation there.
📋 Prerequisites: Your Setup Checklist
Before you begin, you need a fully configured Fiberwise environment. This is the foundation for building any app on the platform.
🔧 Required Setup
✅ All Set?
Once all boxes are checked, you are ready to proceed. If not, please complete the linked guides first.
Step 1: OAuth Architecture Overview
Fiberwise provides a complete OAuth infrastructure. Here's how the pieces fit together:
OAuth Flow in Fiberwise
Key Components
app_manifest.yaml
Defines OAuth authenticators, scopes, and provider configuration
credential-service.js
Frontend service for initiating OAuth flows and managing connections
email-connections.js
Web component UI for connecting/disconnecting OAuth providers
email_agent.py
Python agent using oauth_service for authenticated API requests
Step 2: OAuth Configuration in app_manifest.yaml
The app manifest is where you define OAuth authenticators. Here's the real configuration from the email-agent-app:
# app_manifest.yaml - OAuth configuration section
oauth:
authenticators:
- name: Gmail
type: oauth2
file: .fiber/local/oauth/fiberwise_developer.json
scopes:
- openid
- profile
- email
- https://www.googleapis.com/auth/gmail.readonly
- https://www.googleapis.com/auth/gmail.send
additional_params:
access_type: offline # Request refresh token
prompt: consent # Always show consent screen
Configuration Breakdown
| Field | Purpose |
|---|---|
name |
Display name for the OAuth provider |
type |
Authentication type (oauth2 for OAuth 2.0) |
file |
Path to OAuth credentials JSON (client_id, client_secret) |
scopes |
OAuth scopes to request from the provider |
additional_params |
Extra OAuth parameters (access_type, prompt, etc.) |
Agent Permissions for OAuth
Your agent must declare OAuth permissions in the manifest:
# Agent section of app_manifest.yaml
agents:
- name: email-agent
implementation_path: agents/email_agent.py
language: python
agent_type_id: custom
description: Agent for working with emails across different providers using OAuth
permissions:
- credentials.oauth # Required for OAuth operations
- data.read
- data.write
- llm.completion
allowed_context_variables:
- user_id
- app_id
- provider_id
- template_name
OAuth Credentials File
The .fiber/local/oauth/fiberwise_developer.json file contains your OAuth client credentials. Never commit this file to version control. It should contain:
{
"client_id": "YOUR_OAUTH_CLIENT_ID",
"client_secret": "YOUR_OAUTH_CLIENT_SECRET",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token"
}
Step 3: The CredentialService Class
The CredentialService handles all OAuth operations on the frontend. Here's the real implementation:
/**
* FiberWise Credential Service
* Provides functionality for OAuth authentication and credential management
*/
class CredentialService {
/**
* Create a new Credential Service client
* @param {Object} options - Configuration options
* @param {FiberwiseAppConfig} options.config - Simple configuration instance
* @param {string} options.appId - Optional app ID to override config
* @param {string} options.baseUrl - Optional API base URL
*/
constructor(options = {}) {
this.config = options.config;
this.appId = options.appId;
this.baseUrl = options.baseUrl || 'https://api.fiberwise.ai/api/v1';
this.credentialsBasePath = '/credentials';
}
/**
* Get the app ID for this instance
* @returns {string} - App ID from options or config
*/
getAppId() {
return this.appId || (this.config && this.config.get('appId'));
}
/**
* Get headers for API requests
* @returns {Object} - Headers object
*/
getHeaders() {
const headers = {
'Content-Type': 'application/json'
};
// Add API key if available
const apiKey = this.config && this.config.get('apiKey');
if (apiKey) {
headers['X-API-Key'] = apiKey;
}
return headers;
}
/**
* Get the full URL to initiate OAuth authentication for a provider
* @param {string} providerId - The provider ID to authenticate with
* @param {Object} options - Additional options
* @param {string} options.returnTo - Path to return to after authentication
* @returns {string} - Full URL to initiate authentication
*/
getAuthUrl(providerId, options = {}) {
if (!providerId) {
throw new Error('Provider ID is required');
}
const appId = this.getAppId();
const queryParams = new URLSearchParams();
// Add return path if available
if (options.returnTo) {
queryParams.set('return_to', options.returnTo);
}
// Add app ID if available
if (appId) {
queryParams.set('app_id', appId);
}
// Build the full URL with query parameters
const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
return `${this.baseUrl}${this.credentialsBasePath}/auth/initiate/${providerId}${queryString}`;
}
/**
* Initiate OAuth authentication with a provider (redirects browser)
* @param {string} providerId - The provider ID to authenticate with
* @param {Object} options - Additional options
* @param {string} options.returnTo - Path to return to after authentication
* @returns {Promise} - Redirects the browser
*/
initiateAuth(providerId, options = {}) {
try {
const authUrl = this.getAuthUrl(providerId, options);
// Use AppBridge router for navigation instead of direct window.location
if (window.FIBER?._appBridge?._router) {
window.FIBER._appBridge._router.navigateTo(authUrl);
} else {
console.error('AppBridge router not available for OAuth navigation');
throw new Error('Router not available for OAuth flow');
}
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
}
/**
* List all OAuth connections for the current user
* @param {Object} options - List options
* @param {string} options.appId - Optional app ID to filter connections
* @returns {Promise} - List of connections
*/
async listConnections(options = {}) {
try {
// Get app ID from options or from instance
const appId = options.appId || this.getAppId();
// Build query parameters
const queryParams = new URLSearchParams();
if (appId) {
queryParams.set('app_id', appId);
}
const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
// Use the service providers endpoint
const response = await fetch(
`${this.baseUrl}/apps/${appId}/service-providers${queryString}`,
{
method: 'GET',
headers: this.getHeaders(),
credentials: 'include'
}
);
if (!response.ok) {
throw new Error(`Failed to list connections: ${response.statusText}`);
}
const providers = await response.json();
// Filter to only return actually connected providers
return (providers || []).filter(provider => Boolean(provider.is_connected));
} catch (error) {
console.error('Error listing connections:', error);
return [];
}
}
/**
* Revoke an OAuth connection
* @param {string} connectionId - The connection ID to revoke
* @param {Object} options - Additional options
* @param {string} options.appId - Optional app ID context
* @returns {Promise
Key CredentialService Patterns
- AppBridge Integration: Uses
window.FIBER._appBridge._routerfor OAuth navigation - Secure Credentials: Uses
credentials: 'include'for cookie-based auth - Return URL Handling:
returnToparameter for post-OAuth redirect - Connection Filtering: Filters by
is_connectedstatus
Step 4: Building the Connection UI Component
The email-connections component provides a complete UI for managing OAuth connections. Here are the key patterns:
Component Structure
// email-connections.js - Web Component for OAuth connections
import { FIBER } from '../../index.js';
import { emailService } from '../services/email-service.js';
import { dataService } from '../services/data-service.js';
class EmailConnections extends HTMLElement {
constructor() {
super();
this.providers = [];
this.connections = [];
this.loading = true;
this.error = null;
this.successMessage = null;
// Create shadow DOM for encapsulation
this.attachShadow({ mode: 'open' });
// Initialize the DOM
this.render();
// Bind event handlers
this.handleConnectionStatus = this.handleConnectionStatus.bind(this);
this.clearMessages = this.clearMessages.bind(this);
}
// Lifecycle: Component added to DOM
connectedCallback() {
console.log('EMAIL-CONNECTIONS: Component connected');
this.loadData();
// Listen for connection changes from credential service
window.addEventListener('connection-status', this.handleConnectionStatus);
}
// Lifecycle: Component removed from DOM
disconnectedCallback() {
window.removeEventListener('connection-status', this.handleConnectionStatus);
}
// Handle connection status updates from OAuth callback
handleConnectionStatus(event) {
const status = event.detail;
if (status.status === 'connected') {
this.successMessage = `Successfully connected to ${status.authenticator_name || 'provider'}`;
this.loadData(); // Refresh the data
this.render(); // Update the UI
}
}
}
Loading Provider Data
// Load providers and connections data
async loadData() {
console.log('EMAIL-CONNECTIONS: loadData() called');
this.loading = true;
this.error = null;
this.render();
try {
// Get available providers for this app
const providers = await dataService.getServiceProviders();
// Transform provider data with proper ID mapping
this.providers = (providers || []).map(provider => ({
id: provider.provider_id || provider.authenticator_id,
provider_id: provider.provider_id || provider.authenticator_id,
authenticator_id: provider.authenticator_id || provider.provider_id,
provider_key: provider.provider_key,
name: provider.name || provider.display_name,
icon: `/static/icons/${(provider.authenticator_type || 'email').toLowerCase()}.svg`,
description: `Connect your ${provider.name || provider.display_name} account.`,
scopes: provider.scopes || [],
type: provider.authenticator_type,
is_connected: Boolean(provider.is_connected),
connection_status: provider.connection_status ||
(provider.is_connected ? 'connected' : 'disconnected')
}));
// Filter connected providers from the same list
this.connections = this.providers.filter(provider => provider.is_connected);
} catch (error) {
console.error('Error loading provider data:', error);
this.error = 'Failed to load provider information. Please try again.';
} finally {
this.loading = false;
this.render();
}
}
Connecting to a Provider
// Connect to a provider - initiates OAuth flow
async connectAuthenticator(authenticatorId) {
console.log('EMAIL-CONNECTIONS: connectAuthenticator called with ID:', authenticatorId);
try {
// Find the provider to get proper ID
const provider = this.providers.find(p => p.id === authenticatorId);
if (!provider) {
throw new Error('Provider not found');
}
// Use the actual authenticator_id from the provider
const actualId = provider.authenticator_id || provider.provider_id;
if (!actualId) {
throw new Error('No valid authenticator ID found for provider');
}
// Update the UI immediately to show connecting state
const providerIndex = this.providers.findIndex(p => p.id === authenticatorId);
if (providerIndex >= 0 && provider.connection_status !== 'pending') {
this.providers[providerIndex].connection_status = 'pending';
this.providers[providerIndex].is_connected = false;
this.render(); // Re-render to show pending state immediately
}
// Determine the return URL for after authentication
const returnPath = `/email-agent-app/settings/email-connections`;
// Use the connectAuthenticator method from the FIBER.credentials service
if (!FIBER || !FIBER.credentials) {
throw new Error('FIBER SDK not available');
}
await FIBER.credentials.connectAuthenticator(actualId, {
returnTo: returnPath
});
// The connectAuthenticator method handles the redirect
} catch (error) {
console.error('Error initiating connection:', error);
// Reset the provider state on error
const providerIndex = this.providers.findIndex(p => p.id === authenticatorId);
if (providerIndex >= 0) {
this.providers[providerIndex].connection_status = 'disconnected';
this.providers[providerIndex].is_connected = false;
}
this.error = `Failed to connect to provider. Please try again: ${error.message}`;
this.render();
}
}
Disconnecting a Provider
// Disconnect a provider - revokes OAuth connection
async disconnectConnection(connectionId) {
console.log('EMAIL-CONNECTIONS: disconnectConnection called with:', connectionId);
if (!connectionId || connectionId === 'undefined') {
console.error('EMAIL-CONNECTIONS: Invalid connection ID:', connectionId);
this.error = 'Invalid connection ID. Please refresh the page and try again.';
this.render();
return;
}
if (!confirm('Are you sure you want to disconnect this provider? ' +
'This will remove access to your email account.')) {
return;
}
try {
await dataService.revokeConnection(connectionId);
this.successMessage = 'Provider disconnected successfully';
// Refresh connections
await this.loadData();
} catch (error) {
console.error('Error disconnecting provider:', error);
this.error = 'Failed to disconnect provider. Please try again.';
this.render();
}
}
Testing a Connection
// Test a connection - verify OAuth tokens are still valid
async testConnection(connectionId) {
try {
this.loading = true;
this.render();
const result = await emailService.checkConnectionStatus(connectionId);
if (result.status === 'success' && result.result?.connected) {
this.successMessage = 'Connection verified successfully';
} else {
this.error = result.result?.message || 'Connection verification failed';
}
} catch (error) {
console.error('Error testing connection:', error);
this.error = 'Failed to test connection. Please try again.';
} finally {
this.loading = false;
this.render();
}
}
// Register the custom element
customElements.define('email-connections', EmailConnections);
UI State Management Patterns
- Optimistic UI: Show "pending" state immediately on connect
- Event-driven updates: Listen for 'connection-status' events
- Error recovery: Reset state on connection failure
- Confirmation dialogs: Confirm before destructive operations
Step 5: Using OAuth in Python Agents
The EmailAgent demonstrates the proper pattern for using OAuth credentials in FiberWise agents. Here's the real implementation:
Agent Class Structure
"""
Generic email agent that uses OAuth credentials to access email services
across different providers (Google, Microsoft, Yahoo)
This agent demonstrates the FiberWise agent development pattern with dependency injection.
It handles email operations across multiple providers while following the platform spec.
Architecture:
- Inherits from FiberAgent for proper SDK integration
- Uses execute method for ia_modules compatibility
- Leverages dependency injection for FiberApp and services via kwargs
- Implements comprehensive email operations across providers
"""
import asyncio
import base64
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional, Union, Literal
from enum import Enum
from pydantic import BaseModel, Field, validator, model_validator
from fiberwise_sdk import FiberApp, FiberAgent
from fiberwise_sdk.llm_provider_service import LLMProviderService
from fiberwise_sdk.credential_agent_service import BaseCredentialService
# Set up logger
logger = logging.getLogger(__name__)
class OperationType(str, Enum):
"""Supported email operations"""
SEARCH_EMAILS = "search_emails"
GET_EMAIL = "get_email"
CREATE_DRAFT = "create_draft"
SEND_EMAIL = "send_email"
ANALYZE_EMAIL = "analyze_email"
UPDATE_LABELS = "update_labels"
LIST_LABELS = "list_labels"
CHECK_CONNECTION = "check_connection"
GET_ACCOUNT_STATS = "get_account_stats"
GET_INBOX = "get_inbox"
The execute() Pattern with Dependency Injection
class EmailAgent(FiberAgent):
"""
Agent for working with emails across different providers using OAuth.
Dependency Injection:
- fiber: FiberApp SDK instance for platform API access
- llm_service: LLMProviderService for AI-powered email analysis
- oauth_service: BaseCredentialService for secure OAuth operations
Execution Pattern:
The activation processor detects this class inherits from FiberAgent and calls
the execute method with proper dependency injection via kwargs.
"""
# Provider-specific endpoint mappings
PROVIDER_ENDPOINTS = {
"google": {
"list_messages": "https://gmail.googleapis.com/gmail/v1/users/me/messages",
"get_message": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{message_id}",
"list_labels": "https://gmail.googleapis.com/gmail/v1/users/me/labels",
"modify_message": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{message_id}/modify",
"send_message": "https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
"drafts": "https://gmail.googleapis.com/gmail/v1/users/me/drafts"
},
"microsoft": {
"list_messages": "https://graph.microsoft.com/v1.0/me/messages",
"get_message": "https://graph.microsoft.com/v1.0/me/messages/{message_id}",
"list_labels": "https://graph.microsoft.com/v1.0/me/mailFolders",
"modify_message": "https://graph.microsoft.com/v1.0/me/messages/{message_id}",
"send_message": "https://graph.microsoft.com/v1.0/me/sendMail",
"drafts": "https://graph.microsoft.com/v1.0/me/mailFolders/drafts/messages"
},
"yahoo": {
"list_messages": "https://mail.yahooapis.com/v1/users/me/messages",
"get_message": "https://mail.yahooapis.com/v1/users/me/messages/{message_id}",
"list_labels": "https://mail.yahooapis.com/v1/users/me/folders",
"modify_message": "https://mail.yahooapis.com/v1/users/me/messages/{message_id}",
"send_message": "https://mail.yahooapis.com/v1/users/me/messages",
"drafts": "https://mail.yahooapis.com/v1/users/me/drafts"
}
}
async def execute(self, input_data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
"""
Process input data to perform email operations using credential service.
Args:
input_data: The input data for the agent
**kwargs: Injected services (fiber, llm_service, oauth_service, etc.)
Returns:
Processing results
Note:
Services are injected via kwargs or from self._injected_services
"""
# Access injected services from kwargs or self
fiber = kwargs.get('fiber') or (self.fiber if hasattr(self, 'fiber') else None)
llm_service = kwargs.get('llm_service') or (self.llm_service if hasattr(self, 'llm_service') else None)
oauth_service = kwargs.get('oauth_service') or (self.oauth_service if hasattr(self, 'oauth_service') else None)
logger.info(f"Processing email operation: {input_data}")
# Extract operation from input data
operation = input_data.get("operation", "search_emails")
# Get connection_id (supports both naming conventions)
connection_id = input_data.get("connection_id") or input_data.get("authenticator_id")
if not connection_id:
return {
"status": "error",
"message": "connection_id or authenticator_id is required"
}
# Generate a unique task ID for this operation
task_id = f"email_agent_{operation}_{datetime.now().strftime('%Y%m%d%H%M%S')}_{id(input_data)}"
# Send initial update - operation started
await self.send_agent_update(
fiber=fiber,
task_id=task_id,
status="started",
progress=0.1,
message=f"Starting email operation: {operation}",
provider_id=connection_id,
operation=operation
)
try:
# Get provider info to determine the provider type
provider_info = await oauth_service.get_provider_info(connection_id)
# Check if the OAuth service returned an error
if not provider_info.get("success", True) or "error" in provider_info:
error_msg = provider_info.get("error", "Unknown error from OAuth service")
logger.error(f"OAuth service error for authenticator {connection_id}: {error_msg}")
return {
"status": "error",
"message": f"OAuth service error: {error_msg}"
}
# Detect provider type from authenticator data
inner_provider_info = provider_info.get('provider_info', provider_info)
provider_type = self._detect_provider_type_from_authenticator(inner_provider_info)
if provider_type not in self.PROVIDER_ENDPOINTS:
return {
"status": "error",
"message": f"Unsupported provider type: {provider_type}"
}
# Route to appropriate operation handler
if operation == "search_emails":
result = await self.search_emails(
cred_service=oauth_service,
connection_id=connection_id,
provider_type=provider_type,
query=input_data.get("query", ""),
max_results=input_data.get("max_results", 10),
label=input_data.get("label"),
days_back=input_data.get("days_back", 30),
fiber=fiber
)
elif operation == "analyze_email":
result = await self.analyze_email(
fiber=fiber,
cred_service=oauth_service,
llm_service=llm_service,
connection_id=connection_id,
provider_type=provider_type,
message_id=input_data.get("message_id"),
user_id=input_data.get("user_id"),
app_id=input_data.get("app_id")
)
# ... other operations
# Send completion update
await self.send_agent_update(
fiber=fiber,
task_id=task_id,
status="completed",
progress=1.0,
message=f"Operation {operation} completed successfully",
provider_id=connection_id,
operation=operation
)
return {
"status": "success",
"connection_id": connection_id,
"provider_type": provider_type,
"operation": operation,
"result": result,
"timestamp": datetime.now().isoformat(),
"task_id": task_id
}
except Exception as e:
# Send error update
await self.send_agent_update(
fiber=fiber,
task_id=task_id,
status="failed",
progress=0,
message=f"Operation failed: {str(e)}",
provider_id=connection_id,
operation=operation
)
return {
"status": "error",
"message": str(e),
"connection_id": connection_id,
"operation": operation
}
Provider Type Detection
def _detect_provider_type_from_authenticator(self, provider_info: Dict[str, Any]) -> str:
"""
Detect the provider type (google, microsoft, yahoo) from OAuth authenticator data.
Args:
provider_info: OAuth authenticator information
Returns:
Provider type string ("google", "microsoft", "yahoo") or empty string if unknown
"""
# Get various fields that might contain provider information
auth_url = (provider_info.get("auth_url") or
provider_info.get("authorization_url") or "").lower()
token_url = (provider_info.get("token_url") or
provider_info.get("access_token_url") or "").lower()
authenticator_name = (provider_info.get("authenticator_name") or
provider_info.get("name") or "").lower()
# Combine all fields for comprehensive checking
all_fields = f"{auth_url} {token_url} {authenticator_name}".lower()
# Check for Google indicators
google_indicators = [
"googleapis.com", "google.com", "accounts.google",
"oauth2.googleapis", "gmail", "google"
]
if any(indicator in all_fields for indicator in google_indicators):
logger.info("Detected Google provider")
return "google"
# Check for Microsoft indicators
microsoft_indicators = [
"microsoftonline.com", "login.microsoftonline", "graph.microsoft",
"outlook.office365", "microsoft", "outlook", "office365", "azure"
]
if any(indicator in all_fields for indicator in microsoft_indicators):
logger.info("Detected Microsoft provider")
return "microsoft"
# Check for Yahoo indicators
yahoo_indicators = [
"yahoo.com", "login.yahoo", "api.login.yahoo", "yahoo"
]
if any(indicator in all_fields for indicator in yahoo_indicators):
logger.info("Detected Yahoo provider")
return "yahoo"
logger.warning(f"Unknown provider type for authenticator")
return ""
Making Authenticated Requests
async def search_emails(
self,
cred_service: BaseCredentialService,
connection_id: str,
provider_type: str,
query: str = "",
max_results: int = 10,
label: Optional[str] = None,
days_back: int = 30,
fiber = None
) -> Dict[str, Any]:
"""
Search for emails using the provider's API
Args:
cred_service: Credential service for making authenticated requests
connection_id: Connection ID (OAuth authenticator) to use
provider_type: Type of provider (google, microsoft, yahoo)
query: Search query
max_results: Maximum number of results to return
label: Filter by label/folder
days_back: How many days back to search
Returns:
Search results
"""
# Get the appropriate endpoint for this provider
endpoint = self.PROVIDER_ENDPOINTS[provider_type]["list_messages"]
# Build provider-specific query parameters
params = {}
date_start = datetime.now() - timedelta(days=days_back)
if provider_type == "google":
# Gmail API
search_query = []
if query:
search_query.append(query)
search_query.append(f"after:{date_start.strftime('%Y/%m/%d')}")
if label:
params["labelIds"] = [label]
params["q"] = " ".join(search_query)
params["maxResults"] = max_results
elif provider_type == "microsoft":
# Microsoft Graph API
search_query = []
if query:
search_query.append(f"contains(subject,'{query}')")
search_query.append(f"receivedDateTime ge {date_start.isoformat()}Z")
if search_query:
params["$filter"] = " and ".join(search_query)
params["$top"] = max_results
params["$orderby"] = "receivedDateTime desc"
if label:
endpoint = f"https://graph.microsoft.com/v1.0/me/mailFolders/{label}/messages"
# Make the authenticated request using the credential service
result = await cred_service.make_authenticated_request(
credential_id=connection_id,
url=endpoint,
method="GET",
params=params
)
# Process and standardize the response format
if result.get("success", False):
messages = []
if provider_type == "google":
raw_messages = result.get("data", {}).get("messages", [])
messages = [
{
"id": msg.get("id"),
"thread_id": msg.get("threadId"),
"snippet": msg.get("snippet", "")
}
for msg in raw_messages
]
elif provider_type == "microsoft":
raw_messages = result.get("data", {}).get("value", [])
messages = [
{
"id": msg.get("id"),
"thread_id": msg.get("conversationId"),
"subject": msg.get("subject", ""),
"sender": msg.get("from", {}).get("emailAddress", {}).get("address", ""),
"date": msg.get("receivedDateTime"),
"is_read": msg.get("isRead", False),
"preview": msg.get("bodyPreview", "")
}
for msg in raw_messages
]
return {
"messages": messages,
"total_count": len(messages),
"query": query,
"max_results": max_results
}
else:
raise Exception(f"Search failed: {result.get('error')}")
Key Agent Patterns
- Dependency Injection: Services passed via
**kwargs - Provider Abstraction: Single interface for Gmail, Outlook, Yahoo
- Authenticated Requests: Use
cred_service.make_authenticated_request() - Progress Updates: Real-time status via
send_agent_update() - Error Handling: Comprehensive try/catch with status updates
Step 6: Production Error Handling
Robust error handling is critical for OAuth integrations. Here are the patterns from the real implementation:
Real-time Progress Updates
async def send_agent_update(
self,
fiber: FiberApp,
task_id: str,
status: str,
progress: float,
message: str,
provider_id: Optional[str] = None,
message_id: Optional[str] = None,
operation: Optional[str] = None
) -> bool:
"""
Send an agent update through the realtime system
Args:
fiber: FiberWise SDK instance
task_id: Unique ID for this task/operation
status: Current status (started, processing, completed, failed)
progress: Progress value between 0.0 and 1.0
message: Human-readable status message
provider_id: Optional provider ID for context
message_id: Optional email message ID for context
operation: Optional operation type
Returns:
bool: True if successful, False otherwise
"""
try:
# Ensure we have a realtime connection
if not await self.ensure_realtime_connected(fiber):
return False
# Create the update payload
update_data = {
"type": "task_progress",
"task_id": task_id,
"status": status,
"progress": progress,
"message": message,
"agent_id": "email_agent",
"timestamp": datetime.now().isoformat()
}
# Add optional context info if provided
if provider_id:
update_data["service_provider_id"] = provider_id
if message_id:
update_data["message_id"] = message_id
if operation:
update_data["operation"] = operation
# Send the update
await fiber.realtime.send("agent_update", update_data)
# For key progress points, also send user notifications
if status == "completed":
await self.send_notification(
fiber=fiber,
type="success",
title="Operation Completed",
message=message,
level="success"
)
elif status == "failed":
await self.send_notification(
fiber=fiber,
type="error",
title="Operation Failed",
message=message,
level="error"
)
return True
except Exception as e:
logger.error(f"[EmailAgent] Error sending agent update: {str(e)}")
return False
Graceful Degradation
async def ensure_realtime_connected(self, fiber: FiberApp) -> bool:
"""
Ensure the realtime connection is established.
Agents can still function without realtime - it's not a fatal error.
"""
if not hasattr(self, 'realtime_connected'):
self.realtime_connected = False
if not self.realtime_connected:
try:
await fiber.realtime.connect()
self.realtime_connected = True
logger.info("[EmailAgent] Realtime connection established")
return True
except Exception as e:
# Don't treat this as fatal - agents can work without realtime
logger.warning(f"[EmailAgent] Realtime connection not available: {str(e)}")
self.realtime_connected = False
return False
return True
async def send_notification(
self,
fiber: FiberApp,
type: str,
title: str,
message: str,
level: str = "info",
data: Dict[str, Any] = None
) -> bool:
"""Send a user-friendly notification with fallback to logging"""
try:
# Try realtime, but don't fail if unavailable
realtime_available = await self.ensure_realtime_connected(fiber)
if not realtime_available:
# Log the notification instead of sending via realtime
logger.info(f"[EmailAgent] Notification (realtime unavailable): {title} - {message}")
return True
# Send via realtime
await fiber.realtime.send("notification", {
"type": "user_notification",
"notification_type": type,
"title": title,
"message": message,
"level": level,
"agent_id": "email_agent",
"timestamp": datetime.now().isoformat(),
**({"data": data} if data else {})
})
return True
except Exception as e:
# Log as fallback
logger.info(f"[EmailAgent] Notification (fallback): {title} - {message}")
return False
Error Handling Patterns Summary
Progress Tracking
Send updates at key progress points: started, processing, completed, failed
Graceful Degradation
Realtime unavailable? Log instead. Don't let optional features block core functionality.
Comprehensive Context
Include provider_id, operation, task_id in all error responses for debugging
User Notifications
Send human-readable notifications on completion and failure
Step 7: Multi-Provider OAuth Support
The email agent supports Gmail, Outlook, and Yahoo through a unified abstraction. Here's how to configure multiple providers:
Manifest Configuration for Multiple Providers
# app_manifest.yaml - Multiple OAuth authenticators
oauth:
authenticators:
# Gmail (Google)
- name: Gmail
type: oauth2
file: .fiber/local/oauth/google_credentials.json
scopes:
- openid
- profile
- email
- https://www.googleapis.com/auth/gmail.readonly
- https://www.googleapis.com/auth/gmail.send
additional_params:
access_type: offline
prompt: consent
# Microsoft Outlook
- name: Outlook
type: oauth2
file: .fiber/local/oauth/microsoft_credentials.json
scopes:
- openid
- profile
- email
- Mail.Read
- Mail.Send
- User.Read
additional_params:
response_mode: query
# Yahoo Mail
- name: Yahoo
type: oauth2
file: .fiber/local/oauth/yahoo_credentials.json
scopes:
- mail-r
- mail-w
- profile
Provider-Specific API Handling
# Different providers return data in different formats
# The agent normalizes responses for consistent handling
if provider_type == "google":
# Gmail returns message IDs that need to be fetched individually
raw_messages = result.get("data", {}).get("messages", [])
messages = [
{
"id": msg.get("id"),
"thread_id": msg.get("threadId"),
"snippet": msg.get("snippet", "")
}
for msg in raw_messages
]
elif provider_type == "microsoft":
# Microsoft Graph API returns full message details
raw_messages = result.get("data", {}).get("value", [])
messages = [
{
"id": msg.get("id"),
"thread_id": msg.get("conversationId"),
"subject": msg.get("subject", ""),
"sender": msg.get("from", {}).get("emailAddress", {}).get("address", ""),
"date": msg.get("receivedDateTime"),
"is_read": msg.get("isRead", False),
"preview": msg.get("bodyPreview", "")
}
for msg in raw_messages
]
elif provider_type == "yahoo":
# Yahoo Mail API format
raw_messages = result.get("data", {}).get("messages", [])
messages = [
{
"id": msg.get("id"),
"thread_id": msg.get("threadId"),
"subject": msg.get("subject", ""),
"sender": msg.get("from", {}).get("email", ""),
"date": msg.get("receivedDate"),
"is_read": msg.get("isRead", False)
}
for msg in raw_messages
]
Provider OAuth Comparison
| Provider | Auth Endpoint | Key Scopes | Notes |
|---|---|---|---|
| Gmail | accounts.google.com | gmail.readonly, gmail.send | Requires access_type: offline for refresh tokens |
| Outlook | login.microsoftonline.com | Mail.Read, Mail.Send | Use response_mode: query for SPA compatibility |
| Yahoo | api.login.yahoo.com | mail-r, mail-w | Requires developer account approval |
Congratulations!
You've learned the complete OAuth integration patterns used in production Fiberwise applications:
OAuth Configuration
- Multi-provider manifest setup
- Scope configuration
- Credential file management
- Additional OAuth parameters
Frontend Integration
- CredentialService class
- Connection UI component
- OAuth flow initiation
- Callback handling
Agent Development
- execute() pattern
- Dependency injection
- Authenticated API requests
- Provider abstraction
Production Patterns
- Error handling
- Progress updates
- Graceful degradation
- Multi-provider support