Efren Yevale Varela 1 lună în urmă
părinte
comite
ce4411872c
2 a modificat fișierele cu 108 adăugiri și 13 ștergeri
  1. 56 13
      src/stt_daemon/assistant/triage.py
  2. 52 0
      tests/test_triage.py

+ 56 - 13
src/stt_daemon/assistant/triage.py

@@ -379,11 +379,52 @@ class TriageAnalyzer:
         parts.append("Return only the updated brief as plain text.")
         parts.append("Return only the updated brief as plain text.")
         return "\n\n".join(parts)
         return "\n\n".join(parts)
 
 
-    def _unwrap_tool_call(self, text: str) -> str:
-        """Unwrap a stray model tool-call envelope into its plain brief payload.
+    @staticmethod
+    def _first_arg_start(text: str) -> int:
+        name_at = text.find("updated_brief")
+        if name_at != -1:
+            cursor = name_at + len("updated_brief")
+            while cursor < len(text) and text[cursor] == " ":
+                cursor += 1
+            if cursor < len(text) and text[cursor] == "=":
+                return cursor + 1
+        call_at = text.find("update_brief(")
+        if call_at == -1:
+            call_at = text.find("update_brief (")
+        return call_at + len("update_brief(") if call_at != -1 else 0
+
+    @staticmethod
+    def _first_quote(text: str, start: int) -> tuple[int, str]:
+        opening = -1
+        quote = "'"
+        for candidate in ("'", '"'):
+            index = text.find(candidate, start)
+            if index != -1 and (opening == -1 or index < opening):
+                opening, quote = index, candidate
+        return opening, quote
+
+    @staticmethod
+    def _arg_boundary_quote(text: str, opening: int, quote: str) -> int:
+        index = opening + 1
+        while True:
+            candidate = text.find(quote, index)
+            if candidate == -1:
+                return -1
+            after = candidate + 1
+            while after < len(text) and text[after] == " ":
+                after += 1
+            if after >= len(text) or text[after] in ",)]<":
+                return candidate
+            index = candidate + 1
+
+    def _unwrap_tool_call(self, text: str) -> str | None:
+        """Unwrap a stray tool-call envelope into its first plain argument value.
 
 
         Only fires when an explicit wrapper marker is present so clean markdown
         Only fires when an explicit wrapper marker is present so clean markdown
-        containing ``[``, ``(``, or ``'`` is passed through untouched.
+        containing ``[``, ``(``, or ``'`` is passed through untouched. The first
+        argument is taken up to its closing quote boundary, so a multi-argument
+        call never swallows the separators; an envelope that cannot be reduced to
+        a clean payload is rejected (None) rather than stored verbatim.
         """
         """
         markers = ("<|tool_call_start|>", "<|tool_call_end|>", "update_brief(", "update_brief (")
         markers = ("<|tool_call_start|>", "<|tool_call_end|>", "update_brief(", "update_brief (")
         if not any(marker in text for marker in markers):
         if not any(marker in text for marker in markers):
@@ -394,17 +435,19 @@ class TriageAnalyzer:
             text = text.split("<|tool_call_end|>", 1)[0]
             text = text.split("<|tool_call_end|>", 1)[0]
         text = text.strip()
         text = text.strip()
         if "update_brief(" not in text and "update_brief (" not in text:
         if "update_brief(" not in text and "update_brief (" not in text:
+            if any(token in text for token in ("<|", "previous_brief=", "mode=")):
+                return None
             return text
             return text
-        # Extract the argument between the first and last matching quote. The
-        # payload itself may contain apostrophes, so pair on the LAST quote of
-        # whichever delimiter opens first.
-        positions = [(text.find(quote), quote) for quote in ("'", '"') if text.find(quote) != -1]
-        if positions:
-            first, quote = min(positions)
-            last = text.rfind(quote)
-            if last > first:
-                text = text[first + 1 : last]
-        return text.strip()
+        opening, quote = self._first_quote(text, self._first_arg_start(text))
+        if opening == -1:
+            return None
+        closing = self._arg_boundary_quote(text, opening, quote)
+        if closing == -1:
+            return None
+        value = text[opening + 1 : closing].strip()
+        if any(token in value for token in ("<|", "previous_brief=", "mode=")):
+            return None
+        return value
 
 
     def parse_brief(self, text) -> str | None:
     def parse_brief(self, text) -> str | None:
         """Strip a code fence and any stray tool-call envelope; None when empty."""
         """Strip a code fence and any stray tool-call envelope; None when empty."""

+ 52 - 0
tests/test_triage.py

@@ -471,6 +471,58 @@ def test_parse_brief_empty_tool_call_payload_is_none(tmp_path):
     assert analyzer.parse_brief("[update_brief(updated_brief='')]") is None
     assert analyzer.parse_brief("[update_brief(updated_brief='')]") is None
 
 
 
 
+def test_parse_brief_unwraps_multi_arg_tool_call(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    wrapped = (
+        "[update_brief(updated_brief='## Opportunities\n- ship Friday', "
+        "previous_brief='## old\n- y', mode='read')]"
+    )
+    assert analyzer.parse_brief(wrapped) == "## Opportunities\n- ship Friday"
+
+
+def test_parse_brief_unwraps_multi_arg_with_markers(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    wrapped = (
+        "<|tool_call_start|>[update_brief(updated_brief='## Opportunities\n- ship Friday', "
+        "previous_brief='## old\n- y', mode='read')]<|tool_call_end|>"
+    )
+    assert analyzer.parse_brief(wrapped) == "## Opportunities\n- ship Friday"
+
+
+def test_parse_brief_unwraps_multi_arg_apostrophes_preserved(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    wrapped = (
+        "[update_brief(updated_brief='the company's game failed', "
+        "previous_brief='## old', mode='read')]"
+    )
+    assert analyzer.parse_brief(wrapped) == "the company's game failed"
+
+
+def test_parse_brief_unwraps_positional_tool_call(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    assert analyzer.parse_brief("[update_brief('payload here', mode='read')]") == "payload here"
+
+
+def test_parse_brief_rejects_polluted_tool_call(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    polluted = "[update_brief(updated_brief='oops, previous_brief=', mode='read')]"
+    assert analyzer.parse_brief(polluted) is None
+    no_arg = "<|tool_call_start|>[update_brief(previous_brief=old, mode=read)]"
+    assert analyzer.parse_brief(no_arg) is None
+
+
+def test_parse_brief_clean_mode_text_not_mangled(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    clean = "Set mode=read, then read [the brief] - it's fine (really)."
+    assert analyzer.parse_brief(clean) == clean
+
+
+def test_parse_brief_rejects_marker_with_artifacts_but_no_call(tmp_path):
+    analyzer = _analyzer(tmp_path, _score_questions())
+    assert analyzer.parse_brief("<|tool_call_start|>previous_brief='x', mode='read'") is None
+    assert analyzer.parse_brief("<|tool_call_start|>mode='read'") is None
+
+
 def _brief_signals(p=None):
 def _brief_signals(p=None):
     item = {"kind": "noul"}
     item = {"kind": "noul"}
     if p is not None:
     if p is not None: