diff --git a/GramAddict/core/config.py b/GramAddict/core/config.py index d55fd8d..4dc7a7c 100644 --- a/GramAddict/core/config.py +++ b/GramAddict/core/config.py @@ -86,8 +86,8 @@ class Config: self.debug = self.config.get("debug", False) self.app_id = self.config.get("app_id", "com.instagram.android") - # Autonomous Agent Goals - self.goals = self.config.get("goals", []) + # Autonomous goals removed — the bot now derives tasks from mission + plugins + # via GoalDecomposer. See GramAddict/core/goal_decomposer.py. else: if "--debug" in self.args: self.debug = True diff --git a/GramAddict/core/growth_brain.py b/GramAddict/core/growth_brain.py index f3ea7d4..920db8e 100644 --- a/GramAddict/core/growth_brain.py +++ b/GramAddict/core/growth_brain.py @@ -99,6 +99,9 @@ class GrowthBrain: Autonomously selects the next strategic goal. If no goals are configured, falls back to legacy desires. Weights goals based on session success rates if provided. + + .. deprecated:: + Use select_task() instead for concrete, plugin-linked task selection. """ import random @@ -121,6 +124,33 @@ class GrowthBrain: return random.choices(available_goals, weights=weights, k=1)[0] + def select_task(self, dopamine_engine, available_tasks: list) -> "Optional[Task]": + """Select the next concrete Task using weighted random selection. + + This is the primary interface for the orchestrator. Unlike get_current_goal() + which returns abstract strings, this returns a Task object with a specific + target_screen, budget, and success metric. + + Returns: + Task: The selected task to execute. + None: If no tasks available or boredom is too high (ShiftContext signal). + """ + if not available_tasks: + return None + + # High boredom = ShiftContext (take a break, switch feed) + if dopamine_engine.boredom > 85.0: + logger.info("🧠 [GrowthBrain] Boredom too high for task selection. ShiftContext.") + return None + + weights = [task.weight for task in available_tasks] + selected = random.choices(available_tasks, weights=weights, k=1)[0] + logger.info( + f"🧠 [GrowthBrain] Selected task: {selected.verb} → {selected.target_screen} " + f"(weight={selected.weight:.2f}, budget={selected.budget_posts})" + ) + return selected + def get_circadian_pacing(self) -> float: """ Adjusts activity levels based on the current local time diff --git a/tests/tdd/test_goal_decomposer.py b/tests/tdd/test_goal_decomposer.py index 6d376df..16a2eb0 100644 --- a/tests/tdd/test_goal_decomposer.py +++ b/tests/tdd/test_goal_decomposer.py @@ -210,3 +210,110 @@ class TestGoalDecomposerGeneratesTasks: f"Task target_screen '{task.target_screen}' is not in ScreenTopology! " f"The bot cannot navigate there." ) + + +class TestGoalDecomposerFromConfig: + """Tests that GoalDecomposer correctly derives tasks from config-like dicts. + Validates that the legacy `goals:` config is not needed.""" + + def test_decomposer_produces_tasks_without_goals(self): + """Tasks come from plugins + actions, NOT from goals list.""" + from GramAddict.core.goal_decomposer import GoalDecomposer + + plugins = {"likes": {"percentage": 100}} + actions = {"feed": "5-10"} + mission = {"strategy": "aggressive_growth"} + + decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission) + tasks = decomposer.generate_tasks() + + assert len(tasks) > 0, "Tasks must come from plugins, not goals" + + def test_decomposer_accepts_empty_mission(self): + """If no mission is provided, default to aggressive_growth.""" + from GramAddict.core.goal_decomposer import GoalDecomposer + + plugins = {"likes": {"percentage": 100}} + actions = {"feed": "5-10"} + + decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission={}) + tasks = decomposer.generate_tasks() + + assert len(tasks) > 0, "Empty mission must fall back to aggressive_growth" + + +class TestGrowthBrainTaskSelection: + """Tests that GrowthBrain.select_task() picks from concrete Task objects.""" + + def test_select_task_returns_task_object(self): + from GramAddict.core.goal_decomposer import GoalDecomposer, Task + from GramAddict.core.growth_brain import GrowthBrain + from GramAddict.core.dopamine_engine import DopamineEngine + + plugins = {"likes": {"percentage": 100}, "story_view": {"percentage": 80}} + actions = {"feed": "5-10", "explore": "5-10"} + mission = {"strategy": "aggressive_growth"} + + decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission) + tasks = decomposer.generate_tasks() + dopamine = DopamineEngine() + + brain = GrowthBrain(username="test", persona_interests=[]) + selected = brain.select_task(dopamine, tasks) + + assert isinstance(selected, Task), f"Expected Task, got {type(selected)}" + assert selected in tasks + + def test_select_task_returns_none_on_empty_tasks(self): + from GramAddict.core.growth_brain import GrowthBrain + from GramAddict.core.dopamine_engine import DopamineEngine + + dopamine = DopamineEngine() + brain = GrowthBrain(username="test", persona_interests=[]) + selected = brain.select_task(dopamine, []) + + assert selected is None, "Empty task list must return None" + + def test_select_task_returns_none_on_high_boredom(self): + from GramAddict.core.goal_decomposer import GoalDecomposer + from GramAddict.core.growth_brain import GrowthBrain + from GramAddict.core.dopamine_engine import DopamineEngine + + plugins = {"likes": {"percentage": 100}} + actions = {"feed": "5-10"} + mission = {"strategy": "aggressive_growth"} + + decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission) + tasks = decomposer.generate_tasks() + dopamine = DopamineEngine() + dopamine.boredom = 95.0 + + brain = GrowthBrain(username="test", persona_interests=[]) + selected = brain.select_task(dopamine, tasks) + + assert selected is None, "High boredom must return None (= ShiftContext signal)" + + def test_select_task_uses_weights(self): + """Over many selections, higher-weighted tasks should appear more often.""" + from GramAddict.core.goal_decomposer import GoalDecomposer, Task + from GramAddict.core.growth_brain import GrowthBrain + from GramAddict.core.dopamine_engine import DopamineEngine + + plugins = {"likes": {"percentage": 100}, "story_view": {"percentage": 80}} + actions = {"feed": "5-10", "explore": "5-10"} + mission = {"strategy": "aggressive_growth"} + + decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission) + tasks = decomposer.generate_tasks() + dopamine = DopamineEngine() + brain = GrowthBrain(username="test", persona_interests=[]) + + # Run 100 selections and count + counts: dict = {} + for _ in range(100): + selected = brain.select_task(dopamine, tasks) + if selected: + counts[selected.target_screen] = counts.get(selected.target_screen, 0) + 1 + + # With aggressive_growth, ExploreFeed (weight 0.45) should dominate + assert len(counts) > 1, "Selection must pick from multiple screens"