AgentLex

Everything AgentLex does, in the order it does it.

A document goes in; an answer you can check comes out. This page follows one library through every step, with what each step does, how it works, and what the API returns.

01

Bring documents in

Any text format

Send documents the way you already have them. AgentLex turns each one into pages before anything else happens.

  • Text and Markdown, split into pages at form feeds or --- page 12 --- markers, which also carry the printed page number.
  • HTML, with navigation, scripts and footers dropped, headings and lists kept, tables turned into Markdown tables, and the title and author read from the page.
  • CSV and TSV, quoted fields handled, 40 rows per page with the header repeated, and the column names kept as metadata.
  • JSON records, one page per record, nested fields flattened; or printed pages with their number, volume and heading.
POST /v1/collections/:id/documents · format detected, or set with format
request
POST /v1/collections/col_8hQ/documents
{
  "title": "Data Processing Agreement",
  "metadata": { "jurisdiction": "Arcadia" },
  "format": "html",
  "html": "<h1>Article 1</h1>..."
}
response
{ "pages": 9, "chunks": 61, "chunker": "legal",
  "format": "html", "identifiers": { "total": 0 } }

Structure-aware chunking

A passage should be a unit its readers would recognize, so documents are cut where their authors cut them. Four chunkers, chosen by detection or by name:

  • Clinical notes: one passage per section (chief complaint, medications, labs, assessment, plan), headers like HPI and A/P mapped to canonical names, ICD-10 codes, dates and drug names kept as metadata.
  • Legal texts: the numbering hierarchy (Article, Section, §, (a), (1), (i)) with a breadcrumb on every passage, each defined term a passage of its own, citations collected.
  • Classical Arabic print: body and editor's footnotes apart and linked, a hadith kept whole with its chain, a verse with the commentary after it, a poem's lines together.
  • Prose: headings and paragraphs, packed to about 700 characters, long paragraphs cut at sentence ends.
POST /v1/chunk previews the cut without a key and without storing anything
one clinical note, cut into passages
Chief Complaintheading
Medications, 4 items in one passagemedication · list
Laboratory Resultslab · table
Assessment (E11.9, I10)diagnosis
Planplan · date
one contract, cut by its hierarchy
Definitions › “Personal Data”definition
Article 4 › 4.2 › (b)clause
Article 9 › 9.1clause · citation

Tables kept whole

A table row is a fact, and a row cut off from its header is a fact without its meaning. Markdown, CSV and space-aligned tables are found in every document and never split mid-row.

  • A table that fits becomes one passage, flagged table.
  • A long table is split between rows, and the header row is repeated at the top of every piece.
  • Text before and after a table stays in its own passages; code blocks are never mistaken for tables.
a 60-row lab table, piece 2 of 2
| Test       | 03/02  | 06/01  |   header repeated
|------------|--------|--------|
| Potassium  | 4.6    | 4.4    |
| eGFR       | 44     | 47     |
| ...

Pages, passages and offsets

Two units do two jobs. Whole pages are what get found and cited; passages are what get read and shown. Every passage records exactly where it sits on its page.

  • Each passage carries start and end offsets: page.text.slice(start, end) is the passage, letter for letter.
  • Each passage carries its kinds (medication, lab, definition, clause, footnote, hadith, verse and more), its heading or breadcrumb, and its metadata.
  • Any page can be fetched whole with its passages, so an interface can open the source and highlight the line.
GET /v1/collections/:id/documents/:doc/pages/:n
a passage
{ "pos": 3,
  "text": "Metformin increased from 500 mg to 1000 mg BID",
  "kinds": ["plan", "medication"],
  "heading": "Assessment and Plan",
  "start": 1184, "end": 1231,
  "meta": { "drugs": ["Metformin"] } }
02

Find

Keyword search

