diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 789e71b..db6e0d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,4 +21,4 @@ repos: entry: ./scripts/pre_commit_tests.sh language: system types: [python] - pass_filenames: false + pass_filenames: true diff --git a/GramAddict/core/bot_flow.py b/GramAddict/core/bot_flow.py index c5d60b6..5e78823 100644 --- a/GramAddict/core/bot_flow.py +++ b/GramAddict/core/bot_flow.py @@ -1,131 +1,141 @@ import logging - -import os -import re import random -import time from datetime import datetime from time import sleep from colorama import Fore, Style -from GramAddict.core.version import __version__, __tested_ig_version__ +from GramAddict.core.account_switcher import verify_and_switch_account +from GramAddict.core.active_inference import ActiveInferenceEngine from GramAddict.core.config import Config +from GramAddict.core.darwin_engine import DarwinEngine from GramAddict.core.device_facade import create_device, get_device_info -from GramAddict.core.log import configure_logger -from GramAddict.core.persistent_list import PersistentList -from GramAddict.core.session_state import SessionState, SessionStateEncoder -from GramAddict.core.utils import ( - check_if_updated, - close_instagram, - get_instagram_version, - open_instagram, - random_sleep, - set_time_delta, - wait_for_next_session, - is_ad, -) +from GramAddict.core.diagnostic_dump import dump_ui_state +from GramAddict.core.dm_engine import _run_zero_latency_dm_loop +from GramAddict.core.dojo_engine import DojoEngine # Cognitive Stack from GramAddict.core.dopamine_engine import DopamineEngine -from GramAddict.core.resonance_engine import ResonanceEngine -from GramAddict.core.active_inference import ActiveInferenceEngine from GramAddict.core.growth_brain import GrowthBrain -from GramAddict.core.swarm_protocol import SwarmProtocol -from GramAddict.core.darwin_engine import DarwinEngine -from GramAddict.core.q_nav_graph import QNavGraph -from GramAddict.core.zero_latency_engine import ZeroLatencyEngine -from GramAddict.core.unfollow_engine import _run_zero_latency_unfollow_loop -from GramAddict.core.dm_engine import _run_zero_latency_dm_loop -from GramAddict.core.telepathic_engine import TelepathicEngine -from GramAddict.core.sensors.honeypot_radome import HoneypotRadome - -from GramAddict.core.diagnostic_dump import dump_ui_state -from GramAddict.core.qdrant_memory import ParasocialCRMDB -from GramAddict.core.dojo_engine import DojoEngine -from GramAddict.core.account_switcher import verify_and_switch_account +from GramAddict.core.log import configure_logger +from GramAddict.core.perception.feed_analysis import ( + FEED_MARKERS, +) +from GramAddict.core.perception.feed_analysis import ( + extract_post_content as _extract_post_content_impl, +) +from GramAddict.core.persistent_list import PersistentList +from GramAddict.core.physics.humanized_input import ( + humanized_click as _humanized_click_impl, +) +from GramAddict.core.physics.humanized_input import ( + humanized_horizontal_swipe as _humanized_horizontal_swipe_impl, +) # ── Decomposed Modules (Phase 1 extraction) ── from GramAddict.core.physics.humanized_input import ( humanized_scroll as _humanized_scroll_impl, - humanized_click as _humanized_click_impl, - humanized_horizontal_swipe as _humanized_horizontal_swipe_impl, +) +from GramAddict.core.physics.timing import ( + align_active_post as _align_active_post_impl, ) from GramAddict.core.physics.timing import ( wait_for_post_loaded as _wait_for_post_loaded_impl, - wait_for_story_loaded as _wait_for_story_loaded_impl, - align_active_post as _align_active_post_impl, +) +from GramAddict.core.physics.timing import ( wait_for_profile_loaded as _wait_for_profile_loaded_impl, ) -from GramAddict.core.perception.feed_analysis import ( - FEED_MARKERS, - has_carousel_in_view, - extract_post_content as _extract_post_content_impl, - has_feed_markers, +from GramAddict.core.physics.timing import ( + wait_for_story_loaded as _wait_for_story_loaded_impl, ) +from GramAddict.core.q_nav_graph import QNavGraph +from GramAddict.core.qdrant_memory import ParasocialCRMDB +from GramAddict.core.resonance_engine import ResonanceEngine +from GramAddict.core.sensors.honeypot_radome import HoneypotRadome +from GramAddict.core.session_state import SessionState, SessionStateEncoder +from GramAddict.core.swarm_protocol import SwarmProtocol +from GramAddict.core.telepathic_engine import TelepathicEngine +from GramAddict.core.unfollow_engine import _run_zero_latency_unfollow_loop +from GramAddict.core.utils import ( + check_if_updated, + close_instagram, + get_instagram_version, + is_ad, + open_instagram, + random_sleep, + set_time_delta, + wait_for_next_session, +) +from GramAddict.core.zero_latency_engine import ZeroLatencyEngine logger = logging.getLogger(__name__) + def start_bot(**kwargs): configs = Config(first_run=True, **kwargs) configure_logger(configs.debug, configs.username) check_if_updated() - + from GramAddict.core.benchmark_guard import check_model_benchmarks + check_model_benchmarks(configs) - + from GramAddict.core.llm_provider import log_openrouter_burn + log_openrouter_burn() - + # Check for direct execution modes that bypass normal bot state configs.parse_args() - + try: from GramAddict.core.llm_provider import prewarm_ollama_models + prewarm_ollama_models(configs) except Exception as e: logger.debug(f"Prewarm failed: {e}") - + sessions = PersistentList("sessions", SessionStateEncoder) device = create_device(configs.device_id, configs.app_id, configs.args) # ── Initialize Biomechanical Physics ── from GramAddict.core.physics.biomechanics import PhysicsBody - from GramAddict.core.physics.biomechanics import PhysicsBody - + handedness = getattr(configs.args, "handedness", "right") or "right" PhysicsBody.reset() # Clean state for new session PhysicsBody.get_session_instance(device, handedness=handedness) logger.info(f"🦴 [Biomechanics] Session initialized: {handedness}-handed thumb model") - - # Initialize Cognitive Stack with proper dependencies username = getattr(configs.args, "username", "") or "unknown_user" - + # Parse persona interests from config (comma-separated string → list) - persona_raw = getattr(configs.args, "ai_target_audience", getattr(configs.args, "persona_interests", getattr(configs.args, "target_audience", ""))) + persona_raw = getattr( + configs.args, + "ai_target_audience", + getattr(configs.args, "persona_interests", getattr(configs.args, "target_audience", "")), + ) persona_interests = [p.strip() for p in persona_raw.split(",") if p.strip()] if persona_raw else [] - + dopamine = DopamineEngine() crm_db = ParasocialCRMDB() resonance_oracle = ResonanceEngine(username, persona_interests=persona_interests, crm=crm_db) active_inference = ActiveInferenceEngine(username) - + # Core Autonomous Engines zero_engine = ZeroLatencyEngine(device) nav_graph = QNavGraph(device) growth_brain = GrowthBrain(username, persona_interests=persona_interests) - + info = device.get_info() radome = HoneypotRadome(info.get("displayWidth", 1080), info.get("displayHeight", 2400)) - + swarm = SwarmProtocol(username) darwin = DarwinEngine(username) from GramAddict.core.telepathic_engine import TelepathicEngine + telepathic = TelepathicEngine.get_instance() - + # ── Stage 0: Blank Start (Scorched Earth) ── if getattr(configs.args, "blank_start", False): logger.warning(f"⚠️ [Blank Start] Wiping ALL persistent AI memories for '{username}'...") @@ -133,6 +143,7 @@ def start_bot(**kwargs): # Wipe navigation paths too try: from GramAddict.core.goap import PathMemory + path_mem = PathMemory(username) path_mem.wipe() logger.info("🗑️ Wiped PathMemory collection.") @@ -154,11 +165,11 @@ def start_bot(**kwargs): } from GramAddict.core.behaviors import PluginRegistry - from GramAddict.core.behaviors.profile_guard import ProfileGuardPlugin + from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin from GramAddict.core.behaviors.follow import FollowPlugin from GramAddict.core.behaviors.grid_like import GridLikePlugin + from GramAddict.core.behaviors.profile_guard import ProfileGuardPlugin from GramAddict.core.behaviors.story_view import StoryViewPlugin - from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin PluginRegistry.reset() plugin_registry = PluginRegistry.get_instance() @@ -167,17 +178,16 @@ def start_bot(**kwargs): plugin_registry.register(FollowPlugin()) plugin_registry.register(GridLikePlugin()) plugin_registry.register(CarouselBrowsingPlugin()) - - cognitive_stack["plugin_registry"] = plugin_registry + cognitive_stack["plugin_registry"] = plugin_registry is_first_session = True has_scanned_own_profile = False - + dojo = DojoEngine.get_instance(device) dojo.start() cognitive_stack["dojo"] = dojo - + try: while True: set_time_delta(configs.args) @@ -186,13 +196,13 @@ def start_bot(**kwargs): ) if not inside_working_hours: wait_for_next_session(time_left, None, sessions, device) - + get_device_info(device) session_state = SessionState(configs) session_state.set_limits_session() sessions.append(session_state) device.wake_up() - + logger.info( "-------- START AGENT SESSION: " + str(session_state.startTime.strftime("%H:%M:%S - %Y/%m/%d")) @@ -206,11 +216,11 @@ def start_bot(**kwargs): # QNavGraph will try to dynamically resolve from UNKNOWN using the bottom navigation bar. nav_graph.current_state = "UNKNOWN" logger.info("Initializing Top-Level Graph context...") - + if not verify_and_switch_account(device, nav_graph, username): logger.error(f"Cannot verify or switch to target account '{username}'. Halting session.") break - + is_first_session = False try: running_ig_version = get_instagram_version(device) @@ -222,41 +232,52 @@ def start_bot(**kwargs): # 🤖 AGENT ORCHESTRATOR LOOP # ════════════════════════════════════════════════════════════════════════════ dopamine.reset_session() - + # Establish Initial Strategy from Config growth_brain.strategy = getattr(configs.args, "agent_strategy", "aggressive_growth") - - logger.info(f"🧠 [Agent Orchestrator] Session started. Strategy: {growth_brain.strategy} | Persona: {getattr(configs.args, 'agent_persona', 'unknown')}") - + + logger.info( + f"🧠 [Agent Orchestrator] Session started. Strategy: {growth_brain.strategy} | Persona: {getattr(configs.args, 'agent_persona', 'unknown')}" + ) + from GramAddict.core.goap import GoalExecutor + goap = GoalExecutor.get_instance(device, username) - + # --- PHASE 0: Autonomous Profile Scanning --- if getattr(configs.args, "ai_learn_own_profile", False) and not has_scanned_own_profile: - logger.info("🧠 [Identity Boot] Autonomous Profile Scanning Triggered: Learning own content...", extra={"color": f"{Fore.MAGENTA}"}) + logger.info( + "🧠 [Identity Boot] Autonomous Profile Scanning Triggered: Learning own content...", + extra={"color": f"{Fore.MAGENTA}"}, + ) success = goap.achieve("learn own profile") if success: sleep(2.0) try: profile_xml = device.dump_hierarchy() all_nodes = telepathic._extract_semantic_nodes(profile_xml) - + raw_bio_text = [] for node in all_nodes: text = node.get("original_attribs", {}).get("text", "") desc = node.get("original_attribs", {}).get("desc", "") - if len(text) > 4: raw_bio_text.append(text) - if len(desc) > 4: raw_bio_text.append(desc) - + if len(text) > 4: + raw_bio_text.append(text) + if len(desc) > 4: + raw_bio_text.append(desc) + # Ensure grid is visible by scrolling down slightly _humanized_scroll(device, is_skip=True) sleep(1.5) - + # Tap first grid post to learn from actual captions if nav_graph.do("tap first image post in profile grid"): post_loaded = _wait_for_post_loaded(device, timeout=5) if post_loaded: - logger.info("📸 [Identity Boot] Reading recent posts to analyze actual content vibe...", extra={"color": f"{Fore.CYAN}"}) + logger.info( + "📸 [Identity Boot] Reading recent posts to analyze actual content vibe...", + extra={"color": f"{Fore.CYAN}"}, + ) for _ in range(3): post_xml = device.dump_hierarchy() if isinstance(post_xml, str): @@ -265,41 +286,46 @@ def start_bot(**kwargs): raw_bio_text.append(post_data["caption"]) elif post_data.get("description"): raw_bio_text.append(post_data["description"]) - + _humanized_scroll(device, is_skip=False) sleep(2.0) - + device.press("back") sleep(1.5) - + # Deduplicate while preserving order unique_texts = list(dict.fromkeys(raw_bio_text)) condensed_profile = " | ".join(unique_texts[:30]) # Take top substantive elements - + logger.debug(f"Captured Profile Payload: {condensed_profile[:200]}...") - + prompt = ( "You are an analytical profiling engine. Read the following text ripped straight from an Instagram profile page " "(which contains bio, follower counts, button labels, and recent post descriptions). " "Determine the exact 'persona' (2-3 words) and 'vibe' (3-4 adjectives) that represents THIS specific user.\n\n" f"PROFILE TEXT: {condensed_profile}\n\n" - "Respond ONLY in valid JSON format: {\"persona\": \"\", \"vibe\": \"\"}" + 'Respond ONLY in valid JSON format: {"persona": "", "vibe": ""}' ) - + from GramAddict.core.llm_provider import query_llm + model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b") url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate") - + response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=True, timeout=120) if response_dict and isinstance(response_dict, dict) and "persona" in response_dict: new_persona_raw = response_dict.get("persona", "") new_vibe = response_dict.get("vibe", "") - + if new_persona_raw and new_vibe: - new_persona_list = [p.strip() for p in new_persona_raw.split(",") if p.strip()] if "," in new_persona_raw else [new_persona_raw] + new_persona_list = ( + [p.strip() for p in new_persona_raw.split(",") if p.strip()] + if "," in new_persona_raw + else [new_persona_raw] + ) resonance_oracle.update_identity(new_persona_list, new_vibe) growth_brain.persona_interests = new_persona_list - + # Overwrite config values in-memory setattr(configs.args, "agent_persona", new_persona_raw) setattr(configs.args, "ai_vibe", new_vibe) @@ -307,13 +333,13 @@ def start_bot(**kwargs): logger.error(f"Failed to learn own profile autonomously: {e}") else: logger.warning("🧠 [Identity Boot] Failed to navigate to own profile.") - + has_scanned_own_profile = True - + while not dopamine.is_app_session_over(): # 1. Ask the Growth Brain for a Desire current_desire = growth_brain.get_current_desire(dopamine) - + if current_desire == "ShiftContext": logger.info("🧠 [Free Will] Boredom critical. Forcing app restart to clear context.") device.app_stop(device.app_id) @@ -322,36 +348,37 @@ def start_bot(**kwargs): random_sleep(4.0, 6.0) dopamine.boredom = max(0.0, dopamine.boredom * 0.2) continue - + # 2. Map Desire to Sub-Feed target_map = { "DiscoverNewContent": ["ExploreFeed", "ReelsFeed"], "NurtureCommunity": ["HomeFeed", "StoriesFeed"], - "SocialReciprocity": ["FollowingList", "MessageInbox"] + "SocialReciprocity": ["FollowingList", "MessageInbox"], } - + import secrets + options = target_map.get(current_desire, ["HomeFeed"]) current_target = secrets.choice(options) - + logger.info(f"🧠 [Agent Orchestrator] Desire '{current_desire}' -> Routed to {current_target}") - + logger.info(f"⚡ Navigating to {current_target}") success = nav_graph.navigate_to(current_target, zero_engine) - + if success: if current_target == "ExploreFeed": # [Phase 2] Visual selection of the first post logger.info("📱 [Vision Core] Evaluating explore grid for the most resonant post...") res_eval = telepathic.evaluate_grid_visuals(device, persona_interests) - + if res_eval: logger.info(f"✨ [Vision Core] Clicking visual match: {res_eval.get('semantic')}") _humanized_click(device, res_eval["x"], res_eval["y"]) else: logger.info("📱 Falling back to default: Opening first explore item from the grid...") nav_graph.do("tap first image in explore grid") - + # Wait for post to actually load (poll for feed markers) post_loaded = _wait_for_post_loaded(device, nav_graph=nav_graph, timeout=5) if not post_loaded: @@ -364,136 +391,166 @@ def start_bot(**kwargs): if not post_loaded: logger.warning("❌ Stories failed to open from HomeFeed. Retrying next loop.") continue - + if current_target == "StoriesFeed": result = _run_zero_latency_stories_loop(device, configs, session_state, cognitive_stack) elif current_target == "FollowingList": - result = _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack) + result = _run_zero_latency_unfollow_loop( + device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack + ) elif current_target == "MessageInbox": - result = _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack) + result = _run_zero_latency_dm_loop( + device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack + ) elif current_target == "SearchFeed": - result = _run_zero_latency_search_loop(device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack) + result = _run_zero_latency_search_loop( + device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack + ) else: - is_reels = (current_target == "ReelsFeed") - result = _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack, is_reels=is_reels) - + is_reels = current_target == "ReelsFeed" + result = _run_zero_latency_feed_loop( + device, + zero_engine, + nav_graph, + configs, + session_state, + current_target, + cognitive_stack, + is_reels=is_reels, + ) + # Evaluate outcome from loop if result in ("BOREDOM_CHANGE_FEED", "FEED_EXHAUSTED"): logger.info(f"🧠 [Free Will] Sub-routine in {current_target} exhausted/bored.") if result == "BOREDOM_CHANGE_FEED": - dopamine.reset_boredom() # Reset boredom allowing new desire - continue # Loops back to get_current_desire() - + dopamine.reset_boredom() # Reset boredom allowing new desire + continue # Loops back to get_current_desire() + elif result == "CONTEXT_LOST": - logger.warning(f"⚠️ Context was lost in {current_target}. Forcing app restart and returning to HomeFeed to escape softlock.") + logger.warning( + f"⚠️ Context was lost in {current_target}. Forcing app restart and returning to HomeFeed to escape softlock." + ) device.app_stop(device.app_id) random_sleep(1.0, 2.0) device.app_start(device.app_id, use_monkey=True) random_sleep(3.0, 5.0) nav_graph.current_state = "UNKNOWN" - + # Force context reset to HomeFeed so we don't repeat the same error loop continue else: logger.info(f"Session concluding due to state: {result}") - break # Session over or unhandled state + break # Session over or unhandled state else: logger.error(f"Aborting target {current_target} due to navigation failure.") break - + logger.info(f"Session complete. Boredom: {dopamine.boredom:.1f}%. Sleeping before next iteration...") close_instagram(device) random_sleep(30, 60) - + except KeyboardInterrupt: logger.info("🛑 Caught KeyboardInterrupt! Exiting immediately.") raise finally: - if 'dojo' in locals() and dojo.is_running: + if "dojo" in locals() and dojo.is_running: dojo.stop() - + # ❄️ Release VRAM try: from GramAddict.core.llm_provider import unload_ollama_models + unload_ollama_models(configs) # Give the thread a tiny bit of time to send the request before process exits sleep(0.5) except Exception as e: logger.debug(f"Failed to trigger VRAM cleanup: {e}") + # FEED_MARKERS: imported from GramAddict.core.perception.feed_analysis (see top imports) - - def _wait_for_post_loaded(device, timeout=5, nav_graph=None): """Delegate to physics.timing. See GramAddict.core.physics.timing.""" return _wait_for_post_loaded_impl(device, timeout=timeout, nav_graph=nav_graph) + def _wait_for_story_loaded(device, timeout=5): """Delegate to physics.timing. See GramAddict.core.physics.timing.""" return _wait_for_story_loaded_impl(device, timeout=timeout) + def _wait_for_profile_loaded(device, timeout=5): """Delegate to physics.timing. See GramAddict.core.physics.timing.""" return _wait_for_profile_loaded_impl(device, timeout=timeout) + def _humanized_scroll(device, is_skip=False, resonance_score=None): """Delegate to physics module. See GramAddict.core.physics.humanized_input.""" _humanized_scroll_impl(device, is_skip=is_skip, resonance_score=resonance_score) + def _humanized_click(device, x, y, double=False, sleep_mod=1.0): """Delegate to physics module. See GramAddict.core.physics.humanized_input.""" _humanized_click_impl(device, x, y, double=double, sleep_mod=sleep_mod) + def _humanized_horizontal_swipe(device, start_x, end_x, y, duration_ms): """Delegate to physics module. See GramAddict.core.physics.humanized_input.""" _humanized_horizontal_swipe_impl(device, start_x, end_x, y, duration_ms) -# has_carousel_in_view: imported from GramAddict.core.perception.feed_analysis (see top imports) +# has_carousel_in_view: imported from GramAddict.core.perception.feed_analysis (see top imports) def _interact_with_profile(device, configs, username, session_state, sleep_mod, logger, cognitive_stack=None): """Deep interaction on a profile: Stories, Grid Likes, Follows""" import random + from colorama import Fore - + if cognitive_stack is None: cognitive_stack = {} - growth = cognitive_stack.get("growth_brain") - - if hasattr(session_state, 'my_username') and username == session_state.my_username: + + if hasattr(session_state, "my_username") and username == session_state.my_username: logger.info(f"🤝 [Deep Interaction] Skipping own profile @{username} to prevent self-interactions.") return - - info = device.get_info() - w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400) - + + # info = device.get_info() + # w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400) + xml_check = device.dump_hierarchy() if not isinstance(xml_check, str): return - + xml_check_lower = xml_check.lower() - + # ── 1. Profile Guards (Private / Empty) ── if "this account is private" in xml_check_lower or "konto ist privat" in xml_check_lower: - logger.info(f"🔒 [Profile Guard] @{username} is private. Aborting deep interaction.", extra={"color": f"{Fore.YELLOW}"}) + logger.info( + f"🔒 [Profile Guard] @{username} is private. Aborting deep interaction.", extra={"color": f"{Fore.YELLOW}"} + ) return - + if "no posts yet" in xml_check_lower or "noch keine beiträge" in xml_check_lower: - logger.info(f"📭 [Profile Guard] @{username} has no posts. Aborting deep interaction.", extra={"color": f"{Fore.YELLOW}"}) + logger.info( + f"📭 [Profile Guard] @{username} has no posts. Aborting deep interaction.", + extra={"color": f"{Fore.YELLOW}"}, + ) return - + if getattr(configs.args, "ignore_close_friends", False): if "enge freunde" in xml_check_lower or "close friend" in xml_check_lower: - logger.info(f"💚 [Profile Guard] @{username} is a Close Friend. Ignoring completely.", extra={"color": f"\\033[32m"}) + logger.info( + f"💚 [Profile Guard] @{username} is a Close Friend. Ignoring completely.", extra={"color": "\\033[32m"} + ) return - + # ── 1.5 Visual Vibe Check (AI Aesthetic Quality Guard) ── vibe_check_pct = float(getattr(configs.args, "visual_vibe_check_percentage", 0)) / 100.0 if vibe_check_pct > 0 and random.random() < vibe_check_pct: from GramAddict.core.telepathic_engine import TelepathicEngine + telepathic = cognitive_stack.get("telepathic") or TelepathicEngine.get_instance() persona_interests = cognitive_stack.get("persona_interests", []) if cognitive_stack else [] vibe_result = telepathic.evaluate_profile_vibe(device, persona_interests) @@ -502,42 +559,50 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod, score = vibe_result.get("quality_score", 5) matches_niche = vibe_result.get("matches_niche", True) if score < 5 or not matches_niche: - logger.warning(f"🚫 [Vibe Check] Profile @{username} rejected (Score: {score}, Niche: {matches_niche}). Reason: {vibe_result.get('reason')}") + logger.warning( + f"🚫 [Vibe Check] Profile @{username} rejected (Score: {score}, Niche: {matches_niche}). Reason: {vibe_result.get('reason')}" + ) return else: - logger.info(f"✅ [Vibe Check] Profile @{username} approved (Score: {score}). Continuing interaction.", extra={"color": f"\\033[36m"}) - + logger.info( + f"✅ [Vibe Check] Profile @{username} approved (Score: {score}). Continuing interaction.", + extra={"color": "\\033[36m"}, + ) + # Profile Scraping (Phase 11: Data Extraction) if getattr(configs.args, "scrape_profiles", False): try: logger.info(f"📊 [Scraping] Extracting metadata for @{username}...", extra={"color": f"{Fore.CYAN}"}) from GramAddict.core.telepathic_engine import TelepathicEngine + telepathic = TelepathicEngine.get_instance() crm = cognitive_stack.get("crm") if cognitive_stack else None - + # Simple heuristic for extraction (followers/following) f_node = telepathic.find_best_node(xml_check, "Followers count text or number", device=device) fg_node = telepathic.find_best_node(xml_check, "Following count text or number", device=device) bio_node = telepathic.find_best_node(xml_check, "User biography or description text", device=device) - + scraped_data = { "username": username, "followers": f_node.get("text") if f_node else "unknown", "following": fg_node.get("text") if fg_node else "unknown", - "bio": bio_node.get("text") if bio_node else "No bio" + "bio": bio_node.get("text") if bio_node else "No bio", } - - logger.info(f"✅ [Scraping] Data acquired: {scraped_data['followers']} followers, {scraped_data['following']} following.") + + logger.info( + f"✅ [Scraping] Data acquired: {scraped_data['followers']} followers, {scraped_data['following']} following." + ) session_state.add_interaction(source=username, succeed=False, followed=False, scraped=True) - + if crm: - crm.log_interaction(username, "scrape", metadata=scraped_data) + crm.log_interaction(username, "scrape", metadata=scraped_data) except Exception as e: logger.warning(f"⚠️ [Scraping] Error during profiling: {e}") - + # ── Execute Plugin Registry Behaviors ── from GramAddict.core.behaviors import BehaviorContext, PluginRegistry - + ctx = BehaviorContext( device=device, configs=configs, @@ -545,32 +610,35 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod, cognitive_stack=cognitive_stack, context_xml=xml_check, sleep_mod=sleep_mod, - username=username + username=username, ) - + registry = PluginRegistry.get_instance() results = registry.execute_all(ctx) - + # Check if any plugin requested skipping further profile interaction for result in results: if result.executed and result.should_skip: - logger.debug(f"⏭️ Profile interaction aborted early by a plugin.") + logger.debug("⏭️ Profile interaction aborted early by a plugin.") return # Let the native UI momentum scroll finish just like a human watching the feed sleep(random.uniform(1.2, 2.0)) # Hesitation mistake (humans sometimes pause randomly) - if random.randint(1, 100) <= 5: + if random.randint(1, 100) <= 5: sleep(random.uniform(1.5, 3.5)) + def _align_active_post(device): """Delegate to physics.timing. See GramAddict.core.physics.timing.""" return _align_active_post_impl(device) + def _extract_post_content(context_xml: str) -> dict: """Delegate to perception module. See GramAddict.core.perception.feed_analysis.""" return _extract_post_content_impl(context_xml) + def _run_zero_latency_stories_loop(device, configs, session_state, cognitive_stack): """ Top-Level Stories Bingewatching Loop @@ -579,61 +647,70 @@ def _run_zero_latency_stories_loop(device, configs, session_state, cognitive_sta """ import random from time import sleep + from colorama import Fore - from GramAddict.core.utils import random_sleep - from GramAddict.core.dopamine_engine import DopamineEngine + logger.info("🎬 [StoriesFeed] Starting native story binging loop...", extra={"color": f"{Fore.CYAN}"}) - + dopamine = cognitive_stack.get("dopamine") info = device.get_info() w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400) sleep_mod = float(getattr(configs.args, "speed_multiplier", 1.0)) - + stories_arg_str = getattr(configs.args, "stories", "999") or "999" try: - min_st, max_st = map(int, stories_arg_str.split('-')) + min_st, max_st = map(int, stories_arg_str.split("-")) limit = random.randint(min_st, max_st) - except: + except Exception: try: limit = int(stories_arg_str) except ValueError: limit = 999 - + iteration = 0 - + while not dopamine.is_app_session_over() and iteration < limit: iteration += 1 - + # Check for boredom if dopamine.wants_to_change_feed(): logger.info("🧠 [Dopamine] Bored. Escaping StoriesFeed to seek new stimuli.") - device.press("back") # Attempt to back out to feed + device.press("back") # Attempt to back out to feed sleep(random.uniform(1.0, 2.0) * sleep_mod) return "BOREDOM_CHANGE_FEED" - + xml_dump = device.dump_hierarchy() if not xml_dump: logger.warning("Failed to dump UI hierarchy in StoriesFeed.") return "CONTEXT_LOST" - + if getattr(configs.args, "ignore_close_friends", False): if "enge freunde" in xml_dump.lower() or "close friend" in xml_dump.lower(): - logger.info("💚 [Anti-Friend] Story is from a Close Friend. Swiping horizontally to skip User.", extra={"color": f"\\033[32m"}) - _humanized_horizontal_swipe(device, start_x=int(w * 0.8), end_x=int(w * 0.2), y=int(h * 0.5), duration_ms=250) + logger.info( + "💚 [Anti-Friend] Story is from a Close Friend. Swiping horizontally to skip User.", + extra={"color": "\\033[32m"}, + ) + _humanized_horizontal_swipe( + device, start_x=int(w * 0.8), end_x=int(w * 0.2), y=int(h * 0.5), duration_ms=250 + ) sleep(random.uniform(0.5, 1.0) * sleep_mod) continue - + # Tap right to go next _humanized_click(device, int(w * 0.85), int(h * 0.5), sleep_mod=sleep_mod) sleep(random.uniform(2.0, 5.0) * sleep_mod) - + logger.info("🎬 [StoriesFeed] Session completed naturally.") device.press("back") return "FEED_EXHAUSTED" -def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, job_target, cognitive_stack, is_reels=False): + + +def _run_zero_latency_feed_loop( + device, zero_engine, nav_graph, configs, session_state, job_target, cognitive_stack, is_reels=False +): """ The ultra-fast autonomous Free Will loop. - + ALL engines are wired in a closed feedback loop: - ResonanceEngine evaluates content → drives Dopamine + Darwin + Interactions - ActiveInference predicts UI state → modulates caution level @@ -642,19 +719,19 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session - SwarmProtocol emits pheromones after successful interactions """ logger.info(f"🔄 Entering Zero-Latency Interaction Pool. Feed: {job_target}") - + dopamine = cognitive_stack.get("dopamine") darwin = cognitive_stack.get("darwin") resonance = cognitive_stack.get("resonance") ai = cognitive_stack.get("active_inference") growth = cognitive_stack.get("growth_brain") swarm = cognitive_stack.get("swarm") - + # Track interaction outcomes for end-of-session learning session_outcomes = [] consecutive_marker_misses = 0 consecutive_ads = 0 - + from GramAddict.core.session_state import SessionState iteration = 0 @@ -663,21 +740,21 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session if any(limit_tuple): logger.info("🚧 [Limits] Total interactions limit reached. Ending session.") break - + iteration += 1 # ── Global Governance (GrowthBrain Strategy Oracle) ── governance_decision = growth.evaluate_governance(dopamine, job_target, is_reels) if growth else "STAY" - + if governance_decision == "SHIFT_CONTEXT": # Store session learning before leaving if growth: growth.refine_persona(session_outcomes) return "BOREDOM_CHANGE_FEED" - + elif governance_decision == "CHECK_CURIOSITY": logger.info("👀 [Curiosity] Spontaneously checking DMs / Notifications...") explore_target = random.choice(["MessageInbox", "Notifications"]) - + if explore_target == "MessageInbox": nav_graph.do("tap direct message icon inbox") sleep(random.uniform(3.0, 7.0)) @@ -686,7 +763,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session sleep(random.uniform(3.0, 7.0)) _humanized_scroll(device, is_skip=True) sleep(random.uniform(2.0, 4.0)) - + # Return to feed nav_graph.navigate_to("HomeFeed", zero_engine) sleep(random.uniform(1.0, 2.5)) @@ -696,23 +773,25 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session circadian = growth.get_circadian_pacing() if growth else 1.0 caution_mod = ai.get_sleep_modifier() if ai else 1.0 sleep_mod = circadian * caution_mod # Combined sleep multiplier - + if dopamine.wants_to_doomscroll(): logger.info("🏃 [Drive] Doomscrolling engaged. Fast-skipping feed.", extra={"color": f"{Fore.CYAN}"}) # Reverse-flick correction logic is now handled internally by _humanized_scroll _humanized_scroll(device, is_skip=True) sleep(random.uniform(0.1, 0.4) * sleep_mod) continue - + context_xml = device.dump_hierarchy() if cognitive_stack.get("radome"): context_xml = cognitive_stack.get("radome").sanitize_xml(context_xml) - + # ── PRE-EMPTIVE AD SKIP (Fast Path) ── if is_ad(context_xml, cognitive_stack): consecutive_ads += 1 if consecutive_ads >= 3: - logger.warning("📺 [Anti-Stuck] Stuck on ad! Executing aggressive skip.", extra={"color": f"{Fore.RED}"}) + logger.warning( + "📺 [Anti-Stuck] Stuck on ad! Executing aggressive skip.", extra={"color": f"{Fore.RED}"} + ) _humanized_scroll(device, is_skip=True) sleep(2.0) else: @@ -720,13 +799,16 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session _humanized_scroll(device, is_skip=True) sleep(random.uniform(0.5, 1.0) * sleep_mod) continue - + consecutive_ads = 0 - + # ── PRE-EMPTIVE CLOSE FRIENDS SKIP ── if getattr(configs.args, "ignore_close_friends", False): if "enge freunde" in context_xml.lower() or "close friend" in context_xml.lower(): - logger.info("💚 [Anti-Friend] Post is from a Close Friend. Skipping to prevent weird interactions.", extra={"color": f"\\033[32m"}) + logger.info( + "💚 [Anti-Friend] Post is from a Close Friend. Skipping to prevent weird interactions.", + extra={"color": "\\033[32m"}, + ) _humanized_scroll(device, is_skip=True) sleep(random.uniform(0.5, 1.0) * sleep_mod) continue @@ -737,51 +819,86 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session if len(interactive_nodes) == 0: logger.warning( "⚠️ [Anomaly Handler] 0 interactive nodes extracted. UI is blind/stuck! Pressing BACK and scrolling...", - extra={"color": f"{Fore.YELLOW}"} + extra={"color": f"{Fore.YELLOW}"}, ) device.press("back") sleep(0.5) _humanized_scroll(device) sleep(random.uniform(1.0, 2.0) * sleep_mod) continue - + # ── Context Validation (Is the bot ACTUALLY on a post?) ── has_feed_markers = any(marker in context_xml for marker in FEED_MARKERS) - + # ── Autonomous Obstacle Detection ── from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType + sae = SituationalAwarenessEngine(device) has_obstacle = sae.perceive(context_xml) == SituationType.OBSTACLE_MODAL - + if has_obstacle: + consecutive_marker_misses += 1 + if consecutive_marker_misses >= 3: + logger.error("❌ Lost context completely. Aborting feed loop to force reset.") + dump_ui_state(device, "context_lost", {"feed": job_target, "misses": consecutive_marker_misses}) + return "CONTEXT_LOST" + + if consecutive_marker_misses == 2: + logger.warning( + "⚠️ [Anomaly Handler] Hardware 'Back' button failed to clear obstacle. Engaging VLM to find escape route...", + extra={"color": f"{Fore.YELLOW}"}, + ) + telepathic = TelepathicEngine.get_instance() + best_node = telepathic.find_best_node( + context_xml, intent_description="Dismiss Obstacle/Modal", device=device + ) + + if best_node: + logger.info( + f" -> Recovery attempt! Clicking {best_node.get('semantic', 'Dismiss Button')} at ({best_node['x']}, {best_node['y']})" + ) + device.click(best_node["x"], best_node["y"]) + sleep(2.5) + consecutive_marker_misses = 0 + continue + else: + logger.warning("⚠️ [Anomaly Handler] No viable escape route found. Forcing scroll...") + _humanized_scroll(device) + sleep(random.uniform(1.0, 2.0) * sleep_mod) + continue + logger.warning( "⚠️ [Self-Check] Obstacle (sheet/dialog/keyboard) is blocking the view! Pressing BACK to dismiss...", - extra={"color": f"{Fore.YELLOW}"} + extra={"color": f"{Fore.YELLOW}"}, ) device.press("back") sleep(0.5) continue - + elif not has_feed_markers: consecutive_marker_misses += 1 if consecutive_marker_misses >= 3: logger.error("❌ Lost context completely. Aborting feed loop to force reset.") dump_ui_state(device, "context_lost", {"feed": job_target, "misses": consecutive_marker_misses}) return "CONTEXT_LOST" - + if consecutive_marker_misses == 2: logger.warning( "⚠️ [Anomaly Handler] Hardware 'Back' button failed to clear obstacle. Engaging VLM to find escape route...", - extra={"color": f"{Fore.YELLOW}"} + extra={"color": f"{Fore.YELLOW}"}, ) telepathic = TelepathicEngine.get_instance() - best_node = telepathic.find_best_node(context_xml, intent_description="Dismiss Obstacle/Modal", device=device) - + best_node = telepathic.find_best_node( + context_xml, intent_description="Dismiss Obstacle/Modal", device=device + ) + if best_node: - logger.info(f" -> Recovery attempt! Clicking {best_node.get('semantic', 'Dismiss Button')} at ({best_node['x']}, {best_node['y']})") + logger.info( + f" -> Recovery attempt! Clicking {best_node.get('semantic', 'Dismiss Button')} at ({best_node['x']}, {best_node['y']})" + ) device.click(best_node["x"], best_node["y"]) sleep(2.5) - + # Verification: Check if markers are now visible post_recovery_xml = device.dump_hierarchy() if any(marker in post_recovery_xml for marker in FEED_MARKERS): @@ -793,7 +910,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session logger.warning("⚠️ [Recovery] Click failed to clear obstacle. Learning from failure.") telepathic.reject_click("Dismiss Obstacle/Modal") # Fallback to scroll - + logger.warning("⚠️ [Anomaly Handler] No viable escape route found. Forcing scroll...") _humanized_scroll(device) sleep(random.uniform(1.0, 2.0) * sleep_mod) @@ -801,15 +918,15 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session logger.warning( "⚠️ [Self-Check] Feed markers missing. Mid-scroll or tall post? Scrolling to reveal markers...", - extra={"color": f"{Fore.YELLOW}"} + extra={"color": f"{Fore.YELLOW}"}, ) # DO NOT press 'back' here as we are just on the timeline. It would trigger a scroll-to-top refresh. _humanized_scroll(device) sleep(random.uniform(1.0, 2.0) * sleep_mod) continue - + consecutive_marker_misses = 0 - + # ── Perfect Snapping Enforcer ── # Fixes the issue where UI gets stuck halfway between two posts. if _align_active_post(device): @@ -817,28 +934,30 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session context_xml = device.dump_hierarchy() if cognitive_stack.get("radome"): context_xml = cognitive_stack.get("radome").sanitize_xml(context_xml) - + # ── Content Extraction (The Bot's Eyes) ── post_data = _extract_post_content(context_xml) - - # ── Self-Correction: Did extraction actually work? ── + # ── Self-Correction: Did extraction actually work? ── has_content = bool(post_data.get("username") or post_data.get("description")) if not has_content: logger.warning( - "⚠️ [Self-Check] On a post but content extraction failed. Skipping.", - extra={"color": f"{Fore.YELLOW}"} + "⚠️ [Self-Check] On a post but content extraction failed. Skipping.", extra={"color": f"{Fore.YELLOW}"} ) dump_ui_state(device, "content_extraction_failed", {"feed": job_target}) _humanized_scroll(device) sleep(random.uniform(0.5, 1.2) * sleep_mod) continue - - logger.info(f"✅ Post by @{post_data['username'] or '?'}: {post_data['description'][:60]}...", extra={"color": f"{Fore.GREEN}"}) - + + logger.info( + f"✅ Post by @{post_data['username'] or '?'}: {post_data['description'][:60]}...", + extra={"color": f"{Fore.GREEN}"}, + ) + # ── Execute Plugin Registry Behaviors (Feed Level) ── from GramAddict.core.behaviors import BehaviorContext, PluginRegistry + ctx = BehaviorContext( device=device, configs=configs, @@ -847,18 +966,18 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session context_xml=context_xml, sleep_mod=sleep_mod, post_data=post_data, - username=post_data.get("username", "") + username=post_data.get("username", ""), ) registry = PluginRegistry.get_instance() plugin_results = registry.execute_all(ctx) - + skip_feed = False for result in plugin_results: if result.executed and result.should_skip: logger.debug("⏭️ Feed interaction aborted early by a plugin.") skip_feed = True break - + if skip_feed: _humanized_scroll(device) sleep(random.uniform(0.5, 1.2) * sleep_mod) @@ -871,7 +990,9 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session if is_ad(context_xml, cognitive_stack): consecutive_ads += 1 if consecutive_ads >= 3: - logger.warning("🚩 [Ad Trap] Detected 3 consecutive ads. High density zone. Force scrolling to escape...") + logger.warning( + "🚩 [Ad Trap] Detected 3 consecutive ads. High density zone. Force scrolling to escape..." + ) _humanized_scroll(device) consecutive_ads = 0 else: @@ -879,12 +1000,12 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session _humanized_scroll(device) sleep(random.uniform(0.5, 1.2) * sleep_mod) continue - + consecutive_ads = 0 # ── Resonance Engine (Real AI Content Evaluation) ── res_score = resonance.calculate_resonance(post_data) if resonance else 0.5 - + # ── Visual Vibe Check for Content (Using LLM More) ── vibe_check_pct = float(getattr(configs.args, "visual_vibe_check_percentage", 0)) / 100.0 if vibe_check_pct > 0 and random.random() < vibe_check_pct: @@ -893,45 +1014,51 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session if telepathic: vibe_result = telepathic.evaluate_post_vibe(device, persona_interests) if vibe_result: - visual_score = vibe_result.get("quality_score", 5) / 10.0 # scale 0-1 + visual_score = vibe_result.get("quality_score", 5) / 10.0 # scale 0-1 # Combine text resonance and visual resonance res_score = (res_score * 0.3) + (visual_score * 0.7) - logger.info(f"👁️ [Vision Core] Adjusted Resonance with Visual Score: {res_score:.2f} (Visual: {visual_score:.2f})") + logger.info( + f"👁️ [Vision Core] Adjusted Resonance with Visual Score: {res_score:.2f} (Visual: {visual_score:.2f})" + ) if not vibe_result.get("matches_niche", True): logger.info("🚫 [Vision Core] Content strictly rejected as out-of-niche.") - res_score = 0.1 # Force skip + res_score = 0.1 # Force skip # ── Dopamine Engine (fed with REAL resonance, not random) ── - dopamine.process_content({ - "score": res_score * 10, - "quality": "high" if res_score > 0.7 else "medium" if res_score > 0.4 else "low" - }) - + dopamine.process_content( + {"score": res_score * 10, "quality": "high" if res_score > 0.7 else "medium" if res_score > 0.4 else "low"} + ) + # ── Human-like Selective Skipping (Anti-Bot Drip) ── base_skip_prob = 0.85 if res_score < 0.35 else 0.45 if res_score < 0.70 else 0.10 - + # User defined interact_percentage modulates the skip rate. # Default is 80%, so factor = 1.0. If 100%, factor = 0.0 (never skip). interact_pct_val = float(getattr(configs.args, "interact_percentage", 80)) / 100.0 skip_factor = max(0.0, (1.0 - interact_pct_val) * 5.0) skip_prob = base_skip_prob * skip_factor - + rnd_skip = random.random() - logger.info(f"⚙️ [Decision] Resonance {res_score:.2f} -> Base Skip: {base_skip_prob:.2f}. Config Interact={interact_pct_val*100}% -> Skip Factor: {skip_factor:.2f}. Final Skip Prob: {skip_prob:.2f} (Roll: {rnd_skip:.2f})") - + logger.info( + f"⚙️ [Decision] Resonance {res_score:.2f} -> Base Skip: {base_skip_prob:.2f}. Config Interact={interact_pct_val*100}% -> Skip Factor: {skip_factor:.2f}. Final Skip Prob: {skip_prob:.2f} (Roll: {rnd_skip:.2f})" + ) + if rnd_skip < skip_prob: - logger.info(f"⏭️ [Resonance Skip] Human-like selective engagement ({skip_prob*100:.0f}% chance). Skipping post.") - session_outcomes.append({ - "username": post_data.get("username", ""), - "action": "skip", - "resonance": res_score - }) + logger.info( + f"⏭️ [Resonance Skip] Human-like selective engagement ({skip_prob*100:.0f}% chance). Skipping post." + ) + session_outcomes.append( + {"username": post_data.get("username", ""), "action": "skip", "resonance": res_score} + ) _humanized_scroll(device) sleep(random.uniform(0.5, 1.2) * sleep_mod) continue # ── The Rabbit Hole (Deep Dive into high-resonance profiles) ── if res_score >= 0.9 and random.random() < 0.4: - logger.info("💥 [Rabbit Hole] Extreme resonance! Sidetracking into user profile...", extra={"color": f"{Fore.MAGENTA}"}) + logger.info( + "💥 [Rabbit Hole] Extreme resonance! Sidetracking into user profile...", + extra={"color": f"{Fore.MAGENTA}"}, + ) if nav_graph.do("tap post username") is True: sleep(random.uniform(1.2, 2.5) * sleep_mod) _humanized_scroll(device, is_skip=True) @@ -945,35 +1072,34 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session if darwin: darwin.execute_micro_wobble(device) darwin.execute_proof_of_resonance( - device, - res_score, + device, + res_score, text_length=len(post_data.get("description", "")), - nav_graph=nav_graph, - zero_engine=zero_engine, - configs=configs, - resonance_oracle=resonance, + nav_graph=nav_graph, + zero_engine=zero_engine, + configs=configs, + resonance_oracle=resonance, username=post_data.get("username", "unknown"), - context_xml=context_xml + context_xml=context_xml, ) else: # Absolute fallback if Darwin is not available sleep(random.uniform(2.0, 5.0) * sleep_mod) - + # ── Interaction Engine ── did_interact = False - did_like = False did_comment = False interact_chance = float(getattr(configs.args, "interact_percentage", 80)) / 100.0 - + profile_context = "" # ── Profile Learning (Before heavy engagement) ── - target_user = post_data.get('username', 'target') - + target_user = post_data.get("username", "target") + # Pull follow chance early to see if the user explicitly wants high follow rates follow_chance_val = float(getattr(configs.args, "follow_percentage", 30)) / 100.0 if getattr(configs.args, "agent_strategy", "") == "passive_learning": follow_chance_val = 0.0 # Force 0 for dry-runs - + # If resonance is poor, never engage deeply. rnd_follow = random.random() if res_score < 0.40: @@ -981,83 +1107,101 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session else: profile_learning_chance = float(getattr(configs.args, "profile_learning_percentage", 0)) / 100.0 rnd_profile_learn = random.random() - - will_visit_profile = res_score >= 0.8 or (follow_chance_val > 0.0 and rnd_follow < follow_chance_val) or (profile_learning_chance > 0.0 and rnd_profile_learn < profile_learning_chance) - - logger.info(f"⚙️ [Decision] Profile Visit -> Resonance: {res_score:.2f} (>=0.8?), Follow Config: {follow_chance_val*100}% (Roll: {rnd_follow:.2f}) -> Proceed: {will_visit_profile}") - + + will_visit_profile = ( + res_score >= 0.8 + or (follow_chance_val > 0.0 and rnd_follow < follow_chance_val) + or (profile_learning_chance > 0.0 and rnd_profile_learn < profile_learning_chance) + ) + + logger.info( + f"⚙️ [Decision] Profile Visit -> Resonance: {res_score:.2f} (>=0.8?), Follow Config: {follow_chance_val*100}% (Roll: {rnd_follow:.2f}) -> Proceed: {will_visit_profile}" + ) + if will_visit_profile: - logger.info(f"🕵️‍♂️ [Profile Learning] Visiting @{target_user}'s profile to learn context or follow...", extra={"color": f"{Fore.CYAN}"}) + logger.info( + f"🕵️‍♂️ [Profile Learning] Visiting @{target_user}'s profile to learn context or follow...", + extra={"color": f"{Fore.CYAN}"}, + ) # Navigate to profile via Targeted UX to prevent clicking Ads nav_success = False telepathic = cognitive_stack.get("telepathic") crm = cognitive_stack.get("crm") - + if telepathic: xml_dump = device.dump_hierarchy() nodes = telepathic._extract_semantic_nodes(xml_dump) - + # Targeted check for the actual user to avoid hallucinated Ad clicks (e.g. 'raidrpg') for n in nodes: res_id = n.get("resource_id", "").lower() text_lower = (n.get("text", "") or n.get("content_desc", "")).lower() - if target_user.lower() in text_lower and ("profile_name" in res_id or "title" in res_id or "username" in res_id or "avatar" in res_id): + if target_user.lower() in text_lower and ( + "profile_name" in res_id or "title" in res_id or "username" in res_id or "avatar" in res_id + ): if n.get("x") and n.get("y"): - logger.info(f"⚡ [Targeted UX] Exact matched username '{target_user}' on screen. Tapping directly.") + logger.info( + f"⚡ [Targeted UX] Exact matched username '{target_user}' on screen. Tapping directly." + ) device.click(n["x"], n["y"]) nav_success = True break - + if not nav_success: - logger.info(f"⚠️ [Targeted UX] Could not find explicit text for '{target_user}'. Falling back to generalized intent...") + logger.info( + f"⚠️ [Targeted UX] Could not find explicit text for '{target_user}'. Falling back to generalized intent..." + ) nav_success = nav_graph.do("tap post username") - + if nav_success: _wait_for_profile_loaded(device, timeout=5) sleep(random.uniform(0.5, 1.0) * sleep_mod) - + # Extract context try: if telepathic: # Fetch dump again post-navigation xml_dump = device.dump_hierarchy() nodes = telepathic._extract_semantic_nodes(xml_dump) - + texts = [] actual_username = None - + for n in nodes: t = n.get("text", "").strip() or n.get("content_desc", "").strip() res_id = n.get("resource_id", "").lower() - + # Identify the actual profile we landed on (e.g., from top action bar) if not actual_username and t and len(t) > 2: if "action_bar_title" in res_id or "profile_name" in res_id or "username" in res_id: actual_username = t.split("•")[0].strip() - + # Ignore small numbers, but keep bio/followers if t and t not in texts and len(t) > 1: texts.append(t) - + # Correct context if targeted UX failed and we landed on the wrong profile if actual_username and actual_username.lower() != target_user.lower(): logger.warning( f"⚠️ [Context Correction] Visited '{actual_username}' instead of '{target_user}'. Updating target...", - extra={"color": f"{Fore.YELLOW}"} + extra={"color": f"{Fore.YELLOW}"}, ) target_user = actual_username - + profile_context = " | ".join(texts[:15]) - logger.info(f"🧠 [Profile Learning] Extracted bio/stats: {profile_context[:50]}...", extra={"color": f"{Fore.GREEN}"}) - + logger.info( + f"🧠 [Profile Learning] Extracted bio/stats: {profile_context[:50]}...", + extra={"color": f"{Fore.GREEN}"}, + ) + if crm and target_user: crm.log_profile_context(target_user, profile_context) except Exception as e: logger.debug(f"Failed to learn profile context: {e}") - + # Execute Deep Profile Interaction (Likes, Follows, Stories) _interact_with_profile(device, configs, target_user, session_state, sleep_mod, logger, cognitive_stack) - + # Return to feed logger.info("🔙 [Profile Learning] Returning to main feed.") device.press("back") @@ -1065,41 +1209,54 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session sleep(random.uniform(1.0, 1.5) * sleep_mod) rnd_interact = random.random() - logger.info(f"⚙️ [Decision] Sub-Interactions (Likes/Comments) -> Interact Config: {interact_chance*100}% (Roll: {rnd_interact:.2f})") - + logger.info( + f"⚙️ [Decision] Sub-Interactions (Likes/Comments) -> Interact Config: {interact_chance*100}% (Roll: {rnd_interact:.2f})" + ) + if rnd_interact < interact_chance: likes_chance = float(getattr(configs.args, "likes_percentage", 100)) / 100.0 if session_state.check_limit(SessionState.Limit.LIKES): likes_chance = 0.0 - + # If user explicitly configures likes_chance > 0, we lower the strict AI resonance requirement rnd_like = random.random() - needs_like = (likes_chance > 0.0 and rnd_like < likes_chance) + needs_like = likes_chance > 0.0 and rnd_like < likes_chance will_like = needs_like or res_score >= 0.35 - + # Global Override: Passive Learning (Dry Run) if getattr(configs.args, "agent_strategy", "") == "passive_learning": - logger.info("🚫 [Safety Onboarding] Skipping Like action (Agent is learning the UI).", extra={"color": f"{Fore.MAGENTA}"}) + logger.info( + "🚫 [Safety Onboarding] Skipping Like action (Agent is learning the UI).", + extra={"color": f"{Fore.MAGENTA}"}, + ) will_like = False - - logger.info(f"⚙️ [Decision] Like -> Like Config: {likes_chance*100}% (Roll: {rnd_like:.2f}), Resonance: {res_score:.2f} -> Proceed: {will_like}") - + + logger.info( + f"⚙️ [Decision] Like -> Like Config: {likes_chance*100}% (Roll: {rnd_like:.2f}), Resonance: {res_score:.2f} -> Proceed: {will_like}" + ) + if will_like: logger.info("❤️ [Interaction] Deciding like method...") - + xml_check = device.dump_hierarchy() - if not isinstance(xml_check, str): xml_check = "" + if not isinstance(xml_check, str): + xml_check = "" xml_check_lower = xml_check.lower() - + is_reel_feed = "reel_viewer" in xml_check_lower or "clips_viewer" in xml_check_lower - is_liked_feed = "gefällt mir nicht mehr" in xml_check_lower or "unlike" in xml_check_lower or 'content-desc="liked"' in xml_check_lower - + is_liked_feed = ( + "gefällt mir nicht mehr" in xml_check_lower + or "unlike" in xml_check_lower + or 'content-desc="liked"' in xml_check_lower + ) + use_double_tap = growth.wants_to_double_tap(is_reel=is_reel_feed) - did_like = False - + if use_double_tap: if is_liked_feed: - logger.debug("Telepathic Like failed or post was unlikable (already liked). Skipping increment.") + logger.debug( + "Telepathic Like failed or post was unlikable (already liked). Skipping increment." + ) else: info = device.get_info() w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400) @@ -1110,7 +1267,6 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session session_state.totalLikes += 1 sleep(random.uniform(1.2, 2.5) * sleep_mod) did_interact = True - did_like = True else: logger.info("❤️ [Interaction] Liking post via Heart Button...") success = nav_graph.do("tap like button") @@ -1118,41 +1274,45 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session session_state.totalLikes += 1 sleep(random.uniform(1.2, 2.5) * sleep_mod) did_interact = True - did_like = True else: logger.debug("Telepathic Like failed or post was unlikable. Skipping increment.") - # Comment: requires high resonance alignment comment_chance = float(getattr(configs.args, "comment_percentage", 40)) / 100.0 if session_state.check_limit(SessionState.Limit.COMMENTS): comment_chance = 0.0 - + # If user explicitly configures comment_chance > 0, we lower the strict AI resonance requirement rnd_comment = random.random() - needs_comment = (comment_chance > 0.0 and rnd_comment < comment_chance) + needs_comment = comment_chance > 0.0 and rnd_comment < comment_chance will_comment = needs_comment or res_score >= 0.4 - + # Global Override: Passive Learning (Dry Run) if getattr(configs.args, "agent_strategy", "") == "passive_learning": - logger.info("🚫 [Safety Onboarding] Skipping Comment action (Agent is learning the UI).", extra={"color": f"{Fore.MAGENTA}"}) + logger.info( + "🚫 [Safety Onboarding] Skipping Comment action (Agent is learning the UI).", + extra={"color": f"{Fore.MAGENTA}"}, + ) will_comment = False - - logger.info(f"⚙️ [Decision] Comment -> Comment Config: {comment_chance*100}% (Roll: {rnd_comment:.2f}), Resonance: {res_score:.2f} -> Proceed: {will_comment}") - - if will_comment: + logger.info( + f"⚙️ [Decision] Comment -> Comment Config: {comment_chance*100}% (Roll: {rnd_comment:.2f}), Resonance: {res_score:.2f} -> Proceed: {will_comment}" + ) + + if will_comment: logger.info("💬 [Interaction] Entering Comment Sheet for deep engagement...") success = nav_graph.do("tap comment button") if success is True: sleep(random.uniform(2.0, 4.0) * sleep_mod) - + # 1. Scrape Context from the comment sheet sheet_xml = device.dump_hierarchy() - + # 🛡️ [Semantic Gate] Verify we are actually in the comment sheet via basic semantic checks if not any(x in sheet_xml.lower() for x in ["comment", "reply", "kommentieren", "antworten"]): - logger.warning("❌ [Ambiguity Guard] Transition reported success, but Comment markers not found in UI. Bailing engagement.") + logger.warning( + "❌ [Ambiguity Guard] Transition reported success, but Comment markers not found in UI. Bailing engagement." + ) did_interact = False _humanized_scroll(device) continue @@ -1166,102 +1326,130 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session text = node.get("original_attribs", {}).get("text", "") # If it's a substantive string (e.g., > 10 chars) and isn't a UI button if text and len(text) > 10 and not telepathic._is_forbidden_action(node): - if not any(k in text.lower() for k in ["reply", "translate", "view replies", "see translation", "hide replies", "comment"]): + if not any( + k in text.lower() + for k in [ + "reply", + "translate", + "view replies", + "see translation", + "hide replies", + "comment", + ] + ): existing_comments.append(text) - comment_nodes.append({ - "text": text, - "semantic_string": node.get("semantic_string") - }) + comment_nodes.append({"text": text, "semantic_string": node.get("semantic_string")}) except Exception as e: logger.error(f"Failed to extract comments semantically: {e}") - + # --- Deep Engagement Actions (Liking and Sub-Commenting) --- replying_to = None telepathic = TelepathicEngine.get_instance() try: for idx, c_node in enumerate(comment_nodes): - if len(c_node["text"]) > 15: # Filter out short garbage + if len(c_node["text"]) > 15: # Filter out short garbage # 40% chance to like a substantive comment if random.random() < 0.4: # Use Telepathic to find the like button for this specific comment text intent = f"Heart like button for comment: '{c_node['text'][:20]}...'" xml_dump = device.dump_hierarchy() like_btn = telepathic.find_best_node(xml_dump, intent, device=device) - + if like_btn and not like_btn.get("skip"): _humanized_click(device, like_btn["x"], like_btn["y"], sleep_mod=sleep_mod) sleep(random.uniform(0.8, 1.5)) - + # Verification: Simple XML change check if device.dump_hierarchy() != xml_dump: telepathic.confirm_click(intent) - logger.info(f"❤️ [Interaction] Liked user comment: '{c_node['text'][:30]}...'") + logger.info( + f"❤️ [Interaction] Liked user comment: '{c_node['text'][:30]}...'" + ) else: telepathic.reject_click(intent) - + # 20% chance to randomly visit commenter's profile # [Phase 3] Deep engagement decision if resonance.wants_to_deep_engage(res_score): intent = f"Avatar profile picture for commenter: '{c_node['text'][:20]}...'" xml_dump = device.dump_hierarchy() avatar_node = telepathic.find_best_node(xml_dump, intent, device=device) - + if avatar_node: - logger.info(f"🦸‍♂️ [Randomization] Navigating to commenter's profile to explore...") - _humanized_click(device, avatar_node["x"], avatar_node["y"], sleep_mod=sleep_mod) + logger.info( + "🦸‍♂️ [Randomization] Navigating to commenter's profile to explore..." + ) + _humanized_click( + device, avatar_node["x"], avatar_node["y"], sleep_mod=sleep_mod + ) sleep(random.uniform(2.5, 4.5) * sleep_mod) - + # Verification: Did we reach a profile? post_xml = device.dump_hierarchy() if "profile" in post_xml.lower() or "button_follow" in post_xml.lower(): telepathic.confirm_click(intent) - _interact_with_profile(device, configs, "commenter", session_state, sleep_mod, logger, cognitive_stack) + _interact_with_profile( + device, + configs, + "commenter", + session_state, + sleep_mod, + logger, + cognitive_stack, + ) logger.info("🔙 [Randomization] Returning to comment sheet.") device.press("back") sleep(random.uniform(1.5, 3.0) * sleep_mod) else: telepathic.reject_click(intent) - logger.warning("⚠️ [Randomization] Failed to reach commenter profile. Learning from failure.") - + logger.warning( + "⚠️ [Randomization] Failed to reach commenter profile. Learning from failure." + ) + # 15% chance to Sub-Comment (Reply) # [Phase 3] Reply decision if resonance.wants_to_reply(res_score) and not replying_to: intent = f"Reply button for comment: '{c_node['text'][:20]}...'" xml_dump = device.dump_hierarchy() reply_btn = telepathic.find_best_node(xml_dump, intent, device=device) - + if reply_btn: _humanized_click(device, reply_btn["x"], reply_btn["y"], sleep_mod=sleep_mod) sleep(random.uniform(1.2, 2.0)) - + # Verification: Did the screen change or input field appear? if device.dump_hierarchy() != xml_dump: telepathic.confirm_click(intent) replying_to = c_node["text"] - logger.info(f"🔁 [Interaction] Replying directly to comment: '{replying_to[:30]}...'") + logger.info( + f"🔁 [Interaction] Replying directly to comment: '{replying_to[:30]}...'" + ) else: telepathic.reject_click(intent) except Exception as e: logger.debug(f"[Interaction] Deep engagement parsing failed: {e}") - + # [Phase 2] Determine Suggested Action based on Resonance + CRM suggested_action = resonance.get_suggested_action(post_data.get("username"), res_score) - logger.info(f"🧠 [Governance] CRM/Resonance Suggestion: {suggested_action} (Stage: {resonance.crm.get_relationship_stage(post_data.get('username')).get('stage', 0)})") - + logger.info( + f"🧠 [Governance] CRM/Resonance Suggestion: {suggested_action} (Stage: {resonance.crm.get_relationship_stage(post_data.get('username')).get('stage', 0)})" + ) + # Decide if we proceed with commenting - skip_comment = (suggested_action == "SKIP" or (suggested_action == "LIKE" and random.random() < 0.9)) + skip_comment = suggested_action == "SKIP" or (suggested_action == "LIKE" and random.random() < 0.9) if skip_comment: - logger.info("🧠 [Governance] Decision: Relationship not warm enough for comment. Skipping.") + logger.info("🧠 [Governance] Decision: Relationship not warm enough for comment. Skipping.") else: - # 2. Contextual Prompting context_str = "\\n- ".join(existing_comments[:3]) vibe = getattr(configs.args, "ai_vibe", "friendly") - + # Persona & CRM Context injection persona_context = growth.get_persona_context() if growth else "" - crm_context = resonance.crm.get_conversation_context(post_data.get("username")) if resonance.crm else "" - + crm_context = ( + resonance.crm.get_conversation_context(post_data.get("username")) if resonance.crm else "" + ) + if replying_to: prompt = ( f"Reply to this Instagram comment as a '{vibe}' person.\n" @@ -1283,32 +1471,45 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session "Ask a question or share a specific observation. No generic phrases.\n" "Output ONLY the comment text, nothing else." ) - + try: from GramAddict.core.llm_provider import query_llm from GramAddict.core.stealth_typing import ghost_type - + model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b") url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate") logger.info(f"🧠 [Comment Gen] Sending prompt to {model} (Timeout: 120s)...") - response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=False, timeout=120, max_tokens=60, temperature=0.7) - + response_dict = query_llm( + url=url, + model=model, + prompt=prompt, + format_json=False, + timeout=120, + max_tokens=60, + temperature=0.7, + ) + if response_dict and "response" in response_dict: clean_comment = response_dict["response"].strip().strip('"').strip("'") if clean_comment and len(clean_comment) > 2: # Tap the Edit Text field to focus keyboard telepathic = cognitive_stack.get("telepathic") if telepathic: - comment_box = telepathic.find_best_node(sheet_xml, "Comment input text box editfield", device=device) + comment_box = telepathic.find_best_node( + sheet_xml, "Comment input text box editfield", device=device + ) if comment_box: is_dry = getattr(configs.args, "dry_run_comments", False) if is_dry: - logger.info(f"🚫 [DRY RUN] Generated comment: '{clean_comment}'. Skipping UI injection.", extra={"color": f"{Fore.MAGENTA}"}) + logger.info( + f"🚫 [DRY RUN] Generated comment: '{clean_comment}'. Skipping UI injection.", + extra={"color": f"{Fore.MAGENTA}"}, + ) sleep(1.5) else: device.click(comment_box["x"], comment_box["y"]) sleep(random.uniform(1.2, 2.2)) - + # Verification: Did the keyboard open or cursor move to box? # We check if the XML changed and focus is on an edittext post_focus_xml = device.dump_hierarchy() @@ -1316,14 +1517,17 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session telepathic.confirm_click("Comment input text box editfield") else: telepathic.reject_click("Comment input text box editfield") - + # Inject via Ghost Keyboard ghost_type(device, clean_comment) - + # Umentscheidung (Change of mind) # Umentscheidung (Change of mind / Hesitation) [Phase 3] if growth.evaluate_hesitation(): - logger.info("🧠 [Umentscheidung] Hesitating. Deciding not to post the comment.", extra={"color": f"{Fore.YELLOW}"}) + logger.info( + "🧠 [Umentscheidung] Hesitating. Deciding not to post the comment.", + extra={"color": f"{Fore.YELLOW}"}, + ) sleep(random.uniform(1.0, 3.0)) if random.random() < 0.5: # Rapid backspace (Manual deletion) @@ -1335,40 +1539,57 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session device.press("back") sleep(1.0) xml_dump = device.dump_hierarchy() - discard_btn = telepathic.find_best_node(xml_dump, "Discard or Verwerfen popup button to cancel comment", device=device) + discard_btn = telepathic.find_best_node( + xml_dump, + "Discard or Verwerfen popup button to cancel comment", + device=device, + ) if discard_btn: device.click(discard_btn["x"], discard_btn["y"]) - telepathic.confirm_click("Discard or Verwerfen popup button to cancel comment") - + telepathic.confirm_click( + "Discard or Verwerfen popup button to cancel comment" + ) + logger.info("🔙 [Umentscheidung] Comment successfully aborted.") sleep(2.0) else: # Tap Post sleep(random.uniform(0.5, 1.5)) pre_post_xml = device.dump_hierarchy() - post_btn = telepathic.find_best_node(pre_post_xml, "Post submit comment button", device=device) + post_btn = telepathic.find_best_node( + pre_post_xml, "Post submit comment button", device=device + ) if post_btn: device.click(post_btn["x"], post_btn["y"]) sleep(random.uniform(2.0, 3.5)) - + # Verification: Did the button disappear or layout change? post_post_xml = device.dump_hierarchy() # If "Post" button is gone from the area or XML changed significantly - if "button_post" not in post_post_xml.lower() or post_post_xml != pre_post_xml: + if ( + "button_post" not in post_post_xml.lower() + or post_post_xml != pre_post_xml + ): telepathic.confirm_click("Post submit comment button") session_state.totalComments += 1 did_comment = True - logger.info(f"✅ [Interaction] Comment deployed successfully: '{clean_comment}'", extra={"color": f"{Fore.GREEN}"}) + logger.info( + f"✅ [Interaction] Comment deployed successfully: '{clean_comment}'", + extra={"color": f"{Fore.GREEN}"}, + ) else: telepathic.reject_click("Post submit comment button") - logger.warning("⚠️ [Comment] Post button click didn't seem to work. Learning from failure.") + logger.warning( + "⚠️ [Comment] Post button click didn't seem to work. Learning from failure." + ) except Exception as e: logger.error(f"❌ [Interaction] AI Comment deployment failed: {e}") - + # Safely exit the comment sheet from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType + sae = SituationalAwarenessEngine(device) - + _exit_xml = device.dump_hierarchy() if sae.perceive(_exit_xml) == SituationType.OBSTACLE_MODAL: device.press("back") @@ -1377,18 +1598,24 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session if sae.perceive(_exit_xml2) == SituationType.OBSTACLE_MODAL: device.press("back") sleep(1.0) - + did_interact = True # Repost: requires medium-high resonance alignment [Phase 3] if growth.wants_to_repost(res_score): logger.info("🔁 [Interaction] Reposting highly resonant content...", extra={"color": f"{Fore.CYAN}"}) - + # Fast Path: Check if Repost button is ALREADY on the screen (Direct Repost for Reels) telepathic = TelepathicEngine.get_instance() current_xml = device.dump_hierarchy() - direct_repost = telepathic.find_best_node(current_xml, "Repost interaction button with two arrows", device=device, threshold=0.90) if is_reels else None - + direct_repost = ( + telepathic.find_best_node( + current_xml, "Repost interaction button with two arrows", device=device, threshold=0.90 + ) + if is_reels + else None + ) + success = True if direct_repost and not direct_repost.get("skip"): logger.info("⚡ [Fast Path] Found direct Repost button. Skipping share sheet.") @@ -1398,51 +1625,61 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session if success is True: sleep(random.uniform(1.8, 3.5) * sleep_mod) xml_dump = device.dump_hierarchy() - repost_btn = telepathic.find_best_node(xml_dump, "Repost interaction button with two arrows", device=device) + repost_btn = telepathic.find_best_node( + xml_dump, "Repost interaction button with two arrows", device=device + ) else: repost_btn = None if success is True and repost_btn and not repost_btn.get("skip"): _humanized_click(device, repost_btn["x"], repost_btn["y"], sleep_mod=sleep_mod) sleep(random.uniform(2.0, 4.0) * sleep_mod) - + # Verification: Did the share menu close or repost confirmation appear? post_xml = device.dump_hierarchy() repost_success = post_xml != current_xml or "reposted" in post_xml.lower() - + if repost_success: telepathic.confirm_click("Repost interaction button with two arrows") - logger.info("✅ [Interaction] Content successfully reposted to feed/followers.", extra={"color": f"{Fore.GREEN}"}) + logger.info( + "✅ [Interaction] Content successfully reposted to feed/followers.", + extra={"color": f"{Fore.GREEN}"}, + ) did_interact = True else: telepathic.reject_click("Repost interaction button with two arrows") logger.warning("⚠️ [Repost] Click failed to trigger repost. Learning from failure.") - + # Close share menu if still open device.press("back") sleep(random.uniform(1.0, 2.0) * sleep_mod) - # ── Parasocial CRM & SwarmProtocol ── crm = cognitive_stack.get("crm") - session_state.add_interaction(source=post_data.get('username', 'Unknown'), succeed=did_interact, followed=False, scraped=False) - + session_state.add_interaction( + source=post_data.get("username", "Unknown"), succeed=did_interact, followed=False, scraped=False + ) + if did_interact: - logger.info(f"DEBUG CRM logic: did_interact={did_interact}, crm={bool(crm)}, username='{post_data.get('username')}'") + logger.info( + f"DEBUG CRM logic: did_interact={did_interact}, crm={bool(crm)}, username='{post_data.get('username')}'" + ) if crm and post_data.get("username"): intent_type = "comment_reply" if did_comment else "like" crm.log_interaction(post_data["username"], intent_type) - + if swarm: post_hash = f"{post_data['username']}_{post_data['description'][:30]}" swarm.emit_pheromone(post_hash, "interacted") - + # ── Track outcome for GrowthBrain session learning ── - session_outcomes.append({ - "username": post_data.get("username", ""), - "action": "interact" if did_interact else "skip", - "resonance": res_score - }) + session_outcomes.append( + { + "username": post_data.get("username", ""), + "action": "interact" if did_interact else "skip", + "resonance": res_score, + } + ) # ── Active Inference: Evaluate prediction (after action) ── if ai: @@ -1454,49 +1691,52 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session # ── Advance to next post ── _humanized_scroll(device, resonance_score=res_score) sleep(random.uniform(0.5, 1.2) * sleep_mod) - + # ── End of session: Store learnings ── if growth: growth.refine_persona(session_outcomes) - + if darwin: now = datetime.now() duration_minutes = (now - session_state.startTime).total_seconds() / 60.0 followers_gained = sum(session_state.totalFollowed.values()) darwin.evaluate_session_end(duration_minutes, followers_gained) - + logger.info("🏁 [Drive] Feed loop terminated. Session over.") return "FEED_EXHAUSTED" -def _run_zero_latency_search_loop(device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack): +def _run_zero_latency_search_loop( + device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack +): """ Executes the autonomous Search & Interact logic. """ logger.info("🧠 [Search Engine] Initiating keyword discovery...", extra={"color": f"{Style.BRIGHT}{Fore.CYAN}"}) - + import random - from GramAddict.core.bot_flow import sleep, _humanized_click + + from GramAddict.core.bot_flow import _humanized_click, sleep from GramAddict.core.stealth_typing import ghost_type - + # Select search term search_str = getattr(configs.args, "search", "") interests_str = getattr(configs.args, "persona_interests", "") - + all_terms = [t.strip() for t in (search_str + "," + interests_str).split(",") if t.strip()] if not all_terms: - all_terms = ["photography", "travel", "nature"] # Fail-safe - + all_terms = ["photography", "travel", "nature"] # Fail-safe + keyword = random.choice(all_terms) logger.info(f"🔎 Searching for keyword: '{keyword}'") - + # 1. Navigation to Search is handled by nav_graph.navigate_to("SearchFeed") or Explore # We assume we are on the Explore tab now (Global Navigation Bar) - + try: xml = device.dump_hierarchy() telepathic = cognitive_stack.get("telepathic") - + # Find search bar search_bar = telepathic.find_best_node(xml, "Search edit text box or magnifying glass input", device=device) if search_bar: @@ -1505,20 +1745,24 @@ def _run_zero_latency_search_loop(device, zero_engine, nav_graph, configs, sessi ghost_type(device, keyword, speed="fast") device.press("enter") sleep(3.0) - + # 2. Pick a result (Top, Accounts, or Tags) results_xml = device.dump_hierarchy() - target_result = telepathic.find_best_node(results_xml, "First relevant search result (Account or Hashtag)", device=device) - + target_result = telepathic.find_best_node( + results_xml, "First relevant search result (Account or Hashtag)", device=device + ) + if target_result: logger.info(f"👉 Selecting search result: {target_result.get('semantic', 'Top result')}") _humanized_click(device, target_result["x"], target_result["y"]) sleep(3.0) - + # 3. If we are on a hashtag feed or account profile, start a mini-feed loop # We reuse _run_zero_latency_feed_loop but with a special boredom multiplier - return _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, f"Search:{keyword}", cognitive_stack) - + return _run_zero_latency_feed_loop( + device, zero_engine, nav_graph, configs, session_state, f"Search:{keyword}", cognitive_stack + ) + return "BOREDOM_CHANGE_FEED" except Exception as e: logger.error(f"⚠️ [Search Engine] Failed: {e}") diff --git a/GramAddict/core/telepathic_engine.py b/GramAddict/core/telepathic_engine.py index 9e55b26..dfc7ba1 100644 --- a/GramAddict/core/telepathic_engine.py +++ b/GramAddict/core/telepathic_engine.py @@ -224,6 +224,8 @@ class TelepathicEngine: "class_name": class_name, "naf": attrib.get("NAF", "false").lower() == "true", "selected": attrib.get("selected", "false").lower() == "true", + "text": text, + "content_desc": content_desc, "original_attribs": {"text": text, "desc": content_desc}, } diff --git a/scripts/pre_commit_tests.sh b/scripts/pre_commit_tests.sh index 28da769..c7d3c48 100755 --- a/scripts/pre_commit_tests.sh +++ b/scripts/pre_commit_tests.sh @@ -2,11 +2,46 @@ set -e echo "========================================" -echo "🧪 Running Fast Unit Tests & Coverage" +echo "⚡ Fast Pre-Commit Tests & Coverage" echo "========================================" -# Run only unit tests to keep it fast, generate coverage XML for diff-cover -venv/bin/pytest tests/unit --cov=GramAddict --cov-report=xml -q +# Pre-commit passes staged files as arguments +STAGED_FILES="$@" +TEST_TARGETS="" + +if [ -z "$STAGED_FILES" ]; then + echo "No files provided. Running default unit tests." + TEST_TARGETS="tests/unit" +else + for file in $STAGED_FILES; do + if [[ "$file" == tests/* ]]; then + TEST_TARGETS="$TEST_TARGETS $file" + elif [[ "$file" == GramAddict/* ]]; then + filename=$(basename "$file") + # Heuristic: Try to find a matching unit test + test_file="tests/unit/test_${filename}" + if [ -f "$test_file" ]; then + TEST_TARGETS="$TEST_TARGETS $test_file" + else + # If no direct unit test, fallback to running all unit tests to be safe + echo "⚠️ No direct unit test found for $file, falling back to all unit tests." + TEST_TARGETS="tests/unit" + break + fi + fi + done +fi + +# Trim whitespace +TEST_TARGETS=$(echo "$TEST_TARGETS" | xargs) + +if [ -z "$TEST_TARGETS" ]; then + echo "No Python files changed that require testing. Skipping." + exit 0 +fi + +echo "🧪 Running tests on: $TEST_TARGETS" +venv/bin/pytest $TEST_TARGETS --cov=GramAddict --cov-report=xml -q echo "" echo "========================================" @@ -22,7 +57,7 @@ if ! git rev-parse --verify "$COMPARE_BRANCH" >/dev/null 2>&1; then COMPARE_BRANCH="HEAD" fi -# Run diff-cover requiring 100% coverage on new/changed lines +# Run diff-cover requiring 30% coverage on new/changed lines venv/bin/diff-cover coverage.xml --compare-branch=$COMPARE_BRANCH --fail-under=30 -echo "✅ All tests passed and coverage is 100% on new lines!" +echo "✅ All targeted tests passed and coverage is sufficient on new lines!"