File size: 9,064 Bytes
268b40a | 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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | import anthropic
import os
import json
from dotenv import load_dotenv
from vector_store import retrieve_similar_notes
load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
# ββ Tool Definitions ββββββββββββββββββββββββββββββββββββββββββ
# These are the tools the agent can choose to call
TOOLS = [
{
"name": "retrieve_similar_cases",
"description": "Search the clinical knowledge base for similar past cases. Use this when the transcript mentions specific symptoms, diagnoses, or conditions that would benefit from historical context.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A clinical query based on the patient's symptoms or condition"
},
"n_results": {
"type": "integer",
"description": "Number of similar cases to retrieve (1-5)",
"default": 3
}
},
"required": ["query"]
}
},
{
"name": "check_medications",
"description": "Extract all medications mentioned in the transcript and flag any that need attention, such as missing dosages or potential interactions.",
"input_schema": {
"type": "object",
"properties": {
"transcript": {
"type": "string",
"description": "The full transcript to extract medications from"
}
},
"required": ["transcript"]
}
},
{
"name": "assess_completeness",
"description": "Assess what critical information is missing from the transcript before generating the SOAP note. Use this for vague or incomplete transcripts.",
"input_schema": {
"type": "object",
"properties": {
"transcript": {
"type": "string",
"description": "The transcript to assess for completeness"
}
},
"required": ["transcript"]
}
}
]
# ββ Tool Execution Functions ββββββββββββββββββββββββββββββββββ
# What actually runs when the agent calls each tool
def run_tool(tool_name: str, tool_input: dict) -> str:
if tool_name == "retrieve_similar_cases":
query = tool_input["query"]
n = tool_input.get("n_results", 3)
notes, metadatas = retrieve_similar_notes(query, n_results=n)
result = f"Retrieved {len(notes)} similar cases:\n"
for i, (note, meta) in enumerate(zip(notes, metadatas)):
result += f"\n[Case {i+1}] Age: {meta['age']} | Visit: {meta['visit_motivation'][:100]}\n"
result += f"Note: {note[:400]}\n"
return result
elif tool_name == "check_medications":
transcript = tool_input["transcript"]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=400,
messages=[{
"role": "user",
"content": f"""Extract all medications from this transcript. For each one list:
- Medication name
- Dosage (or flag as MISSING if not mentioned)
- Frequency (or flag as MISSING if not mentioned)
- Any concerns
Transcript: {transcript}
Be concise. If no medications mentioned, say so."""
}]
)
return response.content[0].text
elif tool_name == "generate_soap_note":
transcript = tool_input["transcript"]
prior_context = tool_input.get("prior_context", "")
context_block = ""
if prior_context:
context_block = f"\n\nCLINICAL CONTEXT FROM SIMILAR CASES:\n{prior_context}\n"
system_prompt = """You are an expert medical scribe. Given a doctor-patient
conversation transcript, extract and structure ALL information into a detailed SOAP note.
If prior case context is provided, use it to inform your clinical reasoning and
reference relevant patterns in the Assessment section.
Return the note in EXACTLY this format β do not skip any section:
**SUBJECTIVE:**
- Chief Complaint:
- History of Present Illness:
- Onset:
- Duration:
- Quality/Character:
- Severity (1-10):
- Location/Radiation:
- Aggravating Factors:
- Relieving Factors:
- Associated Symptoms:
- Current Medications:
- Allergies:
- Past Medical History:
- Family History:
- Social History:
- Review of Symptoms:
**OBJECTIVE:**
- Vital Signs:
- Blood Pressure:
- Heart Rate:
- Temperature:
- Respiratory Rate:
- O2 Saturation:
- General Appearance:
- Physical Exam Findings:
- Diagnostic Results (if mentioned):
**ASSESSMENT:**
- Primary Diagnosis:
- Differential Diagnoses:
- Clinical Reasoning:
**PLAN:**
- Medications/Orders:
- Diagnostics Ordered:
- Referrals:
- Patient Education:
- Follow-up:
- Precautions/Return to ED if:
Fill every field with information from the transcript.
For fields not mentioned, write [Not reported].
Flag critical missing information with [MISSING - REQUIRED].
Never leave a field blank."""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=3000,
system=system_prompt,
messages=[{
"role": "user",
"content": f"{context_block}\nToday's transcript:\n{transcript}"
}]
)
return response.content[0].text
return f"Unknown tool: {tool_name}"
# ββ The Agent Loop ββββββββββββββββββββββββββββββββββββββββββββ
def run_agent(transcript: str):
steps = []
collected_context = []
system_prompt = """You are an expert AI clinical scribe agent.
Your job is to gather context before a SOAP note is generated.
You have access to 3 tools:
1. assess_completeness - check what info is missing from the transcript
2. retrieve_similar_cases - search for similar past clinical cases
3. check_medications - extract and validate medications mentioned
ALWAYS call all 3 tools in that order. Do not skip any.
After calling all 3 tools, stop. Do not write anything else."""
messages = [
{
"role": "user",
"content": f"Gather context for this transcript:\n\n{transcript}"
}
]
# ββ Agent loop: only runs the 3 context tools ββββββββββ
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
system=system_prompt,
tools=[t for t in TOOLS if t["name"] != "generate_soap_note"],
messages=messages
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
break
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
steps.append({
"type": "tool_call",
"tool": tool_name,
"input": tool_input
})
result = run_tool(tool_name, tool_input)
collected_context.append(f"[{tool_name}]:\n{result}")
steps.append({
"type": "tool_result",
"tool": tool_name,
"result": result
})
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})
else:
break
# ββ Directly call SOAP generator with all context ββββββ
steps.append({
"type": "tool_call",
"tool": "generate_soap_note",
"input": {}
})
prior_context = "\n\n".join(collected_context)
soap_note = run_tool("generate_soap_note", {
"transcript": transcript,
"prior_context": prior_context
})
steps.append({
"type": "tool_result",
"tool": "generate_soap_note",
"result": soap_note
})
steps.append({
"type": "final",
"content": soap_note
})
from evaluate_note import evaluate_note
evaluation = evaluate_note(transcript, soap_note)
steps.append({
"type": "evaluation",
"content": evaluation
})
return steps |