<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Document Intelligence Lab]]></title><description><![CDATA[Explore open-source PDF extraction, evidence validation, hybrid RAG, local AI, Azure, and human review with Python, Java, and Next.js.]]></description><link>https://document-intelligence-lab.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ad5113d07dbc0e6a9a64665/b73f4dfd-a3fe-4c7b-adc8-9b30f0f5097a.png</url><title>Document Intelligence Lab</title><link>https://document-intelligence-lab.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 26 Oct 2026 03:33:36 GMT</lastBuildDate><atom:link href="https://document-intelligence-lab.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Document Intelligence Lab: From PDFs to Traceable AI Answers]]></title><description><![CDATA[Explore a Python PDF evidence library and a Java/Next.js workspace with Docling, hybrid RAG, local AI, Azure, human review, and DuckDB/dbt.]]></description><link>https://document-intelligence-lab.hashnode.dev/document-intelligence-lab-pdf-ai-rag-java-azure</link><guid isPermaLink="true">https://document-intelligence-lab.hashnode.dev/document-intelligence-lab-pdf-ai-rag-java-azure</guid><category><![CDATA[Java]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Viet Phung]]></dc:creator><pubDate>Fri, 23 Oct 2026 14:27:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/gql/6ad5113d07dbc0e6a9a64665/0753d9bc-c1bb-4ba5-b093-bc93dd07201e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Turning a PDF into text is useful. Turning it into an answer that someone can inspect, challenge, and trace back to the original page is a much more interesting engineering problem.</p>
<p>That is why I built <strong><a href="https://github.com/quocvietphung/document-intelligence-lab">Document Intelligence Lab</a></strong>: an MIT-licensed project with a standalone Python evidence library and a full research workspace for document extraction, hybrid retrieval, source-based AI answers, human review, and local analytical data pipelines.</p>
<p>The lab brings together <strong>Python, Java 21, Spring Boot, LangChain4j, Next.js, Docling, Ollama, Qwen3, Azure AI, PostgreSQL, Qdrant, Keycloak, Redis, DuckDB, and dbt</strong>. Each component has a specific responsibility. This article explains those responsibilities, how the pieces communicate, and where the current implementation still has limits.</p>
<blockquote>
<p><strong>The central design principle:</strong> keep the original document, extracted observations, generated answers, and human approval as separate things. A fluent answer is not automatically an approved fact.</p>
</blockquote>
<p><strong>Updated 24 October 2026.</strong> This walkthrough follows the public repository at <a href="https://github.com/quocvietphung/document-intelligence-lab/tree/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9">commit <code>c7e3fc5</code></a>. The standalone <a href="https://pypi.org/project/document-evidence-lab/0.1.0a1/">Python alpha <code>0.1.0a1</code></a> is published on PyPI; its immutable release source is tagged <a href="https://github.com/quocvietphung/document-intelligence-lab/tree/python-v0.1.0a1"><code>python-v0.1.0a1</code></a>, at commit <code>cf019b4</code>. The newer commit adds installation and release documentation. The full workspace uses local Docker; the Python library runs independently. Optional cloud integrations require their own setup and verification.</p>
<p><strong>What changed since the earlier article?</strong> There is now a published Python library and CLI, a lightweight authenticated SDK for the Java API, released fixes for Nova mode selection and local inference handling, and ETL v2 amount normalization. The sections below explain how these additions fit the architecture and which limits still apply.</p>
<h2>1. What does the project actually do?</h2>
<p>The workspace studies three document domains: payroll and banking references, insurance documents, and general business documents such as invoices and contracts.</p>
<p>A typical session follows this sequence:</p>
<ol>
<li>Sign in and select an authorized <strong>dossier</strong>, the application's container for related documents and evidence.</li>
<li>Upload a permitted PDF and explicitly start analysis.</li>
<li>Follow the durable processing job through scanning and extraction.</li>
<li>Open extracted fields and tables beside the original PDF.</li>
<li>Search the dossier's evidence or ask <strong>Nova</strong>, the assistant, a source-based question.</li>
<li>Inspect citations, report mistakes, and request human review.</li>
<li>Optionally create reference labels, compare research methods, or build private analytical layers.</li>
</ol>
<p>This is research software. It does not approve loans, settle insurance claims, transfer money, or automatically send the emails it drafts.</p>
<p>The interface organizes the work into seven destinations: <strong>Overview, Documents &amp; review, Data Pipelines, AI Studio, Research lab, Review &amp; trust, and Operations</strong>. English, German, and Vietnamese interfaces share the same source evidence.</p>
<p><img src="https://cdn.hashnode.com/uploads/gql/6ad5113d07dbc0e6a9a64665/6f261b59-a564-4584-97b6-072f899b0d6d.jpg" alt="The English Document Intelligence Lab overview with the configured Docling, MiniLM, Qdrant, and Ollama engines." /></p>
<p><em>An existing local workspace capture from the public repository, dated 21 October 2026. The displayed identity is a development fixture. Configuration labels and saved counts are not a live model-health or accuracy test.</em></p>
<h2>2. Start with Python—or choose the full workspace</h2>
<p>You no longer need to launch the entire application to explore the project's evidence contract. The repository now offers three entry points:</p>
<table>
<thead>
<tr>
<th>Entry point</th>
<th>What you get</th>
<th>What you need</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Standalone Python library</strong></td>
<td>Local PDF parsing, page/table evidence, deterministic validation, and a CLI</td>
<td>Python 3.10+; the optional Docling extra and prepared weights for parsing</td>
</tr>
<tr>
<td><strong>Full research workspace</strong></td>
<td>Dossiers, scanned upload workflows, Nova, hybrid RAG, human review, experiments, and analytical layers</td>
<td>Java, Next.js, Docker services, and Keycloak sign-in</td>
</tr>
<tr>
<td><strong>Python SDK</strong></td>
<td>Scripted access to protected workflows, batch receipts, trust information, and audit history</td>
<td>The lightweight Python package, an existing Java API, and an authorized access token</td>
</tr>
</tbody></table>
<p>The standalone library has no Java, Docker, Azure, login, or LLM dependency. Its base installation has <strong>no required third-party runtime dependencies</strong>; PDF conversion is an optional extra. The SDK is included in that lightweight package and contacts Java only when your script explicitly calls it.</p>
<h3>Install the published alpha</h3>
<p>Check that your selected interpreter is Python <strong>3.10 or newer</strong>, then create an isolated environment:</p>
<pre><code class="language-bash">python3 -m venv .venv
source .venv/bin/activate
python -m pip install 'document-evidence-lab[docling]==0.1.0a1'
</code></pre>
<p>On Windows PowerShell, activate with <code>.venv\Scripts\Activate.ps1</code>. If you only need validation or API access, install <code>document-evidence-lab==0.1.0a1</code> without the extra. Version pinning makes the example reproducible while this API is still alpha.</p>
<p>The names differ deliberately: <strong><code>document-evidence-lab</code></strong> is the PyPI distribution, <strong><code>document_evidence</code></strong> is the Python import, and <strong><code>document-evidence</code></strong> is the terminal command. The <code>docling</code> extra pins Docling <code>2.142.1</code> and includes pypdf for PDF checks. Installing it does not itself authorize model downloads or start Nova.</p>
<h3>Convert a PDF into inspectable evidence</h3>
<pre><code class="language-python">from document_evidence import DoclingReader, validate_document