BM25 over every passage, for exact terms, names, codes and numbers. A keyword hit on a passage counts for the page it belongs to.

  • English is lowercased, stopwords dropped and words stemmed, so transfers finds transfer and children finds child.
  • Arabic is normalized: vowel marks, tatweel and letter variants set aside, the article and common prefixes stripped, so a search matches however a word was printed.
  • Section titles and headings are searchable too, so a search for allergies lands on the Allergies section.
POST /search with "mode": "keyword"
the same word, printed three ways, one match
الْمُقَابَلَةُvowelled
المقابلةplain
مقابلةno article

Semantic search

For a question asked in different words than the document uses. Each page is embedded whole, with its document title, author and heading in front, and the question is compared with every page by meaning.

  • Whole pages, not small chunks: on 23 books and 20 questions, page vectors found the right page with nDCG@10 of 0.81, against 0.69 for 500-character chunks.
  • A question like “what did the doctor change and why” finds a plan that says “increased from 500 mg to 1000 mg; tolerated without GI upset”, with no word in common.
  • Works across English and Arabic with a multilingual model. A collection records the model it was built with, and vectors from two models are never compared.
POST /search with "mode": "semantic"
a question and the page it finds
Was the patient's kidney function a concern?question
“eGFR now under 45; metformin reduced accordingly”p. 2

No word of the question appears in the passage; the meaning does.

Hybrid fusion

The default. Keyword and semantic search run side by side and their rankings are fused, so a page that both find rises to the top.

  • Reciprocal rank fusion: scores from different methods are not comparable, so pages are fused by rank, not by score.
  • Keyword lists weigh 0.5 against semantic 1.0, the setting that won a 40-question blind benchmark (nDCG@10 0.787 against 0.722 for equal weights).
  • A third, lighter list ranks pages whose title or headings match the question.
POST /search with "mode": "hybrid" (default)
which lists found each result
{ "rank": 1, "citation": "AL-7C21E0B4",
  "retrievers": ["keyword", "semantic", "fields"],
  "score": 0.0312 }

Query expansion

Questions are written in shorthand; documents often are not. Each query is expanded into the forms the documents are likely to use, and each form is searched.

  • Clinical abbreviations (over 150): HTN, MI, CHF, A1c, BID, s/p. MI in capitals expands; mi in a sentence does not.
  • Legal abbreviations and symbols: § becomes section, ¶ paragraph, U.S.C., CFR, GDPR.
  • Arabic spelling variants, and optionally up to three paraphrases written by a model in the documents' own vocabulary.
  • The question's intent is detected (question, list, entity, phrase, keyword) and returned with the results.
"expand": true (default), false, or "llm"
returned with the results
"intent": "question",
"expansions": [
  { "text": "hypertension medication change", "kind": "abbreviation" },
  { "text": "high blood pressure medication change", "kind": "llm" }
]

Quoted phrases must match

Put words in quotation marks and only pages that contain them, in that order, qualify. Vowel marks and letter variants are set aside when comparing Arabic.

  • The passage holding the phrase is shown first on each page.
  • If no page contains the phrase, the results fall back to the best matches and say so in warnings, rather than returning nothing.
{ "query": "\"nighttime decibel\" limits" }

"phrases": ["nighttime decibel"],
"warnings": []

Filters

Narrow the search before it runs, so the top results are the top results within what you asked for.

  • documents: only these document ids.
  • kinds: only passages of these kinds, such as medication, lab, definition, footnote or hadith. A page with no such passage still shows its best ones.
  • metadata: equality on any document field, like a patient id, a jurisdiction or a speaker.
  • date: a from and to range over the document's date or the dates found in its pages.
  • headings: words that must appear in a page or passage heading.
{ "query": "anticoagulation plan",
  "filters": {
    "metadata": { "patient": "MRN-SYN-0042" },
    "kinds": ["plan", "medication"],
    "date": { "from": "2026-01-01" }
  } }

Reranking

