summaryrefslogtreecommitdiff
path: root/tests/test_llm.py
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-23 17:28:10 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-23 17:28:39 +0900
commit33f6b4dbb69e9366e312a8b44dbed9d22e7cba5b (patch)
tree29a7480317af94fefe9f69a53095af2f233207ce /tests/test_llm.py
parent68969bfbeea68ded7ff863d229760562072f910b (diff)
fix(tools): PDFページングを損失なしカーソル方式へ再設計
PDF本文をページ単位で抽出した後に文字数上限で切断しつつ、継続位置だけ次ページへ進めていたため、ページ末尾が復元不能になる問題を修正する。ツール側の本文上限とLLM側の結果上限の不整合、25ページの処理上限、添付経路での継続情報欠落も共通契約で解消する。 - 共通PageResultと、メタデータ込みの結果上限内で本文量を決めるフォーマッターを追加 - v2トークンにPDFのページ番号とページ内文字位置を保持し、v1トークンの読み取り互換性を維持 - pdf_to_text、scrape_url、search_and_fetchを返却済み本文だけカーソルが進む方式へ統一 - max_pdf_pagesを文書全体ではなく1呼び出しの処理上限として扱い、長い単一ページと26ページ目以降の継続取得に対応 - attachment:<file_id>をセッション限定で解決し、既存添付も保存済みプレビューではなく元PDFから再解析 - Web取得ファイルをセッションとURL単位で重複排除し、HTMLおよびテキスト経路の全文ページングも維持 - 正常なページング結果の後段切断を禁止し、結果上限・コンテキスト予算・ラウンド上限では未取得状態を明示 - 日英プロンプト、UIメッセージ、ツールスキーマ、変更履歴を実装へ同期 - 実PDF、30ページ超、長い単一ページ、旧トークン、セッション分離、コンテキスト境界の回帰テストを追加 検証: - pytest: 130 passed - ruff check src tests: passed - mypy src: passed - c2144.pdf: 19ページ、抽出本文46015文字(ページ区切り込み46051文字)を8回で重複・欠落なく完全再構成
Diffstat (limited to 'tests/test_llm.py')
-rw-r--r--tests/test_llm.py109
1 files changed, 109 insertions, 0 deletions
diff --git a/tests/test_llm.py b/tests/test_llm.py
index 0e1b146..edaaef2 100644
--- a/tests/test_llm.py
+++ b/tests/test_llm.py
@@ -236,3 +236,112 @@ def test_stream_chat_with_tools_disables_mcp_tools_for_final_answer_after_limit(
assert any(event.get("type") == "status" for event in events)
assert any(event == {"type": "delta", "content": "final answer"} for event in events)
assert events[-1] == {"type": "done"}
+
+
+def test_paged_source_limit_emits_incomplete_warning(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ from ollama_web import llm
+
+ registry = ToolRegistry()
+
+ def scrape_url(url: str) -> str:
+ """Return a paged result."""
+ return "body\n---page-info---\nhas_more: true\nnext_token: token"
+
+ registry.register(scrape_url)
+
+ class FakeClient:
+ def __init__(self) -> None:
+ self.calls: list[dict[str, Any]] = []
+
+ def chat(self, **kwargs: Any) -> list[SimpleNamespace]:
+ self.calls.append(kwargs)
+ if len(self.calls) == 1:
+ return [
+ SimpleNamespace(
+ message={
+ "content": "",
+ "tool_calls": [
+ {
+ "function": {
+ "name": "scrape_url",
+ "arguments": {"url": "https://example.com"},
+ }
+ }
+ ],
+ }
+ )
+ ]
+ return [SimpleNamespace(message={"content": "partial answer"})]
+
+ client = FakeClient()
+ monkeypatch.setattr(llm, "get_client", lambda _host=None: client)
+ monkeypatch.setattr(llm, "MAX_TOOL_ROUNDS", 1)
+ events = list(
+ llm.stream_chat_with_tools(
+ [Message(role="user", content="read all")],
+ model="test-model",
+ registry=registry,
+ capabilities={"tools"},
+ language="en",
+ )
+ )
+ assert any(event.get("type") == "warning" for event in events)
+ final_instruction = str(client.calls[-1]["messages"][-1].content)
+ assert "unread content" in final_instruction
+
+
+def test_paged_tool_is_not_executed_when_context_budget_is_exhausted(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ from ollama_web import llm
+
+ executed: list[str] = []
+ registry = ToolRegistry()
+
+ def pdf_to_text(url_or_path: str) -> str:
+ """Read a PDF."""
+ executed.append(url_or_path)
+ return "should not execute"
+
+ registry.register(pdf_to_text)
+
+ class FakeClient:
+ def __init__(self) -> None:
+ self.calls = 0
+
+ def chat(self, **kwargs: Any) -> list[SimpleNamespace]:
+ self.calls += 1
+ if self.calls == 1:
+ return [
+ SimpleNamespace(
+ message={
+ "content": "",
+ "tool_calls": [
+ {
+ "function": {
+ "name": "pdf_to_text",
+ "arguments": {"url_or_path": "https://example.com/a.pdf"},
+ }
+ }
+ ],
+ }
+ )
+ ]
+ return [SimpleNamespace(message={"content": "partial"})]
+
+ monkeypatch.setattr(llm, "get_client", lambda _host=None: FakeClient())
+ monkeypatch.setattr(llm, "MAX_TOOL_CONTEXT_CHARS", 1000)
+ monkeypatch.setattr(llm, "MAX_TOOL_ROUNDS", 1)
+ events = list(
+ llm.stream_chat_with_tools(
+ [Message(role="user", content="read all")],
+ model="test-model",
+ registry=registry,
+ capabilities={"tools"},
+ language="en",
+ )
+ )
+ assert executed == []
+ assert any("budget exhausted" in event.get("result", "") for event in events)