# First use: explicitly allow preparation of Docling's model weights.
document = DoclingReader(
    allow_model_download=True,
    timeout_seconds=900,
).read("invoice.pdf")

report = validate_document(document, required_labels=("Invoice no",))
print(document.pages[0].text)
print(document.tables)
print(report.to_dict())
</code></pre>
<p>The library wraps Docling's parser with a normalized evidence model: a <strong>SHA-256 document identity</strong>, page-bound source IDs, original strings, optional normalized bounding boxes, and table cells with row/column spans. Unknown confidence stays <code>None</code>; the wrapper does not manufacture a confidence score or human approval.</p>
<p>First preparation fetches layout, table, and OCR weights. A successful text-layer PDF alone would not establish that OCR weights are cached, so preparation includes the configured RapidOCR Torch backend. Later, <code>DoclingReader().read("invoice.pdf")</code> uses prepared weights with offline settings and a DNS/socket audit guard in the conversion child process. A separate directory can be supplied with <code>artifacts_path</code>. Missing weights fail; no cloud parser is substituted.</p>
<p>Conversion runs in a short-lived child process to bound its timeout and scope its environment. This is an application safeguard, <strong>not an OS security sandbox</strong>. Defaults are an unencrypted PDF, <strong>four pages, 3 MB, and 180 seconds</strong>; the explicit 900-second example gives initial preparation more time. Standalone parsing does not include the platform's ClamAV scanner, authentication, queue, or durable audit store. OCR language coverage and resource use still need testing on your documents.</p>
<p>The equivalent CLI is:</p>
<pre><code class="language-bash">document-evidence invoice.pdf --allow-model-download --timeout 900 --output private-result.json
</code></pre>
<p>The output contains extracted document text and validation findings. It stays a private artifact, and the command refuses to overwrite an existing file.</p>
<h3>Validate a contract without pretending to verify meaning</h3>
<p>These checks work with the lightweight installation, without Docling or a language model:</p>
<pre><code class="language-python">from document_evidence import normalize_money, validate_citations

money = normalize_money("2967,11 EUR")
assert str(money.amount) == "2967.11"
assert money.currency == "EUR"

answer = validate_citations(
    "Invoice 51109301 [document:p1].",
    ["document:p1"],
)
</code></pre>
<p>Money normalization supports EUR, INR, USD, and GBP. Unknown/mixed currencies and ambiguous separators are rejected as findings rather than guessed. Raw source strings remain unchanged. <code>validate_citations</code> checks the allowed source-ID contract; <code>check_evidence(document, source_id, quote, value="...")</code> checks exact quote/value containment. Neither proves that a statement has the right business meaning.</p>
<p><code>validate_document</code> checks source structure, empty pages, ambiguous numeric dates, table consistency, and explicitly requested literal labels. A clean report does not certify document authenticity, perform trained field classification, or approve a financial decision. Shared fixtures compare citation rules with Java and strict amount rules with the ETL worker; Java's permissive comparison helper is only checked on its documented supported subset.</p>
<h3>Use Python with an existing Java platform</h3>
<pre><code class="language-python">import os
from document_evidence.client import PlatformClient

