├── src ├── requirements.txt ├── healthcare_record.avsc ├── main.py ├── dummy_data_producer.py └── agent.py ├── .gitignore ├── Dockerfile ├── LICENSE └── README.md /src/requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic>=2.7.4,<3.0.0 2 | pydantic-settings>=2.0.0,<3.0.0 3 | langchain==0.3.25 4 | langchain-community==0.3.24 5 | langchain-aws 6 | boto3 7 | requests>=2.31.0,<2.32.0 8 | confluent-kafka==2.10.0 9 | python-dotenv 10 | fastapi 11 | uvicorn 12 | authlib 13 | cachetools 14 | fastavro 15 | httpx 16 | attrs 17 | avro-python3 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Virtual environment 7 | .venv/ 8 | venv/ 9 | ENV/ 10 | env/ 11 | 12 | # Environment variables 13 | .env 14 | 15 | # IDE / Editor specific 16 | .vscode/ 17 | .idea/ 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sublime-project 23 | *.sublime-workspace 24 | 25 | # Operating System files 26 | .DS_Store 27 | Thumbs.db 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the requirements file into the container at /app 8 | COPY ./source_code/requirements.txt /app/requirements.txt 9 | 10 | # Install any needed packages specified in requirements.txt 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Copy the rest of the application's source code from the host to the container at /app 14 | COPY ./source_code /app/source_code 15 | 16 | # Make port 8000 available to the world outside this container 17 | EXPOSE 8000 18 | 19 | # Define environment variable 20 | ENV PYTHONPATH=/app 21 | 22 | # Run main.py when the container launches 23 | # The command will be to run uvicorn server for the FastAPI app defined in source_code/main.py 24 | CMD ["uvicorn", "source_code.main:app", "--host", "0.0.0.0", "--port", "8000"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Daniel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/healthcare_record.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "HealthcareRecord", 4 | "namespace": "com.mycorp.healthcare", 5 | "fields": [ 6 | { 7 | "name": "record_type", 8 | "type": { 9 | "type": "enum", 10 | "name": "RecordType", 11 | "symbols": ["ICU_PATIENT_UPDATE", "APPOINTMENT_SCHEDULE"] 12 | } 13 | }, 14 | { 15 | "name": "patient_id", 16 | "type": "string" 17 | }, 18 | { 19 | "name": "timestamp", 20 | "type": "long", 21 | "logicalType": "timestamp-millis" 22 | }, 23 | { 24 | "name": "icu_details", 25 | "type": [ 26 | "null", 27 | { 28 | "type": "record", 29 | "name": "ICUDetails", 30 | "fields": [ 31 | {"name": "bed_number", "type": "string"}, 32 | {"name": "heart_rate", "type": "int"}, 33 | {"name": "blood_pressure", "type": "string"}, 34 | {"name": "respiratory_rate", "type": "int"}, 35 | {"name": "temperature_celsius", "type": "float"}, 36 | {"name": "notes", "type": ["null", "string"]} 37 | ] 38 | } 39 | ], 40 | "default": null 41 | }, 42 | { 43 | "name": "appointment_details", 44 | "type": [ 45 | "null", 46 | { 47 | "type": "record", 48 | "name": "AppointmentDetails", 49 | "fields": [ 50 | {"name": "doctor_id", "type": "string"}, 51 | {"name": "appointment_time", "type": "long", "logicalType": "timestamp-millis"}, 52 | {"name": "reason", "type": "string"} 53 | ] 54 | } 55 | ], 56 | "default": null 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException 2 | from typing import List, Dict, Any 3 | from pydantic import BaseModel 4 | 5 | from agent import HealthcareAgent 6 | 7 | agent = HealthcareAgent() 8 | 9 | app = FastAPI( 10 | title="Healthcare AI Agent", 11 | description="An AI agent to provide details on ICU patients and upcoming appointments.", 12 | version="0.1.0" 13 | ) 14 | 15 | # Data will now be managed by the agent instance 16 | # icu_patients: Dict[str, Dict] = {} 17 | # appointments: Dict[str, List[Dict]] = {} 18 | 19 | @app.on_event("startup") 20 | async def startup_event(): 21 | """Placeholder for startup logic, like starting the Kafka consumer.""" 22 | print("Healthcare AI Agent is starting up...") 23 | agent.start_consumer() # Start the Kafka consumer 24 | 25 | @app.get("/") 26 | async def root(): 27 | return {"message": "Healthcare AI Agent is running. Use /docs for API documentation."} 28 | 29 | @app.get("/icu/patients", response_model=Dict[str, Dict]) 30 | async def get_icu_patients(): 31 | """Retrieve a list of all patients currently in the ICU.""" 32 | if not agent.icu_patients: 33 | return {"message": "No ICU patient data available at the moment."} 34 | return agent.icu_patients 35 | 36 | class ChatQuery(BaseModel): 37 | query: str 38 | session_id: str = "default_session" # Optional: for maintaining conversation history 39 | 40 | class ChatResponse(BaseModel): 41 | response: str 42 | session_id: str 43 | 44 | @app.post("/chat", response_model=ChatResponse) 45 | async def chat_with_agent(chat_query: ChatQuery): 46 | """Endpoint for doctors to chat with the Healthcare AI agent.""" 47 | response_text = await agent.handle_chat(chat_query.query, chat_query.session_id) 48 | return ChatResponse(response=response_text, session_id=chat_query.session_id) 49 | 50 | @app.get("/alerts", response_model=List[Dict[str, Any]]) 51 | async def get_active_alerts(): 52 | """Retrieve a list of active alerts generated by the agent.""" 53 | alerts = agent.get_alerts() 54 | return alerts 55 | 56 | @app.get("/icu/patients/{patient_id}", response_model=Dict) 57 | async def get_icu_patient_details(patient_id: str): 58 | """Retrieve details for a specific ICU patient.""" 59 | patient = agent.icu_patients.get(patient_id) 60 | if not patient: 61 | raise HTTPException(status_code=404, detail=f"Patient {patient_id} not found in ICU") 62 | return patient 63 | 64 | @app.get("/appointments/{doctor_id}", response_model=List[Dict]) 65 | async def get_upcoming_appointments(doctor_id: str): 66 | """Retrieve upcoming appointments for a specific doctor.""" 67 | doctor_appointments = agent.appointments.get(doctor_id) 68 | if not doctor_appointments: 69 | return [] # Return empty list if no appointments, or a message 70 | # return {"message": f"No upcoming appointments found for Dr. {doctor_id}."} 71 | return doctor_appointments 72 | 73 | @app.on_event("shutdown") 74 | async def shutdown_event(): 75 | """Placeholder for shutdown logic.""" 76 | print("Healthcare AI Agent is shutting down...") 77 | agent.stop_consumer() # Stop the Kafka consumer 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Healthcare AI Agent 2 | 3 | A comprehensive healthcare management system that provides a REST API for accessing ICU patient data and medical appointments. The system leverages AWS Bedrock for AI-powered clinical assistance and real-time alert generation for critical patient conditions. 4 | 5 | ## Overview 6 | 7 | The Healthcare AI Agent is designed to streamline healthcare operations by providing: 8 | - Real-time ICU patient monitoring and data management 9 | - Intelligent appointment scheduling and tracking 10 | - AI-powered clinical decision support through chat interface 11 | - Automated alert system for critical patient conditions 12 | - Scalable data ingestion via Apache Kafka 13 | 14 | ## Architecture 15 | 16 | The system utilizes a microservices architecture with the following components: 17 | - **FastAPI REST API**: Provides endpoints for data access and AI interactions 18 | - **Apache Kafka Integration**: Real-time data streaming via Confluent Cloud 19 | - **AWS Bedrock**: AI/ML services for chat assistance and data generation 20 | - **Avro Schema**: Structured data serialization for reliable message processing 21 | 22 | ## Project Structure 23 | 24 | ``` 25 | healthcare_agent/ 26 | ├── Dockerfile # Container configuration 27 | ├── README.md # Project documentation 28 | ├── .gitignore # Git ignore rules 29 | └── src/ 30 | ├── agent.py # Core agent logic and Kafka consumer 31 | ├── main.py # FastAPI application and API endpoints 32 | ├── dummy_data_producer.py # Test data generator 33 | ├── healthcare_record.avsc # Avro schema definition 34 | ├── requirements.txt # Python dependencies 35 | └── .env.example # Environment configuration template 36 | ``` 37 | 38 | ## Prerequisites 39 | 40 | - Python 3.8 or higher 41 | - Docker (optional, for containerized deployment) 42 | - AWS Account with Bedrock access 43 | - Confluent Cloud Kafka cluster 44 | - Valid AWS credentials configured 45 | 46 | ## Installation & Setup 47 | 48 | ### 1. Environment Setup 49 | 50 | Clone the repository and navigate to the source directory: 51 | 52 | ```bash 53 | cd src 54 | ``` 55 | 56 | Create and activate a Python virtual environment: 57 | 58 | ```bash 59 | python -m venv venv 60 | ``` 61 | 62 | **Windows:** 63 | ```bash 64 | .\venv\Scripts\activate 65 | ``` 66 | 67 | **macOS/Linux:** 68 | ```bash 69 | source venv/bin/activate 70 | ``` 71 | 72 | ### 2. Install Dependencies 73 | 74 | ```bash 75 | pip install -r requirements.txt 76 | ``` 77 | 78 | ### 3. Configuration 79 | 80 | Create a `.env` file in the `src` directory with the following configuration: 81 | 82 | ```env 83 | # Confluent Cloud Kafka Configuration 84 | BOOTSTRAP_SERVERS="pkc-xxxx.region.provider.confluent.cloud:9092" 85 | SCHEMA_REGISTRY_URL="https://sr-xxxx.region.provider.confluent.cloud" 86 | SCHEMA_REGISTRY_API_KEY="YOUR_SR_API_KEY" 87 | SCHEMA_REGISTRY_API_SECRET="YOUR_SR_API_SECRET" 88 | CLUSTER_API_KEY="YOUR_CLUSTER_API_KEY" 89 | CLUSTER_API_SECRET="YOUR_CLUSTER_API_SECRET" 90 | 91 | # AWS Bedrock Configuration 92 | BEDROCK_MODEL_ID="anthropic.claude-sonnet-4-20250514-v1:0" 93 | AWS_DEFAULT_REGION="us-east-1" 94 | AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY" 95 | AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_KEY" 96 | ``` 97 | 98 | > **Note**: Ensure your AWS credentials have appropriate permissions for Bedrock service access. 99 | 100 | ## Deployment 101 | 102 | ### Local Development 103 | 104 | 1. **Start the Data Producer** (Terminal 1): 105 | ```bash 106 | python dummy_data_producer.py 107 | ``` 108 | 109 | 2. **Start the Healthcare Agent** (Terminal 2): 110 | ```bash 111 | uvicorn main:app --reload --port 8000 112 | ``` 113 | 114 | The API will be available at `http://localhost:8000` with interactive documentation at `http://localhost:8000/docs`. 115 | 116 | ### Docker Deployment 117 | 118 | Build the Docker image: 119 | 120 | ```bash 121 | docker build -t healthcare-agent . 122 | ``` 123 | 124 | Run the containerized application: 125 | 126 | ```bash 127 | docker run -p 8000:8000 --env-file ./src/.env healthcare-agent 128 | ``` 129 | 130 | ## API Reference 131 | 132 | ### Patient Management 133 | 134 | | Endpoint | Method | Description | 135 | |----------|--------|-------------| 136 | | `/icu/patients` | GET | Retrieve all ICU patient data | 137 | | `/icu/patients/{patient_id}` | GET | Get specific patient information | 138 | 139 | ### Appointment Management 140 | 141 | | Endpoint | Method | Description | 142 | |----------|--------|-------------| 143 | | `/appointments/{doctor_id}` | GET | Get doctor's upcoming appointments | 144 | 145 | ### Alert System 146 | 147 | | Endpoint | Method | Description | 148 | |----------|--------|-------------| 149 | | `/alerts` | GET | Retrieve critical patient alerts | 150 | 151 | ### AI Chat Interface 152 | 153 | | Endpoint | Method | Description | Request Body | 154 | |----------|--------|-------------|--------------| 155 | | `/chat` | POST | AI-powered clinical assistance | `{"session_id": "string", "query": "string"}` | 156 | 157 | ### Example API Usage 158 | 159 | **Chat with AI Assistant:** 160 | ```json 161 | POST /chat 162 | { 163 | "session_id": "session_001", 164 | "query": "What is the current status of patient P123?" 165 | } 166 | ``` 167 | 168 | **Get Patient Data:** 169 | ```bash 170 | curl -X GET "http://localhost:8000/icu/patients/P123" 171 | ``` 172 | 173 | ## Data Schema 174 | 175 | Healthcare events follow the Avro schema defined in `healthcare_record.avsc`, supporting: 176 | - ICU patient updates with vital signs and medical status 177 | - Appointment scheduling with doctor and patient details 178 | - Structured data validation and serialization 179 | 180 | ## Monitoring & Alerts 181 | 182 | The system automatically generates alerts for: 183 | - Critical vital sign thresholds 184 | - Abnormal patient conditions 185 | - System health and connectivity issues 186 | 187 | Alerts are timestamped and stored for historical analysis and reporting. 188 | 189 | ## Security Considerations 190 | 191 | - All API endpoints should be secured with appropriate authentication in production 192 | - Environment variables contain sensitive credentials and should be properly managed 193 | - AWS IAM roles and policies should follow the principle of least privilege 194 | - Network security groups should restrict access to necessary ports only 195 | 196 | ## Contributing 197 | 198 | 1. Fork the repository 199 | 2. Create a feature branch (`git checkout -b feature/new-feature`) 200 | 3. Commit your changes (`git commit -am 'Add new feature'`) 201 | 4. Push to the branch (`git push origin feature/new-feature`) 202 | 5. Create a Pull Request 203 | 204 | ## Support 205 | 206 | For technical support or questions: 207 | - Review the API documentation at `/docs` endpoint 208 | - Check system logs for error details 209 | - Verify environment configuration and credentials 210 | 211 | ## License 212 | 213 | This project is licensed under the [MIT License](./LICENSE) -------------------------------------------------------------------------------- /src/dummy_data_producer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import time 4 | import random 5 | from datetime import datetime 6 | from dotenv import load_dotenv 7 | from confluent_kafka import avro 8 | from confluent_kafka.avro import AvroProducer 9 | from langchain_aws import ChatBedrock 10 | from langchain_core.prompts import PromptTemplate 11 | from langchain_core.output_parsers import StrOutputParser 12 | 13 | # Load environment variables 14 | load_dotenv() 15 | 16 | # --- Configuration --- 17 | KAFKA_TOPIC = "healthcare_events" 18 | SCHEMA_FILE_PATH = "healthcare_record.avsc" 19 | 20 | # Confluent Cloud Config 21 | bootstrap_servers = os.getenv("BOOTSTRAP_SERVERS") 22 | schema_registry_url = os.getenv("SCHEMA_REGISTRY_URL") 23 | sr_api_key = os.getenv("SCHEMA_REGISTRY_API_KEY") 24 | sr_api_secret = os.getenv("SCHEMA_REGISTRY_API_SECRET") 25 | cluster_api_key = os.getenv("CLUSTER_API_KEY") 26 | cluster_api_secret = os.getenv("CLUSTER_API_SECRET") 27 | 28 | print(f"DEBUG: Loaded SCHEMA_REGISTRY_URL = '{schema_registry_url}'") # Diagnostic print 29 | 30 | # AWS Bedrock Config 31 | BEDROCK_MODEL_ID = os.getenv("BEDROCK_MODEL_ID", "anthropic.claude-sonnet-4-20250514-v1:0") 32 | AWS_REGION = os.getenv("AWS_DEFAULT_REGION", "us-east-1") 33 | 34 | # --- Initialize LLM --- 35 | llm = None 36 | try: 37 | llm = ChatBedrock( 38 | model_id=BEDROCK_MODEL_ID, 39 | region_name=AWS_REGION, 40 | model_kwargs={"temperature": 0.8, "max_tokens_to_sample": 1500} # Increased tokens for JSON 41 | ) 42 | print(f"Successfully initialized Bedrock LLM: {BEDROCK_MODEL_ID}") 43 | except Exception as e: 44 | print(f"Error initializing Bedrock LLM: {e}. Producer will not run.") 45 | exit() 46 | 47 | # --- Load Avro Schema --- 48 | value_schema = None 49 | try: 50 | value_schema = avro.load(SCHEMA_FILE_PATH) 51 | print(f"Successfully loaded Avro schema from {SCHEMA_FILE_PATH}") 52 | except Exception as e: 53 | print(f"Error loading Avro schema: {e}. Producer will not run.") 54 | exit() 55 | 56 | # --- Kafka Producer Config --- 57 | producer_config = { 58 | 'bootstrap.servers': bootstrap_servers, 59 | 'schema.registry.url': schema_registry_url, 60 | 'schema.registry.basic.auth.user.info': f'{sr_api_key}:{sr_api_secret}', 61 | 'security.protocol': 'SASL_SSL', 62 | 'sasl.mechanisms': 'PLAIN', 63 | 'sasl.username': cluster_api_key, 64 | 'sasl.password': cluster_api_secret, 65 | } 66 | 67 | avro_producer = AvroProducer(producer_config, default_value_schema=value_schema) 68 | 69 | # --- LLM Prompt Templates for Data Generation --- 70 | 71 | icu_update_prompt_template = PromptTemplate.from_template( 72 | """Generate a realistic JSON object for an ICU patient update. 73 | The patient_id should be a string like 'P' followed by 3 digits (e.g., 'P123'). 74 | The timestamp should be the current Unix timestamp in milliseconds. 75 | Follow this JSON structure strictly for the 'icu_details' part: 76 | {{ 'bed_number': 'string (e.g., B-105)', 'heart_rate': integer (50-180), 'blood_pressure': 'string (e.g., 120/80 mmHg)', 'respiratory_rate': integer (10-30), 'temperature_celsius': float (35.0-41.0), 'notes': 'string (brief, 5-15 words, or null)' }} 77 | 78 | The top-level JSON should be: 79 | {{ 'record_type': 'ICU_PATIENT_UPDATE', 'patient_id': 'PXXX', 'timestamp': {current_timestamp_ms}, 'icu_details': {{ ... }} }} 80 | 81 | Example for icu_details: 82 | {{ 'bed_number': 'B-201', 'heart_rate': 75, 'blood_pressure': '110/70 mmHg', 'respiratory_rate': 18, 'temperature_celsius': 37.2, 'notes': 'Patient stable and resting.' }} 83 | 84 | Provide only the JSON object as a string, no other text. 85 | Current timestamp (ms): {current_timestamp_ms} 86 | Random Patient ID: P{random_patient_id_suffix} 87 | JSON object:""" 88 | ) 89 | 90 | appointment_schedule_prompt_template = PromptTemplate.from_template( 91 | """Generate a realistic JSON object for an upcoming doctor appointment. 92 | The patient_id should be a string like 'P' followed by 3 digits (e.g., 'P456'). 93 | The doctor_id should be a string like 'D' followed by 3 digits (e.g., 'D789'). 94 | The appointment_time should be a future Unix timestamp in milliseconds (e.g., within the next 1-7 days). 95 | The reason should be a brief medical reason for the appointment (5-10 words). 96 | Follow this JSON structure strictly for the 'appointment_details' part: 97 | {{ 'doctor_id': 'DXXX', 'appointment_time': {future_timestamp_ms}, 'reason': 'string' }} 98 | 99 | The top-level JSON should be: 100 | {{ 'record_type': 'APPOINTMENT_SCHEDULE', 'patient_id': 'PXXX', 'timestamp': {current_timestamp_ms}, 'appointment_details': {{ ... }} }} 101 | 102 | Example for appointment_details: 103 | {{ 'doctor_id': 'D007', 'appointment_time': {example_future_timestamp_ms}, 'reason': 'Follow-up consultation for recent lab results.' }} 104 | 105 | Provide only the JSON object as a string, no other text. 106 | Current timestamp (ms): {current_timestamp_ms} 107 | Future appointment timestamp (ms): {future_timestamp_ms} 108 | Random Patient ID: P{random_patient_id_suffix} 109 | Random Doctor ID: D{random_doctor_id_suffix} 110 | JSON object:""" 111 | ) 112 | 113 | chain = llm | StrOutputParser() 114 | 115 | # --- Helper Functions --- 116 | def generate_llm_data(prompt_template, **kwargs): 117 | """Generates data using LLM based on the provided prompt template and arguments.""" 118 | prompt = prompt_template.format(**kwargs) 119 | try: 120 | response_str = chain.invoke(prompt) # Using invoke for synchronous producer 121 | # The LLM might return the JSON string within triple backticks or with surrounding text. 122 | # Basic cleaning to extract JSON: 123 | if '```json' in response_str: 124 | response_str = response_str.split('```json')[1].split('```')[0].strip() 125 | elif '```' in response_str: 126 | response_str = response_str.split('```')[1].split('```')[0].strip() 127 | else: 128 | # Try to find the first '{' and last '}' 129 | first_brace = response_str.find('{') 130 | last_brace = response_str.rfind('}') 131 | if first_brace != -1 and last_brace != -1 and last_brace > first_brace: 132 | response_str = response_str[first_brace:last_brace+1] 133 | else: 134 | print(f"LLM response doesn't look like valid JSON: {response_str}") 135 | return None 136 | 137 | return json.loads(response_str) 138 | except json.JSONDecodeError as e: 139 | print(f"Error decoding JSON from LLM response: {e}") 140 | print(f"LLM Raw Response was: {response_str}") 141 | return None 142 | except Exception as e: 143 | print(f"Error during LLM data generation: {e}") 144 | return None 145 | 146 | def delivery_report(err, msg): 147 | if err is not None: 148 | print(f"Message delivery failed: {err}") 149 | else: 150 | print(f"Message delivered to {msg.topic()} [{msg.partition()}] at offset {msg.offset()}") 151 | 152 | # --- Main Production Loop --- 153 | def main(): 154 | if not llm or not value_schema: 155 | print("LLM or Schema not initialized. Exiting producer.") 156 | return 157 | 158 | print(f"Starting dummy data producer for topic '{KAFKA_TOPIC}'. Press Ctrl+C to stop.") 159 | try: 160 | while True: 161 | current_ts_ms = int(datetime.now().timestamp() * 1000) 162 | patient_id_suffix = str(random.randint(100, 999)) 163 | record_to_produce = None 164 | 165 | if random.choice([True, False]): # 50/50 chance for ICU update or appointment 166 | print("\nGenerating ICU Patient Update...") 167 | record_to_produce = generate_llm_data( 168 | icu_update_prompt_template, 169 | current_timestamp_ms=current_ts_ms, 170 | random_patient_id_suffix=patient_id_suffix 171 | ) 172 | else: 173 | print("\nGenerating Appointment Schedule...") 174 | future_ts_ms = current_ts_ms + random.randint(1, 7) * 24 * 60 * 60 * 1000 # 1-7 days in future 175 | doctor_id_suffix = str(random.randint(100, 999)) 176 | record_to_produce = generate_llm_data( 177 | appointment_schedule_prompt_template, 178 | current_timestamp_ms=current_ts_ms, 179 | future_timestamp_ms=future_ts_ms, 180 | example_future_timestamp_ms=current_ts_ms + 3 * 24 * 60 * 60 * 1000, # For example in prompt 181 | random_patient_id_suffix=patient_id_suffix, 182 | random_doctor_id_suffix=doctor_id_suffix 183 | ) 184 | 185 | if record_to_produce: 186 | try: 187 | # Basic validation (can be more thorough) 188 | if 'record_type' not in record_to_produce or \ 189 | ('icu_details' not in record_to_produce and 'appointment_details' not in record_to_produce): 190 | print(f"Generated data missing key fields: {record_to_produce}") 191 | continue 192 | 193 | # Ensure patient_id is in the top level for appointment_details as per schema 194 | if record_to_produce['record_type'] == 'APPOINTMENT_SCHEDULE' and 'patient_id' not in record_to_produce: 195 | record_to_produce['patient_id'] = f"P{patient_id_suffix}" # Add if missing 196 | 197 | print(f"Producing record: {json.dumps(record_to_produce, indent=2)}") 198 | avro_producer.produce(topic=KAFKA_TOPIC, value=record_to_produce, key=record_to_produce.get('patient_id', str(random.randint(0,1000000))) , callback=delivery_report) 199 | avro_producer.poll(0) # Trigger delivery reports 200 | except Exception as e: 201 | print(f"Error producing message: {e}") 202 | print(f"Record was: {record_to_produce}") 203 | else: 204 | print("Failed to generate data from LLM for this cycle.") 205 | 206 | time.sleep(60) # Wait for 1 minute 207 | 208 | except KeyboardInterrupt: 209 | print("\nShutting down producer...") 210 | finally: 211 | avro_producer.flush() 212 | print("Producer flushed and shut down.") 213 | 214 | if __name__ == "__main__": 215 | main() 216 | -------------------------------------------------------------------------------- /src/agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | import json 4 | from confluent_kafka import DeserializingConsumer 5 | from confluent_kafka.schema_registry import SchemaRegistryClient 6 | from confluent_kafka.schema_registry.avro import AvroDeserializer 7 | from confluent_kafka.serialization import StringDeserializer 8 | import os 9 | import threading 10 | import json 11 | from datetime import datetime # Added for timestamping alerts 12 | from confluent_kafka import DeserializingConsumer 13 | from confluent_kafka.schema_registry import SchemaRegistryClient 14 | from confluent_kafka.schema_registry.avro import AvroDeserializer 15 | from confluent_kafka.serialization import StringDeserializer 16 | from dotenv import load_dotenv 17 | 18 | from langchain_aws import ChatBedrock 19 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 20 | from langchain_core.messages import HumanMessage, AIMessage 21 | from langchain_core.output_parsers import StrOutputParser 22 | 23 | # Load environment variables from .env file 24 | load_dotenv() 25 | 26 | # AWS Bedrock Model ID and Region 27 | BEDROCK_MODEL_ID = os.getenv("BEDROCK_MODEL_ID", "anthropic.claude-sonnet-4-20250514-v1:0") 28 | AWS_REGION = os.getenv("AWS_DEFAULT_REGION", "us-east-1") # Ensure this is set in .env or environment 29 | 30 | class HealthcareAgent: 31 | def __init__(self): 32 | # Initialize LLM and chain 33 | try: 34 | self.llm = ChatBedrock( 35 | model_id=BEDROCK_MODEL_ID, 36 | region_name=AWS_REGION, 37 | model_kwargs={"temperature": 0.7, "max_tokens_to_sample": 1024} # Adjusted max_tokens 38 | ) 39 | # Prompt template for chat, including history and current input with context 40 | self.chat_prompt_template = ChatPromptTemplate.from_messages([ 41 | MessagesPlaceholder(variable_name="history"), 42 | HumanMessage(content="{input}") 43 | ]) 44 | self.chain = self.chat_prompt_template | self.llm | StrOutputParser() 45 | print(f"Successfully initialized Bedrock LLM with model: {BEDROCK_MODEL_ID} in region {AWS_REGION}") 46 | except Exception as e: 47 | print(f"Error initializing Bedrock LLM: {e}. Ensure AWS credentials and region are set.") 48 | self.llm = None 49 | self.chain = None 50 | 51 | self.kafka_topic = "healthcare_events" # Topic for n8n to produce to 52 | self.consumer = self._create_kafka_consumer() 53 | self.running = False 54 | self.consumer_thread = threading.Thread(target=self._consume_loop) 55 | 56 | # In-memory data stores (to be populated by the consumer) 57 | self.icu_patients = {} 58 | self.appointments = {} 59 | self.alerts = [] # For storing generated alerts 60 | self.conversation_histories = {} # {session_id: [BaseMessage]} 61 | 62 | def _create_kafka_consumer(self): 63 | """Creates and configures the Kafka DeserializingConsumer.""" 64 | schema_registry_conf = { 65 | 'url': os.getenv("SCHEMA_REGISTRY_URL"), 66 | 'basic.auth.user.info': os.getenv("SCHEMA_REGISTRY_API_KEY") + ':' + os.getenv("SCHEMA_REGISTRY_API_SECRET") 67 | } 68 | schema_registry_client = SchemaRegistryClient(schema_registry_conf) 69 | avro_deserializer = AvroDeserializer(schema_registry_client) 70 | 71 | consumer_conf = { 72 | 'bootstrap.servers': os.getenv("BOOTSTRAP_SERVERS"), 73 | 'security.protocol': 'SASL_SSL', 74 | 'sasl.mechanisms': 'PLAIN', 75 | 'sasl.username': os.getenv("CLUSTER_API_KEY"), 76 | 'sasl.password': os.getenv("CLUSTER_API_SECRET"), 77 | 'group.id': 'healthcare_agent_consumer_group', 78 | 'auto.offset.reset': 'earliest', 79 | 'key.deserializer': StringDeserializer('utf_8'), 80 | 'value.deserializer': avro_deserializer 81 | } 82 | return DeserializingConsumer(consumer_conf) 83 | 84 | def _consume_loop(self): 85 | """The main loop for consuming and processing messages from Kafka.""" 86 | self.consumer.subscribe([self.kafka_topic]) 87 | print(f"Consumer subscribed to topic: {self.kafka_topic}") 88 | 89 | while self.running: 90 | try: 91 | msg = self.consumer.poll(1.0) # Poll for new messages 92 | if msg is None: 93 | continue 94 | if msg.error(): 95 | print(f"Consumer error: {msg.error()}") 96 | continue 97 | 98 | record = msg.value() 99 | print(f"Successfully consumed record: {record}") 100 | self._process_record(record) 101 | 102 | except Exception as e: 103 | print(f"An error occurred in the consumer loop: {e}") 104 | 105 | self.consumer.close() 106 | print("Kafka consumer closed.") 107 | 108 | def _process_record(self, record: dict): 109 | """Processes a single record from Kafka, updates data stores, and checks for alerts.""" 110 | record_type = record.get('record_type') 111 | if record_type == 'ICU_PATIENT_UPDATE': 112 | patient_id = record.get('patient_id') 113 | icu_details_data = record.get('icu_details') 114 | if patient_id and icu_details_data: 115 | self.icu_patients[patient_id] = icu_details_data 116 | print(f"Updated ICU data for patient {patient_id}: {icu_details_data}") 117 | # Use current time for alert timestamp if not present in record 118 | alert_timestamp = record.get('timestamp', int(datetime.now().timestamp() * 1000)) 119 | self._check_for_alerts(patient_id, icu_details_data, alert_timestamp) 120 | 121 | elif record_type == 'APPOINTMENT_SCHEDULE': 122 | details = record.get('appointment_details') 123 | if details: 124 | doctor_id = details.get('doctor_id') 125 | if doctor_id not in self.appointments: 126 | self.appointments[doctor_id] = [] 127 | self.appointments[doctor_id].append(details) 128 | print(f"Added new appointment for Dr. {doctor_id}") 129 | 130 | def _check_for_alerts(self, patient_id: str, icu_details: dict, timestamp: int): 131 | """Checks ICU data for critical conditions and generates alerts.""" 132 | alerts_generated_this_cycle = [] 133 | 134 | # Define critical thresholds (these are examples, adjust as needed) 135 | CRITICAL_HEART_RATE_LOW = 50 136 | CRITICAL_HEART_RATE_HIGH = 130 137 | CRITICAL_TEMP_HIGH = 39.0 138 | # Add more conditions: blood_pressure, respiratory_rate, etc. 139 | 140 | heart_rate = icu_details.get('heart_rate') 141 | if heart_rate is not None: 142 | if heart_rate < CRITICAL_HEART_RATE_LOW: 143 | alerts_generated_this_cycle.append({ 144 | "alert_id": f"alert_{patient_id}_hr_low_{timestamp}", 145 | "patient_id": patient_id, 146 | "condition": "Low Heart Rate", 147 | "value": heart_rate, 148 | "severity": "critical", 149 | "timestamp": timestamp, 150 | "message": f"Patient {patient_id} has critically low heart rate: {heart_rate} bpm." 151 | }) 152 | elif heart_rate > CRITICAL_HEART_RATE_HIGH: 153 | alerts_generated_this_cycle.append({ 154 | "alert_id": f"alert_{patient_id}_hr_high_{timestamp}", 155 | "patient_id": patient_id, 156 | "condition": "High Heart Rate", 157 | "value": heart_rate, 158 | "severity": "critical", 159 | "timestamp": timestamp, 160 | "message": f"Patient {patient_id} has critically high heart rate: {heart_rate} bpm." 161 | }) 162 | 163 | temp = icu_details.get('temperature_celsius') 164 | if temp is not None and temp > CRITICAL_TEMP_HIGH: 165 | alerts_generated_this_cycle.append({ 166 | "alert_id": f"alert_{patient_id}_temp_high_{timestamp}", 167 | "patient_id": patient_id, 168 | "condition": "High Temperature", 169 | "value": temp, 170 | "severity": "warning", 171 | "timestamp": timestamp, 172 | "message": f"Patient {patient_id} has high temperature: {temp}°C." 173 | }) 174 | 175 | for new_alert in alerts_generated_this_cycle: 176 | # Simple de-duplication: remove any existing alert for the same patient and condition before adding new one. 177 | self.alerts = [a for a in self.alerts if not (a['patient_id'] == new_alert['patient_id'] and a['condition'] == new_alert['condition'])] 178 | self.alerts.append(new_alert) 179 | print(f"Generated alert: {new_alert['message']}") 180 | 181 | def start_consumer(self): 182 | """Starts the Kafka consumer in a separate thread.""" 183 | if not self.running: 184 | self.running = True 185 | self.consumer_thread.start() 186 | print("Kafka consumer thread started.") 187 | 188 | def stop_consumer(self): 189 | """Stops the Kafka consumer thread gracefully.""" 190 | if self.running: 191 | self.running = False 192 | self.consumer_thread.join() 193 | print("Kafka consumer thread stopped.") 194 | 195 | def get_alerts(self) -> list: 196 | """Returns the list of currently active alerts. Can be expanded to filter old alerts.""" 197 | # Example: Filter alerts older than 1 hour (3600000 ms) 198 | # current_time_ms = int(datetime.now().timestamp() * 1000) 199 | # recent_alerts = [a for a in self.alerts if (current_time_ms - a.get('timestamp', 0)) < 3600000] 200 | # return recent_alerts 201 | return self.alerts 202 | 203 | async def handle_chat(self, query: str, session_id: str = "default_session") -> str: 204 | """Handles a chat query using Bedrock LLM and maintains conversation history.""" 205 | if not self.chain: 206 | return "AI service is not available at the moment. Please check configuration." 207 | 208 | if session_id not in self.conversation_histories: 209 | self.conversation_histories[session_id] = [] 210 | 211 | # Prepare context for the LLM 212 | context_parts = ["You are a helpful Healthcare AI assistant for doctors. Your knowledge includes current ICU patient data and doctor appointments."] 213 | 214 | context_parts.append("Current ICU Patients Data (summarized for brevity):") 215 | if self.icu_patients: 216 | for patient_id, details in list(self.icu_patients.items())[:5]: # Limit context size 217 | context_parts.append(f" - Patient {patient_id}: Vital signs - HR: {details.get('heart_rate')}, BP: {details.get('blood_pressure')}, Temp: {details.get('temperature_celsius')}°C. Notes: {details.get('notes', 'N/A')[:50]}...") 218 | else: 219 | context_parts.append(" No ICU patient data currently available.") 220 | 221 | context_parts.append("Upcoming Appointments Data (summarized for brevity):") 222 | if self.appointments: 223 | for doctor_id, appts in list(self.appointments.items())[:3]: # Limit context size 224 | context_parts.append(f" - Dr. {doctor_id}:") 225 | for appt_detail in appts[:2]: # Limit context size 226 | appt_time_dt = datetime.fromtimestamp(appt_detail.get('appointment_time') / 1000) 227 | context_parts.append(f" - Patient {appt_detail.get('patient_id', 'N/A')} on {appt_time_dt.strftime('%Y-%m-%d %H:%M')} for {appt_detail.get('reason', 'N/A')[:50]}...") 228 | else: 229 | context_parts.append(" No appointment data currently available.") 230 | 231 | final_context = "\n".join(context_parts) 232 | llm_input_text = f"{final_context}\n\nDoctor's query: {query}" 233 | 234 | history_messages = self.conversation_histories[session_id] 235 | 236 | try: 237 | # Use ainvoke for async FastAPI endpoint 238 | response = await self.chain.ainvoke({"input": llm_input_text, "history": history_messages}) 239 | 240 | # Update history 241 | self.conversation_histories[session_id].append(HumanMessage(content=query)) 242 | self.conversation_histories[session_id].append(AIMessage(content=response)) 243 | # Limit history size 244 | if len(self.conversation_histories[session_id]) > 10: # Keep last 5 interactions (10 messages) 245 | self.conversation_histories[session_id] = self.conversation_histories[session_id][-10:] 246 | 247 | return response 248 | except Exception as e: 249 | print(f"Error during Bedrock LLM call for session {session_id}: {e}") 250 | # Consider logging the full error for debugging 251 | return "I encountered an error trying to process your request with the AI model. Please try again or check the logs." 252 | 253 | # To run this agent independently for testing: 254 | if __name__ == '__main__': 255 | agent = HealthcareAgent() 256 | agent.start_consumer() 257 | 258 | try: 259 | # Keep the main thread alive 260 | while True: 261 | pass 262 | except KeyboardInterrupt: 263 | print("Shutting down agent...") 264 | agent.stop_consumer() 265 | --------------------------------------------------------------------------------