The fused top 30 are read again by a cross-encoder, a model that looks at the question and each passage together, with the document's title, author and heading in front.

  • Jina, Cohere, Voyage or your own server, in that order of preference; if none answers, the fused order stands.
  • On the shipped legal benchmark, reranking lifted nDCG@10 from 0.73 to 0.95; on clinical notes from 0.83 to 0.89.
  • The result says which reranker ran, so a change in quality can be traced.
"rerank": false keeps the fused order
legal benchmark, nDCG@10
Keyword
Semantic
Hybrid
Hybrid + rerank

Duplicates and attestations

Large libraries repeat themselves: several editions of one book, the same text on adjacent pages, one quotation carried by many later works.

  • Near-identical text within one document is folded into its best-ranked copy, so results are not ten copies of one page.
  • The same text in a different document is kept and counted as an independent attestation, because a second source saying it matters.
{ "duplicates": 2,   // folded copies
  "alsoIn": 1 }      // other documents carrying it

Stable citations

Every page has a citation id derived from its document and position. It does not change between searches, so an answer stored today still points at the same page tomorrow.

  • Results carry the document's title and author, the page's printed number and volume when the document had them, and its heading.
{ "citation": "AL-7C21E0B4",
  "document": { "title": "Progress note" },
  "page": { "n": 2, "number": "3", "heading": "Assessment and Plan" } }
03

Answer

Extraction

When you need the evidence, not a written answer. A fast model reads each page found and copies out only the sentences that answer the question.

  • Every excerpt is then looked up on its page by code. One that is not there, letter for letter, is discarded and counted, never returned.
  • An explanation copied without the text it explains is widened back to include it.
  • Each page gets a one-sentence note on what it contributes; pages with nothing for the question are dropped.
POST /v1/collections/:id/extract
{ "kept": 3, "rejectedExcerpts": 1,
  "pages": [{
    "citation": "AL-7C21E0B4",
    "note": "The plan doubles metformin and says why.",
    "excerpts": [{ "text": "Metformin increased...",
                   "match": "exact", "offset": { "start": 1184 } }]
  }] }

Research agent

For questions one search cannot answer. An agent searches, reads and decides when it has enough, then a stronger model writes the answer from those pages alone.

  • The question itself is searched first, before the model speaks, so sources appear in under a second.
  • The agent then issues several searches at once for the parts the first pass missed, reads whole pages or the next page where a passage is cut short, and calls done.
  • It thinks in the domain: encounters and sections for clinical notes, definitions and cross-references for law, shelves and footnotes for classical texts.
  • fast: up to 3 steps, about ten seconds. deep: up to 8 steps with a stronger model.
  • When the sources do not answer, the answer says so. Nothing is filled in from memory.
POST /v1/collections/:id/research · tier, filters, history
what the agent did
search the question itself 8 pages search "warfarin" kinds=medication 3 pages search INR results 4 pages read Discharge summary, p. 2 done "INR history found; no bleeding events recorded"

Streaming

Research can stream as newline-delimited JSON, so an interface shows the searches, then the sources, then the answer word by word, then the checks.

  • Events, in order: status, query, sources, delta, answer, claims, done, or error.
  • If the client disconnects, the work stops.
"stream": true
{"type":"query","query":"\"warfarin\"","results":3}
{"type":"sources","sources":[...]}
{"type":"delta","text":"The INR stayed "}
{"type":"delta","text":"between 2.1 and 2.8"}
{"type":"claims","summary":"4 of 4 claims supported"}
{"type":"done","result":{...}}
04

Check

Quotation verification

After an answer is written, every quotation in it is looked up on the page it cites.

  • exact: found letter for letter. normalized_exact: equal once vowel marks and letter variants are set aside.
  • A quotation with an ellipsis must match each part, in order.
  • A quotation that is not on its page is reported as unverified, never silently kept. In KhateebNotes' research race, 60 of 61 quotations were found on their page.
