mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
a0d39bba6d
Only DeepSeek's Anthropic-compatible surface runs a hosted ``web_search``: the
Responses and chat-completions surfaces answer "I can't browse the web" with
``{"type": "web_search"}`` and with the server-tool shape alike, so a DeepSeek
provider that talks them had no search at all.
``config.routing.local_web_search`` decides per provider: a DeepSeek provider
whose ``provider_tools`` ask for ``web_search`` and whose convention is not
``anthropic`` gets the local tool (``tools/net/search.py``), which asks the
search-capable endpoint behind the scenes and hands the model one ``title —
url`` line per hit — read a page with ``fetch``, the bodies stay server-side.
DeepSeek's preset asks for no hosted tool by default, so this stays opt-in.
59 lines
2.2 KiB
Python
59 lines
2.2 KiB
Python
"""The local ``web_search`` tool DeepSeek uses where its surface cannot host one."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from plyngent.agent.tools import ToolTag
|
|
from plyngent.lmproto.deepseek.anthropic.search import SearchHit
|
|
from plyngent.tools.net.search import WEB_SEARCH_TOOL_NAME, build_web_search_tool
|
|
|
|
|
|
class _StubSearch:
|
|
"""Stands in for :class:`DeepseekSearchClient`."""
|
|
|
|
def __init__(self, hits: list[SearchHit]) -> None:
|
|
self.hits = hits
|
|
self.queries: list[tuple[str, str]] = []
|
|
|
|
async def search(self, query: str, *, model: str, **kwargs: object) -> list[SearchHit]:
|
|
self.queries.append((query, model))
|
|
return self.hits
|
|
|
|
|
|
def _definition(hits: list[SearchHit]) -> tuple[Any, _StubSearch]:
|
|
client = _StubSearch(hits)
|
|
return build_web_search_tool(client, model="deepseek-flash"), client
|
|
|
|
|
|
def test_tool_definition_shape() -> None:
|
|
definition, _ = _definition([])
|
|
assert definition.name == WEB_SEARCH_TOOL_NAME
|
|
assert definition.tags == ToolTag.LOCAL | ToolTag.READ_ONLY
|
|
assert "fetch" in definition.description
|
|
parameters = definition.parameters
|
|
assert parameters["properties"]["query"] == {"type": "string"}
|
|
assert parameters["required"] == ["query"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handler_returns_one_line_per_hit() -> None:
|
|
definition, client = _definition([SearchHit(title="DeepSeek-V4 preview", url="https://api-docs.deepseek.com/x")])
|
|
assert await definition.handler(query="DeepSeek-V4") == ("- DeepSeek-V4 preview — https://api-docs.deepseek.com/x")
|
|
assert client.queries == [("DeepSeek-V4", "deepseek-flash")]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handler_caps_the_hits_and_names_an_untitled_page() -> None:
|
|
definition, _ = _definition([SearchHit(title="", url=f"https://example.com/{index}") for index in range(5)])
|
|
lines = (await definition.handler(query="anything", hits=2)).splitlines()
|
|
assert lines == ["- (untitled) — https://example.com/0", "- (untitled) — https://example.com/1"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handler_reports_an_empty_search() -> None:
|
|
definition, _ = _definition([])
|
|
assert "no results" in await definition.handler(query="nothing matches this")
|