Skip to content

Commit 70cdf24

Browse files
authored
Put the user request last in the planner prompt (#7773)
Two changes to the `pdf_edit` selection prompt. **The user's request moves to the end.** When a prompt exceeds the model's context, Ollama keeps the tail and discards the head. The request was the second line, so it was the first thing thrown away. Ending with it makes the most important line the most likely to survive. Reproduced on a local qwen3:8b before the change: five completely different requests - convert to Word, merge two files, run OCR, add a password, add a watermark - run sequentially at temperature 0 through the production prompt shape all returned `COMPRESS_PDF`, and every one reported `prompt_tokens: 2050` against a much larger prompt. The model was not choosing badly; it was answering a question it had never been shown. This is defensive rather than a fix on its own - the real repair is giving the model a context window large enough, and a prompt small enough, to avoid truncation entirely. But the ordering is free and correct regardless of context size. **The duplicated unavailable-operations list is removed from the user prompt.** It was rendered twice, once in the system prompt and again in the user prompt. It is also counterproductive: it spends tokens teaching the model names it must not use, and `handle()` already rejects a plan referencing an unavailable operation in Python afterwards. The system-prompt copy stays, so the model can still explain that an operation exists but is not available here. The existing test that asserted the list appears in the user prompt now asserts it appears in the system prompt and not the user prompt.
1 parent cbc8af1 commit 70cdf24

2 files changed

Lines changed: 17 additions & 35 deletions

File tree

engine/src/stirling/agents/pdf_edit.py

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,7 @@ async def _select_plan(
282282
unavailable_operations,
283283
allow_need_content=can_request_content,
284284
)
285-
return await agent.select(
286-
self._build_selection_prompt(request, supported_operations, unavailable_operations, repair_note)
287-
)
285+
return await agent.select(self._build_selection_prompt(request, supported_operations, repair_note))
288286

289287
def _build_selection_agent(
290288
self,
@@ -330,7 +328,6 @@ def _build_selection_prompt(
330328
self,
331329
request: PdfEditRequest,
332330
supported_operations: Iterable[ToolEndpoint],
333-
unavailable_operations: Iterable[ToolEndpoint],
334331
repair_note: str = "",
335332
) -> str:
336333
repair_line = (
@@ -343,36 +340,20 @@ def _build_selection_prompt(
343340
if repair_note
344341
else ""
345342
)
346-
unavailable_line = (
347-
"Unavailable operations (exist but not currently usable): "
348-
f"{self._get_operations_prompt(unavailable_operations)}\n"
349-
if unavailable_operations
350-
else ""
351-
)
352343
return (
353-
f"Conversation history:\n{format_conversation_history(request.conversation_history)}\n"
354-
f"User request: {request.user_message}\n"
355-
f"Files: {format_file_names(request.files)}\n"
356344
f"Supported operations:\n{self._get_supported_operations_prompt(supported_operations)}\n"
357-
f"{unavailable_line}"
358345
f"{repair_line}"
359-
f"Extracted page text:\n{format_page_text(request.page_text)}"
360-
f"\n{language_directive()}"
346+
f"Conversation history:\n{format_conversation_history(request.conversation_history)}\n"
347+
f"Files: {format_file_names(request.files)}\n"
348+
f"Extracted page text:\n{format_page_text(request.page_text)}\n"
349+
f"{language_directive()}\n"
350+
f"User request: {request.user_message}"
361351
)
362352

363-
# Endpoints that exist on the server and are callable via the direct API or the manual UI,
364-
# but are never offered to the AI agent as a routing option.
365-
#
366-
# Why: REDACT_EXECUTE is the preferred AI-driven redaction route. AUTO_REDACT and REDACT are
367-
# legacy endpoints that remain fully functional for human callers (the manual redact UI, direct
368-
# API consumers, pipelines) but would produce a worse experience if the AI routed to them —
369-
# they accept a simpler, less expressive schema and pre-date the unified operation model.
370-
# Hiding them here channels all AI redaction traffic through REDACT_EXECUTE without disabling
371-
# the legacy endpoints for anyone else.
372-
#
373-
# How to reuse: add an endpoint here whenever a legacy endpoint has a preferred replacement
374-
# that the AI should use exclusively. The endpoint remains live on the server; only the AI
375-
# planner is prevented from selecting it.
353+
# Hidden from the AI planner only; still live for the manual UI, direct API and pipelines.
354+
# AUTO_REDACT and REDACT pre-date the unified operation model and take a less expressive
355+
# schema, so AI redaction is channelled through REDACT_EXECUTE. Add an endpoint here when a
356+
# legacy one has a preferred replacement the AI should use exclusively.
376357
_AGENT_HIDDEN_ENDPOINTS: frozenset[ToolEndpoint] = frozenset({ToolEndpoint.AUTO_REDACT, ToolEndpoint.REDACT})
377358

378359
def _classify_operations(self, request: PdfEditRequest) -> tuple[list[ToolEndpoint], list[ToolEndpoint]]:

engine/tests/test_pdf_edit_agent.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def test_selection_prompt_says_nothing_about_output_formats(runtime: AppRuntime)
144144
# Compatibility is only raised once a plan has actually failed, so the operation list stays
145145
# about what each tool does. Leaking format hints here re-inflates an already large prompt.
146146
agent = StubPdfEditAgent(runtime, _ANY_SELECTION)
147-
prompt = agent._build_selection_prompt(PdfEditRequest(user_message="anything", files=[]), list(OPERATIONS), [])
147+
prompt = agent._build_selection_prompt(PdfEditRequest(user_message="anything", files=[]), list(OPERATIONS))
148148
assert "outputs:" not in prompt
149149
assert "IMAGE (several files)" not in prompt
150150

@@ -156,7 +156,6 @@ def test_repair_prompt_offers_reorder_or_telling_the_user(runtime: AppRuntime) -
156156
prompt = agent._build_selection_prompt(
157157
PdfEditRequest(user_message="anything", files=[]),
158158
list(OPERATIONS),
159-
[],
160159
"step 3 (SANITIZE_PDF) accepts PDF but the previous step produces IMAGE.",
161160
)
162161
assert "SANITIZE_PDF" in prompt
@@ -494,11 +493,13 @@ def test_pdf_edit_selection_prompt_includes_unavailable_operations(runtime: AppR
494493
)
495494
supported, unavailable = agent._classify_operations(request)
496495

497-
prompt = agent._build_selection_prompt(request, supported, unavailable)
496+
selection_agent = agent._build_selection_agent(supported, unavailable, allow_need_content=False)
497+
system_prompt = "".join(selection_agent.agent._system_prompts)
498+
prompt = agent._build_selection_prompt(request, supported)
498499

499-
assert "Unavailable operations" in prompt
500-
assert "OCR_PDF" in prompt
501-
assert ToolEndpoint.OCR_PDF.value in prompt
500+
assert "NOT currently available" in system_prompt
501+
assert "OCR_PDF" in system_prompt
502+
assert "OCR_PDF" not in prompt
502503

503504

504505
@pytest.mark.anyio

0 commit comments

Comments
 (0)