跳到正文
DEV Community· Antonio Pasolini·· 4 小時前AI 評分43

BioFlow:十三年的化驗報告,以及一次令資料夾不敷應用的覆診

BioFlow — Thirteen Years of Lab Reports, and the Appointment That Broke the Folder

AI 導讀

BioFlow 是一個縱向整理化驗結果的示範工具,使用 Google Gemma 4 26B A4B 從 PDF 提取日期、項目、數值及來源頁面,待用戶核對確認後才匯入臨時紀錄。公開示範使用虛構資料,包含 26 份合成報告及 604 項數值;私人文件保管庫目前已實作但未啟用。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady.

My father is 64. He has had diabetes for thirteen years. In that time he has produced a small archive of laboratory reports — dozens of PDFs from several different laboratories, each with its own layout and idea of where the reference range should go.

He is not someone who lives inside a computer. For most of those years the archive lived the way most family archives live: a folder, a downloads directory with the same report under three names, a few printouts in a drawer, and a set of dates nobody could reconstruct without opening the files one by one.

The folder was not the problem. The folder was a symptom.

The real problem was the appointment

Every time he sees a new specialist, the doctor needs the whole history. So we would sit down before each appointment and rebuild thirteen years of context by hand. Open this PDF for the 2019 glucose. Open that one for the 2021 glycated hemoglobin. Compare decimal commas and points without letting a parser quietly turn 5,190 into the wrong number. Then carry the whole thing — sometimes on a USB drive, sometimes as an email chain nobody wants to open.

It was relentlessly tedious, and always at the worst possible moment. The effort was never distributed fairly. He was the one who had to carry it, and he was the one with the least patience for the tools involved. Every extra step I added was a step he would eventually stop taking.

That is where BioFlow came from. Not from a hackathon prompt. From a folder, a USB drive, and one appointment too many.

What I actually wanted to build

My first plan was modest: a spreadsheet or a small database he could carry on a pendrive or open in the waiting room.

What changed my mind was realizing that a spreadsheet holds numbers. It does not read a new PDF for you, it does not know that “hemoglobina glicada” and “HbA1c” are the same measurement, and it does not let a doctor look at thirteen years of trend on one screen without someone sitting next to them explaining which column is which.

The people who need this most are not the ones who are comfortable with software. They are the ones who have been accumulating lab reports since before smartphones existed — 60, 70, 80 — who see two or three different doctors, who have a drawer of paper and a folder of PDFs, and who have no interest in learning a new system.

For them, the value is not a dashboard. The value is that the thing they already have to do becomes shorter: hand the doctor a complete history instead of a plastic bag of paper.

What BioFlow is — and what it refuses to be

BioFlow has two complementary goals, but only one is publicly operational today.

The panel is live, using fictional data. It is a longitudinal view of laboratory results: analytes on timelines, points linked to their source reports and page-level provenance, and reference-range markers only where a source interval is available. It makes past observations easier to find and compare; it does not interpret them clinically.

The private document vault is an implemented, disabled foundation. The source includes a design for preserving original PDF bytes unchanged in private Cloudflare R2 storage, protected by identity and ownership checks. In the public challenge deployment those endpoints are intentionally off. I will not call a future privacy feature a live feature just because the code exists.

BioFlow is not a medical document and will never replace one. It does not diagnose, predict risk, determine severity or recommend treatment. Original laboratory PDFs remain authoritative; extracted readings are indexes to the source, not replacements for it. A clinician must inspect the original document before relying on any value.

This separation is the product’s primary engineering constraint: did we read the right number, keep the right unit, link the right page and refuse incomplete work?

What I Built

BioFlow is a longitudinal laboratory-history demo with Google Gemma 4 26B A4B, MongoDB Atlas, Cloudflare Workers and a React frontend hosted on Render.

A visitor enters a public, published six-digit demonstration code, downloads a ZIP containing three fictional sample PDFs and chooses one to upload. Cloudflare converts its contents to text; Gemma proposes the collection date, laboratory, analyte names, numerical values, units and source pages. The visitor examines that proposal against the sample PDF and confirms it. Only then does the application write the approved structure to an isolated, expiring session. It appears alongside a baseline of 26 synthetic reports and 604 numerical readings. The historical baseline itself is never modified by an import.

Why three sample reports? One report proves a parser can output JSON. Three, with different collection dates and overlapping analytes, test whether imports extend meaningful trends rather than create orphan charts. The three public samples were deliberately reduced to one page each and 23, 21 and 24 named numeric result rows. They are distributed together as one ZIP.

