| 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")) |
|
|
| |
| |
|
|
| 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"] |
| } |
| } |
| ] |
|
|
| |
| |
|
|
| 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}" |
|
|
|
|
| |
|
|
| 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}" |
| } |
| ] |
|
|
| |
| 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 |
|
|
| |
| 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 |