Five stages.
Every one swappable.
Quire is a pipeline, not a framework. Each stage is an entry point you can replace without forking, and the default for each is the boring choice that survived benchmarking.
The stage nobody writes blog posts about.
Roughly 60% of the parsing code exists because of PDFs, and most of the rest exists because of spreadsheets where the real data starts at row 14.
- Layout-aware PDF extraction: merged cells, multi-row headers, rotated tables
- OCR fallback for scans and screenshots, via Tesseract or a vision model
- Markdown, HTML, docx, xlsx, pptx, epub, mbox, plus 30 community formats
- Code-aware chunking that respects function and class boundaries
- Parsers register through
quire.parsersentry points. Adding one is a small file
# pyproject.toml of your plugin
[project.entry-points."quire.parsers"]
jupyter = "my_plugin:NotebookParser"
# my_plugin.py
class NotebookParser(Parser):
extensions = {".ipynb"}
def parse(self, path) -> Iterable[Span]:
nb = json.loads(path.read_text())
for i, cell in enumerate(nb["cells"]):
yield Span(
text="".join(cell["source"]),
locator=f"cell {i}",
kind=cell["cell_type"],
)
Why hybrid, still
Pure vector search fails on exact tokens: error codes, ticket numbers, a person's surname. Pure lexical fails on paraphrase. Fusing them costs about 40ms and removes an entire class of "why didn't it find that" issues.
Supersession is a retrieval problem
Quire builds a document graph from links, filenames, frontmatter and git history, so a policy that was replaced ranks below the one replacing it, unless you ask a question about history, detected separately.
SQLite until it hurts.
The default store is a single file next to your documents. It handles about 500k documents on a laptop, more than most people have, and it means there's no service to stand up before you can try the tool.
- Dense vectors plus BM25, fused with reciprocal rank fusion
- Cross-encoder rerank on the top 50, locally by default
- Document graph for supersession, authorship and thread context
- Postgres with pgvector for anything larger: one config line
- Index format is documented and stable; it's just SQL you can query
Generation writes. Verification decides.
These want different objectives, so Quire runs them as different models. A big model drafts; a small cheap one checks each claim against its span and never sees the question. Given the question it starts reasoning about plausibility instead of support.
- Claims decomposed to atomic statements before checking
- Verifier sees the span and the claim, nothing else
- Explicit numeric rules: "roughly 340" entails "340" one way only
- Conflicts surfaced, not resolved: you get both numbers
- Every stage logged to a trace you can replay with
quire trace
for claim in decompose(draft):
span = citations[claim.cite_id]
if not entails(span, claim):
draft.drop(claim) # quietly, always
if not draft.claims:
raise InsufficientEvidence(
nearest=retriever.top(3),
)
What this buys, measured
| Metric | Off | On |
|---|---|---|
| Claim attributable to its span | 71% | 99.2% |
| Correct refusal when absent | 12% | 94% |
| Recall on answerable questions | 100% | 98.8% |
| Added latency | — | +180ms |
Fictional figures, illustrating the tradeoff: verification costs a little recall and buys a lot of trust.
A cron job with a good memory.
An agent is a YAML file: an instruction, a schedule or trigger, and somewhere to put the output. No canvas, no node graph, no orchestration runtime.
name: weekly-changes
schedule: "0 7 * * 1"
ask: |
What changed in the RFCs in the last 7 days?
Cite everything. Max 400 words.
sources: ["docs/rfc/**"]
output:
to: "stdout" # or file, webhook, slack
strict: true # no output beats a wrong one
Agents never act on their own
The only outputs are stdout, a file, a webhook you configured, or an exit code. There is no browser, no shell access, no "do the thing" step. If you want it to open a pull request, you pipe it into something that does, visibly, in your own CI.
They run where you run them
No scheduler service, no hosted runner. quire agent run inside a GitHub Action, a systemd timer, or a laptop crontab. The state is a file in .quire/.
Read the source instead.
The pipeline is about 4,000 lines. Everything on this page lives in quire/pipeline/, and the benchmarks justifying each default are in bench/.