All records in the public application are fictional. My father’s actual reports were not published in the repository, public PDFs or demonstration database. His story explains the need; his medical information is not the test data.

Demo

Open BioFlow on jrpasolini.tech

To follow the complete tested path:

  1. Enter the published demo code 110526. It is intentionally public, grants access only to fictional data and is not an Auth0 credential or a second factor.
  2. Download the single ZIP containing three fictional sample PDFs and extract one PDF.
  3. Upload one of the three samples. The Worker rejects other PDF hashes before inference.
  4. Compare Gemma’s proposed readings with the PDF; select explicit confirmation.
  5. See the approved import in that visitor’s temporary history alongside the 26-report baseline. Try another sample if you wish.
  6. Open a separate session: it cannot see or reuse the first visitor’s records. Session access ends after one hour; MongoDB’s TTL cleanup is asynchronous rather than instantaneous.

The independent production run on October 22, 2026 processed the current one-page samples in 20, 24 and 34 seconds, respectively, after deployment of the validation-hardening changes in PR #11 (commit 679d9c7). Inference time varies and a provider failure can still interrupt a run. Never upload real patient documents to this public demo.

Code

Source code — jrpasolini/bioflow

The repository started on October 19, 2026, inside the Week 3 window, as a new project. Nothing from my personal health panel was reused — not code, not data, not names. The 29 PDFs used for the demo were independently invented and supplied as fictitious, and the repository records their hashes so the claim is checkable rather than rhetorical.

Architecture, decisions, validation evidence and the development history are in docs/, including the production verification record.

Original BioFlow application code and documentation are released under the MIT License; third-party fonts, libraries, icons and model weights keep their own applicable terms.

How I Built It

1. The model proposes; the person decides

The most important design choice was not technical. It was deciding that the AI never writes to the database.

Visitor uploads a PDF
  |
  v
Cloudflare Worker
  | 1. size cap, PDF signature, SHA-256 against a 3-file allowlist
  | 2. toMarkdown: PDF pages -> text (no model involved)
  v
Gemma 4 26B A4B  (chunked, parallel, per-chunk deadline)
  | proposes { date, lab, name, value, unit, sourcePage }
  v
Validator
  | decimal parsing, unit canonicalisation, analyte resolution,
  | legend rejection, schema check
  v
HMAC-signed proof (20 minutes)
  |
  v
Visitor reviews the draft against the source PDF
  |
  v  explicit confirmation + unchanged report digest
MongoDB Atlas — bioflow_sessions.demo_imports (TTL: 60 minutes)

No new import is persisted before confirmation. The proof is bound to the proposed report; changing a value before commit produces report_changed, not a quiet write. The Atlas users have separately scoped roles: the baseline reader reads bioflow_demo, while the temporary writer has readWrite on bioflow_sessions.

A language model reading a medical document is a transcription assistant with a plausible failure mode, and the failure mode is a number that looks fine. Human review is not a compliance checkbox here; it is the mechanism that makes the rest of the pipeline safe to build.

2. “Slow and Steady” was the constraint

Earlier test files were much larger. One historical 24-page sample extracted in about 44 seconds on a successful run; other long runs failed. My instinct was to optimize latency. The project got better when I optimized honesty instead.

The converted text is divided at page boundaries into approximately 2,500-character chunks, with parallel inference, bounded deadlines and limited retries. A failed or malformed chunk is not silently ignored: if it cannot be recovered, the document fails with incomplete_extraction. Successful schema validation is not a guarantee that Gemma captured every numerical reading correctly.

During the final production push the provider started rejecting every chunk with a quota error (4006). The pipeline had been retrying each rejection immediately and reporting a generic 502. The fix was to separate transient failures from permanent ones: a quota, access or request rejection now stops retries at once and returns 503 with a fixed provider_quota reason. The three public samples were re-cut to at least 60% fewer result rows each, and files that formerly needed multiple chunks now fit in one chunk of about one thousand characters.

A failed document chunk or an invalid numeric-looking reading is an error, not a result. The validator rejects unreadable numeric values instead of quietly omitting them from an otherwise valid extraction, and it refuses conflicting duplicated measurements. Genuine qualitative findings remain separate from numeric chart data. This cannot prove that a model did not omit a source line entirely; the visitor must still verify the draft against the PDF.

3. The bug that taught me the most

Early on I had a chunk ceiling of ten. Two of my test reports needed more. The pipeline silently dropped the excess, returned a partial extraction, and reported success — cutting 41% and 49% of two reports while the interface said the import had worked.

