mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
cli/display: render web_search as a pretty line
``web_search`` is composed by the CLI host rather than the builtin catalog, so it had no ``_PRETTY_LINES`` entry and fell back to the ``[tool] …`` / ``[tool ok] …`` whole lines. It now leads with ``* Web Search '<query>' `` (the query as a 60-character preview, like the ask and skill subjects) and lands on the hit count — or on ``(no results)`` when every source came up empty, an emptiness the tool reports as ``error: the search found nothing`` and which is therefore not shown as a failure. A real error still prints in red with its message. The guard test that keeps a builtin tool from shipping without a renderer now checks this host-composed tool too, since a catalog walk cannot see it.
This commit is contained in:
@@ -423,6 +423,27 @@ def _fetch_detail(_args_json: str, result: str) -> str:
|
|||||||
return _pretty_segments((f"({status or 'done'})", _http_status_fg(status)))
|
return _pretty_segments((f"({status or 'done'})", _http_status_fg(status)))
|
||||||
|
|
||||||
|
|
||||||
|
# ``web_search`` reports "every source found nothing (or failed)" as an error, so
|
||||||
|
# the renderer splits that emptiness off the failures that are real errors.
|
||||||
|
_WEB_SEARCH_EMPTY = "error: the search found nothing"
|
||||||
|
|
||||||
|
|
||||||
|
def _web_search_prefix(args_json: str) -> str:
|
||||||
|
query = _json_str_arg(args_json, "query") or "?"
|
||||||
|
return _pretty_prefix(f"* Web Search '{_preview(query, _ASK_SUBJECT_PREVIEW)}' ")
|
||||||
|
|
||||||
|
|
||||||
|
def _web_search_detail(_args_json: str, result: str) -> str:
|
||||||
|
"""``web_search`` outcome: hit count, nothing found, or a failing source."""
|
||||||
|
if result.startswith(_WEB_SEARCH_EMPTY):
|
||||||
|
return _pretty_segments(("(no results)", "dim"))
|
||||||
|
if result.startswith("error:"):
|
||||||
|
return _pretty_segments((f"({result})", "red"))
|
||||||
|
hits = sum(1 for line in result.splitlines() if line.startswith("- "))
|
||||||
|
unit = "hit" if hits == 1 else "hits"
|
||||||
|
return _pretty_segments((f"({hits} {unit})", None))
|
||||||
|
|
||||||
|
|
||||||
_MUTATOR_VERBS: dict[str, str] = {
|
_MUTATOR_VERBS: dict[str, str] = {
|
||||||
"edit_replace": "Edit",
|
"edit_replace": "Edit",
|
||||||
"edit_lineno": "Edit",
|
"edit_lineno": "Edit",
|
||||||
@@ -825,6 +846,7 @@ _PRETTY_LINES: dict[str, PrettyLine] = {
|
|||||||
"run_argv": PrettyLine(prefix=_run_argv_prefix, detail=_run_argv_detail),
|
"run_argv": PrettyLine(prefix=_run_argv_prefix, detail=_run_argv_detail),
|
||||||
"run_argv_batch": PrettyLine(prefix=_run_argv_batch_prefix, detail=_run_argv_batch_detail),
|
"run_argv_batch": PrettyLine(prefix=_run_argv_batch_prefix, detail=_run_argv_batch_detail),
|
||||||
"fetch": PrettyLine(prefix=_fetch_prefix, detail=_fetch_detail),
|
"fetch": PrettyLine(prefix=_fetch_prefix, detail=_fetch_detail),
|
||||||
|
"web_search": PrettyLine(prefix=_web_search_prefix, detail=_web_search_detail),
|
||||||
"edit_replace": _mutator_line("edit_replace"),
|
"edit_replace": _mutator_line("edit_replace"),
|
||||||
"edit_lineno": _mutator_line("edit_lineno"),
|
"edit_lineno": _mutator_line("edit_lineno"),
|
||||||
"write_file": _mutator_line("write_file"),
|
"write_file": _mutator_line("write_file"),
|
||||||
|
|||||||
@@ -509,6 +509,42 @@ async def test_pretty_fetch_error(capsys: pytest.CaptureFixture[str]) -> None:
|
|||||||
assert "* Fetch GET https://example.com (error: fetch failed: boom)" in out
|
assert "* Fetch GET https://example.com (error: fetch failed: boom)" in out
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pretty_web_search(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
result = (
|
||||||
|
"- CPython free-threading — https://example.com/ft\n"
|
||||||
|
" PEP 703 landed in 3.13\n"
|
||||||
|
"- Another page — https://example.com/other\n"
|
||||||
|
)
|
||||||
|
call = _pretty_call("web_search", '{"query": "python 3.14 free-threading"}')
|
||||||
|
await render_events(_aiter([call, _result(result)]))
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "* Web Search 'python 3.14 free-threading' (2 hits)" in out
|
||||||
|
assert "[tool]" not in out
|
||||||
|
assert "[tool ok]" not in out
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pretty_web_search_truncates_the_query(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
query = "a" * 80
|
||||||
|
await render_events(
|
||||||
|
_aiter([_pretty_call("web_search", f'{{"query": "{query}"}}'), _result("- One — https://example.com\n")])
|
||||||
|
)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert f"* Web Search '{'a' * 60}…' (1 hit)" in out
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pretty_web_search_no_results(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
result = "error: the search found nothing (DeepseekSearchBackend: no results; BingSearchBackend: no results)"
|
||||||
|
await render_events(_aiter([_pretty_call("web_search", '{"query": "zzz"}'), _result(result)]))
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "* Web Search 'zzz' (no results)" in out
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pretty_web_search_failing_source(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
await render_events(_aiter([_pretty_call("web_search", '{"query": "x"}'), _result("error: search HTTP 401")]))
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "* Web Search 'x' (error: search HTTP 401)" in out
|
||||||
|
|
||||||
|
|
||||||
async def test_pretty_mutators(capsys: pytest.CaptureFixture[str]) -> None:
|
async def test_pretty_mutators(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
cases = [
|
cases = [
|
||||||
("write_file", '{"path": "src/x.py"}', "wrote 120 characters to src/x.py", "* Write 'src/x.py' (120 chars)"),
|
("write_file", '{"path": "src/x.py"}', "wrote 120 characters to src/x.py", "* Write 'src/x.py' (120 chars)"),
|
||||||
@@ -862,8 +898,13 @@ async def test_pretty_mcp_tools_use_one_generic_line(capsys: pytest.CaptureFixtu
|
|||||||
def test_every_builtin_tool_has_a_pretty_line() -> None:
|
def test_every_builtin_tool_has_a_pretty_line() -> None:
|
||||||
"""A new builtin tool must ship a renderer, or it silently loses the syntax."""
|
"""A new builtin tool must ship a renderer, or it silently loses the syntax."""
|
||||||
from plyngent.tools.catalog import default_tool_definitions
|
from plyngent.tools.catalog import default_tool_definitions
|
||||||
|
from plyngent.tools.net.search import WEB_SEARCH_TOOL_NAME
|
||||||
|
|
||||||
missing = [d.name for d in default_tool_definitions() if _pretty_line_for(d.name) is None]
|
names = [d.name for d in default_tool_definitions()]
|
||||||
|
# The CLI host composes ``web_search`` over the builtins, so a catalog walk
|
||||||
|
# on its own would let a renderer-less tool ship.
|
||||||
|
names.append(WEB_SEARCH_TOOL_NAME)
|
||||||
|
missing = [name for name in names if _pretty_line_for(name) is None]
|
||||||
assert missing == []
|
assert missing == []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user