Spaces:
Runtime error
Runtime error
Claude commited on
Fix HF Spaces runtime errors: missing SimulationOrchestrator, httpx proxies, ChromaDB telemetry
Browse files- Add SimulationOrchestrator class to orchestrator.py (was only AgentOrchestrator,
causing ImportError that prevented app startup)
- Pin httpx<0.28.0 to fix 'proxies' keyword argument error in Anthropic client
- Disable ChromaDB telemetry to fix posthog capture() argument errors
https://claude.ai/code/session_01EtTBqEZVEmdWihzhSden2o
- app.py +3 -0
- backend/app/core/agents/orchestrator.py +163 -0
- backend/requirements.txt +1 -0
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -14,6 +14,9 @@ sys.path.insert(0, str(Path(__file__).parent / "backend"))
|
|
| 14 |
# Set environment variables for Hugging Face Spaces
|
| 15 |
os.environ["HF_SPACE"] = "1"
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
from fastapi import FastAPI
|
| 18 |
from fastapi.staticfiles import StaticFiles
|
| 19 |
from fastapi.responses import FileResponse, HTMLResponse
|
|
|
|
| 14 |
# Set environment variables for Hugging Face Spaces
|
| 15 |
os.environ["HF_SPACE"] = "1"
|
| 16 |
|
| 17 |
+
# Disable ChromaDB telemetry to avoid posthog capture() errors
|
| 18 |
+
os.environ["ANONYMIZED_TELEMETRY"] = "False"
|
| 19 |
+
|
| 20 |
from fastapi import FastAPI
|
| 21 |
from fastapi.staticfiles import StaticFiles
|
| 22 |
from fastapi.responses import FileResponse, HTMLResponse
|
backend/app/core/agents/orchestrator.py
CHANGED
|
@@ -721,3 +721,166 @@ class AgentOrchestrator:
|
|
| 721 |
|
| 722 |
# Singleton orchestrator shared across the app
|
| 723 |
orchestrator = AgentOrchestrator()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
|
| 722 |
# Singleton orchestrator shared across the app
|
| 723 |
orchestrator = AgentOrchestrator()
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
class SimulationOrchestrator:
|
| 727 |
+
"""Simplified orchestrator for the /api/simulation endpoints.
|
| 728 |
+
|
| 729 |
+
Wraps case generation + simulation state management to provide
|
| 730 |
+
the interface expected by simulation.py (start_simulation,
|
| 731 |
+
process_student_message, complete_simulation, get_simulation).
|
| 732 |
+
"""
|
| 733 |
+
|
| 734 |
+
def __init__(self):
|
| 735 |
+
from app.core.rag.shared import case_generator
|
| 736 |
+
from app.models.simulation import (
|
| 737 |
+
SimulationState,
|
| 738 |
+
PatientProfile,
|
| 739 |
+
PatientGender,
|
| 740 |
+
EmotionalState,
|
| 741 |
+
RapportLevel,
|
| 742 |
+
SimulationMessage,
|
| 743 |
+
TutorFeedback,
|
| 744 |
+
FeedbackType,
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
self._case_generator = case_generator
|
| 748 |
+
self._simulations: dict[str, SimulationState] = {}
|
| 749 |
+
|
| 750 |
+
# Store model refs for use in methods
|
| 751 |
+
self._SimulationState = SimulationState
|
| 752 |
+
self._PatientProfile = PatientProfile
|
| 753 |
+
self._PatientGender = PatientGender
|
| 754 |
+
self._EmotionalState = EmotionalState
|
| 755 |
+
self._RapportLevel = RapportLevel
|
| 756 |
+
self._SimulationMessage = SimulationMessage
|
| 757 |
+
self._TutorFeedback = TutorFeedback
|
| 758 |
+
self._FeedbackType = FeedbackType
|
| 759 |
+
|
| 760 |
+
def start_simulation(self, specialty: str = "general_medicine", difficulty: str = "intermediate"):
|
| 761 |
+
"""Start a new patient simulation, returning a SimulationState."""
|
| 762 |
+
case = self._case_generator.generate_case(specialty=specialty, difficulty=difficulty)
|
| 763 |
+
case_id = case.get("id", str(uuid.uuid4())[:8])
|
| 764 |
+
|
| 765 |
+
# Map case data to PatientProfile
|
| 766 |
+
gender_raw = case.get("patient_gender", "male").lower()
|
| 767 |
+
gender_map = {"male": self._PatientGender.MALE, "female": self._PatientGender.FEMALE,
|
| 768 |
+
"pregnant": self._PatientGender.PREGNANT}
|
| 769 |
+
gender = gender_map.get(gender_raw, self._PatientGender.MALE)
|
| 770 |
+
|
| 771 |
+
profile = self._PatientProfile(
|
| 772 |
+
age=case.get("patient_age", 45),
|
| 773 |
+
gender=gender,
|
| 774 |
+
name=case.get("patient_name", "Patient"),
|
| 775 |
+
chief_complaint=case.get("chief_complaint", ""),
|
| 776 |
+
setting=case.get("setting", "OPD"),
|
| 777 |
+
specialty=specialty,
|
| 778 |
+
difficulty=difficulty,
|
| 779 |
+
actual_diagnosis=case.get("diagnosis", "Unknown"),
|
| 780 |
+
key_history_points=case.get("key_history", case.get("learning_points", [])),
|
| 781 |
+
physical_exam_findings=case.get("physical_exam", {}),
|
| 782 |
+
)
|
| 783 |
+
|
| 784 |
+
initial_message = self._SimulationMessage(
|
| 785 |
+
role="patient",
|
| 786 |
+
content=case.get("initial_presentation", f"Doctor, {profile.chief_complaint}"),
|
| 787 |
+
emotional_state=self._EmotionalState.CONCERNED,
|
| 788 |
+
)
|
| 789 |
+
|
| 790 |
+
sim = self._SimulationState(
|
| 791 |
+
case_id=case_id,
|
| 792 |
+
patient_profile=profile,
|
| 793 |
+
emotional_state=self._EmotionalState.CONCERNED,
|
| 794 |
+
rapport_level=self._RapportLevel.MODERATE,
|
| 795 |
+
messages=[initial_message],
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
self._simulations[case_id] = sim
|
| 799 |
+
return sim
|
| 800 |
+
|
| 801 |
+
def process_student_message(self, case_id: str, student_message: str):
|
| 802 |
+
"""Process a student message and return the updated SimulationState."""
|
| 803 |
+
sim = self._get_or_raise(case_id)
|
| 804 |
+
|
| 805 |
+
# Record student message
|
| 806 |
+
sim.messages.append(self._SimulationMessage(role="student", content=student_message))
|
| 807 |
+
|
| 808 |
+
# Generate patient response using the agent orchestrator if possible
|
| 809 |
+
patient_response = self._generate_patient_response(sim, student_message)
|
| 810 |
+
|
| 811 |
+
sim.messages.append(self._SimulationMessage(
|
| 812 |
+
role="patient",
|
| 813 |
+
content=patient_response,
|
| 814 |
+
emotional_state=sim.emotional_state,
|
| 815 |
+
))
|
| 816 |
+
|
| 817 |
+
# Generate tutor feedback
|
| 818 |
+
feedback_type, feedback_msg = self._evaluate_student_message(student_message)
|
| 819 |
+
sim.tutor_feedback.append(self._TutorFeedback(type=feedback_type, message=feedback_msg))
|
| 820 |
+
|
| 821 |
+
return sim
|
| 822 |
+
|
| 823 |
+
def complete_simulation(self, case_id: str, diagnosis: str, reasoning: str):
|
| 824 |
+
"""Mark simulation as complete with student's diagnosis."""
|
| 825 |
+
from datetime import datetime
|
| 826 |
+
|
| 827 |
+
sim = self._get_or_raise(case_id)
|
| 828 |
+
sim.student_diagnosis = diagnosis
|
| 829 |
+
sim.student_reasoning = reasoning
|
| 830 |
+
sim.completed_at = datetime.now()
|
| 831 |
+
return sim
|
| 832 |
+
|
| 833 |
+
def get_simulation(self, case_id: str):
|
| 834 |
+
"""Get simulation state by case_id."""
|
| 835 |
+
return self._get_or_raise(case_id)
|
| 836 |
+
|
| 837 |
+
def _get_or_raise(self, case_id: str):
|
| 838 |
+
sim = self._simulations.get(case_id)
|
| 839 |
+
if not sim:
|
| 840 |
+
raise ValueError(f"Simulation {case_id} not found")
|
| 841 |
+
return sim
|
| 842 |
+
|
| 843 |
+
def _generate_patient_response(self, sim, student_message: str) -> str:
|
| 844 |
+
"""Generate a contextual patient response."""
|
| 845 |
+
complaint = sim.patient_profile.chief_complaint
|
| 846 |
+
name = sim.patient_profile.name
|
| 847 |
+
|
| 848 |
+
open_ended_markers = ["tell me", "describe", "how", "what", "when", "where"]
|
| 849 |
+
is_open = any(m in student_message.lower() for m in open_ended_markers)
|
| 850 |
+
|
| 851 |
+
empathy_markers = ["understand", "worried", "difficult", "sorry", "must be"]
|
| 852 |
+
shows_empathy = any(m in student_message.lower() for m in empathy_markers)
|
| 853 |
+
|
| 854 |
+
if shows_empathy:
|
| 855 |
+
if sim.rapport_level.value < 5:
|
| 856 |
+
sim.rapport_level = self._RapportLevel(min(5, sim.rapport_level.value + 1))
|
| 857 |
+
sim.emotional_state = self._EmotionalState.CALM
|
| 858 |
+
return (
|
| 859 |
+
f"Thank you doctor, that makes me feel better. "
|
| 860 |
+
f"Actually, I also wanted to mention that the {complaint} has been getting worse at night."
|
| 861 |
+
)
|
| 862 |
+
|
| 863 |
+
if is_open:
|
| 864 |
+
return (
|
| 865 |
+
f"Doctor, the {complaint} started about 3-4 days ago. "
|
| 866 |
+
f"First I thought it was nothing, tried some home remedies. "
|
| 867 |
+
f"But it kept getting worse so my family brought me here."
|
| 868 |
+
)
|
| 869 |
+
|
| 870 |
+
return (
|
| 871 |
+
f"Yes doctor, the {complaint} is still bothering me. "
|
| 872 |
+
f"What do you think it could be?"
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
def _evaluate_student_message(self, message: str):
|
| 876 |
+
"""Simple heuristic evaluation of student communication."""
|
| 877 |
+
empathy_markers = ["understand", "worried", "difficult", "sorry", "must be", "concern"]
|
| 878 |
+
open_markers = ["tell me", "describe", "how do you", "what happened", "can you explain"]
|
| 879 |
+
|
| 880 |
+
if any(m in message.lower() for m in empathy_markers):
|
| 881 |
+
return self._FeedbackType.POSITIVE, "Good empathetic communication. This builds rapport."
|
| 882 |
+
if any(m in message.lower() for m in open_markers):
|
| 883 |
+
return self._FeedbackType.POSITIVE, "Nice open-ended question. This encourages the patient to share more."
|
| 884 |
+
if message.strip().endswith("?") and len(message.split()) > 5:
|
| 885 |
+
return self._FeedbackType.WARNING, "Consider using more open-ended questions to gather richer history."
|
| 886 |
+
return self._FeedbackType.WARNING, "Try to build rapport with empathetic language before diving into clinical questions."
|
backend/requirements.txt
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
fastapi==0.115.0
|
| 2 |
uvicorn==0.30.0
|
| 3 |
anthropic==0.39.0
|
|
|
|
| 4 |
chromadb==0.5.0
|
| 5 |
langchain==0.3.0
|
| 6 |
langchain-community==0.3.0
|
|
|
|
| 1 |
fastapi==0.115.0
|
| 2 |
uvicorn==0.30.0
|
| 3 |
anthropic==0.39.0
|
| 4 |
+
httpx<0.28.0
|
| 5 |
chromadb==0.5.0
|
| 6 |
langchain==0.3.0
|
| 7 |
langchain-community==0.3.0
|
requirements.txt
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
fastapi==0.115.0
|
| 2 |
uvicorn==0.30.0
|
| 3 |
anthropic==0.39.0
|
|
|
|
| 4 |
chromadb==0.5.0
|
| 5 |
langchain==0.3.0
|
| 6 |
langchain-community==0.3.0
|
|
|
|
| 1 |
fastapi==0.115.0
|
| 2 |
uvicorn==0.30.0
|
| 3 |
anthropic==0.39.0
|
| 4 |
+
httpx<0.28.0
|
| 5 |
chromadb==0.5.0
|
| 6 |
langchain==0.3.0
|
| 7 |
langchain-community==0.3.0
|