100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
AI Agents & Agentic Workflows
55 minadvanced

Advanced Agent Patterns Exercise

What You'll Build

In this exercise, you will construct a multi-agent cricket analytics system that simulates real-time match intelligence gathering and decision-making. The project demonstrates how autonomous agents can collaborate to monitor player performance, detect anomalies in bowling patterns, predict run outcomes, and recommend strategic field placements.

The system comprises four specialized agents, each with a distinct responsibility. A data ingestion agent parses live match events, a performance analysis agent calculates metrics such as economy rate and strike rotation, a pattern recognition agent identifies bowling trends and batting weaknesses, and a strategy recommendation agent synthesizes insights into actionable coaching decisions.

Beyond individual agent logic, you will implement agent communication protocols and shared memory mechanisms for context persistence. A coordination layer orchestrates agent workflows without central control, showcasing true agentic decentralization as a core architectural principle.

Analogy🏏Cricket
🏏 Think of it like cricket: Imagine Rohit Sharma as captain during a T20 match facing a world-class bowling attack in the powerplay. Rohit cannot make all decisions alone—he must delegate: he instructs his batting coach (via field signals) to assess whether the opposition is bowling tight lines (the Batting Strategist Agent), simultaneously his fast-bowling coach monitors the opposing batsman's weaknesses to suggest death bowling strategies (the Bowling Analyst Agent), and his fielding captain repositions the field based on match phases and batter tendencies (the Field Placement Agent). Each agent operates independently with their own observation-reasoning-decision loop: the Batting Strategist watches run rate versus wicket ratio and decides acceleration timing; the Bowling Analyst monitors economy and suggests bowling changes; the Field Placement Agent tracks field gaps and batter strengths. Rohit (the Primary Agent) integrates all three recommendations, resolves conflicts (e.g., aggressive batting vs. cautious bowling), and issues final instructions. If the situation changes—a key wicket falls, run rate drops—all agents recalibrate simultaneously, and the captain re-orchestrates. This mirrors how agentic workflows operate: multiple specialized reasoning units working in parallel with autonomous decision-making, coordinated by a primary orchestrator that synthesizes recommendations and handles conflicts under time pressure.

Prerequisites

  • Understanding of agent architecture: roles, responsibilities, communication protocols, and state management in distributed systems.
  • Python 3.9+ with asyncio fundamentals: async/await syntax, event loops, coroutines, and concurrent task management for parallel agent execution.
  • Knowledge of pub-sub messaging patterns: message brokers, event queues, topic-based routing, and asynchronous event handling architectures.
  • Familiarity with cricket domain: understanding match phases (powerplay, middle overs, death overs), player roles, bowling economy, strike rate, and field positioning strategies.
  • JSON data handling: parsing match events, structuring agent messages, serialization/deserialization, and working with nested configuration objects.

Setup & Project Structure

Begin by creating a new Python project directory with a modular structure that cleanly separates agent implementations, communication infrastructure, and data models. Initialize a virtual environment to isolate dependencies, including aiohttp for asynchronous HTTP operations, pydantic for data validation, and redis-py if you choose to use Redis for agent communication.

Analogy🏏Cricket
🏏 Think of it like cricket: structuring this project is like laying out a team's operations before a series, with every function in its own clearly labelled room. Just as batters, bowlers, and support staff each have a dedicated space, you separate concerns across directories — an agents directory holding each specialist implementation, a models directory holding shared records like Match, Innings, and Bowler that everyone references as the single source of truth, and a communication layer that carries messages between them like the captain's signals to the field. Just as a squad relies on one agreed scorebook so batters and analysts never argue over the numbers, the shared data models give every agent one consistent view of the match. This reveals the payoff: with logic, shared state, and protocols cleanly isolated, you can develop and test each specialist agent on its own, exactly as a coach drills the bowlers separately before bringing the full XI together.

Organize the project into dedicated directories for each of the four agents — data_ingestion_agent, performance_analysis_agent, pattern_recognition_agent, and strategy_recommendation_agent — alongside a shared models directory for cricket data structures, a messaging module for inter-agent communication, and a coordinator responsible for orchestrating the overall workflow.

This separation ensures that each agent can be developed, tested, and deployed independently. Clear contracts are maintained across the system through well-defined message schemas and event definitions, making the architecture both maintainable and extensible.

bash
# Create project structure for multi-agent cricket analytics
mkdir -p cricket-agentic-system
cd cricket-agentic-system

# Initialize Python virtual environment
python3.9 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Create project directories
mkdir -p agents/data_ingestion_agent
mkdir -p agents/performance_analysis_agent
mkdir -p agents/pattern_recognition_agent
mkdir -p agents/strategy_recommendation_agent
mkdir -p shared/models
mkdir -p shared/messaging
mkdir -p coordinator
mkdir -p tests
mkdir -p data/fixtures

# Create main requirements.txt
cat > requirements.txt << 'EOF'
aiofiles==23.2.1
aiohttp==3.8.5
pydantic==2.0.2
python-dotenv==1.0.0
pytest==7.4.0
pytest-asyncio==0.21.1
pytest-cov==4.1.0
EOF

# Install dependencies
pip install -r requirements.txt

# Create main entry point and __init__ files
touch main.py
touch agents/__init__.py
touch shared/__init__.py
touch shared/models/__init__.py
touch shared/messaging/__init__.py
touch coordinator/__init__.py

echo "Project structure created successfully for cricket-agentic-system"

Step 1 — Foundation

Before implementing individual agents, you must build the foundational data models and messaging infrastructure upon which all agents depend. Using Pydantic, define models for the core cricket domain entities: Match (containing match_id, ground, and format), Player (player_id, name, role, and statistics), Ball (ball_number, bowler, batter, runs, and wicket_status), and Innings (innings_number, runs_scored, and wickets_fallen).

Inter-agent communication is represented through an Event class, where each event carries a source_agent, event_type, timestamp, and payload. This uniform structure ensures that all messages across the system share a consistent, inspectable format.

To distribute these events, implement a MessageBus class using asyncio that manages publish-subscribe subscriptions. Agents register listeners for specific event types, publish events to the bus, and the bus asynchronously delivers matching events to all relevant subscribers. This design decouples agents from any knowledge of each other's existence — they interact solely through the abstraction of typed events, enabling independent scaling and isolated testing.

Analogy🏏Cricket
🏏 Think of it like cricket: Before any match analysis can happen, the cricket commentary team (our agents) needs a shared scorecard format and communication protocol. Just as the official scorecard standardizes how runs, wickets, and overs are recorded—allowing commentators, analysts, and broadcasters to reference identical match state—our BaseAgent and data models create a standardized 'match scorecard' that all agents read from. Each commentator (specialist agent) observes the same scorecard and broadcasts their analysis using standard terminology: "Confidence 0.9: Recommend aggressive batting" (the Batting Strategist), "Confidence 0.85: Bowl yorkers" (the Bowling Analyst). The immutability of our match state (like an official scorecard that cannot be retroactively changed) prevents commentators from accidentally contradicting each other because they're all reading the exact same source of truth. The confidence score mirrors a commentator's certainty level: a seasoned cricket analyst might say "Definitely recommend field adjustment" (high confidence) versus "Possibly consider slip placement" (lower confidence). This standardized foundation allows the Primary Agent (match captain) to orchestrate responses without micromanaging each specialist's internal reasoning.
python
# shared/models/__init__.py - Define cricket domain models and events
from enum import Enum
from datetime import datetime
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field
import uuid

class PlayerRole(str, Enum):
    """Player roles in cricket."""
    OPENER = "opener"
    MIDDLE_ORDER = "middle_order"
    LOWER_ORDER = "lower_order"
    BOWLER = "bowler"
    ALL_ROUNDER = "all_rounder"
    WICKETKEEPER = "wicketkeeper"

class Player(BaseModel):
    """Represents a cricket player with statistics."""
    player_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    name: str
    role: PlayerRole
    matches_played: int = 0
    innings_count: int = 0
    runs_scored: int = 0
    strike_rate: float = 0.0
    bowling_economy: float = 0.0
    wickets_taken: int = 0
    average: float = 0.0

class Match(BaseModel):
    """Represents a cricket match context."""
    match_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    ground: str
    match_format: str  # T20, ODI, Test
    team_a: str
    team_b: str
    toss_winner: str
    elected_to_bat: str
    created_at: datetime = Field(default_factory=datetime.utcnow)

class Ball(BaseModel):
    """Represents a single delivery in cricket."""
    match_id: str
    innings_number: int
    over_number: int
    ball_number: int
    bowler: str  # Player name
    batter: str  # Player name
    runs_off_bat: int
    extras: int
    is_wicket: bool = False
    wicket_type: Optional[str] = None  # bowled, caught, lbw, run_out
    ball_speed_kmh: float = 0.0
    ball_type: str = "other"  # fast, spin, slower
    timestamp: datetime = Field(default_factory=datetime.utcnow)

class Innings(BaseModel):
    """Represents an innings (batting phase)."""
    match_id: str
    innings_number: int
    batting_team: str
    bowling_team: str
    runs_scored: int = 0
    wickets_fallen: int = 0
    overs_completed: int = 0
    balls_played: int = 0
    target: Optional[int] = None
    status: str = "ongoing"  # ongoing, completed, discontinued

class EventType(str, Enum):
    """Types of events agents communicate via."""
    MATCH_STARTED = "match_started"
    BALL_DELIVERED = "ball_delivered"
    WICKET_FALLEN = "wicket_fallen"
    OVER_COMPLETED = "over_completed"
    INNINGS_COMPLETED = "innings_completed"
    PERFORMANCE_CALCULATED = "performance_calculated"
    PATTERN_DETECTED = "pattern_detected"
    STRATEGY_RECOMMENDED = "strategy_recommended"
    ANALYSIS_REQUIRED = "analysis_required"

class Event(BaseModel):
    """Inter-agent communication event."""
    event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    event_type: EventType
    source_agent: str  # Name of agent that published
    timestamp: datetime = Field(default_factory=datetime.utcnow)
    match_id: str
    payload: Dict[str, Any]

# shared/messaging/__init__.py - Event bus for agent communication
import asyncio
from typing import Callable, List, Dict, Set

class MessageBus:
    """Pub-Sub message bus for inter-agent communication."""
    
    def __init__(self):
        # Dictionary mapping event_type -> list of subscriber coroutines
        self._subscribers: Dict[EventType, List[Callable]] = {}
        self._event_history: List[Event] = []
        self._lock = asyncio.Lock()
    
    def subscribe(self, event_type: EventType, handler: Callable) -> None:
        """Register a handler for a specific event type.
        
        Args:
            event_type: Type of event to listen for
            handler: Async callable that processes the event
        """
        if event_type not in self._subscribers:
            self._subscribers[event_type] = []
        self._subscribers[event_type].append(handler)
        print(f"Subscriber registered for {event_type.value}")
    
    async def publish(self, event: Event) -> None:
        """Publish an event to all interested subscribers.
        
        Args:
            event: Event object to publish
        """
        async with self._lock:
            self._event_history.append(event)
        
        # Get all handlers for this event type
        handlers = self._subscribers.get(event.event_type, [])
        
        if handlers:
            # Execute all handlers concurrently
            tasks = [handler(event) for handler in handlers]
            await asyncio.gather(*tasks, return_exceptions=True)
        else:
            print(f"No subscribers for event type: {event.event_type.value}")
    
    async def get_events_by_type(self, event_type: EventType) -> List[Event]:
        """Retrieve historical events of a specific type."""
        return [e for e in self._event_history if e.event_type == event_type]
    
    async def get_recent_events(self, match_id: str, limit: int = 10) -> List[Event]:
        """Get the most recent events for a match."""
        match_events = [e for e in self._event_history if e.match_id == match_id]
        return match_events[-limit:]

print("✓ Foundation models and messaging infrastructure established")

Step 2 — Core Logic

With the infrastructure in place, implement the four specialized agents, each embodying distinct responsibilities and autonomous decision-making logic. The DataIngestionAgent continuously parses live match events — including balls bowled, runs scored, and wickets — creates Ball and Innings objects from that data, and publishes raw match data events to the message bus.

The PerformanceAnalysisAgent subscribes to ball_delivered events and maintains running player statistics, calculating strike rate (runs per 100 deliveries), bowling economy (runs conceded per 6 deliveries), and batting averages. It publishes performance_calculated events whenever key statistical thresholds change, ensuring downstream agents always have access to current performance data.

The PatternRecognitionAgent also subscribes to ball_delivered events but focuses on behavioral sequences rather than aggregate statistics. It detects patterns such as a bowler consistently bowling yorkers in death overs, a batter struggling against short-pitched deliveries, or a drop in powerplay acceleration against spinners. When a pattern is identified, the agent publishes a pattern_detected event containing the specific insight.

Each agent runs as an asynchronous coroutine with its own state management, message queue, and periodic processing cycles. This architecture enables true concurrency and independence, allowing agents to operate simultaneously without blocking one another.

Analogy🏏Cricket
🏏 Think of it like cricket: Consider the Indian team facing Australia in a T20 World Cup final. Rohit's batting coach (BattingStrategistAgent) watches the scorecard: India is 45/2 in 6 overs requiring 160, and the run rate is 7.5 but the required rate is 10.2 with death bowling coming. The coach reasons: "Acceleration needed, but Hazlewood is fresh—risk is moderate." Confidence: 0.75. Simultaneously, Bharat Arun (bowling coach, BowlingAnalystAgent) observes that Starc has bowled 3 overs for 18 runs with economy of 6.0, but has 2 overs left and tends to excel in death. He reasons: "Hold Starc for death overs, deploy Bumrah now against Travis Head (left-hander who struggles to short balls)." Confidence: 0.88. Meanwhile, the fielding coach (FieldPlacementAgent) watches Travis Head's batting patterns—he's aggressive against spinners on off-side, averaging 45 runs in that corridor. Coach reasons: "Place short fine-leg and deep extra-cover to block his favorite zones." Confidence: 0.82. Each coach observes independently, reasons from their expertise, and delivers a recommendation with conviction level. Rohit (Primary Agent, coming in Step 3) synthesizes these three independent analyses into one coherent match strategy.
python
# agents/data_ingestion_agent/__init__.py
import asyncio
from datetime import datetime
from typing import List
from shared.models import Ball, Match, Innings, Event, EventType, Player
from shared.messaging import MessageBus