"quotes": [
  { "quote": "Repeat A1c in three months", "source": 1, "status": "exact" },
  { "quote": "dose raised for weight", "source": 1, "status": "unverified" }
],
"verified": 1, "total": 2

Claim verification

Quotations are not the only thing that can be wrong. Every sentence of the answer is split out as a claim and judged against the sources it cites, and only those.

  • supported: the source states it. partial: only part of it, or the wording goes further. unsupported: the source does not say it. uncited: no source was given.
  • Each judgment comes with a short span of evidence from the source, itself checked to be on the page.
  • Character offsets let an interface underline each sentence by its status.
The dose was doubled after an A1c of 8.4%.supported
It was tolerated without side effects.partial
The patient also started insulin.unsupported
Kidney function was normal.uncited
"summary": "1 of 4 claims supported by their sources; 1 partially."
05

Protect

Identifier masking

Turn it on for a collection and identifiers are replaced with placeholders before any text reaches an embedding or language model. Your stored documents stay as they are.

  • Social Security numbers, medical record numbers, phone numbers, emails, dates of birth, card numbers, addresses, NPI and passport numbers, and names after labels like Patient:, Plaintiff: or Dear.
  • The same value always gets the same placeholder, so the text still reads correctly.
  • Built to avoid false alarms: ICD codes, lab values, doses, section numbers and case citations are not mistaken for identifiers.
"settings": { "mask_identifiers": true }
what a model sees
Patient NAME-1 (MRN MRN-1), DOB DOB-1, was reached at PHONE-1. NAME-1 agreed to repeat labs.

Redaction before storage

For collections where identifiers should never be kept at all. Text is masked before it is chunked, indexed or stored, and the originals are discarded.

  • This cannot be undone, by design. The document records how many identifiers of each kind were removed.
  • Search, extraction and research all work on the redacted text.
"settings": { "redact_storage": true }
"meta": { "redacted": true,
  "identifiers": { "person": 3, "mrn": 1, "phone": 2 } }

Identifier scan

Check a text before you decide how to store it. The scan reports what kinds of identifiers it holds and how many, and a masked preview, without echoing the identifiers back.

  • Request bodies and queries are never written to logs; unexpected errors are masked before they are.
POST /v1/scan
{ "total": 4,
  "counts": { "ssn": 1, "phone": 2, "email": 1 },
  "masked_preview": "Call [PHONE-1] or write [EMAIL-1]..." }

PHI collections

Mark a collection as holding protected health information and AgentLex guards every path text can take out of the server.

  • Every text sent to the language model or the embedding provider (questions, passages, page reads, prompts) passes a privacy boundary: identifiers become placeholders before they leave, and the originals are restored in what comes back.
  • The same person keeps the same placeholder for the whole request, so the model can still reason about who did what, and quotation and claim checks still run on the original words.
  • A PHI collection can only be created on a server with encryption at rest switched on.
"settings": { "phi": true }
what the provider receives, and what you get back
→ Patient NAME-1, phone PHONE-1, A1c 8.4%... ← NAME-1 had metformin doubled [1].
John Smith had metformin doubled [1].

Encryption at rest

Everything that can hold a patient's or a client's words is sealed with AES-256-GCM before it is written: page and passage text, the search index, headings, titles, authors, metadata and page vectors.

  • The key lives in your environment, never in the database, the logs or a response.
  • Every sealed value is authenticated: a value changed outside AgentLex, or read with the wrong key, fails to open instead of being served.
  • A server can be told to refuse to start without a key.
AGENTLEX_ENCRYPTION_KEY · AGENTLEX_REQUIRE_ENCRYPTION=1
a passage as it sits on disk
text  enc1:Vq3J0f2c9Lh8...Xw==
title enc1:a81KpQe0Zt7m...0g==

Audit trail

Every API call leaves a record of who made it, with which key, against which collection and document, from where, when, and what happened. Refused keys are recorded too.

  • The trail holds ids and outcomes only: never a query, a document's text, or an identifier.
  • It survives deleting the account, so access can still be reviewed afterwards.
