fix(agent): unambiguous document listing format for LLM parsing
Build and Push Containers / build-and-push-app (push) Successful in 1m34s
Build and Push Containers / build-and-push-db (push) Successful in 10s

This commit is contained in:
2026-09-01 12:44:53 -04:00
parent c738105932
commit 15a16a8fe0
5 changed files with 81 additions and 40 deletions
+25 -18
View File
@@ -65,9 +65,10 @@ Implements just enough of the aipi surface:
``call_0``, no arguments), ``finish_reason: "tool_calls"``, no
content;
* request 2 (a ``tool``-role catalog result in the messages):
parse the FIRST catalog line (``source/path — title`` → split on
``" — "`` → ``rsplit("/", 1)``) and stream a ``tool_calls`` delta
calling ``read_document`` on it (id ``call_1``);
parse the FIRST catalog line (``source: X | path: Y | title: Z``
— the labeled ``source:`` / ``path:`` fields, phase 63) and
stream a ``tool_calls`` delta calling ``read_document`` on it
(id ``call_1``);
* request 3 (a ``tool``-role read result in the messages): a
content answer, deterministic: ``Read <source/path>. <first 80
chars of the read document's content>`` — so a suite can assert
@@ -269,6 +270,14 @@ TABLE_ANSWER = (
#: ``_execute_tool``): ``"Document <source/path>:\n<content>"``.
_READ_RESULT_PREFIX = "Document "
#: One line of the agent's ``list_documents`` output (app.rag.agent
#: ``_execute_tool``, phase 63): labeled, pipe-delimited fields —
#: ``source: X | path: Y | title: Z`` — unambiguous for LLM parsing even
#: when the path contains ``/`` characters.
_CATALOG_LINE_RE = re.compile(
r"^source: (?P<source>.+?) \| path: (?P<path>.+?) \| title: .+$"
)
def _read_results(body: dict[str, Any]) -> list[tuple[str, str]]:
"""The read results in the messages, in order: ``(source/path, content)``.
@@ -291,15 +300,15 @@ def _read_results(body: dict[str, Any]) -> list[tuple[str, str]]:
def _catalog_docs(body: dict[str, Any]) -> list[tuple[str, str]]:
"""Every ``source/path`` in the catalog tool result, in listing order.
"""Every ``(source, path)`` in the catalog tool result, in listing order.
Catalog lines are ``source/path — title`` (the agent's
``list_documents`` output): split on ``" — "``, keep the head, and
recover ``(source, path)`` with ``rsplit("/", 1)`` (``rpartition``)
— the same convention the single-read flow's read step uses. The
``"N documents:"`` header line carries no ``/`` and is skipped; read-
result messages are full documents, not listings, and are skipped
too.
Catalog lines are ``source: X | path: Y | title: Z`` (the agent's
``list_documents`` output — phase 63: labeled, pipe-delimited
fields, unambiguous even for paths full of ``/``): the line-level
regex recovers the ``source`` and ``path`` fields directly. The
``"N documents:"`` header line matches no line and is skipped;
read-result messages are full documents, not listings, and are
skipped too.
"""
docs: list[tuple[str, str]] = []
for m in _messages(body):
@@ -309,11 +318,9 @@ def _catalog_docs(body: dict[str, Any]) -> list[tuple[str, str]]:
if content.startswith(_READ_RESULT_PREFIX):
continue
for line in content.splitlines():
head = line.split(" — ", 1)[0].strip()
if "/" in head:
source, _, path = head.rpartition("/")
if source and path:
docs.append((source, path))
match = _CATALOG_LINE_RE.match(line)
if match:
docs.append((match.group("source"), match.group("path")))
return docs
@@ -327,8 +334,8 @@ def _tool_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
are in the messages yet: the model lists the catalog.
* ``("read", source, path, "call_1")`` — a ``tool``-role catalog
result is in the messages: the model reads its FIRST
``source/path — title`` line (split on ``" — "``, then
``rsplit("/", 1)``).
``source: X | path: Y | title: Z`` line (the labeled
``source:`` / ``path:`` fields, phase 63).
* ``("answer", "source/path", content)`` — a ``tool``-role read
result (``"Document <source/path>:\n<content>"``) is in the
messages: the model answers, quoting the read document. Reached