Nothing crashed. No test failed. The dashboard looked correct. It was simply describing a document that did not exist.

The fix is not clever: oversized documents are now refused with document_too_large, never truncated. The lesson generalizes:

A successful HTTP response is not evidence that your pipeline did what you think it did.

I added explicit error codes for each failure branch — sample_not_allowed, document_too_large, incomplete_extraction, report_changed, demo_limit_reached — and the interface translates each one into a sentence a human can act on.

4. One vocabulary, or the chart lies

The model is asked to name each analyte. It does not use a stable name. Across three runs, hemoglobin came back as hb, hemoglobin, and hemoglobina. Since the history is keyed by exact code, every import created a new orphan series instead of extending the existing one.

Of 53 extracted readings, only 3 joined an existing series. The fix was to stop letting the model own the vocabulary. packages/fixtures/src/analytes.ts declares one canonical code per analyte, with explicit aliases, display terms and accepted unit spellings. The validator resolves every reading through it before any other check. Result: 97 distinct codes collapsed to 68, and readings joining existing series went from 3 to 27 on the same inputs.

The rules inside that file matter more than the mechanism:

  • Longest term wins. “Hemoglobina glicada” must never resolve to hemoglobin.
  • Derived values get their own series. Estimated average glucose is computed from HbA1c. Plotting it inside the glucose series would invent a measurement that was never taken.
  • Units only normalise between known spellings. g/L never becomes g/dL.
  • An unknown analyte is kept. It gets its own code and starts its own series.
  • A differential count is not its parent panel. Band neutrophils and segmented neutrophils are not the same line.

Laboratory PDFs also print grading legends in footers. The model read seven of those thresholds as measurements. isLegendReading() now rejects them.

5. Read deterministically what is already printed

The best decision I made late in the build was to use no model at all for the baseline corpus.

The 26 baseline reports already contained text-readable laboratory values. Running a generative model over them would have introduced plausible errors where deterministic extraction was possible. So the baseline was assembled with a deterministic text-parsing pipeline: 604 numeric readings across 71 analytes, from 275 pages, with documented provenance and checks for non-finite and duplicate values. The original baseline had to be repaired in Atlas when an independent read showed only 258 of those 604 readings had been stored; the other 346 were restored without overwriting existing records.

The open-weight model does the part that actually needs judgment — the part where a new document arrives and the fields must be found, not looked up. Everything that could be read mechanically, is.

6. Production verification

An October 21 validation found a real distinction: the public overview could display fixtures even while the actual Atlas read credential was missing. Once the owner corrected the secret, the running Worker independently returned HTTP 200 with source: mongodb-atlas-synthetic, 26 reports and 604 readings.

We also caught a database issue that a successful first upload hid. A legacy unique index expected a second SHA field that newer temporary imports did not populate. Subsequent imports in the same session collided. The fix writes both expected digest fields.

On October 22, 2026, after deploying the validation-hardening changes from PR #11, an independent production smoke test ran 19 checks, all passed, against Worker commit 679d9c7db1ff1c628dbe69876c7afc0e6668e9f1. The health endpoint identified that commit during the run. It confirmed the Atlas-backed overview, separate one-hour signed sessions, rejection of tampered tokens, refusal of cross-session proof replay, no write before confirmation, disabled private routes and successful extraction-plus-commit for all three current samples.

Public fictional sample AI extraction Readings returned Confirmed write
May 15, 2026 20 s 23 HTTP 201
October 1, 2026 24 s 21 HTTP 201
October 2, 2026 34 s 24 HTTP 201

The final run started at 21:58:43 and finished at 22:02:25 (UTC−3, São Paulo) on October 22. All three samples completed on their first recorded extraction attempt, each using one Gemma chunk, and all three confirmed writes returned HTTP 201. Together they returned 68 numeric readings. The 22 local Gemma-validation tests also passed, including regression cases for malformed numeric values and conflicting duplicate measurements.

The smoke test separately verified no persistence before confirmation, rejection of a confirmation proof presented from a different session, and isolation of imported history between sessions. These are concrete checks of the review-and-confirm boundary, rather than a claim that every possible model failure has been eliminated. The verification output records HTTP statuses and counts, not raw PDFs, tokens or private credentials. It proves a working transport, validation and persistence path; it does not establish the clinical accuracy of every model-extracted number. Review before confirmation remains mandatory.

Read the production evidence and the 19/19 report.

7. Privacy is a feature of the architecture

