6 Commits

13 changed files with 212 additions and 18 deletions

View File

@@ -47,7 +47,7 @@ def verify_and_switch_account(device, nav_graph, target_username):
telepath = TelepathicEngine.get_instance()
# We ask the semantic engine to find the profile tab, ensuring 100% ID-agnostic behavior
profile_tab_node = telepath.find_best_node(xml_dump, "tap profile tab", min_threshold=0.3)
profile_tab_node = telepath.find_best_node(xml_dump, "tap profile tab", min_threshold=0.3, device=device)
if profile_tab_node:
profile_tab = (profile_tab_node["x"], profile_tab_node["y"])
except Exception as e:
@@ -113,7 +113,7 @@ def verify_and_switch_account(device, nav_graph, target_username):
dump_ui_state(
device, "identity_guard", {"reason": "account_not_found_in_bottom_sheet", "target": target_username}
)
except:
except Exception:
pass
# Escape the bottom sheet
device.press("back")

View File

@@ -56,7 +56,7 @@ class ObstacleGuardPlugin(BehaviorPlugin):
# Check recovery
new_xml = ctx.device.dump_hierarchy()
tele = TelepathicEngine.get_instance()
best_node = tele.find_best_node(new_xml, intent_description="Dismiss obstacle")
best_node = tele.find_best_node(new_xml, intent_description="Dismiss obstacle", device=ctx.device)
if best_node:
ctx.device.click(best_node.get("x", 0), best_node.get("y", 0))

View File

@@ -30,7 +30,7 @@ class PostDataExtractionPlugin(BehaviorPlugin):
def execute(self, ctx: BehaviorContext) -> BehaviorResult:
logger.debug("🧩 [PostDataExtraction] Extracting post metadata...")
post_data = extract_post_content(ctx.context_xml)
post_data = extract_post_content(ctx.context_xml, device=ctx.device)
if post_data:
ctx.post_data = post_data

View File

@@ -66,6 +66,16 @@ class ResonanceEvaluatorPlugin(BehaviorPlugin):
"✨ [Resonance] VLM vibe check returned None (truncated JSON?). Keeping neutral score."
)
else:
if vibe.get("is_ad"):
logger.info("🛡️ [Resonance Oracle] Visually identified post as an Ad! Skipping...")
marker = vibe.get("ad_marker_text")
if marker and marker.strip():
from GramAddict.core.utils import learn_ad_marker
learn_ad_marker(marker, ctx.context_xml)
from GramAddict.core.utils import humanized_scroll
humanized_scroll(ctx.device)
return BehaviorResult(executed=True, should_skip=True)
# BUG 6 Fix: VLM returns {"should_like": true/false}, not "quality_score"
should_like = vibe.get("should_like", False)
vibe_score = 1.0 if should_like else 0.2

View File

@@ -177,6 +177,11 @@ def start_bot(**kwargs):
)
persona_interests = [p.strip() for p in persona_raw.split(",") if p.strip()] if persona_raw else []
global_goal = getattr(configs.args, "goal", None)
if global_goal:
persona_interests.insert(0, global_goal)
logger.info(f"🎯 [Autonomous Directive] Overriding target audience with high-level goal: {global_goal}", extra={"color": f"{Style.BRIGHT}{Fore.GREEN}"})
from GramAddict.core.interaction import LLMWriter
from GramAddict.core.qdrant_memory import DMMemoryDB, ParasocialCRMDB
from GramAddict.core.resonance_engine import ResonanceEngine

View File

@@ -152,6 +152,13 @@ class Config:
help="Wipe all learned navigation and telepathic memories on boot to start 100%% blank.",
)
self.parser.add_argument(
"--goal",
type=str,
help="High-level autonomous goal for the bot (Tesla-style). Overrides config.yml goals.",
default=None,
)
# Interaction settings
self.parser.add_argument("--likes-count", help="Likes count", default="2-3")
self.parser.add_argument("--likes-percentage", help="Likes percentage", default="100")

View File

