This commit is contained in:
2026-04-17 00:44:41 +02:00
parent fa1d01527d
commit 89f14463c5
42 changed files with 2452 additions and 314 deletions

View File

@@ -5,7 +5,7 @@ from colorama import Fore, Style
logger = logging.getLogger(__name__)
BENCHMARKS_FILE = os.path.join(os.path.dirname(__file__), "llm_benchmarks.json")
BENCHMARKS_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "benchmarks", "data", "llm_benchmarks.json")
def check_model_benchmarks(configs):
"""

View File

@@ -164,7 +164,7 @@ def start_bot(**kwargs):
available_targets.append("ReelsFeed")
if getattr(configs.args, "stories", None):
available_targets.append("StoriesFeed")
if getattr(configs.args, "total_unfollows_limit", 0):
if getattr(configs.args, "smart_unfollow", False):
available_targets.append("FollowingList")
if not getattr(configs.args, "disable_ai_messaging", False):
available_targets.append("MessageInbox")
@@ -207,15 +207,33 @@ def start_bot(**kwargs):
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 == "BOREDOM_CHANGE_FEED":
if result in ("BOREDOM_CHANGE_FEED", "FEED_EXHAUSTED"):
if result == "FEED_EXHAUSTED":
logger.info(f"✅ Finished watching {current_target}. Removing from this session's options.")
if current_target in available_targets:
available_targets.remove(current_target)
# Find new targets excluding the current one
available_targets_copy = [t for t in available_targets if t != current_target]
current_target = secrets.choice(available_targets_copy) if available_targets_copy else current_target
logger.info(f"🧠 [Free Will] Spontaneous desire changed. Switching to {current_target}.")
if not available_targets_copy:
logger.info("🧠 Session natural conclusion: All desired feeds visited and exhausted.")
break # No more feeds to visit!
current_target = secrets.choice(available_targets_copy)
if result == "BOREDOM_CHANGE_FEED":
logger.info(f"🧠 [Free Will] Spontaneous desire changed. Switching to {current_target}. (Restoring Dopamine)")
dopamine.boredom = max(0.0, dopamine.boredom * 0.2) # Reset boredom so we actually execute the new feed!
else:
logger.info(f"🧠 [Free Will] Moving on to {current_target}.")
elif result == "CONTEXT_LOST":
logger.warning(f"⚠️ Context was lost in {current_target}. Forcing re-navigation to recover.")
nav_graph.current_state = "UNKNOWN"
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.")
@@ -462,37 +480,17 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod,
except:
count = 1
telepathic = TelepathicEngine.get_instance()
xml_dump = device.deviceV2.dump_hierarchy()
story_ring_node = telepathic.find_best_node(xml_dump, "Profile picture avatar story ring", device=device)
from GramAddict.core.q_nav_graph import QNavGraph
nav_graph = QNavGraph(device)
if story_ring_node and not story_ring_node.get("skip"):
_humanized_click(device, story_ring_node["x"], story_ring_node["y"], sleep_mod=sleep_mod)
sleep(random.uniform(2.5, 4.0) * sleep_mod)
# ── Post-Click Verification: Did the story viewer actually open? ──
post_xml = device.deviceV2.dump_hierarchy()
story_indicators = ["reel_viewer", "story_viewer", "reel_pager", "camera_settings", "story_media"]
story_opened = any(ind in post_xml for ind in story_indicators)
if not story_opened:
# Fallback: if profile elements (header/bio) disappeared, something opened
story_opened = "profile_header" in xml_dump and "profile_header" not in post_xml
if story_opened:
telepathic.confirm_click("Profile picture avatar story ring")
logger.info(f"📸 [Story] Viewing @{username}'s story ({count} times)...")
for i in range(count):
sleep(random.uniform(2.0, 5.0) * sleep_mod)
if i < count - 1:
_humanized_click(device, int(w * 0.9), int(h * 0.5), sleep_mod=sleep_mod)
device.deviceV2.press("back")
sleep(random.uniform(1.0, 2.0) * sleep_mod)
else:
telepathic.reject_click("Profile picture avatar story ring")
logger.warning(f"⚠️ [Story] Click did NOT open story viewer for @{username}. Learning from failure.")
device.deviceV2.press("back")
sleep(random.uniform(0.5, 1.0))
if nav_graph._execute_transition("tap_story_tray_item"):
logger.info(f"📸 [Story] Viewing @{username}'s story ({count} times)...")
for i in range(count):
sleep(random.uniform(2.0, 5.0) * sleep_mod)
if i < count - 1:
_humanized_click(device, int(w * 0.9), int(h * 0.5), sleep_mod=sleep_mod)
device.deviceV2.press("back")
sleep(random.uniform(1.0, 2.0) * sleep_mod)
# Random Follow
follow_pct = float(getattr(configs.args, "follow_percentage", 0)) / 100.0
@@ -501,23 +499,16 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod,
follow_pct = 0.0
if random.random() < follow_pct:
xml_dump = device.deviceV2.dump_hierarchy()
telepathic = TelepathicEngine.get_instance()
follow_btn = telepathic.find_best_node(xml_dump, "tap follow button on profile", device=device)
if follow_btn and not follow_btn.get("skip"):
_humanized_click(device, follow_btn["x"], follow_btn["y"], sleep_mod=sleep_mod)
sleep(random.uniform(2.0, 4.0) * sleep_mod)
# ── Post-Click Verification: Did follow state change? ──
post_xml = device.deviceV2.dump_hierarchy().lower()
# If the button now says "Following", "Abonniert", or "Requested"
if any(word in post_xml for word in ["following", "abonniert", "requested"]):
telepathic.confirm_click("tap follow button on profile")
logger.info(f"🤝 [Deep Interaction] Followed @{username}")
session_state.totalFollowed[username] = 1
else:
telepathic.reject_click("tap follow button on profile")
logger.warning(f"⚠️ [Follow] Click did not change follow state for @{username}. Learning from failure.")
from GramAddict.core.q_nav_graph import QNavGraph
nav_graph = QNavGraph(device)
if nav_graph._execute_transition("tap_follow_button"):
logger.info(f"🤝 [Deep Interaction] Followed @{username}")
session_state.totalFollowed[username] = 1
# ── TDD Sync Guard: Profile Animations ──
# Provide buffer for follow-related animations/menus to close before parsing the grid
sleep(random.uniform(1.8, 3.2) * sleep_mod)
# Grid Likes
likes_pct = float(getattr(configs.args, "likes_percentage", 0)) / 100.0
@@ -532,42 +523,24 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod,
except:
count = 1
xml_dump = device.deviceV2.dump_hierarchy()
telepathic = TelepathicEngine.get_instance()
first_post = telepathic.find_best_node(xml_dump, "first image post in profile grid", device=device)
if first_post and not first_post.get("skip"):
_humanized_click(device, first_post["x"], first_post["y"], sleep_mod=sleep_mod)
sleep(random.uniform(2.5, 4.5) * sleep_mod)
from GramAddict.core.q_nav_graph import QNavGraph
nav_graph = QNavGraph(device)
if nav_graph._execute_transition("tap_grid_first_post"):
logger.info(f"❤️ [Deep Interaction] Opening grid to drop {count} likes on @{username}...")
# ── Post-Click Verification: Did a post open? ──
post_xml = device.deviceV2.dump_hierarchy()
# Post view has like buttons or media groups
post_opened = any(ind in post_xml for ind in ["button_like", "button_comment", "media_group"])
if not post_opened:
# If grid is gone, something opened
post_opened = "profile_grid" in xml_dump and "profile_grid" not in post_xml
if post_opened:
telepathic.confirm_click("first image post in profile grid")
logger.info(f"❤️ [Deep Interaction] Opening grid to drop {count} likes on @{username}...")
for i in range(count):
_humanized_click(device, w // 2, h // 2, double=True, sleep_mod=sleep_mod)
session_state.totalLikes += 1
logger.debug(f"Liked grid post {i+1}/{count}")
sleep(random.uniform(1.0, 2.0) * sleep_mod)
start_scroll_y, end_scroll_y = int(h * 0.7) + random.randint(-20, 20), int(h * 0.2) + random.randint(-40, 40)
scroll_x = w // 2 + random.randint(-30, 30)
device.deviceV2.shell(f"input swipe {scroll_x} {start_scroll_y} {scroll_x + random.randint(-15,15)} {end_scroll_y} {random.randint(250, 400)}")
sleep(random.uniform(1.5, 3.0) * sleep_mod)
device.deviceV2.press("back")
for i in range(count):
_humanized_click(device, w // 2, h // 2, double=True, sleep_mod=sleep_mod)
session_state.totalLikes += 1
logger.debug(f"Liked grid post {i+1}/{count}")
sleep(random.uniform(1.0, 2.0) * sleep_mod)
else:
telepathic.reject_click("first image post in profile grid")
logger.warning(f"⚠️ [Grid] Click did not open post for @{username}. Learning from failure.")
device.deviceV2.press("back")
sleep(random.uniform(0.5, 1.0))
start_scroll_y, end_scroll_y = int(h * 0.7) + random.randint(-20, 20), int(h * 0.2) + random.randint(-40, 40)
scroll_x = w // 2 + random.randint(-30, 30)
device.deviceV2.shell(f"input swipe {scroll_x} {start_scroll_y} {scroll_x + random.randint(-15,15)} {end_scroll_y} {random.randint(250, 400)}")
sleep(random.uniform(1.5, 3.0) * sleep_mod)
device.deviceV2.press("back")
sleep(random.uniform(1.0, 2.0) * sleep_mod)
# Let the native UI momentum scroll finish just like a human watching the feed
sleep(random.uniform(1.2, 2.0))
@@ -667,11 +640,15 @@ def _detect_ad_structural(context_xml: str) -> bool:
AD_RESOURCE_IDS = {
"com.instagram.android:id/ad_cta_button",
"com.instagram.android:id/clips_single_image_ads_media_content",
"com.instagram.android:id/intent_aware_ad_pivot_container"
}
GENERIC_CTA_IDS = {
"com.instagram.android:id/clips_browser_cta",
"com.instagram.android:id/universal_cta_description_layout",
"com.instagram.android:id/universal_cta_text",
"com.instagram.android:id/intent_aware_ad_pivot_container"
}
AD_CTA_WORDS = {"install", "learn more", "shop now", "sign up", "mehr dazu", "jetzt einkaufen", "installieren", "registrieren", "anmelden", "download", "herunterladen", "get offer", "abonnieren", "subscribe"}
try:
root = ET.fromstring(context_xml)
@@ -682,6 +659,14 @@ def _detect_ad_structural(context_xml: str) -> bool:
if res_id in AD_RESOURCE_IDS:
return True
# 1.5 Generic CTAs require text checking to avoid flagging 'Use template' or 'Original audio'
if res_id in GENERIC_CTA_IDS:
text = node.attrib.get("text", "").strip().lower()
desc = node.attrib.get("content-desc", "").strip().lower()
combined = text + " " + desc
if any(w in combined for w in AD_CTA_WORDS):
return True
# 2. Secondary Label Exact Match
if res_id == "com.instagram.android:id/secondary_label":
text = node.attrib.get("text", "").strip().lower()
@@ -793,9 +778,7 @@ def _run_zero_latency_stories_loop(device, configs, session_state, cognitive_sta
logger.info("🎬 [StoriesFeed] Session completed naturally.")
device.deviceV2.press("back")
return "SESSION_OVER"
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.
@@ -873,28 +856,22 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
sleep(random.uniform(0.1, 0.4) * sleep_mod)
continue
# ── Boredom Engine (Notifications / DMs) ──
if random.random() < 0.03:
logger.info("🥱 [Boredom] Checking something else (Notifications/DMs) for a second...", extra={"color": f"{Fore.MAGENTA}"})
# Activity tab usually has content-desc "Activity" or resource-id "newsfeed_tab"
newsfeed_tab = device.deviceV2(resourceId="com.instagram.android:id/newsfeed_tab")
direct_tab = device.deviceV2(descriptionContains="Messaging")
# Fallbacks:
if not direct_tab.exists: direct_tab = device.deviceV2(descriptionContains="Message")
if random.random() < 0.5 and newsfeed_tab.exists:
newsfeed_tab.click()
elif direct_tab.exists:
direct_tab.click()
# ── Boredom ──
if random.random() < 0.03 and not is_reels:
logger.info("🥱 [Boredom] Checking something else (Notifications/DMs) for a second...")
# Use NavGraph transitions instead of raw selectors
if random.random() < 0.5:
# Try to visit Notifications
nav_graph._execute_transition("tap_newsfeed_tab", zero_engine)
else:
# Try to visit DMs
nav_graph._execute_transition("tap_message_icon", zero_engine)
sleep(random.uniform(3.0, 6.0) * sleep_mod)
# Return to feed natively
feed_tab = device.deviceV2(resourceId="com.instagram.android:id/feed_tab")
if feed_tab.exists:
feed_tab.click()
else:
device.deviceV2.press("back")
# Return to feed natively through robust navigation
nav_graph.navigate_to("HomeFeed", zero_engine)
sleep(random.uniform(1.0, 2.5) * sleep_mod)
context_xml = device.deviceV2.dump_hierarchy()
@@ -988,11 +965,10 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
consecutive_ads += 1
if consecutive_ads >= 3:
logger.warning("📺 [Anti-Stuck] Stuck on ad! Executing aggressive mechanical drag.", extra={"color": f"{Fore.RED}"})
# Massive slow drag from top to bottom (finger down to up -> screen moves down? No we want to go down in feed so finger moves UP)
# h * 0.8 to h * 0.1
# Fast drag from bottom to top (~0.15s) to guarantee Native Android Fling event for Reels
info = device.get_info()
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
device.deviceV2.swipe(w // 2, int(h * 0.8), w // 2, int(h * 0.2), duration=0.8)
device.deviceV2.shell(f"input swipe {w // 2} {int(h * 0.8)} {w // 2} {int(h * 0.2)} 150")
sleep(2.0)
else:
logger.info("📺 skipping ad (structural match)...")
@@ -1053,7 +1029,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# ── 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._execute_transition("tap_post_username", zero_engine):
if nav_graph._execute_transition("tap_post_username", zero_engine) 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)
@@ -1091,7 +1067,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
logger.info(f"🕵️‍♂️ [Profile Learning] Highly resonant post ({res_score:.2f}). Visiting @{target_user}'s profile to learn context...", extra={"color": f"{Fore.CYAN}"})
# Navigate to profile
if nav_graph._execute_transition("tap_post_username", zero_engine):
if nav_graph._execute_transition("tap_post_username", zero_engine) is True:
sleep(random.uniform(1.2, 2.5) * sleep_mod)
# Extract context
@@ -1134,7 +1110,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
if res_score >= 0.35 and random.random() < likes_chance:
logger.info("❤️ [Interaction] Liking post...")
success = nav_graph._execute_transition("tap_like_button", zero_engine)
if not success:
if success is not True:
logger.debug("Telepathic Like failed, falling back to double-tap.")
info = device.get_info()
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
@@ -1163,11 +1139,18 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
logger.info("💬 [Interaction] Entering Comment Sheet for deep engagement...")
success = nav_graph._execute_transition("tap_comment_button", zero_engine)
if success:
if success is True:
sleep(random.uniform(2.0, 4.0) * sleep_mod)
# 1. Scrape Context from the comment sheet
sheet_xml = device.deviceV2.dump_hierarchy()
# 🛡️ [Semantic Gate] Verify we are actually in the comment sheet
if not any(x in sheet_xml for x in ["layout_comment_thread", "comment_composer", "comment_button_post"]):
logger.warning("❌ [Ambiguity Guard] Transition reported success, but Comment Sheet markers not found in UI. Bailing engagement.")
did_interact = False
continue
import xml.etree.ElementTree as ET
existing_comments = []
comment_nodes = []
@@ -1285,9 +1268,9 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
from GramAddict.core.llm_provider import query_llm
from GramAddict.core.stealth_typing import ghost_type
model = getattr(configs.args, "ai_condenser_model", "google/gemini-2.5-flash-lite-preview")
url = getattr(configs.args, "ai_condenser_url", "https://openrouter.ai/api/v1/chat/completions")
response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=False)
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=False, timeout=45)
if response_dict and "response" in response_dict:
clean_comment = response_dict["response"].strip().strip('"').strip("'")
@@ -1375,7 +1358,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
if res_score >= 0.70 and random.random() < repost_chance:
logger.info("🔁 [Interaction] Reposting highly resonant content...", extra={"color": f"{Fore.CYAN}"})
success = nav_graph._execute_transition("tap_share_button", zero_engine)
if success:
if success is True:
sleep(random.uniform(1.8, 3.5) * sleep_mod)
telepathic = TelepathicEngine.get_instance()
xml_dump = device.deviceV2.dump_hierarchy()
@@ -1443,7 +1426,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
darwin.evaluate_session_end(duration_minutes, followers_gained)
logger.info("🏁 [Drive] Feed loop terminated. Session over.")
return "SESSION_OVER"
return "FEED_EXHAUSTED"
def _run_zero_latency_search_loop(device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack):

View File

@@ -22,24 +22,38 @@ class VLMCompilerEngine:
logger.warning(f"🧠 [Compiler Engine] Deterministic heuristic failed for: '{intent_description}'. Synthesizing new rule...", extra={"color": "\x1b[1m\x1b[35m"})
args = getattr(self.device, "args", None)
model = getattr(args, "ai_telepathic_model", "google/gemini-3.1-flash-lite-preview") if args else "google/gemini-3.1-flash-lite-preview"
url = getattr(args, "ai_telepathic_url", "https://openrouter.ai/api/v1/chat/completions") if args else "https://openrouter.ai/api/v1/chat/completions"
model = getattr(args, "ai_telepathic_model", "llama3.2:1b") if args else "llama3.2:1b"
url = getattr(args, "ai_telepathic_url", "http://localhost:11434/api/generate") if args else "http://localhost:11434/api/generate"
use_local = "11434" in url or "localhost" in url
simplified_xml = self._simplify_xml(context_xml)
# --- Model Trust Logging ---
from GramAddict.core.benchmark_guard import BENCHMARKS_FILE
trust_log = f"Using {model}"
try:
if os.path.exists(BENCHMARKS_FILE):
with open(BENCHMARKS_FILE, "r") as f:
bench_data = json.load(f).get("models", {}).get(model, {})
if bench_data:
score = bench_data.get("telepathic_score", 0)
passed = "PASS" if bench_data.get("passed_all", False) else "FAIL"
unsuitable = bench_data.get("is_unsuitable", False)
trust_level = "HIGH" if score >= 80 and not unsuitable else "MEDIUM" if score >= 50 and not unsuitable else "LOW/UNSAFE"
trust_log += f" [Benchmark: {score}/100 | {passed} | Trust: {trust_level}]"
if unsuitable:
logger.error(f"⛔ [Safety Alert] {model} is marked as UNSUITABLE for this task!")
except Exception:
pass
logger.info(f"🧠 [Compiler] Intent: '{intent_description}' -> {trust_log}")
# ---------------------------
system_prompt = (
"You write Python regex rules to find Android UI elements. "
"Given UI XML, find the element matching the intent. "
"Generate a regex pattern to match its resource-id.\n\n"
"OUTPUT FORMAT (JSON only):\n"
"{\"rule_type\": \"regex\", \"target_attribute\": \"resource-id\", "
"\"pattern\": \".*your_regex.*\", \"confidence\": 0.95, "
"\"reasoning\": \"brief explanation\"}\n\n"
"RULES:\n"
"- ONLY use rule_type='regex'. NEVER use xpath.\n"
"- Target resource-id for dynamic elements, not text or usernames.\n"
"- Make patterns globally reusable, not hardcoded to specific content."
"You are a regex engineering expert for Android UI. Identify the most stable resource-id regex for the intent.\n"
"Rules:\n"
"1. Output ONLY a raw JSON object.\n"
"2. NO markdown, NO triple backticks.\n"
"3. Format: {\"rule_type\": \"regex\", \"target_attribute\": \"resource-id\", \"pattern\": \".*regex.*\", \"confidence\": 0.95, \"reasoning\": \"string\"}"
)
user_prompt = f"TARGET INTENT: {intent_description}\n\nUI XML:\n{simplified_xml[:2000]}"
@@ -55,10 +69,32 @@ class VLMCompilerEngine:
use_local_edge=use_local
)
if res_text and res_text.startswith("```"):
if not res_text:
logger.error("Compiler LLM returned empty response.")
return None
if "```json" in res_text:
res_text = res_text.split("```json")[1].split("```")[0].strip()
elif res_text.startswith("```"):
res_text = "\n".join(res_text.strip().split("\n")[1:-1])
decision = json.loads(res_text) if res_text else {}
try:
decision = json.loads(res_text)
except json.JSONDecodeError:
logger.error(f"Compiler LLM returned invalid JSON: {res_text[:100]}...")
return None
# If LLM returned a list, take the first item if it's a dict
if isinstance(decision, list):
if len(decision) > 0 and isinstance(decision[0], dict):
decision = decision[0]
else:
logger.error(f"Compiler LLM returned unexpected list format: {decision}")
return None
if not isinstance(decision, dict):
logger.error(f"Compiler LLM returned non-object response: {type(decision)}")
return None
pattern = decision.get('pattern')
if not pattern:

View File

@@ -157,10 +157,10 @@ class Config:
self.parser.add_argument("--speed-multiplier", help="Speed multiplier", default="1.0")
# AI Model Configuration (centralized — no hardcoded model names anywhere)
self.parser.add_argument("--ai-model", "--ai-text-model", help="Primary LLM model (OpenRouter or Ollama)", default="google/gemini-2.5-flash-lite-preview")
self.parser.add_argument("--ai-model-url", "--ai-text-url", help="Primary LLM endpoint URL", default="https://openrouter.ai/api/v1/chat/completions")
self.parser.add_argument("--ai-telepathic-model", help="Text-based model for Telepathic Engine Fallbacks", default="google/gemini-3.1-flash-lite-preview")
self.parser.add_argument("--ai-telepathic-url", help="Telepathic model endpoint URL", default="https://openrouter.ai/api/v1/chat/completions")
self.parser.add_argument("--ai-model", "--ai-text-model", help="Primary LLM model (OpenRouter or Ollama)", default="llama3.2:1b")
self.parser.add_argument("--ai-model-url", "--ai-text-url", help="Primary LLM endpoint URL", default="http://localhost:11434/api/generate")
self.parser.add_argument("--ai-telepathic-model", help="Text-based model for Telepathic Engine Fallbacks", default="llama3.2:1b")
self.parser.add_argument("--ai-telepathic-url", help="Telepathic model endpoint URL", default="http://localhost:11434/api/generate")
self.parser.add_argument("--ai-fallback-model", "--ai-text-fallback-model", help="Fallback model when primary fails", default="llama3.2:1b")
self.parser.add_argument("--ai-fallback-url", "--ai-text-fallback-url", help="Fallback model endpoint URL", default="http://localhost:11434/api/generate")
self.parser.add_argument("--ai-embedding-model", help="Embedding model for vector operations", default="nomic-embed-text")
@@ -176,8 +176,8 @@ class Config:
self.parser.add_argument("--scrape-profiles", action="store_true", help="Extract and store profile metadata in CRM")
# Phase 10: RAG Comment Learning & Extractor Settings
self.parser.add_argument("--ai-condenser-model", help="LLM used for condensing text/comments", default="google/gemini-2.5-flash-lite-preview")
self.parser.add_argument("--ai-condenser-url", help="URL for the condenser model", default="https://openrouter.ai/api/v1/chat/completions")
self.parser.add_argument("--ai-condenser-model", help="LLM used for condensing text/comments", default="llama3.2:1b")
self.parser.add_argument("--ai-condenser-url", help="URL for the condenser model", default="http://localhost:11434/api/generate")
self.parser.add_argument("--ai-learn-comments", action="store_true", help="Extract and learn from comment sections")
self.parser.add_argument("--ai-learn-niche-posts", action="store_true", help="Learn from niche posts")
self.parser.add_argument("--ai-learn-own-profile", action="store_true", help="Learn from your own profile interactions")
@@ -185,6 +185,9 @@ class Config:
self.parser.add_argument("--ai-vibe", help="The specific vibe to extract from comments (e.g., friendly, controversial)", default="")
self.parser.add_argument("--ai-blacklist-topics", help="Comma-separated topics heavily penalized or skipped", default="")
self.parser.add_argument("--ai-quality-filter", action="store_true", help="Use AI to strictly filter the quality of posts and comments")
self.parser.add_argument("--smart-unfollow", action="store_true", help="Enable agentic decision making for clearing the following list")
self.parser.add_argument("--ai-vision-navigation", action="store_true", help="Capture and send base64 UI screenshots to the LLM for structural element finding")
self.parser.add_argument("--ai-vision-context", action="store_true", help="Capture and send base64 post/DM screenshots to the LLM for contextual semantic generation")
# on first run, we must wait to proceed with loading
if not self.first_run:

View File

@@ -111,6 +111,19 @@ class DarwinEngine(QdrantBase):
if profile["comment_read_dwell"] > 1.0 and resonance > 0.4 and random.random() < 0.3:
if nav_graph and zero_engine:
logger.debug(f" -> Opening comments section for {profile['comment_read_dwell']:.1f}s depth simulation")
# Capture image context of post BEFORE opening comment sheet
b64_img_payload = None
if configs and getattr(configs.args, "ai_vision_context", False):
try:
import base64
raw = device.screenshot()
if raw:
b64_img_payload = [base64.b64encode(raw).decode('utf-8')]
logger.debug("👁️ [Vision Context] Captured post screenshot for True Vision semantic analysis.")
except Exception as e:
logger.warning(f"⚠️ [Vision Context] Failed to capture screenshot: {e}")
success = nav_graph._execute_transition("tap_comment_button", zero_engine)
if success:
# ---- Phase 10: RAG Comment Extraction ----
@@ -121,7 +134,7 @@ class DarwinEngine(QdrantBase):
try:
xml_data = device.deviceV2.dump_hierarchy()
t0 = time.time()
resonance_oracle.extract_and_learn_comments(xml_data, configs, author=username or "unknown")
resonance_oracle.extract_and_learn_comments(xml_data, configs, author=username or "unknown", images_b64=b64_img_payload)
t1 = time.time()
remaining_sleep = profile["comment_read_dwell"] - (t1 - t0)
if remaining_sleep > 0:

View File

@@ -148,7 +148,18 @@ class DeviceFacade:
@adb_retry()
def _get_current_app(self):
return self.deviceV2.app_current().get("package")
"""
Hardened app package detection.
Transient notifications (e.g. Amazon, WhatsApp) can spoof uiautomator2's app_current() report.
We verify the package with a retry if it doesn't match our expected app_id.
"""
pkg = self.deviceV2.app_current().get("package")
if pkg != self.app_id:
# Maybe a notification popped up? Wait and re-check.
sleep(0.5)
pkg = self.deviceV2.app_current().get("package")
return pkg
@adb_retry()
def find(self, **kwargs):

View File

@@ -110,4 +110,4 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
if failed_attempts > 2:
return "CONTEXT_LOST"
return "SESSION_OVER"
return "FEED_EXHAUSTED"

View File

@@ -84,7 +84,7 @@ def query_llm(
images_b64: Optional[List[str]] = None,
system: Optional[str] = None,
format_json: bool = False,
timeout: int = 60,
timeout: int = 180,
fallback_model: Optional[str] = None,
fallback_url: Optional[str] = None
) -> Optional[dict]:
@@ -191,12 +191,14 @@ def query_llm(
return {"response": content}
else:
# Ollama returns response
content = resp_json.get("response", "")
# Ollama returns response OR thinking (for reasoning models)
content = resp_json.get("response") or resp_json.get("thinking") or ""
if format_json:
extracted = extract_json(content)
if not extracted:
raise ValueError(f"Ollama returned non-JSON content when JSON was expected: {content[:100]}...")
# Log more context if JSON extraction fails
logger.debug(f"Ollama raw content (for JSON extraction): {content[:200]}...")
raise ValueError(f"Ollama returned non-JSON content when JSON was expected.")
resp_json["response"] = extracted
return resp_json
@@ -227,12 +229,17 @@ def query_llm(
f_model = f_model or "llama3.2:1b"
f_url = f_url or "http://localhost:11434/api/generate"
else:
f_model = f_model or "google/gemini-2.5-flash-lite-preview"
f_url = f_url or "https://openrouter.ai/api/v1/chat/completions"
f_model = f_model or "llama3.2:1b"
f_url = f_url or "http://localhost:11434/api/generate"
# Circuit Breaker: If fallback is identical to primary, don't waste time retrying
if f_model == model and f_url == url:
logger.warning(f"⚠️ [Circuit Breaker] Fallback AI matches Primary AI ({model}). Skipping redundant retry.")
return None
query_llm._is_fallback = True
try:
logger.warning(f"Primary AI ({model}) failed or returned garbage. Attempting fallback to {f_model}...")
logger.warning(f"🔄 Primary AI ({model}) failed. Attempting fallback to {f_model}...")
return query_llm(
url=f_url,
model=f_model,
@@ -252,12 +259,14 @@ def query_telepathic_llm(
system_prompt: str,
user_prompt: str,
temperature: float = 0.0,
use_local_edge: bool = False
use_local_edge: bool = False,
images_b64: Optional[List[str]] = None
) -> str:
"""
Routes UI Telepathic requests purely based on textual interpretation of the screen's XML nodes.
If use_local_edge is manually enabled, routes to localhost:11434.
Otherwise honors the provided URL and model (e.g. OpenRouter).
Passes optional Base64 screenshots (images_b64) if ai-vision-navigation is enabled.
"""
target_url = url
target_model = model
@@ -277,7 +286,7 @@ def query_telepathic_llm(
url=target_url,
model=target_model,
prompt=user_prompt,
images_b64=None,
images_b64=images_b64,
system=system_prompt,
format_json=True
)

View File

@@ -109,6 +109,13 @@ class QNavGraph:
self.current_state = "HomeFeed"
return self.navigate_to(target_state, zero_engine, recovery_attempts=recovery_attempts + 1)
else:
# NEW: Attempt Back-out recovery if we are in UNKNOWN and direct tap failed
if self.current_state == "UNKNOWN":
logger.warning(f"📍 [Recovery] Semantic tap failed from UNKNOWN. Attempting to back out of sub-view...")
self.device.deviceV2.press("back")
time.sleep(2)
# We stay in UNKNOWN, but next attempt might see the nav bar
return self.navigate_to(target_state, zero_engine, recovery_attempts=recovery_attempts + 0.5)
path = None
if path is None:
@@ -168,83 +175,117 @@ class QNavGraph:
return None
def _execute_transition(self, action: str, zero_engine) -> bool:
def _execute_transition(self, action: str, zero_engine=None, max_retries: int = 2) -> bool:
"""
Executes a transition (e.g. 'tap_explore_tab') using the Telepathic Semantic Engine.
"""
from GramAddict.core.telepathic_engine import TelepathicEngine
engine = TelepathicEngine.get_instance()
context_xml = self.device.deviceV2.dump_hierarchy()
# ── Z-Depth Guard / Obstacle Clearance ──
import re
if re.search(r'bottom_sheet_container|dialog_container|dialog_root|bottom_sheet_drag', str(context_xml)):
logger.warning("🛡️ [Z-Depth Guard] Obstacle overlay detected during navigation. Pressing BACK to clear...")
self.device.deviceV2.press("back")
time.sleep(1.5)
# Re-acquire context after clearing obstacle
for attempt in range(max_retries + 1):
context_xml = self.device.deviceV2.dump_hierarchy()
# We phrase the action as an intent for the semantic engine
# e.g. "tap_explore_tab" -> "tap explore tab"
# We add some common synonyms for Instagram to help the vector engine
intent_map = {
# Navigation (Bottom Bar) — aligned with fast-path keys
"tap_home_tab": "tap home tab",
"tap_explore_tab": "tap explore tab",
"tap_profile_tab": "tap profile tab",
"tap_reels_tab": "tap reels tab",
"tap_create_tab": "tap create post tab",
# Post Interaction — aligned with fast-path keys
"tap_like_button": "tap like button",
"tap_comment_button": "tap comment button",
"tap_post_username": "tap post username",
"tap_share_button": "tap share button",
"tap_save_button": "tap save button",
# Grid & Profile
"tap_explore_grid_item": "first image in explore grid",
"tap_story_tray_item": "profile picture avatar story ring",
"tap_follow_button": "tap follow button on profile",
"tap_grid_first_post": "first image post in profile grid",
}
intent_description = intent_map.get(action, action.replace("_", " "))
# ── Z-Depth Guard / Obstacle Clearance ──
import re
if re.search(r'bottom_sheet_container|dialog_container|dialog_root|bottom_sheet_drag', str(context_xml)):
logger.warning("🛡️ [Z-Depth Guard] Obstacle overlay detected during navigation. Pressing BACK to clear...")
self.device.deviceV2.press("back")
time.sleep(1.5)
# Re-acquire context after clearing obstacle
context_xml = self.device.deviceV2.dump_hierarchy()
# Use TelepathicEngine to find the most likely node for this intent
# If vector score < 0.82, it will trigger the Vision Cortex Fallback (VLM)
best_node = engine.find_best_node(context_xml, intent_description, min_confidence=0.82, device=self.device)
if not best_node:
logger.debug(f"_execute_transition: TelepathicEngine found no matching node for '{action}'")
# Check if we are even in the right app
current_app = self.device._get_current_app()
if current_app != self.device.app_id:
logger.warning(f"⚠️ [Context Lost] Currently in '{current_app}', expected '{self.device.app_id}'. Transition '{action}' aborted.")
return "CONTEXT_LOST"
return False
if best_node.get("skip"):
logger.info(f"⏭️ Skipping physical tap for '{action}' (Semantic Fast-Path indicated state already fulfilled)")
return True
# We phrase the action as an intent for the semantic engine
# e.g. "tap_explore_tab" -> "tap explore tab"
# We add some common synonyms for Instagram to help the vector engine
intent_map = {
# Navigation (Bottom Bar) — aligned with fast-path keys
"tap_home_tab": "tap home tab",
"tap_explore_tab": "tap explore tab",
"tap_profile_tab": "tap profile tab",
"tap_reels_tab": "tap reels tab",
"tap_create_tab": "tap create post tab",
# Post Interaction — aligned with fast-path keys
"tap_like_button": "tap like button",
"tap_comment_button": "tap comment button",
"tap_post_username": "tap post username",
"tap_share_button": "tap share button",
"tap_save_button": "tap save button",
# Grid & Profile
"tap_explore_grid_item": "first image in explore grid",
"tap_story_tray_item": "profile picture avatar story ring",
"tap_follow_button": "tap follow button on profile",
"tap_grid_first_post": "first image post in profile grid",
"tap_back": "tap back button icon arrow",
"tap_message_icon": "tap direct message icon inbox",
"tap_newsfeed_tab": "tap activity heart icon notifications",
}
intent_description = intent_map.get(action, action.replace("_", " "))
source_tag = best_node.get("source", "telepathic").replace("_", " ").title()
logger.info(f"QNavGraph executing transition '{action}' via [{source_tag}] (Score: {best_node.get('score', 1.0):.3f})")
# Use TelepathicEngine to find the most likely node for this intent
# If vector score < 0.82, it will trigger the Vision Cortex Fallback (VLM)
best_node = engine.find_best_node(context_xml, intent_description, min_confidence=0.82, device=self.device)
if not best_node:
logger.debug(f"_execute_transition: TelepathicEngine found no matching node for '{action}'")
# Check if we are even in the right app
current_app = self.device._get_current_app()
if current_app != self.device.app_id:
logger.warning(f"⚠️ [Context Lost] Currently in '{current_app}', expected '{self.device.app_id}'. Transition '{action}' aborted.")
return "CONTEXT_LOST"
# Try again if within retries, UI might be animating
if attempt < max_retries:
time.sleep(1.0)
continue
return False
# Execute click
self.device.click(obj=best_node)
time.sleep(random.uniform(1.2, 2.5))
if best_node.get("skip") or (best_node.get("selected") and "tab" in action):
logger.info(f"⏭️ Skipping physical tap for '{action}' (Semantic Fast-Path indicated state already fulfilled)")
return True
source_tag = best_node.get("source", "telepathic").replace("_", " ").title()
logger.info(f"QNavGraph executing transition '{action}' via [{source_tag}] (Score: {best_node.get('score', 1.0):.3f})")
# Execute click
self.device.click(obj=best_node)
time.sleep(random.uniform(1.2, 2.5))
# ── Post-Click Verification: Did it work? ──
post_click_xml = self.device.deviceV2.dump_hierarchy()
# 1. Semantic Verification (Hardened)
is_verified = engine.verify_success(intent_description, post_click_xml)
# 2. UI Change Verification (Fallback/Navigation)
ui_changed = post_click_xml != context_xml
# ── Post-Click Verification: Did the screen change? ──
post_click_xml = self.device.deviceV2.dump_hierarchy()
# For navigation, we expect the UI to change or specific markers to appear
# Comparison of XML strings is a good baseline for navigation success
if post_click_xml != context_xml:
engine.confirm_click(intent_description)
return True
else:
logger.warning(f"⚠️ [Nav] Click on '{action}' did not change UI. Learning from failure.")
engine.reject_click(intent_description)
return False
if is_verified and ui_changed:
engine.confirm_click(intent_description)
return True
elif not ui_changed:
logger.warning(f"⚠️ [Nav] Click on '{action}' did not change UI. Learning from failure.")
engine.reject_click(intent_description)
if attempt < max_retries:
logger.info(f"🔄 [Autonomy] UI unchanged. Retrying transition '{action}' ({attempt + 1}/{max_retries})...")
continue
else:
return False
else:
# UI changed but semantic verification failed (accidental click or false positive)
logger.warning(f"❌ [Ambiguity Guard] UI changed after '{action}', but semantic verification FAILED. Rejecting mapping.")
engine.reject_click(intent_description)
# Safety: If we're not where we expect to be, try to back out to clear any accidentally opened menus
logger.info("🛡️ [Safety Reset] Pressing BACK to clear potential accidental menu/sub-view.")
self.device.deviceV2.press("back")
time.sleep(1.0)
if attempt < max_retries:
logger.info(f"🔄 [Autonomy] Negative learning acquired. Retrying transition '{action}' ({attempt + 1}/{max_retries})...")
continue
else:
return False
return False
def _repair_transition(self, action: str):
"""