Real medical records cannot share the access rules of a public hackathon demo. The design separates three boundaries:

  • Public challenge access uses code 110526 to show entirely fictional data. It is a convenience switch, not authentication.
  • Private account authentication has Auth0 Universal Login and authenticator-app OTP/TOTP integration implemented in source. Its end-to-end MFA token flow has not been independently certified for the current public deployment.
  • Private PDF preservation and patient-controlled sharing are goals with guarded R2/authorization foundations in source, but neither the private bucket nor the private document endpoints are enabled in the challenge Worker.

The runtime tells the truth in plain fields: gemmaReady: true, databaseConfigured: true, authReady: false, storageReady: false, pdfEndpointsEnabled: false. The Worker accepts only the three audited fictional PDFs; no real report belongs in this public pipeline.

8. Why these pieces

Gemma, because it is reachable. The model runs through Cloudflare Workers AI. I chose it because it is an open-weight model I could call from the same platform that already hosted the API, without a new vendor, a new bill, or a new failure surface.

MongoDB Atlas, because the shape of the data is a document. Each exam is a report with a date, a laboratory, and a set of readings with provenance. That nests naturally, and the free tier is enough to hold a synthetic corpus and a short-lived import session.

Auth0, because I did not want to invent an identity provider. The private-login integration exists as a separate source path, but I am not confusing that with verified agent delegation or enabled private-record access.

GitHub Copilot and GitHub Actions, for reviewable engineering. The repository-specific .github/copilot-instructions.md encodes constraints on fictional-only data, PDF fidelity, bilingual accessibility, least-privilege authorization and human confirmation of AI output. The GitHub Actions workflow (.github/workflows/ci.yml) makes each proposed change independently checkable: lockfile-based installation, automated application and extraction-safety regressions, TypeScript checks, production builds, the /bioflow/ subpath, PWA behavior and the Cloudflare Worker bundle. These checks run on pushes and pull requests with read-only repository permissions and clearly named results. The final hardening PR adds tests demonstrating that malformed numeric values and contradictory duplicate analytes cannot silently disappear from an otherwise readable report. This is the role of the automation: expose specific failures for review before merging, rather than merely report a green build. Early Copilot review requests were made; several did not complete because of quota limits and are not counted as approvals.

Cloudflare Workers, because the trust boundary belongs on the server. The browser sends a file; the server decides whether it may be read, by which model, and under which limits.

Why Does Open Innovation Matter?

Because the model is a replaceable component, and the software owns the contract around it.

The extraction contract is mine, not a vendor’s. BioFlow asks for a specific JSON shape, validates it itself, and resolves every name through its own vocabulary. Swapping Gemma for another open-weight model is a model-adapter change followed by regression and real-output testing, not a rewrite of the product.

The privacy story is architectural, not promotional. The end state I am building toward is inference on hardware I own, where a PDF never leaves the machine and only the structured result is stored. I am not there yet — the demo runs on a hosted open-weight endpoint, and I say so. But the pipeline was built to make that move cheap: text conversion, chunking, validation, provenance and storage are all model-independent.

Open innovation makes the pipeline inspectable. The failure boundaries in our code — chunk ceilings, decimal conventions, footer legends, identifier drift, session replay and database separation — are reviewable and testable independently of a model provider. Open weights add the freedom to deploy elsewhere or evaluate another runtime later; they do not magically make an individual output correct. That comes from tests, provenance and human verification.

And cost. The baseline used deterministic parsing instead of running every page through an inference service. The public model demonstration uses smaller, auditable samples. We documented latency and actual quota failure rather than claiming an unverified dollar saving.

The thing that makes this project worth publishing is not that it uses AI. It is that the open pieces are what make it trustworthy: a model you can swap, a pipeline you can inspect, and a data layer cheap enough that a family can actually keep it running.

My Agent Session

I developed BioFlow with Melissa, an AI agent running on the open-source OpenClaw framework. I defined the problem, scope, architecture, security boundaries and release decisions; Melissa helped implement features, investigate failures, add regression tests and maintain the development record. Other AI-assisted coding and editorial reviews also informed the work, but I checked their conclusions against source code, automated tests and the deployed application before adopting them. A suggestion was not an approval, and a passing unit test was not automatically proof of production behavior.

The most useful habit was asking why the pipeline believed it had succeeded. That question exposed silent document truncation, an Atlas index collision and the difference between a database credential being configured and the application actually reading the database.

The GitHub repository contains the development log, pull requests, test records and independent production checks. I am not publishing a DevRelay session recording for this entry.

The Lazy Payoff

This is a synthetic-data development comparison, not a clinical validation study or a timed usability trial with patients.