@@ -1,10 +1,46 @@
import json
import logging
import re
from typing import Any, Dict, Optional
from GramAddict.core.perception.spatial_parser import SpatialNode
logger = logging.getLogger(__name__)
def _parse_yes_no(response: str) -> Optional[bool]:
"""Parses a VLM response to find a definitive YES or NO without substring-matching 'not' or 'now'."""
text = response.strip()
# Try parsing as JSON first
if text.startswith("{"):
try:
data = json.loads(text)
for k, v in data.items():
if str(k).strip().upper() == "YES" or str(v).strip().upper() == "YES":
return True
if str(k).strip().upper() == "NO" or str(v).strip().upper() == "NO":
return False
except Exception:
pass
text_lower = text.lower()
if text_lower.startswith("yes"):
return True
if text_lower.startswith("no") and not text_lower.startswith("now") and not text_lower.startswith("not"):
return False
has_yes = re.search(r"\byes\b", text_lower) is not None
has_no = re.search(r"\bno\b", text_lower) is not None
if has_yes and not has_no:
return True
if has_no and not has_yes:
return False
return None
# ═══════════════════════════════════════════════════════
# Semantic Match Keywords — SSOT for intent → element validation
# ═══════════════════════════════════════════════════════
@@ -189,10 +225,12 @@ class ActionMemory:
raise ValueError("No screenshot available from device")
response = evaluator._query_vlm(prompt, screenshot)
if response and "yes" in response.lower() and "no" not in response.lower():
decision = _parse_yes_no(response) if response else None
if decision is True:
logger.debug(f"🧠 [ActionMemory] VLM visually confirmed success for '{intent}'.")
return True
elif response and "no" in response.lower() and "yes" not in response.lower():
elif decision is False:
logger.warning(
f"⚠️ [ActionMemory] VLM visual verification FAILED for '{intent}'. VLM replied: '{response}'"
)
@@ -265,10 +303,13 @@ class ActionMemory:
prompt = f"The user just attempted to perform the action: '{intent}'. Does the current screen match the expected outcome? Answer ONLY with the word YES or NO."
try:
response = evaluator._query_vlm(prompt, device.get_screenshot_b64())
if response and "yes" in response.lower() and "no" not in response.lower():
decision = _parse_yes_no(response) if response else None
if decision is True:
return True
else:
logger.warning(f"⚠️ [ActionMemory] VLM rejected success for abstract intent '{intent}'.")
logger.warning(
f"⚠️ [ActionMemory] VLM rejected success for abstract intent '{intent}'. Response: '{response}'"
)
return False
except Exception as e:
logger.error(f"VLM visual verification failed: {e}")

View File

@@ -46,7 +46,7 @@ def has_carousel_in_view(xml_dump: str) -> bool:
return any(ind in xml_dump for ind in CAROUSEL_INDICATORS)
def extract_post_content(context_xml: str) -> dict:
def extract_post_content(context_xml: str, device=None) -> dict:
"""
Extracts meaningful content data from the current feed post's XML.
This is the BOT'S EYES — what it actually "sees" about each post.
@@ -62,14 +62,18 @@ def extract_post_content(context_xml: str) -> dict:
telepath = TelepathicEngine.get_instance()
# 1. Learn/extract post author dynamically
author_node = telepath.find_best_node(context_xml, "post author username header", min_confidence=0.75)
author_node = telepath.find_best_node(
context_xml, "post author username text (exclude bottom tabs)", min_confidence=0.75, device=device
)
# 🛡️ Anti-Hallucination Guard: Ensure we actually found text.
if author_node and author_node.get("original_attribs", {}).get("text"):
result["username"] = author_node["original_attribs"]["text"].strip()
# 2. Learn/extract post media description dynamically
media_node = telepath.find_best_node(context_xml, "post media content", min_confidence=0.35)
media_node = telepath.find_best_node(
context_xml, "post media content (the actual image or video, exclude bottom tabs)", min_confidence=0.35, device=device
)
if media_node and media_node.get("original_attribs", {}).get("desc"):
result["description"] = media_node["original_attribs"]["desc"].strip()

View File

@@ -365,6 +365,12 @@ class IntentResolver:
)
data = json.loads(res)
box_idx = data.get("box")
if box_idx is None:
box_idx = data.get("selected_index")
if box_idx is None:
box_idx = data.get("box_index")
if box_idx is None:
box_idx = data.get("index")
if box_idx is not None and box_idx in box_map:
selected = box_map[box_idx]

View File

@@ -117,11 +117,13 @@ class SemanticEvaluator:
You are a user with the following interests: {', '.join(persona_interests)}.
You are looking at an Instagram post.
Evaluate if this post is highly relevant to your interests and if you should like/comment on it.
CRITICAL: Check if this post is an advertisement or sponsored content (look for "Sponsored", "Ad", or promotional product placement).
Reply ONLY in valid JSON format:
{{
"should_like": true/false,
"should_comment": true/false,
"is_ad": true/false,
"reasoning": "brief explanation"
}}
"""