client = PlatformClient(
    "http://localhost:8081",
    token=lambda: os.environ["LAB_ACCESS_TOKEN"],
)
case_id = os.environ["LAB_CASE_ID"]
workflow = client.workflow(case_id)
audit = client.audit(case_id)
</code></pre>
<p>Supply a <strong>Keycloak access token</strong> with the API audience and authorized tenant/roles. The SDK does not log in or refresh credentials automatically; a callback can provide a token refreshed by your approved client flow. Java remains responsible for authorization. Remote origins require HTTPS; redirects and environment proxy discovery are disabled to avoid forwarding bearer credentials through those paths.</p>
<p>Read methods include <code>list_cases</code>, <code>get_case</code>, <code>workflow</code>, <code>trust</code>, <code>audit</code>, and <code>batches</code>. An explicit <code>submit_pdf</code> call also exists: it requires consent and a stable UUID request ID before sending the original PDF to Java's scanned batch workflow. There is no automatic retry. For an uncertain submission, inspect the workflow and retain the same UUID, bytes, and metadata. An intake receipt does not mean extraction or review is complete.</p>
<p>Python conversion source IDs are not Java dossier IDs, and local validation results are not trusted platform approval assertions. The library and platform share some contracts while keeping their process, identity, and review boundaries explicit.</p>
<p><strong>Implementation:</strong> <a href="https://github.com/quocvietphung/document-intelligence-lab/tree/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/python">Python package and examples</a>, <a href="https://pypi.org/project/document-evidence-lab/0.1.0a1/">published release</a>.</p>
<h2>3. The architecture at a glance</h2>
<p><img src="https://cdn.hashnode.com/uploads/gql/6ad5113d07dbc0e6a9a64665/4112f648-527e-43fb-ae17-133c2fdc0911.png" alt="Architecture of Document Intelligence Lab: authenticated browser, Java application boundary, extraction, retrieval, generation, persistence, analytical layers, and human review." /></p>
<p><em>The full workspace path, illustrated in the earlier architecture schematic. The standalone Python path above is separate. This is a conceptual map of responsibilities rather than a literal network topology: application services mediate access, and local and Azure providers are explicitly selected alternatives.</em></p>
<p>The browser talks to an authenticated <strong>Next.js API proxy</strong>. The proxy forwards authorized requests to <strong>Spring Boot</strong>, where dossier access, document processing, retrieval, model calls, and review rules are enforced.</p>
<p><strong>PostgreSQL</strong> keeps application state. <strong>Qdrant</strong> supports vector retrieval. <strong>Redis</strong> adds optional short-window limits. <strong>Keycloak</strong> supplies user identity. Document parsers, the local language model, and the analytical worker run in separate containers.</p>
<h3>Process boundaries and job handoffs</h3>
<p>The business backend is a <strong>modular Spring Boot application</strong>: case management, extraction coordination, retrieval, research, and review are modules within one Java service. Parsers, the local model runtime, and the analytical worker have separate process/container boundaries. The diagram groups responsibilities rather than presenting every box as an independently deployed business microservice.</p>
<p>Document jobs are backed by <strong>PostgreSQL records and leases</strong>. The ETL worker receives a <strong>private, version-bound file handoff</strong> and writes a result receipt that Java reconciles against the current source version. This path does not require a public worker REST endpoint or unrestricted access to the application database.</p>
<p>The application also includes deterministic intent routing through <code>AgentSupervisor</code>. It selects a bounded specialist and evidence/tool policy. Local Nova receives supplied context; the Azure path can use a bounded read-only tool loop. Specialist labels do not mean that multiple independent autonomous agents are collaborating on each answer.</p>
<p>The system separates two different AI jobs:</p>
<ul>
<li><strong>Document extraction:</strong> reading text, layout, fields, and tables from the original.</li>
<li><strong>Language generation:</strong> explaining selected evidence or producing a draft answer.</li>
</ul>
<p>Changing the language model does not automatically improve OCR. Improving OCR does not automatically make an answer correct.</p>
<h3>The technology map</h3>
<table>
<thead>
<tr>
<th>Technology</th>
<th>Responsibility in this project</th>
</tr>
</thead>
<tbody><tr>
<td>Java 21 and Spring Boot</td>
<td>Application APIs, authorization, workers, domain rules, and provider integration</td>
</tr>
<tr>
<td>LangChain4j</td>
<td>Java integration with embedding and language-model components</td>
</tr>
<tr>
<td>Next.js, React, and TypeScript</td>
<td>The browser workspace and authenticated server-side proxy</td>
</tr>
<tr>
<td>Tailwind CSS and Lucide</td>
<td>Interface styling and icons</td>
</tr>
<tr>
<td>next-intl</td>
<td>Localized application routes and interface text</td>
</tr>
<tr>
<td>Zod and AI SDK</td>
<td>Request/response contracts and the chat UI message transport</td>
</tr>
<tr>
<td>PDF.js</td>
<td>Viewing protected originals inside the workspace</td>
</tr>
<tr>
<td>Docling, PDFBox, and Tesseract</td>
<td>Configurable local PDF extraction and OCR</td>
</tr>
<tr>
<td>Azure Document Intelligence</td>
<td>Explicit cloud layout and document extraction</td>
</tr>
<tr>
<td>MiniLM and ONNX</td>
<td>Local semantic text embeddings</td>
</tr>
<tr>
<td>Qdrant and BM25-style scoring</td>
<td>Semantic and keyword evidence retrieval</td>
</tr>
<tr>
<td>Ollama and Qwen3</td>
<td>Local Nova language generation</td>
</tr>
<tr>
<td>Azure OpenAI</td>
<td>Separately configured cloud generation</td>
</tr>
<tr>
<td>Keycloak, OIDC, and PKCE</td>
<td>User sign-in and identity tokens</td>
</tr>
<tr>
<td>PostgreSQL, JDBC, and Flyway</td>
<td>Persistent records, database access, and schema migrations</td>
</tr>
<tr>
<td>Redis and ClamAV</td>
<td>Shared usage limits and malware scanning</td>
</tr>
<tr>
<td>document-evidence-lab and Python</td>
<td>Standalone evidence conversion/validation, CLI, and an explicit Java platform SDK</td>
</tr>
<tr>
<td>Python, DuckDB, dbt, and Parquet</td>
<td>A separate offline analytical worker and portable data artifacts</td>
</tr>
<tr>
<td>Docker Compose</td>
<td>Reproducible service composition, networks, and persistent volumes</td>
</tr>
<tr>
<td>Micrometer and OpenTelemetry</td>
<td>Operational metrics and optional tracing</td>
</tr>
<tr>
<td>GitHub Actions, Vitest, and Java/Python tests</td>
<td>Automated verification and release checks</td>
</tr>
</tbody></table>
<p>The next sections explain the main paths in more detail.</p>
<h2>4. Reading PDFs: Docling, PDFBox, Tesseract, and Azure</h2>
<h3>Docling: structure matters as much as text</h3>
<p>A PDF may visually contain a clear table while storing its characters in an inconvenient order. Reading those characters alone can lose the relationship between a label, an amount, and its row or column.</p>
<p>The optional <strong>Docling CPU</strong> profile extracts text, page structure, and tables locally through Docling Serve. A Java adapter converts the provider response into the application's shared document representation.</p>
<p>The integration preserves page provenance and uses the existing queue, PDF viewer, and review process. The parser receives document bytes through its bounded interface; it does not need application credentials or arbitrary source URLs. The supplied local profile gives it no public host port or runtime network egress.</p>
<p>Unknown confidence remains unknown. A missing confidence value is not replaced with an impressive-looking score.</p>
<h3>PDFBox and Tesseract: a local baseline</h3>
<p><strong>Apache PDFBox</strong> provides PDF handling and the baseline local extraction path. <strong>Tesseract</strong> supplies OCR for image-based content in that path.</p>
<p>These tools provide a useful baseline because digital text extraction and scanned-page recognition have different failure modes. Keeping the baseline helps make provider comparisons concrete instead of assuming that a more complex model must always be better.</p>
<h3>Azure Document Intelligence: an explicitly selected cloud provider</h3>
<p>The Azure path uses <strong>Azure Document Intelligence</strong> with <code>prebuilt-layout</code>. It requires the operator's own resource configuration and authorized Azure access.</p>
<p>The application maps the result into the same internal evidence model used by local extraction. Reviewers can therefore inspect comparable concepts—pages, fields, tables, and provenance—without treating provider-specific output as approval.</p>
<p>Cloud analysis can incur charges and sends the selected document to Azure. It is an explicit processing choice. A local provider failure does not silently switch the document to Azure.</p>
<p>The extraction coordinator also supports an explicitly configured <code>azure-with-local-fallback</code> mode: selected Azure provider failures can use the local PDFBox/Tesseract path, while invalid-document responses are propagated. The supplied Azure walkthrough selects plain <code>azure</code>, rather than this fallback mode. This optional extraction behavior is separate from Nova chat, whose selected model does not fall back to another provider.</p>
<h3>From extracted text to business fields</h3>
<p>OCR and layout models recover text and document structure. Java then maps recognized labels, page layouts, and table relationships into a bounded domain schema. <code>DocumentExtractor</code>, <code>PayrollLayoutFields</code>, and <code>InvoiceTableFields</code> implement explicit application rules. These rules are inspectable, but they are not a fine-tuned payroll, insurance, or invoice model.</p>
<p>For example, recognizing the text beside <strong>Netto-Verdienst</strong> and deciding which canonical field should contain it are separate steps. Better character recognition cannot by itself correct a wrong field mapping. Conflicting candidates require inspection rather than selecting the most convenient value.</p>
<p><strong>Extraction is an observation stage.</strong> Even a well-structured result can attach the wrong amount to the right-looking field. The original remains the reference for review.</p>
<h2>5. Finding the right evidence: MiniLM, ONNX, Qdrant, and hybrid retrieval</h2>
<p>Retrieval-augmented generation, or <strong>RAG</strong>, supplies selected evidence to a language model before it answers. Its usefulness depends heavily on whether the selected evidence is relevant and properly scoped.</p>
<h3>MiniLM embeddings explain semantic similarity</h3>
<p>The local retrieval profile uses a quantized <strong>all-MiniLM-L6-v2</strong> embedding model through <strong>ONNX</strong>, producing <strong>384-dimensional</strong> vectors.</p>
<p>An embedding represents a piece of text numerically so that a question and a passage with related meaning can be compared. This is useful when the question's wording differs from the document's wording.</p>
<p>This model is English-focused. A multilingual interface does not automatically imply equally strong multilingual retrieval. The project includes a separate BGE-M3 adapter for experiments, discussed below.</p>
<h3>Qdrant stores vectors with access scope</h3>
<p><strong>Qdrant</strong> stores vector representations with metadata that includes the tenant, dossier, and chunk identifier. Searches include tenant and dossier filters.</p>
<p>These filters complement server-side authorization. The application must first establish that the user can access the dossier; a vector database filter is not a replacement for that decision.</p>
<p>Live retrieval can synchronize the selected dossier's index on demand. OCR completion and retrieval indexing remain separate stages. If Qdrant is unavailable, the implementation can report a local-vector retrieval fallback; this is different from switching to a cloud language model.</p>
<h3>Keyword retrieval still matters</h3>
<p>Document questions often depend on exact labels, identifiers, or unusual terminology. The application combines semantic similarity with a <strong>BM25-style lexical score</strong> and <strong>query-term coverage</strong>.</p>
<p>At this source snapshot, <code>RagEngine</code> uses the following hybrid heuristic:</p>
<pre><code class="language-text">hybrid_score =
    0.48 × max(0, cosine_similarity)
  + 0.32 × (lexical_score / (lexical_score + 2))
  + 0.20 × query_term_coverage
