From b261263c5e66dc7690a03b15a6560c212704e3a8 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 16:54:03 +0000
Subject: [PATCH 01/33] Add all top-level DSM categories to dsm.yaml
---
dsm.yaml | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/dsm.yaml b/dsm.yaml
index e69de29..0d9c15e 100644
--- a/dsm.yaml
+++ b/dsm.yaml
@@ -0,0 +1,22 @@
+Neurodevelopmental Disorders: []
+Schizophrenia Spectrum and Other Psychotic Disorders: []
+Bipolar and Related Disorders: []
+Depressive Disorders: []
+Anxiety Disorders: []
+Obsessive-Compulsive and Related Disorders: []
+Trauma- and Stressor-Related Disorders: []
+Dissociative Disorders: []
+Somatic Symptom and Related Disorders: []
+Feeding and Eating Disorders: []
+Elimination Disorders: []
+Sleep-Wake Disorders: []
+Sexual Dysfunctions: []
+Gender Dysphoria: []
+Disruptive, Impulse-Control, and Conduct Disorders: []
+Substance-Related and Addictive Disorders: []
+Neurocognitive Disorders: []
+Personality Disorders: []
+Paraphilic Disorders: []
+Other Mental Disorders and Additonal Codes: []
+Medication-Induced Movement Disorders and Other Adverse Effects of Medication: []
+Other Conditions That May Be a Focus of Clinical Attention: []
\ No newline at end of file
From 656c161f79003bf85326bd9535da668e9a9a4fbd Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:01:54 +0000
Subject: [PATCH 02/33] Add config and hide navbar
---
.streamlit/config.toml | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 .streamlit/config.toml
diff --git a/.streamlit/config.toml b/.streamlit/config.toml
new file mode 100644
index 0000000..74812cd
--- /dev/null
+++ b/.streamlit/config.toml
@@ -0,0 +1,2 @@
+[client]
+showSidebarNavigation = false
\ No newline at end of file
From 43ae7756c3810a1693ef425a62aeb7584b45a745 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:02:39 +0000
Subject: [PATCH 03/33] Set up homepage
---
main.py | 28 +++++++++++++++++++++++++---
1 file changed, 25 insertions(+), 3 deletions(-)
diff --git a/main.py b/main.py
index 14ca0b8..2ac71bd 100644
--- a/main.py
+++ b/main.py
@@ -1,6 +1,28 @@
import streamlit as st
-import pyyaml
+import yaml
-st.set_page_config(page_title="Arwen DSM", layout="wide")
+# Load data
+with open("router.yaml", "r") as f:
+ data = yaml.safe_load(f)
-st.title("Arwen DSM-5 Diagnostic Criteria Aggregator")
+st.set_page_config(page_title="Arwen DCA", layout="centered")
+
+st.title("Diagnostic Criteria Aggregator")
+
+cat = st.selectbox("Category", list(data.keys()))
+dis = st.selectbox("Disorder", list(data[cat].keys()))
+
+if st.button("Open Assessment"):
+ # 1. Store the selection so the next page can see it
+ st.session_state.current_cat = cat
+ st.session_state.current_dis = dis
+ st.session_state.current_dis_data = data[cat][dis]["data_path"]
+
+ # 2. Forwarding Logic
+ logic_type = data[cat][dis].get("logic", "simple")
+
+ if logic_type == "complex":
+ # Note: You must provide the path relative to the root
+ st.switch_page("pages/adhd.py")
+ else:
+ st.switch_page("pages/simple.py")
\ No newline at end of file
From fec2d651b7b408d8ead8296509ad7bee836dac7d Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:03:01 +0000
Subject: [PATCH 04/33] Add page router config
---
router.yaml | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
create mode 100644 router.yaml
diff --git a/router.yaml b/router.yaml
new file mode 100644
index 0000000..1f26eb0
--- /dev/null
+++ b/router.yaml
@@ -0,0 +1,55 @@
+Neurodevelopmental Disorders:
+ Attention-Deficit/Hyperactivity Disorder:
+ logic: complex
+ display_name: "Attention-Deficit/Hyperactivity Disorder"
+ icd_10: F90.2, F90.0, F90.1
+ data_path: "data/adhd.yaml"
+ Autism Spectrum Disorder:
+ logic: simple
+ display_name: "Autism Spectrum Disorder"
+ icd_10: F84.0, F84.5, F84.8, F84.9
+ data_path: "data/asd.yaml"
+Depressive Disorders:
+ Major Depressive Disorder:
+ logic: complex
+ display_name: "Major Depressive Disorder"
+ icd_10: F32.0-F32.9, F33.0-F33.9
+ data_path: "data/mdd.yaml"
+ Persistent Depressive Disorder:
+ logic: complex
+ display_name: "Persistent Depressive Disorder (Dysthymia)"
+ icd_10: F34.1
+ data_path: "data/pdd.yaml"
+Anxiety Disorders:
+ Generalized Anxiety Disorder:
+ logic: complex
+ display_name: "Generalized Anxiety Disorder"
+ icd_10: F41.1
+ data_path: "data/gad.yaml"
+Trauma- and Stressor-Related Disorders:
+ Post-Traumatic Stress Disorder:
+ logic: complex
+ display_name: "Post-Traumatic Stress Disorder"
+ icd_10: F43.1
+ data_path: "data/ptsd.yaml"
+Personality Disorders:
+ Antisocial Personality Disorder:
+ logic: complex
+ display_name: "Antisocial Personality Disorder"
+ icd_10: F60.2
+ data_path: "data/aspd.yaml"
+ Borderline Personality Disorder:
+ logic: simple
+ display_name: "Borderline Personality Disorder"
+ icd_10: F60.3
+ data_path: none
+ Histrionic Personality Disorder:
+ logic: simple
+ display_name: "Histrionic Personality Disorder"
+ icd_10: F60.4
+ data_path: none
+ Narcissistic Personality Disorder:
+ logic: simple
+ display_name: "Narcissistic Personality Disorder"
+ icd_10: F60.81
+ data_path: none
\ No newline at end of file
From 83907d0e34ca512ac889b589cb1ff317c0f53e61 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:03:17 +0000
Subject: [PATCH 05/33] Add simple placeholder page
---
pages/simple.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 pages/simple.py
diff --git a/pages/simple.py b/pages/simple.py
new file mode 100644
index 0000000..96fe399
--- /dev/null
+++ b/pages/simple.py
@@ -0,0 +1,14 @@
+import streamlit as st
+import yaml
+
+# Safety check
+if "current_dis" not in st.session_state:
+ st.switch_page("main.py")
+
+st.title(f"{st.session_state.current_dis}")
+
+
+
+# Back button
+if st.button("<- Change Disorder"):
+ st.switch_page("main.py")
\ No newline at end of file
From 76b0f4854a16402f96947670432c31b2c865a5eb Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:03:31 +0000
Subject: [PATCH 06/33] Add ADHD page and data
---
data/adhd.yaml | 36 ++++++++++++++
pages/adhd.py | 132 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 168 insertions(+)
create mode 100644 data/adhd.yaml
create mode 100644 pages/adhd.py
diff --git a/data/adhd.yaml b/data/adhd.yaml
new file mode 100644
index 0000000..d59f030
--- /dev/null
+++ b/data/adhd.yaml
@@ -0,0 +1,36 @@
+A: "A persistent pattern of inattention and/or hyperactivityimpulsivity that interferes with functioning or development, as characterized by (1) and/or (2):"
+A1: "Inattention: Six (or more) of the following symptoms have persisted for at least 6 months to a degree that is inconsistent with developmental level and that negatively impacts directly on social and academic/occupational activities:"
+A1note: "Note: The symptoms are not solely a manifestation of oppositional behavior, defiance, hostility, or failure to understand tasks or instructions. For older adolescents ad adults (age 17 and older), at least five symptoms are required."
+A1a: "Often fails to give close attention to details or makes careless mistakes in schoolwork, at work, or during other activities (e.g., overlooks or misses details, work is inaccurate)."
+A1b: "Often has difficulty sustaining attention in tasks or play activities (e.g., has difficulty remaining focused during lectures, conversations, or lengthy reading)."
+A1c: "Often does not seem to listen when spoken to directly (e.g., mind seems elsewhere, even in the absence of any obvious distraction)."
+A1d: "Often does not follow through on instructions and fails to finish schoolwork, chores, or duties in the workplace (e.g., starts tasks but quickly loses focus and is easily sidetracked)."
+A1e: "Often has difficulty organizing tasks and activities (e.g., difficulty managing sequential tasks; difficulty keeping materials and belongings in order; messy, disorganized work; has poor time management; fails to meet deadlines)."
+A1f: "Often avoids, dislikes, or is reluctant to engage in tasks that require sustained mental effort (e.g., schoolwork or homework; for older adolescents and adults, preparing reports, completing forms, reviewing lengthy papers)."
+A1g: "Often loses things necessary for tasks or activities (e.g., school materials, pencils, books, tools, wallets, keys, paperwork, eyeglasses, mobile telephones)."
+A1h: "Is often easily distracted by extraneous stimuli (for older adolescents and adults, may include unrelated thoughts)."
+A1i: "Is often forgetful in daily activities (e.g., doing chores, running errands; for older adolescents and adults, returning calls, paying bills, keeping appointments)."
+A2: "Hyperactivity and Impulsivity: Six (or more) of the following symptoms have persisted for at least 6 months to a degree that is inconsistent with developmental level and that negatively impacts directly on social and academic/occupational activities:"
+A2note: "Note: The symptoms are not solely a manifestation of oppositional behavior, defiance, hostility, or failure to understand tasks or instructions. For older adolescents ad adults (age 17 and older), at least five symptoms are required."
+A2a: "Often fidgets with or taps hands or feet or squirms in seat."
+A2b: "Often leaves seat in situations when remaining seated is expected (e.g., leaves his or her place in the classroom, in the office or other workplace, or in other situations that require remaining in place)."
+A2c: "Often runs about or climbs in situations where it is inappropriate. (Note: In adolescents or adults, may be limited to feeling restless.)"
+A2d: "Often unable to play or engage in leisure activities quietly."
+A2e: "Is often 'on the go,' acting as if 'driven by a motor' (e.g., is unable to be or uncomfortable being still for extended time, as in restaurants, meetings; may be experienced by others as being restless or difficult to keep up with)."
+A2f: "Often talks excessively."
+A2g: "Often blurts out an answer before a question has been completed (e.g., completes people's sentences; cannot wait for turn in conversation)."
+A2h: "Often has difficulty waiting his or her turn (e.g., while waiting in line)."
+A2i: "Often interrupts or intrudes on others (e.g., butts into conversations, games, or activities; may start using other people's things without asking or receiving permission; for adolescents and adults, may intrude into or take over what others are doing)."
+B: "Several inattentive or hyperactive-impulsive symptoms were present prior to age 12 years."
+C: "Several inattentive or hyperactive-impulsive symptoms are present in two or more settings (e.g., at home, school, or work; with friends or relatives; in other activities)."
+D: "There is clear evidence that the symptoms interfere with, or reduce the quality of, social, academic, or occupational functioning."
+E: "The symptoms do not occur exclusively during the course of schizophrenia or another psychotic disorder and are not better explained by another mental disorder (e.g., mood disorder, anxiety disorder, dissociative disorder, personality disorder, substance intoxication or withdrawal)."
+S1a: "Both inattentive and hyperactive-impulsive presentation: If both Criterion A1 and Criterion A2 are met for the past 6 months."
+S1b: "Predominantly inattentive presentation: If Criterion A1 is met but Criterion A2 is not met for the past 6 months."
+S1c: "Predominantly hyperactive-impulsive presentation: If Criterion A2 is met but Criterion A1 is not met for the past 6 months."
+S2: "In partial remission: Full criteria were previously met, but symptoms currently do not meet full criteria, and the symptoms that are present cause clinically significant impairment in social, academic, or occupational functioning."
+S3a: "Mild: Few, if any, symptoms in excess of those required to make the diagnosis are present, and symptoms result in no more than mild impairment in social or occupational functioning."
+S3b: "Moderate: Symptoms or functional impairment between 'mild' and 'severe' are present."
+S3c: "Severe: Many symptoms in excess of those required to make the diagnosis, or several symptoms that are particularly severe, are present, and symptoms result in marked impairment in social or occupational functioning."
+S1: none # key only
+S3: none # key only
\ No newline at end of file
diff --git a/pages/adhd.py b/pages/adhd.py
new file mode 100644
index 0000000..031f25f
--- /dev/null
+++ b/pages/adhd.py
@@ -0,0 +1,132 @@
+import streamlit as st
+import yaml
+
+# Safety check
+if "current_dis" not in st.session_state:
+ st.switch_page("main.py")
+
+with open(st.session_state.current_dis_data, "r") as f:
+ data = yaml.safe_load(f)
+
+for key in data.keys():
+ if key not in st.session_state:
+ st.session_state[key] = False
+
+st.title(f"{st.session_state.current_dis}")
+
+st.markdown("## Client Metadata")
+
+age = st.radio("Client Age", ["Under 17", "17 or older"], key="age")
+threshold = 5 if age == "17 or older" else 6
+
+a1_score = sum(st.session_state.get(f"A1{char}", False) for char in "abcdefghi")
+a2_score = sum(st.session_state.get(f"A2{char}", False) for char in "abcdefghi")
+a1_score_badge = f":red-badge[{a1_score}/{threshold}]" if a1_score < threshold else f":green-badge[{a1_score}/{threshold}]"
+a2_score_badge = f":red-badge[{a2_score}/{threshold}]" if a2_score < threshold else f":green-badge[{a2_score}/{threshold}]"
+
+st.session_state.A1 = a1_score >= threshold
+st.session_state.A2 = a2_score >= threshold
+st.session_state.A = st.session_state.A1 or st.session_state.A2
+
+if st.session_state.A1 and st.session_state.A2:
+ S1text = [f":blue[**F90.2**] Combined presentation :green-badge[RECOMMENDED]\n\n{data['S1a']}", f":blue[**F90.0**] Predominantly inattentive presentation\n\n{data['S1b']}", f":blue[**F90.1**] Predominantly hyperactive/impulsive presentation\n\n{data['S1c']}"]
+ recommended = ":blue[**F90.2**] Combined presentation"
+elif st.session_state.A1:
+ S1text = [f":blue[**F90.2**] Combined presentation\n\n{data['S1a']}", f":blue[**F90.0**] Predominantly inattentive presentation :green-badge[RECOMMENDED]\n\n{data['S1b']}", f":blue[**F90.1**] Predominantly hyperactive/impulsive presentation\n\n{data['S1c']}"]
+ recommended = ":blue[**F90.0**] Predominantly inattentive presentation"
+elif st.session_state.A2:
+ S1text = [f":blue[**F90.2**] Combined presentation\n\n{data['S1a']}", f":blue[**F90.0**] Predominantly inattentive presentation\n\n{data['S1b']}", f":blue[**F90.1**] Predominantly hyperactive/impulsive presentation :green-badge[RECOMMENDED]\n\n{data['S1c']}"]
+ recommended = ":blue[**F90.1**] Predominantly hyperactive/impulsive presentation"
+else:
+ S1text = [f":blue[**F90.2**] Combined presentation\n\n{data['S1a']}", f":blue[**F90.0**] Predominantly inattentive presentation\n\n{data['S1b']}", f":blue[**F90.1**] Predominantly hyperactive/impulsive presentation\n\n{data['S1c']}"]
+ recommended = None
+
+st.markdown("## Diagnostic Criteria")
+st.info("Criteria A, A1, and A2 will check automatically based on sub-criteria.", icon="ℹ️")
+
+A = st.checkbox(f"A. {data['A']}", key="A", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ A1 = st.checkbox(f"1\\. {data['A1'].replace('Inattention', '**Inattention**', 1)}\n\n:orange[{data['A1note']}]\n\n{a1_score_badge}", key="A1", disabled=True)
+ subcol1, subcol2 = st.columns([0.05, 0.95])
+ with subcol2:
+ A1a = st.checkbox(f"a. {data['A1a']}", key="A1a")
+ A1b = st.checkbox(f"b. {data['A1b']}", key="A1b")
+ A1c = st.checkbox(f"c. {data['A1c']}", key="A1c")
+ A1d = st.checkbox(f"d. {data['A1d']}", key="A1d")
+ A1e = st.checkbox(f"e. {data['A1e']}", key="A1e")
+ A1f = st.checkbox(f"f. {data['A1f']}", key="A1f")
+ A1g = st.checkbox(f"g. {data['A1g']}", key="A1g")
+ A1h = st.checkbox(f"h. {data['A1h']}", key="A1h")
+ A1i = st.checkbox(f"i. {data['A1i']}", key="A1i")
+ A2 = st.checkbox(f"2\\. {data['A2'].replace('Hyperactivity and Impulsivity', '**Hyperactivity and Impulsivity**', 1)}\n\n:orange[{data['A2note']}]\n\n{a2_score_badge}", key="A2", disabled=True)
+ subcol3, subcol4 = st.columns([0.05, 0.95])
+ with subcol4:
+ A2a = st.checkbox(f"a. {data['A2a']}", key="A2a")
+ A2b = st.checkbox(f"b. {data['A2b']}", key="A2b")
+ A2c = st.checkbox(f"c. {data['A2c']}", key="A2c")
+ A2d = st.checkbox(f"d. {data['A2d']}", key="A2d")
+ A2e = st.checkbox(f"e. {data['A2e']}", key="A2e")
+ A2f = st.checkbox(f"f. {data['A2f']}", key="A2f")
+ A2g = st.checkbox(f"g. {data['A2g']}", key="A2g")
+ A2h = st.checkbox(f"h. {data['A2h']}", key="A2h")
+ A2i = st.checkbox(f"i. {data['A2i']}", key="A2i")
+B = st.checkbox(f"B. {data['B']}", key="B")
+C = st.checkbox(f"C. {data['C']}", key="C")
+D = st.checkbox(f"D. {data['D']}", key="D")
+E = st.checkbox(f"E. {data['E']}", key="E")
+
+st.markdown("## Specifiers")
+st.info("Presentation recommendation is based on criteria A1 and A2, but you can select otherwise.", icon="ℹ️")
+
+S1 = st.radio("Specify whether:", S1text, key="S1")
+st.markdown(":small[Specify if:]")
+S2 = st.checkbox(f"{data['S2']}", key="S2")
+S3 = st.radio("Specify current severity:", [f":green[**Mild**]\n\n{data['S3a'].replace("Mild: ", "")}", f":orange[**Moderate**]\n\n{data['S3b'].replace("Moderate: ", "")}", f":red[**Severe**]\n\n{data['S3c'].replace("Severe: ", "")}"], key="S3")
+
+st.markdown("## Output")
+
+omitBCDE = st.toggle("Ignore criteria B, C, D, and E", key="omitBCDE", value=True)
+
+if omitBCDE:
+ st.info("Output will not include criteria B, C, D, and E.", icon="ℹ️")
+
+if not omitBCDE:
+ criteria_met = all([st.session_state.get(key, False) for key in ["A", "B", "C", "D", "E"]])
+ if not criteria_met:
+ unmet = [key for key in ["A", "B", "C", "D", "E"] if not st.session_state.get(key, False)]
+ st.error(f"Criteria not met: **{'**, **'.join(unmet)}**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-E are met.\n\nConsider diagnosing :blue[F90.8] Other Specified AttentionDeficit/Hyperactivity Disorder or :blue[F90.9] Unspecified Attention-Deficit/Hyperactivity Disorder", icon="🚨")
+else:
+ criteria_met = st.session_state.A
+ if not criteria_met:
+ st.error(f"Criteria not met: **A**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-E are met.\n\nConsider diagnosing :blue[F90.8] Other Specified AttentionDeficit/Hyperactivity Disorder or :blue[F90.9] Unspecified Attention-Deficit/Hyperactivity Disorder", icon="🚨")
+
+if ":green-badge[RECOMMENDED]" not in S1 and st.session_state.A:
+ st.warning(f"Presentation specifier does not match recommendation. **{S1.split('\n\n')[0]}** was selected instead of **{recommended}**.", icon="⚠️")
+
+st.markdown(":blue-badge[Triple click anywhere in the text below to select all of it at once!]")
+with st.container(border=True):
+ data_out = data.copy()
+ output = []
+ pop = ["A", "A1", "A1note", "A2", "A2note", "S1a", "S1b", "S1c", "S2", "S3a", "S3b", "S3c", "S1", "S3"]
+ if omitBCDE:
+ pop.extend(["B", "C", "D", "E"])
+ for p in pop:
+ data_out.pop(p, None)
+ for key in data_out.keys():
+ if st.session_state.get(key, False):
+ if key == "A2c":
+ output.append(data_out[key].replace("(Note:", "(note:"))
+ else:
+ output.append(data_out[key])
+ output.append([data["S1a"], data["S1b"], data["S1c"]][S1text.index(S1)].replace(" If ", " if ", 1).replace(" Criterion ", " criterion "))
+ if st.session_state.S2:
+ output.append(data["S2"].replace(" Full ", " full ", 1))
+ output.append([data["S3a"].replace("Few", "few", 1), data["S3b"].replace("Symptoms", "symptoms", 1), data["S3c"].replace("Many", "many", 1)][[f":green[**Mild**]\n\n{data['S3a'].replace("Mild: ", "")}", f":orange[**Moderate**]\n\n{data['S3b'].replace("Moderate: ", "")}", f":red[**Severe**]\n\n{data['S3c'].replace("Severe: ", "")}"].index(S3)])
+ for i in range(len(output)):
+ output[i] = output[i][0].lower() + output[i][1:].rstrip(".")
+ st.markdown(", ".join(output))
+
+# Back button
+if st.button("<- Change Disorder"):
+ st.switch_page("main.py")
\ No newline at end of file
From 8ff5ee2d7477981c8f863086f5e27de746fc3d02 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:07:11 +0000
Subject: [PATCH 07/33] Add Docker configs
---
.dockerignore | 4 ++++
Dockerfile | 28 ++++++++++++++++++++++++++++
2 files changed, 32 insertions(+)
create mode 100644 .dockerignore
create mode 100644 Dockerfile
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..0b5a4c0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+.venv
+.uv
+__pycache__
+*.pyc
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3c6b5de
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,28 @@
+# Use the official uv image for the binary
+FROM ghcr.io/astral-sh/uv:latest AS uv_bin
+
+# Use the official Python 3.13 slim image
+FROM python:3.13-slim
+
+# Copy uv binaries
+COPY --from=uv_bin /uv /uvx /bin/
+
+WORKDIR /app
+
+# Enable bytecode compilation for faster startup in Python 3.13
+ENV UV_COMPILE_BYTECODE=1
+
+# Copy lockfiles
+COPY pyproject.toml uv.lock ./
+
+# Install dependencies
+# We use --system because we are inside a dedicated container
+RUN uv pip install --system --no-cache -r pyproject.toml
+
+# Copy the rest of your router app
+COPY . .
+
+EXPOSE 8501
+
+# Entrypoint remains the same
+ENTRYPOINT ["streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"]
\ No newline at end of file
From c43f08e7dc6ff9a794d23fa0ccfc0fa5d59be164 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:08:39 +0000
Subject: [PATCH 08/33] Add Docker compose file
---
docker-compose.yml | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 docker-compose.yml
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..e812fa9
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,11 @@
+services:
+ app:
+ build: .
+ container_name: diagnostic_router
+ ports:
+ - "8501:8501"
+ environment:
+ - PYTHONUNBUFFERED=1
+ - UV_COMPILE_BYTECODE=1
+ # This ensures the app restarts if the server reboots
+ restart: always
\ No newline at end of file
From 557d28b57d9841211c5665800c5135976a73f23a Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:20:33 +0000
Subject: [PATCH 09/33] Update routing logic
---
main.py | 3 ++-
router.yaml | 10 ++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/main.py b/main.py
index 2ac71bd..061d5c8 100644
--- a/main.py
+++ b/main.py
@@ -16,6 +16,7 @@
# 1. Store the selection so the next page can see it
st.session_state.current_cat = cat
st.session_state.current_dis = dis
+ st.session_state.current_dis_page = data[cat][dis]["page_path"]
st.session_state.current_dis_data = data[cat][dis]["data_path"]
# 2. Forwarding Logic
@@ -23,6 +24,6 @@
if logic_type == "complex":
# Note: You must provide the path relative to the root
- st.switch_page("pages/adhd.py")
+ st.switch_page(st.session_state.current_dis_page)
else:
st.switch_page("pages/simple.py")
\ No newline at end of file
diff --git a/router.yaml b/router.yaml
index 1f26eb0..53240e2 100644
--- a/router.yaml
+++ b/router.yaml
@@ -3,53 +3,63 @@ Neurodevelopmental Disorders:
logic: complex
display_name: "Attention-Deficit/Hyperactivity Disorder"
icd_10: F90.2, F90.0, F90.1
+ page_path: "pages/adhd.py"
data_path: "data/adhd.yaml"
Autism Spectrum Disorder:
logic: simple
display_name: "Autism Spectrum Disorder"
icd_10: F84.0, F84.5, F84.8, F84.9
+ page_path: "pages/simple.py"
data_path: "data/asd.yaml"
Depressive Disorders:
Major Depressive Disorder:
logic: complex
display_name: "Major Depressive Disorder"
icd_10: F32.0-F32.9, F33.0-F33.9
+ page_path: "pages/mdd.py"
data_path: "data/mdd.yaml"
Persistent Depressive Disorder:
logic: complex
display_name: "Persistent Depressive Disorder (Dysthymia)"
icd_10: F34.1
+ page_path: "pages/pdd.py"
data_path: "data/pdd.yaml"
Anxiety Disorders:
Generalized Anxiety Disorder:
logic: complex
display_name: "Generalized Anxiety Disorder"
icd_10: F41.1
+ page_path: "pages/gad.py"
data_path: "data/gad.yaml"
Trauma- and Stressor-Related Disorders:
Post-Traumatic Stress Disorder:
logic: complex
display_name: "Post-Traumatic Stress Disorder"
icd_10: F43.1
+ page_path: "pages/ptsd.py"
data_path: "data/ptsd.yaml"
Personality Disorders:
Antisocial Personality Disorder:
logic: complex
display_name: "Antisocial Personality Disorder"
icd_10: F60.2
+ page_path: "pages/aspd.py"
data_path: "data/aspd.yaml"
Borderline Personality Disorder:
logic: simple
display_name: "Borderline Personality Disorder"
icd_10: F60.3
+ page_path: "pages/simple.py"
data_path: none
Histrionic Personality Disorder:
logic: simple
display_name: "Histrionic Personality Disorder"
icd_10: F60.4
+ page_path: "pages/simple.py"
data_path: none
Narcissistic Personality Disorder:
logic: simple
display_name: "Narcissistic Personality Disorder"
icd_10: F60.81
+ page_path: "pages/simple.py"
data_path: none
\ No newline at end of file
From 6318d0e3ede78a9836d4f79f171c9b7efcee51b6 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:21:25 +0000
Subject: [PATCH 10/33] Update Docker container name
---
docker-compose.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index e812fa9..43b06a0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,7 @@
services:
app:
build: .
- container_name: diagnostic_router
+ container_name: arwen
ports:
- "8501:8501"
environment:
From db1014f0e372303348a6f66e5f48a21b98574091 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:58:37 +0000
Subject: [PATCH 11/33] Add util module
---
utils.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 utils.py
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..449394a
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,16 @@
+import streamlit as st
+import subprocess
+import platform
+import os
+
+def add_clean_footer():
+ try:
+ branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip()
+ except:
+ branch = "Main"
+
+ is_docker = "Active" if os.path.exists('/.dockerenv') else "Inactive"
+
+ st.markdown("---")
+ st.markdown(f"Made with ❤️ by [Herma](https://github.com/hermaplusplus/Arwen) | Provided under the [MIT License](https://github.com/hermaplusplus/Arwen/blob/main/LICENSE)", text_alignment="center")
+ st.markdown(f"Branch: {branch} | Docker: {is_docker} | Python: {platform.python_version()}", text_alignment="center")
\ No newline at end of file
From c2f05978ddc88900b86de5a7e7462b30212500f3 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:58:48 +0000
Subject: [PATCH 12/33] Update readme
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d9eab2b..6dd38f7 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
# Arwen
-DSM-5 Diagnostic Criteria aggregator and formatter for EHRs
+Diagnostic Criteria aggregator and formatter for EHRs
From 088e7418f94a50d0e7f879b5032cd1db67c54b18 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:59:04 +0000
Subject: [PATCH 13/33] Update homepage
---
main.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/main.py b/main.py
index 061d5c8..7486d10 100644
--- a/main.py
+++ b/main.py
@@ -1,5 +1,6 @@
import streamlit as st
import yaml
+from utils import add_clean_footer
# Load data
with open("router.yaml", "r") as f:
@@ -12,7 +13,7 @@
cat = st.selectbox("Category", list(data.keys()))
dis = st.selectbox("Disorder", list(data[cat].keys()))
-if st.button("Open Assessment"):
+if st.button("Open Diagnostic Criteria"):
# 1. Store the selection so the next page can see it
st.session_state.current_cat = cat
st.session_state.current_dis = dis
@@ -26,4 +27,9 @@
# Note: You must provide the path relative to the root
st.switch_page(st.session_state.current_dis_page)
else:
- st.switch_page("pages/simple.py")
\ No newline at end of file
+ st.switch_page("pages/simple.py")
+
+st.markdown("## About")
+st.markdown("This tool is designed to help clinicians aggregate diagnostic criteria based on the structure found in teh DSM-5-TR. :red[This is **not** a diagnostic tool and should not be used as such]. Clinicians should always sue their judgement and veri")
+
+add_clean_footer()
\ No newline at end of file
From 8a92c0665a32237317963b10780359cd547e0650 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 01:20:54 +0000
Subject: [PATCH 14/33] Rewrite ADHD text file
---
data/adhd.yaml | 36 ++++++++++++++++++------------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/data/adhd.yaml b/data/adhd.yaml
index d59f030..39357b0 100644
--- a/data/adhd.yaml
+++ b/data/adhd.yaml
@@ -1,26 +1,26 @@
A: "A persistent pattern of inattention and/or hyperactivityimpulsivity that interferes with functioning or development, as characterized by (1) and/or (2):"
A1: "Inattention: Six (or more) of the following symptoms have persisted for at least 6 months to a degree that is inconsistent with developmental level and that negatively impacts directly on social and academic/occupational activities:"
A1note: "Note: The symptoms are not solely a manifestation of oppositional behavior, defiance, hostility, or failure to understand tasks or instructions. For older adolescents ad adults (age 17 and older), at least five symptoms are required."
-A1a: "Often fails to give close attention to details or makes careless mistakes in schoolwork, at work, or during other activities (e.g., overlooks or misses details, work is inaccurate)."
-A1b: "Often has difficulty sustaining attention in tasks or play activities (e.g., has difficulty remaining focused during lectures, conversations, or lengthy reading)."
-A1c: "Often does not seem to listen when spoken to directly (e.g., mind seems elsewhere, even in the absence of any obvious distraction)."
-A1d: "Often does not follow through on instructions and fails to finish schoolwork, chores, or duties in the workplace (e.g., starts tasks but quickly loses focus and is easily sidetracked)."
-A1e: "Often has difficulty organizing tasks and activities (e.g., difficulty managing sequential tasks; difficulty keeping materials and belongings in order; messy, disorganized work; has poor time management; fails to meet deadlines)."
-A1f: "Often avoids, dislikes, or is reluctant to engage in tasks that require sustained mental effort (e.g., schoolwork or homework; for older adolescents and adults, preparing reports, completing forms, reviewing lengthy papers)."
-A1g: "Often loses things necessary for tasks or activities (e.g., school materials, pencils, books, tools, wallets, keys, paperwork, eyeglasses, mobile telephones)."
-A1h: "Is often easily distracted by extraneous stimuli (for older adolescents and adults, may include unrelated thoughts)."
-A1i: "Is often forgetful in daily activities (e.g., doing chores, running errands; for older adolescents and adults, returning calls, paying bills, keeping appointments)."
+A1a: "Fails to give close attention to detail, makes careless mistakes, overlooks or misses details, work is inaccurate."
+A1b: "Difficulty focusing."
+A1c: "Does not seem to listen when spoken to directly, even in the absence of any obvious distraction."
+A1d: "Struggles to follow through on instructions and fails to finish tasks, starts tasks but quickly loses focus and is easily sidetracked."
+A1e: "Difficulty organizing tasks and activities, difficulty managing sequential tasks, difficulty keeping materials and belongings in order, messy, disorganized work, has poor time management, fails to meet deadlines."
+A1f: "Avoids, dislikes, or is reluctant to engage in tasks that require sustained mental effort."
+A1g: "Loses things necessary for tasks or activities."
+A1h: "Easily distracted by extraneous stimuli."
+A1i: "Forgetful in daily activities."
A2: "Hyperactivity and Impulsivity: Six (or more) of the following symptoms have persisted for at least 6 months to a degree that is inconsistent with developmental level and that negatively impacts directly on social and academic/occupational activities:"
A2note: "Note: The symptoms are not solely a manifestation of oppositional behavior, defiance, hostility, or failure to understand tasks or instructions. For older adolescents ad adults (age 17 and older), at least five symptoms are required."
-A2a: "Often fidgets with or taps hands or feet or squirms in seat."
-A2b: "Often leaves seat in situations when remaining seated is expected (e.g., leaves his or her place in the classroom, in the office or other workplace, or in other situations that require remaining in place)."
-A2c: "Often runs about or climbs in situations where it is inappropriate. (Note: In adolescents or adults, may be limited to feeling restless.)"
-A2d: "Often unable to play or engage in leisure activities quietly."
-A2e: "Is often 'on the go,' acting as if 'driven by a motor' (e.g., is unable to be or uncomfortable being still for extended time, as in restaurants, meetings; may be experienced by others as being restless or difficult to keep up with)."
-A2f: "Often talks excessively."
-A2g: "Often blurts out an answer before a question has been completed (e.g., completes people's sentences; cannot wait for turn in conversation)."
-A2h: "Often has difficulty waiting his or her turn (e.g., while waiting in line)."
-A2i: "Often interrupts or intrudes on others (e.g., butts into conversations, games, or activities; may start using other people's things without asking or receiving permission; for adolescents and adults, may intrude into or take over what others are doing)."
+A2a: "Fidgets with or taps hands or feet or squirms in seat."
+A2b: "Leaves seat in situations when remaining seated is expected."
+A2c: "Feeling restless."
+A2d: "Unable to play or engage in leisure activities quietly."
+A2e: "Is often 'on the go,' acting as if 'driven by a motor'."
+A2f: "Talks excessively."
+A2g: "Blurts out an answer before a question has been completed, completes people's sentences, cannot wait for turn in conversation."
+A2h: "Difficulty waiting his or her turn."
+A2i: "Interrupts or intrudes on others, butts into conversations, games, or activities, may start using other people's things without asking or receiving permission, may intrude into or take over what others are doing."
B: "Several inattentive or hyperactive-impulsive symptoms were present prior to age 12 years."
C: "Several inattentive or hyperactive-impulsive symptoms are present in two or more settings (e.g., at home, school, or work; with friends or relatives; in other activities)."
D: "There is clear evidence that the symptoms interfere with, or reduce the quality of, social, academic, or occupational functioning."
From 4bfe706a54fe5c6d2c2244b4c879cf3fe26cc2c2 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 01:21:23 +0000
Subject: [PATCH 15/33] Add homepage info
---
main.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/main.py b/main.py
index 7486d10..ad52084 100644
--- a/main.py
+++ b/main.py
@@ -10,6 +10,10 @@
st.title("Diagnostic Criteria Aggregator")
+st.error("This is **not** a diagnostic tool and should not be used as such. Please read the **About** section below for more information.", icon="🚨")
+
+st.markdown("Select a category and disorder, then press the button below to get started:")
+
cat = st.selectbox("Category", list(data.keys()))
dis = st.selectbox("Disorder", list(data[cat].keys()))
@@ -30,6 +34,10 @@
st.switch_page("pages/simple.py")
st.markdown("## About")
-st.markdown("This tool is designed to help clinicians aggregate diagnostic criteria based on the structure found in teh DSM-5-TR. :red[This is **not** a diagnostic tool and should not be used as such]. Clinicians should always sue their judgement and veri")
+st.markdown("This tool is designed to help clinicians aggregate diagnostic criteria based on the structure found in the DSM-5-TR. :red[This is **not** a diagnostic tool and should not be used as such]. Clinicians should always use their own judgement and verify that criteria and codes are correct.")
+st.markdown("Clincians can select all diagnostic criteria that apply to their patient/client and the tool will output a list of criteria met formatted in a way that can be easily copied into an EHR or note (including systems which parse criteria to :sparkles: :rainbow[automagically*] :sparkles: create a note)")
+st.markdown("This website does **not store any data** that is submitted and **does not allow the input of any Protected Health Information**. When a user refreshes a page or navigates away, all data is irrevocably lost.")
+st.markdown("This project is open source and available on [GitHub](https://github.com/hermaplusplus/Arwen). It is provided under the [MIT License](https://github.com/hermaplusplus/Arwen/blob/main/LICENSE). Contributions, issue reports, feedback, and suggestions are welcome.")
+st.markdown(":grey[:small[* Automagically, meaning using a Large Language Model.]]")
add_clean_footer()
\ No newline at end of file
From edf0d6187b02c25b7c3f03560ebe07637af0bf4d Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 01:22:08 +0000
Subject: [PATCH 16/33] Add extra toggles and information to ADHD page
---
pages/adhd.py | 42 ++++++++++++++++++++++++++++++++++++------
1 file changed, 36 insertions(+), 6 deletions(-)
diff --git a/pages/adhd.py b/pages/adhd.py
index 031f25f..8e587df 100644
--- a/pages/adhd.py
+++ b/pages/adhd.py
@@ -1,10 +1,17 @@
import streamlit as st
import yaml
+from utils import add_clean_footer
# Safety check
if "current_dis" not in st.session_state:
st.switch_page("main.py")
+st.title(f"{st.session_state.current_dis}")
+
+# Back button
+if st.button("<- Return to Homepage", key="backtop"):
+ st.switch_page("main.py")
+
with open(st.session_state.current_dis_data, "r") as f:
data = yaml.safe_load(f)
@@ -12,11 +19,11 @@
if key not in st.session_state:
st.session_state[key] = False
-st.title(f"{st.session_state.current_dis}")
-
st.markdown("## Client Metadata")
-age = st.radio("Client Age", ["Under 17", "17 or older"], key="age")
+st.info("This is a :rainbow[reminder] that no data on this site is stored or shared. Specific age under 90 is not considered PHI regardless! This age selection determines the threshold for the number of subcriteria required to satisfy criteria A1 and A2.", icon="ℹ️")
+
+age = st.radio("Client Age", ["17 or older", "Under 17"], key="age")
threshold = 5 if age == "17 or older" else 6
a1_score = sum(st.session_state.get(f"A1{char}", False) for char in "abcdefghi")
@@ -87,9 +94,30 @@
st.markdown("## Output")
omitBCDE = st.toggle("Ignore criteria B, C, D, and E", key="omitBCDE", value=True)
+omitS1 = st.toggle("Ignore presentation specifier", key="omitS1", value=True)
+omitS2 = st.toggle("Ignore remission specifier", key="omitS2", value=True)
+omitS3 = st.toggle("Ignore severity specifier", key="omitS3", value=True)
-if omitBCDE:
+if omitBCDE and not any([omitS1, omitS2, omitS3]):
st.info("Output will not include criteria B, C, D, and E.", icon="ℹ️")
+elif omitBCDE and all([omitS1, omitS2, omitS3]):
+ st.info("Output will not include criteria B, C, D, and E, nor presentation, remission, and severity specifiers.", icon="ℹ️")
+elif omitBCDE and any([omitS1, omitS2, omitS3]):
+ omitted = [o for o in [omitS1, omitS2, omitS3] if o]
+ omittext = ["presentation", "remission", "severity"]
+ if len(omitted) == 1:
+ st.info(f"Output will not include criteria B, C, D, and E, nor {omittext[[omitS1, omitS2, omitS3].index(True)]} specifier.", icon="ℹ️")
+ else:
+ st.info(f"Output will not include criteria B, C, D, and E, nor {omittext[[omitS1, omitS2, omitS3].index(True)]} and {omittext[[omitS1, omitS2, omitS3].index(True, [omitS1, omitS2, omitS3].index(True)+1)]} specifiers.", icon="ℹ️")
+elif all([omitS1, omitS2, omitS3]):
+ st.info("Output will not include presentation, remission, and severity specifiers.", icon="ℹ️")
+elif any([omitS1, omitS2, omitS3]):
+ omitted = [o for o in [omitS1, omitS2, omitS3] if o]
+ omittext = ["presentation", "remission", "severity"]
+ if len(omitted) == 1:
+ st.info(f"Output will not include {omittext[[omitS1, omitS2, omitS3].index(True)]} specifier.", icon="ℹ️")
+ else:
+ st.info(f"Output will not include {omittext[[omitS1, omitS2, omitS3].index(True)]} and {omittext[[omitS1, omitS2, omitS3].index(True, [omitS1, omitS2, omitS3].index(True)+1)]} specifiers.", icon="ℹ️")
if not omitBCDE:
criteria_met = all([st.session_state.get(key, False) for key in ["A", "B", "C", "D", "E"]])
@@ -128,5 +156,7 @@
st.markdown(", ".join(output))
# Back button
-if st.button("<- Change Disorder"):
- st.switch_page("main.py")
\ No newline at end of file
+if st.button("<- Return to Homepage", key="backbottom"):
+ st.switch_page("main.py")
+
+add_clean_footer()
\ No newline at end of file
From f2db38431f2036573caa5cf4a3fc8dc26d354a4c Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 01:22:30 +0000
Subject: [PATCH 17/33] Add return buttons and footer to simple template page
---
pages/simple.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/pages/simple.py b/pages/simple.py
index 96fe399..757ce87 100644
--- a/pages/simple.py
+++ b/pages/simple.py
@@ -1,5 +1,6 @@
import streamlit as st
import yaml
+from utils import add_clean_footer
# Safety check
if "current_dis" not in st.session_state:
@@ -7,8 +8,14 @@
st.title(f"{st.session_state.current_dis}")
+# Back button
+if st.button("<- Return to Homepage", key="backtop"):
+ st.switch_page("main.py")
+
# Back button
-if st.button("<- Change Disorder"):
- st.switch_page("main.py")
\ No newline at end of file
+if st.button("<- Return to Homepage", key="backbottom"):
+ st.switch_page("main.py")
+
+add_clean_footer()
\ No newline at end of file
From 2b3d56cc108b8883b251b689c5995189a3e5fa64 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 01:27:53 +0000
Subject: [PATCH 18/33] Fix git branch except result
---
utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/utils.py b/utils.py
index 449394a..e8dd468 100644
--- a/utils.py
+++ b/utils.py
@@ -7,7 +7,7 @@ def add_clean_footer():
try:
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip()
except:
- branch = "Main"
+ branch = ":red-badge[UNKNOWN]"
is_docker = "Active" if os.path.exists('/.dockerenv') else "Inactive"
From aa93e7032eac23404d2ec21dea2e81403fa4fcbc Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 01:35:29 +0000
Subject: [PATCH 19/33] Add commit info and styles to footer
---
utils.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/utils.py b/utils.py
index e8dd468..938bb2d 100644
--- a/utils.py
+++ b/utils.py
@@ -6,11 +6,18 @@
def add_clean_footer():
try:
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip()
+ branch = f":green-badge[**{branch}**]" if branch == "main" else f":orange-badge[**{branch}**]"
except:
branch = ":red-badge[UNKNOWN]"
- is_docker = "Active" if os.path.exists('/.dockerenv') else "Inactive"
+ try:
+ commit = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip()
+ commit = f":blue-badge[**{commit}**]"
+ except:
+ commit = ":red-badge[UNKNOWN]"
+
+ is_docker = ":green-badge[ACTIVE]" if os.path.exists('/.dockerenv') else ":red-badge[INACTIVE]"
st.markdown("---")
st.markdown(f"Made with ❤️ by [Herma](https://github.com/hermaplusplus/Arwen) | Provided under the [MIT License](https://github.com/hermaplusplus/Arwen/blob/main/LICENSE)", text_alignment="center")
- st.markdown(f"Branch: {branch} | Docker: {is_docker} | Python: {platform.python_version()}", text_alignment="center")
\ No newline at end of file
+ st.markdown(f"Branch: {branch}@{commit} | Docker: {is_docker} | Python: :blue-badge[{platform.python_version()}]", text_alignment="center")
\ No newline at end of file
From 504d14fde099caaac788d46e7c657e0304106988 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 01:35:43 +0000
Subject: [PATCH 20/33] Make go button green
---
main.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/main.py b/main.py
index ad52084..e6a3a04 100644
--- a/main.py
+++ b/main.py
@@ -17,7 +17,7 @@
cat = st.selectbox("Category", list(data.keys()))
dis = st.selectbox("Disorder", list(data[cat].keys()))
-if st.button("Open Diagnostic Criteria"):
+if st.button(":green[Open Diagnostic Criteria ->]"):
# 1. Store the selection so the next page can see it
st.session_state.current_cat = cat
st.session_state.current_dis = dis
From 8c563c509e5f8884a57ba4ff2d453a2d37909ae3 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 01:59:37 +0000
Subject: [PATCH 21/33] Add start script and parameters to Docker files
---
Dockerfile | 8 ++++++++
docker-compose.yml | 7 +++++--
start.sh | 9 +++++++++
3 files changed, 22 insertions(+), 2 deletions(-)
create mode 100644 start.sh
diff --git a/Dockerfile b/Dockerfile
index 3c6b5de..ea8a0b8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,6 +4,14 @@ FROM ghcr.io/astral-sh/uv:latest AS uv_bin
# Use the official Python 3.13 slim image
FROM python:3.13-slim
+# Declare build-time arguments
+ARG GIT_BRANCH
+ARG GIT_COMMIT
+
+# Set them as environment variables so the app can see them
+ENV APP_GIT_BRANCH=$GIT_BRANCH
+ENV APP_GIT_COMMIT=$GIT_COMMIT
+
# Copy uv binaries
COPY --from=uv_bin /uv /uvx /bin/
diff --git a/docker-compose.yml b/docker-compose.yml
index 43b06a0..569aba2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,11 +1,14 @@
services:
app:
- build: .
+ build:
+ context: .
+ args:
+ - GIT_BRANCH=${GIT_BRANCH:-unknown}
+ - GIT_COMMIT=${GIT_COMMIT:-unknown}
container_name: arwen
ports:
- "8501:8501"
environment:
- PYTHONUNBUFFERED=1
- UV_COMPILE_BYTECODE=1
- # This ensures the app restarts if the server reboots
restart: always
\ No newline at end of file
diff --git a/start.sh b/start.sh
new file mode 100644
index 0000000..470a79b
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+export GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+export GIT_COMMIT=$(git rev-parse --short HEAD)
+
+echo "GIT_BRANCH=$GIT_BRANCH" > .env
+echo "GIT_COMMIT=$GIT_COMMIT" >> .env
+
+docker compose up -d --build
\ No newline at end of file
From ddf309323dc4643430155192d9e8d14be8497b3b Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 02:00:06 +0000
Subject: [PATCH 22/33] Update text on home and ADHD pages
---
main.py | 7 ++++---
pages/adhd.py | 4 ++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/main.py b/main.py
index e6a3a04..d20d7b2 100644
--- a/main.py
+++ b/main.py
@@ -8,7 +8,7 @@
st.set_page_config(page_title="Arwen DCA", layout="centered")
-st.title("Diagnostic Criteria Aggregator")
+st.title("Arwen Diagnostic Criteria Aggregator 📋🧝♀️🩺")
st.error("This is **not** a diagnostic tool and should not be used as such. Please read the **About** section below for more information.", icon="🚨")
@@ -34,10 +34,11 @@
st.switch_page("pages/simple.py")
st.markdown("## About")
-st.markdown("This tool is designed to help clinicians aggregate diagnostic criteria based on the structure found in the DSM-5-TR. :red[This is **not** a diagnostic tool and should not be used as such]. Clinicians should always use their own judgement and verify that criteria and codes are correct.")
+st.markdown("**Arwen DCA** is designed to help clinicians aggregate diagnostic criteria based on the structure found in the DSM-5-TR. :red[This is **not** a diagnostic tool and should not be used as such]. Clinicians should always use their own judgement and verify that criteria and codes are correct.")
st.markdown("Clincians can select all diagnostic criteria that apply to their patient/client and the tool will output a list of criteria met formatted in a way that can be easily copied into an EHR or note (including systems which parse criteria to :sparkles: :rainbow[automagically*] :sparkles: create a note)")
-st.markdown("This website does **not store any data** that is submitted and **does not allow the input of any Protected Health Information**. When a user refreshes a page or navigates away, all data is irrevocably lost.")
+st.markdown("This website does :green[**not store any data**] that is submitted and :green[**does not allow the input of any PHI**]. When a user refreshes a page or navigates away, all data is irrevocably lost.")
st.markdown("This project is open source and available on [GitHub](https://github.com/hermaplusplus/Arwen). It is provided under the [MIT License](https://github.com/hermaplusplus/Arwen/blob/main/LICENSE). Contributions, issue reports, feedback, and suggestions are welcome.")
+st.markdown(":grey[Why is the tool called 'Arwen'? I watched Lord of the Rings recently. That's it.]")
st.markdown(":grey[:small[* Automagically, meaning using a Large Language Model.]]")
add_clean_footer()
\ No newline at end of file
diff --git a/pages/adhd.py b/pages/adhd.py
index 8e587df..01783ea 100644
--- a/pages/adhd.py
+++ b/pages/adhd.py
@@ -123,11 +123,11 @@
criteria_met = all([st.session_state.get(key, False) for key in ["A", "B", "C", "D", "E"]])
if not criteria_met:
unmet = [key for key in ["A", "B", "C", "D", "E"] if not st.session_state.get(key, False)]
- st.error(f"Criteria not met: **{'**, **'.join(unmet)}**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-E are met.\n\nConsider diagnosing :blue[F90.8] Other Specified AttentionDeficit/Hyperactivity Disorder or :blue[F90.9] Unspecified Attention-Deficit/Hyperactivity Disorder", icon="🚨")
+ st.error(f"Criteria not met: **{'**, **'.join(unmet)}**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-E are met.\n\nConsider diagnosing :blue[F90.8] Other Specified Attention Deficit/Hyperactivity Disorder or :blue[F90.9] Unspecified Attention-Deficit/Hyperactivity Disorder", icon="🚨")
else:
criteria_met = st.session_state.A
if not criteria_met:
- st.error(f"Criteria not met: **A**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-E are met.\n\nConsider diagnosing :blue[F90.8] Other Specified AttentionDeficit/Hyperactivity Disorder or :blue[F90.9] Unspecified Attention-Deficit/Hyperactivity Disorder", icon="🚨")
+ st.error(f"Criteria not met: **A**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-E are met.\n\nConsider diagnosing :blue[F90.8] Other Specified Attention Deficit/Hyperactivity Disorder or :blue[F90.9] Unspecified Attention-Deficit/Hyperactivity Disorder", icon="🚨")
if ":green-badge[RECOMMENDED]" not in S1 and st.session_state.A:
st.warning(f"Presentation specifier does not match recommendation. **{S1.split('\n\n')[0]}** was selected instead of **{recommended}**.", icon="⚠️")
From 24078ccc50eec2b85840c491df780e9ee685c6c3 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Fri, 6 Feb 2026 02:08:42 +0000
Subject: [PATCH 23/33] Fix git branch and commit display
---
utils.py | 32 ++++++++++++++++++++++----------
1 file changed, 22 insertions(+), 10 deletions(-)
diff --git a/utils.py b/utils.py
index 938bb2d..c08cecd 100644
--- a/utils.py
+++ b/utils.py
@@ -4,17 +4,29 @@
import os
def add_clean_footer():
- try:
- branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip()
- branch = f":green-badge[**{branch}**]" if branch == "main" else f":orange-badge[**{branch}**]"
- except:
- branch = ":red-badge[UNKNOWN]"
+
+ branch = os.environ.get("GIT_BRANCH", "UNKNOWN")
+ branch_url = f"https://github.com/hermaplusplus/Arwen/tree/{branch}"
+ commit = os.environ.get("GIT_COMMIT", "UNKNOWN")
+ commit_url = f"https://github.com/hermaplusplus/Arwen/commit/{commit}"
+
+ if branch != "UNKNOWN":
+ branch = f":green-badge[**[{branch}]({branch_url})**]" if branch == "main" else f":orange-badge[**[{branch}]({branch_url})**]"
+ else:
+ try:
+ branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip()
+ branch = f":green-badge[**[{branch}]({branch_url})**]" if branch == "main" else f":orange-badge[**[{branch}]({branch_url})**]"
+ except:
+ branch = ":red-badge[UNKNOWN]"
- try:
- commit = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip()
- commit = f":blue-badge[**{commit}**]"
- except:
- commit = ":red-badge[UNKNOWN]"
+ if commit != "UNKNOWN":
+ commit = f":blue-badge[**[{commit}]({commit_url})**]"
+ else:
+ try:
+ commit = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip()
+ commit = f":blue-badge[**[{commit}]({commit_url})**]"
+ except:
+ commit = ":red-badge[UNKNOWN]"
is_docker = ":green-badge[ACTIVE]" if os.path.exists('/.dockerenv') else ":red-badge[INACTIVE]"
From 875afdfd67098e6efda953b95769b379bd18d688 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 19:34:46 +0000
Subject: [PATCH 24/33] Update default state for specifier toggles
---
pages/adhd.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pages/adhd.py b/pages/adhd.py
index 01783ea..af946a5 100644
--- a/pages/adhd.py
+++ b/pages/adhd.py
@@ -94,9 +94,9 @@
st.markdown("## Output")
omitBCDE = st.toggle("Ignore criteria B, C, D, and E", key="omitBCDE", value=True)
-omitS1 = st.toggle("Ignore presentation specifier", key="omitS1", value=True)
-omitS2 = st.toggle("Ignore remission specifier", key="omitS2", value=True)
-omitS3 = st.toggle("Ignore severity specifier", key="omitS3", value=True)
+omitS1 = st.toggle("Ignore presentation specifier", key="omitS1", value=False)
+omitS2 = st.toggle("Ignore remission specifier", key="omitS2", value=False)
+omitS3 = st.toggle("Ignore severity specifier", key="omitS3", value=False)
if omitBCDE and not any([omitS1, omitS2, omitS3]):
st.info("Output will not include criteria B, C, D, and E.", icon="ℹ️")
From c5f492f5e3010ffa2fa2960fd77cec75fc236cce Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 19:35:08 +0000
Subject: [PATCH 25/33] Add WIP disclaimer
---
main.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/main.py b/main.py
index d20d7b2..33f89a6 100644
--- a/main.py
+++ b/main.py
@@ -12,6 +12,8 @@
st.error("This is **not** a diagnostic tool and should not be used as such. Please read the **About** section below for more information.", icon="🚨")
+st.error("This tool is work-in-progress. Some pages may not be fully functional yet or may contain errors.", icon="🚧")
+
st.markdown("Select a category and disorder, then press the button below to get started:")
cat = st.selectbox("Category", list(data.keys()))
From 4bf5e9334e8539d3559f73abfd21a922364da44d Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 20:41:02 +0000
Subject: [PATCH 26/33] Remove leftover comments on main page
---
main.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/main.py b/main.py
index 33f89a6..ac2692d 100644
--- a/main.py
+++ b/main.py
@@ -20,17 +20,14 @@
dis = st.selectbox("Disorder", list(data[cat].keys()))
if st.button(":green[Open Diagnostic Criteria ->]"):
- # 1. Store the selection so the next page can see it
st.session_state.current_cat = cat
st.session_state.current_dis = dis
st.session_state.current_dis_page = data[cat][dis]["page_path"]
st.session_state.current_dis_data = data[cat][dis]["data_path"]
- # 2. Forwarding Logic
logic_type = data[cat][dis].get("logic", "simple")
if logic_type == "complex":
- # Note: You must provide the path relative to the root
st.switch_page(st.session_state.current_dis_page)
else:
st.switch_page("pages/simple.py")
From e11058eda44582bbd823d1efe11ce78f37c6f799 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 20:47:24 +0000
Subject: [PATCH 27/33] Split PTSD into o6 and u6 due to different criteria
---
router.yaml | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/router.yaml b/router.yaml
index 53240e2..925fad4 100644
--- a/router.yaml
+++ b/router.yaml
@@ -32,12 +32,18 @@ Anxiety Disorders:
page_path: "pages/gad.py"
data_path: "data/gad.yaml"
Trauma- and Stressor-Related Disorders:
- Post-Traumatic Stress Disorder:
+ Post-Traumatic Stress Disorder in Individuals Older than 6 Years:
logic: complex
- display_name: "Post-Traumatic Stress Disorder"
+ display_name: "Post-Traumatic Stress Disorder in Individuals Older than 6 Years"
icd_10: F43.1
- page_path: "pages/ptsd.py"
- data_path: "data/ptsd.yaml"
+ page_path: "pages/ptsd-o6.py"
+ data_path: "data/ptsd-o6.yaml"
+ Post-Traumatic Stress Disorder in Children 6 Years and Younger:
+ logic: complex
+ display_name: "Post-Traumatic Stress Disorder in Children 6 Years and Younger"
+ icd_10: F43.1
+ page_path: "pages/ptsd-u6.py"
+ data_path: "data/ptsd-u6.yaml"
Personality Disorders:
Antisocial Personality Disorder:
logic: complex
From 5930477c55a3d05653de5eb4ed1b91b4c2cd1965 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 20:48:00 +0000
Subject: [PATCH 28/33] Add PTSD o6 page and content
---
data/ptsd-o6.yaml | 42 ++++++++++++
pages/ptsd-o6.py | 167 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 209 insertions(+)
create mode 100644 data/ptsd-o6.yaml
create mode 100644 pages/ptsd-o6.py
diff --git a/data/ptsd-o6.yaml b/data/ptsd-o6.yaml
new file mode 100644
index 0000000..5e4c057
--- /dev/null
+++ b/data/ptsd-o6.yaml
@@ -0,0 +1,42 @@
+Note: "The following criteria apply to adults, adolescents, and children older than 6 years. For children 6 years and younger, return to the homepage and select :green[Post-Traumatic Stress Disorder in Children 6 Years and Younger]."
+A: "Exposure to actual or threatened death, serious injury, or sexual violence in one (or more) of the following ways:"
+A1: "Directly experiencing the traumatic event(s)."
+A2: "Witnessing, in person, the event(s) as it occurred to others."
+A3: "Learning that the traumatic event(s) occurred to a close family member or close friend. In cases of actual or threatened death of a family member or friend, the event(s) must have been violent or accidental."
+A4: "Experiencing repeated or extreme exposure to aversive details of the traumatic event(s) (e.g., first responders collecting human remains; police officers repeatedlyexposed to details of child abuse)."
+A4note: "Note: Criterion A4 does not apply to exposure through electronic media, television, movies, or pictures, unless this exposure is work related."
+B: "Presence of one (or more) of the following intrusion symptoms associated with the traumatic event(s), beginning after the traumatic event(s) occurred:"
+B1: "Recurrent, involuntary, and intrusive distressing memories of the traumatic event(s)."
+B1note: "Note: In children older than 6 years, repetitive play may occur in which themes or aspects of the traumatic event(s) are expressed."
+B2: "Recurrent distressing dreams in which the content and/or affect of the dream are related to the traumatic event(s)."
+B2note: "Note: In children, there may be frightening dreams without recognizable content."
+B3: "Dissociative reactions (e.g., flashbacks) in which the individual feels or acts as if the traumatic event(s) were recurring. (Such reactions may occur on a continuum, with the most extreme expression being a complete loss of awareness of present surroundings.)"
+B3note: "Note: In children, trauma-specific reenactment may occur in play."
+B4: "Intense or prolonged psychological distress at exposure to internal or external cues that symbolize or resemble an aspect of the traumatic event(s)."
+B5: "Marked physiological reactions to internal or external cues that symbolize or resemble an aspect of the traumatic event(s)."
+C: "Persistent avoidance of stimuli associated with the traumatic event(s), beginning after the traumatic event(s) occurred, as evidenced by one or both of the following:"
+C1: "Avoidance of or efforts to avoid distressing memories, thoughts, or feelings about or closely associated with the traumatic event(s)."
+C2: "Avoidance of or efforts to avoid external reminders (people, places, conversations, activities, objects, situations) that arouse distressing memories, thoughts, or feelings about or closely associated with the traumatic event(s)."
+D: "Negative alterations in cognitions and mood associated with the traumatic event(s), beginning or worsening after the traumatic event(s) occurred, as evidenced by two (or more) of the following:"
+D1: "Inability to remember an important aspect of the traumatic event(s) (typically due to dissociative amnesia and not to other factors such as head injury, alcohol, or drugs)."
+D2: "Persistent and exaggerated negative beliefs or expectations about oneself, others, or the world (e.g., 'I am bad,' 'No one can be trusted,' 'The world is completely dangerous', 'My whole nervous system is permanently ruined')."
+D3: "Persistent, distorted cognitions about the cause or consequences of the traumatic event(s) that lead the individual to blame himself/herself or others."
+D4: "Persistent negative emotional state (e.g., fear, horror, anger, guilt, or shame)."
+D5: "Markedly diminished interest or participation in significant activities."
+D6: "Feelings of detachment or estrangement from others."
+D7: "Persistent inability to experience positive emotions (e.g., inability to experience happiness, satisfaction, or loving feelings)."
+E: "Marked alterations in arousal and reactivity associated with the traumatic event(s), beginning or worsening after the traumatic event(s) occurred, as evidenced by two (or more) of the following:"
+E1: "Irritable behavior and angry outbursts (with little or no provocation) typically expressed as verbal or physical aggression toward people or objects."
+E2: "Reckless or self-destructive behavior."
+E3: "Hypervigilance."
+E4: "Exaggerated startle response."
+E5: "Problems with concentration."
+E6: "Sleep disturbance (e.g., difficulty falling or staying asleep or restless sleep)."
+F: "Duration of the disturbance (Criteria B, C, D, and E) is more than 1 month."
+G: "The disturbance causes clinically significant distress or impairment in social, occupational, or other important areas of functioning."
+H: "The disturbance is not attributable to the physiological effects of a substance (e.g., medication, alcohol) or another medical condition."
+S1: "With dissociative symptoms: The individual's symptoms meet criteria for post-traumatic stress disorder, and in addition, in response to the stressor, the individual experiences persistent or recurrent symptoms of either of the following:"
+S11: "Depersonalization: Persistent or recurrent experiences of feeling detached from, and as if one were an outside observer of, one's mental processes or body (e.g., feeling as though one were in a dream; feeling a sense of unreality of self or body or of time moving slowly)."
+S12: "Derealization: Persistent or recurrent experiences of unreality of surroundings (e.g., the world around the individual is experienced as unreal, dreamlike, distant, or distorted)."
+S1note: "Note: To use this subtype, the dissociative symptoms must not be attributable to the physiological effects of a substance (e.g., blacking out from alcohol intoxication) or another medical condition (e.g., complex partial seizures)."
+S2: "With delayed expression: If the full diagnostic criteria are not met until at least 6 months after the event (although the onset and expression of some symptoms may be immediate)."
\ No newline at end of file
diff --git a/pages/ptsd-o6.py b/pages/ptsd-o6.py
new file mode 100644
index 0000000..2e18d12
--- /dev/null
+++ b/pages/ptsd-o6.py
@@ -0,0 +1,167 @@
+import streamlit as st
+import yaml
+from utils import add_clean_footer
+
+# Safety check
+if "current_dis" not in st.session_state:
+ st.switch_page("main.py")
+
+st.title(f"{st.session_state.current_dis}")
+
+# Back button
+if st.button("<- Return to Homepage", key="backtop"):
+ st.switch_page("main.py")
+
+with open(st.session_state.current_dis_data, "r") as f:
+ data = yaml.safe_load(f)
+
+for key in data.keys():
+ if key not in st.session_state:
+ st.session_state[key] = False
+
+a_score = sum(st.session_state.get(f"A{char}", False) for char in "1234")
+b_score = sum(st.session_state.get(f"B{char}", False) for char in "12345")
+c_score = sum(st.session_state.get(f"C{char}", False) for char in "12")
+d_score = sum(st.session_state.get(f"D{char}", False) for char in "1234567")
+e_score = sum(st.session_state.get(f"E{char}", False) for char in "123456")
+s1_score = sum(st.session_state.get(f"S1{char}", False) for char in "12")
+a_score_badge = f":red-badge[{a_score}/1]" if a_score < 1 else f":green-badge[{a_score}/1]"
+b_score_badge = f":red-badge[{b_score}/1]" if b_score < 1 else f":green-badge[{b_score}/1]"
+c_score_badge = f":red-badge[{c_score}/1]" if c_score < 1 else f":green-badge[{c_score}/1]"
+d_score_badge = f":red-badge[{d_score}/2]" if d_score < 2 else f":green-badge[{d_score}/2]"
+e_score_badge = f":red-badge[{e_score}/2]" if e_score < 2 else f":green-badge[{e_score}/2]"
+
+st.session_state.A = a_score >= 1
+st.session_state.B = b_score >= 1
+st.session_state.C = c_score >= 1
+st.session_state.D = d_score >= 2
+st.session_state.E = e_score >= 2
+st.session_state.S1 = s1_score >= 1
+
+st.markdown("## Diagnostic Criteria")
+st.info("Criteria A, B, C, D, and E will check automatically based on sub-criteria.", icon="ℹ️")
+
+A = st.checkbox(f"A. {data['A']}\n\n{a_score_badge}", key="A", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ A1 = st.checkbox(f"1\\. {data['A1']}", key="A1")
+ A2 = st.checkbox(f"2\\. {data['A2']}", key="A2")
+ A3 = st.checkbox(f"3\\. {data['A3']}", key="A3")
+ A4 = st.checkbox(f"4\\. {data['A4']}\n\n:orange[{data['A4note']}]", key="A4")
+B = st.checkbox(f"B. {data['B']}\n\n{b_score_badge}", key="B", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ B1 = st.checkbox(f"1\\. {data['B1']}\n\n:orange[{data['B1note']}]", key="B1")
+ B2 = st.checkbox(f"2\\. {data['B2']}\n\n:orange[{data['B2note']}]", key="B2")
+ B3 = st.checkbox(f"3\\. {data['B3']}\n\n:orange[{data['B3note']}]", key="B3")
+ B4 = st.checkbox(f"4\\. {data['B4']}", key="B4")
+ B5 = st.checkbox(f"5\\. {data['B5']}", key="B5")
+C = st.checkbox(f"C. {data['C']}\n\n{c_score_badge}", key="C", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ C1 = st.checkbox(f"1\\. {data['C1']}", key="C1")
+ C2 = st.checkbox(f"2\\. {data['C2']}", key="C2")
+D = st.checkbox(f"D. {data['D']}\n\n{d_score_badge}", key="D", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ D1 = st.checkbox(f"1\\. {data['D1']}", key="D1")
+ D2 = st.checkbox(f"2\\. {data['D2']}", key="D2")
+ D3 = st.checkbox(f"3\\. {data['D3']}", key="D3")
+ D4 = st.checkbox(f"4\\. {data['D4']}", key="D4")
+ D5 = st.checkbox(f"5\\. {data['D5']}", key="D5")
+ D6 = st.checkbox(f"6\\. {data['D6']}", key="D6")
+ D7 = st.checkbox(f"7\\. {data['D7']}", key="D7")
+E = st.checkbox(f"E. {data['E']}\n\n{e_score_badge}", key="E", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ E1 = st.checkbox(f"1\\. {data['E1']}", key="E1")
+ E2 = st.checkbox(f"2\\. {data['E2']}", key="E2")
+ E3 = st.checkbox(f"3\\. {data['E3']}", key="E3")
+ E4 = st.checkbox(f"4\\. {data['E4']}", key="E4")
+ E5 = st.checkbox(f"5\\. {data['E5']}", key="E5")
+ E6 = st.checkbox(f"6\\. {data['E6']}", key="E6")
+F = st.checkbox(f"F. {data['F']}", key="F")
+G = st.checkbox(f"G. {data['G']}", key="G")
+H = st.checkbox(f"H. {data['H']}", key="H")
+
+st.markdown("## Specifiers")
+
+S1text = data["S1"].replace("With dissociative symptoms:", "**With dissociative symptoms:**")
+S11text = data["S11"].replace("Depersonalization:", "**Depersonalization:**")
+S12text = data["S12"].replace("Derealization:", "**Derealization:**")
+
+st.markdown(":small[Specify whether:]")
+S1 = st.checkbox(f"{S1text}", key="S1", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ S11 = st.checkbox(f"{S11text}", key="S11")
+ S12 = st.checkbox(f"{S12text}", key="S12")
+ st.markdown(f":orange[{data['S1note']}]")
+st.markdown(":small[Specify if:]")
+S2 = st.checkbox(f"{data['S2']}", key="S2")
+
+st.markdown("## Output")
+
+omitFGH = st.toggle("Ignore criteria F, G, and H", key="omitFGH", value=True)
+omitS1 = st.toggle("Ignore dissociative symptoms specifier", key="omitS1", value=False)
+omitS2 = st.toggle("Ignore delayed expression specifier", key="omitS2", value=False)
+
+if omitFGH and not any([omitS1, omitS2]):
+ st.info("Output will not include criteria F, G, and H.", icon="ℹ️")
+elif omitFGH and all([omitS1, omitS2]):
+ st.info("Output will not include criteria F, G, and H, nor dissociative symptoms and delayed expression specifiers.", icon="ℹ️")
+elif omitFGH and any([omitS1, omitS2]):
+ omitted = [o for o in [omitS1, omitS2] if o]
+ omittext = ["dissociative symptoms", "delayed expression"]
+ if len(omitted) == 1:
+ st.info(f"Output will not include criteria F, G, and H, nor {omittext[[omitS1, omitS2].index(True)]} specifier.", icon="ℹ️")
+ else:
+ st.info(f"Output will not include criteria F, G, and H, nor {omittext[[omitS1, omitS2].index(True)]} and {omittext[[omitS1, omitS2].index(True, [omitS1, omitS2].index(True)+1)]} specifiers.", icon="ℹ️")
+elif all([omitS1, omitS2]):
+ st.info("Output will not include dissociative symptoms and delayed expression specifiers.", icon="ℹ️")
+elif any([omitS1, omitS2]):
+ omitted = [o for o in [omitS1, omitS2] if o]
+ omittext = ["dissociative symptoms", "delayed expression"]
+ if len(omitted) == 1:
+ st.info(f"Output will not include {omittext[[omitS1, omitS2].index(True)]} specifier.", icon="ℹ️")
+ else:
+ st.info(f"Output will not include {omittext[[omitS1, omitS2].index(True)]} and {omittext[[omitS1, omitS2].index(True, [omitS1, omitS2].index(True)+1)]} specifiers.", icon="ℹ️")
+
+if not omitFGH:
+ criteria_met = all([st.session_state.get(key, False) for key in ["A", "B", "C", "D", "E", "F", "G", "H"]])
+ if not criteria_met:
+ unmet = [key for key in ["A", "B", "C", "D", "E", "F", "G", "H"] if not st.session_state.get(key, False)]
+ st.error(f"Criteria not met: **{'**, **'.join(unmet)}**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-H are met.\n\nConsider diagnosing :blue[F43.8] Other Specified Trauma- and Stressor-Related Disorder or :blue[F43.9] Unspecified Trauma- and Stressor-Related Disorder", icon="🚨")
+else:
+ criteria_met = all([st.session_state.get(key, False) for key in ["A", "B", "C", "D", "E"]])
+ if not criteria_met:
+ unmet = [key for key in ["A", "B", "C", "D", "E"] if not st.session_state.get(key, False)]
+ st.error(f"Criteria not met: **{'**, **'.join(unmet)}**\n\n{st.session_state.current_dis} cannot be diagnosed unless criteria A-E are met.\n\nConsider diagnosing :blue[F43.8] Other Specified Trauma- and Stressor-Related Disorder or :blue[F43.9] Unspecified Trauma- and Stressor-Related Disorder", icon="🚨")
+
+st.markdown(":blue-badge[Triple click anywhere in the text below to select all of it at once!]")
+with st.container(border=True):
+ data_out = data.copy()
+ output = []
+ pop = ["A", "A4note", "B", "B1note", "B2note", "B3note", "C", "D", "E", "S1", "S11", "S12", "S1note", "S2"]
+ if omitFGH:
+ pop.extend(["F", "G", "H"])
+ for p in pop:
+ data_out.pop(p, None)
+ for key in data_out.keys():
+ if st.session_state.get(key, False):
+ output.append(data_out[key])
+ if st.session_state.get("S11", False) and not omitS1:
+ output.append(data["S11"].replace("Depersonalization: ", ""))
+ if st.session_state.get("S12", False) and not omitS1:
+ output.append(data["S12"].replace("Derealization: ", ""))
+ if st.session_state.get("S2", False) and not omitS2:
+ output.append(data["S2"].split(": ")[0])
+ for i in range(len(output)):
+ output[i] = output[i][0].lower() + output[i][1:].rstrip(".")
+ st.markdown(", ".join(output))
+
+# Back button
+if st.button("<- Return to Homepage", key="backbottom"):
+ st.switch_page("main.py")
+
+add_clean_footer()
\ No newline at end of file
From 66e2c8c3104ce5700e14624db56ca2cdaa145d35 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 20:48:24 +0000
Subject: [PATCH 29/33] Fix ADHD specifier toggles not removing data from
output
---
pages/adhd.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/pages/adhd.py b/pages/adhd.py
index af946a5..8c53034 100644
--- a/pages/adhd.py
+++ b/pages/adhd.py
@@ -147,10 +147,12 @@
output.append(data_out[key].replace("(Note:", "(note:"))
else:
output.append(data_out[key])
- output.append([data["S1a"], data["S1b"], data["S1c"]][S1text.index(S1)].replace(" If ", " if ", 1).replace(" Criterion ", " criterion "))
- if st.session_state.S2:
+ if not omitS1:
+ output.append([data["S1a"], data["S1b"], data["S1c"]][S1text.index(S1)].replace(" If ", " if ", 1).replace(" Criterion ", " criterion "))
+ if st.session_state.S2 and not omitS2:
output.append(data["S2"].replace(" Full ", " full ", 1))
- output.append([data["S3a"].replace("Few", "few", 1), data["S3b"].replace("Symptoms", "symptoms", 1), data["S3c"].replace("Many", "many", 1)][[f":green[**Mild**]\n\n{data['S3a'].replace("Mild: ", "")}", f":orange[**Moderate**]\n\n{data['S3b'].replace("Moderate: ", "")}", f":red[**Severe**]\n\n{data['S3c'].replace("Severe: ", "")}"].index(S3)])
+ if not omitS3:
+ output.append([data["S3a"].replace("Few", "few", 1), data["S3b"].replace("Symptoms", "symptoms", 1), data["S3c"].replace("Many", "many", 1)][[f":green[**Mild**]\n\n{data['S3a'].replace("Mild: ", "")}", f":orange[**Moderate**]\n\n{data['S3b'].replace("Moderate: ", "")}", f":red[**Severe**]\n\n{data['S3c'].replace("Severe: ", "")}"].index(S3)])
for i in range(len(output)):
output[i] = output[i][0].lower() + output[i][1:].rstrip(".")
st.markdown(", ".join(output))
From 1c101ae8c345c131ac1851c0de59ae2b1c18bb55 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 20:55:19 +0000
Subject: [PATCH 30/33] Fix wrong env var get in footer
---
utils.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/utils.py b/utils.py
index c08cecd..445a2dc 100644
--- a/utils.py
+++ b/utils.py
@@ -5,9 +5,9 @@
def add_clean_footer():
- branch = os.environ.get("GIT_BRANCH", "UNKNOWN")
+ branch = os.environ.get("APP_GIT_BRANCH", "UNKNOWN")
branch_url = f"https://github.com/hermaplusplus/Arwen/tree/{branch}"
- commit = os.environ.get("GIT_COMMIT", "UNKNOWN")
+ commit = os.environ.get("APP_GIT_COMMIT", "UNKNOWN")
commit_url = f"https://github.com/hermaplusplus/Arwen/commit/{commit}"
if branch != "UNKNOWN":
From 565cb4490566185d58d78a87a0344810c43df3ea Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 21:04:02 +0000
Subject: [PATCH 31/33] Update README
---
README.md | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6dd38f7..c0052ea 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,16 @@
# Arwen
-Diagnostic Criteria aggregator and formatter for EHRs
+
+**Arwen DCA** is designed to help clinicians aggregate diagnostic criteria based on the structure found in the DSM-5-TR.
+
+> [!CAUTION]
+> This is **not** a diagnostic tool and should not be used as such. Clinicians should always use their own judgement and verify that criteria and codes are correct.
+
+Clincians can select all diagnostic criteria that apply to their patient/client and the tool will output a list of criteria met formatted in a way that can be easily copied into an EHR or note (including systems which parse criteria to :sparkles: automagically* :sparkles: create a note)
+
+The tool does **not store any data** that is submitted and **does not allow the input of any PHI**. When a user refreshes a page or navigates away, all data is irrevocably lost.
+
+This project is open source and available on [GitHub](https://github.com/hermaplusplus/Arwen). It is provided under the [MIT License](https://github.com/hermaplusplus/Arwen/blob/main/LICENSE). Contributions, issue reports, feedback, and suggestions are welcome.
+
+Why is the tool called 'Arwen'? I watched Lord of the Rings recently. That's it.
+
+* Automagically, meaning using a Large Language Model.
\ No newline at end of file
From bf165d86d8e00639001ab8c147efdee03461a82f Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 21:11:33 +0000
Subject: [PATCH 32/33] Add more information to README
---
README.md | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index c0052ea..93165ef 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Arwen
+## About
+
**Arwen DCA** is designed to help clinicians aggregate diagnostic criteria based on the structure found in the DSM-5-TR.
> [!CAUTION]
@@ -13,4 +15,14 @@ This project is open source and available on [GitHub](https://github.com/hermapl
Why is the tool called 'Arwen'? I watched Lord of the Rings recently. That's it.
-* Automagically, meaning using a Large Language Model.
\ No newline at end of file
+* Automagically, meaning using a Large Language Model.
+
+## Development
+
+Arwen uses [uv](https://docs.astral.sh/uv/) as a package manager and [Docker Compose](https://docs.docker.com/compose/) for containerisation. To run the project, make sure you have these installed, as well as Python 3.11+.
+
+To activate the virtual environment, run the appropriate (for your system) script under `./.venv/Scripts/`.
+
+To run the project during development, run `streamlit run main.py`.
+
+To deploy the project in production, run `./start.sh`.
From ef2ee6bdf5cdf23d17020c4f8669d58b3cbb5254 Mon Sep 17 00:00:00 2001
From: hermaplusplus <49307226+hermaplusplus@users.noreply.github.com>
Date: Wed, 11 Feb 2026 21:41:30 +0000
Subject: [PATCH 33/33] Delete dsm.yaml
---
dsm.yaml | 22 ----------------------
1 file changed, 22 deletions(-)
delete mode 100644 dsm.yaml
diff --git a/dsm.yaml b/dsm.yaml
deleted file mode 100644
index 0d9c15e..0000000
--- a/dsm.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-Neurodevelopmental Disorders: []
-Schizophrenia Spectrum and Other Psychotic Disorders: []
-Bipolar and Related Disorders: []
-Depressive Disorders: []
-Anxiety Disorders: []
-Obsessive-Compulsive and Related Disorders: []
-Trauma- and Stressor-Related Disorders: []
-Dissociative Disorders: []
-Somatic Symptom and Related Disorders: []
-Feeding and Eating Disorders: []
-Elimination Disorders: []
-Sleep-Wake Disorders: []
-Sexual Dysfunctions: []
-Gender Dysphoria: []
-Disruptive, Impulse-Control, and Conduct Disorders: []
-Substance-Related and Addictive Disorders: []
-Neurocognitive Disorders: []
-Personality Disorders: []
-Paraphilic Disorders: []
-Other Mental Disorders and Additonal Codes: []
-Medication-Induced Movement Disorders and Other Adverse Effects of Medication: []
-Other Conditions That May Be a Focus of Clinical Attention: []
\ No newline at end of file