Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 49 additions & 119 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,147 +1,77 @@
# docket
# Docket

Research assistant over SEC EDGAR filings: retrieval, rerank, embeddings and
grounded generation all live in the **mettle** engine, consumed over its
OpenAI-compatible `/v1` HTTP API; docket owns the UI, the agent, the lineage
and the trust layer.
A self-hosted research assistant for SEC filings. Drop in 10-Ks and 10-Qs, ask questions in plain English, and get answers that cite the exact filing and page they came from. Everything runs on your own machine with a small local model, so no filing or question ever leaves it.

Self-contained. The release wheel ships the dashboard; no Node, no external
services required to start. The only supported engine is **mettle** — any
server that speaks the same OpenAI-compatible `/v1` contract (chat with
per-token logprobs, embeddings, rerank, collections) is a drop-in backend.
![Asking Docket about AMD's 2022 revenue](assets/screenshots/ask.png)

## Install
## What it does

Linux and macOS:
- **Answers with receipts.** Every answer points back to the filing and page it was drawn from, so you can check it in seconds.
- **Works across your whole coverage list.** Ask about one company or compare several; Docket searches every filing you have added.
- **Shows its work.** Each answer keeps a full trace: what was searched, which passages were picked, and the exact prompt the model saw.
- **Runs locally.** Retrieval and generation run on [mettle](https://github.com/adityam23/mettle), a local inference engine that fits on a 6 GB GPU.

```sh
curl -fsSL https://raw.githubusercontent.com/adityam23/docket/main/install.sh -o install.sh
sh install.sh
## How it works

```mermaid
flowchart LR
A[Filing PDFs] -->|extract, chunk| B[Docket]
B -->|index| C[(mettle engine<br/>search + rerank)]
Q[Your question] --> B
B -->|find best passages| C
C -->|top passages| B
B -->|grounded prompt| D[Local model]
D --> E[Answer with<br/>filing + page citations]
```

The installer downloads the latest release wheel, installs it as an isolated
[uv tool](https://docs.astral.sh/uv/concepts/tools/), and puts `dk` on your
PATH. State lands in XDG dirs (`~/.local/share/docket`, `~/.config/docket`).
Docket handles the app side: the dashboard, ingesting filings, the question-answering agent and the citations. The [mettle](https://github.com/adityam23/mettle) engine does the heavy lifting: embeddings, search, reranking and the language model itself.

Optional local engine:
## Getting started

```sh
sh install.sh --with-engine /path/to/mettle
```
You need Python 3.11+, [uv](https://docs.astral.sh/uv/), an NVIDIA GPU with about 6 GB of memory, and a built [mettle](https://github.com/adityam23/mettle) engine.

This copies the binary next to `dk` as `docket-engine` and records it in the
manifest. It is not started or configured for you.
```sh
git clone https://github.com/adityam23/docket && cd docket
uv sync --extra ingest --extra agent

Uninstall:
# Let Docket start and manage the engine for you
export DK_ENGINE_BINARY=/path/to/mettled
export DK_METTLE_CONFIG=/path/to/mettle-config.toml

```sh
curl -fsSL https://raw.githubusercontent.com/adityam23/docket/main/uninstall.sh -o uninstall.sh
sh uninstall.sh # add --data to also delete the ingested index
uv run dk serve
```

## Desktop app (AppImage / DMG)
Click-to-run builds are produced by CI (`.github/workflows/release-desktop.yml`,
on version tags): a thin Tauri shell spawns the frozen `docket-server` sidecar
on a free port, points it at the bundled `mettled` engine, and opens the OS
webview at the dashboard. Model weights are downloaded by the engine on first
run (mettle's own first-run flow). Artifacts are **unsigned** by design:
macOS shows a one-time right-click → Open warning; Linux AppImages need
`libfuse2` (or run with `--appimage-extract`) and the system
`webkit2gtk-4.1` webview.
Open http://127.0.0.1:8760, go to **filings** and add some PDFs, then ask away on the **ask** page. If you already run mettle yourself, set `DK_BACKEND_URL` to its `/v1` address instead of the two engine variables.

Settings can also go in a `.env` file in the project folder.

Local Linux build (everything except the engine binary, which you point at via
`DK_ENGINE_BINARY`):
### From the terminal

These talk to the engine directly, so keep `dk serve` (or your own mettle) running.

```sh
packaging/build-sidecar.sh # PyInstaller sidecar → dist/docket-server/
cp -a dist/docket-server desktop/src-tauri/resources/server/
mkdir -p desktop/src-tauri/resources/engine # put (or symlink) a mettled binary here
cd desktop/src-tauri && npx --yes @tauri-apps/cli build
uv run dk ingest ~/filings # add every PDF in a folder
uv run dk ask "What was AMD's revenue in 2022?"
uv run dk docs # list what's indexed
```

The CLI/uv-tool install below remains the developer path.
## A quick tour

## Quickstart (dev)
**Home:** what's indexed and whether the engine is up.

```sh
git clone https://github.com/adityam23/docket && cd docket
uv sync # base deps only
uv run dk serve # dashboard + API on http://127.0.0.1:8760
uv run dk chat "hello" # one-shot generation smoke test
uv run pytest # offline suite, no backend needed
```
![Home](assets/screenshots/home.png)

Extras: `uv sync --extra ingest` (PDF text extraction), `--extra agent`
(LangGraph). The dashboard is a SvelteKit (Svelte 5) SPA: rebuild it with
`cd docket/web/frontend && npm ci && npm run build`; releases
ship it prebuilt.

## Configuration

Every knob is a `DK_`-prefixed env var or a line in `.env`. All retrieval
capability is engine-side: the engine serves chat with per-token logprobs,
embeddings, `/v1/rerank` and the collections surface on ONE base URL — there is
no separate embed or rerank endpoint. If the engine is unreachable, docket
features fail loudly (no silent local fallback).

| Variable | Purpose |
|---|---|
| `DK_BACKEND_URL` | `/v1` base of the mettle engine. Default `http://127.0.0.1:11434/v1` |
| `DK_CHAT_MODEL` | Chat model id (or `role:<kind>` resolved from the engine's active profile) |
| `DK_EMBED_MODEL` | Embedding model id for the offline `dk embed` path only; ingest POSTs chunks to the engine's collection, which embeds with its own default model |
| `DK_RERANK_MODEL` | Cross-encoder reranker id. Default `qwen3-reranker-0.6b:latest` |
| `DK_ENGINE_BINARY` | Path to the `mettled` binary; set to let docket supervise (spawn + crash-restart) it |
| `DK_METTLE_CONFIG` | Passed to the engine as `METTLE_CONFIG` when spawning |
| `DK_ENGINE_KEY` | Bearer for the engine-management surface, when the engine enforces auth |
| `DK_ENGINE_COLLECTION` | Collection name filings are ingested into |
**Filings:** add PDFs and watch each one get indexed.

## How it works
![Filings](assets/screenshots/filings.png)

```
ingest: pdf -> ocr -> chunk -> POST chunks to the engine collection
answer: query -> engine search (dense/keyword/hybrid) -> cross-encoder rerank
-> grounded LLM hops -> cited answer + per-token confidence label
```
**Observability:** a trace for every answer, step by step.

![Observability](assets/screenshots/observability.png)

## Status

docket stores only chunk **lineage** locally (doc ids, pages, sources, filing
metadata) — chunk text and vectors live engine-side. Search, rerank, embed and
chat are engine capabilities reached through the one OpenAI-`/v1` client; the
offline test suite runs the full pipeline against a fake provider.

## Benchmarks

FinanceBench, 150 questions, production grounded prompt, k=10 context. The
full-RAG accuracy runs used `embed-gemma` + `qwen3-reranker-0.6b`; the shipped
default embedder is now `qwen3-embedding-0.6b` (higher recall@10 below), and the
full-RAG numbers have not yet been re-measured under it. They were also
measured on the direct grounded-QA path, before the aggregate router was part
of the harness; the harness now runs the production `answer()` path and a
re-run is pending. Two metrics,
never mixed: **answer accuracy** (numeric match within tolerance for metrics
questions, Claude-judged equivalence for free-form ones) and **gold-evidence
recall@k** (token overlap of retrieved chunks vs the gold evidence span).
Reproduction commands:
[`benchmarks/financebench/README.md`](benchmarks/financebench/README.md).

Answer accuracy:

| Generator | Oracle evidence | Full RAG @6 | Full RAG @10 (shipped) |
|---|---|---|---|
| Qwen3.5-4B Opus-distill (default chat model) | 80.7% | 68.0% | **82.0%** |
| gemma4:e2b | 55.3% | 40.0% | — |

Retrieval recall@6 / recall@10 at threshold 0.5 (36,920 chunks):

| Embedder | no reranker | + bge-reranker-v2-m3 | + qwen3-reranker-0.6b |
|---|---|---|---|
| embed-gemma (768d) | 0.527 / 0.613 | 0.773 / 0.840 | **0.840 / 0.900** |
| bge-m3 (1024d) | 0.513 / 0.600 | 0.747 / 0.807 | 0.793 / 0.847 |
| qwen3-embedding (1024d) | 0.540 / 0.667 | 0.793 / 0.860 | **0.840 / 0.913** |

Raising the context from 6 to 10 chunks recovers +14 pt of answer accuracy,
closing the gap to the oracle ceiling; at k=10 retrieval is no longer the
distill's bottleneck. The cross-encoder reranker remains the dominant retrieval
lever (+31 pts recall@6 over the fused order).
Docket is an early, actively developed project. Next on the list are a per-answer confidence signal and an interpretability "lab mode" that shows which parts of the model's internals drove an answer.

## License

Expand Down
Binary file added assets/screenshots/ask.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshots/filings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshots/home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshots/observability.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ fn main() {
.parse()
.expect("localhost URL");
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::External(url))
.title("docket")
.title("Docket")
.inner_size(1280.0, 860.0)
.build()?;
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "docket",
"productName": "Docket",
"version": "0.2.0",
"identifier": "app.docket.desktop",
"build": {
Expand Down
2 changes: 1 addition & 1 deletion docket/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
app = typer.Typer(
add_completion=False,
pretty_exceptions_enable=False, # clean one-line errors, no rich traceback
help="docket — self-hostable agentic document engine",
help="Docket — a self-hosted research assistant for SEC filings: answers cited to the exact filing page, from a local model.",
)


Expand Down
2 changes: 1 addition & 1 deletion docket/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
_MAX_FILE_BYTES = 50 * 1024 * 1024
_MAX_UPLOAD_BYTES = 200 * 1024 * 1024

app = FastAPI(title="docket", version="0.2.0")
app = FastAPI(title="Docket", version="0.2.0")


# --- Host/Origin protection (DNS rebinding + CSRF) ---------------------------
Expand Down
8 changes: 8 additions & 0 deletions docket/web/frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ code, .mono { font-family: var(--mono); }
}
.page-sub { color: var(--text-dim); margin: 0; max-width: 68ch; font-size: var(--fs-normal); }

/* Numbered citation pill — answers, the Explain drawer's source list. */
.cite-pill {
display: inline-grid; place-items: center; min-width: 19px; height: 19px; padding: 0 4px;
background: var(--accent-soft); color: var(--accent-bright); border-radius: 2px;
font: 600 0.72rem/1 var(--mono); vertical-align: baseline; margin: 0 1px;
cursor: default;
}

/* ALL-CAPS small utility label — the counterpoint to large lowercase display. */
.caps {
font-size: 0.72rem;
Expand Down
4 changes: 2 additions & 2 deletions docket/web/frontend/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<meta charset="utf-8" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%236d7cff'/%3E%3Cpath d='M9 21l4-10h2l4 10h-2.2l-.9-2.4h-3.9L11.2 21H9zm3.6-4.2h2.8L14 12.9l-1.4 3.9z' fill='white'/%3E%3C/svg%3E" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="docket — self-hostable agentic document engine" />
<title>docket</title>
<meta name="description" content="Docket — a self-hosted research assistant for SEC filings: answers cited to the exact filing page, from a local model." />
<title>Docket</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
Expand Down
35 changes: 35 additions & 0 deletions docket/web/frontend/src/lib/components/Answer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script>
// A model answer rendered as formatted, citation-pilled text. Shared by the
// Ask view and the per-chat permalink so both read identically.
import { renderAnswer } from '$lib/markdown.js';

let { text = '' } = $props();
</script>

<!-- renderAnswer escapes all model text before adding its fixed tag set. -->
<div class="answer">{@html renderAnswer(text)}</div>

<style>
.answer { font-size: var(--fs-normal); line-height: 1.7; max-width: 78ch; }
.answer :global(p) { margin: 0 0 0.8em; }
.answer :global(h4) {
margin: 1.1em 0 0.4em; font-size: 0.78rem; font-weight: 600;
letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-mute);
}
.answer :global(h4:first-child) { margin-top: 0; }
.answer :global(strong) { color: var(--text); font-weight: 600; }
.answer :global(code) { font: 0.88em var(--mono); }
.answer :global(blockquote) {
margin: 0 0 0.8em; padding: 2px 0 2px 14px;
border-left: 2px solid var(--accent); color: var(--text-mute);
}
.answer :global(ul), .answer :global(ol) { margin: 0 0 0.8em; padding-left: 1.3em; }
.answer :global(table) {
border-collapse: collapse; margin: 0 0 0.9em; font: 0.85rem/1.5 var(--mono);
}
.answer :global(th), .answer :global(td) {
padding: 5px 14px 5px 0; text-align: left; border-bottom: 1px solid var(--line);
}
.answer :global(th) { color: var(--text-mute); font-weight: 600; }
@media (max-width: 720px) { .answer { max-width: 100%; } }
</style>
6 changes: 1 addition & 5 deletions docket/web/frontend/src/lib/components/ExplainDrawer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,6 @@
.source { display: flex; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--line); }
.source:last-child { border-bottom: none; }
.quote { color: var(--text-dim); font-size: 0.85rem; margin-top: 3px; }
.cite-pill {
display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 5px;
background: var(--accent-soft); color: var(--accent-bright);
font: 600 0.74rem/1 var(--mono); flex-shrink: 0;
}
.cite-pill { flex-shrink: 0; }
.hits { display: flex; flex-direction: column; gap: 8px; }
</style>
4 changes: 2 additions & 2 deletions docket/web/frontend/src/lib/components/TopNav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@

<header class="topnav">
<div class="bar">
<a href="/" class="brand caps">agent·assistant</a>
<a href="/" class="brand">Docket</a>

<nav class="pivots" aria-label="Primary">
{#each nav as item}
Expand Down Expand Up @@ -107,7 +107,7 @@
height: 58px; display: flex; align-items: center; gap: 34px;
}
.brand {
color: var(--text); letter-spacing: 0.14em; font-size: 0.72rem;
color: var(--text); font-size: 1.05rem; font-weight: 600;
white-space: nowrap;
}

Expand Down
Loading
Loading