1795 lines
87 KiB
Python
1795 lines
87 KiB
Python
import logging
|
|
import random
|
|
from datetime import datetime
|
|
from time import sleep
|
|
|
|
from colorama import Fore, Style
|
|
|
|
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.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.growth_brain import GrowthBrain
|
|
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,
|
|
)
|
|
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,
|
|
)
|
|
from GramAddict.core.physics.timing import (
|
|
wait_for_profile_loaded as _wait_for_profile_loaded_impl,
|
|
)
|
|
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
|
|
|
|
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_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}'...")
|
|
telepathic.wipe()
|
|
# Wipe navigation paths too
|
|
try:
|
|
from GramAddict.core.goap import PathMemory
|
|
|
|
path_mem = PathMemory(username)
|
|
path_mem.wipe()
|
|
logger.info("🗑️ Wiped PathMemory collection.")
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Failed to wipe PathMemory: {e}")
|
|
|
|
cognitive_stack = {
|
|
"active_inference": active_inference,
|
|
"dopamine": dopamine,
|
|
"swarm": swarm,
|
|
"resonance": resonance_oracle,
|
|
"growth_brain": growth_brain,
|
|
"radome": radome,
|
|
"nav_graph": nav_graph,
|
|
"zero_engine": zero_engine,
|
|
"telepathic": telepathic,
|
|
"darwin": darwin,
|
|
"crm": crm_db,
|
|
}
|
|
|
|
from GramAddict.core.behaviors import PluginRegistry
|
|
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
|
|
|
|
PluginRegistry.reset()
|
|
plugin_registry = PluginRegistry.get_instance()
|
|
plugin_registry.register(ProfileGuardPlugin())
|
|
plugin_registry.register(StoryViewPlugin())
|
|
plugin_registry.register(FollowPlugin())
|
|
plugin_registry.register(GridLikePlugin())
|
|
plugin_registry.register(CarouselBrowsingPlugin())
|
|
|
|
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)
|
|
inside_working_hours, time_left = SessionState.inside_working_hours(
|
|
configs.args.working_hours, configs.args.time_delta_session
|
|
)
|
|
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"))
|
|
+ " --------",
|
|
extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"},
|
|
)
|
|
|
|
if open_instagram(device, force_restart=False):
|
|
if is_first_session:
|
|
# Do not blindly assume we are on HomeFeed if the app was already open somewhere else.
|
|
# 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)
|
|
logger.debug(f"Instagram version: {running_ig_version}")
|
|
except Exception as e:
|
|
logger.error(f"Error retrieving the IG version: {e}")
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# 🤖 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')}"
|
|
)
|
|
|
|
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}"},
|
|
)
|
|
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)
|
|
|
|
# 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}"},
|
|
)
|
|
for _ in range(3):
|
|
post_xml = device.dump_hierarchy()
|
|
if isinstance(post_xml, str):
|
|
post_data = _extract_post_content(post_xml)
|
|
if post_data.get("caption"):
|
|
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": "<value>", "vibe": "<value>"}'
|
|
)
|
|
|
|
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]
|
|
)
|
|
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)
|
|
except Exception as e:
|
|
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)
|
|
random_sleep(2.0, 4.0)
|
|
device.app_start(device.app_id, use_monkey=True)
|
|
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"],
|
|
}
|
|
|
|
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:
|
|
logger.warning("❌ Post failed to open from grid. Retrying next loop.")
|
|
continue
|
|
elif current_target == "StoriesFeed":
|
|
logger.info("📱 Locating story tray on HomeFeed...")
|
|
nav_graph.do("tap story ring avatar")
|
|
post_loaded = _wait_for_story_loaded(device, timeout=5)
|
|
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
|
|
)
|
|
elif current_target == "MessageInbox":
|
|
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
|
|
)
|
|
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,
|
|
)
|
|
|
|
# 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()
|
|
|
|
elif result == "CONTEXT_LOST":
|
|
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
|
|
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:
|
|
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)
|
|
|
|
|
|
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 = {}
|
|
|
|
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)
|
|
|
|
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}"}
|
|
)
|
|
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}"},
|
|
)
|
|
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": "\\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)
|
|
|
|
if vibe_result:
|
|
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')}"
|
|
)
|
|
return
|
|
else:
|
|
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",
|
|
}
|
|
|
|
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)
|
|
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,
|
|
session_state=session_state,
|
|
cognitive_stack=cognitive_stack,
|
|
context_xml=xml_check,
|
|
sleep_mod=sleep_mod,
|
|
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("⏭️ 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:
|
|
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
|
|
Mimics a user opening the story tray and endlessly tapping through stories.
|
|
Relies on DopamineEngine for early exit (boredom).
|
|
"""
|
|
import random
|
|
from time import sleep
|
|
|
|
from colorama import Fore
|
|
|
|
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("-"))
|
|
limit = random.randint(min_st, max_st)
|
|
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
|
|
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": "\\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
|
|
):
|
|
"""
|
|
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
|
|
- GrowthBrain applies circadian pacing → modulates ALL sleep durations
|
|
- Darwin is the SOLE dwell controller → no duplicate sleep calls
|
|
- 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
|
|
while not dopamine.is_app_session_over():
|
|
limit_tuple = session_state.check_limit(SessionState.Limit.ALL)
|
|
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))
|
|
else:
|
|
nav_graph.do("tap heart icon notifications")
|
|
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))
|
|
logger.info("🔙 [Curiosity] Done exploring. Returning to feed.")
|
|
|
|
# ── Circadian Pacing (GrowthBrain) ──
|
|
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 (3-Tier Escape Cascade) ──
|
|
if is_ad(context_xml, cognitive_stack):
|
|
consecutive_ads += 1
|
|
if consecutive_ads >= 6:
|
|
logger.error(
|
|
"🚨 [Ad Trap] Stuck on ad for 6+ cycles! Force-navigating to HomeFeed to escape deadlock.",
|
|
extra={"color": f"{Fore.RED}"},
|
|
)
|
|
nav_graph.navigate_to("HomeFeed", zero_engine)
|
|
consecutive_ads = 0
|
|
elif consecutive_ads >= 3:
|
|
logger.warning(
|
|
"📺 [Anti-Stuck] Stuck on ad! Executing aggressive double-skip.",
|
|
extra={"color": f"{Fore.RED}"},
|
|
)
|
|
_humanized_scroll(device, is_skip=True)
|
|
sleep(0.5)
|
|
_humanized_scroll(device, is_skip=True)
|
|
else:
|
|
logger.info("📺 fast-skipping ad (no AI needed)...")
|
|
_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": "\\033[32m"},
|
|
)
|
|
_humanized_scroll(device, is_skip=True)
|
|
sleep(random.uniform(0.5, 1.0) * sleep_mod)
|
|
continue
|
|
|
|
# ── Zero-Node Recovery (Graceful Degradation) ──
|
|
telepathic = TelepathicEngine.get_instance()
|
|
interactive_nodes = telepathic._extract_semantic_nodes(context_xml)
|
|
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}"},
|
|
)
|
|
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.")
|
|
sae.unlearn_current_state(context_xml)
|
|
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}"},
|
|
)
|
|
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.")
|
|
sae.unlearn_current_state(context_xml)
|
|
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)
|
|
|
|
# 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):
|
|
logger.info("✅ [Recovery] Obstacle cleared successfully. Learning this button works.")
|
|
telepathic.confirm_click("Dismiss Obstacle/Modal")
|
|
consecutive_marker_misses = 0
|
|
continue
|
|
else:
|
|
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)
|
|
continue
|
|
|
|
logger.warning(
|
|
"⚠️ [Self-Check] Feed markers missing. Mid-scroll or tall post? Scrolling to reveal markers...",
|
|
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):
|
|
# Update context_xml because the screen just shifted
|
|
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? ──
|
|
|
|
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}"}
|
|
)
|
|
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}"},
|
|
)
|
|
|
|
# ── Execute Plugin Registry Behaviors (Feed Level) ──
|
|
from GramAddict.core.behaviors import BehaviorContext, PluginRegistry
|
|
|
|
ctx = BehaviorContext(
|
|
device=device,
|
|
configs=configs,
|
|
session_state=session_state,
|
|
cognitive_stack=cognitive_stack,
|
|
context_xml=context_xml,
|
|
sleep_mod=sleep_mod,
|
|
post_data=post_data,
|
|
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)
|
|
continue
|
|
# ── Active Inference: Predict (before action) ──
|
|
if ai:
|
|
ai.predict_state(["row_feed", "button_like"])
|
|
|
|
# ── Ad Check (Structural) ──
|
|
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..."
|
|
)
|
|
_humanized_scroll(device)
|
|
consecutive_ads = 0
|
|
else:
|
|
logger.info("⏭️ [Ad Skip] Detected sponsored content. Skipping interaction.")
|
|
_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:
|
|
telepathic = cognitive_stack.get("telepathic")
|
|
persona_interests = cognitive_stack.get("persona_interests", [])
|
|
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
|
|
# 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})"
|
|
)
|
|
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
|
|
# ── 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"}
|
|
)
|
|
|
|
# ── 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})"
|
|
)
|
|
|
|
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}
|
|
)
|
|
_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}"},
|
|
)
|
|
if nav_graph.do("tap post username") is True:
|
|
sleep(random.uniform(1.2, 2.5) * sleep_mod)
|
|
_humanized_scroll(device, is_skip=True)
|
|
sleep(random.uniform(0.5, 1.5) * sleep_mod)
|
|
logger.info("🔙 [Rabbit Hole] Exiting profile back to main feed.")
|
|
device.press("back")
|
|
sleep(random.uniform(0.8, 1.5) * sleep_mod)
|
|
|
|
# ── Darwin: SOLE Dwell Controller (micro-wobble + proof of resonance) ──
|
|
# Darwin handles ALL viewing time, scrolling, and wobble. No duplicate sleep.
|
|
if darwin:
|
|
darwin.execute_micro_wobble(device)
|
|
darwin.execute_proof_of_resonance(
|
|
device,
|
|
res_score,
|
|
text_length=len(post_data.get("description", "")),
|
|
nav_graph=nav_graph,
|
|
zero_engine=zero_engine,
|
|
configs=configs,
|
|
resonance_oracle=resonance,
|
|
username=post_data.get("username", "unknown"),
|
|
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_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")
|
|
|
|
# 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:
|
|
will_visit_profile = False
|
|
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}"
|
|
)
|
|
|
|
if will_visit_profile:
|
|
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()
|
|
|
|
# 🛡️ Hardened Targeted UX: Use strict equality or boundary checks if possible, or exact substring.
|
|
if target_user.lower() in text_lower.split() or target_user.lower() == text_lower:
|
|
if (
|
|
"profile_name" in res_id
|
|
or "title" in res_id
|
|
or "username" in res_id
|
|
or "avatar" in res_id
|
|
or not res_id
|
|
):
|
|
if n.get("x") and n.get("y"):
|
|
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..."
|
|
)
|
|
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:
|
|
# 🛡️ Hardened Context Correction: Expand matching IDs for the profile action bar
|
|
if (
|
|
"action_bar" in res_id
|
|
or "profile_name" in res_id
|
|
or "username" in res_id
|
|
or "title" in res_id
|
|
):
|
|
if n.get("y", 999) < 300: # Must be at the top of the screen
|
|
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}"},
|
|
)
|
|
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}"},
|
|
)
|
|
|
|
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")
|
|
_wait_for_post_loaded(device, nav_graph=nav_graph)
|
|
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})"
|
|
)
|
|
|
|
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
|
|
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}"},
|
|
)
|
|
will_like = False
|
|
|
|
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 = ""
|
|
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
|
|
)
|
|
|
|
use_double_tap = growth.wants_to_double_tap(is_reel=is_reel_feed)
|
|
|
|
if use_double_tap:
|
|
if is_liked_feed:
|
|
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)
|
|
offset_x = random.randint(int(w * 0.2), int(w * 0.8))
|
|
offset_y = random.randint(int(h * 0.3), int(h * 0.7))
|
|
logger.info(f"❤️ [Interaction] Double-Tapping organically at ({offset_x}, {offset_y})")
|
|
_humanized_click(device, offset_x, offset_y, double=True, sleep_mod=sleep_mod)
|
|
session_state.totalLikes += 1
|
|
sleep(random.uniform(1.2, 2.5) * sleep_mod)
|
|
did_interact = True
|
|
else:
|
|
logger.info("❤️ [Interaction] Liking post via Heart Button...")
|
|
success = nav_graph.do("tap like button")
|
|
if success:
|
|
session_state.totalLikes += 1
|
|
sleep(random.uniform(1.2, 2.5) * sleep_mod)
|
|
did_interact = 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
|
|
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}"},
|
|
)
|
|
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("💬 [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."
|
|
)
|
|
did_interact = False
|
|
_humanized_scroll(device)
|
|
continue
|
|
|
|
existing_comments = []
|
|
comment_nodes = []
|
|
telepathic = TelepathicEngine.get_instance()
|
|
try:
|
|
all_nodes = telepathic._extract_semantic_nodes(sheet_xml)
|
|
for node in all_nodes:
|
|
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",
|
|
]
|
|
):
|
|
existing_comments.append(text)
|
|
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
|
|
# 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]}...'"
|
|
)
|
|
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(
|
|
"🦸♂️ [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,
|
|
)
|
|
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."
|
|
)
|
|
|
|
# 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]}...'"
|
|
)
|
|
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)})"
|
|
)
|
|
|
|
# Decide if we proceed with commenting
|
|
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.")
|
|
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 ""
|
|
)
|
|
|
|
if replying_to:
|
|
prompt = (
|
|
f"Reply to this Instagram comment as a '{vibe}' person.\n"
|
|
f"Context: {persona_context}\n"
|
|
f"Past history with user: {crm_context}\n"
|
|
f"Their comment: '{replying_to}'\n"
|
|
f"Post caption: {post_data.get('description', 'No caption')[:200]}\n\n"
|
|
"Write a natural reply under 15 words. Max 1 emoji. No generic phrases.\n"
|
|
"Output ONLY the comment text, nothing else."
|
|
)
|
|
else:
|
|
prompt = (
|
|
f"Write an Instagram comment as a '{vibe}' person.\n"
|
|
f"Context: {persona_context}\n"
|
|
f"Past history with user: {crm_context}\n"
|
|
f"Post by @{post_data.get('username')}: {post_data.get('description', 'No caption')[:200]}\n"
|
|
f"Other comments: {context_str[:300]}\n\n"
|
|
"Write a specific, insightful comment under 15 words. Max 1 emoji.\n"
|
|
"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,
|
|
)
|
|
|
|
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
|
|
)
|
|
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}"},
|
|
)
|
|
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()
|
|
if "editText" in post_focus_xml.lower() or post_focus_xml != sheet_xml:
|
|
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}"},
|
|
)
|
|
sleep(random.uniform(1.0, 3.0))
|
|
if random.random() < 0.5:
|
|
# Rapid backspace (Manual deletion)
|
|
for _ in range(len(clean_comment) + 2):
|
|
device.press("del")
|
|
sleep(random.uniform(0.01, 0.05))
|
|
else:
|
|
# Press back to trigger Discard popup
|
|
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,
|
|
)
|
|
if discard_btn:
|
|
device.click(discard_btn["x"], discard_btn["y"])
|
|
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
|
|
)
|
|
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
|
|
):
|
|
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}"},
|
|
)
|
|
else:
|
|
telepathic.reject_click("Post submit comment button")
|
|
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")
|
|
sleep(1.0)
|
|
_exit_xml2 = device.dump_hierarchy()
|
|
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
|
|
)
|
|
|
|
success = True
|
|
if direct_repost and not direct_repost.get("skip"):
|
|
logger.info("⚡ [Fast Path] Found direct Repost button. Skipping share sheet.")
|
|
repost_btn = direct_repost
|
|
else:
|
|
success = nav_graph.do("tap share button")
|
|
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
|
|
)
|
|
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}"},
|
|
)
|
|
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
|
|
)
|
|
|
|
if did_interact:
|
|
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,
|
|
}
|
|
)
|
|
|
|
# ── Active Inference: Evaluate prediction (after action) ──
|
|
if ai:
|
|
# Wait for content to settle
|
|
_wait_for_post_loaded(device, timeout=3, nav_graph=nav_graph)
|
|
post_action_xml = device.dump_hierarchy()
|
|
ai.evaluate_prediction(post_action_xml)
|
|
|
|
# ── 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
|
|
):
|
|
"""
|
|
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 _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
|
|
|
|
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:
|
|
_humanized_click(device, search_bar["x"], search_bar["y"])
|
|
sleep(1.5)
|
|
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
|
|
)
|
|
|
|
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 "BOREDOM_CHANGE_FEED"
|
|
except Exception as e:
|
|
logger.error(f"⚠️ [Search Engine] Failed: {e}")
|
|
return "CONTEXT_LOST"
|