class DataIngestionAgent:
    """Ingests live match events and publishes raw ball data."""
    
    def __init__(self, message_bus: MessageBus):
        self.message_bus = message_bus
        self.agent_name = "data_ingestion_agent"
        self.current_match: Match = None
        self.current_innings: Innings = None
        self.ball_sequence: List[Ball] = []
    
    async def initialize_match(self, match: Match, innings: Innings) -> None:
        """Initialize a new match context."""
        self.current_match = match
        self.current_innings = innings
        
        event = Event(
            event_type=EventType.MATCH_STARTED,
            source_agent=self.agent_name,
            match_id=match.match_id,
            payload={
                "ground": match.ground,
                "teams": f"{match.team_a} vs {match.team_b}",
                "format": match.match_format,
                "batting_team": innings.batting_team
            }
        )
        await self.message_bus.publish(event)
    
    async def ingest_ball(self, ball_data: dict) -> Ball:
        """Process a single ball delivery.
        
        Args:
            ball_data: Dictionary with ball details (bowler, batter, runs, wicket, etc.)
        
        Returns:
            Ball object created and published
        """
        ball = Ball(
            match_id=self.current_match.match_id,
            innings_number=self.current_innings.innings_number,
            over_number=ball_data.get("over_number", 0),
            ball_number=ball_data.get("ball_number", 0),
            bowler=ball_data.get("bowler", "Unknown"),
            batter=ball_data.get("batter", "Unknown"),
            runs_off_bat=ball_data.get("runs", 0),
            extras=ball_data.get("extras", 0),
            is_wicket=ball_data.get("is_wicket", False),
            wicket_type=ball_data.get("wicket_type"),
            ball_speed_kmh=ball_data.get("speed", 0.0),
            ball_type=ball_data.get("type", "other")
        )
        
        self.ball_sequence.append(ball)
        
        # Update innings stats
        self.current_innings.runs_scored += ball.runs_off_bat + ball.extras
        if ball.is_wicket:
            self.current_innings.wickets_fallen += 1
        
        # Publish ball delivery event
        event = Event(
            event_type=EventType.BALL_DELIVERED,
            source_agent=self.agent_name,
            match_id=self.current_match.match_id,
            payload={
                "bowler": ball.bowler,
                "batter": ball.batter,
                "runs_total": ball.runs_off_bat + ball.extras,
                "ball_speed": ball.ball_speed_kmh,
                "ball_type": ball.ball_type,
                "is_wicket": ball.is_wicket,
                "over": ball.over_number,
                "ball": ball.ball_number
            }
        )
        await self.message_bus.publish(event)
        return ball
    
    async def complete_over(self, over_number: int) -> None:
        """Mark completion of an over."""
        event = Event(
            event_type=EventType.OVER_COMPLETED,
            source_agent=self.agent_name,
            match_id=self.current_match.match_id,
            payload={
                "over_number": over_number,
                "runs_in_over": sum(b.runs_off_bat + b.extras for b in self.ball_sequence
                                    if b.over_number == over_number),
                "wickets_in_over": sum(1 for b in self.ball_sequence
                                       if b.over_number == over_number and b.is_wicket)
            }
        )
        await self.message_bus.publish(event)

# agents/performance_analysis_agent/__init__.py
from typing import Dict
from collections import defaultdict
from shared.models import Event, EventType

class PerformanceAnalysisAgent:
    """Analyzes player performance metrics in real-time."""
    
    def __init__(self, message_bus: MessageBus):
        self.message_bus = message_bus
        self.agent_name = "performance_analysis_agent"
        # Track player stats: {player_name: {runs, balls, dismissals, etc.}}
        self.player_stats: Dict[str, dict] = defaultdict(lambda: {
            "runs": 0,
            "balls_faced": 0,
            "strike_rate": 0.0,
            "dismissals": 0
        })
        self.bowler_stats: Dict[str, dict] = defaultdict(lambda: {
            "runs_conceded": 0,
            "balls_bowled": 0,
            "economy_rate": 0.0,
            "wickets": 0
        })
    
    async def handle_ball_event(self, event: Event) -> None:
        """Process a ball_delivered event and update player stats."""
        payload = event.payload
        batter = payload["batter"]
        bowler = payload["bowler"]
        runs = payload["runs_total"]
        
        # Update batter stats
        self.player_stats[batter]["runs"] += runs
        self.player_stats[batter]["balls_faced"] += 1
        
        balls = self.player_stats[batter]["balls_faced"]
        runs_scored = self.player_stats[batter]["runs"]
        self.player_stats[batter]["strike_rate"] = (runs_scored / balls * 100) if balls > 0 else 0.0
        
        # Update bowler stats
        self.bowler_stats[bowler]["runs_conceded"] += runs
        self.bowler_stats[bowler]["balls_bowled"] += 1
        
        balls_bowled = self.bowler_stats[bowler]["balls_bowled"]
        runs_conceded = self.bowler_stats[bowler]["runs_conceded"]
        self.bowler_stats[bowler]["economy_rate"] = (runs_conceded / (balls_bowled / 6)) if balls_bowled >= 6 else 0.0
        
        # Handle wicket
        if payload["is_wicket"]:
            self.player_stats[batter]["dismissals"] += 1
            self.bowler_stats[bowler]["wickets"] += 1
        
        # Publish performance update
        performance_event = Event(
            event_type=EventType.PERFORMANCE_CALCULATED,
            source_agent=self.agent_name,
            match_id=event.match_id,
            payload={
                "batter": batter,
                "batter_stats": self.player_stats[batter].copy(),
                "bowler": bowler,
                "bowler_stats": self.bowler_stats[bowler].copy()
            }
        )
        await self.message_bus.publish(performance_event)

