aiml
SearchMy learning
Menu

Lesson 22 of 36Module 9

Add search and read tools

Resolve issued passage handles to pinned source text

Expose bounded documentation search and passage reads, bind handles to a pinned corpus and exact request, and inspect provenance before accepting tool output.

Prerequisites

Python dictionaries and JSON; versioned passages, lexical retrieval and source identity.

By the end, you can

  • Return ranked passage metadata without exposing source bodies in search results.
  • Resolve issued handles to exact versioned source spans.
  • Reject altered handles and read results before accepting retrieved text.
Runtime
Python 3.11+; SQLite with FTS5
Dependencies
Standard library only; no provider SDK or model download

Lab code, tests and fixtures (ZIP, 771 KB)Quiz check your understanding

Progress is saved in this browser.

Give a retrieval controller two distinct operations: search for candidate passages, then read a selected passage. A search rank helps choose what to inspect. The read result supplies the actual text and provenance needed to inspect a claim.

When you need this. Use this boundary when a caller should choose among candidates before receiving source bodies, and every read must refer to a passage returned by a matching search.

The lab uses the existing versioned developer-document summaries and SQLite lexical index. It runs those local tools directly. No model chooses a tool, generates a response or receives these passages.

Pinned developer docsSearch eligiblepassagesRanked passage handlesNo source bodyRead selected passageExact source text+ provenanceInvalid request or handleRefuse tool callcorpus + queryresult IDsselected IDresolved spanrefusedata flowfailure
Search returns passage handles from the pinned corpus. Read resolves a selected handle to its source text; invalid requests or unknown handles are refused.

Separate candidate selection from source reading

A passage is one versioned paragraph in the supplied snapshot. The snapshot contains original teaching summaries with primary-document links; it is not a fresh crawl. The loader checks its pinned file hash and the inherited snapshot validation rules before constructing the index.

search accepts a bounded alphanumeric query term, a requested documentation version and a result limit. This deliberately small interface is not a natural-language query parser. The backend ranks lexical matches, then exact-version eligibility is applied before the caller’s result limit. Fetching every candidate is practical only because this fixture caps corpus size.

Search returns handles: structured metadata containing the corpus identity, search-request identity, passage identity, version, revision, span, text hash, planned text bytes and rank. It returns neither source text nor source URL. A handle names a candidate; it does not assert that its content answers the question.

Read only a handle issued by this tool instance

read resolves a selected handle against the exact search result retained by the current instance. The result includes the authoritative paragraph, URL and a result hash. The line span and hashes connect the returned text to the pinned snapshot.

Changed handle. Editing a handle’s version, passage identity, score or other bound field causes refusal. A handle from an earlier corpus or an unissued search cannot select a different passage. These consistency checks are local application rules, not permissions, publisher authentication or protection from code that can modify the process.

Changed result. validate_read compares the complete result with the authoritative payload for that handle. A plausible URL or matching string length cannot substitute for that comparison. The controller must call this check before it accepts a read observation.

Inspect empty and ineligible searches

No lexical match returns an empty candidate list. The same happens when matches exist but none has the requested version. The caller can distinguish those searches from a read refusal by retaining the query, version and actual result.

OperationObserved result
Matched search1 issued handles
Resolved readspath-write
No lexical match0 candidates
Requested version absent0 candidates
Edited handleinvalid_read_handle

An empty result means this search returned no eligible passages; relevant documents may exist elsewhere. A validated read result matches the supplied snapshot but does not guarantee that a future answer will be correct.

Execute both tool operations

Download the complete search/read lab. It includes the pinned corpus, reused lexical index and version checks, tool implementation, failure tests and execution record. Python needs SQLite with FTS5 enabled; no provider SDK or downloaded model is required.

tools.pySource · pythonDownload
"""Bounded documentation search handles and receipt-bound passage reads."""
import copy
import hashlib
import json
from pathlib import Path
import re
from bm25 import LexicalIndex
from version_documents import validate_snapshot,canonical_hash

SNAPSHOT_SHA256='56bbeff06b0349186c3e5999d2b4fe634c6355f3bc267d6e07c47bf2d5e32b19'

class ToolError(ValueError):pass

def load_snapshot(root=None):
    path=(root or Path(__file__).parent/'fixtures')/'snapshot.json'
    raw=path.read_bytes()
    if hashlib.sha256(raw).hexdigest()!=SNAPSHOT_SHA256:raise ToolError('snapshot_file_hash_mismatch')
    snapshot=json.loads(raw);validate_snapshot(snapshot);return snapshot

