From d49a51a5e8a67485d9094935d5e7ce136d3877f5 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 24 Apr 2026 23:28:18 -0400 Subject: [PATCH] fix(agent): tolerant intent JSON parse + log raw output on failure The classifier was silently falling back to a clarification prompt every time the LLM wrapped its JSON in markdown fences, prefixed it with 'json', or added surrounding prose. The bot then asked 'Could you clarify what you need?' to every message regardless of clarity. Now: strip code fences, slice to the first {...} block, and on parse failure log the raw content (truncated) and treat the message as 'no specialist agent' so the direct-answer fallback responds instead of looping on clarification. --- agent_service/agents/master_agent.py | 40 +++++++++++++++++++--------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/agent_service/agents/master_agent.py b/agent_service/agents/master_agent.py index 8af8eec..eba946d 100644 --- a/agent_service/agents/master_agent.py +++ b/agent_service/agents/master_agent.py @@ -147,23 +147,37 @@ class MasterAgent: msgs = [{'role': 'system', 'content': system}, *history, {'role': 'user', 'content': message}] resp = await self._llm.submit(msgs, caller='master') + raw = (resp.content or '').strip() + # Strip markdown fences like ```json ... ``` + if raw.startswith('```'): + raw = raw.strip('`') + if raw.lower().startswith('json'): + raw = raw[4:] + raw = raw.strip() + # Pull out the first {...} block if there's surrounding prose. + first = raw.find('{') + last = raw.rfind('}') + if first != -1 and last != -1 and last > first: + raw = raw[first:last + 1] try: - raw = resp.content.strip() - if raw.startswith(chr(96)*3): - raw = raw.split(chr(10), 1)[1].rsplit(chr(10), 1)[0] data = json.loads(raw) + if not isinstance(data, dict): + raise ValueError(f'expected JSON object, got {type(data).__name__}') return IntentResult( - needs_clarification=data.get('needs_clarification', False), + needs_clarification=bool(data.get('needs_clarification', False)), clarification_question=data.get('clarification_question'), - is_continuation=data.get('is_continuation', False), - agents=data.get('agents', []), - intent_summary=data.get('intent_summary', ''), - params=data.get('params', {}), - context_hints=data.get('context_hints', [])) - except (json.JSONDecodeError, KeyError) as exc: - logger.warning('Intent classification parse failed: %s', exc) - return IntentResult(needs_clarification=True, - clarification_question='Could you clarify what you need?') + is_continuation=bool(data.get('is_continuation', False)), + agents=data.get('agents') or [], + intent_summary=data.get('intent_summary', '') or '', + params=data.get('params') or {}, + context_hints=data.get('context_hints') or []) + except Exception as exc: + logger.warning('Intent classification parse failed (%s); raw=%r', + exc, (resp.content or '')[:300]) + # Treat unparseable LLM output as 'no specialist agent applies' so + # the direct-answer fallback can handle it instead of looping on + # clarification. + return IntentResult(needs_clarification=False, agents=[]) async def _check_access(self, user_id, agents) -> AccessResult: denied = []