Skip to content

Instantly share code, notes, and snippets.

@theotheo
Last active March 27, 2026 09:05
Show Gist options
  • Select an option

  • Save theotheo/c4bc74160c15aa31f50012e8a0e0587d to your computer and use it in GitHub Desktop.

Select an option

Save theotheo/c4bc74160c15aa31f50012e8a0e0587d to your computer and use it in GitHub Desktop.
BloodGPT: Extraction evolution diagram (draw.io XML)

From "Save Pages" to "Extract Facts"

Evolution of document extraction approach — BloodGPT BG-1059

SOURCE DOCUMENT (МедЛаб Диагностика, 5 дат):
┌──────────────────┬────────┬─────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ Показатель       │ Ед.    │ Норма   │ 15.01.24 │ 28.02.24 │ 10.04.24 │ 22.06.24 │ 05.09.24 │
├──────────────────┼────────┼─────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Гемоглобин (HGB) │ г/л    │ 130–160 │ 118 ↓    │ 122 ↓    │ 135      │ 142      │ 138      │
│ Холестерин общий │ ммоль/л│ <5.2    │ 6.8 ↑    │ 6.2 ↑    │ 5.5 ↑    │ 4.9      │ 4.7      │
│ Креатинин        │ мкмоль │ 62–115  │ 128 ↑    │ 119 ↑    │ 105      │ 98       │ 92       │
└──────────────────┴────────┴─────────┴──────────┴──────────┴──────────┴──────────┴──────────┘

═══════════════════════════════════════════════════════════════════════════════════════════════

 APPROACH               │ EXAMPLE OUTPUT                          │ RESULT
════════════════════════╪═════════════════════════════════════════╪══════════════════════════
                        │                                         │
 Step 0: Original idea  │ Save pages to SQL, numbered by page.    │ Starting point.
 (Vasya, daily 25.03)   │ Agent "spiders" come back to fix        │ How to preserve data
 Save pages + agents    │ dates, ranges, consistency.             │ so pipeline can
                        │ Pole A: workflow | Pole B: agent        │ come back to it?
────────────────────────┼─────────────────────────────────────────┼──────────────────────────
                        │                                         │
 Step 1: Raw text       │ Гемоглобин (HGB) г/л 130–160 118       │ ❌ Loses everything
 PDF → OCR → text       │ 122 135 142 138 Холестерин общий        │ Which 118 is which date?
                        │ ммоль/л <5.2 6.8 6.2 5.5 4.9 4.7      │ No structure at all.
────────────────────────┼─────────────────────────────────────────┼──────────────────────────
                        │                                         │
 Step 2a: Markdown      │ | Показатель | 15.01 | 28.02 | 10.04 | │ ⚠️ Partial
 VLM → markdown         │ |------------|-------|-------|-------| │ Columns keep dates ✓
                        │ | HGB        | 118   | 122   | 135   | │ But cards/mixed break
                        │ | Холестерин | 6.8   | 6.2   | 5.5   | │ Notes detached
────────────────────────┼─────────────────────────────────────────┼──────────────────────────
                        │                                         │
 Step 2b: HTML          │ <table><tr><td>HGB</td>                │ ⚠️ Very verbose
 VLM → HTML             │ <td>118</td><td>122</td>...            │ 3 params = 500+ chars
                        │ </tr></table>                           │ Huge token cost
────────────────────────┼─────────────────────────────────────────┼──────────────────────────
                        │                                         │
 Step 3: Table objects  │ {columns: ["Показатель","15.01",        │ ❌ Not universal
 Extract tables         │  "28.02","10.04"],                      │ Cards, narratives,
                        │  rows: [["HGB","118","122","135"]]}     │ mixed layouts lost
────────────────────────┼─────────────────────────────────────────┼──────────────────────────
                        │                                         │
 Step 4a: obs+metadata  │ {type:"lab_result", name:"HGB",        │ ⚠️ 33% recall!
 Two types only         │  value:"118", unit:"г/л"}               │ Only latest date!
                        │ {type:"metadata",                       │ 122,135,142 — lost.
                        │  key:"patient", val:"Петров"}           │ Metadata = catch-all
────────────────────────┼─────────────────────────────────────────┼──────────────────────────

  💡 FHIR already formalized what exists in medical documents! fhir-services has 7 types.

────────────────────────┼─────────────────────────────────────────┼──────────────────────────
                        │                                         │
 ✅ Fact-based           │ {type: "observation",                   │ ✅ 92%+ recall
 8 FHIR types           │  name: "Гемоглобин (HGB)",             │ ✅ 98% value accuracy
 anyOf strict schema    │  value: "118",                          │
 + date per fact        │  unit: "г/л",                           │ ✓ All 5 dates
 + typed notes          │  range: "130–160",                      │ ✓ 100 observations
 + alt_name             │  date: "2024-01-15",                    │ ✓ Notes attached
 Single-pass GPT-5.2    │  category: "laboratory",                │ ✓ 6+ languages
                        │  page: 1}                               │ ✓ 11 dates/18 pages
                        │                                         │ ✓ 308 facts/27 pages
                        │ {type: "observation",                   │ ✓ Page-break notes ✓
                        │  name: "Гемоглобин (HGB)",             │
                        │  value: "122",                          │
                        │  date: "2024-02-28", ...}               │
                        │                                         │
                        │ {type: "observation",                   │
                        │  name: "Холестерин общий",              │
                        │  value: "6.8",                          │
                        │  date: "2024-01-15",                    │
                        │  notes: [{type:"risk",                  │
                        │   text:"<5.2 норма"}]}                  │
════════════════════════╧═════════════════════════════════════════╧══════════════════════════

  🔑 Problem was in PROMPT + SCHEMA, not in PIPELINE.
     Old: "extract parameters" → 33%    New: "extract facts" + anyOf → 100%
     Same model. Single pass. No SQL. No agents.
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1800">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="h1" value="&lt;b&gt;Approach&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#37474F;strokeColor=none;fontSize=13;fontColor=#FFFFFF;rounded=1;shadow=1;" vertex="1" parent="1"><mxGeometry x="30" y="20" width="220" height="35" as="geometry"/></mxCell>
<mxCell id="h2" value="&lt;b&gt;Example Output&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#37474F;strokeColor=none;fontSize=13;fontColor=#FFFFFF;rounded=1;shadow=1;" vertex="1" parent="1"><mxGeometry x="270" y="20" width="380" height="35" as="geometry"/></mxCell>
<mxCell id="h3" value="&lt;b&gt;Result&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;fillColor=#37474F;strokeColor=none;fontSize=13;fontColor=#FFFFFF;rounded=1;shadow=1;" vertex="1" parent="1"><mxGeometry x="670" y="20" width="220" height="35" as="geometry"/></mxCell>
<mxCell id="r0a" value="&lt;b&gt;Step 0: Original idea&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;Save pages to SQL (text + images)&lt;br&gt;Agent spiders come back to fix&lt;br&gt;Two poles: workflow vs agent&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#FFE0B2;strokeColor=#E65100;strokeWidth=1.5;fontSize=11;fontColor=#BF360C;" vertex="1" parent="1"><mxGeometry x="30" y="80" width="220" height="100" as="geometry"/></mxCell>
<mxCell id="r0b" value="&lt;font style=&quot;font-size:9px&quot;&gt;Two poles discussed:&lt;br&gt;&lt;b&gt;A:&lt;/b&gt; Strict workflow (steps, cheap)&lt;br&gt;&lt;b&gt;B:&lt;/b&gt; Agent + tools (flexible, $$$)&lt;br&gt;Need combination of both&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFF3E0;strokeColor=#E65100;strokeWidth=1;fontSize=9;fontColor=#555;" vertex="1" parent="1"><mxGeometry x="270" y="80" width="380" height="100" as="geometry"/></mxCell>
<mxCell id="r0c" value="&lt;font style=&quot;font-size:9px&quot;&gt;Starting point.&lt;br&gt;How to preserve data&lt;br&gt;so pipeline can come back?&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFF3E0;strokeColor=#E65100;strokeWidth=1;fontSize=9;fontColor=#BF360C;" vertex="1" parent="1"><mxGeometry x="670" y="80" width="220" height="100" as="geometry"/></mxCell>
<mxCell id="r1a" value="&lt;b&gt;Step 1: Raw text&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;PDF → OCR → plain text&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#FFCDD2;strokeColor=#C62828;strokeWidth=1.5;fontSize=11;fontColor=#B71C1C;" vertex="1" parent="1"><mxGeometry x="30" y="210" width="220" height="60" as="geometry"/></mxCell>
<mxCell id="r1b" value="&lt;font style=&quot;font-size:9px;font-family:Consolas,monospace&quot;&gt;Hemoglobin 145 138 g/L 130-160&lt;br&gt;ESR 12 18 mm/hr 2-15&lt;br&gt;Cholesterol 4.9 6.2 mmol/L &amp;lt;5.2&lt;br&gt;Target (ESC): Very high &amp;lt;4.0&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FAFAFA;strokeColor=#BDBDBD;strokeWidth=1;fontSize=9;fontColor=#333;" vertex="1" parent="1"><mxGeometry x="270" y="200" width="380" height="80" as="geometry"/></mxCell>
<mxCell id="r1c" value="&lt;font color=&quot;#C62828&quot;&gt;❌ Loses everything&lt;/font&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;Which 145 is which date?&lt;br&gt;No structure, no columns&lt;br&gt;Notes detached from params&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFEBEE;strokeColor=#C62828;strokeWidth=1;fontSize=9;fontColor=#555;" vertex="1" parent="1"><mxGeometry x="670" y="200" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="r2a" value="&lt;b&gt;Step 2a: Markdown&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;VLM → markdown text&lt;br&gt;Tables preserved&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=1.5;fontSize=11;fontColor=#E65100;" vertex="1" parent="1"><mxGeometry x="30" y="310" width="220" height="70" as="geometry"/></mxCell>
<mxCell id="r2b" value="&lt;font style=&quot;font-size:9px;font-family:Consolas,monospace&quot;&gt;| Param | 15.03 | 10.01 | Unit | Range |&lt;br&gt;|------------|-------|-------|--------|---------|&lt;br&gt;| Hemoglobin | 145 | 138 | g/L | 130-160 |&lt;br&gt;| Cholesterol| 4.9 | 6.2 | mmol/L | &amp;lt;5.2 |&lt;br&gt;&lt;br&gt;Target (ESC): Very high &amp;lt;4.0, High &amp;lt;5.0&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FAFAFA;strokeColor=#BDBDBD;strokeWidth=1;fontSize=9;fontColor=#333;" vertex="1" parent="1"><mxGeometry x="270" y="300" width="380" height="105" as="geometry"/></mxCell>
<mxCell id="r2c" value="&lt;font color=&quot;#F9A825&quot;&gt;⚠️ Partial&lt;/font&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;Columns keep dates ✓&lt;br&gt;Visual position lost&lt;br&gt;Card layouts break&lt;br&gt;Risk tiers detached from Chol&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFFDE7;strokeColor=#F9A825;strokeWidth=1;fontSize=9;fontColor=#555;" vertex="1" parent="1"><mxGeometry x="670" y="300" width="220" height="100" as="geometry"/></mxCell>
<mxCell id="r3a" value="&lt;b&gt;Step 2b: HTML&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;VLM → HTML markup&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=1.5;fontSize=11;fontColor=#E65100;" vertex="1" parent="1"><mxGeometry x="30" y="430" width="220" height="55" as="geometry"/></mxCell>
<mxCell id="r3b" value="&lt;font style=&quot;font-size:9px;font-family:Consolas,monospace&quot;&gt;&amp;lt;table&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;HGB&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;145&amp;lt;/td&amp;gt;...&lt;br&gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FAFAFA;strokeColor=#BDBDBD;strokeWidth=1;fontSize=9;fontColor=#333;" vertex="1" parent="1"><mxGeometry x="270" y="425" width="380" height="55" as="geometry"/></mxCell>
<mxCell id="r3c" value="&lt;font color=&quot;#F9A825&quot;&gt;⚠️ Verbose&lt;/font&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;3 params = 500+ chars of tags&lt;br&gt;Huge token cost&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFFDE7;strokeColor=#F9A825;strokeWidth=1;fontSize=9;fontColor=#555;" vertex="1" parent="1"><mxGeometry x="670" y="425" width="220" height="65" as="geometry"/></mxCell>
<mxCell id="r4a" value="&lt;b&gt;Step 3: Table objects&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;Extract structured tables&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#FFCDD2;strokeColor=#C62828;strokeWidth=1.5;fontSize=11;fontColor=#B71C1C;" vertex="1" parent="1"><mxGeometry x="30" y="520" width="220" height="55" as="geometry"/></mxCell>
<mxCell id="r4b" value="&lt;font style=&quot;font-size:9px;font-family:Consolas,monospace&quot;&gt;{columns:[&quot;Param&quot;,&quot;15.03&quot;,&quot;10.01&quot;],&lt;br&gt; rows:[[&quot;HGB&quot;,&quot;145&quot;,&quot;138&quot;], ...]}&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FAFAFA;strokeColor=#BDBDBD;strokeWidth=1;fontSize=9;fontColor=#333;" vertex="1" parent="1"><mxGeometry x="270" y="515" width="380" height="55" as="geometry"/></mxCell>
<mxCell id="r4c" value="&lt;font color=&quot;#C62828&quot;&gt;❌ Not universal&lt;/font&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;Cards, narratives, mixed layouts&lt;br&gt;Risk tiers lost (not a row)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFEBEE;strokeColor=#C62828;strokeWidth=1;fontSize=9;fontColor=#555;" vertex="1" parent="1"><mxGeometry x="670" y="515" width="220" height="65" as="geometry"/></mxCell>
<mxCell id="r5a" value="&lt;b&gt;Step 4a: obs + metadata&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;Two fact types only&lt;br&gt;lab_result + metadata&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=1.5;fontSize=11;fontColor=#E65100;" vertex="1" parent="1"><mxGeometry x="30" y="610" width="220" height="70" as="geometry"/></mxCell>
<mxCell id="r5b" value="&lt;font style=&quot;font-size:9px;font-family:Consolas,monospace&quot;&gt;{type:&quot;lab_result&quot;, name:&quot;HGB&quot;, value:&quot;145&quot;}&lt;br&gt;{type:&quot;metadata&quot;, key:&quot;esc_risk&quot;, val:&quot;&amp;lt;4.0&quot;}&lt;br&gt;{type:&quot;metadata&quot;, key:&quot;esc_high&quot;, val:&quot;&amp;lt;5.0&quot;}&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FAFAFA;strokeColor=#BDBDBD;strokeWidth=1;fontSize=9;fontColor=#333;" vertex="1" parent="1"><mxGeometry x="270" y="605" width="380" height="70" as="geometry"/></mxCell>
<mxCell id="r5c" value="&lt;font color=&quot;#E65100&quot;&gt;⚠️ 33% recall&lt;/font&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;Only latest date! 138 lost!&lt;br&gt;Risk tiers = random metadata keys&lt;br&gt;Metadata = catch-all dump&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFFDE7;strokeColor=#F9A825;strokeWidth=1;fontSize=9;fontColor=#555;" vertex="1" parent="1"><mxGeometry x="670" y="605" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="r6" value="&lt;font style=&quot;font-size:13px&quot;&gt;💡 &lt;b&gt;Insight:&lt;/b&gt; FHIR already formalized what exists in medical documents! fhir-services has 7 types. No need to invent taxonomy.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#BBDEFB;strokeColor=#1565C0;strokeWidth=2;fontSize=11;fontColor=#0D47A1;" vertex="1" parent="1"><mxGeometry x="30" y="720" width="860" height="45" as="geometry"/></mxCell>
<mxCell id="r7a" value="&lt;b&gt;✅ Fact-based extraction&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;8 FHIR types, anyOf strict schema&lt;br&gt;+ date per fact&lt;br&gt;+ typed notes [{type, text}]&lt;br&gt;+ alt_name (bilingual)&lt;br&gt;Single-pass GPT-5.2&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=11;fontColor=#1B5E20;" vertex="1" parent="1"><mxGeometry x="30" y="795" width="220" height="130" as="geometry"/></mxCell>
<mxCell id="r7b" value="&lt;font style=&quot;font-size:9px;font-family:Consolas,monospace&quot;&gt;{type:&quot;observation&quot;, name:&quot;HGB&quot;,&lt;br&gt; value:&quot;145&quot;, &lt;b&gt;date:&quot;2026-03-15&quot;&lt;/b&gt;,&lt;br&gt; category:&quot;laboratory&quot;, notes:[]}&lt;br&gt;&lt;br&gt;{type:&quot;observation&quot;, name:&quot;HGB&quot;,&lt;br&gt; value:&quot;&lt;b&gt;138&lt;/b&gt;&quot;, &lt;b&gt;date:&quot;2026-01-10&quot;&lt;/b&gt;}&lt;br&gt;&lt;br&gt;{type:&quot;observation&quot;, name:&quot;Chol&quot;,&lt;br&gt; value:&quot;6.2&quot;, date:&quot;2026-01-10&quot;,&lt;br&gt; &lt;b&gt;notes&lt;/b&gt;:[{type:&quot;risk_classification&quot;,&lt;br&gt; text:&quot;ESC: VHigh &amp;lt;4.0, High &amp;lt;5.0&quot;}]}&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#F1F8E9;strokeColor=#2E7D32;strokeWidth=1;fontSize=9;fontColor=#333;" vertex="1" parent="1"><mxGeometry x="270" y="795" width="380" height="200" as="geometry"/></mxCell>
<mxCell id="r7c" value="&lt;font color=&quot;#2E7D32&quot;&gt;&lt;b&gt;✅ 92%+ recall&lt;br&gt;✅ 98% value accuracy&lt;/b&gt;&lt;/font&gt;&lt;br&gt;&lt;br&gt;&lt;font style=&quot;font-size:9px&quot;&gt;✓ Both dates extracted&lt;br&gt;✓ Risk tiers in notes&lt;br&gt;✓ Atomic facts → FHIR&lt;br&gt;✓ 6+ languages&lt;br&gt;✓ 11 dates / 18 pages&lt;br&gt;✓ 308 facts / 27 pages&lt;br&gt;✓ Page-break notes ✓&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;fillColor=#E8F5E9;strokeColor=#2E7D32;strokeWidth=1.5;fontSize=10;fontColor=#1B5E20;" vertex="1" parent="1"><mxGeometry x="670" y="795" width="220" height="185" as="geometry"/></mxCell>
<mxCell id="r8" value="&lt;font style=&quot;font-size:13px&quot;&gt;🔑 &lt;b&gt;Problem was in PROMPT + SCHEMA, not in PIPELINE.&lt;/b&gt; Old: extract parameters → 33%. New: extract facts + anyOf → 100%. Same model. Single pass.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;shadow=1;arcSize=15;fillColor=#E1BEE7;strokeColor=#7B1FA2;strokeWidth=2;fontSize=11;fontColor=#4A148C;" vertex="1" parent="1"><mxGeometry x="30" y="1030" width="860" height="45" as="geometry"/></mxCell>
</root>
</mxGraphModel>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment