-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsoap.py
More file actions
115 lines (92 loc) · 4.27 KB
/
Copy pathsoap.py
File metadata and controls
115 lines (92 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
from __future__ import annotations
import asyncio
import json
import os
from pathlib import Path
from typing import Literal, TypedDict, cast
from voice_note import get_groq_client
DEFAULT_MODEL = "llama-3.3-70b-versatile"
SOAP_FIELDS = ("Subjective", "Objective", "Assessment", "Plan")
DIAGNOSIS_QUERY_FIELDS = {"phrase", "kind", "certainty", "qualifiers"}
class DiagnosisQuery(TypedDict):
phrase: str
kind: Literal["diagnosis", "symptom"]
certainty: Literal["confirmed", "uncertain", "historical"]
qualifiers: list[str]
class SOAPNote(TypedDict):
Subjective: str
Objective: str
Assessment: str
Plan: str
DiagnosisQueries: list[DiagnosisQuery]
SYSTEM_PROMPT = """You are a clinical documentation assistant. Convert the supplied
medical encounter transcript into a concise SOAP note. Use only information stated
in the transcript; do not invent findings, diagnoses, medications, or follow-up.
Return one JSON object with exactly these fields: Subjective, Objective, Assessment,
and Plan as strings, plus DiagnosisQueries as a JSON array. Each DiagnosisQueries
item must contain exactly: phrase (a concise documented condition or symptom), kind
(diagnosis or symptom), certainty (confirmed, uncertain, or historical), and
qualifiers (an array of documented details such as acuity, laterality, anatomical
site, etiology, or encounter type). Preserve uncertainty wording. Include symptoms
that may require coding when no established diagnosis explains them. Do not include
ruled-out conditions, infer diagnoses, or emit ICD codes. Use an empty string for a
SOAP section with no supported content and an empty array when there are no supported
diagnosis queries. Do not include markdown or text outside the JSON object."""
def _valid_diagnosis_query(value: object) -> bool:
if not isinstance(value, dict) or set(value) != DIAGNOSIS_QUERY_FIELDS:
return False
return (
isinstance(value["phrase"], str)
and bool(value["phrase"].strip())
and value["kind"] in {"diagnosis", "symptom"}
and value["certainty"] in {"confirmed", "uncertain", "historical"}
and isinstance(value["qualifiers"], list)
and all(isinstance(item, str) for item in value["qualifiers"])
)
def create_soap_note(transcript: str, model: str | None = None) -> SOAPNote:
"""Generate and validate a SOAP note from a completed transcript."""
transcript = transcript.strip()
if not transcript:
raise ValueError("The completed transcript cannot be empty.")
completion = get_groq_client().chat.completions.create(
model=model or os.getenv("GROQ_SOAP_MODEL", DEFAULT_MODEL),
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": transcript},
],
temperature=0,
response_format={"type": "json_object"},
)
content = completion.choices[0].message.content
if not content:
raise ValueError("Groq returned an empty SOAP note.")
try:
note = json.loads(content)
except json.JSONDecodeError as exc:
raise ValueError("Groq returned invalid JSON for the SOAP note.") from exc
if not isinstance(note, dict):
raise ValueError("Groq returned a SOAP note that is not a JSON object.")
expected_fields = {*SOAP_FIELDS, "DiagnosisQueries"}
if set(note) != expected_fields or not all(
isinstance(note[field], str) for field in SOAP_FIELDS
) or not isinstance(note["DiagnosisQueries"], list) or not all(
_valid_diagnosis_query(query) for query in note["DiagnosisQueries"]
):
raise ValueError(
"Groq SOAP output must contain the four SOAP strings and a valid "
"DiagnosisQueries array."
)
return cast(SOAPNote, note)
def create_soap_note_from_file(
transcript_path: str | Path,
model: str | None = None,
) -> SOAPNote:
"""Read a completed transcript file and generate its SOAP note."""
transcript = Path(transcript_path).read_text(encoding="utf-8")
return create_soap_note(transcript, model=model)
async def create_soap_note_async(
transcript: str,
model: str | None = None,
) -> SOAPNote:
"""Generate a SOAP note without blocking an async request handler."""
return await asyncio.to_thread(create_soap_note, transcript, model)