class DocumentationTools:
    def __init__(self,snapshot):
        validate_snapshot(snapshot)
        self.snapshot=copy.deepcopy(snapshot);self.corpus_id=snapshot['snapshot_id'];self.rows={};self.receipts={};self.closed=False
        for source in self.snapshot['documents']:
            for chunk in source['chunks']:
                self.rows[chunk['chunk_id']]={'passage_id':chunk['chunk_id'],'source_id':source['source_id'],'version':source['version'],'document_revision':source['document_revision'],'source_sha256':source['sha256'],'start_line':chunk['start_line'],'end_line':chunk['end_line'],'text_sha256':chunk['text_sha256'],'byte_count':len(chunk['text'].encode('utf-8')),'text':chunk['text'],'url':source['url']}
        if not self.rows or len(self.rows)>100:raise ToolError('unsupported_corpus_size')
        self.index=LexicalIndex(self.snapshot)
    def close(self):
        if not self.closed:self.index.close();self.closed=True
    def available(self):
        if self.closed:raise ToolError('tools_closed')
    def search(self,query,k=2,version='3.12'):
        self.available()
        if type(k) is not int or not 1<=k<=10:raise ToolError('invalid_k')
        if not isinstance(query,str) or re.fullmatch(r'[A-Za-z0-9]{1,64}',query) is None:raise ToolError('invalid_query')
        if not isinstance(version,str) or re.fullmatch(r'[0-9]+\.[0-9]+',version) is None:raise ToolError('invalid_version')
        query=query.lower()
        request={'corpus_id':self.corpus_id,'query':query,'version':version,'k':k}
        request_id=canonical_hash(request)
        # Corpus is capped at 100. Fetch all native ranks, then version-filter
        # before applying the caller's k, without losing eligible candidates.
        ranked=self.index.search(query,limit=100)['results']
        eligible=[row for row in ranked if row['version']==version][:k]
        handles=[]
        for rank,row in enumerate(eligible,1):
            source=self.rows[row['chunk_id']]
            handle={'corpus_id':self.corpus_id,'search_request_id':request_id,**{key:value for key,value in source.items() if key not in ('text','url')},'native_score':row['native_score'],'rank':rank}
            handle['read_token']=canonical_hash(handle);handles.append(handle)
        result={'corpus_id':self.corpus_id,'search_request_id':request_id,'query':query,'version':version,'k':k,'results':handles}
        self.receipts[request_id]=copy.deepcopy(result)
        return copy.deepcopy(result)
    def validate_handle(self,handle):
        self.available()
        if not isinstance(handle,dict):raise ToolError('invalid_read_handle')
        if handle.get('corpus_id')!=self.corpus_id:raise ToolError('stale_corpus')
        passage=handle.get('passage_id')
        if not isinstance(passage,str) or passage not in self.rows:raise ToolError('unknown_passage')
        request=handle.get('search_request_id')
        if not isinstance(request,str) or request not in self.receipts:raise ToolError('unissued_search_receipt')
        expected=next((row for row in self.receipts[request]['results'] if row['passage_id']==passage),None)
        try:equal=expected is not None and canonical_hash(handle)==canonical_hash(expected)
        except (ValueError,TypeError,OverflowError):equal=False
        if not equal:raise ToolError('invalid_read_handle')
        return copy.deepcopy(expected)
    def read_cost(self,handle):return self.validate_handle(handle)['byte_count']
    def _read_payload(self,handle):
        handle=self.validate_handle(handle);source=self.rows[handle['passage_id']]
        body={**handle,'text':source['text'],'url':source['url'],'read_request_id':canonical_hash({'search_request_id':handle['search_request_id'],'read_token':handle['read_token']})}
        return {**body,'result_sha256':canonical_hash(body)}
    def read(self,handle):return self._read_payload(handle)
    def validate_read(self,result,handle):
        expected=self._read_payload(handle)
        try:equal=canonical_hash(result)==canonical_hash(expected)
        except (ValueError,TypeError,OverflowError):equal=False
        if not equal:raise ToolError('invalid_read_result')
        return copy.deepcopy(expected)
