""" MockBlackboard - Simulates shared memory for multi-agent coordination testing. Provides deterministic blackboard operations for testing multi-agent scenarios without DragonflyDB. """ from typing import Dict, List, Any, Optional, Set from dataclasses import dataclass, field from datetime import datetime from enum import Enum import json import threading class BlackboardSection(Enum): """Standard blackboard sections""" PROBLEM = "problem" SOLUTIONS = "solutions" PROGRESS = "progress" CONSENSUS = "consensus" @dataclass class BlackboardEntry: """A single entry in a blackboard section""" key: str value: Any author: str timestamp: datetime version: int = 1 @dataclass class ConsensusVote: """A vote in the consensus section""" agent: str proposal_id: str vote: str # ACCEPT, REJECT, ABSTAIN reasoning: str timestamp: datetime class MockBlackboard: """ Mock Blackboard implementation for multi-agent testing. Simulates: - Section-based shared memory - Read/write operations - Consensus voting - History tracking - Conflict detection """ def __init__(self, task_id: str = "test-task"): self.task_id = task_id self._sections: Dict[str, Dict[str, BlackboardEntry]] = { section.value: {} for section in BlackboardSection } self._votes: List[ConsensusVote] = [] self._history: List[Dict[str, Any]] = [] self._watchers: Dict[str, List[callable]] = {} self._lock = threading.Lock() def write(self, section: str, key: str, value: Any, author: str) -> bool: """ Write to a blackboard section. Args: section: Section name (problem, solutions, progress, consensus) key: Entry key value: Entry value author: Agent ID writing the entry """ with self._lock: if section not in self._sections: self._sections[section] = {} existing = self._sections[section].get(key) version = existing.version + 1 if existing else 1 entry = BlackboardEntry( key=key, value=value, author=author, timestamp=datetime.utcnow(), version=version ) self._sections[section][key] = entry # Record in history self._history.append({ "operation": "write", "section": section, "key": key, "value": value, "author": author, "version": version, "timestamp": datetime.utcnow().isoformat() }) # Notify watchers self._notify_watchers(section, key, value, author) return True def read(self, section: str, key: str = None) -> Optional[Any]: """ Read from a blackboard section. Args: section: Section name key: Optional specific key (if None, returns all entries) """ with self._lock: if section not in self._sections: return None if key: entry = self._sections[section].get(key) return entry.value if entry else None else: return { k: v.value for k, v in self._sections[section].items() } def read_with_metadata(self, section: str, key: str) -> Optional[Dict[str, Any]]: """Read entry with full metadata""" with self._lock: if section not in self._sections: return None entry = self._sections[section].get(key) if not entry: return None return { "key": entry.key, "value": entry.value, "author": entry.author, "version": entry.version, "timestamp": entry.timestamp.isoformat() } def delete(self, section: str, key: str, author: str) -> bool: """Delete an entry from a section""" with self._lock: if section not in self._sections: return False if key not in self._sections[section]: return False del self._sections[section][key] self._history.append({ "operation": "delete", "section": section, "key": key, "author": author, "timestamp": datetime.utcnow().isoformat() }) return True def list_keys(self, section: str) -> List[str]: """List all keys in a section""" with self._lock: if section not in self._sections: return [] return list(self._sections[section].keys()) # === Consensus Operations === def submit_proposal(self, proposal_id: str, proposal: Any, author: str) -> bool: """Submit a proposal for consensus""" return self.write( BlackboardSection.SOLUTIONS.value, proposal_id, {"proposal": proposal, "status": "pending", "votes": []}, author ) def vote(self, proposal_id: str, agent: str, vote: str, reasoning: str = "") -> bool: """ Vote on a proposal. Args: proposal_id: ID of the proposal agent: Voting agent ID vote: ACCEPT, REJECT, or ABSTAIN reasoning: Optional explanation """ with self._lock: if vote not in ["ACCEPT", "REJECT", "ABSTAIN"]: return False consensus_vote = ConsensusVote( agent=agent, proposal_id=proposal_id, vote=vote, reasoning=reasoning, timestamp=datetime.utcnow() ) self._votes.append(consensus_vote) # Update proposal with vote solutions = self._sections.get(BlackboardSection.SOLUTIONS.value, {}) if proposal_id in solutions: proposal_entry = solutions[proposal_id] proposal_data = proposal_entry.value if "votes" not in proposal_data: proposal_data["votes"] = [] proposal_data["votes"].append({ "agent": agent, "vote": vote, "reasoning": reasoning }) self._history.append({ "operation": "vote", "proposal_id": proposal_id, "agent": agent, "vote": vote, "timestamp": datetime.utcnow().isoformat() }) return True def check_consensus(self, proposal_id: str, required_agents: List[str] = None) -> Dict[str, Any]: """ Check consensus status for a proposal. Returns: { "reached": bool, "votes": {"ACCEPT": n, "REJECT": n, "ABSTAIN": n}, "missing_votes": [agent_ids], "result": "ACCEPT"|"REJECT"|"PENDING" } """ with self._lock: proposal_votes = [v for v in self._votes if v.proposal_id == proposal_id] vote_counts = {"ACCEPT": 0, "REJECT": 0, "ABSTAIN": 0} voted_agents = set() for v in proposal_votes: vote_counts[v.vote] += 1 voted_agents.add(v.agent) missing = [] if required_agents: missing = [a for a in required_agents if a not in voted_agents] all_voted = len(missing) == 0 if required_agents else True # Determine result if not all_voted: result = "PENDING" elif vote_counts["ACCEPT"] > vote_counts["REJECT"]: result = "ACCEPT" elif vote_counts["REJECT"] > vote_counts["ACCEPT"]: result = "REJECT" else: result = "TIE" return { "reached": all_voted and result in ["ACCEPT", "REJECT"], "votes": vote_counts, "missing_votes": missing, "result": result } # === Progress Tracking === def update_progress(self, agent: str, phase: str, step: str, details: Dict = None) -> bool: """Update agent progress""" return self.write( BlackboardSection.PROGRESS.value, agent, { "phase": phase, "step": step, "details": details or {}, "updated_at": datetime.utcnow().isoformat() }, agent ) def get_all_progress(self) -> Dict[str, Any]: """Get progress for all agents""" return self.read(BlackboardSection.PROGRESS.value) or {} # === Watchers === def watch(self, section: str, callback: callable): """Watch a section for changes""" with self._lock: if section not in self._watchers: self._watchers[section] = [] self._watchers[section].append(callback) def unwatch(self, section: str, callback: callable = None): """Remove a watcher""" with self._lock: if section not in self._watchers: return if callback: self._watchers[section] = [ cb for cb in self._watchers[section] if cb != callback ] else: del self._watchers[section] def _notify_watchers(self, section: str, key: str, value: Any, author: str): """Notify watchers of a change (called with lock held)""" callbacks = self._watchers.get(section, []).copy() # Release lock before calling callbacks for callback in callbacks: try: callback(section, key, value, author) except Exception: pass # === Test Helpers === def reset(self): """Reset all state for testing""" with self._lock: self._sections = { section.value: {} for section in BlackboardSection } self._votes.clear() self._history.clear() self._watchers.clear() def get_history(self) -> List[Dict[str, Any]]: """Get operation history for assertions""" with self._lock: return self._history.copy() def get_all_state(self) -> Dict[str, Any]: """Get complete state for test assertions""" with self._lock: return { "sections": { section: { k: {"value": v.value, "author": v.author, "version": v.version} for k, v in entries.items() } for section, entries in self._sections.items() }, "votes": [ {"proposal": v.proposal_id, "agent": v.agent, "vote": v.vote} for v in self._votes ] } def inject_state(self, section: str, entries: Dict[str, Any], author: str = "test"): """Inject state for testing""" with self._lock: if section not in self._sections: self._sections[section] = {} for key, value in entries.items(): self._sections[section][key] = BlackboardEntry( key=key, value=value, author=author, timestamp=datetime.utcnow() ) def simulate_conflict(self, section: str, key: str, agents: List[str], values: List[Any]) -> List[Dict[str, Any]]: """ Simulate a write conflict for testing. Multiple agents write to the same key in rapid succession. Returns the sequence of writes. """ writes = [] for agent, value in zip(agents, values): self.write(section, key, value, agent) entry = self._sections[section][key] writes.append({ "agent": agent, "value": value, "version": entry.version }) return writes