feat(prompts): revamp preset prompts, tool naming, and prompt templating

Rewrite the default/creative/strict presets (classic + agentic) into
structured sections: grounding and cite-by-title guidance, insufficient-
context behavior, current date, respond-in-user-language, scoped mermaid
usage, an untrusted-content guardrail, and a conditional XML-tagged
document context block. A memory directory listing is injected at render
time via the template prefetch mechanism so the model starts oriented
without burning a tool call.

Fixes along the way:
- Agentic preset swap was dead code: _get_prompt_content cached the
  classic preset before create_agent's swap check ran, so agentic and
  research agents always got the classic preset. The swap now happens
  inside _get_prompt_content.
- Jinja autoescape corrupted document content in custom prompts
  (< -> &lt;); prompts are not HTML, autoescape is now off.
- Literal {summaries} leaked into the prompt when no docs were
  retrieved; the placeholder is now stripped.
- Agentic/research prompts referenced tool names from a dropped naming
  scheme (search_internal, reason_think); they now reference the real
  names (search, reason).
- The strict preset told the model to "be very creative and use your
  imagination" right after "never make up information".
- extract_tool_usages recorded intermediate attribute chains as
  bare-tool usages, which meant "run all actions" at prefetch; only
  maximal chains are recorded now.
- Headless runs retrieved docs but never rendered them into the
  prompt; the prompt is now rendered like the streaming path.
- Default tools were unreachable by name in prompt templates
  (prefetch results were keyed by synthetic id only); defaults now
  claim the name key unless an explicit row shadows it.

Tool layer: memory/notes/todo actions are namespaced (memory_view,
note_overwrite, todo_create, ...) with legacy unprefixed names still
accepted via prefix stripping; duplicate action names across tools are
disambiguated with the owning tool's name instead of numeric suffixes;
thin tool descriptions rewritten (brave, duckduckgo, telegram, ntfy,
cryptoprice, read_webpage, internal_search, think).

Docs are now wrapped per chunk in <document index>/<source>/<content>
tags for citation-by-title support.
This commit is contained in:
Alex committed 2026-06-11 11:18:46 +01:00
1 parent 10cc16eab0
commit 636d4d35d1
33 files changed
+730 -233

No files matched your search

@@ -533,6 +533,34 @@ class TestGetPromptContent:
# Even with no agent_config, cached value returned
assert sp._get_prompt_content() == "cached"
def test_agentic_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
sp.agent_config = {"prompt_id": "default", "agent_type": "agentic"}
content = sp._get_prompt_content()
assert "`search` tool" in content
assert "source.summaries" not in content
def test_research_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
sp.agent_config = {"prompt_id": "strict", "agent_type": "research"}
content = sp._get_prompt_content()
assert "`search` tool" in content
def test_classic_agent_gets_classic_preset(self):
from application.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
sp.agent_config = {"prompt_id": "default", "agent_type": "classic"}
content = sp._get_prompt_content()
assert "source.summaries" in content
class TestPreFetchDocs:
def test_skips_when_no_active_docs(self):
@@ -660,8 +688,67 @@ class TestPreFetchTools:
d.get("name") == "read_webpage" and d.get("default")
for d in fetched
)
# Defaults are reachable by synthetic id only — not by name.
assert default_tool_id("read_webpage") in got
# No explicit row of the same name exists, so the default also
# claims the name key (what preset templates reference).
assert got.get("read_webpage") == {"ok": True}
def test_explicit_row_keeps_name_key_over_default(self, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
UserToolsRepository,
)
UserToolsRepository(pg_conn).create(
user_id="u-shadow-default", name="read_webpage", status=True
)
sp = StreamProcessor({}, {"sub": "u-shadow-default"})
sp._required_tool_actions = {"read_webpage": {None}}
def _fake_fetch(tool_doc, required_actions):
return {"is_default": bool(tool_doc.get("default"))}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
assert got is not None
# The explicit row owns the name key; the default stays reachable
# by its synthetic id.
assert got["read_webpage"] == {"is_default": False}
assert got[default_tool_id("read_webpage")] == {"is_default": True}
def test_fetch_tool_data_executes_referenced_memory_view(self, pg_conn):
from unittest.mock import MagicMock
from application.agents.default_tools import synthesize_default_tool
from application.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u-mem-prefetch"})
tool_doc = synthesize_default_tool("memory")
mock_tool = MagicMock()
mock_tool.get_actions_metadata.return_value = tool_doc["actions"]
mock_tool.execute_action.return_value = "Directory: /\n(empty)"
mock_manager = MagicMock()
mock_manager.load_tool.return_value = mock_tool
with patch(
"application.agents.tools.tool_manager.ToolManager",
return_value=mock_manager,
):
got = sp._fetch_tool_data(tool_doc, {"memory_view"})
# Only the referenced action ran, with no path kwarg — the tool's
# own "/" default applies.
assert got == {"memory_view": "Directory: /\n(empty)"}
mock_tool.execute_action.assert_called_once_with("memory_view")
def test_agent_bound_invocation_omits_default_tool_prefetch(self, pg_conn):
from application.api.answer.services.stream_processor import (