Run search and read locallyTerminal · bash
python run.py --output scratch-result.json
python -m unittest discover -v
Saved CLI outputRecorded output · jsonDownload
{
  "changed_handle_refusal": "invalid_read_handle",
  "empty_eligible_count": 0,
  "execution_mode": "offline-native-document-search-and-read",
  "no_match_count": 0,
  "read_source_ids": [
    "path-write"
  ],
  "search_results": 1
}

The deployed unit here is a local CLI. To use these methods across a service boundary, add request decoding, authorization and transport checks separately. The in-memory search-result table is scoped to this object; durable replay and sharing results across processes were not implemented. Corpus-size caps and result limits are guards, not throughput measurements.

View the complete local execution JSON
Actual offline execution recordRecorded output · jsonDownload
{
  "execution_mode": "offline-native-document-search-and-read",
  "model_called": false,
  "corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
  "search": {
    "corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
    "search_request_id": "aeffc68efdee46b8d23c597f651143424c575c0c1a4699ab08a48734d011f127",
    "query": "overwritten",
    "version": "3.12",
    "k": 2,
    "results": [
      {
        "corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
        "search_request_id": "aeffc68efdee46b8d23c597f651143424c575c0c1a4699ab08a48734d011f127",
        "passage_id": "c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7",
        "source_id": "path-write",
        "version": "3.12",
        "document_revision": "28c6349eaf80b629322edb310bb871dc45a04af9fad3b752bc75d12f0d7314e7",
        "source_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
        "start_line": 1,
        "end_line": 1,
        "text_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
        "byte_count": 224,
        "native_score": -0.7992166451067193,
        "rank": 1,
        "read_token": "08b866d8cf00533f3484aac4d78ac512bd4df8767c9ea11fbd2e817eeeb1dd75"
      }
    ]
  },
  "reads": [
    {
      "corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
      "search_request_id": "aeffc68efdee46b8d23c597f651143424c575c0c1a4699ab08a48734d011f127",
      "passage_id": "c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7",
      "source_id": "path-write",
      "version": "3.12",
      "document_revision": "28c6349eaf80b629322edb310bb871dc45a04af9fad3b752bc75d12f0d7314e7",
      "source_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
      "start_line": 1,
      "end_line": 1,
      "text_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
      "byte_count": 224,
      "native_score": -0.7992166451067193,
      "rank": 1,
      "read_token": "08b866d8cf00533f3484aac4d78ac512bd4df8767c9ea11fbd2e817eeeb1dd75",
      "text": "Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.",
      "url": "https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.write_text",
      "read_request_id": "a9ec642e4ed3206ac85e5dd03d4de9ff57de3167e134079eafe4a31de23cec67",
      "result_sha256": "662c96d3b972224ac1549f9a6c3ad495b3ed885b6db477d52aeb7527ec1dbbb7"
    }
  ],
  "no_match": {
    "corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
    "search_request_id": "f76beff608c3023ea069dea052656317ac6e5fbf4654fa39daeaaeed927c000f",
    "query": "xyznotindexed",
    "version": "3.12",
    "k": 2,
    "results": []
  },
  "empty_eligible_version": {
    "corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
    "search_request_id": "98f1cba586c00f8d9a3de99a7735dd9e5e453c850a0cff0ef67d2e2589eac672",
    "query": "overwritten",
    "version": "3.13",
    "k": 2,
    "results": []
  },
  "changed_handle_refusal": "invalid_read_handle",
  "interpretation": "Tool contract checks and native retrieval only; no model selection, answer generation or permission system."
}

The printed summary contains one issued result, the path-write read and the invalid-handle refusal; both empty searches return zero candidates. These are local tool-contract results.

Follow a handle back to its paragraph

Run the program and choose a returned handle. Compare its passage identity, version, span and text hash with the read result. Change its version and run the failure test. Explain why search metadata can select a passage but cannot stand in for reading its text.

A search returns a high-ranked passage handle. What must happen before its source text is accepted?

Choose one answer, then check your reasoning.

Read the explanation

Handle consistency does not provide authorization or prove that a source supports every answer claim.

What’s next

Continue with Refine Queries and Search Again.

References

SQLite FTS5, checked on 23 October 2026, documents the inherited native lexical ranking. The Python copy documentation explains the defensive copying used for returned dictionaries. Search-result validation and version eligibility are application policies defined by the supplied code. The earlier RAG baseline supplies the original developer-document task context.

Written by Arjun Prabhulal.

Module 9

Bounded retrieval policies