From 37e130a7dc69c8956c6002199c5efbe5aa3b12e9 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 17 Oct 2026 16:52:40 +0800 Subject: [PATCH] docs: describe the DeepSeek Search API backend --- AGENTS.md | 2 +- README.md | 8 ++++++++ doc/plyngent.example.toml | 8 ++++++++ src/plyngent/config/models.py | 19 +++++++++++-------- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1010320..cd7de97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,7 @@ Module-level `@tool` handlers. Call `set_workspace_root()` before use. - **`chat`**: human prompts as tools — `ask_user_line` / `ask_user_choice` / `ask_user_form` (shared `prompting` core); `wait` (line prompt with timeout; Enter disturbs). Container args are shape-checked (`tools/chat/shape.py`): a mis-typed string `options`/`fields` (or a non-string `question`) returns an explained error — `options="a, b"` used to iterate characters into a per-character menu. `wait`'s `duration` accepts numeric strings (`"5"` = 5s) and rejects booleans/text/containers. - **`todo`**: LIFO stack of **task groups** (not a queue of tasks) — `todo_push` creates one group of siblings; `todo_pop` removes the whole top group; `todo_update` by the number a listing shows (render order, TOP group first; the stored `id` stays internal); DFS breakdown push[T1,T2]→push[T1.1…]→pop→push[T2.1…]; stored on session row; open items = unfinished work (`[TODO OPEN WORK]`); all-terminal non-empty = hygiene (`[TODO HYGIENE]`); nag channel via `[agent] todo_nag_strategy` (`developer`/`user` = prose nag; `synthetic_tool` = forged `todo_list` call + **real** `stack.render()` result + ToolCall/Result events; `none`). - **`net` / `fetch`**: HTTP GET/POST/PUT/DELETE via niquests (isolated session, not the LLM client). Manual redirects with per-hop SSRF checks (`asyncio` `getaddrinfo`). Private/loopback hosts need instance policy grant (CLI timed confirm; **not** YOLO). Public cleartext HTTP and mutating methods soft-confirm (`YOLO`/`TRUSTABLE`). HTTPS→HTTP redirects blocked unless `allow_http_downgrade`. `user_agent` tool arg (or headers) is never replaced by the default when provided. Caps: body bytes/chars, request body size. -- **`net` / `web_search`** (`tools/net/search.py`, `tools/net/bing.py`): the hosted search a DeepSeek provider cannot use, as a local tool. `build_web_search_tool(backends)` closes over the sources named by `provider.search_sources` and asks them **in fallback order** (`search_in_order`): `DeepseekSearchBackend` (one `/messages` turn with the server tool, `lmproto/deepseek/anthropic/search.py`) and/or `BingSearchBackend` (Bing's RSS results, snippets included, fetched through the same client as `fetch`). The handler returns one `title — url` entry per hit (plus the snippet when the source has one); a source that raises or finds nothing is skipped and reported only if every source fails. Registered by the CLI host only when `config.routing.local_web_search` says so: a DeepSeek provider whose `provider_tools` ask for `web_search`, on a surface that ignores hosted tools (`convention` other than `anthropic`), with a non-empty `search_sources`. Tags LOCAL|READ_ONLY. +- **`net` / `web_search`** (`tools/net/search.py`, `tools/net/bing.py`): the search a DeepSeek provider cannot host, as a local tool. `build_web_search_tool(backends)` closes over the sources named by `provider.search_sources` and asks them **in fallback order** (`search_in_order`): `DeepseekSearchBackend` (one `POST /v1/tools/search` through `lmproto/deepseek/search/`, no model round — `deepseek_api_base_url` drops the convention's `/anthropic` and any `/v1` before the client appends its path; 408/429/5xx are retried 0.5s/1.0s, 402 and other 4xx are not) and/or `BingSearchBackend` (Bing's RSS results, snippets included, fetched through the same client as `fetch`). Hits are `SearchHit` from `lmproto/search.py` (DeepSeek's `snippet`, else its `summary` cut to 200 chars); the handler returns one `title — url` entry per hit (plus the snippet when the source has one), and a source that raises or finds nothing is skipped and reported only if every source fails. Registered by the CLI host only when `config.routing.local_web_search` says so: a DeepSeek provider with a non-empty `search_sources`, unless the Anthropic convention already hosts the search (`convention = "anthropic"` **and** `provider_tools` ask for `web_search`) — so `provider_tools = []` brings the local tool back on that surface too. `provider_tools` owns the hosted tool, `search_sources` owns this one. Tags LOCAL|READ_ONLY. - **`mcp`** (`tools/mcp.py`): register one server's advertised tools into the catalog as namespaced `mcp____` definitions with `ToolSource(kind="mcp", plugin_id=server)`; tags LOCAL (+ READ_ONLY when the server config sets `read_only`) — never YOLO/TRUSTABLE. `register_mcp_tools` purges prior MCP entries then re-registers every connected server (hosts call it on tool-registry rebuild); connections must already be started by `McpManager`. - **`skills`** (`tools/skills.py`): discovery, reading, and writing of skill directories (`plyngent.skills` owns roots, the `SKILL.md` frontmatter subset, the uncached store, and the inside-the-skill path rule). `skill_list` (names, source root, directory, description, shadowed copies, frontmatter notes), `skill_read` (a `SKILL.md` by name, or one bundled file — reuses `read_file` for ranges/line numbers/`truncate_token`), `skill_search` (regex over every visible skill via `regex_files`, hits rebased on `/`), `skill_create` (new `SKILL.md` + optional `files`), `skill_edit` (replace / `append` / rewrite one file; a rewritten `SKILL.md` is re-parsed and notes a lost description). Reads need no grant (the host publishes the roots as `WorkspacePolicy.static_read`, so `read_file`/`regex_files` see them while writes stay denied); writes go through **`SkillWritePolicy`** on the instance — denylist first, then `SessionState.skill_write_roots` (a `session` answer), then `[skills].allow_write`, then the host's confirm hook (CLI `prompt_skill_write_confirm`: `o`nce/`s`ession/`n`o, timed, default deny, **not** skipped by YOLO); a host with no hook denies. Grants are per **skill directory**; `[skills].enabled = false` drops the tools from the registry (`SKILL_TOOL_NAMES`). - **`DEFAULT_TOOLS` / catalog**: access + file + process + vcs + chat + todo + skills + net (`fetch`) for local surface. `ToolTag.READ_ONLY` marks non-mutating tools (`read_file`, `get_truncated`, `listdir`, `glob_paths`, `regex_files`, `tree`, `todo_list`, `wait`, `ask_user_*`, `vcs_*`, `skill_list`/`skill_read`/`skill_search`, `fetch`); `ToolRegistry.clone(read_only_only=True)` selects just those for safe side turns / planning, and `read_only_context()` (in `tools/context.py`) makes `fetch` accept GET only. diff --git a/README.md b/README.md index 4f2d666..738230b 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,14 @@ agent's Anthropic Messages path (tools, streaming, usage). DeepSeek maps DeepSeek ids pass through as-is. `GET /models` is not documented on that base, so model selection is config-driven. +On the Chat Completions and Responses conventions DeepSeek ignores a hosted +search tool, so plyngent offers its own `web_search` tool instead. It queries +DeepSeek's Search API (`POST /v1/tools/search`, one request per search, billed +per successful request, 5 QPS account-wide) and falls back to Bing's RSS when +that fails or finds nothing. `search_sources` sets the source order (`[]` +disables the tool); `provider_tools = []` on the Anthropic convention switches +that surface to the same local tool. + If `[database]` is omitted (or SQLite `url` is unset/empty), chat uses a durable file under the user data dir (e.g. `~/.local/share/plyngent/chat.db` on Linux). Set `url = ":memory:"` for a true in-memory SQLite (CLI warns; no file; useful for tests). ## Chat diff --git a/doc/plyngent.example.toml b/doc/plyngent.example.toml index 701937b..28899b9 100644 --- a/doc/plyngent.example.toml +++ b/doc/plyngent.example.toml @@ -192,6 +192,14 @@ access_key_or_token = "sk-replace-me" # # Thinking strength for this provider; per-model entries below may override it. # # reasoning_effort = "high" # +# # Local ``web_search`` sources, in fallback order (``[]`` disables the tool). +# # "deepseek" is the platform Search API (POST /v1/tools/search; billed per +# # successful request, 5 QPS); "bing" is Bing's RSS backstop (no key needed). +# # ``provider_tools`` controls only the hosted search, these sources control +# # plyngent's own tool: on the Anthropic convention ``provider_tools = []`` +# # leaves you the local Search API tool instead of the hosted one. +# search_sources = ["deepseek", "bing"] +# # [providers.deepseek.models] # "deepseek-flash" = { text = true, convention = "responses" } # "deepseek-v4-pro" = { text = true, reasoning_effort = "max" } diff --git a/src/plyngent/config/models.py b/src/plyngent/config/models.py index 7703c7a..9eb3627 100644 --- a/src/plyngent/config/models.py +++ b/src/plyngent/config/models.py @@ -9,7 +9,8 @@ from msgspec import Struct, field # - "anthropic" → Anthropic Messages API (``POST /messages`` on # ``https://api.deepseek.com/anthropic``) type DeepSeekConvention = Literal["", "openai", "anthropic", "responses"] -# Sources the local ``web_search`` tool can use (see ``SearchBackend``). +# Sources the local ``web_search`` tool can use (see ``SearchBackend``): +# ``deepseek`` = the platform Search API, ``bing`` = Bing's RSS backstop. type SearchSource = Literal["deepseek", "bing"] # Static directory pre-allow mode for ``[agent].allow_paths`` (path → mode). @@ -385,8 +386,8 @@ def _default_deepseek_models() -> dict[str, ModelConfig]: def _default_deepseek_search_sources() -> list[SearchSource]: """Sources for the local ``web_search`` tool, in fallback order. - DeepSeek's own index first (it is the provider's own search), Bing's RSS as - the backstop when a search fails or finds nothing. + DeepSeek's own index (the platform Search API) first, Bing's RSS as the + backstop when a search fails or finds nothing. """ return ["deepseek", "bing"] @@ -401,11 +402,13 @@ class DeepseekProvider(ProviderConfig, tag="deepseek"): ``https://api.deepseek.com/anthropic``) ``provider_tools`` asks for a search tool like OpenAI's does (``[]`` - disables it). DeepSeek runs a hosted search only on the Anthropic surface, so - the other conventions get a local ``web_search`` tool instead (see - ``config.routing.local_web_search``); ``search_sources`` names what that tool - searches with, in fallback order — ``["deepseek", "bing"]`` by default, - ``[]`` to not offer it. + disables the hosted one). DeepSeek hosts a search only on the Anthropic + surface; every other convention gets a local ``web_search`` tool instead + (see ``config.routing.local_web_search``). ``search_sources`` names what + that local tool searches with, in fallback order — ``["deepseek", "bing"]`` + by default (the platform Search API, then Bing's RSS), ``[]`` to not offer + it. The two settings are independent: turning hosted tools off on the + Anthropic convention leaves the local Search API tool in place. ``extras`` keeps arbitrary provider keys; the legacy ``extras.convention`` key is parsed but ignored (prefer the typed ``convention`` field).