Skip to content

Read manuals with nothing else installed, and use ManualForge when it's there (#168) - #170

Merged
TGoodhew merged 4 commits into
mainfrom
fix/manual-search-manualforge
Nov 7, 2026
Merged

TGoodhew merged 4 commits into
mainfrom
fix/manual-search-manualforge

Conversation

@TGoodhew

@TGoodhew TGoodhew commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Fixes #168. manual_search now reads PDFs on a default install, and uses ManualForge when it's there.

The problem

PDFs could only be read with pdftotext, which nothing installs, so every PDF was reported as unreadable. ManualForge didn't help either: its index only chose which manuals to open, and a search with a model never asked it.

What changed

  • Built-in reader: the server ships PdfPig 0.1.16 (managed, Apache-2.0), rebuilding lines from word positions so tables keep their columns. pdftotext still wins when present. The licence is in packaging/THIRD-PARTY-NOTICES.txt.
  • ManualForge pages: when the manuals don't fully answer a search, pages from its index fill in, marked as snippets and as OCR where they were. A search the manuals answer in full starts no process. Its command line can't return a document's text, so results point to read_manual_page for the whole page.
  • Partial matches: a query word missing from a manual no longer hides the words it does have. Passages with every word rank first, then index pages, then partial matches.
  • _Originals: ManualForge's pre-OCR copies are skipped when its index exists. They caused a false "could not be searched" warning.
  • Time limit: one search gets 25 s in all, shared by choosing manuals, reading them and asking ManualForge for pages, so it stays inside Claude Desktop's limit of about 30 s. A manual is read on a thread of its own and the search waits only for the time it has left. One it gave up on keeps reading and fills the cache, is listed as still being read, and a second search joins that read. Manuals not reached are listed too.
  • Damaged pages: a page the built-in reader can't parse is marked in the cached text, so the search names it rather than treating it as a page with no match.
  • One file: a file= search asks ManualForge for 500 pages with --show-duplicates, since its search can't be narrowed to a file and it folds identical copies into one hit (500 took 1.5 s). The model's filename filter doesn't apply to a file the caller named. On the real library, E4438C's 9018-01484.pdf now gets ManualForge's page 705.
  • Same page: an index snippet replaces a passage from the same page that matched only part of the query.

Tested

Run through the real server against a 683-manual library, with pdftotext hidden and an empty cache:

