prepared for productoin
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
arithmedic.egg-info/
|
||||
.env
|
||||
|
||||
@@ -1,5 +1,66 @@
|
||||
# ArithMedic
|
||||
|
||||
LLM-first medical calculations.
|
||||
ArithMedic is a free, open source medical calculator API. It is FHIR-native, EU-hosted, and formula-auditable.
|
||||
|
||||
ArithMedic offers way to address the problem of hallucinated or inconsistent clinical math in LLM‑agents used by health‑tech teams, so that agents can reliably call standardized medical scores like eGFR and CHA₂DS₂‑VASc.
|
||||
## Example:
|
||||
Request body:
|
||||
```
|
||||
curl -X 'POST' \
|
||||
'https://www.arithmedic.eu/fhir/egfr/ckd-epi-2021' \
|
||||
-H 'accept: application/json' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"resourceType": "Parameters",
|
||||
"parameter": [
|
||||
{
|
||||
"name": "serum-creatinine",
|
||||
"valueQuantity": {
|
||||
"value": 88,
|
||||
"unit": "umol/L"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "age",
|
||||
"valueInteger": 65
|
||||
},
|
||||
{
|
||||
"name": "sex",
|
||||
"valueCode": "female"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response body:
|
||||
```
|
||||
{
|
||||
"resourceType": "Parameters",
|
||||
"parameter": [
|
||||
{
|
||||
"name": "egfr",
|
||||
"valueQuantity": {
|
||||
"value": 63,
|
||||
"unit": "mL/min/1.73m2",
|
||||
"system": "http://unitsofmeasure.org",
|
||||
"code": "mL/min/{1.73_m2}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ckd-stage-code",
|
||||
"valueCode": "G2"
|
||||
},
|
||||
{
|
||||
"name": "ckd-stage-description",
|
||||
"valueString": "Mildly decreased (60–89)"
|
||||
},
|
||||
{
|
||||
"name": "formula",
|
||||
"valueString": "2021 CKD-EPI Creatinine"
|
||||
},
|
||||
{
|
||||
"name": "reference",
|
||||
"valueUri": "https://doi.org/10.1056/NEJMoa2102953"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from api.egfr import router as egfr_router
|
||||
|
||||
router = APIRouter(prefix="/fhir")
|
||||
router.include_router(egfr_router)
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi.templating import Jinja2Templates
|
||||
from fhir.resources.parameters import Parameters
|
||||
|
||||
from calculators.egfr.ckd_epi_2021 import ckd_epi_2021
|
||||
from fhir_mapping.egfr.ckd_epi_2021 import build_output, parse_input
|
||||
from fhir_mappings.egfr.ckd_epi_2021 import build_output, parse_input
|
||||
from schemas.egfr.ckd_epi_2021 import CkdEpi2021Output
|
||||
|
||||
FORMULA = "2021 CKD-EPI Creatinine"
|
||||
@@ -123,29 +123,3 @@ def calculate_ckd_epi_2021(params: Parameters) -> dict:
|
||||
|
||||
return build_output(output).model_dump()
|
||||
|
||||
|
||||
@router.post("/ckd-epi-2021/calculate", response_class=HTMLResponse, include_in_schema=False)
|
||||
def calculate_ckd_epi_2021_form(
|
||||
request: Request,
|
||||
serum_creatinine: Annotated[float, Form(gt=0)],
|
||||
unit: Annotated[Literal["umol/L", "mg/dL"], Form()] = "umol/L",
|
||||
age: Annotated[int, Form(ge=18, le=120)] = None,
|
||||
sex: Annotated[Literal["male", "female"], Form()] = None,
|
||||
) -> HTMLResponse:
|
||||
"""Accept a form POST from the HTMX UI and return an HTML partial."""
|
||||
egfr = ckd_epi_2021(
|
||||
serum_creatinine=serum_creatinine,
|
||||
unit=unit,
|
||||
age=age,
|
||||
sex=sex,
|
||||
)
|
||||
output = CkdEpi2021Output(
|
||||
egfr=round(egfr),
|
||||
formula=FORMULA,
|
||||
reference_url=REFERENCE_URL,
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"egfr/ckd_epi_2021_result.html",
|
||||
context=output.model_dump(),
|
||||
)
|
||||
|
||||
@@ -54,3 +54,23 @@ def ckd_epi_2021(serum_creatinine: float, age: int, sex: str, unit: str = "umol/
|
||||
|
||||
# Female sex modifier (Table 1)
|
||||
return result * 1.012 if sex == "female" else result
|
||||
|
||||
|
||||
def ckd_stage(egfr: int) -> tuple[str, str]:
|
||||
"""
|
||||
Return the KDIGO CKD stage code and description for a given eGFR.
|
||||
|
||||
Reference: KDIGO 2012 Clinical Practice Guideline for CKD, Table 1.
|
||||
https://doi.org/10.1038/kisup.2012.73
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[str, str]
|
||||
(code, description) e.g. ("G3a", "Mild–moderate decrease (45–59)")
|
||||
"""
|
||||
if egfr >= 90: return "G1", "Normal or high (≥ 90)"
|
||||
if egfr >= 60: return "G2", "Mildly decreased (60–89)"
|
||||
if egfr >= 45: return "G3a", "Mild–moderate decrease (45–59)"
|
||||
if egfr >= 30: return "G3b", "Moderate–severe decrease (30–44)"
|
||||
if egfr >= 15: return "G4", "Severely decreased (15–29)"
|
||||
return "G5", "Kidney failure (< 15)"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
from fhir.resources.parameters import Parameters
|
||||
from fhir.resources.quantity import Quantity
|
||||
|
||||
from calculators.egfr.ckd_epi_2021 import ckd_stage
|
||||
from schemas.egfr.ckd_epi_2021 import CkdEpi2021Input, CkdEpi2021Output
|
||||
|
||||
# Accepted UCUM unit codes → internal unit strings
|
||||
@@ -92,10 +93,13 @@ def build_output(result: CkdEpi2021Output) -> Parameters:
|
||||
Serialize CKD-EPI output as a FHIR Parameters resource.
|
||||
|
||||
Returned parameters:
|
||||
egfr valueQuantity eGFR in mL/min/1.73m² (UCUM)
|
||||
formula valueString Human-readable formula name
|
||||
reference valueUri DOI of the source publication
|
||||
egfr valueQuantity eGFR in mL/min/1.73m² (UCUM)
|
||||
ckd-stage-code valueCode KDIGO stage code e.g. "G3a"
|
||||
ckd-stage-description valueString KDIGO stage description
|
||||
formula valueString Human-readable formula name
|
||||
reference valueUri DOI of the source publication
|
||||
"""
|
||||
code, description = ckd_stage(result.egfr)
|
||||
return Parameters(**{
|
||||
"parameter": [
|
||||
{
|
||||
@@ -107,13 +111,9 @@ def build_output(result: CkdEpi2021Output) -> Parameters:
|
||||
"code": "mL/min/{1.73_m2}",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "formula",
|
||||
"valueString": result.formula,
|
||||
},
|
||||
{
|
||||
"name": "reference",
|
||||
"valueUri": result.reference_url,
|
||||
},
|
||||
{"name": "ckd-stage-code", "valueCode": code},
|
||||
{"name": "ckd-stage-description", "valueString": description},
|
||||
{"name": "formula", "valueString": result.formula},
|
||||
{"name": "reference", "valueUri": result.reference_url},
|
||||
]
|
||||
})
|
||||
@@ -1,40 +1,47 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from api.egfr import router as egfr_router
|
||||
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from api import router as api_router
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
api_base_url: str = "http://localhost:8000"
|
||||
production: bool = False
|
||||
|
||||
settings = Settings()
|
||||
|
||||
if settings.production:
|
||||
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="127.0.0.1")
|
||||
app.add_middleware(
|
||||
TrustedHostMiddleware,
|
||||
allowed_hosts=["arithmedic.eu", "www.arithmedic.eu"],
|
||||
)
|
||||
|
||||
app = FastAPI(
|
||||
title="ArithMedic",
|
||||
description="LLM-Agent First, Open-Source Medical Calculations.",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
app.include_router(egfr_router)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://arithmedic.eu"],
|
||||
allow_methods=["POST"],
|
||||
allow_headers=["Content-Type"],
|
||||
)
|
||||
|
||||
app.include_router(api_router)
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
# Registry of available calculators shown on the landing page.
|
||||
# Add an entry here whenever a new calculator is added.
|
||||
CALCULATORS = {
|
||||
"Renal": [
|
||||
{
|
||||
"name": "eGFR — CKD-EPI 2021",
|
||||
"tag": "eGFR",
|
||||
"description": (
|
||||
"Estimates glomerular filtration rate using the 2021 CKD-EPI "
|
||||
"Creatinine equation. Race-free, validated for adults ≥ 18 years."
|
||||
),
|
||||
"url": "/egfr/ckd-epi-2021",
|
||||
},
|
||||
],
|
||||
}
|
||||
templates.env.globals["api_base_url"] = settings.api_base_url
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
def index(request: Request) -> HTMLResponse:
|
||||
return templates.TemplateResponse(
|
||||
request, "index.html", context={"calculators": CALCULATORS}
|
||||
)
|
||||
return templates.TemplateResponse(request, "index.html")
|
||||
|
||||
+6
-5
@@ -221,14 +221,14 @@
|
||||
/* ── Submit button ───────────────────────────── */
|
||||
button[type="submit"] {
|
||||
all: unset;
|
||||
display: inline-block;
|
||||
justify-self: start;
|
||||
display: block;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-family: var(--sans);
|
||||
font-size: 0.72rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.1em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
padding: 0.7rem 1.5rem;
|
||||
cursor: pointer;
|
||||
@@ -397,12 +397,13 @@
|
||||
|
||||
<header>
|
||||
<a class="wordmark" href="/">ArithMedic</a>
|
||||
<div class="tagline">Clinical calculations you can audit</div>
|
||||
<div class="tagline">Open Source FHIR Medical Calculations API</div>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<a href="/">Calculators</a>
|
||||
<a href="/docs">API</a>
|
||||
<a href="/">Home</a>
|
||||
<a href="/fhir/egfr/ckd-epi-2021">Example Calculator</a>
|
||||
<a href="/docs">API docs</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}CKD-EPI 2021 · ArithMedic{% endblock %}
|
||||
{% block title %}ArithMedic — {{ formula }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-heading">
|
||||
<h1>eGFR Calculator</h1>
|
||||
<p>2021 CKD-EPI Creatinine equation — race-free, validated for adults ≥ 18 years.</p>
|
||||
<div class="section-label">eGFR</div>
|
||||
<h1>{{ formula }}</h1>
|
||||
<p>Race-free eGFR equation, validated for adults ≥ 18 years.</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
hx-post="/egfr/ckd-epi-2021/calculate"
|
||||
hx-target="#result"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#calculating"
|
||||
>
|
||||
<form id="egfr-form">
|
||||
<div class="form-grid">
|
||||
|
||||
<div class="field">
|
||||
@@ -65,7 +61,9 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p id="calculating" class="htmx-indicator">Calculating…</p>
|
||||
<p id="calculating" style="display:none; font-size:0.7rem; color:var(--ink-muted); letter-spacing:0.08em; text-align:center; margin-top:1rem;">
|
||||
Calculating…
|
||||
</p>
|
||||
|
||||
<div id="result"></div>
|
||||
|
||||
@@ -77,4 +75,77 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = "{{ api_base_url }}/fhir/egfr/ckd-epi-2021";
|
||||
|
||||
function get(fhirResponse, name) {
|
||||
return fhirResponse.parameter.find(p => p.name === name);
|
||||
}
|
||||
|
||||
function renderResult(data) {
|
||||
const egfr = get(data, "egfr")?.valueQuantity;
|
||||
const stageCode = get(data, "ckd-stage-code")?.valueCode;
|
||||
const stageDesc = get(data, "ckd-stage-description")?.valueString;
|
||||
return `
|
||||
<div class="result-card" style="margin-top:2rem;">
|
||||
<div class="egfr-value">${egfr.value}</div>
|
||||
<div class="egfr-unit">${egfr.unit}</div>
|
||||
<div class="badge ${stageCode.toLowerCase()}">${stageCode} — ${stageDesc}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderError(msg) {
|
||||
return `<div class="error-card" style="margin-top:2rem;">${msg}</div>`;
|
||||
}
|
||||
|
||||
function buildFhirParams(creatinine, unit, age, sex) {
|
||||
return {
|
||||
resourceType: "Parameters",
|
||||
parameter: [
|
||||
{ name: "serum-creatinine", valueQuantity: { value: parseFloat(creatinine), unit } },
|
||||
{ name: "age", valueInteger: parseInt(age, 10) },
|
||||
{ name: "sex", valueCode: sex },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById("egfr-form").addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const indicator = document.getElementById("calculating");
|
||||
const resultDiv = document.getElementById("result");
|
||||
|
||||
const body = buildFhirParams(
|
||||
form.serum_creatinine.value,
|
||||
form.querySelector('input[name="unit"]:checked').value,
|
||||
form.age.value,
|
||||
form.querySelector('input[name="sex"]:checked').value,
|
||||
);
|
||||
|
||||
indicator.style.display = "block";
|
||||
resultDiv.innerHTML = "";
|
||||
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
resultDiv.innerHTML = renderError(err.detail ?? `Error ${res.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
resultDiv.innerHTML = renderResult(data);
|
||||
} catch (err) {
|
||||
resultDiv.innerHTML = renderError(`Network error: ${err.message}`);
|
||||
} finally {
|
||||
indicator.style.display = "none";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
+3
-14
@@ -4,19 +4,8 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% for category, calcs in calculators.items() %}
|
||||
<p style="font-size:0.65rem; letter-spacing:0.12em; text-transform:uppercase; color:var(--ink-muted); margin-bottom:0.6rem; {% if not loop.first %}margin-top:2rem;{% endif %}">
|
||||
{{ category }}
|
||||
</p>
|
||||
<div class="calc-list">
|
||||
{% for calc in calcs %}
|
||||
<a class="calc-card" href="{{ calc.url }}">
|
||||
<div class="calc-category">{{ calc.tag }}</div>
|
||||
<div class="calc-name">{{ calc.name }}</div>
|
||||
<div class="calc-desc">{{ calc.description }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p>Welcome to ArithMedic.eu, the open source medical calculator API that is free forever, for all.</p>
|
||||
<br>
|
||||
<p>Example curl call here.</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user