feat(mcp): the bundle's map, and a working method that reads it first
C5. `bundlemap.build_map` lists a bundle in its own words: one line per source document -- its name, then the titles of its concepts in document order -- and documents whose names differ only in their numbers (a changelog per release, a note per week) as ONE line: the name with every number as `#`, the count, the first and last by natural order, and the titles across the series that are words. `SERIES_MIN` = 5, at most `TITLES_PER_LINE` = 24 titles a line, the lines capped at `MAP_MAX_BYTES` = 48 000 together with `lines_truncated` counting the rest. Derived on every call, never stored. The card (`okf card`, `okf_describe`) carries it as `map` and no longer carries `source_files`: that list named every document a second time with no series collapsed, a quarter of the reply on a large bundle, for names the map already carries. Chose removal over keeping both because the describe reply has to fit a client's tool-reply limit and the map says more. The working method now reads: take the map first (`okf card`, or `okf_describe`), write two to four sub-questions in its words, and send them in ONE call (`--question` repeated, or `okf_ask` `questions`). Changed in the skill template, the generated `skills/okf-consume`, and the server instructions (held under the 2 KB a client keeps). The regeneration recipe for `skills/okf-consume` gains `--for-bundle`: since v1.1 the generator writes the generic skill by default, so the recipe as published produced the other file. A test holds a four-sub-question `okf_ask` over concepts far over the passage size under 50 000 bytes of reply text (25 000 tokens at a pessimistic two bytes a token). The real-collection measurements are kept in local state. Suite on a clean tree after `git add`: 2429 passed, 2 skipped, 4 xfailed. ruff, ruff format, mypy --strict clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f7cd84c5e6
commit
da6faf8776
9 changed files with 450 additions and 64 deletions
|
|
@ -247,3 +247,39 @@ def test_okf_ask_refuses_both_forms_at_once(bundle: Path) -> None:
|
|||
with pytest.raises(mcp_server.ToolError) as raised:
|
||||
mcp_server.call_ask(surface, {"question": HEATING, "questions": [ENGINE]})
|
||||
assert raised.value.code == "question_ambiguous"
|
||||
|
||||
|
||||
def test_four_subquestions_over_large_concepts_stay_under_a_tool_reply(tmp_path: Path) -> None:
|
||||
"""The worst case the passage cut exists for: every delivered concept is far
|
||||
over `PASSAGE_CHARS`. A client keeps a tool reply of 25 000 tokens; at a
|
||||
pessimistic two bytes a token that is 50 000 bytes of text."""
|
||||
words = ("stove", "engine", "apples", "roof")
|
||||
spec = retrieval.BundleSpec(
|
||||
"large-concepts",
|
||||
tuple(
|
||||
retrieval.DocumentSpec(
|
||||
f"doc-{word}",
|
||||
f"doc-{word}.md",
|
||||
tuple(
|
||||
retrieval.ConceptSpec(
|
||||
slug=f"part-{n}",
|
||||
title=f"{word.title()} part {n}",
|
||||
body=f"The {word} is described here in detail. ",
|
||||
repeat=400,
|
||||
)
|
||||
for n in range(3)
|
||||
),
|
||||
)
|
||||
for word in words
|
||||
),
|
||||
)
|
||||
bundle = retrieval.build_bundle(tmp_path / "bundle", spec)
|
||||
surface = mcp_server.build_surface(bundle=bundle, roots=())
|
||||
result = mcp_server.call_ask(
|
||||
surface, {"questions": [f"How is the {w} described?" for w in words]}
|
||||
)
|
||||
payload = result["answers"][0]["payload"]
|
||||
assert len(payload["excerpts"]) == consume.DEFAULT_K
|
||||
assert all(len(excerpt["text"]) > consume.PASSAGE_CHARS // 2 for excerpt in payload["excerpts"])
|
||||
text = mcp_server._tool_result(result)["content"][0]["text"]
|
||||
assert len(text.encode("utf-8")) < 50_000
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue