mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(transforms/content-router): route grep/log output away from HTML extractor (#1719)
## Description Follow-up to #1717 (envelope-aware detection). Even when the tool-output envelope is unwrapped, the native (magika) detector still tags dense `grep`/`rg` output and build logs as **HTML** — file paths and `</>`/brackets read as markup. Those then get routed to the HTML article-extractor, which is lossy for that content (it strips the code and identifiers the lines carry). When the structural log/search detectors positively claim the payload, override the HTML verdict: build output / tracebacks → LOG (checked first), `path:line` grep output → SEARCH. It **reuses the existing `_try_detect_log` / `_try_detect_search` detectors**, so no new pattern or regex is introduced, and it only ever reconsiders an HTML verdict — every other detection is untouched. Per-content and deterministic (no cross-turn state), so prefix caching is unaffected. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `_detect_content()`: when the native detector returns `HTML`, re-check with `_try_detect_log` then `_try_detect_search` and return their verdict when they claim the payload (`headroom/transforms/content_router.py`). - Regression test. ## Testing - [x] Unit tests pass - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality ### Test Output ```text $ pytest tests/test_transforms_content_router.py tests/test_transforms_content_detection.py -q 46 passed in 0.94s $ ruff check headroom/transforms/content_router.py tests/test_transforms_content_router.py All checks passed! $ mypy headroom/transforms/content_router.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: local, Python 3.12.6, native `headroom._core` detect backend. - With the native detector forced to `html`: `grep`-over-`.html`-template output detects as `SEARCH_RESULTS`, a build/error log as `BUILD_OUTPUT`, and a genuine HTML article as `HTML` (override does not fire). - Verified directly that raw magika returns `html` for realistic `grep`-over-HTML output, and that this change reroutes it to `search`. - Not tested: end-to-end proxy request replay. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes - Builds on #1717; the two changes live in the same `_detect_content` function (both prevent tool output from being misrouted to the HTML extractor). - Pre-existing mypy findings in `tests/test_transforms_content_router.py` are unrelated and left as-is; `mypy headroom` is clean and the added test is typed.
This commit is contained in:
parent
bec47a1898
commit
0d18ef26f4
2 changed files with 58 additions and 1 deletions
|
|
@ -58,7 +58,7 @@ from ..config import (
|
|||
from ..parser import CCR_RETRIEVAL_MARKER_RE
|
||||
from ..tokenizer import Tokenizer
|
||||
from .base import Transform
|
||||
from .content_detector import ContentType, DetectionResult
|
||||
from .content_detector import ContentType, DetectionResult, _try_detect_log, _try_detect_search
|
||||
from .content_detector import detect_content_type as _regex_detect_content_type
|
||||
from .error_detection import content_has_strong_error_indicators
|
||||
|
||||
|
|
@ -302,6 +302,18 @@ def _detect_content(content: str) -> DetectionResult:
|
|||
)
|
||||
return _regex_detect_content_type(content)
|
||||
|
||||
# HTML misroute guard (native/magika path): dense punctuation in grep
|
||||
# output and build logs (file paths, </>, brackets) can read as markup, so
|
||||
# the native detector tags real search results / logs as HTML. Routing those
|
||||
# to the HTML article-extractor is lossy — it strips code and identifiers.
|
||||
# When the structural log/search detectors positively claim the payload,
|
||||
# trust them over the HTML verdict: tracebacks/build output win as LOG
|
||||
# (checked first), path:line grep output routes to SEARCH.
|
||||
if content_type is ContentType.HTML:
|
||||
override = _try_detect_log(content) or _try_detect_search(content)
|
||||
if override is not None:
|
||||
return override
|
||||
|
||||
if content_type is ContentType.PLAIN_TEXT:
|
||||
regex_result = _regex_detect_content_type(content)
|
||||
if regex_result.content_type is not ContentType.PLAIN_TEXT:
|
||||
|
|
|
|||
|
|
@ -1024,3 +1024,48 @@ def test_detect_content_sees_through_tool_output_envelope() -> None:
|
|||
wrapped = f"<returncode>0</returncode>\n<output>\n{code}\n</output>"
|
||||
assert _detect_content(wrapped).content_type is ContentType.SOURCE_CODE
|
||||
assert _detect_content(wrapped).content_type is _detect_content(code).content_type
|
||||
|
||||
|
||||
def test_detect_content_overrides_html_misroute_for_grep_and_logs(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Regression: the native detector (magika) tags dense grep output and
|
||||
build logs as HTML because file paths and </> read as markup. Routing those
|
||||
to the HTML article-extractor is lossy (it strips code + identifiers). When
|
||||
the structural log/search detectors positively claim the payload they
|
||||
override the HTML verdict (log checked first so tracebacks win); genuine
|
||||
HTML with no such structure is left as HTML."""
|
||||
import headroom._core as _core
|
||||
|
||||
monkeypatch.setenv("HEADROOM_DETECT_BACKEND", "rust")
|
||||
monkeypatch.setattr(
|
||||
_core,
|
||||
"detect_content_type",
|
||||
lambda content: SimpleNamespace(content_type="html", confidence=1.0, metadata={}),
|
||||
)
|
||||
|
||||
# grep over HTML template files: native says html, but it is search results.
|
||||
grep = "\n".join(
|
||||
f'templates/pages/dashboard_{i}.html:{10 + i}: <div class="card" data-id="{i}">'
|
||||
for i in range(6)
|
||||
)
|
||||
assert _detect_content(grep).content_type is ContentType.SEARCH_RESULTS
|
||||
|
||||
# build/error log misread as html -> LOG wins (checked before search).
|
||||
build_log = "\n".join(
|
||||
[
|
||||
"ERROR failed to compile module widget",
|
||||
"WARNING deprecated call near <template>",
|
||||
"Traceback (most recent call last):",
|
||||
"ERROR build aborted after 2 retries",
|
||||
]
|
||||
)
|
||||
assert _detect_content(build_log).content_type is ContentType.BUILD_OUTPUT
|
||||
|
||||
# genuine HTML article: no grep/log structure -> override does not fire.
|
||||
html = (
|
||||
"<!DOCTYPE html>\n<html><head><title>x</title></head>"
|
||||
"<body><main><section><p>An article about widgets and gadgets.</p>"
|
||||
"</section></main></body></html>"
|
||||
)
|
||||
assert _detect_content(html).content_type is ContentType.HTML
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue