People search in the window¶
Type a brief about people — director level or above, based in Europe, with twenty years marketing infant formula in European markets — and the window answers with everyone who fits, each condition resting on the field or the sentence it came from; who is one condition short and what is missing; and how many it could not check. Ticking compare with a vector search runs the same brief through a standard embedding search beside it, so the difference between the ten most similar passages and everyone who fits is on one screen.
This page says what people mode does, from the code. The search underneath is
search for the conditions a field decides and
criteria for the niche; bringing a directory up, the paid
pre-read and the commands are in demo/RUNBOOK.md, and the presenter's script is
demo/walkthrough/SCRIPT.md.
What the agent does with a brief¶
People mode is the same agent loop as the other modes with its own system prompt
(PEOPLE_SYSTEM in demo/agent_demo.py). It reads the search tool's compact reply
— the people, why each is in the set, who is close, and counts of everybody else — and
asks for nothing larger unless a step needs it:
from demo.agent_demo import (PEOPLE_MAX_TURNS, PEOPLE_PROGRESS_CALLS, PEOPLE_SYSTEM,
SEARCH_SYSTEM, _system_for, default_max_turns)
print("its own prompt:", _system_for("people") is PEOPLE_SYSTEM
and PEOPLE_SYSTEM is not SEARCH_SYSTEM)
print(PEOPLE_SYSTEM.split("\n\n")[1].splitlines()[0])
print("progress calls at most:", PEOPLE_PROGRESS_CALLS, "| model turns at most:",
default_max_turns("people"), "(people) and", default_max_turns("answer"), "(the others)")
its own prompt: True
THE REPLY YOU READ is the tool's compact one. Never pass `detail` unless a step says so.
progress calls at most: 20 | model turns at most: 50 (people) and 25 (the others)
Then, in order:
- It splits the brief into ordinary conditions — what a record holds as a field: where someone is based, a role's function or seniority, an employer, dates, a compliance status — and at most ONE niche, kept whole in the brief's own words.
- First call, once: the ordinary part only, as a plain-language
brief. It reads how the search read the brief (constraints) and what it could not search (incomplete_because), and never re-phrases and calls again. A condition the records do not hold as a field is not a "no": it is left for the niche to carry. - Second call: the niche, as one
criteriatext beside those constraints, under a budget of 300 reads and $2.00 that the agent never raises. Only the people the ordinary conditions left are read. A niche whose words the search cannot state as a condition — a "not", an "at most" — is not read at all: the agent says the niche cannot be read as written and quotes the words, instead of reading it without them. A read that does not fit one call goes on without it. The call that starts the read hands the whole plan to a background reader, which reads it to the end at the deployment's pace (sixteen reads in parallel) whether or not anybody calls again; the reply says so (reading.background) and about how long the rest takes (reading.retry_after_s). The agent calls again only to WAIT: by the criterion's address with a page of one person — a call that starts nothing and returns the moment the read is done, or after about 40 seconds with how far it is (reading). It repeats at most twenty times (300 documents take about ten such calls), stopping early when three calls in a row saw nothing land. (Until 2026-10-13 a search read documents only while a call was open, and the same 300 took about thirty-four calls.) If the read would exceed the budget it reads NOTHING and says how many profiles it would take and what would narrow it. - It lists everyone. While the reply says more people fit than it listed
(
matched.next_offset), the agent asks for the next page — 25 people a page, by the criterion's address, reading and spending nothing — until there is none. - Every number in the answer is the tool's count — never a count of the entries the agent happened to be sent, and never its own judgment of a person. Someone the tool could not tell, did not read or holds no document for is not reported as a "no".
- The answer names every person who fits — no cap: a table of everyone with why each fits (the why column is dropped above fifty people; the names never are), then the people who are close with what each lacks, one line of counts, and whether the list is complete — and if not, why, in plain words.
Reading the result¶
Under the agent's turn the page shows the search's own reply as a card, built by the
pure block of demo/ui/index.html marked <search-model> (the test
tests/test_demo_search_card.py runs exactly those lines against real replies). The
agent holds the compact reply, so that is the card it draws:
- The summary line — 2 fit · 3 close · 4 cannot be told · 1 ruled out · 0 not read · 1 no document on file. A read that is not finished says so first (still reading — 8 of 30).
- How the request was read — the ordinary conditions as the search ran them, when the request was a brief.
- The result — the counts over everyone: fit · close · cannot be told · ruled out · not read · no document on file; then Complete: this is everyone who fits, or Not complete and why, in words (4 people's documents do not say enough to tell).
- People who fit, a page of them with the ranks shown — each with one line per condition: what the condition is, the value, the supporting words where the reply carries them, and whether it is from their record, their document, or counted from the dates of their recorded role. More people fit: the next page lists them while there is another page.
- Close — one condition short — each with what they lack and why: their documents do not say, or states it in a sentence that could not be verified, with that sentence. Never a match. At most ten are listed; the count is of all of them.
- The profiles line — how many profiles this call read, or that nothing new was read;
what the read cost is printed only by a window started with
--show-read-cost. - The call as sent and the full reply, collapsed, for anyone who wants the JSON.
A reply asked for with detail: "full" is still drawn as the larger card it always was
(by the records, the niche's conditions, every match's sentences as buttons that open the
profile, what the near-misses say instead, what was left open most often).
Cannot tell is not does not fit: it means the profile does not say, which is a different fact — and the reason the list of matches can be trusted is that nobody is on it by a guess.
Beside a vector search¶
With compare ticked the left pane runs a standard vector search over the same text the product was loaded from — every biography and call transcript of the export, embedded and ranked, the top ten passages shown with whose they are. It retrieves; it writes no answer. The table between the panes is computed from what each side RETURNED, with no answer key:
| vector search | Limma | |
|---|---|---|
| what came back | ten passages, from so many people | so many people who fit |
| how many fit? | cannot say — ten is a setting | a count, over everyone it could check |
| of the people it returned | how many of them fit, as Limma read them | every one fits, each condition on its own sentence or field |
| people who fit and are not in its list | how many | — |
| people who nearly fit | in the same list, unmarked | listed one condition short, with what is missing |
| nobody fits | returns ten passages all the same | says so, and how many it checked |
"Fits" in that table is Limma's reading of the people the vector search returned, and the table says so. How each side does against the truth is a separate, offline step — the scoreboard, below.
A window in front of a directory¶
A window started with --workspace-kind people stands in front of a directory of people
rather than a handful of filings: it opens in people search, offers search as the second
mode, and closes what only makes sense for filings — the document list, the fact panes,
the page images.
from fastapi.testclient import TestClient
from demo import ui_server
window = ui_server.create_ui_app("a-workspace", "http://limma.invalid",
workspace_kind="people", record=False)
with TestClient(window) as page:
config = page.get("/api/config").json()
print("kind:", config["workspace_kind"], "| modes:", config["modes"],
"| prints what a read cost:", config["show_read_cost"])
print("the document list:", page.get("/api/documents").status_code,
"| the analyst's mode:", page.post("/api/run", json={
"question": "what was revenue in 2023?", "mode": "answer"}).status_code)
kind: people | modes: ['people', 'search'] | prints what a read cost: False
the document list: 404 | the analyst's mode: 422
A mode the window does not offer is refused before the agent starts, so nothing is
spent on it. The comparison's index is built ahead of time: --build-baseline-index
embeds every biography and transcript of the export into the cache and exits, and a
window started over an export whose index is not built says so at start — otherwise the
first comparison a visitor asks for would build it inside their request (minutes for
thousands of profiles, over an hour for tens of thousands). The other serving options —
invited visitors who sign in with an access
code (--visitors), a spend cap per visitor and per day (--cap-visitor-usd,
--cap-day-usd), recording and replay — are the window's own and work the same in every
mode; demo/RUNBOOK.md has the commands.
Scoring a run against the answer key¶
The demo's directory is synthetic and carries its answer key: who really fits each
planted niche, and which sentence or field says so. demo/expert_network/scoreboard.py
joins a saved run with that key and prints what the walkthrough may claim — for the
search: returned, right, WRONG, found of the expected; whether the read FINISHED; what
the one-condition-short flag is worth; where each missed person was put — and for the
vector search: whose passages it returned, how many of them fit, and what the rest are.
The scoring itself is two small functions:
from demo.expert_network.scoreboard import score_matches, score_passages
key = { # one brief by the answer key: does the person fit, and does a document say so?
"ada": {"expected": "match", "label": "match"},
"bela": {"expected": "match", "label": "match"},
"cato": {"expected": "undetermined", "label": "match"}, # fits; no document says so
"dov": {"expected": "non_match", "label": "near_miss"}, # one condition fails
"edda": {"expected": "non_match", "label": "unrelated"},
}
search = score_matches(["ada", "dov"], key)
print("search: returned", search["returned"], "· right", search["right"], "· wrong",
search["wrong"], search["wrong_ids"], "· found", search["found"], "of",
search["expected"], "· missed", search["missed"])
vector = score_passages(["dov", "ada", "dov", "edda", "zed"], key, k=5)
print("vector search:", vector["passages"], "passages of", vector["profiles"],
"profiles ·", vector["fit"], "fit ·", vector["do_not_fit"], "do not", vector["labels"],
"· found", vector["found"], "of", vector["expected"])
search: returned 2 · right 1 · wrong 1 ['dov'] · found 1 of 2 · missed ['bela']
vector search: 5 passages of 4 profiles · 1 fit · 3 do not {'match': 1, 'near_miss': 1, 'outside_the_brief': 1, 'unrelated': 1} · found 1 of 2
Three things keep the board honest. It prints the two answers that need no reading beside every figure — return everyone (finds all, wrong about the rest) and return nobody (never wrong). It is generous to the vector search on purpose: a profile counts as found when ANY passage of it is in the top ten, whether or not that passage states the niche. And it refuses a run that lists only part of its matches, and reports a read that did not finish as not finished — a score of half an answer is a score of something else. What it does not check is WHY a person was returned: the quotes are held by the reader's own checks, not by this board.