Host Service Integration
AgentMap's Host Service Integration allows you to extend AgentMap with your own custom services and agents while leveraging AgentMap's dependency injection system. This enables host applications to define domain-specific services that are automatically injected into compatible agents.
- Service Injection Patterns - AgentMap's built-in protocol-based dependency injection system
- Agent Development Contract - Required agent interface and patterns
- Advanced Agent Types - Context configuration for services
Prerequisites
Before implementing host service integration, you should be familiar with:
- AgentMap Fundamentals: Basic workflow creation and agent development
- Service Injection Patterns: Read Service Injection Patterns to understand AgentMap's built-in DI system
- Agent Contract: Review Agent Development Contract for required patterns
- Python Protocols: Understanding of
typing.Protocol
and@runtime_checkable
decorator
Overview
Host Service Integration bridges the gap between AgentMap's workflow orchestration capabilities and your application's domain-specific functionality. Instead of being limited to AgentMap's built-in agents, you can:
- Define Custom Services: Implement services specific to your domain (database access, external APIs, business logic)
- Create Protocol Interfaces: Define contracts that specify how agents interact with services
- Build Custom Agents: Create agents that implement your protocols and automatically receive service dependencies
- Maintain Clean Architecture: Keep services separate from agents while ensuring automatic dependency injection
Architecture
Core Components
Host Application
├── Protocols (interfaces)
│ ├── DatabaseServiceProtocol
│ ├── EmailServiceProtocol
│ └── CustomServiceProtocol
├── Services (implementations)
│ ├── DatabaseService
│ ├── EmailService
│ └── CustomService
└── Agents (consumers)
├── DatabaseAgent
├── EmailAgent
└── CustomAgent
↓ Automatic Injection ↓
AgentMap Container
├── Service Registration
├── Protocol Discovery
├── Dependency Injection
└── Graph Execution
Integration Flow
- Protocol Definition: Define service interfaces using Python protocols
- Service Implementation: Create concrete service classes that provide functionality
- Agent Creation: Build agents that implement protocols to receive services
- Registration: Register services with AgentMap's dependency injection container
- Configuration: Configure services through AgentMap's configuration system
- Execution: AgentMap automatically injects services into compatible agents during graph execution
Core Concepts
Protocols
Protocols define the interface contract between agents and services. They specify what methods an agent must implement to receive a particular service.
from typing import Protocol, runtime_checkable, Any
from abc import abstractmethod
@runtime_checkable
class DatabaseServiceProtocol(Protocol):
"""Protocol for agents that need database access."""
@abstractmethod
def configure_database_service(self, database_service: Any) -> None:
"""Configure the agent with a database service.
Args:
database_service: Database service instance
"""
...
Key Requirements:
- Must use
@runtime_checkable
decorator - Should end with "ServiceProtocol" by convention
- Configuration methods should follow pattern:
configure_{service_name}_service
- Must be abstract protocols, not concrete classes
Follow the pattern {ServiceName}ServiceProtocol
for consistency with AgentMap's built-in protocols like LLMServiceProtocol
and StorageServiceProtocol
.
Services
Services provide the actual implementation of functionality that agents need. They contain business logic, external integrations, or data access code.
class DatabaseService:
"""Concrete database service implementation."""
def __init__(self, config: Dict[str, Any], logger: logging.Logger):
self.config = config
self.logger = logger
self._initialize_database()
def execute_query(self, query: str, params: tuple = None) -> List[Dict]:
"""Execute database query and return results."""
# Implementation here
pass
Key Requirements:
- Should accept configuration and logger in constructor
- Should provide clean, domain-specific APIs
- Should handle errors gracefully
- Should follow dependency injection patterns
Custom Agents
Custom agents implement one or more protocols to automatically receive the corresponding services.
from agentmap.agents.base_agent import BaseAgent
class DatabaseAgent(BaseAgent, DatabaseServiceProtocol):
"""Agent that performs database operations."""
def __init__(self, name: str, prompt: str = "", context: Dict[str, Any] = None,
logger=None, **kwargs):
super().__init__(name, prompt, context, logger, **kwargs)
self.database_service = None
def configure_database_service(self, database_service: Any) -> None:
"""Configure database service (called automatically by AgentMap)."""
self.database_service = database_service
self.logger.debug(f"Database service configured for {self.name}")
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Execute agent logic using injected service."""
if not self.database_service:
raise ValueError("Database service not configured")
operation = state.get("operation", "query")
if operation == "get_users":
users = self.database_service.execute_query("SELECT * FROM users")
return {**state, "users": users}
# Handle other operations...
return state
Key Requirements:
- Must inherit from
BaseAgent
and implement one or more service protocols - Must implement all abstract methods from the protocols
- Should handle cases where services are not available (graceful degradation)
- Should validate service availability before use
Implementation Guide
Step 1: Define Your Protocol
Create a protocol interface that defines how agents will interact with your service.
from typing import Protocol, runtime_checkable, Any
from abc import abstractmethod
@runtime_checkable
class MyServiceProtocol(Protocol):
"""Protocol for agents that need my custom service."""
@abstractmethod
def configure_my_service(self, service: Any) -> None:
"""Configure the agent with my service.
Args:
service: MyService instance
"""
...
Step 2: Implement Your Service
Create a concrete service class that provides the actual functionality.
import logging
from typing import Dict, Any
class MyService:
"""Custom service implementation."""
def __init__(self, config: Dict[str, Any], logger: logging.Logger):
self.config = config
self.logger = logger
self.api_key = config.get("api_key")
self.timeout = config.get("timeout", 30)
def do_something(self, data: Any) -> Dict[str, Any]:
"""Perform service operation."""
try:
# Your service logic here
result = self._process_data(data)
self.logger.info(f"Service operation completed: {result}")
return {"success": True, "result": result}
except Exception as e:
self.logger.error(f"Service operation failed: {e}")
return {"success": False, "error": str(e)}
def _process_data(self, data: Any) -> Any:
"""Internal processing logic."""
# Implementation here
return data
# Factory function for dependency injection
def create_my_service(app_config_service, logging_service) -> MyService:
"""Factory function to create MyService with dependencies.
Args:
app_config_service: AppConfigService instance
logging_service: LoggingService instance
Returns:
Configured MyService instance
"""
config = app_config_service.get_host_service_config("my_service")
logger = logging_service.get_logger("my_service")
return MyService(config["configuration"], logger)
Step 3: Create Your Agent
Build an agent that implements your protocol to automatically receive your service.
from typing import Dict, Any
from agentmap.agents.base_agent import BaseAgent
from protocols.my_service_protocol import MyServiceProtocol
class MyAgent(BaseAgent, MyServiceProtocol):
"""Agent that uses my custom service."""
def __init__(self, name: str, prompt: str = "", context: Dict[str, Any] = None,
logger=None, **kwargs):
super().__init__(name, prompt, context, logger, **kwargs)
self.my_service = None
def configure_my_service(self, service: Any) -> None:
"""Configure my service (called automatically)."""
self.my_service = service
self.logger.debug(f"My service configured for {self.name}")
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Execute agent using my service."""
if not self.my_service:
return {**state, "error": "My service not available"}
# Use your service
data = state.get("data", {})
result = self.my_service.do_something(data)
return {
**state,
"my_service_result": result,
"agent_name": self.name
}
Step 4: Register With AgentMap
Register your service and agent with AgentMap's dependency injection system.
from agentmap.di.containers import ApplicationContainer
from services.my_service import create_my_service
from protocols.my_service_protocol import MyServiceProtocol
from agents.my_agent import MyAgent
# Get the application container
container = ApplicationContainer()
# Register your service
container.register_host_factory(
service_name="my_service",
factory_function=create_my_service,
dependencies=["app_config_service", "logging_service"],
protocols=[MyServiceProtocol]
)
# Register your agent
agent_registry = container.agent_registry_service()
agent_registry.register_agent("my_agent", MyAgent)
Step 5: Configure
Add configuration for your service in AgentMap's configuration file.
host_application:
enabled: true
protocol_folders:
- "protocols" # Where your protocol files are located
services:
my_service:
enabled: true
configuration:
api_key: "${MY_API_KEY}"
timeout: 30
retries: 3
Configuration Reference
Host Application Configuration
The host_application
section in your AgentMap configuration controls host service integration.
host_application:
# Enable/disable host service integration
enabled: true
# Folders to scan for protocol definitions
protocol_folders:
- "protocols"
- "custom_protocols"
# Service configurations
services:
service_name:
enabled: true
configuration:
# Service-specific configuration
key: value
Service Configuration Structure
services:
my_service:
# Enable/disable this specific service
enabled: true
# Configuration passed to service constructor
configuration:
api_key: "${MY_API_KEY}" # Environment variable
timeout: 30
base_url: "https://api.example.com"
retry_attempts: 3
# Nested configuration
database:
host: "localhost"
port: 5432
name: "myapp"
Environment Variables
Host services can use environment variables in configuration:
services:
database_service:
configuration:
host: "${DATABASE_HOST}"
password: "${DATABASE_PASSWORD}"
api_key: "${API_KEY}"
Set these in your environment:
export DATABASE_HOST="prod-db.company.com"
export DATABASE_PASSWORD="secure-password"
export API_KEY="your-api-key"
Advanced Topics
Multi-Service Agents
Agents can implement multiple protocols to receive multiple services:
class MultiServiceAgent(BaseAgent, DatabaseServiceProtocol, EmailServiceProtocol):
"""Agent that uses multiple services."""
def __init__(self, name: str, **kwargs):
super().__init__(name, **kwargs)
self.database_service = None
self.email_service = None
def configure_database_service(self, service: Any) -> None:
self.database_service = service
def configure_email_service(self, service: Any) -> None:
self.email_service = service
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
# Use both services together
data = self.database_service.get_data()
self.email_service.send_report(data)
return {**state, "report_sent": True}
Protocol Composition
Create composite protocols for agents that need multiple related services:
@runtime_checkable
class FullStackProtocol(
DatabaseServiceProtocol,
EmailServiceProtocol,
NotificationServiceProtocol,
Protocol
):
"""Composite protocol for agents needing multiple services."""
pass
class FullStackAgent(BaseAgent, FullStackProtocol):
"""Agent that gets all three services automatically."""
# Implement all three configure methods
pass
Graceful Degradation
Handle cases where services might not be available:
class RobustAgent(BaseAgent, DatabaseServiceProtocol):
"""Agent with graceful degradation."""
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
if hasattr(self, 'database_service') and self.database_service:
# Use database if available
try:
data = self.database_service.get_data()
return {**state, "data": data, "source": "database"}
except Exception as e:
self.logger.warning(f"Database failed, using fallback: {e}")
# Fallback to alternative approach
fallback_data = self._get_fallback_data()
return {**state, "data": fallback_data, "source": "fallback"}
Service Dependencies
Services can depend on other services or AgentMap components:
def create_advanced_service(database_service, email_service, logging_service):
"""Service that depends on other services."""
logger = logging_service.get_logger("advanced_service")
return AdvancedService(database_service, email_service, logger)
# Register with dependencies
container.register_host_factory(
service_name="advanced_service",
factory_function=create_advanced_service,
dependencies=["database_service", "email_service", "logging_service"],
protocols=[AdvancedServiceProtocol]
)
Error Handling Patterns
Implement robust error handling in services and agents:
class RobustService:
"""Service with comprehensive error handling."""
def __init__(self, config: Dict[str, Any], logger: logging.Logger):
self.config = config
self.logger = logger
self.circuit_breaker = CircuitBreaker()
def call_external_api(self, data: Any) -> Dict[str, Any]:
"""Make external API call with error handling."""
try:
# Circuit breaker pattern
if self.circuit_breaker.is_open():
raise Exception("Circuit breaker is open")
# Make API call
response = self._make_api_call(data)
self.circuit_breaker.record_success()
return {"success": True, "data": response}
except Exception as e:
self.circuit_breaker.record_failure()
self.logger.error(f"API call failed: {e}")
# Return error response instead of raising
return {
"success": False,
"error": str(e),
"fallback_available": True
}
API Reference
ApplicationContainer Methods
register_host_factory()
Register a host service using a factory function.
def register_host_factory(
self,
service_name: str,
factory_function: callable,
dependencies: Optional[List[str]] = None,
protocols: Optional[List[Type]] = None,
metadata: Optional[Dict[str, Any]] = None
) -> None
Parameters:
service_name
: Unique name for the servicefactory_function
: Function that creates the service instancedependencies
: List of dependency service names from the containerprotocols
: List of protocols this service implementsmetadata
: Optional metadata about the service
register_host_service()
Register a host service using a class path.
def register_host_service(
self,
service_name: str,
service_class_path: str,
dependencies: Optional[List[str]] = None,
protocols: Optional[List[Type]] = None,
metadata: Optional[Dict[str, Any]] = None,
singleton: bool = True
) -> None
configure_host_protocols()
Configure host protocols on an agent instance.
def configure_host_protocols(self, agent: Any) -> int
Parameters:
agent
: Agent instance to configure
Returns:
- Number of services configured on the agent
get_host_services()
Get information about all registered host services.
def get_host_services(self) -> Dict[str, Dict[str, Any]]
Returns:
- Dictionary mapping service names to service information
AppConfigService Methods
get_host_application_config()
Get host application configuration with defaults.
def get_host_application_config(self) -> Dict[str, Any]
get_host_service_config()
Get configuration for a specific host service.
def get_host_service_config(self, service_name: str) -> Dict[str, Any]
is_host_application_enabled()
Check if host application support is enabled.
def is_host_application_enabled(self) -> bool
Best Practices
Protocol Design
-
Use Descriptive Names: Protocol names should clearly indicate their purpose
# Good
class DatabaseServiceProtocol(Protocol): ...
class EmailNotificationProtocol(Protocol): ...
# Avoid
class ServiceProtocol(Protocol): ...
class Protocol1(Protocol): ... -
Keep Protocols Focused: Each protocol should represent a single responsibility
# Good - focused protocols
class DatabaseReadProtocol(Protocol): ...
class DatabaseWriteProtocol(Protocol): ...
# Avoid - overly broad protocol
class DatabaseEverythingProtocol(Protocol): ... -
Use Runtime Checkable: Always use
@runtime_checkable
decoratorfrom typing import Protocol, runtime_checkable
@runtime_checkable
class MyServiceProtocol(Protocol):
# Protocol definition
Service Implementation
-
Follow Dependency Injection: Accept dependencies in constructor
class MyService:
def __init__(self, config: Dict[str, Any], logger: logging.Logger):
self.config = config
self.logger = logger -
Handle Configuration Gracefully: Provide sensible defaults
def __init__(self, config: Dict[str, Any], logger: logging.Logger):
self.timeout = config.get("timeout", 30)
self.retries = config.get("retries", 3)
self.base_url = config.get("base_url", "https://api.default.com") -
Implement Error Recovery: Don't let service failures crash agents
def call_api(self, data):
try:
return self._make_call(data)
except Exception as e:
self.logger.error(f"API call failed: {e}")
return {"error": str(e), "fallback": True}
Agent Development
-
Validate Service Availability: Check services before use
def run(self, state):
if not self.my_service:
return {**state, "error": "Service not available"}
# Use service -
Implement Graceful Degradation: Provide fallback behavior
def run(self, state):
if hasattr(self, 'database_service'):
return self._use_database(state)
else:
return self._use_fallback(state) -
Log Service Usage: Help with debugging and monitoring
def configure_my_service(self, service):
self.my_service = service
self.logger.info(f"Service configured for {self.name}")
Configuration Management
-
Use Environment Variables: Keep secrets out of config files
services:
my_service:
configuration:
api_key: "${API_KEY}" # From environment
secret: "${MY_SECRET}" -
Organize by Environment: Use different configs for dev/test/prod
# development.yaml
host_application:
services:
database_service:
configuration:
host: "localhost"
# production.yaml
host_application:
services:
database_service:
configuration:
host: "${PROD_DB_HOST}" -
Validate Configuration: Check required settings early
def __init__(self, config, logger):
required_keys = ["api_key", "base_url"]
for key in required_keys:
if key not in config:
raise ValueError(f"Missing required config: {key}")
Troubleshooting
Common Issues
Service Not Being Injected
Symptoms:
- Agent's configure method never called
- Service attribute is None
- "Service not configured" errors
Solutions:
-
Verify protocol implementation:
# Check if agent implements protocol
assert isinstance(my_agent, MyServiceProtocol) -
Check service registration:
# Verify service is registered
services = container.get_host_services()
assert "my_service" in services -
Enable debug logging:
logging:
level: DEBUG
Import Errors
Symptoms:
- ModuleNotFoundError when loading protocols/services
- ImportError during agent registration
Solutions:
- Check Python path includes your modules
- Verify file names and directory structure
- Ensure
__init__.py
files exist in package directories
Configuration Not Loading
Symptoms:
- Service receives empty or default configuration
- Environment variables not being resolved
Solutions:
- Verify YAML syntax is correct
- Check environment variables are set:
echo $MY_API_KEY
- Use absolute paths for protocol folders:
protocol_folders:
- "/absolute/path/to/protocols"
Protocol Not Found
Symptoms:
- Protocol discovery fails
- "No protocol implementations found" warnings
Solutions:
- Check protocol folder configuration
- Ensure protocols use
@runtime_checkable
- Verify protocol naming conventions
Debugging Tips
-
Enable Verbose Logging:
logging:
level: DEBUG
format: "[%(asctime)s] %(name)s %(levelname)s: %(message)s" -
Check Service Registration:
# In your application
container = ApplicationContainer()
services = container.get_host_services()
print(f"Registered services: {list(services.keys())}")
protocols = container.get_protocol_implementations()
print(f"Protocol implementations: {protocols}") -
Test Service Instantiation:
# Verify service can be created
service = container.get_host_service_instance("my_service")
assert service is not None -
Validate Agent Protocol Implementation:
from protocols.my_service_protocol import MyServiceProtocol
agent = MyAgent("test")
assert isinstance(agent, MyServiceProtocol)
assert hasattr(agent, 'configure_my_service')
Migration Guide
If you're migrating from a different AgentMap integration approach:
From Manual Service Injection
Before:
# Manual injection
agent = MyAgent("test")
agent.database_service = DatabaseService(config)
After:
# Protocol-based injection
class MyAgent(BaseAgent, DatabaseServiceProtocol):
def configure_database_service(self, service):
self.database_service = service
# Service automatically injected by AgentMap
From Hardcoded Dependencies
Before:
class MyAgent(BaseAgent):
def __init__(self, name, database_url):
super().__init__(name)
self.db = Database(database_url) # Hardcoded
After:
class MyAgent(BaseAgent, DatabaseServiceProtocol):
def configure_database_service(self, service):
self.database_service = service # Injected
This approach provides better testability, configuration management, and separation of concerns.
Next Steps
After implementing host service integration:
Immediate Next Steps
- Test Your Integration: Use the patterns in this guide to verify your implementation
- Configure Services: Review the configuration reference for production setup
- Debug Issues: Use the troubleshooting section for common problems
Advanced Topics
- Registry Management: Learn about advanced service registry patterns
- Built-in Service Patterns: Study Service Injection Patterns for AgentMap's internal patterns
- Agent Development: Master Agent Development Contract for advanced agent patterns
- Context Configuration: Explore Advanced Agent Types for service-specific configuration
Integration with AgentMap Ecosystem
- Workflow Development: Use in complex multi-agent workflows
- Prompt Management: Integrate with template systems
- Execution Tracking: Monitor with performance tracking tools
- Storage Integration: Combine with data storage operations
For hands-on learning, check out the Tutorial: Building Custom Agents which includes complete working examples of host service integration patterns.