</code></pre>
<p>The lexical implementation uses token-set membership and document-frequency/length normalization. It is a compact application implementation, not a claim to reproduce every detail of a full-text search engine's BM25.</p>
<p>The weights are engineering choices. They are not a learned ranker or calibrated probabilities. A score of <code>0.8</code> does not mean an answer has an 80% chance of being correct.</p>
<h3>Chunking preserves the route back to the original</h3>
<p>The repository compares four page-local chunking strategies:</p>
<table>
<thead>
<tr>
<th>Strategy</th>
<th>What changes</th>
</tr>
</thead>
<tbody><tr>
<td>Fixed</td>
<td>Windows of 780 characters with 160-character overlap</td>
</tr>
<tr>
<td>Recursive / boundary-aware</td>
<td>Groups text around paragraph and sentence boundaries, splitting oversized units</td>
</tr>
<tr>
<td>Semantic</td>
<td>Uses embedding similarity to introduce additional boundaries</td>
</tr>
<tr>
<td>Hierarchical-child</td>
<td>Creates child chunks with parent labels</td>
</tr>
</tbody></table>
<p>These sizes are <strong>characters, not tokens</strong>. Chunking does not merge unrelated PDFs or page numbers. Hierarchical-child labels are implemented; parent-context expansion is not.</p>
<p>The application also represents extracted fields and tables as retrievable evidence. Reference experiments use an isolated corpus rather than overwriting the live dossier index.</p>
<p><strong>Code to inspect:</strong> <a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/services/credit-api/src/main/java/dev/creditlab/platform/RagEngine.java"><code>RagEngine.java</code></a> and <a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/services/credit-api/src/main/java/dev/creditlab/platform/DocumentChunking.java"><code>DocumentChunking.java</code></a>.</p>
<h2>6. Nova: LangChain4j, Ollama, Qwen3, and Azure OpenAI</h2>
<p><strong>Nova</strong> is the assistant inside the workspace. The interface exposes two modes:</p>
<ul>
<li><strong>App guidance</strong> explains the interface through local application logic without a language-model call.</li>
<li><strong>Evidence analysis</strong> is the route for source-based questions to the configured model with authorized dossier context.</li>
</ul>
<p><strong>Explicit mode routing:</strong> the released frontend now respects the selected mode. App guidance remains local guidance; an Evidence analysis question such as “What is the invoice number stated in this PDF?” is no longer diverted merely because it matches a guidance phrase and PDF alias. This corrects the routing limitation described in the earlier article. Provider provenance and a newly saved cited result still need inspection to establish what actually ran.</p>
<p><strong>Each model request is a fresh question against the selected dossier.</strong> The visible chat transcript is not supplied as conversational model memory. Follow-up questions should restate their subject. Retaining answer history for review and sending prior turns back to a model are separate design choices.</p>
<h3>LangChain4j is the Java integration layer</h3>
<p><strong>LangChain4j</strong> connects the Java application to model providers and embedding components. Authorization, evidence selection, quota checks, and review rules remain application responsibilities.</p>
<p>The local Nova path receives supplied authorized context and does not run autonomous tool loops. The separately configured Azure agent can use scoped, read-only tools. Those are different execution paths, not interchangeable capabilities.</p>
<h3>Ollama serves the local language model</h3>
<p>The supplied local walkthrough uses <strong>Ollama</strong> with the explicit model identifier:</p>
<pre><code class="language-text">qwen3:4b-instruct-2507-q4_K_M
</code></pre>
<p>This is a Qwen3 4B Instruct checkpoint with quantized weights. Quantization reduces weight precision to make local execution more practical, with quality and resource trade-offs that still need measurement. The generic <code>qwen3:4b</code> alias is not treated as an equivalent replacement in the project.</p>
<p>The CPU runtime image and the model weights are separate artifacts. The model is prepared explicitly and retained in a private Docker volume. Normal startup does not quietly download a missing model, and a local inference failure does not trigger Azure generation.</p>
<p>Local CPU inference can take minutes. The released Java wrapper admits <strong>one local model call at a time per API process</strong>, shared by Nova and prompt experiments. A competing request receives <code>429 LOCAL_MODEL_BUSY</code>; a provider timeout becomes <code>504 LOCAL_MODEL_TIMEOUT</code>. The local model budget is <strong>360 seconds</strong>, inside the web proxy's <strong>380-second</strong> deadline. The Azure proxy budget remains 45 seconds. The compact prompt preserves selected excerpts and citation IDs while removing repeated instructions. There is no automatic retry or cloud fallback, and stopping a browser request does not guarantee cancellation of every backend inference.</p>
<p>The <a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/README.md#walkthrough">current edited README walkthrough</a> documents a newly generated local invoice-number/currency answer; extraction and ETL receipts in that walkthrough were already saved, and waiting periods are omitted. The <a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/docs/AI_INTEGRATIONS.md#local-reliability-recheck--22-october-2026">dated local acceptance notes</a> report scoped successful checks and distinguish cold loading from warm requests. Earlier timeout observations remain part of the project's history. Neither an edited capture nor a larger timeout establishes general reliability or a latency benchmark.</p>
<h3>Azure OpenAI is a separate configured route</h3>
<p>The Azure route connects the Java application to the operator's own <strong>Azure OpenAI</strong> deployment. It requires explicit Azure configuration and can incur inference charges.</p>
<p>The lab uses <strong>Microsoft Entra</strong> authorization for Azure access. The local Azure CLI token bridge supplies short-lived developer credentials; it is not Managed Identity. A hosted deployment would need an independently designed and scoped identity configuration.</p>
<p>There is a provenance-label limitation in this snapshot: choosing the <code>default</code> credential configuration records the mode as <code>azure-managed-identity</code>, although <code>DefaultAzureCredential</code> is a credential chain and does not establish which identity actually succeeded. Treat that value as a configuration-derived label, not verified proof of Managed Identity.</p>
<p>Exactly one Nova chat provider is selected by configuration. Historical answers retain their original provider provenance instead of being relabeled after a configuration change.</p>
<h3>Structured output and citations constrain the answer</h3>
<p>Selected local review tasks use explicit <strong>JSON Schema</strong>, strict Java decoding, and checks against allowed source IDs and original quotations. Chronology, clause review, and sensitive-data categories are bounded tasks with specific evidence limits.</p>
<p>An invalid or incomplete draft can be rejected. The application should not fabricate a successful replacement to make the interface appear healthy.</p>
<p>However, three statements remain different:</p>
<ol>
<li>The JSON has the expected shape.</li>
<li>The cited source exists.</li>
<li>The claim is factually supported by that source.</li>
</ol>
<p>Passing the first two does not establish the third. Numeric checks can flag absent currency/amount pairs, but they do not fully establish the correct person, period, or financial meaning.</p>
<p>The stored source preview is also shorter than some model contexts: <code>AnswerEvidence</code> retains up to 1,000 characters per excerpt, and the downstream money check uses that excerpt. A supported amount later in a longer authorized context can therefore receive a conservative warning. Such a warning can reflect excerpt truncation rather than a fabricated amount; the original and the inspected scope still need review.</p>
<h2>7. The web workspace: Next.js, React, TypeScript, and PDF.js</h2>
<p>The frontend uses <strong>Next.js 16</strong> and <strong>React 19</strong>, with <strong>TypeScript</strong> for application contracts. The server-side API layer forwards authenticated requests to Java, applies body bounds and origin checks, and translates backend failures into controlled UI responses.</p>
<p><strong>Zod</strong> validates structures such as conversations and trust metadata. TypeScript helps during development; runtime validation is still needed for data arriving across a network boundary.</p>
<p><strong>Tailwind CSS</strong> provides the styling system, while <strong>Lucide</strong> supplies icons. <strong>next-intl</strong> supports localized routes and interface text. Source quotations, labels, currencies, and amounts are preserved rather than silently rewritten to match the UI language.</p>
<p><strong>PDF.js</strong> renders private original documents. Viewer assets are served locally, and document requests use authenticated endpoints. A source link can open the correct page without pretending that an exact text region was found when it was not.</p>
<p>The chat UI uses <strong>AI SDK</strong> message-stream primitives to deliver answer text, source references, and trust metadata. In this implementation, the proxy first waits for the Java response and then emits UI message events. It should not be described as token-by-token model streaming.</p>
<p>The frontend dependency list also includes <strong>Sharp</strong> for image processing, <strong>SWC</strong> helpers for the JavaScript toolchain, and <strong>PostCSS</strong> for CSS processing. These support the web application; they are not additional document AI engines. The release also refreshes web dependencies; its lockfile resolves Next.js <code>16.4.3</code>, Zod <code>4.6.7</code>, and Undici <code>7.30.1</code>. These are reproducible dependency versions, not a claim that every security risk has been eliminated.</p>
<h2>8. Identity and isolation: Keycloak, OIDC, PKCE, and Spring Security</h2>
<p><strong>Keycloak</strong> handles application sign-in using <strong>OpenID Connect</strong>, built on OAuth 2.0, with an authorization-code flow and <strong>PKCE</strong>. PKCE binds code redemption to the initiating client flow.</p>
<p>The Java API uses <strong>Spring Security</strong> as an OAuth2 resource server. It validates JWT issuer and API audience, then applies roles and tenant/dossier authorization.</p>
<p>The roles have different responsibilities. Advisors can work with dossiers in their authorized tenant; reviewers and administrators control designated review transitions. Access combines verified tenant membership, application roles, and tenant-bound dossier lookups. The lab does not implement a separate owner/member sharing list for each dossier. Guessing an ID does not grant cross-tenant access.</p>
<p>There are two separate identity systems:</p>
<table>
<thead>
<tr>
<th>Identity boundary</th>
<th>What it authorizes</th>
</tr>
</thead>
<tbody><tr>
<td>Keycloak</td>
<td>A user's access to the lab and its dossier-scoped operations</td>
</tr>
<tr>
<td>Microsoft Entra</td>
<td>The Java application's authorized calls to selected Azure resources</td>
</tr>
</tbody></table>
<p>Confusing these boundaries can produce both access failures and excessive permissions. An app role is not an Azure resource role.</p>
<p>In the supplied platform profile, <strong>ClamAV</strong> scans uploaded files before extraction and <strong>Redis</strong> adds short-window limits. An enabled Redis limiter fails closed if the service becomes unavailable. Bare-service defaults are different: without a configured ClamAV host the scanner reports structural-only checks, and without enabled Redis the application uses its database quotas. Configuration must be inspected rather than assuming every launch has the same controls.</p>
<p><strong>PostgreSQL atomically reserves account and tenant budgets</strong>, including per-minute and daily buckets. The reservation size counts Unicode characters from the question and selected context plus an allowance; it is not a provider-token meter or a monetary spending cap. Redis supplements these reservations rather than owning every budget. Request/page/file bounds, source checks, and suspicious-passage handling add further controls.</p>
<p>These controls address specific risks. Their presence is not a claim of complete protection against prompt injection or a production security certification.</p>
<h2>9. Durable processing: PostgreSQL, JDBC, Flyway, and workers</h2>
<p>A document-processing request should not depend on a browser tab staying open. The lab persists processing state and lets the UI follow the job.</p>
<p><strong>PostgreSQL</strong> stores dossiers, jobs, evidence, references, and review history. A separate PostgreSQL database holds Keycloak identity state. <strong>JDBC</strong> provides Java database access, and <strong>Flyway</strong> applies versioned schema migrations.</p>
<p>Workers use durable records and recovery behavior, with attempts and failure history available for inspection. Document stage/state events are recorded transactionally with the relevant job update. Saved extraction checkpoints can avoid unnecessary repeat OCR after a later-stage failure.</p>
<p>This is not an exactly-once billing guarantee: if a provider completes work but its response is lost before checkpointing, a subsequent attempt can still repeat inference.</p>
<p>Automatic retry is bounded to selected transient failures. Manual retry is deliberately restricted to approved, byte-hashed DATEV/Kaggle reference inputs and preserves the failed job. Arbitrary uploads use the normal consent and submission flow again.</p>
<p>The UI distinguishes <strong>processed</strong>, <strong>indexed</strong>, <strong>answered</strong>, and <strong>human-confirmed</strong>. A job marked ready for review is not equivalent to an approved document.</p>
<p>Retention also has multiple scopes. Expiring an original PDF does not automatically erase chat history, reference labels, analytical exports, or browser downloads. Each copy has to be understood as a separate data lifecycle.</p>
<h2>10. Local analytics: Python, DuckDB, dbt, Parquet, and SCD Type 2</h2>
<p>Document extraction becomes more useful when its observations can be studied as data. The lab includes an offline <strong>Python worker</strong> with <strong>DuckDB</strong>, <strong>dbt Core</strong>, and <strong>dbt-duckdb</strong>.</p>
<p><img src="https://cdn.hashnode.com/uploads/gql/6ad5113d07dbc0e6a9a64665/f7d109eb-111f-4338-a741-dc5a152c25a2.png" alt="Bronze, Silver, and Gold layers with source-version receipts, preserved raw values, human review, and SCD Type 2 history." /></p>
<h3>Why DuckDB and dbt?</h3>
<p><strong>DuckDB</strong> is an embedded analytical database: it can execute analytical SQL locally without adding another continuously exposed database service. <strong>dbt</strong> organizes SQL transformations and data tests, while the <strong>dbt-duckdb</strong> adapter executes them on DuckDB. No dbt Cloud account is required for this path.</p>
<p>The worker has no public port, network access, database password, or Azure credentials in the supplied profile. It processes an explicitly authorized, version-bound private handoff rather than receiving unrestricted application access.</p>
<h3>Bronze, Silver, and Gold have different promises</h3>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Responsibility</th>
</tr>
</thead>
<tbody><tr>
<td>Bronze</td>
<td>Retain immutable extraction snapshots, provenance, and available original hashes; references do not copy PDF bytes</td>
</tr>
<tr>
<td>Silver</td>
<td>Preserve raw values, normalize fields, and record findings such as ambiguous dates or conflicting candidates</td>
</tr>
<tr>
<td>Gold</td>
<td>Sum eligible reviewed extracted observations, separately by field and currency; no transaction-level deduplication guarantee</td>
</tr>
</tbody></table>
<p>For example, <code>03/07/2023</code> is ambiguous without adequate locale/context. Preserving the literal and flagging ambiguity is preferable to silently choosing a date. Likewise, an amount in INR must not be merged into an EUR aggregate merely because both values are numeric.</p>
<p>Normalization is deliberately narrow: the worker handles five monetary field keys and recognizes EUR, INR, USD, and GBP. The released <strong><code>document-medallion-v2</code></strong> method accepts both <code>2.967,11 EUR</code> and <code>2967,11 EUR</code> as <code>2967.11 EUR</code>, while preserving the raw value and flagging genuinely ambiguous formats. Java now checks the method version when deciding whether a receipt can be reused. A completed v1 run remains historical evidence; it does not become a v2 result automatically. Running v2 requires a new explicit ETL run, not new OCR of an unchanged source.</p>
<h3>Gold counts observations, not unique invoices</h3>
<p>The duplicate check is scoped to <code>(page, field label)</code> within each document. If a document repeats the same <strong>Invoice total: 100 EUR</strong> on two different pages, both reviewed observations can pass that check and contribute <strong>200 EUR</strong> to Gold. The current aggregate is not a reconciled invoice ledger. Reliable invoice-level totals need an explicit document/business grain and a policy for repeated or conflicting candidates; silently dropping equal amounts would also discard some legitimate observations.</p>
<p>The three current <strong>dbt tests</strong> check that selected Gold columns are non-null. They do not establish unique business events, correct sums, valid SCD2 intervals, or semantic source accuracy. Empty Gold can pass these structural checks and is expected when there are no eligible reviewed amounts.</p>
<p><strong>Parquet</strong> provides column-oriented analytical artifacts that can be inspected with other data tools. Downloads remain private and subject to authentication and explicit consent.</p>
<p><strong>Slowly Changing Dimension Type 2</strong>, or <strong>SCD Type 2</strong>, retains observed snapshot history when a document's extracted state changes. The observation timestamp is not automatically the document's business-effective date.</p>
<p>Source-version receipts let unchanged completed work be reused and stale results be rejected. In the documented synthetic-invoice walkthrough, the saved run contained <strong>one Bronze snapshot, two Silver fields, and zero Gold entries</strong> because the source was not reviewer-confirmed. Zero Gold was the intended boundary, not a failed transformation.</p>
<h2>11. Evaluation: measure one claim at a time</h2>
<p>The research workspace supports independently checked reference versions. A person inspects the original and records the expected field or target document/page before running a comparison.</p>
<p>For retrieval, two useful metrics are:</p>
<ul>
<li><strong>Hit rate@5:</strong> the fraction of labelled questions whose target document/page appears in the first five results.</li>
<li><strong>MRR@5:</strong> the average reciprocal rank of the first matching target in that range; a target at rank 1 contributes <code>1</code>, rank 2 contributes <code>0.5</code>, and a miss contributes <code>0</code>.</li>
</ul>
<p>This setup labels one target page per question. It should not be presented as a complete multi-relevant-document Recall@5 benchmark.</p>
<p>For extraction, saved predictions are compared with labelled fields. Parsing, schema validity, quotation support, and reference match are separate measurements. The zero-shot/few-shot experiment makes real local calls while keeping expected reference values out of the model prompts.</p>
<p>The published example is deliberately informative: zero-shot returned a supported invoice field, while few-shot abstained. Valid JSON did not turn the abstention into a correct field prediction. The failed sample remains visible.</p>
<p>The repository records scoped acceptance checks, including successful local Nova rechecks after the routing and timeout changes. This article does not report a new live-model benchmark or claim accuracy across arbitrary PDFs. Earlier failures retain their original version/date; a later successful case does not erase them.</p>
<h2>12. What are the 16 AI-assisted workflows?</h2>
<p>The capability directory lists workflows, not independently deployed models. Multiple tasks share the same extraction or chat provider.</p>
<table>
<thead>
<tr>
<th>Workflow</th>
<th>Practical purpose</th>
</tr>
</thead>
<tbody><tr>
<td>Text, layout, and OCR</td>
<td>Read the source document</td>
</tr>
<tr>
<td>Table extraction</td>
<td>Inspect row and cell structure</td>
</tr>
<tr>
<td>Hybrid evidence search</td>
<td>Find relevant passages with lexical and semantic signals</td>
</tr>
<tr>
<td>Semantic embeddings</td>
<td>Compare meaning-based retrieval with other methods</td>
</tr>
<tr>
<td>Nova document Q&amp;A</td>
<td>Answer a question with source references</td>
</tr>
<tr>
<td>Selected-field explanation</td>
<td>Explain a field in its original context</td>
</tr>
<tr>
<td>Evidence summary</td>
<td>Draft a source-based overview</td>
</tr>
<tr>
<td>Cross-document comparison</td>
<td>Compare separate documents without merging identities or currencies</td>
</tr>
<tr>
<td>Discrepancy explanation</td>
<td>Surface differences for inspection</td>
</tr>
<tr>
<td>Document-request draft</td>
<td>Prepare an editable request; no email is sent</td>
</tr>
<tr>
<td>Review checklist draft</td>
<td>Suggest checks for a human reviewer</td>
</tr>
<tr>
<td>German terminology explanation</td>
<td>Explain labels while preserving the source</td>
</tr>
<tr>
<td>German–Vietnamese summary</td>
<td>Draft bilingual explanations without claiming certified translation</td>
</tr>
<tr>
<td>Document chronology</td>
<td>Identify supported dated events within the inspected evidence</td>
</tr>
<tr>
<td>Clause and obligation review</td>
<td>Draft observations about selected clauses; no coverage or legal decision</td>
</tr>
<tr>
<td>Sensitive-data review draft</td>
<td>Identify categories for manual inspection; no automatic PDF redaction</td>
</tr>
</tbody></table>
<p>The additional rule-based, navigation, research, and review controls are not extra AI models. An exercise being available in the interface does not mean that every task has passed a broad accuracy benchmark.</p>
<h2>13. Optional integrations: what is available and what remains experimental?</h2>
<p>The repository contains explicit boundaries for further experiments. These should be understood as adapters and operator workflows, not a list of services already running successfully.</p>
<table>
<thead>
<tr>
<th>Integration</th>
<th>Why it exists</th>
<th>Current boundary</th>
</tr>
</thead>
<tbody><tr>
<td>PaddleOCR / PP-StructureV3</td>
<td>Alternative local OCR and document structure experiments</td>
<td>Requires its CPU runtime, reviewed cached models, and live extraction checks</td>
</tr>
<tr>
<td>BGE-M3</td>
<td>Multilingual semantic retrieval experiments</td>
<td>Separate 1,024-dimensional collection; weights and benchmarking required</td>
</tr>
<tr>
<td>BGE reranker</td>
<td>Reorder a bounded hybrid candidate set with a learned model</td>
<td>Separate weights; compare relevance gains against latency and memory</td>
</tr>
<tr>
<td>Local vision</td>
<td>Inspect one selected original page with a vision-capable model</td>
<td>Separately prepared model and explicit consent; output remains an observation</td>
</tr>
<tr>
<td>Azure Blob Storage</td>
<td>Controlled intake from an existing private container</td>
<td>Authorized prefix, explicit pull/consent, ETag-conditioned download, then the scanned queue</td>
</tr>
<tr>
<td>Azure Data Factory</td>
<td>Submit a scoped request to an existing pipeline</td>
<td>Operator execution and remote connectivity required; submission is not downstream completion</td>
</tr>
<tr>
<td>Azure AI Evaluation SDK / Foundry-related evaluation</td>
<td>Evaluate exported saved answers and excerpts with an authorized judge</td>
<td>Explicit cloud execution, independent references, and compatibility checks required</td>
</tr>
<tr>
<td>Application Insights</td>
<td>Optional Azure trace delivery</td>
<td>Separate configuration and actual privacy/delivery verification required</td>
</tr>
</tbody></table>
<p>BGE-M3's different dimensionality is why it uses a different vector collection. Reusing an incompatible collection would corrupt the retrieval experiment rather than upgrade it.</p>
<p>The Data Factory helper does not make a developer's <code>localhost</code> reachable from Azure. The evaluation bridge does not automatically create a Foundry project. Optional page vision does not replace verified OCR. These distinctions keep the architecture understandable as it grows.</p>
<h2>14. Operations and delivery: Docker, telemetry, and CI</h2>
<h3>Docker Compose makes the research environment reproducible</h3>
<p><strong>Docker Compose</strong> assembles the web app, Java API, identity provider, databases, scanner, retrieval services, and selected AI profiles. Named volumes retain databases, private documents, vectors, analytical artifacts, and separately prepared model weights.</p>
<p>Development ports bind to <code>127.0.0.1</code>. The supplied parsers and local model workers have constrained internal boundaries. Adding a public cloud deployment is a separate design and verification task.</p>
<h3>Where data lives and what a backup covers</h3>
<table>
<thead>
<tr>
<th>Store</th>
<th>Contents and recovery boundary</th>
</tr>
</thead>
<tbody><tr>
<td>Application PostgreSQL</td>
<td>Dossiers, jobs, answers, audit/review/research records, and original source bytes attached to dossiers in <code>credit_files</code></td>
</tr>
<tr>
<td>Identity PostgreSQL</td>
<td>Keycloak identity state, separate from the application database</td>
</tr>
<tr>
<td>Private document store/volume</td>
<td>Intake and processing source bytes; after extraction Java also attaches source bytes to the application database</td>
</tr>
<tr>
<td>Qdrant volume</td>
<td>Vectors and retrieval metadata; preserve or rebuild from authorized retained evidence</td>
</tr>
<tr>
<td>Private ETL volume</td>
<td>DuckDB history, handoffs, receipts, and Parquet, with a separate retention lifecycle</td>
</tr>
<tr>
<td>Ollama model volume</td>
<td>Explicitly prepared weights, separate from application state and documents</td>
</tr>
</tbody></table>
<p>Backing up PostgreSQL includes its stored source copies, but does not preserve every queued object, vector, analytical artifact, or model volume. Recovery planning needs all relevant stores, not only a successful database dump.</p>
<p>Stopping containers is different from removing persistent data. Do not use <code>down -v</code> or volume pruning as a routine fix for a lab whose records you want to retain.</p>
<p>The repository also contains optional infrastructure source using <strong>Bicep</strong> and <strong>Terraform</strong>, plus historical deployment support such as <strong>Caddy</strong> and a small <strong>Go</strong> identity-proxy component. Infrastructure source is not evidence of an active deployment. The current README identifies local Docker as the primary runtime and says the previous Azure VM backend and Vercel project were removed.</p>
<h3>Micrometer and OpenTelemetry observe behavior</h3>
<p><strong>Spring Boot Actuator</strong> exposes operational health information. <strong>Micrometer</strong> supports metrics, including an administrator-protected Prometheus endpoint. <strong>OpenTelemetry</strong> provides tracing integration and an optional collector.</p>
<p>The optional Application Insights configuration is traces-only and strips or limits attributes. Actual delivery still needs verification. Prompts, tokens, PDF text, and private identifiers do not belong in operational telemetry.</p>
<p>Health and latency are useful engineering signals. They do not measure hallucination rate, document accuracy, customer savings, or billed model costs.</p>
<h3>GitHub Actions checks several layers</h3>
<p>The repository's workflows include:</p>
<ul>
<li>ESLint, TypeScript checks, <strong>Vitest</strong>, and a Next.js build.</li>
<li>Java verification through <strong>Maven</strong> and Spring's testing stack.</li>
<li>Python tests for the document ETL path, plus a dedicated library matrix for Python <strong>3.10 through 3.14</strong>.</li>
<li>Wheel/sdist builds, <strong>Twine</strong> metadata checks, and distribution-content/license checks; actual Docling conversion is a separately opted-in smoke job.</li>
<li>Docker/OAuth integration checks with real local services.</li>
<li><strong>Gitleaks</strong> for secret scanning and <strong>Trivy</strong> for dependency/image checks.</li>
<li><strong>CycloneDX</strong> SBOM generation to inventory Java dependencies.</li>
<li>A manually triggered image-publishing workflow with <strong>Cosign</strong> signing and GitHub OIDC.</li>
</ul>
<p>The new Python publish workflow is <strong>manually dispatched from a matching <code>python-v*</code> tag</strong>. It checks that the tag matches the package version, runs core tests and distribution checks, then passes built artifacts to a registry-specific environment. The publishing job uses PyPI Trusted Publishing through GitHub OIDC with job-scoped <code>id-token: write</code>, rather than a hard-coded API token. Environment approval policies must be configured separately. An ordinary branch push neither publishes a Python release nor deploys Azure/Vercel. The <code>0.1.0a1</code> wheel and source archive are now publicly available on PyPI; that is distinct from merely having a workflow file.</p>
<p>A software bill of materials is an inventory, not a security audit. A signing workflow's presence does not prove that a particular image has been published or signed. Tests provide evidence about the cases they exercise, not universal certification.</p>
<h2>15. Run the local research path</h2>
<p>Choose this path when you need the complete authenticated workspace. For the standalone Python library, use the smaller installation in section 2. The full workspace requires Git, <strong>Node.js 22+</strong>, Docker with Compose v2, and enough available CPU, RAM, and disk. The guide suggests four CPUs and 8 GB of Docker RAM; local inference and initial model preparation can still be resource-intensive.</p>
<pre><code class="language-bash">git clone https://github.com/quocvietphung/document-intelligence-lab.git
cd document-intelligence-lab

