Spaces:
Sleeping
Sleeping
docsa_HD
commited on
Commit
•
2929135
1
Parent(s):
434cfaf
add .env
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.example +16 -0
- .gitignore +64 -0
- environment.yml +24 -0
- examples/usage_examples.py +87 -0
- pytest.ini +6 -0
- requirements.txt +26 -0
- setup.py +47 -0
- src/__init__.py +4 -0
- src/agent.py +264 -0
- src/config/__init__.py +5 -0
- src/config/prompts.py +75 -0
- src/config/settings.py +70 -0
- src/models/__init__.py +32 -0
- src/models/state.py +202 -0
- src/nodes/__init__.py +18 -0
- src/nodes/input_analyzer.py +85 -0
- src/nodes/output_synthesizer.py +109 -0
- src/nodes/patient_flow.py +62 -0
- src/nodes/quality_monitor.py +97 -0
- src/nodes/resource_manager.py +78 -0
- src/nodes/staff_scheduler.py +76 -0
- src/nodes/task_router.py +45 -0
- src/tools/__init__.py +12 -0
- src/tools/patient_tools.py +171 -0
- src/tools/quality_tools.py +176 -0
- src/tools/resource_tools.py +130 -0
- src/tools/scheduling_tools.py +160 -0
- src/ui/__init__.py +3 -0
- src/ui/app.py +522 -0
- src/ui/assets/icons/.gitkeep +0 -0
- src/ui/assets/images/.gitkeep +0 -0
- src/ui/components/__init__.py +11 -0
- src/ui/components/chat.py +78 -0
- src/ui/components/header.py +73 -0
- src/ui/components/metrics.py +139 -0
- src/ui/components/sidebar.py +121 -0
- src/ui/styles/__init__.py +3 -0
- src/ui/styles/custom.css +224 -0
- src/ui/styles/theme.py +123 -0
- src/utils/__init__.py +6 -0
- src/utils/error_handlers.py +122 -0
- src/utils/logger.py +102 -0
- src/utils/validators.py +132 -0
- streamlit_app.py +10 -0
- test_healthcare_agent_basic.py +66 -0
- test_healthcare_scenarios.py +163 -0
- tests/__init__.py +6 -0
- tests/conftest.py +108 -0
- tests/test_agent.py +75 -0
- tests/test_nodes/test_input_analyzer.py +27 -0
.env.example
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# OpenAI Configuration
|
2 |
+
OPENAI_API_KEY=
|
3 |
+
|
4 |
+
# Application Settings
|
5 |
+
LOG_LEVEL=INFO
|
6 |
+
MEMORY_TYPE=sqlite
|
7 |
+
MEMORY_URI=:memory:
|
8 |
+
|
9 |
+
# Hospital Configuration
|
10 |
+
HOSPITAL_NAME=Example Hospital
|
11 |
+
TOTAL_BEDS=300
|
12 |
+
DEPARTMENTS=ER,ICU,General,Surgery,Pediatrics
|
13 |
+
|
14 |
+
# Development Settings
|
15 |
+
DEBUG=False
|
16 |
+
ENVIRONMENT=production
|
.gitignore
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
*.so
|
6 |
+
.Python
|
7 |
+
build/
|
8 |
+
develop-eggs/
|
9 |
+
dist/
|
10 |
+
downloads/
|
11 |
+
eggs/
|
12 |
+
.eggs/
|
13 |
+
lib/
|
14 |
+
lib64/
|
15 |
+
parts/
|
16 |
+
sdist/
|
17 |
+
var/
|
18 |
+
wheels/
|
19 |
+
*.egg-info/
|
20 |
+
.installed.cfg
|
21 |
+
*.egg
|
22 |
+
|
23 |
+
# Environment
|
24 |
+
.env
|
25 |
+
.venv
|
26 |
+
env/
|
27 |
+
venv/
|
28 |
+
ENV/
|
29 |
+
|
30 |
+
# IDE
|
31 |
+
.idea/
|
32 |
+
.vscode/
|
33 |
+
*.swp
|
34 |
+
*.swo
|
35 |
+
|
36 |
+
# Logs
|
37 |
+
*.log
|
38 |
+
logs/
|
39 |
+
|
40 |
+
# Testing
|
41 |
+
.coverage
|
42 |
+
htmlcov/
|
43 |
+
.pytest_cache/
|
44 |
+
|
45 |
+
# macOS
|
46 |
+
.DS_Store
|
47 |
+
.AppleDouble
|
48 |
+
.LSOverride
|
49 |
+
Icon
|
50 |
+
._*
|
51 |
+
.DocumentRevisions-V100
|
52 |
+
.fseventsd
|
53 |
+
.Spotlight-V100
|
54 |
+
.TemporaryItems
|
55 |
+
.Trashes
|
56 |
+
.VolumeIcon.icns
|
57 |
+
.com.apple.timemachine.donotpresent
|
58 |
+
|
59 |
+
# Directories potentially created on remote AFP share
|
60 |
+
.AppleDB
|
61 |
+
.AppleDesktop
|
62 |
+
Network Trash Folder
|
63 |
+
Temporary Items
|
64 |
+
.apdisk
|
environment.yml
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: langgraph
|
2 |
+
channels:
|
3 |
+
- conda-forge
|
4 |
+
- defaults
|
5 |
+
dependencies:
|
6 |
+
- python=3.11
|
7 |
+
- pip
|
8 |
+
- pip:
|
9 |
+
- langgraph>=0.0.15
|
10 |
+
- langchain>=0.1.0
|
11 |
+
- openai>=1.3.0
|
12 |
+
- python-dotenv>=0.19.0
|
13 |
+
- pydantic>=2.0.0
|
14 |
+
- typing-extensions>=4.5.0
|
15 |
+
- python-json-logger>=2.0.7
|
16 |
+
- structlog>=24.1.0
|
17 |
+
- pytest>=7.0.0
|
18 |
+
- pytest-asyncio>=0.23.0
|
19 |
+
- pytest-cov>=4.1.0
|
20 |
+
- black>=23.0.0
|
21 |
+
- isort>=5.12.0
|
22 |
+
- flake8>=6.1.0
|
23 |
+
- mkdocs>=1.5.0
|
24 |
+
- mkdocs-material>=9.5.0
|
examples/usage_examples.py
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# examples/usage_examples.py
|
2 |
+
import os
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
from src.agent import HealthcareAgent
|
5 |
+
|
6 |
+
# Load environment variables
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
def basic_usage_example():
|
10 |
+
"""Basic usage example of the Healthcare Agent"""
|
11 |
+
agent = HealthcareAgent(os.getenv("OPENAI_API_KEY"))
|
12 |
+
|
13 |
+
# Single query example
|
14 |
+
response = agent.process(
|
15 |
+
"What is the current ER wait time and bed availability?"
|
16 |
+
)
|
17 |
+
print("Basic Query Response:", response)
|
18 |
+
|
19 |
+
def conversation_example():
|
20 |
+
"""Example of maintaining conversation context"""
|
21 |
+
agent = HealthcareAgent()
|
22 |
+
thread_id = "example-conversation"
|
23 |
+
|
24 |
+
# Series of related queries
|
25 |
+
queries = [
|
26 |
+
"How many beds are currently available in the ER?",
|
27 |
+
"What is the current staffing level for that department?",
|
28 |
+
"Based on these metrics, what are your recommendations for optimization?"
|
29 |
+
]
|
30 |
+
|
31 |
+
for query in queries:
|
32 |
+
print(f"\nUser: {query}")
|
33 |
+
response = agent.process(query, thread_id=thread_id)
|
34 |
+
print(f"Assistant: {response['response']}")
|
35 |
+
|
36 |
+
def department_analysis_example():
|
37 |
+
"""Example of department-specific analysis"""
|
38 |
+
agent = HealthcareAgent()
|
39 |
+
|
40 |
+
# Context with department-specific metrics
|
41 |
+
context = {
|
42 |
+
"department": "ICU",
|
43 |
+
"metrics": {
|
44 |
+
"bed_capacity": 20,
|
45 |
+
"occupied_beds": 18,
|
46 |
+
"staff_count": {"doctors": 5, "nurses": 15},
|
47 |
+
"average_stay": 4.5 # days
|
48 |
+
}
|
49 |
+
}
|
50 |
+
|
51 |
+
response = agent.process(
|
52 |
+
"Analyze current ICU operations and suggest improvements",
|
53 |
+
context=context
|
54 |
+
)
|
55 |
+
print("Department Analysis:", response)
|
56 |
+
|
57 |
+
def async_streaming_example():
|
58 |
+
"""Example of using async streaming responses"""
|
59 |
+
import asyncio
|
60 |
+
|
61 |
+
async def stream_response():
|
62 |
+
agent = HealthcareAgent()
|
63 |
+
query = "Provide a complete analysis of current hospital operations"
|
64 |
+
|
65 |
+
async for event in agent.graph.astream_events(
|
66 |
+
{"messages": [query]},
|
67 |
+
{"configurable": {"thread_id": "streaming-example"}}
|
68 |
+
):
|
69 |
+
if event["event"] == "on_chat_model_stream":
|
70 |
+
content = event["data"]["chunk"].content
|
71 |
+
if content:
|
72 |
+
print(content, end="", flush=True)
|
73 |
+
|
74 |
+
asyncio.run(stream_response())
|
75 |
+
|
76 |
+
if __name__ == "__main__":
|
77 |
+
print("=== Basic Usage Example ===")
|
78 |
+
basic_usage_example()
|
79 |
+
|
80 |
+
print("\n=== Conversation Example ===")
|
81 |
+
conversation_example()
|
82 |
+
|
83 |
+
print("\n=== Department Analysis Example ===")
|
84 |
+
department_analysis_example()
|
85 |
+
|
86 |
+
print("\n=== Streaming Example ===")
|
87 |
+
async_streaming_example()# Usage examples implementation
|
pytest.ini
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[pytest]
|
2 |
+
testpaths = tests
|
3 |
+
python_files = test_*.py
|
4 |
+
python_classes = Test
|
5 |
+
python_functions = test_*
|
6 |
+
addopts = -v --cov=src --cov-report=html
|
requirements.txt
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Core Dependencies
|
2 |
+
langgraph>=0.0.15
|
3 |
+
langchain>=0.1.0
|
4 |
+
langchain-openai>=0.0.5
|
5 |
+
openai>=1.3.0
|
6 |
+
python-dotenv>=0.19.0
|
7 |
+
typing-extensions>=4.5.0
|
8 |
+
|
9 |
+
# State Management
|
10 |
+
pydantic>=2.0.0
|
11 |
+
|
12 |
+
# Logging and Monitoring
|
13 |
+
python-json-logger>=2.0.7
|
14 |
+
structlog>=24.1.0
|
15 |
+
|
16 |
+
# Development Dependencies
|
17 |
+
pytest>=7.0.0
|
18 |
+
pytest-asyncio>=0.23.0
|
19 |
+
pytest-cov>=4.1.0
|
20 |
+
black>=23.0.0
|
21 |
+
isort>=5.12.0
|
22 |
+
flake8>=6.1.0
|
23 |
+
|
24 |
+
# Documentation
|
25 |
+
mkdocs>=1.5.0
|
26 |
+
mkdocs-material>=9.5.0
|
setup.py
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# setup.py
|
2 |
+
from setuptools import setup, find_packages
|
3 |
+
|
4 |
+
# Read requirements
|
5 |
+
with open('requirements.txt') as f:
|
6 |
+
requirements = f.read().splitlines()
|
7 |
+
|
8 |
+
# Read README for long description
|
9 |
+
with open('README.md', encoding='utf-8') as f:
|
10 |
+
long_description = f.read()
|
11 |
+
|
12 |
+
setup(
|
13 |
+
name='healthcare-ops-agent',
|
14 |
+
version='0.1.0',
|
15 |
+
description='Healthcare Operations Management Agent using LangGraph',
|
16 |
+
long_description=long_description,
|
17 |
+
long_description_content_type='text/markdown',
|
18 |
+
author='Your Name',
|
19 |
+
author_email='[email protected]',
|
20 |
+
url='https://github.com/yourusername/healthcare-ops-agent',
|
21 |
+
packages=find_packages(exclude=['tests*']),
|
22 |
+
install_requires=requirements,
|
23 |
+
classifiers=[
|
24 |
+
'Development Status :: 3 - Alpha',
|
25 |
+
'Intended Audience :: Healthcare Industry',
|
26 |
+
'License :: OSI Approved :: MIT License',
|
27 |
+
'Programming Language :: Python :: 3.9',
|
28 |
+
'Programming Language :: Python :: 3.10',
|
29 |
+
'Programming Language :: Python :: 3.11',
|
30 |
+
],
|
31 |
+
python_requires='>=3.9',
|
32 |
+
include_package_data=True,
|
33 |
+
extras_require={
|
34 |
+
'dev': [
|
35 |
+
'pytest>=7.0.0',
|
36 |
+
'pytest-asyncio>=0.23.0',
|
37 |
+
'pytest-cov>=4.1.0',
|
38 |
+
'black>=23.0.0',
|
39 |
+
'isort>=5.12.0',
|
40 |
+
'flake8>=6.1.0',
|
41 |
+
],
|
42 |
+
'docs': [
|
43 |
+
'mkdocs>=1.5.0',
|
44 |
+
'mkdocs-material>=9.5.0',
|
45 |
+
],
|
46 |
+
}
|
47 |
+
)
|
src/__init__.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/__init__.py
|
2 |
+
from .agent import HealthcareAgent
|
3 |
+
|
4 |
+
__version__ = "0.1.0"
|
src/agent.py
ADDED
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/agent.py
|
2 |
+
from typing import Dict, Optional, List
|
3 |
+
import uuid
|
4 |
+
from datetime import datetime
|
5 |
+
from langchain_core.messages import HumanMessage, SystemMessage, AnyMessage
|
6 |
+
from langgraph.graph import StateGraph, END
|
7 |
+
from langchain_openai import ChatOpenAI
|
8 |
+
|
9 |
+
# Remove this line as it's causing the error
|
10 |
+
# from langgraph.checkpoint import BaseCheckpointSaver
|
11 |
+
|
12 |
+
from .config.settings import Settings
|
13 |
+
from .models.state import (
|
14 |
+
HospitalState,
|
15 |
+
create_initial_state,
|
16 |
+
validate_state
|
17 |
+
)
|
18 |
+
from .nodes import (
|
19 |
+
InputAnalyzerNode,
|
20 |
+
TaskRouterNode,
|
21 |
+
PatientFlowNode,
|
22 |
+
ResourceManagerNode,
|
23 |
+
QualityMonitorNode,
|
24 |
+
StaffSchedulerNode,
|
25 |
+
OutputSynthesizerNode
|
26 |
+
)
|
27 |
+
from .tools import (
|
28 |
+
PatientTools,
|
29 |
+
ResourceTools,
|
30 |
+
QualityTools,
|
31 |
+
SchedulingTools
|
32 |
+
)
|
33 |
+
|
34 |
+
from .utils.logger import setup_logger
|
35 |
+
from .utils.error_handlers import (
|
36 |
+
ErrorHandler,
|
37 |
+
HealthcareError,
|
38 |
+
ValidationError, # Add this import
|
39 |
+
ProcessingError # Add this import
|
40 |
+
)
|
41 |
+
|
42 |
+
|
43 |
+
logger = setup_logger(__name__)
|
44 |
+
|
45 |
+
class HealthcareAgent:
|
46 |
+
def __init__(self, api_key: Optional[str] = None):
|
47 |
+
try:
|
48 |
+
# Initialize settings and validate
|
49 |
+
self.settings = Settings()
|
50 |
+
if api_key:
|
51 |
+
self.settings.OPENAI_API_KEY = api_key
|
52 |
+
self.settings.validate_settings()
|
53 |
+
|
54 |
+
# Initialize LLM
|
55 |
+
self.llm = ChatOpenAI(
|
56 |
+
model=self.settings.MODEL_NAME,
|
57 |
+
temperature=self.settings.MODEL_TEMPERATURE,
|
58 |
+
api_key=self.settings.OPENAI_API_KEY
|
59 |
+
)
|
60 |
+
|
61 |
+
# Initialize tools
|
62 |
+
self.tools = self._initialize_tools()
|
63 |
+
|
64 |
+
# Initialize nodes
|
65 |
+
self.nodes = self._initialize_nodes()
|
66 |
+
|
67 |
+
# Initialize conversation states (replacing checkpointer)
|
68 |
+
self.conversation_states = {}
|
69 |
+
|
70 |
+
# Build graph
|
71 |
+
self.graph = self._build_graph()
|
72 |
+
|
73 |
+
logger.info("Healthcare Agent initialized successfully")
|
74 |
+
|
75 |
+
except Exception as e:
|
76 |
+
logger.error(f"Error initializing Healthcare Agent: {str(e)}")
|
77 |
+
raise HealthcareError(
|
78 |
+
message="Failed to initialize Healthcare Agent",
|
79 |
+
error_code="INIT_ERROR",
|
80 |
+
details={"error": str(e)}
|
81 |
+
)
|
82 |
+
|
83 |
+
def _initialize_tools(self) -> Dict:
|
84 |
+
"""Initialize all tools used by the agent"""
|
85 |
+
return {
|
86 |
+
"patient": PatientTools(),
|
87 |
+
"resource": ResourceTools(),
|
88 |
+
"quality": QualityTools(),
|
89 |
+
"scheduling": SchedulingTools()
|
90 |
+
}
|
91 |
+
|
92 |
+
def _initialize_nodes(self) -> Dict:
|
93 |
+
"""Initialize all nodes in the agent workflow"""
|
94 |
+
return {
|
95 |
+
"input_analyzer": InputAnalyzerNode(self.llm),
|
96 |
+
"task_router": TaskRouterNode(),
|
97 |
+
"patient_flow": PatientFlowNode(self.llm),
|
98 |
+
"resource_manager": ResourceManagerNode(self.llm),
|
99 |
+
"quality_monitor": QualityMonitorNode(self.llm),
|
100 |
+
"staff_scheduler": StaffSchedulerNode(self.llm),
|
101 |
+
"output_synthesizer": OutputSynthesizerNode(self.llm)
|
102 |
+
}
|
103 |
+
|
104 |
+
def _build_graph(self) -> StateGraph:
|
105 |
+
"""Build the workflow graph with all nodes and edges"""
|
106 |
+
try:
|
107 |
+
# Initialize graph
|
108 |
+
builder = StateGraph(HospitalState)
|
109 |
+
|
110 |
+
# Add all nodes
|
111 |
+
for name, node in self.nodes.items():
|
112 |
+
builder.add_node(name, node)
|
113 |
+
|
114 |
+
# Set entry point
|
115 |
+
builder.set_entry_point("input_analyzer")
|
116 |
+
|
117 |
+
# Add edge from input analyzer to task router
|
118 |
+
builder.add_edge("input_analyzer", "task_router")
|
119 |
+
|
120 |
+
# Define conditional routing based on task router output
|
121 |
+
def route_next(state: Dict):
|
122 |
+
return state["context"]["next_node"]
|
123 |
+
|
124 |
+
# Add conditional edges from task router
|
125 |
+
builder.add_conditional_edges(
|
126 |
+
"task_router",
|
127 |
+
route_next,
|
128 |
+
{
|
129 |
+
"patient_flow": "patient_flow",
|
130 |
+
"resource_management": "resource_manager",
|
131 |
+
"quality_monitoring": "quality_monitor",
|
132 |
+
"staff_scheduling": "staff_scheduler",
|
133 |
+
"output_synthesis": "output_synthesizer"
|
134 |
+
}
|
135 |
+
)
|
136 |
+
|
137 |
+
# Add edges from functional nodes to output synthesizer
|
138 |
+
functional_nodes = [
|
139 |
+
"patient_flow",
|
140 |
+
"resource_manager",
|
141 |
+
"quality_monitor",
|
142 |
+
"staff_scheduler"
|
143 |
+
]
|
144 |
+
|
145 |
+
for node in functional_nodes:
|
146 |
+
builder.add_edge(node, "output_synthesizer")
|
147 |
+
|
148 |
+
# Add end condition
|
149 |
+
builder.add_edge("output_synthesizer", END)
|
150 |
+
|
151 |
+
# Compile graph
|
152 |
+
return builder.compile()
|
153 |
+
|
154 |
+
except Exception as e:
|
155 |
+
logger.error(f"Error building graph: {str(e)}")
|
156 |
+
raise HealthcareError(
|
157 |
+
message="Failed to build agent workflow graph",
|
158 |
+
error_code="GRAPH_BUILD_ERROR",
|
159 |
+
details={"error": str(e)}
|
160 |
+
)
|
161 |
+
|
162 |
+
@ErrorHandler.error_decorator
|
163 |
+
def process(
|
164 |
+
self,
|
165 |
+
input_text: str,
|
166 |
+
thread_id: Optional[str] = None,
|
167 |
+
context: Optional[Dict] = None
|
168 |
+
) -> Dict:
|
169 |
+
"""Process input through the healthcare operations workflow"""
|
170 |
+
try:
|
171 |
+
# Validate input
|
172 |
+
ErrorHandler.validate_input(input_text)
|
173 |
+
|
174 |
+
# Create or use thread ID
|
175 |
+
thread_id = thread_id or str(uuid.uuid4())
|
176 |
+
|
177 |
+
# Initialize state
|
178 |
+
initial_state = create_initial_state(thread_id)
|
179 |
+
|
180 |
+
# Add input message as HumanMessage object
|
181 |
+
initial_state["messages"].append(
|
182 |
+
HumanMessage(content=input_text)
|
183 |
+
)
|
184 |
+
|
185 |
+
# Add context if provided
|
186 |
+
if context:
|
187 |
+
initial_state["context"].update(context)
|
188 |
+
|
189 |
+
# Validate state
|
190 |
+
validate_state(initial_state)
|
191 |
+
|
192 |
+
# Store state in conversation states
|
193 |
+
self.conversation_states[thread_id] = initial_state
|
194 |
+
|
195 |
+
# Process through graph
|
196 |
+
result = self.graph.invoke(initial_state)
|
197 |
+
|
198 |
+
return self._format_response(result)
|
199 |
+
|
200 |
+
except ValidationError as ve:
|
201 |
+
logger.error(f"Validation error: {str(ve)}")
|
202 |
+
raise
|
203 |
+
except Exception as e:
|
204 |
+
logger.error(f"Error processing input: {str(e)}")
|
205 |
+
raise HealthcareError(
|
206 |
+
message="Failed to process input",
|
207 |
+
error_code="PROCESSING_ERROR",
|
208 |
+
details={"error": str(e)}
|
209 |
+
)
|
210 |
+
|
211 |
+
def _format_response(self, result: Dict) -> Dict:
|
212 |
+
"""Format the final response from the graph execution"""
|
213 |
+
try:
|
214 |
+
if not result or "messages" not in result:
|
215 |
+
raise ProcessingError(
|
216 |
+
message="Invalid result format",
|
217 |
+
error_code="INVALID_RESULT",
|
218 |
+
details={"result": str(result)}
|
219 |
+
)
|
220 |
+
|
221 |
+
return {
|
222 |
+
"response": result["messages"][-1].content if result["messages"] else "",
|
223 |
+
"analysis": result.get("analysis", {}),
|
224 |
+
"metrics": result.get("metrics", {}),
|
225 |
+
"timestamp": datetime.now()
|
226 |
+
}
|
227 |
+
except Exception as e:
|
228 |
+
logger.error(f"Error formatting response: {str(e)}")
|
229 |
+
raise HealthcareError(
|
230 |
+
message="Failed to format response",
|
231 |
+
error_code="FORMAT_ERROR",
|
232 |
+
details={"error": str(e)}
|
233 |
+
)
|
234 |
+
|
235 |
+
def get_conversation_history(
|
236 |
+
self,
|
237 |
+
thread_id: str
|
238 |
+
) -> List[Dict]:
|
239 |
+
"""Retrieve conversation history for a specific thread"""
|
240 |
+
try:
|
241 |
+
return self.conversation_states.get(thread_id, {}).get("messages", [])
|
242 |
+
except Exception as e:
|
243 |
+
logger.error(f"Error retrieving conversation history: {str(e)}")
|
244 |
+
raise HealthcareError(
|
245 |
+
message="Failed to retrieve conversation history",
|
246 |
+
error_code="HISTORY_ERROR",
|
247 |
+
details={"error": str(e)}
|
248 |
+
)
|
249 |
+
|
250 |
+
def reset_conversation(
|
251 |
+
self,
|
252 |
+
thread_id: str
|
253 |
+
) -> bool:
|
254 |
+
"""Reset conversation state for a specific thread"""
|
255 |
+
try:
|
256 |
+
self.conversation_states[thread_id] = create_initial_state(thread_id)
|
257 |
+
return True
|
258 |
+
except Exception as e:
|
259 |
+
logger.error(f"Error resetting conversation: {str(e)}")
|
260 |
+
raise HealthcareError(
|
261 |
+
message="Failed to reset conversation",
|
262 |
+
error_code="RESET_ERROR",
|
263 |
+
details={"error": str(e)}
|
264 |
+
)
|
src/config/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/config/__init__.py
|
2 |
+
from .settings import Settings
|
3 |
+
from .prompts import PROMPTS
|
4 |
+
|
5 |
+
__all__ = ['Settings', 'PROMPTS']
|
src/config/prompts.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/config/prompts.py
|
2 |
+
PROMPTS = {
|
3 |
+
"system": """You are an expert Healthcare Operations Management Assistant.
|
4 |
+
Your role is to optimize hospital operations through:
|
5 |
+
- Patient flow management
|
6 |
+
- Resource allocation
|
7 |
+
- Quality monitoring
|
8 |
+
- Staff scheduling
|
9 |
+
|
10 |
+
Always maintain HIPAA compliance and healthcare standards in your responses.
|
11 |
+
Base your analysis on the provided metrics and department data.""",
|
12 |
+
|
13 |
+
"input_analyzer": """Analyze the following input and determine:
|
14 |
+
1. Primary task category (patient_flow, resource_management, quality_monitoring, staff_scheduling)
|
15 |
+
2. Required context information
|
16 |
+
3. Priority level (1-5, where 5 is highest)
|
17 |
+
4. Relevant department(s)
|
18 |
+
|
19 |
+
Current input: {input}""",
|
20 |
+
|
21 |
+
"patient_flow": """Analyze patient flow based on:
|
22 |
+
- Current occupancy: {occupancy}%
|
23 |
+
- Waiting times: {wait_times} minutes
|
24 |
+
- Department capacity: {department_capacity}
|
25 |
+
- Admission rate: {admission_rate} per hour
|
26 |
+
|
27 |
+
Provide specific recommendations for optimization.""",
|
28 |
+
|
29 |
+
"resource_manager": """Evaluate resource utilization:
|
30 |
+
- Equipment availability: {equipment_status}
|
31 |
+
- Supply levels: {supply_levels}
|
32 |
+
- Resource allocation: {resource_allocation}
|
33 |
+
- Budget constraints: {budget_info}
|
34 |
+
|
35 |
+
Recommend optimal resource distribution.""",
|
36 |
+
|
37 |
+
"quality_monitor": """Review quality metrics:
|
38 |
+
- Patient satisfaction: {satisfaction_score}/10
|
39 |
+
- Care outcomes: {care_outcomes}
|
40 |
+
- Compliance rates: {compliance_rates}%
|
41 |
+
- Incident reports: {incident_count}
|
42 |
+
|
43 |
+
Identify areas for improvement.""",
|
44 |
+
|
45 |
+
"staff_scheduler": """Optimize staff scheduling considering:
|
46 |
+
- Staff availability: {staff_available}
|
47 |
+
- Department needs: {department_needs}
|
48 |
+
- Skill mix requirements: {skill_requirements}
|
49 |
+
- Work hour regulations: {work_hours}
|
50 |
+
|
51 |
+
Provide scheduling recommendations.""",
|
52 |
+
|
53 |
+
"output_synthesis": """Synthesize findings and provide:
|
54 |
+
1. Key insights
|
55 |
+
2. Actionable recommendations
|
56 |
+
3. Priority actions
|
57 |
+
4. Implementation timeline
|
58 |
+
|
59 |
+
Context: {context}"""
|
60 |
+
}
|
61 |
+
|
62 |
+
# Error message templates
|
63 |
+
ERROR_MESSAGES = {
|
64 |
+
"invalid_input": "Invalid input provided. Please ensure all required information is included.",
|
65 |
+
"processing_error": "An error occurred while processing your request. Please try again.",
|
66 |
+
"data_validation": "Data validation failed. Please check the input format.",
|
67 |
+
"system_error": "System error encountered. Please contact support."
|
68 |
+
}
|
69 |
+
|
70 |
+
# Response templates
|
71 |
+
RESPONSE_TEMPLATES = {
|
72 |
+
"confirmation": "Request received and being processed. Priority level: {priority}",
|
73 |
+
"completion": "Analysis complete. {summary}",
|
74 |
+
"error": "Error: {error_message}. Error code: {error_code}"
|
75 |
+
}# System prompts implementation
|
src/config/settings.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/config/settings.py
|
2 |
+
import os
|
3 |
+
from typing import Dict, Any
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
+
class Settings:
|
9 |
+
"""Configuration settings for the Healthcare Operations Management Agent"""
|
10 |
+
|
11 |
+
# OpenAI Configuration
|
12 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
13 |
+
MODEL_NAME = "gpt-4o-mini-2024-07-18"
|
14 |
+
MODEL_TEMPERATURE = 0
|
15 |
+
|
16 |
+
# LangGraph Configuration
|
17 |
+
MEMORY_TYPE = os.getenv("MEMORY_TYPE", "sqlite")
|
18 |
+
MEMORY_URI = os.getenv("MEMORY_URI", ":memory:")
|
19 |
+
|
20 |
+
# Hospital Configuration
|
21 |
+
HOSPITAL_SETTINGS = {
|
22 |
+
"total_beds": 300,
|
23 |
+
"departments": ["ER", "ICU", "General", "Surgery", "Pediatrics"],
|
24 |
+
"staff_roles": ["Doctor", "Nurse", "Specialist", "Support Staff"]
|
25 |
+
}
|
26 |
+
|
27 |
+
# Application Settings
|
28 |
+
MAX_RETRIES = int(os.getenv("MAX_RETRIES", "3"))
|
29 |
+
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
|
30 |
+
BATCH_SIZE = int(os.getenv("BATCH_SIZE", "10"))
|
31 |
+
|
32 |
+
# Logging Configuration
|
33 |
+
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
34 |
+
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
35 |
+
LOG_FILE = "logs/healthcare_ops_agent.log"
|
36 |
+
|
37 |
+
# Quality Metrics Thresholds
|
38 |
+
QUALITY_THRESHOLDS = {
|
39 |
+
"min_satisfaction_score": 7.0,
|
40 |
+
"max_wait_time_minutes": 45,
|
41 |
+
"optimal_bed_utilization": 0.85,
|
42 |
+
"min_staff_ratio": {
|
43 |
+
"ICU": 0.5, # 1 nurse per 2 patients
|
44 |
+
"General": 0.25 # 1 nurse per 4 patients
|
45 |
+
}
|
46 |
+
}
|
47 |
+
|
48 |
+
@classmethod
|
49 |
+
def get_model_config(cls) -> Dict[str, Any]:
|
50 |
+
"""Get model configuration"""
|
51 |
+
return {
|
52 |
+
"model": cls.MODEL_NAME,
|
53 |
+
"temperature": cls.MODEL_TEMPERATURE,
|
54 |
+
"api_key": cls.OPENAI_API_KEY
|
55 |
+
}
|
56 |
+
|
57 |
+
@classmethod
|
58 |
+
def validate_settings(cls) -> bool:
|
59 |
+
"""Validate required settings"""
|
60 |
+
required_settings = [
|
61 |
+
"OPENAI_API_KEY",
|
62 |
+
"MODEL_NAME",
|
63 |
+
"MEMORY_TYPE"
|
64 |
+
]
|
65 |
+
|
66 |
+
for setting in required_settings:
|
67 |
+
if not getattr(cls, setting):
|
68 |
+
raise ValueError(f"Missing required setting: {setting}")
|
69 |
+
|
70 |
+
return True# Configuration settings implementation
|
src/models/__init__.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/models/__init__.py
|
2 |
+
from .state import (
|
3 |
+
TaskType,
|
4 |
+
PriorityLevel,
|
5 |
+
Department,
|
6 |
+
HospitalState,
|
7 |
+
PatientFlowMetrics,
|
8 |
+
ResourceMetrics,
|
9 |
+
QualityMetrics,
|
10 |
+
StaffingMetrics,
|
11 |
+
HospitalMetrics,
|
12 |
+
AnalysisResult,
|
13 |
+
create_initial_state,
|
14 |
+
validate_state,
|
15 |
+
update_state_metrics
|
16 |
+
)
|
17 |
+
|
18 |
+
__all__ = [
|
19 |
+
'TaskType',
|
20 |
+
'PriorityLevel',
|
21 |
+
'Department',
|
22 |
+
'HospitalState',
|
23 |
+
'PatientFlowMetrics',
|
24 |
+
'ResourceMetrics',
|
25 |
+
'QualityMetrics',
|
26 |
+
'StaffingMetrics',
|
27 |
+
'HospitalMetrics',
|
28 |
+
'AnalysisResult',
|
29 |
+
'create_initial_state',
|
30 |
+
'validate_state',
|
31 |
+
'update_state_metrics'
|
32 |
+
]
|
src/models/state.py
ADDED
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/models/state.py
|
2 |
+
from typing import Annotated, List, Dict, Optional
|
3 |
+
from typing_extensions import TypedDict # Changed this import
|
4 |
+
from langchain_core.messages import AnyMessage
|
5 |
+
from datetime import datetime
|
6 |
+
import operator
|
7 |
+
from enum import Enum
|
8 |
+
from langchain_core.messages import HumanMessage, SystemMessage, AnyMessage
|
9 |
+
|
10 |
+
|
11 |
+
class TaskType(str, Enum):
|
12 |
+
PATIENT_FLOW = "patient_flow"
|
13 |
+
RESOURCE_MANAGEMENT = "resource_management"
|
14 |
+
QUALITY_MONITORING = "quality_monitoring"
|
15 |
+
STAFF_SCHEDULING = "staff_scheduling"
|
16 |
+
GENERAL = "general"
|
17 |
+
|
18 |
+
class PriorityLevel(int, Enum):
|
19 |
+
LOW = 1
|
20 |
+
MEDIUM = 2
|
21 |
+
HIGH = 3
|
22 |
+
URGENT = 4
|
23 |
+
CRITICAL = 5
|
24 |
+
|
25 |
+
class Department(TypedDict):
|
26 |
+
"""Department information"""
|
27 |
+
id: str
|
28 |
+
name: str
|
29 |
+
capacity: int
|
30 |
+
current_occupancy: int
|
31 |
+
staff_count: Dict[str, int]
|
32 |
+
wait_time: int
|
33 |
+
|
34 |
+
class PatientFlowMetrics(TypedDict):
|
35 |
+
"""Metrics related to patient flow"""
|
36 |
+
total_beds: int
|
37 |
+
occupied_beds: int
|
38 |
+
waiting_patients: int
|
39 |
+
average_wait_time: float
|
40 |
+
admission_rate: float
|
41 |
+
discharge_rate: float
|
42 |
+
department_metrics: Dict[str, "Department"]
|
43 |
+
|
44 |
+
class ResourceMetrics(TypedDict):
|
45 |
+
"""Metrics related to resource management"""
|
46 |
+
equipment_availability: Dict[str, bool]
|
47 |
+
supply_levels: Dict[str, float]
|
48 |
+
resource_utilization: float
|
49 |
+
pending_requests: int
|
50 |
+
critical_supplies: List[str]
|
51 |
+
|
52 |
+
class QualityMetrics(TypedDict):
|
53 |
+
"""Metrics related to quality monitoring"""
|
54 |
+
patient_satisfaction: float
|
55 |
+
care_outcomes: Dict[str, float]
|
56 |
+
compliance_rate: float
|
57 |
+
incident_count: int
|
58 |
+
quality_scores: Dict[str, float]
|
59 |
+
last_audit_date: datetime
|
60 |
+
|
61 |
+
class StaffingMetrics(TypedDict):
|
62 |
+
"""Metrics related to staff scheduling"""
|
63 |
+
total_staff: int
|
64 |
+
available_staff: Dict[str, int]
|
65 |
+
shifts_coverage: Dict[str, float]
|
66 |
+
overtime_hours: float
|
67 |
+
skill_mix_index: float
|
68 |
+
staff_satisfaction: float
|
69 |
+
|
70 |
+
class HospitalMetrics(TypedDict):
|
71 |
+
"""Combined hospital metrics"""
|
72 |
+
patient_flow: PatientFlowMetrics
|
73 |
+
resources: ResourceMetrics
|
74 |
+
quality: QualityMetrics
|
75 |
+
staffing: StaffingMetrics
|
76 |
+
last_updated: datetime
|
77 |
+
|
78 |
+
class AnalysisResult(TypedDict):
|
79 |
+
"""Analysis results from nodes"""
|
80 |
+
category: TaskType
|
81 |
+
priority: PriorityLevel
|
82 |
+
findings: List[str]
|
83 |
+
recommendations: List[str]
|
84 |
+
action_items: List[Dict[str, str]]
|
85 |
+
metrics_impact: Dict[str, float]
|
86 |
+
|
87 |
+
|
88 |
+
class HospitalState(TypedDict):
|
89 |
+
"""Main state management for the agent"""
|
90 |
+
messages: Annotated[List[AnyMessage], operator.add]
|
91 |
+
current_task: TaskType
|
92 |
+
priority_level: PriorityLevel
|
93 |
+
department: Optional[str]
|
94 |
+
metrics: HospitalMetrics
|
95 |
+
analysis: Optional[AnalysisResult]
|
96 |
+
context: Dict[str, any] # Will include routing information
|
97 |
+
timestamp: datetime
|
98 |
+
thread_id: str
|
99 |
+
|
100 |
+
|
101 |
+
def create_initial_state(thread_id: str) -> HospitalState:
|
102 |
+
"""Create initial state with default values"""
|
103 |
+
return {
|
104 |
+
"messages": [],
|
105 |
+
"current_task": TaskType.GENERAL,
|
106 |
+
"priority_level": PriorityLevel.MEDIUM,
|
107 |
+
"department": None,
|
108 |
+
"metrics": {
|
109 |
+
"patient_flow": {
|
110 |
+
"total_beds": 300,
|
111 |
+
"occupied_beds": 240,
|
112 |
+
"waiting_patients": 15,
|
113 |
+
"average_wait_time": 35.0,
|
114 |
+
"admission_rate": 4.2,
|
115 |
+
"discharge_rate": 3.8,
|
116 |
+
"department_metrics": {}
|
117 |
+
},
|
118 |
+
"resources": {
|
119 |
+
"equipment_availability": {},
|
120 |
+
"supply_levels": {},
|
121 |
+
"resource_utilization": 0.75,
|
122 |
+
"pending_requests": 5,
|
123 |
+
"critical_supplies": []
|
124 |
+
},
|
125 |
+
"quality": {
|
126 |
+
"patient_satisfaction": 8.5,
|
127 |
+
"care_outcomes": {},
|
128 |
+
"compliance_rate": 0.95,
|
129 |
+
"incident_count": 2,
|
130 |
+
"quality_scores": {},
|
131 |
+
"last_audit_date": datetime.now()
|
132 |
+
},
|
133 |
+
"staffing": {
|
134 |
+
"total_staff": 500,
|
135 |
+
"available_staff": {
|
136 |
+
"doctors": 50,
|
137 |
+
"nurses": 150,
|
138 |
+
"specialists": 30,
|
139 |
+
"support": 70
|
140 |
+
},
|
141 |
+
"shifts_coverage": {},
|
142 |
+
"overtime_hours": 120.5,
|
143 |
+
"skill_mix_index": 0.85,
|
144 |
+
"staff_satisfaction": 7.8
|
145 |
+
},
|
146 |
+
"last_updated": datetime.now()
|
147 |
+
},
|
148 |
+
"analysis": None,
|
149 |
+
"context": {
|
150 |
+
"next_node": None # Add routing context
|
151 |
+
},
|
152 |
+
"timestamp": datetime.now(),
|
153 |
+
"thread_id": thread_id
|
154 |
+
}
|
155 |
+
|
156 |
+
def validate_state(state: HospitalState) -> bool:
|
157 |
+
"""Validate state structure and data types"""
|
158 |
+
try:
|
159 |
+
# Basic structure validation
|
160 |
+
required_keys = [
|
161 |
+
"messages", "current_task", "priority_level",
|
162 |
+
"metrics", "timestamp", "thread_id"
|
163 |
+
]
|
164 |
+
for key in required_keys:
|
165 |
+
if key not in state:
|
166 |
+
raise ValueError(f"Missing required key: {key}")
|
167 |
+
|
168 |
+
# Validate messages
|
169 |
+
if not isinstance(state["messages"], list):
|
170 |
+
raise ValueError("Messages must be a list")
|
171 |
+
|
172 |
+
# Validate each message has required attributes
|
173 |
+
for msg in state["messages"]:
|
174 |
+
if not hasattr(msg, 'content'):
|
175 |
+
raise ValueError("Invalid message format - missing content")
|
176 |
+
|
177 |
+
# Validate types
|
178 |
+
if not isinstance(state["current_task"], TaskType):
|
179 |
+
raise ValueError("Invalid task type")
|
180 |
+
if not isinstance(state["priority_level"], PriorityLevel):
|
181 |
+
raise ValueError("Invalid priority level")
|
182 |
+
if not isinstance(state["timestamp"], datetime):
|
183 |
+
raise ValueError("Invalid timestamp")
|
184 |
+
|
185 |
+
return True
|
186 |
+
|
187 |
+
except Exception as e:
|
188 |
+
raise ValueError(f"State validation failed: {str(e)}")
|
189 |
+
|
190 |
+
def update_state_metrics(
|
191 |
+
state: HospitalState,
|
192 |
+
new_metrics: Dict,
|
193 |
+
category: str
|
194 |
+
) -> HospitalState:
|
195 |
+
"""Update specific category of metrics in state"""
|
196 |
+
if category not in state["metrics"]:
|
197 |
+
raise ValueError(f"Invalid metrics category: {category}")
|
198 |
+
|
199 |
+
state["metrics"][category].update(new_metrics)
|
200 |
+
state["metrics"]["last_updated"] = datetime.now()
|
201 |
+
|
202 |
+
return state
|
src/nodes/__init__.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/nodes/__init__.py
|
2 |
+
from .input_analyzer import InputAnalyzerNode
|
3 |
+
from .task_router import TaskRouterNode
|
4 |
+
from .patient_flow import PatientFlowNode
|
5 |
+
from .resource_manager import ResourceManagerNode
|
6 |
+
from .quality_monitor import QualityMonitorNode
|
7 |
+
from .staff_scheduler import StaffSchedulerNode
|
8 |
+
from .output_synthesizer import OutputSynthesizerNode
|
9 |
+
|
10 |
+
__all__ = [
|
11 |
+
'InputAnalyzerNode',
|
12 |
+
'TaskRouterNode',
|
13 |
+
'PatientFlowNode',
|
14 |
+
'ResourceManagerNode',
|
15 |
+
'QualityMonitorNode',
|
16 |
+
'StaffSchedulerNode',
|
17 |
+
'OutputSynthesizerNode'
|
18 |
+
]
|
src/nodes/input_analyzer.py
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/nodes/input_analyzer.py
|
2 |
+
#from typing import Dict
|
3 |
+
from typing import Dict, List, Optional, Any
|
4 |
+
from typing_extensions import TypedDict # If using TypedDict
|
5 |
+
#from langchain_core.messages import SystemMessage, HumanMessage
|
6 |
+
from ..models.state import HospitalState, TaskType, PriorityLevel
|
7 |
+
from ..config.prompts import PROMPTS
|
8 |
+
from ..utils.logger import setup_logger
|
9 |
+
from langchain_core.messages import HumanMessage, SystemMessage, AnyMessage
|
10 |
+
|
11 |
+
logger = setup_logger(__name__)
|
12 |
+
|
13 |
+
class InputAnalyzerNode:
|
14 |
+
def __init__(self, llm):
|
15 |
+
self.llm = llm
|
16 |
+
self.system_prompt = PROMPTS["input_analyzer"]
|
17 |
+
|
18 |
+
def __call__(self, state: HospitalState) -> Dict:
|
19 |
+
try:
|
20 |
+
# Get the latest message
|
21 |
+
if not state["messages"]:
|
22 |
+
raise ValueError("No messages in state")
|
23 |
+
|
24 |
+
latest_message = state["messages"][-1]
|
25 |
+
|
26 |
+
# Ensure message is a LangChain message object
|
27 |
+
if not hasattr(latest_message, 'content'):
|
28 |
+
raise ValueError("Invalid message format")
|
29 |
+
|
30 |
+
# Prepare messages for LLM
|
31 |
+
messages = [
|
32 |
+
SystemMessage(content=self.system_prompt),
|
33 |
+
latest_message if isinstance(latest_message, HumanMessage)
|
34 |
+
else HumanMessage(content=str(latest_message))
|
35 |
+
]
|
36 |
+
|
37 |
+
# Get LLM response
|
38 |
+
response = self.llm.invoke(messages)
|
39 |
+
|
40 |
+
# Parse response to determine task type and priority
|
41 |
+
parsed_result = self._parse_llm_response(response.content)
|
42 |
+
|
43 |
+
return {
|
44 |
+
"current_task": parsed_result["task_type"],
|
45 |
+
"priority_level": parsed_result["priority"],
|
46 |
+
"department": parsed_result["department"],
|
47 |
+
"context": parsed_result["context"]
|
48 |
+
}
|
49 |
+
|
50 |
+
except Exception as e:
|
51 |
+
logger.error(f"Error in input analysis: {str(e)}")
|
52 |
+
raise
|
53 |
+
|
54 |
+
def _parse_llm_response(self, response: str) -> Dict:
|
55 |
+
"""Parse LLM response to extract task type and other metadata"""
|
56 |
+
try:
|
57 |
+
# Default values
|
58 |
+
result = {
|
59 |
+
"task_type": TaskType.GENERAL,
|
60 |
+
"priority": PriorityLevel.MEDIUM,
|
61 |
+
"department": None,
|
62 |
+
"context": {}
|
63 |
+
}
|
64 |
+
|
65 |
+
# Simple parsing logic (can be made more robust)
|
66 |
+
if "patient flow" in response.lower():
|
67 |
+
result["task_type"] = TaskType.PATIENT_FLOW
|
68 |
+
elif "resource" in response.lower():
|
69 |
+
result["task_type"] = TaskType.RESOURCE_MANAGEMENT
|
70 |
+
elif "quality" in response.lower():
|
71 |
+
result["task_type"] = TaskType.QUALITY_MONITORING
|
72 |
+
elif "staff" in response.lower() or "schedule" in response.lower():
|
73 |
+
result["task_type"] = TaskType.STAFF_SCHEDULING
|
74 |
+
|
75 |
+
# Extract priority from response
|
76 |
+
if "urgent" in response.lower() or "critical" in response.lower():
|
77 |
+
result["priority"] = PriorityLevel.CRITICAL
|
78 |
+
elif "high" in response.lower():
|
79 |
+
result["priority"] = PriorityLevel.HIGH
|
80 |
+
|
81 |
+
return result
|
82 |
+
|
83 |
+
except Exception as e:
|
84 |
+
logger.error(f"Error parsing LLM response: {str(e)}")
|
85 |
+
return result
|
src/nodes/output_synthesizer.py
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/nodes/output_synthesizer.py
|
2 |
+
#from typing import Dict, List
|
3 |
+
from typing import Dict, List, Optional, Any
|
4 |
+
from typing_extensions import TypedDict # If using TypedDict
|
5 |
+
from langchain_core.messages import SystemMessage
|
6 |
+
from ..models.state import HospitalState
|
7 |
+
from ..config.prompts import PROMPTS
|
8 |
+
from ..utils.logger import setup_logger
|
9 |
+
|
10 |
+
logger = setup_logger(__name__)
|
11 |
+
|
12 |
+
class OutputSynthesizerNode:
|
13 |
+
def __init__(self, llm):
|
14 |
+
self.llm = llm
|
15 |
+
self.system_prompt = PROMPTS["output_synthesis"]
|
16 |
+
|
17 |
+
def __call__(self, state: HospitalState) -> Dict:
|
18 |
+
try:
|
19 |
+
# Get analysis results from previous nodes
|
20 |
+
analysis = state.get("analysis", {})
|
21 |
+
|
22 |
+
# Format prompt with context
|
23 |
+
formatted_prompt = self.system_prompt.format(
|
24 |
+
context=self._format_context(state)
|
25 |
+
)
|
26 |
+
|
27 |
+
# Get LLM synthesis
|
28 |
+
response = self.llm.invoke([
|
29 |
+
SystemMessage(content=formatted_prompt)
|
30 |
+
])
|
31 |
+
|
32 |
+
# Structure the final output
|
33 |
+
final_output = self._structure_final_output(
|
34 |
+
response.content,
|
35 |
+
state["current_task"],
|
36 |
+
state["priority_level"]
|
37 |
+
)
|
38 |
+
|
39 |
+
return {
|
40 |
+
"messages": [response],
|
41 |
+
"analysis": final_output
|
42 |
+
}
|
43 |
+
|
44 |
+
except Exception as e:
|
45 |
+
logger.error(f"Error in output synthesis: {str(e)}")
|
46 |
+
raise
|
47 |
+
|
48 |
+
def _format_context(self, state: HospitalState) -> str:
|
49 |
+
"""Format all relevant context for synthesis"""
|
50 |
+
return f"""
|
51 |
+
Task Type: {state['current_task']}
|
52 |
+
Priority Level: {state['priority_level']}
|
53 |
+
Department: {state['department'] or 'All Departments'}
|
54 |
+
Key Metrics Summary:
|
55 |
+
- Patient Flow: {self._summarize_patient_flow(state)}
|
56 |
+
- Resources: {self._summarize_resources(state)}
|
57 |
+
- Quality: {self._summarize_quality(state)}
|
58 |
+
- Staffing: {self._summarize_staffing(state)}
|
59 |
+
"""
|
60 |
+
|
61 |
+
def _structure_final_output(self, response: str, task_type: str, priority: int) -> Dict:
|
62 |
+
"""Structure the final output in a standardized format"""
|
63 |
+
return {
|
64 |
+
"summary": self._extract_summary(response),
|
65 |
+
"key_findings": self._extract_key_findings(response),
|
66 |
+
"recommendations": self._extract_recommendations(response),
|
67 |
+
"action_items": self._extract_action_items(response),
|
68 |
+
"priority_level": priority,
|
69 |
+
"task_type": task_type
|
70 |
+
}
|
71 |
+
|
72 |
+
def _summarize_patient_flow(self, state: HospitalState) -> str:
|
73 |
+
metrics = state["metrics"]["patient_flow"]
|
74 |
+
return f"Occupancy {(metrics['occupied_beds']/metrics['total_beds'])*100:.1f}%"
|
75 |
+
|
76 |
+
def _summarize_resources(self, state: HospitalState) -> str:
|
77 |
+
metrics = state["metrics"]["resources"]
|
78 |
+
return f"Utilization {metrics['resource_utilization']*100:.1f}%"
|
79 |
+
|
80 |
+
def _summarize_quality(self, state: HospitalState) -> str:
|
81 |
+
metrics = state["metrics"]["quality"]
|
82 |
+
return f"Satisfaction {metrics['patient_satisfaction']:.1f}/10"
|
83 |
+
|
84 |
+
def _summarize_staffing(self, state: HospitalState) -> str:
|
85 |
+
metrics = state["metrics"]["staffing"]
|
86 |
+
return f"Staff Available: {sum(metrics['available_staff'].values())}"
|
87 |
+
|
88 |
+
def _extract_summary(self, response: str) -> str:
|
89 |
+
"""Extract high-level summary from response"""
|
90 |
+
# Implementation depends on response structure
|
91 |
+
return response.split('\n')[0]
|
92 |
+
|
93 |
+
def _extract_key_findings(self, response: str) -> List[str]:
|
94 |
+
"""Extract key findings from response"""
|
95 |
+
findings = []
|
96 |
+
# Implementation for parsing findings
|
97 |
+
return findings
|
98 |
+
|
99 |
+
def _extract_recommendations(self, response: str) -> List[str]:
|
100 |
+
"""Extract recommendations from response"""
|
101 |
+
recommendations = []
|
102 |
+
# Implementation for parsing recommendations
|
103 |
+
return recommendations
|
104 |
+
|
105 |
+
def _extract_action_items(self, response: str) -> List[Dict]:
|
106 |
+
"""Extract actionable items from response"""
|
107 |
+
action_items = []
|
108 |
+
# Implementation for parsing action items
|
109 |
+
return action_items# output_synthesizer node implementation
|
src/nodes/patient_flow.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/nodes/patient_flow.py
|
2 |
+
#from typing import Dict
|
3 |
+
from typing import Dict, List, Optional, Any
|
4 |
+
from typing_extensions import TypedDict # If using TypedDict
|
5 |
+
from langchain_core.messages import SystemMessage
|
6 |
+
from ..models.state import HospitalState
|
7 |
+
from ..config.prompts import PROMPTS
|
8 |
+
from ..utils.logger import setup_logger
|
9 |
+
|
10 |
+
logger = setup_logger(__name__)
|
11 |
+
|
12 |
+
class PatientFlowNode:
|
13 |
+
def __init__(self, llm):
|
14 |
+
self.llm = llm
|
15 |
+
self.system_prompt = PROMPTS["patient_flow"]
|
16 |
+
|
17 |
+
def __call__(self, state: HospitalState) -> Dict:
|
18 |
+
try:
|
19 |
+
# Get current metrics
|
20 |
+
metrics = state["metrics"]["patient_flow"]
|
21 |
+
|
22 |
+
# Format prompt with current metrics
|
23 |
+
formatted_prompt = self.system_prompt.format(
|
24 |
+
occupancy=self._calculate_occupancy(metrics),
|
25 |
+
wait_times=metrics["average_wait_time"],
|
26 |
+
department_capacity=self._get_department_capacity(metrics),
|
27 |
+
admission_rate=metrics["admission_rate"]
|
28 |
+
)
|
29 |
+
|
30 |
+
# Get LLM analysis
|
31 |
+
response = self.llm.invoke([
|
32 |
+
SystemMessage(content=formatted_prompt)
|
33 |
+
])
|
34 |
+
|
35 |
+
# Parse and structure the response
|
36 |
+
analysis = self._structure_analysis(response.content)
|
37 |
+
|
38 |
+
return {
|
39 |
+
"analysis": analysis,
|
40 |
+
"messages": [response]
|
41 |
+
}
|
42 |
+
|
43 |
+
except Exception as e:
|
44 |
+
logger.error(f"Error in patient flow analysis: {str(e)}")
|
45 |
+
raise
|
46 |
+
|
47 |
+
def _calculate_occupancy(self, metrics: Dict) -> float:
|
48 |
+
"""Calculate current occupancy percentage"""
|
49 |
+
return (metrics["occupied_beds"] / metrics["total_beds"]) * 100
|
50 |
+
|
51 |
+
def _get_department_capacity(self, metrics: Dict) -> Dict:
|
52 |
+
"""Get capacity details by department"""
|
53 |
+
return metrics.get("department_metrics", {})
|
54 |
+
|
55 |
+
def _structure_analysis(self, response: str) -> Dict:
|
56 |
+
"""Structure the LLM response into a standardized format"""
|
57 |
+
return {
|
58 |
+
"findings": [], # Extract key findings
|
59 |
+
"recommendations": [], # Extract recommendations
|
60 |
+
"action_items": [], # Extract action items
|
61 |
+
"metrics_impact": {} # Expected impact on metrics
|
62 |
+
}# patient_flow node implementation
|
src/nodes/quality_monitor.py
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/nodes/quality_monitor.py
|
2 |
+
|
3 |
+
from typing import Dict, List, Optional, Any
|
4 |
+
from typing_extensions import TypedDict # If using TypedDict
|
5 |
+
from langchain_core.messages import SystemMessage
|
6 |
+
from ..models.state import HospitalState
|
7 |
+
from ..config.prompts import PROMPTS
|
8 |
+
from ..utils.logger import setup_logger
|
9 |
+
|
10 |
+
logger = setup_logger(__name__)
|
11 |
+
|
12 |
+
class QualityMonitorNode:
|
13 |
+
def __init__(self, llm):
|
14 |
+
self.llm = llm
|
15 |
+
self.system_prompt = PROMPTS["quality_monitor"]
|
16 |
+
|
17 |
+
def __call__(self, state: HospitalState) -> Dict:
|
18 |
+
try:
|
19 |
+
# Get current quality metrics
|
20 |
+
metrics = state["metrics"]["quality"]
|
21 |
+
|
22 |
+
# Format prompt with current metrics
|
23 |
+
formatted_prompt = self.system_prompt.format(
|
24 |
+
satisfaction_score=metrics["patient_satisfaction"],
|
25 |
+
care_outcomes=self._format_care_outcomes(metrics),
|
26 |
+
compliance_rates=metrics["compliance_rate"] * 100,
|
27 |
+
incident_count=metrics["incident_count"]
|
28 |
+
)
|
29 |
+
|
30 |
+
# Get LLM analysis
|
31 |
+
response = self.llm.invoke([
|
32 |
+
SystemMessage(content=formatted_prompt)
|
33 |
+
])
|
34 |
+
|
35 |
+
# Process quality assessment
|
36 |
+
analysis = self._analyze_quality_metrics(response.content, metrics)
|
37 |
+
|
38 |
+
return {
|
39 |
+
"analysis": analysis,
|
40 |
+
"messages": [response],
|
41 |
+
"context": {
|
42 |
+
"quality_scores": metrics["quality_scores"],
|
43 |
+
"last_audit": metrics["last_audit_date"]
|
44 |
+
}
|
45 |
+
}
|
46 |
+
|
47 |
+
except Exception as e:
|
48 |
+
logger.error(f"Error in quality monitoring analysis: {str(e)}")
|
49 |
+
raise
|
50 |
+
|
51 |
+
def _format_care_outcomes(self, metrics: Dict) -> str:
|
52 |
+
"""Format care outcomes into readable text"""
|
53 |
+
outcomes = []
|
54 |
+
for metric, value in metrics["care_outcomes"].items():
|
55 |
+
outcomes.append(f"{metric}: {value:.1f}")
|
56 |
+
return ", ".join(outcomes)
|
57 |
+
|
58 |
+
def _analyze_quality_metrics(self, response: str, metrics: Dict) -> Dict:
|
59 |
+
"""Analyze quality metrics and identify areas for improvement"""
|
60 |
+
return {
|
61 |
+
"satisfaction_analysis": self._analyze_satisfaction(metrics),
|
62 |
+
"compliance_analysis": self._analyze_compliance(metrics),
|
63 |
+
"incident_analysis": self._analyze_incidents(metrics),
|
64 |
+
"recommendations": self._extract_recommendations(response),
|
65 |
+
"priority_improvements": []
|
66 |
+
}
|
67 |
+
|
68 |
+
def _analyze_satisfaction(self, metrics: Dict) -> Dict:
|
69 |
+
"""Analyze patient satisfaction trends"""
|
70 |
+
satisfaction = metrics["patient_satisfaction"]
|
71 |
+
return {
|
72 |
+
"current_score": satisfaction,
|
73 |
+
"status": "Good" if satisfaction >= 8.0 else "Needs Improvement",
|
74 |
+
"trend": "Unknown" # Would need historical data
|
75 |
+
}
|
76 |
+
|
77 |
+
def _analyze_compliance(self, metrics: Dict) -> Dict:
|
78 |
+
"""Analyze compliance rates"""
|
79 |
+
return {
|
80 |
+
"rate": metrics["compliance_rate"],
|
81 |
+
"status": "Compliant" if metrics["compliance_rate"] >= 0.95 else "Review Required"
|
82 |
+
}
|
83 |
+
|
84 |
+
def _analyze_incidents(self, metrics: Dict) -> Dict:
|
85 |
+
"""Analyze incident reports"""
|
86 |
+
return {
|
87 |
+
"count": metrics["incident_count"],
|
88 |
+
"severity": "High" if metrics["incident_count"] > 5 else "Low"
|
89 |
+
}
|
90 |
+
|
91 |
+
def _extract_recommendations(self, response: str) -> List[str]:
|
92 |
+
"""Extract recommendations from LLM response"""
|
93 |
+
recommendations = []
|
94 |
+
for line in response.split('\n'):
|
95 |
+
if 'recommend' in line.lower() or 'suggest' in line.lower():
|
96 |
+
recommendations.append(line.strip())
|
97 |
+
return recommendations
|
src/nodes/resource_manager.py
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/nodes/resource_manager.py
|
2 |
+
from typing import Dict, List, Optional, Any
|
3 |
+
from typing_extensions import TypedDict # If using TypedDict
|
4 |
+
#from typing import Dict
|
5 |
+
from langchain_core.messages import SystemMessage
|
6 |
+
from ..models.state import HospitalState
|
7 |
+
from ..config.prompts import PROMPTS
|
8 |
+
from ..utils.logger import setup_logger
|
9 |
+
|
10 |
+
logger = setup_logger(__name__)
|
11 |
+
|
12 |
+
class ResourceManagerNode:
|
13 |
+
def __init__(self, llm):
|
14 |
+
self.llm = llm
|
15 |
+
self.system_prompt = PROMPTS["resource_manager"]
|
16 |
+
|
17 |
+
def __call__(self, state: HospitalState) -> Dict:
|
18 |
+
try:
|
19 |
+
# Get current resource metrics
|
20 |
+
metrics = state["metrics"]["resources"]
|
21 |
+
|
22 |
+
# Format prompt with current metrics
|
23 |
+
formatted_prompt = self.system_prompt.format(
|
24 |
+
equipment_status=self._format_equipment_status(metrics),
|
25 |
+
supply_levels=self._format_supply_levels(metrics),
|
26 |
+
resource_allocation=metrics["resource_utilization"],
|
27 |
+
budget_info=self._get_budget_info(state)
|
28 |
+
)
|
29 |
+
|
30 |
+
# Get LLM analysis
|
31 |
+
response = self.llm.invoke([
|
32 |
+
SystemMessage(content=formatted_prompt)
|
33 |
+
])
|
34 |
+
|
35 |
+
# Update state with recommendations
|
36 |
+
analysis = self._parse_recommendations(response.content)
|
37 |
+
|
38 |
+
return {
|
39 |
+
"analysis": analysis,
|
40 |
+
"messages": [response],
|
41 |
+
"context": {
|
42 |
+
"critical_supplies": metrics["critical_supplies"],
|
43 |
+
"pending_requests": metrics["pending_requests"]
|
44 |
+
}
|
45 |
+
}
|
46 |
+
|
47 |
+
except Exception as e:
|
48 |
+
logger.error(f"Error in resource management analysis: {str(e)}")
|
49 |
+
raise
|
50 |
+
|
51 |
+
def _format_equipment_status(self, metrics: Dict) -> str:
|
52 |
+
"""Format equipment availability into readable text"""
|
53 |
+
status = []
|
54 |
+
for equip, available in metrics["equipment_availability"].items():
|
55 |
+
status.append(f"{equip}: {'Available' if available else 'In Use'}")
|
56 |
+
return ", ".join(status)
|
57 |
+
|
58 |
+
def _format_supply_levels(self, metrics: Dict) -> str:
|
59 |
+
"""Format supply levels into readable text"""
|
60 |
+
levels = []
|
61 |
+
for item, level in metrics["supply_levels"].items():
|
62 |
+
status = "Critical" if level < 0.2 else "Low" if level < 0.4 else "Adequate"
|
63 |
+
levels.append(f"{item}: {status} ({level*100:.0f}%)")
|
64 |
+
return ", ".join(levels)
|
65 |
+
|
66 |
+
def _get_budget_info(self, state: HospitalState) -> str:
|
67 |
+
"""Get budget information from context"""
|
68 |
+
return state.get("context", {}).get("budget_info", "Budget information not available")
|
69 |
+
|
70 |
+
def _parse_recommendations(self, response: str) -> Dict:
|
71 |
+
"""Parse LLM recommendations into structured format"""
|
72 |
+
return {
|
73 |
+
"resource_optimization": [],
|
74 |
+
"supply_management": [],
|
75 |
+
"equipment_maintenance": [],
|
76 |
+
"budget_allocation": [],
|
77 |
+
"priority_actions": []
|
78 |
+
}# resource_manager node implementation
|
src/nodes/staff_scheduler.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/nodes/staff_scheduler.py
|
2 |
+
from typing import Dict, List, Optional, Any
|
3 |
+
from typing_extensions import TypedDict # If using TypedDict
|
4 |
+
from langchain_core.messages import SystemMessage
|
5 |
+
from ..models.state import HospitalState
|
6 |
+
from ..config.prompts import PROMPTS
|
7 |
+
from ..utils.logger import setup_logger
|
8 |
+
|
9 |
+
logger = setup_logger(__name__)
|
10 |
+
|
11 |
+
class StaffSchedulerNode:
|
12 |
+
def __init__(self, llm):
|
13 |
+
self.llm = llm
|
14 |
+
self.system_prompt = PROMPTS["staff_scheduler"]
|
15 |
+
|
16 |
+
def __call__(self, state: HospitalState) -> Dict:
|
17 |
+
try:
|
18 |
+
# Get current staffing metrics
|
19 |
+
metrics = state["metrics"]["staffing"]
|
20 |
+
|
21 |
+
# Format prompt with current metrics
|
22 |
+
formatted_prompt = self.system_prompt.format(
|
23 |
+
staff_available=self._format_staff_availability(metrics),
|
24 |
+
department_needs=self._get_department_needs(state),
|
25 |
+
skill_requirements=self._format_skill_requirements(metrics),
|
26 |
+
work_hours=metrics["overtime_hours"]
|
27 |
+
)
|
28 |
+
|
29 |
+
# Get LLM analysis
|
30 |
+
response = self.llm.invoke([
|
31 |
+
SystemMessage(content=formatted_prompt)
|
32 |
+
])
|
33 |
+
|
34 |
+
# Generate scheduling recommendations
|
35 |
+
analysis = self._generate_schedule_recommendations(response.content, metrics)
|
36 |
+
|
37 |
+
return {
|
38 |
+
"analysis": analysis,
|
39 |
+
"messages": [response],
|
40 |
+
"context": {
|
41 |
+
"staff_satisfaction": metrics["staff_satisfaction"],
|
42 |
+
"skill_mix_index": metrics["skill_mix_index"]
|
43 |
+
}
|
44 |
+
}
|
45 |
+
|
46 |
+
except Exception as e:
|
47 |
+
logger.error(f"Error in staff scheduling analysis: {str(e)}")
|
48 |
+
raise
|
49 |
+
|
50 |
+
def _format_staff_availability(self, metrics: Dict) -> str:
|
51 |
+
"""Format staff availability into readable text"""
|
52 |
+
return ", ".join([
|
53 |
+
f"{role}: {count} available"
|
54 |
+
for role, count in metrics["available_staff"].items()
|
55 |
+
])
|
56 |
+
|
57 |
+
def _get_department_needs(self, state: HospitalState) -> Dict:
|
58 |
+
"""Get staffing needs by department"""
|
59 |
+
return {
|
60 |
+
dept: metrics
|
61 |
+
for dept, metrics in state["metrics"]["patient_flow"]["department_metrics"].items()
|
62 |
+
}
|
63 |
+
|
64 |
+
def _format_skill_requirements(self, metrics: Dict) -> str:
|
65 |
+
"""Format skill requirements into readable text"""
|
66 |
+
return f"Skill Mix Index: {metrics['skill_mix_index']:.2f}"
|
67 |
+
|
68 |
+
def _generate_schedule_recommendations(self, response: str, metrics: Dict) -> Dict:
|
69 |
+
"""Generate scheduling recommendations based on LLM response"""
|
70 |
+
return {
|
71 |
+
"shift_adjustments": [],
|
72 |
+
"staff_assignments": {},
|
73 |
+
"overtime_recommendations": [],
|
74 |
+
"training_needs": [],
|
75 |
+
"efficiency_improvements": []
|
76 |
+
}# staff_scheduler node implementation
|
src/nodes/task_router.py
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/nodes/task_router.py
|
2 |
+
from typing import Literal
|
3 |
+
from typing import Dict, List, Optional, Any
|
4 |
+
from typing_extensions import TypedDict # If using TypedDict
|
5 |
+
from ..models.state import HospitalState, TaskType
|
6 |
+
from ..utils.logger import setup_logger
|
7 |
+
|
8 |
+
logger = setup_logger(__name__)
|
9 |
+
|
10 |
+
class TaskRouterNode:
|
11 |
+
def __call__(self, state: HospitalState) -> Dict:
|
12 |
+
"""Route to appropriate node based on task type and return state update"""
|
13 |
+
try:
|
14 |
+
task_type = state["current_task"]
|
15 |
+
|
16 |
+
# Create base state update
|
17 |
+
state_update = {
|
18 |
+
"messages": state.get("messages", []),
|
19 |
+
"current_task": task_type,
|
20 |
+
"priority_level": state.get("priority_level"),
|
21 |
+
"context": state.get("context", {})
|
22 |
+
}
|
23 |
+
|
24 |
+
# Add routing information to context
|
25 |
+
if task_type == TaskType.PATIENT_FLOW:
|
26 |
+
state_update["context"]["next_node"] = "patient_flow"
|
27 |
+
elif task_type == TaskType.RESOURCE_MANAGEMENT:
|
28 |
+
state_update["context"]["next_node"] = "resource_management"
|
29 |
+
elif task_type == TaskType.QUALITY_MONITORING:
|
30 |
+
state_update["context"]["next_node"] = "quality_monitoring"
|
31 |
+
elif task_type == TaskType.STAFF_SCHEDULING:
|
32 |
+
state_update["context"]["next_node"] = "staff_scheduling"
|
33 |
+
else:
|
34 |
+
state_update["context"]["next_node"] = "output_synthesis"
|
35 |
+
|
36 |
+
return state_update
|
37 |
+
|
38 |
+
except Exception as e:
|
39 |
+
logger.error(f"Error in task routing: {str(e)}")
|
40 |
+
# Return default routing to output synthesis on error
|
41 |
+
return {
|
42 |
+
"messages": state.get("messages", []),
|
43 |
+
"context": {"next_node": "output_synthesis"},
|
44 |
+
"current_task": state.get("current_task")
|
45 |
+
}
|
src/tools/__init__.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/tools/__init__.py
|
2 |
+
from .patient_tools import PatientTools
|
3 |
+
from .resource_tools import ResourceTools
|
4 |
+
from .quality_tools import QualityTools
|
5 |
+
from .scheduling_tools import SchedulingTools
|
6 |
+
|
7 |
+
__all__ = [
|
8 |
+
'PatientTools',
|
9 |
+
'ResourceTools',
|
10 |
+
'QualityTools',
|
11 |
+
'SchedulingTools'
|
12 |
+
]
|
src/tools/patient_tools.py
ADDED
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/tools/patient_tools.py
|
2 |
+
#from typing import Dict, List, Optional, Any
|
3 |
+
from typing_extensions import TypedDict # If using TypedDict
|
4 |
+
from typing import Dict, List, Optional
|
5 |
+
from langchain_core.tools import tool
|
6 |
+
from datetime import datetime
|
7 |
+
from ..utils.logger import setup_logger
|
8 |
+
from ..models.state import Department
|
9 |
+
|
10 |
+
logger = setup_logger(__name__)
|
11 |
+
|
12 |
+
class PatientTools:
|
13 |
+
@tool
|
14 |
+
def calculate_wait_time(
|
15 |
+
self,
|
16 |
+
department: str,
|
17 |
+
current_queue: int,
|
18 |
+
staff_available: int
|
19 |
+
) -> float:
|
20 |
+
"""Calculate estimated wait time for a department based on queue and staff"""
|
21 |
+
try:
|
22 |
+
# Average time per patient (in minutes)
|
23 |
+
AVG_TIME_PER_PATIENT = 15
|
24 |
+
|
25 |
+
# Factor in staff availability
|
26 |
+
wait_time = (current_queue * AVG_TIME_PER_PATIENT) / max(staff_available, 1)
|
27 |
+
|
28 |
+
return round(wait_time, 1)
|
29 |
+
|
30 |
+
except Exception as e:
|
31 |
+
logger.error(f"Error calculating wait time: {str(e)}")
|
32 |
+
raise
|
33 |
+
|
34 |
+
@tool
|
35 |
+
def analyze_bed_capacity(
|
36 |
+
self,
|
37 |
+
total_beds: int,
|
38 |
+
occupied_beds: int,
|
39 |
+
pending_admissions: int
|
40 |
+
) -> Dict:
|
41 |
+
"""Analyze bed capacity and provide utilization metrics"""
|
42 |
+
try:
|
43 |
+
capacity = {
|
44 |
+
"total_beds": total_beds,
|
45 |
+
"occupied_beds": occupied_beds,
|
46 |
+
"available_beds": total_beds - occupied_beds,
|
47 |
+
"utilization_rate": (occupied_beds / total_beds) * 100,
|
48 |
+
"pending_admissions": pending_admissions,
|
49 |
+
"status": "Normal"
|
50 |
+
}
|
51 |
+
|
52 |
+
# Determine status based on utilization
|
53 |
+
if capacity["utilization_rate"] > 90:
|
54 |
+
capacity["status"] = "Critical"
|
55 |
+
elif capacity["utilization_rate"] > 80:
|
56 |
+
capacity["status"] = "High"
|
57 |
+
|
58 |
+
return capacity
|
59 |
+
|
60 |
+
except Exception as e:
|
61 |
+
logger.error(f"Error analyzing bed capacity: {str(e)}")
|
62 |
+
raise
|
63 |
+
|
64 |
+
@tool
|
65 |
+
def predict_discharge_time(
|
66 |
+
self,
|
67 |
+
admission_date: datetime,
|
68 |
+
condition_type: str,
|
69 |
+
department: str
|
70 |
+
) -> datetime:
|
71 |
+
"""Predict expected discharge time based on condition and department"""
|
72 |
+
try:
|
73 |
+
# Average length of stay (in days) by condition
|
74 |
+
LOS_BY_CONDITION = {
|
75 |
+
"routine": 3,
|
76 |
+
"acute": 5,
|
77 |
+
"critical": 7,
|
78 |
+
"emergency": 2
|
79 |
+
}
|
80 |
+
|
81 |
+
# Get base length of stay
|
82 |
+
base_los = LOS_BY_CONDITION.get(condition_type.lower(), 4)
|
83 |
+
|
84 |
+
# Adjust based on department
|
85 |
+
if department.lower() == "icu":
|
86 |
+
base_los *= 1.5
|
87 |
+
|
88 |
+
# Calculate expected discharge date
|
89 |
+
discharge_date = admission_date + timedelta(days=base_los)
|
90 |
+
|
91 |
+
return discharge_date
|
92 |
+
|
93 |
+
except Exception as e:
|
94 |
+
logger.error(f"Error predicting discharge time: {str(e)}")
|
95 |
+
raise
|
96 |
+
|
97 |
+
@tool
|
98 |
+
def optimize_patient_flow(
|
99 |
+
self,
|
100 |
+
departments: List[Department],
|
101 |
+
waiting_patients: List[Dict]
|
102 |
+
) -> Dict:
|
103 |
+
"""Optimize patient flow across departments"""
|
104 |
+
try:
|
105 |
+
optimization_result = {
|
106 |
+
"department_recommendations": {},
|
107 |
+
"patient_transfers": [],
|
108 |
+
"capacity_alerts": []
|
109 |
+
}
|
110 |
+
|
111 |
+
for dept in departments:
|
112 |
+
# Calculate department capacity
|
113 |
+
utilization = dept["current_occupancy"] / dept["capacity"]
|
114 |
+
|
115 |
+
if utilization > 0.9:
|
116 |
+
optimization_result["capacity_alerts"].append({
|
117 |
+
"department": dept["name"],
|
118 |
+
"alert": "Critical capacity",
|
119 |
+
"utilization": utilization
|
120 |
+
})
|
121 |
+
|
122 |
+
# Recommend transfers if needed
|
123 |
+
if utilization > 0.85:
|
124 |
+
optimization_result["patient_transfers"].append({
|
125 |
+
"from_dept": dept["name"],
|
126 |
+
"recommended_transfers": max(1, int((utilization - 0.8) * dept["capacity"]))
|
127 |
+
})
|
128 |
+
|
129 |
+
return optimization_result
|
130 |
+
|
131 |
+
except Exception as e:
|
132 |
+
logger.error(f"Error optimizing patient flow: {str(e)}")
|
133 |
+
raise
|
134 |
+
|
135 |
+
@tool
|
136 |
+
def assess_admission_priority(
|
137 |
+
self,
|
138 |
+
patient_condition: str,
|
139 |
+
wait_time: float,
|
140 |
+
department_load: float
|
141 |
+
) -> Dict:
|
142 |
+
"""Assess admission priority based on multiple factors"""
|
143 |
+
try:
|
144 |
+
# Base priority scores
|
145 |
+
CONDITION_SCORES = {
|
146 |
+
"critical": 10,
|
147 |
+
"urgent": 8,
|
148 |
+
"moderate": 5,
|
149 |
+
"routine": 3
|
150 |
+
}
|
151 |
+
|
152 |
+
# Calculate priority score
|
153 |
+
base_score = CONDITION_SCORES.get(patient_condition.lower(), 3)
|
154 |
+
wait_factor = min(wait_time / 30, 2) # Cap wait time factor at 2
|
155 |
+
load_penalty = department_load if department_load > 0.8 else 0
|
156 |
+
|
157 |
+
final_score = base_score + wait_factor - load_penalty
|
158 |
+
|
159 |
+
return {
|
160 |
+
"priority_score": round(final_score, 2),
|
161 |
+
"priority_level": "High" if final_score > 7 else "Medium" if final_score > 4 else "Low",
|
162 |
+
"factors": {
|
163 |
+
"condition_score": base_score,
|
164 |
+
"wait_factor": round(wait_factor, 2),
|
165 |
+
"load_penalty": round(load_penalty, 2)
|
166 |
+
}
|
167 |
+
}
|
168 |
+
|
169 |
+
except Exception as e:
|
170 |
+
logger.error(f"Error assessing admission priority: {str(e)}")
|
171 |
+
raise# patient_tools implementation
|
src/tools/quality_tools.py
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/tools/quality_tools.py
|
2 |
+
from typing import Dict, List, Optional, Any
|
3 |
+
from typing_extensions import TypedDict # If using TypedDict
|
4 |
+
from langchain_core.tools import tool
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
from ..utils.logger import setup_logger
|
7 |
+
|
8 |
+
logger = setup_logger(__name__)
|
9 |
+
|
10 |
+
class QualityTools:
|
11 |
+
@tool
|
12 |
+
def analyze_patient_satisfaction(
|
13 |
+
self,
|
14 |
+
satisfaction_scores: List[float],
|
15 |
+
feedback_comments: List[str],
|
16 |
+
department: Optional[str] = None
|
17 |
+
) -> Dict:
|
18 |
+
"""Analyze patient satisfaction scores and feedback"""
|
19 |
+
try:
|
20 |
+
analysis = {
|
21 |
+
"metrics": {
|
22 |
+
"average_score": sum(satisfaction_scores) / len(satisfaction_scores),
|
23 |
+
"total_responses": len(satisfaction_scores),
|
24 |
+
"score_distribution": {},
|
25 |
+
"trend": "stable"
|
26 |
+
},
|
27 |
+
"feedback_analysis": {
|
28 |
+
"positive_themes": [],
|
29 |
+
"negative_themes": [],
|
30 |
+
"improvement_areas": []
|
31 |
+
},
|
32 |
+
"recommendations": []
|
33 |
+
}
|
34 |
+
|
35 |
+
# Analyze score distribution
|
36 |
+
for score in satisfaction_scores:
|
37 |
+
category = int(score)
|
38 |
+
analysis["metrics"]["score_distribution"][category] = \
|
39 |
+
analysis["metrics"]["score_distribution"].get(category, 0) + 1
|
40 |
+
|
41 |
+
# Basic sentiment analysis of feedback
|
42 |
+
positive_keywords = ["great", "excellent", "good", "satisfied", "helpful"]
|
43 |
+
negative_keywords = ["poor", "bad", "slow", "unhappy", "dissatisfied"]
|
44 |
+
|
45 |
+
for comment in feedback_comments:
|
46 |
+
comment_lower = comment.lower()
|
47 |
+
|
48 |
+
# Analyze positive feedback
|
49 |
+
for keyword in positive_keywords:
|
50 |
+
if keyword in comment_lower:
|
51 |
+
analysis["feedback_analysis"]["positive_themes"].append(keyword)
|
52 |
+
|
53 |
+
# Analyze negative feedback
|
54 |
+
for keyword in negative_keywords:
|
55 |
+
if keyword in comment_lower:
|
56 |
+
analysis["feedback_analysis"]["negative_themes"].append(keyword)
|
57 |
+
|
58 |
+
# Generate recommendations
|
59 |
+
if analysis["metrics"]["average_score"] < 7.0:
|
60 |
+
analysis["recommendations"].append("Implement immediate satisfaction improvement plan")
|
61 |
+
|
62 |
+
return analysis
|
63 |
+
|
64 |
+
except Exception as e:
|
65 |
+
logger.error(f"Error analyzing patient satisfaction: {str(e)}")
|
66 |
+
raise
|
67 |
+
|
68 |
+
@tool
|
69 |
+
def monitor_clinical_outcomes(
|
70 |
+
self,
|
71 |
+
outcomes_data: List[Dict],
|
72 |
+
benchmark_metrics: Dict[str, float]
|
73 |
+
) -> Dict:
|
74 |
+
"""Monitor and analyze clinical outcomes against benchmarks"""
|
75 |
+
try:
|
76 |
+
analysis = {
|
77 |
+
"outcome_metrics": {},
|
78 |
+
"benchmark_comparison": {},
|
79 |
+
"critical_deviations": [],
|
80 |
+
"success_areas": []
|
81 |
+
}
|
82 |
+
|
83 |
+
# Analyze outcomes by category
|
84 |
+
for outcome in outcomes_data:
|
85 |
+
category = outcome["category"]
|
86 |
+
if category not in analysis["outcome_metrics"]:
|
87 |
+
analysis["outcome_metrics"][category] = {
|
88 |
+
"success_rate": 0,
|
89 |
+
"complication_rate": 0,
|
90 |
+
"readmission_rate": 0,
|
91 |
+
"total_cases": 0
|
92 |
+
}
|
93 |
+
|
94 |
+
# Update metrics
|
95 |
+
metrics = analysis["outcome_metrics"][category]
|
96 |
+
metrics["total_cases"] += 1
|
97 |
+
metrics["success_rate"] = (metrics["success_rate"] * (metrics["total_cases"] - 1) +
|
98 |
+
outcome["success"]) / metrics["total_cases"]
|
99 |
+
|
100 |
+
# Compare with benchmarks
|
101 |
+
if category in benchmark_metrics:
|
102 |
+
benchmark = benchmark_metrics[category]
|
103 |
+
deviation = metrics["success_rate"] - benchmark
|
104 |
+
|
105 |
+
if deviation < -0.1: # More than 10% below benchmark
|
106 |
+
analysis["critical_deviations"].append({
|
107 |
+
"category": category,
|
108 |
+
"deviation": deviation,
|
109 |
+
"current_rate": metrics["success_rate"],
|
110 |
+
"benchmark": benchmark
|
111 |
+
})
|
112 |
+
elif deviation > 0.05: # More than 5% above benchmark
|
113 |
+
analysis["success_areas"].append({
|
114 |
+
"category": category,
|
115 |
+
"improvement": deviation,
|
116 |
+
"current_rate": metrics["success_rate"]
|
117 |
+
})
|
118 |
+
|
119 |
+
return analysis
|
120 |
+
|
121 |
+
except Exception as e:
|
122 |
+
logger.error(f"Error monitoring clinical outcomes: {str(e)}")
|
123 |
+
raise
|
124 |
+
|
125 |
+
@tool
|
126 |
+
def track_compliance_metrics(
|
127 |
+
self,
|
128 |
+
compliance_data: List[Dict],
|
129 |
+
audit_period: str
|
130 |
+
) -> Dict:
|
131 |
+
"""Track and analyze compliance with medical standards and regulations"""
|
132 |
+
try:
|
133 |
+
analysis = {
|
134 |
+
"compliance_rate": 0,
|
135 |
+
"violations": [],
|
136 |
+
"risk_areas": [],
|
137 |
+
"audit_summary": {
|
138 |
+
"period": audit_period,
|
139 |
+
"total_checks": len(compliance_data),
|
140 |
+
"passed_checks": 0,
|
141 |
+
"failed_checks": 0
|
142 |
+
}
|
143 |
+
}
|
144 |
+
|
145 |
+
# Analyze compliance checks
|
146 |
+
for check in compliance_data:
|
147 |
+
if check["compliant"]:
|
148 |
+
analysis["audit_summary"]["passed_checks"] += 1
|
149 |
+
else:
|
150 |
+
analysis["audit_summary"]["failed_checks"] += 1
|
151 |
+
analysis["violations"].append({
|
152 |
+
"standard": check["standard"],
|
153 |
+
"severity": check["severity"],
|
154 |
+
"date": check["date"]
|
155 |
+
})
|
156 |
+
|
157 |
+
# Identify risk areas
|
158 |
+
if check["severity"] == "high" or check.get("repeat_violation", False):
|
159 |
+
analysis["risk_areas"].append({
|
160 |
+
"area": check["standard"],
|
161 |
+
"risk_level": "high",
|
162 |
+
"recommendations": ["Immediate action required",
|
163 |
+
"Staff training needed"]
|
164 |
+
})
|
165 |
+
|
166 |
+
# Calculate overall compliance rate
|
167 |
+
total_checks = analysis["audit_summary"]["total_checks"]
|
168 |
+
if total_checks > 0:
|
169 |
+
analysis["compliance_rate"] = (analysis["audit_summary"]["passed_checks"] /
|
170 |
+
total_checks * 100)
|
171 |
+
|
172 |
+
return analysis
|
173 |
+
|
174 |
+
except Exception as e:
|
175 |
+
logger.error(f"Error tracking compliance metrics: {str(e)}")
|
176 |
+
raise# quality_tools implementation
|
src/tools/resource_tools.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/tools/resource_tools.py
|
2 |
+
#from typing import Dict, List
|
3 |
+
from typing import Dict, List, Optional, Any
|
4 |
+
from typing_extensions import TypedDict # If using TypedDict
|
5 |
+
from langchain_core.tools import tool
|
6 |
+
from ..utils.logger import setup_logger
|
7 |
+
|
8 |
+
logger = setup_logger(__name__)
|
9 |
+
|
10 |
+
class ResourceTools:
|
11 |
+
@tool
|
12 |
+
def analyze_supply_levels(
|
13 |
+
self,
|
14 |
+
current_inventory: Dict[str, float],
|
15 |
+
consumption_rate: Dict[str, float],
|
16 |
+
reorder_thresholds: Dict[str, float]
|
17 |
+
) -> Dict:
|
18 |
+
"""Analyze supply levels and generate reorder recommendations"""
|
19 |
+
try:
|
20 |
+
analysis = {
|
21 |
+
"critical_items": [],
|
22 |
+
"reorder_needed": [],
|
23 |
+
"adequate_supplies": [],
|
24 |
+
"recommendations": []
|
25 |
+
}
|
26 |
+
|
27 |
+
for item, level in current_inventory.items():
|
28 |
+
threshold = reorder_thresholds.get(item, 0.2)
|
29 |
+
consumption = consumption_rate.get(item, 0)
|
30 |
+
|
31 |
+
# Days of supply remaining
|
32 |
+
days_remaining = level / consumption if consumption > 0 else float('inf')
|
33 |
+
|
34 |
+
if level <= threshold:
|
35 |
+
if days_remaining < 2:
|
36 |
+
analysis["critical_items"].append({
|
37 |
+
"item": item,
|
38 |
+
"current_level": level,
|
39 |
+
"days_remaining": days_remaining
|
40 |
+
})
|
41 |
+
else:
|
42 |
+
analysis["reorder_needed"].append({
|
43 |
+
"item": item,
|
44 |
+
"current_level": level,
|
45 |
+
"days_remaining": days_remaining
|
46 |
+
})
|
47 |
+
else:
|
48 |
+
analysis["adequate_supplies"].append(item)
|
49 |
+
|
50 |
+
return analysis
|
51 |
+
|
52 |
+
except Exception as e:
|
53 |
+
logger.error(f"Error analyzing supply levels: {str(e)}")
|
54 |
+
raise
|
55 |
+
|
56 |
+
@tool
|
57 |
+
def track_equipment_utilization(
|
58 |
+
self,
|
59 |
+
equipment_logs: List[Dict],
|
60 |
+
equipment_capacity: Dict[str, int]
|
61 |
+
) -> Dict:
|
62 |
+
"""Track and analyze equipment utilization rates"""
|
63 |
+
try:
|
64 |
+
utilization = {
|
65 |
+
"equipment_stats": {},
|
66 |
+
"underutilized": [],
|
67 |
+
"optimal": [],
|
68 |
+
"overutilized": []
|
69 |
+
}
|
70 |
+
|
71 |
+
for equip, capacity in equipment_capacity.items():
|
72 |
+
usage = len([log for log in equipment_logs if log["equipment"] == equip])
|
73 |
+
utilization_rate = usage / capacity
|
74 |
+
|
75 |
+
utilization["equipment_stats"][equip] = {
|
76 |
+
"usage": usage,
|
77 |
+
"capacity": capacity,
|
78 |
+
"utilization_rate": utilization_rate
|
79 |
+
}
|
80 |
+
|
81 |
+
if utilization_rate < 0.3:
|
82 |
+
utilization["underutilized"].append(equip)
|
83 |
+
elif utilization_rate > 0.8:
|
84 |
+
utilization["overutilized"].append(equip)
|
85 |
+
else:
|
86 |
+
utilization["optimal"].append(equip)
|
87 |
+
|
88 |
+
return utilization
|
89 |
+
|
90 |
+
except Exception as e:
|
91 |
+
logger.error(f"Error tracking equipment utilization: {str(e)}")
|
92 |
+
raise
|
93 |
+
|
94 |
+
@tool
|
95 |
+
def optimize_resource_allocation(
|
96 |
+
self,
|
97 |
+
department_demands: Dict[str, Dict],
|
98 |
+
available_resources: Dict[str, int]
|
99 |
+
) -> Dict:
|
100 |
+
"""Optimize resource allocation across departments"""
|
101 |
+
try:
|
102 |
+
allocation = {
|
103 |
+
"recommended_distribution": {},
|
104 |
+
"unmet_demands": [],
|
105 |
+
"resource_sharing": []
|
106 |
+
}
|
107 |
+
|
108 |
+
total_demand = sum(dept["demand"] for dept in department_demands.values())
|
109 |
+
|
110 |
+
for dept, demand in department_demands.items():
|
111 |
+
# Calculate fair share based on demand
|
112 |
+
for resource, available in available_resources.items():
|
113 |
+
dept_share = int((demand["demand"] / total_demand) * available)
|
114 |
+
|
115 |
+
allocation["recommended_distribution"][dept] = {
|
116 |
+
resource: dept_share
|
117 |
+
}
|
118 |
+
|
119 |
+
if dept_share < demand.get("minimum", 0):
|
120 |
+
allocation["unmet_demands"].append({
|
121 |
+
"department": dept,
|
122 |
+
"resource": resource,
|
123 |
+
"shortfall": demand["minimum"] - dept_share
|
124 |
+
})
|
125 |
+
|
126 |
+
return allocation
|
127 |
+
|
128 |
+
except Exception as e:
|
129 |
+
logger.error(f"Error optimizing resource allocation: {str(e)}")
|
130 |
+
raise# resource_tools implementation
|
src/tools/scheduling_tools.py
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/tools/scheduling_tools.py
|
2 |
+
#from typing import Dict, List, Optional
|
3 |
+
from typing import Dict, List, Optional, Any
|
4 |
+
from typing_extensions import TypedDict # If using TypedDict
|
5 |
+
from langchain_core.tools import tool
|
6 |
+
from datetime import datetime, timedelta
|
7 |
+
from ..utils.logger import setup_logger
|
8 |
+
|
9 |
+
logger = setup_logger(__name__)
|
10 |
+
|
11 |
+
class SchedulingTools:
|
12 |
+
@tool
|
13 |
+
def optimize_staff_schedule(
|
14 |
+
self,
|
15 |
+
staff_availability: List[Dict],
|
16 |
+
department_needs: Dict[str, Dict],
|
17 |
+
shift_preferences: Optional[List[Dict]] = None
|
18 |
+
) -> Dict:
|
19 |
+
"""Generate optimized staff schedules based on availability and department needs"""
|
20 |
+
try:
|
21 |
+
schedule = {
|
22 |
+
"shifts": {},
|
23 |
+
"coverage_gaps": [],
|
24 |
+
"recommendations": [],
|
25 |
+
"staff_assignments": {}
|
26 |
+
}
|
27 |
+
|
28 |
+
# Process each department's needs
|
29 |
+
for dept, needs in department_needs.items():
|
30 |
+
schedule["shifts"][dept] = {
|
31 |
+
"morning": [],
|
32 |
+
"afternoon": [],
|
33 |
+
"night": []
|
34 |
+
}
|
35 |
+
|
36 |
+
required_staff = needs.get("required_staff", {})
|
37 |
+
|
38 |
+
# Match available staff to shifts
|
39 |
+
for staff in staff_availability:
|
40 |
+
if staff["department"] == dept and staff["available"]:
|
41 |
+
preferred_shift = "morning" # Default
|
42 |
+
if shift_preferences:
|
43 |
+
for pref in shift_preferences:
|
44 |
+
if pref["staff_id"] == staff["id"]:
|
45 |
+
preferred_shift = pref["preferred_shift"]
|
46 |
+
|
47 |
+
schedule["shifts"][dept][preferred_shift].append(staff["id"])
|
48 |
+
|
49 |
+
# Identify coverage gaps
|
50 |
+
for shift in ["morning", "afternoon", "night"]:
|
51 |
+
required = required_staff.get(shift, 0)
|
52 |
+
assigned = len(schedule["shifts"][dept][shift])
|
53 |
+
|
54 |
+
if assigned < required:
|
55 |
+
schedule["coverage_gaps"].append({
|
56 |
+
"department": dept,
|
57 |
+
"shift": shift,
|
58 |
+
"shortage": required - assigned
|
59 |
+
})
|
60 |
+
|
61 |
+
return schedule
|
62 |
+
|
63 |
+
except Exception as e:
|
64 |
+
logger.error(f"Error optimizing staff schedule: {str(e)}")
|
65 |
+
raise
|
66 |
+
|
67 |
+
@tool
|
68 |
+
def analyze_workforce_metrics(
|
69 |
+
self,
|
70 |
+
staff_data: List[Dict],
|
71 |
+
time_period: str
|
72 |
+
) -> Dict:
|
73 |
+
"""Analyze workforce metrics including overtime, satisfaction, and skill mix"""
|
74 |
+
try:
|
75 |
+
analysis = {
|
76 |
+
"workforce_metrics": {
|
77 |
+
"total_staff": len(staff_data),
|
78 |
+
"overtime_hours": 0,
|
79 |
+
"skill_distribution": {},
|
80 |
+
"satisfaction_score": 0,
|
81 |
+
"turnover_rate": 0
|
82 |
+
},
|
83 |
+
"recommendations": []
|
84 |
+
}
|
85 |
+
|
86 |
+
total_satisfaction = 0
|
87 |
+
total_overtime = 0
|
88 |
+
|
89 |
+
for staff in staff_data:
|
90 |
+
# Analyze overtime
|
91 |
+
total_overtime += staff.get("overtime_hours", 0)
|
92 |
+
|
93 |
+
# Track skill distribution
|
94 |
+
role = staff.get("role", "unknown")
|
95 |
+
analysis["workforce_metrics"]["skill_distribution"][role] = \
|
96 |
+
analysis["workforce_metrics"]["skill_distribution"].get(role, 0) + 1
|
97 |
+
|
98 |
+
# Track satisfaction
|
99 |
+
total_satisfaction += staff.get("satisfaction_score", 0)
|
100 |
+
|
101 |
+
# Calculate averages
|
102 |
+
if staff_data:
|
103 |
+
analysis["workforce_metrics"]["overtime_hours"] = total_overtime / len(staff_data)
|
104 |
+
analysis["workforce_metrics"]["satisfaction_score"] = \
|
105 |
+
total_satisfaction / len(staff_data)
|
106 |
+
|
107 |
+
# Generate recommendations
|
108 |
+
if analysis["workforce_metrics"]["overtime_hours"] > 10:
|
109 |
+
analysis["recommendations"].append("Reduce overtime hours through better scheduling")
|
110 |
+
|
111 |
+
if analysis["workforce_metrics"]["satisfaction_score"] < 7:
|
112 |
+
analysis["recommendations"].append("Implement staff satisfaction improvement measures")
|
113 |
+
|
114 |
+
return analysis
|
115 |
+
|
116 |
+
except Exception as e:
|
117 |
+
logger.error(f"Error analyzing workforce metrics: {str(e)}")
|
118 |
+
raise
|
119 |
+
|
120 |
+
@tool
|
121 |
+
def calculate_staffing_needs(
|
122 |
+
self,
|
123 |
+
patient_census: Dict[str, int],
|
124 |
+
acuity_levels: Dict[str, float],
|
125 |
+
staff_ratios: Dict[str, float]
|
126 |
+
) -> Dict:
|
127 |
+
"""Calculate staffing needs based on patient census and acuity"""
|
128 |
+
try:
|
129 |
+
staffing_needs = {
|
130 |
+
"required_staff": {},
|
131 |
+
"current_gaps": {},
|
132 |
+
"recommendations": []
|
133 |
+
}
|
134 |
+
|
135 |
+
for department, census in patient_census.items():
|
136 |
+
# Calculate base staffing need
|
137 |
+
acuity = acuity_levels.get(department, 1.0)
|
138 |
+
ratio = staff_ratios.get(department, 4) # default 1:4 ratio
|
139 |
+
|
140 |
+
required_staff = ceil(census * acuity / ratio)
|
141 |
+
|
142 |
+
staffing_needs["required_staff"][department] = {
|
143 |
+
"total_needed": required_staff,
|
144 |
+
"acuity_factor": acuity,
|
145 |
+
"patient_ratio": ratio
|
146 |
+
}
|
147 |
+
|
148 |
+
# Generate staffing recommendations
|
149 |
+
if required_staff > current_staff.get(department, 0):
|
150 |
+
staffing_needs["recommendations"].append({
|
151 |
+
"department": department,
|
152 |
+
"action": "increase_staff",
|
153 |
+
"amount": required_staff - current_staff.get(department, 0)
|
154 |
+
})
|
155 |
+
|
156 |
+
return staffing_needs
|
157 |
+
|
158 |
+
except Exception as e:
|
159 |
+
logger.error(f"Error calculating staffing needs: {str(e)}")
|
160 |
+
raise# scheduling_tools implementation
|
src/ui/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from .app import HealthcareUI
|
2 |
+
|
3 |
+
__all__ = ['HealthcareUI']
|
src/ui/app.py
ADDED
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from datetime import datetime
|
3 |
+
from typing import Optional, Dict, Any
|
4 |
+
import os
|
5 |
+
|
6 |
+
from ..agent import HealthcareAgent
|
7 |
+
from ..models.state import TaskType, PriorityLevel
|
8 |
+
from ..utils.logger import setup_logger
|
9 |
+
|
10 |
+
logger = setup_logger(__name__)
|
11 |
+
|
12 |
+
class HealthcareUI:
|
13 |
+
def __init__(self):
|
14 |
+
"""Initialize the Healthcare Operations Management UI"""
|
15 |
+
try:
|
16 |
+
# Set up Streamlit page configuration
|
17 |
+
st.set_page_config(
|
18 |
+
page_title="Healthcare Operations Assistant",
|
19 |
+
page_icon="🏥",
|
20 |
+
layout="wide",
|
21 |
+
initial_sidebar_state="expanded",
|
22 |
+
menu_items={
|
23 |
+
'About': "Healthcare Operations Management AI Assistant",
|
24 |
+
'Report a bug': "https://github.com/yourusername/repo/issues",
|
25 |
+
'Get Help': "https://your-docs-url"
|
26 |
+
}
|
27 |
+
)
|
28 |
+
|
29 |
+
# Apply custom theme
|
30 |
+
self.setup_theme()
|
31 |
+
|
32 |
+
# Initialize the agent
|
33 |
+
self.agent = HealthcareAgent(os.getenv("OPENAI_API_KEY"))
|
34 |
+
|
35 |
+
# Initialize session state variables only if not already set
|
36 |
+
if 'initialized' not in st.session_state:
|
37 |
+
st.session_state.initialized = True
|
38 |
+
st.session_state.messages = []
|
39 |
+
st.session_state.thread_id = datetime.now().strftime("%Y%m%d-%H%M%S")
|
40 |
+
st.session_state.current_department = "All Departments"
|
41 |
+
st.session_state.metrics_history = []
|
42 |
+
st.session_state.system_status = True
|
43 |
+
|
44 |
+
except Exception as e:
|
45 |
+
logger.error(f"Error initializing UI: {str(e)}")
|
46 |
+
st.error("Failed to initialize the application. Please refresh the page.")
|
47 |
+
|
48 |
+
def setup_theme(self):
|
49 |
+
"""Configure the UI theme and styling"""
|
50 |
+
st.markdown("""
|
51 |
+
<style>
|
52 |
+
/* Main background */
|
53 |
+
.stApp {
|
54 |
+
background-color: #f0f8ff;
|
55 |
+
}
|
56 |
+
|
57 |
+
/* Headers */
|
58 |
+
h1, h2, h3 {
|
59 |
+
color: #2c3e50;
|
60 |
+
}
|
61 |
+
|
62 |
+
/* Chat messages */
|
63 |
+
.user-message {
|
64 |
+
background-color: #e3f2fd;
|
65 |
+
padding: 1rem;
|
66 |
+
border-radius: 10px;
|
67 |
+
margin: 1rem 0;
|
68 |
+
border-left: 5px solid #1976d2;
|
69 |
+
}
|
70 |
+
|
71 |
+
.assistant-message {
|
72 |
+
background-color: #fff;
|
73 |
+
padding: 1rem;
|
74 |
+
border-radius: 10px;
|
75 |
+
margin: 1rem 0;
|
76 |
+
border-left: 5px solid #4caf50;
|
77 |
+
}
|
78 |
+
|
79 |
+
/* Metrics cards */
|
80 |
+
.metric-card {
|
81 |
+
background-color: white;
|
82 |
+
padding: 1rem;
|
83 |
+
border-radius: 10px;
|
84 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
85 |
+
transition: transform 0.2s ease;
|
86 |
+
}
|
87 |
+
|
88 |
+
.metric-card:hover {
|
89 |
+
transform: translateY(-2px);
|
90 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
91 |
+
}
|
92 |
+
|
93 |
+
/* Custom button styling */
|
94 |
+
.stButton>button {
|
95 |
+
background-color: #2196f3;
|
96 |
+
color: white;
|
97 |
+
border-radius: 20px;
|
98 |
+
padding: 0.5rem 2rem;
|
99 |
+
transition: all 0.3s ease;
|
100 |
+
}
|
101 |
+
|
102 |
+
.stButton>button:hover {
|
103 |
+
background-color: #1976d2;
|
104 |
+
transform: translateY(-1px);
|
105 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
106 |
+
}
|
107 |
+
</style>
|
108 |
+
""", unsafe_allow_html=True)
|
109 |
+
|
110 |
+
def render_header(self):
|
111 |
+
"""Render the application header"""
|
112 |
+
try:
|
113 |
+
header_container = st.container()
|
114 |
+
with header_container:
|
115 |
+
col1, col2, col3 = st.columns([1, 4, 1])
|
116 |
+
|
117 |
+
with col1:
|
118 |
+
st.markdown("# 🏥")
|
119 |
+
|
120 |
+
with col2:
|
121 |
+
st.title("Healthcare Operations Assistant")
|
122 |
+
st.markdown("*Your AI-powered healthcare operations management solution* 🤖")
|
123 |
+
|
124 |
+
with col3:
|
125 |
+
# System status indicator
|
126 |
+
status = "🟢 Online" if st.session_state.system_status else "🔴 Offline"
|
127 |
+
st.markdown(f"### {status}")
|
128 |
+
|
129 |
+
except Exception as e:
|
130 |
+
logger.error(f"Error rendering header: {str(e)}")
|
131 |
+
st.error("Error loading header section")
|
132 |
+
|
133 |
+
def render_metrics(self, metrics: Optional[Dict[str, Any]] = None):
|
134 |
+
"""Render the metrics dashboard"""
|
135 |
+
try:
|
136 |
+
if not metrics:
|
137 |
+
metrics = {
|
138 |
+
"patient_flow": {"occupied_beds": 75, "total_beds": 100},
|
139 |
+
"quality": {"patient_satisfaction": 8.5},
|
140 |
+
"staffing": {"available_staff": {"doctors": 20, "nurses": 50}},
|
141 |
+
"resources": {"resource_utilization": 0.75}
|
142 |
+
}
|
143 |
+
|
144 |
+
st.markdown("### 📊 Key Metrics Dashboard")
|
145 |
+
metrics_container = st.container()
|
146 |
+
|
147 |
+
with metrics_container:
|
148 |
+
# First row - Key metrics
|
149 |
+
col1, col2, col3, col4 = st.columns(4)
|
150 |
+
|
151 |
+
with col1:
|
152 |
+
occupancy = (metrics['patient_flow']['occupied_beds'] /
|
153 |
+
metrics['patient_flow']['total_beds'] * 100)
|
154 |
+
st.metric(
|
155 |
+
"Bed Occupancy 🛏️",
|
156 |
+
f"{occupancy:.1f}%",
|
157 |
+
"Normal 🟢" if occupancy < 85 else "High 🟡"
|
158 |
+
)
|
159 |
+
|
160 |
+
with col2:
|
161 |
+
satisfaction = metrics['quality']['patient_satisfaction']
|
162 |
+
st.metric(
|
163 |
+
"Patient Satisfaction 😊",
|
164 |
+
f"{satisfaction}/10",
|
165 |
+
"↗ +0.5" if satisfaction > 8 else "↘ -0.3"
|
166 |
+
)
|
167 |
+
|
168 |
+
with col3:
|
169 |
+
total_staff = sum(metrics['staffing']['available_staff'].values())
|
170 |
+
st.metric(
|
171 |
+
"Available Staff 👥",
|
172 |
+
total_staff,
|
173 |
+
"Optimal 🟢" if total_staff > 80 else "Low 🔴"
|
174 |
+
)
|
175 |
+
|
176 |
+
with col4:
|
177 |
+
utilization = metrics['resources']['resource_utilization'] * 100
|
178 |
+
st.metric(
|
179 |
+
"Resource Utilization 📦",
|
180 |
+
f"{utilization:.1f}%",
|
181 |
+
"↘ -2%"
|
182 |
+
)
|
183 |
+
|
184 |
+
# Add metrics to history
|
185 |
+
st.session_state.metrics_history.append({
|
186 |
+
'timestamp': datetime.now(),
|
187 |
+
'metrics': metrics
|
188 |
+
})
|
189 |
+
|
190 |
+
except Exception as e:
|
191 |
+
logger.error(f"Error rendering metrics: {str(e)}")
|
192 |
+
st.error("Error loading metrics dashboard")
|
193 |
+
|
194 |
+
def render_chat(self):
|
195 |
+
"""Render the chat interface"""
|
196 |
+
try:
|
197 |
+
st.markdown("### 💬 Chat Interface")
|
198 |
+
chat_container = st.container()
|
199 |
+
|
200 |
+
with chat_container:
|
201 |
+
# Display chat messages
|
202 |
+
for message in st.session_state.messages:
|
203 |
+
role = message["role"]
|
204 |
+
content = message["content"]
|
205 |
+
timestamp = message.get("timestamp", datetime.now())
|
206 |
+
|
207 |
+
with st.chat_message(role, avatar="🤖" if role == "assistant" else "👤"):
|
208 |
+
st.markdown(content)
|
209 |
+
st.caption(f":clock2: {timestamp.strftime('%H:%M')}")
|
210 |
+
|
211 |
+
# Chat input
|
212 |
+
if prompt := st.chat_input("How can I assist you with healthcare operations today?"):
|
213 |
+
# Add user message
|
214 |
+
current_time = datetime.now()
|
215 |
+
st.session_state.messages.append({
|
216 |
+
"role": "user",
|
217 |
+
"content": prompt,
|
218 |
+
"timestamp": current_time
|
219 |
+
})
|
220 |
+
|
221 |
+
# Display user message
|
222 |
+
with st.chat_message("user", avatar="👤"):
|
223 |
+
st.markdown(prompt)
|
224 |
+
st.caption(f":clock2: {current_time.strftime('%H:%M')}")
|
225 |
+
|
226 |
+
# Display assistant response
|
227 |
+
with st.chat_message("assistant", avatar="🤖"):
|
228 |
+
with st.spinner("Processing your request... 🔄"):
|
229 |
+
try:
|
230 |
+
# Generate response based on query type
|
231 |
+
response = self._get_department_response(prompt)
|
232 |
+
|
233 |
+
# Display structured response
|
234 |
+
st.markdown("### 🔍 Key Insights")
|
235 |
+
st.markdown(response["insights"])
|
236 |
+
|
237 |
+
st.markdown("### 📋 Actionable Recommendations")
|
238 |
+
st.markdown(response["recommendations"])
|
239 |
+
|
240 |
+
st.markdown("### ⚡ Priority Actions")
|
241 |
+
st.markdown(response["priority_actions"])
|
242 |
+
|
243 |
+
st.markdown("### ⏰ Implementation Timeline")
|
244 |
+
st.markdown(response["timeline"])
|
245 |
+
|
246 |
+
# Update metrics if available
|
247 |
+
if "metrics" in response:
|
248 |
+
self.render_metrics(response["metrics"])
|
249 |
+
|
250 |
+
# Add to chat history
|
251 |
+
st.session_state.messages.append({
|
252 |
+
"role": "assistant",
|
253 |
+
"content": response["full_response"],
|
254 |
+
"timestamp": datetime.now()
|
255 |
+
})
|
256 |
+
|
257 |
+
except Exception as e:
|
258 |
+
st.error(f"Error processing request: {str(e)} ❌")
|
259 |
+
logger.error(f"Error in chat processing: {str(e)}")
|
260 |
+
|
261 |
+
except Exception as e:
|
262 |
+
logger.error(f"Error rendering chat interface: {str(e)}")
|
263 |
+
st.error("Error loading chat interface")
|
264 |
+
|
265 |
+
def _get_department_response(self, query: str) -> Dict[str, Any]:
|
266 |
+
"""Generate response based on query type"""
|
267 |
+
query = query.lower()
|
268 |
+
|
269 |
+
# Waiting times response
|
270 |
+
if "waiting" in query or "wait time" in query:
|
271 |
+
return {
|
272 |
+
"insights": """
|
273 |
+
📊 Current Department Wait Times:
|
274 |
+
- ER: 45 minutes (⚠️ Above target)
|
275 |
+
- ICU: 5 minutes (✅ Within target)
|
276 |
+
- General Ward: 25 minutes (✅ Within target)
|
277 |
+
- Surgery: 30 minutes (⚡ Approaching target)
|
278 |
+
- Pediatrics: 20 minutes (✅ Within target)
|
279 |
+
""",
|
280 |
+
"recommendations": """
|
281 |
+
1. 👥 Deploy additional triage nurses to ER
|
282 |
+
2. 🔄 Optimize patient handoff procedures
|
283 |
+
3. 📱 Implement real-time wait time updates
|
284 |
+
4. 🏥 Activate overflow protocols where needed
|
285 |
+
""",
|
286 |
+
"priority_actions": """
|
287 |
+
Immediate Actions Required:
|
288 |
+
- 🚨 Redirect non-emergency cases from ER
|
289 |
+
- 👨⚕️ Increase ER staffing for next 2 hours
|
290 |
+
- 📢 Update waiting patients every 15 minutes
|
291 |
+
""",
|
292 |
+
"timeline": """
|
293 |
+
Implementation Schedule:
|
294 |
+
- 🕐 0-1 hour: Staff reallocation
|
295 |
+
- 🕒 1-2 hours: Process optimization
|
296 |
+
- 🕓 2-4 hours: Situation reassessment
|
297 |
+
- 🕔 4+ hours: Long-term monitoring
|
298 |
+
""",
|
299 |
+
"metrics": {
|
300 |
+
"patient_flow": {
|
301 |
+
"occupied_beds": 85,
|
302 |
+
"total_beds": 100,
|
303 |
+
"waiting_patients": 18,
|
304 |
+
"average_wait_time": 35.0
|
305 |
+
},
|
306 |
+
"quality": {"patient_satisfaction": 7.8},
|
307 |
+
"staffing": {"available_staff": {"doctors": 22, "nurses": 55}},
|
308 |
+
"resources": {"resource_utilization": 0.82}
|
309 |
+
},
|
310 |
+
"full_response": "Based on current data, we're seeing elevated wait times in the ER department. Immediate actions have been recommended to address this situation."
|
311 |
+
}
|
312 |
+
|
313 |
+
# Bed occupancy response
|
314 |
+
elif "bed" in query or "occupancy" in query:
|
315 |
+
return {
|
316 |
+
"insights": """
|
317 |
+
🛏️ Current Bed Occupancy Status:
|
318 |
+
- Overall Occupancy: 85%
|
319 |
+
- Critical Care: 90% (⚠️ Near capacity)
|
320 |
+
- General Wards: 82% (✅ Optimal)
|
321 |
+
- Available Emergency Beds: 5
|
322 |
+
""",
|
323 |
+
"recommendations": """
|
324 |
+
1. 🔄 Review discharge plans
|
325 |
+
2. 🏥 Prepare overflow areas
|
326 |
+
3. 📋 Optimize bed turnover
|
327 |
+
4. 👥 Adjust staff allocation
|
328 |
+
""",
|
329 |
+
"priority_actions": """
|
330 |
+
Critical Actions:
|
331 |
+
- 🚨 Expedite planned discharges
|
332 |
+
- 🏥 Activate surge capacity plan
|
333 |
+
- 📊 Hourly capacity monitoring
|
334 |
+
""",
|
335 |
+
"timeline": """
|
336 |
+
Action Timeline:
|
337 |
+
- 🕐 Immediate: Discharge reviews
|
338 |
+
- 🕑 2 hours: Capacity reassessment
|
339 |
+
- 🕒 4 hours: Staff reallocation
|
340 |
+
- 🕓 8 hours: Full situation review
|
341 |
+
""",
|
342 |
+
"metrics": {
|
343 |
+
"patient_flow": {
|
344 |
+
"occupied_beds": 90,
|
345 |
+
"total_beds": 100,
|
346 |
+
"waiting_patients": 12,
|
347 |
+
"average_wait_time": 30.0
|
348 |
+
},
|
349 |
+
"quality": {"patient_satisfaction": 8.0},
|
350 |
+
"staffing": {"available_staff": {"doctors": 25, "nurses": 58}},
|
351 |
+
"resources": {"resource_utilization": 0.88}
|
352 |
+
},
|
353 |
+
"full_response": "Current bed occupancy is at 85% with critical care areas approaching capacity. Immediate actions are being taken to optimize bed utilization."
|
354 |
+
}
|
355 |
+
|
356 |
+
# Default response for other queries
|
357 |
+
else:
|
358 |
+
return {
|
359 |
+
"insights": """
|
360 |
+
Please specify your request:
|
361 |
+
- 🏥 Department specific information
|
362 |
+
- ⏰ Wait time inquiries
|
363 |
+
- 🛏️ Bed capacity status
|
364 |
+
- 👥 Staffing information
|
365 |
+
- 📊 Resource utilization
|
366 |
+
""",
|
367 |
+
"recommendations": "To better assist you, please provide more specific details about what you'd like to know.",
|
368 |
+
"priority_actions": "No immediate actions required. Awaiting specific inquiry.",
|
369 |
+
"timeline": "Timeline will be generated based on specific requests.",
|
370 |
+
"full_response": "I'm here to help! Please specify what information you need about healthcare operations."
|
371 |
+
}
|
372 |
+
|
373 |
+
def render_sidebar(self):
|
374 |
+
"""Render the sidebar with controls and filters"""
|
375 |
+
try:
|
376 |
+
with st.sidebar:
|
377 |
+
# Add custom CSS for consistent button styling
|
378 |
+
st.markdown("""
|
379 |
+
<style>
|
380 |
+
/* Container for Quick Actions */
|
381 |
+
.quick-actions-container {
|
382 |
+
display: flex;
|
383 |
+
gap: 10px;
|
384 |
+
margin: 10px 0;
|
385 |
+
}
|
386 |
+
|
387 |
+
/* Button styling */
|
388 |
+
.stButton > button {
|
389 |
+
width: 120px !important; /* Fixed width for both buttons */
|
390 |
+
height: 46px !important; /* Fixed height for both buttons */
|
391 |
+
background-color: #2196f3;
|
392 |
+
color: white;
|
393 |
+
border-radius: 20px;
|
394 |
+
border: none;
|
395 |
+
display: flex;
|
396 |
+
align-items: center;
|
397 |
+
justify-content: center;
|
398 |
+
padding: 0 20px;
|
399 |
+
font-size: 0.9rem;
|
400 |
+
transition: all 0.3s ease;
|
401 |
+
margin: 0;
|
402 |
+
}
|
403 |
+
|
404 |
+
.stButton > button:hover {
|
405 |
+
background-color: #1976d2;
|
406 |
+
transform: translateY(-1px);
|
407 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
408 |
+
}
|
409 |
+
|
410 |
+
/* Column container fixes */
|
411 |
+
div[data-testid="column"] {
|
412 |
+
padding: 0 !important;
|
413 |
+
display: flex;
|
414 |
+
justify-content: center;
|
415 |
+
}
|
416 |
+
</style>
|
417 |
+
""", unsafe_allow_html=True)
|
418 |
+
|
419 |
+
st.markdown("### ⚙️ Settings")
|
420 |
+
|
421 |
+
# Department filter
|
422 |
+
if "department_filter" not in st.session_state:
|
423 |
+
st.session_state.department_filter = "All Departments"
|
424 |
+
|
425 |
+
st.selectbox(
|
426 |
+
"Select Department",
|
427 |
+
["All Departments", "ER", "ICU", "General Ward", "Surgery", "Pediatrics"],
|
428 |
+
key="department_filter"
|
429 |
+
)
|
430 |
+
|
431 |
+
# Priority filter
|
432 |
+
if "priority_filter" not in st.session_state:
|
433 |
+
st.session_state.priority_filter = "Medium"
|
434 |
+
|
435 |
+
st.select_slider(
|
436 |
+
"Priority Level",
|
437 |
+
options=["Low", "Medium", "High", "Urgent", "Critical"],
|
438 |
+
key="priority_filter"
|
439 |
+
)
|
440 |
+
|
441 |
+
# Time range
|
442 |
+
if "time_range_filter" not in st.session_state:
|
443 |
+
st.session_state.time_range_filter = 8
|
444 |
+
|
445 |
+
st.slider(
|
446 |
+
"Time Range (hours)",
|
447 |
+
min_value=1,
|
448 |
+
max_value=24,
|
449 |
+
key="time_range_filter"
|
450 |
+
)
|
451 |
+
|
452 |
+
# Quick actions with consistent styling
|
453 |
+
st.markdown("### ⚡ Quick Actions")
|
454 |
+
|
455 |
+
# Create two columns for buttons
|
456 |
+
col1, col2 = st.columns(2)
|
457 |
+
|
458 |
+
with col1:
|
459 |
+
if st.button("📊 Report"):
|
460 |
+
st.info("Generating comprehensive report...")
|
461 |
+
|
462 |
+
with col2:
|
463 |
+
if st.button("🔄 Refresh"):
|
464 |
+
st.success("Data refreshed successfully!")
|
465 |
+
|
466 |
+
# Emergency Mode
|
467 |
+
st.markdown("### 🚨 Emergency Mode")
|
468 |
+
|
469 |
+
if "emergency_mode" not in st.session_state:
|
470 |
+
st.session_state.emergency_mode = False
|
471 |
+
|
472 |
+
st.toggle(
|
473 |
+
"Activate Emergency Protocol",
|
474 |
+
key="emergency_mode",
|
475 |
+
help="Enable emergency mode for critical situations"
|
476 |
+
)
|
477 |
+
|
478 |
+
if st.session_state.emergency_mode:
|
479 |
+
st.warning("Emergency Mode Active!")
|
480 |
+
|
481 |
+
# Help section
|
482 |
+
st.markdown("### ❓ Help")
|
483 |
+
with st.expander("Usage Guide"):
|
484 |
+
st.markdown("""
|
485 |
+
- 💬 Use the chat to ask questions
|
486 |
+
- 📊 Monitor real-time metrics
|
487 |
+
- ⚙️ Adjust filters as needed
|
488 |
+
- 📋 Generate reports for analysis
|
489 |
+
- 🚨 Toggle emergency mode for critical situations
|
490 |
+
""")
|
491 |
+
|
492 |
+
# Footer
|
493 |
+
st.markdown("---")
|
494 |
+
st.caption(
|
495 |
+
f"*Last updated: {datetime.now().strftime('%H:%M:%S')}*"
|
496 |
+
)
|
497 |
+
|
498 |
+
except Exception as e:
|
499 |
+
logger.error(f"Error rendering sidebar: {str(e)}")
|
500 |
+
st.error("Error loading sidebar")
|
501 |
+
|
502 |
+
def run(self):
|
503 |
+
"""Run the Streamlit application"""
|
504 |
+
try:
|
505 |
+
# Main application container
|
506 |
+
main_container = st.container()
|
507 |
+
|
508 |
+
with main_container:
|
509 |
+
# Render components
|
510 |
+
self.render_header()
|
511 |
+
self.render_sidebar()
|
512 |
+
|
513 |
+
# Main content area
|
514 |
+
content_container = st.container()
|
515 |
+
with content_container:
|
516 |
+
self.render_metrics()
|
517 |
+
st.markdown("<br>", unsafe_allow_html=True) # Spacing
|
518 |
+
self.render_chat()
|
519 |
+
|
520 |
+
except Exception as e:
|
521 |
+
logger.error(f"Error running application: {str(e)}")
|
522 |
+
st.error(f"Application error: {str(e)} ❌")
|
src/ui/assets/icons/.gitkeep
ADDED
File without changes
|
src/ui/assets/images/.gitkeep
ADDED
File without changes
|
src/ui/components/__init__.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .chat import ChatComponent
|
2 |
+
from .metrics import MetricsComponent
|
3 |
+
from .sidebar import SidebarComponent
|
4 |
+
from .header import HeaderComponent
|
5 |
+
|
6 |
+
__all__ = [
|
7 |
+
'ChatComponent',
|
8 |
+
'MetricsComponent',
|
9 |
+
'SidebarComponent',
|
10 |
+
'HeaderComponent'
|
11 |
+
]
|
src/ui/components/chat.py
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from typing import Optional, Dict, Callable
|
3 |
+
from datetime import datetime
|
4 |
+
|
5 |
+
class ChatComponent:
|
6 |
+
def __init__(self, process_message_callback: Callable):
|
7 |
+
"""
|
8 |
+
Initialize the chat component
|
9 |
+
|
10 |
+
Args:
|
11 |
+
process_message_callback: Callback function to process messages
|
12 |
+
"""
|
13 |
+
self.process_message = process_message_callback
|
14 |
+
|
15 |
+
# Initialize session state for messages if not exists
|
16 |
+
if 'messages' not in st.session_state:
|
17 |
+
st.session_state.messages = []
|
18 |
+
|
19 |
+
def _display_message(self, role: str, content: str, timestamp: Optional[datetime] = None):
|
20 |
+
"""Display a single chat message"""
|
21 |
+
avatar = "🤖" if role == "assistant" else "👤"
|
22 |
+
with st.chat_message(role, avatar=avatar):
|
23 |
+
st.markdown(content)
|
24 |
+
if timestamp:
|
25 |
+
st.caption(f":clock2: {timestamp.strftime('%H:%M')}")
|
26 |
+
|
27 |
+
def render(self):
|
28 |
+
"""Render the chat interface"""
|
29 |
+
st.markdown("### 💬 Healthcare Operations Chat")
|
30 |
+
|
31 |
+
# Display chat messages
|
32 |
+
for message in st.session_state.messages:
|
33 |
+
self._display_message(
|
34 |
+
role=message["role"],
|
35 |
+
content=message["content"],
|
36 |
+
timestamp=message.get("timestamp")
|
37 |
+
)
|
38 |
+
|
39 |
+
# Chat input
|
40 |
+
if prompt := st.chat_input(
|
41 |
+
"Ask about patient flow, resources, quality metrics, or staff scheduling..."
|
42 |
+
):
|
43 |
+
# Add user message
|
44 |
+
current_time = datetime.now()
|
45 |
+
st.session_state.messages.append({
|
46 |
+
"role": "user",
|
47 |
+
"content": prompt,
|
48 |
+
"timestamp": current_time
|
49 |
+
})
|
50 |
+
|
51 |
+
# Display user message
|
52 |
+
self._display_message("user", prompt, current_time)
|
53 |
+
|
54 |
+
# Process message and get response
|
55 |
+
with st.spinner("Processing your request... 🔄"):
|
56 |
+
try:
|
57 |
+
response = self.process_message(prompt)
|
58 |
+
|
59 |
+
# Add and display assistant response
|
60 |
+
st.session_state.messages.append({
|
61 |
+
"role": "assistant",
|
62 |
+
"content": response["response"],
|
63 |
+
"timestamp": datetime.now()
|
64 |
+
})
|
65 |
+
|
66 |
+
self._display_message(
|
67 |
+
"assistant",
|
68 |
+
response["response"],
|
69 |
+
datetime.now()
|
70 |
+
)
|
71 |
+
|
72 |
+
except Exception as e:
|
73 |
+
st.error(f"Error processing your request: {str(e)} ❌")
|
74 |
+
|
75 |
+
def clear_chat(self):
|
76 |
+
"""Clear the chat history"""
|
77 |
+
st.session_state.messages = []
|
78 |
+
st.success("Chat history cleared! 🧹")
|
src/ui/components/header.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from datetime import datetime
|
3 |
+
|
4 |
+
class HeaderComponent:
|
5 |
+
def __init__(self):
|
6 |
+
"""Initialize the header component"""
|
7 |
+
# Initialize session state for notifications if not exists
|
8 |
+
if 'notifications' not in st.session_state:
|
9 |
+
st.session_state.notifications = []
|
10 |
+
|
11 |
+
def _add_notification(self, message: str, type: str = "info"):
|
12 |
+
"""Add a notification to the session state"""
|
13 |
+
st.session_state.notifications.append({
|
14 |
+
"message": message,
|
15 |
+
"type": type,
|
16 |
+
"timestamp": datetime.now()
|
17 |
+
})
|
18 |
+
|
19 |
+
def render(self):
|
20 |
+
"""Render the header"""
|
21 |
+
# Main header container
|
22 |
+
header_container = st.container()
|
23 |
+
|
24 |
+
with header_container:
|
25 |
+
# Top row with logo and title
|
26 |
+
col1, col2, col3 = st.columns([1, 4, 1])
|
27 |
+
|
28 |
+
with col1:
|
29 |
+
st.markdown("# 🏥")
|
30 |
+
|
31 |
+
with col2:
|
32 |
+
st.title("Healthcare Operations Assistant")
|
33 |
+
st.markdown("""
|
34 |
+
<div style='padding: 0.5rem 0; color: #4a4a4a;'>
|
35 |
+
*AI-Powered Healthcare Management System* 🤖
|
36 |
+
</div>
|
37 |
+
""", unsafe_allow_html=True)
|
38 |
+
|
39 |
+
with col3:
|
40 |
+
# Status indicator
|
41 |
+
status = "🟢 Online" if st.session_state.get('system_status', True) else "🔴 Offline"
|
42 |
+
st.markdown(f"### {status}")
|
43 |
+
|
44 |
+
# Notification area
|
45 |
+
if st.session_state.notifications:
|
46 |
+
with st.expander("📬 Notifications", expanded=True):
|
47 |
+
for notif in st.session_state.notifications[-3:]: # Show last 3
|
48 |
+
if notif["type"] == "info":
|
49 |
+
st.info(notif["message"])
|
50 |
+
elif notif["type"] == "warning":
|
51 |
+
st.warning(notif["message"])
|
52 |
+
elif notif["type"] == "error":
|
53 |
+
st.error(notif["message"])
|
54 |
+
elif notif["type"] == "success":
|
55 |
+
st.success(notif["message"])
|
56 |
+
|
57 |
+
# System status bar
|
58 |
+
status_cols = st.columns(4)
|
59 |
+
with status_cols[0]:
|
60 |
+
st.markdown("**System Status:** Operational ✅")
|
61 |
+
with status_cols[1]:
|
62 |
+
st.markdown("**API Status:** Connected 🔗")
|
63 |
+
with status_cols[2]:
|
64 |
+
st.markdown("**Load:** Normal 📊")
|
65 |
+
with status_cols[3]:
|
66 |
+
st.markdown(f"**Last Update:** {datetime.now().strftime('%H:%M')} 🕒")
|
67 |
+
|
68 |
+
# Divider
|
69 |
+
st.markdown("---")
|
70 |
+
|
71 |
+
def add_notification(self, message: str, type: str = "info"):
|
72 |
+
"""Public method to add notifications"""
|
73 |
+
self._add_notification(message, type)
|
src/ui/components/metrics.py
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from typing import Dict, Any, Optional
|
3 |
+
|
4 |
+
class MetricsComponent:
|
5 |
+
def __init__(self):
|
6 |
+
"""Initialize the metrics component"""
|
7 |
+
self.default_metrics = {
|
8 |
+
"patient_flow": {
|
9 |
+
"occupied_beds": 75,
|
10 |
+
"total_beds": 100,
|
11 |
+
"waiting_time": 15,
|
12 |
+
"discharge_rate": 8
|
13 |
+
},
|
14 |
+
"quality": {
|
15 |
+
"patient_satisfaction": 8.5,
|
16 |
+
"compliance_rate": 0.95,
|
17 |
+
"incident_count": 2
|
18 |
+
},
|
19 |
+
"staffing": {
|
20 |
+
"available_staff": {
|
21 |
+
"doctors": 20,
|
22 |
+
"nurses": 50,
|
23 |
+
"specialists": 15
|
24 |
+
},
|
25 |
+
"shift_coverage": 0.92
|
26 |
+
},
|
27 |
+
"resources": {
|
28 |
+
"resource_utilization": 0.75,
|
29 |
+
"critical_supplies": 3,
|
30 |
+
"equipment_availability": 0.88
|
31 |
+
}
|
32 |
+
}
|
33 |
+
|
34 |
+
def _render_metric_card(
|
35 |
+
self,
|
36 |
+
title: str,
|
37 |
+
value: Any,
|
38 |
+
delta: Optional[str] = None,
|
39 |
+
help_text: Optional[str] = None
|
40 |
+
):
|
41 |
+
"""Render a single metric card"""
|
42 |
+
st.metric(
|
43 |
+
label=title,
|
44 |
+
value=value,
|
45 |
+
delta=delta,
|
46 |
+
help=help_text
|
47 |
+
)
|
48 |
+
|
49 |
+
def render(self, metrics: Optional[Dict[str, Any]] = None):
|
50 |
+
"""
|
51 |
+
Render the metrics dashboard
|
52 |
+
|
53 |
+
Args:
|
54 |
+
metrics: Optional metrics data to display
|
55 |
+
"""
|
56 |
+
metrics = metrics or self.default_metrics
|
57 |
+
|
58 |
+
st.markdown("### 📊 Operational Metrics Dashboard")
|
59 |
+
|
60 |
+
# Create two rows of metrics
|
61 |
+
row1_cols = st.columns(4)
|
62 |
+
row2_cols = st.columns(4)
|
63 |
+
|
64 |
+
# First row - Key metrics
|
65 |
+
with row1_cols[0]:
|
66 |
+
occupancy = (metrics["patient_flow"]["occupied_beds"] /
|
67 |
+
metrics["patient_flow"]["total_beds"] * 100)
|
68 |
+
self._render_metric_card(
|
69 |
+
"Bed Occupancy 🛏️",
|
70 |
+
f"{occupancy:.1f}%",
|
71 |
+
"Normal" if occupancy < 85 else "High",
|
72 |
+
"Current bed occupancy rate across all departments"
|
73 |
+
)
|
74 |
+
|
75 |
+
with row1_cols[1]:
|
76 |
+
satisfaction = metrics["quality"]["patient_satisfaction"]
|
77 |
+
self._render_metric_card(
|
78 |
+
"Patient Satisfaction 😊",
|
79 |
+
f"{satisfaction}/10",
|
80 |
+
"↗ +0.5" if satisfaction > 8 else "↘ -0.3",
|
81 |
+
"Average patient satisfaction score"
|
82 |
+
)
|
83 |
+
|
84 |
+
with row1_cols[2]:
|
85 |
+
total_staff = sum(metrics["staffing"]["available_staff"].values())
|
86 |
+
self._render_metric_card(
|
87 |
+
"Available Staff 👥",
|
88 |
+
total_staff,
|
89 |
+
"Optimal" if total_staff > 80 else "Low",
|
90 |
+
"Total number of available staff across all roles"
|
91 |
+
)
|
92 |
+
|
93 |
+
with row1_cols[3]:
|
94 |
+
utilization = metrics["resources"]["resource_utilization"] * 100
|
95 |
+
self._render_metric_card(
|
96 |
+
"Resource Utilization 📦",
|
97 |
+
f"{utilization:.1f}%",
|
98 |
+
"Efficient" if utilization < 80 else "High",
|
99 |
+
"Current resource utilization rate"
|
100 |
+
)
|
101 |
+
|
102 |
+
# Second row - Additional metrics
|
103 |
+
with row2_cols[0]:
|
104 |
+
self._render_metric_card(
|
105 |
+
"Waiting Time ⏰",
|
106 |
+
f"{metrics['patient_flow']['waiting_time']} min",
|
107 |
+
help_text="Average patient waiting time"
|
108 |
+
)
|
109 |
+
|
110 |
+
with row2_cols[1]:
|
111 |
+
self._render_metric_card(
|
112 |
+
"Compliance Rate ✅",
|
113 |
+
f"{metrics['quality']['compliance_rate']*100:.1f}%",
|
114 |
+
help_text="Current compliance rate with protocols"
|
115 |
+
)
|
116 |
+
|
117 |
+
with row2_cols[2]:
|
118 |
+
self._render_metric_card(
|
119 |
+
"Critical Supplies ⚠️",
|
120 |
+
metrics['resources']['critical_supplies'],
|
121 |
+
"Action needed" if metrics['resources']['critical_supplies'] > 0 else "All stocked",
|
122 |
+
"Number of supplies needing immediate attention"
|
123 |
+
)
|
124 |
+
|
125 |
+
with row2_cols[3]:
|
126 |
+
self._render_metric_card(
|
127 |
+
"Shift Coverage 📅",
|
128 |
+
f"{metrics['staffing']['shift_coverage']*100:.1f}%",
|
129 |
+
help_text="Current shift coverage rate"
|
130 |
+
)
|
131 |
+
|
132 |
+
# Additional visualization if needed
|
133 |
+
with st.expander("📈 Detailed Metrics Analysis"):
|
134 |
+
st.markdown("""
|
135 |
+
### Trend Analysis
|
136 |
+
- 📈 Patient flow is within normal range
|
137 |
+
- 📉 Resource utilization shows optimization opportunities
|
138 |
+
- 📊 Staff distribution is balanced across departments
|
139 |
+
""")
|
src/ui/components/sidebar.py
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from typing import Dict, Any, Callable
|
3 |
+
from datetime import datetime, timedelta
|
4 |
+
|
5 |
+
class SidebarComponent:
|
6 |
+
def __init__(self, on_filter_change: Optional[Callable] = None):
|
7 |
+
"""
|
8 |
+
Initialize the sidebar component
|
9 |
+
|
10 |
+
Args:
|
11 |
+
on_filter_change: Optional callback for filter changes
|
12 |
+
"""
|
13 |
+
self.on_filter_change = on_filter_change
|
14 |
+
|
15 |
+
# Initialize session state for filters if not exists
|
16 |
+
if 'filters' not in st.session_state:
|
17 |
+
st.session_state.filters = {
|
18 |
+
'department': 'All Departments',
|
19 |
+
'priority': 'Medium',
|
20 |
+
'time_range': 8,
|
21 |
+
'view_mode': 'Standard'
|
22 |
+
}
|
23 |
+
|
24 |
+
def render(self):
|
25 |
+
"""Render the sidebar"""
|
26 |
+
with st.sidebar:
|
27 |
+
st.markdown("# ⚙️ Operations Control")
|
28 |
+
|
29 |
+
# Department Selection
|
30 |
+
st.markdown("### 🏥 Department")
|
31 |
+
department = st.selectbox(
|
32 |
+
"Select Department",
|
33 |
+
[
|
34 |
+
"All Departments",
|
35 |
+
"Emergency Room",
|
36 |
+
"ICU",
|
37 |
+
"General Ward",
|
38 |
+
"Surgery",
|
39 |
+
"Pediatrics",
|
40 |
+
"Cardiology"
|
41 |
+
],
|
42 |
+
index=0,
|
43 |
+
help="Filter data by department"
|
44 |
+
)
|
45 |
+
|
46 |
+
# Priority Filter
|
47 |
+
st.markdown("### 🎯 Priority Level")
|
48 |
+
priority = st.select_slider(
|
49 |
+
"Set Priority",
|
50 |
+
options=["Low", "Medium", "High", "Urgent", "Critical"],
|
51 |
+
value=st.session_state.filters['priority'],
|
52 |
+
help="Filter by priority level"
|
53 |
+
)
|
54 |
+
|
55 |
+
# Time Range
|
56 |
+
st.markdown("### 🕒 Time Range")
|
57 |
+
time_range = st.slider(
|
58 |
+
"Select Time Range",
|
59 |
+
min_value=1,
|
60 |
+
max_value=24,
|
61 |
+
value=st.session_state.filters['time_range'],
|
62 |
+
help="Time range for data analysis (hours)"
|
63 |
+
)
|
64 |
+
|
65 |
+
# View Mode
|
66 |
+
st.markdown("### 👁️ View Mode")
|
67 |
+
view_mode = st.radio(
|
68 |
+
"Select View Mode",
|
69 |
+
["Standard", "Detailed", "Compact"],
|
70 |
+
help="Change the display density"
|
71 |
+
)
|
72 |
+
|
73 |
+
# Quick Actions
|
74 |
+
st.markdown("### ⚡ Quick Actions")
|
75 |
+
col1, col2 = st.columns(2)
|
76 |
+
with col1:
|
77 |
+
if st.button("📊 Report", use_container_width=True):
|
78 |
+
st.info("Generating report...")
|
79 |
+
with col2:
|
80 |
+
if st.button("🔄 Refresh", use_container_width=True):
|
81 |
+
st.success("Data refreshed!")
|
82 |
+
|
83 |
+
# Emergency Mode Toggle
|
84 |
+
st.markdown("### 🚨 Emergency Mode")
|
85 |
+
emergency_mode = st.toggle(
|
86 |
+
"Activate Emergency Protocol",
|
87 |
+
help="Enable emergency mode for critical situations"
|
88 |
+
)
|
89 |
+
if emergency_mode:
|
90 |
+
st.warning("Emergency Mode Active!")
|
91 |
+
|
92 |
+
# Help & Documentation
|
93 |
+
with st.expander("❓ Help & Tips"):
|
94 |
+
st.markdown("""
|
95 |
+
### Quick Guide
|
96 |
+
- 🔍 Use filters to focus on specific areas
|
97 |
+
- 📈 Monitor real-time metrics
|
98 |
+
- 🚨 Toggle emergency mode for critical situations
|
99 |
+
- 📊 Generate reports for analysis
|
100 |
+
- 💡 Access quick actions for common tasks
|
101 |
+
""")
|
102 |
+
|
103 |
+
# Update filters in session state
|
104 |
+
st.session_state.filters.update({
|
105 |
+
'department': department,
|
106 |
+
'priority': priority,
|
107 |
+
'time_range': time_range,
|
108 |
+
'view_mode': view_mode,
|
109 |
+
'emergency_mode': emergency_mode
|
110 |
+
})
|
111 |
+
|
112 |
+
# Call filter change callback if provided
|
113 |
+
if self.on_filter_change:
|
114 |
+
self.on_filter_change(st.session_state.filters)
|
115 |
+
|
116 |
+
# Footer
|
117 |
+
st.markdown("---")
|
118 |
+
st.markdown(
|
119 |
+
f"*Last updated: {datetime.now().strftime('%H:%M:%S')}*",
|
120 |
+
help="Last data refresh timestamp"
|
121 |
+
)
|
src/ui/styles/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from .theme import HealthcareTheme
|
2 |
+
|
3 |
+
__all__ = ['HealthcareTheme']
|
src/ui/styles/custom.css
ADDED
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Healthcare Operations Assistant Custom Styles */
|
2 |
+
|
3 |
+
/* Layout and Structure */
|
4 |
+
.container {
|
5 |
+
max-width: 1200px;
|
6 |
+
margin: 0 auto;
|
7 |
+
padding: 1rem;
|
8 |
+
}
|
9 |
+
|
10 |
+
/* Chat Interface */
|
11 |
+
.chat-container {
|
12 |
+
background-color: #ffffff;
|
13 |
+
border-radius: 12px;
|
14 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
15 |
+
padding: 1rem;
|
16 |
+
margin: 1rem 0;
|
17 |
+
}
|
18 |
+
|
19 |
+
.user-message {
|
20 |
+
background-color: #e3f2fd;
|
21 |
+
padding: 1rem;
|
22 |
+
border-radius: 10px;
|
23 |
+
margin: 1rem 0;
|
24 |
+
border-left: 5px solid #1976d2;
|
25 |
+
}
|
26 |
+
|
27 |
+
.assistant-message {
|
28 |
+
background-color: #f5f5f5;
|
29 |
+
padding: 1rem;
|
30 |
+
border-radius: 10px;
|
31 |
+
margin: 1rem 0;
|
32 |
+
border-left: 5px solid #4caf50;
|
33 |
+
}
|
34 |
+
|
35 |
+
.message-timestamp {
|
36 |
+
font-size: 0.8rem;
|
37 |
+
color: #707070;
|
38 |
+
margin-top: 0.25rem;
|
39 |
+
}
|
40 |
+
|
41 |
+
/* Metrics Dashboard */
|
42 |
+
.metric-card {
|
43 |
+
background-color: white;
|
44 |
+
border-radius: 12px;
|
45 |
+
padding: 1.5rem;
|
46 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
47 |
+
transition: transform 0.2s;
|
48 |
+
}
|
49 |
+
|
50 |
+
.metric-card:hover {
|
51 |
+
transform: translateY(-2px);
|
52 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
53 |
+
}
|
54 |
+
|
55 |
+
.metric-title {
|
56 |
+
font-size: 1rem;
|
57 |
+
font-weight: 600;
|
58 |
+
color: #2c3e50;
|
59 |
+
margin-bottom: 0.5rem;
|
60 |
+
}
|
61 |
+
|
62 |
+
.metric-value {
|
63 |
+
font-size: 1.5rem;
|
64 |
+
font-weight: 700;
|
65 |
+
color: #1976d2;
|
66 |
+
}
|
67 |
+
|
68 |
+
.metric-trend {
|
69 |
+
font-size: 0.9rem;
|
70 |
+
color: #4caf50;
|
71 |
+
}
|
72 |
+
|
73 |
+
.metric-trend.negative {
|
74 |
+
color: #f44336;
|
75 |
+
}
|
76 |
+
|
77 |
+
/* Header Styling */
|
78 |
+
.header {
|
79 |
+
background-color: white;
|
80 |
+
padding: 1rem;
|
81 |
+
border-bottom: 1px solid #e0e0e0;
|
82 |
+
margin-bottom: 2rem;
|
83 |
+
}
|
84 |
+
|
85 |
+
.header-title {
|
86 |
+
font-size: 1.8rem;
|
87 |
+
font-weight: 700;
|
88 |
+
color: #2c3e50;
|
89 |
+
}
|
90 |
+
|
91 |
+
.header-subtitle {
|
92 |
+
font-size: 1rem;
|
93 |
+
color: #707070;
|
94 |
+
}
|
95 |
+
|
96 |
+
/* Sidebar Styling */
|
97 |
+
.sidebar {
|
98 |
+
background-color: white;
|
99 |
+
padding: 1.5rem;
|
100 |
+
border-right: 1px solid #e0e0e0;
|
101 |
+
}
|
102 |
+
|
103 |
+
.sidebar-section {
|
104 |
+
margin-bottom: 2rem;
|
105 |
+
}
|
106 |
+
|
107 |
+
.sidebar-title {
|
108 |
+
font-size: 1.1rem;
|
109 |
+
font-weight: 600;
|
110 |
+
color: #2c3e50;
|
111 |
+
margin-bottom: 1rem;
|
112 |
+
}
|
113 |
+
|
114 |
+
/* Status Indicators */
|
115 |
+
.status-indicator {
|
116 |
+
display: inline-flex;
|
117 |
+
align-items: center;
|
118 |
+
padding: 0.25rem 0.75rem;
|
119 |
+
border-radius: 9999px;
|
120 |
+
font-size: 0.875rem;
|
121 |
+
font-weight: 500;
|
122 |
+
}
|
123 |
+
|
124 |
+
.status-normal {
|
125 |
+
background-color: #4caf50;
|
126 |
+
color: white;
|
127 |
+
}
|
128 |
+
|
129 |
+
.status-warning {
|
130 |
+
background-color: #ff9800;
|
131 |
+
color: white;
|
132 |
+
}
|
133 |
+
|
134 |
+
.status-critical {
|
135 |
+
background-color: #f44336;
|
136 |
+
color: white;
|
137 |
+
}
|
138 |
+
|
139 |
+
/* Buttons and Interactive Elements */
|
140 |
+
.action-button {
|
141 |
+
background-color: #2196f3;
|
142 |
+
color: white;
|
143 |
+
border: none;
|
144 |
+
border-radius: 8px;
|
145 |
+
padding: 0.5rem 1rem;
|
146 |
+
font-weight: 500;
|
147 |
+
cursor: pointer;
|
148 |
+
transition: background-color 0.2s;
|
149 |
+
}
|
150 |
+
|
151 |
+
.action-button:hover {
|
152 |
+
background-color: #1976d2;
|
153 |
+
}
|
154 |
+
|
155 |
+
.action-button.secondary {
|
156 |
+
background-color: #f5f5f5;
|
157 |
+
color: #2c3e50;
|
158 |
+
border: 1px solid #e0e0e0;
|
159 |
+
}
|
160 |
+
|
161 |
+
.action-button.secondary:hover {
|
162 |
+
background-color: #e0e0e0;
|
163 |
+
}
|
164 |
+
|
165 |
+
/* Notifications */
|
166 |
+
.notification {
|
167 |
+
padding: 0.75rem 1rem;
|
168 |
+
border-radius: 8px;
|
169 |
+
margin-bottom: 1rem;
|
170 |
+
}
|
171 |
+
|
172 |
+
.notification.info {
|
173 |
+
background-color: #e3f2fd;
|
174 |
+
border-left: 4px solid #2196f3;
|
175 |
+
}
|
176 |
+
|
177 |
+
.notification.success {
|
178 |
+
background-color: #e8f5e9;
|
179 |
+
border-left: 4px solid #4caf50;
|
180 |
+
}
|
181 |
+
|
182 |
+
.notification.warning {
|
183 |
+
background-color: #fff3e0;
|
184 |
+
border-left: 4px solid #ff9800;
|
185 |
+
}
|
186 |
+
|
187 |
+
.notification.error {
|
188 |
+
background-color: #ffebee;
|
189 |
+
border-left: 4px solid #f44336;
|
190 |
+
}
|
191 |
+
|
192 |
+
/* Responsive Design */
|
193 |
+
@media (max-width: 768px) {
|
194 |
+
.metric-card {
|
195 |
+
margin-bottom: 1rem;
|
196 |
+
}
|
197 |
+
|
198 |
+
.header-title {
|
199 |
+
font-size: 1.5rem;
|
200 |
+
}
|
201 |
+
|
202 |
+
.sidebar {
|
203 |
+
padding: 1rem;
|
204 |
+
}
|
205 |
+
}
|
206 |
+
|
207 |
+
/* Animations */
|
208 |
+
@keyframes fadeIn {
|
209 |
+
from { opacity: 0; }
|
210 |
+
to { opacity: 1; }
|
211 |
+
}
|
212 |
+
|
213 |
+
.fade-in {
|
214 |
+
animation: fadeIn 0.3s ease-in;
|
215 |
+
}
|
216 |
+
|
217 |
+
@keyframes slideIn {
|
218 |
+
from { transform: translateY(20px); opacity: 0; }
|
219 |
+
to { transform: translateY(0); opacity: 1; }
|
220 |
+
}
|
221 |
+
|
222 |
+
.slide-in {
|
223 |
+
animation: slideIn 0.3s ease-out;
|
224 |
+
}
|
src/ui/styles/theme.py
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dataclasses import dataclass
|
2 |
+
from typing import Dict, Any
|
3 |
+
|
4 |
+
@dataclass
|
5 |
+
class HealthcareTheme:
|
6 |
+
"""Healthcare UI theme configuration"""
|
7 |
+
|
8 |
+
# Color palette
|
9 |
+
colors = {
|
10 |
+
'primary': '#2196f3', # Main blue
|
11 |
+
'primary_light': '#e3f2fd', # Light blue
|
12 |
+
'primary_dark': '#1976d2', # Dark blue
|
13 |
+
'success': '#4caf50', # Green
|
14 |
+
'warning': '#ff9800', # Orange
|
15 |
+
'error': '#f44336', # Red
|
16 |
+
'info': '#2196f3', # Blue
|
17 |
+
'background': '#f0f8ff', # Light blue background
|
18 |
+
'surface': '#ffffff', # White
|
19 |
+
'text': '#2c3e50', # Dark gray
|
20 |
+
'text_secondary': '#707070' # Medium gray
|
21 |
+
}
|
22 |
+
|
23 |
+
# Typography
|
24 |
+
fonts = {
|
25 |
+
'primary': '"Source Sans Pro", -apple-system, BlinkMacSystemFont, sans-serif',
|
26 |
+
'monospace': '"Roboto Mono", monospace'
|
27 |
+
}
|
28 |
+
|
29 |
+
# Spacing
|
30 |
+
spacing = {
|
31 |
+
'xs': '0.25rem',
|
32 |
+
'sm': '0.5rem',
|
33 |
+
'md': '1rem',
|
34 |
+
'lg': '1.5rem',
|
35 |
+
'xl': '2rem'
|
36 |
+
}
|
37 |
+
|
38 |
+
# Border radius
|
39 |
+
radius = {
|
40 |
+
'sm': '4px',
|
41 |
+
'md': '8px',
|
42 |
+
'lg': '12px',
|
43 |
+
'xl': '16px',
|
44 |
+
'pill': '9999px'
|
45 |
+
}
|
46 |
+
|
47 |
+
# Shadows
|
48 |
+
shadows = {
|
49 |
+
'sm': '0 1px 3px rgba(0,0,0,0.12)',
|
50 |
+
'md': '0 2px 4px rgba(0,0,0,0.1)',
|
51 |
+
'lg': '0 4px 6px rgba(0,0,0,0.1)',
|
52 |
+
'xl': '0 8px 12px rgba(0,0,0,0.1)'
|
53 |
+
}
|
54 |
+
|
55 |
+
@classmethod
|
56 |
+
def get_streamlit_config(cls) -> Dict[str, Any]:
|
57 |
+
"""Get Streamlit theme configuration"""
|
58 |
+
return {
|
59 |
+
"theme": {
|
60 |
+
"primaryColor": cls.colors['primary'],
|
61 |
+
"backgroundColor": cls.colors['background'],
|
62 |
+
"secondaryBackgroundColor": cls.colors['surface'],
|
63 |
+
"textColor": cls.colors['text'],
|
64 |
+
"font": cls.fonts['primary']
|
65 |
+
}
|
66 |
+
}
|
67 |
+
|
68 |
+
@classmethod
|
69 |
+
def apply_theme(cls):
|
70 |
+
"""Apply theme to Streamlit application"""
|
71 |
+
import streamlit as st
|
72 |
+
|
73 |
+
# Apply theme configuration
|
74 |
+
st.set_page_config(**cls.get_streamlit_config())
|
75 |
+
|
76 |
+
# Apply custom CSS
|
77 |
+
st.markdown("""
|
78 |
+
<style>
|
79 |
+
/* Import fonts */
|
80 |
+
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap');
|
81 |
+
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap');
|
82 |
+
|
83 |
+
/* Base styles */
|
84 |
+
.stApp {
|
85 |
+
font-family: var(--primary-font);
|
86 |
+
color: var(--text-color);
|
87 |
+
}
|
88 |
+
|
89 |
+
/* Custom CSS Variables */
|
90 |
+
:root {
|
91 |
+
--primary-color: """ + cls.colors['primary'] + """;
|
92 |
+
--primary-light: """ + cls.colors['primary_light'] + """;
|
93 |
+
--primary-dark: """ + cls.colors['primary_dark'] + """;
|
94 |
+
--background-color: """ + cls.colors['background'] + """;
|
95 |
+
--surface-color: """ + cls.colors['surface'] + """;
|
96 |
+
--text-color: """ + cls.colors['text'] + """;
|
97 |
+
--primary-font: """ + cls.fonts['primary'] + """;
|
98 |
+
}
|
99 |
+
|
100 |
+
/* Apply theme to Streamlit elements */
|
101 |
+
.stButton>button {
|
102 |
+
background-color: var(--primary-color);
|
103 |
+
color: white;
|
104 |
+
border-radius: """ + cls.radius['pill'] + """;
|
105 |
+
padding: """ + cls.spacing['sm'] + """ """ + cls.spacing['lg'] + """;
|
106 |
+
border: none;
|
107 |
+
box-shadow: """ + cls.shadows['sm'] + """;
|
108 |
+
}
|
109 |
+
|
110 |
+
.stButton>button:hover {
|
111 |
+
background-color: var(--primary-dark);
|
112 |
+
box-shadow: """ + cls.shadows['md'] + """;
|
113 |
+
}
|
114 |
+
|
115 |
+
.stTextInput>div>div>input {
|
116 |
+
border-radius: """ + cls.radius['md'] + """;
|
117 |
+
}
|
118 |
+
|
119 |
+
.stSelectbox>div>div>div {
|
120 |
+
border-radius: """ + cls.radius['md'] + """;
|
121 |
+
}
|
122 |
+
</style>
|
123 |
+
""", unsafe_allow_html=True)
|
src/utils/__init__.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/utils/__init__.py
|
2 |
+
from .logger import setup_logger
|
3 |
+
from .error_handlers import ErrorHandler
|
4 |
+
from .validators import Validator
|
5 |
+
|
6 |
+
__all__ = ['setup_logger', 'ErrorHandler', 'Validator']
|
src/utils/error_handlers.py
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/utils/error_handlers.py
|
2 |
+
from typing import Dict, Any, Optional, Callable
|
3 |
+
from functools import wraps
|
4 |
+
import traceback
|
5 |
+
from .logger import setup_logger
|
6 |
+
|
7 |
+
logger = setup_logger(__name__)
|
8 |
+
|
9 |
+
class HealthcareError(Exception):
|
10 |
+
"""Base exception class for healthcare operations"""
|
11 |
+
def __init__(self, message: str, error_code: str, details: Optional[Dict] = None):
|
12 |
+
self.message = message
|
13 |
+
self.error_code = error_code
|
14 |
+
self.details = details or {}
|
15 |
+
super().__init__(self.message)
|
16 |
+
|
17 |
+
class ValidationError(HealthcareError):
|
18 |
+
"""Raised when input validation fails"""
|
19 |
+
def __init__(self, message: str, details: Optional[Dict] = None):
|
20 |
+
super().__init__(
|
21 |
+
message=message,
|
22 |
+
error_code="INPUT_VALIDATION_ERROR",
|
23 |
+
details=details
|
24 |
+
)
|
25 |
+
|
26 |
+
class ProcessingError(HealthcareError):
|
27 |
+
"""Raised when processing operations fail"""
|
28 |
+
pass
|
29 |
+
|
30 |
+
class ResourceError(HealthcareError):
|
31 |
+
"""Raised when resource-related operations fail"""
|
32 |
+
pass
|
33 |
+
|
34 |
+
class ErrorHandler:
|
35 |
+
@staticmethod
|
36 |
+
def validate_input(input_text: str) -> None:
|
37 |
+
"""Validate input text before processing"""
|
38 |
+
if not input_text or not input_text.strip():
|
39 |
+
raise ValidationError(
|
40 |
+
message="Input text cannot be empty",
|
41 |
+
details={"provided_input": input_text}
|
42 |
+
)
|
43 |
+
|
44 |
+
@staticmethod
|
45 |
+
def handle_error(error: Exception) -> Dict[str, Any]:
|
46 |
+
"""Handle different types of errors and return appropriate response"""
|
47 |
+
if isinstance(error, ValidationError):
|
48 |
+
logger.error(f"Validation Error: {error.message}",
|
49 |
+
extra={"error_code": error.error_code, "details": error.details})
|
50 |
+
raise error # Re-raise ValidationError
|
51 |
+
elif isinstance(error, HealthcareError):
|
52 |
+
logger.error(f"Healthcare Error: {error.message}",
|
53 |
+
extra={"error_code": error.error_code, "details": error.details})
|
54 |
+
return {
|
55 |
+
"error": True,
|
56 |
+
"error_code": error.error_code,
|
57 |
+
"message": error.message,
|
58 |
+
"details": error.details
|
59 |
+
}
|
60 |
+
else:
|
61 |
+
logger.error(f"Unexpected Error: {str(error)}\n{traceback.format_exc()}")
|
62 |
+
return {
|
63 |
+
"error": True,
|
64 |
+
"error_code": "UNEXPECTED_ERROR",
|
65 |
+
"message": "An unexpected error occurred",
|
66 |
+
"details": {"error_type": type(error).__name__}
|
67 |
+
}
|
68 |
+
|
69 |
+
@staticmethod
|
70 |
+
def error_decorator(func: Callable) -> Callable:
|
71 |
+
"""Decorator for handling errors in functions"""
|
72 |
+
@wraps(func)
|
73 |
+
def wrapper(*args, **kwargs):
|
74 |
+
try:
|
75 |
+
return func(*args, **kwargs)
|
76 |
+
except ValidationError:
|
77 |
+
# Let ValidationError propagate up
|
78 |
+
raise
|
79 |
+
except Exception as e:
|
80 |
+
return ErrorHandler.handle_error(e)
|
81 |
+
return wrapper
|
82 |
+
|
83 |
+
@staticmethod
|
84 |
+
def retry_operation(
|
85 |
+
operation: Callable,
|
86 |
+
max_retries: int = 3,
|
87 |
+
retry_delay: float = 1.0
|
88 |
+
) -> Any:
|
89 |
+
"""
|
90 |
+
Retry an operation with exponential backoff
|
91 |
+
"""
|
92 |
+
from time import sleep
|
93 |
+
|
94 |
+
for attempt in range(max_retries):
|
95 |
+
try:
|
96 |
+
return operation()
|
97 |
+
except Exception as e:
|
98 |
+
if attempt == max_retries - 1:
|
99 |
+
raise
|
100 |
+
|
101 |
+
logger.warning(
|
102 |
+
f"Operation failed (attempt {attempt + 1}/{max_retries}): {str(e)}"
|
103 |
+
)
|
104 |
+
sleep(retry_delay * (2 ** attempt))
|
105 |
+
|
106 |
+
@staticmethod
|
107 |
+
def safe_execute(
|
108 |
+
operation: Callable,
|
109 |
+
error_code: str,
|
110 |
+
default_value: Any = None
|
111 |
+
) -> Any:
|
112 |
+
"""
|
113 |
+
Safely execute an operation with error handling
|
114 |
+
"""
|
115 |
+
try:
|
116 |
+
return operation()
|
117 |
+
except Exception as e:
|
118 |
+
logger.error(f"Operation failed: {str(e)}")
|
119 |
+
raise HealthcareError(
|
120 |
+
message=f"Operation failed: {str(e)}",
|
121 |
+
error_code=error_code
|
122 |
+
)# Error handling utilities implementation
|
src/utils/logger.py
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/utils/logger.py
|
2 |
+
import logging
|
3 |
+
import sys
|
4 |
+
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
5 |
+
from pathlib import Path
|
6 |
+
from datetime import datetime
|
7 |
+
from typing import Optional
|
8 |
+
from ..config.settings import Settings
|
9 |
+
|
10 |
+
class CustomFormatter(logging.Formatter):
|
11 |
+
"""Custom formatter with color coding for different log levels"""
|
12 |
+
|
13 |
+
COLORS = {
|
14 |
+
'DEBUG': '\033[0;36m', # Cyan
|
15 |
+
'INFO': '\033[0;32m', # Green
|
16 |
+
'WARNING': '\033[0;33m', # Yellow
|
17 |
+
'ERROR': '\033[0;31m', # Red
|
18 |
+
'CRITICAL': '\033[0;37;41m' # White on Red
|
19 |
+
}
|
20 |
+
RESET = '\033[0m'
|
21 |
+
|
22 |
+
def format(self, record):
|
23 |
+
# Add color to log level if on console
|
24 |
+
if hasattr(self, 'use_color') and self.use_color:
|
25 |
+
record.levelname = f"{self.COLORS.get(record.levelname, '')}{record.levelname}{self.RESET}"
|
26 |
+
return super().format(record)
|
27 |
+
|
28 |
+
def setup_logger(
|
29 |
+
name: str,
|
30 |
+
log_level: Optional[str] = None,
|
31 |
+
log_file: Optional[str] = None
|
32 |
+
) -> logging.Logger:
|
33 |
+
"""
|
34 |
+
Set up logger with both file and console handlers
|
35 |
+
|
36 |
+
Args:
|
37 |
+
name: Logger name
|
38 |
+
log_level: Optional override for log level
|
39 |
+
log_file: Optional override for log file path
|
40 |
+
|
41 |
+
Returns:
|
42 |
+
Configured logger instance
|
43 |
+
"""
|
44 |
+
try:
|
45 |
+
# Create logger
|
46 |
+
logger = logging.getLogger(name)
|
47 |
+
logger.setLevel(log_level or Settings.LOG_LEVEL)
|
48 |
+
|
49 |
+
# Avoid adding handlers if they already exist
|
50 |
+
if logger.handlers:
|
51 |
+
return logger
|
52 |
+
|
53 |
+
# Create formatters
|
54 |
+
file_formatter = logging.Formatter(
|
55 |
+
'%(asctime)s - %(name)s - [%(levelname)s] - %(message)s'
|
56 |
+
)
|
57 |
+
|
58 |
+
console_formatter = CustomFormatter(
|
59 |
+
'%(asctime)s - %(name)s - [%(levelname)s] - %(message)s'
|
60 |
+
)
|
61 |
+
console_formatter.use_color = True
|
62 |
+
|
63 |
+
# Create and configure file handler
|
64 |
+
log_file = log_file or Settings.LOG_FILE
|
65 |
+
log_dir = Path(log_file).parent
|
66 |
+
log_dir.mkdir(parents=True, exist_ok=True)
|
67 |
+
|
68 |
+
# Rotating file handler (size-based)
|
69 |
+
file_handler = RotatingFileHandler(
|
70 |
+
log_file,
|
71 |
+
maxBytes=10 * 1024 * 1024, # 10MB
|
72 |
+
backupCount=5
|
73 |
+
)
|
74 |
+
file_handler.setFormatter(file_formatter)
|
75 |
+
|
76 |
+
# Time-based rotating handler for daily logs
|
77 |
+
daily_handler = TimedRotatingFileHandler(
|
78 |
+
str(log_dir / f"daily_{datetime.now():%Y-%m-%d}.log"),
|
79 |
+
when="midnight",
|
80 |
+
interval=1,
|
81 |
+
backupCount=30
|
82 |
+
)
|
83 |
+
daily_handler.setFormatter(file_formatter)
|
84 |
+
|
85 |
+
# Console handler
|
86 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
87 |
+
console_handler.setFormatter(console_formatter)
|
88 |
+
|
89 |
+
# Add handlers
|
90 |
+
logger.addHandler(file_handler)
|
91 |
+
logger.addHandler(daily_handler)
|
92 |
+
logger.addHandler(console_handler)
|
93 |
+
|
94 |
+
return logger
|
95 |
+
|
96 |
+
except Exception as e:
|
97 |
+
# Fallback to basic logging if setup fails
|
98 |
+
basic_logger = logging.getLogger(name)
|
99 |
+
basic_logger.setLevel(logging.INFO)
|
100 |
+
basic_logger.addHandler(logging.StreamHandler(sys.stdout))
|
101 |
+
basic_logger.error(f"Error setting up logger: {str(e)}")
|
102 |
+
return basic_logger# Logging configuration implementation
|
src/utils/validators.py
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# src/utils/validators.py
|
2 |
+
from typing import Dict, Any, List, Optional
|
3 |
+
from datetime import datetime
|
4 |
+
from .logger import setup_logger
|
5 |
+
from .error_handlers import ValidationError
|
6 |
+
|
7 |
+
logger = setup_logger(__name__)
|
8 |
+
|
9 |
+
class Validator:
|
10 |
+
@staticmethod
|
11 |
+
def validate_state(state: Dict[str, Any]) -> bool:
|
12 |
+
"""Validate the state structure and data types"""
|
13 |
+
required_keys = ["messages", "current_task", "metrics", "timestamp"]
|
14 |
+
|
15 |
+
try:
|
16 |
+
# Check required keys
|
17 |
+
for key in required_keys:
|
18 |
+
if key not in state:
|
19 |
+
raise ValidationError(
|
20 |
+
message=f"Missing required key: {key}",
|
21 |
+
error_code="INVALID_STATE_STRUCTURE"
|
22 |
+
)
|
23 |
+
|
24 |
+
# Validate timestamp
|
25 |
+
if not isinstance(state["timestamp"], datetime):
|
26 |
+
raise ValidationError(
|
27 |
+
message="Invalid timestamp format",
|
28 |
+
error_code="INVALID_TIMESTAMP"
|
29 |
+
)
|
30 |
+
|
31 |
+
return True
|
32 |
+
|
33 |
+
except Exception as e:
|
34 |
+
logger.error(f"State validation failed: {str(e)}")
|
35 |
+
raise
|
36 |
+
|
37 |
+
@staticmethod
|
38 |
+
def validate_metrics(metrics: Dict[str, Any]) -> bool:
|
39 |
+
"""Validate metrics data structure and values"""
|
40 |
+
required_categories = [
|
41 |
+
"patient_flow",
|
42 |
+
"resources",
|
43 |
+
"quality",
|
44 |
+
"staffing"
|
45 |
+
]
|
46 |
+
|
47 |
+
try:
|
48 |
+
# Check required categories
|
49 |
+
for category in required_categories:
|
50 |
+
if category not in metrics:
|
51 |
+
raise ValidationError(
|
52 |
+
message=f"Missing required metrics category: {category}",
|
53 |
+
error_code="INVALID_METRICS_STRUCTURE"
|
54 |
+
)
|
55 |
+
|
56 |
+
# Validate numeric values
|
57 |
+
Validator._validate_numeric_values(metrics)
|
58 |
+
|
59 |
+
return True
|
60 |
+
|
61 |
+
except Exception as e:
|
62 |
+
logger.error(f"Metrics validation failed: {str(e)}")
|
63 |
+
raise
|
64 |
+
|
65 |
+
@staticmethod
|
66 |
+
def validate_tool_input(
|
67 |
+
tool_name: str,
|
68 |
+
params: Dict[str, Any],
|
69 |
+
required_params: List[str]
|
70 |
+
) -> bool:
|
71 |
+
"""Validate input parameters for tools"""
|
72 |
+
try:
|
73 |
+
# Check required parameters
|
74 |
+
for param in required_params:
|
75 |
+
if param not in params:
|
76 |
+
raise ValidationError(
|
77 |
+
message=f"Missing required parameter: {param}",
|
78 |
+
error_code="MISSING_PARAMETER",
|
79 |
+
details={"tool": tool_name, "parameter": param}
|
80 |
+
)
|
81 |
+
|
82 |
+
return True
|
83 |
+
|
84 |
+
except Exception as e:
|
85 |
+
logger.error(f"Tool input validation failed: {str(e)}")
|
86 |
+
raise
|
87 |
+
|
88 |
+
@staticmethod
|
89 |
+
def validate_department_data(department_data: Dict[str, Any]) -> bool:
|
90 |
+
"""Validate department-specific data"""
|
91 |
+
required_fields = [
|
92 |
+
"capacity",
|
93 |
+
"current_occupancy",
|
94 |
+
"staff_count"
|
95 |
+
]
|
96 |
+
|
97 |
+
try:
|
98 |
+
# Check required fields
|
99 |
+
for field in required_fields:
|
100 |
+
if field not in department_data:
|
101 |
+
raise ValidationError(
|
102 |
+
message=f"Missing required field: {field}",
|
103 |
+
error_code="INVALID_DEPARTMENT_DATA"
|
104 |
+
)
|
105 |
+
|
106 |
+
# Validate capacity constraints
|
107 |
+
if department_data["current_occupancy"] > department_data["capacity"]:
|
108 |
+
raise ValidationError(
|
109 |
+
message="Current occupancy exceeds capacity",
|
110 |
+
error_code="INVALID_OCCUPANCY"
|
111 |
+
)
|
112 |
+
|
113 |
+
return True
|
114 |
+
|
115 |
+
except Exception as e:
|
116 |
+
logger.error(f"Department data validation failed: {str(e)}")
|
117 |
+
raise
|
118 |
+
|
119 |
+
@staticmethod
|
120 |
+
def _validate_numeric_values(data: Dict[str, Any], path: str = "") -> None:
|
121 |
+
"""Recursively validate numeric values in nested dictionary"""
|
122 |
+
for key, value in data.items():
|
123 |
+
current_path = f"{path}.{key}" if path else key
|
124 |
+
|
125 |
+
if isinstance(value, (int, float)):
|
126 |
+
if value < 0:
|
127 |
+
raise ValidationError(
|
128 |
+
message=f"Negative value not allowed: {current_path}",
|
129 |
+
error_code="INVALID_NUMERIC_VALUE"
|
130 |
+
)
|
131 |
+
elif isinstance(value, dict):
|
132 |
+
Validator._validate_numeric_values(value, current_path)# Input validation utilities implementation
|
streamlit_app.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from src.ui import HealthcareUI
|
2 |
+
import os
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
|
5 |
+
# Load environment variables
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
+
if __name__ == "__main__":
|
9 |
+
app = HealthcareUI()
|
10 |
+
app.run()
|
test_healthcare_agent_basic.py
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# test_healthcare_agent_basic.py
|
2 |
+
import os
|
3 |
+
from datetime import datetime
|
4 |
+
from src.agent import HealthcareAgent
|
5 |
+
from src.models.state import TaskType, PriorityLevel
|
6 |
+
from src.utils.error_handlers import ValidationError, HealthcareError
|
7 |
+
|
8 |
+
def main():
|
9 |
+
"""Basic test of the Healthcare Operations Management Agent"""
|
10 |
+
try:
|
11 |
+
# 1. Test Agent Initialization
|
12 |
+
print("\n=== Testing Agent Initialization ===")
|
13 |
+
agent = HealthcareAgent(os.getenv("OPENAI_API_KEY"))
|
14 |
+
print("✓ Agent initialized successfully")
|
15 |
+
|
16 |
+
# 2. Test Basic Query - Patient Flow
|
17 |
+
print("\n=== Testing Patient Flow Query ===")
|
18 |
+
patient_query = "What is the current ER occupancy and wait time?"
|
19 |
+
response = agent.process(
|
20 |
+
input_text=patient_query,
|
21 |
+
thread_id="test-thread-1"
|
22 |
+
)
|
23 |
+
print(f"Query: {patient_query}")
|
24 |
+
print(f"Response: {response.get('response', 'No response')}")
|
25 |
+
print(f"Analysis: {response.get('analysis', {})}")
|
26 |
+
|
27 |
+
# 3. Test Resource Management Query
|
28 |
+
print("\n=== Testing Resource Management Query ===")
|
29 |
+
resource_query = "Check the current availability of ventilators and ICU beds"
|
30 |
+
response = agent.process(
|
31 |
+
input_text=resource_query,
|
32 |
+
thread_id="test-thread-1"
|
33 |
+
)
|
34 |
+
print(f"Query: {resource_query}")
|
35 |
+
print(f"Response: {response.get('response', 'No response')}")
|
36 |
+
print(f"Analysis: {response.get('analysis', {})}")
|
37 |
+
|
38 |
+
# 4. Test Conversation History
|
39 |
+
print("\n=== Testing Conversation History ===")
|
40 |
+
history = agent.get_conversation_history("test-thread-1")
|
41 |
+
print(f"Conversation history length: {len(history)}")
|
42 |
+
|
43 |
+
# 5. Test Reset Conversation
|
44 |
+
print("\n=== Testing Conversation Reset ===")
|
45 |
+
reset_success = agent.reset_conversation("test-thread-1")
|
46 |
+
print(f"Reset successful: {reset_success}")
|
47 |
+
|
48 |
+
# 6. Test Error Handling
|
49 |
+
print("\n=== Testing Error Handling ===")
|
50 |
+
try:
|
51 |
+
agent.process("")
|
52 |
+
print("❌ Error handling test failed - empty input accepted")
|
53 |
+
except ValidationError as ve:
|
54 |
+
print(f"✓ Error handling working correctly: Empty input rejected with validation error")
|
55 |
+
except HealthcareError as he:
|
56 |
+
print(f"✓ Error handling working correctly: {str(he)}")
|
57 |
+
except Exception as e:
|
58 |
+
print(f"❌ Unexpected error type: {type(e).__name__}: {str(e)}")
|
59 |
+
|
60 |
+
except Exception as e:
|
61 |
+
print(f"\n❌ Test failed with error: {str(e)}")
|
62 |
+
|
63 |
+
if __name__ == "__main__":
|
64 |
+
print("Starting Healthcare Agent Basic Tests...")
|
65 |
+
print(f"Test Time: {datetime.now()}")
|
66 |
+
main()
|
test_healthcare_scenarios.py
ADDED
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from time import sleep
|
3 |
+
import pytest
|
4 |
+
from datetime import datetime
|
5 |
+
|
6 |
+
class HealthcareAssistantTester:
|
7 |
+
def __init__(self):
|
8 |
+
self.test_results = []
|
9 |
+
|
10 |
+
def run_test_suite(self):
|
11 |
+
"""Run all test scenarios"""
|
12 |
+
print("\n=== Starting Healthcare Assistant Test Suite ===\n")
|
13 |
+
|
14 |
+
# Run all test categories
|
15 |
+
self.test_patient_flow()
|
16 |
+
self.test_resource_management()
|
17 |
+
self.test_staff_scheduling()
|
18 |
+
self.test_quality_metrics()
|
19 |
+
self.test_emergency_scenarios()
|
20 |
+
self.test_department_specific()
|
21 |
+
|
22 |
+
# Print test summary
|
23 |
+
self.print_test_summary()
|
24 |
+
|
25 |
+
def test_patient_flow(self):
|
26 |
+
"""Test Patient Flow Related Queries"""
|
27 |
+
print("\n1. Testing Patient Flow Queries:")
|
28 |
+
queries = [
|
29 |
+
"Show me waiting times across all departments",
|
30 |
+
"What is the current bed occupancy in the ER?",
|
31 |
+
"How many patients are currently waiting for admission?",
|
32 |
+
"What's the average wait time in the ICU?",
|
33 |
+
"Show patient flow trends for the last 8 hours",
|
34 |
+
"Which department has the longest waiting time right now?"
|
35 |
+
]
|
36 |
+
self._run_test_batch("Patient Flow", queries)
|
37 |
+
|
38 |
+
def test_resource_management(self):
|
39 |
+
"""Test Resource Management Queries"""
|
40 |
+
print("\n2. Testing Resource Management Queries:")
|
41 |
+
queries = [
|
42 |
+
"Check medical supplies inventory status",
|
43 |
+
"What is the current ventilator availability?",
|
44 |
+
"Are there any critical supply shortages?",
|
45 |
+
"Show resource utilization across departments",
|
46 |
+
"Which supplies need immediate reordering?",
|
47 |
+
"What's the equipment maintenance status?"
|
48 |
+
]
|
49 |
+
self._run_test_batch("Resource Management", queries)
|
50 |
+
|
51 |
+
def test_staff_scheduling(self):
|
52 |
+
"""Test Staff Scheduling Queries"""
|
53 |
+
print("\n3. Testing Staff Scheduling Queries:")
|
54 |
+
queries = [
|
55 |
+
"Show current staff distribution",
|
56 |
+
"How many nurses are available in ICU?",
|
57 |
+
"What is the current shift coverage?",
|
58 |
+
"Show staff overtime hours this week",
|
59 |
+
"Is there adequate staff coverage for next shift?",
|
60 |
+
"Which departments need additional staff right now?"
|
61 |
+
]
|
62 |
+
self._run_test_batch("Staff Scheduling", queries)
|
63 |
+
|
64 |
+
def test_quality_metrics(self):
|
65 |
+
"""Test Quality Metrics Queries"""
|
66 |
+
print("\n4. Testing Quality Metrics Queries:")
|
67 |
+
queries = [
|
68 |
+
"What's our current patient satisfaction score?",
|
69 |
+
"Show me compliance rates for the last 24 hours",
|
70 |
+
"Are there any quality metrics below target?",
|
71 |
+
"What's the current incident report status?",
|
72 |
+
"Show quality trends across departments",
|
73 |
+
"Which department has the highest patient satisfaction?"
|
74 |
+
]
|
75 |
+
self._run_test_batch("Quality Metrics", queries)
|
76 |
+
|
77 |
+
def test_emergency_scenarios(self):
|
78 |
+
"""Test Emergency Scenario Queries"""
|
79 |
+
print("\n5. Testing Emergency Scenarios:")
|
80 |
+
queries = [
|
81 |
+
"Activate emergency protocol for mass casualty incident",
|
82 |
+
"Need immediate bed availability status for emergency",
|
83 |
+
"Require rapid staff mobilization plan",
|
84 |
+
"Emergency resource allocation needed",
|
85 |
+
"Critical capacity alert in ER",
|
86 |
+
"Emergency department overflow protocol status"
|
87 |
+
]
|
88 |
+
self._run_test_batch("Emergency Scenarios", queries)
|
89 |
+
|
90 |
+
def test_department_specific(self):
|
91 |
+
"""Test Department-Specific Queries"""
|
92 |
+
print("\n6. Testing Department-Specific Queries:")
|
93 |
+
queries = [
|
94 |
+
"Show complete metrics for ER department",
|
95 |
+
"What's the ICU capacity and staff status?",
|
96 |
+
"General ward patient distribution",
|
97 |
+
"Surgery department resource utilization",
|
98 |
+
"Pediatrics department waiting times",
|
99 |
+
"Cardiology unit staff coverage"
|
100 |
+
]
|
101 |
+
self._run_test_batch("Department-Specific", queries)
|
102 |
+
|
103 |
+
def _run_test_batch(self, category: str, queries: list):
|
104 |
+
"""Run a batch of test queries"""
|
105 |
+
for query in queries:
|
106 |
+
try:
|
107 |
+
print(f"\nTesting: {query}")
|
108 |
+
print("-" * 50)
|
109 |
+
|
110 |
+
# Simulate processing time
|
111 |
+
print("Processing query...")
|
112 |
+
sleep(1)
|
113 |
+
|
114 |
+
# Record test execution
|
115 |
+
self.test_results.append({
|
116 |
+
'category': category,
|
117 |
+
'query': query,
|
118 |
+
'timestamp': datetime.now(),
|
119 |
+
'status': 'Success'
|
120 |
+
})
|
121 |
+
|
122 |
+
print("✓ Test completed successfully")
|
123 |
+
|
124 |
+
except Exception as e:
|
125 |
+
print(f"✗ Test failed: {str(e)}")
|
126 |
+
self.test_results.append({
|
127 |
+
'category': category,
|
128 |
+
'query': query,
|
129 |
+
'timestamp': datetime.now(),
|
130 |
+
'status': 'Failed',
|
131 |
+
'error': str(e)
|
132 |
+
})
|
133 |
+
|
134 |
+
def print_test_summary(self):
|
135 |
+
"""Print summary of all test results"""
|
136 |
+
print("\n=== Test Execution Summary ===")
|
137 |
+
print(f"Total Tests Run: {len(self.test_results)}")
|
138 |
+
|
139 |
+
# Calculate statistics
|
140 |
+
successful_tests = len([t for t in self.test_results if t['status'] == 'Success'])
|
141 |
+
failed_tests = len([t for t in self.test_results if t['status'] == 'Failed'])
|
142 |
+
|
143 |
+
print(f"Successful Tests: {successful_tests}")
|
144 |
+
print(f"Failed Tests: {failed_tests}")
|
145 |
+
|
146 |
+
# Print results by category
|
147 |
+
print("\nResults by Category:")
|
148 |
+
categories = set([t['category'] for t in self.test_results])
|
149 |
+
for category in categories:
|
150 |
+
category_tests = [t for t in self.test_results if t['category'] == category]
|
151 |
+
category_success = len([t for t in category_tests if t['status'] == 'Success'])
|
152 |
+
print(f"{category}: {category_success}/{len(category_tests)} passed")
|
153 |
+
|
154 |
+
print("\n=== Test Suite Completed ===")
|
155 |
+
|
156 |
+
def main():
|
157 |
+
"""Main test execution function"""
|
158 |
+
# Initialize and run tests
|
159 |
+
tester = HealthcareAssistantTester()
|
160 |
+
tester.run_test_suite()
|
161 |
+
|
162 |
+
if __name__ == "__main__":
|
163 |
+
main()
|
tests/__init__.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# tests/__init__.py
|
2 |
+
import os
|
3 |
+
import sys
|
4 |
+
|
5 |
+
# Add project root to Python path
|
6 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
tests/conftest.py
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# tests/conftest.py
|
2 |
+
import pytest
|
3 |
+
from datetime import datetime
|
4 |
+
from typing import Dict
|
5 |
+
|
6 |
+
from src.config.settings import Settings
|
7 |
+
from src.models.state import HospitalState, TaskType, PriorityLevel
|
8 |
+
|
9 |
+
@pytest.fixture
|
10 |
+
def mock_settings():
|
11 |
+
"""Fixture for test settings"""
|
12 |
+
return {
|
13 |
+
"OPENAI_API_KEY": "test-api-key",
|
14 |
+
"MODEL_NAME": "gpt-4o-mini-2024-07-18",
|
15 |
+
"MODEL_TEMPERATURE": 0,
|
16 |
+
"MEMORY_TYPE": "sqlite",
|
17 |
+
"MEMORY_URI": ":memory:",
|
18 |
+
"LOG_LEVEL": "DEBUG"
|
19 |
+
}
|
20 |
+
|
21 |
+
@pytest.fixture
|
22 |
+
def mock_llm_response():
|
23 |
+
"""Fixture for mock LLM responses"""
|
24 |
+
return {
|
25 |
+
"input_analysis": {
|
26 |
+
"task_type": TaskType.PATIENT_FLOW,
|
27 |
+
"priority": PriorityLevel.HIGH,
|
28 |
+
"department": "ER",
|
29 |
+
"context": {"urgent": True}
|
30 |
+
},
|
31 |
+
"patient_flow": {
|
32 |
+
"recommendations": ["Optimize bed allocation", "Increase staff in ER"],
|
33 |
+
"metrics": {"waiting_time": 25, "bed_utilization": 0.85}
|
34 |
+
},
|
35 |
+
"quality_monitoring": {
|
36 |
+
"satisfaction_score": 8.5,
|
37 |
+
"compliance_rate": 0.95,
|
38 |
+
"recommendations": ["Maintain current standards"]
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
@pytest.fixture
|
43 |
+
def mock_hospital_state() -> HospitalState:
|
44 |
+
"""Fixture for mock hospital state"""
|
45 |
+
return {
|
46 |
+
"messages": [],
|
47 |
+
"current_task": TaskType.GENERAL,
|
48 |
+
"priority_level": PriorityLevel.MEDIUM,
|
49 |
+
"department": None,
|
50 |
+
"metrics": {
|
51 |
+
"patient_flow": {
|
52 |
+
"total_beds": 100,
|
53 |
+
"occupied_beds": 75,
|
54 |
+
"waiting_patients": 10,
|
55 |
+
"average_wait_time": 30.0
|
56 |
+
},
|
57 |
+
"resources": {
|
58 |
+
"equipment_availability": {"ventilators": True},
|
59 |
+
"supply_levels": {"masks": 0.8},
|
60 |
+
"resource_utilization": 0.75
|
61 |
+
},
|
62 |
+
"quality": {
|
63 |
+
"patient_satisfaction": 8.5,
|
64 |
+
"compliance_rate": 0.95,
|
65 |
+
"incident_count": 2
|
66 |
+
},
|
67 |
+
"staffing": {
|
68 |
+
"total_staff": 200,
|
69 |
+
"available_staff": {"doctors": 20, "nurses": 50},
|
70 |
+
"overtime_hours": 45.5
|
71 |
+
}
|
72 |
+
},
|
73 |
+
"analysis": None,
|
74 |
+
"context": {},
|
75 |
+
"timestamp": datetime.now(),
|
76 |
+
"thread_id": "test-thread-id"
|
77 |
+
}
|
78 |
+
|
79 |
+
@pytest.fixture
|
80 |
+
def mock_tools_response():
|
81 |
+
"""Fixture for mock tool responses"""
|
82 |
+
return {
|
83 |
+
"patient_tools": {
|
84 |
+
"wait_time": 30.5,
|
85 |
+
"bed_capacity": {"available": 25, "total": 100},
|
86 |
+
"discharge_time": datetime.now()
|
87 |
+
},
|
88 |
+
"resource_tools": {
|
89 |
+
"supply_levels": {"critical": [], "reorder": ["masks"]},
|
90 |
+
"equipment_status": {"available": ["xray"], "in_use": ["mri"]}
|
91 |
+
}
|
92 |
+
}
|
93 |
+
|
94 |
+
@pytest.fixture
|
95 |
+
def mock_error_response():
|
96 |
+
"""Fixture for mock error responses"""
|
97 |
+
return {
|
98 |
+
"validation_error": {
|
99 |
+
"code": "INVALID_INPUT",
|
100 |
+
"message": "Invalid input parameters",
|
101 |
+
"details": {"field": "department", "issue": "required"}
|
102 |
+
},
|
103 |
+
"processing_error": {
|
104 |
+
"code": "PROCESSING_FAILED",
|
105 |
+
"message": "Failed to process request",
|
106 |
+
"details": {"step": "analysis", "reason": "timeout"}
|
107 |
+
}
|
108 |
+
}# Test configuration implementation
|
tests/test_agent.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# tests/test_agent.py
|
2 |
+
import pytest
|
3 |
+
from src.agent import HealthcareAgent
|
4 |
+
from src.utils.error_handlers import HealthcareError
|
5 |
+
|
6 |
+
class TestHealthcareAgent:
|
7 |
+
def test_agent_initialization(self, mock_settings):
|
8 |
+
"""Test agent initialization"""
|
9 |
+
agent = HealthcareAgent(api_key=mock_settings["OPENAI_API_KEY"])
|
10 |
+
assert agent is not None
|
11 |
+
assert agent.llm is not None
|
12 |
+
assert agent.tools is not None
|
13 |
+
assert agent.nodes is not None
|
14 |
+
|
15 |
+
def test_process_input(self, mock_hospital_state):
|
16 |
+
"""Test processing of input through agent"""
|
17 |
+
agent = HealthcareAgent()
|
18 |
+
result = agent.process(
|
19 |
+
"What is the current ER waiting time?",
|
20 |
+
thread_id="test-thread"
|
21 |
+
)
|
22 |
+
|
23 |
+
assert "response" in result
|
24 |
+
assert "analysis" in result
|
25 |
+
assert "metrics" in result
|
26 |
+
assert "timestamp" in result
|
27 |
+
|
28 |
+
def test_conversation_history(self):
|
29 |
+
"""Test conversation history retrieval"""
|
30 |
+
agent = HealthcareAgent()
|
31 |
+
thread_id = "test-thread"
|
32 |
+
|
33 |
+
# Add some messages
|
34 |
+
agent.process("Test message 1", thread_id=thread_id)
|
35 |
+
agent.process("Test message 2", thread_id=thread_id)
|
36 |
+
|
37 |
+
history = agent.get_conversation_history(thread_id)
|
38 |
+
assert len(history) >= 2
|
39 |
+
|
40 |
+
def test_error_handling(self):
|
41 |
+
"""Test error handling in agent"""
|
42 |
+
agent = HealthcareAgent()
|
43 |
+
|
44 |
+
with pytest.raises(HealthcareError):
|
45 |
+
agent.process("", thread_id="test-thread")
|
46 |
+
|
47 |
+
def test_state_management(self, mock_hospital_state):
|
48 |
+
"""Test state management"""
|
49 |
+
agent = HealthcareAgent()
|
50 |
+
thread_id = "test-thread"
|
51 |
+
|
52 |
+
# Process message
|
53 |
+
result = agent.process("Test message", thread_id=thread_id)
|
54 |
+
assert result is not None
|
55 |
+
|
56 |
+
# Reset conversation
|
57 |
+
reset_success = agent.reset_conversation(thread_id)
|
58 |
+
assert reset_success is True
|
59 |
+
|
60 |
+
# Verify reset
|
61 |
+
history = agent.get_conversation_history(thread_id)
|
62 |
+
assert len(history) == 0
|
63 |
+
|
64 |
+
@pytest.mark.asyncio
|
65 |
+
async def test_async_processing(self):
|
66 |
+
"""Test async processing capabilities"""
|
67 |
+
agent = HealthcareAgent()
|
68 |
+
thread_id = "test-thread"
|
69 |
+
|
70 |
+
# Test streaming response
|
71 |
+
async for event in agent.graph.astream_events(
|
72 |
+
{"messages": ["Test message"]},
|
73 |
+
{"configurable": {"thread_id": thread_id}}
|
74 |
+
):
|
75 |
+
assert event is not None# Integration tests implementation
|
tests/test_nodes/test_input_analyzer.py
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# tests/test_nodes/test_input_analyzer.py
|
2 |
+
import pytest
|
3 |
+
from src.nodes.input_analyzer import InputAnalyzerNode
|
4 |
+
from src.models.state import TaskType, PriorityLevel
|
5 |
+
|
6 |
+
def test_input_analyzer_initialization(mock_llm_response):
|
7 |
+
"""Test InputAnalyzer node initialization"""
|
8 |
+
analyzer = InputAnalyzerNode(mock_llm_response)
|
9 |
+
assert analyzer is not None
|
10 |
+
|
11 |
+
def test_input_analysis(mock_hospital_state, mock_llm_response):
|
12 |
+
"""Test input analysis functionality"""
|
13 |
+
analyzer = InputAnalyzerNode(mock_llm_response)
|
14 |
+
result = analyzer(mock_hospital_state)
|
15 |
+
|
16 |
+
assert "current_task" in result
|
17 |
+
assert "priority_level" in result
|
18 |
+
assert isinstance(result["current_task"], TaskType)
|
19 |
+
assert isinstance(result["priority_level"], PriorityLevel)
|
20 |
+
|
21 |
+
def test_invalid_input_handling(mock_hospital_state):
|
22 |
+
"""Test handling of invalid input"""
|
23 |
+
analyzer = InputAnalyzerNode(None)
|
24 |
+
mock_hospital_state["messages"] = []
|
25 |
+
|
26 |
+
with pytest.raises(ValueError):
|
27 |
+
analyzer(mock_hospital_state)
|