# agents/pattern_recognition_agent/__init__.py
from collections import deque

class PatternRecognitionAgent:
    """Detects patterns in bowling and batting behavior."""
    
    def __init__(self, message_bus: MessageBus):
        self.message_bus = message_bus
        self.agent_name = "pattern_recognition_agent"
        # Store recent balls for each bowler: {bowler: deque of last 10 balls}
        self.bowler_history: Dict[str, deque] = defaultdict(lambda: deque(maxlen=10))
        self.batter_weaknesses: Dict[str, list] = defaultdict(list)
    
    async def handle_ball_event(self, event: Event) -> None:
        """Analyze ball delivery for patterns."""
        payload = event.payload
        bowler = payload["bowler"]
        batter = payload["batter"]
        ball_type = payload["ball_type"]
        runs = payload["runs_total"]
        
        # Record ball for bowler
        self.bowler_history[bowler].append({
            "type": ball_type,
            "speed": payload["ball_speed"],
            "runs": runs,
            "over": payload["over"]
        })
        
        # Detect if bowler is specializing in yorkers (death bowling pattern)
        yorker_count = sum(1 for b in self.bowler_history[bowler] if b["type"] == "yorker")
        if len(self.bowler_history[bowler]) >= 7 and yorker_count >= 5:
            pattern_event = Event(
                event_type=EventType.PATTERN_DETECTED,
                source_agent=self.agent_name,
                match_id=event.match_id,
                payload={
                    "pattern_type": "bowler_specialization",
                    "bowler": bowler,
                    "specialization": "death_bowling_yorkers",
                    "confidence": (yorker_count / len(self.bowler_history[bowler])) * 100,
                    "recommendation": "Batter should prepare for yorkers; position leg-side fielders accordingly"
                }
            )
            await self.message_bus.publish(pattern_event)
        
        # Track batter weakness against specific ball types
        if runs == 0 and ball_type == "yorker":
            self.batter_weaknesses[batter].append("struggles_against_yorkers")
        
        if len(self.batter_weaknesses[batter]) >= 3:
            weakness_event = Event(
                event_type=EventType.PATTERN_DETECTED,
                source_agent=self.agent_name,
                match_id=event.match_id,
                payload={
                    "pattern_type": "batter_weakness",
                    "batter": batter,
                    "weakness": "vulnerable_to_yorkers",
                    "occurrences": len(self.batter_weaknesses[batter])
                }
            )
            await self.message_bus.publish(weakness_event)

print("✓ Core agent logic implemented with autonomous decision-making")

Step 3 — Integration & Enhancement

The StrategyRecommendationAgent completes the agent pipeline by subscribing to both performance_calculated and pattern_detected events, synthesizing intelligence from multiple sources into actionable coaching decisions. Internally, it maintains a decision state machine with phases covering powerplay analysis, middle-overs optimization, and death bowling preparation, applying heuristics that combine player performance thresholds with the patterns detected upstream.

In addition to the four core agents, implement a Coordinator class that manages the entire workflow. The Coordinator handles agent lifecycle operations — including startup, event subscription, and graceful shutdown — maintains shared match context, and exposes an API for injecting simulated match events. It ensures agents start in dependency order and remain synchronized through event timestamps.

To make the system observable and maintainable, add event logging throughout the pipeline and build a real-time dashboard that tracks agent health, event flow rates, and emerging strategic recommendations. This observability layer is essential for diagnosing coordination issues and validating that the system behaves as expected under realistic conditions.

Analogy🏏Cricket
🏏 Think of it like cricket: Picture the Indian war room during India's T20 World Cup final against Australia. The captain (Primary Agent/Orchestrator) simultaneously receives three specialist reports during a timeout: (1) Batting Coach (confidence 0.80): "Continue aggressive batting, we're 2 runs ahead of required rate." (2) Bowling Coach (confidence 0.88): "Starc will bowl next; field a defensive setup." (3) Field Coach (confidence 0.75): "Head has 4 boundaries this powerplay; place deep cover." The captain faces conflict: aggressive batting strategy contradicts defensive bowling field. But the captain applies domain wisdom: bowling coach's confidence (0.88) is highest, and a fresh bowler like Starc warrants caution. Additionally, a hidden rule applies—if India loses a wicket in the next over, the aggressive approach becomes too risky (conditional override). The captain synthesizes: "Batting: stay aggressive but rotate strike to avoid dot balls against Starc. Bowling: field defensively with deep cover per Head's pattern. Fielding: place that cover immediately." This unified strategy honors all three specialists' expertise while applying contextual judgment and conflict resolution rules. The same pattern occurs in our Orchestrator: gather parallel recommendations, weight by confidence, apply domain rules, and synthesize coherent output.
python
# agents/strategy_recommendation_agent/__init__.py
from enum import Enum
from typing import List, Optional

