diff --git a/GramAddict/core/goap.py b/GramAddict/core/goap.py
index 48676df..f8cfc2d 100644
--- a/GramAddict/core/goap.py
+++ b/GramAddict/core/goap.py
@@ -329,7 +329,7 @@ class GoalExecutor:
return False
- def _execute_action(self, action: str, goal: str = None) -> bool:
+ def _execute_action(self, action: str, goal: str = None, **kwargs) -> bool:
"""Execute a single natural-language action using the TelepathicEngine."""
if action == "press back":
@@ -349,6 +349,9 @@ class GoalExecutor:
random_sleep(2.0, 3.5)
return True
+ if action == "type and post comment":
+ return self._execute_type_and_post_comment(kwargs.get("text", ""))
+
# Use TelepathicEngine for any semantic click
from GramAddict.core.telepathic_engine import TelepathicEngine
@@ -555,6 +558,83 @@ class GoalExecutor:
achieved = self.planner.plan_next_step(goal, screen) is None
return achieved
+ def _execute_type_and_post_comment(self, fallback_text: str) -> bool:
+ """Handles the specific typing interaction, prioritizing Meta AI chips if available."""
+ import xml.etree.ElementTree as ET
+
+ from GramAddict.core.telepathic_engine import TelepathicEngine
+
+ logger.info("💬 [GOAP] Executing 'type and post comment'...")
+ engine = TelepathicEngine.get_instance()
+
+ # 1. Tap the comment input field to open the keyboard
+ xml_dump = self.device.dump_hierarchy()
+ if "com.google.android.inputmethod.latin" not in xml_dump:
+ logger.info("⌨️ [GOAP] Tapping comment composer to open keyboard...")
+ best_node = engine.find_best_node(
+ xml_dump, "tap comment input field", min_confidence=0.7, device=self.device
+ )
+ if best_node:
+ self.device.click(obj=best_node)
+ random_sleep(1.5, 2.5)
+ xml_dump = self.device.dump_hierarchy()
+ else:
+ logger.warning("⚠️ [GOAP] Could not find comment input field.")
+ return False
+
+ # 2. Check for Meta AI chips (Supportive, Funny, etc.)
+ meta_ai_chips = []
+ try:
+ root = ET.fromstring(xml_dump.encode("utf-8"))
+ for node in root.iter("node"):
+ node_text = node.attrib.get("text", "")
+ if node_text in ["Supportive", "Rewrite", "Absurd", "Casual", "Funny", "Heartfelt", "Professional"]:
+ meta_ai_chips.append(node_text)
+ except Exception as e:
+ logger.debug(f"XML parse error for Meta AI chips: {e}")
+
+ if meta_ai_chips:
+ logger.info(f"✨ [Meta AI] Detected Meta AI chips: {meta_ai_chips}")
+ logger.info("🧠 [Meta AI] Asking VLM to select the best tone...")
+
+ # Use Telepathic Engine to pick the best chip via VLM
+ chip_node = engine.find_best_node(
+ xml_dump,
+ "tap the best Meta AI tone chip to generate a comment (e.g. Supportive, Funny, Casual)",
+ min_confidence=0.6,
+ device=self.device,
+ )
+
+ if chip_node:
+ logger.info(f"✨ [Meta AI] VLM selected chip: '{chip_node.get('text', 'Unknown')}'")
+ self.device.click(obj=chip_node)
+ random_sleep(3.0, 4.5) # Wait for Meta AI to generate the text
+ else:
+ logger.warning("⚠️ [Meta AI] VLM failed to pick a chip, falling back to manual typing.")
+ from GramAddict.core.stealth_typing import ghost_type
+
+ ghost_type(self.device, fallback_text)
+ random_sleep(1.0, 2.0)
+ else:
+ # 3. Fallback: Type the text provided by our own VLM writer
+ logger.info(f"⌨️ [GOAP] Typing comment manually: {fallback_text}")
+ from GramAddict.core.stealth_typing import ghost_type
+
+ ghost_type(self.device, fallback_text)
+ random_sleep(1.0, 2.0)
+
+ # 4. Click the Post button
+ xml_dump = self.device.dump_hierarchy()
+ post_btn = engine.find_best_node(xml_dump, "tap post comment button", min_confidence=0.7, device=self.device)
+ if post_btn:
+ self.device.click(obj=post_btn)
+ random_sleep(1.5, 2.5)
+ logger.info("✅ [GOAP] Comment posted successfully.")
+ return True
+ else:
+ logger.warning("⚠️ [GOAP] Could not find post button after typing.")
+ return False
+
# ── Convenience methods (backward compatibility with navigate_to) ──
def navigate_to_screen(self, target: str) -> bool:
diff --git a/tests/e2e/test_goap_meta_ai_comment.py b/tests/e2e/test_goap_meta_ai_comment.py
new file mode 100644
index 0000000..c1620bc
--- /dev/null
+++ b/tests/e2e/test_goap_meta_ai_comment.py
@@ -0,0 +1,133 @@
+import pytest
+
+
+@pytest.mark.live_llm
+def test_goap_meta_ai_comment_selection(make_real_device_with_image):
+ """
+ TDD Test: Verifies that when the comment keyboard is open and Meta AI chips
+ are present on the screen, the GOAP engine detects them and uses the VLM
+ to select the best tone chip, entirely bypassing manual fallback typing.
+ """
+ from GramAddict.core.goap import GoalExecutor
+
+ # Define an XML hierarchy that simulates the comment keyboard open with Meta AI chips.
+ xml_chip_first = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+
+ xml_post_first = """
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+
+ # We provide this XML twice: once for the initial check (picks chip), once for the 'Post' button click.
+ device = make_real_device_with_image(
+ "tests/fixtures/home_feed_with_ad.jpg", [xml_chip_first, xml_post_first, xml_post_first]
+ )
+
+ # To track if ghost_type was incorrectly called
+ type_text_calls = []
+
+ def mock_ghost_type(dev, text, speed="normal"):
+ type_text_calls.append(text)
+
+ # Monkeypatch the module where ghost_type is imported, or just the whole module.
+ # Actually, goap.py imports ghost_type dynamically:
+ # `from GramAddict.core.stealth_typing import ghost_type`
+ # So we monkeypatch it in stealth_typing
+ import GramAddict.core.stealth_typing
+
+ original_ghost_type = GramAddict.core.stealth_typing.ghost_type
+ GramAddict.core.stealth_typing.ghost_type = mock_ghost_type
+
+ import GramAddict.core.telepathic_engine
+
+ original_find_best_node = GramAddict.core.telepathic_engine.TelepathicEngine.find_best_node
+
+ def mock_find_best_node(self, xml_dump, instruction, *args, **kwargs):
+ # We parse the XML to return a specific node based on the instruction
+ import re
+ import xml.etree.ElementTree as ET
+
+ root = ET.fromstring(xml_dump.encode("utf-8"))
+
+ def _enrich_node(node):
+ attribs = dict(node.attrib)
+ bounds_str = attribs.get("bounds", "")
+ match = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds_str)
+ if match:
+ left, top, right, bottom = map(int, match.groups())
+ attribs["x"] = (left + right) // 2
+ attribs["y"] = (top + bottom) // 2
+ return attribs
+
+ if "tap the best Meta AI tone chip" in instruction:
+ for node in root.iter("node"):
+ if node.attrib.get("text") == "Funny":
+ return _enrich_node(node)
+ return None
+ elif "tap post comment button" in instruction:
+ for node in root.iter("node"):
+ if node.attrib.get("text") == "Post":
+ return _enrich_node(node)
+ return None
+ return None
+
+ GramAddict.core.telepathic_engine.TelepathicEngine.find_best_node = mock_find_best_node
+
+ try:
+ goap = GoalExecutor.get_instance(device, bot_username="testuser")
+
+ # Execute the new GOAP action
+ result = goap._execute_action("type and post comment", text="This is a fallback text")
+ finally:
+ # Cleanup mocks
+ GramAddict.core.stealth_typing.ghost_type = original_ghost_type
+ GramAddict.core.telepathic_engine.TelepathicEngine.find_best_node = original_find_best_node
+
+ # Assertions
+ assert result is True, "GOAP action 'type and post comment' failed."
+
+ # We expect 2 clicks:
+ # 1. Tapping one of the Meta AI chips (y between 1150 and 1250)
+ # 2. Tapping the "Post" button (y between 1000 and 1100)
+ # (Since keyboard is already open, it shouldn't tap the input field)
+ assert len(device.clicks) >= 2, f"Expected at least 2 clicks (Meta AI chip + Post), got {len(device.clicks)}"
+
+ print(f"DEBUG: device.clicks = {device.clicks}")
+ # Ensure a Meta AI chip was clicked (y between 1150 and 1250)
+ chip_clicked = any(1150 <= y <= 1250 for (x, y) in device.clicks)
+ assert chip_clicked, f"VLM failed to select and click a Meta AI chip! Clicks were: {device.clicks}"
+
+ # Ensure manual typing was bypassed!
+ assert len(type_text_calls) == 0, f"Expected 0 ghost_type calls, but got: {type_text_calls}"
+
+ # Cleanup
+ GramAddict.core.stealth_typing.ghost_type = original_ghost_type