fix: eliminate thinking-block poisoning + no-op navigation trap
ROOT CAUSE: qwen3.5 (reasoning model) returns response='' with thinking block containing all reasoning. llm_provider.py line 352 silently substituted the thinking block as the response via: content = raw_response or raw_thinking or '' The Brain then extracted random actions from the reasoning text. FIXES: 1. llm_provider.py: Conditional thinking isolation - format_json=True (SAE/perception): thinking fallback preserved - format_json=False (Brain): thinking NEVER substituted - Added think=false for Ollama free-text calls to force direct response 2. planner.py: No-Op Guard strips tab actions that navigate to the current screen (e.g. 'tap profile tab' on OWN_PROFILE) 3. test_brain_live.py: Stochastic testing (5 runs, 60% min valid) to handle non-deterministic LLM behavior reliably 4. tests/integration/test_llm_provider_pipeline.py: NEW test layer mocking at HTTP level (requests.post) to exercise the FULL llm_provider → Brain pipeline. This would have caught the thinking substitution bug from day one. Suite: 168 passed, 0 failed
This commit is contained in:
@@ -287,6 +287,12 @@ def query_llm(
|
||||
req_data["images"] = images_b64
|
||||
if format_json:
|
||||
req_data["format"] = "json"
|
||||
else:
|
||||
# For free-text calls (Brain action extraction), explicitly disable
|
||||
# thinking mode. Reasoning models like qwen3.5 put EVERYTHING in
|
||||
# the thinking block and return response='', which is useless for
|
||||
# action extraction. think=false forces a direct response.
|
||||
req_data["think"] = False
|
||||
|
||||
# Ollama passes configs inside 'options'
|
||||
if temperature is not None or max_tokens is not None:
|
||||
@@ -349,13 +355,20 @@ def query_llm(
|
||||
|
||||
logger.debug(f"DEBUG LLM PAYLOAD: response='{raw_response}', thinking='{raw_thinking}'")
|
||||
|
||||
content = raw_response or raw_thinking or ""
|
||||
# CRITICAL: For free-text mode (format_json=False), do NOT substitute
|
||||
# thinking for empty response. The thinking block is REASONING, not
|
||||
# a decision. The Brain parser would extract random actions from it.
|
||||
# For JSON mode (format_json=True), falling back to thinking IS correct
|
||||
# because reasoning models may place structured output in the thinking block.
|
||||
if format_json:
|
||||
content = raw_response or raw_thinking or ""
|
||||
extracted = extract_json(content)
|
||||
if not extracted:
|
||||
logger.warning(f"Failed to extract JSON from content: {content[:100]}")
|
||||
else:
|
||||
content = extracted
|
||||
else:
|
||||
content = raw_response
|
||||
|
||||
return {"response": content}
|
||||
except requests.exceptions.ConnectionError:
|
||||
|
||||
@@ -94,6 +94,27 @@ class GoalPlanner:
|
||||
logger.debug(f"🛡️ [Aversive Filter] Masking trapped action: '{action}'")
|
||||
available = safe_available
|
||||
|
||||
# 0b. No-Op Guard: Strip tab actions that navigate to the CURRENT screen.
|
||||
# e.g. 'tap profile tab' on OWN_PROFILE is always a no-op.
|
||||
noop_actions = set()
|
||||
for action in available:
|
||||
expected = ScreenTopology.expected_screen_for_action(action, screen_type)
|
||||
if expected == screen_type:
|
||||
noop_actions.add(action)
|
||||
logger.debug(f"🛡️ [No-Op Guard] Stripping '{action}' — leads back to {screen_type.name}")
|
||||
|
||||
# Also strip actions where the HD Map says they go TO the current screen from OTHER screens
|
||||
# (e.g., 'tap profile tab' isn't in OWN_PROFILE's transitions, but it goes to OWN_PROFILE from other screens)
|
||||
for src_screen, transitions in ScreenTopology.TRANSITIONS.items():
|
||||
if src_screen == screen_type:
|
||||
continue # We already handled this screen's own transitions
|
||||
for action, dest in transitions.items():
|
||||
if dest == screen_type and action in available:
|
||||
noop_actions.add(action)
|
||||
logger.debug(f"🛡️ [No-Op Guard] Stripping '{action}' — known to navigate to current {screen_type.name}")
|
||||
|
||||
available = [a for a in available if a not in noop_actions]
|
||||
|
||||
# Build avoid_actions for HD Map route planning
|
||||
avoid_actions = (explored_nav_actions or set()).copy()
|
||||
if action_failures:
|
||||
|
||||
Reference in New Issue
Block a user