class MatchPhase(str, Enum):
    POWERPLAY = "powerplay"
    MIDDLE_OVERS = "middle_overs"
    DEATH_OVERS = "death_overs"

class StrategyRecommendationAgent:
    """Synthesizes multi-source intelligence into strategic recommendations."""
    
    def __init__(self, message_bus: MessageBus):
        self.message_bus = message_bus
        self.agent_name = "strategy_recommendation_agent"
        self.current_phase = MatchPhase.POWERPLAY
        self.recent_patterns: List[dict] = []
        self.recent_performances: List[dict] = []
    
    async def handle_performance_event(self, event: Event) -> None:
        """Store performance data for decision-making."""
        self.recent_performances.append(event.payload)
        if len(self.recent_performances) > 20:
            self.recent_performances.pop(0)
    
    async def handle_pattern_event(self, event: Event) -> None:
        """Store pattern data for decision-making."""
        self.recent_patterns.append(event.payload)
        if len(self.recent_patterns) > 20:
            self.recent_patterns.pop(0)
        
        # Generate recommendation when pattern is detected
        await self._generate_recommendation(event)
    
    async def _generate_recommendation(self, trigger_event: Event) -> None:
        """Generate a strategic recommendation based on accumulated data."""
        payload = trigger_event.payload
        recommendation = None
        reasoning = []
        
        if payload.get("pattern_type") == "bowler_specialization":
            bowler = payload["bowler"]
            specialization = payload["specialization"]
            
            # Check if batter is aggressive
            aggressive_batters = [p for p in self.recent_performances
                                 if p.get("batter_stats", {}).get("strike_rate", 0) > 140]
            
            if specialization == "death_bowling_yorkers" and aggressive_batters:
                recommendation = {
                    "action": "adjust_field_placement",
                    "details": f"{bowler} is specializing in death yorkers. Place deep point and fine-leg to restrict boundaries.",
                    "priority": "high"
                }
                reasoning.append(f"Bowler {bowler} detected practicing yorkers")
                reasoning.append(f"Aggressive batters present with 140+ strike rates")
        
        elif payload.get("pattern_type") == "batter_weakness":
            batter = payload["batter"]
            weakness = payload["weakness"]
            
            if weakness == "vulnerable_to_yorkers":
                recommendation = {
                    "action": "bowling_recommendation",
                    "details": f"{batter} is vulnerable to yorkers. Deploy yorker specialist bowlers.",
                    "priority": "high"
                }
                reasoning.append(f"Batter {batter} weakness detected: {weakness}")
                reasoning.append("Suggest yorker-specialist bowler for next over")
        
        if recommendation:
            strategy_event = Event(
                event_type=EventType.STRATEGY_RECOMMENDED,
                source_agent=self.agent_name,
                match_id=trigger_event.match_id,
                payload={
                    "recommendation": recommendation,
                    "reasoning": reasoning,
                    "data_sources": [
                        f"{len(self.recent_performances)} performance updates",
                        f"{len(self.recent_patterns)} pattern detections"
                    ],
                    "timestamp": datetime.utcnow().isoformat()
                }
            )
            await self.message_bus.publish(strategy_event)

# coordinator/__init__.py
import logging
from typing import Callable