View File

@@ -14,7 +14,12 @@ import uuid
logger = logging.getLogger(__name__)
class QdrantBase:
_connection_failed_logged = False
def __init__(self, collection_name, vector_size=768):
if QdrantBase._connection_failed_logged:
self.client = None
return
self.collection_name = collection_name
self.client = None
self._vector_size = vector_size
@@ -52,7 +57,10 @@ class QdrantBase:
)
logger.info(f"Created Qdrant collection '{collection_name}' with dimension {vector_size}.")
except Exception as e:
logger.error(f"Failed to initialize Qdrant memory for '{collection_name}': {e}")
if not QdrantBase._connection_failed_logged:
logger.error(f"Failed to initialize Qdrant memory (likely not running): {e}")
QdrantBase._connection_failed_logged = True
# No debug logs here to prevent noise in --debug mode
self.client = None
@property
@@ -745,6 +753,10 @@ class BannedPathsDB:
MAX_AGE_SECONDS = 7 * 24 * 3600
def __init__(self):
if QdrantBase._connection_failed_logged:
self._client = None
return
self._banned = {} # {goal_hash: set(element_ids)}
self._client = None
self._collection = "gramaddict_banned_paths"

View File

@@ -154,11 +154,12 @@ class ResonanceEngine:
logger.info("✨ [Resonance] NEGATIVE ALIGNMENT. Skipping profile.", extra={"color": f"{Fore.MAGENTA}"})
return False
def extract_and_learn_comments(self, xml_hierarchy: str, configs, author: str = "unknown"):
def extract_and_learn_comments(self, xml_hierarchy: str, configs, author: str = "unknown", images_b64: list = None):
"""
Phase 10: RAG Comment Learning Implementation
Extracts comments from the UI hierarchy, filters them using the assigned VLM against
configured blacklists and vibes, and stores them in Qdrant CommentMemoryDB.
Supports True Vision Context (images_b64) to mathematically align comments to visual aesthetics.
"""
if not configs or not getattr(configs.args, "ai_learn_comments", False):
return
@@ -199,24 +200,34 @@ class ResonanceEngine:
# 2. Filter via VLM Condenser
prompt = (
f"Filter these Instagram comments. Keep ONLY real comments that generally match this vibe: '{vibe}'.\n"
f"Remove comments about: {blacklist}\n"
f"Remove UI junk text (buttons, labels, timestamps).\n\n"
f"Comments:\n{chr(10).join(raw_comments)}\n\n"
"Output a JSON array of matching comment strings. If none match, output []."
f"Evaluate Instagram comments for SPAM. Your only goal is blocking bad topics.\n"
f"VIBE = '{vibe}'\n"
f"BLACKLIST = {blacklist}\n\n"
f"Comments:\n{chr(10).join(['- ' + c for c in raw_comments])}\n\n"
"Return a JSON formatting exactly like this example:\n"
"{\n"
" \"evaluations\": [\n"
" {\"text\": \"love it!\", \"has_blacklist_words\": false, \"keep\": true},\n"
" {\"text\": \"dm me for bitcoin\", \"has_blacklist_words\": true, \"keep\": false}\n"
" ]\n"
"}"
)
model = getattr(configs.args, "ai_condenser_model", "google/gemini-2.5-flash-lite-preview")
url = getattr(configs.args, "ai_condenser_url", "https://openrouter.ai/api/v1/chat/completions")
model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b")
url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate")
try:
import json
system = "You are a precise JSON filtering agent."
# Fix: kwargs match query_llm signature EXACTLY to evade TypeError
response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=True)
response_dict = query_llm(url=url, model=model, prompt=prompt, system=system, format_json=True, images_b64=images_b64)
if not response_dict or "response" not in response_dict:
return
response_text = response_dict["response"]
# DEBUG
logger.debug(f"DEBUG CONDENSER RAW: {response_text}")
print(f"DEBUG CONDENSER RAW: {response_text}")
# Parse json gracefully
if type(response_text) is str:
@@ -234,8 +245,19 @@ class ResonanceEngine:
# In case expect_json already returned a parsed list somehow, though extract_json returns str
learned_comments = response_text
# Filter the dict based on evaluations array
if isinstance(learned_comments, dict):
valid_list = []
evals = learned_comments.get("evaluations", [])
for ev in evals:
# Qwen 3.5 correctly identifies 'has_blacklist_words' but hallucinates 'keep': true
has_spam = ev.get("has_blacklist_words", False)
if not has_spam:
valid_list.append(ev.get("text"))
learned_comments = valid_list
if not isinstance(learned_comments, list):
logger.error("🧠 [Comment Learning] Condenser failed to return a JSON list.")
logger.error(f"🧠 [Comment Learning] Condenser failed to return a valid JSON structure: {learned_comments}")
return
if not learned_comments:

