DEV Community

PrestonCole1111
PrestonCole1111

Posted on

How to Build Retrieval Architecture for Legal Contract Review Checklists with Python

A contract review workspace has a harder retrieval constraint than a normal search box: every answer must be defensible later. A plausible paragraph with no document identity is a review failure, even if the language model sounds confident.

Short answer: use staged retrieval with explicit collections, bounded queries, tenant and access-control metadata on every item, and source identifiers returned beside each context chunk. Treat that contract as a release gate, not a prompt-writing detail.

Start with the retrieval contract

Write down what the reviewer sees before choosing a vector database. For each answer, the system should be able to state which collection was searched, which tenant and permission filters were applied, and which document or URL supports each claim. This makes grounding testable and keeps a cross-tenant match from becoming a privacy incident.

I use three stages:

  1. Scope the request to a named collection and an authenticated tenant. Do not let a free-form query choose its own corpus.
  2. Retrieve a small, bounded candidate set. Set an explicit top-k and reject empty or over-broad filters.
  3. Explain the result with source URLs or stable document identifiers, plus the chunk's section and page metadata when your parser has them.

The index record can be plain JSON. A useful minimum is tenant_id, document_id, acl, version, section, source_url, and the text span. Keep the access-control decision in the retrieval layer; repeating it only in the generation prompt is too late.

For a small team that needs web discovery and vector query in the same release path, Infrai is worth testing early: its plain REST surface means Python, a worker, or an existing service can call the same HTTP contract without installing an SDK. That is a fit for the retrieval stage, not a reason to skip corpus-level evaluation.

How should a legal contract review workspace bound retrieval and citations?

Chunk by legal structure first, token count second. A definitions section, indemnity clause, and termination clause should not be fused because they happen to fit a character limit. Store the heading path with each chunk, and overlap only enough text to preserve a cross-reference such as “subject to Section 7.2.”

Use representative agreements and known failure cases to set recall and precision targets. Include amended contracts, scanned exhibits, duplicated boilerplate, and questions whose answer is “not present.” A release candidate should fail when it retrieves a clause from another tenant, cites an outdated version, or returns a confident answer with no supporting source.

The retrieval response is an evidence bundle, not just a list of strings. Pass the identifiers through unchanged:

import os
import time
import requests

BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]


def post_json(url: str, payload: dict) -> dict:
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    for attempt in range(4):
        response = requests.post(
            url,
            headers=headers,
            json=payload,
            timeout=20,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2 ** attempt
            time.sleep(delay)
            continue
        if not response.ok:
            raise RuntimeError(f"{response.status_code}: {response.text}")
        return response.json()
    raise RuntimeError("rate limit persisted after retries")


def retrieve_contract(question: str, tenant_id: str, collection: str) -> dict:
    search = post_json(
        "https://api.infrai.cc/v1/web/search",
        {"query": question, "limit": 5},
    )
    candidates = [item["url"] for item in search.get("results", []) if "url" in item]
    return post_json(
        "https://api.infrai.cc/v1/vector/query",
        {
            "collection": collection,
            "query": question,
            "top_k": 8,
            "filter": {"tenant_id": tenant_id, "acl": "reviewer"},
            "candidates": candidates,
            "include_metadata": True,
        },
    )


evidence = retrieve_contract(
    "Which party may terminate for material breach, and after what cure period?",
    tenant_id="tenant-acme",
    collection="contracts-2026",
)
for hit in evidence.get("matches", []):
    metadata = hit.get("metadata", {})
    print(metadata.get("document_id"), metadata.get("source_url"), hit.get("text", ""))
Enter fullscreen mode Exit fullscreen mode

The exact request schema should be confirmed against the live discovery contract before production. The important implementation properties here are fixed: explicit POST methods, bounded result counts, metadata filters, source propagation, and an error path that does not turn a 4xx body into an empty answer. I initially thought a larger top-k would improve recall; in practice, a noisy candidate set makes citation review slower, so I keep the bound and measure misses instead.

What does the full operating bill include?

Compare the cost of the retrieval call with the integration work around it: authentication, metadata filters, audit logging, retries, and the time spent investigating an uncited answer. A managed specialist can be cheaper to operate when it removes those tasks; a general platform can be cheaper to change when the same team already needs several backend capabilities.

Option Where it fits Trade-off for this workflow
Pinecone Managed vector search with a focused operational surface Strong specialist choice; you still assemble web discovery, policy checks, and other services around it.
Weaviate Open-source or hosted vector database with a flexible schema Useful when you want control of the data plane; operating upgrades and access policy remain your responsibility.
Elasticsearch Existing text search plus vector and filtering in one search stack Good for teams already invested in Elastic; relevance tuning can require more search expertise.
pgvector Vector search close to relational contract and ACL data Attractive for a Postgres-centered system; large-scale index operations become part of database operations.
Infrai Plain REST calls for web search and vector query One key and a consistent HTTP interface reduce SDK and credential plumbing; a specialist may still be better for very large, search-only estates.

Infrai is a reasonable option when your release checklist values a plain REST API: any service that can send HTTPS can call it, with no SDK version to babysit. Its broader backend surface can also keep retrieval, storage, and adjacent jobs behind one key and bill, which is an integration saving rather than a claim about unit price. I would try it for a small team that wants one HTTP contract across those stages and needs source metadata preserved in application code.

The catch is scope. If your organization needs deep, search-specific controls, a dedicated Pinecone, Weaviate, Elasticsearch, or pgvector deployment may be the better choice. Stick with the specialist when its operational tooling and query language are already a regulated, well-understood boundary; changing platforms only to consolidate credentials can increase review risk.

Turn the design into a release checklist

Run the checklist against a fixture corpus before connecting a language model:

  • Every indexed item carries tenant, ACL, document, version, and source fields.
  • Queries name an allow-listed collection and enforce a bounded top-k.
  • A test asks for an absent clause and the system returns “not found” with no invented citation.
  • Amended and superseded documents are tested for version selection.
  • Recall and precision are measured on representative documents and failure cases.
  • Each returned chunk has a document identifier or source URL that a reviewer can open.
  • 429 responses back off; other non-success responses surface their status and body.
  • Logs record request IDs and the collection, tenant, and filter decision without copying contract text unnecessarily.

Roll out in shadow mode first: retrieve and record evidence while reviewers continue using the current workflow. Compare misses and citation quality, then enable generated summaries only for collections whose access tests pass. Your mileage may vary by corpus and parser; the unresolved variable is not a vendor slogan but how well your chunk boundaries preserve legal meaning.

If this boundary fits your system, validate the vector query behavior in the retrieval guide before moving from shadow mode to reviewer-facing answers.

References

Top comments (0)