Search Result
54845A, five manuals 4.8 s, then about 125 ms from cache, none unreadable
OCR-only DATA? diagram, with ManualForge Found on p. 46, marked OCR
Wide search, no model 16.8 s, 7 manuals skipped and listed
8902A, twelve manuals (#168) 15.1 s for eight, then 6.5 s for the rest, none unreadable
8902A again, after the review fixes 15.1 s with one manual still being read, then 7.2 s with all twelve, then 0.4 s
Wide search, no model, with ManualForge, after the fixes 16.2 s, unfinished manuals listed

Known limits

  • A pdftotext failure doesn't fall back to the built-in reader.
  • ManualForge needs every query word to match, so a long query with a wrong guess gets nothing from it.
  • Scans with no text layer still need ManualForge.

Related: TGoodhew/ManualForge#95 fills in GPIB MCP's manual folder when ManualForge is installed second.

🤖 Generated with Claude Code

TGoodhew and others added 2 commits October 14, 2026 09:52
New since 1.3.0: VS Code's Copilot now loads gpib_batch - its 'steps'
schema says what a step is, and every tool's arrays now say what their
items are (#164). The installer's sandbox tests also close themselves
when they finish (#163). Local builds now report 1.3.1; the release
workflow stamps the tag's version as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s there

manual_search couldn't read a PDF unless pdftotext happened to be on the
server's PATH, and nothing installs it, so a default install reported every
PDF as unreadable. With ManualForge installed it was no better: the index
only ever chose which manuals to open, and a search with a model never
asked it, so the text it holds was never reached.

The server now ships a PDF reader, PdfPig (managed, Apache-2.0), which
keeps a table's columns as pdftotext -layout does. pdftotext still wins
when it's there, so nothing changes for anyone who has it.

ManualForge's command line can't give a document's text, only pages and
snippets, so that is how it's used: when the manuals don't answer a search
in full, its pages are added, ahead of passages with only part of the
query. That reaches text only its OCR recovered, such as the 54845A's
syntax diagrams, and each is marked as a snippet and as OCR. A search the
manuals answer in full still starts no process.

Reading now works by default, so a dozen manuals read for the first time
can run past Claude Desktop's limit: a search stops opening manuals at
15 s and lists those it didn't reach. Running it on the real library also
showed two old faults. A query word missing from a manual hid the words it
did have, and ManualForge's _Originals copies were searched as if they were
more manuals, reporting a false "could not read".

Tested through the built exe on a 683-manual library with no pdftotext and
a cold cache, with ManualForge on and off: the 54845A's manuals read in
about 5 s and 50 ms after that, and p. 46's diagram came back marked OCR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 7, 2026 04:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved defects can suppress valid search matches, silently cache incomplete manual text, and let extraction and index lookup exceed the client’s timeout.

4 open issues
What changed in this PR

This PR addresses #168 by adding built-in PDF reading to manual_search and using ManualForge index snippets to supplement local results, including OCR-only content.

Changes:

  • Bundles PdfPig for PDF extraction with page citations and table-column alignment.
  • Adds ManualForge page results, partial-match ranking, and filtering of pre-OCR copies.
  • Introduces a budget for opening manuals, expands regression tests, and updates packaging and documentation.
File Description
tests/​GpibMcp.Tests/​ManualSearchTests.cs Tests PDF extraction, partial matches, OCR warnings, and search budgets.
tests/​GpibMcp.Tests/​ManualForgeIndexTests.cs Tests index snippets, ranking, filtering, and fallback behavior.
tests/​GpibMcp.Tests/​GpibMcp.Tests.csproj Adds System.Memory for test-host compatibility.
src/​GpibMcp.Core/​Tools/​ManualTools.cs Supplements results with index snippets and exposes source/OCR metadata.
src/​GpibMcp.Core/​Manuals/​ManualText.cs Adds built-in PDF extraction as a fallback.
src/​GpibMcp.Core/​Manuals/​ManualSearch.cs Adds partial matches, result merging, and a search budget.
src/​GpibMcp.Core/​Manuals/​ManualLibrary.cs Filters indexed pages and excludes pre-OCR copies.
src/​GpibMcp.Core/​Manuals/​ManualForgeIndex.cs Queries and parses ManualForge JSON page results.
src/​GpibMcp.Core/​Manuals/​BuiltInPdfText.cs Implements page-separated, position-based PDF text extraction.
src/​GpibMcp.Core/​GpibMcp.Core.csproj Adds PdfPig and distributes its license notice.
src/​Directory.Build.props Bumps the shared version to 1.3.1.
README.md Documents built-in PDF support and ManualForge integration.
packaging/​THIRD-PARTY-NOTICES.txt Includes PdfPig’s Apache 2.0 license.
packaging/​Build-Mcpb.ps1 Updates manual-folder setup guidance for scanned PDFs.

Review effort: Balanced

Comment on lines +151 to +155
foreach (ManualForgeIndex.PageHit page in ManualForgeIndex.SearchPages(_root, query, model, Math.Max(1, limit) * 4).Pages)
{
ManualFile file = Resolve(page.Path); // as in Choose: nothing outside this root
if (file == null) continue;
if (only != null && !string.Equals(file.FullPath, only.FullPath, StringComparison.OrdinalIgnoreCase)) continue;
for (int i = 0; i < files.Count; i++)
{
ManualFile file = files[i];
if (clock.ElapsedMilliseconds >= timeBudgetMs)
From the review of #170:
- A manual is read on a thread of its own and the search waits only for the
  time it has left. One it gave up on keeps reading and fills the cache, and
  is reported as still being read; a second search joins the same read.
- The tool shares one 25 s budget between choosing manuals, reading them and
  asking ManualForge for pages, and each index lookup has its own timeout.
- A page the built-in reader can't parse is marked in the cached text, so the
  search names it rather than treating it as a page with no match. Text
  cached before this is retired.
- A search of one file asks ManualForge for 500 pages, since its search
  can't be narrowed to a file.
- An index snippet replaces a passage from the same page that matched only
  part of the query.

Tested on the real library: 8902A cold 15.1 s (one manual still being read),
then 7.2 s with all twelve, then 0.4 s; with ManualForge, a cold search with
no model took 16.2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Explicit-file searches can discard OCR-only matches due to model filtering and unhandled alternate paths, while minimum stage allowances can exceed the shared 25-second deadline.

3 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Include duplicate paths in file-specific manual searches

src/​GpibMcp.Core/​Manuals/​ManualLibrary.cs:165

ManualForge folds identical copies by default and reports alternate paths in alsoAt, but this filter only considers the primary path. A file= search for an alternate copy therefore discards every indexed hit for that document, even with the 500-page limit, so its OCR-only content remains unavailable. Request --show-duplicates for file-specific searches, or retain alsoAt and map matching hits to the requested copy.

Medium severity Honor explicit file selection when filtering OCR index pages

src/​GpibMcp.Core/​Manuals/​ManualLibrary.cs:166

A file= search bypasses filename/model selection in ManualTools.Search, but this filter still rejects index pages whose filename does not mention the model. For example, an explicitly selected Operating Manual.pdf outside a model-named folder can be read locally, yet all of its OCR-only matches are discarded when the caller also passes model. Honor the explicit file selection here and apply the filename/model filter only when no specific file was supplied.

2 resolved since last review

Review effort: Balanced

Comment thread src/GpibMcp.Core/Tools/ManualTools.cs Outdated
// some with only part of the query - may have its answer on a page only ManualForge can read.
if (results.Hits.Count(h => h.HasEveryWord) < maxResults)
ManualSearch.Fill(results, library.IndexedPages(query, model, one, maxResults,
(int)Math.Max(MinPagesMs, CallBudgetMs - clock.ElapsedMilliseconds)), maxResults);
…left

From Copilot's second review of #170:
- A file= search asks ManualForge with --show-duplicates, since it folds
  identical copies into one hit and a search for the other copy got none of
  its pages. Seen on the real library: E4438C's 9018-01484.pdf now gets
  ManualForge's page 705.
- A file= search no longer applies the model's filename filter to index
  pages: the caller chose the file.
- Reading and the page lookup are capped by the time left, and the index
  isn't asked for pages when less than a second remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TGoodhew
TGoodhew merged commit e63e673 into main Nov 7, 2026
1 check passed
@TGoodhew
TGoodhew deleted the fix/manual-search-manualforge branch November 7, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

manual_search returns 'no text extractor available' for every 8902A PDF

2 participants