Spaces:
Sleeping
Sleeping
Commit
·
3f61e65
0
Parent(s):
backend
Browse files- .gitignore +47 -0
- Dockerfile +33 -0
- LICENSE +55 -0
- README.md +49 -0
- app.py +89 -0
- app/api/admin.py +116 -0
- app/api/auth.py +51 -0
- app/api/beta.py +87 -0
- app/api/data.py +38 -0
- app/api/generator.py +98 -0
- app/api/medical.py +33 -0
- app/core/config.py +23 -0
- app/core/database.py +21 -0
- app/main.py +40 -0
- app/models/models.py +20 -0
- app/routers/data.py +57 -0
- app/services/data_generator.py +251 -0
- app/services/medical_generator.py +137 -0
- create_tables.py +22 -0
- init_db.py +10 -0
- main.py +39 -0
- pyproject.toml +17 -0
- recreate_tables.py +14 -0
- requirements-dev.txt +6 -0
- requirements.txt +9 -0
- run_linters.py +52 -0
- run_tests.py +25 -0
- setup.cfg +31 -0
- tests/test_api.py +140 -0
- tests/test_integration.py +29 -0
.gitignore
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
# Virtual Environment
|
24 |
+
venv/
|
25 |
+
env/
|
26 |
+
ENV/
|
27 |
+
|
28 |
+
# IDE
|
29 |
+
.idea/
|
30 |
+
.vscode/
|
31 |
+
*.swp
|
32 |
+
*.swo
|
33 |
+
|
34 |
+
# Database
|
35 |
+
*.db
|
36 |
+
*.sqlite3
|
37 |
+
|
38 |
+
# Environment variables
|
39 |
+
.env
|
40 |
+
.env.local
|
41 |
+
|
42 |
+
# Logs
|
43 |
+
*.log
|
44 |
+
|
45 |
+
# Hugging Face specific
|
46 |
+
.cache/
|
47 |
+
transformers/
|
Dockerfile
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9
|
2 |
+
|
3 |
+
# Set up a new user named "user" with user ID 1000
|
4 |
+
RUN useradd -m -u 1000 user
|
5 |
+
|
6 |
+
# Switch to the "user" user
|
7 |
+
USER user
|
8 |
+
|
9 |
+
# Set home to the user's home directory
|
10 |
+
ENV HOME=/home/user \
|
11 |
+
PATH=/home/user/.local/bin:$PATH
|
12 |
+
|
13 |
+
# Set the working directory to the user's home directory
|
14 |
+
WORKDIR $HOME/app
|
15 |
+
|
16 |
+
# Try and run pip command after setting the user with `USER user` to avoid permission issues with Python
|
17 |
+
RUN pip install --no-cache-dir --upgrade pip
|
18 |
+
|
19 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
20 |
+
COPY --chown=user . $HOME/app
|
21 |
+
|
22 |
+
# Install dependencies
|
23 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
24 |
+
|
25 |
+
# Create a directory for model cache
|
26 |
+
RUN mkdir -p $HOME/.cache/huggingface
|
27 |
+
RUN chmod 777 $HOME/.cache/huggingface
|
28 |
+
|
29 |
+
# Expose the port the app runs on
|
30 |
+
EXPOSE 7860
|
31 |
+
|
32 |
+
# Command to run the application
|
33 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Synthex AI - Commercial License
|
2 |
+
|
3 |
+
Copyright (c) 2024 Synthex AI
|
4 |
+
|
5 |
+
This software and associated documentation files (the "Software") are proprietary and confidential.
|
6 |
+
The Software is protected by copyright laws and international copyright treaties, as well as other
|
7 |
+
intellectual property laws and treaties.
|
8 |
+
|
9 |
+
TERMS AND CONDITIONS
|
10 |
+
|
11 |
+
1. License Grant
|
12 |
+
This license grants you a limited, non-exclusive, non-transferable license to use the Software
|
13 |
+
solely for your internal business purposes, subject to the terms and conditions of this Agreement.
|
14 |
+
|
15 |
+
2. Restrictions
|
16 |
+
You may not:
|
17 |
+
- Copy, modify, or create derivative works of the Software
|
18 |
+
- Reverse engineer, decompile, or disassemble the Software
|
19 |
+
- Remove or alter any proprietary notices or labels on the Software
|
20 |
+
- Use the Software for any illegal purpose
|
21 |
+
- Transfer, sublicense, or resell the Software
|
22 |
+
|
23 |
+
3. Proprietary Rights
|
24 |
+
The Software and all copies, modifications, and derivative works are owned by Synthex AI and
|
25 |
+
are protected by copyright, trade secret, and other intellectual property laws.
|
26 |
+
|
27 |
+
4. Confidentiality
|
28 |
+
You agree to maintain the confidentiality of the Software and not disclose it to any third party
|
29 |
+
without Synthex AI's prior written consent.
|
30 |
+
|
31 |
+
5. Warranty Disclaimer
|
32 |
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. SYNTHEX AI DISCLAIMS ALL
|
33 |
+
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
34 |
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
35 |
+
|
36 |
+
6. Limitation of Liability
|
37 |
+
IN NO EVENT SHALL SYNTHEX AI BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM,
|
38 |
+
OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
39 |
+
|
40 |
+
7. Termination
|
41 |
+
This license is effective until terminated. Your rights under this license will terminate
|
42 |
+
automatically without notice if you fail to comply with any of its terms.
|
43 |
+
|
44 |
+
8. Governing Law
|
45 |
+
This Agreement shall be governed by and construed in accordance with the laws of the State of
|
46 |
+
Delaware, without regard to its conflict of law provisions.
|
47 |
+
|
48 |
+
9. Contact Information
|
49 |
+
For licensing inquiries, please contact:
|
50 |
+
Synthex AI
|
51 |
+
Email: [email protected]
|
52 |
+
Website: https://synthex.ai
|
53 |
+
|
54 |
+
By using the Software, you acknowledge that you have read this Agreement, understand it, and agree
|
55 |
+
to be bound by its terms and conditions.
|
README.md
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Synthex Medical Text Generator
|
3 |
+
emoji: 🏥
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: indigo
|
6 |
+
sdk: docker
|
7 |
+
app_port: 7860
|
8 |
+
---
|
9 |
+
|
10 |
+
# Synthex Medical Text Generator
|
11 |
+
|
12 |
+
A synthetic medical text generator built with FastAPI and Hugging Face Transformers.
|
13 |
+
|
14 |
+
## Features
|
15 |
+
|
16 |
+
- Generate synthetic medical text data
|
17 |
+
- Multiple record types:
|
18 |
+
- Clinical Notes
|
19 |
+
- Discharge Summaries
|
20 |
+
- Lab Reports
|
21 |
+
- Prescriptions
|
22 |
+
- HIPAA-compliant fictional data
|
23 |
+
- RESTful API endpoints
|
24 |
+
|
25 |
+
## API Endpoints
|
26 |
+
|
27 |
+
- `GET /`: Get API information
|
28 |
+
- `POST /generate`: Generate medical records
|
29 |
+
- `GET /health`: Health check endpoint
|
30 |
+
|
31 |
+
## Example Usage
|
32 |
+
|
33 |
+
```bash
|
34 |
+
# Generate a clinical note
|
35 |
+
curl -X POST "https://theaniketgiri-synthex.hf.space/generate" \
|
36 |
+
-H "Content-Type: application/json" \
|
37 |
+
-d '{"record_type": "clinical_note", "count": 1}'
|
38 |
+
```
|
39 |
+
|
40 |
+
## Technical Details
|
41 |
+
|
42 |
+
- Built with FastAPI
|
43 |
+
- Uses Bio_ClinicalBERT model from Hugging Face
|
44 |
+
- Docker container with Python 3.9
|
45 |
+
- Exposed on port 7860
|
46 |
+
|
47 |
+
## License
|
48 |
+
|
49 |
+
MIT License
|
app.py
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, HTTPException
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
from pydantic import BaseModel
|
4 |
+
from typing import List, Optional
|
5 |
+
from datetime import datetime
|
6 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
7 |
+
import torch
|
8 |
+
import json
|
9 |
+
|
10 |
+
app = FastAPI(
|
11 |
+
title="Synthex Medical Text Generator",
|
12 |
+
description="Generate synthetic medical text data for research and development purposes.",
|
13 |
+
version="1.0.0"
|
14 |
+
)
|
15 |
+
|
16 |
+
# Add CORS middleware
|
17 |
+
app.add_middleware(
|
18 |
+
CORSMiddleware,
|
19 |
+
allow_origins=["*"], # Allows all origins
|
20 |
+
allow_credentials=True,
|
21 |
+
allow_methods=["*"], # Allows all methods
|
22 |
+
allow_headers=["*"], # Allows all headers
|
23 |
+
)
|
24 |
+
|
25 |
+
# Initialize model and tokenizer
|
26 |
+
model = None
|
27 |
+
tokenizer = None
|
28 |
+
|
29 |
+
def load_model():
|
30 |
+
global model, tokenizer
|
31 |
+
if model is None or tokenizer is None:
|
32 |
+
model_name = "emilyalsentzer/Bio_ClinicalBERT"
|
33 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
34 |
+
model = AutoModelForCausalLM.from_pretrained(model_name)
|
35 |
+
return model, tokenizer
|
36 |
+
|
37 |
+
class GenerateRequest(BaseModel):
|
38 |
+
record_type: str
|
39 |
+
count: int = 1
|
40 |
+
|
41 |
+
class MedicalRecord(BaseModel):
|
42 |
+
type: str
|
43 |
+
content: str
|
44 |
+
generated_at: str
|
45 |
+
|
46 |
+
@app.get("/")
|
47 |
+
def read_root():
|
48 |
+
return {
|
49 |
+
"name": "Synthex Medical Text Generator",
|
50 |
+
"version": "1.0.0",
|
51 |
+
"description": "Generate synthetic medical text data for research and development purposes."
|
52 |
+
}
|
53 |
+
|
54 |
+
@app.post("/generate", response_model=List[MedicalRecord])
|
55 |
+
async def generate_records(request: GenerateRequest):
|
56 |
+
try:
|
57 |
+
model, tokenizer = load_model()
|
58 |
+
|
59 |
+
records = []
|
60 |
+
for i in range(request.count):
|
61 |
+
# Generate text using the model
|
62 |
+
input_text = f"Generate a {request.record_type}:"
|
63 |
+
inputs = tokenizer(input_text, return_tensors="pt", max_length=512, truncation=True)
|
64 |
+
outputs = model.generate(
|
65 |
+
inputs["input_ids"],
|
66 |
+
max_length=200,
|
67 |
+
num_return_sequences=1,
|
68 |
+
temperature=0.7,
|
69 |
+
top_p=0.9,
|
70 |
+
do_sample=True
|
71 |
+
)
|
72 |
+
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
73 |
+
|
74 |
+
# Create record
|
75 |
+
record = MedicalRecord(
|
76 |
+
type=request.record_type,
|
77 |
+
content=generated_text,
|
78 |
+
generated_at=datetime.now().isoformat()
|
79 |
+
)
|
80 |
+
records.append(record)
|
81 |
+
|
82 |
+
return records
|
83 |
+
|
84 |
+
except Exception as e:
|
85 |
+
raise HTTPException(status_code=500, detail=str(e))
|
86 |
+
|
87 |
+
@app.get("/health")
|
88 |
+
def health_check():
|
89 |
+
return {"status": "healthy"}
|
app/api/admin.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
2 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
3 |
+
from sqlalchemy.orm import Session
|
4 |
+
from app.core.database import get_db
|
5 |
+
from app.models.models import BetaApplication, ApplicationStatus
|
6 |
+
from app.core.config import settings
|
7 |
+
from datetime import datetime, timedelta
|
8 |
+
from jose import JWTError, jwt
|
9 |
+
from pydantic import BaseModel
|
10 |
+
from typing import List, Optional
|
11 |
+
|
12 |
+
router = APIRouter()
|
13 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
14 |
+
|
15 |
+
class Token(BaseModel):
|
16 |
+
access_token: str
|
17 |
+
token_type: str
|
18 |
+
|
19 |
+
class TokenData(BaseModel):
|
20 |
+
email: Optional[str] = None
|
21 |
+
|
22 |
+
class ApplicationUpdate(BaseModel):
|
23 |
+
status: ApplicationStatus
|
24 |
+
|
25 |
+
class BetaApplicationResponse(BaseModel):
|
26 |
+
id: str
|
27 |
+
email: str
|
28 |
+
company: str
|
29 |
+
use_case: str
|
30 |
+
status: str
|
31 |
+
created_at: datetime
|
32 |
+
updated_at: Optional[datetime] = None
|
33 |
+
|
34 |
+
class Config:
|
35 |
+
from_attributes = True
|
36 |
+
|
37 |
+
class BetaApplicationList(BaseModel):
|
38 |
+
applications: List[BetaApplicationResponse]
|
39 |
+
|
40 |
+
def create_access_token(data: dict):
|
41 |
+
to_encode = data.copy()
|
42 |
+
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
43 |
+
to_encode.update({"exp": expire})
|
44 |
+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
|
45 |
+
return encoded_jwt
|
46 |
+
|
47 |
+
async def get_current_admin(token: str = Depends(oauth2_scheme)):
|
48 |
+
credentials_exception = HTTPException(
|
49 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
50 |
+
detail="Could not validate credentials",
|
51 |
+
headers={"WWW-Authenticate": "Bearer"},
|
52 |
+
)
|
53 |
+
try:
|
54 |
+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
55 |
+
email: str = payload.get("sub")
|
56 |
+
if email is None or email != settings.ADMIN_EMAIL:
|
57 |
+
raise credentials_exception
|
58 |
+
token_data = TokenData(email=email)
|
59 |
+
except JWTError:
|
60 |
+
raise credentials_exception
|
61 |
+
return token_data
|
62 |
+
|
63 |
+
@router.post("/login", response_model=Token)
|
64 |
+
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
65 |
+
if form_data.username != settings.ADMIN_EMAIL or form_data.password != settings.ADMIN_PASSWORD:
|
66 |
+
raise HTTPException(
|
67 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
68 |
+
detail="Incorrect email or password",
|
69 |
+
headers={"WWW-Authenticate": "Bearer"},
|
70 |
+
)
|
71 |
+
access_token = create_access_token(data={"sub": form_data.username})
|
72 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
73 |
+
|
74 |
+
@router.get("/applications")
|
75 |
+
async def get_applications(
|
76 |
+
db: Session = Depends(get_db),
|
77 |
+
current_admin: TokenData = Depends(get_current_admin)
|
78 |
+
):
|
79 |
+
applications = db.query(BetaApplication).all()
|
80 |
+
return {
|
81 |
+
"applications": [
|
82 |
+
{
|
83 |
+
"id": app.id,
|
84 |
+
"email": app.email,
|
85 |
+
"company": app.company,
|
86 |
+
"use_case": app.use_case,
|
87 |
+
"status": app.status.value,
|
88 |
+
"created_at": app.created_at.isoformat(),
|
89 |
+
"updated_at": app.updated_at.isoformat() if app.updated_at else None
|
90 |
+
}
|
91 |
+
for app in applications
|
92 |
+
]
|
93 |
+
}
|
94 |
+
|
95 |
+
@router.patch("/applications/{application_id}")
|
96 |
+
async def update_application(
|
97 |
+
application_id: str,
|
98 |
+
update: ApplicationUpdate,
|
99 |
+
db: Session = Depends(get_db),
|
100 |
+
current_admin: TokenData = Depends(get_current_admin)
|
101 |
+
):
|
102 |
+
application = db.query(BetaApplication).filter(
|
103 |
+
BetaApplication.id == application_id
|
104 |
+
).first()
|
105 |
+
|
106 |
+
if not application:
|
107 |
+
raise HTTPException(
|
108 |
+
status_code=404,
|
109 |
+
detail="Application not found"
|
110 |
+
)
|
111 |
+
|
112 |
+
application.status = update.status
|
113 |
+
db.commit()
|
114 |
+
db.refresh(application)
|
115 |
+
|
116 |
+
return {"message": "Application updated successfully"}
|
app/api/auth.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
2 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
3 |
+
from jose import JWTError, jwt
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
from pydantic import BaseModel
|
6 |
+
from typing import Optional
|
7 |
+
from app.core.config import settings
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
11 |
+
|
12 |
+
class Token(BaseModel):
|
13 |
+
access_token: str
|
14 |
+
token_type: str
|
15 |
+
|
16 |
+
class TokenData(BaseModel):
|
17 |
+
username: Optional[str] = None
|
18 |
+
|
19 |
+
def create_access_token(data: dict):
|
20 |
+
to_encode = data.copy()
|
21 |
+
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
22 |
+
to_encode.update({"exp": expire})
|
23 |
+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
|
24 |
+
return encoded_jwt
|
25 |
+
|
26 |
+
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
27 |
+
credentials_exception = HTTPException(
|
28 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
29 |
+
detail="Could not validate credentials",
|
30 |
+
headers={"WWW-Authenticate": "Bearer"},
|
31 |
+
)
|
32 |
+
try:
|
33 |
+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
34 |
+
username: str = payload.get("sub")
|
35 |
+
if username is None:
|
36 |
+
raise credentials_exception
|
37 |
+
token_data = TokenData(username=username)
|
38 |
+
except JWTError:
|
39 |
+
raise credentials_exception
|
40 |
+
return token_data
|
41 |
+
|
42 |
+
@router.post("/token", response_model=Token)
|
43 |
+
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
44 |
+
if form_data.username != settings.ADMIN_EMAIL or form_data.password != settings.ADMIN_PASSWORD:
|
45 |
+
raise HTTPException(
|
46 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
47 |
+
detail="Incorrect username or password",
|
48 |
+
headers={"WWW-Authenticate": "Bearer"},
|
49 |
+
)
|
50 |
+
access_token = create_access_token(data={"sub": form_data.username})
|
51 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
app/api/beta.py
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException, Body
|
2 |
+
from sqlalchemy.orm import Session
|
3 |
+
from app.core.database import get_db
|
4 |
+
from app.models.models import BetaApplication, ApplicationStatus
|
5 |
+
from pydantic import BaseModel, Field
|
6 |
+
import uuid
|
7 |
+
from datetime import datetime
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
|
11 |
+
class BetaApplicationCreate(BaseModel):
|
12 |
+
email: str
|
13 |
+
company: str
|
14 |
+
useCase: str = Field(..., alias="use_case")
|
15 |
+
|
16 |
+
class Config:
|
17 |
+
populate_by_name = True
|
18 |
+
|
19 |
+
class BetaApplicationResponse(BaseModel):
|
20 |
+
id: str
|
21 |
+
email: str
|
22 |
+
company: str
|
23 |
+
useCase: str = Field(..., alias="use_case")
|
24 |
+
status: str
|
25 |
+
createdAt: datetime = Field(..., alias="created_at")
|
26 |
+
updatedAt: datetime | None = Field(None, alias="updated_at")
|
27 |
+
|
28 |
+
class Config:
|
29 |
+
from_attributes = True
|
30 |
+
populate_by_name = True
|
31 |
+
|
32 |
+
class VerifyApplicationRequest(BaseModel):
|
33 |
+
application_id: str
|
34 |
+
|
35 |
+
@router.post("/apply", response_model=BetaApplicationResponse)
|
36 |
+
def create_application(
|
37 |
+
application: BetaApplicationCreate,
|
38 |
+
db: Session = Depends(get_db)
|
39 |
+
):
|
40 |
+
# Check if email already exists
|
41 |
+
existing = db.query(BetaApplication).filter(
|
42 |
+
BetaApplication.email == application.email
|
43 |
+
).first()
|
44 |
+
|
45 |
+
if existing:
|
46 |
+
raise HTTPException(
|
47 |
+
status_code=400,
|
48 |
+
detail="Email already registered"
|
49 |
+
)
|
50 |
+
|
51 |
+
# Create new application
|
52 |
+
db_application = BetaApplication(
|
53 |
+
id=str(uuid.uuid4()),
|
54 |
+
email=application.email,
|
55 |
+
company=application.company,
|
56 |
+
use_case=application.useCase,
|
57 |
+
status=ApplicationStatus.PENDING
|
58 |
+
)
|
59 |
+
|
60 |
+
db.add(db_application)
|
61 |
+
db.commit()
|
62 |
+
db.refresh(db_application)
|
63 |
+
|
64 |
+
return db_application
|
65 |
+
|
66 |
+
@router.post("/verify")
|
67 |
+
def verify_application(
|
68 |
+
request: VerifyApplicationRequest,
|
69 |
+
db: Session = Depends(get_db)
|
70 |
+
):
|
71 |
+
application = db.query(BetaApplication).filter(
|
72 |
+
BetaApplication.id == request.application_id
|
73 |
+
).first()
|
74 |
+
|
75 |
+
if not application:
|
76 |
+
raise HTTPException(
|
77 |
+
status_code=404,
|
78 |
+
detail="Application not found"
|
79 |
+
)
|
80 |
+
|
81 |
+
if application.status != ApplicationStatus.APPROVED:
|
82 |
+
raise HTTPException(
|
83 |
+
status_code=403,
|
84 |
+
detail="Application not approved"
|
85 |
+
)
|
86 |
+
|
87 |
+
return {"message": "Access granted"}
|
app/api/data.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
2 |
+
from sqlalchemy.orm import Session
|
3 |
+
from app.core.database import get_db
|
4 |
+
from app.models.models import BetaApplication, ApplicationStatus
|
5 |
+
from app.services.data_generator import DataGenerator
|
6 |
+
from pydantic import BaseModel
|
7 |
+
from typing import List, Dict, Any
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
data_generator = DataGenerator()
|
11 |
+
|
12 |
+
class GenerateDataRequest(BaseModel):
|
13 |
+
count: int = 10
|
14 |
+
record_types: List[str] = ["lab_report", "clinical_note", "discharge_summary"]
|
15 |
+
|
16 |
+
class GenerateDataResponse(BaseModel):
|
17 |
+
records: List[Dict[str, Any]]
|
18 |
+
|
19 |
+
@router.post("/generate", response_model=GenerateDataResponse)
|
20 |
+
async def generate_data(
|
21 |
+
request: GenerateDataRequest,
|
22 |
+
db: Session = Depends(get_db)
|
23 |
+
):
|
24 |
+
# Verify application status
|
25 |
+
application = db.query(BetaApplication).filter(
|
26 |
+
BetaApplication.status == ApplicationStatus.APPROVED
|
27 |
+
).first()
|
28 |
+
|
29 |
+
if not application:
|
30 |
+
raise HTTPException(
|
31 |
+
status_code=403,
|
32 |
+
detail="No approved application found"
|
33 |
+
)
|
34 |
+
|
35 |
+
# Generate records
|
36 |
+
records = data_generator.generate_records(request.count, request.record_types)
|
37 |
+
|
38 |
+
return {"records": records}
|
app/api/generator.py
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException
|
2 |
+
from pydantic import BaseModel
|
3 |
+
from typing import List, Optional
|
4 |
+
import random
|
5 |
+
|
6 |
+
router = APIRouter()
|
7 |
+
|
8 |
+
class GenerationRequest(BaseModel):
|
9 |
+
recordType: str
|
10 |
+
quantity: int
|
11 |
+
|
12 |
+
class GeneratedRecord(BaseModel):
|
13 |
+
id: str
|
14 |
+
content: str
|
15 |
+
recordType: str
|
16 |
+
|
17 |
+
# Templates for different record types
|
18 |
+
TEMPLATES = {
|
19 |
+
"Clinical Note": [
|
20 |
+
"Patient presents with {symptom}. Vital signs: BP {bp}, HR {hr}, Temp {temp}. Assessment: {assessment}. Plan: {plan}.",
|
21 |
+
"Follow-up visit for {condition}. Patient reports {symptom}. Current medications: {meds}. Plan: {plan}."
|
22 |
+
],
|
23 |
+
"Discharge Summary": [
|
24 |
+
"Patient admitted for {condition}. Treatment included {treatment}. Discharged on {date} with instructions to {instructions}.",
|
25 |
+
"Hospital course: {course}. Discharge medications: {meds}. Follow-up with {specialist} in {timeframe}."
|
26 |
+
],
|
27 |
+
"Lab Report": [
|
28 |
+
"Lab results for {test}: {result}. Reference range: {range}. Interpretation: {interpretation}.",
|
29 |
+
"Blood work shows {finding}. Additional tests recommended: {tests}."
|
30 |
+
],
|
31 |
+
"Prescription": [
|
32 |
+
"Prescribed {medication} {dose} {frequency} for {condition}. Duration: {duration}. Instructions: {instructions}.",
|
33 |
+
"Medication: {medication}. Dosage: {dose}. Take {frequency} with {food}. Refills: {refills}."
|
34 |
+
],
|
35 |
+
"Patient Intake": [
|
36 |
+
"New patient intake. Chief complaint: {complaint}. History: {history}. Allergies: {allergies}. Current medications: {meds}.",
|
37 |
+
"Patient demographics: {demographics}. Insurance: {insurance}. Emergency contact: {contact}."
|
38 |
+
]
|
39 |
+
}
|
40 |
+
|
41 |
+
# Placeholder data for template variables
|
42 |
+
PLACEHOLDERS = {
|
43 |
+
"symptom": ["headache", "fever", "cough", "fatigue", "nausea"],
|
44 |
+
"bp": ["120/80", "130/85", "118/75", "125/82"],
|
45 |
+
"hr": ["72", "80", "68", "75"],
|
46 |
+
"temp": ["98.6", "99.1", "98.2", "99.5"],
|
47 |
+
"assessment": ["stable", "improving", "requires monitoring", "critical"],
|
48 |
+
"plan": ["continue current treatment", "schedule follow-up", "refer to specialist", "discharge"],
|
49 |
+
"condition": ["hypertension", "diabetes", "asthma", "arthritis"],
|
50 |
+
"meds": ["aspirin", "metformin", "albuterol", "ibuprofen"],
|
51 |
+
"treatment": ["antibiotics", "physical therapy", "surgery", "bed rest"],
|
52 |
+
"date": ["2023-10-01", "2023-10-15", "2023-11-01"],
|
53 |
+
"instructions": ["rest", "take medications as prescribed", "follow up in 2 weeks", "avoid strenuous activity"],
|
54 |
+
"course": ["stable", "complicated", "uneventful", "critical"],
|
55 |
+
"specialist": ["cardiologist", "neurologist", "orthopedist", "dermatologist"],
|
56 |
+
"timeframe": ["1 week", "2 weeks", "1 month", "3 months"],
|
57 |
+
"test": ["CBC", "lipid panel", "glucose", "thyroid function"],
|
58 |
+
"result": ["normal", "elevated", "low", "critical"],
|
59 |
+
"range": ["10-20", "70-100", "3.5-5.0", "0.5-1.5"],
|
60 |
+
"interpretation": ["within normal limits", "requires follow-up", "abnormal", "critical"],
|
61 |
+
"finding": ["anemia", "hyperlipidemia", "hypoglycemia", "hyperthyroidism"],
|
62 |
+
"tests": ["MRI", "CT scan", "ultrasound", "biopsy"],
|
63 |
+
"medication": ["amoxicillin", "lisinopril", "atorvastatin", "levothyroxine"],
|
64 |
+
"dose": ["500mg", "10mg", "20mg", "50mcg"],
|
65 |
+
"frequency": ["once daily", "twice daily", "three times daily", "as needed"],
|
66 |
+
"duration": ["7 days", "30 days", "90 days", "indefinite"],
|
67 |
+
"refills": ["0", "1", "2", "3"],
|
68 |
+
"food": ["with food", "on an empty stomach", "with water", "with milk"],
|
69 |
+
"complaint": ["chest pain", "shortness of breath", "joint pain", "dizziness"],
|
70 |
+
"history": ["hypertension", "diabetes", "asthma", "arthritis"],
|
71 |
+
"allergies": ["penicillin", "sulfa", "latex", "none"],
|
72 |
+
"demographics": ["45-year-old male", "60-year-old female", "30-year-old male", "50-year-old female"],
|
73 |
+
"insurance": ["Blue Cross", "Aetna", "Medicare", "Medicaid"],
|
74 |
+
"contact": ["John Doe (555-123-4567)", "Jane Smith (555-987-6543)"]
|
75 |
+
}
|
76 |
+
|
77 |
+
def generate_record(record_type: str) -> GeneratedRecord:
|
78 |
+
if record_type not in TEMPLATES:
|
79 |
+
raise ValueError(f"Unsupported record type: {record_type}")
|
80 |
+
|
81 |
+
template = random.choice(TEMPLATES[record_type])
|
82 |
+
content = template.format(**{k: random.choice(v) for k, v in PLACEHOLDERS.items()})
|
83 |
+
|
84 |
+
return GeneratedRecord(
|
85 |
+
id=f"record-{random.randint(1000, 9999)}",
|
86 |
+
content=content,
|
87 |
+
recordType=record_type
|
88 |
+
)
|
89 |
+
|
90 |
+
@router.post("/generate", response_model=List[GeneratedRecord])
|
91 |
+
async def generate_records(request: GenerationRequest):
|
92 |
+
if request.quantity <= 0:
|
93 |
+
raise HTTPException(status_code=400, detail="Quantity must be positive")
|
94 |
+
if request.quantity > 100:
|
95 |
+
raise HTTPException(status_code=400, detail="Quantity cannot exceed 100")
|
96 |
+
|
97 |
+
records = [generate_record(request.recordType) for _ in range(request.quantity)]
|
98 |
+
return records
|
app/api/medical.py
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException
|
2 |
+
from pydantic import BaseModel
|
3 |
+
from typing import List, Dict, Any
|
4 |
+
from app.services.medical_generator import MedicalTextGenerator
|
5 |
+
|
6 |
+
router = APIRouter()
|
7 |
+
generator = MedicalTextGenerator()
|
8 |
+
|
9 |
+
class GenerateRequest(BaseModel):
|
10 |
+
count: int = 1
|
11 |
+
record_type: str = "clinical_note"
|
12 |
+
|
13 |
+
class GenerateResponse(BaseModel):
|
14 |
+
records: List[Dict[str, Any]]
|
15 |
+
|
16 |
+
@router.post("/generate", response_model=GenerateResponse)
|
17 |
+
async def generate_medical_text(request: GenerateRequest):
|
18 |
+
"""
|
19 |
+
Generate synthetic medical text records.
|
20 |
+
|
21 |
+
Args:
|
22 |
+
request: GenerateRequest containing count and record_type
|
23 |
+
|
24 |
+
Returns:
|
25 |
+
GenerateResponse containing the generated records
|
26 |
+
"""
|
27 |
+
try:
|
28 |
+
records = generator.batch_generate(request.count, request.record_type)
|
29 |
+
return GenerateResponse(records=records)
|
30 |
+
except ValueError as e:
|
31 |
+
raise HTTPException(status_code=400, detail=str(e))
|
32 |
+
except Exception as e:
|
33 |
+
raise HTTPException(status_code=500, detail=f"Error generating records: {str(e)}")
|
app/core/config.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic_settings import BaseSettings
|
2 |
+
from typing import Optional
|
3 |
+
|
4 |
+
class Settings(BaseSettings):
|
5 |
+
# API Settings
|
6 |
+
API_V1_STR: str = "/api/v1"
|
7 |
+
PROJECT_NAME: str = "Synthex"
|
8 |
+
|
9 |
+
# Security
|
10 |
+
SECRET_KEY: str = "your-secret-key-here" # Change this in production
|
11 |
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days
|
12 |
+
|
13 |
+
# Database
|
14 |
+
DATABASE_URL: str = "postgresql://neondb_owner:npg_tRvA0lD3GrZk@ep-lucky-rain-a8rbsfj4-pooler.eastus2.azure.neon.tech/neondb?sslmode=require"
|
15 |
+
|
16 |
+
# Admin
|
17 |
+
ADMIN_EMAIL: str = "[email protected]"
|
18 |
+
ADMIN_PASSWORD: str = "admin123" # Change this in production
|
19 |
+
|
20 |
+
class Config:
|
21 |
+
case_sensitive = True
|
22 |
+
|
23 |
+
settings = Settings()
|
app/core/database.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import create_engine
|
2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
3 |
+
from sqlalchemy.orm import sessionmaker
|
4 |
+
from app.core.config import settings
|
5 |
+
|
6 |
+
engine = create_engine(settings.DATABASE_URL)
|
7 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
8 |
+
|
9 |
+
Base = declarative_base()
|
10 |
+
|
11 |
+
# Dependency
|
12 |
+
def get_db():
|
13 |
+
db = SessionLocal()
|
14 |
+
try:
|
15 |
+
yield db
|
16 |
+
finally:
|
17 |
+
db.close()
|
18 |
+
|
19 |
+
# Create tables
|
20 |
+
def init_db():
|
21 |
+
Base.metadata.create_all(bind=engine)
|
app/main.py
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
from app.api import beta, admin, generator, auth, data, medical
|
4 |
+
from app.core.config import settings
|
5 |
+
from app.core.database import init_db, engine, Base
|
6 |
+
|
7 |
+
app = FastAPI(
|
8 |
+
title="Synthex API",
|
9 |
+
description="API for generating synthetic medical records",
|
10 |
+
version="1.0.0",
|
11 |
+
)
|
12 |
+
|
13 |
+
# Initialize database on startup
|
14 |
+
@app.on_event("startup")
|
15 |
+
async def startup_event():
|
16 |
+
init_db()
|
17 |
+
|
18 |
+
# Configure CORS
|
19 |
+
app.add_middleware(
|
20 |
+
CORSMiddleware,
|
21 |
+
allow_origins=["http://localhost:3000"], # Frontend URL
|
22 |
+
allow_credentials=True,
|
23 |
+
allow_methods=["*"],
|
24 |
+
allow_headers=["*"],
|
25 |
+
)
|
26 |
+
|
27 |
+
# Include routers
|
28 |
+
app.include_router(beta.router, prefix="/api/beta", tags=["beta"])
|
29 |
+
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
|
30 |
+
app.include_router(generator.router, prefix="/api/generator", tags=["generator"])
|
31 |
+
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
32 |
+
app.include_router(data.router, prefix="/api/data", tags=["data"])
|
33 |
+
app.include_router(medical.router, prefix="/api/medical", tags=["medical"])
|
34 |
+
|
35 |
+
@app.get("/")
|
36 |
+
async def root():
|
37 |
+
return {"message": "Welcome to Synthex API"}
|
38 |
+
|
39 |
+
# Create database tables
|
40 |
+
Base.metadata.create_all(bind=engine)
|
app/models/models.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import Column, String, DateTime, Enum
|
2 |
+
from sqlalchemy.sql import func
|
3 |
+
from app.core.database import Base
|
4 |
+
import enum
|
5 |
+
|
6 |
+
class ApplicationStatus(str, enum.Enum):
|
7 |
+
PENDING = "pending"
|
8 |
+
APPROVED = "approved"
|
9 |
+
REJECTED = "rejected"
|
10 |
+
|
11 |
+
class BetaApplication(Base):
|
12 |
+
__tablename__ = "beta_applications"
|
13 |
+
|
14 |
+
id = Column(String, primary_key=True, index=True)
|
15 |
+
email = Column(String, unique=True, index=True)
|
16 |
+
company = Column(String)
|
17 |
+
use_case = Column(String)
|
18 |
+
status = Column(Enum(ApplicationStatus), default=ApplicationStatus.PENDING)
|
19 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
20 |
+
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
app/routers/data.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException
|
2 |
+
from pydantic import BaseModel
|
3 |
+
from typing import List, Optional
|
4 |
+
from ..services.data_generator import DataGenerator
|
5 |
+
import logging
|
6 |
+
|
7 |
+
# Configure logging
|
8 |
+
logging.basicConfig(level=logging.DEBUG)
|
9 |
+
logger = logging.getLogger(__name__)
|
10 |
+
|
11 |
+
router = APIRouter()
|
12 |
+
generator = DataGenerator()
|
13 |
+
|
14 |
+
class GenerateRequest(BaseModel):
|
15 |
+
count: int
|
16 |
+
types: List[str]
|
17 |
+
export_format: Optional[str] = "JSON"
|
18 |
+
|
19 |
+
@router.post("/generate")
|
20 |
+
async def generate_records(request: GenerateRequest):
|
21 |
+
"""Generate synthetic medical records."""
|
22 |
+
try:
|
23 |
+
logger.debug(f"Received request: count={request.count}, types={request.types}, export_format={request.export_format}")
|
24 |
+
|
25 |
+
# Validate count
|
26 |
+
if request.count < 1 or request.count > 100:
|
27 |
+
raise HTTPException(status_code=400, detail="Count must be between 1 and 100")
|
28 |
+
|
29 |
+
# Validate types
|
30 |
+
valid_types = ["clinical_note", "discharge_summary", "lab_report", "prescription", "patient_intake"]
|
31 |
+
if not all(t in valid_types for t in request.types):
|
32 |
+
raise HTTPException(status_code=400, detail=f"Invalid record type. Must be one of: {', '.join(valid_types)}")
|
33 |
+
|
34 |
+
# Generate records
|
35 |
+
logger.debug("Generating records...")
|
36 |
+
records = generator.generate_records(count=request.count, types=request.types)
|
37 |
+
logger.debug(f"Generated {len(records)} records")
|
38 |
+
|
39 |
+
# Export if format specified
|
40 |
+
if request.export_format:
|
41 |
+
try:
|
42 |
+
logger.debug(f"Exporting records in {request.export_format} format")
|
43 |
+
exported_data = generator.export_records(records, request.export_format)
|
44 |
+
return {
|
45 |
+
"records": records,
|
46 |
+
"exported_data": exported_data,
|
47 |
+
"export_format": request.export_format
|
48 |
+
}
|
49 |
+
except ValueError as e:
|
50 |
+
logger.error(f"Export error: {str(e)}")
|
51 |
+
raise HTTPException(status_code=400, detail=str(e))
|
52 |
+
|
53 |
+
return {"records": records}
|
54 |
+
|
55 |
+
except Exception as e:
|
56 |
+
logger.error(f"Error generating records: {str(e)}", exc_info=True)
|
57 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/services/data_generator.py
ADDED
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Dict, Any
|
2 |
+
from datetime import datetime, timedelta
|
3 |
+
import random
|
4 |
+
import json
|
5 |
+
import csv
|
6 |
+
import io
|
7 |
+
|
8 |
+
class DataGenerator:
|
9 |
+
def __init__(self):
|
10 |
+
self.first_names = [
|
11 |
+
"John", "Jane", "Michael", "Emily", "David", "Sarah", "James", "Emma",
|
12 |
+
"Robert", "Olivia", "William", "Sophia", "Daniel", "Ava", "Matthew", "Isabella"
|
13 |
+
]
|
14 |
+
self.last_names = [
|
15 |
+
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
|
16 |
+
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas"
|
17 |
+
]
|
18 |
+
self.doctors = [
|
19 |
+
"Dr. Sarah Chen", "Dr. Michael Patel", "Dr. Emily Rodriguez", "Dr. James Wilson",
|
20 |
+
"Dr. Lisa Thompson", "Dr. Robert Kim", "Dr. Maria Garcia", "Dr. David Lee"
|
21 |
+
]
|
22 |
+
self.conditions = [
|
23 |
+
"Hypertension", "Type 2 Diabetes", "Asthma", "Arthritis", "Migraine",
|
24 |
+
"Anxiety", "Depression", "GERD", "Hypothyroidism", "Osteoporosis"
|
25 |
+
]
|
26 |
+
self.medications = [
|
27 |
+
"Lisinopril", "Metformin", "Albuterol", "Ibuprofen", "Sumatriptan",
|
28 |
+
"Sertraline", "Omeprazole", "Levothyroxine", "Alendronate", "Atorvastatin"
|
29 |
+
]
|
30 |
+
self.lab_tests = [
|
31 |
+
"Complete Blood Count", "Basic Metabolic Panel", "Lipid Panel", "Hemoglobin A1C",
|
32 |
+
"Thyroid Function Test", "Vitamin D Level", "Liver Function Test", "Urinalysis"
|
33 |
+
]
|
34 |
+
|
35 |
+
def generate_patient_info(self) -> Dict[str, Any]:
|
36 |
+
first_name = random.choice(self.first_names)
|
37 |
+
last_name = random.choice(self.last_names)
|
38 |
+
age = random.randint(18, 85)
|
39 |
+
gender = random.choice(["Male", "Female"])
|
40 |
+
return {
|
41 |
+
"patient_name": f"{first_name} {last_name}",
|
42 |
+
"age": age,
|
43 |
+
"gender": gender,
|
44 |
+
"doctor_name": random.choice(self.doctors),
|
45 |
+
"date": datetime.now().strftime("%Y-%m-%d")
|
46 |
+
}
|
47 |
+
|
48 |
+
def generate_clinical_note(self, metadata: Dict[str, Any]) -> str:
|
49 |
+
condition = random.choice(self.conditions)
|
50 |
+
medications = random.sample(self.medications, random.randint(1, 3))
|
51 |
+
return f"""CLINICAL NOTE
|
52 |
+
|
53 |
+
Patient: {metadata['patient_name']}
|
54 |
+
Date: {metadata['date']}
|
55 |
+
Provider: {metadata['doctor_name']}
|
56 |
+
|
57 |
+
Chief Complaint:
|
58 |
+
Patient presents with symptoms related to {condition.lower()}.
|
59 |
+
|
60 |
+
History of Present Illness:
|
61 |
+
Patient reports ongoing symptoms for the past {random.randint(1, 12)} months.
|
62 |
+
Symptoms include {', '.join(random.sample(['fatigue', 'pain', 'discomfort', 'anxiety', 'depression'], 2))}.
|
63 |
+
|
64 |
+
Assessment:
|
65 |
+
Primary diagnosis: {condition}
|
66 |
+
|
67 |
+
Plan:
|
68 |
+
1. Continue current medications: {', '.join(medications)}
|
69 |
+
2. Follow up in {random.randint(1, 3)} months
|
70 |
+
3. Recommended lifestyle modifications
|
71 |
+
4. Schedule routine lab work
|
72 |
+
|
73 |
+
Signed: {metadata['doctor_name']}"""
|
74 |
+
|
75 |
+
def generate_discharge_summary(self, metadata: Dict[str, Any]) -> str:
|
76 |
+
condition = random.choice(self.conditions)
|
77 |
+
medications = random.sample(self.medications, random.randint(1, 3))
|
78 |
+
admission_date = (datetime.now().replace(day=1) - timedelta(days=random.randint(1, 30))).strftime("%Y-%m-%d")
|
79 |
+
discharge_date = datetime.now().strftime("%Y-%m-%d")
|
80 |
+
return f"""DISCHARGE SUMMARY
|
81 |
+
|
82 |
+
Patient: {metadata['patient_name']}
|
83 |
+
Admission Date: {admission_date}
|
84 |
+
Discharge Date: {discharge_date}
|
85 |
+
Attending Physician: {metadata['doctor_name']}
|
86 |
+
|
87 |
+
Admission Diagnosis:
|
88 |
+
{condition}
|
89 |
+
|
90 |
+
Hospital Course:
|
91 |
+
Patient was admitted for management of {condition.lower()}.
|
92 |
+
Treatment included {', '.join(medications)}.
|
93 |
+
Patient showed significant improvement during hospitalization.
|
94 |
+
|
95 |
+
Discharge Medications:
|
96 |
+
{', '.join(medications)}
|
97 |
+
|
98 |
+
Discharge Instructions:
|
99 |
+
1. Follow up with primary care physician in 1 week
|
100 |
+
2. Continue medications as prescribed
|
101 |
+
3. Maintain healthy lifestyle
|
102 |
+
4. Call if symptoms worsen
|
103 |
+
|
104 |
+
Signed: {metadata['doctor_name']}"""
|
105 |
+
|
106 |
+
def generate_lab_report(self, metadata: Dict[str, Any]) -> str:
|
107 |
+
test = random.choice(self.lab_tests)
|
108 |
+
result = random.uniform(1.0, 10.0)
|
109 |
+
reference_range = "1.0 - 10.0"
|
110 |
+
status = "Normal" if 1.0 <= result <= 10.0 else "Abnormal"
|
111 |
+
return f"""LABORATORY REPORT
|
112 |
+
|
113 |
+
Patient: {metadata['patient_name']}
|
114 |
+
Date: {metadata['date']}
|
115 |
+
Ordering Physician: {metadata['doctor_name']}
|
116 |
+
|
117 |
+
Test: {test}
|
118 |
+
Result: {result:.1f}
|
119 |
+
Reference Range: {reference_range}
|
120 |
+
Status: {status}
|
121 |
+
|
122 |
+
Comments:
|
123 |
+
Results have been reviewed and interpreted by the laboratory director.
|
124 |
+
Please contact the laboratory if you have any questions.
|
125 |
+
|
126 |
+
Signed: Laboratory Director"""
|
127 |
+
|
128 |
+
def generate_prescription(self, metadata: Dict[str, Any]) -> str:
|
129 |
+
medication = random.choice(self.medications)
|
130 |
+
dosage = f"{random.randint(1, 10)}mg"
|
131 |
+
frequency = random.choice(["once daily", "twice daily", "three times daily", "as needed"])
|
132 |
+
quantity = random.randint(30, 90)
|
133 |
+
refills = random.randint(0, 3)
|
134 |
+
return f"""PRESCRIPTION
|
135 |
+
|
136 |
+
Patient: {metadata['patient_name']}
|
137 |
+
Date: {metadata['date']}
|
138 |
+
Prescribing Physician: {metadata['doctor_name']}
|
139 |
+
|
140 |
+
Medication: {medication}
|
141 |
+
Dosage: {dosage}
|
142 |
+
Frequency: {frequency}
|
143 |
+
Quantity: {quantity}
|
144 |
+
Refills: {refills}
|
145 |
+
|
146 |
+
Instructions:
|
147 |
+
Take {dosage} {frequency} with food.
|
148 |
+
Store at room temperature.
|
149 |
+
Call if side effects occur.
|
150 |
+
|
151 |
+
Signed: {metadata['doctor_name']}"""
|
152 |
+
|
153 |
+
def generate_patient_intake(self, metadata: Dict[str, Any]) -> str:
|
154 |
+
conditions = random.sample(self.conditions, random.randint(1, 3))
|
155 |
+
medications = random.sample(self.medications, random.randint(1, 3))
|
156 |
+
allergies = random.sample(["Penicillin", "Sulfa", "Latex", "Peanuts"], random.randint(0, 2))
|
157 |
+
return f"""PATIENT INTAKE FORM
|
158 |
+
|
159 |
+
Patient Information:
|
160 |
+
Name: {metadata['patient_name']}
|
161 |
+
Age: {metadata['age']}
|
162 |
+
Gender: {metadata['gender']}
|
163 |
+
Date: {metadata['date']}
|
164 |
+
|
165 |
+
Medical History:
|
166 |
+
Current Conditions: {', '.join(conditions)}
|
167 |
+
Current Medications: {', '.join(medications)}
|
168 |
+
Allergies: {', '.join(allergies) if allergies else 'None reported'}
|
169 |
+
|
170 |
+
Family History:
|
171 |
+
- Mother: {random.choice(self.conditions)}
|
172 |
+
- Father: {random.choice(self.conditions)}
|
173 |
+
|
174 |
+
Social History:
|
175 |
+
- Smoking: {random.choice(['Never', 'Former', 'Current'])}
|
176 |
+
- Alcohol: {random.choice(['None', 'Occasional', 'Regular'])}
|
177 |
+
- Exercise: {random.choice(['None', 'Occasional', 'Regular'])}
|
178 |
+
|
179 |
+
Review of Systems:
|
180 |
+
- General: No significant findings
|
181 |
+
- Cardiovascular: No significant findings
|
182 |
+
- Respiratory: No significant findings
|
183 |
+
- Gastrointestinal: No significant findings
|
184 |
+
- Neurological: No significant findings
|
185 |
+
|
186 |
+
Signed: {metadata['patient_name']}"""
|
187 |
+
|
188 |
+
def generate_records(self, count: int, types: List[str]) -> List[Dict[str, Any]]:
|
189 |
+
if not types:
|
190 |
+
raise ValueError("At least one record type must be specified")
|
191 |
+
records = []
|
192 |
+
type_generators = {
|
193 |
+
"clinical_note": self.generate_clinical_note,
|
194 |
+
"discharge_summary": self.generate_discharge_summary,
|
195 |
+
"lab_report": self.generate_lab_report,
|
196 |
+
"prescription": self.generate_prescription,
|
197 |
+
"patient_intake": self.generate_patient_intake
|
198 |
+
}
|
199 |
+
invalid_types = [t for t in types if t not in type_generators]
|
200 |
+
if invalid_types:
|
201 |
+
raise ValueError(f"Invalid record types: {', '.join(invalid_types)}")
|
202 |
+
for _ in range(count):
|
203 |
+
record_type = random.choice(types)
|
204 |
+
metadata = self.generate_patient_info()
|
205 |
+
content = type_generators[record_type](metadata)
|
206 |
+
records.append({
|
207 |
+
"id": f"REC-{random.randint(10000, 99999)}",
|
208 |
+
"type": record_type,
|
209 |
+
"content": content,
|
210 |
+
"generated_at": datetime.now().isoformat(),
|
211 |
+
"metadata": metadata
|
212 |
+
})
|
213 |
+
return records
|
214 |
+
|
215 |
+
def export_records(self, records: List[Dict[str, Any]], format: str) -> str:
|
216 |
+
if format == "JSON":
|
217 |
+
return json.dumps(records, indent=2)
|
218 |
+
elif format == "CSV":
|
219 |
+
output = io.StringIO()
|
220 |
+
writer = csv.writer(output)
|
221 |
+
writer.writerow(["ID", "Type", "Content", "Generated At", "Patient Name", "Age", "Gender", "Doctor Name", "Date"])
|
222 |
+
for record in records:
|
223 |
+
writer.writerow([
|
224 |
+
record["id"],
|
225 |
+
record["type"],
|
226 |
+
record["content"],
|
227 |
+
record["generated_at"],
|
228 |
+
record["metadata"]["patient_name"],
|
229 |
+
record["metadata"]["age"],
|
230 |
+
record["metadata"]["gender"],
|
231 |
+
record["metadata"]["doctor_name"],
|
232 |
+
record["metadata"]["date"]
|
233 |
+
])
|
234 |
+
return output.getvalue()
|
235 |
+
elif format == "TXT":
|
236 |
+
output = []
|
237 |
+
for record in records:
|
238 |
+
output.append(f"Record ID: {record['id']}")
|
239 |
+
output.append(f"Type: {record['type']}")
|
240 |
+
output.append(f"Generated At: {record['generated_at']}")
|
241 |
+
output.append(f"Patient: {record['metadata']['patient_name']}")
|
242 |
+
output.append(f"Age: {record['metadata']['age']}")
|
243 |
+
output.append(f"Gender: {record['metadata']['gender']}")
|
244 |
+
output.append(f"Doctor: {record['metadata']['doctor_name']}")
|
245 |
+
output.append(f"Date: {record['metadata']['date']}")
|
246 |
+
output.append("\nContent:")
|
247 |
+
output.append(record['content'])
|
248 |
+
output.append("\n" + "="*80 + "\n")
|
249 |
+
return "\n".join(output)
|
250 |
+
else:
|
251 |
+
raise ValueError(f"Unsupported export format: {format}")
|
app/services/medical_generator.py
ADDED
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import time
|
3 |
+
from typing import List, Dict, Any
|
4 |
+
from transformers import pipeline, AutoModelForCausalLM, AutoTokenizer
|
5 |
+
from datetime import datetime
|
6 |
+
|
7 |
+
class MedicalTextGenerator:
|
8 |
+
def __init__(self):
|
9 |
+
# Initialize with a medical-specific model
|
10 |
+
self.model_name = "emilyalsentzer/Bio_ClinicalBERT"
|
11 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
12 |
+
self.model = AutoModelForCausalLM.from_pretrained(self.model_name)
|
13 |
+
|
14 |
+
# Create text generation pipeline
|
15 |
+
self.generator = pipeline(
|
16 |
+
"text-generation",
|
17 |
+
model=self.model,
|
18 |
+
tokenizer=self.tokenizer,
|
19 |
+
max_length=512,
|
20 |
+
num_return_sequences=1
|
21 |
+
)
|
22 |
+
|
23 |
+
def _generate_text(self, prompt: str) -> str:
|
24 |
+
"""Generate text using the model."""
|
25 |
+
try:
|
26 |
+
# Generate text
|
27 |
+
outputs = self.generator(
|
28 |
+
prompt,
|
29 |
+
max_length=512,
|
30 |
+
num_return_sequences=1,
|
31 |
+
temperature=0.7,
|
32 |
+
top_p=0.9,
|
33 |
+
do_sample=True
|
34 |
+
)
|
35 |
+
|
36 |
+
# Extract generated text
|
37 |
+
generated_text = outputs[0]['generated_text']
|
38 |
+
|
39 |
+
# Clean up the text
|
40 |
+
generated_text = generated_text.replace(prompt, "").strip()
|
41 |
+
return generated_text
|
42 |
+
except Exception as e:
|
43 |
+
print(f"Error generating text: {e}")
|
44 |
+
return ""
|
45 |
+
|
46 |
+
def generate_clinical_note(self, template_type: str = "general") -> str:
|
47 |
+
"""Generate a clinical note based on the template type."""
|
48 |
+
prompt = f"""
|
49 |
+
Generate a realistic but completely fictional clinical note for {template_type}.
|
50 |
+
Include:
|
51 |
+
- Chief complaint
|
52 |
+
- History of present illness
|
53 |
+
- Physical exam
|
54 |
+
- Assessment
|
55 |
+
- Plan
|
56 |
+
|
57 |
+
Make it medically accurate but use fictional patient details.
|
58 |
+
Follow HIPAA guidelines and ensure no real PII is included.
|
59 |
+
"""
|
60 |
+
return self._generate_text(prompt)
|
61 |
+
|
62 |
+
def generate_discharge_summary(self) -> str:
|
63 |
+
"""Generate a discharge summary."""
|
64 |
+
prompt = """
|
65 |
+
Generate a realistic but completely fictional hospital discharge summary.
|
66 |
+
Include:
|
67 |
+
- Admission date
|
68 |
+
- Discharge date
|
69 |
+
- Reason for admission
|
70 |
+
- Hospital course
|
71 |
+
- Discharge medications
|
72 |
+
- Discharge instructions
|
73 |
+
- Follow-up plan
|
74 |
+
|
75 |
+
Make it medically accurate but use fictional patient details.
|
76 |
+
Follow HIPAA guidelines and ensure no real PII is included.
|
77 |
+
"""
|
78 |
+
return self._generate_text(prompt)
|
79 |
+
|
80 |
+
def generate_lab_report(self) -> str:
|
81 |
+
"""Generate a laboratory report."""
|
82 |
+
prompt = """
|
83 |
+
Generate a realistic but completely fictional laboratory report.
|
84 |
+
Include:
|
85 |
+
- Test name
|
86 |
+
- Results
|
87 |
+
- Reference ranges
|
88 |
+
- Interpretation
|
89 |
+
- Comments
|
90 |
+
|
91 |
+
Make it medically accurate but use fictional patient details.
|
92 |
+
Follow HIPAA guidelines and ensure no real PII is included.
|
93 |
+
"""
|
94 |
+
return self._generate_text(prompt)
|
95 |
+
|
96 |
+
def generate_prescription(self) -> str:
|
97 |
+
"""Generate a prescription."""
|
98 |
+
prompt = """
|
99 |
+
Generate a realistic but completely fictional prescription.
|
100 |
+
Include:
|
101 |
+
- Medication name
|
102 |
+
- Dosage
|
103 |
+
- Frequency
|
104 |
+
- Duration
|
105 |
+
- Special instructions
|
106 |
+
|
107 |
+
Make it medically accurate but use fictional patient details.
|
108 |
+
Follow HIPAA guidelines and ensure no real PII is included.
|
109 |
+
"""
|
110 |
+
return self._generate_text(prompt)
|
111 |
+
|
112 |
+
def batch_generate(self, count: int, record_type: str) -> List[Dict[str, Any]]:
|
113 |
+
"""Generate multiple records of the specified type."""
|
114 |
+
records = []
|
115 |
+
type_generators = {
|
116 |
+
"clinical_note": self.generate_clinical_note,
|
117 |
+
"discharge_summary": self.generate_discharge_summary,
|
118 |
+
"lab_report": self.generate_lab_report,
|
119 |
+
"prescription": self.generate_prescription
|
120 |
+
}
|
121 |
+
|
122 |
+
if record_type not in type_generators:
|
123 |
+
raise ValueError(f"Unsupported record type: {record_type}")
|
124 |
+
|
125 |
+
generator = type_generators[record_type]
|
126 |
+
|
127 |
+
for i in range(count):
|
128 |
+
content = generator()
|
129 |
+
records.append({
|
130 |
+
"id": f"REC-{datetime.now().strftime('%Y%m%d%H%M%S')}-{i+1}",
|
131 |
+
"type": record_type,
|
132 |
+
"content": content,
|
133 |
+
"generated_at": datetime.now().isoformat()
|
134 |
+
})
|
135 |
+
time.sleep(1) # Small delay to prevent overload
|
136 |
+
|
137 |
+
return records
|
create_tables.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.core.database import Base, engine
|
2 |
+
from app.models.models import BetaApplication
|
3 |
+
from pydantic import BaseModel, Field
|
4 |
+
from datetime import datetime
|
5 |
+
|
6 |
+
# Create the tables
|
7 |
+
Base.metadata.create_all(engine)
|
8 |
+
|
9 |
+
print("Tables created successfully.")
|
10 |
+
|
11 |
+
class BetaApplicationSchema(BaseModel):
|
12 |
+
id: str
|
13 |
+
email: str
|
14 |
+
company: str
|
15 |
+
useCase: str = Field(..., alias="use_case")
|
16 |
+
status: str
|
17 |
+
createdAt: datetime = Field(..., alias="created_at")
|
18 |
+
updatedAt: datetime | None = Field(None, alias="updated_at")
|
19 |
+
|
20 |
+
class Config:
|
21 |
+
orm_mode = True
|
22 |
+
allow_population_by_field_name = True
|
init_db.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.core.database import Base, engine
|
2 |
+
from app.models.models import BetaApplication
|
3 |
+
|
4 |
+
def init_db():
|
5 |
+
Base.metadata.create_all(bind=engine)
|
6 |
+
|
7 |
+
if __name__ == "__main__":
|
8 |
+
print("Creating database tables...")
|
9 |
+
init_db()
|
10 |
+
print("Database tables created successfully!")
|
main.py
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
import os
|
5 |
+
|
6 |
+
# Load environment variables
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
# Create FastAPI app
|
10 |
+
app = FastAPI(
|
11 |
+
title="Synthex API",
|
12 |
+
description="Backend API for Synthex Medical Text Generator",
|
13 |
+
version="1.0.0"
|
14 |
+
)
|
15 |
+
|
16 |
+
# Configure CORS
|
17 |
+
app.add_middleware(
|
18 |
+
CORSMiddleware,
|
19 |
+
allow_origins=["http://localhost:3000"], # Frontend URL
|
20 |
+
allow_credentials=True,
|
21 |
+
allow_methods=["*"],
|
22 |
+
allow_headers=["*"],
|
23 |
+
)
|
24 |
+
|
25 |
+
# Health check endpoint
|
26 |
+
@app.get("/")
|
27 |
+
async def root():
|
28 |
+
return {"status": "healthy", "message": "Synthex API is running"}
|
29 |
+
|
30 |
+
# Import and include routers
|
31 |
+
from app.api import beta, admin, generator
|
32 |
+
|
33 |
+
app.include_router(beta.router, prefix="/api/beta", tags=["beta"])
|
34 |
+
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
|
35 |
+
app.include_router(generator.router, prefix="/api/generator", tags=["generator"])
|
36 |
+
|
37 |
+
if __name__ == "__main__":
|
38 |
+
import uvicorn
|
39 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
pyproject.toml
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[tool.black]
|
2 |
+
line-length = 88
|
3 |
+
target-version = ['py38']
|
4 |
+
include = '\.pyi?$'
|
5 |
+
exclude = '''
|
6 |
+
/(
|
7 |
+
\.git
|
8 |
+
| \.hg
|
9 |
+
| \.mypy_cache
|
10 |
+
| \.tox
|
11 |
+
| \.venv
|
12 |
+
| _build
|
13 |
+
| buck-out
|
14 |
+
| build
|
15 |
+
| dist
|
16 |
+
)/
|
17 |
+
'''
|
recreate_tables.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import text
|
2 |
+
from app.core.database import Base, engine
|
3 |
+
from app.models.models import BetaApplication
|
4 |
+
|
5 |
+
# Drop all tables with CASCADE
|
6 |
+
with engine.connect() as conn:
|
7 |
+
conn.execute(text("DROP SCHEMA public CASCADE;"))
|
8 |
+
conn.execute(text("CREATE SCHEMA public;"))
|
9 |
+
conn.commit()
|
10 |
+
|
11 |
+
# Create all tables
|
12 |
+
Base.metadata.create_all(engine)
|
13 |
+
|
14 |
+
print("Tables recreated successfully.")
|
requirements-dev.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
-r requirements.txt
|
2 |
+
pytest==7.4.3
|
3 |
+
pytest-cov==4.1.0
|
4 |
+
black==23.11.0
|
5 |
+
flake8==6.1.0
|
6 |
+
mypy==1.7.1
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit>=1.24.0
|
2 |
+
transformers>=4.30.0
|
3 |
+
torch>=2.0.0
|
4 |
+
pydantic>=1.8.0
|
5 |
+
python-dotenv>=0.19.0
|
6 |
+
fastapi>=0.68.0
|
7 |
+
uvicorn[standard]>=0.15.0
|
8 |
+
sqlalchemy>=1.4.0
|
9 |
+
python-multipart>=0.0.5
|
run_linters.py
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
import sys
|
3 |
+
import os
|
4 |
+
import platform
|
5 |
+
|
6 |
+
def run_black():
|
7 |
+
print("\nRunning Black...")
|
8 |
+
try:
|
9 |
+
subprocess.run(
|
10 |
+
["black", "app/", "tests/"],
|
11 |
+
check=True
|
12 |
+
)
|
13 |
+
print("Black completed successfully!")
|
14 |
+
except subprocess.CalledProcessError:
|
15 |
+
print("Black found issues!")
|
16 |
+
sys.exit(1)
|
17 |
+
|
18 |
+
def run_flake8():
|
19 |
+
print("\nRunning Flake8...")
|
20 |
+
try:
|
21 |
+
subprocess.run(
|
22 |
+
["flake8", "app/", "tests/"],
|
23 |
+
check=True
|
24 |
+
)
|
25 |
+
print("Flake8 completed successfully!")
|
26 |
+
except subprocess.CalledProcessError:
|
27 |
+
print("Flake8 found issues!")
|
28 |
+
sys.exit(1)
|
29 |
+
|
30 |
+
def run_mypy():
|
31 |
+
print("\nRunning MyPy...")
|
32 |
+
try:
|
33 |
+
subprocess.run(
|
34 |
+
["mypy", "app/", "tests/"],
|
35 |
+
check=True
|
36 |
+
)
|
37 |
+
print("MyPy completed successfully!")
|
38 |
+
except subprocess.CalledProcessError:
|
39 |
+
print("MyPy found issues!")
|
40 |
+
sys.exit(1)
|
41 |
+
|
42 |
+
def main():
|
43 |
+
print("Running linters...")
|
44 |
+
|
45 |
+
run_black()
|
46 |
+
run_flake8()
|
47 |
+
run_mypy()
|
48 |
+
|
49 |
+
print("\nAll linters passed successfully!")
|
50 |
+
|
51 |
+
if __name__ == "__main__":
|
52 |
+
main()
|
run_tests.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
import sys
|
3 |
+
import os
|
4 |
+
import platform
|
5 |
+
|
6 |
+
def run_tests():
|
7 |
+
print("Running tests...")
|
8 |
+
|
9 |
+
if platform.system() == "Windows":
|
10 |
+
python = "venv/Scripts/python"
|
11 |
+
else:
|
12 |
+
python = "venv/bin/python"
|
13 |
+
|
14 |
+
try:
|
15 |
+
subprocess.run(
|
16 |
+
[python, "-m", "pytest", "tests/", "-v"],
|
17 |
+
check=True
|
18 |
+
)
|
19 |
+
print("\nAll tests passed successfully!")
|
20 |
+
except subprocess.CalledProcessError:
|
21 |
+
print("\nSome tests failed!")
|
22 |
+
sys.exit(1)
|
23 |
+
|
24 |
+
if __name__ == "__main__":
|
25 |
+
run_tests()
|
setup.cfg
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[flake8]
|
2 |
+
max-line-length = 88
|
3 |
+
extend-ignore = E203
|
4 |
+
exclude = .git,__pycache__,build,dist
|
5 |
+
|
6 |
+
[mypy]
|
7 |
+
python_version = 3.8
|
8 |
+
warn_return_any = True
|
9 |
+
warn_unused_configs = True
|
10 |
+
disallow_untyped_defs = True
|
11 |
+
disallow_incomplete_defs = True
|
12 |
+
check_untyped_defs = True
|
13 |
+
disallow_untyped_decorators = True
|
14 |
+
no_implicit_optional = True
|
15 |
+
warn_redundant_casts = True
|
16 |
+
warn_unused_ignores = True
|
17 |
+
warn_no_return = True
|
18 |
+
warn_unreachable = True
|
19 |
+
|
20 |
+
[coverage:run]
|
21 |
+
source = app
|
22 |
+
omit = tests/*
|
23 |
+
|
24 |
+
[coverage:report]
|
25 |
+
exclude_lines =
|
26 |
+
pragma: no cover
|
27 |
+
def __repr__
|
28 |
+
raise NotImplementedError
|
29 |
+
if __name__ == .__main__.:
|
30 |
+
pass
|
31 |
+
raise ImportError
|
tests/test_api.py
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi.testclient import TestClient
|
2 |
+
from app.main import app
|
3 |
+
from app.core.database import Base, engine
|
4 |
+
from app.models.models import BetaApplication
|
5 |
+
import pytest
|
6 |
+
|
7 |
+
client = TestClient(app)
|
8 |
+
|
9 |
+
@pytest.fixture(autouse=True)
|
10 |
+
def setup_database():
|
11 |
+
Base.metadata.create_all(bind=engine)
|
12 |
+
yield
|
13 |
+
Base.metadata.drop_all(bind=engine)
|
14 |
+
|
15 |
+
def test_create_application():
|
16 |
+
response = client.post(
|
17 |
+
"/api/beta/apply",
|
18 |
+
json={
|
19 |
+
"email": "[email protected]",
|
20 |
+
"company": "Test Company",
|
21 |
+
"useCase": "Testing"
|
22 |
+
}
|
23 |
+
)
|
24 |
+
assert response.status_code == 200
|
25 |
+
data = response.json()
|
26 |
+
assert data["email"] == "[email protected]"
|
27 |
+
assert data["company"] == "Test Company"
|
28 |
+
assert data["useCase"] == "Testing"
|
29 |
+
assert data["status"] == "pending"
|
30 |
+
|
31 |
+
def test_create_duplicate_application():
|
32 |
+
# Create first application
|
33 |
+
client.post(
|
34 |
+
"/api/beta/apply",
|
35 |
+
json={
|
36 |
+
"email": "[email protected]",
|
37 |
+
"company": "Test Company",
|
38 |
+
"useCase": "Testing"
|
39 |
+
}
|
40 |
+
)
|
41 |
+
|
42 |
+
# Try to create duplicate
|
43 |
+
response = client.post(
|
44 |
+
"/api/beta/apply",
|
45 |
+
json={
|
46 |
+
"email": "[email protected]",
|
47 |
+
"company": "Another Company",
|
48 |
+
"useCase": "Another Use Case"
|
49 |
+
}
|
50 |
+
)
|
51 |
+
assert response.status_code == 400
|
52 |
+
assert response.json()["detail"] == "Email already registered"
|
53 |
+
|
54 |
+
def test_verify_application():
|
55 |
+
# Create application
|
56 |
+
response = client.post(
|
57 |
+
"/api/beta/apply",
|
58 |
+
json={
|
59 |
+
"email": "[email protected]",
|
60 |
+
"company": "Test Company",
|
61 |
+
"useCase": "Testing"
|
62 |
+
}
|
63 |
+
)
|
64 |
+
application_id = response.json()["id"]
|
65 |
+
|
66 |
+
# Try to verify unapproved application
|
67 |
+
response = client.post(
|
68 |
+
"/api/beta/verify",
|
69 |
+
json={"application_id": application_id}
|
70 |
+
)
|
71 |
+
assert response.status_code == 403
|
72 |
+
assert response.json()["detail"] == "Application not approved"
|
73 |
+
|
74 |
+
def test_admin_login():
|
75 |
+
response = client.post(
|
76 |
+
"/api/admin/login",
|
77 |
+
data={
|
78 |
+
"username": "[email protected]",
|
79 |
+
"password": "admin123"
|
80 |
+
}
|
81 |
+
)
|
82 |
+
assert response.status_code == 200
|
83 |
+
data = response.json()
|
84 |
+
assert "access_token" in data
|
85 |
+
assert data["token_type"] == "bearer"
|
86 |
+
|
87 |
+
def test_admin_login_invalid_credentials():
|
88 |
+
response = client.post(
|
89 |
+
"/api/admin/login",
|
90 |
+
data={
|
91 |
+
"username": "[email protected]",
|
92 |
+
"password": "wrong_password"
|
93 |
+
}
|
94 |
+
)
|
95 |
+
assert response.status_code == 401
|
96 |
+
assert response.json()["detail"] == "Incorrect email or password"
|
97 |
+
|
98 |
+
def test_get_applications_unauthorized():
|
99 |
+
response = client.get("/api/admin/applications")
|
100 |
+
assert response.status_code == 401
|
101 |
+
assert response.json()["detail"] == "Not authenticated"
|
102 |
+
|
103 |
+
def test_update_application():
|
104 |
+
# Create application
|
105 |
+
response = client.post(
|
106 |
+
"/api/beta/apply",
|
107 |
+
json={
|
108 |
+
"email": "[email protected]",
|
109 |
+
"company": "Test Company",
|
110 |
+
"useCase": "Testing"
|
111 |
+
}
|
112 |
+
)
|
113 |
+
application_id = response.json()["id"]
|
114 |
+
|
115 |
+
# Login as admin
|
116 |
+
login_response = client.post(
|
117 |
+
"/api/admin/login",
|
118 |
+
data={
|
119 |
+
"username": "[email protected]",
|
120 |
+
"password": "admin123"
|
121 |
+
}
|
122 |
+
)
|
123 |
+
token = login_response.json()["access_token"]
|
124 |
+
|
125 |
+
# Update application
|
126 |
+
response = client.patch(
|
127 |
+
f"/api/admin/applications/{application_id}",
|
128 |
+
json={"status": "approved"},
|
129 |
+
headers={"Authorization": f"Bearer {token}"}
|
130 |
+
)
|
131 |
+
assert response.status_code == 200
|
132 |
+
assert response.json()["message"] == "Application updated successfully"
|
133 |
+
|
134 |
+
# Verify application
|
135 |
+
response = client.post(
|
136 |
+
"/api/beta/verify",
|
137 |
+
json={"application_id": application_id}
|
138 |
+
)
|
139 |
+
assert response.status_code == 200
|
140 |
+
assert response.json()["message"] == "Access granted"
|
tests/test_integration.py
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pytest
|
2 |
+
from fastapi.testclient import TestClient
|
3 |
+
from app.main import app
|
4 |
+
|
5 |
+
client = TestClient(app)
|
6 |
+
|
7 |
+
def test_beta_verification():
|
8 |
+
response = client.post("/api/beta/verify", json={"application_id": "test123"})
|
9 |
+
assert response.status_code == 200
|
10 |
+
assert response.json()["message"] == "Access granted"
|
11 |
+
|
12 |
+
def test_jwt_authentication():
|
13 |
+
response = client.post("/api/auth/token", data={"username": "[email protected]", "password": "admin123"})
|
14 |
+
assert response.status_code == 200
|
15 |
+
assert "access_token" in response.json()
|
16 |
+
|
17 |
+
def test_record_generation():
|
18 |
+
# First, get a token
|
19 |
+
token_response = client.post("/api/auth/token", data={"username": "[email protected]", "password": "admin123"})
|
20 |
+
token = token_response.json()["access_token"]
|
21 |
+
|
22 |
+
# Then, generate records
|
23 |
+
response = client.post(
|
24 |
+
"/api/generator/generate",
|
25 |
+
json={"recordType": "Clinical Note", "quantity": 5},
|
26 |
+
headers={"Authorization": f"Bearer {token}"}
|
27 |
+
)
|
28 |
+
assert response.status_code == 200
|
29 |
+
assert len(response.json()) == 5
|