-
Notifications
You must be signed in to change notification settings - Fork 0
Intelligence Layer
Design doc. Describes Gradient's "intelligence layer" — the chain that turns raw study material (PDF pages, captured questions) into calibrated, outline-grounded tags and grounded atomic facts. This is the load-bearing half of the PKM loop (SPEC §G).
SPEC.md governs; this page is the longer-form narrative the spec invariants
(V-L*, V-E*, V-KB*, V69) compress. Where a claim has an invariant, it's cited.
The tagging engine is a single-user reimplementation of LLM4Tag (Tang et al., LLM4Tag: Automatic Tagging System for Information Retrieval via Large Language Models, KDD '25, arXiv:2502.13481). The paper's thesis drives the whole design:
An embedder alone is a weak tagger; an embedder as recall plus an LLM as judge is state-of-the-art.
In the paper's Table 1, pure embedding models (BGE/GTE/CONAN, picking tags by cosine) are the bottom rows; LLM4Tag beats them by 25–67%. So the embedder is demoted to retrieval and the LLM makes the actual decision. Gradient inherits this division of labour and the paper's three-module shape: recall → knowledge-enhanced generation → confidence calibration.
Two upstream jobs manufacture content; three downstream stages tag it. All
stages run as scheduled, async jobs (app/scheduler.py) — never on a request
path — which is why they default to the Flex processing tier (V-L5).
PDF inbox ──poll_inbox──► ingest_pdf (content creation)
│ render page → image (PyMuPDF)
│ vision transcribe → gpt-5.4-mini (V-KB3)
│ extract atomic facts → gpt-5.4-nano (V-KB4, structured)
▼
atomic_facts (node_id = NULL)
run_embed_job ────────────► embed_pending (the ENCODER step)
│ outline_node → embed >>-path (B, V-L6)
│ atomic_fact / question → embed text
▼
content_embeddings (text-embedding-3-small, dim 1536, V-E1)
run_grounded_tag_job ─────► tag_pending → _tag_one (LLM4Tag proper)
│ 1. recall → retrieve_candidates (V-L3 / V-L6)
│ 2. generate → gpt-5.4-nano picks from candidates (V44/V45)
│ 3. calibrate→ gpt-5.4-nano (reasoning off) Yes/No logprobs (V69)
▼
atomic_fact_tags / question_tags + denormalized node_id (V-T2/V-T3)
(downstream) Notion write-out: one page per node, facts as blocks (V-N1/V-N2)
Key seam: PDF ingest is not part of LLM4Tag — it produces the content vertices the paper assumes already exist. Content quality therefore propagates down the entire chain: a less-lossy fact → a better fact embedding → better recall → a better tag.
| Component | File | Role |
|---|---|---|
| PDF ingest | app/services/kb/pdf_ingest.py |
render → vision-transcribe → extract atomic facts. Creates content. (V-KB3/V-KB4) |
| Inbox poller | app/services/kb/inbox.py |
<slug>/*.pdf → course → ingest_pdf, V41 per-file isolation |
| Embedder |
app/services/kb/embeddings.py + embed_pending
|
the LLM4Tag encoder: vectorize nodes + content (V-E1) |
| Similarity edges | app/services/kb/similarity.py |
derive concept_edges.kind='similarity' (node↔node cosine, V-E2) |
| Recall | app/services/kb/recall.py |
build the constrained candidate set — C2T + C2C2T + T2T (V-L3/V-L6) |
| Generation | app/services/llm/grounded.py |
LLM picks from candidates, strict structured output (V44/V45) |
| Calibration | app/services/llm/calibrator.py |
Yes/No logprob grade per tag → confidence (V69) |
| Persist | app/services/kb/persist_tags.py |
write canonical tags, denormalize primary node_id (V-T2/V-T3) |
| LLM4Tag (paper) | Gradient |
|---|---|
Content vertex c
|
atomic_fact / question
|
Tag vertex t / tag repository |
outline_node / outline_nodes tree |
Deterministic edge c–t
|
tag rows source ∈ {manual, schema_map} → node_id (V-T2) |
| Similarity edge (cosine, BGE) |
concept_edges.kind='similarity' (V-E2) |
| Encoder (BGE), Eq 2 | text-embedding-3-small → content_embeddings |
| C2T meta-path |
_embedding_candidates (fact → outline-node cosine) |
| C2C2T meta-path |
_content_candidates (fact → similar tagged fact → its tag) — V-L6 |
| Candidate tag set Φ(c) |
RecallResult.candidates, capped fan-out |
| Knowledge-enhanced generation |
generate_grounded_tags (nano picks from Φ) |
| SRKI (retrieved ICL) | few-shot exemplars from prior calibrated tags + C2C2T neighbours |
| LSKI (SFT fine-tune) | not implemented — see Divergences |
| Confidence calibration (Eq 10) |
calibrate_tag — V69, the same formula |
| Drop tags < 0.5 | V-T3: < 0.5 ⇒ manual_review (surfaced, not dropped) |
The candidate set is the recall ceiling: a node never recalled can never be
tagged (the paper's HR#k metric, Table 2). Everything downstream — model tier,
calibration — can only reject a bad candidate, never add a missing one. So
recall completeness is the highest-leverage knob. retrieve_candidates merges
three meta-paths, deduped by node (V-L3, fixes in V-L6):
-
C2T — cosine of the target's vector against outline-node vectors in the
course. Floored at
min_score(δ, provisional 0.25) so a "least-bad" non-match isn't handed up; capped attop_k. -
C2C2T (
_content_candidates) — the paper's hard-case rescuer. Cosine the target against other already-taggedatomic_facts(content↔content, first hop), then borrow each neighbour's tag (second hop). This reaches nodes whose label doesn't lexically match the content but whose meaning does — the case C2T fails. The second hop is trust-weighted:-
gold (
source ∈ {manual, schema_map}) → weight 1.0,via='content-gold'. -
silver (
source='llm' ∧ ¬manual_review ∧ confidence ≥ δ_silver) →silver_factor(0.6),via='content-silver'. The discount + themanual_reviewexclusion damp the echo / feedback loop (yesterday's model guess propagating to similar new content). The calibrator (V69) remains a hard backstop — C2C2T only proposes.
-
gold (
-
T2T —
concept_edges.kind='similarity'neighbours of the C2T hits (tag → similar tag). Capped atedge_top_n(5).
Both fan-out caps (C2C2T ≤ 5, T2T ≤ 5) mirror the paper's meta-path caps so the candidate list can't balloon and dilute the LLM's pick.
Why node embeddings use the full path (B): a node is embedded as its
ancestor-qualified >> path (Metabolism >> Glycolysis >> Regulation), not the
bare leaf token. The cosine matches a fact's prose against the tag's
meaning, and the path carries far more of it than one word. Changing this
embed input bumped embedding_version to -v2 (V-E1: full re-embed).
The embedder turns each fact and each node into a 1536-dim vector. Cosine over those vectors does exactly two jobs: (a) C2T fact→node candidate recall, and (b) the C2C2T content↔content first hop. It does not assign tags — that is the LLM's job, with the calibrator scoring. The embedding choice matters not for its per-token price but because it sets the recall ceiling everything else is bounded by. Improving the embed input (B) typically beats upgrading the model tier.
After generation, each picked (content, tag) pair is re-graded by a discrete
Yes/No discriminator on a plain completion (not structured — the single
token must be readable), and confidence is read from token logprobs:
Conf = exp(L_yes) / (exp(L_yes) + exp(L_no))
< 0.5 ⇒ manual_review = true (V-T3) — surfaced for the single user to
curate, not silently dropped. The denormalized atomic_facts.node_id is set
only from the highest-confidence non-review pick; a low-confidence fact stays
NULL and is re-attempted next run.
The calibrator model must expose logprobs and run non-reasoning (V69). 5.4
models run with reasoning disabled satisfy this, so the calibrator is
gpt-5.4-nano (reasoning off) — same model as generation, different flag
(V-L5).
Per-task, pinned by capability and cost; all jobs async ⇒ Flex tier (~50% off Standard).
| Task | Model | Why |
|---|---|---|
| Vision PDF transcription | gpt-5.4-mini |
nano vision is weaker than even the retired gpt-5-mini; a bad transcription poisons every downstream fact/tag/embedding |
| Classification / extraction / grounded-tag / ranking | gpt-5.4-nano |
beats gpt-5-mini on text reasoning at ~4× lower cost; OpenAI's recommended tier for exactly these tasks |
| Calibration (logprobs Yes/No) |
gpt-5.4-nano, reasoning OFF |
exposes logprobs when non-reasoning (V69) |
| Embeddings |
text-embedding-3-small (1536) |
no 5.4-class embedding model; recall-ceiling input, not a price decision |
Prices (Standard /1M tok): mini $0.75 / $4.50, nano $0.20 / $1.25, both 400k context. Flex ≈ half. Any rotation re-triggers the V-L2 measurement harness.
- No LSKI / fine-tuning. The paper SFTs a 7B model biweekly on 10k labels, and its ablation says dropping LSKI hurts more than dropping SRKI. Gradient gives up the bigger lever — justified because (a) a single user has no 10k-label corpus and no fine-tune ops, and (b) the tag repository is one outline tree (hundreds–thousands of nodes), not the paper's millions, so recall is tractable without learned recall. The non-parametric half (SRKI) is kept.
-
manual_reviewinstead of drop. A single user can curate; the paper's hundreds-of-millions-of-users scale cannot, so it drops. - Calibrator not fine-tuned. Raw logprobs from the chosen model; fine at single-user scale.
-
pgvector ANN. All cosine is Python-side over JSONB vectors today. Fine at
one-outline scale; C2C2T's content↔content scan is the first thing that will
force the swap to pgvector
vector(N)+<=>operators (similarity.pynote, V-E1). - C2C2T is corpus-dependent. It contributes ~nothing on a cold KB and strengthens as content tags up — expect no day-one lift.
-
Edges aren't versioned.
concept_edgessimilarity rows carry noembedding_version; after a re-embed (e.g. the-v2bump) stale edges persist. Re-derive after a bump; consider stamping edges. -
Vision/extraction fusion (planned). A single mini vision call emitting
{transcription, facts}(image-grounded, less lossy) with prior-page context for cross-page facts — raises content quality and thus the recall ceiling. Amends V-KB3/V-KB4 when landed. -
Question tagging is single-course only. Atomic facts carry
course_id; questions are tagged only when exactly one course exists (recall needs a scope).
Reference
Features
Design Notes