Task Traditional folder workflow Current BioFlow demonstration
Structure a new allowed fictional PDF Manually copy dates, test names, numbers and units Gemma proposes rows in 20–34 s for the three current one-page samples; person still checks every value
Compare a laboratory indicator over years Open and scan several separate reports One chronological chart tied to source reports
Find a past collection Remember a filename, directory and date Browse the synthetic history by date and analyte
See how a reading relates to its source interval Locate and check the original PDF’s printed range Display a reference marker only when a source interval exists
Revisit an import Keep a manual copy or spreadsheet Review temporary rows in the same signed demo session (one hour)
Keep an original real-world PDF Maintain the original document outside the demo Private R2 vault architecture exists in source but is not deployed for patient data

I used the system on fictional documents during development, which is why so many failures surfaced before the final smoke test. The honest payoff is not a made-up percentage of time saved. It is less mechanical copying, easier navigation and a workflow that refuses to hide failed extraction behind a pretty chart.

The personal payoff remains the same: my father should not need to become a software expert simply to keep his laboratory history understandable. The current prototype proves a technical path with fictional documents; user research and real-world medical-data safeguards are separate, future work.

Prize Categories

The following are the partner categories for which the public project has a concrete, inspectable use of the partner’s technology:

  • Best Use of Gemma — Google Gemma 4 26B A4B (@cf/google/gemma-4-26b-a4b-it) performs live extraction on SHA-256-allowlisted fictional PDFs. A strict chunk-validation and review stage prevents unapproved model text from reaching the database.
  • Best Use of MongoDB Atlas — the public Worker has been verified reading the 26-report, 604-reading synthetic baseline from Atlas; independently scoped session storage persists confirmed imports and expires them with TTL.
  • Best Use of Render — Render hosts the production React/TypeScript frontend as a Static Site, available at jrpasolini.tech/bioflow. This is more than a landing page: it is the human-facing interface to the open-weight AI workflow. Visitors download an audited fictional PDF, submit it for Gemma extraction, compare the proposed readings against the original document, and explicitly confirm before anything enters their temporary history. The frontend communicates with the separate Cloudflare Worker through VITE_API_BASE_URL; inference, PDF allowlisting, signed confirmation proofs, and MongoDB access stay on the server, not in the Render-delivered browser code. I configured the monorepo build (npm install --no-audit --no-fund && npm run build -w @bioflow/web) and publish directory (apps/web/dist) to ship the bilingual interface without requiring visitors to install or configure anything. Render's contribution is to make a cautious AI pipeline usable: the model proposes, but the person gets a clear, bilingual place to review and decide. The deployment contract is documented in docs/RENDER_DEPLOY.md, with the Worker boundary in docs/WORKER_DEPLOY.md.
  • Best Use of GitHub Copilot — Copilot receives project-specific engineering rules from .github/copilot-instructions.md, covering privacy, synthetic fixtures, bilingual UI, provenance, and explicit human confirmation. The GitHub Actions pipeline provides reproducible checks on every push and PR: lockfile installation, application and safety-regression tests, TypeScript, production builds, /bioflow/ routing, PWA checks, and the Worker bundle. In the release-hardening PR, the pipeline also exercises new fail-closed tests for malformed numeric readings and contradictory duplicates; stages are individually named, with read-only permissions. This makes it easier to detect and review regressions before accepting changes. Copilot review requests that exceeded quota are explicitly not claimed as completed approvals.
  • Best Use of a .tech Domain — jrpasolini.tech/bioflow is the public, reproducible demonstration entry point. The domain was claimed through the Hacktoberfest 2026 promo (via hacktoberfest.com/my/promos) and is used to host the live demo.

I am not entering “Best Use of Auth0 for AI Agents” on the current evidence. Human Auth0/TOTP integration in source is not the same thing as an identity-authorized AI agent that invokes delegated tools or data access. That category would require its own tested flow and proof. The project remains an entry in the overall Week 3: Slow and Steady challenge.


Thirteen years of my father’s laboratory reports fit in folders, and the folders never once answered a question on their own.

What I wanted was never a clever model. I wanted a path to a history that could arrive at an appointment intact — complete, readable, and still under the patient’s control. And I wanted it to ask as little as possible of a 64-year-old man who has better things to do than learn a new system.

BioFlow is not a medical document and will never replace one. Today it is a working synthetic demonstration of a better way to organize a history; tomorrow, if we satisfy the harder privacy and access requirements, it could also help preserve the real originals. The pipeline that refuses to say “done” when it isn’t is the part I am proudest of — because the only useful version of this idea is an honest one.

來源:DEV Community · dev.to