class AgentCoordinator:
    """Orchestrates multi-agent cricket analytics workflow."""
    
    def __init__(self):
        self.message_bus = MessageBus()
        self.data_ingestion_agent = DataIngestionAgent(self.message_bus)
        self.performance_agent = PerformanceAnalysisAgent(self.message_bus)
        self.pattern_agent = PatternRecognitionAgent(self.message_bus)
        self.strategy_agent = StrategyRecommendationAgent(self.message_bus)
        
        self.current_match: Optional[Match] = None
        self.current_innings: Optional[Innings] = None
        self.recommendations: List[Event] = []
        
        logging.basicConfig(level=logging.INFO)
        self.logger = logging.getLogger("AgentCoordinator")
    
    async def initialize(self) -> None:
        """Set up agent subscriptions and event handlers."""
        # Subscribe agents to relevant events
        self.message_bus.subscribe(
            EventType.BALL_DELIVERED,
            self.performance_agent.handle_ball_event
        )
        self.message_bus.subscribe(
            EventType.BALL_DELIVERED,
            self.pattern_agent.handle_ball_event
        )
        self.message_bus.subscribe(
            EventType.PERFORMANCE_CALCULATED,
            self.strategy_agent.handle_performance_event
        )
        self.message_bus.subscribe(
            EventType.PATTERN_DETECTED,
            self.strategy_agent.handle_pattern_event
        )
        self.message_bus.subscribe(
            EventType.STRATEGY_RECOMMENDED,
            self._store_recommendation
        )
        
        self.logger.info("✓ All agents initialized and subscribed")
    
    async def _store_recommendation(self, event: Event) -> None:
        """Store strategy recommendations for analysis."""
        self.recommendations.append(event)
        self.logger.info(f"Recommendation stored: {event.payload['recommendation']['action']}")
    
    async def start_match(
        self,
        match: Match,
        innings: Innings
    ) -> None:
        """Initialize a new match in the multi-agent system."""
        self.current_match = match
        self.current_innings = innings
        await self.data_ingestion_agent.initialize_match(match, innings)
        self.logger.info(f"Match started: {match.ground} - {match.team_a} vs {match.team_b}")
    
    async def process_ball(
        self,
        ball_data: dict
    ) -> Ball:
        """Inject a ball delivery into the system.
        
        Triggers a cascade of agent processing:
        1. DataIngestionAgent creates Ball object
        2. Performance agent updates stats and publishes event
        3. Pattern agent analyzes delivery
        4. Strategy agent may generate recommendation
        """
        ball = await self.data_ingestion_agent.ingest_ball(ball_data)
        # Small delay to allow async handlers to complete
        await asyncio.sleep(0.1)
        return ball
    
    async def get_match_dashboard(self) -> dict:
        """Generate a dashboard view of current match state and recommendations."""
        return {
            "match": self.current_match.dict() if self.current_match else None,
            "innings": self.current_innings.dict() if self.current_innings else None,
            "batter_stats": dict(self.performance_agent.player_stats),
            "bowler_stats": dict(self.performance_agent.bowler_stats),
            "detected_patterns": [p for p in self.message_bus._event_history
                                 if p.event_type == EventType.PATTERN_DETECTED],
            "recommendations": [r.payload for r in self.recommendations[-5:]],
            "total_recommendations": len(self.recommendations)
        }
    
    async def shutdown(self) -> None:
        """Gracefully shutdown the coordinator and agents."""
        self.logger.info("Shutting down agent coordinator...")
        self.logger.info(f"Total events processed: {len(self.message_bus._event_history)}")
        self.logger.info(f"Total recommendations generated: {len(self.recommendations)}")

print("✓ Integration and orchestration layer complete")

Step 4 — Testing & Verification

Once the system is fully implemented, run a complete simulation using realistic cricket match data to verify that all agents operate correctly and produce valid recommendations. Execute the coordinator with a sequence of ball deliveries drawn from a live T20 match — for example, Rohit Sharma and Virat Kohli opening for India against Australia — to provide authentic, contextually meaningful input.

Analogy🏏Cricket
🏏 Think of it like cricket: this verification step is like a captain gathering specialist advice at a tense mid-innings India-versus-Australia T20 before committing to a move. You build a Match object frozen at a real situation — say fifteen overs gone, wickets in hand, rate climbing — then initialise three specialist agents and run the Orchestrator, exactly as a captain consults the bowling coach, the analyst, and the senior pro before acting. Each agent returns a decision with a confidence score, just as each advisor offers a recommendation with more or less conviction, and the Orchestrator synthesises them into one unified strategy the way a captain weighs the counsel and settles on a single plan. The payoff is proof that the pipeline works end to end: specialists reason independently, their confidence is quantified, and their advice is fused into a coherent decision — the same discipline that turns scattered dressing-room opinions into a match-winning call.

During the simulation, verify that the performance analysis agent calculates strike rates and economy rates accurately, and confirm that the pattern recognition agent correctly identifies bowler specializations and batter weaknesses. Validate that the strategy recommendation agent synthesizes these inputs into coherent, actionable coaching decisions.

Finally, check that the event bus routes messages correctly and preserves event history throughout the simulation. Examine the generated recommendations to ensure they are logically consistent with the match context and the underlying data, confirming that the system as a whole behaves as a reliable, integrated intelligence platform.

bash
# Run the complete multi-agent cricket system
cd cricket-agentic-system

# Execute the main workflow
python3 main.py

