Spaces:
Running
Running
Commit
·
2a8faae
0
Parent(s):
Initial commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +15 -0
- .env +10 -0
- .env.example +10 -0
- Dockerfile +50 -0
- api/__init__.py +0 -0
- api/__pycache__/__init__.cpython-311.pyc +0 -0
- api/__pycache__/__init__.cpython-312.pyc +0 -0
- api/__pycache__/__init__.cpython-313.pyc +0 -0
- api/__pycache__/app.cpython-311.pyc +0 -0
- api/__pycache__/app.cpython-312.pyc +0 -0
- api/__pycache__/app.cpython-313.pyc +0 -0
- api/__pycache__/exceptions.cpython-311.pyc +0 -0
- api/__pycache__/exceptions.cpython-313.pyc +0 -0
- api/__pycache__/middleware.cpython-311.pyc +0 -0
- api/__pycache__/middleware.cpython-313.pyc +0 -0
- api/__pycache__/models.cpython-311.pyc +0 -0
- api/__pycache__/models.cpython-313.pyc +0 -0
- api/app.py +105 -0
- api/exceptions.py +71 -0
- api/middleware.py +100 -0
- api/models.py +97 -0
- api/routers/__init__.py +0 -0
- api/routers/__pycache__/__init__.cpython-311.pyc +0 -0
- api/routers/__pycache__/__init__.cpython-313.pyc +0 -0
- api/routers/__pycache__/health.cpython-311.pyc +0 -0
- api/routers/__pycache__/health.cpython-313.pyc +0 -0
- api/routers/__pycache__/medical.cpython-311.pyc +0 -0
- api/routers/__pycache__/medical.cpython-313.pyc +0 -0
- api/routers/health.py +136 -0
- api/routers/medical.py +58 -0
- api/routers/side_effects.py +165 -0
- core/__init__.py +0 -0
- core/__pycache__/__init__.cpython-311.pyc +0 -0
- core/__pycache__/__init__.cpython-313.pyc +0 -0
- core/__pycache__/agent.cpython-311.pyc +0 -0
- core/__pycache__/agent.cpython-313.pyc +0 -0
- core/__pycache__/background_init.cpython-311.pyc +0 -0
- core/__pycache__/background_init.cpython-313.pyc +0 -0
- core/__pycache__/config.cpython-311.pyc +0 -0
- core/__pycache__/config.cpython-313.pyc +0 -0
- core/__pycache__/data_loaders.cpython-311.pyc +0 -0
- core/__pycache__/data_loaders.cpython-313.pyc +0 -0
- core/__pycache__/github_storage.cpython-311.pyc +0 -0
- core/__pycache__/github_storage.cpython-313.pyc +0 -0
- core/__pycache__/retrievers.cpython-311.pyc +0 -0
- core/__pycache__/retrievers.cpython-313.pyc +0 -0
- core/__pycache__/text_processors.cpython-311.pyc +0 -0
- core/__pycache__/text_processors.cpython-313.pyc +0 -0
- core/__pycache__/tools.cpython-311.pyc +0 -0
- core/__pycache__/tools.cpython-313.pyc +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
*.pyd
|
| 5 |
+
*.sqlite3
|
| 6 |
+
.env
|
| 7 |
+
.env.*
|
| 8 |
+
.git/
|
| 9 |
+
.gitignore
|
| 10 |
+
.vscode/
|
| 11 |
+
.idea/
|
| 12 |
+
logs/
|
| 13 |
+
data/
|
| 14 |
+
**/__pycache__/
|
| 15 |
+
**/*.py[cod]
|
.env
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenAI Configuration
|
| 2 |
+
OPENAI_API_KEY=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
|
| 3 |
+
|
| 4 |
+
LANGSMITH_API_KEY=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
|
| 5 |
+
|
| 6 |
+
LANGSMITH_PROJECT=medical_chatbot
|
| 7 |
+
LANGCHAIN_PROJECT=medical_chatbot
|
| 8 |
+
|
| 9 |
+
LANGSMITH_URL=https://api.smith.langchain.com
|
| 10 |
+
|
.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenAI Configuration
|
| 2 |
+
OPENAI_API_KEY=
|
| 3 |
+
OPENAI_BASE_URL=
|
| 4 |
+
|
| 5 |
+
LANGSMITH_API_KEY=
|
| 6 |
+
|
| 7 |
+
LANGSMITH_PROJECT=
|
| 8 |
+
LANGCHAIN_PROJECT=
|
| 9 |
+
|
| 10 |
+
LANGSMITH_URL=
|
Dockerfile
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ========================
|
| 2 |
+
# Stage 1 - Builder
|
| 3 |
+
# ========================
|
| 4 |
+
FROM python:3.10-slim AS builder
|
| 5 |
+
|
| 6 |
+
# Install build dependencies
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
build-essential \
|
| 9 |
+
gcc \
|
| 10 |
+
g++ \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
|
| 15 |
+
# Copy only requirements first (better caching)
|
| 16 |
+
COPY requirements.txt .
|
| 17 |
+
|
| 18 |
+
# Install dependencies
|
| 19 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# ========================
|
| 22 |
+
# Stage 2 - Final Runtime Image
|
| 23 |
+
# ========================
|
| 24 |
+
FROM python:3.10-slim
|
| 25 |
+
|
| 26 |
+
# Install minimal runtime dependencies
|
| 27 |
+
RUN apt-get update && apt-get install -y \
|
| 28 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 29 |
+
|
| 30 |
+
# Create a non-root user
|
| 31 |
+
RUN useradd --create-home --shell /bin/bash appuser
|
| 32 |
+
WORKDIR /app
|
| 33 |
+
|
| 34 |
+
# Copy installed packages and application code
|
| 35 |
+
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
| 36 |
+
COPY --from=builder /usr/local/bin /usr/local/bin
|
| 37 |
+
COPY . /app
|
| 38 |
+
|
| 39 |
+
# Set permissions
|
| 40 |
+
RUN chown -R appuser:appuser /app
|
| 41 |
+
USER appuser
|
| 42 |
+
|
| 43 |
+
# Expose port
|
| 44 |
+
EXPOSE 8000
|
| 45 |
+
|
| 46 |
+
# Set the working directory to the backend folder
|
| 47 |
+
WORKDIR /app
|
| 48 |
+
|
| 49 |
+
# Command to run the FastAPI app
|
| 50 |
+
CMD ["sh", "-c", "uvicorn api.app:app --host 0.0.0.0 --port ${PORT:-8000}"]
|
api/__init__.py
ADDED
|
File without changes
|
api/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (144 Bytes). View file
|
|
|
api/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (132 Bytes). View file
|
|
|
api/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (132 Bytes). View file
|
|
|
api/__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (4.32 kB). View file
|
|
|
api/__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (3.76 kB). View file
|
|
|
api/__pycache__/app.cpython-313.pyc
ADDED
|
Binary file (3.74 kB). View file
|
|
|
api/__pycache__/exceptions.cpython-311.pyc
ADDED
|
Binary file (3.83 kB). View file
|
|
|
api/__pycache__/exceptions.cpython-313.pyc
ADDED
|
Binary file (3.43 kB). View file
|
|
|
api/__pycache__/middleware.cpython-311.pyc
ADDED
|
Binary file (5.82 kB). View file
|
|
|
api/__pycache__/middleware.cpython-313.pyc
ADDED
|
Binary file (5.28 kB). View file
|
|
|
api/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (9.21 kB). View file
|
|
|
api/__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (7.53 kB). View file
|
|
|
api/app.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import logging
|
| 3 |
+
from contextlib import asynccontextmanager
|
| 4 |
+
from fastapi import FastAPI, HTTPException
|
| 5 |
+
from fastapi.exceptions import RequestValidationError
|
| 6 |
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
| 7 |
+
|
| 8 |
+
# Import routers
|
| 9 |
+
from api.routers import medical, health
|
| 10 |
+
from api.middleware import (
|
| 11 |
+
ProcessTimeMiddleware,
|
| 12 |
+
LoggingMiddleware,
|
| 13 |
+
RateLimitMiddleware,
|
| 14 |
+
get_cors_middleware_config
|
| 15 |
+
)
|
| 16 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
+
from api.exceptions import (
|
| 18 |
+
http_exception_handler,
|
| 19 |
+
validation_exception_handler,
|
| 20 |
+
general_exception_handler,
|
| 21 |
+
starlette_exception_handler
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Configure logging
|
| 25 |
+
logging.basicConfig(level=logging.INFO)
|
| 26 |
+
logger = logging.getLogger(__name__)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@asynccontextmanager
|
| 30 |
+
async def lifespan(app: FastAPI):
|
| 31 |
+
"""Application lifespan management with background initialization"""
|
| 32 |
+
# Startup
|
| 33 |
+
logger.info("Starting Medical RAG AI Advisor API...")
|
| 34 |
+
|
| 35 |
+
# Start background initialization of heavy components
|
| 36 |
+
try:
|
| 37 |
+
from background_init import start_background_initialization
|
| 38 |
+
logger.info("🚀 Starting background initialization of components...")
|
| 39 |
+
start_background_initialization()
|
| 40 |
+
logger.info("API started successfully (components loading in background)")
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logger.error(f"Failed to start background initialization: {e}")
|
| 43 |
+
logger.info("API started with lazy loading fallback")
|
| 44 |
+
|
| 45 |
+
yield
|
| 46 |
+
|
| 47 |
+
# Shutdown
|
| 48 |
+
logger.info("Shutting down Medical RAG AI Advisor API...")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# Create FastAPI application
|
| 52 |
+
app = FastAPI(
|
| 53 |
+
title="Medical RAG AI Advisor API",
|
| 54 |
+
description="Professional API for medical information retrieval and advisory services",
|
| 55 |
+
version="1.0.0",
|
| 56 |
+
docs_url="/docs",
|
| 57 |
+
redoc_url="/redoc",
|
| 58 |
+
lifespan=lifespan
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Add middleware
|
| 62 |
+
app.add_middleware(CORSMiddleware, **get_cors_middleware_config())
|
| 63 |
+
app.add_middleware(ProcessTimeMiddleware)
|
| 64 |
+
app.add_middleware(LoggingMiddleware)
|
| 65 |
+
app.add_middleware(RateLimitMiddleware, calls_per_minute=100) # Adjust as needed
|
| 66 |
+
|
| 67 |
+
# Add exception handlers
|
| 68 |
+
app.add_exception_handler(HTTPException, http_exception_handler)
|
| 69 |
+
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
| 70 |
+
app.add_exception_handler(StarletteHTTPException, starlette_exception_handler)
|
| 71 |
+
app.add_exception_handler(Exception, general_exception_handler)
|
| 72 |
+
|
| 73 |
+
# Include routers
|
| 74 |
+
app.include_router(health.router)
|
| 75 |
+
app.include_router(medical.router)
|
| 76 |
+
|
| 77 |
+
# Root endpoint
|
| 78 |
+
@app.get("/")
|
| 79 |
+
async def root():
|
| 80 |
+
"""Root endpoint with API information"""
|
| 81 |
+
return {
|
| 82 |
+
"name": "Medical RAG AI Advisor API",
|
| 83 |
+
"version": "1.0.0",
|
| 84 |
+
"description": "Professional API for medical information retrieval and advisory services",
|
| 85 |
+
"docs": "/docs",
|
| 86 |
+
"health": "/health",
|
| 87 |
+
"endpoints": {
|
| 88 |
+
"ask": "/ask",
|
| 89 |
+
"ask_stream": "/ask/stream",
|
| 90 |
+
"initialization_status": "/health/initialization"
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
import uvicorn
|
| 99 |
+
uvicorn.run(
|
| 100 |
+
"api.app:app",
|
| 101 |
+
host="127.0.0.1",
|
| 102 |
+
port=8000,
|
| 103 |
+
reload=True,
|
| 104 |
+
log_level="info"
|
| 105 |
+
)
|
api/exceptions.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Exception handlers for Medical RAG AI Advisor API
|
| 3 |
+
"""
|
| 4 |
+
import logging
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from fastapi import Request, HTTPException
|
| 7 |
+
from fastapi.responses import JSONResponse
|
| 8 |
+
from fastapi.exceptions import RequestValidationError
|
| 9 |
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
| 10 |
+
|
| 11 |
+
from api.models import ErrorResponse
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
| 17 |
+
"""Handle HTTP exceptions"""
|
| 18 |
+
logger.error(f"HTTP Exception: {exc.status_code} - {exc.detail}")
|
| 19 |
+
|
| 20 |
+
return JSONResponse(
|
| 21 |
+
status_code=exc.status_code,
|
| 22 |
+
content=ErrorResponse(
|
| 23 |
+
error="HTTP_ERROR",
|
| 24 |
+
message=exc.detail,
|
| 25 |
+
timestamp=datetime.now().isoformat()
|
| 26 |
+
).dict()
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
| 31 |
+
"""Handle request validation errors"""
|
| 32 |
+
logger.error(f"Validation Error: {exc.errors()}")
|
| 33 |
+
|
| 34 |
+
return JSONResponse(
|
| 35 |
+
status_code=422,
|
| 36 |
+
content=ErrorResponse(
|
| 37 |
+
error="VALIDATION_ERROR",
|
| 38 |
+
message="Request validation failed",
|
| 39 |
+
details={"validation_errors": exc.errors()},
|
| 40 |
+
timestamp=datetime.now().isoformat()
|
| 41 |
+
).dict()
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
async def general_exception_handler(request: Request, exc: Exception):
|
| 46 |
+
"""Handle general exceptions"""
|
| 47 |
+
logger.error(f"Unhandled Exception: {type(exc).__name__} - {str(exc)}")
|
| 48 |
+
|
| 49 |
+
return JSONResponse(
|
| 50 |
+
status_code=500,
|
| 51 |
+
content=ErrorResponse(
|
| 52 |
+
error="INTERNAL_SERVER_ERROR",
|
| 53 |
+
message="An internal server error occurred",
|
| 54 |
+
details={"exception_type": type(exc).__name__},
|
| 55 |
+
timestamp=datetime.now().isoformat()
|
| 56 |
+
).dict()
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
async def starlette_exception_handler(request: Request, exc: StarletteHTTPException):
|
| 61 |
+
"""Handle Starlette HTTP exceptions"""
|
| 62 |
+
logger.error(f"Starlette HTTP Exception: {exc.status_code} - {exc.detail}")
|
| 63 |
+
|
| 64 |
+
return JSONResponse(
|
| 65 |
+
status_code=exc.status_code,
|
| 66 |
+
content=ErrorResponse(
|
| 67 |
+
error="HTTP_ERROR",
|
| 68 |
+
message=exc.detail,
|
| 69 |
+
timestamp=datetime.now().isoformat()
|
| 70 |
+
).dict()
|
| 71 |
+
)
|
api/middleware.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Middleware for Medical RAG AI Advisor API
|
| 3 |
+
"""
|
| 4 |
+
import time
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Callable, Awaitable
|
| 7 |
+
from fastapi import Request, Response, HTTPException
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ProcessTimeMiddleware(BaseHTTPMiddleware):
|
| 15 |
+
"""Middleware to add processing time to response headers"""
|
| 16 |
+
|
| 17 |
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
| 18 |
+
start_time = time.time()
|
| 19 |
+
response = await call_next(request)
|
| 20 |
+
process_time = time.time() - start_time
|
| 21 |
+
response.headers["X-Process-Time"] = f"{process_time:.4f}"
|
| 22 |
+
return response
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class LoggingMiddleware(BaseHTTPMiddleware):
|
| 26 |
+
"""Middleware for request/response logging"""
|
| 27 |
+
|
| 28 |
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
| 29 |
+
start_time = time.time()
|
| 30 |
+
|
| 31 |
+
# Log request
|
| 32 |
+
logger.info(f"Request: {request.method} {request.url}")
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
response = await call_next(request)
|
| 36 |
+
process_time = time.time() - start_time
|
| 37 |
+
|
| 38 |
+
# Log response
|
| 39 |
+
logger.info(
|
| 40 |
+
f"Response: {response.status_code} - "
|
| 41 |
+
f"Time: {process_time:.4f}s - "
|
| 42 |
+
f"Path: {request.url.path}"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
return response
|
| 46 |
+
|
| 47 |
+
except Exception as e:
|
| 48 |
+
process_time = time.time() - start_time
|
| 49 |
+
logger.error(
|
| 50 |
+
f"Error: {str(e)} - "
|
| 51 |
+
f"Time: {process_time:.4f}s - "
|
| 52 |
+
f"Path: {request.url.path}"
|
| 53 |
+
)
|
| 54 |
+
raise
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class RateLimitMiddleware(BaseHTTPMiddleware):
|
| 58 |
+
"""Simple rate limiting middleware"""
|
| 59 |
+
|
| 60 |
+
def __init__(self, app, calls_per_minute: int = 60):
|
| 61 |
+
super().__init__(app)
|
| 62 |
+
self.calls_per_minute = calls_per_minute
|
| 63 |
+
self.client_calls = {}
|
| 64 |
+
|
| 65 |
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
| 66 |
+
client_ip = request.client.host
|
| 67 |
+
current_time = time.time()
|
| 68 |
+
|
| 69 |
+
# Clean old entries
|
| 70 |
+
self.client_calls = {
|
| 71 |
+
ip: calls for ip, calls in self.client_calls.items()
|
| 72 |
+
if any(call_time > current_time - 60 for call_time in calls)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
# Check rate limit
|
| 76 |
+
if client_ip in self.client_calls:
|
| 77 |
+
recent_calls = [
|
| 78 |
+
call_time for call_time in self.client_calls[client_ip]
|
| 79 |
+
if call_time > current_time - 60
|
| 80 |
+
]
|
| 81 |
+
if len(recent_calls) >= self.calls_per_minute:
|
| 82 |
+
raise HTTPException(
|
| 83 |
+
status_code=429,
|
| 84 |
+
detail="Rate limit exceeded. Please try again later."
|
| 85 |
+
)
|
| 86 |
+
self.client_calls[client_ip] = recent_calls + [current_time]
|
| 87 |
+
else:
|
| 88 |
+
self.client_calls[client_ip] = [current_time]
|
| 89 |
+
|
| 90 |
+
return await call_next(request)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def get_cors_middleware_config():
|
| 94 |
+
"""Get CORS middleware configuration"""
|
| 95 |
+
return {
|
| 96 |
+
"allow_origins": ["*"], # Configure appropriately for production
|
| 97 |
+
"allow_credentials": True,
|
| 98 |
+
"allow_methods": ["*"],
|
| 99 |
+
"allow_headers": ["*"],
|
| 100 |
+
}
|
api/models.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API Models and Schemas for Medical RAG AI Advisor
|
| 3 |
+
"""
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import Optional, List, Dict, Any
|
| 6 |
+
from enum import Enum
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class QueryType(str, Enum):
|
| 10 |
+
"""Types of medical queries supported"""
|
| 11 |
+
GENERAL = "general"
|
| 12 |
+
DRUG_INTERACTION = "drug_interaction"
|
| 13 |
+
SIDE_EFFECTS = "side_effects"
|
| 14 |
+
GUIDELINES = "guidelines"
|
| 15 |
+
COMPARISON = "comparison"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class QueryRequest(BaseModel):
|
| 19 |
+
"""Request model for medical queries"""
|
| 20 |
+
query: str = Field(..., description="Medical question or query", min_length=1)
|
| 21 |
+
query_type: Optional[QueryType] = Field(None, description="Type of medical query")
|
| 22 |
+
context: Optional[str] = Field(None, description="Additional context for the query")
|
| 23 |
+
patient_info: Optional[Dict[str, Any]] = Field(None, description="Patient information if relevant")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class QueryResponse(BaseModel):
|
| 27 |
+
"""Response model for medical queries"""
|
| 28 |
+
response: str = Field(..., description="AI-generated medical response")
|
| 29 |
+
sources: Optional[List[str]] = Field(None, description="Sources used for the response")
|
| 30 |
+
confidence: Optional[float] = Field(None, description="Confidence score of the response")
|
| 31 |
+
query_type: Optional[QueryType] = Field(None, description="Detected or specified query type")
|
| 32 |
+
processing_time: Optional[float] = Field(None, description="Time taken to process the query")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class StreamChunk(BaseModel):
|
| 36 |
+
"""Model for streaming response chunks"""
|
| 37 |
+
chunk: str = Field(..., description="Chunk of the streaming response")
|
| 38 |
+
is_final: bool = Field(False, description="Whether this is the final chunk")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class SideEffectReport(BaseModel):
|
| 42 |
+
"""Model for side effect reporting"""
|
| 43 |
+
drug_name: str = Field(..., description="Name of the drug")
|
| 44 |
+
side_effects: str = Field(..., description="Reported side effects")
|
| 45 |
+
patient_age: Optional[int] = Field(None, description="Patient age")
|
| 46 |
+
patient_gender: Optional[str] = Field(None, description="Patient gender")
|
| 47 |
+
dosage: Optional[str] = Field(None, description="Drug dosage")
|
| 48 |
+
duration: Optional[str] = Field(None, description="Duration of treatment")
|
| 49 |
+
severity: Optional[str] = Field(None, description="Severity of side effects")
|
| 50 |
+
outcome: Optional[str] = Field(None, description="Outcome of the side effects")
|
| 51 |
+
additional_details: Optional[str] = Field(None, description="Additional clinical details")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class SideEffectResponse(BaseModel):
|
| 55 |
+
"""Response model for side effect reporting"""
|
| 56 |
+
report_id: str = Field(..., description="Unique identifier for the report")
|
| 57 |
+
status: str = Field(..., description="Status of the report submission")
|
| 58 |
+
message: str = Field(..., description="Confirmation message")
|
| 59 |
+
recommendations: Optional[List[str]] = Field(None, description="Clinical recommendations")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class ComparisonRequest(BaseModel):
|
| 63 |
+
"""Request model for provider/treatment comparisons"""
|
| 64 |
+
providers: List[str] = Field(..., description="List of providers to compare", min_items=2)
|
| 65 |
+
criteria: Optional[List[str]] = Field(None, description="Specific criteria for comparison")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class ComparisonResponse(BaseModel):
|
| 69 |
+
"""Response model for provider/treatment comparisons"""
|
| 70 |
+
comparison: str = Field(..., description="Detailed comparison analysis")
|
| 71 |
+
summary: Dict[str, Any] = Field(..., description="Summary of key differences")
|
| 72 |
+
recommendations: Optional[str] = Field(None, description="Recommendations based on comparison")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class InitializationStatus(BaseModel):
|
| 76 |
+
"""Initialization status response model"""
|
| 77 |
+
is_complete: bool = Field(..., description="Whether initialization is complete")
|
| 78 |
+
status_message: str = Field(..., description="Current initialization status")
|
| 79 |
+
is_successful: bool = Field(..., description="Whether initialization was successful")
|
| 80 |
+
error: Optional[str] = Field(None, description="Initialization error if any")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class HealthStatus(BaseModel):
|
| 84 |
+
"""Health check response model"""
|
| 85 |
+
status: str = Field(..., description="API health status")
|
| 86 |
+
version: str = Field(..., description="API version")
|
| 87 |
+
timestamp: str = Field(..., description="Current timestamp")
|
| 88 |
+
components: Dict[str, str] = Field(..., description="Status of system components")
|
| 89 |
+
initialization: Optional[InitializationStatus] = Field(None, description="Background initialization status")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class ErrorResponse(BaseModel):
|
| 93 |
+
"""Error response model"""
|
| 94 |
+
error: str = Field(..., description="Error type")
|
| 95 |
+
message: str = Field(..., description="Error message")
|
| 96 |
+
details: Optional[Dict[str, Any]] = Field(None, description="Additional error details")
|
| 97 |
+
timestamp: str = Field(..., description="Error timestamp")
|
api/routers/__init__.py
ADDED
|
File without changes
|
api/routers/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (182 Bytes). View file
|
|
|
api/routers/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (170 Bytes). View file
|
|
|
api/routers/__pycache__/health.cpython-311.pyc
ADDED
|
Binary file (5.52 kB). View file
|
|
|
api/routers/__pycache__/health.cpython-313.pyc
ADDED
|
Binary file (4.73 kB). View file
|
|
|
api/routers/__pycache__/medical.cpython-311.pyc
ADDED
|
Binary file (2.93 kB). View file
|
|
|
api/routers/__pycache__/medical.cpython-313.pyc
ADDED
|
Binary file (2.6 kB). View file
|
|
|
api/routers/health.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Health Check and System Status Router
|
| 3 |
+
"""
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from fastapi import APIRouter
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Add src to path for imports
|
| 10 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
| 11 |
+
|
| 12 |
+
from api.models import HealthStatus, InitializationStatus
|
| 13 |
+
|
| 14 |
+
router = APIRouter(prefix="/health", tags=["health"])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@router.get("/", response_model=HealthStatus)
|
| 18 |
+
async def health_check():
|
| 19 |
+
"""
|
| 20 |
+
Check the health status of the API and its components
|
| 21 |
+
"""
|
| 22 |
+
components = {}
|
| 23 |
+
|
| 24 |
+
# Check agent availability
|
| 25 |
+
try:
|
| 26 |
+
from agent import safe_run_agent
|
| 27 |
+
components["agent"] = "healthy"
|
| 28 |
+
except Exception:
|
| 29 |
+
components["agent"] = "unhealthy"
|
| 30 |
+
|
| 31 |
+
# Check vector store
|
| 32 |
+
try:
|
| 33 |
+
from vector_store import VectorStore
|
| 34 |
+
components["vector_store"] = "healthy"
|
| 35 |
+
except Exception:
|
| 36 |
+
components["vector_store"] = "unhealthy"
|
| 37 |
+
|
| 38 |
+
# Check data loaders
|
| 39 |
+
try:
|
| 40 |
+
from data_loaders import load_pdf_documents
|
| 41 |
+
components["data_loaders"] = "healthy"
|
| 42 |
+
except Exception:
|
| 43 |
+
components["data_loaders"] = "unhealthy"
|
| 44 |
+
|
| 45 |
+
# Check tools
|
| 46 |
+
try:
|
| 47 |
+
from tools import medical_guidelines_knowledge_tool
|
| 48 |
+
components["tools"] = "healthy"
|
| 49 |
+
except Exception:
|
| 50 |
+
components["tools"] = "unhealthy"
|
| 51 |
+
|
| 52 |
+
# Check initialization status
|
| 53 |
+
initialization_status = None
|
| 54 |
+
try:
|
| 55 |
+
from background_init import (
|
| 56 |
+
is_initialization_complete,
|
| 57 |
+
get_initialization_status,
|
| 58 |
+
is_initialization_successful,
|
| 59 |
+
get_initialization_error
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
initialization_status = InitializationStatus(
|
| 63 |
+
is_complete=is_initialization_complete(),
|
| 64 |
+
status_message=get_initialization_status(),
|
| 65 |
+
is_successful=is_initialization_successful(),
|
| 66 |
+
error=str(get_initialization_error()) if get_initialization_error() else None
|
| 67 |
+
)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
initialization_status = InitializationStatus(
|
| 70 |
+
is_complete=False,
|
| 71 |
+
status_message=f"Unable to check initialization status: {str(e)}",
|
| 72 |
+
is_successful=False,
|
| 73 |
+
error=str(e)
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# Overall status
|
| 77 |
+
overall_status = "healthy" if all(
|
| 78 |
+
status == "healthy" for status in components.values()
|
| 79 |
+
) else "degraded"
|
| 80 |
+
|
| 81 |
+
return HealthStatus(
|
| 82 |
+
status=overall_status,
|
| 83 |
+
version="1.0.0",
|
| 84 |
+
timestamp=datetime.now().isoformat(),
|
| 85 |
+
components=components,
|
| 86 |
+
initialization=initialization_status
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@router.get("/ping")
|
| 91 |
+
async def ping():
|
| 92 |
+
"""
|
| 93 |
+
Simple ping endpoint for basic connectivity check
|
| 94 |
+
"""
|
| 95 |
+
return {"message": "pong", "timestamp": datetime.now().isoformat()}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@router.get("/initialization", response_model=InitializationStatus)
|
| 99 |
+
async def get_initialization_status():
|
| 100 |
+
"""
|
| 101 |
+
Get the current initialization status of background components
|
| 102 |
+
"""
|
| 103 |
+
try:
|
| 104 |
+
from background_init import (
|
| 105 |
+
is_initialization_complete,
|
| 106 |
+
get_initialization_status,
|
| 107 |
+
is_initialization_successful,
|
| 108 |
+
get_initialization_error
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
return InitializationStatus(
|
| 112 |
+
is_complete=is_initialization_complete(),
|
| 113 |
+
status_message=get_initialization_status(),
|
| 114 |
+
is_successful=is_initialization_successful(),
|
| 115 |
+
error=str(get_initialization_error()) if get_initialization_error() else None
|
| 116 |
+
)
|
| 117 |
+
except Exception as e:
|
| 118 |
+
return InitializationStatus(
|
| 119 |
+
is_complete=False,
|
| 120 |
+
status_message=f"Unable to check initialization status: {str(e)}",
|
| 121 |
+
is_successful=False,
|
| 122 |
+
error=str(e)
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@router.get("/version")
|
| 127 |
+
async def get_version():
|
| 128 |
+
"""
|
| 129 |
+
Get API version information
|
| 130 |
+
"""
|
| 131 |
+
return {
|
| 132 |
+
"version": "1.0.0",
|
| 133 |
+
"name": "Medical RAG AI Advisor API",
|
| 134 |
+
"description": "Professional API for medical information retrieval and advisory services",
|
| 135 |
+
"build_date": "2024-01-01"
|
| 136 |
+
}
|
api/routers/medical.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Medical Query Router for RAG AI Advisor
|
| 3 |
+
"""
|
| 4 |
+
import asyncio
|
| 5 |
+
from fastapi import APIRouter, HTTPException
|
| 6 |
+
from fastapi.responses import StreamingResponse
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
# Add src to path for imports
|
| 11 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
| 12 |
+
|
| 13 |
+
from core.agent import safe_run_agent, safe_run_agent_streaming
|
| 14 |
+
|
| 15 |
+
router = APIRouter(tags=["medical"])
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@router.get("/ask")
|
| 19 |
+
async def ask(query: str):
|
| 20 |
+
"""
|
| 21 |
+
Process a medical query - agent decides which tools to use
|
| 22 |
+
"""
|
| 23 |
+
try:
|
| 24 |
+
response = await safe_run_agent(user_input=query)
|
| 25 |
+
return {"response": response}
|
| 26 |
+
|
| 27 |
+
except Exception as e:
|
| 28 |
+
raise HTTPException(
|
| 29 |
+
status_code=500,
|
| 30 |
+
detail=f"Error processing medical query: {str(e)}"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@router.get("/ask/stream")
|
| 35 |
+
async def ask_stream(query: str):
|
| 36 |
+
"""
|
| 37 |
+
Process a medical query with streaming response - agent decides which tools to use
|
| 38 |
+
"""
|
| 39 |
+
async def event_stream():
|
| 40 |
+
try:
|
| 41 |
+
chunk_buffer = ""
|
| 42 |
+
async for chunk in safe_run_agent_streaming(user_input=query):
|
| 43 |
+
chunk_buffer += chunk
|
| 44 |
+
|
| 45 |
+
# Send chunks in reasonable sizes for smoother streaming
|
| 46 |
+
if len(chunk_buffer) >= 10: # Adjust this value as needed
|
| 47 |
+
yield chunk_buffer
|
| 48 |
+
chunk_buffer = ""
|
| 49 |
+
await asyncio.sleep(0.01) # Small delay for smoother streaming
|
| 50 |
+
|
| 51 |
+
# Send any remaining content
|
| 52 |
+
if chunk_buffer:
|
| 53 |
+
yield chunk_buffer
|
| 54 |
+
|
| 55 |
+
except Exception as e:
|
| 56 |
+
yield f"Error: {str(e)}"
|
| 57 |
+
|
| 58 |
+
return StreamingResponse(event_stream(), media_type="text/markdown")
|
api/routers/side_effects.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Side Effects Reporting Router
|
| 3 |
+
"""
|
| 4 |
+
import uuid
|
| 5 |
+
import time
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from fastapi import APIRouter, HTTPException
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
# Add backend and src directories to path for imports
|
| 12 |
+
backend_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
| 13 |
+
src_path = os.path.join(backend_path, 'src')
|
| 14 |
+
sys.path.append(backend_path)
|
| 15 |
+
sys.path.append(src_path)
|
| 16 |
+
|
| 17 |
+
from api.models import SideEffectReport
|
| 18 |
+
from tools import side_effect_recording_tool
|
| 19 |
+
from github_storage import get_github_storage
|
| 20 |
+
|
| 21 |
+
router = APIRouter(prefix="/side-effects", tags=["side-effects"])
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@router.post("/report")
|
| 25 |
+
async def report_side_effect(report: SideEffectReport):
|
| 26 |
+
"""
|
| 27 |
+
Submit a side effect report for pharmacovigilance
|
| 28 |
+
"""
|
| 29 |
+
try:
|
| 30 |
+
# Prepare the report text for the side effect tool
|
| 31 |
+
report_text = f"Drug: {report.drug_name}, Side effects: {report.side_effects}"
|
| 32 |
+
|
| 33 |
+
if report.patient_age:
|
| 34 |
+
report_text += f", Patient age: {report.patient_age}"
|
| 35 |
+
if report.patient_gender:
|
| 36 |
+
report_text += f", Patient gender: {report.patient_gender}"
|
| 37 |
+
if report.dosage:
|
| 38 |
+
report_text += f", Dosage: {report.dosage}"
|
| 39 |
+
if report.duration:
|
| 40 |
+
report_text += f", Duration: {report.duration}"
|
| 41 |
+
if report.severity:
|
| 42 |
+
report_text += f", Severity: {report.severity}"
|
| 43 |
+
if report.outcome:
|
| 44 |
+
report_text += f", Outcome: {report.outcome}"
|
| 45 |
+
if report.additional_details:
|
| 46 |
+
report_text += f", Additional details: {report.additional_details}"
|
| 47 |
+
|
| 48 |
+
# Process through the existing side effect recording tool
|
| 49 |
+
result = side_effect_recording_tool(report_text)
|
| 50 |
+
|
| 51 |
+
return {"result": result}
|
| 52 |
+
|
| 53 |
+
except Exception as e:
|
| 54 |
+
raise HTTPException(
|
| 55 |
+
status_code=500,
|
| 56 |
+
detail=f"Error processing side effect report: {str(e)}"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@router.get("/reports/summary")
|
| 61 |
+
async def get_reports_summary():
|
| 62 |
+
"""
|
| 63 |
+
Get summary of side effect reports from GitHub repository
|
| 64 |
+
"""
|
| 65 |
+
try:
|
| 66 |
+
import pandas as pd
|
| 67 |
+
import os
|
| 68 |
+
|
| 69 |
+
# Try to get reports from GitHub first
|
| 70 |
+
github_storage = get_github_storage()
|
| 71 |
+
reports = github_storage.get_side_effects_reports()
|
| 72 |
+
|
| 73 |
+
if not reports:
|
| 74 |
+
# Fallback to local file if GitHub fails
|
| 75 |
+
csv_path = "side_effects_reports.csv"
|
| 76 |
+
|
| 77 |
+
if not os.path.exists(csv_path):
|
| 78 |
+
return {
|
| 79 |
+
"total_reports": 0,
|
| 80 |
+
"message": "No side effect reports found"
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
df = pd.read_csv(csv_path)
|
| 84 |
+
else:
|
| 85 |
+
# Convert GitHub reports to DataFrame
|
| 86 |
+
df = pd.DataFrame(reports)
|
| 87 |
+
|
| 88 |
+
if df.empty:
|
| 89 |
+
return {
|
| 90 |
+
"total_reports": 0,
|
| 91 |
+
"message": "No side effect reports found"
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
summary = {
|
| 95 |
+
"total_reports": len(df),
|
| 96 |
+
"unique_drugs": df['drug_name'].nunique() if 'drug_name' in df.columns else 0,
|
| 97 |
+
"recent_reports": len(df[df['timestamp'] >= (datetime.now() - pd.Timedelta(days=30)).strftime('%Y-%m-%d')]) if 'timestamp' in df.columns else 0,
|
| 98 |
+
"most_reported_drugs": df['drug_name'].value_counts().head(5).to_dict() if 'drug_name' in df.columns else {}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return summary
|
| 102 |
+
|
| 103 |
+
except Exception as e:
|
| 104 |
+
raise HTTPException(
|
| 105 |
+
status_code=500,
|
| 106 |
+
detail=f"Error retrieving reports summary: {str(e)}"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@router.get("/drug/{drug_name}/reports")
|
| 111 |
+
async def get_drug_reports(drug_name: str):
|
| 112 |
+
"""
|
| 113 |
+
Get side effect reports for a specific drug from GitHub repository
|
| 114 |
+
"""
|
| 115 |
+
try:
|
| 116 |
+
# Try to get reports from GitHub first
|
| 117 |
+
github_storage = get_github_storage()
|
| 118 |
+
reports = github_storage.get_drug_reports(drug_name)
|
| 119 |
+
|
| 120 |
+
if reports:
|
| 121 |
+
return {
|
| 122 |
+
"drug_name": drug_name,
|
| 123 |
+
"total_reports": len(reports),
|
| 124 |
+
"reports": reports
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
# Fallback to local file if GitHub fails or no reports found
|
| 128 |
+
import pandas as pd
|
| 129 |
+
import os
|
| 130 |
+
|
| 131 |
+
csv_path = "side_effects_reports.csv"
|
| 132 |
+
|
| 133 |
+
if not os.path.exists(csv_path):
|
| 134 |
+
return {
|
| 135 |
+
"drug_name": drug_name,
|
| 136 |
+
"reports": [],
|
| 137 |
+
"message": "No reports found"
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
df = pd.read_csv(csv_path)
|
| 141 |
+
|
| 142 |
+
# Filter reports for the specific drug (case-insensitive)
|
| 143 |
+
drug_reports = df[df['drug_name'].str.lower() == drug_name.lower()]
|
| 144 |
+
|
| 145 |
+
if drug_reports.empty:
|
| 146 |
+
return {
|
| 147 |
+
"drug_name": drug_name,
|
| 148 |
+
"reports": [],
|
| 149 |
+
"message": f"No reports found for {drug_name}"
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
# Convert to list of dictionaries
|
| 153 |
+
local_reports = drug_reports.to_dict('records')
|
| 154 |
+
|
| 155 |
+
return {
|
| 156 |
+
"drug_name": drug_name,
|
| 157 |
+
"total_reports": len(local_reports),
|
| 158 |
+
"reports": local_reports
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
raise HTTPException(
|
| 163 |
+
status_code=500,
|
| 164 |
+
detail=f"Error retrieving drug reports: {str(e)}"
|
| 165 |
+
)
|
core/__init__.py
ADDED
|
File without changes
|
core/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (145 Bytes). View file
|
|
|
core/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (133 Bytes). View file
|
|
|
core/__pycache__/agent.cpython-311.pyc
ADDED
|
Binary file (36.1 kB). View file
|
|
|
core/__pycache__/agent.cpython-313.pyc
ADDED
|
Binary file (32.5 kB). View file
|
|
|
core/__pycache__/background_init.cpython-311.pyc
ADDED
|
Binary file (8.47 kB). View file
|
|
|
core/__pycache__/background_init.cpython-313.pyc
ADDED
|
Binary file (7.77 kB). View file
|
|
|
core/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (6.3 kB). View file
|
|
|
core/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (6.24 kB). View file
|
|
|
core/__pycache__/data_loaders.cpython-311.pyc
ADDED
|
Binary file (4.93 kB). View file
|
|
|
core/__pycache__/data_loaders.cpython-313.pyc
ADDED
|
Binary file (4.34 kB). View file
|
|
|
core/__pycache__/github_storage.cpython-311.pyc
ADDED
|
Binary file (17.7 kB). View file
|
|
|
core/__pycache__/github_storage.cpython-313.pyc
ADDED
|
Binary file (15.7 kB). View file
|
|
|
core/__pycache__/retrievers.cpython-311.pyc
ADDED
|
Binary file (8.81 kB). View file
|
|
|
core/__pycache__/retrievers.cpython-313.pyc
ADDED
|
Binary file (8 kB). View file
|
|
|
core/__pycache__/text_processors.cpython-311.pyc
ADDED
|
Binary file (654 Bytes). View file
|
|
|
core/__pycache__/text_processors.cpython-313.pyc
ADDED
|
Binary file (556 Bytes). View file
|
|
|
core/__pycache__/tools.cpython-311.pyc
ADDED
|
Binary file (41.1 kB). View file
|
|
|
core/__pycache__/tools.cpython-313.pyc
ADDED
|
Binary file (37.5 kB). View file
|
|
|