docling-studio/experiments/reasoning-trace/README.md
Pier-Jean Malandrino 8103460e9c feat(reasoning): reasoning-trace viewer v1 with SQLite-backed graph
Adds the `docling-agent` reasoning-trace viewer as a Studio tunnel, per
`docs/design/reasoning-trace.md`. Users pick an analyzed document, import
a RAGResult JSON, and the iterations are overlaid on the document graph.

Graph source is decoupled from Neo4j: a new pure builder
(`infra/docling_graph.build_graph_payload`) reads `document_json` from
SQLite and emits the same Cytoscape-shaped payload that `fetch_graph`
returns from Neo4j. Neo4j stays exclusive to the Maintain ingestion
pipeline. Shared DoclingDocument helpers live in `infra/docling_tree.py`
so TreeWriter and the builder can't drift on label taxonomy or tree walks.

Also removes the Cytoscape minimap (cytoscape-navigator) from GraphView:
second render instance hurt perf on large documents for no UX win.

Backend
- new `GET /api/documents/:id/reasoning-graph` (SQLite-only)
- new `infra/docling_tree.py`, `infra/docling_graph.py`
- `analysis_repo.find_latest_completed_by_document`
- tests: `test_docling_graph.py` (builder), `test_graph_api.py` (endpoint)

Frontend
- `features/reasoning/` — store, overlay, types, panel, import dialog,
  workspace, doc picker
- new `ReasoningPage` + `/reasoning` and `/reasoning/:docId` routes
- `GraphView` gains a `fetcher` prop so reasoning can inject the
  SQLite-backed fetcher while Maintain keeps using the Neo4j one
- drops minimap (nav container, dep, CSS)
- legend filters + section parenting extracted for reuse
- i18n base strings (FR + EN)
2026-04-29 14:00:00 +02:00

4.1 KiB
Raw Permalink Blame History

Reasoning Trace — R&D sandbox

Goal: run docling-agent's RAG loop against a document already ingested in Docling-Studio, capture the RAGResult (per-iteration reasoning trace), and inspect what the agent does.

Fully isolated from the Studio backend: no deps added to document-parser/, no services modified. Just a script + uv inline deps.


What it does

  1. Reads the pre-parsed DoclingDocument directly from Studio's SQLite (analysis_jobs.document_json) — no PDF re-conversion.
  2. Instantiates DoclingRAGAgent against a local Ollama model.
  3. Calls agent._rag_loop() directly (the public .run() method discards the RAGResult; we need the iterations to see the reasoning trace).
  4. Dumps the full RAGResult as JSON to output/.

Prerequisites

1. Ollama running

# If not already running as a service:
ollama serve          # in another terminal

2. A model pulled

Recommended (Peter Staar's default, ~3B params, good JSON adherence):

ollama pull granite4:micro-h

Alternative already on your machine (2 GB, may struggle with strict JSON rejection sampling):

llama3.2:3b

Bigger/more reliable but slower (20B):

ollama pull gpt-oss:20b

3. Pick an analysis job id

Any COMPLETED row from analysis_jobs with a non-null document_json:

sqlite3 document-parser/data/docling_studio.db \
  "SELECT aj.id, d.filename, length(aj.document_json)
   FROM analysis_jobs aj JOIN documents d ON d.id=aj.document_id
   WHERE aj.document_json IS NOT NULL AND aj.status='COMPLETED'
   ORDER BY length(aj.document_json) DESC LIMIT 5;"

On this machine, the biggest one right now is:

722d5631-0089-44a3-a64a-7ce5b99579d3  — CCI - Conférence IA - Offre Commerciale v1.0

Run

uv run experiments/reasoning-trace/inspect_doc.py \
    --job-id 722d5631-0089-44a3-a64a-7ce5b99579d3 \
    --query "Quels sont les livrables principaux proposés ?" \
    --model granite4:micro-h

Flags:

  • --job-id — required, analysis_jobs.id
  • --query — required, the question
  • --model — either a mellea catalog constant (IBM_GRANITE_4_HYBRID_MICRO) or a raw Ollama tag (granite4:micro-h, llama3.2:3b). Default: granite4:micro-h.
  • --max-iters — default 5 (agent's own default)
  • --quiet — disable the rich panels during the loop

First run will take ~12 min: uv solves the docling-agent env (pulls docling-core, mellea, pydantic, rich, …) into a cached virtualenv. Subsequent runs are instant.


Output

experiments/reasoning-trace/output/<job-id-prefix>_<utc>.json

Schema:

{
  "job_id": "…",
  "filename": "…",
  "query": "…",
  "model": { "ollama_name": "…", "hf_model_name": "…" },
  "max_iterations": 5,
  "result": {
    "answer": "…",
    "converged": true,
    "iterations": [
      { "iteration": 1, "section_ref": "#/texts/3",
        "reason": "…", "section_text_length": 412,
        "can_answer": false, "response": "…" },
      
    ]
  }
}

This is the artifact the v1 Studio endpoint (POST /api/rag/inspect) will import — so anything that works here should work there.


Things to check on first run

  • Do we actually get a trace? iterations list should have ≥ 1 entries (empty means "no section headers found" fallback — bad sign for the viz idea).
  • Are section_ref values #/texts/N paths or #/groups/N? Determines how the resolver walks the tree.
  • Reasoning quality: does reason actually explain the pick, or is it LLM filler? That affects whether the trace is worth surfacing visually.
  • Convergence rate: with max_iters=5, does a small model converge at all, or hit the cap and return a partial answer?
  • Latency: per-iteration wall-clock on your M-series machine with granite4.

Next step (if the above looks promising)

Resolve each iteration.section_ref(page_no, bbox) using the same DoclingDocument that was loaded here. That's the reasoning_service.py resolver described in docs/design/reasoning-trace.md §3.2 — implement it in a second script here (resolve_trace.py) before touching Studio.