# Expected output:
# ✓ Foundation models and messaging infrastructure established
# ✓ Core agent logic implemented with autonomous decision-making
# ✓ Integration and orchestration layer complete
# Subscriber registered for match_started
# Subscriber registered for ball_delivered
# Subscriber registered for ball_delivered
# Subscriber registered for performance_calculated
# Subscriber registered for pattern_detected
# Subscriber registered for strategy_recommended
# ✓ All agents initialized and subscribed
# Match started: MCG - India vs Australia
# 
# === SIMULATING POWERPLAY OVERS ===
# Over 1, Ball 1: Jasprit Bumrah → Rohit Sharma (120 kmh yorker) - 1 run
# Over 1, Ball 2: Jasprit Bumrah → Virat Kohli (140 kmh fast) - 4 runs
# Over 1, Ball 3: Jasprit Bumrah → Virat Kohli (138 kmh fast) - 0 runs
# Over 1, Ball 4: Jasprit Bumrah → Virat Kohli (125 kmh yorker) - 0 runs
# Over 1, Ball 5: Jasprit Bumrah → Virat Kohli (135 kmh fast) - 2 runs
# Over 1, Ball 6: Jasprit Bumrah → Rohit Sharma (122 kmh yorker) - 1 run
#
# === PERFORMANCE UPDATES ===
# Rohit Sharma - Runs: 2, Balls: 3, Strike Rate: 66.67
# Virat Kohli - Runs: 6, Balls: 3, Strike Rate: 200.00
# Jasprit Bumrah (bowling) - Runs Conceded: 8, Balls: 6, Economy: 8.00
#
# Over 2, Ball 1: Rohit Sharma (fast) → Rohit Sharma - 1 run
# Over 2, Ball 2: Rohit Sharma (fast) → Virat Kohli - 6 runs
# Over 2, Ball 3: Rohit Sharma (yorker) → Virat Kohli - 0 runs
# Over 2, Ball 4: Rohit Sharma (yorker) → Virat Kohli - 0 runs
# Over 2, Ball 5: Rohit Sharma (yorker) → Rohit Sharma - 0 runs
# Over 2, Ball 6: Rohit Sharma (yorker) → Virat Kohli - 1 run
#
# Pattern detected: Rohit Sharma specializing in death bowling yorkers
# Confidence: 83.33%
#
# Recommendation generated: Adjust field placement
# Action: Place deep point and fine-leg to restrict boundaries
# Priority: HIGH
# Reasoning:
#   - Bowler Rohit Sharma detected practicing yorkers
#   - Aggressive batters present with 140+ strike rates
#
# === MATCH DASHBOARD ===
# Match: India vs Australia at MCG (T20)
# Innings: Batting Team India, 16 runs, 0 wickets, 2 overs
# 
# Batter Performance:
#   Rohit Sharma: SR=60.0, Runs=3, Balls=5
#   Virat Kohli: SR=200.0, Runs=7, Balls=3
# 
# Bowler Performance:
#   Jasprit Bumrah: Economy=8.00, Wickets=0, Balls=6
#   Rohit Sharma: Economy=13.00, Wickets=0, Balls=6
#
# Detected Patterns: 1
#   - Bowler Rohit Sharma specializing in death bowling yorkers
#
# Latest Recommendations: 1
#   - Adjust field placement to counter aggressive batters vs yorker specialist
#
# Total Recommendations Generated: 1
# Total Events Processed: 34

Warning: AsyncIO event loop synchronization issues. If you see 'RuntimeError: Event loop is closed', ensure you're using `asyncio.run()` to manage the event loop lifecycle properly. Never create multiple event loops or close the loop while coroutines are still pending. When testing agents individually, use `pytest-asyncio` with the `@pytest.mark.asyncio` decorator to ensure proper loop management. Common mistake: calling async functions without `await` will return a coroutine object that never executes—always verify all agent method calls use `await`.

Extension Challenge: Extend the system with a ReinforcementLearningAgent that learns from recommendation outcomes. Modify the strategy agent to include confidence scores (0-1) for each recommendation based on historical accuracy. After each match, calculate whether the recommended field placement actually prevented runs or enabled a wicket. Use a simple reward signal (+1 for correct recommendation, -1 for incorrect) to update an agent preference model. Over multiple matches, the system should converge toward recommending the most successful strategies for specific player-bowler matchups. This transforms your multi-agent system from reactive intelligence to adaptive intelligence.

  • Multi-agent architecture enables domain decomposition: each agent owns specific expertise (ingestion, performance, patterns, strategy) and operates autonomously without tight coupling or centralized control.
  • Message bus abstraction decouples agent communication from implementation: agents publish typed events and subscribe to event types, allowing new agents to join without modifying existing code.
  • Async/await with asyncio ensures concurrent agent processing: agents run in parallel within a single event loop, enabling real-time performance calculations and pattern detection without blocking.
  • Event sourcing pattern preserves complete audit trail: all inter-agent communication is logged as immutable events, enabling replay, debugging, post-match analysis, and machine learning on agent behavior.
  • Coordinator pattern manages agent lifecycle and synchronization: the coordinator initializes agents, establishes subscriptions, injects domain data, and aggregates outputs into a coherent system view.
  • Cricket domain modeling with Pydantic ensures data validation and type safety: Match, Player, Ball, and Innings models enforce schema contracts, reducing bugs and improving agent interoperability.
Lesson 25 of 35
0% complete