View File

@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
# ── Screen Zone Constants (fraction of screen height) ──
# Used for positional sanity checking instead of hardcoded resource-IDs.
STATUS_BAR_ZONE = 0.04 # Top 4% = Android status bar (wifi, battery, clock)
NAV_BAR_ZONE = 0.92 # Bottom 8% = Android nav bar / Instagram bottom tabs
NAV_BAR_ZONE = 0.94 # Bottom 6% = Android nav bar / Instagram bottom tabs
MAX_BUTTON_AREA = 150000 # Buttons/icons should be smaller than this (px²)
MAX_CONTAINER_AREA = 500000 # Anything above this is a full-screen container
@@ -112,8 +112,11 @@ class TelepathicEngine:
# XML Parsing
# ──────────────────────────────────────────────
def _extract_semantic_nodes(self, xml_string: str) -> list[dict]:
"""Parses Android UI XML and extracts clickable/interactive nodes."""
def _extract_semantic_nodes(self, xml_string: str, intent: str = None, threshold: float = 0.0) -> list[dict]:
"""
Parses Android UI XML and extracts clickable/interactive nodes.
If intent and threshold are provided, it filters nodes by semantic score.
"""
nodes = []
try:
clean_xml = re.sub(r'<\?xml.*?\?>', '', xml_string).strip()
@@ -160,24 +163,47 @@ class TelepathicEngine:
center_y = (top + bottom) // 2
width = right - left
height = bottom - top
area = width * height
nodes.append({
node = {
"semantic_string": semantic_string,
"x": center_x,
"y": center_y,
"width": width,
"height": height,
"area": width * height,
"area": area,
"raw_bounds": bounds_str,
"resource_id": res_id,
"class_name": class_name,
"selected": attrib.get('selected', 'false').lower() == 'true',
"original_attribs": {"text": text, "desc": content_desc}
})
}
# Apply structural filter before scoring if intent is provided
if intent:
# We use a default screen_height if we call extract directly with intent
if not self._structural_sanity_check(node, intent, screen_height=2400):
continue
# Compute score for filtering
score = self._compute_quick_score(intent, semantic_string)
if score < threshold:
continue
node["score"] = score
nodes.append(node)
except Exception as e:
logger.error(f"Telepathic XML parsing failed: {e}")
return nodes
def _compute_quick_score(self, intent: str, semantic: str) -> float:
"""Helper for legacy _extract_semantic_nodes filtering."""
intent_words = set(intent.lower().replace("_", " ").split())
semantic_words = set(semantic.lower().replace("_", " ").split())
common = intent_words.intersection(semantic_words)
return len(common) / len(intent_words) if intent_words else 0.0
# ──────────────────────────────────────────────
# Structural Sanity (app-agnostic, no hardcoded IDs)
# ──────────────────────────────────────────────
@@ -191,7 +217,15 @@ class TelepathicEngine:
"""
# 1. Reject massive containers (full-screen views, recycler views)
# UNLESS the intent explicitly targets media
is_media_intent = any(k in intent_description.lower() for k in ["video", "photo", "reel", "media", "post"])
low_intent = intent_description.lower()
is_media_intent = any(k in low_intent for k in ["video", "photo", "reel", "media", "post"])
is_grid_item_intent = any(k in low_intent for k in ["grid", "list", "first", "item", "row"])
# If the intent specifically mentions looking for an item within a grid/list,
# we must block massive parent containers even if the word 'post', 'photo' or 'video' is present.
if is_grid_item_intent:
is_media_intent = False
if node.get("area", 0) > MAX_CONTAINER_AREA and not is_media_intent:
return False
@@ -199,10 +233,59 @@ class TelepathicEngine:
if node.get("y", 0) < screen_height * STATUS_BAR_ZONE:
return False
# 3. Reject nodes with zero area (invisible)
if node.get("area", 0) == 0:
# 3. Reject nodes in the Navigation Bar zone (bottom 6% - adjusted for accuracy)
# UNLESS the intent is explicitly about navigation tabs, profile stats, OR popup modals
nav_keywords = ["tab", "navigation", "explore tab", "reels tab", "profile tab", "home tab", "message tab", "following", "follower", "followers"]
modal_keywords = ["dismiss", "ok", "cancel", "accept", "allow", "deny", "action", "obstacle", "popup"]
low_intent = intent_description.lower()
is_nav_intent = any(k in low_intent for k in nav_keywords)
is_modal_intent = any(k in low_intent for k in modal_keywords)
# Resource-ID bypass for profile header elements that sit low
safe_ids = ["following", "follower", "post_count", "button_edit_profile", "button_share_profile"]
res_id = node.get("resource_id", "").lower()
id_bypass = any(k in res_id for k in safe_ids)
threshold = screen_height * NAV_BAR_ZONE
if node.get("y", 0) > threshold and not (is_nav_intent or is_modal_intent or id_bypass):
# Not an AI error—this is the deterministic prep-filter culling the NavBar before VLM logic.
logger.debug(f"🛡️ [Pre-LLM Pruning] Ignored navbar/overlay element (y={node.get('y')}) to prevent VLM hallucination.")
return False
# 4. Reject own profile/story if the intent is not explicitly looking for it.
# Intuitively, "profile picture avatar story ring" means "click a user's story".
# If we are looking for a story/profile, we must NOT click our OWN story.
is_targeting_own_profile = any(k in low_intent for k in ["own profile", "my profile", "own story"])
if not is_targeting_own_profile:
semantic_lower = node.get("semantic_string", "").lower()
if "your story" in semantic_lower:
logger.debug(f"🛡️ [Structural Guard] Rejecting own story overlay for intent '{intent_description}': {node['semantic_string']}")
return False
bot_username = self._get_current_username()
if bot_username and bot_username in semantic_lower:
# Rejecting bot's own username to prevent clicking itself (e.g. "marisaundmarc's story, 0 of 27")
logger.debug(f"🛡️ [Structural Guard] Rejecting own username '{bot_username}' for intent '{intent_description}': {node['semantic_string']}")
return False
# 5. Language-Agnostic Modal/Menu Guard
# Prevent clicks on items inside dialogs, bottom sheets, or context menus
# UNLESS the intent explicitly targets a menu or modal interaction.
# This completely nullifies the risk of accidentally clicking "Favorites" or "Unfollow" from a dropdown.
menu_id_indicators = ["menu_item", "option", "bottom_sheet", "dialog", "action_sheet", "popup"]
is_menu_node = any(m in node.get("resource_id", "").lower() for m in menu_id_indicators)
# If the intent doesn't sound like it wants a menu...
wants_menu_intent = any(k in low_intent for k in ["menu", "option", "more", "dismiss", "cancel", "modal"])
if is_menu_node and not wants_menu_intent:
logger.debug(f"🛡️ [Modal Guard] Rejecting menu/dialog item for non-menu intent '{intent_description}': {node['semantic_string']}")
return False
# 6. Reject nodes with zero area (invisible)
if node.get("area", 0) == 0:
return False
return True
def _is_blacklisted(self, intent: str, semantic_string: str) -> bool:
@@ -214,6 +297,26 @@ class TelepathicEngine:
# App Context Guard
# ──────────────────────────────────────────────
def _get_current_username(self) -> str:
"""Helper to get the current IG username from config, safely."""
username = getattr(self, "_cached_username", None)
if not username:
try:
from GramAddict.core.config import Config
cfg = Config()
raw_u = cfg.args.username if hasattr(cfg, "args") and hasattr(cfg.args, "username") else None
if raw_u and isinstance(raw_u, list) and len(raw_u)>0:
username = str(raw_u[0]).lower()
elif isinstance(raw_u, str):
username = raw_u.lower()
else:
username = ""
except Exception:
username = ""
self._cached_username = username
return username
# ──────────────────────────────────────────────
def _is_instagram_context(self, nodes: list[dict]) -> bool:
"""
Returns True only if the extracted nodes appear to come from the target app.
@@ -249,7 +352,7 @@ class TelepathicEngine:
ZERO AI cost — runs entirely on CPU string ops.
"""
# Extract meaningful keywords from intent (strip common filler words)
filler = {"tap", "the", "button", "tab", "on", "in", "a", "an", "of", "for", "to", "and", "or", "input", "text", "box"}
filler = {"tap", "the", "button", "on", "in", "a", "an", "of", "for", "to", "and", "or", "input", "text", "box"}
intent_words = set(w.lower() for w in re.split(r'\W+', intent_description) if w and w.lower() not in filler and len(w) > 1)
if not intent_words:
@@ -292,8 +395,8 @@ class TelepathicEngine:
# Score = ratio of intent keywords matched
score = hits / len(intent_words)
# Require at least 40% keyword overlap to avoid false positives
if score >= 0.4:
# Require at least 75% keyword overlap to avoid fatal false positives (e.g. 'post username' matching 'Send post')
if score >= 0.75:
scored.append((node, score))
if not scored:
@@ -352,7 +455,11 @@ class TelepathicEngine:
if interactive_nodes:
max_y = max(n.get("y", 0) + n.get("height", 0) // 2 for n in interactive_nodes)
if max_y > 100:
screen_height = int(max_y * 1.05)
# If we detect a screen height near standard android heights, don't inflate it.
if 2200 < max_y < 2600:
screen_height = max_y
else:
screen_height = int(max_y * 1.02)
# Pre-filter: Remove structurally implausible nodes and blacklisted mappings
viable_nodes = []
@@ -403,6 +510,27 @@ class TelepathicEngine:
"source": "memory"
}
# ── Stage 1.25: Structural Grid Fast Path ──
# Direct resource-ID + spatial sorting for grid navigation intents.
# This bypasses keyword/vector/VLM entirely — O(n) deterministic.
low_intent = intent_description.lower()
is_grid_intent = any(k in low_intent for k in ["explore grid", "grid item", "first image", "profile grid"])
if is_grid_intent:
grid_nodes = [n for n in viable_nodes if "image_button" in n.get("resource_id", "")]
if grid_nodes:
# Sort by Y (topmost first), then X (leftmost first) for "first"
grid_nodes.sort(key=lambda n: (n["y"], n["x"]))
best = grid_nodes[0]
logger.info(f"⚡ [Grid Fast-Path] Matched '{intent_description}'{best['semantic_string']} (y={best['y']})")
self._track_click(intent_description, best)
return {
"x": best["x"],
"y": best["y"],
"score": 0.98,
"semantic": best["semantic_string"],
"source": "grid_fastpath"
}
# ── Stage 1.5: Deterministic Keyword Fast Path ──
fast_path_result = self._keyword_match_score(intent_description, viable_nodes)
if fast_path_result:
@@ -529,6 +657,23 @@ class TelepathicEngine:
actual_intent = intent or ctx["intent"]
sem = ctx["semantic_string"]
# ── Anti-Poisoning Guard ──
# A semantic string that contains NO text and NO description is too generic
# to blacklist. Example: "id context: 'image button'" matches ALL buttons
# in a grid, so blacklisting it would kill the entire explore page.
# NOTE: must use regex word boundary to avoid "context:" matching "text:"
has_text = bool(re.search(r'(?<!\w)text:', sem.lower()))
has_desc = bool(re.search(r'(?<!\w)description:', sem.lower()))
is_generic = not has_text and not has_desc
if is_generic:
logger.warning(
f"⚠️ [Anti-Poisoning] Refusing to blacklist generic semantic: '{sem}'. "
f"It would poison all similar nodes. Skipping blacklist for '{actual_intent}'."
)
TelepathicEngine._last_click_context = None
return
# Add to blacklist
if actual_intent not in self._blacklist:
self._blacklist[actual_intent] = []
@@ -545,6 +690,74 @@ class TelepathicEngine:
TelepathicEngine._last_click_context = None
def verify_success(self, intent: str, post_click_xml: str) -> bool:
"""
Hardened verification. Does NOT rely on raw XML changes.
Inspects the post-click XML for semantic markers that prove the intent worked.
"""
ctx = TelepathicEngine._last_click_context
if not ctx:
return True # No context to verify against
# 1. Positional Consistency (Atomic Guard)
# If the screen changed so much that NO elements are near the original click,
# it might be a context-switch (navigation success).
# 2. Intent-Specific Markers
low_intent = intent.lower()
low_xml = post_click_xml.lower()
# 0. Global Hallucination Guards
# If we didn't intend to share/send and the share sheet opened, the click was a hallucination.
if "share" not in low_intent and "send" not in low_intent and "share_sheet" in low_xml:
logger.warning(f"❌ [Semantic Verification] FAILED: Hallucinated click opened the direct share sheet. ({intent})")
return False
if "poll" not in low_intent and "survey" not in low_intent and ("_poll_" in low_xml or "survey_" in low_xml):
logger.warning(f"❌ [Semantic Verification] FAILED: Hallucinated click opened a survey/poll. ({intent})")
return False
# Success markers for common actions
if "like" in low_intent:
# Check for "Liked" or "gefällt mir nicht mehr" in content-desc or text
marker_found = re.search(r"\b(liked|gefällt mir nicht mehr|gefällt mir am)\b", low_xml)
if marker_found:
logger.debug("✅ [Semantic Verification] Success confirmed: 'Liked' state detected.")
return True
else:
logger.warning("❌ [Semantic Verification] FAILED: Post does not report 'Liked' state after click.")
return False
if "follow" in low_intent:
# Check if button changed to "Following" or "Requested"
marker_found = re.search(r"\b(following|requested|folgst du|angefragt)\b", low_xml)
if marker_found:
logger.debug("✅ [Semantic Verification] Success confirmed: 'Following/Requested' state detected.")
return True
else:
logger.warning("❌ [Semantic Verification] FAILED: Profile does not report 'Following' state.")
return False
if any(k in low_intent for k in ["explore grid", "profile grid", "first image"]):
# Clicking a grid item MUST open a post view.
# Posts have feed markers. Reels have clips markers.
feed_markers = [
"row_feed_button_like", "row_feed_button_comment", "row_feed_button_share",
"row_feed_comment_textview_layout", "row_feed_view_group",
"clips_media_component", "row_feed_photo_profile_name", "row_feed_photo_imageview"
]
marker_found = any(m in low_xml for m in feed_markers)
if marker_found:
logger.debug("✅ [Semantic Verification] Success confirmed: Post opened from grid (Feed markers detected).")
return True
else:
logger.warning("❌ [Semantic Verification] FAILED: Grid tap did not open a valid post view.")
return False
# For general navigation, raw XML change is still the fallback
# (covered by the caller in q_nav_graph for now)
return True
# ──────────────────────────────────────────────
# Vision Cortex Fallback (VLM)
# ──────────────────────────────────────────────
@@ -552,15 +765,37 @@ class TelepathicEngine:
def _vision_cortex_fallback(self, intent: str, nodes: list[dict], device, screen_height: int = 2400) -> Optional[dict]:
"""
Uses a Language Model to identify the correct node from parsed screen XML
when embeddings are insufficient. 100% Screenshot-free for maximum speed and zero hallucination.
when embeddings are insufficient. Opt-in Native Vision Processing via Device Screenshots!
Guards are STRUCTURAL (size, position, class) not ID-based.
Learning happens via the confirm/reject feedback loop, not here.
"""
try:
# Limit to 20 nodes for token efficiency
from GramAddict.core.config import Config
args = getattr(Config(), "args", None)
use_vision = getattr(args, "ai_vision_navigation", False) if args else False
images_payload = None
if use_vision and device is not None:
try:
logger.debug("👁️ [Vision Inference] Capturing screen for spatial understanding...")
img_obj = device.screenshot()
if img_obj:
if hasattr(img_obj, "save"):
import io
buf = io.BytesIO()
img_obj.save(buf, format='JPEG')
raw_bytes = buf.getvalue()
else:
raw_bytes = img_obj
b64_str = base64.b64encode(raw_bytes).decode('utf-8')
images_payload = [b64_str]
except Exception as e:
logger.warning(f"⚠️ [Vision Inference] Failed to capture or encode screenshot: {e}")
# Expand context to 150 nodes to harness local 8k-32k models natively
simplified_nodes = []
for i, n in enumerate(nodes[:20]):
for i, n in enumerate(nodes[:150]):
simplified_nodes.append({
"index": i,
"bounds": n["raw_bounds"],
@@ -568,21 +803,39 @@ class TelepathicEngine:
})
# Get model config
from GramAddict.core.config import Config
try:
args = Config().args
except Exception:
args = None
model = getattr(args, "ai_telepathic_model", "google/gemini-3.1-flash-lite-preview") if args else "google/gemini-3.1-flash-lite-preview"
url = getattr(args, "ai_telepathic_url", "https://openrouter.ai/api/v1/chat/completions") if args else "https://openrouter.ai/api/v1/chat/completions"
if device and hasattr(device, 'args') and device.args:
model = getattr(device.args, "ai_telepathic_model", model)
url = getattr(device.args, "ai_telepathic_url", url)
args = getattr(device, "args", None)
model = getattr(args, "ai_telepathic_model", "llama3.2:1b") if args else "llama3.2:1b"
url = getattr(args, "ai_telepathic_url", "http://localhost:11434/api/generate") if args else "http://localhost:11434/api/generate"
# --- Model Trust Logging ---
from GramAddict.core.benchmark_guard import BENCHMARKS_FILE
trust_log = f"Using {model}"
try:
if os.path.exists(BENCHMARKS_FILE):
with open(BENCHMARKS_FILE, "r") as f:
bench_data = json.load(f).get("models", {}).get(model, {})
if bench_data:
score = bench_data.get("telepathic_score", 0)
passed = "PASS" if bench_data.get("passed_all", False) else "FAIL"
unsuitable = bench_data.get("is_unsuitable", False)
trust_level = "HIGH" if score >= 80 and not unsuitable else "MEDIUM" if score >= 50 and not unsuitable else "LOW/UNSAFE"
trust_log += f" [Benchmark: {score}/100 | {passed} | Trust: {trust_level}]"
if unsuitable:
logger.error(f"⛔ [Safety Alert] {model} is marked as UNSUITABLE for this task!")
except Exception:
pass
logger.info(f"🧠 [Telepathic] Intent: '{intent}' -> {trust_log}")
# ---------------------------
system_prompt = (
"You identify which UI element to tap based ONLY on a JSON array of parsed Android elements. "
"Each element has an 'index', structural 'bounds', and a 'semantic' description. "
"Output ONLY valid JSON containing the exact `index` to interact with, and a `reason`. "
"You are an Android UI expert. Identify the correct element index to tap based on the provided JSON.\n"
"Rules:\n"
"1. Output ONLY a raw JSON object.\n"
"2. NO markdown formatting, NO triple backticks, NO explanation.\n"
"3. Format: {\"index\": number, \"reason\": \"string\"}\n"
"4. If no element matches, return {\"index\": -1, \"reason\": \"no match\"}\n"
"5. FATAL RULES: NEVER select 'Share', 'Send post', 'Poll', or 'Survey' buttons UNLESS the intent explicitly commands it!\n"
"6. ACCOUNT SAFETY: NEVER select buttons that modify account state (Favorite, Mute, Block, Unfollow, Restrict) unless specifically commanded."
)
user_prompt = (
@@ -592,12 +845,27 @@ class TelepathicEngine:
"- Pick the SMALLEST, most specific button or icon\n"
"- NEVER pick large containers, full-screen views, or recycler views\n"
"- NEVER pick system icons (wifi, battery, status bar, clock)\n"
"- IGNORE BOTTOM NAVIGATION TABS (Home, Search, Reels, Message, Profile) if the intent is to interact with a post or comment.\n"
"- A 'Comment input' is usually an EditText or a region near the bottom but ABOVE the navigation bar.\n"
"- A 'story tray' or 'story ring' is ALWAYS located at the very TOP of the screen (low Y coordinates).\n"
"Return: {\"index\": number, \"reason\": \"...\"}"
)
resp_str = query_telepathic_llm(model, url, system_prompt, user_prompt)
resp_str = query_telepathic_llm(model, url, system_prompt, user_prompt, images_b64=images_payload)
data = json.loads(resp_str)
# ── Robustness: Handle list responses from LLM ──
if isinstance(data, list):
if len(data) > 0 and isinstance(data[0], dict):
data = data[0]
else:
logger.error(f"VLM returned unexpected list format: {data}")
return None
if not isinstance(data, dict):
logger.error(f"VLM returned non-dict response: {type(data)}")
return None
idx = data.get("index")
if idx is not None and 0 <= idx < len(nodes):
match = nodes[idx]
@@ -617,12 +885,14 @@ class TelepathicEngine:
})
return None
# ── Structural Guard 2: Position (status bar) ──
# ── Structural Guard 2: Position (status / nav bar) ──
if match.get("y", 0) < screen_height * STATUS_BAR_ZONE:
logger.error(
f"❌ [Structural Guard] VLM selected element in status bar zone "
f"(y={match.get('y')}): {match['semantic_string']}. REJECTING."
)
logger.error(f"❌ [Structural Guard] VLM selected element in status bar zone: {match['semantic_string']}. REJECTING.")
return None
is_nav_intent = any(k in intent.lower() for k in ["tab", "navigation", "search and explore", "reels", "profile", "home", "message"])
if match.get("y", 0) > screen_height * NAV_BAR_ZONE and not is_nav_intent:
logger.error(f"❌ [Structural Guard] VLM selected element in nav bar zone for non-nav intent '{intent}': {match['semantic_string']}. REJECTING.")
return None
# ── Structural Guard 3: Already blacklisted ──

View File

@@ -110,4 +110,4 @@ def _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, ses
if failed_scrolls > 3:
return "CONTEXT_LOST"
return "SESSION_OVER"
return "FEED_EXHAUSTED"