GET /v1/audit
{ "at": "2026-10-15T14:02:11Z",
  "action": "GET /v1/collections/:id/documents/:doc/pages/:n",
  "key_prefix": "al_7Hc2aQ",
  "collection_id": "col_8hQ", "document_id": "doc_k2P",
  "status": 200, "ip": "10.0.4.12", "ms": 14 }

Retention and deletion

Keep records only as long as your policy says.

  • Set retention_days on a collection and documents older than that are deleted every hour, each deletion audited.
  • Deleted rows are overwritten on disk, not just unlinked.
  • An account can be erased with every key, collection and document in it.
  • API calls can be refused unless they arrive over HTTPS, with HSTS and strict security headers on every response.
"retention_days": 2555 · DELETE /v1/account · AGENTLEX_REQUIRE_TLS=1
{ "action": "retention.delete",
  "collection_id": "col_8hQ",
  "document_id": "doc_9Lm" }
06

Measure and run

Evaluation on your own corpus

Judge a change by the numbers on your documents, not ours. Send questions and the passages that answer them; AgentLex runs every search mode and reports how each did.

  • Relevant pages can be named by citation, by document and page, or by a snippet of their text.
  • Returns nDCG, precision, recall, MRR, MAP and p50 and p95 latency for keyword, semantic, hybrid and reranked search, plus which questions missed.
POST /v1/collections/:id/evaluate
{ "questions": [{
    "query": "How does the DPA define Personal Data?",
    "relevant": [{ "text": "means any information relating to" }]
}] }

"hybrid+rerank": { "ndcg": 0.95, "recall": 1,
                   "latency": { "p50": 439 } }

Sample libraries

Three fictional libraries ship with AgentLex, so you can see it work before you upload anything: 53 clinical encounters across 10 patients, 12 contracts, codes and court opinions, and 32 letters, minutes and oral histories with a few Arabic essays.

  • Each comes with 25 questions whose answers are verbatim passages, the same benchmark shown on the home page.
  • One click in the console loads any of them into a collection.
POST /v1/collections/:id/sample { "domain": "medical" }
Clinical notes0.89 nDCG@10
Legal texts0.95 nDCG@10
Archive0.96 nDCG@10

Models you choose

AgentLex does not tie you to one provider.

  • Embeddings: Jina, OpenAI, or any model on your own embedding server.
  • Reranking: Jina, Cohere, Voyage, or your own server.
  • Reading and writing: Claude, a fast model for extraction and the fast tier, a stronger one for deep research and answers.
  • On Enterprise, all of it in your own cloud account, so identifiers never leave it.
GET /v1/health
{ "embedder": "jina:jina-embeddings-v3@1024",
  "rerankers": ["jina"],
  "llm": true }

Keys, limits and accounts

Everything a team needs to run AgentLex in production.

  • API keys: create, list and revoke; only a hash is stored, and a key is shown once.
  • Rate limits per account, reported on every response in x-ratelimit-* headers, with retry-after when you go over.
  • A cap on concurrent research calls per account, and monthly allowances for search, extraction and research.
  • Collections are private to the account that made them; every query is scoped to it.
x-ratelimit-limit: 600
x-ratelimit-remaining: 597
x-ratelimit-reset: 1

SDKs and OpenAPI

JSON over HTTP, with clients that have no dependencies.

  • JavaScript for Node and browsers, with streaming research as an async iterable.
  • Python using only the standard library, with streaming research as a generator.
  • An OpenAPI 3.1 file for every endpoint, and a full reference.
from agentlex import AgentLex

q = AgentLex(api_key="al_...")
for event in q.research(col, {"question": question, "stream": True}):
    if event["type"] == "delta":
        print(event["text"], end="")

See it on your own documents.

Load a sample library in the console, or talk to us about a dedicated index.