Custom Agents
Learn how to create custom agents that extend the MARSYS framework with specialized capabilities and behaviors.
Overview
Custom agents enable you to:
- Extend BaseAgent: Create agents with specialized behavior
- Add Domain Knowledge: Incorporate specific expertise
- Implement State Machines: Complex multi-step workflows
- Custom Response Processing: Specialized output formats
Creating Custom Agents
Extending BaseAgent
The fundamental pattern for custom agents:
from marsys.agents import BaseAgentfrom marsys.agents.memory import Messagefrom typing import Dict, Any, Optionalclass CustomAgent(BaseAgent):"""Custom agent with specialized behavior."""def __init__(self,model,specialized_knowledge: Optional[Dict] = None,**kwargs):super().__init__(model=model,name=kwargs.pop("name", "CustomAgent"),goal="Custom specialized agent",instruction="A custom agent with specialized capabilities.",**kwargs)self.specialized_knowledge = specialized_knowledge or {}async def _run(self,prompt: Any,context: Dict[str, Any],**kwargs) -> Message:"""Pure execution logic - NO side effects!This method must be stateless and pure."""# Prepare messages with base functionalitymessages = self._prepare_messages(prompt)# Add custom logicif self._should_use_knowledge(prompt):messages.append(Message(role="system",content=f"Use this knowledge: {self.specialized_knowledge}"))# Call modelresponse = await self.model.run(messages)# Return pure Message objectreturn Message(role="assistant",content=response.content,tool_calls=getattr(response, 'tool_calls', None))def _should_use_knowledge(self, prompt: str) -> bool:"""Determine if specialized knowledge is relevant."""return True
Pure Execution Rule
The _run() method must be pure - no memory manipulation, no logging to files, no global state changes. All side effects are handled by the coordination layer.
Domain-Specific Agents
Create agents with domain expertise:
class FinancialAnalyst(BaseAgent):"""Financial analysis specialist."""def __init__(self, model, market_data_api=None, **kwargs):super().__init__(model=model,name=kwargs.pop("name", "FinancialAnalyst"),goal="Expert financial analyst with real-time market access",instruction="You are a senior financial analyst.",tools={"get_stock_price": self._get_stock_price,"calculate_metrics": self._calculate_metrics},**kwargs)self.market_data_api = market_data_apiasync def _run(self, prompt, context, **kwargs):"""Analyze financial data with expertise."""# Add market context for financial queriesif self._is_financial_query(prompt):market_context = await self._get_market_context()enriched_prompt = f"{prompt}\n\nMarket: {market_context}"else:enriched_prompt = promptmessages = self._prepare_messages(enriched_prompt)response = await self.model.run(messages)return Message(role="assistant", content=response.content)def _get_stock_price(self, symbol: str) -> Dict:"""Get real-time stock price."""if self.market_data_api:return self.market_data_api.get_price(symbol)return {"error": "No market data API configured"}
Advanced Patterns
State Machine Agent
Implement complex workflows with state transitions:
from enum import Enumclass ProcessingState(Enum):INITIAL = "initial"ANALYZING = "analyzing"VALIDATING = "validating"COMPLETE = "complete"class StateMachineAgent(BaseAgent):"""Agent with state machine behavior."""def __init__(self, model, **kwargs):super().__init__(model=model, **kwargs)self.state = ProcessingState.INITIALasync def _run(self, prompt, context, **kwargs):# Handle based on current stateif self.state == ProcessingState.INITIAL:return await self._handle_initial(prompt)elif self.state == ProcessingState.ANALYZING:return await self._handle_analyzing(prompt)# ... etcasync def _handle_initial(self, prompt):# Initial processing logicpass
Agent with Custom Validation
from pydantic import BaseModelclass AnalysisInput(BaseModel):query: strdata_source: stroutput_format: str = "json"class AnalysisOutput(BaseModel):result: Dict[str, Any]confidence: floatmethodology: strclass ValidatedAgent(BaseAgent):"""Agent with input/output validation."""def __init__(self, model, **kwargs):super().__init__(model=model,input_schema=AnalysisInput,output_schema=AnalysisOutput,**kwargs)
Best Practices
- Keep _run() pure: No side effects in the execution method
- Use descriptive names: Agent names should reflect their purpose
- Document tools: Clear docstrings help the model use tools correctly
- Validate inputs: Use schemas for type safety
- Handle edge cases: Consider empty inputs and error scenarios
Reusing Components
Consider creating reusable base classes for common patterns in your domain. For example, a DataAnalysisAgent base that multiple specialized analyzers can extend.