# Reproduce the public source snapshot described in this article.
git checkout c7e3fc5c0206264fd906bf832a3f2a5e437b13e9
mkdir -p .local/reference-pdfs

# Explicit first-time model preparation and startup.
node scripts/docker-local.mjs --prepare-model
</code></pre>
<p>The chosen local model download is approximately 2.5 GB, separate from images and build caches. A fresh clone does not contain private PDFs, model weights, or saved research results.</p>
<p>Subsequent starts reuse the prepared model and persistent data:</p>
<pre><code class="language-bash">node scripts/docker-local.mjs
</code></pre>
<h3>Input and inspected-evidence limits</h3>
<p>The current lab bounds document uploads to <strong>3 MB</strong> and local PDFs to <strong>four pages</strong>; the supplied Azure profile sets a <strong>two-page</strong> PDF limit. Provider-specific validation and page-text bounds apply as well.</p>
<p>The bounded <strong>local Ollama</strong> chronology, clause, and privacy tasks select the first <strong>three eligible original-page excerpts across the dossier</strong>, each limited to <strong>2,000 characters</strong>, and return up to <strong>three findings</strong>. A statement that no clause was found applies to those inspected excerpts rather than clearing the whole document or dossier. Other retrieval and provider paths have their own context bounds.</p>
<p>Open <code>http://localhost:3000/en</code> and follow the README's local Keycloak sign-in instructions. Use a fictional or explicitly permitted document, wait for its extraction result, and ask a question such as:</p>
<blockquote>
<p>What invoice number, currency, and gross total are stated? Cite the source page. If the evidence is insufficient, say so.</p>
</blockquote>
<p>Inspect the actual provider identity, open each source, and compare the original. Do not treat a running container, a greeting, or a model badge as a successful evidence-answer test.</p>
<p>For source verification and contribution checks, the repository documents:</p>
<pre><code class="language-bash">npm run verify
mvn -f services/credit-api/pom.xml test

