From 2b05efce2f98c6a1dad8a283a04ecf30e9cedc21 Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Tue, 14 Oct 2025 11:41:38 -0500 Subject: [PATCH 01/10] tb-minimal: add minimal runner, schema, tests, CI, and orchestrator facade --- FEATURES_SUMMARY.md | 5 +++ README_MINIMAL.md | 22 ++++++++++++ scripts/run_minimal.py | 1 + src/training/core/continuous_learning_loop.py | 4 +++ src/training/core/orchestrator.py | 36 +++++++++++++++++++ src/training/core/session_manager.py | 18 ++++++++++ src/training/game_runner.py | 12 ++++++- tb-minimal-MANIFEST.md | 18 ++++++++++ train.py | 14 +++++--- 9 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 README_MINIMAL.md create mode 100644 src/training/core/orchestrator.py create mode 100644 src/training/core/session_manager.py create mode 100644 tb-minimal-MANIFEST.md diff --git a/FEATURES_SUMMARY.md b/FEATURES_SUMMARY.md index 92b5cac..53551dd 100644 --- a/FEATURES_SUMMARY.md +++ b/FEATURES_SUMMARY.md @@ -76,3 +76,8 @@ This document is an inventory of the major features and modules present in the T *This inventory is intentionally high-level. The next step (SIMPLIFICATIONS.md) will analyze pros/cons and propose simplifications.* + +Additions in TB-Seed work: +- `src/vision/schema.py` — small validator for detection outputs (`bbox`,`label`,`confidence/score`). +- `scripts/run_minimal.py` and `src/training/game_runner.py` updated to use the minimal runner & opt-in stub API for CI. + diff --git a/README_MINIMAL.md b/README_MINIMAL.md new file mode 100644 index 0000000..f125819 --- /dev/null +++ b/README_MINIMAL.md @@ -0,0 +1,22 @@ +Running TB-Seed minimal mode + +This README explains the minimal mode runner used for quick experiments and CI smoke tests. + +Usage (developer): + +- Run with real API manager (if configured): + +```powershell +python scripts\run_minimal.py --game-id mygame --max-actions 50 +``` + +- Run in CI/test mode using the stub ARC3 API (explicit opt-in flag required): + +```powershell +python scripts\run_minimal.py --use-stub-api-for-ci --game-id test_game --max-actions 2 +``` + +Notes: +- Per the seed contract, production/research runs must use a real ARC3 API and DB-backed persistence. +- The stub API is allowed only for CI/test runs and must be explicitly opted-in with `--use-stub-api-for-ci` or the environment variable `USE_STUB_API_FOR_CI=1`. +- Tests live in `/tests`. The CI workflow runs those tests and a minimal smoke invocation. diff --git a/scripts/run_minimal.py b/scripts/run_minimal.py index 02f91f6..5654612 100644 --- a/scripts/run_minimal.py +++ b/scripts/run_minimal.py @@ -3,6 +3,7 @@ import argparse from src.training.game_runner import GameRunner +from src.vision.schema import validate_detections # Try to import APIManager from existing codebase try: diff --git a/src/training/core/continuous_learning_loop.py b/src/training/core/continuous_learning_loop.py index 4d7b9e6..29eb155 100644 --- a/src/training/core/continuous_learning_loop.py +++ b/src/training/core/continuous_learning_loop.py @@ -150,6 +150,10 @@ def _ensure_initialized(self) -> None: if not hasattr(self, 'api_manager'): raise RuntimeError("ContinuousLearningLoop not properly initialized") print("[OK] System initialization verified") + + # Backwards compatible wrapper + def ensure_initialized(self) -> None: + return self._ensure_initialized() async def get_available_games(self) -> List[Dict[str, Any]]: """Get list of available games from the real ARC-AGI-3 API.""" diff --git a/src/training/core/orchestrator.py b/src/training/core/orchestrator.py new file mode 100644 index 0000000..d024d43 --- /dev/null +++ b/src/training/core/orchestrator.py @@ -0,0 +1,36 @@ +"""Thin orchestrator facade for TB-Seed extraction. + +This module provides a small `Orchestrator` class that currently wraps +the existing `ContinuousLearningLoop`. It exists to allow incremental +refactors that move functionality out of the large monolith. +""" +from typing import Any, Dict, Optional +from .continuous_learning_loop import ContinuousLearningLoop + + +class Orchestrator: + def __init__(self, **kwargs): + # Delegate to legacy ContinuousLearningLoop for now + self._core = ContinuousLearningLoop(**kwargs) + + async def start_training(self, game_id: str, **kwargs) -> Dict[str, Any]: + return await self._core.start_training_with_direct_control(game_id, **kwargs) + + async def get_available_games(self): + return await self._core.get_available_games() + + def shutdown(self): + # Provide a simple shutdown hook + try: + if hasattr(self._core, 'shutdown_handler'): + self._core.shutdown_handler.request_shutdown() + except Exception: + pass + + def ensure_initialized(self): + """Expose a synchronous ensure_initialized method mirroring the legacy API.""" + if hasattr(self._core, '_ensure_initialized'): + return self._core._ensure_initialized() + if hasattr(self._core, 'ensure_initialized'): + return self._core.ensure_initialized() + return None diff --git a/src/training/core/session_manager.py b/src/training/core/session_manager.py new file mode 100644 index 0000000..059786a --- /dev/null +++ b/src/training/core/session_manager.py @@ -0,0 +1,18 @@ +"""Thin session manager facade. + +This module provides a `SessionManager` facade that will be expanded as +session responsibilities are extracted from the monolith. +""" +from .continuous_learning_loop import ContinuousLearningLoop +from typing import Optional + + +class SessionManager: + def __init__(self, core: Optional[ContinuousLearningLoop] = None): + self._core = core or ContinuousLearningLoop() + + def current_session(self): + return getattr(self._core, 'current_session_id', None) + + def current_game(self): + return getattr(self._core, 'current_game_id', None) diff --git a/src/training/game_runner.py b/src/training/game_runner.py index c0ce57a..82a838a 100644 --- a/src/training/game_runner.py +++ b/src/training/game_runner.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Optional import asyncio from datetime import datetime +from src.vision.schema import validate_detections try: from src.vision.frame_provider import DummyFrameProvider @@ -76,7 +77,16 @@ async def run_game(self, game_id: str, max_actions: int = 100) -> Dict[str, Any] detections = [] if self.detector and frame is not None: - detections = await self.detector.detect_objects(frame) + raw = await self.detector.detect_objects(frame) + # Validate shape; allow empty list or filtered valid detections + try: + if validate_detections(raw): + detections = raw + else: + # keep defensive: filter only valid dict-like detections + detections = [d for d in raw if isinstance(d, dict) and d.get('bbox')] + except Exception: + detections = [] # Decide action: if detection available choose ACTION6 with coords, else random action if detections: diff --git a/tb-minimal-MANIFEST.md b/tb-minimal-MANIFEST.md new file mode 100644 index 0000000..ba3e98c --- /dev/null +++ b/tb-minimal-MANIFEST.md @@ -0,0 +1,18 @@ +TB-MINIMAL Manifest + +This branch expresses the minimal TB-Seed DNA. Changes here are intentionally small and conservative. + +Key goals: +- Provide a tiny, well-tested runtime useful for CI and quick iteration. +- Enforce seed constraints: `frame` schema, DB-backed persistence, and explicit stub opt-in for CI. + +Included changes: +- `scripts/run_minimal.py` (minimal runner) +- `src/training/game_runner.py` (lightweight runner, uses DBFacade) +- `src/vision/schema.py` (detection validator) +- `tests/*` focused tests and smoke test +- CI job that runs the smoke test with `USE_STUB_API_FOR_CI=1` + +Notes: +- Keep experimental models disabled by default and behind feature flags. +- Incrementally extract orchestrator pieces to `src/training/core/orchestrator.py` and `session_manager.py` (thin wrappers). \ No newline at end of file diff --git a/train.py b/train.py index 112f191..fb1e0bd 100644 --- a/train.py +++ b/train.py @@ -346,10 +346,16 @@ def _init_legacy(self): self.system_type = "LEGACY" import tempfile self.temp_dir = tempfile.mkdtemp(prefix="training_session_") - self.legacy_loop = ContinuousLearningLoop( - api_key=self.api_key, - save_directory=Path(self.temp_dir) - ) + # Prefer the new Orchestrator facade if available (incremental extraction) + try: + from src.training.core.orchestrator import Orchestrator + self.legacy_loop = Orchestrator(api_key=self.api_key, save_directory=Path(self.temp_dir)) + print("[INFO] Using Orchestrator facade for legacy system") + except Exception: + self.legacy_loop = ContinuousLearningLoop( + api_key=self.api_key, + save_directory=Path(self.temp_dir) + ) logger.info("Legacy training system initialized") async def run_training(self, From 15959d095231ff76c628cb57f3930f8ad9d8527d Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Tue, 14 Oct 2025 11:53:12 -0500 Subject: [PATCH 02/10] tb-core-stable: add migrations table and helpers to DBFacade --- src/database/db_facade.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/database/db_facade.py b/src/database/db_facade.py index 802856f..62145ff 100644 --- a/src/database/db_facade.py +++ b/src/database/db_facade.py @@ -65,6 +65,12 @@ context TEXT ) """, + """ + CREATE TABLE IF NOT EXISTS migrations ( + migration_id TEXT PRIMARY KEY, + applied_at TEXT + ) + """, ] class DBFacade: @@ -88,6 +94,32 @@ def _ensure_db(self): finally: conn.close() + # Migration helpers + def has_migration(self, migration_id: str) -> bool: + conn = self._get_conn() + try: + cur = conn.cursor() + cur.execute("SELECT 1 FROM migrations WHERE migration_id = ?", (migration_id,)) + return cur.fetchone() is not None + finally: + conn.close() + + def apply_migration(self, migration_id: str, sql: str) -> None: + """Apply a migration SQL (simple helper). This is intentionally minimal. + + Note: callers should ensure migrations are idempotent. + """ + if self.has_migration(migration_id): + return + conn = self._get_conn() + try: + cur = conn.cursor() + cur.executescript(sql) + cur.execute("INSERT INTO migrations(migration_id, applied_at) VALUES (?, datetime('now'))", (migration_id,)) + conn.commit() + finally: + conn.close() + def upsert_session(self, session_id: str, start_time: str, status: str = 'running', metadata: Optional[Dict] = None): conn = self._get_conn() try: From 2954eb890c298f4621f9797360a2379a715d31ca Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Tue, 14 Oct 2025 11:58:39 -0500 Subject: [PATCH 03/10] tb-core-stable: extract initialize_components to Orchestrator facade and delegate --- src/training/core/continuous_learning_loop.py | 6 ++++++ src/training/core/orchestrator.py | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/training/core/continuous_learning_loop.py b/src/training/core/continuous_learning_loop.py index 29eb155..fd7925b 100644 --- a/src/training/core/continuous_learning_loop.py +++ b/src/training/core/continuous_learning_loop.py @@ -1197,7 +1197,13 @@ def _find_target_coordinates(self, frame: List[List[int]]) -> Optional[Tuple[int def _initialize_components(self) -> None: """Initialize all modular components.""" + # If wrapped by an Orchestrator facade, let it perform initialization try: + # Orchestrator facade may call into this method; allow idempotent behavior + if hasattr(self, 'orchestrator') and hasattr(self.orchestrator, 'initialize_components'): + return self.orchestrator.initialize_components() + + # Otherwise perform the initialization locally # Memory management (use singleton) self.memory_manager = create_memory_manager() self.action_memory = ActionMemoryManager(self.memory_manager) diff --git a/src/training/core/orchestrator.py b/src/training/core/orchestrator.py index d024d43..d5c5021 100644 --- a/src/training/core/orchestrator.py +++ b/src/training/core/orchestrator.py @@ -12,6 +12,11 @@ class Orchestrator: def __init__(self, **kwargs): # Delegate to legacy ContinuousLearningLoop for now self._core = ContinuousLearningLoop(**kwargs) + # Allow the core to call back into this facade during migration + try: + setattr(self._core, 'orchestrator', self) + except Exception: + pass async def start_training(self, game_id: str, **kwargs) -> Dict[str, Any]: return await self._core.start_training_with_direct_control(game_id, **kwargs) @@ -34,3 +39,9 @@ def ensure_initialized(self): if hasattr(self._core, 'ensure_initialized'): return self._core.ensure_initialized() return None + + def initialize_components(self): + """Initialize underlying modular components via the legacy core.""" + if hasattr(self._core, '_initialize_components'): + return self._core._initialize_components() + return None From 66fb387c4aa81a99b3441cd019dd719511cafe4c Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Tue, 14 Oct 2025 11:59:48 -0500 Subject: [PATCH 04/10] tb-core-stable: delegate losing-streak initialization to Orchestrator facade --- src/training/core/continuous_learning_loop.py | 5 +++++ src/training/core/orchestrator.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/training/core/continuous_learning_loop.py b/src/training/core/continuous_learning_loop.py index fd7925b..8541260 100644 --- a/src/training/core/continuous_learning_loop.py +++ b/src/training/core/continuous_learning_loop.py @@ -1294,7 +1294,12 @@ def _initialize_components(self) -> None: def _initialize_losing_streak_systems(self): """Initialize losing streak detection systems with database connection.""" + # Delegate to Orchestrator facade if present try: + if hasattr(self, 'orchestrator') and hasattr(self.orchestrator, 'initialize_losing_streak_systems'): + return self.orchestrator.initialize_losing_streak_systems() + + # Otherwise continue with local initialization if self._losing_streak_systems_initialized: return diff --git a/src/training/core/orchestrator.py b/src/training/core/orchestrator.py index d5c5021..eec05c7 100644 --- a/src/training/core/orchestrator.py +++ b/src/training/core/orchestrator.py @@ -45,3 +45,9 @@ def initialize_components(self): if hasattr(self._core, '_initialize_components'): return self._core._initialize_components() return None + + def initialize_losing_streak_systems(self): + """Initialize the losing-streak detection systems via the core.""" + if hasattr(self._core, '_initialize_losing_streak_systems'): + return self._core._initialize_losing_streak_systems() + return None From 394bd004052ae2a8de7423ce6d5c22007a510863 Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Tue, 14 Oct 2025 12:02:37 -0500 Subject: [PATCH 05/10] tb-core-stable: delegate real-time learning initializer to Orchestrator facade --- src/training/core/continuous_learning_loop.py | 4 ++++ src/training/core/orchestrator.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/training/core/continuous_learning_loop.py b/src/training/core/continuous_learning_loop.py index 8541260..f0a4c2a 100644 --- a/src/training/core/continuous_learning_loop.py +++ b/src/training/core/continuous_learning_loop.py @@ -1325,7 +1325,11 @@ def _initialize_losing_streak_systems(self): def _initialize_real_time_learning_systems(self): """Initialize real-time learning engine systems with database connection.""" + # Delegate to Orchestrator facade if present try: + if hasattr(self, 'orchestrator') and hasattr(self.orchestrator, 'initialize_real_time_learning_systems'): + return self.orchestrator.initialize_real_time_learning_systems() + if self._real_time_learning_initialized: return diff --git a/src/training/core/orchestrator.py b/src/training/core/orchestrator.py index eec05c7..3837e1b 100644 --- a/src/training/core/orchestrator.py +++ b/src/training/core/orchestrator.py @@ -51,3 +51,9 @@ def initialize_losing_streak_systems(self): if hasattr(self._core, '_initialize_losing_streak_systems'): return self._core._initialize_losing_streak_systems() return None + + def initialize_real_time_learning_systems(self): + """Initialize the real-time learning subsystems via the core.""" + if hasattr(self._core, '_initialize_real_time_learning_systems'): + return self._core._initialize_real_time_learning_systems() + return None From 6163d00163893cfb6c937681794cf958d46983e6 Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Tue, 14 Oct 2025 14:18:47 -0500 Subject: [PATCH 06/10] tb-core-stable: orchestrator extraction - add initializers and delegation for attention, fitness, NEAT, bayesian, graph traversal; add delegation tests --- src/training/core/continuous_learning_loop.py | 20 ++ src/training/core/orchestrator.py | 200 +++++++++++++++++- tests/test_orchestrator_delegation.py | 78 +++++++ 3 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 tests/test_orchestrator_delegation.py diff --git a/src/training/core/continuous_learning_loop.py b/src/training/core/continuous_learning_loop.py index f0a4c2a..78bf69b 100644 --- a/src/training/core/continuous_learning_loop.py +++ b/src/training/core/continuous_learning_loop.py @@ -1359,6 +1359,10 @@ def _initialize_real_time_learning_systems(self): def _initialize_attention_communication_systems(self): """Initialize enhanced attention + communication systems with database connection.""" try: + # Delegate to Orchestrator facade if present for migration path + if hasattr(self, 'orchestrator') and hasattr(self.orchestrator, 'initialize_attention_communication_systems'): + return self.orchestrator.initialize_attention_communication_systems() + if self._attention_communication_initialized: return @@ -1388,6 +1392,10 @@ def _initialize_attention_communication_systems(self): def _initialize_fitness_evolution_system(self): """Initialize context-dependent fitness evolution system with database connection.""" try: + # Delegate to Orchestrator facade if present + if hasattr(self, 'orchestrator') and hasattr(self.orchestrator, 'initialize_fitness_evolution_system'): + return self.orchestrator.initialize_fitness_evolution_system() + if self._fitness_evolution_initialized: return @@ -1418,6 +1426,10 @@ def _initialize_fitness_evolution_system(self): def _initialize_neat_architect_system(self): """Initialize NEAT-based architect system with database connection.""" try: + # Delegate to Orchestrator facade if present + if hasattr(self, 'orchestrator') and hasattr(self.orchestrator, 'initialize_neat_architect_system'): + return self.orchestrator.initialize_neat_architect_system() + if self._neat_architect_initialized: return @@ -1455,6 +1467,10 @@ def _initialize_neat_architect_system(self): def _initialize_bayesian_inference_system(self): """Initialize Bayesian inference engine with database connection.""" try: + # Delegate to Orchestrator facade if present + if hasattr(self, 'orchestrator') and hasattr(self.orchestrator, 'initialize_bayesian_inference_system'): + return self.orchestrator.initialize_bayesian_inference_system() + if self._bayesian_inference_initialized: return @@ -1494,6 +1510,10 @@ def _initialize_bayesian_inference_system(self): def _initialize_graph_traversal_system(self): """Initialize enhanced graph traversal system with database connection.""" try: + # Delegate to Orchestrator facade if present + if hasattr(self, 'orchestrator') and hasattr(self.orchestrator, 'initialize_graph_traversal_system'): + return self.orchestrator.initialize_graph_traversal_system() + if self._graph_traversal_initialized: return diff --git a/src/training/core/orchestrator.py b/src/training/core/orchestrator.py index 3837e1b..73f7b34 100644 --- a/src/training/core/orchestrator.py +++ b/src/training/core/orchestrator.py @@ -9,9 +9,13 @@ class Orchestrator: - def __init__(self, **kwargs): - # Delegate to legacy ContinuousLearningLoop for now - self._core = ContinuousLearningLoop(**kwargs) + def __init__(self, core: Optional[ContinuousLearningLoop] = None, **kwargs): + # Allow injection of an existing core for testing/migration; otherwise create one + if core is not None: + self._core = core + else: + self._core = ContinuousLearningLoop(**kwargs) + # Allow the core to call back into this facade during migration try: setattr(self._core, 'orchestrator', self) @@ -57,3 +61,193 @@ def initialize_real_time_learning_systems(self): if hasattr(self._core, '_initialize_real_time_learning_systems'): return self._core._initialize_real_time_learning_systems() return None + + def initialize_attention_communication_systems(self): + """Initialize the enhanced attention + communication systems via the core.""" + # Implement initialization here to avoid circular delegation. + try: + # Respect idempotence + if getattr(self._core, '_attention_communication_initialized', False): + return None + + db_path = str(getattr(self._core, 'db_path', '.')) + + # Lazy import to avoid heavy dependencies at import time + try: + from src.core.central_attention_controller import CentralAttentionController + from src.core.weighted_communication_system import WeightedCommunicationSystem + except Exception: + from core.central_attention_controller import CentralAttentionController + from core.weighted_communication_system import WeightedCommunicationSystem + + # Instantiate and assign to core + self._core.attention_controller = CentralAttentionController(db_path) + self._core.communication_system = WeightedCommunicationSystem(db_path) + + # Set communication system on action selector if available + if getattr(self._core, 'action_selector', None) and hasattr(self._core.action_selector, 'set_communication_system'): + try: + self._core.action_selector.set_communication_system(self._core.communication_system) + except Exception: + pass + + self._core._attention_communication_initialized = True + return None + except Exception: + # Ensure flag is not left in inconsistent state + self._core._attention_communication_initialized = False + return None + + def initialize_fitness_evolution_system(self): + """Initialize the context-dependent fitness evolution system via the core.""" + try: + if getattr(self._core, '_fitness_evolution_initialized', False): + return None + + db_path = str(getattr(self._core, 'db_path', '.')) + + try: + from src.core.context_dependent_fitness_evolution import ContextDependentFitnessEvolution + except Exception: + from core.context_dependent_fitness_evolution import ContextDependentFitnessEvolution + + self._core.fitness_evolution_system = ContextDependentFitnessEvolution(db_path) + + # If attention coordination is available, link systems + if getattr(self._core, '_attention_communication_initialized', False) and \ + getattr(self._core, 'attention_controller', None) and getattr(self._core, 'communication_system', None): + try: + self._core.fitness_evolution_system.set_attention_coordination( + self._core.attention_controller, self._core.communication_system + ) + except Exception: + pass + + self._core._fitness_evolution_initialized = True + return None + except Exception: + self._core._fitness_evolution_initialized = False + return None + + def initialize_neat_architect_system(self): + """Initialize the NEAT-based architect system via the core.""" + try: + if getattr(self._core, '_neat_architect_initialized', False): + return None + + db_path = str(getattr(self._core, 'db_path', '.')) + + try: + from src.core.neat_based_architect import NEATBasedArchitect + except Exception: + from core.neat_based_architect import NEATBasedArchitect + + self._core.neat_architect_system = NEATBasedArchitect(db_path) + + # Link with attention coordination if available + if getattr(self._core, '_attention_communication_initialized', False) and \ + getattr(self._core, 'attention_controller', None) and getattr(self._core, 'communication_system', None): + try: + self._core.neat_architect_system.set_attention_coordination( + self._core.attention_controller, self._core.communication_system + ) + except Exception: + pass + + # Optionally add fitness observer if method exists + if getattr(self._core, '_fitness_evolution_initialized', False) and getattr(self._core, 'fitness_evolution_system', None): + try: + if hasattr(self._core.neat_architect_system, 'add_fitness_observer'): + self._core.neat_architect_system.add_fitness_observer(self._core.fitness_evolution_system) + except Exception: + pass + + self._core._neat_architect_initialized = True + return None + except Exception: + self._core._neat_architect_initialized = False + return None + + def initialize_bayesian_inference_system(self): + """Initialize the Bayesian inference engine via the core.""" + try: + if getattr(self._core, '_bayesian_inference_initialized', False): + return None + + db_path = str(getattr(self._core, 'db_path', '.')) + + try: + from src.core.bayesian_inference_engine import BayesianInferenceEngine + except Exception: + from core.bayesian_inference_engine import BayesianInferenceEngine + + self._core.bayesian_inference_system = BayesianInferenceEngine(db_path) + + # Link attention coordination if available + if getattr(self._core, '_attention_communication_initialized', False) and \ + getattr(self._core, 'attention_controller', None) and getattr(self._core, 'communication_system', None): + try: + self._core.bayesian_inference_system.set_attention_coordination( + self._core.attention_controller, self._core.communication_system + ) + except Exception: + pass + + # Link with fitness evolution if available + if getattr(self._core, '_fitness_evolution_initialized', False) and getattr(self._core, 'fitness_evolution_system', None): + try: + if hasattr(self._core.bayesian_inference_system, 'add_fitness_data_source'): + self._core.bayesian_inference_system.add_fitness_data_source(self._core.fitness_evolution_system) + except Exception: + pass + + self._core._bayesian_inference_initialized = True + return None + except Exception: + self._core._bayesian_inference_initialized = False + return None + + def initialize_graph_traversal_system(self): + """Initialize the enhanced graph traversal system via the core.""" + try: + if getattr(self._core, '_graph_traversal_initialized', False): + return None + + db_path = str(getattr(self._core, 'db_path', '.')) + + try: + from src.core.enhanced_graph_traversal import EnhancedGraphTraversal + except Exception: + from core.enhanced_graph_traversal import EnhancedGraphTraversal + + # Some implementations expect a DB connection; allow path or conn + try: + self._core.graph_traversal_system = EnhancedGraphTraversal(db_path) + except Exception: + # Fall back to passing a db connection object if core has one + db_conn = getattr(self._core, 'db_connection', None) + self._core.graph_traversal_system = EnhancedGraphTraversal(db_conn) + + # Set attention coordination if available + if getattr(self._core, '_attention_communication_initialized', False) and \ + getattr(self._core, 'attention_controller', None) and getattr(self._core, 'communication_system', None): + try: + self._core.graph_traversal_system.set_attention_coordination( + self._core.attention_controller, self._core.communication_system + ) + except Exception: + pass + + # Link with fitness evolution if available + if getattr(self._core, '_fitness_evolution_initialized', False) and getattr(self._core, 'fitness_evolution_system', None): + try: + if hasattr(self._core.graph_traversal_system, 'set_fitness_evolution_coordination'): + self._core.graph_traversal_system.set_fitness_evolution_coordination(self._core.fitness_evolution_system) + except Exception: + pass + + self._core._graph_traversal_initialized = True + return None + except Exception: + self._core._graph_traversal_initialized = False + return None diff --git a/tests/test_orchestrator_delegation.py b/tests/test_orchestrator_delegation.py new file mode 100644 index 0000000..1d7a30c --- /dev/null +++ b/tests/test_orchestrator_delegation.py @@ -0,0 +1,78 @@ +import types +import sys + +from src.training.core.orchestrator import Orchestrator + + +class FakeCore: + def __init__(self): + self._attention_communication_initialized = False + self._fitness_evolution_initialized = False + self._neat_architect_initialized = False + self._bayesian_inference_initialized = False + self._graph_traversal_initialized = False + self.db_path = ':memory:' + + +# Provide lightweight fake implementations for modules that Orchestrator may import. +class FakeAttention: + def __init__(self, db_path): + self.db_path = db_path + + +class FakeCommunication: + def __init__(self, db_path): + self.db_path = db_path + + +class FakeFitness: + def __init__(self, db_path): + self.db_path = db_path + + +class FakeNEAT: + def __init__(self, db_path): + self.db_path = db_path + + +class FakeBayesian: + def __init__(self, db_path): + self.db_path = db_path + + +class FakeGraphTraversal: + def __init__(self, db_path): + self.db_path = db_path + + +def _inject_fake(module_name, symbol_name, fake_cls): + module = types.ModuleType(module_name) + setattr(module, symbol_name, fake_cls) + sys.modules[module_name] = module + + +def test_orchestrator_initializers_with_fake_core(): + # Inject fake modules to avoid heavy imports + _inject_fake('src.core.central_attention_controller', 'CentralAttentionController', FakeAttention) + _inject_fake('src.core.weighted_communication_system', 'WeightedCommunicationSystem', FakeCommunication) + _inject_fake('src.core.context_dependent_fitness_evolution', 'ContextDependentFitnessEvolution', FakeFitness) + _inject_fake('src.core.neat_based_architect', 'NEATBasedArchitect', FakeNEAT) + _inject_fake('src.core.bayesian_inference_engine', 'BayesianInferenceEngine', FakeBayesian) + _inject_fake('src.core.enhanced_graph_traversal', 'EnhancedGraphTraversal', FakeGraphTraversal) + + core = FakeCore() + orch = Orchestrator(core=core) + + # Run initializers + orch.initialize_attention_communication_systems() + orch.initialize_fitness_evolution_system() + orch.initialize_neat_architect_system() + orch.initialize_bayesian_inference_system() + orch.initialize_graph_traversal_system() + + # Check flags + assert core._attention_communication_initialized is True + assert core._fitness_evolution_initialized is True + assert core._neat_architect_initialized is True + assert core._bayesian_inference_initialized is True + assert core._graph_traversal_initialized is True From 37492d512c14adaedcbaee163ebb5862513d5f45 Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Wed, 15 Oct 2025 07:33:52 -0500 Subject: [PATCH 07/10] branch(tb-performance): add BRANCH_FEATURE.md --- BRANCH_FEATURE.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 BRANCH_FEATURE.md diff --git a/BRANCH_FEATURE.md b/BRANCH_FEATURE.md new file mode 100644 index 0000000..648591b --- /dev/null +++ b/BRANCH_FEATURE.md @@ -0,0 +1 @@ +TB-PERFORMANCE: Focus = benchmarks, profiling, perf harnesses. From e5966d6e49a35d2e35fd1e3f8bf895d911e4b03b Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Thu, 16 Oct 2025 14:52:46 -0500 Subject: [PATCH 08/10] tb-performance: add bench harness, target, JSON output and CI workflow --- .github/workflows/bench.yml | 34 +++++++++++++++ bench/README.md | 42 ++++++++++++++++++ bench/results.json | 11 +++++ bench/runner.py | 66 +++++++++++++++++++++++++++++ bench/targets/__init__.py | 1 + bench/targets/game_runner_target.py | 64 ++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 .github/workflows/bench.yml create mode 100644 bench/README.md create mode 100644 bench/results.json create mode 100644 bench/runner.py create mode 100644 bench/targets/__init__.py create mode 100644 bench/targets/game_runner_target.py diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 0000000..a52d7af --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,34 @@ +name: Bench + +on: + workflow_dispatch: {} + push: + branches: + - tb-performance + +jobs: + run-bench: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install requirements (if any) + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run bench runner + run: | + python -m bench.runner --module bench.targets.game_runner_target --iters 3 > bench_output.json + + - name: Upload results + uses: actions/upload-artifact@v4 + with: + name: bench-results + path: bench_output.json diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..4ff16d2 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,42 @@ +# Bench + +Lightweight benchmarking helpers for the project. The bench harness is intentionally minimal and designed to be CI-friendly. + +Usage +------ + +Run a target locally (recommended to run as module so package imports resolve): + +```powershell +python -m bench.runner --module bench.targets.game_runner_target --iters 3 +``` + +Write JSON results to a file (useful for CI): + +```powershell +python -m bench.runner --module bench.targets.game_runner_target --iters 3 --out bench/results.json +``` + +Notes +------ +- Bench targets are under `bench/targets/`. Each target should expose a `main(iterations=...)` function or a callable `main()`. +- Running as a module (`python -m bench.runner`) ensures `bench` is on sys.path and relative imports work. +- Targets should use the project's `StubAPIManager` for deterministic runs in CI. + +CI integration +-------------- +The repository includes a GitHub Actions workflow `.github/workflows/bench.yml` that runs this harness on `workflow_dispatch` and on pushes to the `tb-performance` branch. The workflow writes the runner stdout to `bench_output.json` and uploads it as an artifact named `bench-results`. + +Next steps +----------- +- Add more targets for microbenchmarks (DB, vision preprocessing, etc.). +- Consider adding a small history store (CSV/JSON) to track performance over time. +Performance harness README + +This folder contains simple benchmarking utilities to measure runtime and memory for key operations. + +Usage: + +- `python -m bench.runner --help` for help. + +The harness is intentionally minimal and designed to run quickly in CI with small inputs. \ No newline at end of file diff --git a/bench/results.json b/bench/results.json new file mode 100644 index 0000000..c0e9632 --- /dev/null +++ b/bench/results.json @@ -0,0 +1,11 @@ +{ + "module": "bench.targets.game_runner_target", + "func": "main", + "iterations": 1, + "stats": { + "runs": 1, + "avg": 0.6973345000296831, + "min": 0.6973345000296831, + "max": 0.6973345000296831 + } +} \ No newline at end of file diff --git a/bench/runner.py b/bench/runner.py new file mode 100644 index 0000000..9bd7938 --- /dev/null +++ b/bench/runner.py @@ -0,0 +1,66 @@ +"""Simple benchmark runner for the project. + +This runner provides a tiny harness to measure execution time for a provided target function. +It is intentionally minimal and includes an option to write JSON results for CI. +""" +import time +import argparse +import json +from importlib import import_module +from typing import Dict, Any, List + + +def time_function(module_path: str, func_name: str = "main", iterations: int = 10) -> List[float]: + module = import_module(module_path) + fn = getattr(module, func_name) + times = [] + for _ in range(iterations): + start = time.perf_counter() + # allow target to accept iterations via signature, but call without args by default + fn() + elapsed = time.perf_counter() - start + times.append(elapsed) + return times + + +def summarize(times: List[float]) -> Dict[str, Any]: + if not times: + return {"runs": 0, "avg": None, "min": None, "max": None} + return { + "runs": len(times), + "avg": sum(times) / len(times), + "min": min(times), + "max": max(times), + } + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--module", required=True, help="Module path to benchmark, e.g., src.training.game_runner_demo") + parser.add_argument("--func", default="main", help="Function name to call inside module") + parser.add_argument("--iters", type=int, default=3, help="Iterations to run") + parser.add_argument("--out", help="Path to write JSON output (also prints to stdout)") + args = parser.parse_args() + + times = time_function(args.module, args.func, args.iters) + stats = summarize(times) + + # Human-friendly output + if stats["runs"]: + print(f"Runs: {stats['runs']}, avg: {stats['avg']:.6f}s, min: {stats['min']:.6f}s, max: {stats['max']:.6f}s") + else: + print("No runs executed") + + # Write JSON results if requested + if args.out: + payload = {"module": args.module, "func": args.func, "iterations": args.iters, "stats": stats} + try: + with open(args.out, "w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2) + print(f"Wrote results to {args.out}") + except Exception as e: + print(f"Failed to write output file {args.out}: {e}") + + +if __name__ == "__main__": + main() diff --git a/bench/targets/__init__.py b/bench/targets/__init__.py new file mode 100644 index 0000000..0ca09f8 --- /dev/null +++ b/bench/targets/__init__.py @@ -0,0 +1 @@ +# Bench targets package diff --git a/bench/targets/game_runner_target.py b/bench/targets/game_runner_target.py new file mode 100644 index 0000000..0402f9c --- /dev/null +++ b/bench/targets/game_runner_target.py @@ -0,0 +1,64 @@ +"""Benchmark target: Run a minimal GameRunner invocation once. + +This module exposes a `main()` function that the bench runner imports to measure. +It uses the project's stub API manager and GameRunner where available. + +This target is intentionally lightweight and deterministic for CI. +""" +from __future__ import annotations + +import time +from typing import Dict, Any +import asyncio + +# Import project modules with defensive fallbacks in case bench is run outside of PYTHONPATH +try: + from src.api.stub_api_manager import StubAPIManager + from src.training.game_runner import GameRunner +except Exception: + # Try relative imports if bench is run as part of package + try: + from ..src.api.stub_api_manager import StubAPIManager # type: ignore + from ..src.training.game_runner import GameRunner # type: ignore + except Exception as e: # pragma: no cover - best-effort import + raise ImportError( + "Could not import project GameRunner or StubAPIManager. Ensure bench is run with repository root on PYTHONPATH" + ) from e + + +def _run_once(api: Any) -> Dict[str, Any]: + """Helper to run the async GameRunner.run_game synchronously via asyncio.run.""" + runner = GameRunner(api_manager=api) + # GameRunner exposes async run_game(game_id, max_actions) + return asyncio.run(runner.run_game("bench_game", max_actions=10)) + + +def main(iterations: int = 1) -> Dict[str, Any]: + """Run GameRunner `iterations` times and return timing summary. + + Returns a dict with keys: iterations, total_time, avg_time, sample_result + """ + api = StubAPIManager() + results = [] + start = time.perf_counter() + sample_result = None + for _ in range(iterations): + res = _run_once(api) + sample_result = res + results.append(res) + end = time.perf_counter() + total = end - start + avg = total / iterations if iterations else 0 + return { + "iterations": iterations, + "total_time": total, + "avg_time": avg, + "sample_result": sample_result, + } + + +if __name__ == "__main__": + import json + + out = main(iterations=1) + print(json.dumps(out, indent=2)) From 782fd5e9c6279439baca2fb4675f21591b1b3b7e Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Thu, 16 Oct 2025 16:01:50 -0500 Subject: [PATCH 09/10] chore(bench): append benchmark history --- bench/history.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 bench/history.json diff --git a/bench/history.json b/bench/history.json new file mode 100644 index 0000000..8c2be51 --- /dev/null +++ b/bench/history.json @@ -0,0 +1,16 @@ +[ + { + "timestamp": "2025-10-16T21:01:50.518805", + "results": { + "module": "bench.targets.game_runner_target", + "func": "main", + "iterations": 1, + "stats": { + "runs": 1, + "avg": 0.7940893999766558, + "min": 0.7940893999766558, + "max": 0.7940893999766558 + } + } + } +] \ No newline at end of file From a490a3e5abbb729078bd5bbe71097f588f8ef6f5 Mon Sep 17 00:00:00 2001 From: Isaiah Nwukor Date: Fri, 17 Oct 2025 06:14:20 -0500 Subject: [PATCH 10/10] updat --- bench/results.json | 6 +- bench_output.json | 3 + scripts/run_branch.py | 156 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 bench_output.json create mode 100644 scripts/run_branch.py diff --git a/bench/results.json b/bench/results.json index c0e9632..427d6ce 100644 --- a/bench/results.json +++ b/bench/results.json @@ -4,8 +4,8 @@ "iterations": 1, "stats": { "runs": 1, - "avg": 0.6973345000296831, - "min": 0.6973345000296831, - "max": 0.6973345000296831 + "avg": 0.7940893999766558, + "min": 0.7940893999766558, + "max": 0.7940893999766558 } } \ No newline at end of file diff --git a/bench_output.json b/bench_output.json new file mode 100644 index 0000000..a43af00 --- /dev/null +++ b/bench_output.json @@ -0,0 +1,3 @@ +[OK] ADVANCED SYSTEMS IMPORTED SUCCESSFULLY +[OK] Environment variables loaded from .env file in centralized_config +Runs: 1, avg: 0.771294s, min: 0.771294s, max: 0.771294s diff --git a/scripts/run_branch.py b/scripts/run_branch.py new file mode 100644 index 0000000..a95a5da --- /dev/null +++ b/scripts/run_branch.py @@ -0,0 +1,156 @@ +"""Run branch-specific smoke/demo commands. + +Usage: + python scripts/run_branch.py --branch tb-performance --iters 1 + python scripts/run_branch.py --all --dry-run + +The script runs short, safe commands per branch and defaults to using the stub API where applicable. +""" +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PYTHON = sys.executable + + +BRANCH_COMMANDS = { + "tb-minimal": { + "desc": "Run minimal runner (stub API)", + "cmd": [PYTHON, str(ROOT / "scripts" / "run_minimal.py"), "--game-id", "smoke_minimal", "--max-actions", "10", "--use-stub-api-for-ci"] + }, + "tb-core-stable": { + "desc": "Run core smoke (GameRunner with stub)", + "cmd": [PYTHON, str(ROOT / "scripts" / "run_minimal.py"), "--game-id", "core_smoke", "--max-actions", "20", "--use-stub-api-for-ci"] + }, + "tb-performance": { + "desc": "Run bench target once", + "cmd": [PYTHON, "-m", "bench.runner", "--module", "bench.targets.game_runner_target", "--iters", "1", "--out", str(ROOT / "bench" / "results.json")] # runs as module + }, + "tb-research-playground": { + "desc": "Run a short notebook-free demo using stub", + "cmd": [PYTHON, str(ROOT / "scripts" / "run_minimal.py"), "--game-id", "research_smoke", "--max-actions", "5", "--use-stub-api-for-ci"] + } +} + + +def run_command(cmd, dry_run=False): + print(" ") + print("Running:", " ".join(map(str, cmd))) + if dry_run: + return 0 + try: + proc = subprocess.run(cmd, check=True, capture_output=False) + return proc.returncode + except subprocess.CalledProcessError as e: + print(f"Command failed with exit {e.returncode}") + return e.returncode + + +def git_checkout(branch: str, dry_run: bool = False): + print(f"Checking out branch {branch}") + if dry_run: + return 0 + try: + subprocess.run(["git", "checkout", branch], check=True) + return 0 + except subprocess.CalledProcessError as e: + print(f"Git checkout failed: {e}") + return e.returncode + + +def append_history(results_path: Path, history_path: Path): + import json + now = __import__("datetime").datetime.utcnow().isoformat() + try: + with open(results_path, "r", encoding="utf-8") as fh: + latest = fh.read() + # Try parse json; otherwise wrap as text + try: + data = json.loads(latest) + except Exception: + data = {"raw": latest} + entry = {"timestamp": now, "results": data} + hist = [] + if history_path.exists(): + with open(history_path, "r", encoding="utf-8") as fh: + try: + hist = json.load(fh) + except Exception: + hist = [] + hist.append(entry) + with open(history_path, "w", encoding="utf-8") as fh: + json.dump(hist, fh, indent=2) + print(f"Appended results to {history_path}") + return True + except Exception as e: + print(f"Failed to append history: {e}") + return False + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--branch", help="Branch to run (tb-minimal|tb-core-stable|tb-performance|tb-research-playground)") + parser.add_argument("--all", action="store_true", help="Run all branch smoke commands") + parser.add_argument("--dry-run", action="store_true", help="Print commands but don't execute") + parser.add_argument("--iters", type=int, default=1, help="Iterations for bench where applicable") + parser.add_argument("--checkout", action="store_true", help="Checkout the branch before running the command") + parser.add_argument("--append-history", action="store_true", help="Append bench results to bench/history.json") + parser.add_argument("--commit-history", action="store_true", help="Commit and push history.json after appending") + args = parser.parse_args() + + to_run = [] + if args.all: + to_run = list(BRANCH_COMMANDS.items()) + elif args.branch: + if args.branch not in BRANCH_COMMANDS: + print(f"Unknown branch: {args.branch}") + sys.exit(2) + to_run = [(args.branch, BRANCH_COMMANDS[args.branch])] + else: + parser.print_help() + sys.exit(1) + + for name, info in to_run: + cmd = info["cmd"].copy() + # if bench and iters provided, replace --iters arg + if name == "tb-performance": + # find --iters or --iters position and update + if "--iters" in cmd: + idx = cmd.index("--iters") + cmd[idx+1] = str(args.iters) + print(f"--- {name}: {info['desc']} ---") + # optionally checkout branch first + if getattr(args, 'checkout', False): + co_rc = git_checkout(name, dry_run=args.dry_run) + if co_rc != 0: + print(f"Skipping run for {name} due to checkout failure") + continue + + rc = run_command(cmd, dry_run=args.dry_run) + if rc != 0: + print(f"Branch {name} command failed (rc={rc})") + # continue to next but record failure + else: + # If requested, append bench results to history + if name == 'tb-performance' and getattr(args, 'append_history', False): + results_path = ROOT / 'bench' / 'results.json' + history_path = ROOT / 'bench' / 'history.json' + ok = append_history(results_path, history_path) + if ok and getattr(args, 'commit_history', False): + try: + subprocess.run(["git", "add", str(history_path)], check=True) + subprocess.run(["git", "commit", "-m", "chore(bench): append benchmark history"], check=True) + subprocess.run(["git", "push"], check=True) + print("Committed and pushed history.json") + except subprocess.CalledProcessError as e: + print(f"Failed to commit/push history: {e}") + + print("All requested branch runs completed.") + + +if __name__ == '__main__': + main()