View File

@@ -140,8 +140,8 @@ def align_active_post(device):
# Intents for structural discovery
intents = [
"post author username text (exclude follow buttons)",
"post author header profile",
"post username name",
"row_feed_photo_profile_name", # ID fallback
"clips_viewer_author_container", # Reels fallback
"feed post content", # Final desperation
@@ -151,6 +151,10 @@ def align_active_post(device):
attempts += 1
try:
xml = device.dump_hierarchy()
if "clips_video_container" in xml or "clips_viewer_container" in xml:
logger.info("🎯 [Alignment] Reels view detected. Auto-snapping is native.")
return True
from GramAddict.core.telepathic_engine import TelepathicEngine
telepath = TelepathicEngine.get_instance()

View File

@@ -1,4 +1,6 @@
import logging
import json
import os
import random
from time import sleep
@@ -95,6 +97,62 @@ def get_value(count, name, default=0):
return default
_LEARNED_AD_MARKERS_FILE = os.path.join(os.getcwd(), "learned_ad_markers.json")
_LEARNED_AD_MARKERS_CACHE = None
def get_learned_ad_markers() -> set:
global _LEARNED_AD_MARKERS_CACHE
if _LEARNED_AD_MARKERS_CACHE is not None:
return _LEARNED_AD_MARKERS_CACHE
if os.path.exists(_LEARNED_AD_MARKERS_FILE):
try:
with open(_LEARNED_AD_MARKERS_FILE, "r") as f:
_LEARNED_AD_MARKERS_CACHE = set(json.load(f))
except Exception as e:
logger.error(f"Failed to load learned ad markers: {e}")
_LEARNED_AD_MARKERS_CACHE = set()
else:
_LEARNED_AD_MARKERS_CACHE = set()
return _LEARNED_AD_MARKERS_CACHE
def learn_ad_marker(marker: str, xml_hierarchy: str):
global _LEARNED_AD_MARKERS_CACHE
if not marker or len(marker) > 30:
return
marker = marker.strip().lower()
# Structural verification: the VLM-suggested marker MUST exist as an exact node text/desc in the current UI!
import xml.etree.ElementTree as ET
try:
root = ET.fromstring(xml_hierarchy)
found_in_ui = False
for node in root.iter("node"):
text = node.attrib.get("text", "").strip().lower()
desc = node.attrib.get("content-desc", "").strip().lower()
if text == marker or desc == marker:
found_in_ui = True
break
if not found_in_ui:
logger.debug(f"🧠 [Autonomous FSD] Rejected hallucinated Ad marker '{marker}' (not found as exact node match in UI).")
return
except Exception:
return
markers = get_learned_ad_markers()
if marker not in markers and marker not in {"ad", "sponsored", "advertisement", "gesponsert", "anzeige", "werbung"}:
markers.add(marker)
logger.info(f"🧠 [Autonomous FSD] Verified and Learned new Ad marker: '{marker}'. Persisting for zero-latency detection.", extra={"color": f"{Style.BRIGHT}{Fore.GREEN}"})
try:
with open(_LEARNED_AD_MARKERS_FILE, "w") as f:
json.dump(list(markers), f)
except Exception as e:
logger.error(f"Failed to save learned ad markers: {e}")
def is_ad(xml_hierarchy: str, cognitive_stack: dict = None) -> bool:
"""
Checks if the current view contains an advertisement using autonomous learning.
@@ -125,26 +183,34 @@ def is_ad(xml_hierarchy: str, cognitive_stack: dict = None) -> bool:
# Standalone label patterns: match only when the text/desc IS the ad marker,
# not when "ad" appears inside longer phrases like "Create messaging ad"
AD_EXACT_LABELS = {"ad", "sponsored", "advertisement", "gesponsert", "anzeige", "werbung"}
AD_EXACT_LABELS.update(get_learned_ad_markers())
try:
root = ET.fromstring(xml_hierarchy)
# Check if we are in a feed (to prevent false positives on profiles with 'Ad Tools' buttons)
from GramAddict.core.perception.feed_analysis import FEED_MARKERS
in_feed = any(marker in xml_hierarchy for marker in FEED_MARKERS)
for node in root.iter("node"):
attrib = node.attrib
content_desc = attrib.get("content-desc", "")
text = attrib.get("text", "")
res_id = attrib.get("resource-id", "")
# Structural check (Instagram specific)
# Structural check (Instagram specific) is always trusted
if any(marker_id in res_id for marker_id in AD_RESOURCE_IDS):
return True
# Exact label match: only trigger when the entire text/desc
# IS an ad marker (e.g. text="Ad", content-desc="Sponsored")
# This prevents false positives from "Create messaging ad"
if text.strip().lower() in AD_EXACT_LABELS:
return True
if content_desc.strip().lower() in AD_EXACT_LABELS:
return True
# We ONLY trust this if we are actually in a feed, to prevent triggering
# on the "Ad Tools" / "Ad" buttons present on business profiles.
if in_feed:
if text.strip().lower() in AD_EXACT_LABELS:
return True
if content_desc.strip().lower() in AD_EXACT_LABELS:
return True
except Exception:
pass

View File

@@ -0,0 +1,49 @@
import pytest
import os
import json
from GramAddict.core.utils import is_ad, learn_ad_marker, get_learned_ad_markers
@pytest.fixture(autouse=True)
def clean_ad_markers():
# Clean up any existing learned markers file before and after tests
file_path = os.path.join(os.getcwd(), "learned_ad_markers.json")
if os.path.exists(file_path):
os.remove(file_path)
import GramAddict.core.utils
GramAddict.core.utils._LEARNED_AD_MARKERS_CACHE = None
yield
if os.path.exists(file_path):
os.remove(file_path)
GramAddict.core.utils._LEARNED_AD_MARKERS_CACHE = None
def test_learn_ad_marker_validates_against_xml():
xml_dump = """<?xml version="1.0" encoding="utf-8"?>
<hierarchy>
<node class="android.widget.TextView" text="Sponsorisé" resource-id="com.instagram.android:id/some_id" content-desc=""/>
</hierarchy>
"""
# Attempt to learn a hallucinated marker
learn_ad_marker("Hallucination", xml_dump)
assert "hallucination" not in get_learned_ad_markers()
# Attempt to learn an actual marker present in XML
learn_ad_marker("Sponsorisé", xml_dump)
assert "sponsorisé" in get_learned_ad_markers()
def test_is_ad_uses_learned_markers():
xml_dump = """<?xml version="1.0" encoding="utf-8"?>
<hierarchy>
<node class="android.widget.TextView" text="Sponsorisé" resource-id="com.instagram.android:id/some_id" content-desc=""/>
<node resource-id="row_feed_photo_profile_name" text="someone" />
</hierarchy>
"""
# Initially, it shouldn't recognize "Sponsorisé" because it's not in the hardcoded list
assert is_ad(xml_dump) is False
# Learn the new marker
learn_ad_marker("Sponsorisé", xml_dump)
# Now, is_ad should return True immediately without VLM
assert is_ad(xml_dump) is True