# Requires the local Docker environment.
node scripts/test-oauth.mjs
</code></pre>
<p>Azure and optional-model experiments have separate instructions. They should not be enabled simply to complete the basic local walkthrough.</p>
<h3>What changed in the public release?</h3>
<p>The changes previously marked unreleased are now in public source: explicit Nova mode routing, process-local inference admission, distinct busy/timeout responses, and ETL v2 parsing with method-aware receipt reuse. The Python library, CLI, SDK, packaging tests, and manual publishing workflow are also present. The package release is pinned to <code>python-v0.1.0a1</code>; this article's newer <code>c7e3fc5</code> snapshot includes the accompanying PyPI installation documentation.</p>
<p>Those improvements do not turn the lab into a production service or resolve every previously identified edge case. Gold still aggregates reviewed observations rather than deduplicated invoices; evidence selection remains bounded; citation existence still does not prove semantic support; local CPU latency remains hardware-dependent. Public source changes and uncommitted local infrastructure experiments are different states.</p>
<h2>16. What I want readers to take away</h2>
<p>The interesting part of document AI is how the system handles a difficult question: <strong>what evidence would let another person check this answer?</strong></p>
<p>In this lab, that question shapes the architecture. Page provenance influences chunking. Authorization scopes retrieval. Provider identities remain visible. Unsupported drafts can fail. Human review controls which values enter analytical Gold. Source versions determine whether results can be reused.</p>
<p>There is still work to do: broader evaluation datasets, multilingual retrieval comparisons, local inference reliability, measured optional-model trade-offs, and independently reviewed deployment and recovery procedures. The current project is a place to study these problems openly, with both successful and failed observations visible.</p>
<p>If you work on Python document processing, OCR, Java AI systems, RAG evaluation, or document data engineering, I would welcome a reproducible issue, a carefully scoped improvement, or feedback on an experiment. The Python package offers a smaller starting point; the full workspace makes the surrounding identity, review, retrieval, and analytical boundaries visible.</p>
<p><strong>Explore the source:</strong> <a href="https://github.com/quocvietphung/document-intelligence-lab">quocvietphung/document-intelligence-lab</a>. If it is useful to your work, a GitHub star helps other developers discover it.</p>
<h3>Source guide and further reading</h3>
<ul>
<li><p><a href="https://pypi.org/project/document-evidence-lab/0.1.0a1/">Published Python alpha <code>0.1.0a1</code></a> and <a href="https://github.com/quocvietphung/document-intelligence-lab/tree/python-v0.1.0a1">immutable package release source</a></p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/tree/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/python">Python API, CLI, SDK, and tests</a></p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/README.md">README and complete local setup</a></p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/package.json">Frontend dependencies</a> and <a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/services/credit-api/pom.xml">Java dependencies</a></p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/docs/LOCAL_DOCUMENT_AI.md">Local document AI</a></p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/docs/AI_INTEGRATIONS.md">Optional AI integrations and their limits</a></p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/docs/RESEARCH_WORKSPACES.md">Research workspaces</a>: this snapshot still describes the older logical research JSON export. Its Gold definition is different from the physical DuckDB/Parquet ETL covered here; read it alongside the snapshot README and optional-integration guide.</p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/docs/NOVA_TRUST.md">Nova evidence, privacy, and review</a>: its Azure-centric destination table predates the local Ollama path. For this snapshot, interpret data destinations according to the explicitly selected provider and the local/cloud boundaries explained above.</p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/docs/RELIABILITY_LAB.md">Operations and recovery boundaries</a></p>
</li>
<li><p><a href="https://github.com/quocvietphung/document-intelligence-lab/blob/c7e3fc5c0206264fd906bf832a3f2a5e437b13e9/THIRD_PARTY_NOTICES.md">Third-party licenses and document rights</a></p>
</li>
</ul>
<p><em>Update verification, 24 October 2026: compared the public source changes and current README with the earlier article, confirmed PyPI wheel/sdist availability, and installed the published base wheel in a clean Python 3.13 environment. The validation examples and CLI help passed. The source Python/SDK suite completed 23 tests successfully; the separately opted-in actual Docling conversion test was skipped. No new PDF conversion, full-stack run, cloud inference, or performance benchmark was performed for this article update.</em></p>
<p><em>The MIT license covers the project's own code. Dependencies, container images, model weights, and publisher documents retain their own terms. The diagrams in this article describe the architecture; they are not performance or accuracy results.</em></p>
]]></content:encoded></item></channel></rss>