Spaces:
Running
Running
AILIXIR Bot commited on
Commit ·
907b200
1
Parent(s): c1aa524
Auto-sync: 201d269f287526269b8ffded5f450ff36b46c740
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .editorconfig +18 -0
- .env.docker +36 -0
- .env.example +16 -0
- .env.hf +37 -0
- .gitattributes +11 -35
- .github/workflows/api-tests-hf.yml +63 -0
- .github/workflows/api-tests.yml +135 -0
- .github/workflows/e2e-md-test.yml +139 -0
- .github/workflows/integration-test-hf.yml +492 -0
- .github/workflows/integration-test.yml +553 -0
- .github/workflows/keep-hf-alive.yml +21 -0
- .github/workflows/md-simulation-health.yml +117 -0
- .github/workflows/push-models-to-hf.yml +31 -0
- .github/workflows/sync-hf-all.yml +232 -0
- .gitignore +37 -0
- .scribe/.filehashes +4 -0
- .scribe/auth.md +3 -0
- .scribe/endpoints.cache/00.yaml +1503 -0
- .scribe/endpoints/00.yaml +1501 -0
- .scribe/endpoints/custom.0.yaml +53 -0
- .scribe/intro.md +13 -0
- API.md +0 -0
- ARCHITECTURE.md +224 -0
- CHANGELOG_SQLITE_HF.md +214 -0
- DOCKER.md +63 -0
- DOCKER_FIXES.md +231 -0
- DOCUMENTATION_STRUCTURE.md +449 -0
- Dockerfile +115 -0
- HF_SPACES_GUIDE.md +277 -0
- HUGGINGFACE_FLOW.md +186 -0
- README.md +16 -36
- README_HF.md +24 -0
- __pycache__/app.cpython-310.pyc +0 -0
- _ide_helper.php +0 -0
- _ide_helper_models.php +774 -0
- ailixir-api-tests.zip +0 -0
- app.py +0 -736
- app.py.backup_before_final_ui +0 -754
- app.py.backup_old_tabs +0 -754
- app/Helpers/OtpHelper.php +28 -0
- app/Http/Controllers/Api/AdmetController.php +29 -0
- app/Http/Controllers/Api/AiServicesIntegrationController.php +121 -0
- app/Http/Controllers/Api/AuthController.php +115 -0
- app/Http/Controllers/Api/AwardController.php +52 -0
- app/Http/Controllers/Api/BaseController.php +13 -0
- app/Http/Controllers/Api/ChemicalSearchController.php +74 -0
- app/Http/Controllers/Api/ChemistryController.php +524 -0
- app/Http/Controllers/Api/ConvertSmilesController.php +180 -0
- app/Http/Controllers/Api/DockingController.php +214 -0
- app/Http/Controllers/Api/EmailVerificationController.php +85 -0
.editorconfig
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
root = true
|
| 2 |
+
|
| 3 |
+
[*]
|
| 4 |
+
charset = utf-8
|
| 5 |
+
end_of_line = lf
|
| 6 |
+
indent_size = 4
|
| 7 |
+
indent_style = space
|
| 8 |
+
insert_final_newline = true
|
| 9 |
+
trim_trailing_whitespace = true
|
| 10 |
+
|
| 11 |
+
[*.md]
|
| 12 |
+
trim_trailing_whitespace = false
|
| 13 |
+
|
| 14 |
+
[*.{yml,yaml}]
|
| 15 |
+
indent_size = 2
|
| 16 |
+
|
| 17 |
+
[compose.yaml]
|
| 18 |
+
indent_size = 4
|
.env.docker
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
APP_NAME=AILIXIR
|
| 2 |
+
APP_ENV=production
|
| 3 |
+
APP_KEY=base64:CHANGEME_GENERATE_ON_FIRST_BOOT=
|
| 4 |
+
APP_DEBUG=false
|
| 5 |
+
APP_URL=http://localhost:8080
|
| 6 |
+
|
| 7 |
+
LOG_CHANNEL=stderr
|
| 8 |
+
LOG_LEVEL=info
|
| 9 |
+
|
| 10 |
+
DB_CONNECTION=sqlite
|
| 11 |
+
DB_DATABASE=/var/www/html/database/ailixir.sqlite
|
| 12 |
+
|
| 13 |
+
SESSION_DRIVER=database
|
| 14 |
+
SESSION_LIFETIME=120
|
| 15 |
+
|
| 16 |
+
CACHE_STORE=database
|
| 17 |
+
QUEUE_CONNECTION=database
|
| 18 |
+
|
| 19 |
+
# AI microservices (Docker internal DNS)
|
| 20 |
+
GENERATION_SERVICE_URL=http://generation:8000
|
| 21 |
+
CHEMICAL_AI_URL=http://chemical-rag:5000
|
| 22 |
+
ADMET_AI_URL=http://admet:8000
|
| 23 |
+
AI_ADMET_SERVICE_URL=http://admet:8000
|
| 24 |
+
DRUG_REPURPOSING_URL=http://drug-repurposing:8000
|
| 25 |
+
AI_SERVICE_URL=http://drug-repurposing:8000
|
| 26 |
+
|
| 27 |
+
AI_INTEGRATION_ROUTES_ENABLED=true
|
| 28 |
+
|
| 29 |
+
MAIL_MAILER=smtp
|
| 30 |
+
MAIL_HOST=smtp.gmail.com
|
| 31 |
+
MAIL_PORT=587
|
| 32 |
+
MAIL_USERNAME=yasmeensaleh747@gmail.com
|
| 33 |
+
MAIL_PASSWORD="fghu ijct vhfu xnud"
|
| 34 |
+
MAIL_ENCRYPTION=tls
|
| 35 |
+
MAIL_FROM_ADDRESS=yasmeensaleh747@gmail.com
|
| 36 |
+
MAIL_FROM_NAME="AILIXIR"
|
.env.example
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AILIXIR API Test Configuration
|
| 2 |
+
# Copy this to .env and fill in your values
|
| 3 |
+
|
| 4 |
+
# API URLs
|
| 5 |
+
BASE_URL=https://america-hyperlipemic-grazyna.ngrok-free.dev/api
|
| 6 |
+
AUTH_BASE_URL=https://america-hyperlipemic-grazyna.ngrok-free.dev/api
|
| 7 |
+
|
| 8 |
+
# Test User Credentials
|
| 9 |
+
TEST_EMAIL=your_email@example.com
|
| 10 |
+
TEST_PASSWORD=your_password
|
| 11 |
+
TEST_NAME=Your Name
|
| 12 |
+
|
| 13 |
+
# Database (SQLite)
|
| 14 |
+
DB_CONNECTION=sqlite
|
| 15 |
+
DB_DATABASE=database/ailixir.sqlite
|
| 16 |
+
MD_SIMULATION_URL=http://localhost:5005
|
.env.hf
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AILIXIR Hugging Face Spaces Environment
|
| 2 |
+
# Copy the relevant values to HF Space secrets/settings
|
| 3 |
+
|
| 4 |
+
APP_NAME=AILIXIR
|
| 5 |
+
APP_ENV=production
|
| 6 |
+
APP_DEBUG=false
|
| 7 |
+
APP_URL=https://Ailixir-AI-Team-ailixir-api.hf.space
|
| 8 |
+
|
| 9 |
+
LOG_CHANNEL=stderr
|
| 10 |
+
LOG_LEVEL=info
|
| 11 |
+
|
| 12 |
+
DB_CONNECTION=sqlite
|
| 13 |
+
DB_DATABASE=/var/www/html/database/ailixir.sqlite
|
| 14 |
+
|
| 15 |
+
SESSION_DRIVER=database
|
| 16 |
+
SESSION_LIFETIME=120
|
| 17 |
+
CACHE_STORE=database
|
| 18 |
+
QUEUE_CONNECTION=database
|
| 19 |
+
|
| 20 |
+
# AI Microservice URLs (HF Spaces)
|
| 21 |
+
GENERATION_SERVICE_URL=https://shdwRow-ailixir-generation.hf.space
|
| 22 |
+
CHEMICAL_AI_URL=https://RottenShadow-ailixir-chemical-rag.hf.space
|
| 23 |
+
ADMET_AI_URL=https://shdwRow-ailixir-admet.hf.space
|
| 24 |
+
AI_ADMET_SERVICE_URL=https://shdwRow-ailixir-admet.hf.space
|
| 25 |
+
DRUG_REPURPOSING_URL=https://RottenShadow-ailixir-drug-repurposing.hf.space
|
| 26 |
+
AI_SERVICE_URL=https://RottenShadow-ailixir-drug-repurposing.hf.space
|
| 27 |
+
|
| 28 |
+
AI_INTEGRATION_ROUTES_ENABLED=true
|
| 29 |
+
MAIL_MAILER=log
|
| 30 |
+
|
| 31 |
+
CLOUDINARY_URL=cloudinary://dummy_key:dummy_secret@dummy_cloud
|
| 32 |
+
CLOUDINARY_CLOUD_NAME=dummy_cloud
|
| 33 |
+
CLOUDINARY_API_KEY=dummy_key
|
| 34 |
+
CLOUDINARY_API_SECRET=dummy_secret
|
| 35 |
+
|
| 36 |
+
# Hugging Face Spaces mode (triggers supervisord)
|
| 37 |
+
RUN_MODE=hf
|
.gitattributes
CHANGED
|
@@ -1,35 +1,11 @@
|
|
| 1 |
-
*
|
| 2 |
-
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
*.
|
| 7 |
-
*.
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
* text=auto eol=lf
|
| 2 |
+
|
| 3 |
+
*.blade.php diff=html
|
| 4 |
+
*.css diff=css
|
| 5 |
+
*.html diff=html
|
| 6 |
+
*.md diff=markdown
|
| 7 |
+
*.php diff=php
|
| 8 |
+
|
| 9 |
+
/.github export-ignore
|
| 10 |
+
CHANGELOG.md export-ignore
|
| 11 |
+
.styleci.yml export-ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.github/workflows/api-tests-hf.yml
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: AILIXIR API Tests (HF Spaces)
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main, develop]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
schedule:
|
| 9 |
+
- cron: "0 6 * * *"
|
| 10 |
+
workflow_dispatch:
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
test:
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
|
| 16 |
+
steps:
|
| 17 |
+
- name: Checkout code
|
| 18 |
+
uses: actions/checkout@v4
|
| 19 |
+
|
| 20 |
+
- name: Set up Python
|
| 21 |
+
uses: actions/setup-python@v5
|
| 22 |
+
with:
|
| 23 |
+
python-version: '3.11'
|
| 24 |
+
|
| 25 |
+
- name: Install test dependencies
|
| 26 |
+
run: pip install -r requirements.txt
|
| 27 |
+
|
| 28 |
+
- name: Wake up HF Space
|
| 29 |
+
run: |
|
| 30 |
+
echo "Waiting for ailixir-api HF Space to be ready..."
|
| 31 |
+
for i in $(seq 1 60); do
|
| 32 |
+
if curl -sf https://Ailixir-AI-Team-ailixir-api.hf.space/ > /dev/null 2>&1; then
|
| 33 |
+
echo "ailixir-api is up"
|
| 34 |
+
sleep 5
|
| 35 |
+
exit 0
|
| 36 |
+
fi
|
| 37 |
+
echo "Attempt $i/60: not ready yet..."
|
| 38 |
+
sleep 5
|
| 39 |
+
done
|
| 40 |
+
echo "ailixir-api failed to become healthy in time"
|
| 41 |
+
exit 1
|
| 42 |
+
|
| 43 |
+
- name: Run API tests
|
| 44 |
+
env:
|
| 45 |
+
BASE_URL: https://Ailixir-AI-Team-ailixir-api.hf.space/api
|
| 46 |
+
TEST_EMAIL: test@example.com
|
| 47 |
+
TEST_PASSWORD: password123
|
| 48 |
+
TEST_NAME: testuser
|
| 49 |
+
run: |
|
| 50 |
+
python -m pytest tests/ -v --tb=short --junitxml=junit.xml || true
|
| 51 |
+
|
| 52 |
+
- name: Upload test results
|
| 53 |
+
uses: actions/upload-artifact@v4
|
| 54 |
+
if: always()
|
| 55 |
+
with:
|
| 56 |
+
name: test-results-hf
|
| 57 |
+
path: junit.xml
|
| 58 |
+
|
| 59 |
+
- name: Test Summary
|
| 60 |
+
uses: test-summary/action@v2
|
| 61 |
+
with:
|
| 62 |
+
paths: junit.xml
|
| 63 |
+
if: always()
|
.github/workflows/api-tests.yml
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: AILIXIR API Tests (Docker)
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
|
| 6 |
+
jobs:
|
| 7 |
+
test:
|
| 8 |
+
runs-on: ubuntu-latest
|
| 9 |
+
|
| 10 |
+
steps:
|
| 11 |
+
- name: Checkout code
|
| 12 |
+
uses: actions/checkout@v4
|
| 13 |
+
with:
|
| 14 |
+
lfs: true
|
| 15 |
+
|
| 16 |
+
- name: Pull and verify Git LFS files
|
| 17 |
+
run: |
|
| 18 |
+
git lfs version
|
| 19 |
+
git lfs pull
|
| 20 |
+
git lfs checkout
|
| 21 |
+
git lfs ls-files
|
| 22 |
+
|
| 23 |
+
python - <<'PY'
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
paths = [
|
| 27 |
+
"ai_apps/generation/bundle/models/affinity/config.pkl",
|
| 28 |
+
"ai_apps/generation/bundle/models/affinity/model.pt",
|
| 29 |
+
"ai_apps/generation/bundle/models/generator/egfr_generator.chkpt",
|
| 30 |
+
"ai_apps/generation/bundle/models/generator/reinvent.prior",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
for p in paths:
|
| 34 |
+
path = Path(p)
|
| 35 |
+
if not path.exists():
|
| 36 |
+
raise SystemExit(f"Missing required artifact: {p}")
|
| 37 |
+
|
| 38 |
+
data = path.read_bytes()[:80]
|
| 39 |
+
size = path.stat().st_size
|
| 40 |
+
|
| 41 |
+
print(f"{p}: {size} bytes")
|
| 42 |
+
|
| 43 |
+
if data.startswith(b"version https://git-lfs.github.com"):
|
| 44 |
+
raise SystemExit(f"{p} is still a Git LFS pointer file")
|
| 45 |
+
PY
|
| 46 |
+
- name: Set up Docker Buildx
|
| 47 |
+
uses: docker/setup-buildx-action@v3
|
| 48 |
+
|
| 49 |
+
- name: Create .env file for Docker
|
| 50 |
+
run: |
|
| 51 |
+
cp docker/laravel.env .env
|
| 52 |
+
|
| 53 |
+
- name: Start Docker containers
|
| 54 |
+
run: |
|
| 55 |
+
docker compose up -d --build
|
| 56 |
+
|
| 57 |
+
- name: Wait for services to be healthy
|
| 58 |
+
run: |
|
| 59 |
+
echo "Waiting for Laravel API to be ready..."
|
| 60 |
+
for i in {1..60}; do
|
| 61 |
+
if docker exec ailixir-laravel curl -f http://127.0.0.1:8000 > /dev/null 2>&1; then
|
| 62 |
+
echo "Laravel API is ready"
|
| 63 |
+
break
|
| 64 |
+
fi
|
| 65 |
+
echo "Attempt $i/60: Laravel API not ready yet..."
|
| 66 |
+
sleep 3
|
| 67 |
+
done
|
| 68 |
+
|
| 69 |
+
echo "Waiting for ADMET service to be healthy..."
|
| 70 |
+
for i in {1..120}; do
|
| 71 |
+
if docker exec ailixir-admet python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" > /dev/null 2>&1; then
|
| 72 |
+
echo "ADMET service is healthy internally inside container"
|
| 73 |
+
echo "Waiting additional 10 seconds for service to fully initialize..."
|
| 74 |
+
sleep 10
|
| 75 |
+
break
|
| 76 |
+
fi
|
| 77 |
+
echo "Attempt $i/120: ADMET service not ready yet..."
|
| 78 |
+
sleep 5
|
| 79 |
+
done
|
| 80 |
+
|
| 81 |
+
echo "All services are up and running!"
|
| 82 |
+
docker compose ps
|
| 83 |
+
|
| 84 |
+
- name: Run database migrations
|
| 85 |
+
run: |
|
| 86 |
+
docker exec ailixir-laravel php artisan migrate --force
|
| 87 |
+
|
| 88 |
+
- name: Run API tests
|
| 89 |
+
env:
|
| 90 |
+
BASE_URL: http://localhost:8080/api
|
| 91 |
+
TEST_EMAIL: test@example.com
|
| 92 |
+
TEST_PASSWORD: password123
|
| 93 |
+
TEST_NAME: testuser
|
| 94 |
+
run: |
|
| 95 |
+
docker exec ailixir-laravel \
|
| 96 |
+
python -m pytest tests/ -v --tb=short --junitxml=junit.xml || true
|
| 97 |
+
|
| 98 |
+
- name: Copy test results from container
|
| 99 |
+
if: always()
|
| 100 |
+
run: |
|
| 101 |
+
docker cp ailixir-laravel:/var/www/html/junit.xml ./junit.xml || true
|
| 102 |
+
|
| 103 |
+
- name: Upload test results
|
| 104 |
+
uses: actions/upload-artifact@v4
|
| 105 |
+
if: always()
|
| 106 |
+
with:
|
| 107 |
+
name: test-results
|
| 108 |
+
path: junit.xml
|
| 109 |
+
|
| 110 |
+
- name: Test Summary
|
| 111 |
+
uses: test-summary/action@v2
|
| 112 |
+
with:
|
| 113 |
+
paths: "junit.xml"
|
| 114 |
+
if: always()
|
| 115 |
+
|
| 116 |
+
- name: Collect Docker logs
|
| 117 |
+
if: failure()
|
| 118 |
+
run: |
|
| 119 |
+
mkdir -p logs
|
| 120 |
+
docker compose logs laravel > logs/laravel.log
|
| 121 |
+
docker compose logs admet > logs/admet.log
|
| 122 |
+
docker compose logs drug-repurposing > logs/drug-repurposing.log
|
| 123 |
+
docker compose logs chemical-rag > logs/chemical-rag.log
|
| 124 |
+
|
| 125 |
+
- name: Upload Docker logs
|
| 126 |
+
uses: actions/upload-artifact@v4
|
| 127 |
+
if: failure()
|
| 128 |
+
with:
|
| 129 |
+
name: docker-logs
|
| 130 |
+
path: logs/
|
| 131 |
+
|
| 132 |
+
- name: Cleanup Docker containers
|
| 133 |
+
if: always()
|
| 134 |
+
run: |
|
| 135 |
+
docker compose down -v
|
.github/workflows/e2e-md-test.yml
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: MD Simulation E2E Test
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main, develop, MD-integration]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
schedule:
|
| 9 |
+
- cron: "0 6 * * *"
|
| 10 |
+
workflow_dispatch:
|
| 11 |
+
|
| 12 |
+
concurrency:
|
| 13 |
+
group: e2e-md-${{ github.ref }}
|
| 14 |
+
cancel-in-progress: true
|
| 15 |
+
|
| 16 |
+
jobs:
|
| 17 |
+
e2e-md-simulation:
|
| 18 |
+
runs-on: ubuntu-latest
|
| 19 |
+
timeout-minutes: 30
|
| 20 |
+
|
| 21 |
+
steps:
|
| 22 |
+
- name: Checkout code
|
| 23 |
+
uses: actions/checkout@v4
|
| 24 |
+
with:
|
| 25 |
+
lfs: true
|
| 26 |
+
|
| 27 |
+
- name: Set up Docker Buildx
|
| 28 |
+
uses: docker/setup-buildx-action@v3
|
| 29 |
+
|
| 30 |
+
- name: Build Laravel image
|
| 31 |
+
run: docker build -t ailixir-laravel .
|
| 32 |
+
|
| 33 |
+
- name: Build MD simulation image
|
| 34 |
+
run: docker build -t md-simulation ai_apps/Protein-Ligand-MD-Simulation/
|
| 35 |
+
|
| 36 |
+
- name: Create Docker network
|
| 37 |
+
run: docker network create ailixir-md
|
| 38 |
+
|
| 39 |
+
- name: Start Redis
|
| 40 |
+
run: |
|
| 41 |
+
docker run -d --name md-redis \
|
| 42 |
+
--network ailixir-md \
|
| 43 |
+
--network-alias md-redis \
|
| 44 |
+
redis:7-alpine
|
| 45 |
+
|
| 46 |
+
- name: Start MD Simulation API
|
| 47 |
+
run: |
|
| 48 |
+
docker run -d --name md-api \
|
| 49 |
+
--network ailixir-md \
|
| 50 |
+
-p 5005:5005 \
|
| 51 |
+
-e REDIS_HOST=md-redis \
|
| 52 |
+
-e REDIS_PORT=6379 \
|
| 53 |
+
-e USE_REDIS=true \
|
| 54 |
+
-e OPENMM_PLATFORM=CPU \
|
| 55 |
+
-e FLASK_HOST=0.0.0.0 \
|
| 56 |
+
-e FLASK_PORT=5005 \
|
| 57 |
+
-e MD_DATA_DIR=/app/data \
|
| 58 |
+
-e LOG_LEVEL=INFO \
|
| 59 |
+
md-simulation
|
| 60 |
+
|
| 61 |
+
- name: Start Laravel API
|
| 62 |
+
run: |
|
| 63 |
+
docker run -d --name ailixir-laravel \
|
| 64 |
+
--network ailixir-md \
|
| 65 |
+
-p 8000:8000 \
|
| 66 |
+
--env-file docker/laravel.env \
|
| 67 |
+
-e MD_SIMULATION_URL=http://md-api:5005 \
|
| 68 |
+
-e QUEUE_CONNECTION=sync \
|
| 69 |
+
-e APP_KEY=base64:l+P8vPcH6w+2qV6vYC3h8K9xN0mR4sT5uW6xY7zA8bB= \
|
| 70 |
+
ailixir-laravel
|
| 71 |
+
|
| 72 |
+
- name: Wait for Laravel API to be ready
|
| 73 |
+
run: |
|
| 74 |
+
echo "Waiting for Laravel API to be ready..."
|
| 75 |
+
for i in $(seq 1 30); do
|
| 76 |
+
if curl -sf http://localhost:8000/ > /dev/null 2>&1; then
|
| 77 |
+
echo "Laravel API is ready"
|
| 78 |
+
exit 0
|
| 79 |
+
fi
|
| 80 |
+
echo "Waiting for Laravel ($i/30)..."
|
| 81 |
+
sleep 2
|
| 82 |
+
done
|
| 83 |
+
docker logs ailixir-laravel
|
| 84 |
+
exit 1
|
| 85 |
+
|
| 86 |
+
- name: Wait for MD Simulation API to be healthy
|
| 87 |
+
run: |
|
| 88 |
+
echo "Waiting for MD Simulation API to be healthy..."
|
| 89 |
+
for i in $(seq 1 60); do
|
| 90 |
+
if curl -sf http://localhost:5005/health > /dev/null 2>&1; then
|
| 91 |
+
echo "MD Simulation API is healthy"
|
| 92 |
+
exit 0
|
| 93 |
+
fi
|
| 94 |
+
echo "Waiting for MD API ($i/60)..."
|
| 95 |
+
sleep 5
|
| 96 |
+
done
|
| 97 |
+
docker logs md-api
|
| 98 |
+
exit 1
|
| 99 |
+
|
| 100 |
+
- name: Run MD Simulation E2E tests
|
| 101 |
+
run: |
|
| 102 |
+
docker exec \
|
| 103 |
+
-e MD_E2E_BASE_URL=http://localhost:8000/api \
|
| 104 |
+
ailixir-laravel \
|
| 105 |
+
./vendor/bin/pest --filter=MdSimulationE2ETest --log-junit=/var/www/html/junit.xml
|
| 106 |
+
|
| 107 |
+
- name: Copy test results from container
|
| 108 |
+
if: always()
|
| 109 |
+
run: |
|
| 110 |
+
docker cp ailixir-laravel:/var/www/html/junit.xml ./junit.xml || true
|
| 111 |
+
|
| 112 |
+
- name: Upload test results
|
| 113 |
+
uses: actions/upload-artifact@v4
|
| 114 |
+
if: always()
|
| 115 |
+
with:
|
| 116 |
+
name: e2e-md-test-results
|
| 117 |
+
path: junit.xml
|
| 118 |
+
|
| 119 |
+
- name: Test Summary
|
| 120 |
+
uses: test-summary/action@v2
|
| 121 |
+
if: always()
|
| 122 |
+
with:
|
| 123 |
+
paths: junit.xml
|
| 124 |
+
|
| 125 |
+
- name: Dump logs on failure
|
| 126 |
+
if: failure()
|
| 127 |
+
run: |
|
| 128 |
+
echo "=== Laravel logs ==="
|
| 129 |
+
docker logs ailixir-laravel || true
|
| 130 |
+
echo "=== MD API logs ==="
|
| 131 |
+
docker logs md-api || true
|
| 132 |
+
echo "=== Redis logs ==="
|
| 133 |
+
docker logs md-redis || true
|
| 134 |
+
|
| 135 |
+
- name: Cleanup
|
| 136 |
+
if: always()
|
| 137 |
+
run: |
|
| 138 |
+
docker rm -f ailixir-laravel md-api md-redis 2>/dev/null || true
|
| 139 |
+
docker network rm ailixir-md 2>/dev/null || true
|
.github/workflows/integration-test-hf.yml
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: HF Spaces Integration Test
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main, develop, "feature/**"]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main, develop]
|
| 8 |
+
workflow_dispatch:
|
| 9 |
+
|
| 10 |
+
concurrency:
|
| 11 |
+
group: hf-integration-${{ github.ref }}
|
| 12 |
+
cancel-in-progress: true
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
core-ai-proxy:
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
timeout-minutes: 30
|
| 18 |
+
|
| 19 |
+
steps:
|
| 20 |
+
- name: Checkout
|
| 21 |
+
uses: actions/checkout@v4
|
| 22 |
+
|
| 23 |
+
- name: Warm up ailixir-api Space
|
| 24 |
+
run: |
|
| 25 |
+
echo "Waiting for ailixir-api HF Space..."
|
| 26 |
+
for i in $(seq 1 60); do
|
| 27 |
+
if curl -sf https://Ailixir-AI-Team-ailixir-api.hf.space/ > /dev/null 2>&1; then
|
| 28 |
+
echo "ailixir-api is up"
|
| 29 |
+
sleep 5
|
| 30 |
+
exit 0
|
| 31 |
+
fi
|
| 32 |
+
echo "Attempt $i/60..."
|
| 33 |
+
sleep 5
|
| 34 |
+
done
|
| 35 |
+
exit 1
|
| 36 |
+
|
| 37 |
+
- name: Warm up ailixir-drug-repurposing Space
|
| 38 |
+
run: |
|
| 39 |
+
echo "Waiting for ailixir-drug-repurposing HF Space..."
|
| 40 |
+
for i in $(seq 1 60); do
|
| 41 |
+
if curl -sf https://RottenShadow-ailixir-drug-repurposing.hf.space/health > /dev/null 2>&1; then
|
| 42 |
+
echo "ailixir-drug-repurposing is up"
|
| 43 |
+
sleep 3
|
| 44 |
+
exit 0
|
| 45 |
+
fi
|
| 46 |
+
echo "Attempt $i/60..."
|
| 47 |
+
sleep 5
|
| 48 |
+
done
|
| 49 |
+
exit 1
|
| 50 |
+
|
| 51 |
+
- name: Warm up ailixir-admet Space
|
| 52 |
+
run: |
|
| 53 |
+
echo "Waiting for ailixir-admet HF Space..."
|
| 54 |
+
for i in $(seq 1 60); do
|
| 55 |
+
if curl -sf https://shdwRow-ailixir-admet.hf.space/health > /dev/null 2>&1; then
|
| 56 |
+
echo "ailixir-admet is up"
|
| 57 |
+
sleep 3
|
| 58 |
+
exit 0
|
| 59 |
+
fi
|
| 60 |
+
echo "Attempt $i/60..."
|
| 61 |
+
sleep 5
|
| 62 |
+
done
|
| 63 |
+
exit 1
|
| 64 |
+
|
| 65 |
+
- name: Warm up ailixir-chemical-rag Space
|
| 66 |
+
run: |
|
| 67 |
+
echo "Waiting for ailixir-chemical-rag HF Space..."
|
| 68 |
+
for i in $(seq 1 60); do
|
| 69 |
+
if curl -sf https://RottenShadow-ailixir-chemical-rag.hf.space/health > /dev/null 2>&1; then
|
| 70 |
+
echo "ailixir-chemical-rag is up"
|
| 71 |
+
sleep 3
|
| 72 |
+
exit 0
|
| 73 |
+
fi
|
| 74 |
+
echo "Attempt $i/60..."
|
| 75 |
+
sleep 5
|
| 76 |
+
done
|
| 77 |
+
exit 1
|
| 78 |
+
|
| 79 |
+
- name: Login and get Bearer token
|
| 80 |
+
run: |
|
| 81 |
+
echo "POST /api/user/login"
|
| 82 |
+
HTTP_CODE=$(curl -s -o login.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/user/login \
|
| 83 |
+
-H "Content-Type: application/json" \
|
| 84 |
+
-d '{"email":"salehyasmeen080@gmail.com","password":"123456789"}')
|
| 85 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 86 |
+
cat login.json
|
| 87 |
+
echo ""
|
| 88 |
+
|
| 89 |
+
if [ "$HTTP_CODE" != "200" ]; then
|
| 90 |
+
echo "Login failed with HTTP $HTTP_CODE"
|
| 91 |
+
exit 1
|
| 92 |
+
fi
|
| 93 |
+
|
| 94 |
+
python3 - <<'PY'
|
| 95 |
+
import json, sys
|
| 96 |
+
try:
|
| 97 |
+
data = json.load(open("login.json"))
|
| 98 |
+
except json.JSONDecodeError as e:
|
| 99 |
+
print("Invalid JSON response:", e)
|
| 100 |
+
with open("login.json") as f:
|
| 101 |
+
print("Raw response:", f.read())
|
| 102 |
+
sys.exit(1)
|
| 103 |
+
|
| 104 |
+
if not data.get("success"):
|
| 105 |
+
print("Login failed:", json.dumps(data, indent=2))
|
| 106 |
+
sys.exit(1)
|
| 107 |
+
|
| 108 |
+
token = data.get("data", {}).get("token")
|
| 109 |
+
if not token:
|
| 110 |
+
print("No token in response:", json.dumps(data, indent=2))
|
| 111 |
+
sys.exit(1)
|
| 112 |
+
|
| 113 |
+
with open("token.txt", "w") as f:
|
| 114 |
+
f.write(token)
|
| 115 |
+
print("Login OK - token saved")
|
| 116 |
+
PY
|
| 117 |
+
|
| 118 |
+
- name: Test aggregated AI health via Laravel
|
| 119 |
+
run: |
|
| 120 |
+
TOKEN=$(cat token.txt)
|
| 121 |
+
echo "GET /api/ai-services/health"
|
| 122 |
+
HTTP_CODE=$(curl -s -o health.json -w "%{http_code}" https://Ailixir-AI-Team-ailixir-api.hf.space/api/ai-services/health \
|
| 123 |
+
-H "Authorization: Bearer $TOKEN")
|
| 124 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 125 |
+
cat health.json
|
| 126 |
+
echo ""
|
| 127 |
+
python3 - <<'PY'
|
| 128 |
+
import json, sys
|
| 129 |
+
data = json.load(open("health.json"))
|
| 130 |
+
if not data.get("success"):
|
| 131 |
+
print("AI services health check failed:", json.dumps(data, indent=2))
|
| 132 |
+
sys.exit(1)
|
| 133 |
+
print("All AI services healthy via Laravel proxy")
|
| 134 |
+
PY
|
| 135 |
+
|
| 136 |
+
- name: Test ADMET prediction via Laravel proxy
|
| 137 |
+
run: |
|
| 138 |
+
TOKEN=$(cat token.txt)
|
| 139 |
+
echo "POST /api/admet/predict"
|
| 140 |
+
HTTP_CODE=$(curl -s -o admet.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/admet/predict \
|
| 141 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 142 |
+
-H "Content-Type: application/json" \
|
| 143 |
+
-d '{"smiles": "c1ccccc1, CCO, CCC"}')
|
| 144 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 145 |
+
cat admet.json
|
| 146 |
+
echo ""
|
| 147 |
+
|
| 148 |
+
if [ "$HTTP_CODE" != "200" ]; then
|
| 149 |
+
echo "ADMET prediction failed with HTTP $HTTP_CODE"
|
| 150 |
+
exit 1
|
| 151 |
+
fi
|
| 152 |
+
|
| 153 |
+
python3 - <<'PY'
|
| 154 |
+
import json, sys
|
| 155 |
+
try:
|
| 156 |
+
data = json.load(open("admet.json"))
|
| 157 |
+
except json.JSONDecodeError as e:
|
| 158 |
+
print("Invalid JSON response:", e)
|
| 159 |
+
with open("admet.json") as f:
|
| 160 |
+
print("Raw response:", f.read())
|
| 161 |
+
sys.exit(1)
|
| 162 |
+
|
| 163 |
+
if not data.get("success"):
|
| 164 |
+
print("ADMET prediction test failed:", json.dumps(data, indent=2))
|
| 165 |
+
sys.exit(1)
|
| 166 |
+
|
| 167 |
+
results = data.get("data", {}).get("results", [])
|
| 168 |
+
if len(results) < 3:
|
| 169 |
+
print(f"Expected at least 3 results, got {len(results)}")
|
| 170 |
+
sys.exit(1)
|
| 171 |
+
|
| 172 |
+
print(f"ADMET prediction OK - {len(results)} compounds processed")
|
| 173 |
+
PY
|
| 174 |
+
|
| 175 |
+
- name: Test Chemical RAG proxy via Laravel
|
| 176 |
+
run: |
|
| 177 |
+
TOKEN=$(cat token.txt)
|
| 178 |
+
echo "POST /api/ai-services/test/chemical-search"
|
| 179 |
+
HTTP_CODE=$(curl -s -o chemical.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/ai-services/test/chemical-search \
|
| 180 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 181 |
+
-H "Content-Type: application/json" \
|
| 182 |
+
-d '{"query": "malaria drug"}')
|
| 183 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 184 |
+
cat chemical.json
|
| 185 |
+
echo ""
|
| 186 |
+
python3 - <<'PY'
|
| 187 |
+
import json, sys
|
| 188 |
+
data = json.load(open("chemical.json"))
|
| 189 |
+
if not data.get("success"):
|
| 190 |
+
print("Chemical RAG proxy test failed:", json.dumps(data, indent=2))
|
| 191 |
+
sys.exit(1)
|
| 192 |
+
print("Chemical RAG proxy OK")
|
| 193 |
+
PY
|
| 194 |
+
|
| 195 |
+
- name: Test Drug Repurposing proxy via Laravel
|
| 196 |
+
run: |
|
| 197 |
+
TOKEN=$(cat token.txt)
|
| 198 |
+
echo "GET /api/ai-services/test/drug-repurposing"
|
| 199 |
+
HTTP_CODE=$(curl -s -o drug.json -w "%{http_code}" https://Ailixir-AI-Team-ailixir-api.hf.space/api/ai-services/test/drug-repurposing \
|
| 200 |
+
-H "Authorization: Bearer $TOKEN")
|
| 201 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 202 |
+
cat drug.json
|
| 203 |
+
echo ""
|
| 204 |
+
python3 - <<'PY'
|
| 205 |
+
import json, sys
|
| 206 |
+
data = json.load(open("drug.json"))
|
| 207 |
+
if not data.get("success"):
|
| 208 |
+
print("Drug Repurposing proxy test failed:", json.dumps(data, indent=2))
|
| 209 |
+
sys.exit(1)
|
| 210 |
+
print("Drug Repurposing proxy OK")
|
| 211 |
+
PY
|
| 212 |
+
|
| 213 |
+
chemical-search:
|
| 214 |
+
runs-on: ubuntu-latest
|
| 215 |
+
timeout-minutes: 15
|
| 216 |
+
|
| 217 |
+
steps:
|
| 218 |
+
- name: Checkout
|
| 219 |
+
uses: actions/checkout@v4
|
| 220 |
+
|
| 221 |
+
- name: Warm up ailixir-api Space
|
| 222 |
+
run: |
|
| 223 |
+
for i in $(seq 1 60); do
|
| 224 |
+
if curl -sf https://Ailixir-AI-Team-ailixir-api.hf.space/ > /dev/null 2>&1; then
|
| 225 |
+
echo "ailixir-api is up"
|
| 226 |
+
sleep 5
|
| 227 |
+
exit 0
|
| 228 |
+
fi
|
| 229 |
+
echo "Waiting ($i/60)..."
|
| 230 |
+
sleep 5
|
| 231 |
+
done
|
| 232 |
+
exit 1
|
| 233 |
+
|
| 234 |
+
- name: Login and get Bearer token
|
| 235 |
+
run: |
|
| 236 |
+
HTTP_CODE=$(curl -s -o login.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/user/login \
|
| 237 |
+
-H "Content-Type: application/json" \
|
| 238 |
+
-d '{"email":"salehyasmeen080@gmail.com","password":"123456789"}')
|
| 239 |
+
if [ "$HTTP_CODE" != "200" ]; then exit 1; fi
|
| 240 |
+
python3 - <<'PY'
|
| 241 |
+
import json
|
| 242 |
+
with open('login.json') as f: data = json.load(f)
|
| 243 |
+
token = data.get('data', {}).get('token')
|
| 244 |
+
if not token: exit(1)
|
| 245 |
+
with open('token.txt','w') as f: f.write(token)
|
| 246 |
+
print('Login OK')
|
| 247 |
+
PY
|
| 248 |
+
|
| 249 |
+
- name: Test retrieval-only chemical search
|
| 250 |
+
run: |
|
| 251 |
+
TOKEN=$(cat token.txt)
|
| 252 |
+
echo "POST /api/chemical-search (retrieval-only)"
|
| 253 |
+
HTTP_CODE=$(curl -s -o search.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/chemical-search \
|
| 254 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 255 |
+
-H "Content-Type: application/json" \
|
| 256 |
+
-d '{"smiles":"CC(=O)O"}')
|
| 257 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 258 |
+
cat search.json
|
| 259 |
+
echo ""
|
| 260 |
+
python3 - <<'PY'
|
| 261 |
+
import json, sys
|
| 262 |
+
data = json.load(open("search.json"))
|
| 263 |
+
if not data.get("success"):
|
| 264 |
+
print("Chemical search (retrieval-only) failed:", json.dumps(data, indent=2))
|
| 265 |
+
sys.exit(1)
|
| 266 |
+
if not isinstance(data.get("compounds"), list):
|
| 267 |
+
print("Missing 'compounds' array in response")
|
| 268 |
+
sys.exit(1)
|
| 269 |
+
if data.get("metadata", {}).get("source") != "retrieval":
|
| 270 |
+
print("Expected source='retrieval', got:", data.get("metadata", {}).get("source"))
|
| 271 |
+
sys.exit(1)
|
| 272 |
+
print("Chemical search (retrieval-only) OK - found", len(data["compounds"]), "compounds")
|
| 273 |
+
PY
|
| 274 |
+
|
| 275 |
+
- name: Test full RAG chemical search
|
| 276 |
+
run: |
|
| 277 |
+
TOKEN=$(cat token.txt)
|
| 278 |
+
echo "POST /api/chemical-search/full-rag"
|
| 279 |
+
HTTP_CODE=$(curl -s -o rag.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/chemical-search/full-rag \
|
| 280 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 281 |
+
-H "Content-Type: application/json" \
|
| 282 |
+
-d '{"smiles":"CC(=O)O"}')
|
| 283 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 284 |
+
cat rag.json
|
| 285 |
+
echo ""
|
| 286 |
+
python3 - <<'PY'
|
| 287 |
+
import json, sys
|
| 288 |
+
data = json.load(open("rag.json"))
|
| 289 |
+
if not data.get("success"):
|
| 290 |
+
print("Chemical search (full RAG) failed:", json.dumps(data, indent=2))
|
| 291 |
+
sys.exit(1)
|
| 292 |
+
if not isinstance(data.get("compounds"), list):
|
| 293 |
+
print("Missing 'compounds' array in response")
|
| 294 |
+
sys.exit(1)
|
| 295 |
+
if data.get("metadata", {}).get("source") != "full_rag":
|
| 296 |
+
print("Expected source='full_rag', got:", data.get("metadata", {}).get("source"))
|
| 297 |
+
sys.exit(1)
|
| 298 |
+
for compound in data["compounds"]:
|
| 299 |
+
if not compound.get("explanation"):
|
| 300 |
+
print("Missing explanation for compound:", compound.get("name"))
|
| 301 |
+
sys.exit(1)
|
| 302 |
+
print("Chemical search (full RAG) OK - found", len(data["compounds"]), "compounds with explanations")
|
| 303 |
+
PY
|
| 304 |
+
|
| 305 |
+
drug-repurposing-integration:
|
| 306 |
+
runs-on: ubuntu-latest
|
| 307 |
+
timeout-minutes: 30
|
| 308 |
+
|
| 309 |
+
steps:
|
| 310 |
+
- name: Checkout
|
| 311 |
+
uses: actions/checkout@v4
|
| 312 |
+
|
| 313 |
+
- name: Warm up ailixir-api Space
|
| 314 |
+
run: |
|
| 315 |
+
for i in $(seq 1 60); do
|
| 316 |
+
if curl -sf https://Ailixir-AI-Team-ailixir-api.hf.space/ > /dev/null 2>&1; then
|
| 317 |
+
echo "ailixir-api is up"
|
| 318 |
+
sleep 5
|
| 319 |
+
exit 0
|
| 320 |
+
fi
|
| 321 |
+
echo "Waiting ($i/60)..."
|
| 322 |
+
sleep 5
|
| 323 |
+
done
|
| 324 |
+
exit 1
|
| 325 |
+
|
| 326 |
+
- name: Login and get Bearer token
|
| 327 |
+
run: |
|
| 328 |
+
HTTP_CODE=$(curl -s -o login.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/user/login \
|
| 329 |
+
-H "Content-Type: application/json" \
|
| 330 |
+
-d '{"email":"salehyasmeen080@gmail.com","password":"123456789"}')
|
| 331 |
+
if [ "$HTTP_CODE" != "200" ]; then exit 1; fi
|
| 332 |
+
python3 - <<'PY'
|
| 333 |
+
import json
|
| 334 |
+
with open('login.json') as f: data = json.load(f)
|
| 335 |
+
token = data.get('data', {}).get('token')
|
| 336 |
+
if not token: exit(1)
|
| 337 |
+
with open('token.txt','w') as f: f.write(token)
|
| 338 |
+
print('Login OK')
|
| 339 |
+
PY
|
| 340 |
+
|
| 341 |
+
- name: Test unauthenticated DR request is rejected
|
| 342 |
+
run: |
|
| 343 |
+
echo "POST /api/drug-repurposing/targets (no auth)"
|
| 344 |
+
HTTP_CODE=$(curl -s -o dr_unauth.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/drug-repurposing/targets \
|
| 345 |
+
-H "Content-Type: application/json" \
|
| 346 |
+
-d '{"disease_name":"Type 2 Diabetes"}')
|
| 347 |
+
if [ "$HTTP_CODE" != "401" ]; then
|
| 348 |
+
echo "Expected 401, got $HTTP_CODE"
|
| 349 |
+
exit 1
|
| 350 |
+
fi
|
| 351 |
+
python3 - <<'PY'
|
| 352 |
+
import json
|
| 353 |
+
with open('dr_unauth.json') as f: data = json.load(f)
|
| 354 |
+
assert not data.get('success', True), 'Expected success=false'
|
| 355 |
+
print('Unauthenticated DR request correctly rejected')
|
| 356 |
+
PY
|
| 357 |
+
|
| 358 |
+
- name: Test DR target lookup requires disease_name
|
| 359 |
+
run: |
|
| 360 |
+
TOKEN=$(cat token.txt)
|
| 361 |
+
echo "POST /api/drug-repurposing/targets (missing disease_name)"
|
| 362 |
+
HTTP_CODE=$(curl -s -o dr_validation.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/drug-repurposing/targets \
|
| 363 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 364 |
+
-H "Content-Type: application/json" \
|
| 365 |
+
-d '{"top_n": 2}')
|
| 366 |
+
if [ "$HTTP_CODE" != "422" ]; then
|
| 367 |
+
echo "Expected 422, got $HTTP_CODE"
|
| 368 |
+
exit 1
|
| 369 |
+
fi
|
| 370 |
+
echo "Missing disease_name correctly rejected"
|
| 371 |
+
|
| 372 |
+
- name: Test DR target lookup via Laravel proxy
|
| 373 |
+
run: |
|
| 374 |
+
TOKEN=$(cat token.txt)
|
| 375 |
+
echo "POST /api/drug-repurposing/targets"
|
| 376 |
+
HTTP_CODE=$(curl -s -o dr_targets.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/drug-repurposing/targets \
|
| 377 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 378 |
+
-H "Content-Type: application/json" \
|
| 379 |
+
-d '{"disease_name":"Type 2 Diabetes","top_n":2}')
|
| 380 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 381 |
+
cat dr_targets.json
|
| 382 |
+
echo ""
|
| 383 |
+
|
| 384 |
+
if [ "$HTTP_CODE" != "200" ]; then
|
| 385 |
+
echo "DR target lookup failed with HTTP $HTTP_CODE"
|
| 386 |
+
exit 1
|
| 387 |
+
fi
|
| 388 |
+
|
| 389 |
+
python3 - <<'PY'
|
| 390 |
+
import json, sys
|
| 391 |
+
data = json.load(open("dr_targets.json"))
|
| 392 |
+
if not data.get("success"):
|
| 393 |
+
print("DR target lookup failed:", json.dumps(data, indent=2))
|
| 394 |
+
sys.exit(1)
|
| 395 |
+
job_id = data.get("data", {}).get("job_id")
|
| 396 |
+
status = data.get("data", {}).get("status")
|
| 397 |
+
print(f"DR target lookup OK - job_id={job_id}, status={status}")
|
| 398 |
+
|
| 399 |
+
# Poll for completion
|
| 400 |
+
import time, urllib.request
|
| 401 |
+
base = "https://Ailixir-AI-Team-ailixir-api.hf.space/api/drug-repurposing/targets"
|
| 402 |
+
with open("token.txt") as f: token = f.read().strip()
|
| 403 |
+
for i in range(30):
|
| 404 |
+
req = urllib.request.Request(f"{base}/{job_id}")
|
| 405 |
+
req.add_header("Authorization", f"Bearer {token}")
|
| 406 |
+
resp = urllib.request.urlopen(req)
|
| 407 |
+
status_data = json.loads(resp.read())
|
| 408 |
+
s = status_data.get("data", {}).get("status")
|
| 409 |
+
if s == "completed":
|
| 410 |
+
output = status_data.get("data", {}).get("output", {})
|
| 411 |
+
assert len(output.get("targets", [])) > 0, "No targets found"
|
| 412 |
+
print(f"DR targets completed - {len(output['targets'])} targets found")
|
| 413 |
+
sys.exit(0)
|
| 414 |
+
elif s == "failed":
|
| 415 |
+
print("DR target lookup failed:", json.dumps(status_data, indent=2))
|
| 416 |
+
sys.exit(1)
|
| 417 |
+
print(f" Polling... attempt {i+1}/30, status={s}")
|
| 418 |
+
time.sleep(5)
|
| 419 |
+
print("DR target lookup did not complete in time")
|
| 420 |
+
sys.exit(1)
|
| 421 |
+
PY
|
| 422 |
+
|
| 423 |
+
docking-integration:
|
| 424 |
+
runs-on: ubuntu-latest
|
| 425 |
+
timeout-minutes: 15
|
| 426 |
+
|
| 427 |
+
steps:
|
| 428 |
+
- name: Checkout
|
| 429 |
+
uses: actions/checkout@v4
|
| 430 |
+
|
| 431 |
+
- name: Warm up ailixir-api Space
|
| 432 |
+
run: |
|
| 433 |
+
for i in $(seq 1 60); do
|
| 434 |
+
if curl -sf https://Ailixir-AI-Team-ailixir-api.hf.space/ > /dev/null 2>&1; then
|
| 435 |
+
echo "ailixir-api is up"
|
| 436 |
+
sleep 5
|
| 437 |
+
exit 0
|
| 438 |
+
fi
|
| 439 |
+
echo "Waiting ($i/60)..."
|
| 440 |
+
sleep 5
|
| 441 |
+
done
|
| 442 |
+
exit 1
|
| 443 |
+
|
| 444 |
+
- name: Login and get Bearer token
|
| 445 |
+
run: |
|
| 446 |
+
HTTP_CODE=$(curl -s -o login.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/user/login \
|
| 447 |
+
-H "Content-Type: application/json" \
|
| 448 |
+
-d '{"email":"salehyasmeen080@gmail.com","password":"123456789"}')
|
| 449 |
+
if [ "$HTTP_CODE" != "200" ]; then exit 1; fi
|
| 450 |
+
python3 - <<'PY'
|
| 451 |
+
import json
|
| 452 |
+
with open('login.json') as f: data = json.load(f)
|
| 453 |
+
token = data.get('data', {}).get('token')
|
| 454 |
+
if not token: exit(1)
|
| 455 |
+
with open('token.txt','w') as f: f.write(token)
|
| 456 |
+
print('Login OK')
|
| 457 |
+
PY
|
| 458 |
+
|
| 459 |
+
- name: Test docking submit
|
| 460 |
+
run: |
|
| 461 |
+
TOKEN=$(cat token.txt)
|
| 462 |
+
echo "POST /api/docking/submit"
|
| 463 |
+
HTTP_CODE=$(curl -s -o docking.json -w "%{http_code}" -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/docking/submit \
|
| 464 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 465 |
+
-F "protein_name=EGFR" \
|
| 466 |
+
-F "protein_file=@tests/Fixtures/docking/egfr_clean.pdbqt" \
|
| 467 |
+
-F "ligand_name=Erlotinib" \
|
| 468 |
+
-F "ligand_file=@tests/Fixtures/docking/ligand.pdbqt" \
|
| 469 |
+
-F "center_x=10.0" \
|
| 470 |
+
-F "center_y=15.0" \
|
| 471 |
+
-F "center_z=20.0" \
|
| 472 |
+
-F "box_size_x=25.0" \
|
| 473 |
+
-F "box_size_y=25.0" \
|
| 474 |
+
-F "box_size_z=25.0" \
|
| 475 |
+
-F "exhaustiveness=8")
|
| 476 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 477 |
+
cat docking.json
|
| 478 |
+
echo ""
|
| 479 |
+
|
| 480 |
+
if [ "$HTTP_CODE" != "200" ]; then
|
| 481 |
+
echo "Docking submit failed with HTTP $HTTP_CODE"
|
| 482 |
+
exit 1
|
| 483 |
+
fi
|
| 484 |
+
|
| 485 |
+
python3 - <<'PY'
|
| 486 |
+
import json, sys
|
| 487 |
+
data = json.load(open("docking.json"))
|
| 488 |
+
if not data.get("success"):
|
| 489 |
+
print("Docking submit failed:", json.dumps(data, indent=2))
|
| 490 |
+
sys.exit(1)
|
| 491 |
+
print("Docking submit OK - job created")
|
| 492 |
+
PY
|
.github/workflows/integration-test.yml
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Docker Integration Test
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
|
| 6 |
+
concurrency:
|
| 7 |
+
group: docker-integration-${{ github.ref }}
|
| 8 |
+
cancel-in-progress: true
|
| 9 |
+
|
| 10 |
+
jobs:
|
| 11 |
+
laravel-to-ai-services:
|
| 12 |
+
runs-on: ubuntu-latest
|
| 13 |
+
timeout-minutes: 90
|
| 14 |
+
|
| 15 |
+
steps:
|
| 16 |
+
- name: Checkout
|
| 17 |
+
uses: actions/checkout@v4
|
| 18 |
+
with:
|
| 19 |
+
lfs: true
|
| 20 |
+
- name: Pull and verify Git LFS files
|
| 21 |
+
run: |
|
| 22 |
+
git lfs version
|
| 23 |
+
git lfs pull
|
| 24 |
+
git lfs checkout
|
| 25 |
+
git lfs ls-files
|
| 26 |
+
|
| 27 |
+
python - <<'PY'
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
|
| 30 |
+
paths = [
|
| 31 |
+
"ai_apps/generation/bundle/models/affinity/config.pkl",
|
| 32 |
+
"ai_apps/generation/bundle/models/affinity/model.pt",
|
| 33 |
+
"ai_apps/generation/bundle/models/generator/egfr_generator.chkpt",
|
| 34 |
+
"ai_apps/generation/bundle/models/generator/reinvent.prior",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
for p in paths:
|
| 38 |
+
path = Path(p)
|
| 39 |
+
if not path.exists():
|
| 40 |
+
raise SystemExit(f"Missing required artifact: {p}")
|
| 41 |
+
|
| 42 |
+
data = path.read_bytes()[:80]
|
| 43 |
+
size = path.stat().st_size
|
| 44 |
+
|
| 45 |
+
print(f"{p}: {size} bytes")
|
| 46 |
+
|
| 47 |
+
if data.startswith(b"version https://git-lfs.github.com"):
|
| 48 |
+
raise SystemExit(f"{p} is still a Git LFS pointer file")
|
| 49 |
+
PY
|
| 50 |
+
- name: Set up Docker Buildx
|
| 51 |
+
uses: docker/setup-buildx-action@v3
|
| 52 |
+
|
| 53 |
+
- name: Build and start stack
|
| 54 |
+
run: |
|
| 55 |
+
# Clean up volumes from previous runs
|
| 56 |
+
docker compose down -v 2>/dev/null || true
|
| 57 |
+
|
| 58 |
+
docker builder prune -af || true
|
| 59 |
+
docker system df
|
| 60 |
+
|
| 61 |
+
COMPOSE_PARALLEL_LIMIT=1 docker compose build
|
| 62 |
+
docker system df
|
| 63 |
+
|
| 64 |
+
docker compose up -d --wait
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# Run fresh migrations to ensure clean database state
|
| 68 |
+
docker compose exec -T laravel php artisan migrate:fresh --force
|
| 69 |
+
|
| 70 |
+
- name: Seed verified test user
|
| 71 |
+
run: |
|
| 72 |
+
echo "=== Creating verified test user via PHP script ==="
|
| 73 |
+
docker compose exec -T laravel php -r "
|
| 74 |
+
require 'vendor/autoload.php';
|
| 75 |
+
\$app = require_once 'bootstrap/app.php';
|
| 76 |
+
\$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class);
|
| 77 |
+
\$kernel->bootstrap();
|
| 78 |
+
|
| 79 |
+
use App\Models\User;
|
| 80 |
+
use Illuminate\Support\Facades\Hash;
|
| 81 |
+
|
| 82 |
+
\$user = User::firstOrCreate(
|
| 83 |
+
['email' => 'salehyasmeen080@gmail.com'],
|
| 84 |
+
[
|
| 85 |
+
'name' => 'Test User',
|
| 86 |
+
'password' => Hash::make('123456789'),
|
| 87 |
+
'role' => 'normal',
|
| 88 |
+
'is_verified' => true,
|
| 89 |
+
'email_verified_at' => now(),
|
| 90 |
+
'created_at' => now(),
|
| 91 |
+
'updated_at' => now()
|
| 92 |
+
]
|
| 93 |
+
);
|
| 94 |
+
echo 'User ID: ' . \$user->id . PHP_EOL;
|
| 95 |
+
echo 'Email: ' . \$user->email . PHP_EOL;
|
| 96 |
+
echo 'Verified: ' . (\$user->is_verified ? 'YES' : 'NO') . PHP_EOL;
|
| 97 |
+
"
|
| 98 |
+
|
| 99 |
+
- name: Login and get Bearer token
|
| 100 |
+
run: |
|
| 101 |
+
echo "POST /api/user/login"
|
| 102 |
+
HTTP_CODE=$(curl -s -o login.json -w "%{http_code}" -X POST http://localhost:8080/api/user/login \
|
| 103 |
+
-H "Content-Type: application/json" \
|
| 104 |
+
-d '{"email":"salehyasmeen080@gmail.com","password":"123456789"}')
|
| 105 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 106 |
+
cat login.json
|
| 107 |
+
echo ""
|
| 108 |
+
|
| 109 |
+
if [ "$HTTP_CODE" != "200" ]; then
|
| 110 |
+
echo "Login failed with HTTP $HTTP_CODE"
|
| 111 |
+
docker compose logs laravel
|
| 112 |
+
exit 1
|
| 113 |
+
fi
|
| 114 |
+
|
| 115 |
+
python3 - <<'PY'
|
| 116 |
+
import json, sys
|
| 117 |
+
try:
|
| 118 |
+
data = json.load(open("login.json"))
|
| 119 |
+
except json.JSONDecodeError as e:
|
| 120 |
+
print("Invalid JSON response:", e)
|
| 121 |
+
with open("login.json") as f:
|
| 122 |
+
print("Raw response:", f.read())
|
| 123 |
+
sys.exit(1)
|
| 124 |
+
|
| 125 |
+
if not data.get("success"):
|
| 126 |
+
print("Login failed:", json.dumps(data, indent=2))
|
| 127 |
+
sys.exit(1)
|
| 128 |
+
|
| 129 |
+
token = data.get("data", {}).get("token")
|
| 130 |
+
if not token:
|
| 131 |
+
print("No token in response:", json.dumps(data, indent=2))
|
| 132 |
+
sys.exit(1)
|
| 133 |
+
|
| 134 |
+
with open("token.txt", "w") as f:
|
| 135 |
+
f.write(token)
|
| 136 |
+
print("Login OK - token saved")
|
| 137 |
+
PY
|
| 138 |
+
|
| 139 |
+
- name: Show service status
|
| 140 |
+
if: always()
|
| 141 |
+
run: docker compose ps -a
|
| 142 |
+
|
| 143 |
+
- name: Wait for Laravel API
|
| 144 |
+
run: |
|
| 145 |
+
for i in $(seq 1 60); do
|
| 146 |
+
if curl -sf http://localhost:8080/ > /dev/null 2>&1; then
|
| 147 |
+
echo "Laravel is up"
|
| 148 |
+
exit 0
|
| 149 |
+
fi
|
| 150 |
+
echo "Waiting for Laravel ($i/60)..."
|
| 151 |
+
sleep 5
|
| 152 |
+
done
|
| 153 |
+
docker compose logs laravel
|
| 154 |
+
exit 1
|
| 155 |
+
|
| 156 |
+
- name: Wait for ADMET Service to be Healthy
|
| 157 |
+
run: |
|
| 158 |
+
echo "Waiting for ADMET service to be healthy..."
|
| 159 |
+
for i in $(seq 1 120); do
|
| 160 |
+
if curl -sf http://localhost:8002/health > /dev/null 2>&1; then
|
| 161 |
+
echo "ADMET service is healthy and responding on port 8002"
|
| 162 |
+
echo "Waiting additional 10 seconds for service to fully initialize..."
|
| 163 |
+
sleep 10
|
| 164 |
+
exit 0
|
| 165 |
+
fi
|
| 166 |
+
if docker exec ailixir-admet python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" > /dev/null 2>&1; then
|
| 167 |
+
echo "ADMET service is healthy internally inside container"
|
| 168 |
+
echo "Waiting additional 10 seconds for service to fully initialize..."
|
| 169 |
+
sleep 10
|
| 170 |
+
exit 0
|
| 171 |
+
fi
|
| 172 |
+
echo "Attempt $i/120: ADMET service not ready yet..."
|
| 173 |
+
sleep 5
|
| 174 |
+
done
|
| 175 |
+
echo "ADMET service failed to become healthy in time"
|
| 176 |
+
docker compose logs admet
|
| 177 |
+
exit 1
|
| 178 |
+
|
| 179 |
+
- name: Test aggregated AI health via Laravel
|
| 180 |
+
run: |
|
| 181 |
+
TOKEN=$(cat token.txt)
|
| 182 |
+
echo "GET /api/ai-services/health"
|
| 183 |
+
curl -sf http://localhost:8080/api/ai-services/health \
|
| 184 |
+
-H "Authorization: Bearer $TOKEN" | tee health.json
|
| 185 |
+
python3 - <<'PY'
|
| 186 |
+
import json, sys
|
| 187 |
+
data = json.load(open("health.json"))
|
| 188 |
+
if not data.get("success"):
|
| 189 |
+
print("AI services health check failed:", json.dumps(data, indent=2))
|
| 190 |
+
sys.exit(1)
|
| 191 |
+
print("All AI services healthy via Laravel")
|
| 192 |
+
PY
|
| 193 |
+
|
| 194 |
+
- name: Test ADMET prediction via
|
| 195 |
+
run: |
|
| 196 |
+
TOKEN=$(cat token.txt)
|
| 197 |
+
echo "POST /api/admet/predict"
|
| 198 |
+
HTTP_CODE=$(curl -s -o admet.json -w "%{http_code}" -X POST http://localhost:8080/api/admet/predict \
|
| 199 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 200 |
+
-H "Content-Type: application/json" \
|
| 201 |
+
-d '{"smiles": "c1ccccc1, CCO, CCC"}')
|
| 202 |
+
|
| 203 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 204 |
+
cat admet.json
|
| 205 |
+
echo ""
|
| 206 |
+
|
| 207 |
+
if [ "$HTTP_CODE" != "200" ]; then
|
| 208 |
+
echo "ADMET prediction request failed with HTTP $HTTP_CODE"
|
| 209 |
+
docker compose logs laravel
|
| 210 |
+
exit 1
|
| 211 |
+
fi
|
| 212 |
+
|
| 213 |
+
python3 - <<'PY'
|
| 214 |
+
import json, sys
|
| 215 |
+
try:
|
| 216 |
+
data = json.load(open("admet.json"))
|
| 217 |
+
except json.JSONDecodeError as e:
|
| 218 |
+
print("Invalid JSON response:", e)
|
| 219 |
+
with open("admet.json") as f:
|
| 220 |
+
print("Raw response:", f.read())
|
| 221 |
+
sys.exit(1)
|
| 222 |
+
|
| 223 |
+
if not data.get("success"):
|
| 224 |
+
print("ADMET prediction test failed:", json.dumps(data, indent=2))
|
| 225 |
+
sys.exit(1)
|
| 226 |
+
|
| 227 |
+
results = data.get("data", {}).get("results", [])
|
| 228 |
+
if len(results) < 3:
|
| 229 |
+
print(f"Expected at least 3 results, got {len(results)}")
|
| 230 |
+
sys.exit(1)
|
| 231 |
+
|
| 232 |
+
print(f"ADMET prediction OK - {len(results)} compounds processed")
|
| 233 |
+
PY
|
| 234 |
+
|
| 235 |
+
- name: Test Chemical RAG proxy via Laravel
|
| 236 |
+
run: |
|
| 237 |
+
TOKEN=$(cat token.txt)
|
| 238 |
+
echo "POST /api/ai-services/test/chemical-search"
|
| 239 |
+
HTTP_CODE=$(curl -s -o chemical.json -w "%{http_code}" -X POST http://localhost:8080/api/ai-services/test/chemical-search \
|
| 240 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 241 |
+
-H "Content-Type: application/json" \
|
| 242 |
+
-d '{"query": "malaria drug"}')
|
| 243 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 244 |
+
cat chemical.json
|
| 245 |
+
echo ""
|
| 246 |
+
python3 - <<'PY'
|
| 247 |
+
import json, sys
|
| 248 |
+
data = json.load(open("chemical.json"))
|
| 249 |
+
if not data.get("success"):
|
| 250 |
+
print("Chemical RAG proxy test failed:", json.dumps(data, indent=2))
|
| 251 |
+
sys.exit(1)
|
| 252 |
+
print("Chemical RAG proxy OK")
|
| 253 |
+
PY
|
| 254 |
+
|
| 255 |
+
- name: Test Drug Repurposing proxy via Laravel
|
| 256 |
+
run: |
|
| 257 |
+
TOKEN=$(cat token.txt)
|
| 258 |
+
echo "GET /api/ai-services/test/drug-repurposing"
|
| 259 |
+
HTTP_CODE=$(curl -s -o drug.json -w "%{http_code}" http://localhost:8080/api/ai-services/test/drug-repurposing \
|
| 260 |
+
-H "Authorization: Bearer $TOKEN")
|
| 261 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 262 |
+
cat drug.json
|
| 263 |
+
echo ""
|
| 264 |
+
python3 - <<'PY'
|
| 265 |
+
import json, sys
|
| 266 |
+
data = json.load(open("drug.json"))
|
| 267 |
+
if not data.get("success"):
|
| 268 |
+
print("Drug Repurposing proxy test failed:", json.dumps(data, indent=2))
|
| 269 |
+
sys.exit(1)
|
| 270 |
+
print("Drug Repurposing proxy OK")
|
| 271 |
+
PY
|
| 272 |
+
|
| 273 |
+
- name: Dump logs on failure
|
| 274 |
+
if: failure()
|
| 275 |
+
run: |
|
| 276 |
+
docker compose logs --no-color
|
| 277 |
+
|
| 278 |
+
- name: Tear down
|
| 279 |
+
if: always()
|
| 280 |
+
run: docker compose down -v
|
| 281 |
+
|
| 282 |
+
drug-repurposing-integration-test:
|
| 283 |
+
runs-on: ubuntu-latest
|
| 284 |
+
timeout-minutes: 45
|
| 285 |
+
|
| 286 |
+
steps:
|
| 287 |
+
- name: Checkout
|
| 288 |
+
uses: actions/checkout@v4
|
| 289 |
+
with:
|
| 290 |
+
lfs: true
|
| 291 |
+
- name: Set up Docker Buildx
|
| 292 |
+
uses: docker/setup-buildx-action@v3
|
| 293 |
+
|
| 294 |
+
- name: Build and start stack
|
| 295 |
+
|
| 296 |
+
run: |
|
| 297 |
+
docker compose down -v 2>/dev/null || true
|
| 298 |
+
docker builder prune -af || true
|
| 299 |
+
docker system df
|
| 300 |
+
|
| 301 |
+
COMPOSE_PARALLEL_LIMIT=1 docker compose build
|
| 302 |
+
docker system df
|
| 303 |
+
|
| 304 |
+
docker compose up -d --wait
|
| 305 |
+
|
| 306 |
+
- name: Verify drug-repurposing reachable from Laravel container
|
| 307 |
+
run: |
|
| 308 |
+
echo "=== Laravel → drug-repurposing DNS ==="
|
| 309 |
+
docker compose exec -T laravel getent hosts drug-repurposing
|
| 310 |
+
|
| 311 |
+
echo "=== Health check from Laravel container ==="
|
| 312 |
+
docker compose exec -T laravel curl -sf http://drug-repurposing:8000/health | tee /tmp/drug-repurposing-health.json
|
| 313 |
+
python3 - <<'PY'
|
| 314 |
+
import json, sys
|
| 315 |
+
data = json.load(open("/tmp/drug-repurposing-health.json"))
|
| 316 |
+
if data.get("status") != "healthy":
|
| 317 |
+
print("Expected healthy status:", data)
|
| 318 |
+
sys.exit(1)
|
| 319 |
+
print("drug-repurposing reachable from Laravel:", data.get("service"))
|
| 320 |
+
PY
|
| 321 |
+
|
| 322 |
+
echo "=== DRUG_REPURPOSING_URL in Laravel env ==="
|
| 323 |
+
docker compose exec -T laravel php -r 'echo "DRUG_REPURPOSING_URL=" . getenv("DRUG_REPURPOSING_URL") . PHP_EOL;'
|
| 324 |
+
|
| 325 |
+
- name: Run drug repurposing integration tests
|
| 326 |
+
run: |
|
| 327 |
+
# Match phpunit.xml: sync queue + sqlite so jobs run inside the test process
|
| 328 |
+
docker compose exec -T \
|
| 329 |
+
-e QUEUE_CONNECTION=sync \
|
| 330 |
+
laravel php artisan test --filter=DrugRepurposingIntegrationTest
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
- name: Dump logs on failure
|
| 334 |
+
if: failure()
|
| 335 |
+
run: |
|
| 336 |
+
docker compose logs --no-color laravel drug-repurposing queue
|
| 337 |
+
|
| 338 |
+
- name: Tear down
|
| 339 |
+
if: always()
|
| 340 |
+
run: docker compose down -v
|
| 341 |
+
|
| 342 |
+
docking-integration-test:
|
| 343 |
+
runs-on: ubuntu-latest
|
| 344 |
+
timeout-minutes: 45
|
| 345 |
+
|
| 346 |
+
steps:
|
| 347 |
+
- name: Checkout
|
| 348 |
+
uses: actions/checkout@v4
|
| 349 |
+
with:
|
| 350 |
+
lfs: true
|
| 351 |
+
- name: Set up Docker Buildx
|
| 352 |
+
uses: docker/setup-buildx-action@v3
|
| 353 |
+
|
| 354 |
+
- name: Build and start stack
|
| 355 |
+
run: |
|
| 356 |
+
docker compose down -v 2>/dev/null || true
|
| 357 |
+
docker compose build --build-arg COMPOSER_DEV="" laravel
|
| 358 |
+
docker compose up -d laravel
|
| 359 |
+
|
| 360 |
+
- name: Verify RDKit, Vina, and Open Babel in container
|
| 361 |
+
run: |
|
| 362 |
+
echo "Verifying python3 import rdkit"
|
| 363 |
+
docker compose exec -T laravel python3 -c "import rdkit; print(rdkit.__version__)"
|
| 364 |
+
echo "Verifying vina --help"
|
| 365 |
+
docker compose exec -T laravel vina --help
|
| 366 |
+
echo "Verifying obabel -V"
|
| 367 |
+
docker compose exec -T laravel obabel -V
|
| 368 |
+
|
| 369 |
+
- name: Run Laravel Feature Tests
|
| 370 |
+
run: |
|
| 371 |
+
docker compose exec -T laravel php artisan test --filter=DockingSubmitTest
|
| 372 |
+
|
| 373 |
+
- name: Tear down
|
| 374 |
+
if: always()
|
| 375 |
+
run: docker compose down -v
|
| 376 |
+
|
| 377 |
+
rag-integration-test:
|
| 378 |
+
runs-on: ubuntu-latest
|
| 379 |
+
timeout-minutes: 90
|
| 380 |
+
|
| 381 |
+
steps:
|
| 382 |
+
- name: Checkout
|
| 383 |
+
uses: actions/checkout@v4
|
| 384 |
+
with:
|
| 385 |
+
lfs: true
|
| 386 |
+
- name: Set up Docker Buildx
|
| 387 |
+
uses: docker/setup-buildx-action@v3
|
| 388 |
+
|
| 389 |
+
- name: Build and start stack
|
| 390 |
+
run: |
|
| 391 |
+
docker compose down -v 2>/dev/null || true
|
| 392 |
+
docker builder prune -af || true
|
| 393 |
+
docker system df
|
| 394 |
+
|
| 395 |
+
COMPOSE_PARALLEL_LIMIT=1 docker compose build
|
| 396 |
+
docker system df
|
| 397 |
+
|
| 398 |
+
docker compose up -d --wait
|
| 399 |
+
|
| 400 |
+
# Run fresh migrations to ensure clean database state
|
| 401 |
+
docker compose exec -T laravel php artisan migrate:fresh --force
|
| 402 |
+
|
| 403 |
+
- name: Seed verified test user
|
| 404 |
+
run: |
|
| 405 |
+
echo "=== Creating verified test user via PHP script ==="
|
| 406 |
+
docker compose exec -T laravel php -r "
|
| 407 |
+
require 'vendor/autoload.php';
|
| 408 |
+
\$app = require_once 'bootstrap/app.php';
|
| 409 |
+
\$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class);
|
| 410 |
+
\$kernel->bootstrap();
|
| 411 |
+
|
| 412 |
+
use App\Models\User;
|
| 413 |
+
use Illuminate\Support\Facades\Hash;
|
| 414 |
+
|
| 415 |
+
\$user = User::firstOrCreate(
|
| 416 |
+
['email' => 'salehyasmeen080@gmail.com'],
|
| 417 |
+
[
|
| 418 |
+
'name' => 'Test User',
|
| 419 |
+
'password' => Hash::make('123456789'),
|
| 420 |
+
'role' => 'normal',
|
| 421 |
+
'is_verified' => true,
|
| 422 |
+
'email_verified_at' => now(),
|
| 423 |
+
'created_at' => now(),
|
| 424 |
+
'updated_at' => now()
|
| 425 |
+
]
|
| 426 |
+
);
|
| 427 |
+
echo 'User ID: ' . \$user->id . PHP_EOL;
|
| 428 |
+
echo 'Email: ' . \$user->email . PHP_EOL;
|
| 429 |
+
echo 'Verified: ' . (\$user->is_verified ? 'YES' : 'NO') . PHP_EOL;
|
| 430 |
+
"
|
| 431 |
+
|
| 432 |
+
- name: Login and get Bearer token
|
| 433 |
+
run: |
|
| 434 |
+
echo "POST /api/user/login"
|
| 435 |
+
HTTP_CODE=$(curl -s -o login.json -w "%{http_code}" -X POST http://localhost:8080/api/user/login \
|
| 436 |
+
-H "Content-Type: application/json" \
|
| 437 |
+
-d '{"email":"salehyasmeen080@gmail.com","password":"123456789"}')
|
| 438 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 439 |
+
cat login.json
|
| 440 |
+
echo ""
|
| 441 |
+
|
| 442 |
+
if [ "$HTTP_CODE" != "200" ]; then
|
| 443 |
+
echo "Login failed with HTTP $HTTP_CODE"
|
| 444 |
+
docker compose logs laravel
|
| 445 |
+
exit 1
|
| 446 |
+
fi
|
| 447 |
+
|
| 448 |
+
python3 - <<'PY'
|
| 449 |
+
import json, sys
|
| 450 |
+
try:
|
| 451 |
+
data = json.load(open("login.json"))
|
| 452 |
+
except json.JSONDecodeError as e:
|
| 453 |
+
print("Invalid JSON response:", e)
|
| 454 |
+
with open("login.json") as f:
|
| 455 |
+
print("Raw response:", f.read())
|
| 456 |
+
sys.exit(1)
|
| 457 |
+
|
| 458 |
+
if not data.get("success"):
|
| 459 |
+
print("Login failed:", json.dumps(data, indent=2))
|
| 460 |
+
sys.exit(1)
|
| 461 |
+
|
| 462 |
+
token = data.get("data", {}).get("token")
|
| 463 |
+
if not token:
|
| 464 |
+
print("No token in response:", json.dumps(data, indent=2))
|
| 465 |
+
sys.exit(1)
|
| 466 |
+
|
| 467 |
+
with open("token.txt", "w") as f:
|
| 468 |
+
f.write(token)
|
| 469 |
+
print("Login OK - token saved")
|
| 470 |
+
PY
|
| 471 |
+
|
| 472 |
+
- name: Show service status
|
| 473 |
+
if: always()
|
| 474 |
+
run: docker compose ps -a
|
| 475 |
+
|
| 476 |
+
- name: Wait for Laravel API
|
| 477 |
+
run: |
|
| 478 |
+
for i in $(seq 1 60); do
|
| 479 |
+
if curl -sf http://localhost:8080/ > /dev/null 2>&1; then
|
| 480 |
+
echo "Laravel is up"
|
| 481 |
+
exit 0
|
| 482 |
+
fi
|
| 483 |
+
echo "Waiting for Laravel ($i/60)..."
|
| 484 |
+
sleep 5
|
| 485 |
+
done
|
| 486 |
+
docker compose logs laravel
|
| 487 |
+
exit 1
|
| 488 |
+
|
| 489 |
+
- name: Test retrieval-only chemical search
|
| 490 |
+
run: |
|
| 491 |
+
TOKEN=$(cat token.txt)
|
| 492 |
+
echo "POST /api/chemical-search (retrieval-only)"
|
| 493 |
+
HTTP_CODE=$(curl -s -o chemical_search.json -w "%{http_code}" -X POST http://localhost:8080/api/chemical-search \
|
| 494 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 495 |
+
-H "Content-Type: application/json" \
|
| 496 |
+
-d '{"smiles":"CC(=O)O"}')
|
| 497 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 498 |
+
cat chemical_search.json
|
| 499 |
+
echo ""
|
| 500 |
+
python3 - <<'PY'
|
| 501 |
+
import json, sys
|
| 502 |
+
data = json.load(open("chemical_search.json"))
|
| 503 |
+
if not data.get("success"):
|
| 504 |
+
print("Chemical search (retrieval-only) failed:", json.dumps(data, indent=2))
|
| 505 |
+
sys.exit(1)
|
| 506 |
+
if not isinstance(data.get("compounds"), list):
|
| 507 |
+
print("Missing 'compounds' array in response")
|
| 508 |
+
sys.exit(1)
|
| 509 |
+
if data.get("metadata", {}).get("source") != "retrieval":
|
| 510 |
+
print("Expected source='retrieval', got:", data.get("metadata", {}).get("source"))
|
| 511 |
+
sys.exit(1)
|
| 512 |
+
print("Chemical search (retrieval-only) OK - found", len(data["compounds"]), "compounds")
|
| 513 |
+
PY
|
| 514 |
+
|
| 515 |
+
- name: Test full RAG chemical search
|
| 516 |
+
run: |
|
| 517 |
+
TOKEN=$(cat token.txt)
|
| 518 |
+
echo "POST /api/chemical-search/full-rag"
|
| 519 |
+
HTTP_CODE=$(curl -s -o chemical_rag.json -w "%{http_code}" -X POST http://localhost:8080/api/chemical-search/full-rag \
|
| 520 |
+
-H "Authorization: Bearer $TOKEN" \
|
| 521 |
+
-H "Content-Type: application/json" \
|
| 522 |
+
-d '{"smiles":"CC(=O)O"}')
|
| 523 |
+
echo "HTTP Status: $HTTP_CODE"
|
| 524 |
+
cat chemical_rag.json
|
| 525 |
+
echo ""
|
| 526 |
+
python3 - <<'PY'
|
| 527 |
+
import json, sys
|
| 528 |
+
data = json.load(open("chemical_rag.json"))
|
| 529 |
+
if not data.get("success"):
|
| 530 |
+
print("Chemical search (full RAG) failed:", json.dumps(data, indent=2))
|
| 531 |
+
sys.exit(1)
|
| 532 |
+
if not isinstance(data.get("compounds"), list):
|
| 533 |
+
print("Missing 'compounds' array in response")
|
| 534 |
+
sys.exit(1)
|
| 535 |
+
if data.get("metadata", {}).get("source") != "full_rag":
|
| 536 |
+
print("Expected source='full_rag', got:", data.get("metadata", {}).get("source"))
|
| 537 |
+
sys.exit(1)
|
| 538 |
+
# Verify explanations exist for full RAG
|
| 539 |
+
for compound in data["compounds"]:
|
| 540 |
+
if not compound.get("explanation"):
|
| 541 |
+
print("Missing explanation for compound:", compound.get("name"))
|
| 542 |
+
sys.exit(1)
|
| 543 |
+
print("Chemical search (full RAG) OK - found", len(data["compounds"]), "compounds with explanations")
|
| 544 |
+
PY
|
| 545 |
+
|
| 546 |
+
- name: Dump logs on failure
|
| 547 |
+
if: failure()
|
| 548 |
+
run: |
|
| 549 |
+
docker compose logs --no-color
|
| 550 |
+
|
| 551 |
+
- name: Tear down
|
| 552 |
+
if: always()
|
| 553 |
+
run: docker compose down -v
|
.github/workflows/keep-hf-alive.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Keep HF Alive
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
schedule:
|
| 5 |
+
- cron: "0 */24 * * *"
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
keep_hf_alive_job:
|
| 10 |
+
name: keep hf alive job
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- name: Hit main API health endpoint
|
| 14 |
+
run: |
|
| 15 |
+
curl -s -o /dev/null -w "%{http_code}" \
|
| 16 |
+
"https://Ailixir-AI-Team-ailixir-api.hf.space/api/ai-services/health"
|
| 17 |
+
|
| 18 |
+
- name: Hit generation health endpoint
|
| 19 |
+
run: |
|
| 20 |
+
curl -s -o /dev/null -w "%{http_code}" \
|
| 21 |
+
"https://shdwrow-ailixir-generation.hf.space/health"
|
.github/workflows/md-simulation-health.yml
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: MD Simulation Health Check
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [MD-integration]
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
concurrency:
|
| 9 |
+
group: md-simulation-health-${{ github.ref }}
|
| 10 |
+
cancel-in-progress: true
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
md-simulation-health-check:
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
timeout-minutes: 30
|
| 16 |
+
|
| 17 |
+
steps:
|
| 18 |
+
- name: Checkout code
|
| 19 |
+
uses: actions/checkout@v4
|
| 20 |
+
with:
|
| 21 |
+
lfs: true
|
| 22 |
+
|
| 23 |
+
- name: Set up Docker Buildx
|
| 24 |
+
uses: docker/setup-buildx-action@v3
|
| 25 |
+
|
| 26 |
+
- name: Build Laravel image
|
| 27 |
+
run: docker build -t ailixir-laravel .
|
| 28 |
+
|
| 29 |
+
- name: Build MD simulation image
|
| 30 |
+
run: docker build -t md-simulation ai_apps/Protein-Ligand-MD-Simulation/
|
| 31 |
+
|
| 32 |
+
- name: Create Docker network
|
| 33 |
+
run: docker network create ailixir-md
|
| 34 |
+
|
| 35 |
+
- name: Start Laravel API
|
| 36 |
+
run: |
|
| 37 |
+
docker run -d --name ailixir-laravel \
|
| 38 |
+
--network ailixir-md \
|
| 39 |
+
-p 8000:8000 \
|
| 40 |
+
--env-file docker/laravel.env \
|
| 41 |
+
-e MD_SIMULATION_URL=http://md-api:5005 \
|
| 42 |
+
ailixir-laravel
|
| 43 |
+
|
| 44 |
+
- name: Start Redis
|
| 45 |
+
run: |
|
| 46 |
+
docker run -d --name md-redis \
|
| 47 |
+
--network ailixir-md \
|
| 48 |
+
--network-alias md-redis \
|
| 49 |
+
redis:7-alpine
|
| 50 |
+
|
| 51 |
+
- name: Start MD Simulation API
|
| 52 |
+
run: |
|
| 53 |
+
docker run -d --name md-api \
|
| 54 |
+
--network ailixir-md \
|
| 55 |
+
-p 5005:5005 \
|
| 56 |
+
-e REDIS_HOST=md-redis \
|
| 57 |
+
-e REDIS_PORT=6379 \
|
| 58 |
+
-e USE_REDIS=true \
|
| 59 |
+
-e OPENMM_PLATFORM=CPU \
|
| 60 |
+
-e FLASK_HOST=0.0.0.0 \
|
| 61 |
+
-e FLASK_PORT=5005 \
|
| 62 |
+
-e MD_DATA_DIR=/app/data \
|
| 63 |
+
-e LOG_LEVEL=INFO \
|
| 64 |
+
md-simulation
|
| 65 |
+
|
| 66 |
+
- name: Wait for Laravel API to be ready
|
| 67 |
+
run: |
|
| 68 |
+
echo "Waiting for Laravel API to be ready..."
|
| 69 |
+
for i in $(seq 1 30); do
|
| 70 |
+
if curl -sf http://localhost:8000/ > /dev/null 2>&1; then
|
| 71 |
+
echo "Laravel API is ready"
|
| 72 |
+
exit 0
|
| 73 |
+
fi
|
| 74 |
+
echo "Waiting for Laravel ($i/30)..."
|
| 75 |
+
sleep 2
|
| 76 |
+
done
|
| 77 |
+
docker logs ailixir-laravel
|
| 78 |
+
exit 1
|
| 79 |
+
|
| 80 |
+
- name: Wait for MD Simulation API to be healthy
|
| 81 |
+
run: |
|
| 82 |
+
echo "Waiting for MD Simulation API to be healthy..."
|
| 83 |
+
for i in $(seq 1 60); do
|
| 84 |
+
if curl -sf http://localhost:5005/health > /dev/null 2>&1; then
|
| 85 |
+
echo "MD Simulation API is healthy"
|
| 86 |
+
exit 0
|
| 87 |
+
fi
|
| 88 |
+
echo "Waiting for MD API ($i/60)..."
|
| 89 |
+
sleep 5
|
| 90 |
+
done
|
| 91 |
+
docker logs md-api
|
| 92 |
+
exit 1
|
| 93 |
+
|
| 94 |
+
- name: Test MD simulation health via Laravel proxy
|
| 95 |
+
run: |
|
| 96 |
+
echo "GET /api/md-simulation/health via Laravel proxy"
|
| 97 |
+
RESPONSE=$(curl -sf http://localhost:8000/api/md-simulation/health)
|
| 98 |
+
echo "$RESPONSE"
|
| 99 |
+
STATUS=$(echo "$RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status'))")
|
| 100 |
+
if [ "$STATUS" != "ok" ]; then
|
| 101 |
+
echo "Expected status 'ok', got '$STATUS'"
|
| 102 |
+
exit 1
|
| 103 |
+
fi
|
| 104 |
+
echo "MD Simulation service is healthy via Laravel proxy!"
|
| 105 |
+
|
| 106 |
+
- name: Dump logs on failure
|
| 107 |
+
if: failure()
|
| 108 |
+
run: |
|
| 109 |
+
docker logs ailixir-laravel || true
|
| 110 |
+
docker logs md-api || true
|
| 111 |
+
docker logs md-redis || true
|
| 112 |
+
|
| 113 |
+
- name: Cleanup
|
| 114 |
+
if: always()
|
| 115 |
+
run: |
|
| 116 |
+
docker rm -f ailixir-laravel md-api md-redis 2>/dev/null || true
|
| 117 |
+
docker network rm ailixir-md 2>/dev/null || true
|
.github/workflows/push-models-to-hf.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Push Models to HF Generation Space
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
|
| 6 |
+
jobs:
|
| 7 |
+
push-models:
|
| 8 |
+
runs-on: ubuntu-latest
|
| 9 |
+
steps:
|
| 10 |
+
- uses: actions/checkout@v4
|
| 11 |
+
|
| 12 |
+
- name: Upload models to HF dataset
|
| 13 |
+
run: |
|
| 14 |
+
pip install huggingface_hub
|
| 15 |
+
python -c "
|
| 16 |
+
from huggingface_hub import HfApi
|
| 17 |
+
api = HfApi()
|
| 18 |
+
api.upload_folder(
|
| 19 |
+
repo_id='shdwRow/ailixir-generation-models',
|
| 20 |
+
folder_path='ai_apps/generation/bundle/models',
|
| 21 |
+
repo_type='dataset',
|
| 22 |
+
token='${{ secrets.HF_TOKEN_SHDWROW }}'
|
| 23 |
+
)
|
| 24 |
+
print('Models uploaded successfully')
|
| 25 |
+
"
|
| 26 |
+
|
| 27 |
+
- name: Rebuild HF Space with latest models
|
| 28 |
+
run: |
|
| 29 |
+
curl -X POST \
|
| 30 |
+
"https://huggingface.co/api/spaces/shdwRow/ailixir-generation/restart" \
|
| 31 |
+
-H "Authorization: Bearer ${{ secrets.HF_TOKEN_SHDWROW }}"
|
.github/workflows/sync-hf-all.yml
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
# Automatically syncs code from GitHub main branch to all 5 Hugging Face Spaces.
|
| 4 |
+
# Each job syncs a different subset of the repo to a specific HF Space.
|
| 5 |
+
#
|
| 6 |
+
# Two HF accounts are used:
|
| 7 |
+
# - RottenShadow: ailixir-drug-repurposing, ailixir-chemical-rag
|
| 8 |
+
# - shdwRow (secondary): ailixir-admet, ailixir-generation
|
| 9 |
+
# - Ailixir-AI-Team: ailixir-api
|
| 10 |
+
#
|
| 11 |
+
# Required GitHub secrets:
|
| 12 |
+
# HF_TOKEN — HF access token for RottenShadow account (read+write permissions)
|
| 13 |
+
# HF_TOKEN_AILIXIR — HF access token for Ailixir-AI-Team account (read+write permissions)
|
| 14 |
+
# HF_TOKEN_SHDWROW — HF access token for shdwRow account (read+write permissions)
|
| 15 |
+
# GOOGLE_CLIENT_ID — Google OAuth client ID
|
| 16 |
+
# GOOGLE_CLIENT_SECRET — Google OAuth client secret
|
| 17 |
+
|
| 18 |
+
on:
|
| 19 |
+
push:
|
| 20 |
+
branches: [main, feature/hugging-face-deploy]
|
| 21 |
+
workflow_dispatch:
|
| 22 |
+
|
| 23 |
+
jobs:
|
| 24 |
+
# ── Backend API (Laravel) ──────────────────────────────────────
|
| 25 |
+
# Syncs the full repo minus AI app directories to Ailixir-AI-Team/ailixir-api.
|
| 26 |
+
# The live SQLite database (database/ailixir.sqlite) is preserved across syncs:
|
| 27 |
+
# - Excluded from rsync so it's never overwritten
|
| 28 |
+
# - Restored from HEAD via git checkout so live data isn't committed
|
| 29 |
+
backend:
|
| 30 |
+
runs-on: ubuntu-latest
|
| 31 |
+
steps:
|
| 32 |
+
- uses: actions/checkout@v4
|
| 33 |
+
with:
|
| 34 |
+
fetch-depth: 0
|
| 35 |
+
- name: Sync ailixir-api
|
| 36 |
+
env:
|
| 37 |
+
HF_TOKEN_AILIXIR: ${{ secrets.HF_TOKEN_AILIXIR }}
|
| 38 |
+
run: |
|
| 39 |
+
SPACE="Ailixir-AI-Team/ailixir-api"
|
| 40 |
+
REPO_URL="https://Ailixir-AI-Team:${HF_TOKEN_AILIXIR}@huggingface.co/spaces/${SPACE}"
|
| 41 |
+
git clone --depth 1 "$REPO_URL" /tmp/hf-space
|
| 42 |
+
cd /tmp/hf-space
|
| 43 |
+
# Sync files (new, modified, deleted) but preserve the live database
|
| 44 |
+
rsync -av --delete --exclude=ai_apps --exclude=.git --exclude=.env --exclude=.dockerignore --exclude=public/imgs --exclude=storage --exclude=Diagrams --exclude=database/ailixir.sqlite "$GITHUB_WORKSPACE/" .
|
| 45 |
+
cp "$GITHUB_WORKSPACE/README_HF.md" README.md 2>/dev/null || true
|
| 46 |
+
# Write .env with all required environment variables for HF Spaces
|
| 47 |
+
printf '%s\n' \
|
| 48 |
+
'RUN_MODE=hf' \
|
| 49 |
+
'APP_KEY=base64:T/MgQZht16genNJN3aZ5flCuvfe7Zfs4yFhuAFO2itU=' \
|
| 50 |
+
'AI_INTEGRATION_ROUTES_ENABLED=true' \
|
| 51 |
+
'APP_URL=https://Ailixir-AI-Team-ailixir-api.hf.space' \
|
| 52 |
+
'APP_DEBUG=true' \
|
| 53 |
+
'ADMET_AI_URL=https://shdwRow-ailixir-admet.hf.space' \
|
| 54 |
+
'CHEMICAL_AI_URL=https://RottenShadow-ailixir-chemical-rag.hf.space' \
|
| 55 |
+
'DRUG_REPURPOSING_URL=https://RottenShadow-ailixir-drug-repurposing.hf.space' \
|
| 56 |
+
'GENERATION_SERVICE_URL=https://shdwRow-ailixir-generation.hf.space' \
|
| 57 |
+
'GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}' \
|
| 58 |
+
'GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}' \
|
| 59 |
+
'GOOGLE_REDIRECT_URI=https://Ailixir-AI-Team-ailixir-api.hf.space/api/user/auth/google' \
|
| 60 |
+
'CLOUDINARY_CLOUD_NAME=dummy_cloud' \
|
| 61 |
+
'CLOUDINARY_API_KEY=dummy_key' \
|
| 62 |
+
'CLOUDINARY_API_SECRET=dummy_secret' \
|
| 63 |
+
'QUEUE_CONNECTION=sync' \
|
| 64 |
+
> .env
|
| 65 |
+
git config user.name "AILIXIR Bot"
|
| 66 |
+
git config user.email "bot@ailixir.app"
|
| 67 |
+
# Prevent the live database from being tracked/committed
|
| 68 |
+
git checkout HEAD -- database/ailixir.sqlite 2>/dev/null || true
|
| 69 |
+
git add -A
|
| 70 |
+
if git diff --cached --quiet; then echo " No changes"; else git commit -m "Auto-sync: $GITHUB_SHA"; git push; fi
|
| 71 |
+
|
| 72 |
+
# ── Drug Repurposing (FastAPI) ─────────────────────────────────
|
| 73 |
+
# Syncs ai_apps/Drug Reporposing/ to RottenShadow/ailixir-drug-repurposing.
|
| 74 |
+
# Renames Dockerfile.hf → Dockerfile if present.
|
| 75 |
+
# Can run in mock mode for testing (set USE_MOCK_MODEL=true in HF secrets).
|
| 76 |
+
drug-repurposing:
|
| 77 |
+
runs-on: ubuntu-latest
|
| 78 |
+
steps:
|
| 79 |
+
- uses: actions/checkout@v4
|
| 80 |
+
with:
|
| 81 |
+
fetch-depth: 0
|
| 82 |
+
- name: Sync ailixir-drug-repurposing
|
| 83 |
+
env:
|
| 84 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 85 |
+
run: |
|
| 86 |
+
SPACE="RottenShadow/ailixir-drug-repurposing"
|
| 87 |
+
REPO_URL="https://RottenShadow:${HF_TOKEN}@huggingface.co/spaces/${SPACE}"
|
| 88 |
+
git clone --depth 1 "$REPO_URL" /tmp/hf-space
|
| 89 |
+
cd /tmp/hf-space
|
| 90 |
+
rsync -av --delete --exclude=.git --exclude=__pycache__ --exclude='*.pyc' --exclude=digrams --exclude=docs --exclude=docker "$GITHUB_WORKSPACE/ai_apps/Drug Reporposing/" .
|
| 91 |
+
cp "$GITHUB_WORKSPACE/ai_apps/Drug Reporposing/README_HF.md" README.md 2>/dev/null || true
|
| 92 |
+
if [ -f Dockerfile.hf ]; then mv Dockerfile.hf Dockerfile; fi
|
| 93 |
+
git config user.name "AILIXIR Bot"
|
| 94 |
+
git config user.email "bot@ailixir.app"
|
| 95 |
+
git add -A
|
| 96 |
+
if git diff --cached --quiet; then echo " No changes"; else git commit -m "Auto-sync: $GITHUB_SHA"; git push; fi
|
| 97 |
+
|
| 98 |
+
# ── ADMET Prediction (FastAPI + PyTorch) ───────────────────────
|
| 99 |
+
# Syncs ai_apps/ADMIT/admet_inference/ to shdwRow/ailixir-admet.
|
| 100 |
+
# Uses HF Xet for binary .ckpt model files.
|
| 101 |
+
# Requires HF_TOKEN_SHDWROW secret (shdwRow account).
|
| 102 |
+
admet:
|
| 103 |
+
runs-on: ubuntu-latest
|
| 104 |
+
steps:
|
| 105 |
+
- uses: actions/checkout@v4
|
| 106 |
+
with:
|
| 107 |
+
fetch-depth: 0
|
| 108 |
+
- name: Install git-xet for binary file support
|
| 109 |
+
run: |
|
| 110 |
+
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/huggingface/xet-core/refs/heads/main/git_xet/install.sh | sh
|
| 111 |
+
git xet install
|
| 112 |
+
- name: Sync ailixir-admet
|
| 113 |
+
env:
|
| 114 |
+
HF_TOKEN_SHDWROW: ${{ secrets.HF_TOKEN_SHDWROW }}
|
| 115 |
+
run: |
|
| 116 |
+
SPACE="shdwRow/ailixir-admet"
|
| 117 |
+
REPO_URL="https://shdwRow:${HF_TOKEN_SHDWROW}@huggingface.co/spaces/${SPACE}"
|
| 118 |
+
git clone --depth 1 "$REPO_URL" /tmp/hf-space
|
| 119 |
+
cd /tmp/hf-space
|
| 120 |
+
rsync -av --delete --exclude=.git --exclude=datasets.rar "$GITHUB_WORKSPACE/ai_apps/ADMIT/admet_inference/" .
|
| 121 |
+
cp README_HF.md README.md 2>/dev/null || true
|
| 122 |
+
git config user.name "AILIXIR Bot"
|
| 123 |
+
git config user.email "bot@ailixir.app"
|
| 124 |
+
git config lfs.allowincompletepush true
|
| 125 |
+
git lfs install
|
| 126 |
+
git lfs track '*.ckpt'
|
| 127 |
+
git add -A
|
| 128 |
+
if git diff --cached --quiet; then echo " No changes"; else git commit -m "Auto-sync: $GITHUB_SHA"; git push; fi
|
| 129 |
+
|
| 130 |
+
# ── Chemical RAG (FastAPI + ChromaDB) ─────────────────────────
|
| 131 |
+
# Syncs ai_apps/chemical-rag-system/chemical-rag-system/ to RottenShadow/ailixir-chemical-rag.
|
| 132 |
+
chemical-rag:
|
| 133 |
+
runs-on: ubuntu-latest
|
| 134 |
+
steps:
|
| 135 |
+
- uses: actions/checkout@v4
|
| 136 |
+
with:
|
| 137 |
+
fetch-depth: 0
|
| 138 |
+
- name: Sync ailixir-chemical-rag
|
| 139 |
+
env:
|
| 140 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 141 |
+
run: |
|
| 142 |
+
SPACE="RottenShadow/ailixir-chemical-rag"
|
| 143 |
+
REPO_URL="https://RottenShadow:${HF_TOKEN}@huggingface.co/spaces/${SPACE}"
|
| 144 |
+
git clone --depth 1 "$REPO_URL" /tmp/hf-space
|
| 145 |
+
cd /tmp/hf-space
|
| 146 |
+
rsync -av --delete --exclude=.git --exclude=digrams "$GITHUB_WORKSPACE/ai_apps/chemical-rag-system/chemical-rag-system/" .
|
| 147 |
+
cp README_HF.md README.md 2>/dev/null || true
|
| 148 |
+
git config user.name "AILIXIR Bot"
|
| 149 |
+
git config user.email "bot@ailixir.app"
|
| 150 |
+
git add -A
|
| 151 |
+
if git diff --cached --quiet; then echo " No changes"; else git commit -m "Auto-sync: $GITHUB_SHA"; git push; fi
|
| 152 |
+
|
| 153 |
+
# ── Molecular Generation (FastAPI + REINVENT4) ────────────────
|
| 154 |
+
# Syncs ai_apps/generation/ to shdwRow/ailixir-generation.
|
| 155 |
+
# Renames Dockerfile.hf → Dockerfile (or Dockerfile.api → Dockerfile as fallback).
|
| 156 |
+
# Uses HF Xet for binary .ckpt, .pt, .pkl model files.
|
| 157 |
+
# Requires HF_TOKEN_SHDWROW secret (shdwRow account).
|
| 158 |
+
generation:
|
| 159 |
+
runs-on: ubuntu-latest
|
| 160 |
+
steps:
|
| 161 |
+
- uses: actions/checkout@v4
|
| 162 |
+
with:
|
| 163 |
+
fetch-depth: 0
|
| 164 |
+
- name: Install git-xet for binary file support
|
| 165 |
+
run: |
|
| 166 |
+
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/huggingface/xet-core/refs/heads/main/git_xet/install.sh | sh
|
| 167 |
+
git xet install
|
| 168 |
+
- name: Sync ailixir-generation
|
| 169 |
+
env:
|
| 170 |
+
HF_TOKEN_SHDWROW: ${{ secrets.HF_TOKEN_SHDWROW }}
|
| 171 |
+
run: |
|
| 172 |
+
SPACE="shdwRow/ailixir-generation"
|
| 173 |
+
REPO_URL="https://shdwRow:${HF_TOKEN_SHDWROW}@huggingface.co/spaces/${SPACE}"
|
| 174 |
+
git clone --depth 1 "$REPO_URL" /tmp/hf-space
|
| 175 |
+
cd /tmp/hf-space
|
| 176 |
+
rsync -av --delete --exclude=.git --exclude=bundle/models "$GITHUB_WORKSPACE/ai_apps/generation/" .
|
| 177 |
+
cp "$GITHUB_WORKSPACE/ai_apps/generation/README_HF.md" README.md 2>/dev/null || true
|
| 178 |
+
if [ -f Dockerfile.hf ]; then mv Dockerfile.hf Dockerfile; elif [ -f Dockerfile.api ]; then mv Dockerfile.api Dockerfile; fi
|
| 179 |
+
git config user.name "AILIXIR Bot"
|
| 180 |
+
git config user.email "bot@ailixir.app"
|
| 181 |
+
git add -A
|
| 182 |
+
if git diff --cached --quiet; then echo " No changes"; else git commit -m "Auto-sync: $GITHUB_SHA"; git push; fi
|
| 183 |
+
|
| 184 |
+
chat-bot:
|
| 185 |
+
runs-on: ubuntu-latest
|
| 186 |
+
steps:
|
| 187 |
+
- uses: actions/checkout@v4
|
| 188 |
+
with:
|
| 189 |
+
fetch-depth: 0
|
| 190 |
+
|
| 191 |
+
- name: Install Hugging Face CLI
|
| 192 |
+
run: pip install -U "huggingface_hub[cli]"
|
| 193 |
+
|
| 194 |
+
- name: Add Gemini key to Hugging Face Space secrets
|
| 195 |
+
env:
|
| 196 |
+
HF_TOKEN_SHDWROW: ${{ secrets.HF_TOKEN_SHDWROW }}
|
| 197 |
+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
| 198 |
+
run: |
|
| 199 |
+
hf spaces secrets add shdwRow/ailixir-chat-bot \
|
| 200 |
+
-s GEMINI_API_KEY="$GEMINI_API_KEY" \
|
| 201 |
+
--token "$HF_TOKEN_SHDWROW"
|
| 202 |
+
|
| 203 |
+
- name: Sync ailixir-chat-bot
|
| 204 |
+
env:
|
| 205 |
+
HF_TOKEN_SHDWROW: ${{ secrets.HF_TOKEN_SHDWROW }}
|
| 206 |
+
run: |
|
| 207 |
+
SPACE="shdwRow/ailixir-chat-bot"
|
| 208 |
+
REPO_URL="https://shdwRow:${HF_TOKEN_SHDWROW}@huggingface.co/spaces/${SPACE}"
|
| 209 |
+
|
| 210 |
+
git clone --depth 1 "$REPO_URL" /tmp/hf-chat-bot
|
| 211 |
+
cd /tmp/hf-chat-bot
|
| 212 |
+
|
| 213 |
+
rsync -av --delete \
|
| 214 |
+
--exclude=.git \
|
| 215 |
+
--exclude=.env \
|
| 216 |
+
--exclude=__pycache__ \
|
| 217 |
+
--exclude='*.pyc' \
|
| 218 |
+
--exclude='*.db' \
|
| 219 |
+
--exclude='*.db-shm' \
|
| 220 |
+
--exclude='*.db-wal' \
|
| 221 |
+
"$GITHUB_WORKSPACE/ai_apps/chem_agent_api/" .
|
| 222 |
+
|
| 223 |
+
git config user.name "AILIXIR Bot"
|
| 224 |
+
git config user.email "bot@ailixir.app"
|
| 225 |
+
|
| 226 |
+
git add -A
|
| 227 |
+
if git diff --cached --quiet; then
|
| 228 |
+
echo "No changes"
|
| 229 |
+
else
|
| 230 |
+
git commit -m "Auto-sync chat bot: $GITHUB_SHA"
|
| 231 |
+
git push
|
| 232 |
+
fi
|
.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.log
|
| 2 |
+
.DS_Store
|
| 3 |
+
.env
|
| 4 |
+
.env.backup
|
| 5 |
+
.env.production
|
| 6 |
+
.phpactor.json
|
| 7 |
+
.phpunit.result.cache
|
| 8 |
+
/.fleet
|
| 9 |
+
/.idea
|
| 10 |
+
/.nova
|
| 11 |
+
/.phpunit.cache
|
| 12 |
+
/.vscode
|
| 13 |
+
/.zed
|
| 14 |
+
/auth.json
|
| 15 |
+
/node_modules
|
| 16 |
+
/public/build
|
| 17 |
+
/public/hot
|
| 18 |
+
/public/storage
|
| 19 |
+
/storage/*.key
|
| 20 |
+
/storage/pail
|
| 21 |
+
/vendor
|
| 22 |
+
Homestead.json
|
| 23 |
+
Homestead.yaml
|
| 24 |
+
Thumbs.db
|
| 25 |
+
.env
|
| 26 |
+
__pycache__/
|
| 27 |
+
*.pyc
|
| 28 |
+
*.sqlite
|
| 29 |
+
*.sqlite-wal
|
| 30 |
+
*.sqlite-shm
|
| 31 |
+
|
| 32 |
+
ai_apps/chem_agent_api/.env
|
| 33 |
+
ai_apps/chem_agent_api/_pycache_/
|
| 34 |
+
ai_apps/chem_agent_api/pycache/
|
| 35 |
+
ai_apps/chem_agent_api/*.db
|
| 36 |
+
ai_apps/chem_agent_api/*.db-shm
|
| 37 |
+
ai_apps/chem_agent_api/*.db-wal
|
.scribe/.filehashes
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GENERATED. YOU SHOULDN'T MODIFY OR DELETE THIS FILE.
|
| 2 |
+
# Scribe uses this file to know when you change something manually in your docs.
|
| 3 |
+
.scribe/intro.md=ac7eb5bb370d74c7b07631610e041687
|
| 4 |
+
.scribe/auth.md=9bee2b1ef8a238b2e58613fa636d5f39
|
.scribe/auth.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Authenticating requests
|
| 2 |
+
|
| 3 |
+
This API is not authenticated.
|
.scribe/endpoints.cache/00.yaml
ADDED
|
@@ -0,0 +1,1503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Autogenerated by Scribe. DO NOT MODIFY.
|
| 2 |
+
|
| 3 |
+
name: Endpoints
|
| 4 |
+
description: ''
|
| 5 |
+
endpoints:
|
| 6 |
+
-
|
| 7 |
+
custom: []
|
| 8 |
+
httpMethods:
|
| 9 |
+
- GET
|
| 10 |
+
uri: api/awards
|
| 11 |
+
metadata:
|
| 12 |
+
custom: []
|
| 13 |
+
groupName: Endpoints
|
| 14 |
+
groupDescription: ''
|
| 15 |
+
subgroup: ''
|
| 16 |
+
subgroupDescription: ''
|
| 17 |
+
title: ''
|
| 18 |
+
description: ''
|
| 19 |
+
authenticated: false
|
| 20 |
+
deprecated: false
|
| 21 |
+
headers:
|
| 22 |
+
Content-Type: application/json
|
| 23 |
+
Accept: application/json
|
| 24 |
+
urlParameters: []
|
| 25 |
+
cleanUrlParameters: []
|
| 26 |
+
queryParameters: []
|
| 27 |
+
cleanQueryParameters: []
|
| 28 |
+
bodyParameters: []
|
| 29 |
+
cleanBodyParameters: []
|
| 30 |
+
fileParameters: []
|
| 31 |
+
responses:
|
| 32 |
+
-
|
| 33 |
+
custom: []
|
| 34 |
+
status: 404
|
| 35 |
+
content: '{"success":false,"message":"The route api\/awards could not be found."}'
|
| 36 |
+
headers:
|
| 37 |
+
cache-control: 'no-cache, private'
|
| 38 |
+
content-type: application/json
|
| 39 |
+
access-control-allow-origin: '*'
|
| 40 |
+
description: null
|
| 41 |
+
responseFields: []
|
| 42 |
+
auth: []
|
| 43 |
+
controller: null
|
| 44 |
+
method: null
|
| 45 |
+
route: null
|
| 46 |
+
-
|
| 47 |
+
custom: []
|
| 48 |
+
httpMethods:
|
| 49 |
+
- GET
|
| 50 |
+
uri: 'api/awards/{id}'
|
| 51 |
+
metadata:
|
| 52 |
+
custom: []
|
| 53 |
+
groupName: Endpoints
|
| 54 |
+
groupDescription: ''
|
| 55 |
+
subgroup: ''
|
| 56 |
+
subgroupDescription: ''
|
| 57 |
+
title: ''
|
| 58 |
+
description: ''
|
| 59 |
+
authenticated: false
|
| 60 |
+
deprecated: false
|
| 61 |
+
headers:
|
| 62 |
+
Content-Type: application/json
|
| 63 |
+
Accept: application/json
|
| 64 |
+
urlParameters:
|
| 65 |
+
id:
|
| 66 |
+
custom: []
|
| 67 |
+
name: id
|
| 68 |
+
description: 'The ID of the award.'
|
| 69 |
+
required: true
|
| 70 |
+
example: consequatur
|
| 71 |
+
type: string
|
| 72 |
+
enumValues: []
|
| 73 |
+
exampleWasSpecified: false
|
| 74 |
+
nullable: false
|
| 75 |
+
deprecated: false
|
| 76 |
+
cleanUrlParameters:
|
| 77 |
+
id: consequatur
|
| 78 |
+
queryParameters: []
|
| 79 |
+
cleanQueryParameters: []
|
| 80 |
+
bodyParameters: []
|
| 81 |
+
cleanBodyParameters: []
|
| 82 |
+
fileParameters: []
|
| 83 |
+
responses:
|
| 84 |
+
-
|
| 85 |
+
custom: []
|
| 86 |
+
status: 404
|
| 87 |
+
content: '{"success":false,"message":"The route api\/awards\/consequatur could not be found."}'
|
| 88 |
+
headers:
|
| 89 |
+
cache-control: 'no-cache, private'
|
| 90 |
+
content-type: application/json
|
| 91 |
+
access-control-allow-origin: '*'
|
| 92 |
+
description: null
|
| 93 |
+
responseFields: []
|
| 94 |
+
auth: []
|
| 95 |
+
controller: null
|
| 96 |
+
method: null
|
| 97 |
+
route: null
|
| 98 |
+
-
|
| 99 |
+
custom: []
|
| 100 |
+
httpMethods:
|
| 101 |
+
- GET
|
| 102 |
+
uri: 'api/awards/{id}/scientists'
|
| 103 |
+
metadata:
|
| 104 |
+
custom: []
|
| 105 |
+
groupName: Endpoints
|
| 106 |
+
groupDescription: ''
|
| 107 |
+
subgroup: ''
|
| 108 |
+
subgroupDescription: ''
|
| 109 |
+
title: ''
|
| 110 |
+
description: ''
|
| 111 |
+
authenticated: false
|
| 112 |
+
deprecated: false
|
| 113 |
+
headers:
|
| 114 |
+
Content-Type: application/json
|
| 115 |
+
Accept: application/json
|
| 116 |
+
urlParameters:
|
| 117 |
+
id:
|
| 118 |
+
custom: []
|
| 119 |
+
name: id
|
| 120 |
+
description: 'The ID of the award.'
|
| 121 |
+
required: true
|
| 122 |
+
example: consequatur
|
| 123 |
+
type: string
|
| 124 |
+
enumValues: []
|
| 125 |
+
exampleWasSpecified: false
|
| 126 |
+
nullable: false
|
| 127 |
+
deprecated: false
|
| 128 |
+
cleanUrlParameters:
|
| 129 |
+
id: consequatur
|
| 130 |
+
queryParameters: []
|
| 131 |
+
cleanQueryParameters: []
|
| 132 |
+
bodyParameters: []
|
| 133 |
+
cleanBodyParameters: []
|
| 134 |
+
fileParameters: []
|
| 135 |
+
responses:
|
| 136 |
+
-
|
| 137 |
+
custom: []
|
| 138 |
+
status: 404
|
| 139 |
+
content: '{"success":false,"message":"The route api\/awards\/consequatur\/scientists could not be found."}'
|
| 140 |
+
headers:
|
| 141 |
+
cache-control: 'no-cache, private'
|
| 142 |
+
content-type: application/json
|
| 143 |
+
access-control-allow-origin: '*'
|
| 144 |
+
description: null
|
| 145 |
+
responseFields: []
|
| 146 |
+
auth: []
|
| 147 |
+
controller: null
|
| 148 |
+
method: null
|
| 149 |
+
route: null
|
| 150 |
+
-
|
| 151 |
+
custom: []
|
| 152 |
+
httpMethods:
|
| 153 |
+
- GET
|
| 154 |
+
uri: api/scientists
|
| 155 |
+
metadata:
|
| 156 |
+
custom: []
|
| 157 |
+
groupName: Endpoints
|
| 158 |
+
groupDescription: ''
|
| 159 |
+
subgroup: ''
|
| 160 |
+
subgroupDescription: ''
|
| 161 |
+
title: ''
|
| 162 |
+
description: ''
|
| 163 |
+
authenticated: false
|
| 164 |
+
deprecated: false
|
| 165 |
+
headers:
|
| 166 |
+
Content-Type: application/json
|
| 167 |
+
Accept: application/json
|
| 168 |
+
urlParameters: []
|
| 169 |
+
cleanUrlParameters: []
|
| 170 |
+
queryParameters: []
|
| 171 |
+
cleanQueryParameters: []
|
| 172 |
+
bodyParameters: []
|
| 173 |
+
cleanBodyParameters: []
|
| 174 |
+
fileParameters: []
|
| 175 |
+
responses:
|
| 176 |
+
-
|
| 177 |
+
custom: []
|
| 178 |
+
status: 404
|
| 179 |
+
content: '{"success":false,"message":"The route api\/scientists could not be found."}'
|
| 180 |
+
headers:
|
| 181 |
+
cache-control: 'no-cache, private'
|
| 182 |
+
content-type: application/json
|
| 183 |
+
access-control-allow-origin: '*'
|
| 184 |
+
description: null
|
| 185 |
+
responseFields: []
|
| 186 |
+
auth: []
|
| 187 |
+
controller: null
|
| 188 |
+
method: null
|
| 189 |
+
route: null
|
| 190 |
+
-
|
| 191 |
+
custom: []
|
| 192 |
+
httpMethods:
|
| 193 |
+
- GET
|
| 194 |
+
uri: 'api/scientists/{id}'
|
| 195 |
+
metadata:
|
| 196 |
+
custom: []
|
| 197 |
+
groupName: Endpoints
|
| 198 |
+
groupDescription: ''
|
| 199 |
+
subgroup: ''
|
| 200 |
+
subgroupDescription: ''
|
| 201 |
+
title: ''
|
| 202 |
+
description: ''
|
| 203 |
+
authenticated: false
|
| 204 |
+
deprecated: false
|
| 205 |
+
headers:
|
| 206 |
+
Content-Type: application/json
|
| 207 |
+
Accept: application/json
|
| 208 |
+
urlParameters:
|
| 209 |
+
id:
|
| 210 |
+
custom: []
|
| 211 |
+
name: id
|
| 212 |
+
description: 'The ID of the scientist.'
|
| 213 |
+
required: true
|
| 214 |
+
example: consequatur
|
| 215 |
+
type: string
|
| 216 |
+
enumValues: []
|
| 217 |
+
exampleWasSpecified: false
|
| 218 |
+
nullable: false
|
| 219 |
+
deprecated: false
|
| 220 |
+
cleanUrlParameters:
|
| 221 |
+
id: consequatur
|
| 222 |
+
queryParameters: []
|
| 223 |
+
cleanQueryParameters: []
|
| 224 |
+
bodyParameters: []
|
| 225 |
+
cleanBodyParameters: []
|
| 226 |
+
fileParameters: []
|
| 227 |
+
responses:
|
| 228 |
+
-
|
| 229 |
+
custom: []
|
| 230 |
+
status: 404
|
| 231 |
+
content: '{"success":false,"message":"The route api\/scientists\/consequatur could not be found."}'
|
| 232 |
+
headers:
|
| 233 |
+
cache-control: 'no-cache, private'
|
| 234 |
+
content-type: application/json
|
| 235 |
+
access-control-allow-origin: '*'
|
| 236 |
+
description: null
|
| 237 |
+
responseFields: []
|
| 238 |
+
auth: []
|
| 239 |
+
controller: null
|
| 240 |
+
method: null
|
| 241 |
+
route: null
|
| 242 |
+
-
|
| 243 |
+
custom: []
|
| 244 |
+
httpMethods:
|
| 245 |
+
- GET
|
| 246 |
+
uri: 'api/scientists/{id}/awards'
|
| 247 |
+
metadata:
|
| 248 |
+
custom: []
|
| 249 |
+
groupName: Endpoints
|
| 250 |
+
groupDescription: ''
|
| 251 |
+
subgroup: ''
|
| 252 |
+
subgroupDescription: ''
|
| 253 |
+
title: ''
|
| 254 |
+
description: ''
|
| 255 |
+
authenticated: false
|
| 256 |
+
deprecated: false
|
| 257 |
+
headers:
|
| 258 |
+
Content-Type: application/json
|
| 259 |
+
Accept: application/json
|
| 260 |
+
urlParameters:
|
| 261 |
+
id:
|
| 262 |
+
custom: []
|
| 263 |
+
name: id
|
| 264 |
+
description: 'The ID of the scientist.'
|
| 265 |
+
required: true
|
| 266 |
+
example: consequatur
|
| 267 |
+
type: string
|
| 268 |
+
enumValues: []
|
| 269 |
+
exampleWasSpecified: false
|
| 270 |
+
nullable: false
|
| 271 |
+
deprecated: false
|
| 272 |
+
cleanUrlParameters:
|
| 273 |
+
id: consequatur
|
| 274 |
+
queryParameters: []
|
| 275 |
+
cleanQueryParameters: []
|
| 276 |
+
bodyParameters: []
|
| 277 |
+
cleanBodyParameters: []
|
| 278 |
+
fileParameters: []
|
| 279 |
+
responses:
|
| 280 |
+
-
|
| 281 |
+
custom: []
|
| 282 |
+
status: 404
|
| 283 |
+
content: '{"success":false,"message":"The route api\/scientists\/consequatur\/awards could not be found."}'
|
| 284 |
+
headers:
|
| 285 |
+
cache-control: 'no-cache, private'
|
| 286 |
+
content-type: application/json
|
| 287 |
+
access-control-allow-origin: '*'
|
| 288 |
+
description: null
|
| 289 |
+
responseFields: []
|
| 290 |
+
auth: []
|
| 291 |
+
controller: null
|
| 292 |
+
method: null
|
| 293 |
+
route: null
|
| 294 |
+
-
|
| 295 |
+
custom: []
|
| 296 |
+
httpMethods:
|
| 297 |
+
- POST
|
| 298 |
+
uri: api/user/register
|
| 299 |
+
metadata:
|
| 300 |
+
custom: []
|
| 301 |
+
groupName: Endpoints
|
| 302 |
+
groupDescription: ''
|
| 303 |
+
subgroup: ''
|
| 304 |
+
subgroupDescription: ''
|
| 305 |
+
title: '---------------- Auth ----------------'
|
| 306 |
+
description: ''
|
| 307 |
+
authenticated: false
|
| 308 |
+
deprecated: false
|
| 309 |
+
headers:
|
| 310 |
+
Content-Type: application/json
|
| 311 |
+
Accept: application/json
|
| 312 |
+
urlParameters: []
|
| 313 |
+
cleanUrlParameters: []
|
| 314 |
+
queryParameters: []
|
| 315 |
+
cleanQueryParameters: []
|
| 316 |
+
bodyParameters:
|
| 317 |
+
name:
|
| 318 |
+
custom: []
|
| 319 |
+
name: name
|
| 320 |
+
description: 'Must not be greater than 255 characters.'
|
| 321 |
+
required: true
|
| 322 |
+
example: vmqeopfuudtdsufvyvddq
|
| 323 |
+
type: string
|
| 324 |
+
enumValues: []
|
| 325 |
+
exampleWasSpecified: false
|
| 326 |
+
nullable: false
|
| 327 |
+
deprecated: false
|
| 328 |
+
email:
|
| 329 |
+
custom: []
|
| 330 |
+
name: email
|
| 331 |
+
description: 'Must be a valid email address.'
|
| 332 |
+
required: true
|
| 333 |
+
example: kunde.eloisa@example.com
|
| 334 |
+
type: string
|
| 335 |
+
enumValues: []
|
| 336 |
+
exampleWasSpecified: false
|
| 337 |
+
nullable: false
|
| 338 |
+
deprecated: false
|
| 339 |
+
password:
|
| 340 |
+
custom: []
|
| 341 |
+
name: password
|
| 342 |
+
description: 'Must be at least 6 characters.'
|
| 343 |
+
required: true
|
| 344 |
+
example: '4[*UyPJ"}6'
|
| 345 |
+
type: string
|
| 346 |
+
enumValues: []
|
| 347 |
+
exampleWasSpecified: false
|
| 348 |
+
nullable: false
|
| 349 |
+
deprecated: false
|
| 350 |
+
cleanBodyParameters:
|
| 351 |
+
name: vmqeopfuudtdsufvyvddq
|
| 352 |
+
email: kunde.eloisa@example.com
|
| 353 |
+
password: '4[*UyPJ"}6'
|
| 354 |
+
fileParameters: []
|
| 355 |
+
responses: []
|
| 356 |
+
responseFields: []
|
| 357 |
+
auth: []
|
| 358 |
+
controller: null
|
| 359 |
+
method: null
|
| 360 |
+
route: null
|
| 361 |
+
-
|
| 362 |
+
custom: []
|
| 363 |
+
httpMethods:
|
| 364 |
+
- POST
|
| 365 |
+
uri: api/user/login
|
| 366 |
+
metadata:
|
| 367 |
+
custom: []
|
| 368 |
+
groupName: Endpoints
|
| 369 |
+
groupDescription: ''
|
| 370 |
+
subgroup: ''
|
| 371 |
+
subgroupDescription: ''
|
| 372 |
+
title: ''
|
| 373 |
+
description: ''
|
| 374 |
+
authenticated: false
|
| 375 |
+
deprecated: false
|
| 376 |
+
headers:
|
| 377 |
+
Content-Type: application/json
|
| 378 |
+
Accept: application/json
|
| 379 |
+
urlParameters: []
|
| 380 |
+
cleanUrlParameters: []
|
| 381 |
+
queryParameters: []
|
| 382 |
+
cleanQueryParameters: []
|
| 383 |
+
bodyParameters:
|
| 384 |
+
email:
|
| 385 |
+
custom: []
|
| 386 |
+
name: email
|
| 387 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 388 |
+
required: true
|
| 389 |
+
example: qkunze@example.com
|
| 390 |
+
type: string
|
| 391 |
+
enumValues: []
|
| 392 |
+
exampleWasSpecified: false
|
| 393 |
+
nullable: false
|
| 394 |
+
deprecated: false
|
| 395 |
+
password:
|
| 396 |
+
custom: []
|
| 397 |
+
name: password
|
| 398 |
+
description: ''
|
| 399 |
+
required: true
|
| 400 |
+
example: 'O[2UZ5ij-e/dl4m{o,'
|
| 401 |
+
type: string
|
| 402 |
+
enumValues: []
|
| 403 |
+
exampleWasSpecified: false
|
| 404 |
+
nullable: false
|
| 405 |
+
deprecated: false
|
| 406 |
+
cleanBodyParameters:
|
| 407 |
+
email: qkunze@example.com
|
| 408 |
+
password: 'O[2UZ5ij-e/dl4m{o,'
|
| 409 |
+
fileParameters: []
|
| 410 |
+
responses: []
|
| 411 |
+
responseFields: []
|
| 412 |
+
auth: []
|
| 413 |
+
controller: null
|
| 414 |
+
method: null
|
| 415 |
+
route: null
|
| 416 |
+
-
|
| 417 |
+
custom: []
|
| 418 |
+
httpMethods:
|
| 419 |
+
- POST
|
| 420 |
+
uri: api/user/verify-email
|
| 421 |
+
metadata:
|
| 422 |
+
custom: []
|
| 423 |
+
groupName: Endpoints
|
| 424 |
+
groupDescription: ''
|
| 425 |
+
subgroup: ''
|
| 426 |
+
subgroupDescription: ''
|
| 427 |
+
title: '---------------- Email Verification ----------------'
|
| 428 |
+
description: ''
|
| 429 |
+
authenticated: false
|
| 430 |
+
deprecated: false
|
| 431 |
+
headers:
|
| 432 |
+
Content-Type: application/json
|
| 433 |
+
Accept: application/json
|
| 434 |
+
urlParameters: []
|
| 435 |
+
cleanUrlParameters: []
|
| 436 |
+
queryParameters: []
|
| 437 |
+
cleanQueryParameters: []
|
| 438 |
+
bodyParameters:
|
| 439 |
+
email:
|
| 440 |
+
custom: []
|
| 441 |
+
name: email
|
| 442 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 443 |
+
required: true
|
| 444 |
+
example: qkunze@example.com
|
| 445 |
+
type: string
|
| 446 |
+
enumValues: []
|
| 447 |
+
exampleWasSpecified: false
|
| 448 |
+
nullable: false
|
| 449 |
+
deprecated: false
|
| 450 |
+
otp:
|
| 451 |
+
custom: []
|
| 452 |
+
name: otp
|
| 453 |
+
description: 'Must be 6 digits.'
|
| 454 |
+
required: true
|
| 455 |
+
example: '810798'
|
| 456 |
+
type: string
|
| 457 |
+
enumValues: []
|
| 458 |
+
exampleWasSpecified: false
|
| 459 |
+
nullable: false
|
| 460 |
+
deprecated: false
|
| 461 |
+
cleanBodyParameters:
|
| 462 |
+
email: qkunze@example.com
|
| 463 |
+
otp: '810798'
|
| 464 |
+
fileParameters: []
|
| 465 |
+
responses: []
|
| 466 |
+
responseFields: []
|
| 467 |
+
auth: []
|
| 468 |
+
controller: null
|
| 469 |
+
method: null
|
| 470 |
+
route: null
|
| 471 |
+
-
|
| 472 |
+
custom: []
|
| 473 |
+
httpMethods:
|
| 474 |
+
- POST
|
| 475 |
+
uri: api/user/resend-otp
|
| 476 |
+
metadata:
|
| 477 |
+
custom: []
|
| 478 |
+
groupName: Endpoints
|
| 479 |
+
groupDescription: ''
|
| 480 |
+
subgroup: ''
|
| 481 |
+
subgroupDescription: ''
|
| 482 |
+
title: ''
|
| 483 |
+
description: ''
|
| 484 |
+
authenticated: false
|
| 485 |
+
deprecated: false
|
| 486 |
+
headers:
|
| 487 |
+
Content-Type: application/json
|
| 488 |
+
Accept: application/json
|
| 489 |
+
urlParameters: []
|
| 490 |
+
cleanUrlParameters: []
|
| 491 |
+
queryParameters: []
|
| 492 |
+
cleanQueryParameters: []
|
| 493 |
+
bodyParameters:
|
| 494 |
+
email:
|
| 495 |
+
custom: []
|
| 496 |
+
name: email
|
| 497 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 498 |
+
required: true
|
| 499 |
+
example: qkunze@example.com
|
| 500 |
+
type: string
|
| 501 |
+
enumValues: []
|
| 502 |
+
exampleWasSpecified: false
|
| 503 |
+
nullable: false
|
| 504 |
+
deprecated: false
|
| 505 |
+
cleanBodyParameters:
|
| 506 |
+
email: qkunze@example.com
|
| 507 |
+
fileParameters: []
|
| 508 |
+
responses: []
|
| 509 |
+
responseFields: []
|
| 510 |
+
auth: []
|
| 511 |
+
controller: null
|
| 512 |
+
method: null
|
| 513 |
+
route: null
|
| 514 |
+
-
|
| 515 |
+
custom: []
|
| 516 |
+
httpMethods:
|
| 517 |
+
- POST
|
| 518 |
+
uri: api/user/forgot-password
|
| 519 |
+
metadata:
|
| 520 |
+
custom: []
|
| 521 |
+
groupName: Endpoints
|
| 522 |
+
groupDescription: ''
|
| 523 |
+
subgroup: ''
|
| 524 |
+
subgroupDescription: ''
|
| 525 |
+
title: '---------------- Password Reset ----------------'
|
| 526 |
+
description: ''
|
| 527 |
+
authenticated: false
|
| 528 |
+
deprecated: false
|
| 529 |
+
headers:
|
| 530 |
+
Content-Type: application/json
|
| 531 |
+
Accept: application/json
|
| 532 |
+
urlParameters: []
|
| 533 |
+
cleanUrlParameters: []
|
| 534 |
+
queryParameters: []
|
| 535 |
+
cleanQueryParameters: []
|
| 536 |
+
bodyParameters:
|
| 537 |
+
email:
|
| 538 |
+
custom: []
|
| 539 |
+
name: email
|
| 540 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 541 |
+
required: true
|
| 542 |
+
example: qkunze@example.com
|
| 543 |
+
type: string
|
| 544 |
+
enumValues: []
|
| 545 |
+
exampleWasSpecified: false
|
| 546 |
+
nullable: false
|
| 547 |
+
deprecated: false
|
| 548 |
+
cleanBodyParameters:
|
| 549 |
+
email: qkunze@example.com
|
| 550 |
+
fileParameters: []
|
| 551 |
+
responses: []
|
| 552 |
+
responseFields: []
|
| 553 |
+
auth: []
|
| 554 |
+
controller: null
|
| 555 |
+
method: null
|
| 556 |
+
route: null
|
| 557 |
+
-
|
| 558 |
+
custom: []
|
| 559 |
+
httpMethods:
|
| 560 |
+
- POST
|
| 561 |
+
uri: api/user/reset-password
|
| 562 |
+
metadata:
|
| 563 |
+
custom: []
|
| 564 |
+
groupName: Endpoints
|
| 565 |
+
groupDescription: ''
|
| 566 |
+
subgroup: ''
|
| 567 |
+
subgroupDescription: ''
|
| 568 |
+
title: ''
|
| 569 |
+
description: ''
|
| 570 |
+
authenticated: false
|
| 571 |
+
deprecated: false
|
| 572 |
+
headers:
|
| 573 |
+
Content-Type: application/json
|
| 574 |
+
Accept: application/json
|
| 575 |
+
urlParameters: []
|
| 576 |
+
cleanUrlParameters: []
|
| 577 |
+
queryParameters: []
|
| 578 |
+
cleanQueryParameters: []
|
| 579 |
+
bodyParameters:
|
| 580 |
+
email:
|
| 581 |
+
custom: []
|
| 582 |
+
name: email
|
| 583 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 584 |
+
required: true
|
| 585 |
+
example: qkunze@example.com
|
| 586 |
+
type: string
|
| 587 |
+
enumValues: []
|
| 588 |
+
exampleWasSpecified: false
|
| 589 |
+
nullable: false
|
| 590 |
+
deprecated: false
|
| 591 |
+
otp:
|
| 592 |
+
custom: []
|
| 593 |
+
name: otp
|
| 594 |
+
description: 'Must be 6 digits.'
|
| 595 |
+
required: true
|
| 596 |
+
example: '810798'
|
| 597 |
+
type: string
|
| 598 |
+
enumValues: []
|
| 599 |
+
exampleWasSpecified: false
|
| 600 |
+
nullable: false
|
| 601 |
+
deprecated: false
|
| 602 |
+
password:
|
| 603 |
+
custom: []
|
| 604 |
+
name: password
|
| 605 |
+
description: 'Must be at least 6 characters.'
|
| 606 |
+
required: true
|
| 607 |
+
example: '[2UZ5ij-e/dl4'
|
| 608 |
+
type: string
|
| 609 |
+
enumValues: []
|
| 610 |
+
exampleWasSpecified: false
|
| 611 |
+
nullable: false
|
| 612 |
+
deprecated: false
|
| 613 |
+
cleanBodyParameters:
|
| 614 |
+
email: qkunze@example.com
|
| 615 |
+
otp: '810798'
|
| 616 |
+
password: '[2UZ5ij-e/dl4'
|
| 617 |
+
fileParameters: []
|
| 618 |
+
responses: []
|
| 619 |
+
responseFields: []
|
| 620 |
+
auth: []
|
| 621 |
+
controller: null
|
| 622 |
+
method: null
|
| 623 |
+
route: null
|
| 624 |
+
-
|
| 625 |
+
custom: []
|
| 626 |
+
httpMethods:
|
| 627 |
+
- POST
|
| 628 |
+
uri: api/user/resend-reset-password-otp
|
| 629 |
+
metadata:
|
| 630 |
+
custom: []
|
| 631 |
+
groupName: Endpoints
|
| 632 |
+
groupDescription: ''
|
| 633 |
+
subgroup: ''
|
| 634 |
+
subgroupDescription: ''
|
| 635 |
+
title: ''
|
| 636 |
+
description: ''
|
| 637 |
+
authenticated: false
|
| 638 |
+
deprecated: false
|
| 639 |
+
headers:
|
| 640 |
+
Content-Type: application/json
|
| 641 |
+
Accept: application/json
|
| 642 |
+
urlParameters: []
|
| 643 |
+
cleanUrlParameters: []
|
| 644 |
+
queryParameters: []
|
| 645 |
+
cleanQueryParameters: []
|
| 646 |
+
bodyParameters:
|
| 647 |
+
email:
|
| 648 |
+
custom: []
|
| 649 |
+
name: email
|
| 650 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 651 |
+
required: true
|
| 652 |
+
example: qkunze@example.com
|
| 653 |
+
type: string
|
| 654 |
+
enumValues: []
|
| 655 |
+
exampleWasSpecified: false
|
| 656 |
+
nullable: false
|
| 657 |
+
deprecated: false
|
| 658 |
+
cleanBodyParameters:
|
| 659 |
+
email: qkunze@example.com
|
| 660 |
+
fileParameters: []
|
| 661 |
+
responses: []
|
| 662 |
+
responseFields: []
|
| 663 |
+
auth: []
|
| 664 |
+
controller: null
|
| 665 |
+
method: null
|
| 666 |
+
route: null
|
| 667 |
+
-
|
| 668 |
+
custom: []
|
| 669 |
+
httpMethods:
|
| 670 |
+
- POST
|
| 671 |
+
uri: api/user/auth/google
|
| 672 |
+
metadata:
|
| 673 |
+
custom: []
|
| 674 |
+
groupName: Endpoints
|
| 675 |
+
groupDescription: ''
|
| 676 |
+
subgroup: ''
|
| 677 |
+
subgroupDescription: ''
|
| 678 |
+
title: '---------------- Google OAuth ----------------'
|
| 679 |
+
description: ''
|
| 680 |
+
authenticated: false
|
| 681 |
+
deprecated: false
|
| 682 |
+
headers:
|
| 683 |
+
Content-Type: application/json
|
| 684 |
+
Accept: application/json
|
| 685 |
+
urlParameters: []
|
| 686 |
+
cleanUrlParameters: []
|
| 687 |
+
queryParameters: []
|
| 688 |
+
cleanQueryParameters: []
|
| 689 |
+
bodyParameters:
|
| 690 |
+
access_token:
|
| 691 |
+
custom: []
|
| 692 |
+
name: access_token
|
| 693 |
+
description: ''
|
| 694 |
+
required: true
|
| 695 |
+
example: consequatur
|
| 696 |
+
type: string
|
| 697 |
+
enumValues: []
|
| 698 |
+
exampleWasSpecified: false
|
| 699 |
+
nullable: false
|
| 700 |
+
deprecated: false
|
| 701 |
+
cleanBodyParameters:
|
| 702 |
+
access_token: consequatur
|
| 703 |
+
fileParameters: []
|
| 704 |
+
responses: []
|
| 705 |
+
responseFields: []
|
| 706 |
+
auth: []
|
| 707 |
+
controller: null
|
| 708 |
+
method: null
|
| 709 |
+
route: null
|
| 710 |
+
-
|
| 711 |
+
custom: []
|
| 712 |
+
httpMethods:
|
| 713 |
+
- GET
|
| 714 |
+
uri: api/user/profile
|
| 715 |
+
metadata:
|
| 716 |
+
custom: []
|
| 717 |
+
groupName: Endpoints
|
| 718 |
+
groupDescription: ''
|
| 719 |
+
subgroup: ''
|
| 720 |
+
subgroupDescription: ''
|
| 721 |
+
title: '---------------- Profile ----------------'
|
| 722 |
+
description: ''
|
| 723 |
+
authenticated: false
|
| 724 |
+
deprecated: false
|
| 725 |
+
headers:
|
| 726 |
+
Content-Type: application/json
|
| 727 |
+
Accept: application/json
|
| 728 |
+
urlParameters: []
|
| 729 |
+
cleanUrlParameters: []
|
| 730 |
+
queryParameters: []
|
| 731 |
+
cleanQueryParameters: []
|
| 732 |
+
bodyParameters: []
|
| 733 |
+
cleanBodyParameters: []
|
| 734 |
+
fileParameters: []
|
| 735 |
+
responses:
|
| 736 |
+
-
|
| 737 |
+
custom: []
|
| 738 |
+
status: 404
|
| 739 |
+
content: '{"success":false,"message":"The route api\/user\/profile could not be found."}'
|
| 740 |
+
headers:
|
| 741 |
+
cache-control: 'no-cache, private'
|
| 742 |
+
content-type: application/json
|
| 743 |
+
access-control-allow-origin: '*'
|
| 744 |
+
description: null
|
| 745 |
+
responseFields: []
|
| 746 |
+
auth: []
|
| 747 |
+
controller: null
|
| 748 |
+
method: null
|
| 749 |
+
route: null
|
| 750 |
+
-
|
| 751 |
+
custom: []
|
| 752 |
+
httpMethods:
|
| 753 |
+
- POST
|
| 754 |
+
uri: api/user/logout
|
| 755 |
+
metadata:
|
| 756 |
+
custom: []
|
| 757 |
+
groupName: Endpoints
|
| 758 |
+
groupDescription: ''
|
| 759 |
+
subgroup: ''
|
| 760 |
+
subgroupDescription: ''
|
| 761 |
+
title: ''
|
| 762 |
+
description: ''
|
| 763 |
+
authenticated: false
|
| 764 |
+
deprecated: false
|
| 765 |
+
headers:
|
| 766 |
+
Content-Type: application/json
|
| 767 |
+
Accept: application/json
|
| 768 |
+
urlParameters: []
|
| 769 |
+
cleanUrlParameters: []
|
| 770 |
+
queryParameters: []
|
| 771 |
+
cleanQueryParameters: []
|
| 772 |
+
bodyParameters: []
|
| 773 |
+
cleanBodyParameters: []
|
| 774 |
+
fileParameters: []
|
| 775 |
+
responses: []
|
| 776 |
+
responseFields: []
|
| 777 |
+
auth: []
|
| 778 |
+
controller: null
|
| 779 |
+
method: null
|
| 780 |
+
route: null
|
| 781 |
+
-
|
| 782 |
+
custom: []
|
| 783 |
+
httpMethods:
|
| 784 |
+
- GET
|
| 785 |
+
uri: api/news
|
| 786 |
+
metadata:
|
| 787 |
+
custom: []
|
| 788 |
+
groupName: Endpoints
|
| 789 |
+
groupDescription: ''
|
| 790 |
+
subgroup: ''
|
| 791 |
+
subgroupDescription: ''
|
| 792 |
+
title: ''
|
| 793 |
+
description: ''
|
| 794 |
+
authenticated: false
|
| 795 |
+
deprecated: false
|
| 796 |
+
headers:
|
| 797 |
+
Content-Type: application/json
|
| 798 |
+
Accept: application/json
|
| 799 |
+
urlParameters: []
|
| 800 |
+
cleanUrlParameters: []
|
| 801 |
+
queryParameters: []
|
| 802 |
+
cleanQueryParameters: []
|
| 803 |
+
bodyParameters: []
|
| 804 |
+
cleanBodyParameters: []
|
| 805 |
+
fileParameters: []
|
| 806 |
+
responses:
|
| 807 |
+
-
|
| 808 |
+
custom: []
|
| 809 |
+
status: 404
|
| 810 |
+
content: '{"success":false,"message":"The route api\/news could not be found."}'
|
| 811 |
+
headers:
|
| 812 |
+
cache-control: 'no-cache, private'
|
| 813 |
+
content-type: application/json
|
| 814 |
+
access-control-allow-origin: '*'
|
| 815 |
+
description: null
|
| 816 |
+
responseFields: []
|
| 817 |
+
auth: []
|
| 818 |
+
controller: null
|
| 819 |
+
method: null
|
| 820 |
+
route: null
|
| 821 |
+
-
|
| 822 |
+
custom: []
|
| 823 |
+
httpMethods:
|
| 824 |
+
- GET
|
| 825 |
+
uri: api/news/refresh
|
| 826 |
+
metadata:
|
| 827 |
+
custom: []
|
| 828 |
+
groupName: Endpoints
|
| 829 |
+
groupDescription: ''
|
| 830 |
+
subgroup: ''
|
| 831 |
+
subgroupDescription: ''
|
| 832 |
+
title: ''
|
| 833 |
+
description: ''
|
| 834 |
+
authenticated: false
|
| 835 |
+
deprecated: false
|
| 836 |
+
headers:
|
| 837 |
+
Content-Type: application/json
|
| 838 |
+
Accept: application/json
|
| 839 |
+
urlParameters: []
|
| 840 |
+
cleanUrlParameters: []
|
| 841 |
+
queryParameters: []
|
| 842 |
+
cleanQueryParameters: []
|
| 843 |
+
bodyParameters: []
|
| 844 |
+
cleanBodyParameters: []
|
| 845 |
+
fileParameters: []
|
| 846 |
+
responses:
|
| 847 |
+
-
|
| 848 |
+
custom: []
|
| 849 |
+
status: 404
|
| 850 |
+
content: '{"success":false,"message":"The route api\/news\/refresh could not be found."}'
|
| 851 |
+
headers:
|
| 852 |
+
cache-control: 'no-cache, private'
|
| 853 |
+
content-type: application/json
|
| 854 |
+
access-control-allow-origin: '*'
|
| 855 |
+
description: null
|
| 856 |
+
responseFields: []
|
| 857 |
+
auth: []
|
| 858 |
+
controller: null
|
| 859 |
+
method: null
|
| 860 |
+
route: null
|
| 861 |
+
-
|
| 862 |
+
custom: []
|
| 863 |
+
httpMethods:
|
| 864 |
+
- GET
|
| 865 |
+
uri: api/news/clear
|
| 866 |
+
metadata:
|
| 867 |
+
custom: []
|
| 868 |
+
groupName: Endpoints
|
| 869 |
+
groupDescription: ''
|
| 870 |
+
subgroup: ''
|
| 871 |
+
subgroupDescription: ''
|
| 872 |
+
title: ''
|
| 873 |
+
description: ''
|
| 874 |
+
authenticated: false
|
| 875 |
+
deprecated: false
|
| 876 |
+
headers:
|
| 877 |
+
Content-Type: application/json
|
| 878 |
+
Accept: application/json
|
| 879 |
+
urlParameters: []
|
| 880 |
+
cleanUrlParameters: []
|
| 881 |
+
queryParameters: []
|
| 882 |
+
cleanQueryParameters: []
|
| 883 |
+
bodyParameters: []
|
| 884 |
+
cleanBodyParameters: []
|
| 885 |
+
fileParameters: []
|
| 886 |
+
responses:
|
| 887 |
+
-
|
| 888 |
+
custom: []
|
| 889 |
+
status: 404
|
| 890 |
+
content: '{"success":false,"message":"The route api\/news\/clear could not be found."}'
|
| 891 |
+
headers:
|
| 892 |
+
cache-control: 'no-cache, private'
|
| 893 |
+
content-type: application/json
|
| 894 |
+
access-control-allow-origin: '*'
|
| 895 |
+
description: null
|
| 896 |
+
responseFields: []
|
| 897 |
+
auth: []
|
| 898 |
+
controller: null
|
| 899 |
+
method: null
|
| 900 |
+
route: null
|
| 901 |
+
-
|
| 902 |
+
custom: []
|
| 903 |
+
httpMethods:
|
| 904 |
+
- GET
|
| 905 |
+
uri: api/news/categories
|
| 906 |
+
metadata:
|
| 907 |
+
custom: []
|
| 908 |
+
groupName: Endpoints
|
| 909 |
+
groupDescription: ''
|
| 910 |
+
subgroup: ''
|
| 911 |
+
subgroupDescription: ''
|
| 912 |
+
title: ''
|
| 913 |
+
description: ''
|
| 914 |
+
authenticated: false
|
| 915 |
+
deprecated: false
|
| 916 |
+
headers:
|
| 917 |
+
Content-Type: application/json
|
| 918 |
+
Accept: application/json
|
| 919 |
+
urlParameters: []
|
| 920 |
+
cleanUrlParameters: []
|
| 921 |
+
queryParameters: []
|
| 922 |
+
cleanQueryParameters: []
|
| 923 |
+
bodyParameters: []
|
| 924 |
+
cleanBodyParameters: []
|
| 925 |
+
fileParameters: []
|
| 926 |
+
responses:
|
| 927 |
+
-
|
| 928 |
+
custom: []
|
| 929 |
+
status: 404
|
| 930 |
+
content: '{"success":false,"message":"The route api\/news\/categories could not be found."}'
|
| 931 |
+
headers:
|
| 932 |
+
cache-control: 'no-cache, private'
|
| 933 |
+
content-type: application/json
|
| 934 |
+
access-control-allow-origin: '*'
|
| 935 |
+
description: null
|
| 936 |
+
responseFields: []
|
| 937 |
+
auth: []
|
| 938 |
+
controller: null
|
| 939 |
+
method: null
|
| 940 |
+
route: null
|
| 941 |
+
-
|
| 942 |
+
custom: []
|
| 943 |
+
httpMethods:
|
| 944 |
+
- POST
|
| 945 |
+
uri: 'api/news/{articleId}/share'
|
| 946 |
+
metadata:
|
| 947 |
+
custom: []
|
| 948 |
+
groupName: Endpoints
|
| 949 |
+
groupDescription: ''
|
| 950 |
+
subgroup: ''
|
| 951 |
+
subgroupDescription: ''
|
| 952 |
+
title: ''
|
| 953 |
+
description: ''
|
| 954 |
+
authenticated: false
|
| 955 |
+
deprecated: false
|
| 956 |
+
headers:
|
| 957 |
+
Content-Type: application/json
|
| 958 |
+
Accept: application/json
|
| 959 |
+
urlParameters:
|
| 960 |
+
articleId:
|
| 961 |
+
custom: []
|
| 962 |
+
name: articleId
|
| 963 |
+
description: ''
|
| 964 |
+
required: true
|
| 965 |
+
example: consequatur
|
| 966 |
+
type: string
|
| 967 |
+
enumValues: []
|
| 968 |
+
exampleWasSpecified: false
|
| 969 |
+
nullable: false
|
| 970 |
+
deprecated: false
|
| 971 |
+
cleanUrlParameters:
|
| 972 |
+
articleId: consequatur
|
| 973 |
+
queryParameters: []
|
| 974 |
+
cleanQueryParameters: []
|
| 975 |
+
bodyParameters: []
|
| 976 |
+
cleanBodyParameters: []
|
| 977 |
+
fileParameters: []
|
| 978 |
+
responses: []
|
| 979 |
+
responseFields: []
|
| 980 |
+
auth: []
|
| 981 |
+
controller: null
|
| 982 |
+
method: null
|
| 983 |
+
route: null
|
| 984 |
+
-
|
| 985 |
+
custom: []
|
| 986 |
+
httpMethods:
|
| 987 |
+
- POST
|
| 988 |
+
uri: 'api/news/{articleId}/save'
|
| 989 |
+
metadata:
|
| 990 |
+
custom: []
|
| 991 |
+
groupName: Endpoints
|
| 992 |
+
groupDescription: ''
|
| 993 |
+
subgroup: ''
|
| 994 |
+
subgroupDescription: ''
|
| 995 |
+
title: ''
|
| 996 |
+
description: ''
|
| 997 |
+
authenticated: false
|
| 998 |
+
deprecated: false
|
| 999 |
+
headers:
|
| 1000 |
+
Content-Type: application/json
|
| 1001 |
+
Accept: application/json
|
| 1002 |
+
urlParameters:
|
| 1003 |
+
articleId:
|
| 1004 |
+
custom: []
|
| 1005 |
+
name: articleId
|
| 1006 |
+
description: ''
|
| 1007 |
+
required: true
|
| 1008 |
+
example: consequatur
|
| 1009 |
+
type: string
|
| 1010 |
+
enumValues: []
|
| 1011 |
+
exampleWasSpecified: false
|
| 1012 |
+
nullable: false
|
| 1013 |
+
deprecated: false
|
| 1014 |
+
cleanUrlParameters:
|
| 1015 |
+
articleId: consequatur
|
| 1016 |
+
queryParameters: []
|
| 1017 |
+
cleanQueryParameters: []
|
| 1018 |
+
bodyParameters: []
|
| 1019 |
+
cleanBodyParameters: []
|
| 1020 |
+
fileParameters: []
|
| 1021 |
+
responses: []
|
| 1022 |
+
responseFields: []
|
| 1023 |
+
auth: []
|
| 1024 |
+
controller: null
|
| 1025 |
+
method: null
|
| 1026 |
+
route: null
|
| 1027 |
+
-
|
| 1028 |
+
custom: []
|
| 1029 |
+
httpMethods:
|
| 1030 |
+
- GET
|
| 1031 |
+
uri: api/news/saved
|
| 1032 |
+
metadata:
|
| 1033 |
+
custom: []
|
| 1034 |
+
groupName: Endpoints
|
| 1035 |
+
groupDescription: ''
|
| 1036 |
+
subgroup: ''
|
| 1037 |
+
subgroupDescription: ''
|
| 1038 |
+
title: ''
|
| 1039 |
+
description: ''
|
| 1040 |
+
authenticated: false
|
| 1041 |
+
deprecated: false
|
| 1042 |
+
headers:
|
| 1043 |
+
Content-Type: application/json
|
| 1044 |
+
Accept: application/json
|
| 1045 |
+
urlParameters: []
|
| 1046 |
+
cleanUrlParameters: []
|
| 1047 |
+
queryParameters: []
|
| 1048 |
+
cleanQueryParameters: []
|
| 1049 |
+
bodyParameters: []
|
| 1050 |
+
cleanBodyParameters: []
|
| 1051 |
+
fileParameters: []
|
| 1052 |
+
responses:
|
| 1053 |
+
-
|
| 1054 |
+
custom: []
|
| 1055 |
+
status: 404
|
| 1056 |
+
content: '{"success":false,"message":"The route api\/news\/saved could not be found."}'
|
| 1057 |
+
headers:
|
| 1058 |
+
cache-control: 'no-cache, private'
|
| 1059 |
+
content-type: application/json
|
| 1060 |
+
access-control-allow-origin: '*'
|
| 1061 |
+
description: null
|
| 1062 |
+
responseFields: []
|
| 1063 |
+
auth: []
|
| 1064 |
+
controller: null
|
| 1065 |
+
method: null
|
| 1066 |
+
route: null
|
| 1067 |
+
-
|
| 1068 |
+
custom: []
|
| 1069 |
+
httpMethods:
|
| 1070 |
+
- DELETE
|
| 1071 |
+
uri: 'api/news/saved/{savedArticleId}'
|
| 1072 |
+
metadata:
|
| 1073 |
+
custom: []
|
| 1074 |
+
groupName: Endpoints
|
| 1075 |
+
groupDescription: ''
|
| 1076 |
+
subgroup: ''
|
| 1077 |
+
subgroupDescription: ''
|
| 1078 |
+
title: ''
|
| 1079 |
+
description: ''
|
| 1080 |
+
authenticated: false
|
| 1081 |
+
deprecated: false
|
| 1082 |
+
headers:
|
| 1083 |
+
Content-Type: application/json
|
| 1084 |
+
Accept: application/json
|
| 1085 |
+
urlParameters:
|
| 1086 |
+
savedArticleId:
|
| 1087 |
+
custom: []
|
| 1088 |
+
name: savedArticleId
|
| 1089 |
+
description: ''
|
| 1090 |
+
required: true
|
| 1091 |
+
example: consequatur
|
| 1092 |
+
type: string
|
| 1093 |
+
enumValues: []
|
| 1094 |
+
exampleWasSpecified: false
|
| 1095 |
+
nullable: false
|
| 1096 |
+
deprecated: false
|
| 1097 |
+
cleanUrlParameters:
|
| 1098 |
+
savedArticleId: consequatur
|
| 1099 |
+
queryParameters: []
|
| 1100 |
+
cleanQueryParameters: []
|
| 1101 |
+
bodyParameters: []
|
| 1102 |
+
cleanBodyParameters: []
|
| 1103 |
+
fileParameters: []
|
| 1104 |
+
responses: []
|
| 1105 |
+
responseFields: []
|
| 1106 |
+
auth: []
|
| 1107 |
+
controller: null
|
| 1108 |
+
method: null
|
| 1109 |
+
route: null
|
| 1110 |
+
-
|
| 1111 |
+
custom: []
|
| 1112 |
+
httpMethods:
|
| 1113 |
+
- POST
|
| 1114 |
+
uri: api/ai/run
|
| 1115 |
+
metadata:
|
| 1116 |
+
custom: []
|
| 1117 |
+
groupName: Endpoints
|
| 1118 |
+
groupDescription: ''
|
| 1119 |
+
subgroup: ''
|
| 1120 |
+
subgroupDescription: ''
|
| 1121 |
+
title: ''
|
| 1122 |
+
description: ''
|
| 1123 |
+
authenticated: false
|
| 1124 |
+
deprecated: false
|
| 1125 |
+
headers:
|
| 1126 |
+
Content-Type: application/json
|
| 1127 |
+
Accept: application/json
|
| 1128 |
+
urlParameters: []
|
| 1129 |
+
cleanUrlParameters: []
|
| 1130 |
+
queryParameters: []
|
| 1131 |
+
cleanQueryParameters: []
|
| 1132 |
+
bodyParameters:
|
| 1133 |
+
num_smiles:
|
| 1134 |
+
custom: []
|
| 1135 |
+
name: num_smiles
|
| 1136 |
+
description: ''
|
| 1137 |
+
required: true
|
| 1138 |
+
example: 17
|
| 1139 |
+
type: integer
|
| 1140 |
+
enumValues: []
|
| 1141 |
+
exampleWasSpecified: false
|
| 1142 |
+
nullable: false
|
| 1143 |
+
deprecated: false
|
| 1144 |
+
top_k:
|
| 1145 |
+
custom: []
|
| 1146 |
+
name: top_k
|
| 1147 |
+
description: ''
|
| 1148 |
+
required: true
|
| 1149 |
+
example: 17
|
| 1150 |
+
type: integer
|
| 1151 |
+
enumValues: []
|
| 1152 |
+
exampleWasSpecified: false
|
| 1153 |
+
nullable: false
|
| 1154 |
+
deprecated: false
|
| 1155 |
+
Docking:
|
| 1156 |
+
custom: []
|
| 1157 |
+
name: Docking
|
| 1158 |
+
description: ''
|
| 1159 |
+
required: true
|
| 1160 |
+
example: 11613.31890586
|
| 1161 |
+
type: number
|
| 1162 |
+
enumValues: []
|
| 1163 |
+
exampleWasSpecified: false
|
| 1164 |
+
nullable: false
|
| 1165 |
+
deprecated: false
|
| 1166 |
+
DeepPurpose_Affinity:
|
| 1167 |
+
custom: []
|
| 1168 |
+
name: DeepPurpose_Affinity
|
| 1169 |
+
description: ''
|
| 1170 |
+
required: true
|
| 1171 |
+
example: 11613.31890586
|
| 1172 |
+
type: number
|
| 1173 |
+
enumValues: []
|
| 1174 |
+
exampleWasSpecified: false
|
| 1175 |
+
nullable: false
|
| 1176 |
+
deprecated: false
|
| 1177 |
+
SA:
|
| 1178 |
+
custom: []
|
| 1179 |
+
name: SA
|
| 1180 |
+
description: ''
|
| 1181 |
+
required: true
|
| 1182 |
+
example: 11613.31890586
|
| 1183 |
+
type: number
|
| 1184 |
+
enumValues: []
|
| 1185 |
+
exampleWasSpecified: false
|
| 1186 |
+
nullable: false
|
| 1187 |
+
deprecated: false
|
| 1188 |
+
TPSA:
|
| 1189 |
+
custom: []
|
| 1190 |
+
name: TPSA
|
| 1191 |
+
description: ''
|
| 1192 |
+
required: true
|
| 1193 |
+
example: 11613.31890586
|
| 1194 |
+
type: number
|
| 1195 |
+
enumValues: []
|
| 1196 |
+
exampleWasSpecified: false
|
| 1197 |
+
nullable: false
|
| 1198 |
+
deprecated: false
|
| 1199 |
+
MW:
|
| 1200 |
+
custom: []
|
| 1201 |
+
name: MW
|
| 1202 |
+
description: ''
|
| 1203 |
+
required: true
|
| 1204 |
+
example: 11613.31890586
|
| 1205 |
+
type: number
|
| 1206 |
+
enumValues: []
|
| 1207 |
+
exampleWasSpecified: false
|
| 1208 |
+
nullable: false
|
| 1209 |
+
deprecated: false
|
| 1210 |
+
cleanBodyParameters:
|
| 1211 |
+
num_smiles: 17
|
| 1212 |
+
top_k: 17
|
| 1213 |
+
Docking: 11613.31890586
|
| 1214 |
+
DeepPurpose_Affinity: 11613.31890586
|
| 1215 |
+
SA: 11613.31890586
|
| 1216 |
+
TPSA: 11613.31890586
|
| 1217 |
+
MW: 11613.31890586
|
| 1218 |
+
fileParameters: []
|
| 1219 |
+
responses: []
|
| 1220 |
+
responseFields: []
|
| 1221 |
+
auth: []
|
| 1222 |
+
controller: null
|
| 1223 |
+
method: null
|
| 1224 |
+
route: null
|
| 1225 |
+
-
|
| 1226 |
+
custom: []
|
| 1227 |
+
httpMethods:
|
| 1228 |
+
- GET
|
| 1229 |
+
uri: 'api/ai/status/{job_job_id}'
|
| 1230 |
+
metadata:
|
| 1231 |
+
custom: []
|
| 1232 |
+
groupName: Endpoints
|
| 1233 |
+
groupDescription: ''
|
| 1234 |
+
subgroup: ''
|
| 1235 |
+
subgroupDescription: ''
|
| 1236 |
+
title: ''
|
| 1237 |
+
description: ''
|
| 1238 |
+
authenticated: false
|
| 1239 |
+
deprecated: false
|
| 1240 |
+
headers:
|
| 1241 |
+
Content-Type: application/json
|
| 1242 |
+
Accept: application/json
|
| 1243 |
+
urlParameters:
|
| 1244 |
+
job_job_id:
|
| 1245 |
+
custom: []
|
| 1246 |
+
name: job_job_id
|
| 1247 |
+
description: 'The ID of the job job.'
|
| 1248 |
+
required: true
|
| 1249 |
+
example: consequatur
|
| 1250 |
+
type: string
|
| 1251 |
+
enumValues: []
|
| 1252 |
+
exampleWasSpecified: false
|
| 1253 |
+
nullable: false
|
| 1254 |
+
deprecated: false
|
| 1255 |
+
cleanUrlParameters:
|
| 1256 |
+
job_job_id: consequatur
|
| 1257 |
+
queryParameters: []
|
| 1258 |
+
cleanQueryParameters: []
|
| 1259 |
+
bodyParameters: []
|
| 1260 |
+
cleanBodyParameters: []
|
| 1261 |
+
fileParameters: []
|
| 1262 |
+
responses:
|
| 1263 |
+
-
|
| 1264 |
+
custom: []
|
| 1265 |
+
status: 404
|
| 1266 |
+
content: '{"success":false,"message":"The route api\/ai\/status\/consequatur could not be found."}'
|
| 1267 |
+
headers:
|
| 1268 |
+
cache-control: 'no-cache, private'
|
| 1269 |
+
content-type: application/json
|
| 1270 |
+
access-control-allow-origin: '*'
|
| 1271 |
+
description: null
|
| 1272 |
+
responseFields: []
|
| 1273 |
+
auth: []
|
| 1274 |
+
controller: null
|
| 1275 |
+
method: null
|
| 1276 |
+
route: null
|
| 1277 |
+
-
|
| 1278 |
+
custom: []
|
| 1279 |
+
httpMethods:
|
| 1280 |
+
- GET
|
| 1281 |
+
uri: 'api/ai/preview/{job_job_id}'
|
| 1282 |
+
metadata:
|
| 1283 |
+
custom: []
|
| 1284 |
+
groupName: Endpoints
|
| 1285 |
+
groupDescription: ''
|
| 1286 |
+
subgroup: ''
|
| 1287 |
+
subgroupDescription: ''
|
| 1288 |
+
title: ''
|
| 1289 |
+
description: ''
|
| 1290 |
+
authenticated: false
|
| 1291 |
+
deprecated: false
|
| 1292 |
+
headers:
|
| 1293 |
+
Content-Type: application/json
|
| 1294 |
+
Accept: application/json
|
| 1295 |
+
urlParameters:
|
| 1296 |
+
job_job_id:
|
| 1297 |
+
custom: []
|
| 1298 |
+
name: job_job_id
|
| 1299 |
+
description: 'The ID of the job job.'
|
| 1300 |
+
required: true
|
| 1301 |
+
example: consequatur
|
| 1302 |
+
type: string
|
| 1303 |
+
enumValues: []
|
| 1304 |
+
exampleWasSpecified: false
|
| 1305 |
+
nullable: false
|
| 1306 |
+
deprecated: false
|
| 1307 |
+
cleanUrlParameters:
|
| 1308 |
+
job_job_id: consequatur
|
| 1309 |
+
queryParameters: []
|
| 1310 |
+
cleanQueryParameters: []
|
| 1311 |
+
bodyParameters: []
|
| 1312 |
+
cleanBodyParameters: []
|
| 1313 |
+
fileParameters: []
|
| 1314 |
+
responses:
|
| 1315 |
+
-
|
| 1316 |
+
custom: []
|
| 1317 |
+
status: 404
|
| 1318 |
+
content: '{"success":false,"message":"The route api\/ai\/preview\/consequatur could not be found."}'
|
| 1319 |
+
headers:
|
| 1320 |
+
cache-control: 'no-cache, private'
|
| 1321 |
+
content-type: application/json
|
| 1322 |
+
access-control-allow-origin: '*'
|
| 1323 |
+
description: null
|
| 1324 |
+
responseFields: []
|
| 1325 |
+
auth: []
|
| 1326 |
+
controller: null
|
| 1327 |
+
method: null
|
| 1328 |
+
route: null
|
| 1329 |
+
-
|
| 1330 |
+
custom: []
|
| 1331 |
+
httpMethods:
|
| 1332 |
+
- GET
|
| 1333 |
+
uri: 'api/ai/download/top/{job_job_id}'
|
| 1334 |
+
metadata:
|
| 1335 |
+
custom: []
|
| 1336 |
+
groupName: Endpoints
|
| 1337 |
+
groupDescription: ''
|
| 1338 |
+
subgroup: ''
|
| 1339 |
+
subgroupDescription: ''
|
| 1340 |
+
title: ''
|
| 1341 |
+
description: ''
|
| 1342 |
+
authenticated: false
|
| 1343 |
+
deprecated: false
|
| 1344 |
+
headers:
|
| 1345 |
+
Content-Type: application/json
|
| 1346 |
+
Accept: application/json
|
| 1347 |
+
urlParameters:
|
| 1348 |
+
job_job_id:
|
| 1349 |
+
custom: []
|
| 1350 |
+
name: job_job_id
|
| 1351 |
+
description: 'The ID of the job job.'
|
| 1352 |
+
required: true
|
| 1353 |
+
example: consequatur
|
| 1354 |
+
type: string
|
| 1355 |
+
enumValues: []
|
| 1356 |
+
exampleWasSpecified: false
|
| 1357 |
+
nullable: false
|
| 1358 |
+
deprecated: false
|
| 1359 |
+
cleanUrlParameters:
|
| 1360 |
+
job_job_id: consequatur
|
| 1361 |
+
queryParameters: []
|
| 1362 |
+
cleanQueryParameters: []
|
| 1363 |
+
bodyParameters: []
|
| 1364 |
+
cleanBodyParameters: []
|
| 1365 |
+
fileParameters: []
|
| 1366 |
+
responses:
|
| 1367 |
+
-
|
| 1368 |
+
custom: []
|
| 1369 |
+
status: 404
|
| 1370 |
+
content: '{"success":false,"message":"The route api\/ai\/download\/top\/consequatur could not be found."}'
|
| 1371 |
+
headers:
|
| 1372 |
+
cache-control: 'no-cache, private'
|
| 1373 |
+
content-type: application/json
|
| 1374 |
+
access-control-allow-origin: '*'
|
| 1375 |
+
description: null
|
| 1376 |
+
responseFields: []
|
| 1377 |
+
auth: []
|
| 1378 |
+
controller: null
|
| 1379 |
+
method: null
|
| 1380 |
+
route: null
|
| 1381 |
+
-
|
| 1382 |
+
custom: []
|
| 1383 |
+
httpMethods:
|
| 1384 |
+
- GET
|
| 1385 |
+
uri: 'api/ai/download/full/{job_job_id}'
|
| 1386 |
+
metadata:
|
| 1387 |
+
custom: []
|
| 1388 |
+
groupName: Endpoints
|
| 1389 |
+
groupDescription: ''
|
| 1390 |
+
subgroup: ''
|
| 1391 |
+
subgroupDescription: ''
|
| 1392 |
+
title: ''
|
| 1393 |
+
description: ''
|
| 1394 |
+
authenticated: false
|
| 1395 |
+
deprecated: false
|
| 1396 |
+
headers:
|
| 1397 |
+
Content-Type: application/json
|
| 1398 |
+
Accept: application/json
|
| 1399 |
+
urlParameters:
|
| 1400 |
+
job_job_id:
|
| 1401 |
+
custom: []
|
| 1402 |
+
name: job_job_id
|
| 1403 |
+
description: 'The ID of the job job.'
|
| 1404 |
+
required: true
|
| 1405 |
+
example: consequatur
|
| 1406 |
+
type: string
|
| 1407 |
+
enumValues: []
|
| 1408 |
+
exampleWasSpecified: false
|
| 1409 |
+
nullable: false
|
| 1410 |
+
deprecated: false
|
| 1411 |
+
cleanUrlParameters:
|
| 1412 |
+
job_job_id: consequatur
|
| 1413 |
+
queryParameters: []
|
| 1414 |
+
cleanQueryParameters: []
|
| 1415 |
+
bodyParameters: []
|
| 1416 |
+
cleanBodyParameters: []
|
| 1417 |
+
fileParameters: []
|
| 1418 |
+
responses:
|
| 1419 |
+
-
|
| 1420 |
+
custom: []
|
| 1421 |
+
status: 404
|
| 1422 |
+
content: '{"success":false,"message":"The route api\/ai\/download\/full\/consequatur could not be found."}'
|
| 1423 |
+
headers:
|
| 1424 |
+
cache-control: 'no-cache, private'
|
| 1425 |
+
content-type: application/json
|
| 1426 |
+
access-control-allow-origin: '*'
|
| 1427 |
+
description: null
|
| 1428 |
+
responseFields: []
|
| 1429 |
+
auth: []
|
| 1430 |
+
controller: null
|
| 1431 |
+
method: null
|
| 1432 |
+
route: null
|
| 1433 |
+
-
|
| 1434 |
+
custom: []
|
| 1435 |
+
httpMethods:
|
| 1436 |
+
- GET
|
| 1437 |
+
uri: api/ai/history
|
| 1438 |
+
metadata:
|
| 1439 |
+
custom: []
|
| 1440 |
+
groupName: Endpoints
|
| 1441 |
+
groupDescription: ''
|
| 1442 |
+
subgroup: ''
|
| 1443 |
+
subgroupDescription: ''
|
| 1444 |
+
title: ''
|
| 1445 |
+
description: ''
|
| 1446 |
+
authenticated: false
|
| 1447 |
+
deprecated: false
|
| 1448 |
+
headers:
|
| 1449 |
+
Content-Type: application/json
|
| 1450 |
+
Accept: application/json
|
| 1451 |
+
urlParameters: []
|
| 1452 |
+
cleanUrlParameters: []
|
| 1453 |
+
queryParameters: []
|
| 1454 |
+
cleanQueryParameters: []
|
| 1455 |
+
bodyParameters: []
|
| 1456 |
+
cleanBodyParameters: []
|
| 1457 |
+
fileParameters: []
|
| 1458 |
+
responses:
|
| 1459 |
+
-
|
| 1460 |
+
custom: []
|
| 1461 |
+
status: 404
|
| 1462 |
+
content: '{"success":false,"message":"The route api\/ai\/history could not be found."}'
|
| 1463 |
+
headers:
|
| 1464 |
+
cache-control: 'no-cache, private'
|
| 1465 |
+
content-type: application/json
|
| 1466 |
+
access-control-allow-origin: '*'
|
| 1467 |
+
description: null
|
| 1468 |
+
responseFields: []
|
| 1469 |
+
auth: []
|
| 1470 |
+
controller: null
|
| 1471 |
+
method: null
|
| 1472 |
+
route: null
|
| 1473 |
+
-
|
| 1474 |
+
custom: []
|
| 1475 |
+
httpMethods:
|
| 1476 |
+
- POST
|
| 1477 |
+
uri: api/upload-file
|
| 1478 |
+
metadata:
|
| 1479 |
+
custom: []
|
| 1480 |
+
groupName: Endpoints
|
| 1481 |
+
groupDescription: ''
|
| 1482 |
+
subgroup: ''
|
| 1483 |
+
subgroupDescription: ''
|
| 1484 |
+
title: ''
|
| 1485 |
+
description: ''
|
| 1486 |
+
authenticated: false
|
| 1487 |
+
deprecated: false
|
| 1488 |
+
headers:
|
| 1489 |
+
Content-Type: application/json
|
| 1490 |
+
Accept: application/json
|
| 1491 |
+
urlParameters: []
|
| 1492 |
+
cleanUrlParameters: []
|
| 1493 |
+
queryParameters: []
|
| 1494 |
+
cleanQueryParameters: []
|
| 1495 |
+
bodyParameters: []
|
| 1496 |
+
cleanBodyParameters: []
|
| 1497 |
+
fileParameters: []
|
| 1498 |
+
responses: []
|
| 1499 |
+
responseFields: []
|
| 1500 |
+
auth: []
|
| 1501 |
+
controller: null
|
| 1502 |
+
method: null
|
| 1503 |
+
route: null
|
.scribe/endpoints/00.yaml
ADDED
|
@@ -0,0 +1,1501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Endpoints
|
| 2 |
+
description: ''
|
| 3 |
+
endpoints:
|
| 4 |
+
-
|
| 5 |
+
custom: []
|
| 6 |
+
httpMethods:
|
| 7 |
+
- GET
|
| 8 |
+
uri: api/awards
|
| 9 |
+
metadata:
|
| 10 |
+
custom: []
|
| 11 |
+
groupName: Endpoints
|
| 12 |
+
groupDescription: ''
|
| 13 |
+
subgroup: ''
|
| 14 |
+
subgroupDescription: ''
|
| 15 |
+
title: ''
|
| 16 |
+
description: ''
|
| 17 |
+
authenticated: false
|
| 18 |
+
deprecated: false
|
| 19 |
+
headers:
|
| 20 |
+
Content-Type: application/json
|
| 21 |
+
Accept: application/json
|
| 22 |
+
urlParameters: []
|
| 23 |
+
cleanUrlParameters: []
|
| 24 |
+
queryParameters: []
|
| 25 |
+
cleanQueryParameters: []
|
| 26 |
+
bodyParameters: []
|
| 27 |
+
cleanBodyParameters: []
|
| 28 |
+
fileParameters: []
|
| 29 |
+
responses:
|
| 30 |
+
-
|
| 31 |
+
custom: []
|
| 32 |
+
status: 404
|
| 33 |
+
content: '{"success":false,"message":"The route api\/awards could not be found."}'
|
| 34 |
+
headers:
|
| 35 |
+
cache-control: 'no-cache, private'
|
| 36 |
+
content-type: application/json
|
| 37 |
+
access-control-allow-origin: '*'
|
| 38 |
+
description: null
|
| 39 |
+
responseFields: []
|
| 40 |
+
auth: []
|
| 41 |
+
controller: null
|
| 42 |
+
method: null
|
| 43 |
+
route: null
|
| 44 |
+
-
|
| 45 |
+
custom: []
|
| 46 |
+
httpMethods:
|
| 47 |
+
- GET
|
| 48 |
+
uri: 'api/awards/{id}'
|
| 49 |
+
metadata:
|
| 50 |
+
custom: []
|
| 51 |
+
groupName: Endpoints
|
| 52 |
+
groupDescription: ''
|
| 53 |
+
subgroup: ''
|
| 54 |
+
subgroupDescription: ''
|
| 55 |
+
title: ''
|
| 56 |
+
description: ''
|
| 57 |
+
authenticated: false
|
| 58 |
+
deprecated: false
|
| 59 |
+
headers:
|
| 60 |
+
Content-Type: application/json
|
| 61 |
+
Accept: application/json
|
| 62 |
+
urlParameters:
|
| 63 |
+
id:
|
| 64 |
+
custom: []
|
| 65 |
+
name: id
|
| 66 |
+
description: 'The ID of the award.'
|
| 67 |
+
required: true
|
| 68 |
+
example: consequatur
|
| 69 |
+
type: string
|
| 70 |
+
enumValues: []
|
| 71 |
+
exampleWasSpecified: false
|
| 72 |
+
nullable: false
|
| 73 |
+
deprecated: false
|
| 74 |
+
cleanUrlParameters:
|
| 75 |
+
id: consequatur
|
| 76 |
+
queryParameters: []
|
| 77 |
+
cleanQueryParameters: []
|
| 78 |
+
bodyParameters: []
|
| 79 |
+
cleanBodyParameters: []
|
| 80 |
+
fileParameters: []
|
| 81 |
+
responses:
|
| 82 |
+
-
|
| 83 |
+
custom: []
|
| 84 |
+
status: 404
|
| 85 |
+
content: '{"success":false,"message":"The route api\/awards\/consequatur could not be found."}'
|
| 86 |
+
headers:
|
| 87 |
+
cache-control: 'no-cache, private'
|
| 88 |
+
content-type: application/json
|
| 89 |
+
access-control-allow-origin: '*'
|
| 90 |
+
description: null
|
| 91 |
+
responseFields: []
|
| 92 |
+
auth: []
|
| 93 |
+
controller: null
|
| 94 |
+
method: null
|
| 95 |
+
route: null
|
| 96 |
+
-
|
| 97 |
+
custom: []
|
| 98 |
+
httpMethods:
|
| 99 |
+
- GET
|
| 100 |
+
uri: 'api/awards/{id}/scientists'
|
| 101 |
+
metadata:
|
| 102 |
+
custom: []
|
| 103 |
+
groupName: Endpoints
|
| 104 |
+
groupDescription: ''
|
| 105 |
+
subgroup: ''
|
| 106 |
+
subgroupDescription: ''
|
| 107 |
+
title: ''
|
| 108 |
+
description: ''
|
| 109 |
+
authenticated: false
|
| 110 |
+
deprecated: false
|
| 111 |
+
headers:
|
| 112 |
+
Content-Type: application/json
|
| 113 |
+
Accept: application/json
|
| 114 |
+
urlParameters:
|
| 115 |
+
id:
|
| 116 |
+
custom: []
|
| 117 |
+
name: id
|
| 118 |
+
description: 'The ID of the award.'
|
| 119 |
+
required: true
|
| 120 |
+
example: consequatur
|
| 121 |
+
type: string
|
| 122 |
+
enumValues: []
|
| 123 |
+
exampleWasSpecified: false
|
| 124 |
+
nullable: false
|
| 125 |
+
deprecated: false
|
| 126 |
+
cleanUrlParameters:
|
| 127 |
+
id: consequatur
|
| 128 |
+
queryParameters: []
|
| 129 |
+
cleanQueryParameters: []
|
| 130 |
+
bodyParameters: []
|
| 131 |
+
cleanBodyParameters: []
|
| 132 |
+
fileParameters: []
|
| 133 |
+
responses:
|
| 134 |
+
-
|
| 135 |
+
custom: []
|
| 136 |
+
status: 404
|
| 137 |
+
content: '{"success":false,"message":"The route api\/awards\/consequatur\/scientists could not be found."}'
|
| 138 |
+
headers:
|
| 139 |
+
cache-control: 'no-cache, private'
|
| 140 |
+
content-type: application/json
|
| 141 |
+
access-control-allow-origin: '*'
|
| 142 |
+
description: null
|
| 143 |
+
responseFields: []
|
| 144 |
+
auth: []
|
| 145 |
+
controller: null
|
| 146 |
+
method: null
|
| 147 |
+
route: null
|
| 148 |
+
-
|
| 149 |
+
custom: []
|
| 150 |
+
httpMethods:
|
| 151 |
+
- GET
|
| 152 |
+
uri: api/scientists
|
| 153 |
+
metadata:
|
| 154 |
+
custom: []
|
| 155 |
+
groupName: Endpoints
|
| 156 |
+
groupDescription: ''
|
| 157 |
+
subgroup: ''
|
| 158 |
+
subgroupDescription: ''
|
| 159 |
+
title: ''
|
| 160 |
+
description: ''
|
| 161 |
+
authenticated: false
|
| 162 |
+
deprecated: false
|
| 163 |
+
headers:
|
| 164 |
+
Content-Type: application/json
|
| 165 |
+
Accept: application/json
|
| 166 |
+
urlParameters: []
|
| 167 |
+
cleanUrlParameters: []
|
| 168 |
+
queryParameters: []
|
| 169 |
+
cleanQueryParameters: []
|
| 170 |
+
bodyParameters: []
|
| 171 |
+
cleanBodyParameters: []
|
| 172 |
+
fileParameters: []
|
| 173 |
+
responses:
|
| 174 |
+
-
|
| 175 |
+
custom: []
|
| 176 |
+
status: 404
|
| 177 |
+
content: '{"success":false,"message":"The route api\/scientists could not be found."}'
|
| 178 |
+
headers:
|
| 179 |
+
cache-control: 'no-cache, private'
|
| 180 |
+
content-type: application/json
|
| 181 |
+
access-control-allow-origin: '*'
|
| 182 |
+
description: null
|
| 183 |
+
responseFields: []
|
| 184 |
+
auth: []
|
| 185 |
+
controller: null
|
| 186 |
+
method: null
|
| 187 |
+
route: null
|
| 188 |
+
-
|
| 189 |
+
custom: []
|
| 190 |
+
httpMethods:
|
| 191 |
+
- GET
|
| 192 |
+
uri: 'api/scientists/{id}'
|
| 193 |
+
metadata:
|
| 194 |
+
custom: []
|
| 195 |
+
groupName: Endpoints
|
| 196 |
+
groupDescription: ''
|
| 197 |
+
subgroup: ''
|
| 198 |
+
subgroupDescription: ''
|
| 199 |
+
title: ''
|
| 200 |
+
description: ''
|
| 201 |
+
authenticated: false
|
| 202 |
+
deprecated: false
|
| 203 |
+
headers:
|
| 204 |
+
Content-Type: application/json
|
| 205 |
+
Accept: application/json
|
| 206 |
+
urlParameters:
|
| 207 |
+
id:
|
| 208 |
+
custom: []
|
| 209 |
+
name: id
|
| 210 |
+
description: 'The ID of the scientist.'
|
| 211 |
+
required: true
|
| 212 |
+
example: consequatur
|
| 213 |
+
type: string
|
| 214 |
+
enumValues: []
|
| 215 |
+
exampleWasSpecified: false
|
| 216 |
+
nullable: false
|
| 217 |
+
deprecated: false
|
| 218 |
+
cleanUrlParameters:
|
| 219 |
+
id: consequatur
|
| 220 |
+
queryParameters: []
|
| 221 |
+
cleanQueryParameters: []
|
| 222 |
+
bodyParameters: []
|
| 223 |
+
cleanBodyParameters: []
|
| 224 |
+
fileParameters: []
|
| 225 |
+
responses:
|
| 226 |
+
-
|
| 227 |
+
custom: []
|
| 228 |
+
status: 404
|
| 229 |
+
content: '{"success":false,"message":"The route api\/scientists\/consequatur could not be found."}'
|
| 230 |
+
headers:
|
| 231 |
+
cache-control: 'no-cache, private'
|
| 232 |
+
content-type: application/json
|
| 233 |
+
access-control-allow-origin: '*'
|
| 234 |
+
description: null
|
| 235 |
+
responseFields: []
|
| 236 |
+
auth: []
|
| 237 |
+
controller: null
|
| 238 |
+
method: null
|
| 239 |
+
route: null
|
| 240 |
+
-
|
| 241 |
+
custom: []
|
| 242 |
+
httpMethods:
|
| 243 |
+
- GET
|
| 244 |
+
uri: 'api/scientists/{id}/awards'
|
| 245 |
+
metadata:
|
| 246 |
+
custom: []
|
| 247 |
+
groupName: Endpoints
|
| 248 |
+
groupDescription: ''
|
| 249 |
+
subgroup: ''
|
| 250 |
+
subgroupDescription: ''
|
| 251 |
+
title: ''
|
| 252 |
+
description: ''
|
| 253 |
+
authenticated: false
|
| 254 |
+
deprecated: false
|
| 255 |
+
headers:
|
| 256 |
+
Content-Type: application/json
|
| 257 |
+
Accept: application/json
|
| 258 |
+
urlParameters:
|
| 259 |
+
id:
|
| 260 |
+
custom: []
|
| 261 |
+
name: id
|
| 262 |
+
description: 'The ID of the scientist.'
|
| 263 |
+
required: true
|
| 264 |
+
example: consequatur
|
| 265 |
+
type: string
|
| 266 |
+
enumValues: []
|
| 267 |
+
exampleWasSpecified: false
|
| 268 |
+
nullable: false
|
| 269 |
+
deprecated: false
|
| 270 |
+
cleanUrlParameters:
|
| 271 |
+
id: consequatur
|
| 272 |
+
queryParameters: []
|
| 273 |
+
cleanQueryParameters: []
|
| 274 |
+
bodyParameters: []
|
| 275 |
+
cleanBodyParameters: []
|
| 276 |
+
fileParameters: []
|
| 277 |
+
responses:
|
| 278 |
+
-
|
| 279 |
+
custom: []
|
| 280 |
+
status: 404
|
| 281 |
+
content: '{"success":false,"message":"The route api\/scientists\/consequatur\/awards could not be found."}'
|
| 282 |
+
headers:
|
| 283 |
+
cache-control: 'no-cache, private'
|
| 284 |
+
content-type: application/json
|
| 285 |
+
access-control-allow-origin: '*'
|
| 286 |
+
description: null
|
| 287 |
+
responseFields: []
|
| 288 |
+
auth: []
|
| 289 |
+
controller: null
|
| 290 |
+
method: null
|
| 291 |
+
route: null
|
| 292 |
+
-
|
| 293 |
+
custom: []
|
| 294 |
+
httpMethods:
|
| 295 |
+
- POST
|
| 296 |
+
uri: api/user/register
|
| 297 |
+
metadata:
|
| 298 |
+
custom: []
|
| 299 |
+
groupName: Endpoints
|
| 300 |
+
groupDescription: ''
|
| 301 |
+
subgroup: ''
|
| 302 |
+
subgroupDescription: ''
|
| 303 |
+
title: '---------------- Auth ----------------'
|
| 304 |
+
description: ''
|
| 305 |
+
authenticated: false
|
| 306 |
+
deprecated: false
|
| 307 |
+
headers:
|
| 308 |
+
Content-Type: application/json
|
| 309 |
+
Accept: application/json
|
| 310 |
+
urlParameters: []
|
| 311 |
+
cleanUrlParameters: []
|
| 312 |
+
queryParameters: []
|
| 313 |
+
cleanQueryParameters: []
|
| 314 |
+
bodyParameters:
|
| 315 |
+
name:
|
| 316 |
+
custom: []
|
| 317 |
+
name: name
|
| 318 |
+
description: 'Must not be greater than 255 characters.'
|
| 319 |
+
required: true
|
| 320 |
+
example: vmqeopfuudtdsufvyvddq
|
| 321 |
+
type: string
|
| 322 |
+
enumValues: []
|
| 323 |
+
exampleWasSpecified: false
|
| 324 |
+
nullable: false
|
| 325 |
+
deprecated: false
|
| 326 |
+
email:
|
| 327 |
+
custom: []
|
| 328 |
+
name: email
|
| 329 |
+
description: 'Must be a valid email address.'
|
| 330 |
+
required: true
|
| 331 |
+
example: kunde.eloisa@example.com
|
| 332 |
+
type: string
|
| 333 |
+
enumValues: []
|
| 334 |
+
exampleWasSpecified: false
|
| 335 |
+
nullable: false
|
| 336 |
+
deprecated: false
|
| 337 |
+
password:
|
| 338 |
+
custom: []
|
| 339 |
+
name: password
|
| 340 |
+
description: 'Must be at least 6 characters.'
|
| 341 |
+
required: true
|
| 342 |
+
example: '4[*UyPJ"}6'
|
| 343 |
+
type: string
|
| 344 |
+
enumValues: []
|
| 345 |
+
exampleWasSpecified: false
|
| 346 |
+
nullable: false
|
| 347 |
+
deprecated: false
|
| 348 |
+
cleanBodyParameters:
|
| 349 |
+
name: vmqeopfuudtdsufvyvddq
|
| 350 |
+
email: kunde.eloisa@example.com
|
| 351 |
+
password: '4[*UyPJ"}6'
|
| 352 |
+
fileParameters: []
|
| 353 |
+
responses: []
|
| 354 |
+
responseFields: []
|
| 355 |
+
auth: []
|
| 356 |
+
controller: null
|
| 357 |
+
method: null
|
| 358 |
+
route: null
|
| 359 |
+
-
|
| 360 |
+
custom: []
|
| 361 |
+
httpMethods:
|
| 362 |
+
- POST
|
| 363 |
+
uri: api/user/login
|
| 364 |
+
metadata:
|
| 365 |
+
custom: []
|
| 366 |
+
groupName: Endpoints
|
| 367 |
+
groupDescription: ''
|
| 368 |
+
subgroup: ''
|
| 369 |
+
subgroupDescription: ''
|
| 370 |
+
title: ''
|
| 371 |
+
description: ''
|
| 372 |
+
authenticated: false
|
| 373 |
+
deprecated: false
|
| 374 |
+
headers:
|
| 375 |
+
Content-Type: application/json
|
| 376 |
+
Accept: application/json
|
| 377 |
+
urlParameters: []
|
| 378 |
+
cleanUrlParameters: []
|
| 379 |
+
queryParameters: []
|
| 380 |
+
cleanQueryParameters: []
|
| 381 |
+
bodyParameters:
|
| 382 |
+
email:
|
| 383 |
+
custom: []
|
| 384 |
+
name: email
|
| 385 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 386 |
+
required: true
|
| 387 |
+
example: qkunze@example.com
|
| 388 |
+
type: string
|
| 389 |
+
enumValues: []
|
| 390 |
+
exampleWasSpecified: false
|
| 391 |
+
nullable: false
|
| 392 |
+
deprecated: false
|
| 393 |
+
password:
|
| 394 |
+
custom: []
|
| 395 |
+
name: password
|
| 396 |
+
description: ''
|
| 397 |
+
required: true
|
| 398 |
+
example: 'O[2UZ5ij-e/dl4m{o,'
|
| 399 |
+
type: string
|
| 400 |
+
enumValues: []
|
| 401 |
+
exampleWasSpecified: false
|
| 402 |
+
nullable: false
|
| 403 |
+
deprecated: false
|
| 404 |
+
cleanBodyParameters:
|
| 405 |
+
email: qkunze@example.com
|
| 406 |
+
password: 'O[2UZ5ij-e/dl4m{o,'
|
| 407 |
+
fileParameters: []
|
| 408 |
+
responses: []
|
| 409 |
+
responseFields: []
|
| 410 |
+
auth: []
|
| 411 |
+
controller: null
|
| 412 |
+
method: null
|
| 413 |
+
route: null
|
| 414 |
+
-
|
| 415 |
+
custom: []
|
| 416 |
+
httpMethods:
|
| 417 |
+
- POST
|
| 418 |
+
uri: api/user/verify-email
|
| 419 |
+
metadata:
|
| 420 |
+
custom: []
|
| 421 |
+
groupName: Endpoints
|
| 422 |
+
groupDescription: ''
|
| 423 |
+
subgroup: ''
|
| 424 |
+
subgroupDescription: ''
|
| 425 |
+
title: '---------------- Email Verification ----------------'
|
| 426 |
+
description: ''
|
| 427 |
+
authenticated: false
|
| 428 |
+
deprecated: false
|
| 429 |
+
headers:
|
| 430 |
+
Content-Type: application/json
|
| 431 |
+
Accept: application/json
|
| 432 |
+
urlParameters: []
|
| 433 |
+
cleanUrlParameters: []
|
| 434 |
+
queryParameters: []
|
| 435 |
+
cleanQueryParameters: []
|
| 436 |
+
bodyParameters:
|
| 437 |
+
email:
|
| 438 |
+
custom: []
|
| 439 |
+
name: email
|
| 440 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 441 |
+
required: true
|
| 442 |
+
example: qkunze@example.com
|
| 443 |
+
type: string
|
| 444 |
+
enumValues: []
|
| 445 |
+
exampleWasSpecified: false
|
| 446 |
+
nullable: false
|
| 447 |
+
deprecated: false
|
| 448 |
+
otp:
|
| 449 |
+
custom: []
|
| 450 |
+
name: otp
|
| 451 |
+
description: 'Must be 6 digits.'
|
| 452 |
+
required: true
|
| 453 |
+
example: '810798'
|
| 454 |
+
type: string
|
| 455 |
+
enumValues: []
|
| 456 |
+
exampleWasSpecified: false
|
| 457 |
+
nullable: false
|
| 458 |
+
deprecated: false
|
| 459 |
+
cleanBodyParameters:
|
| 460 |
+
email: qkunze@example.com
|
| 461 |
+
otp: '810798'
|
| 462 |
+
fileParameters: []
|
| 463 |
+
responses: []
|
| 464 |
+
responseFields: []
|
| 465 |
+
auth: []
|
| 466 |
+
controller: null
|
| 467 |
+
method: null
|
| 468 |
+
route: null
|
| 469 |
+
-
|
| 470 |
+
custom: []
|
| 471 |
+
httpMethods:
|
| 472 |
+
- POST
|
| 473 |
+
uri: api/user/resend-otp
|
| 474 |
+
metadata:
|
| 475 |
+
custom: []
|
| 476 |
+
groupName: Endpoints
|
| 477 |
+
groupDescription: ''
|
| 478 |
+
subgroup: ''
|
| 479 |
+
subgroupDescription: ''
|
| 480 |
+
title: ''
|
| 481 |
+
description: ''
|
| 482 |
+
authenticated: false
|
| 483 |
+
deprecated: false
|
| 484 |
+
headers:
|
| 485 |
+
Content-Type: application/json
|
| 486 |
+
Accept: application/json
|
| 487 |
+
urlParameters: []
|
| 488 |
+
cleanUrlParameters: []
|
| 489 |
+
queryParameters: []
|
| 490 |
+
cleanQueryParameters: []
|
| 491 |
+
bodyParameters:
|
| 492 |
+
email:
|
| 493 |
+
custom: []
|
| 494 |
+
name: email
|
| 495 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 496 |
+
required: true
|
| 497 |
+
example: qkunze@example.com
|
| 498 |
+
type: string
|
| 499 |
+
enumValues: []
|
| 500 |
+
exampleWasSpecified: false
|
| 501 |
+
nullable: false
|
| 502 |
+
deprecated: false
|
| 503 |
+
cleanBodyParameters:
|
| 504 |
+
email: qkunze@example.com
|
| 505 |
+
fileParameters: []
|
| 506 |
+
responses: []
|
| 507 |
+
responseFields: []
|
| 508 |
+
auth: []
|
| 509 |
+
controller: null
|
| 510 |
+
method: null
|
| 511 |
+
route: null
|
| 512 |
+
-
|
| 513 |
+
custom: []
|
| 514 |
+
httpMethods:
|
| 515 |
+
- POST
|
| 516 |
+
uri: api/user/forgot-password
|
| 517 |
+
metadata:
|
| 518 |
+
custom: []
|
| 519 |
+
groupName: Endpoints
|
| 520 |
+
groupDescription: ''
|
| 521 |
+
subgroup: ''
|
| 522 |
+
subgroupDescription: ''
|
| 523 |
+
title: '---------------- Password Reset ----------------'
|
| 524 |
+
description: ''
|
| 525 |
+
authenticated: false
|
| 526 |
+
deprecated: false
|
| 527 |
+
headers:
|
| 528 |
+
Content-Type: application/json
|
| 529 |
+
Accept: application/json
|
| 530 |
+
urlParameters: []
|
| 531 |
+
cleanUrlParameters: []
|
| 532 |
+
queryParameters: []
|
| 533 |
+
cleanQueryParameters: []
|
| 534 |
+
bodyParameters:
|
| 535 |
+
email:
|
| 536 |
+
custom: []
|
| 537 |
+
name: email
|
| 538 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 539 |
+
required: true
|
| 540 |
+
example: qkunze@example.com
|
| 541 |
+
type: string
|
| 542 |
+
enumValues: []
|
| 543 |
+
exampleWasSpecified: false
|
| 544 |
+
nullable: false
|
| 545 |
+
deprecated: false
|
| 546 |
+
cleanBodyParameters:
|
| 547 |
+
email: qkunze@example.com
|
| 548 |
+
fileParameters: []
|
| 549 |
+
responses: []
|
| 550 |
+
responseFields: []
|
| 551 |
+
auth: []
|
| 552 |
+
controller: null
|
| 553 |
+
method: null
|
| 554 |
+
route: null
|
| 555 |
+
-
|
| 556 |
+
custom: []
|
| 557 |
+
httpMethods:
|
| 558 |
+
- POST
|
| 559 |
+
uri: api/user/reset-password
|
| 560 |
+
metadata:
|
| 561 |
+
custom: []
|
| 562 |
+
groupName: Endpoints
|
| 563 |
+
groupDescription: ''
|
| 564 |
+
subgroup: ''
|
| 565 |
+
subgroupDescription: ''
|
| 566 |
+
title: ''
|
| 567 |
+
description: ''
|
| 568 |
+
authenticated: false
|
| 569 |
+
deprecated: false
|
| 570 |
+
headers:
|
| 571 |
+
Content-Type: application/json
|
| 572 |
+
Accept: application/json
|
| 573 |
+
urlParameters: []
|
| 574 |
+
cleanUrlParameters: []
|
| 575 |
+
queryParameters: []
|
| 576 |
+
cleanQueryParameters: []
|
| 577 |
+
bodyParameters:
|
| 578 |
+
email:
|
| 579 |
+
custom: []
|
| 580 |
+
name: email
|
| 581 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 582 |
+
required: true
|
| 583 |
+
example: qkunze@example.com
|
| 584 |
+
type: string
|
| 585 |
+
enumValues: []
|
| 586 |
+
exampleWasSpecified: false
|
| 587 |
+
nullable: false
|
| 588 |
+
deprecated: false
|
| 589 |
+
otp:
|
| 590 |
+
custom: []
|
| 591 |
+
name: otp
|
| 592 |
+
description: 'Must be 6 digits.'
|
| 593 |
+
required: true
|
| 594 |
+
example: '810798'
|
| 595 |
+
type: string
|
| 596 |
+
enumValues: []
|
| 597 |
+
exampleWasSpecified: false
|
| 598 |
+
nullable: false
|
| 599 |
+
deprecated: false
|
| 600 |
+
password:
|
| 601 |
+
custom: []
|
| 602 |
+
name: password
|
| 603 |
+
description: 'Must be at least 6 characters.'
|
| 604 |
+
required: true
|
| 605 |
+
example: '[2UZ5ij-e/dl4'
|
| 606 |
+
type: string
|
| 607 |
+
enumValues: []
|
| 608 |
+
exampleWasSpecified: false
|
| 609 |
+
nullable: false
|
| 610 |
+
deprecated: false
|
| 611 |
+
cleanBodyParameters:
|
| 612 |
+
email: qkunze@example.com
|
| 613 |
+
otp: '810798'
|
| 614 |
+
password: '[2UZ5ij-e/dl4'
|
| 615 |
+
fileParameters: []
|
| 616 |
+
responses: []
|
| 617 |
+
responseFields: []
|
| 618 |
+
auth: []
|
| 619 |
+
controller: null
|
| 620 |
+
method: null
|
| 621 |
+
route: null
|
| 622 |
+
-
|
| 623 |
+
custom: []
|
| 624 |
+
httpMethods:
|
| 625 |
+
- POST
|
| 626 |
+
uri: api/user/resend-reset-password-otp
|
| 627 |
+
metadata:
|
| 628 |
+
custom: []
|
| 629 |
+
groupName: Endpoints
|
| 630 |
+
groupDescription: ''
|
| 631 |
+
subgroup: ''
|
| 632 |
+
subgroupDescription: ''
|
| 633 |
+
title: ''
|
| 634 |
+
description: ''
|
| 635 |
+
authenticated: false
|
| 636 |
+
deprecated: false
|
| 637 |
+
headers:
|
| 638 |
+
Content-Type: application/json
|
| 639 |
+
Accept: application/json
|
| 640 |
+
urlParameters: []
|
| 641 |
+
cleanUrlParameters: []
|
| 642 |
+
queryParameters: []
|
| 643 |
+
cleanQueryParameters: []
|
| 644 |
+
bodyParameters:
|
| 645 |
+
email:
|
| 646 |
+
custom: []
|
| 647 |
+
name: email
|
| 648 |
+
description: 'Must be a valid email address. The <code>email</code> of an existing record in the users table.'
|
| 649 |
+
required: true
|
| 650 |
+
example: qkunze@example.com
|
| 651 |
+
type: string
|
| 652 |
+
enumValues: []
|
| 653 |
+
exampleWasSpecified: false
|
| 654 |
+
nullable: false
|
| 655 |
+
deprecated: false
|
| 656 |
+
cleanBodyParameters:
|
| 657 |
+
email: qkunze@example.com
|
| 658 |
+
fileParameters: []
|
| 659 |
+
responses: []
|
| 660 |
+
responseFields: []
|
| 661 |
+
auth: []
|
| 662 |
+
controller: null
|
| 663 |
+
method: null
|
| 664 |
+
route: null
|
| 665 |
+
-
|
| 666 |
+
custom: []
|
| 667 |
+
httpMethods:
|
| 668 |
+
- POST
|
| 669 |
+
uri: api/user/auth/google
|
| 670 |
+
metadata:
|
| 671 |
+
custom: []
|
| 672 |
+
groupName: Endpoints
|
| 673 |
+
groupDescription: ''
|
| 674 |
+
subgroup: ''
|
| 675 |
+
subgroupDescription: ''
|
| 676 |
+
title: '---------------- Google OAuth ----------------'
|
| 677 |
+
description: ''
|
| 678 |
+
authenticated: false
|
| 679 |
+
deprecated: false
|
| 680 |
+
headers:
|
| 681 |
+
Content-Type: application/json
|
| 682 |
+
Accept: application/json
|
| 683 |
+
urlParameters: []
|
| 684 |
+
cleanUrlParameters: []
|
| 685 |
+
queryParameters: []
|
| 686 |
+
cleanQueryParameters: []
|
| 687 |
+
bodyParameters:
|
| 688 |
+
access_token:
|
| 689 |
+
custom: []
|
| 690 |
+
name: access_token
|
| 691 |
+
description: ''
|
| 692 |
+
required: true
|
| 693 |
+
example: consequatur
|
| 694 |
+
type: string
|
| 695 |
+
enumValues: []
|
| 696 |
+
exampleWasSpecified: false
|
| 697 |
+
nullable: false
|
| 698 |
+
deprecated: false
|
| 699 |
+
cleanBodyParameters:
|
| 700 |
+
access_token: consequatur
|
| 701 |
+
fileParameters: []
|
| 702 |
+
responses: []
|
| 703 |
+
responseFields: []
|
| 704 |
+
auth: []
|
| 705 |
+
controller: null
|
| 706 |
+
method: null
|
| 707 |
+
route: null
|
| 708 |
+
-
|
| 709 |
+
custom: []
|
| 710 |
+
httpMethods:
|
| 711 |
+
- GET
|
| 712 |
+
uri: api/user/profile
|
| 713 |
+
metadata:
|
| 714 |
+
custom: []
|
| 715 |
+
groupName: Endpoints
|
| 716 |
+
groupDescription: ''
|
| 717 |
+
subgroup: ''
|
| 718 |
+
subgroupDescription: ''
|
| 719 |
+
title: '---------------- Profile ----------------'
|
| 720 |
+
description: ''
|
| 721 |
+
authenticated: false
|
| 722 |
+
deprecated: false
|
| 723 |
+
headers:
|
| 724 |
+
Content-Type: application/json
|
| 725 |
+
Accept: application/json
|
| 726 |
+
urlParameters: []
|
| 727 |
+
cleanUrlParameters: []
|
| 728 |
+
queryParameters: []
|
| 729 |
+
cleanQueryParameters: []
|
| 730 |
+
bodyParameters: []
|
| 731 |
+
cleanBodyParameters: []
|
| 732 |
+
fileParameters: []
|
| 733 |
+
responses:
|
| 734 |
+
-
|
| 735 |
+
custom: []
|
| 736 |
+
status: 404
|
| 737 |
+
content: '{"success":false,"message":"The route api\/user\/profile could not be found."}'
|
| 738 |
+
headers:
|
| 739 |
+
cache-control: 'no-cache, private'
|
| 740 |
+
content-type: application/json
|
| 741 |
+
access-control-allow-origin: '*'
|
| 742 |
+
description: null
|
| 743 |
+
responseFields: []
|
| 744 |
+
auth: []
|
| 745 |
+
controller: null
|
| 746 |
+
method: null
|
| 747 |
+
route: null
|
| 748 |
+
-
|
| 749 |
+
custom: []
|
| 750 |
+
httpMethods:
|
| 751 |
+
- POST
|
| 752 |
+
uri: api/user/logout
|
| 753 |
+
metadata:
|
| 754 |
+
custom: []
|
| 755 |
+
groupName: Endpoints
|
| 756 |
+
groupDescription: ''
|
| 757 |
+
subgroup: ''
|
| 758 |
+
subgroupDescription: ''
|
| 759 |
+
title: ''
|
| 760 |
+
description: ''
|
| 761 |
+
authenticated: false
|
| 762 |
+
deprecated: false
|
| 763 |
+
headers:
|
| 764 |
+
Content-Type: application/json
|
| 765 |
+
Accept: application/json
|
| 766 |
+
urlParameters: []
|
| 767 |
+
cleanUrlParameters: []
|
| 768 |
+
queryParameters: []
|
| 769 |
+
cleanQueryParameters: []
|
| 770 |
+
bodyParameters: []
|
| 771 |
+
cleanBodyParameters: []
|
| 772 |
+
fileParameters: []
|
| 773 |
+
responses: []
|
| 774 |
+
responseFields: []
|
| 775 |
+
auth: []
|
| 776 |
+
controller: null
|
| 777 |
+
method: null
|
| 778 |
+
route: null
|
| 779 |
+
-
|
| 780 |
+
custom: []
|
| 781 |
+
httpMethods:
|
| 782 |
+
- GET
|
| 783 |
+
uri: api/news
|
| 784 |
+
metadata:
|
| 785 |
+
custom: []
|
| 786 |
+
groupName: Endpoints
|
| 787 |
+
groupDescription: ''
|
| 788 |
+
subgroup: ''
|
| 789 |
+
subgroupDescription: ''
|
| 790 |
+
title: ''
|
| 791 |
+
description: ''
|
| 792 |
+
authenticated: false
|
| 793 |
+
deprecated: false
|
| 794 |
+
headers:
|
| 795 |
+
Content-Type: application/json
|
| 796 |
+
Accept: application/json
|
| 797 |
+
urlParameters: []
|
| 798 |
+
cleanUrlParameters: []
|
| 799 |
+
queryParameters: []
|
| 800 |
+
cleanQueryParameters: []
|
| 801 |
+
bodyParameters: []
|
| 802 |
+
cleanBodyParameters: []
|
| 803 |
+
fileParameters: []
|
| 804 |
+
responses:
|
| 805 |
+
-
|
| 806 |
+
custom: []
|
| 807 |
+
status: 404
|
| 808 |
+
content: '{"success":false,"message":"The route api\/news could not be found."}'
|
| 809 |
+
headers:
|
| 810 |
+
cache-control: 'no-cache, private'
|
| 811 |
+
content-type: application/json
|
| 812 |
+
access-control-allow-origin: '*'
|
| 813 |
+
description: null
|
| 814 |
+
responseFields: []
|
| 815 |
+
auth: []
|
| 816 |
+
controller: null
|
| 817 |
+
method: null
|
| 818 |
+
route: null
|
| 819 |
+
-
|
| 820 |
+
custom: []
|
| 821 |
+
httpMethods:
|
| 822 |
+
- GET
|
| 823 |
+
uri: api/news/refresh
|
| 824 |
+
metadata:
|
| 825 |
+
custom: []
|
| 826 |
+
groupName: Endpoints
|
| 827 |
+
groupDescription: ''
|
| 828 |
+
subgroup: ''
|
| 829 |
+
subgroupDescription: ''
|
| 830 |
+
title: ''
|
| 831 |
+
description: ''
|
| 832 |
+
authenticated: false
|
| 833 |
+
deprecated: false
|
| 834 |
+
headers:
|
| 835 |
+
Content-Type: application/json
|
| 836 |
+
Accept: application/json
|
| 837 |
+
urlParameters: []
|
| 838 |
+
cleanUrlParameters: []
|
| 839 |
+
queryParameters: []
|
| 840 |
+
cleanQueryParameters: []
|
| 841 |
+
bodyParameters: []
|
| 842 |
+
cleanBodyParameters: []
|
| 843 |
+
fileParameters: []
|
| 844 |
+
responses:
|
| 845 |
+
-
|
| 846 |
+
custom: []
|
| 847 |
+
status: 404
|
| 848 |
+
content: '{"success":false,"message":"The route api\/news\/refresh could not be found."}'
|
| 849 |
+
headers:
|
| 850 |
+
cache-control: 'no-cache, private'
|
| 851 |
+
content-type: application/json
|
| 852 |
+
access-control-allow-origin: '*'
|
| 853 |
+
description: null
|
| 854 |
+
responseFields: []
|
| 855 |
+
auth: []
|
| 856 |
+
controller: null
|
| 857 |
+
method: null
|
| 858 |
+
route: null
|
| 859 |
+
-
|
| 860 |
+
custom: []
|
| 861 |
+
httpMethods:
|
| 862 |
+
- GET
|
| 863 |
+
uri: api/news/clear
|
| 864 |
+
metadata:
|
| 865 |
+
custom: []
|
| 866 |
+
groupName: Endpoints
|
| 867 |
+
groupDescription: ''
|
| 868 |
+
subgroup: ''
|
| 869 |
+
subgroupDescription: ''
|
| 870 |
+
title: ''
|
| 871 |
+
description: ''
|
| 872 |
+
authenticated: false
|
| 873 |
+
deprecated: false
|
| 874 |
+
headers:
|
| 875 |
+
Content-Type: application/json
|
| 876 |
+
Accept: application/json
|
| 877 |
+
urlParameters: []
|
| 878 |
+
cleanUrlParameters: []
|
| 879 |
+
queryParameters: []
|
| 880 |
+
cleanQueryParameters: []
|
| 881 |
+
bodyParameters: []
|
| 882 |
+
cleanBodyParameters: []
|
| 883 |
+
fileParameters: []
|
| 884 |
+
responses:
|
| 885 |
+
-
|
| 886 |
+
custom: []
|
| 887 |
+
status: 404
|
| 888 |
+
content: '{"success":false,"message":"The route api\/news\/clear could not be found."}'
|
| 889 |
+
headers:
|
| 890 |
+
cache-control: 'no-cache, private'
|
| 891 |
+
content-type: application/json
|
| 892 |
+
access-control-allow-origin: '*'
|
| 893 |
+
description: null
|
| 894 |
+
responseFields: []
|
| 895 |
+
auth: []
|
| 896 |
+
controller: null
|
| 897 |
+
method: null
|
| 898 |
+
route: null
|
| 899 |
+
-
|
| 900 |
+
custom: []
|
| 901 |
+
httpMethods:
|
| 902 |
+
- GET
|
| 903 |
+
uri: api/news/categories
|
| 904 |
+
metadata:
|
| 905 |
+
custom: []
|
| 906 |
+
groupName: Endpoints
|
| 907 |
+
groupDescription: ''
|
| 908 |
+
subgroup: ''
|
| 909 |
+
subgroupDescription: ''
|
| 910 |
+
title: ''
|
| 911 |
+
description: ''
|
| 912 |
+
authenticated: false
|
| 913 |
+
deprecated: false
|
| 914 |
+
headers:
|
| 915 |
+
Content-Type: application/json
|
| 916 |
+
Accept: application/json
|
| 917 |
+
urlParameters: []
|
| 918 |
+
cleanUrlParameters: []
|
| 919 |
+
queryParameters: []
|
| 920 |
+
cleanQueryParameters: []
|
| 921 |
+
bodyParameters: []
|
| 922 |
+
cleanBodyParameters: []
|
| 923 |
+
fileParameters: []
|
| 924 |
+
responses:
|
| 925 |
+
-
|
| 926 |
+
custom: []
|
| 927 |
+
status: 404
|
| 928 |
+
content: '{"success":false,"message":"The route api\/news\/categories could not be found."}'
|
| 929 |
+
headers:
|
| 930 |
+
cache-control: 'no-cache, private'
|
| 931 |
+
content-type: application/json
|
| 932 |
+
access-control-allow-origin: '*'
|
| 933 |
+
description: null
|
| 934 |
+
responseFields: []
|
| 935 |
+
auth: []
|
| 936 |
+
controller: null
|
| 937 |
+
method: null
|
| 938 |
+
route: null
|
| 939 |
+
-
|
| 940 |
+
custom: []
|
| 941 |
+
httpMethods:
|
| 942 |
+
- POST
|
| 943 |
+
uri: 'api/news/{articleId}/share'
|
| 944 |
+
metadata:
|
| 945 |
+
custom: []
|
| 946 |
+
groupName: Endpoints
|
| 947 |
+
groupDescription: ''
|
| 948 |
+
subgroup: ''
|
| 949 |
+
subgroupDescription: ''
|
| 950 |
+
title: ''
|
| 951 |
+
description: ''
|
| 952 |
+
authenticated: false
|
| 953 |
+
deprecated: false
|
| 954 |
+
headers:
|
| 955 |
+
Content-Type: application/json
|
| 956 |
+
Accept: application/json
|
| 957 |
+
urlParameters:
|
| 958 |
+
articleId:
|
| 959 |
+
custom: []
|
| 960 |
+
name: articleId
|
| 961 |
+
description: ''
|
| 962 |
+
required: true
|
| 963 |
+
example: consequatur
|
| 964 |
+
type: string
|
| 965 |
+
enumValues: []
|
| 966 |
+
exampleWasSpecified: false
|
| 967 |
+
nullable: false
|
| 968 |
+
deprecated: false
|
| 969 |
+
cleanUrlParameters:
|
| 970 |
+
articleId: consequatur
|
| 971 |
+
queryParameters: []
|
| 972 |
+
cleanQueryParameters: []
|
| 973 |
+
bodyParameters: []
|
| 974 |
+
cleanBodyParameters: []
|
| 975 |
+
fileParameters: []
|
| 976 |
+
responses: []
|
| 977 |
+
responseFields: []
|
| 978 |
+
auth: []
|
| 979 |
+
controller: null
|
| 980 |
+
method: null
|
| 981 |
+
route: null
|
| 982 |
+
-
|
| 983 |
+
custom: []
|
| 984 |
+
httpMethods:
|
| 985 |
+
- POST
|
| 986 |
+
uri: 'api/news/{articleId}/save'
|
| 987 |
+
metadata:
|
| 988 |
+
custom: []
|
| 989 |
+
groupName: Endpoints
|
| 990 |
+
groupDescription: ''
|
| 991 |
+
subgroup: ''
|
| 992 |
+
subgroupDescription: ''
|
| 993 |
+
title: ''
|
| 994 |
+
description: ''
|
| 995 |
+
authenticated: false
|
| 996 |
+
deprecated: false
|
| 997 |
+
headers:
|
| 998 |
+
Content-Type: application/json
|
| 999 |
+
Accept: application/json
|
| 1000 |
+
urlParameters:
|
| 1001 |
+
articleId:
|
| 1002 |
+
custom: []
|
| 1003 |
+
name: articleId
|
| 1004 |
+
description: ''
|
| 1005 |
+
required: true
|
| 1006 |
+
example: consequatur
|
| 1007 |
+
type: string
|
| 1008 |
+
enumValues: []
|
| 1009 |
+
exampleWasSpecified: false
|
| 1010 |
+
nullable: false
|
| 1011 |
+
deprecated: false
|
| 1012 |
+
cleanUrlParameters:
|
| 1013 |
+
articleId: consequatur
|
| 1014 |
+
queryParameters: []
|
| 1015 |
+
cleanQueryParameters: []
|
| 1016 |
+
bodyParameters: []
|
| 1017 |
+
cleanBodyParameters: []
|
| 1018 |
+
fileParameters: []
|
| 1019 |
+
responses: []
|
| 1020 |
+
responseFields: []
|
| 1021 |
+
auth: []
|
| 1022 |
+
controller: null
|
| 1023 |
+
method: null
|
| 1024 |
+
route: null
|
| 1025 |
+
-
|
| 1026 |
+
custom: []
|
| 1027 |
+
httpMethods:
|
| 1028 |
+
- GET
|
| 1029 |
+
uri: api/news/saved
|
| 1030 |
+
metadata:
|
| 1031 |
+
custom: []
|
| 1032 |
+
groupName: Endpoints
|
| 1033 |
+
groupDescription: ''
|
| 1034 |
+
subgroup: ''
|
| 1035 |
+
subgroupDescription: ''
|
| 1036 |
+
title: ''
|
| 1037 |
+
description: ''
|
| 1038 |
+
authenticated: false
|
| 1039 |
+
deprecated: false
|
| 1040 |
+
headers:
|
| 1041 |
+
Content-Type: application/json
|
| 1042 |
+
Accept: application/json
|
| 1043 |
+
urlParameters: []
|
| 1044 |
+
cleanUrlParameters: []
|
| 1045 |
+
queryParameters: []
|
| 1046 |
+
cleanQueryParameters: []
|
| 1047 |
+
bodyParameters: []
|
| 1048 |
+
cleanBodyParameters: []
|
| 1049 |
+
fileParameters: []
|
| 1050 |
+
responses:
|
| 1051 |
+
-
|
| 1052 |
+
custom: []
|
| 1053 |
+
status: 404
|
| 1054 |
+
content: '{"success":false,"message":"The route api\/news\/saved could not be found."}'
|
| 1055 |
+
headers:
|
| 1056 |
+
cache-control: 'no-cache, private'
|
| 1057 |
+
content-type: application/json
|
| 1058 |
+
access-control-allow-origin: '*'
|
| 1059 |
+
description: null
|
| 1060 |
+
responseFields: []
|
| 1061 |
+
auth: []
|
| 1062 |
+
controller: null
|
| 1063 |
+
method: null
|
| 1064 |
+
route: null
|
| 1065 |
+
-
|
| 1066 |
+
custom: []
|
| 1067 |
+
httpMethods:
|
| 1068 |
+
- DELETE
|
| 1069 |
+
uri: 'api/news/saved/{savedArticleId}'
|
| 1070 |
+
metadata:
|
| 1071 |
+
custom: []
|
| 1072 |
+
groupName: Endpoints
|
| 1073 |
+
groupDescription: ''
|
| 1074 |
+
subgroup: ''
|
| 1075 |
+
subgroupDescription: ''
|
| 1076 |
+
title: ''
|
| 1077 |
+
description: ''
|
| 1078 |
+
authenticated: false
|
| 1079 |
+
deprecated: false
|
| 1080 |
+
headers:
|
| 1081 |
+
Content-Type: application/json
|
| 1082 |
+
Accept: application/json
|
| 1083 |
+
urlParameters:
|
| 1084 |
+
savedArticleId:
|
| 1085 |
+
custom: []
|
| 1086 |
+
name: savedArticleId
|
| 1087 |
+
description: ''
|
| 1088 |
+
required: true
|
| 1089 |
+
example: consequatur
|
| 1090 |
+
type: string
|
| 1091 |
+
enumValues: []
|
| 1092 |
+
exampleWasSpecified: false
|
| 1093 |
+
nullable: false
|
| 1094 |
+
deprecated: false
|
| 1095 |
+
cleanUrlParameters:
|
| 1096 |
+
savedArticleId: consequatur
|
| 1097 |
+
queryParameters: []
|
| 1098 |
+
cleanQueryParameters: []
|
| 1099 |
+
bodyParameters: []
|
| 1100 |
+
cleanBodyParameters: []
|
| 1101 |
+
fileParameters: []
|
| 1102 |
+
responses: []
|
| 1103 |
+
responseFields: []
|
| 1104 |
+
auth: []
|
| 1105 |
+
controller: null
|
| 1106 |
+
method: null
|
| 1107 |
+
route: null
|
| 1108 |
+
-
|
| 1109 |
+
custom: []
|
| 1110 |
+
httpMethods:
|
| 1111 |
+
- POST
|
| 1112 |
+
uri: api/ai/run
|
| 1113 |
+
metadata:
|
| 1114 |
+
custom: []
|
| 1115 |
+
groupName: Endpoints
|
| 1116 |
+
groupDescription: ''
|
| 1117 |
+
subgroup: ''
|
| 1118 |
+
subgroupDescription: ''
|
| 1119 |
+
title: ''
|
| 1120 |
+
description: ''
|
| 1121 |
+
authenticated: false
|
| 1122 |
+
deprecated: false
|
| 1123 |
+
headers:
|
| 1124 |
+
Content-Type: application/json
|
| 1125 |
+
Accept: application/json
|
| 1126 |
+
urlParameters: []
|
| 1127 |
+
cleanUrlParameters: []
|
| 1128 |
+
queryParameters: []
|
| 1129 |
+
cleanQueryParameters: []
|
| 1130 |
+
bodyParameters:
|
| 1131 |
+
num_smiles:
|
| 1132 |
+
custom: []
|
| 1133 |
+
name: num_smiles
|
| 1134 |
+
description: ''
|
| 1135 |
+
required: true
|
| 1136 |
+
example: 17
|
| 1137 |
+
type: integer
|
| 1138 |
+
enumValues: []
|
| 1139 |
+
exampleWasSpecified: false
|
| 1140 |
+
nullable: false
|
| 1141 |
+
deprecated: false
|
| 1142 |
+
top_k:
|
| 1143 |
+
custom: []
|
| 1144 |
+
name: top_k
|
| 1145 |
+
description: ''
|
| 1146 |
+
required: true
|
| 1147 |
+
example: 17
|
| 1148 |
+
type: integer
|
| 1149 |
+
enumValues: []
|
| 1150 |
+
exampleWasSpecified: false
|
| 1151 |
+
nullable: false
|
| 1152 |
+
deprecated: false
|
| 1153 |
+
Docking:
|
| 1154 |
+
custom: []
|
| 1155 |
+
name: Docking
|
| 1156 |
+
description: ''
|
| 1157 |
+
required: true
|
| 1158 |
+
example: 11613.31890586
|
| 1159 |
+
type: number
|
| 1160 |
+
enumValues: []
|
| 1161 |
+
exampleWasSpecified: false
|
| 1162 |
+
nullable: false
|
| 1163 |
+
deprecated: false
|
| 1164 |
+
DeepPurpose_Affinity:
|
| 1165 |
+
custom: []
|
| 1166 |
+
name: DeepPurpose_Affinity
|
| 1167 |
+
description: ''
|
| 1168 |
+
required: true
|
| 1169 |
+
example: 11613.31890586
|
| 1170 |
+
type: number
|
| 1171 |
+
enumValues: []
|
| 1172 |
+
exampleWasSpecified: false
|
| 1173 |
+
nullable: false
|
| 1174 |
+
deprecated: false
|
| 1175 |
+
SA:
|
| 1176 |
+
custom: []
|
| 1177 |
+
name: SA
|
| 1178 |
+
description: ''
|
| 1179 |
+
required: true
|
| 1180 |
+
example: 11613.31890586
|
| 1181 |
+
type: number
|
| 1182 |
+
enumValues: []
|
| 1183 |
+
exampleWasSpecified: false
|
| 1184 |
+
nullable: false
|
| 1185 |
+
deprecated: false
|
| 1186 |
+
TPSA:
|
| 1187 |
+
custom: []
|
| 1188 |
+
name: TPSA
|
| 1189 |
+
description: ''
|
| 1190 |
+
required: true
|
| 1191 |
+
example: 11613.31890586
|
| 1192 |
+
type: number
|
| 1193 |
+
enumValues: []
|
| 1194 |
+
exampleWasSpecified: false
|
| 1195 |
+
nullable: false
|
| 1196 |
+
deprecated: false
|
| 1197 |
+
MW:
|
| 1198 |
+
custom: []
|
| 1199 |
+
name: MW
|
| 1200 |
+
description: ''
|
| 1201 |
+
required: true
|
| 1202 |
+
example: 11613.31890586
|
| 1203 |
+
type: number
|
| 1204 |
+
enumValues: []
|
| 1205 |
+
exampleWasSpecified: false
|
| 1206 |
+
nullable: false
|
| 1207 |
+
deprecated: false
|
| 1208 |
+
cleanBodyParameters:
|
| 1209 |
+
num_smiles: 17
|
| 1210 |
+
top_k: 17
|
| 1211 |
+
Docking: 11613.31890586
|
| 1212 |
+
DeepPurpose_Affinity: 11613.31890586
|
| 1213 |
+
SA: 11613.31890586
|
| 1214 |
+
TPSA: 11613.31890586
|
| 1215 |
+
MW: 11613.31890586
|
| 1216 |
+
fileParameters: []
|
| 1217 |
+
responses: []
|
| 1218 |
+
responseFields: []
|
| 1219 |
+
auth: []
|
| 1220 |
+
controller: null
|
| 1221 |
+
method: null
|
| 1222 |
+
route: null
|
| 1223 |
+
-
|
| 1224 |
+
custom: []
|
| 1225 |
+
httpMethods:
|
| 1226 |
+
- GET
|
| 1227 |
+
uri: 'api/ai/status/{job_job_id}'
|
| 1228 |
+
metadata:
|
| 1229 |
+
custom: []
|
| 1230 |
+
groupName: Endpoints
|
| 1231 |
+
groupDescription: ''
|
| 1232 |
+
subgroup: ''
|
| 1233 |
+
subgroupDescription: ''
|
| 1234 |
+
title: ''
|
| 1235 |
+
description: ''
|
| 1236 |
+
authenticated: false
|
| 1237 |
+
deprecated: false
|
| 1238 |
+
headers:
|
| 1239 |
+
Content-Type: application/json
|
| 1240 |
+
Accept: application/json
|
| 1241 |
+
urlParameters:
|
| 1242 |
+
job_job_id:
|
| 1243 |
+
custom: []
|
| 1244 |
+
name: job_job_id
|
| 1245 |
+
description: 'The ID of the job job.'
|
| 1246 |
+
required: true
|
| 1247 |
+
example: consequatur
|
| 1248 |
+
type: string
|
| 1249 |
+
enumValues: []
|
| 1250 |
+
exampleWasSpecified: false
|
| 1251 |
+
nullable: false
|
| 1252 |
+
deprecated: false
|
| 1253 |
+
cleanUrlParameters:
|
| 1254 |
+
job_job_id: consequatur
|
| 1255 |
+
queryParameters: []
|
| 1256 |
+
cleanQueryParameters: []
|
| 1257 |
+
bodyParameters: []
|
| 1258 |
+
cleanBodyParameters: []
|
| 1259 |
+
fileParameters: []
|
| 1260 |
+
responses:
|
| 1261 |
+
-
|
| 1262 |
+
custom: []
|
| 1263 |
+
status: 404
|
| 1264 |
+
content: '{"success":false,"message":"The route api\/ai\/status\/consequatur could not be found."}'
|
| 1265 |
+
headers:
|
| 1266 |
+
cache-control: 'no-cache, private'
|
| 1267 |
+
content-type: application/json
|
| 1268 |
+
access-control-allow-origin: '*'
|
| 1269 |
+
description: null
|
| 1270 |
+
responseFields: []
|
| 1271 |
+
auth: []
|
| 1272 |
+
controller: null
|
| 1273 |
+
method: null
|
| 1274 |
+
route: null
|
| 1275 |
+
-
|
| 1276 |
+
custom: []
|
| 1277 |
+
httpMethods:
|
| 1278 |
+
- GET
|
| 1279 |
+
uri: 'api/ai/preview/{job_job_id}'
|
| 1280 |
+
metadata:
|
| 1281 |
+
custom: []
|
| 1282 |
+
groupName: Endpoints
|
| 1283 |
+
groupDescription: ''
|
| 1284 |
+
subgroup: ''
|
| 1285 |
+
subgroupDescription: ''
|
| 1286 |
+
title: ''
|
| 1287 |
+
description: ''
|
| 1288 |
+
authenticated: false
|
| 1289 |
+
deprecated: false
|
| 1290 |
+
headers:
|
| 1291 |
+
Content-Type: application/json
|
| 1292 |
+
Accept: application/json
|
| 1293 |
+
urlParameters:
|
| 1294 |
+
job_job_id:
|
| 1295 |
+
custom: []
|
| 1296 |
+
name: job_job_id
|
| 1297 |
+
description: 'The ID of the job job.'
|
| 1298 |
+
required: true
|
| 1299 |
+
example: consequatur
|
| 1300 |
+
type: string
|
| 1301 |
+
enumValues: []
|
| 1302 |
+
exampleWasSpecified: false
|
| 1303 |
+
nullable: false
|
| 1304 |
+
deprecated: false
|
| 1305 |
+
cleanUrlParameters:
|
| 1306 |
+
job_job_id: consequatur
|
| 1307 |
+
queryParameters: []
|
| 1308 |
+
cleanQueryParameters: []
|
| 1309 |
+
bodyParameters: []
|
| 1310 |
+
cleanBodyParameters: []
|
| 1311 |
+
fileParameters: []
|
| 1312 |
+
responses:
|
| 1313 |
+
-
|
| 1314 |
+
custom: []
|
| 1315 |
+
status: 404
|
| 1316 |
+
content: '{"success":false,"message":"The route api\/ai\/preview\/consequatur could not be found."}'
|
| 1317 |
+
headers:
|
| 1318 |
+
cache-control: 'no-cache, private'
|
| 1319 |
+
content-type: application/json
|
| 1320 |
+
access-control-allow-origin: '*'
|
| 1321 |
+
description: null
|
| 1322 |
+
responseFields: []
|
| 1323 |
+
auth: []
|
| 1324 |
+
controller: null
|
| 1325 |
+
method: null
|
| 1326 |
+
route: null
|
| 1327 |
+
-
|
| 1328 |
+
custom: []
|
| 1329 |
+
httpMethods:
|
| 1330 |
+
- GET
|
| 1331 |
+
uri: 'api/ai/download/top/{job_job_id}'
|
| 1332 |
+
metadata:
|
| 1333 |
+
custom: []
|
| 1334 |
+
groupName: Endpoints
|
| 1335 |
+
groupDescription: ''
|
| 1336 |
+
subgroup: ''
|
| 1337 |
+
subgroupDescription: ''
|
| 1338 |
+
title: ''
|
| 1339 |
+
description: ''
|
| 1340 |
+
authenticated: false
|
| 1341 |
+
deprecated: false
|
| 1342 |
+
headers:
|
| 1343 |
+
Content-Type: application/json
|
| 1344 |
+
Accept: application/json
|
| 1345 |
+
urlParameters:
|
| 1346 |
+
job_job_id:
|
| 1347 |
+
custom: []
|
| 1348 |
+
name: job_job_id
|
| 1349 |
+
description: 'The ID of the job job.'
|
| 1350 |
+
required: true
|
| 1351 |
+
example: consequatur
|
| 1352 |
+
type: string
|
| 1353 |
+
enumValues: []
|
| 1354 |
+
exampleWasSpecified: false
|
| 1355 |
+
nullable: false
|
| 1356 |
+
deprecated: false
|
| 1357 |
+
cleanUrlParameters:
|
| 1358 |
+
job_job_id: consequatur
|
| 1359 |
+
queryParameters: []
|
| 1360 |
+
cleanQueryParameters: []
|
| 1361 |
+
bodyParameters: []
|
| 1362 |
+
cleanBodyParameters: []
|
| 1363 |
+
fileParameters: []
|
| 1364 |
+
responses:
|
| 1365 |
+
-
|
| 1366 |
+
custom: []
|
| 1367 |
+
status: 404
|
| 1368 |
+
content: '{"success":false,"message":"The route api\/ai\/download\/top\/consequatur could not be found."}'
|
| 1369 |
+
headers:
|
| 1370 |
+
cache-control: 'no-cache, private'
|
| 1371 |
+
content-type: application/json
|
| 1372 |
+
access-control-allow-origin: '*'
|
| 1373 |
+
description: null
|
| 1374 |
+
responseFields: []
|
| 1375 |
+
auth: []
|
| 1376 |
+
controller: null
|
| 1377 |
+
method: null
|
| 1378 |
+
route: null
|
| 1379 |
+
-
|
| 1380 |
+
custom: []
|
| 1381 |
+
httpMethods:
|
| 1382 |
+
- GET
|
| 1383 |
+
uri: 'api/ai/download/full/{job_job_id}'
|
| 1384 |
+
metadata:
|
| 1385 |
+
custom: []
|
| 1386 |
+
groupName: Endpoints
|
| 1387 |
+
groupDescription: ''
|
| 1388 |
+
subgroup: ''
|
| 1389 |
+
subgroupDescription: ''
|
| 1390 |
+
title: ''
|
| 1391 |
+
description: ''
|
| 1392 |
+
authenticated: false
|
| 1393 |
+
deprecated: false
|
| 1394 |
+
headers:
|
| 1395 |
+
Content-Type: application/json
|
| 1396 |
+
Accept: application/json
|
| 1397 |
+
urlParameters:
|
| 1398 |
+
job_job_id:
|
| 1399 |
+
custom: []
|
| 1400 |
+
name: job_job_id
|
| 1401 |
+
description: 'The ID of the job job.'
|
| 1402 |
+
required: true
|
| 1403 |
+
example: consequatur
|
| 1404 |
+
type: string
|
| 1405 |
+
enumValues: []
|
| 1406 |
+
exampleWasSpecified: false
|
| 1407 |
+
nullable: false
|
| 1408 |
+
deprecated: false
|
| 1409 |
+
cleanUrlParameters:
|
| 1410 |
+
job_job_id: consequatur
|
| 1411 |
+
queryParameters: []
|
| 1412 |
+
cleanQueryParameters: []
|
| 1413 |
+
bodyParameters: []
|
| 1414 |
+
cleanBodyParameters: []
|
| 1415 |
+
fileParameters: []
|
| 1416 |
+
responses:
|
| 1417 |
+
-
|
| 1418 |
+
custom: []
|
| 1419 |
+
status: 404
|
| 1420 |
+
content: '{"success":false,"message":"The route api\/ai\/download\/full\/consequatur could not be found."}'
|
| 1421 |
+
headers:
|
| 1422 |
+
cache-control: 'no-cache, private'
|
| 1423 |
+
content-type: application/json
|
| 1424 |
+
access-control-allow-origin: '*'
|
| 1425 |
+
description: null
|
| 1426 |
+
responseFields: []
|
| 1427 |
+
auth: []
|
| 1428 |
+
controller: null
|
| 1429 |
+
method: null
|
| 1430 |
+
route: null
|
| 1431 |
+
-
|
| 1432 |
+
custom: []
|
| 1433 |
+
httpMethods:
|
| 1434 |
+
- GET
|
| 1435 |
+
uri: api/ai/history
|
| 1436 |
+
metadata:
|
| 1437 |
+
custom: []
|
| 1438 |
+
groupName: Endpoints
|
| 1439 |
+
groupDescription: ''
|
| 1440 |
+
subgroup: ''
|
| 1441 |
+
subgroupDescription: ''
|
| 1442 |
+
title: ''
|
| 1443 |
+
description: ''
|
| 1444 |
+
authenticated: false
|
| 1445 |
+
deprecated: false
|
| 1446 |
+
headers:
|
| 1447 |
+
Content-Type: application/json
|
| 1448 |
+
Accept: application/json
|
| 1449 |
+
urlParameters: []
|
| 1450 |
+
cleanUrlParameters: []
|
| 1451 |
+
queryParameters: []
|
| 1452 |
+
cleanQueryParameters: []
|
| 1453 |
+
bodyParameters: []
|
| 1454 |
+
cleanBodyParameters: []
|
| 1455 |
+
fileParameters: []
|
| 1456 |
+
responses:
|
| 1457 |
+
-
|
| 1458 |
+
custom: []
|
| 1459 |
+
status: 404
|
| 1460 |
+
content: '{"success":false,"message":"The route api\/ai\/history could not be found."}'
|
| 1461 |
+
headers:
|
| 1462 |
+
cache-control: 'no-cache, private'
|
| 1463 |
+
content-type: application/json
|
| 1464 |
+
access-control-allow-origin: '*'
|
| 1465 |
+
description: null
|
| 1466 |
+
responseFields: []
|
| 1467 |
+
auth: []
|
| 1468 |
+
controller: null
|
| 1469 |
+
method: null
|
| 1470 |
+
route: null
|
| 1471 |
+
-
|
| 1472 |
+
custom: []
|
| 1473 |
+
httpMethods:
|
| 1474 |
+
- POST
|
| 1475 |
+
uri: api/upload-file
|
| 1476 |
+
metadata:
|
| 1477 |
+
custom: []
|
| 1478 |
+
groupName: Endpoints
|
| 1479 |
+
groupDescription: ''
|
| 1480 |
+
subgroup: ''
|
| 1481 |
+
subgroupDescription: ''
|
| 1482 |
+
title: ''
|
| 1483 |
+
description: ''
|
| 1484 |
+
authenticated: false
|
| 1485 |
+
deprecated: false
|
| 1486 |
+
headers:
|
| 1487 |
+
Content-Type: application/json
|
| 1488 |
+
Accept: application/json
|
| 1489 |
+
urlParameters: []
|
| 1490 |
+
cleanUrlParameters: []
|
| 1491 |
+
queryParameters: []
|
| 1492 |
+
cleanQueryParameters: []
|
| 1493 |
+
bodyParameters: []
|
| 1494 |
+
cleanBodyParameters: []
|
| 1495 |
+
fileParameters: []
|
| 1496 |
+
responses: []
|
| 1497 |
+
responseFields: []
|
| 1498 |
+
auth: []
|
| 1499 |
+
controller: null
|
| 1500 |
+
method: null
|
| 1501 |
+
route: null
|
.scribe/endpoints/custom.0.yaml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# To include an endpoint that isn't a part of your Laravel app (or belongs to a vendor package),
|
| 2 |
+
# you can define it in a custom.*.yaml file, like this one.
|
| 3 |
+
# Each custom file should contain an array of endpoints. Here's an example:
|
| 4 |
+
# See https://scribe.knuckles.wtf/laravel/documenting/custom-endpoints#extra-sorting-groups-in-custom-endpoint-files for more options
|
| 5 |
+
|
| 6 |
+
#- httpMethods:
|
| 7 |
+
# - POST
|
| 8 |
+
# uri: api/doSomething/{param}
|
| 9 |
+
# metadata:
|
| 10 |
+
# groupName: The group the endpoint belongs to. Can be a new group or an existing group.
|
| 11 |
+
# groupDescription: A description for the group. You don't need to set this for every endpoint; once is enough.
|
| 12 |
+
# subgroup: You can add a subgroup, too.
|
| 13 |
+
# title: Do something
|
| 14 |
+
# description: 'This endpoint allows you to do something.'
|
| 15 |
+
# authenticated: false
|
| 16 |
+
# headers:
|
| 17 |
+
# Content-Type: application/json
|
| 18 |
+
# Accept: application/json
|
| 19 |
+
# urlParameters:
|
| 20 |
+
# param:
|
| 21 |
+
# name: param
|
| 22 |
+
# description: A URL param for no reason.
|
| 23 |
+
# required: true
|
| 24 |
+
# example: 2
|
| 25 |
+
# type: integer
|
| 26 |
+
# queryParameters:
|
| 27 |
+
# speed:
|
| 28 |
+
# name: speed
|
| 29 |
+
# description: How fast the thing should be done. Can be `slow` or `fast`.
|
| 30 |
+
# required: false
|
| 31 |
+
# example: fast
|
| 32 |
+
# type: string
|
| 33 |
+
# bodyParameters:
|
| 34 |
+
# something:
|
| 35 |
+
# name: something
|
| 36 |
+
# description: The things we should do.
|
| 37 |
+
# required: true
|
| 38 |
+
# example:
|
| 39 |
+
# - string 1
|
| 40 |
+
# - string 2
|
| 41 |
+
# type: 'string[]'
|
| 42 |
+
# responses:
|
| 43 |
+
# - status: 200
|
| 44 |
+
# description: 'When the thing was done smoothly.'
|
| 45 |
+
# content: # Your response content can be an object, an array, a string or empty.
|
| 46 |
+
# {
|
| 47 |
+
# "hey": "ho ho ho"
|
| 48 |
+
# }
|
| 49 |
+
# responseFields:
|
| 50 |
+
# hey:
|
| 51 |
+
# name: hey
|
| 52 |
+
# description: Who knows?
|
| 53 |
+
# type: string # This is optional
|
.scribe/intro.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Introduction
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
<aside>
|
| 6 |
+
<strong>Base URL</strong>: <code>https://america-hyperlipemic-grazyna.ngrok-free.dev/</code>
|
| 7 |
+
</aside>
|
| 8 |
+
|
| 9 |
+
This documentation aims to provide all the information you need to work with our API.
|
| 10 |
+
|
| 11 |
+
<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
|
| 12 |
+
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
|
| 13 |
+
|
API.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AILIXIR Architecture & Component Documentation
|
| 2 |
+
|
| 3 |
+
Last updated: 2026-05-29
|
| 4 |
+
|
| 5 |
+
This document provides detailed diagrams and per-component explanations for the AILIXIR BackEnd. It is intended for developers and SREs onboarding onto the project and for inclusion in production runbooks.
|
| 6 |
+
|
| 7 |
+
Contents
|
| 8 |
+
--------
|
| 9 |
+
- System-level architecture (diagram + explanation)
|
| 10 |
+
- Data flow and request lifecycle
|
| 11 |
+
- Per-service component diagrams and responsibilities
|
| 12 |
+
- Laravel (orchestration)
|
| 13 |
+
- ADMET inference (FastAPI)
|
| 14 |
+
- Drug Repurposing (FastAPI)
|
| 15 |
+
- Chemical RAG (FAISS + RAG)
|
| 16 |
+
- Environment variables & configuration locations
|
| 17 |
+
- Deployment notes and scaling considerations
|
| 18 |
+
- Troubleshooting checkpoints
|
| 19 |
+
|
| 20 |
+
System-level architecture (Mermaid)
|
| 21 |
+
----------------------------------
|
| 22 |
+
|
| 23 |
+
```mermaid
|
| 24 |
+
flowchart LR
|
| 25 |
+
subgraph CLIENTS [Clients]
|
| 26 |
+
A[Web UI / Frontend]
|
| 27 |
+
B[External Integrations / APIs]
|
| 28 |
+
end
|
| 29 |
+
|
| 30 |
+
subgraph DOCKER_NET [Docker network: ailixir]
|
| 31 |
+
direction TB
|
| 32 |
+
LAR(Laravel API)
|
| 33 |
+
Q(Queue Worker)
|
| 34 |
+
MYSQL[(MariaDB)]
|
| 35 |
+
ADMET(ADMET Inference)
|
| 36 |
+
DRUG(Drug Repurposing)
|
| 37 |
+
CRAG(Chemical RAG)
|
| 38 |
+
end
|
| 39 |
+
|
| 40 |
+
A -->|HTTP| LAR
|
| 41 |
+
B -->|HTTP| LAR
|
| 42 |
+
LAR -->|DB| MYSQL
|
| 43 |
+
LAR -->|enqueue| Q
|
| 44 |
+
LAR -->|AI calls| ADMET
|
| 45 |
+
LAR -->|AI calls| DRUG
|
| 46 |
+
LAR -->|AI calls| CRAG
|
| 47 |
+
ADMET -->|models stored on disk| ADMET
|
| 48 |
+
CRAG -->|FAISS index on volume| CRAG
|
| 49 |
+
DRUG -->|model artifacts| DRUG
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
Explanation
|
| 53 |
+
-----------
|
| 54 |
+
|
| 55 |
+
- Clients (web, CLI, external services) interact with the `laravel` orchestration API. The Laravel layer handles authentication, request validation, and orchestration of multi-step workflows.
|
| 56 |
+
- Long-running tasks are delegated to the `queue` worker via Laravel's queue system. The queue worker communicates with the same Laravel codebase and uses the internal database for job bookkeeping.
|
| 57 |
+
- AI microservices are independent FastAPI services: `admet`, `drug-repurposing`, and `chemical-rag`. Laravel calls these services over the internal Docker network by service name.
|
| 58 |
+
- Persistent storage: `mysql` holds application state, users, and job metadata; AI services use container volumes for model artifacts and FAISS indexes.
|
| 59 |
+
|
| 60 |
+
Request lifecycle — typical screening flow
|
| 61 |
+
---------------------------------------
|
| 62 |
+
|
| 63 |
+
1. Client POSTs request to Laravel endpoint (e.g., run screening).
|
| 64 |
+
2. Laravel validates and creates a job record in MySQL and either performs synchronous work or enqueues a background job.
|
| 65 |
+
3. Background job triggers calls to AI services in parallel or sequence depending on pipeline stage:
|
| 66 |
+
- `drug-repurposing` to run virtual screening
|
| 67 |
+
- `admet` to score ADMET properties
|
| 68 |
+
- `chemical-rag` to fetch similar compounds or augment results
|
| 69 |
+
4. AI services return raw predictions to Laravel which aggregates results, persists summary, and notifies the client.
|
| 70 |
+
|
| 71 |
+
Per-service diagrams & responsibilities
|
| 72 |
+
--------------------------------------
|
| 73 |
+
|
| 74 |
+
1) Laravel (orchestration)
|
| 75 |
+
|
| 76 |
+
```mermaid
|
| 77 |
+
flowchart TD
|
| 78 |
+
In[HTTP Request]
|
| 79 |
+
Auth[Auth Layer]
|
| 80 |
+
Validate[Validation]
|
| 81 |
+
Controller[Controller]
|
| 82 |
+
DB[(MySQL)]
|
| 83 |
+
Queue[Queue Worker]
|
| 84 |
+
AIcalls[AI Service Calls]
|
| 85 |
+
Response[Response to Client]
|
| 86 |
+
|
| 87 |
+
In --> Auth --> Validate --> Controller --> DB
|
| 88 |
+
Controller -->|enqueue| Queue
|
| 89 |
+
Controller -->|direct call| AIcalls
|
| 90 |
+
AIcalls --> Response
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
Responsibilities:
|
| 94 |
+
|
| 95 |
+
- Authentication and authorization (API tokens, OAuth, or guards configured in `config/auth.php`).
|
| 96 |
+
- Centralized error handling and logging.
|
| 97 |
+
- Exposes REST endpoints used by UI and third-party clients; routes live under `routes/`.
|
| 98 |
+
- Job dispatching and scheduling; queue worker executes heavy pipelines.
|
| 99 |
+
|
| 100 |
+
Where to look in repository:
|
| 101 |
+
|
| 102 |
+
- `app/Http/Controllers/` — controllers and endpoints
|
| 103 |
+
- `routes/api.php` — API routes and versioning
|
| 104 |
+
- `jobs/` and `app/Jobs` — background job implementations
|
| 105 |
+
- `docker/laravel.env` — example env values
|
| 106 |
+
|
| 107 |
+
2) ADMET inference (FastAPI)
|
| 108 |
+
|
| 109 |
+
```mermaid
|
| 110 |
+
flowchart TD
|
| 111 |
+
Req[POST /predict]
|
| 112 |
+
Validate[SMILES Validation (RDKit)]
|
| 113 |
+
Feat[Featurization / Graph builder]
|
| 114 |
+
Models[MPNN Models (5 tasks)]
|
| 115 |
+
Aggregate[Aggregate predictions]
|
| 116 |
+
Return[JSON response]
|
| 117 |
+
|
| 118 |
+
Req --> Validate --> Feat --> Models --> Aggregate --> Return
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Responsibilities:
|
| 122 |
+
|
| 123 |
+
- Load pre-trained MPNN models on startup (check `models/` inside service).
|
| 124 |
+
- Validate SMILES strings with RDKit and sanitize input.
|
| 125 |
+
- Support single and batch prediction endpoints; expose `/health` and `/docs`.
|
| 126 |
+
- CPU-optimized inference (no GPU assumed by default) and async handling via Uvicorn.
|
| 127 |
+
|
| 128 |
+
Where to look:
|
| 129 |
+
|
| 130 |
+
- `ai_apps/ADMIT/admet_inference/` — service code, `README.md`, `Dockerfile`, `requirements.txt`.
|
| 131 |
+
- Health and docs endpoints available at `http://<host>:<port>/health` and `/docs`.
|
| 132 |
+
|
| 133 |
+
3) Drug Repurposing (FastAPI)
|
| 134 |
+
|
| 135 |
+
```mermaid
|
| 136 |
+
flowchart TD
|
| 137 |
+
Req[POST /api/v1/screen]
|
| 138 |
+
Targets[OpenTargets integration]
|
| 139 |
+
Sequences[UniProt fetch]
|
| 140 |
+
Drugs[Drug library (TDC / fallback)]
|
| 141 |
+
Model[DeepPurpose MPNN-CNN]
|
| 142 |
+
Results[Rank & postprocess]
|
| 143 |
+
Return[JSON summary]
|
| 144 |
+
|
| 145 |
+
Req --> Targets --> Sequences --> Drugs --> Model --> Results --> Return
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
Responsibilities:
|
| 149 |
+
|
| 150 |
+
- Full screening pipeline: disease→targets→proteins→screening→results.
|
| 151 |
+
- Integrates external APIs (OpenTargets, UniProt) and TDC for drug libraries.
|
| 152 |
+
- Provides mock modes for offline testing (use env `USE_MOCK_MODEL` and `USE_MOCK_DRUGS`).
|
| 153 |
+
|
| 154 |
+
Where to look:
|
| 155 |
+
|
| 156 |
+
- `ai_apps/Drug Reporposing/` — service implementation, `docker/` folder, `requirements.txt`, `README.md` and helper scripts (`start.sh`, `start.bat`).
|
| 157 |
+
|
| 158 |
+
4) Chemical RAG (FAISS + LLM)
|
| 159 |
+
|
| 160 |
+
```mermaid
|
| 161 |
+
flowchart TD
|
| 162 |
+
Req[/search/full-rag]
|
| 163 |
+
Validate[SMILES validation]
|
| 164 |
+
FP[Morgan fingerprint / embedding]
|
| 165 |
+
FAISS[FAISS-IVF index lookup]
|
| 166 |
+
LLM[LLM explanation (optional)]
|
| 167 |
+
Format[Result formatting]
|
| 168 |
+
Return[JSON results]
|
| 169 |
+
|
| 170 |
+
Req --> Validate --> FP --> FAISS --> LLM --> Format --> Return
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
Responsibilities:
|
| 174 |
+
|
| 175 |
+
- High-performance similarity search for large compound libraries (1M+).
|
| 176 |
+
- Persistent FAISS index stored on volume (`chemical-rag-data`).
|
| 177 |
+
- Two endpoints: retrieval-only and full RAG (with LLM explanations); auto-detects and ingests compounds on first run.
|
| 178 |
+
|
| 179 |
+
Where to look:
|
| 180 |
+
|
| 181 |
+
- `ai_apps/chemical-rag-system/chemical-rag-system/` — engine, ingestion scripts, FAISS index code, `run_server.py`, and `README.md`.
|
| 182 |
+
|
| 183 |
+
Environment variables & configuration locations
|
| 184 |
+
---------------------------------------------
|
| 185 |
+
|
| 186 |
+
- Root compose: `docker-compose.yml` — service names, build contexts, ports, and env files.
|
| 187 |
+
- Laravel env template: `docker/laravel.env` — application and DB defaults.
|
| 188 |
+
- Each AI service: check `ai_apps/<service>/` for `requirements.txt`, `Dockerfile`, and service-specific config (e.g., `app/config.py`).
|
| 189 |
+
|
| 190 |
+
Production deployment notes
|
| 191 |
+
-------------------------
|
| 192 |
+
|
| 193 |
+
- Resource limits: `docker-compose.yml` contains `deploy.resources.limits` for AI services; tune these for production (memory, CPU).
|
| 194 |
+
- Secrets: move sensitive env values into secret managers (Kubernetes Secrets, Vault) in production.
|
| 195 |
+
- HTTPS: terminate TLS at a reverse proxy in front of `laravel` (Nginx/Load Balancer) and restrict internal-only exposure for AI services.
|
| 196 |
+
|
| 197 |
+
Scaling considerations
|
| 198 |
+
---------------------
|
| 199 |
+
|
| 200 |
+
- Horizontal scaling of AI services: deploy multiple replicas behind a load balancer. Ensure models are loaded on container start and center model size is within available RAM.
|
| 201 |
+
- FAISS index: store on a shared volume or use a dedicated retrieval service with replicated indices.
|
| 202 |
+
- Database: use managed RDS/MariaDB clusters or replicas for HA.
|
| 203 |
+
|
| 204 |
+
Troubleshooting checkpoints
|
| 205 |
+
--------------------------
|
| 206 |
+
|
| 207 |
+
- Service not healthy: `docker compose logs -f <service>` and `docker compose ps` to check ports and status.
|
| 208 |
+
- Model load errors: inspect service logs, confirm model files under `models/` or configured model path.
|
| 209 |
+
- FAISS ingestion slow: watch `chemical-rag` logs on first-run ingestion; ensure sufficient CPU and disk I/O.
|
| 210 |
+
|
| 211 |
+
Appendix — quick visual references
|
| 212 |
+
---------------------------------
|
| 213 |
+
|
| 214 |
+
- Root system diagram: see top of this file (Mermaid block).
|
| 215 |
+
- Per-service diagrams: each service section above contains its own Mermaid block.
|
| 216 |
+
|
| 217 |
+
Next steps I can implement for you
|
| 218 |
+
----------------------------------
|
| 219 |
+
|
| 220 |
+
1. Generate PNG/SVG exports of these Mermaid diagrams and add them to the repo docs folder.
|
| 221 |
+
2. Standardize and replace `ai_apps/*/README.md` with a consistent template including: purpose, architecture diagram, setup (Docker + local), env vars, run commands, API endpoints, troubleshooting.
|
| 222 |
+
3. Create `docs/` folder and add `QUICK_START.md`, per-service `.env.example`, and the generated diagrams.
|
| 223 |
+
|
| 224 |
+
Tell me which of the next steps you'd like me to do now (1, 2, 3), or if you want all of them performed.
|
CHANGELOG_SQLITE_HF.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changelog: SQLite Migration & Hugging Face Spaces Deployment
|
| 2 |
+
|
| 3 |
+
> **Date:** 2026-06-09
|
| 4 |
+
> **Branch:** `feature/hugging-face-deploy`
|
| 5 |
+
> **Commits:** `fd94aa0..1cbbcaa` (3 commits)
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Overview
|
| 10 |
+
|
| 11 |
+
Migrated AILIXIR from **MySQL/MariaDB** to **SQLite** and deployed the entire platform to **5 Hugging Face Spaces** with GitHub-to-HF auto-sync.
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## Part 1: SQLite Database Migration
|
| 16 |
+
|
| 17 |
+
### Migration Fixes
|
| 18 |
+
|
| 19 |
+
**4 migration files were modified** to remove MySQL-specific syntax:
|
| 20 |
+
|
| 21 |
+
| File | Problem | Fix |
|
| 22 |
+
|------|---------|-----|
|
| 23 |
+
| `database/migrations/2026_04_20_135553_create_admets_table.php` | `useCurrentOnUpdate()` — MySQL-only | Replaced manual timestamps with `$table->timestamps()` |
|
| 24 |
+
| `database/migrations/2026_04_20_044726_create_screening_results_table.php` | `output` column not nullable, causing `->change()` failure | Changed `json('output')` to `json('output')->nullable()` |
|
| 25 |
+
| `database/migrations/2026_04_20_055149_create_target_lookups_table.php` | Same as above | Same fix |
|
| 26 |
+
| `database/migrations/2026_04_21_155344_add_status_to_screening_tables.php` | Used `->change()` which requires MySQL-like `ALTER TABLE MODIFY` | Rewritten to only add `status` column, removed all `->change()` calls |
|
| 27 |
+
|
| 28 |
+
### Configuration Changes
|
| 29 |
+
|
| 30 |
+
- **`config/database.php`** — SQLite connection configured with:
|
| 31 |
+
- `journal_mode` = `wal` (Write-Ahead Logging for concurrent reads)
|
| 32 |
+
- `busy_timeout` = `5000` (5 second wait on locked DB)
|
| 33 |
+
- `transaction_mode` = `IMMEDIATE` (prevents "database is locked" errors)
|
| 34 |
+
|
| 35 |
+
### Environment Files Updated
|
| 36 |
+
|
| 37 |
+
| File | Before | After |
|
| 38 |
+
|------|--------|-------|
|
| 39 |
+
| `.env.example` | `DB_CONNECTION=mysql` + host/port/user/pass | `DB_CONNECTION=sqlite`, `DB_DATABASE=database/ailixir.sqlite` |
|
| 40 |
+
| `.env.docker` | Same MySQL config | Same SQLite config |
|
| 41 |
+
| `docker/laravel.env` | Same MySQL config | Same SQLite config |
|
| 42 |
+
|
| 43 |
+
### `.gitignore`
|
| 44 |
+
Added `*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm` to prevent accidental commits of database files.
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## Part 2: Docker Infrastructure Changes
|
| 49 |
+
|
| 50 |
+
### `Dockerfile`
|
| 51 |
+
- **PHP extension**: `pdo_mysql` → `pdo_sqlite`
|
| 52 |
+
- **System packages**: Added `libsqlite3-dev`, `supervisor`
|
| 53 |
+
- **Ports**: Added `EXPOSE 7860` for Hugging Face Spaces
|
| 54 |
+
- **Database directory**: Creates `database/` directory during build
|
| 55 |
+
- Removed `COPY docker/laravel.env .env` (env vars now come from HF Space secrets)
|
| 56 |
+
|
| 57 |
+
### `docker/supervisord.conf` (new)
|
| 58 |
+
Runs two processes in a single container for HF Spaces:
|
| 59 |
+
- `[program:laravel]` — `php artisan serve --host=0.0.0.0 --port=7860`
|
| 60 |
+
- `[program:queue]` — `php artisan queue:work --sleep=3 --tries=3 --timeout=300`
|
| 61 |
+
|
| 62 |
+
### `docker/entrypoint.sh`
|
| 63 |
+
- Creates SQLite database file at runtime if it doesn't exist
|
| 64 |
+
- Checks `RUN_MODE=hf` environment variable — if set, starts supervisord instead of the default CMD
|
| 65 |
+
- Still supports Docker Compose mode (runs migrations, then exec's the passed CMD)
|
| 66 |
+
|
| 67 |
+
### `docker-compose.yml`
|
| 68 |
+
- **Removed** the entire `mysql:` service (was MariaDB 11.4)
|
| 69 |
+
- **Removed** `mysql-data` volume
|
| 70 |
+
- **Removed** `depends_on: mysql` from `laravel` and `queue` services
|
| 71 |
+
- **Removed** `DB_HOST: mysql` environment overrides
|
| 72 |
+
- AI microservice configs remain unchanged
|
| 73 |
+
|
| 74 |
+
### `.env.hf` (new)
|
| 75 |
+
Template file with all HF Space URLs pre-configured for the AI microservice URLs.
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
## Part 3: Hugging Face Spaces Deployment
|
| 80 |
+
|
| 81 |
+
### Spaces Created (5)
|
| 82 |
+
|
| 83 |
+
| Space | Type | Content |
|
| 84 |
+
|-------|------|---------|
|
| 85 |
+
| `ailixir-api` | Docker (Laravel) | Root repo (excludes `ai_apps/`, `Diagrams/`) |
|
| 86 |
+
| `ailixir-admet` | Docker (FastAPI) | `ai_apps/ADMIT/admet_inference/` |
|
| 87 |
+
| `ailixir-drug-repurposing` | Docker (FastAPI) | `ai_apps/Drug Reporposing/` |
|
| 88 |
+
| `ailixir-chemical-rag` | Docker (FastAPI) | `ai_apps/chemical-rag-system/chemical-rag-system/` |
|
| 89 |
+
| `ailixir-generation` | Docker (FastAPI) | `ai_apps/generation/` (CPU-only Dockerfile) |
|
| 90 |
+
|
| 91 |
+
### Space Configuration
|
| 92 |
+
- All spaces use `cpu-basic` hardware (free tier)
|
| 93 |
+
- Port configured to **7860** in each Dockerfile
|
| 94 |
+
- `README.md` with proper YAML frontmatter added to each space
|
| 95 |
+
- 15-minute sleep timeout for free tier cost savings
|
| 96 |
+
|
| 97 |
+
### Generation Service Adaptation
|
| 98 |
+
The generation service originally required NVIDIA CUDA + GPU. For HF Spaces (CPU-only), a **lightweight Dockerfile** was created that:
|
| 99 |
+
- Uses `python:3.10-slim` base (instead of `nvidia/cuda`)
|
| 100 |
+
- Installs only `fastapi`, `uvicorn`, `pydantic`, `requests`, `rdkit`
|
| 101 |
+
- Disables AutoDock-GPU and DeepPurpose (CPU-only)
|
| 102 |
+
- Provides basic molecule generation and docking scoring
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## Part 4: GitHub-to-HF Auto-Sync
|
| 107 |
+
|
| 108 |
+
### 5 GitHub Actions Workflows
|
| 109 |
+
|
| 110 |
+
| File | Triggers | Syncs |
|
| 111 |
+
|------|----------|-------|
|
| 112 |
+
| `.github/workflows/sync-hf-backend.yml` | Push to `main`/`develop` (excl. `ai_apps/`) | Whole repo → `ailixir-api` |
|
| 113 |
+
| `.github/workflows/sync-hf-admet.yml` | Push to `ai_apps/ADMIT/admet_inference/` | Subdir → `ailixir-admet` |
|
| 114 |
+
| `.github/workflows/sync-hf-drug-repurposing.yml` | Push to `ai_apps/Drug Reporposing/` | Subdir → `ailixir-drug-repurposing` |
|
| 115 |
+
| `.github/workflows/sync-hf-chemical-rag.yml` | Push to `ai_apps/chemical-rag-system/` | Subdir → `ailixir-chemical-rag` |
|
| 116 |
+
| `.github/workflows/sync-hf-generation.yml` | Push to `ai_apps/generation/` | Subdir → `ailixir-generation` |
|
| 117 |
+
|
| 118 |
+
Each workflow:
|
| 119 |
+
1. Clones the target HF Space repo
|
| 120 |
+
2. Rsyncs the relevant files (excluding `.git`, `__pycache__`, etc.)
|
| 121 |
+
3. Commits with the GitHub commit SHA
|
| 122 |
+
4. Pushes to HF Space
|
| 123 |
+
|
| 124 |
+
### GitHub Actions Workflow Updates
|
| 125 |
+
- **`api-tests.yml`** — Removed MySQL wait loop and health check; removed MySQL log collection
|
| 126 |
+
- **`integration-test.yml`** — Same changes across all 4 job definitions
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## Part 5: Files Modified/Created Summary
|
| 131 |
+
|
| 132 |
+
### Modified (15 files)
|
| 133 |
+
```
|
| 134 |
+
.config/database.php
|
| 135 |
+
.env.docker
|
| 136 |
+
.env.example
|
| 137 |
+
.gitignore
|
| 138 |
+
Dockerfile
|
| 139 |
+
docker-compose.yml
|
| 140 |
+
docker/entrypoint.sh
|
| 141 |
+
docker/laravel.env
|
| 142 |
+
.github/workflows/api-tests.yml
|
| 143 |
+
.github/workflows/integration-test.yml
|
| 144 |
+
database/migrations/2026_04_20_044726_create_screening_results_table.php
|
| 145 |
+
database/migrations/2026_04_20_055149_create_target_lookups_table.php
|
| 146 |
+
database/migrations/2026_04_20_135553_create_admets_table.php
|
| 147 |
+
database/migrations/2026_04_21_155344_add_status_to_screening_tables.php
|
| 148 |
+
database/ailixir.sqlite (new, empty)
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### Created (9 files)
|
| 152 |
+
```
|
| 153 |
+
.env.hf
|
| 154 |
+
docker/supervisord.conf
|
| 155 |
+
.github/workflows/sync-hf-backend.yml
|
| 156 |
+
.github/workflows/sync-hf-admet.yml
|
| 157 |
+
.github/workflows/sync-hf-drug-repurposing.yml
|
| 158 |
+
.github/workflows/sync-hf-chemical-rag.yml
|
| 159 |
+
.github/workflows/sync-hf-generation.yml
|
| 160 |
+
HF_SPACES_GUIDE.md (this file)
|
| 161 |
+
CHANGELOG_SQLITE_HF.md (this file)
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
## Architecture Diagram
|
| 167 |
+
|
| 168 |
+
```
|
| 169 |
+
┌─────────────────────────────────────────────────────────┐
|
| 170 |
+
│ GitHub Repository │
|
| 171 |
+
│ RottenShadow/ │
|
| 172 |
+
│ AILIXIR_BackEnd │
|
| 173 |
+
└────────────────────┬────────────────────────────────────┘
|
| 174 |
+
│ Push triggers
|
| 175 |
+
▼
|
| 176 |
+
┌─────────────────────────────┐
|
| 177 |
+
│ GitHub Actions (5 WFs) │
|
| 178 |
+
│ Auto-sync to HF Spaces │
|
| 179 |
+
└─────────────┬───────────────┘
|
| 180 |
+
│
|
| 181 |
+
┌───────────────┼───────────────────────┐
|
| 182 |
+
│ │ │
|
| 183 |
+
▼ ▼ ▼
|
| 184 |
+
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
| 185 |
+
│ailixir- │ │ailixir- │ │ailixir- │ │ailixir- │ │ailixir- │
|
| 186 |
+
│api │ │admet │ │drug- │ │chemical- │ │generation│
|
| 187 |
+
│(Laravel) │ │(ADMET) │ │repurpos. │ │rag │ │(Molec. │
|
| 188 |
+
│SQLite │ │ │ │ │ │ │ │Gen.) │
|
| 189 |
+
└────┬─────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
|
| 190 |
+
│
|
| 191 |
+
│ HTTP proxying to AI microservices
|
| 192 |
+
├──→ https://shdwRow-ailixir-admet.hf.space
|
| 193 |
+
├──→ https://RottenShadow-ailixir-drug-repurposing.hf.space
|
| 194 |
+
├──→ https://RottenShadow-ailixir-chemical-rag.hf.space
|
| 195 |
+
└──→ https://shdwRow-ailixir-generation.hf.space
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
## Remaining Steps (Manual)
|
| 201 |
+
|
| 202 |
+
1. **Set HF tokens as GitHub secrets** — Required for auto-sync workflows
|
| 203 |
+
- Go to: https://github.com/RottenShadow/AILIXIR_BackEnd/settings/secrets/actions
|
| 204 |
+
- Add secret: `HF_TOKEN` = `YOUR_ROTTENSHADOW_TOKEN`
|
| 205 |
+
- Add secret: `HF_TOKEN_AILIXIR` = `YOUR_AILIXIR_AI_TEAM_TOKEN`
|
| 206 |
+
|
| 207 |
+
2. **Set backend Space secrets** — Required for `ailixir-api` to function
|
| 208 |
+
- Go to: https://huggingface.co/spaces/Ailixir-AI-Team/ailixir-api/settings
|
| 209 |
+
- Add all secrets listed in `HF_SPACES_GUIDE.md` section 3.1
|
| 210 |
+
- **Minimum:** `APP_KEY`, `RUN_MODE=hf`, and all AI service URLs
|
| 211 |
+
|
| 212 |
+
3. **Wait for builds to complete** — Backend Docker image is large (~5GB) and takes 15–45 minutes to build on HF free tier
|
| 213 |
+
|
| 214 |
+
4. **Verify connectivity** — Test the health endpoints and ensure the backend can reach all AI services
|
DOCKER.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AILIXIR Docker Stack
|
| 2 |
+
|
| 3 |
+
Monorepo stack: **Laravel API** + **ADMET** + **Drug Repurposing** + **Chemical RAG**.
|
| 4 |
+
|
| 5 |
+
## Requirements
|
| 6 |
+
|
| 7 |
+
- Docker 24+ with Compose v2
|
| 8 |
+
- ~16 GB RAM recommended (PyTorch + ADMET models)
|
| 9 |
+
- Model artifacts present locally:
|
| 10 |
+
- `ai_apps/ADMIT/admet_inference/models/*/best_model.ckpt`
|
| 11 |
+
- `ai_apps/Drug Reporposing/save_folder/pretrained_models/...`
|
| 12 |
+
|
| 13 |
+
## Quick start
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
# From repository root
|
| 17 |
+
docker compose build
|
| 18 |
+
docker compose up -d
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
| Service | Host URL |
|
| 22 |
+
|---------|----------|
|
| 23 |
+
| Laravel API | http://localhost:8080 |
|
| 24 |
+
| ADMET | http://localhost:8002 |
|
| 25 |
+
| Drug Repurposing | http://localhost:8001 |
|
| 26 |
+
| Chemical RAG | http://localhost:5000 |
|
| 27 |
+
|
| 28 |
+
## Laravel → AI integration endpoints
|
| 29 |
+
|
| 30 |
+
Enabled when `AI_INTEGRATION_ROUTES_ENABLED=true` (set in `docker/laravel.env`).
|
| 31 |
+
|
| 32 |
+
| Method | Endpoint | Description |
|
| 33 |
+
|--------|----------|-------------|
|
| 34 |
+
| GET | `/api/ai-services/health` | Ping all AI `/health` endpoints |
|
| 35 |
+
| POST | `/api/ai-services/test/admet` | Proxy ADMET `/predict` |
|
| 36 |
+
| POST | `/api/ai-services/test/chemical-search` | Proxy Chemical RAG retrieval |
|
| 37 |
+
| GET | `/api/ai-services/test/drug-repurposing` | Health + model status |
|
| 38 |
+
|
| 39 |
+
Example:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
curl http://localhost:8080/api/ai-services/health
|
| 43 |
+
curl -X POST http://localhost:8080/api/ai-services/test/admet \
|
| 44 |
+
-H "Content-Type: application/json" \
|
| 45 |
+
-d '{"smiles":"c1ccccc1"}'
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## Images
|
| 49 |
+
|
| 50 |
+
- **Laravel**: multi-stage (`composer` + `node` + `php:8.3-cli-alpine`), runs as `www-data`
|
| 51 |
+
- **ADMET / Drug Repurposing**: existing multi-stage Python slim images, non-root UID 1000
|
| 52 |
+
- **Chemical RAG**: multi-stage `python:3.11-slim`, non-root `raguser`
|
| 53 |
+
- **MySQL**: `mariadb:11.4`
|
| 54 |
+
|
| 55 |
+
## CI
|
| 56 |
+
|
| 57 |
+
GitHub Actions workflow: `.github/workflows/integration-test.yml`
|
| 58 |
+
|
| 59 |
+
Builds the full stack and tests Laravel proxy endpoints against all AI services.
|
| 60 |
+
|
| 61 |
+
## Environment
|
| 62 |
+
|
| 63 |
+
Copy and adjust `docker/laravel.env` for container-specific values. For local (non-Docker) development, set URLs in `.env` using `.env.example` as reference.
|
DOCKER_FIXES.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔧 Docker Build Fixes - AILIXIR v2.0
|
| 2 |
+
|
| 3 |
+
**Last Updated**: 2026 | **Status**: ✅ All Issues Resolved
|
| 4 |
+
|
| 5 |
+
This document outlines the Docker build failures discovered in CI/CD and the solutions applied.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 🚨 Issues Resolved
|
| 10 |
+
|
| 11 |
+
### Issue #1: PyTorch Version Incompatibility (CRITICAL)
|
| 12 |
+
|
| 13 |
+
#### Problem
|
| 14 |
+
```
|
| 15 |
+
ERROR: Could not find a version that satisfies the requirement
|
| 16 |
+
torchaudio==0.15.2
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
#### Root Cause
|
| 20 |
+
- **File**: `ai_apps/Drug Reporposing/requirements.txt`
|
| 21 |
+
- **Line**: 24 (originally specified `torchaudio==0.15.2`)
|
| 22 |
+
- **Issue**: `torchaudio==0.15.2` does not exist on PyPI
|
| 23 |
+
- **Version Gap**: PyPI contains versions 0.11.0 through 2.11.0, but the 0.15.x and 1.x ranges were never released
|
| 24 |
+
- **Incompatibility**: The version was inconsistent with torch==2.0.1 and torchvision==0.15.2
|
| 25 |
+
|
| 26 |
+
#### Solution
|
| 27 |
+
Updated to compatible PyTorch audio library version:
|
| 28 |
+
|
| 29 |
+
**Before:**
|
| 30 |
+
```txt
|
| 31 |
+
torch==2.0.1
|
| 32 |
+
torchvision==0.15.2
|
| 33 |
+
torchaudio==0.15.2 # ❌ Does not exist
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
**After:**
|
| 37 |
+
```txt
|
| 38 |
+
torch==2.0.1
|
| 39 |
+
torchvision==0.15.2
|
| 40 |
+
torchaudio==2.0.1 # ✅ Compatible with torch 2.0.1
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
#### Files Changed
|
| 44 |
+
- `ai_apps/Drug Reporposing/requirements.txt` (line 24)
|
| 45 |
+
|
| 46 |
+
#### Verification
|
| 47 |
+
```bash
|
| 48 |
+
# These versions are now available and compatible
|
| 49 |
+
pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.1
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
### Issue #2: Dockerfile Casing Warnings (STYLE)
|
| 55 |
+
|
| 56 |
+
#### Problem
|
| 57 |
+
```
|
| 58 |
+
FromAsCasing: 'as' and 'FROM' keywords' casing do not match
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
#### Root Cause
|
| 62 |
+
- Docker Dockerfile linter enforces consistent keyword casing
|
| 63 |
+
- Multi-stage build syntax uses `FROM ... as builder` (mixed case)
|
| 64 |
+
- Should be `FROM ... AS builder` (uppercase AS)
|
| 65 |
+
- **Affected Files**: 2 of 4 Dockerfiles had this warning
|
| 66 |
+
|
| 67 |
+
#### Solution
|
| 68 |
+
Updated all multi-stage Dockerfile build syntax to use uppercase `AS`:
|
| 69 |
+
|
| 70 |
+
**Before:**
|
| 71 |
+
```dockerfile
|
| 72 |
+
FROM python:3.10-slim as builder # ❌ Mixed case
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**After:**
|
| 76 |
+
```dockerfile
|
| 77 |
+
FROM python:3.10-slim AS builder # ✅ Proper casing
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
#### Files Changed
|
| 81 |
+
1. `ai_apps/ADMIT/admet_inference/Dockerfile` (line 4)
|
| 82 |
+
- Changed: `FROM python:3.11-slim as builder` → `FROM python:3.11-slim AS builder`
|
| 83 |
+
|
| 84 |
+
2. `ai_apps/Drug Reporposing/docker/Dockerfile` (line 1)
|
| 85 |
+
- Changed: `FROM python:3.10-slim as builder` → `FROM python:3.10-slim AS builder`
|
| 86 |
+
|
| 87 |
+
#### Status Note
|
| 88 |
+
- `ai_apps/chemical-rag-system/chemical-rag-system/Dockerfile` already had correct casing (`AS builder`)
|
| 89 |
+
- No changes were needed for this file
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 📊 Fix Summary
|
| 94 |
+
|
| 95 |
+
| Issue | File(s) | Change | Status |
|
| 96 |
+
|-------|---------|--------|--------|
|
| 97 |
+
| PyTorch incompatibility | Drug Reporposing/requirements.txt | torchaudio 0.15.2 → 2.0.1 | ✅ Fixed |
|
| 98 |
+
| Dockerfile casing | ADMET/Dockerfile | `as` → `AS` | ✅ Fixed |
|
| 99 |
+
| Dockerfile casing | Drug Repurposing/Dockerfile | `as` → `AS` | ✅ Fixed |
|
| 100 |
+
| Dockerfile casing | Chemical-RAG/Dockerfile | Already correct | ✅ OK |
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
## ✅ Verification Steps
|
| 105 |
+
|
| 106 |
+
After fixes, verify the Docker build succeeds:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# Clear Docker cache (optional)
|
| 110 |
+
docker compose down
|
| 111 |
+
docker system prune -a
|
| 112 |
+
|
| 113 |
+
# Build all services with fixes applied
|
| 114 |
+
cd AILIXIR_BackEnd
|
| 115 |
+
docker compose build --parallel
|
| 116 |
+
|
| 117 |
+
# Expected output:
|
| 118 |
+
# [+] Building 5/5
|
| 119 |
+
# - Image ailixir-admet Built
|
| 120 |
+
# - Image ailixir-drug-repurposing Built
|
| 121 |
+
# - Image ailixir-chemical-rag Built
|
| 122 |
+
# - Image ailixir-laravel Built
|
| 123 |
+
# - Image ailixir-queue Built
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
### Verify Individual Services
|
| 127 |
+
```bash
|
| 128 |
+
# Check each image built successfully
|
| 129 |
+
docker images | grep ailixir
|
| 130 |
+
|
| 131 |
+
# Test container startup
|
| 132 |
+
docker compose up -d
|
| 133 |
+
docker compose ps
|
| 134 |
+
# All services should show "Up" status ✅
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## 🔍 Technical Details
|
| 140 |
+
|
| 141 |
+
### PyTorch Version Compatibility Matrix
|
| 142 |
+
|
| 143 |
+
For Python 3.10 with CUDA-free (CPU) installation:
|
| 144 |
+
|
| 145 |
+
| torch | torchvision | torchaudio | Status |
|
| 146 |
+
|-------|-------------|-----------|--------|
|
| 147 |
+
| 2.0.1 | 0.15.2 | 0.15.2 | ❌ torchaudio doesn't exist |
|
| 148 |
+
| 2.0.1 | 0.15.2 | 2.0.1 | ✅ Valid (current) |
|
| 149 |
+
| 2.0.1 | 0.15.2 | 2.0.2 | ✅ Valid alternative |
|
| 150 |
+
| 1.13.1 | 0.14.1 | 0.13.1 | ✅ Legacy option |
|
| 151 |
+
|
| 152 |
+
**Selected**: torch 2.0.1 + torchaudio 2.0.1 (latest stable, best compatibility)
|
| 153 |
+
|
| 154 |
+
### Dockerfile Multi-Stage Build Syntax
|
| 155 |
+
|
| 156 |
+
The styleguide requires proper case:
|
| 157 |
+
|
| 158 |
+
```dockerfile
|
| 159 |
+
# Correct syntax
|
| 160 |
+
FROM <image> AS <name>
|
| 161 |
+
COPY --from=<name> /path /path
|
| 162 |
+
|
| 163 |
+
# Example (Drug Repurposing)
|
| 164 |
+
FROM python:3.10-slim AS builder
|
| 165 |
+
RUN apt-get update && pip install -r requirements.txt
|
| 166 |
+
FROM python:3.10-slim
|
| 167 |
+
COPY --from=builder /opt/venv /opt/venv
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## 📋 Checklist for Future Updates
|
| 173 |
+
|
| 174 |
+
When updating dependencies or Dockerfiles:
|
| 175 |
+
|
| 176 |
+
- [ ] Verify PyPI has the exact version (search on pypi.org)
|
| 177 |
+
- [ ] Check PyTorch compatibility: https://pytorch.org/get-started/locally/
|
| 178 |
+
- [ ] Test locally: `docker compose build --parallel`
|
| 179 |
+
- [ ] Check for Dockerfile linter warnings: `docker build --check`
|
| 180 |
+
- [ ] Run services: `docker compose up -d`
|
| 181 |
+
- [ ] Verify API endpoints respond: `curl http://localhost:8000`
|
| 182 |
+
|
| 183 |
+
---
|
| 184 |
+
|
| 185 |
+
## 🆘 Troubleshooting
|
| 186 |
+
|
| 187 |
+
### If Docker Build Still Fails
|
| 188 |
+
|
| 189 |
+
1. **Clear Docker cache**:
|
| 190 |
+
```bash
|
| 191 |
+
docker compose down
|
| 192 |
+
docker system prune -a -f
|
| 193 |
+
docker compose build --no-cache --parallel
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
2. **Check individual service logs**:
|
| 197 |
+
```bash
|
| 198 |
+
docker compose build admet 2>&1 | tail -50
|
| 199 |
+
docker compose build drug-repurposing 2>&1 | tail -50
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
3. **Verify file changes**:
|
| 203 |
+
```bash
|
| 204 |
+
# Check torchaudio version
|
| 205 |
+
grep -n "torchaudio" ai_apps/Drug\ Reporposing/requirements.txt
|
| 206 |
+
|
| 207 |
+
# Check Dockerfile casing
|
| 208 |
+
grep "FROM.*as\|FROM.*AS" ai_apps/*/*/Dockerfile ai_apps/*/*/Dockerfile
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
4. **Network/Registry Issues**:
|
| 212 |
+
- Ensure internet connectivity for PyPI, Docker Hub
|
| 213 |
+
- Try building at different time (registry mirrors may have delays)
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
## 📚 References
|
| 218 |
+
|
| 219 |
+
- PyPI Package Index: https://pypi.org/
|
| 220 |
+
- PyTorch Installation: https://pytorch.org/get-started/locally/
|
| 221 |
+
- Docker Best Practices: https://docs.docker.com/develop/dev-best-practices/
|
| 222 |
+
- Dockerfile Reference: https://docs.docker.com/engine/reference/builder/
|
| 223 |
+
|
| 224 |
+
---
|
| 225 |
+
|
| 226 |
+
## 🎯 Next Steps
|
| 227 |
+
|
| 228 |
+
- Monitor CI/CD pipeline for successful builds: `docker compose build --parallel`
|
| 229 |
+
- Add pre-commit hooks to validate Dockerfile syntax
|
| 230 |
+
- Document dependency update procedures
|
| 231 |
+
- Set up automated version compatibility checks
|
DOCUMENTATION_STRUCTURE.md
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AILIXIR Documentation Structure (Recommended)
|
| 2 |
+
|
| 3 |
+
**Status:** Documentation refactoring complete | **Last Updated:** May 2026
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 📋 Overview
|
| 8 |
+
|
| 9 |
+
The AILIXIR project has been refactored to follow modern open-source documentation standards:
|
| 10 |
+
|
| 11 |
+
- **Concise main README** (~800 lines, down from 2500+) focused on high-level overview
|
| 12 |
+
- **Dedicated documentation files** for specific topics
|
| 13 |
+
- **Service-specific READMEs** in each microservice directory
|
| 14 |
+
- **Clear navigation** with links between related documents
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 📁 Recommended Documentation File Structure
|
| 19 |
+
|
| 20 |
+
### Root Documentation Files
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
ailixir-backend/
|
| 24 |
+
├── README.md ✅ REFACTORED (concise, high-level)
|
| 25 |
+
├── QUICK_START.md 📝 REFERENCED (needs creation/verification)
|
| 26 |
+
├── ARCHITECTURE.md ✅ EXISTS (link from README)
|
| 27 |
+
├── DOCKER.md ✅ EXISTS (link from README)
|
| 28 |
+
├── PRODUCTION_GUIDE.md 📝 REFERENCED (needs creation/verification)
|
| 29 |
+
├── TROUBLESHOOTING.md 📝 REFERENCED (needs creation)
|
| 30 |
+
├── API_REFERENCE.md 📝 REFERENCED (needs creation)
|
| 31 |
+
├── CONTRIBUTING.md ✅ REFERENCED (guidelines included in README)
|
| 32 |
+
├── DOCKER_FIXES.md ✅ EXISTS (known issues)
|
| 33 |
+
└── LICENSE ✅ EXISTS
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### Service Documentation Files
|
| 37 |
+
|
| 38 |
+
```
|
| 39 |
+
ai_apps/
|
| 40 |
+
├── ADMIT/
|
| 41 |
+
│ ├── README.md ✅ UPDATED (training + inference)
|
| 42 |
+
│ └── train_ADMET_model.ipynb ✅ EXISTS
|
| 43 |
+
│
|
| 44 |
+
├── Drug Reporposing/
|
| 45 |
+
│ ├── README.md ✅ GOOD (keep as-is)
|
| 46 |
+
│ ├── QUICK_START.md ✅ EXISTS
|
| 47 |
+
│ ├── PRODUCTION_GUIDE.md ✅ EXISTS
|
| 48 |
+
│ └── IMPLEMENTATION_SUMMARY.md ✅ EXISTS
|
| 49 |
+
│
|
| 50 |
+
└── chemical-rag-system/
|
| 51 |
+
└── README.md ✅ UPDATED (v2.1)
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## 📄 Documentation Files Detail
|
| 57 |
+
|
| 58 |
+
### 1. **README.md** ✅ (Completed)
|
| 59 |
+
|
| 60 |
+
**Purpose:** Landing page, high-level overview
|
| 61 |
+
|
| 62 |
+
**Current Status:** ✅ Refactored and polished
|
| 63 |
+
|
| 64 |
+
**Length:** ~800 lines (reduced from 2500+)
|
| 65 |
+
|
| 66 |
+
**Content:**
|
| 67 |
+
- System overview and positioning
|
| 68 |
+
- Feature table
|
| 69 |
+
- Architecture diagram (ASCII, fixed alignment)
|
| 70 |
+
- Quick start (minimal, links to detailed guides)
|
| 71 |
+
- Services overview table
|
| 72 |
+
- API summary with examples
|
| 73 |
+
- Environment variables reference
|
| 74 |
+
- Project structure tree
|
| 75 |
+
- Links to all detailed documentation
|
| 76 |
+
|
| 77 |
+
**Strengths:**
|
| 78 |
+
- Professional and concise
|
| 79 |
+
- Clear navigation to other docs
|
| 80 |
+
- Suitable for GitHub landing page
|
| 81 |
+
- Impresses recruiters and contributors
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
### 2. **QUICK_START.md** 📝 (Reference, needs verification)
|
| 86 |
+
|
| 87 |
+
**Purpose:** 5-minute setup for developers
|
| 88 |
+
|
| 89 |
+
**Recommended Content:**
|
| 90 |
+
- System requirements (minimal)
|
| 91 |
+
- Docker setup (3 commands)
|
| 92 |
+
- Local setup (per service, brief)
|
| 93 |
+
- Health check verification
|
| 94 |
+
- "Next Steps" links to detailed docs
|
| 95 |
+
|
| 96 |
+
**File Structure:**
|
| 97 |
+
```markdown
|
| 98 |
+
# Quick Start
|
| 99 |
+
|
| 100 |
+
## Prerequisites
|
| 101 |
+
- Docker & Docker Compose v2+
|
| 102 |
+
- OR: PHP 8.2+, Python 3.10+, MariaDB
|
| 103 |
+
|
| 104 |
+
## Docker (5 minutes)
|
| 105 |
+
# 3-4 commands
|
| 106 |
+
|
| 107 |
+
## Local Development
|
| 108 |
+
# Brief per-service setup
|
| 109 |
+
|
| 110 |
+
## Verify Installation
|
| 111 |
+
# Health checks
|
| 112 |
+
|
| 113 |
+
## Next Steps
|
| 114 |
+
# Links to detailed guides
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
**Cross-references:** README.md → QUICK_START.md → DOCKER.md
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
### 3. **ARCHITECTURE.md** ✅ (Exists, referenced)
|
| 122 |
+
|
| 123 |
+
**Purpose:** System design, diagrams, component details
|
| 124 |
+
|
| 125 |
+
**Current Status:** ✅ Already comprehensive (use as-is)
|
| 126 |
+
|
| 127 |
+
**Content:**
|
| 128 |
+
- System-level architecture diagram (Mermaid)
|
| 129 |
+
- Component responsibilities table
|
| 130 |
+
- Data flow explanation
|
| 131 |
+
- Request lifecycle walkthrough
|
| 132 |
+
- Per-service component details
|
| 133 |
+
- Environment variables location guide
|
| 134 |
+
- Deployment notes and scaling
|
| 135 |
+
|
| 136 |
+
**Cross-references:** README.md ← ARCHITECTURE.md
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
### 4. **DOCKER.md** ✅ (Exists, referenced)
|
| 141 |
+
|
| 142 |
+
**Purpose:** Docker-specific setup and configuration
|
| 143 |
+
|
| 144 |
+
**Current Status:** ✅ Comprehensive (use as-is)
|
| 145 |
+
|
| 146 |
+
**Content:**
|
| 147 |
+
- Docker requirements
|
| 148 |
+
- Quick start with docker-compose
|
| 149 |
+
- Service URLs and ports table
|
| 150 |
+
- Laravel AI integration endpoints
|
| 151 |
+
- Docker image details
|
| 152 |
+
- CI/CD workflow
|
| 153 |
+
- Environment configuration
|
| 154 |
+
|
| 155 |
+
**Cross-references:** README.md → DOCKER.md
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
### 5. **PRODUCTION_GUIDE.md** 📝 (Reference, needs verification)
|
| 160 |
+
|
| 161 |
+
**Purpose:** Deployment architecture, scaling, monitoring, security
|
| 162 |
+
|
| 163 |
+
**Recommended Content:**
|
| 164 |
+
```markdown
|
| 165 |
+
# Production Guide
|
| 166 |
+
|
| 167 |
+
## Pre-Deployment Checklist
|
| 168 |
+
- Configuration review
|
| 169 |
+
- Security setup
|
| 170 |
+
- Backup strategy
|
| 171 |
+
- Monitoring setup
|
| 172 |
+
|
| 173 |
+
## Deployment Architecture
|
| 174 |
+
- Architecture diagram (Kubernetes vs Docker Swarm)
|
| 175 |
+
- Service replication strategies
|
| 176 |
+
- Load balancing setup
|
| 177 |
+
- Database replication
|
| 178 |
+
|
| 179 |
+
## Scaling Strategies
|
| 180 |
+
- Horizontal scaling for stateless services
|
| 181 |
+
- Database scaling and replication
|
| 182 |
+
- Queue worker scaling
|
| 183 |
+
- Memory and resource limits
|
| 184 |
+
|
| 185 |
+
## Monitoring & Observability
|
| 186 |
+
- Health check endpoints
|
| 187 |
+
- Log aggregation setup
|
| 188 |
+
- Metrics collection (Prometheus)
|
| 189 |
+
- Alert configuration
|
| 190 |
+
|
| 191 |
+
## Security Hardening
|
| 192 |
+
- Environment variable management
|
| 193 |
+
- HTTPS/TLS setup
|
| 194 |
+
- API authentication
|
| 195 |
+
- Database security
|
| 196 |
+
|
| 197 |
+
## Performance Tuning
|
| 198 |
+
- Connection pooling
|
| 199 |
+
- Cache configuration
|
| 200 |
+
- Query optimization
|
| 201 |
+
- Rate limiting
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
**Cross-references:** README.md → PRODUCTION_GUIDE.md
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
### 6. **TROUBLESHOOTING.md** 📝 (Reference, needs creation)
|
| 209 |
+
|
| 210 |
+
**Purpose:** Common issues and solutions
|
| 211 |
+
|
| 212 |
+
**Recommended Content:**
|
| 213 |
+
```markdown
|
| 214 |
+
# Troubleshooting
|
| 215 |
+
|
| 216 |
+
## Quick Reference Table
|
| 217 |
+
| Problem | Cause | Solution |
|
| 218 |
+
| ... |
|
| 219 |
+
|
| 220 |
+
## Container Issues
|
| 221 |
+
- Service won't start
|
| 222 |
+
- Port already in use
|
| 223 |
+
- Out of memory
|
| 224 |
+
|
| 225 |
+
## Database Issues
|
| 226 |
+
- Connection failures
|
| 227 |
+
- Migration errors
|
| 228 |
+
- Data persistence
|
| 229 |
+
|
| 230 |
+
## AI Service Issues
|
| 231 |
+
- Model loading errors
|
| 232 |
+
- GPU out of memory
|
| 233 |
+
- Timeout issues
|
| 234 |
+
- FAISS index slow
|
| 235 |
+
|
| 236 |
+
## Development Issues
|
| 237 |
+
- Python environment setup
|
| 238 |
+
- Dependency conflicts
|
| 239 |
+
- CUDA not found
|
| 240 |
+
- Permission errors
|
| 241 |
+
|
| 242 |
+
## Debugging
|
| 243 |
+
- How to check logs
|
| 244 |
+
- Health check procedures
|
| 245 |
+
- Debug mode configuration
|
| 246 |
+
|
| 247 |
+
## FAQ
|
| 248 |
+
- Common questions
|
| 249 |
+
- Best practices
|
| 250 |
+
- Performance optimization
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
**Cross-references:** README.md → TROUBLESHOOTING.md
|
| 254 |
+
|
| 255 |
+
---
|
| 256 |
+
|
| 257 |
+
### 7. **API_REFERENCE.md** 📝 (Reference, needs creation)
|
| 258 |
+
|
| 259 |
+
**Purpose:** Complete API endpoint documentation
|
| 260 |
+
|
| 261 |
+
**Recommended Content:**
|
| 262 |
+
```markdown
|
| 263 |
+
# API Reference
|
| 264 |
+
|
| 265 |
+
## Overview
|
| 266 |
+
- Base URLs
|
| 267 |
+
- Authentication
|
| 268 |
+
- Error handling
|
| 269 |
+
- Rate limiting
|
| 270 |
+
|
| 271 |
+
## Laravel Orchestration API
|
| 272 |
+
- Health endpoints
|
| 273 |
+
- AI service proxy endpoints
|
| 274 |
+
- Job management endpoints
|
| 275 |
+
- Result retrieval endpoints
|
| 276 |
+
|
| 277 |
+
## ADMET Service API
|
| 278 |
+
- Health endpoint
|
| 279 |
+
- Info endpoint
|
| 280 |
+
- Predict endpoints (single & batch)
|
| 281 |
+
- Model status endpoint
|
| 282 |
+
|
| 283 |
+
## Drug Repurposing Service API
|
| 284 |
+
- Disease targets endpoint
|
| 285 |
+
- Drug library endpoint
|
| 286 |
+
- Screening endpoint
|
| 287 |
+
- Model status endpoint
|
| 288 |
+
|
| 289 |
+
## Chemical RAG Service API
|
| 290 |
+
- Retrieval-only endpoint
|
| 291 |
+
- Full RAG endpoint
|
| 292 |
+
- Health endpoint
|
| 293 |
+
- Stats endpoint
|
| 294 |
+
|
| 295 |
+
## Response Formats
|
| 296 |
+
- Success responses
|
| 297 |
+
- Error responses
|
| 298 |
+
- Status codes
|
| 299 |
+
|
| 300 |
+
## Examples
|
| 301 |
+
- Real curl requests for each endpoint
|
| 302 |
+
- Request/response pairs
|
| 303 |
+
- Error scenarios
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
**Cross-references:** README.md → API_REFERENCE.md, Service READMEs
|
| 307 |
+
|
| 308 |
+
---
|
| 309 |
+
|
| 310 |
+
### 8. **CONTRIBUTING.md** ✅ (Referenced in README)
|
| 311 |
+
|
| 312 |
+
**Purpose:** Development guidelines
|
| 313 |
+
|
| 314 |
+
**Current Status:** ✅ Guidelines included in README.md
|
| 315 |
+
|
| 316 |
+
**Recommended Dedicated File:**
|
| 317 |
+
```markdown
|
| 318 |
+
# Contributing
|
| 319 |
+
|
| 320 |
+
## Getting Started
|
| 321 |
+
- Fork and clone
|
| 322 |
+
- Setup development environment
|
| 323 |
+
- Create feature branch
|
| 324 |
+
|
| 325 |
+
## Code Style
|
| 326 |
+
- PHP: PSR-12
|
| 327 |
+
- Python: PEP 8
|
| 328 |
+
- Commits: Conventional format
|
| 329 |
+
|
| 330 |
+
## Development Workflow
|
| 331 |
+
- Local development
|
| 332 |
+
- Testing requirements
|
| 333 |
+
- Documentation updates
|
| 334 |
+
- Pull request process
|
| 335 |
+
|
| 336 |
+
## Testing
|
| 337 |
+
- Unit tests
|
| 338 |
+
- Integration tests
|
| 339 |
+
- Running test suites
|
| 340 |
+
|
| 341 |
+
## Commit Messages
|
| 342 |
+
- Format guidelines
|
| 343 |
+
- Examples
|
| 344 |
+
|
| 345 |
+
## Pull Request Process
|
| 346 |
+
- Description template
|
| 347 |
+
- Code review expectations
|
| 348 |
+
- Merge requirements
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
---
|
| 352 |
+
|
| 353 |
+
## 🎯 Implementation Checklist
|
| 354 |
+
|
| 355 |
+
### Completed ✅
|
| 356 |
+
- [x] README.md refactored (concise, professional, links to docs)
|
| 357 |
+
- [x] ARCHITECTURE.md (exists, comprehensive)
|
| 358 |
+
- [x] DOCKER.md (exists, comprehensive)
|
| 359 |
+
- [x] ADMET/README.md (updated with full training + inference)
|
| 360 |
+
- [x] Chemical-RAG/README.md (updated with v2.1 focus)
|
| 361 |
+
- [x] Drug Reporposing/README.md (already excellent, kept as-is)
|
| 362 |
+
|
| 363 |
+
### Recommended Verification/Creation 📝
|
| 364 |
+
- [ ] QUICK_START.md - Verify or create based on template
|
| 365 |
+
- [ ] PRODUCTION_GUIDE.md - Create based on template
|
| 366 |
+
- [ ] TROUBLESHOOTING.md - Create based on template
|
| 367 |
+
- [ ] API_REFERENCE.md - Create based on template
|
| 368 |
+
- [ ] CONTRIBUTING.md - Extract to standalone file (optional)
|
| 369 |
+
|
| 370 |
+
---
|
| 371 |
+
|
| 372 |
+
## 📊 Documentation Standards Applied
|
| 373 |
+
|
| 374 |
+
### Format & Structure
|
| 375 |
+
✅ Consistent markdown formatting across all files
|
| 376 |
+
✅ Clear section hierarchy (H1-H4)
|
| 377 |
+
✅ Emoji headers for visual organization
|
| 378 |
+
✅ Markdown best practices (lists, tables, code blocks)
|
| 379 |
+
|
| 380 |
+
### Navigation
|
| 381 |
+
✅ Table of contents in root README
|
| 382 |
+
✅ Cross-references between related docs
|
| 383 |
+
✅ Clear "Next Steps" links at end of each section
|
| 384 |
+
✅ Service-specific READMEs with complete info
|
| 385 |
+
|
| 386 |
+
### Content Quality
|
| 387 |
+
✅ Professional tone (enterprise-grade)
|
| 388 |
+
✅ Concise main README with detailed docs
|
| 389 |
+
✅ Real examples (ports, commands, endpoints)
|
| 390 |
+
✅ Troubleshooting guides with solutions
|
| 391 |
+
|
| 392 |
+
### GitHub Readability
|
| 393 |
+
✅ Clean whitespace between sections
|
| 394 |
+
✅ Compact tables
|
| 395 |
+
✅ Reduced code block sizes
|
| 396 |
+
✅ Links instead of repetition
|
| 397 |
+
|
| 398 |
+
---
|
| 399 |
+
|
| 400 |
+
## 🎓 Best Practices References
|
| 401 |
+
|
| 402 |
+
This documentation structure follows conventions from:
|
| 403 |
+
- **Apache Software Foundation** - Modular documentation
|
| 404 |
+
- **Kubernetes** - Clear architecture docs with links
|
| 405 |
+
- **Popular open-source projects** - Concise main README with satellite docs
|
| 406 |
+
- **Enterprise software standards** - Professional, scalable approach
|
| 407 |
+
|
| 408 |
+
---
|
| 409 |
+
|
| 410 |
+
## 📋 File Status Summary
|
| 411 |
+
|
| 412 |
+
| File | Status | Purpose |
|
| 413 |
+
|------|--------|---------|
|
| 414 |
+
| README.md | ✅ Complete | High-level overview + navigation |
|
| 415 |
+
| ARCHITECTURE.md | ✅ Exists | System design details |
|
| 416 |
+
| DOCKER.md | ✅ Exists | Container setup |
|
| 417 |
+
| PRODUCTION_GUIDE.md | 📝 Recommended | Deployment & scaling |
|
| 418 |
+
| TROUBLESHOOTING.md | 📝 Recommended | Problem solving |
|
| 419 |
+
| API_REFERENCE.md | 📝 Recommended | Endpoint documentation |
|
| 420 |
+
| CONTRIBUTING.md | ✅ Included in README | Contributing guidelines |
|
| 421 |
+
| Service READMEs | ✅ All updated | Component-specific docs |
|
| 422 |
+
|
| 423 |
+
---
|
| 424 |
+
|
| 425 |
+
## 🚀 Next Steps
|
| 426 |
+
|
| 427 |
+
1. **Verify** QUICK_START.md exists and is up-to-date
|
| 428 |
+
2. **Create or verify** PRODUCTION_GUIDE.md using template
|
| 429 |
+
3. **Create** TROUBLESHOOTING.md from existing content
|
| 430 |
+
4. **Create** API_REFERENCE.md for clarity
|
| 431 |
+
5. **(Optional) Extract** CONTRIBUTING.md as standalone file
|
| 432 |
+
6. **Update** navigation links once all files are created
|
| 433 |
+
7. **Test** all links render correctly on GitHub
|
| 434 |
+
|
| 435 |
+
---
|
| 436 |
+
|
| 437 |
+
## 📞 Questions?
|
| 438 |
+
|
| 439 |
+
Reference this document when:
|
| 440 |
+
- Creating new documentation files
|
| 441 |
+
- Updating existing docs
|
| 442 |
+
- Organizing repository documentation
|
| 443 |
+
- Onboarding new contributors
|
| 444 |
+
|
| 445 |
+
**Maintain consistency:** Keep all docs at the same quality level and style for professional appearance.
|
| 446 |
+
|
| 447 |
+
---
|
| 448 |
+
|
| 449 |
+
**Last Updated:** May 2026 | **Status:** Documentation structure defined ✅
|
Dockerfile
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1
|
| 2 |
+
|
| 3 |
+
# ============================================================
|
| 4 |
+
# Stage 1: Composer dependencies
|
| 5 |
+
# ============================================================
|
| 6 |
+
FROM composer:2.8 AS vendor
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
RUN apk add --no-cache icu-dev libzip-dev oniguruma-dev \
|
| 11 |
+
&& docker-php-ext-install intl zip mbstring bcmath pcntl
|
| 12 |
+
|
| 13 |
+
# Add build argument for composer dev dependencies
|
| 14 |
+
|
| 15 |
+
COPY composer.json composer.lock ./
|
| 16 |
+
RUN composer install \
|
| 17 |
+
--no-interaction \
|
| 18 |
+
--no-scripts \
|
| 19 |
+
--prefer-dist \
|
| 20 |
+
--optimize-autoloader
|
| 21 |
+
|
| 22 |
+
COPY . .
|
| 23 |
+
RUN composer dump-autoload --optimize --classmap-authoritative
|
| 24 |
+
|
| 25 |
+
# ============================================================
|
| 26 |
+
# Stage 2: Frontend assets (Vite)
|
| 27 |
+
# ============================================================
|
| 28 |
+
FROM node:22-alpine AS assets
|
| 29 |
+
|
| 30 |
+
WORKDIR /app
|
| 31 |
+
|
| 32 |
+
COPY package.json package-lock.json ./
|
| 33 |
+
RUN npm ci
|
| 34 |
+
|
| 35 |
+
COPY vite.config.js ./
|
| 36 |
+
COPY resources ./resources
|
| 37 |
+
COPY public ./public
|
| 38 |
+
|
| 39 |
+
RUN npm run build
|
| 40 |
+
|
| 41 |
+
# ============================================================
|
| 42 |
+
# Stage 3: Runtime (PHP CLI on Debian — artisan serve)
|
| 43 |
+
# ============================================================
|
| 44 |
+
FROM php:8.3-cli AS runtime
|
| 45 |
+
|
| 46 |
+
RUN apt-get update && apt-get install -y \
|
| 47 |
+
libicu-dev \
|
| 48 |
+
libzip-dev \
|
| 49 |
+
libonig-dev \
|
| 50 |
+
libsqlite3-dev \
|
| 51 |
+
wget \
|
| 52 |
+
bzip2 \
|
| 53 |
+
ca-certificates \
|
| 54 |
+
openbabel \
|
| 55 |
+
supervisor \
|
| 56 |
+
&& docker-php-ext-install \
|
| 57 |
+
pdo_sqlite \
|
| 58 |
+
zip \
|
| 59 |
+
intl \
|
| 60 |
+
mbstring \
|
| 61 |
+
bcmath \
|
| 62 |
+
opcache \
|
| 63 |
+
pcntl \
|
| 64 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 65 |
+
|
| 66 |
+
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
|
| 67 |
+
|
| 68 |
+
# Install Miniconda
|
| 69 |
+
ENV CONDA_DIR=/opt/conda
|
| 70 |
+
|
| 71 |
+
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && \
|
| 72 |
+
/bin/bash /tmp/miniconda.sh -b -p $CONDA_DIR && \
|
| 73 |
+
rm /tmp/miniconda.sh
|
| 74 |
+
|
| 75 |
+
ENV PATH=$CONDA_DIR/bin:$PATH
|
| 76 |
+
|
| 77 |
+
# Accept Anaconda ToS (required in CI/Docker)
|
| 78 |
+
RUN conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main && \
|
| 79 |
+
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r
|
| 80 |
+
|
| 81 |
+
# Create environment
|
| 82 |
+
RUN conda create -y -p /var/www/html/vina_env -c conda-forge \
|
| 83 |
+
vina=1.2.5 \
|
| 84 |
+
python=3.10 \
|
| 85 |
+
pip
|
| 86 |
+
|
| 87 |
+
# Install RDKit via pip to bypass Boost C++ solver compatibility conflicts
|
| 88 |
+
RUN /var/www/html/vina_env/bin/pip install rdkit==2025.09.5
|
| 89 |
+
|
| 90 |
+
# Install testing and service dependencies from requirements.txt
|
| 91 |
+
COPY requirements.txt /tmp/requirements.txt
|
| 92 |
+
RUN /var/www/html/vina_env/bin/pip install -r /tmp/requirements.txt
|
| 93 |
+
|
| 94 |
+
ENV PATH=/var/www/html/vina_env/bin:$PATH
|
| 95 |
+
|
| 96 |
+
WORKDIR /var/www/html
|
| 97 |
+
|
| 98 |
+
COPY --from=vendor --chown=www-data:www-data /app /var/www/html
|
| 99 |
+
COPY --from=assets --chown=www-data:www-data /app/public/build /var/www/html/public/build
|
| 100 |
+
|
| 101 |
+
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
| 102 |
+
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
| 103 |
+
RUN chmod +x /usr/local/bin/entrypoint.sh \
|
| 104 |
+
&& mkdir -p storage/framework/{cache,sessions,views} storage/logs bootstrap/cache database \
|
| 105 |
+
&& chown -R www-data:www-data storage bootstrap/cache database /var/www/html/vina_env
|
| 106 |
+
|
| 107 |
+
USER www-data
|
| 108 |
+
|
| 109 |
+
EXPOSE 8000 7860
|
| 110 |
+
|
| 111 |
+
HEALTHCHECK --interval=15s --timeout=5s --start-period=40s --retries=5 \
|
| 112 |
+
CMD php -r "exit((int)(@file_get_contents('http://127.0.0.1:7860') === false));"
|
| 113 |
+
|
| 114 |
+
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
| 115 |
+
CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]
|
HF_SPACES_GUIDE.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AILIXIR Hugging Face Spaces Deployment Guide
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The AILIXIR platform is deployed across **5 Hugging Face Spaces**:
|
| 6 |
+
|
| 7 |
+
| Space | Purpose | URL |
|
| 8 |
+
|-------|---------|-----|
|
| 9 |
+
| `ailixir-api` | Backend API (Laravel + SQLite) | https://huggingface.co/spaces/Ailixir-AI-Team/ailixir-api |
|
| 10 |
+
| `ailixir-admet` | ADMET Prediction Service | https://huggingface.co/spaces/shdwRow/ailixir-admet |
|
| 11 |
+
| `ailixir-drug-repurposing` | Drug Repurposing Service | https://huggingface.co/spaces/RottenShadow/ailixir-drug-repurposing |
|
| 12 |
+
| `ailixir-chemical-rag` | Chemical RAG Service | https://huggingface.co/spaces/RottenShadow/ailixir-chemical-rag |
|
| 13 |
+
| `ailixir-generation` | Molecular Generation Service | https://huggingface.co/spaces/shdwRow/ailixir-generation |
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 1. Accessing the Spaces
|
| 18 |
+
|
| 19 |
+
Each Space builds a Docker container from its respective `Dockerfile`. Once built and running, the services are accessible at:
|
| 20 |
+
|
| 21 |
+
```
|
| 22 |
+
https://RottenShadow-{space-name}.hf.space
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
For example:
|
| 26 |
+
- **Backend API:** `https://Ailixir-AI-Team-ailixir-api.hf.space`
|
| 27 |
+
- **ADMET:** `https://shdwRow-ailixir-admet.hf.space`
|
| 28 |
+
- **Drug Repurposing:** `https://RottenShadow-ailixir-drug-repurposing.hf.space`
|
| 29 |
+
- **Chemical RAG:** `https://RottenShadow-ailixir-chemical-rag.hf.space`
|
| 30 |
+
- **Generation:** `https://shdwRow-ailixir-generation.hf.space`
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 2. Space Lifecycle
|
| 35 |
+
|
| 36 |
+
### Wake-up / Sleep
|
| 37 |
+
Spaces on the **free tier** (CPU Basic) go to sleep after **15 minutes of inactivity**. When you access a sleeping Space's URL, it will take **30–120 seconds** to wake up.
|
| 38 |
+
|
| 39 |
+
To keep a Space awake, you can:
|
| 40 |
+
- Use a **cron job** to ping the health endpoint every 10 minutes
|
| 41 |
+
- Upgrade to a **paid hardware tier** (CPU Upgrade or GPU)
|
| 42 |
+
- Set a **longer sleep timeout** in the Space settings
|
| 43 |
+
|
| 44 |
+
### Health Check
|
| 45 |
+
Each Space exposes a `/health` endpoint. You can check if a service is running:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
curl https://shdwRow-ailixir-admet.hf.space/health
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
Expected response: `{"status": "healthy", ...}`
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## 3. Environment Variables (Secrets)
|
| 56 |
+
|
| 57 |
+
Each Space requires certain environment variables. These are set as **Space Secrets** in the Hugging Face Space settings.
|
| 58 |
+
|
| 59 |
+
### 3.1 Backend Space: `ailixir-api`
|
| 60 |
+
|
| 61 |
+
These are **required** secrets — set them in:
|
| 62 |
+
https://huggingface.co/spaces/Ailixir-AI-Team/ailixir-api/settings
|
| 63 |
+
|
| 64 |
+
| Secret | Value | Description |
|
| 65 |
+
|--------|-------|-------------|
|
| 66 |
+
| `APP_KEY` | *(generate one)* | Laravel app key. Run: `php artisan key:generate --show` |
|
| 67 |
+
| `APP_ENV` | `production` | Application environment |
|
| 68 |
+
| `APP_DEBUG` | `false` | Debug mode (keep false in production) |
|
| 69 |
+
| `DB_CONNECTION` | `sqlite` | Database engine |
|
| 70 |
+
| `SESSION_DRIVER` | `database` | Session storage |
|
| 71 |
+
| `CACHE_STORE` | `database` | Cache storage |
|
| 72 |
+
| `QUEUE_CONNECTION` | `database` | Queue driver |
|
| 73 |
+
| `RUN_MODE` | `hf` | **Must be set to `hf`** to enable supervisor mode |
|
| 74 |
+
| `AI_INTEGRATION_ROUTES_ENABLED` | `true` | Enable AI proxy routes |
|
| 75 |
+
| `CHEMICAL_AI_URL` | `https://RottenShadow-ailixir-chemical-rag.hf.space` | Chemical RAG service URL |
|
| 76 |
+
| `ADMET_AI_URL` | `https://shdwRow-ailixir-admet.hf.space` | ADMET service URL |
|
| 77 |
+
| `AI_ADMET_SERVICE_URL` | `https://shdwRow-ailixir-admet.hf.space` | ADMET service URL (alias) |
|
| 78 |
+
| `DRUG_REPURPOSING_URL` | `https://RottenShadow-ailixir-drug-repurposing.hf.space` | Drug repurposing service URL |
|
| 79 |
+
| `AI_SERVICE_URL` | `https://RottenShadow-ailixir-drug-repurposing.hf.space` | Drug repurposing URL (alias) |
|
| 80 |
+
| `GENERATION_SERVICE_URL` | `https://shdwRow-ailixir-generation.hf.space` | Generation service URL |
|
| 81 |
+
| `MAIL_MAILER` | `log` | Mail driver (log to console) |
|
| 82 |
+
| `CLOUDINARY_URL` | `cloudinary://dummy_key:dummy_secret@dummy_cloud` | Cloudinary (can be dummy) |
|
| 83 |
+
| `CLOUDINARY_CLOUD_NAME` | `dummy_cloud` | Cloudinary cloud name |
|
| 84 |
+
| `CLOUDINARY_API_KEY` | `dummy_key` | Cloudinary API key |
|
| 85 |
+
| `CLOUDINARY_API_SECRET` | `dummy_secret` | Cloudinary API secret |
|
| 86 |
+
| `LOG_CHANNEL` | `stderr` | Log output channel |
|
| 87 |
+
| `LOG_LEVEL` | `info` | Log level |
|
| 88 |
+
|
| 89 |
+
**How to generate APP_KEY:**
|
| 90 |
+
```bash
|
| 91 |
+
# If you have PHP locally:
|
| 92 |
+
php artisan key:generate --show
|
| 93 |
+
|
| 94 |
+
# If not, use any online base64 generator to create a random 32-byte base64 string
|
| 95 |
+
# Format: base64:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### 3.2 ADMET Space: `ailixir-admet` (owner: shdwRow)
|
| 99 |
+
|
| 100 |
+
**Space settings:** https://huggingface.co/spaces/shdwRow/ailixir-admet/settings
|
| 101 |
+
|
| 102 |
+
| Secret | Value | Description |
|
| 103 |
+
|--------|-------|-------------|
|
| 104 |
+
| `PYTHONUNBUFFERED` | `1` | Python unbuffered output |
|
| 105 |
+
| `LOG_LEVEL` | `INFO` | Logging level |
|
| 106 |
+
|
| 107 |
+
### 3.3 Drug Repurposing Space: `ailixir-drug-repurposing`
|
| 108 |
+
|
| 109 |
+
| Secret | Value | Description |
|
| 110 |
+
|--------|-------|-------------|
|
| 111 |
+
| `HOST` | `0.0.0.0` | Bind address |
|
| 112 |
+
| `PORT` | `7860` | Port (must match Dockerfile) |
|
| 113 |
+
| `DEBUG` | `False` | Debug mode |
|
| 114 |
+
| `LOG_LEVEL` | `INFO` | Log level |
|
| 115 |
+
| `USE_MOCK_MODEL` | `False` | Use mock model (for testing) |
|
| 116 |
+
| `USE_MOCK_DRUGS` | `False` | Use mock drug list (for testing) |
|
| 117 |
+
|
| 118 |
+
### 3.4 Chemical RAG Space: `ailixir-chemical-rag`
|
| 119 |
+
|
| 120 |
+
| Secret | Value | Description |
|
| 121 |
+
|--------|-------|-------------|
|
| 122 |
+
| `PYTHONUNBUFFERED` | `1` | Python unbuffered output |
|
| 123 |
+
| `API_PORT` | `7860` | API port |
|
| 124 |
+
| `API_HOST` | `0.0.0.0` | Bind address |
|
| 125 |
+
|
| 126 |
+
### 3.5 Generation Space: `ailixir-generation` (owner: shdwRow)
|
| 127 |
+
|
| 128 |
+
**Space settings:** https://huggingface.co/spaces/shdwRow/ailixir-generation/settings
|
| 129 |
+
|
| 130 |
+
| Secret | Value | Description |
|
| 131 |
+
|--------|-------|-------------|
|
| 132 |
+
| `PUBLIC_BASE_URL` | `https://shdwRow-ailixir-generation.hf.space` | Public URL of the service |
|
| 133 |
+
| `REINVENT_DEVICE` | `cpu` | Device for REINVENT (CPU-only on HF free tier) |
|
| 134 |
+
| `DEEPPURPOSE_URL` | *(leave empty)* | DeepPurpose service URL (CPU-only version) |
|
| 135 |
+
| `ADGPU_BIN` | *(leave empty)* | AutoDock-GPU binary (not available on CPU) |
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## 4. Setting Secrets in HF Spaces
|
| 140 |
+
|
| 141 |
+
1. Go to the Space's settings page:
|
| 142 |
+
`https://huggingface.co/spaces/RottenShadow/{space-name}/settings`
|
| 143 |
+
|
| 144 |
+
2. Scroll down to **"Repository Secrets"** section
|
| 145 |
+
|
| 146 |
+
3. Click **"New secret"**
|
| 147 |
+
|
| 148 |
+
4. Enter the key-value pair and click **"Add"**
|
| 149 |
+
|
| 150 |
+
5. The Space will automatically **rebuild** with the new secrets
|
| 151 |
+
|
| 152 |
+
---
|
| 153 |
+
|
| 154 |
+
## 5. API Endpoints
|
| 155 |
+
|
| 156 |
+
Once all spaces are running, the backend API is available at:
|
| 157 |
+
|
| 158 |
+
```
|
| 159 |
+
https://Ailixir-AI-Team-ailixir-api.hf.space/api
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
### Authentication
|
| 163 |
+
```bash
|
| 164 |
+
# Register
|
| 165 |
+
curl -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/user/register \
|
| 166 |
+
-H "Content-Type: application/json" \
|
| 167 |
+
-d '{"name":"User","email":"user@example.com","password":"password123"}'
|
| 168 |
+
|
| 169 |
+
# Login
|
| 170 |
+
curl -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/user/login \
|
| 171 |
+
-H "Content-Type: application/json" \
|
| 172 |
+
-d '{"email":"user@example.com","password":"password123"}'
|
| 173 |
+
|
| 174 |
+
# Response includes Bearer token
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### AI Service Proxies (via Backend)
|
| 178 |
+
All AI endpoints are proxied through the backend:
|
| 179 |
+
|
| 180 |
+
```bash
|
| 181 |
+
# ADMET prediction
|
| 182 |
+
curl -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/admet/predict \
|
| 183 |
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
| 184 |
+
-H "Content-Type: application/json" \
|
| 185 |
+
-d '{"smiles": "c1ccccc1"}'
|
| 186 |
+
|
| 187 |
+
# Chemical search
|
| 188 |
+
curl -X POST https://Ailixir-AI-Team-ailixir-api.hf.space/api/chemical-search \
|
| 189 |
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
| 190 |
+
-H "Content-Type: application/json" \
|
| 191 |
+
-d '{"smiles": "CC(=O)O"}'
|
| 192 |
+
|
| 193 |
+
# Health check (no auth needed)
|
| 194 |
+
curl https://Ailixir-AI-Team-ailixir-api.hf.space/api/ai-services/health
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
---
|
| 198 |
+
|
| 199 |
+
## 6. Auto-Sync from GitHub
|
| 200 |
+
|
| 201 |
+
The repository has **5 GitHub Actions workflows** that automatically sync code pushes to HF Spaces:
|
| 202 |
+
|
| 203 |
+
| Workflow | Triggers On | Syncs To |
|
| 204 |
+
|----------|-------------|----------|
|
| 205 |
+
| `sync-hf-all.yml` | Push to `main` | All 5 spaces (matrix job) |
|
| 206 |
+
|
| 207 |
+
### ❗ PREREQUISITE: Set `HF_TOKEN` as a GitHub Secret
|
| 208 |
+
|
| 209 |
+
For the auto-sync to work, you **must** add both HF tokens as GitHub Actions secrets:
|
| 210 |
+
|
| 211 |
+
1. Go to: https://github.com/RottenShadow/AILIXIR_BackEnd/settings/secrets/actions
|
| 212 |
+
2. Click **"New repository secret"**
|
| 213 |
+
3. **Name:** `HF_TOKEN` — **Value:** RottenShadow account token (for `RottenShadow` spaces)
|
| 214 |
+
4. **Name:** `HF_TOKEN_SHDWROW` — **Value:** shdwRow account token (for `shdwRow` spaces)
|
| 215 |
+
5. **Name:** `HF_TOKEN_AILIXIR` — **Value:** Ailixir-AI-Team account token (for `ailixir-api`)
|
| 216 |
+
|
| 217 |
+
Without these, the auto-sync workflows will fail with an authentication error.
|
| 218 |
+
|
| 219 |
+
---
|
| 220 |
+
|
| 221 |
+
## 7. Troubleshooting
|
| 222 |
+
|
| 223 |
+
### Space stuck at `BUILDING`
|
| 224 |
+
- Large Docker images can take **15–45 minutes** to build on HF free tier
|
| 225 |
+
- Check the build logs: Space page → **"Factory"** or **"Builder"** tab
|
| 226 |
+
- Common issues: missing files, Dockerfile errors, out of memory
|
| 227 |
+
|
| 228 |
+
### Space shows `NO_APP_FILE`
|
| 229 |
+
- The Dockerfile is missing from the root of the Space repo
|
| 230 |
+
- Ensure `Dockerfile` exists at the root (not in a subdirectory)
|
| 231 |
+
|
| 232 |
+
### Space shows `CONFIG_ERROR`
|
| 233 |
+
- Missing or invalid `README.md` YAML frontmatter
|
| 234 |
+
- Ensure the `README.md` has the correct format:
|
| 235 |
+
```yaml
|
| 236 |
+
---
|
| 237 |
+
title: Space Name
|
| 238 |
+
emoji: 🧬
|
| 239 |
+
colorFrom: blue
|
| 240 |
+
colorTo: green
|
| 241 |
+
sdk: docker
|
| 242 |
+
app_port: 7860
|
| 243 |
+
---
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
### API returns 500 errors
|
| 247 |
+
- Check the Space logs: Space page → **"Factory"** tab
|
| 248 |
+
- Verify all required environment secrets are set
|
| 249 |
+
- For the backend, ensure `APP_KEY` is a valid Laravel key
|
| 250 |
+
|
| 251 |
+
### Service not reachable from backend
|
| 252 |
+
- Verify the AI service URL in the backend's secrets is correct
|
| 253 |
+
- The URLs should use the format: `https://RottenShadow-{name}.hf.space` (or `https://shdwRow-{name}.hf.space` for ADMET/Generation)
|
| 254 |
+
- Test the AI service directly first:
|
| 255 |
+
```bash
|
| 256 |
+
curl https://shdwRow-ailixir-admet.hf.space/health
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
### Space keeps going to sleep
|
| 260 |
+
- Free tier spaces sleep after 15 minutes of inactivity
|
| 261 |
+
- Use a cron job (e.g., GitHub Actions scheduled workflow) to ping every 10 minutes
|
| 262 |
+
- Or upgrade to a paid hardware tier (starting at $5/month)
|
| 263 |
+
|
| 264 |
+
---
|
| 265 |
+
|
| 266 |
+
## 8. Upgrading Hardware
|
| 267 |
+
|
| 268 |
+
To upgrade a Space from CPU Basic to a paid tier:
|
| 269 |
+
|
| 270 |
+
1. Go to the Space settings
|
| 271 |
+
2. Under **"Hardware"**, select a tier:
|
| 272 |
+
- `cpu-upgrade` — 4 vCPU, 16GB RAM ($5/month)
|
| 273 |
+
- `cpu-basic` — 2 vCPU, 16GB RAM (free)
|
| 274 |
+
- `t4-small` — NVIDIA T4 GPU ($20/month, for generation service)
|
| 275 |
+
3. Save — the Space will rebuild
|
| 276 |
+
|
| 277 |
+
**Note:** The generation service (`ailixir-generation`) can benefit from a GPU upgrade for AutoDock-GPU docking.
|
HUGGINGFACE_FLOW.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AILIXIR Hugging Face Spaces — Migration Flow
|
| 2 |
+
|
| 3 |
+
> **Base commit:** `fd94aa0` — pre-HF, Docker Compose with MySQL+Redis
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. Why Hugging Face Spaces?
|
| 8 |
+
|
| 9 |
+
| Need | Solution |
|
| 10 |
+
|------|----------|
|
| 11 |
+
| Free hosting for 5 microservices | 5 HF Spaces (2 accounts to keep more awake) |
|
| 12 |
+
| No MySQL/Redis infrastructure | SQLite for storage, `database` queue driver |
|
| 13 |
+
| Simple CI/CD | GitHub Actions auto-syncs code to each HF Space |
|
| 14 |
+
| Binary model files | Git LFS + Xet storage on HF |
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 2. Architecture Decisions
|
| 19 |
+
|
| 20 |
+
### SQLite over MySQL
|
| 21 |
+
- **Why:** HF Spaces provide ephemeral storage; SQLite has zero dependencies
|
| 22 |
+
- **Trade-off:** No concurrent write scaling; fine for single-user/small-team usage
|
| 23 |
+
- **DB Location:** `database/ailixir.sqlite` (mounted in the container)
|
| 24 |
+
- **Migrations:** Run automatically via `entrypoint.sh` on startup
|
| 25 |
+
|
| 26 |
+
### Two HF Accounts
|
| 27 |
+
- **`Ailixir-AI-Team`**: Backend API
|
| 28 |
+
- **`RottenShadow`**: Chemical RAG, Drug Repurposing
|
| 29 |
+
- **`shdwRow`** (secondary): ADMET, Generation
|
| 30 |
+
- **Why:** HF Spaces free tier puts unused spaces to sleep; spreading across 2 accounts keeps more services warm
|
| 31 |
+
|
| 32 |
+
### Queue: `sync` driver
|
| 33 |
+
- **Why:** No need for Redis/DB-backed queue worker; jobs run inline
|
| 34 |
+
- **Note:** For production scale, switch back to `database` and run a queue worker
|
| 35 |
+
|
| 36 |
+
### Binary Model Files: Git LFS + Xet
|
| 37 |
+
- **Why:** HF rejects large files in raw Git; Xet provides efficient binary storage
|
| 38 |
+
- **Tracked:** `*.ckpt`, `*.pt`, `*.pkl`
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 3. Per-Service Details
|
| 43 |
+
|
| 44 |
+
### Backend API (`Ailixir-AI-Team/ailixir-api`)
|
| 45 |
+
- **Tech:** Laravel 12 + PHP 8.3 CLI
|
| 46 |
+
- **Port:** 7860 (HF default)
|
| 47 |
+
- **Dockerfile:** Multi-stage (composer → vite → runtime)
|
| 48 |
+
- **Entrypoint:** Sources `RUN_MODE=hf` → starts supervisord (laravel serve + queue worker)
|
| 49 |
+
- **Fixes applied:**
|
| 50 |
+
- `config/view.php` — explicit compiled path (fixes 500 on `/`)
|
| 51 |
+
- `bootstrap/app.php` — `trustProxies(at: '*')` (fixes mixed-content CSS)
|
| 52 |
+
- `config/scribe.php` — `docs_url` moved to `/scribe-docs`
|
| 53 |
+
- `/docs` route — serves rendered `API.md` (curl examples, responses, grouped)
|
| 54 |
+
- `QUEUE_CONNECTION=sync` — no worker needed
|
| 55 |
+
|
| 56 |
+
### ADMET Prediction (`shdwRow/ailixir-admet`)
|
| 57 |
+
- **Tech:** FastAPI + PyTorch
|
| 58 |
+
- **Port:** 7860
|
| 59 |
+
- **Model:** `best_model.ckpt` (LFS-tracked)
|
| 60 |
+
- **Sync:** Xet-enabled checkout in workflow
|
| 61 |
+
|
| 62 |
+
### Chemical RAG (`RottenShadow/ailixir-chemical-rag`)
|
| 63 |
+
- **Tech:** FastAPI + ChromaDB
|
| 64 |
+
- **Port:** 7860
|
| 65 |
+
|
| 66 |
+
### Drug Repurposing (`RottenShadow/ailixir-drug-repurposing`)
|
| 67 |
+
- **Tech:** FastAPI + DeepPurpose
|
| 68 |
+
- **Port:** 7860
|
| 69 |
+
- **Special:** `Dockerfile.hf` with mock-mode fallback
|
| 70 |
+
|
| 71 |
+
### Generation (`shdwRow/ailixir-generation`)
|
| 72 |
+
- **Tech:** FastAPI + REINVENT4 + RDKit + DeepPurpose (optional)
|
| 73 |
+
- **Port:** 7860
|
| 74 |
+
- **Fixes applied:**
|
| 75 |
+
- **REINVENT4** — git cloned and `install.py cpu` in `Dockerfile.hf`
|
| 76 |
+
- **libXrender** — `libxrender1 libxext6` added (RDKit image rendering)
|
| 77 |
+
- **DeepPurpose** — optional install; integrated `/reinvent_predict` route on same port 7860
|
| 78 |
+
- **Graceful fallback** — if DeepPurpose unavailable, enrichment returns null affinity values
|
| 79 |
+
- **Enrichment** — catches connection errors, sets `pred_pAff_mean` to `None`
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 4. Sync Workflow
|
| 84 |
+
|
| 85 |
+
**File:** `.github/workflows/sync-hf-all.yml`
|
| 86 |
+
|
| 87 |
+
### Trigger
|
| 88 |
+
- Push to `main` branch
|
| 89 |
+
- Manual dispatch via GitHub UI
|
| 90 |
+
|
| 91 |
+
### Jobs (5 total)
|
| 92 |
+
|
| 93 |
+
| Job | HF Space | Account | Token | Notes |
|
| 94 |
+
|-----|----------|---------|-------|-------|
|
| 95 |
+
| `backend` | `ailixir-api` | Ailixir-AI-Team | `HF_TOKEN_AILIXIR` | Excludes `ai_apps/`, `database/ailixir.sqlite`, `storage/` |
|
| 96 |
+
| `drug-repurposing` | `ailixir-drug-repurposing` | RottenShadow | `HF_TOKEN` | Renames `Dockerfile.hf` → `Dockerfile` |
|
| 97 |
+
| `admet` | `ailixir-admet` | shdwRow | `HF_TOKEN_SHDWROW` | LFS + Xet for `.ckpt` |
|
| 98 |
+
| `chemical-rag` | `ailixir-chemical-rag` | RottenShadow | `HF_TOKEN` | |
|
| 99 |
+
| `generation` | `ailixir-generation` | shdwRow | `HF_TOKEN_SHDWROW` | LFS + Xet for `.ckpt`, `.pt`, `.pkl` |
|
| 100 |
+
|
| 101 |
+
### How each job works
|
| 102 |
+
1. Clone the HF Space repo (shallow, depth 1)
|
| 103 |
+
2. `rsync -av --delete` — sync files from main repo, delete files removed in source
|
| 104 |
+
3. Write `.env` with all required vars
|
| 105 |
+
4. Rename `Dockerfile.hf` → `Dockerfile` if present
|
| 106 |
+
5. Setup LFS/Xet for model files (ADMET, Generation)
|
| 107 |
+
6. Commit and push to HF Space
|
| 108 |
+
|
| 109 |
+
### Key exclusions (backend job)
|
| 110 |
+
```
|
| 111 |
+
--exclude=ai_apps
|
| 112 |
+
--exclude=.git
|
| 113 |
+
--exclude=.env
|
| 114 |
+
--exclude=.dockerignore
|
| 115 |
+
--exclude=public/imgs
|
| 116 |
+
--exclude=storage
|
| 117 |
+
--exclude=Diagrams
|
| 118 |
+
--exclude=database/ailixir.sqlite
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### Required GitHub Secrets
|
| 122 |
+
| Secret | Used by |
|
| 123 |
+
|--------|---------|
|
| 124 |
+
| `HF_TOKEN` | drug-repurposing, chemical-rag |
|
| 125 |
+
| `HF_TOKEN_AILIXIR` | backend |
|
| 126 |
+
| `HF_TOKEN_SHDWROW` | admet, generation |
|
| 127 |
+
| `GOOGLE_CLIENT_ID` | backend .env |
|
| 128 |
+
| `GOOGLE_CLIENT_SECRET` | backend .env |
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
## 5. Environment Variables (`.env` written by sync)
|
| 133 |
+
|
| 134 |
+
```
|
| 135 |
+
RUN_MODE=hf
|
| 136 |
+
APP_KEY=base64:...
|
| 137 |
+
AI_INTEGRATION_ROUTES_ENABLED=true
|
| 138 |
+
APP_URL=https://Ailixir-AI-Team-ailixir-api.hf.space
|
| 139 |
+
APP_DEBUG=true
|
| 140 |
+
QUEUE_CONNECTION=sync
|
| 141 |
+
ADMET_AI_URL=https://shdwRow-ailixir-admet.hf.space
|
| 142 |
+
CHEMICAL_AI_URL=https://RottenShadow-ailixir-chemical-rag.hf.space
|
| 143 |
+
DRUG_REPURPOSING_URL=https://RottenShadow-ailixir-drug-repurposing.hf.space
|
| 144 |
+
GENERATION_SERVICE_URL=https://shdwRow-ailixir-generation.hf.space
|
| 145 |
+
GOOGLE_CLIENT_ID=...
|
| 146 |
+
GOOGLE_CLIENT_SECRET=...
|
| 147 |
+
GOOGLE_REDIRECT_URI=https://Ailixir-AI-Team-ailixir-api.hf.space/api/user/auth/google
|
| 148 |
+
CLOUDINARY_CLOUD_NAME=dummy_cloud
|
| 149 |
+
CLOUDINARY_API_KEY=dummy_key
|
| 150 |
+
CLOUDINARY_API_SECRET=dummy_secret
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## 6. Database Protection
|
| 156 |
+
|
| 157 |
+
The sync workflow **excludes** `database/ailixir.sqlite` from rsync and runs `git checkout HEAD -- database/ailixir.sqlite` before commit to prevent the live database from being pushed to git.
|
| 158 |
+
|
| 159 |
+
On HF Spaces, the database file persists across restarts (HF Spaces keep filesystem between restarts) but is **reset** when the Space rebuilds (code push).
|
| 160 |
+
|
| 161 |
+
---
|
| 162 |
+
|
| 163 |
+
## 7. Known Issues & Future Work
|
| 164 |
+
|
| 165 |
+
| Issue | Status | Resolution |
|
| 166 |
+
|-------|--------|------------|
|
| 167 |
+
| DeepPurpose on Python 3.10 | Unstable | Falls back to null affinity predictions; could switch to conda base image |
|
| 168 |
+
| Google OAuth secrets | Not set | Needs `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` added to GitHub secrets |
|
| 169 |
+
| Space sleep on free tier | Ongoing | Two-account setup mitigates; upgrade to HF Pro for always-on |
|
| 170 |
+
| Cloudinary credentials | Dummy values | Set real Cloudinary keys for image upload features |
|
| 171 |
+
| ADMET model files in Xet | Working | Large `.ckpt` files stored via HF Xet |
|
| 172 |
+
| Docs route `/docs` | Working | Serves rendered `API.md` with all examples and grouping |
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## 8. Commit History (cleaned)
|
| 177 |
+
|
| 178 |
+
```
|
| 179 |
+
c49c94f Add HF Spaces sync workflow with two-account setup, DB protection
|
| 180 |
+
30f0a11 Generation HF: REINVENT4, DeepPurpose, libXrender, graceful fallback
|
| 181 |
+
a342297 AI microservices HF: ports 8000→7860, Dockerfiles, README_HF
|
| 182 |
+
af62962 Backend HF fixes: trustProxies, view cache path, docs route with API.md
|
| 183 |
+
fd80b74 Add HF Spaces environment config, deployment guide, and changelog
|
| 184 |
+
b4a5d77 Foundation: migrate MySQL→SQLite, add supervisor, port 7860, APP_KEY auto-gen
|
| 185 |
+
fd94aa0 Limit Docker Compose build parallelism in CI (original base)
|
| 186 |
+
```
|
README.md
CHANGED
|
@@ -1,44 +1,24 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
|
| 8 |
-
python_version: "3.10"
|
| 9 |
-
app_file: app.py
|
| 10 |
-
pinned: false
|
| 11 |
-
license: other
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
-
-
|
| 23 |
-
-
|
| 24 |
-
- `
|
| 25 |
-
-
|
| 26 |
-
-
|
| 27 |
-
|
| 28 |
-
## Docking note
|
| 29 |
-
|
| 30 |
-
Real AutoDock-GPU docking is not enabled in this CPU Space.
|
| 31 |
-
|
| 32 |
-
The backend schema already supports:
|
| 33 |
-
|
| 34 |
-
- `docking_score`
|
| 35 |
-
- `docking_status`
|
| 36 |
-
- `docking_pose_file`
|
| 37 |
-
|
| 38 |
-
Supported docking modes in the full backend:
|
| 39 |
-
|
| 40 |
-
- `off`
|
| 41 |
-
- `top_k`
|
| 42 |
-
- `all`
|
| 43 |
-
|
| 44 |
-
In this CPU demo Space, docking status is returned as `not_run`.
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AILIXIR - Backend API
|
| 3 |
+
emoji: 🧠
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# AILIXIR Backend API
|
| 11 |
|
| 12 |
+
Laravel backend with SQLite database for the AILIXIR drug discovery platform.
|
| 13 |
|
| 14 |
+
- **Port:** 7860
|
| 15 |
+
- **SDK:** Docker
|
| 16 |
+
- **Database:** SQLite
|
| 17 |
|
| 18 |
+
## Endpoints
|
| 19 |
|
| 20 |
+
- `GET /api/ai-services/health` — Health check
|
| 21 |
+
- `POST /api/admet/predict` — ADMET prediction
|
| 22 |
+
- `POST /api/chemical-search` — Chemical search
|
| 23 |
+
- `POST /api/user/register` — User registration
|
| 24 |
+
- `POST /api/user/login` — User login
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README_HF.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AILIXIR - Backend API
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# AILIXIR Backend API
|
| 11 |
+
|
| 12 |
+
Laravel backend with SQLite database for the AILIXIR drug discovery platform.
|
| 13 |
+
|
| 14 |
+
- **Port:** 7860
|
| 15 |
+
- **SDK:** Docker
|
| 16 |
+
- **Database:** SQLite
|
| 17 |
+
|
| 18 |
+
## Endpoints
|
| 19 |
+
|
| 20 |
+
- `GET /api/ai-services/health` — Health check
|
| 21 |
+
- `POST /api/admet/predict` — ADMET prediction
|
| 22 |
+
- `POST /api/chemical-search` — Chemical search
|
| 23 |
+
- `POST /api/user/register` — User registration
|
| 24 |
+
- `POST /api/user/login` — User login
|
__pycache__/app.cpython-310.pyc
DELETED
|
Binary file (16.4 kB)
|
|
|
_ide_helper.php
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
_ide_helper_models.php
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
// @formatter:off
|
| 4 |
+
// phpcs:ignoreFile
|
| 5 |
+
/**
|
| 6 |
+
* A helper file for your Eloquent Models
|
| 7 |
+
* Copy the phpDocs from this file to the correct Model,
|
| 8 |
+
* And remove them from this file, to prevent double declarations.
|
| 9 |
+
*
|
| 10 |
+
* @author Barry vd. Heuvel <barryvdh@gmail.com>
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
namespace App\Models{
|
| 15 |
+
/**
|
| 16 |
+
* @property int $id
|
| 17 |
+
* @property string $smiles
|
| 18 |
+
* @property float|null $absorption
|
| 19 |
+
* @property float|null $distribution
|
| 20 |
+
* @property float|null $metabolism
|
| 21 |
+
* @property float|null $excretion
|
| 22 |
+
* @property float|null $toxicity
|
| 23 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 24 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 25 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet newModelQuery()
|
| 26 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet newQuery()
|
| 27 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet query()
|
| 28 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereAbsorption($value)
|
| 29 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereCreatedAt($value)
|
| 30 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereDistribution($value)
|
| 31 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereExcretion($value)
|
| 32 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereId($value)
|
| 33 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereMetabolism($value)
|
| 34 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereSmiles($value)
|
| 35 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereToxicity($value)
|
| 36 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admet whereUpdatedAt($value)
|
| 37 |
+
*/
|
| 38 |
+
class Admet extends \Eloquent {}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
namespace App\Models{
|
| 42 |
+
/**
|
| 43 |
+
* @property int $id
|
| 44 |
+
* @property int $user_id
|
| 45 |
+
* @property string $job_id
|
| 46 |
+
* @property string $status
|
| 47 |
+
* @property string $preset
|
| 48 |
+
* @property int $num_molecules
|
| 49 |
+
* @property int $return_top_k
|
| 50 |
+
* @property string $docking_mode
|
| 51 |
+
* @property int $dock_top_k
|
| 52 |
+
* @property array<array-key, mixed>|null $summary
|
| 53 |
+
* @property array<array-key, mixed>|null $files
|
| 54 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 55 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 56 |
+
* @property array<array-key, mixed>|null $ligands
|
| 57 |
+
* @property string|null $stage
|
| 58 |
+
* @property-read \App\Models\User $user
|
| 59 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob completed()
|
| 60 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob failed()
|
| 61 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob newModelQuery()
|
| 62 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob newQuery()
|
| 63 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob query()
|
| 64 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob running()
|
| 65 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereCreatedAt($value)
|
| 66 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereDockTopK($value)
|
| 67 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereDockingMode($value)
|
| 68 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereFiles($value)
|
| 69 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereId($value)
|
| 70 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereJobId($value)
|
| 71 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereLigands($value)
|
| 72 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereNumMolecules($value)
|
| 73 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob wherePreset($value)
|
| 74 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereReturnTopK($value)
|
| 75 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereStage($value)
|
| 76 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereStatus($value)
|
| 77 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereSummary($value)
|
| 78 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereUpdatedAt($value)
|
| 79 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|AiJob whereUserId($value)
|
| 80 |
+
*/
|
| 81 |
+
class AiJob extends \Eloquent {}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
namespace App\Models{
|
| 85 |
+
/**
|
| 86 |
+
* @property int $id
|
| 87 |
+
* @property string $name
|
| 88 |
+
* @property string|null $category
|
| 89 |
+
* @property array<array-key, mixed>|null $images
|
| 90 |
+
* @property string|null $description
|
| 91 |
+
* @property string|null $notable_winners
|
| 92 |
+
* @property string|null $country
|
| 93 |
+
* @property int|null $year_started
|
| 94 |
+
* @property string|null $website
|
| 95 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 96 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 97 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Scientist> $scientists
|
| 98 |
+
* @property-read int|null $scientists_count
|
| 99 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award newModelQuery()
|
| 100 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award newQuery()
|
| 101 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award query()
|
| 102 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereCategory($value)
|
| 103 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereCountry($value)
|
| 104 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereCreatedAt($value)
|
| 105 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereDescription($value)
|
| 106 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereId($value)
|
| 107 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereImages($value)
|
| 108 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereName($value)
|
| 109 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereNotableWinners($value)
|
| 110 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereUpdatedAt($value)
|
| 111 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereWebsite($value)
|
| 112 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Award whereYearStarted($value)
|
| 113 |
+
*/
|
| 114 |
+
class Award extends \Eloquent {}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
namespace App\Models{
|
| 118 |
+
/**
|
| 119 |
+
* @property int $id
|
| 120 |
+
* @property int $job_id
|
| 121 |
+
* @property int $rank
|
| 122 |
+
* @property string $smiles
|
| 123 |
+
* @property string|null $name
|
| 124 |
+
* @property string|null $cid
|
| 125 |
+
* @property float|null $similarity
|
| 126 |
+
* @property string|null $explanation
|
| 127 |
+
* @property string|null $image_url
|
| 128 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 129 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 130 |
+
* @property-read \App\Models\ChemicalSearchJob $job
|
| 131 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound newModelQuery()
|
| 132 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound newQuery()
|
| 133 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound query()
|
| 134 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereCid($value)
|
| 135 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereCreatedAt($value)
|
| 136 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereExplanation($value)
|
| 137 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereId($value)
|
| 138 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereImageUrl($value)
|
| 139 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereJobId($value)
|
| 140 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereName($value)
|
| 141 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereRank($value)
|
| 142 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereSimilarity($value)
|
| 143 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereSmiles($value)
|
| 144 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalCompound whereUpdatedAt($value)
|
| 145 |
+
*/
|
| 146 |
+
class ChemicalCompound extends \Eloquent {}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
namespace App\Models{
|
| 150 |
+
/**
|
| 151 |
+
* @property int $id
|
| 152 |
+
* @property int $user_id
|
| 153 |
+
* @property string $query_smiles
|
| 154 |
+
* @property int $top_k
|
| 155 |
+
* @property string $status
|
| 156 |
+
* @property array<array-key, mixed>|null $results
|
| 157 |
+
* @property array<array-key, mixed>|null $reason
|
| 158 |
+
* @property array<array-key, mixed>|null $image_urls
|
| 159 |
+
* @property array<array-key, mixed>|null $metadata
|
| 160 |
+
* @property string|null $error_message
|
| 161 |
+
* @property \Illuminate\Support\Carbon|null $started_at
|
| 162 |
+
* @property \Illuminate\Support\Carbon|null $completed_at
|
| 163 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 164 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 165 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ChemicalCompound> $compounds
|
| 166 |
+
* @property-read int|null $compounds_count
|
| 167 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob newModelQuery()
|
| 168 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob newQuery()
|
| 169 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob query()
|
| 170 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereCompletedAt($value)
|
| 171 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereCreatedAt($value)
|
| 172 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereErrorMessage($value)
|
| 173 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereId($value)
|
| 174 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereImageUrls($value)
|
| 175 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereMetadata($value)
|
| 176 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereQuerySmiles($value)
|
| 177 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereReason($value)
|
| 178 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereResults($value)
|
| 179 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereStartedAt($value)
|
| 180 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereStatus($value)
|
| 181 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereTopK($value)
|
| 182 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereUpdatedAt($value)
|
| 183 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemicalSearchJob whereUserId($value)
|
| 184 |
+
*/
|
| 185 |
+
class ChemicalSearchJob extends \Eloquent {}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
namespace App\Models{
|
| 189 |
+
/**
|
| 190 |
+
* @property int $id
|
| 191 |
+
* @property int $user_id
|
| 192 |
+
* @property int|null $chemistry_thread_id
|
| 193 |
+
* @property string $type
|
| 194 |
+
* @property string $input_data
|
| 195 |
+
* @property string|null $response
|
| 196 |
+
* @property array<array-key, mixed>|null $properties
|
| 197 |
+
* @property array<array-key, mixed>|null $drug_likeness
|
| 198 |
+
* @property array<array-key, mixed>|null $admet
|
| 199 |
+
* @property int|null $processing_time_ms
|
| 200 |
+
* @property string $status
|
| 201 |
+
* @property string|null $error_message
|
| 202 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 203 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 204 |
+
* @property-read \App\Models\ChemistryThread|null $thread
|
| 205 |
+
* @property-read \App\Models\User $user
|
| 206 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis newModelQuery()
|
| 207 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis newQuery()
|
| 208 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis query()
|
| 209 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereAdmet($value)
|
| 210 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereChemistryThreadId($value)
|
| 211 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereCreatedAt($value)
|
| 212 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereDrugLikeness($value)
|
| 213 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereErrorMessage($value)
|
| 214 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereId($value)
|
| 215 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereInputData($value)
|
| 216 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereProcessingTimeMs($value)
|
| 217 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereProperties($value)
|
| 218 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereResponse($value)
|
| 219 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereStatus($value)
|
| 220 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereType($value)
|
| 221 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereUpdatedAt($value)
|
| 222 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryAnalysis whereUserId($value)
|
| 223 |
+
*/
|
| 224 |
+
class ChemistryAnalysis extends \Eloquent {}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
namespace App\Models{
|
| 228 |
+
/**
|
| 229 |
+
* @property int $id
|
| 230 |
+
* @property int $user_id
|
| 231 |
+
* @property string $job_id
|
| 232 |
+
* @property string $filename
|
| 233 |
+
* @property string $analysis_type
|
| 234 |
+
* @property int $total_rows
|
| 235 |
+
* @property int $completed_rows
|
| 236 |
+
* @property int $failed_rows
|
| 237 |
+
* @property int $progress_percent
|
| 238 |
+
* @property string $status
|
| 239 |
+
* @property string|null $result_file_path
|
| 240 |
+
* @property string|null $error_message
|
| 241 |
+
* @property \Illuminate\Support\Carbon|null $started_at
|
| 242 |
+
* @property \Illuminate\Support\Carbon|null $completed_at
|
| 243 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 244 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 245 |
+
* @property-read \App\Models\User $user
|
| 246 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob newModelQuery()
|
| 247 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob newQuery()
|
| 248 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob query()
|
| 249 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereAnalysisType($value)
|
| 250 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereCompletedAt($value)
|
| 251 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereCompletedRows($value)
|
| 252 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereCreatedAt($value)
|
| 253 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereErrorMessage($value)
|
| 254 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereFailedRows($value)
|
| 255 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereFilename($value)
|
| 256 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereId($value)
|
| 257 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereJobId($value)
|
| 258 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereProgressPercent($value)
|
| 259 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereResultFilePath($value)
|
| 260 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereStartedAt($value)
|
| 261 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereStatus($value)
|
| 262 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereTotalRows($value)
|
| 263 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereUpdatedAt($value)
|
| 264 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryCsvJob whereUserId($value)
|
| 265 |
+
*/
|
| 266 |
+
class ChemistryCsvJob extends \Eloquent {}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
namespace App\Models{
|
| 270 |
+
/**
|
| 271 |
+
* @property int $id
|
| 272 |
+
* @property int $user_id
|
| 273 |
+
* @property string $thread_id
|
| 274 |
+
* @property string|null $title
|
| 275 |
+
* @property string $status
|
| 276 |
+
* @property \Illuminate\Support\Carbon|null $last_used_at
|
| 277 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 278 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 279 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ChemistryAnalysis> $analyses
|
| 280 |
+
* @property-read int|null $analyses_count
|
| 281 |
+
* @property-read \App\Models\User $user
|
| 282 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread newModelQuery()
|
| 283 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread newQuery()
|
| 284 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread query()
|
| 285 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread whereCreatedAt($value)
|
| 286 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread whereId($value)
|
| 287 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread whereLastUsedAt($value)
|
| 288 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread whereStatus($value)
|
| 289 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread whereThreadId($value)
|
| 290 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread whereTitle($value)
|
| 291 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread whereUpdatedAt($value)
|
| 292 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ChemistryThread whereUserId($value)
|
| 293 |
+
*/
|
| 294 |
+
class ChemistryThread extends \Eloquent {}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
namespace App\Models{
|
| 298 |
+
/**
|
| 299 |
+
* @property int $id
|
| 300 |
+
* @property int $user_id
|
| 301 |
+
* @property string $input_type
|
| 302 |
+
* @property string|null $smiles
|
| 303 |
+
* @property string|null $protein_name
|
| 304 |
+
* @property string|null $ligand_name
|
| 305 |
+
* @property string $protein_path
|
| 306 |
+
* @property string $ligand_path
|
| 307 |
+
* @property string $status
|
| 308 |
+
* @property array<array-key, mixed>|null $result_data
|
| 309 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 310 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 311 |
+
* @property-read \App\Models\User $user
|
| 312 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob newModelQuery()
|
| 313 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob newQuery()
|
| 314 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob query()
|
| 315 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereCreatedAt($value)
|
| 316 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereId($value)
|
| 317 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereInputType($value)
|
| 318 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereLigandName($value)
|
| 319 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereLigandPath($value)
|
| 320 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereProteinName($value)
|
| 321 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereProteinPath($value)
|
| 322 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereResultData($value)
|
| 323 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereSmiles($value)
|
| 324 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereStatus($value)
|
| 325 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereUpdatedAt($value)
|
| 326 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|DockingJob whereUserId($value)
|
| 327 |
+
*/
|
| 328 |
+
class DockingJob extends \Eloquent {}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
namespace App\Models{
|
| 332 |
+
/**
|
| 333 |
+
* @property int $id
|
| 334 |
+
* @property string $job_id
|
| 335 |
+
* @property string $status
|
| 336 |
+
* @property string $file_format
|
| 337 |
+
* @property string $smiles
|
| 338 |
+
* @property string|null $filename
|
| 339 |
+
* @property string|null $download_url
|
| 340 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 341 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 342 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport newModelQuery()
|
| 343 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport newQuery()
|
| 344 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport query()
|
| 345 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereCreatedAt($value)
|
| 346 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereDownloadUrl($value)
|
| 347 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereFileFormat($value)
|
| 348 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereFilename($value)
|
| 349 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereId($value)
|
| 350 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereJobId($value)
|
| 351 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereSmiles($value)
|
| 352 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereStatus($value)
|
| 353 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|LigandExport whereUpdatedAt($value)
|
| 354 |
+
*/
|
| 355 |
+
class LigandExport extends \Eloquent {}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
namespace App\Models{
|
| 359 |
+
/**
|
| 360 |
+
* @property int $id
|
| 361 |
+
* @property string $experiment_name
|
| 362 |
+
* @property string|null $description
|
| 363 |
+
* @property string|null $SYS_pdb
|
| 364 |
+
* @property string|null $SYS_nw_pdb
|
| 365 |
+
* @property string|null $ligand_H_pdb
|
| 366 |
+
* @property string|null $ligand_gaff_pdb
|
| 367 |
+
* @property string|null $ligand_h_1_pdb
|
| 368 |
+
* @property string|null $ligand_h_nonprot_pdb
|
| 369 |
+
* @property string|null $protein_ligand_pdb
|
| 370 |
+
* @property string|null $prot_lig_equil_pdb
|
| 371 |
+
* @property string|null $starting_end_pdb
|
| 372 |
+
* @property string|null $prot_lig_equil_dcd
|
| 373 |
+
* @property string|null $prot_lig_prod1_1_nw_dcd
|
| 374 |
+
* @property string|null $SYS_gaff2_crd
|
| 375 |
+
* @property string|null $SYS_nw_crd
|
| 376 |
+
* @property string|null $SYS_gaff2_prmtop
|
| 377 |
+
* @property string|null $SYS_nw_prmtop
|
| 378 |
+
* @property string|null $prot_lig_equil_rst
|
| 379 |
+
* @property string|null $prepareforleap_in
|
| 380 |
+
* @property string|null $tleap_in
|
| 381 |
+
* @property string|null $lig_lib
|
| 382 |
+
* @property string|null $ligand_frcmod
|
| 383 |
+
* @property string|null $ligand_mol2
|
| 384 |
+
* @property string|null $ligand_h_renum_txt
|
| 385 |
+
* @property string|null $ligand_h_sslink
|
| 386 |
+
* @property string|null $prot_lig_equil_log
|
| 387 |
+
* @property string|null $_2D_rmsd_csv
|
| 388 |
+
* @property string|null $Interaction_energy_eelec_csv
|
| 389 |
+
* @property string|null $Interaction_energy_evdw_csv
|
| 390 |
+
* @property string|null $PC1_csv
|
| 391 |
+
* @property string|null $PC2_csv
|
| 392 |
+
* @property string|null $cross_correlation_csv
|
| 393 |
+
* @property string|null $distance_csv
|
| 394 |
+
* @property string|null $distance_select_csv
|
| 395 |
+
* @property string|null $radius_gyration_csv
|
| 396 |
+
* @property string|null $rmsd_ca_csv
|
| 397 |
+
* @property string|null $rmsf_ca_csv
|
| 398 |
+
* @property string|null $_2D_rmsd_png
|
| 399 |
+
* @property string|null $Interaction_energy_png
|
| 400 |
+
* @property string|null $PCA_png
|
| 401 |
+
* @property string|null $PCA_dist_png
|
| 402 |
+
* @property string|null $cross_correlation_png
|
| 403 |
+
* @property string|null $distance_png
|
| 404 |
+
* @property string|null $distance_select_png
|
| 405 |
+
* @property string|null $radius_gyration_png
|
| 406 |
+
* @property string|null $radius_gyration_dist_png
|
| 407 |
+
* @property string|null $rmsd_ca_png
|
| 408 |
+
* @property string|null $rmsd_dist_png
|
| 409 |
+
* @property string|null $rmsf_ca_png
|
| 410 |
+
* @property string|null $Interaction_html
|
| 411 |
+
* @property string|null $initial_html
|
| 412 |
+
* @property string|null $FINAL_RESULTS_MMPBSA_dat
|
| 413 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 414 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 415 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile newModelQuery()
|
| 416 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile newQuery()
|
| 417 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile query()
|
| 418 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile where2DRmsdCsv($value)
|
| 419 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile where2DRmsdPng($value)
|
| 420 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereCreatedAt($value)
|
| 421 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereCrossCorrelationCsv($value)
|
| 422 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereCrossCorrelationPng($value)
|
| 423 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereDescription($value)
|
| 424 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereDistanceCsv($value)
|
| 425 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereDistancePng($value)
|
| 426 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereDistanceSelectCsv($value)
|
| 427 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereDistanceSelectPng($value)
|
| 428 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereExperimentName($value)
|
| 429 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereFINALRESULTSMMPBSADat($value)
|
| 430 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereId($value)
|
| 431 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereInitialHtml($value)
|
| 432 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereInteractionEnergyEelecCsv($value)
|
| 433 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereInteractionEnergyEvdwCsv($value)
|
| 434 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereInteractionEnergyPng($value)
|
| 435 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereInteractionHtml($value)
|
| 436 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigLib($value)
|
| 437 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigandFrcmod($value)
|
| 438 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigandGaffPdb($value)
|
| 439 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigandH1Pdb($value)
|
| 440 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigandHNonprotPdb($value)
|
| 441 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigandHPdb($value)
|
| 442 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigandHRenumTxt($value)
|
| 443 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigandHSslink($value)
|
| 444 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereLigandMol2($value)
|
| 445 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile wherePC1Csv($value)
|
| 446 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile wherePC2Csv($value)
|
| 447 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile wherePCADistPng($value)
|
| 448 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile wherePCAPng($value)
|
| 449 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile wherePrepareforleapIn($value)
|
| 450 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereProtLigEquilDcd($value)
|
| 451 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereProtLigEquilLog($value)
|
| 452 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereProtLigEquilPdb($value)
|
| 453 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereProtLigEquilRst($value)
|
| 454 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereProtLigProd11NwDcd($value)
|
| 455 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereProteinLigandPdb($value)
|
| 456 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereRadiusGyrationCsv($value)
|
| 457 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereRadiusGyrationDistPng($value)
|
| 458 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereRadiusGyrationPng($value)
|
| 459 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereRmsdCaCsv($value)
|
| 460 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereRmsdCaPng($value)
|
| 461 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereRmsdDistPng($value)
|
| 462 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereRmsfCaCsv($value)
|
| 463 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereRmsfCaPng($value)
|
| 464 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereSYSGaff2Crd($value)
|
| 465 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereSYSGaff2Prmtop($value)
|
| 466 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereSYSNwCrd($value)
|
| 467 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereSYSNwPdb($value)
|
| 468 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereSYSNwPrmtop($value)
|
| 469 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereSYSPdb($value)
|
| 470 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereStartingEndPdb($value)
|
| 471 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereTleapIn($value)
|
| 472 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|MdFile whereUpdatedAt($value)
|
| 473 |
+
*/
|
| 474 |
+
class MdFile extends \Eloquent {}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
namespace App\Models{
|
| 478 |
+
/**
|
| 479 |
+
* @property int $id
|
| 480 |
+
* @property string $title
|
| 481 |
+
* @property string|null $summary
|
| 482 |
+
* @property string $source
|
| 483 |
+
* @property string $url
|
| 484 |
+
* @property \Illuminate\Support\Carbon|null $published_at
|
| 485 |
+
* @property string|null $category
|
| 486 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 487 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 488 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News newModelQuery()
|
| 489 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News newQuery()
|
| 490 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News query()
|
| 491 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News whereCategory($value)
|
| 492 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News whereCreatedAt($value)
|
| 493 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News whereId($value)
|
| 494 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News wherePublishedAt($value)
|
| 495 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News whereSource($value)
|
| 496 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News whereSummary($value)
|
| 497 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News whereTitle($value)
|
| 498 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News whereUpdatedAt($value)
|
| 499 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|News whereUrl($value)
|
| 500 |
+
*/
|
| 501 |
+
class News extends \Eloquent {}
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
namespace App\Models{
|
| 505 |
+
/**
|
| 506 |
+
* @property int $id
|
| 507 |
+
* @property string $name
|
| 508 |
+
* @property string $type
|
| 509 |
+
* @property string|null $billing_period
|
| 510 |
+
* @property numeric $price
|
| 511 |
+
* @property string $currency
|
| 512 |
+
* @property string|null $stripe_product_id
|
| 513 |
+
* @property string|null $stripe_price_id
|
| 514 |
+
* @property bool $is_active
|
| 515 |
+
* @property string|null $description
|
| 516 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 517 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 518 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan newModelQuery()
|
| 519 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan newQuery()
|
| 520 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan query()
|
| 521 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereBillingPeriod($value)
|
| 522 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereCreatedAt($value)
|
| 523 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereCurrency($value)
|
| 524 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereDescription($value)
|
| 525 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereId($value)
|
| 526 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereIsActive($value)
|
| 527 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereName($value)
|
| 528 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan wherePrice($value)
|
| 529 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereStripePriceId($value)
|
| 530 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereStripeProductId($value)
|
| 531 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereType($value)
|
| 532 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plan whereUpdatedAt($value)
|
| 533 |
+
*/
|
| 534 |
+
class Plan extends \Eloquent {}
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
namespace App\Models{
|
| 538 |
+
/**
|
| 539 |
+
* @property int $id
|
| 540 |
+
* @property int $user_id
|
| 541 |
+
* @property string|null $specialization
|
| 542 |
+
* @property string|null $university
|
| 543 |
+
* @property int|null $years_of_experience
|
| 544 |
+
* @property string|null $bio
|
| 545 |
+
* @property string|null $photo
|
| 546 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 547 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 548 |
+
* @property-read \App\Models\User $user
|
| 549 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher newModelQuery()
|
| 550 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher newQuery()
|
| 551 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher query()
|
| 552 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher whereBio($value)
|
| 553 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher whereCreatedAt($value)
|
| 554 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher whereId($value)
|
| 555 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher wherePhoto($value)
|
| 556 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher whereSpecialization($value)
|
| 557 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher whereUniversity($value)
|
| 558 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher whereUpdatedAt($value)
|
| 559 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher whereUserId($value)
|
| 560 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Researcher whereYearsOfExperience($value)
|
| 561 |
+
*/
|
| 562 |
+
class Researcher extends \Eloquent {}
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
namespace App\Models{
|
| 566 |
+
/**
|
| 567 |
+
* @property int $id
|
| 568 |
+
* @property int $user_id
|
| 569 |
+
* @property int $news_id
|
| 570 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 571 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 572 |
+
* @property-read \App\Models\News $news
|
| 573 |
+
* @property-read \App\Models\User $user
|
| 574 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|SavedArticle newModelQuery()
|
| 575 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|SavedArticle newQuery()
|
| 576 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|SavedArticle query()
|
| 577 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|SavedArticle whereCreatedAt($value)
|
| 578 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|SavedArticle whereId($value)
|
| 579 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|SavedArticle whereNewsId($value)
|
| 580 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|SavedArticle whereUpdatedAt($value)
|
| 581 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|SavedArticle whereUserId($value)
|
| 582 |
+
*/
|
| 583 |
+
class SavedArticle extends \Eloquent {}
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
namespace App\Models{
|
| 587 |
+
/**
|
| 588 |
+
* @property int $id
|
| 589 |
+
* @property string $name
|
| 590 |
+
* @property string|null $nationality
|
| 591 |
+
* @property int|null $birth_year
|
| 592 |
+
* @property int|null $death_year
|
| 593 |
+
* @property array<array-key, mixed>|null $images
|
| 594 |
+
* @property string $bio
|
| 595 |
+
* @property string|null $impact
|
| 596 |
+
* @property string|null $field
|
| 597 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 598 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 599 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Award> $awards
|
| 600 |
+
* @property-read int|null $awards_count
|
| 601 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist newModelQuery()
|
| 602 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist newQuery()
|
| 603 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist query()
|
| 604 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereBio($value)
|
| 605 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereBirthYear($value)
|
| 606 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereCreatedAt($value)
|
| 607 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereDeathYear($value)
|
| 608 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereField($value)
|
| 609 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereId($value)
|
| 610 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereImages($value)
|
| 611 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereImpact($value)
|
| 612 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereName($value)
|
| 613 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereNationality($value)
|
| 614 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Scientist whereUpdatedAt($value)
|
| 615 |
+
*/
|
| 616 |
+
class Scientist extends \Eloquent {}
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
namespace App\Models{
|
| 620 |
+
/**
|
| 621 |
+
* @property int $id
|
| 622 |
+
* @property int $user_id
|
| 623 |
+
* @property array<array-key, mixed> $input
|
| 624 |
+
* @property array<array-key, mixed>|null $output
|
| 625 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 626 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 627 |
+
* @property string $status
|
| 628 |
+
* @property-read \App\Models\User $user
|
| 629 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult newModelQuery()
|
| 630 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult newQuery()
|
| 631 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult query()
|
| 632 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult whereCreatedAt($value)
|
| 633 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult whereId($value)
|
| 634 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult whereInput($value)
|
| 635 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult whereOutput($value)
|
| 636 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult whereStatus($value)
|
| 637 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult whereUpdatedAt($value)
|
| 638 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|ScreeningResult whereUserId($value)
|
| 639 |
+
*/
|
| 640 |
+
class ScreeningResult extends \Eloquent {}
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
namespace App\Models{
|
| 644 |
+
/**
|
| 645 |
+
* @property int $id
|
| 646 |
+
* @property int $user_id
|
| 647 |
+
* @property string $protein
|
| 648 |
+
* @property string|null $ligand
|
| 649 |
+
* @property string $status
|
| 650 |
+
* @property int $progress
|
| 651 |
+
* @property string|null $error_message
|
| 652 |
+
* @property string|null $trajectory
|
| 653 |
+
* @property string|null $log_file
|
| 654 |
+
* @property array<array-key, mixed>|null $analysis
|
| 655 |
+
* @property string $force_field
|
| 656 |
+
* @property float $temperature
|
| 657 |
+
* @property float $simulation_time_ns
|
| 658 |
+
* @property int $box_size
|
| 659 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 660 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 661 |
+
* @property-read \App\Models\User $user
|
| 662 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation newModelQuery()
|
| 663 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation newQuery()
|
| 664 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation query()
|
| 665 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereAnalysis($value)
|
| 666 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereBoxSize($value)
|
| 667 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereCreatedAt($value)
|
| 668 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereErrorMessage($value)
|
| 669 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereForceField($value)
|
| 670 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereId($value)
|
| 671 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereLigand($value)
|
| 672 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereLogFile($value)
|
| 673 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereProgress($value)
|
| 674 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereProtein($value)
|
| 675 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereSimulationTimeNs($value)
|
| 676 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereStatus($value)
|
| 677 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereTemperature($value)
|
| 678 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereTrajectory($value)
|
| 679 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereUpdatedAt($value)
|
| 680 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simulation whereUserId($value)
|
| 681 |
+
*/
|
| 682 |
+
class Simulation extends \Eloquent {}
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
namespace App\Models{
|
| 686 |
+
/**
|
| 687 |
+
* @property int $id
|
| 688 |
+
* @property int $user_id
|
| 689 |
+
|
| 690 |
+
* @property string $status
|
| 691 |
+
|
| 692 |
+
* @property array<array-key, mixed> $input
|
| 693 |
+
* @property array<array-key, mixed>|null $output
|
| 694 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 695 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 696 |
+
* @property string $status
|
| 697 |
+
* @property-read \App\Models\User $user
|
| 698 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup newModelQuery()
|
| 699 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup newQuery()
|
| 700 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup query()
|
| 701 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup whereCreatedAt($value)
|
| 702 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup whereId($value)
|
| 703 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup whereInput($value)
|
| 704 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup whereOutput($value)
|
| 705 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup whereStatus($value)
|
| 706 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup whereUpdatedAt($value)
|
| 707 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|TargetLookup whereUserId($value)
|
| 708 |
+
*/
|
| 709 |
+
class TargetLookup extends \Eloquent {}
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
namespace App\Models{
|
| 713 |
+
/**
|
| 714 |
+
* @property int $id
|
| 715 |
+
* @property string $name
|
| 716 |
+
* @property string $email
|
| 717 |
+
* @property \Illuminate\Support\Carbon|null $email_verified_at
|
| 718 |
+
* @property \Illuminate\Support\Carbon|null $last_otp_sent_at
|
| 719 |
+
* @property string $password
|
| 720 |
+
* @property string $role
|
| 721 |
+
* @property string|null $remember_token
|
| 722 |
+
* @property string|null $stripe_customer_id
|
| 723 |
+
* @property \Illuminate\Support\Carbon|null $created_at
|
| 724 |
+
* @property \Illuminate\Support\Carbon|null $updated_at
|
| 725 |
+
* @property string|null $email_verification_otp
|
| 726 |
+
* @property \Illuminate\Support\Carbon|null $email_verification_otp_expires_at
|
| 727 |
+
* @property bool $is_verified
|
| 728 |
+
* @property string|null $password_reset_otp
|
| 729 |
+
* @property \Illuminate\Support\Carbon|null $password_reset_otp_expires_at
|
| 730 |
+
* @property int|null $current_plan_id
|
| 731 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\AiJob> $aiJobs
|
| 732 |
+
* @property-read int|null $ai_jobs_count
|
| 733 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ChemistryAnalysis> $chemistryAnalyses
|
| 734 |
+
* @property-read int|null $chemistry_analyses_count
|
| 735 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ChemistryCsvJob> $chemistryCsvJobs
|
| 736 |
+
* @property-read int|null $chemistry_csv_jobs_count
|
| 737 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ChemistryThread> $chemistryThreads
|
| 738 |
+
* @property-read int|null $chemistry_threads_count
|
| 739 |
+
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
|
| 740 |
+
* @property-read int|null $notifications_count
|
| 741 |
+
* @property-read \App\Models\Researcher|null $researcher
|
| 742 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Simulation> $simulations
|
| 743 |
+
* @property-read int|null $simulations_count
|
| 744 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Cashier\Subscription> $subscriptions
|
| 745 |
+
* @property-read int|null $subscriptions_count
|
| 746 |
+
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Sanctum\PersonalAccessToken> $tokens
|
| 747 |
+
* @property-read int|null $tokens_count
|
| 748 |
+
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
| 749 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User hasExpiredGenericTrial()
|
| 750 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
|
| 751 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
|
| 752 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User onGenericTrial()
|
| 753 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
|
| 754 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
|
| 755 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCurrentPlanId($value)
|
| 756 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
|
| 757 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerificationOtp($value)
|
| 758 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerificationOtpExpiresAt($value)
|
| 759 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
|
| 760 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
|
| 761 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereIsVerified($value)
|
| 762 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereLastOtpSentAt($value)
|
| 763 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereName($value)
|
| 764 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
|
| 765 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePasswordResetOtp($value)
|
| 766 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePasswordResetOtpExpiresAt($value)
|
| 767 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
|
| 768 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRole($value)
|
| 769 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereStripeCustomerId($value)
|
| 770 |
+
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
|
| 771 |
+
*/
|
| 772 |
+
class User extends \Eloquent {}
|
| 773 |
+
}
|
| 774 |
+
|
ailixir-api-tests.zip
ADDED
|
Binary file (10.9 kB). View file
|
|
|
app.py
DELETED
|
@@ -1,736 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import os
|
| 3 |
-
import subprocess
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
import uuid
|
| 6 |
-
from datetime import datetime
|
| 7 |
-
|
| 8 |
-
import gradio as gr
|
| 9 |
-
import pandas as pd
|
| 10 |
-
from huggingface_hub import snapshot_download
|
| 11 |
-
from rdkit import Chem
|
| 12 |
-
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED, AllChem
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
# Prefer setting these in Hugging Face Space settings:
|
| 16 |
-
# Secret: HF_TOKEN
|
| 17 |
-
# Variable: BUNDLE_REPO = Ailixir-AI-Team/egfr-ai-bundle
|
| 18 |
-
BUNDLE_REPO = os.getenv("BUNDLE_REPO", "Ailixir-AI-Team/egfr-ai-bundle")
|
| 19 |
-
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 20 |
-
|
| 21 |
-
CACHE_DIR = Path("bundle_cache")
|
| 22 |
-
EXAMPLE_JSON = None
|
| 23 |
-
API_SPEC_TEXT = None
|
| 24 |
-
BACKEND_CONTRACT_TEXT = None
|
| 25 |
-
BUNDLE_ROOT = None
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def download_bundle_light():
|
| 29 |
-
"""Download only the light docs/examples/config needed by the Space."""
|
| 30 |
-
global EXAMPLE_JSON, API_SPEC_TEXT, BACKEND_CONTRACT_TEXT, BUNDLE_ROOT
|
| 31 |
-
|
| 32 |
-
try:
|
| 33 |
-
local_dir = snapshot_download(
|
| 34 |
-
repo_id=BUNDLE_REPO,
|
| 35 |
-
repo_type="dataset",
|
| 36 |
-
token=HF_TOKEN,
|
| 37 |
-
allow_patterns=[
|
| 38 |
-
"examples/generate_response.example.json",
|
| 39 |
-
"docs/API_RESPONSE_SPEC.md",
|
| 40 |
-
"configs/backend_contract.json",
|
| 41 |
-
"configs/sampling_model.bundle.toml",
|
| 42 |
-
"models/generator/egfr_generator.chkpt",
|
| 43 |
-
"README.md",
|
| 44 |
-
],
|
| 45 |
-
local_dir=str(CACHE_DIR),
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
root = Path(local_dir)
|
| 49 |
-
BUNDLE_ROOT = root
|
| 50 |
-
|
| 51 |
-
example_path = root / "examples" / "generate_response.example.json"
|
| 52 |
-
spec_path = root / "docs" / "API_RESPONSE_SPEC.md"
|
| 53 |
-
contract_path = root / "configs" / "backend_contract.json"
|
| 54 |
-
|
| 55 |
-
if example_path.exists():
|
| 56 |
-
EXAMPLE_JSON = json.loads(example_path.read_text(encoding="utf-8"))
|
| 57 |
-
|
| 58 |
-
if spec_path.exists():
|
| 59 |
-
API_SPEC_TEXT = spec_path.read_text(encoding="utf-8")
|
| 60 |
-
|
| 61 |
-
if contract_path.exists():
|
| 62 |
-
BACKEND_CONTRACT_TEXT = contract_path.read_text(encoding="utf-8")
|
| 63 |
-
|
| 64 |
-
return True, f"Loaded docs/examples from {BUNDLE_REPO}"
|
| 65 |
-
|
| 66 |
-
except Exception as e:
|
| 67 |
-
return False, f"Could not load private bundle. Check HF_TOKEN secret and BUNDLE_REPO variable. Error: {e}"
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def canonicalize(smiles: str):
|
| 71 |
-
mol = Chem.MolFromSmiles(str(smiles).strip())
|
| 72 |
-
if mol is None:
|
| 73 |
-
return None, None
|
| 74 |
-
return mol, Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def try_sa_score(mol):
|
| 78 |
-
try:
|
| 79 |
-
from rdkit.Contrib.SA_Score import sascorer
|
| 80 |
-
return round(float(sascorer.calculateScore(mol)), 4)
|
| 81 |
-
except Exception:
|
| 82 |
-
return None
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def compute_row(smiles: str):
|
| 86 |
-
mol, can = canonicalize(smiles)
|
| 87 |
-
|
| 88 |
-
if mol is None:
|
| 89 |
-
return {
|
| 90 |
-
"SMILES": smiles,
|
| 91 |
-
"valid": False,
|
| 92 |
-
"canonical_smiles": None,
|
| 93 |
-
"mw": None,
|
| 94 |
-
"logp": None,
|
| 95 |
-
"tpsa": None,
|
| 96 |
-
"hbd": None,
|
| 97 |
-
"hba": None,
|
| 98 |
-
"rot_bonds": None,
|
| 99 |
-
"qed": None,
|
| 100 |
-
"sa_score": None,
|
| 101 |
-
"pred_pAff_mean": None,
|
| 102 |
-
"docking_score": None,
|
| 103 |
-
"docking_status": "not_run",
|
| 104 |
-
"docking_pose_file": None,
|
| 105 |
-
"ligand_3d_file": None,
|
| 106 |
-
"note": "Invalid SMILES",
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
return {
|
| 110 |
-
"SMILES": smiles,
|
| 111 |
-
"valid": True,
|
| 112 |
-
"canonical_smiles": can,
|
| 113 |
-
"mw": round(float(Descriptors.MolWt(mol)), 4),
|
| 114 |
-
"logp": round(float(Crippen.MolLogP(mol)), 4),
|
| 115 |
-
"tpsa": round(float(rdMolDescriptors.CalcTPSA(mol)), 4),
|
| 116 |
-
"hbd": int(Lipinski.NumHDonors(mol)),
|
| 117 |
-
"hba": int(Lipinski.NumHAcceptors(mol)),
|
| 118 |
-
"rot_bonds": int(Lipinski.NumRotatableBonds(mol)),
|
| 119 |
-
"qed": round(float(QED.qed(mol)), 4),
|
| 120 |
-
"sa_score": try_sa_score(mol),
|
| 121 |
-
"pred_pAff_mean": None,
|
| 122 |
-
"docking_score": None,
|
| 123 |
-
"docking_status": "not_run",
|
| 124 |
-
"docking_pose_file": None,
|
| 125 |
-
"ligand_3d_file": None,
|
| 126 |
-
"note": "CPU demo: DeepPurpose and real docking are disabled in this Space.",
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
def add_ligand_ids_to_rows(rows, prefix="lig"):
|
| 131 |
-
output = []
|
| 132 |
-
for i, row in enumerate(rows, start=1):
|
| 133 |
-
clean = dict(row)
|
| 134 |
-
if not clean.get("ligand_id"):
|
| 135 |
-
clean["ligand_id"] = f"{prefix}_{i:04d}"
|
| 136 |
-
output.append(clean)
|
| 137 |
-
return output
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
def add_ligand_ids_to_df(df: pd.DataFrame, prefix="lig"):
|
| 141 |
-
df = df.copy()
|
| 142 |
-
if "ligand_id" not in df.columns:
|
| 143 |
-
df.insert(0, "ligand_id", [f"{prefix}_{i:04d}" for i in range(1, len(df) + 1)])
|
| 144 |
-
return df
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def clean_records_from_df(df: pd.DataFrame):
|
| 148 |
-
if df is None or df.empty:
|
| 149 |
-
return []
|
| 150 |
-
clean_df = df.astype(object).where(pd.notnull(df), None)
|
| 151 |
-
return clean_df.to_dict(orient="records")
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def write_results_csv(df: pd.DataFrame, filename: str = "score_smiles_results.csv"):
|
| 155 |
-
out_dir = Path("outputs")
|
| 156 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 157 |
-
out_path = out_dir / filename
|
| 158 |
-
df.to_csv(out_path, index=False)
|
| 159 |
-
return str(out_path)
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def score_smiles(smiles_text):
|
| 163 |
-
smiles_list = []
|
| 164 |
-
for part in str(smiles_text).replace(",", "\n").splitlines():
|
| 165 |
-
part = part.strip()
|
| 166 |
-
if part:
|
| 167 |
-
smiles_list.append(part)
|
| 168 |
-
|
| 169 |
-
if not smiles_list:
|
| 170 |
-
return pd.DataFrame(), {
|
| 171 |
-
"status": "error",
|
| 172 |
-
"message": "Please enter at least one SMILES.",
|
| 173 |
-
}, None
|
| 174 |
-
|
| 175 |
-
rows = add_ligand_ids_to_rows([compute_row(s) for s in smiles_list])
|
| 176 |
-
df = pd.DataFrame(rows)
|
| 177 |
-
|
| 178 |
-
csv_path = write_results_csv(df)
|
| 179 |
-
response = {
|
| 180 |
-
"status": "completed",
|
| 181 |
-
"mode": "score_smiles_cpu_demo",
|
| 182 |
-
"docking_mode": "off",
|
| 183 |
-
"summary": {
|
| 184 |
-
"num_input": len(smiles_list),
|
| 185 |
-
"num_valid": int(df["valid"].sum()) if "valid" in df.columns else 0,
|
| 186 |
-
"num_returned": int(len(df)),
|
| 187 |
-
},
|
| 188 |
-
"columns": list(df.columns),
|
| 189 |
-
"results": clean_records_from_df(df),
|
| 190 |
-
"download_csv_file": csv_path,
|
| 191 |
-
"warnings": [
|
| 192 |
-
"This Space is a CPU demo.",
|
| 193 |
-
"DeepPurpose pred_pAff_mean is not computed in this Space version.",
|
| 194 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 195 |
-
"Outputs are computational predictions only.",
|
| 196 |
-
],
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
return df, response, csv_path
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
def get_api_spec():
|
| 203 |
-
return API_SPEC_TEXT or "API_RESPONSE_SPEC.md not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
def get_contract():
|
| 207 |
-
if BACKEND_CONTRACT_TEXT:
|
| 208 |
-
try:
|
| 209 |
-
return json.loads(BACKEND_CONTRACT_TEXT)
|
| 210 |
-
except Exception:
|
| 211 |
-
return {"raw": BACKEND_CONTRACT_TEXT}
|
| 212 |
-
|
| 213 |
-
return {
|
| 214 |
-
"status": "not_loaded",
|
| 215 |
-
"message": "backend_contract.json not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
def _write_reinvent_sampling_config(job_id, num_molecules):
|
| 220 |
-
if BUNDLE_ROOT is None:
|
| 221 |
-
raise RuntimeError("Bundle root is not loaded. Check HF_TOKEN and BUNDLE_REPO.")
|
| 222 |
-
|
| 223 |
-
checkpoint = Path(BUNDLE_ROOT) / "models" / "generator" / "egfr_generator.chkpt"
|
| 224 |
-
if not checkpoint.exists():
|
| 225 |
-
raise FileNotFoundError(f"Generator checkpoint not found: {checkpoint}")
|
| 226 |
-
|
| 227 |
-
output_dir = Path("outputs")
|
| 228 |
-
output_dir.mkdir(exist_ok=True)
|
| 229 |
-
|
| 230 |
-
raw_csv = output_dir / f"{job_id}_raw_sampling.csv"
|
| 231 |
-
log_file = output_dir / f"{job_id}_reinvent.log"
|
| 232 |
-
resolved_json = output_dir / f"{job_id}_sampling_resolved.json"
|
| 233 |
-
config_path = output_dir / f"{job_id}_sampling.toml"
|
| 234 |
-
|
| 235 |
-
config_text = f"""
|
| 236 |
-
run_type = "sampling"
|
| 237 |
-
device = "cpu"
|
| 238 |
-
|
| 239 |
-
json_out_config = "{resolved_json.resolve()}"
|
| 240 |
-
|
| 241 |
-
[parameters]
|
| 242 |
-
model_file = "{checkpoint.resolve()}"
|
| 243 |
-
output_file = "{raw_csv.resolve()}"
|
| 244 |
-
|
| 245 |
-
num_smiles = {int(num_molecules)}
|
| 246 |
-
unique_molecules = true
|
| 247 |
-
randomize_smiles = true
|
| 248 |
-
"""
|
| 249 |
-
|
| 250 |
-
config_path.write_text(config_text.strip() + "\n", encoding="utf-8")
|
| 251 |
-
return config_path, raw_csv, log_file
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
def _run_reinvent_sampling(job_id, num_molecules):
|
| 255 |
-
config_path, raw_csv, log_file = _write_reinvent_sampling_config(job_id, num_molecules)
|
| 256 |
-
|
| 257 |
-
cmd = [
|
| 258 |
-
"reinvent",
|
| 259 |
-
"-l",
|
| 260 |
-
str(log_file.resolve()),
|
| 261 |
-
str(config_path.resolve()),
|
| 262 |
-
]
|
| 263 |
-
|
| 264 |
-
proc = subprocess.run(
|
| 265 |
-
cmd,
|
| 266 |
-
text=True,
|
| 267 |
-
stdout=subprocess.PIPE,
|
| 268 |
-
stderr=subprocess.STDOUT,
|
| 269 |
-
timeout=300,
|
| 270 |
-
)
|
| 271 |
-
|
| 272 |
-
if proc.returncode != 0:
|
| 273 |
-
raise RuntimeError(
|
| 274 |
-
"REINVENT sampling failed.\n"
|
| 275 |
-
f"Command: {' '.join(cmd)}\n"
|
| 276 |
-
f"Output:\n{proc.stdout[-4000:]}"
|
| 277 |
-
)
|
| 278 |
-
|
| 279 |
-
if not raw_csv.exists():
|
| 280 |
-
raise RuntimeError(f"REINVENT finished but output CSV was not found: {raw_csv}")
|
| 281 |
-
|
| 282 |
-
return raw_csv, log_file
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
def _enrich_reinvent_csv(raw_csv, return_top_k):
|
| 286 |
-
raw_df = pd.read_csv(raw_csv)
|
| 287 |
-
|
| 288 |
-
if "SMILES" not in raw_df.columns:
|
| 289 |
-
raise ValueError(f"REINVENT output missing SMILES column. Columns: {list(raw_df.columns)}")
|
| 290 |
-
|
| 291 |
-
rows = []
|
| 292 |
-
for _, r in raw_df.iterrows():
|
| 293 |
-
row = compute_row(str(r["SMILES"]))
|
| 294 |
-
|
| 295 |
-
if "SMILES_state" in raw_df.columns:
|
| 296 |
-
try:
|
| 297 |
-
row["SMILES_state"] = int(r["SMILES_state"])
|
| 298 |
-
except Exception:
|
| 299 |
-
row["SMILES_state"] = None
|
| 300 |
-
else:
|
| 301 |
-
row["SMILES_state"] = None
|
| 302 |
-
|
| 303 |
-
if "NLL" in raw_df.columns:
|
| 304 |
-
try:
|
| 305 |
-
row["NLL"] = float(r["NLL"])
|
| 306 |
-
except Exception:
|
| 307 |
-
row["NLL"] = None
|
| 308 |
-
else:
|
| 309 |
-
row["NLL"] = None
|
| 310 |
-
|
| 311 |
-
rows.append(row)
|
| 312 |
-
|
| 313 |
-
rows = add_ligand_ids_to_rows(rows)
|
| 314 |
-
df = pd.DataFrame(rows)
|
| 315 |
-
|
| 316 |
-
preferred_cols = [
|
| 317 |
-
"ligand_id",
|
| 318 |
-
"SMILES",
|
| 319 |
-
"SMILES_state",
|
| 320 |
-
"NLL",
|
| 321 |
-
"valid",
|
| 322 |
-
"canonical_smiles",
|
| 323 |
-
"mw",
|
| 324 |
-
"logp",
|
| 325 |
-
"tpsa",
|
| 326 |
-
"hbd",
|
| 327 |
-
"hba",
|
| 328 |
-
"rot_bonds",
|
| 329 |
-
"qed",
|
| 330 |
-
"sa_score",
|
| 331 |
-
"pred_pAff_mean",
|
| 332 |
-
"docking_score",
|
| 333 |
-
"docking_status",
|
| 334 |
-
"docking_pose_file",
|
| 335 |
-
"ligand_3d_file",
|
| 336 |
-
"note",
|
| 337 |
-
]
|
| 338 |
-
|
| 339 |
-
cols = [c for c in preferred_cols if c in df.columns] + [c for c in df.columns if c not in preferred_cols]
|
| 340 |
-
df = df[cols]
|
| 341 |
-
|
| 342 |
-
return_top_k = int(return_top_k)
|
| 343 |
-
if return_top_k > 0:
|
| 344 |
-
df = df.head(return_top_k)
|
| 345 |
-
|
| 346 |
-
return df
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_backend):
|
| 350 |
-
"""
|
| 351 |
-
Generation endpoint for the Space.
|
| 352 |
-
|
| 353 |
-
demo_rows: uses packaged example rows for stable demo.
|
| 354 |
-
live_reinvent_cpu: tries to run real REINVENT CPU sampling if available.
|
| 355 |
-
"""
|
| 356 |
-
job_id = f"gen_{uuid.uuid4().hex[:8]}"
|
| 357 |
-
created_at = datetime.utcnow().isoformat() + "Z"
|
| 358 |
-
num_molecules = int(num_molecules)
|
| 359 |
-
return_top_k = int(return_top_k)
|
| 360 |
-
|
| 361 |
-
warnings = []
|
| 362 |
-
|
| 363 |
-
try:
|
| 364 |
-
if generation_backend == "live_reinvent_cpu":
|
| 365 |
-
raw_csv, _log_file = _run_reinvent_sampling(job_id, num_molecules)
|
| 366 |
-
df = _enrich_reinvent_csv(raw_csv, return_top_k)
|
| 367 |
-
mode = "live_reinvent_cpu_sampling"
|
| 368 |
-
status = "completed"
|
| 369 |
-
warnings.extend([
|
| 370 |
-
"Live REINVENT CPU sampling was executed in this Space.",
|
| 371 |
-
"DeepPurpose scoring is not enabled in this Space version; pred_pAff_mean is null.",
|
| 372 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 373 |
-
])
|
| 374 |
-
|
| 375 |
-
else:
|
| 376 |
-
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 377 |
-
rows = EXAMPLE_JSON["results"]
|
| 378 |
-
else:
|
| 379 |
-
rows = [compute_row("CCO"), compute_row("c1ccccc1")]
|
| 380 |
-
|
| 381 |
-
selected = rows[: max(1, min(return_top_k, len(rows)))]
|
| 382 |
-
|
| 383 |
-
clean_rows = []
|
| 384 |
-
for row in selected:
|
| 385 |
-
clean = dict(row)
|
| 386 |
-
clean["docking_score"] = None
|
| 387 |
-
clean["docking_status"] = "not_run"
|
| 388 |
-
clean["docking_pose_file"] = None
|
| 389 |
-
clean["ligand_3d_file"] = clean.get("ligand_3d_file", None)
|
| 390 |
-
if "pred_pAff_mean" not in clean:
|
| 391 |
-
clean["pred_pAff_mean"] = None
|
| 392 |
-
clean_rows.append(clean)
|
| 393 |
-
|
| 394 |
-
clean_rows = add_ligand_ids_to_rows(clean_rows)
|
| 395 |
-
df = pd.DataFrame(clean_rows)
|
| 396 |
-
mode = "generate_job_cpu_demo"
|
| 397 |
-
status = "completed"
|
| 398 |
-
warnings.extend([
|
| 399 |
-
"This is a CPU demo generation response.",
|
| 400 |
-
"Live REINVENT generation was not executed because demo_rows was selected.",
|
| 401 |
-
"The response shape matches the planned backend contract.",
|
| 402 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 403 |
-
])
|
| 404 |
-
|
| 405 |
-
except Exception as e:
|
| 406 |
-
df = pd.DataFrame()
|
| 407 |
-
mode = "live_reinvent_cpu_sampling"
|
| 408 |
-
status = "failed"
|
| 409 |
-
warnings.append(str(e))
|
| 410 |
-
|
| 411 |
-
df = add_ligand_ids_to_df(df)
|
| 412 |
-
|
| 413 |
-
output_dir = Path("outputs")
|
| 414 |
-
output_dir.mkdir(exist_ok=True)
|
| 415 |
-
csv_path = output_dir / f"{job_id}_results.csv"
|
| 416 |
-
df.to_csv(csv_path, index=False)
|
| 417 |
-
|
| 418 |
-
job_summary = {
|
| 419 |
-
"job_id": job_id,
|
| 420 |
-
"status": status,
|
| 421 |
-
"mode": mode,
|
| 422 |
-
"created_at": created_at,
|
| 423 |
-
"preset": "egfr_generator",
|
| 424 |
-
"docking_mode": docking_mode,
|
| 425 |
-
"num_requested": num_molecules,
|
| 426 |
-
"num_returned": int(len(df)),
|
| 427 |
-
"download_csv_file": str(csv_path),
|
| 428 |
-
"live_generation": generation_backend == "live_reinvent_cpu",
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
response = {
|
| 432 |
-
"job_id": job_id,
|
| 433 |
-
"status": status,
|
| 434 |
-
"mode": mode,
|
| 435 |
-
"created_at": created_at,
|
| 436 |
-
"preset": "egfr_generator",
|
| 437 |
-
"docking_mode": docking_mode,
|
| 438 |
-
"summary": {
|
| 439 |
-
"num_requested": num_molecules,
|
| 440 |
-
"num_returned": int(len(df)),
|
| 441 |
-
},
|
| 442 |
-
"columns": list(df.columns),
|
| 443 |
-
"results": clean_records_from_df(df),
|
| 444 |
-
"download_csv_file": str(csv_path),
|
| 445 |
-
"warnings": warnings,
|
| 446 |
-
}
|
| 447 |
-
|
| 448 |
-
return job_summary, df, response, str(csv_path)
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
def smiles_to_3d_file(smiles: str, file_format: str = "sdf"):
|
| 452 |
-
smiles = str(smiles).strip()
|
| 453 |
-
file_format = str(file_format).lower().strip()
|
| 454 |
-
|
| 455 |
-
if file_format not in ["sdf", "mol", "pdb"]:
|
| 456 |
-
return None, {
|
| 457 |
-
"status": "error",
|
| 458 |
-
"message": "Unsupported format. Choose sdf, mol, or pdb.",
|
| 459 |
-
}
|
| 460 |
-
|
| 461 |
-
mol = Chem.MolFromSmiles(smiles)
|
| 462 |
-
if mol is None:
|
| 463 |
-
return None, {
|
| 464 |
-
"status": "error",
|
| 465 |
-
"input_smiles": smiles,
|
| 466 |
-
"message": "Invalid SMILES.",
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
canonical_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 470 |
-
|
| 471 |
-
mol = Chem.AddHs(mol)
|
| 472 |
-
|
| 473 |
-
params = AllChem.ETKDGv3()
|
| 474 |
-
params.randomSeed = 42
|
| 475 |
-
|
| 476 |
-
embed_status = AllChem.EmbedMolecule(mol, params)
|
| 477 |
-
if embed_status != 0:
|
| 478 |
-
return None, {
|
| 479 |
-
"status": "error",
|
| 480 |
-
"input_smiles": smiles,
|
| 481 |
-
"canonical_smiles": canonical_smiles,
|
| 482 |
-
"message": "3D embedding failed.",
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
try:
|
| 486 |
-
AllChem.UFFOptimizeMolecule(mol, maxIters=500)
|
| 487 |
-
optimization_status = "UFF optimized"
|
| 488 |
-
except Exception as e:
|
| 489 |
-
optimization_status = f"UFF optimization failed, using embedded geometry only: {e}"
|
| 490 |
-
|
| 491 |
-
out_dir = Path("outputs/ligands")
|
| 492 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 493 |
-
|
| 494 |
-
safe_name = "ligand_3d"
|
| 495 |
-
out_path = out_dir / f"{safe_name}.{file_format}"
|
| 496 |
-
|
| 497 |
-
if file_format == "sdf":
|
| 498 |
-
writer = Chem.SDWriter(str(out_path))
|
| 499 |
-
writer.write(mol)
|
| 500 |
-
writer.close()
|
| 501 |
-
elif file_format == "mol":
|
| 502 |
-
Chem.MolToMolFile(mol, str(out_path))
|
| 503 |
-
elif file_format == "pdb":
|
| 504 |
-
Chem.MolToPDBFile(mol, str(out_path))
|
| 505 |
-
|
| 506 |
-
meta = {
|
| 507 |
-
"status": "completed",
|
| 508 |
-
"input_smiles": smiles,
|
| 509 |
-
"canonical_smiles": canonical_smiles,
|
| 510 |
-
"file_format": file_format,
|
| 511 |
-
"file_path": str(out_path),
|
| 512 |
-
"optimization_status": optimization_status,
|
| 513 |
-
"note": "This is a generated 3D conformer from SMILES, not a docked protein-ligand pose.",
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
return str(out_path), meta
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
loaded_ok, loaded_msg = download_bundle_light()
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
with gr.Blocks(title="EGFR AI Demo") as demo:
|
| 523 |
-
gr.Markdown(
|
| 524 |
-
"""
|
| 525 |
-
# EGFR AI Demo
|
| 526 |
-
|
| 527 |
-
Private CPU demo for the EGFR AI backend workflow.
|
| 528 |
-
|
| 529 |
-
This demo focuses on the 3 things needed by filters, backend, and frontend:
|
| 530 |
-
|
| 531 |
-
1. Generated ligands returned as a list of objects.
|
| 532 |
-
2. CSV download for generated/scored ligands.
|
| 533 |
-
3. SMILES to downloadable 3D ligand file.
|
| 534 |
-
|
| 535 |
-
Real AutoDock-GPU docking is disabled in this CPU Space, but docking fields are kept in the response schema.
|
| 536 |
-
"""
|
| 537 |
-
)
|
| 538 |
-
|
| 539 |
-
gr.Markdown(f"**Bundle load status:** {loaded_msg}")
|
| 540 |
-
|
| 541 |
-
with gr.Tab("Generate Ligands"):
|
| 542 |
-
gr.Markdown(
|
| 543 |
-
"""
|
| 544 |
-
## Generate Ligands
|
| 545 |
-
|
| 546 |
-
This tab returns generated ligands as `results`, a list of ligand objects.
|
| 547 |
-
|
| 548 |
-
Each ligand object contains properties, affinity/docking placeholders, and stable IDs for filters/frontend.
|
| 549 |
-
"""
|
| 550 |
-
)
|
| 551 |
-
|
| 552 |
-
generation_backend = gr.Dropdown(
|
| 553 |
-
label="generation_backend",
|
| 554 |
-
choices=["demo_rows", "live_reinvent_cpu"],
|
| 555 |
-
value="demo_rows",
|
| 556 |
-
info="Use demo_rows for stable demo. live_reinvent_cpu tries REINVENT CPU sampling if available.",
|
| 557 |
-
)
|
| 558 |
-
|
| 559 |
-
gen_num_molecules = gr.Number(
|
| 560 |
-
label="num_molecules",
|
| 561 |
-
value=20,
|
| 562 |
-
precision=0,
|
| 563 |
-
)
|
| 564 |
-
|
| 565 |
-
gen_return_top_k = gr.Number(
|
| 566 |
-
label="return_top_k",
|
| 567 |
-
value=20,
|
| 568 |
-
precision=0,
|
| 569 |
-
)
|
| 570 |
-
|
| 571 |
-
gen_docking_mode = gr.Dropdown(
|
| 572 |
-
label="docking_mode",
|
| 573 |
-
choices=["off", "top_k", "all"],
|
| 574 |
-
value="off",
|
| 575 |
-
info="CPU Space does not run real docking. This field is included for backend/frontend contract.",
|
| 576 |
-
)
|
| 577 |
-
|
| 578 |
-
gen_btn = gr.Button("Run Generate Job")
|
| 579 |
-
|
| 580 |
-
gen_summary = gr.JSON(label="Job summary")
|
| 581 |
-
gen_table = gr.Dataframe(label="Generated ligand objects table")
|
| 582 |
-
gen_json = gr.JSON(label="Backend-style JSON response")
|
| 583 |
-
gen_file = gr.File(label="Download CSV")
|
| 584 |
-
|
| 585 |
-
gen_btn.click(
|
| 586 |
-
generate_job_demo,
|
| 587 |
-
inputs=[gen_num_molecules, gen_return_top_k, gen_docking_mode, generation_backend],
|
| 588 |
-
outputs=[gen_summary, gen_table, gen_json, gen_file],
|
| 589 |
-
)
|
| 590 |
-
|
| 591 |
-
with gr.Tab("Score SMILES & 3D File"):
|
| 592 |
-
gr.Markdown(
|
| 593 |
-
"""
|
| 594 |
-
## Score SMILES
|
| 595 |
-
|
| 596 |
-
Paste one or more SMILES. The response returns a list of ligand objects and a downloadable CSV file.
|
| 597 |
-
"""
|
| 598 |
-
)
|
| 599 |
-
|
| 600 |
-
smiles_input = gr.Textbox(
|
| 601 |
-
label="SMILES input",
|
| 602 |
-
value="CCO\nc1ccccc1",
|
| 603 |
-
lines=5,
|
| 604 |
-
)
|
| 605 |
-
|
| 606 |
-
score_btn = gr.Button("Score SMILES")
|
| 607 |
-
score_table = gr.Dataframe(label="Ligand objects table")
|
| 608 |
-
score_json = gr.JSON(label="Backend-style JSON response")
|
| 609 |
-
score_csv = gr.File(label="Download CSV")
|
| 610 |
-
|
| 611 |
-
score_btn.click(
|
| 612 |
-
score_smiles,
|
| 613 |
-
inputs=smiles_input,
|
| 614 |
-
outputs=[score_table, score_json, score_csv],
|
| 615 |
-
)
|
| 616 |
-
|
| 617 |
-
gr.Markdown(
|
| 618 |
-
"""
|
| 619 |
-
---
|
| 620 |
-
|
| 621 |
-
## SMILES to 3D Ligand File
|
| 622 |
-
|
| 623 |
-
Send one SMILES and download a generated 3D ligand file.
|
| 624 |
-
|
| 625 |
-
Supported formats:
|
| 626 |
-
- SDF
|
| 627 |
-
- MOL
|
| 628 |
-
- PDB
|
| 629 |
-
|
| 630 |
-
Important: this creates a 3D conformer from SMILES. It is not a docked pose inside the EGFR binding pocket.
|
| 631 |
-
"""
|
| 632 |
-
)
|
| 633 |
-
|
| 634 |
-
smiles_3d_input = gr.Textbox(
|
| 635 |
-
label="SMILES for 3D file",
|
| 636 |
-
value="CNCCC(=O)Nc1ccc2nncc(-c3ccc4cncnc4c3)c2c1",
|
| 637 |
-
lines=2,
|
| 638 |
-
)
|
| 639 |
-
|
| 640 |
-
format_input = gr.Radio(
|
| 641 |
-
choices=["sdf", "mol", "pdb"],
|
| 642 |
-
value="sdf",
|
| 643 |
-
label="Output file format",
|
| 644 |
-
)
|
| 645 |
-
|
| 646 |
-
make_3d_btn = gr.Button("Generate 3D ligand file")
|
| 647 |
-
ligand_file = gr.File(label="Download 3D ligand file")
|
| 648 |
-
ligand_meta = gr.JSON(label="3D file metadata")
|
| 649 |
-
|
| 650 |
-
make_3d_btn.click(
|
| 651 |
-
smiles_to_3d_file,
|
| 652 |
-
inputs=[smiles_3d_input, format_input],
|
| 653 |
-
outputs=[ligand_file, ligand_meta],
|
| 654 |
-
)
|
| 655 |
-
|
| 656 |
-
with gr.Tab("Contract & Notes"):
|
| 657 |
-
gr.Markdown(
|
| 658 |
-
"""
|
| 659 |
-
## Backend / Filters Contract
|
| 660 |
-
|
| 661 |
-
### Main response shape
|
| 662 |
-
|
| 663 |
-
The important part is:
|
| 664 |
-
|
| 665 |
-
```json
|
| 666 |
-
{
|
| 667 |
-
"results": [
|
| 668 |
-
{
|
| 669 |
-
"ligand_id": "lig_0001",
|
| 670 |
-
"SMILES": "...",
|
| 671 |
-
"canonical_smiles": "...",
|
| 672 |
-
"mw": 358.405,
|
| 673 |
-
"logp": 2.788,
|
| 674 |
-
"tpsa": 92.69,
|
| 675 |
-
"hbd": 2,
|
| 676 |
-
"hba": 6,
|
| 677 |
-
"rot_bonds": 5,
|
| 678 |
-
"qed": 0.5698,
|
| 679 |
-
"sa_score": 2.5204,
|
| 680 |
-
"pred_pAff_mean": null,
|
| 681 |
-
"docking_score": null,
|
| 682 |
-
"docking_status": "not_run",
|
| 683 |
-
"docking_pose_file": null,
|
| 684 |
-
"ligand_3d_file": null
|
| 685 |
-
}
|
| 686 |
-
]
|
| 687 |
-
}
|
| 688 |
-
```
|
| 689 |
-
|
| 690 |
-
### CSV
|
| 691 |
-
|
| 692 |
-
Each Generate or Score action returns a downloadable CSV file.
|
| 693 |
-
|
| 694 |
-
### 3D ligand file
|
| 695 |
-
|
| 696 |
-
The SMILES to 3D tool returns a downloadable SDF/MOL/PDB file.
|
| 697 |
-
|
| 698 |
-
### Docking modes in the full backend
|
| 699 |
-
|
| 700 |
-
- `off`
|
| 701 |
-
- `top_k`
|
| 702 |
-
- `all`
|
| 703 |
-
|
| 704 |
-
In this CPU Space:
|
| 705 |
-
- `docking_score = null`
|
| 706 |
-
- `docking_status = not_run`
|
| 707 |
-
- `docking_pose_file = null`
|
| 708 |
-
|
| 709 |
-
### Recommended DB columns
|
| 710 |
-
|
| 711 |
-
- `docking_score FLOAT NULL`
|
| 712 |
-
- `docking_status VARCHAR(30) DEFAULT 'not_run'`
|
| 713 |
-
- `docking_pose_file TEXT NULL`
|
| 714 |
-
- `docking_mode VARCHAR(20) DEFAULT 'off'`
|
| 715 |
-
- `docking_error TEXT NULL`
|
| 716 |
-
"""
|
| 717 |
-
)
|
| 718 |
-
|
| 719 |
-
contract_btn = gr.Button("Load backend_contract.json")
|
| 720 |
-
contract_json = gr.JSON(label="backend_contract.json")
|
| 721 |
-
|
| 722 |
-
contract_btn.click(
|
| 723 |
-
get_contract,
|
| 724 |
-
outputs=contract_json,
|
| 725 |
-
)
|
| 726 |
-
|
| 727 |
-
spec_btn = gr.Button("Load API_RESPONSE_SPEC.md")
|
| 728 |
-
spec_output = gr.Markdown(label="API_RESPONSE_SPEC.md")
|
| 729 |
-
|
| 730 |
-
spec_btn.click(
|
| 731 |
-
get_api_spec,
|
| 732 |
-
outputs=spec_output,
|
| 733 |
-
)
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py.backup_before_final_ui
DELETED
|
@@ -1,754 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import os
|
| 3 |
-
import subprocess
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
import uuid
|
| 6 |
-
from datetime import datetime
|
| 7 |
-
import gradio as gr
|
| 8 |
-
import pandas as pd
|
| 9 |
-
from huggingface_hub import snapshot_download
|
| 10 |
-
from rdkit import Chem
|
| 11 |
-
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED, AllChem
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
BUNDLE_REPO = os.getenv("BUNDLE_REPO", "abdullahsamra/egfr-ai-bundle")
|
| 15 |
-
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 16 |
-
|
| 17 |
-
CACHE_DIR = Path("bundle_cache")
|
| 18 |
-
EXAMPLE_JSON = None
|
| 19 |
-
API_SPEC_TEXT = None
|
| 20 |
-
BACKEND_CONTRACT_TEXT = None
|
| 21 |
-
BUNDLE_ROOT = None
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def download_bundle_light():
|
| 25 |
-
global EXAMPLE_JSON, API_SPEC_TEXT, BACKEND_CONTRACT_TEXT, BUNDLE_ROOT
|
| 26 |
-
|
| 27 |
-
try:
|
| 28 |
-
local_dir = snapshot_download(
|
| 29 |
-
repo_id=BUNDLE_REPO,
|
| 30 |
-
repo_type="dataset",
|
| 31 |
-
token=HF_TOKEN,
|
| 32 |
-
allow_patterns=[
|
| 33 |
-
"examples/generate_response.example.json",
|
| 34 |
-
"docs/API_RESPONSE_SPEC.md",
|
| 35 |
-
"configs/backend_contract.json",
|
| 36 |
-
"configs/sampling_model.bundle.toml",
|
| 37 |
-
"models/generator/egfr_generator.chkpt",
|
| 38 |
-
"README.md",
|
| 39 |
-
],
|
| 40 |
-
local_dir=str(CACHE_DIR),
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
root = Path(local_dir)
|
| 44 |
-
BUNDLE_ROOT = root
|
| 45 |
-
|
| 46 |
-
example_path = root / "examples" / "generate_response.example.json"
|
| 47 |
-
spec_path = root / "docs" / "API_RESPONSE_SPEC.md"
|
| 48 |
-
contract_path = root / "configs" / "backend_contract.json"
|
| 49 |
-
|
| 50 |
-
if example_path.exists():
|
| 51 |
-
EXAMPLE_JSON = json.loads(example_path.read_text(encoding="utf-8"))
|
| 52 |
-
|
| 53 |
-
if spec_path.exists():
|
| 54 |
-
API_SPEC_TEXT = spec_path.read_text(encoding="utf-8")
|
| 55 |
-
|
| 56 |
-
if contract_path.exists():
|
| 57 |
-
BACKEND_CONTRACT_TEXT = contract_path.read_text(encoding="utf-8")
|
| 58 |
-
|
| 59 |
-
return True, f"Loaded docs/examples from {BUNDLE_REPO}"
|
| 60 |
-
|
| 61 |
-
except Exception as e:
|
| 62 |
-
return False, f"Could not load private bundle. Check HF_TOKEN secret and BUNDLE_REPO variable. Error: {e}"
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def canonicalize(smiles: str):
|
| 66 |
-
mol = Chem.MolFromSmiles(str(smiles).strip())
|
| 67 |
-
if mol is None:
|
| 68 |
-
return None, None
|
| 69 |
-
return mol, Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def try_sa_score(mol):
|
| 73 |
-
try:
|
| 74 |
-
from rdkit.Contrib.SA_Score import sascorer
|
| 75 |
-
return round(float(sascorer.calculateScore(mol)), 4)
|
| 76 |
-
except Exception:
|
| 77 |
-
return None
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def compute_row(smiles: str):
|
| 81 |
-
mol, can = canonicalize(smiles)
|
| 82 |
-
|
| 83 |
-
if mol is None:
|
| 84 |
-
return {
|
| 85 |
-
"SMILES": smiles,
|
| 86 |
-
"valid": False,
|
| 87 |
-
"canonical_smiles": None,
|
| 88 |
-
"mw": None,
|
| 89 |
-
"logp": None,
|
| 90 |
-
"tpsa": None,
|
| 91 |
-
"hbd": None,
|
| 92 |
-
"hba": None,
|
| 93 |
-
"rot_bonds": None,
|
| 94 |
-
"qed": None,
|
| 95 |
-
"sa_score": None,
|
| 96 |
-
"pred_pAff_mean": None,
|
| 97 |
-
"docking_score": None,
|
| 98 |
-
"docking_status": "not_run",
|
| 99 |
-
"docking_pose_file": None,
|
| 100 |
-
"note": "Invalid SMILES",
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
return {
|
| 104 |
-
"SMILES": smiles,
|
| 105 |
-
"valid": True,
|
| 106 |
-
"canonical_smiles": can,
|
| 107 |
-
"mw": round(float(Descriptors.MolWt(mol)), 4),
|
| 108 |
-
"logp": round(float(Crippen.MolLogP(mol)), 4),
|
| 109 |
-
"tpsa": round(float(rdMolDescriptors.CalcTPSA(mol)), 4),
|
| 110 |
-
"hbd": int(Lipinski.NumHDonors(mol)),
|
| 111 |
-
"hba": int(Lipinski.NumHAcceptors(mol)),
|
| 112 |
-
"rot_bonds": int(Lipinski.NumRotatableBonds(mol)),
|
| 113 |
-
"qed": round(float(QED.qed(mol)), 4),
|
| 114 |
-
"sa_score": try_sa_score(mol),
|
| 115 |
-
"pred_pAff_mean": None,
|
| 116 |
-
"docking_score": None,
|
| 117 |
-
"docking_status": "not_run",
|
| 118 |
-
"docking_pose_file": None,
|
| 119 |
-
"note": "CPU demo: DeepPurpose and real docking are disabled in this Space.",
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
def add_ligand_ids_to_rows(rows, prefix="lig"):
|
| 125 |
-
output = []
|
| 126 |
-
for i, row in enumerate(rows, start=1):
|
| 127 |
-
clean = dict(row)
|
| 128 |
-
if not clean.get("ligand_id"):
|
| 129 |
-
clean["ligand_id"] = f"{prefix}_{i:04d}"
|
| 130 |
-
output.append(clean)
|
| 131 |
-
return output
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def add_ligand_ids_to_df(df, prefix="lig"):
|
| 135 |
-
df = df.copy()
|
| 136 |
-
if "ligand_id" not in df.columns:
|
| 137 |
-
df.insert(0, "ligand_id", [f"{prefix}_{i:04d}" for i in range(1, len(df) + 1)])
|
| 138 |
-
return df
|
| 139 |
-
|
| 140 |
-
def score_smiles(smiles_text):
|
| 141 |
-
smiles_list = []
|
| 142 |
-
for part in str(smiles_text).replace(",", "\n").splitlines():
|
| 143 |
-
part = part.strip()
|
| 144 |
-
if part:
|
| 145 |
-
smiles_list.append(part)
|
| 146 |
-
|
| 147 |
-
if not smiles_list:
|
| 148 |
-
return pd.DataFrame(), {
|
| 149 |
-
"status": "error",
|
| 150 |
-
"message": "Please enter at least one SMILES.",
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
rows = add_ligand_ids_to_rows([compute_row(s) for s in smiles_list])
|
| 154 |
-
df = pd.DataFrame(rows)
|
| 155 |
-
|
| 156 |
-
response = {
|
| 157 |
-
"status": "completed",
|
| 158 |
-
"mode": "score_smiles_cpu_demo",
|
| 159 |
-
"docking_mode": "off",
|
| 160 |
-
"summary": {
|
| 161 |
-
"num_input": len(smiles_list),
|
| 162 |
-
"num_valid": int(df["valid"].sum()),
|
| 163 |
-
"num_returned": len(df),
|
| 164 |
-
},
|
| 165 |
-
"columns": list(df.columns),
|
| 166 |
-
"results": rows,
|
| 167 |
-
"warnings": [
|
| 168 |
-
"This Space is a CPU demo.",
|
| 169 |
-
"DeepPurpose pred_pAff_mean is not computed in this Space version.",
|
| 170 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 171 |
-
"Outputs are computational predictions only.",
|
| 172 |
-
],
|
| 173 |
-
}
|
| 174 |
-
csv_path = write_results_csv(df)
|
| 175 |
-
response["download_csv_file"] = csv_path
|
| 176 |
-
return df, response, csv_path
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
def get_example_table():
|
| 180 |
-
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 181 |
-
return pd.DataFrame(EXAMPLE_JSON["results"])
|
| 182 |
-
return pd.DataFrame([
|
| 183 |
-
{"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."}
|
| 184 |
-
])
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def get_example_json():
|
| 188 |
-
if EXAMPLE_JSON:
|
| 189 |
-
return EXAMPLE_JSON
|
| 190 |
-
return {
|
| 191 |
-
"status": "not_loaded",
|
| 192 |
-
"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
def get_api_spec():
|
| 197 |
-
return API_SPEC_TEXT or "API_RESPONSE_SPEC.md not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
def get_contract():
|
| 201 |
-
if BACKEND_CONTRACT_TEXT:
|
| 202 |
-
try:
|
| 203 |
-
return json.loads(BACKEND_CONTRACT_TEXT)
|
| 204 |
-
except Exception:
|
| 205 |
-
return {"raw": BACKEND_CONTRACT_TEXT}
|
| 206 |
-
|
| 207 |
-
return {
|
| 208 |
-
"status": "not_loaded",
|
| 209 |
-
"message": "backend_contract.json not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
def _write_reinvent_sampling_config(job_id, num_molecules):
|
| 213 |
-
if BUNDLE_ROOT is None:
|
| 214 |
-
raise RuntimeError("Bundle root is not loaded. Check HF_TOKEN and BUNDLE_REPO.")
|
| 215 |
-
|
| 216 |
-
checkpoint = Path(BUNDLE_ROOT) / "models" / "generator" / "egfr_generator.chkpt"
|
| 217 |
-
if not checkpoint.exists():
|
| 218 |
-
raise FileNotFoundError(f"Generator checkpoint not found: {checkpoint}")
|
| 219 |
-
|
| 220 |
-
output_dir = Path("outputs")
|
| 221 |
-
output_dir.mkdir(exist_ok=True)
|
| 222 |
-
|
| 223 |
-
raw_csv = output_dir / f"{job_id}_raw_sampling.csv"
|
| 224 |
-
log_file = output_dir / f"{job_id}_reinvent.log"
|
| 225 |
-
resolved_json = output_dir / f"{job_id}_sampling_resolved.json"
|
| 226 |
-
config_path = output_dir / f"{job_id}_sampling.toml"
|
| 227 |
-
|
| 228 |
-
config_text = f"""
|
| 229 |
-
run_type = "sampling"
|
| 230 |
-
device = "cpu"
|
| 231 |
-
|
| 232 |
-
json_out_config = "{resolved_json.resolve()}"
|
| 233 |
-
|
| 234 |
-
[parameters]
|
| 235 |
-
model_file = "{checkpoint.resolve()}"
|
| 236 |
-
output_file = "{raw_csv.resolve()}"
|
| 237 |
-
|
| 238 |
-
num_smiles = {int(num_molecules)}
|
| 239 |
-
unique_molecules = true
|
| 240 |
-
randomize_smiles = true
|
| 241 |
-
"""
|
| 242 |
-
|
| 243 |
-
config_path.write_text(config_text.strip() + "\n", encoding="utf-8")
|
| 244 |
-
return config_path, raw_csv, log_file
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
def _run_reinvent_sampling(job_id, num_molecules):
|
| 248 |
-
config_path, raw_csv, log_file = _write_reinvent_sampling_config(job_id, num_molecules)
|
| 249 |
-
|
| 250 |
-
cmd = [
|
| 251 |
-
"reinvent",
|
| 252 |
-
"-l",
|
| 253 |
-
str(log_file.resolve()),
|
| 254 |
-
str(config_path.resolve()),
|
| 255 |
-
]
|
| 256 |
-
|
| 257 |
-
proc = subprocess.run(
|
| 258 |
-
cmd,
|
| 259 |
-
text=True,
|
| 260 |
-
stdout=subprocess.PIPE,
|
| 261 |
-
stderr=subprocess.STDOUT,
|
| 262 |
-
timeout=300,
|
| 263 |
-
)
|
| 264 |
-
|
| 265 |
-
if proc.returncode != 0:
|
| 266 |
-
raise RuntimeError(
|
| 267 |
-
"REINVENT sampling failed.\n"
|
| 268 |
-
f"Command: {' '.join(cmd)}\n"
|
| 269 |
-
f"Output:\n{proc.stdout[-4000:]}"
|
| 270 |
-
)
|
| 271 |
-
|
| 272 |
-
if not raw_csv.exists():
|
| 273 |
-
raise RuntimeError(f"REINVENT finished but output CSV was not found: {raw_csv}")
|
| 274 |
-
|
| 275 |
-
return raw_csv, log_file
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
def _enrich_reinvent_csv(raw_csv, return_top_k):
|
| 279 |
-
raw_df = pd.read_csv(raw_csv)
|
| 280 |
-
|
| 281 |
-
if "SMILES" not in raw_df.columns:
|
| 282 |
-
raise ValueError(f"REINVENT output missing SMILES column. Columns: {list(raw_df.columns)}")
|
| 283 |
-
|
| 284 |
-
rows = []
|
| 285 |
-
for _, r in raw_df.iterrows():
|
| 286 |
-
row = compute_row(str(r["SMILES"]))
|
| 287 |
-
|
| 288 |
-
if "SMILES_state" in raw_df.columns:
|
| 289 |
-
try:
|
| 290 |
-
row["SMILES_state"] = int(r["SMILES_state"])
|
| 291 |
-
except Exception:
|
| 292 |
-
row["SMILES_state"] = None
|
| 293 |
-
else:
|
| 294 |
-
row["SMILES_state"] = None
|
| 295 |
-
|
| 296 |
-
if "NLL" in raw_df.columns:
|
| 297 |
-
try:
|
| 298 |
-
row["NLL"] = float(r["NLL"])
|
| 299 |
-
except Exception:
|
| 300 |
-
row["NLL"] = None
|
| 301 |
-
else:
|
| 302 |
-
row["NLL"] = None
|
| 303 |
-
|
| 304 |
-
# Keep these stable for frontend/backend filters.
|
| 305 |
-
row["pred_pAff_mean"] = None
|
| 306 |
-
row["docking_score"] = None
|
| 307 |
-
row["docking_status"] = "not_run"
|
| 308 |
-
row["docking_pose_file"] = None
|
| 309 |
-
|
| 310 |
-
rows.append(row)
|
| 311 |
-
|
| 312 |
-
df = pd.DataFrame(rows)
|
| 313 |
-
|
| 314 |
-
preferred_cols = [
|
| 315 |
-
"SMILES",
|
| 316 |
-
"SMILES_state",
|
| 317 |
-
"NLL",
|
| 318 |
-
"valid",
|
| 319 |
-
"canonical_smiles",
|
| 320 |
-
"mw",
|
| 321 |
-
"logp",
|
| 322 |
-
"tpsa",
|
| 323 |
-
"hbd",
|
| 324 |
-
"hba",
|
| 325 |
-
"rot_bonds",
|
| 326 |
-
"qed",
|
| 327 |
-
"sa_score",
|
| 328 |
-
"pred_pAff_mean",
|
| 329 |
-
"docking_score",
|
| 330 |
-
"docking_status",
|
| 331 |
-
"docking_pose_file",
|
| 332 |
-
"note",
|
| 333 |
-
]
|
| 334 |
-
|
| 335 |
-
cols = [c for c in preferred_cols if c in df.columns] + [c for c in df.columns if c not in preferred_cols]
|
| 336 |
-
df = df[cols]
|
| 337 |
-
|
| 338 |
-
return_top_k = int(return_top_k)
|
| 339 |
-
if return_top_k > 0:
|
| 340 |
-
df = df.head(return_top_k)
|
| 341 |
-
|
| 342 |
-
return df
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_backend):
|
| 346 |
-
"""
|
| 347 |
-
Generation endpoint for the Space.
|
| 348 |
-
|
| 349 |
-
Demo mode uses packaged example rows.
|
| 350 |
-
Live mode tries to run REINVENT CPU sampling from the packaged checkpoint.
|
| 351 |
-
"""
|
| 352 |
-
job_id = f"gen_{uuid.uuid4().hex[:8]}"
|
| 353 |
-
created_at = datetime.utcnow().isoformat() + "Z"
|
| 354 |
-
num_molecules = int(num_molecules)
|
| 355 |
-
return_top_k = int(return_top_k)
|
| 356 |
-
|
| 357 |
-
warnings = []
|
| 358 |
-
|
| 359 |
-
try:
|
| 360 |
-
if generation_backend == "live_reinvent_cpu":
|
| 361 |
-
raw_csv, log_file = _run_reinvent_sampling(job_id, num_molecules)
|
| 362 |
-
df = _enrich_reinvent_csv(raw_csv, return_top_k)
|
| 363 |
-
mode = "live_reinvent_cpu_sampling"
|
| 364 |
-
status = "completed"
|
| 365 |
-
warnings.extend([
|
| 366 |
-
"Live REINVENT CPU sampling was executed in this Space.",
|
| 367 |
-
"DeepPurpose scoring is not enabled in this Space version; pred_pAff_mean is null.",
|
| 368 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 369 |
-
])
|
| 370 |
-
|
| 371 |
-
else:
|
| 372 |
-
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 373 |
-
rows = EXAMPLE_JSON["results"]
|
| 374 |
-
else:
|
| 375 |
-
rows = [compute_row("CCO"), compute_row("c1ccccc1")]
|
| 376 |
-
|
| 377 |
-
selected = rows[: max(1, min(return_top_k, len(rows)))]
|
| 378 |
-
|
| 379 |
-
clean_rows = []
|
| 380 |
-
for row in selected:
|
| 381 |
-
clean = dict(row)
|
| 382 |
-
clean["docking_score"] = None
|
| 383 |
-
clean["docking_status"] = "not_run"
|
| 384 |
-
clean["docking_pose_file"] = None
|
| 385 |
-
if "pred_pAff_mean" not in clean:
|
| 386 |
-
clean["pred_pAff_mean"] = None
|
| 387 |
-
clean_rows.append(clean)
|
| 388 |
-
|
| 389 |
-
df = pd.DataFrame(clean_rows)
|
| 390 |
-
mode = "generate_job_cpu_demo"
|
| 391 |
-
status = "completed"
|
| 392 |
-
warnings.extend([
|
| 393 |
-
"This is a CPU demo generation response.",
|
| 394 |
-
"Live REINVENT generation was not executed because demo mode was selected.",
|
| 395 |
-
"The response shape matches the planned backend contract.",
|
| 396 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 397 |
-
])
|
| 398 |
-
|
| 399 |
-
except Exception as e:
|
| 400 |
-
df = pd.DataFrame()
|
| 401 |
-
mode = "live_reinvent_cpu_sampling"
|
| 402 |
-
status = "failed"
|
| 403 |
-
warnings.append(str(e))
|
| 404 |
-
|
| 405 |
-
df = add_ligand_ids_to_df(df)
|
| 406 |
-
|
| 407 |
-
output_dir = Path("outputs")
|
| 408 |
-
output_dir.mkdir(exist_ok=True)
|
| 409 |
-
csv_path = output_dir / f"{job_id}_results.csv"
|
| 410 |
-
df.to_csv(csv_path, index=False)
|
| 411 |
-
|
| 412 |
-
job_summary = {
|
| 413 |
-
"job_id": job_id,
|
| 414 |
-
"status": status,
|
| 415 |
-
"mode": mode,
|
| 416 |
-
"created_at": created_at,
|
| 417 |
-
"preset": "egfr_generator",
|
| 418 |
-
"docking_mode": docking_mode,
|
| 419 |
-
"num_requested": num_molecules,
|
| 420 |
-
"num_returned": int(len(df)),
|
| 421 |
-
"download_csv_file": str(csv_path),
|
| 422 |
-
"live_generation": generation_backend == "live_reinvent_cpu",
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
response = {
|
| 426 |
-
"job_id": job_id,
|
| 427 |
-
"status": status,
|
| 428 |
-
"mode": mode,
|
| 429 |
-
"created_at": created_at,
|
| 430 |
-
"preset": "egfr_generator",
|
| 431 |
-
"docking_mode": docking_mode,
|
| 432 |
-
"summary": {
|
| 433 |
-
"num_requested": num_molecules,
|
| 434 |
-
"num_returned": int(len(df)),
|
| 435 |
-
},
|
| 436 |
-
"columns": list(df.columns),
|
| 437 |
-
"results": df.to_dict(orient="records"),
|
| 438 |
-
"download_csv_file": str(csv_path),
|
| 439 |
-
"warnings": warnings,
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
return job_summary, df, response, str(csv_path)
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
def write_results_csv(df: pd.DataFrame, filename: str = "score_smiles_results.csv"):
|
| 446 |
-
out_dir = Path("outputs")
|
| 447 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 448 |
-
out_path = out_dir / filename
|
| 449 |
-
df.to_csv(out_path, index=False)
|
| 450 |
-
return str(out_path)
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
def smiles_to_3d_file(smiles: str, file_format: str = "sdf"):
|
| 454 |
-
smiles = str(smiles).strip()
|
| 455 |
-
file_format = str(file_format).lower().strip()
|
| 456 |
-
|
| 457 |
-
if file_format not in ["sdf", "mol", "pdb"]:
|
| 458 |
-
return None, {
|
| 459 |
-
"status": "error",
|
| 460 |
-
"message": "Unsupported format. Choose sdf, mol, or pdb.",
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
mol = Chem.MolFromSmiles(smiles)
|
| 464 |
-
if mol is None:
|
| 465 |
-
return None, {
|
| 466 |
-
"status": "error",
|
| 467 |
-
"input_smiles": smiles,
|
| 468 |
-
"message": "Invalid SMILES.",
|
| 469 |
-
}
|
| 470 |
-
|
| 471 |
-
canonical_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 472 |
-
|
| 473 |
-
mol = Chem.AddHs(mol)
|
| 474 |
-
|
| 475 |
-
params = AllChem.ETKDGv3()
|
| 476 |
-
params.randomSeed = 42
|
| 477 |
-
|
| 478 |
-
embed_status = AllChem.EmbedMolecule(mol, params)
|
| 479 |
-
if embed_status != 0:
|
| 480 |
-
return None, {
|
| 481 |
-
"status": "error",
|
| 482 |
-
"input_smiles": smiles,
|
| 483 |
-
"canonical_smiles": canonical_smiles,
|
| 484 |
-
"message": "3D embedding failed.",
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
try:
|
| 488 |
-
AllChem.UFFOptimizeMolecule(mol, maxIters=500)
|
| 489 |
-
optimization_status = "UFF optimized"
|
| 490 |
-
except Exception as e:
|
| 491 |
-
optimization_status = f"UFF optimization failed, using embedded geometry only: {e}"
|
| 492 |
-
|
| 493 |
-
out_dir = Path("outputs/ligands")
|
| 494 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 495 |
-
|
| 496 |
-
safe_name = "ligand_3d"
|
| 497 |
-
out_path = out_dir / f"{safe_name}.{file_format}"
|
| 498 |
-
|
| 499 |
-
if file_format == "sdf":
|
| 500 |
-
writer = Chem.SDWriter(str(out_path))
|
| 501 |
-
writer.write(mol)
|
| 502 |
-
writer.close()
|
| 503 |
-
elif file_format == "mol":
|
| 504 |
-
Chem.MolToMolFile(mol, str(out_path))
|
| 505 |
-
elif file_format == "pdb":
|
| 506 |
-
Chem.MolToPDBFile(mol, str(out_path))
|
| 507 |
-
|
| 508 |
-
meta = {
|
| 509 |
-
"status": "completed",
|
| 510 |
-
"input_smiles": smiles,
|
| 511 |
-
"canonical_smiles": canonical_smiles,
|
| 512 |
-
"file_format": file_format,
|
| 513 |
-
"file_path": str(out_path),
|
| 514 |
-
"optimization_status": optimization_status,
|
| 515 |
-
"note": "This is a generated 3D conformer from SMILES, not a docked protein-ligand pose.",
|
| 516 |
-
}
|
| 517 |
-
|
| 518 |
-
return str(out_path), meta
|
| 519 |
-
|
| 520 |
-
loaded_ok, loaded_msg = download_bundle_light()
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
with gr.Blocks(title="EGFR AI Demo") as demo:
|
| 524 |
-
gr.Markdown(
|
| 525 |
-
"""
|
| 526 |
-
# EGFR AI Demo
|
| 527 |
-
|
| 528 |
-
Private CPU demo for the EGFR AI backend workflow.
|
| 529 |
-
|
| 530 |
-
This demo shows the backend response shape for generation, scoring, filtering, and docking integration.
|
| 531 |
-
|
| 532 |
-
Real AutoDock-GPU docking is disabled in this CPU Space, but docking fields are present so backend/frontend teams can implement database columns and filters.
|
| 533 |
-
"""
|
| 534 |
-
)
|
| 535 |
-
|
| 536 |
-
gr.Markdown(f"Bundle load status: {loaded_msg}")
|
| 537 |
-
|
| 538 |
-
with gr.Tab("Score SMILES"):
|
| 539 |
-
gr.Markdown(
|
| 540 |
-
"""
|
| 541 |
-
Enter one or more SMILES.
|
| 542 |
-
|
| 543 |
-
This CPU demo calculates molecular properties and returns the same response columns expected by the backend.
|
| 544 |
-
|
| 545 |
-
Important:
|
| 546 |
-
- pred_pAff_mean is an integration field in this Space version.
|
| 547 |
-
- real docking is disabled in this CPU Space.
|
| 548 |
-
- docking_status is returned as not_run.
|
| 549 |
-
"""
|
| 550 |
-
)
|
| 551 |
-
|
| 552 |
-
smiles_input = gr.Textbox(
|
| 553 |
-
label="SMILES input",
|
| 554 |
-
value="CCO\nc1ccccc1",
|
| 555 |
-
lines=5,
|
| 556 |
-
)
|
| 557 |
-
|
| 558 |
-
score_btn = gr.Button("Score SMILES")
|
| 559 |
-
score_table = gr.Dataframe(label="Results table")
|
| 560 |
-
score_json = gr.JSON(label="Backend-style JSON response")
|
| 561 |
-
score_csv = gr.File(label="Download CSV")
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
score_btn.click(
|
| 565 |
-
score_smiles,
|
| 566 |
-
inputs=smiles_input,
|
| 567 |
-
outputs=[score_table, score_json, score_csv],
|
| 568 |
-
)
|
| 569 |
-
with gr.Tab("Generate Job Demo"):
|
| 570 |
-
gr.Markdown(
|
| 571 |
-
"""
|
| 572 |
-
This tab demonstrates generation job behavior.
|
| 573 |
-
|
| 574 |
-
Generation backend options:
|
| 575 |
-
|
| 576 |
-
- demo_rows: uses packaged example rows. Fast and stable.
|
| 577 |
-
- live_reinvent_cpu: tries to run real REINVENT sampling from the packaged checkpoint on CPU.
|
| 578 |
-
|
| 579 |
-
The output is split into:
|
| 580 |
-
|
| 581 |
-
1. Job summary: job_id, status, preset, mode, and metadata.
|
| 582 |
-
2. Generated molecule results table.
|
| 583 |
-
3. Full backend-style generation JSON.
|
| 584 |
-
4. Download CSV.
|
| 585 |
-
|
| 586 |
-
Notes:
|
| 587 |
-
- DeepPurpose live scoring is not enabled yet in this Space version.
|
| 588 |
-
- Real docking is disabled in this CPU Space, so docking_status is returned as not_run.
|
| 589 |
-
"""
|
| 590 |
-
)
|
| 591 |
-
|
| 592 |
-
generation_backend = gr.Dropdown(
|
| 593 |
-
label="generation_backend",
|
| 594 |
-
choices=["demo_rows", "live_reinvent_cpu"],
|
| 595 |
-
value="demo_rows",
|
| 596 |
-
)
|
| 597 |
-
|
| 598 |
-
gen_num_molecules = gr.Number(
|
| 599 |
-
label="num_molecules",
|
| 600 |
-
value=20,
|
| 601 |
-
precision=0,
|
| 602 |
-
)
|
| 603 |
-
|
| 604 |
-
gen_return_top_k = gr.Number(
|
| 605 |
-
label="return_top_k",
|
| 606 |
-
value=20,
|
| 607 |
-
precision=0,
|
| 608 |
-
)
|
| 609 |
-
|
| 610 |
-
gen_docking_mode = gr.Dropdown(
|
| 611 |
-
label="docking_mode",
|
| 612 |
-
choices=["off", "top_k", "all"],
|
| 613 |
-
value="off",
|
| 614 |
-
)
|
| 615 |
-
|
| 616 |
-
gen_btn = gr.Button("Run Generation Job")
|
| 617 |
-
|
| 618 |
-
gen_summary = gr.JSON(label="Job summary")
|
| 619 |
-
gen_table = gr.Dataframe(label="Generated molecule results table")
|
| 620 |
-
gen_json = gr.JSON(label="Full backend-style generation JSON")
|
| 621 |
-
gen_file = gr.File(label="Download CSV")
|
| 622 |
-
|
| 623 |
-
gen_btn.click(
|
| 624 |
-
generate_job_demo,
|
| 625 |
-
inputs=[gen_num_molecules, gen_return_top_k, gen_docking_mode, generation_backend],
|
| 626 |
-
outputs=[gen_summary, gen_table, gen_json, gen_file],
|
| 627 |
-
)
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
with gr.Tab("Example Generate Response"):
|
| 631 |
-
gr.Markdown(
|
| 632 |
-
"""
|
| 633 |
-
This example is loaded from the private Hugging Face Dataset bundle.
|
| 634 |
-
|
| 635 |
-
It shows the expected response shape after:
|
| 636 |
-
|
| 637 |
-
Generate -> Enrich -> Optional Docking
|
| 638 |
-
"""
|
| 639 |
-
)
|
| 640 |
-
|
| 641 |
-
example_btn = gr.Button("Load example response")
|
| 642 |
-
example_table = gr.Dataframe(label="Example results table")
|
| 643 |
-
example_json = gr.JSON(label="Example JSON response")
|
| 644 |
-
|
| 645 |
-
example_btn.click(
|
| 646 |
-
lambda: (get_example_table(), get_example_json()),
|
| 647 |
-
outputs=[example_table, example_json],
|
| 648 |
-
)
|
| 649 |
-
|
| 650 |
-
with gr.Tab("SMILES to 3D File"):
|
| 651 |
-
gr.Markdown(
|
| 652 |
-
"""
|
| 653 |
-
Convert a SMILES string into a downloadable 3D ligand file.
|
| 654 |
-
|
| 655 |
-
Supported formats:
|
| 656 |
-
|
| 657 |
-
- SDF
|
| 658 |
-
- MOL
|
| 659 |
-
- PDB
|
| 660 |
-
|
| 661 |
-
Important: this creates a 3D conformer from SMILES. It is not a docked pose inside the EGFR binding pocket.
|
| 662 |
-
"""
|
| 663 |
-
)
|
| 664 |
-
|
| 665 |
-
smiles_3d_input = gr.Textbox(
|
| 666 |
-
label="SMILES",
|
| 667 |
-
value="CCO",
|
| 668 |
-
lines=2,
|
| 669 |
-
)
|
| 670 |
-
|
| 671 |
-
format_input = gr.Radio(
|
| 672 |
-
choices=["sdf", "mol", "pdb"],
|
| 673 |
-
value="sdf",
|
| 674 |
-
label="Output file format",
|
| 675 |
-
)
|
| 676 |
-
|
| 677 |
-
make_3d_btn = gr.Button("Generate 3D ligand file")
|
| 678 |
-
ligand_file = gr.File(label="Download 3D ligand file")
|
| 679 |
-
ligand_meta = gr.JSON(label="3D file metadata")
|
| 680 |
-
|
| 681 |
-
make_3d_btn.click(
|
| 682 |
-
smiles_to_3d_file,
|
| 683 |
-
inputs=[smiles_3d_input, format_input],
|
| 684 |
-
outputs=[ligand_file, ligand_meta],
|
| 685 |
-
)
|
| 686 |
-
|
| 687 |
-
with gr.Tab("Docking Contract"):
|
| 688 |
-
gr.Markdown(
|
| 689 |
-
"""
|
| 690 |
-
## Docking modes
|
| 691 |
-
|
| 692 |
-
The full backend supports:
|
| 693 |
-
|
| 694 |
-
- off
|
| 695 |
-
- top_k
|
| 696 |
-
- all
|
| 697 |
-
|
| 698 |
-
In this CPU Space, real AutoDock-GPU docking is disabled.
|
| 699 |
-
|
| 700 |
-
Returned docking fields in CPU demo:
|
| 701 |
-
|
| 702 |
-
- docking_score: null
|
| 703 |
-
- docking_status: not_run
|
| 704 |
-
- docking_pose_file: null
|
| 705 |
-
|
| 706 |
-
Recommended database columns:
|
| 707 |
-
|
| 708 |
-
- docking_score FLOAT NULL
|
| 709 |
-
- docking_status VARCHAR(30) DEFAULT 'not_run'
|
| 710 |
-
- docking_pose_file TEXT NULL
|
| 711 |
-
- docking_mode VARCHAR(20) DEFAULT 'off'
|
| 712 |
-
- docking_error TEXT NULL
|
| 713 |
-
|
| 714 |
-
Recommended frontend and filter behavior:
|
| 715 |
-
|
| 716 |
-
- Show all valid molecules: valid == true
|
| 717 |
-
- Show docked only: docking_status == completed
|
| 718 |
-
- Show not-yet-docked molecules: docking_status == not_run
|
| 719 |
-
- Sort docking results ascending, because more negative docking scores are usually better
|
| 720 |
-
"""
|
| 721 |
-
)
|
| 722 |
-
|
| 723 |
-
contract_btn = gr.Button("Load backend contract")
|
| 724 |
-
contract_json = gr.JSON(label="backend_contract.json")
|
| 725 |
-
|
| 726 |
-
contract_btn.click(
|
| 727 |
-
get_contract,
|
| 728 |
-
outputs=contract_json,
|
| 729 |
-
)
|
| 730 |
-
|
| 731 |
-
with gr.Tab("API Spec"):
|
| 732 |
-
gr.Markdown(
|
| 733 |
-
"""
|
| 734 |
-
This tab loads the API response specification from the private artifact bundle.
|
| 735 |
-
|
| 736 |
-
It explains the expected backend/frontend response format for:
|
| 737 |
-
|
| 738 |
-
- generated molecules
|
| 739 |
-
- scored SMILES
|
| 740 |
-
- docking fields
|
| 741 |
-
- frontend filters
|
| 742 |
-
"""
|
| 743 |
-
)
|
| 744 |
-
|
| 745 |
-
spec_btn = gr.Button("Load API response spec")
|
| 746 |
-
spec_output = gr.Markdown(label="API_RESPONSE_SPEC.md")
|
| 747 |
-
|
| 748 |
-
spec_btn.click(
|
| 749 |
-
get_api_spec,
|
| 750 |
-
outputs=spec_output,
|
| 751 |
-
)
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py.backup_old_tabs
DELETED
|
@@ -1,754 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import os
|
| 3 |
-
import subprocess
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
import uuid
|
| 6 |
-
from datetime import datetime
|
| 7 |
-
import gradio as gr
|
| 8 |
-
import pandas as pd
|
| 9 |
-
from huggingface_hub import snapshot_download
|
| 10 |
-
from rdkit import Chem
|
| 11 |
-
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED, AllChem
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
BUNDLE_REPO = os.getenv("BUNDLE_REPO", "abdullahsamra/egfr-ai-bundle")
|
| 15 |
-
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 16 |
-
|
| 17 |
-
CACHE_DIR = Path("bundle_cache")
|
| 18 |
-
EXAMPLE_JSON = None
|
| 19 |
-
API_SPEC_TEXT = None
|
| 20 |
-
BACKEND_CONTRACT_TEXT = None
|
| 21 |
-
BUNDLE_ROOT = None
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def download_bundle_light():
|
| 25 |
-
global EXAMPLE_JSON, API_SPEC_TEXT, BACKEND_CONTRACT_TEXT, BUNDLE_ROOT
|
| 26 |
-
|
| 27 |
-
try:
|
| 28 |
-
local_dir = snapshot_download(
|
| 29 |
-
repo_id=BUNDLE_REPO,
|
| 30 |
-
repo_type="dataset",
|
| 31 |
-
token=HF_TOKEN,
|
| 32 |
-
allow_patterns=[
|
| 33 |
-
"examples/generate_response.example.json",
|
| 34 |
-
"docs/API_RESPONSE_SPEC.md",
|
| 35 |
-
"configs/backend_contract.json",
|
| 36 |
-
"configs/sampling_model.bundle.toml",
|
| 37 |
-
"models/generator/egfr_generator.chkpt",
|
| 38 |
-
"README.md",
|
| 39 |
-
],
|
| 40 |
-
local_dir=str(CACHE_DIR),
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
root = Path(local_dir)
|
| 44 |
-
BUNDLE_ROOT = root
|
| 45 |
-
|
| 46 |
-
example_path = root / "examples" / "generate_response.example.json"
|
| 47 |
-
spec_path = root / "docs" / "API_RESPONSE_SPEC.md"
|
| 48 |
-
contract_path = root / "configs" / "backend_contract.json"
|
| 49 |
-
|
| 50 |
-
if example_path.exists():
|
| 51 |
-
EXAMPLE_JSON = json.loads(example_path.read_text(encoding="utf-8"))
|
| 52 |
-
|
| 53 |
-
if spec_path.exists():
|
| 54 |
-
API_SPEC_TEXT = spec_path.read_text(encoding="utf-8")
|
| 55 |
-
|
| 56 |
-
if contract_path.exists():
|
| 57 |
-
BACKEND_CONTRACT_TEXT = contract_path.read_text(encoding="utf-8")
|
| 58 |
-
|
| 59 |
-
return True, f"Loaded docs/examples from {BUNDLE_REPO}"
|
| 60 |
-
|
| 61 |
-
except Exception as e:
|
| 62 |
-
return False, f"Could not load private bundle. Check HF_TOKEN secret and BUNDLE_REPO variable. Error: {e}"
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def canonicalize(smiles: str):
|
| 66 |
-
mol = Chem.MolFromSmiles(str(smiles).strip())
|
| 67 |
-
if mol is None:
|
| 68 |
-
return None, None
|
| 69 |
-
return mol, Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def try_sa_score(mol):
|
| 73 |
-
try:
|
| 74 |
-
from rdkit.Contrib.SA_Score import sascorer
|
| 75 |
-
return round(float(sascorer.calculateScore(mol)), 4)
|
| 76 |
-
except Exception:
|
| 77 |
-
return None
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def compute_row(smiles: str):
|
| 81 |
-
mol, can = canonicalize(smiles)
|
| 82 |
-
|
| 83 |
-
if mol is None:
|
| 84 |
-
return {
|
| 85 |
-
"SMILES": smiles,
|
| 86 |
-
"valid": False,
|
| 87 |
-
"canonical_smiles": None,
|
| 88 |
-
"mw": None,
|
| 89 |
-
"logp": None,
|
| 90 |
-
"tpsa": None,
|
| 91 |
-
"hbd": None,
|
| 92 |
-
"hba": None,
|
| 93 |
-
"rot_bonds": None,
|
| 94 |
-
"qed": None,
|
| 95 |
-
"sa_score": None,
|
| 96 |
-
"pred_pAff_mean": None,
|
| 97 |
-
"docking_score": None,
|
| 98 |
-
"docking_status": "not_run",
|
| 99 |
-
"docking_pose_file": None,
|
| 100 |
-
"note": "Invalid SMILES",
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
return {
|
| 104 |
-
"SMILES": smiles,
|
| 105 |
-
"valid": True,
|
| 106 |
-
"canonical_smiles": can,
|
| 107 |
-
"mw": round(float(Descriptors.MolWt(mol)), 4),
|
| 108 |
-
"logp": round(float(Crippen.MolLogP(mol)), 4),
|
| 109 |
-
"tpsa": round(float(rdMolDescriptors.CalcTPSA(mol)), 4),
|
| 110 |
-
"hbd": int(Lipinski.NumHDonors(mol)),
|
| 111 |
-
"hba": int(Lipinski.NumHAcceptors(mol)),
|
| 112 |
-
"rot_bonds": int(Lipinski.NumRotatableBonds(mol)),
|
| 113 |
-
"qed": round(float(QED.qed(mol)), 4),
|
| 114 |
-
"sa_score": try_sa_score(mol),
|
| 115 |
-
"pred_pAff_mean": None,
|
| 116 |
-
"docking_score": None,
|
| 117 |
-
"docking_status": "not_run",
|
| 118 |
-
"docking_pose_file": None,
|
| 119 |
-
"note": "CPU demo: DeepPurpose and real docking are disabled in this Space.",
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
def add_ligand_ids_to_rows(rows, prefix="lig"):
|
| 125 |
-
output = []
|
| 126 |
-
for i, row in enumerate(rows, start=1):
|
| 127 |
-
clean = dict(row)
|
| 128 |
-
if not clean.get("ligand_id"):
|
| 129 |
-
clean["ligand_id"] = f"{prefix}_{i:04d}"
|
| 130 |
-
output.append(clean)
|
| 131 |
-
return output
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def add_ligand_ids_to_df(df, prefix="lig"):
|
| 135 |
-
df = df.copy()
|
| 136 |
-
if "ligand_id" not in df.columns:
|
| 137 |
-
df.insert(0, "ligand_id", [f"{prefix}_{i:04d}" for i in range(1, len(df) + 1)])
|
| 138 |
-
return df
|
| 139 |
-
|
| 140 |
-
def score_smiles(smiles_text):
|
| 141 |
-
smiles_list = []
|
| 142 |
-
for part in str(smiles_text).replace(",", "\n").splitlines():
|
| 143 |
-
part = part.strip()
|
| 144 |
-
if part:
|
| 145 |
-
smiles_list.append(part)
|
| 146 |
-
|
| 147 |
-
if not smiles_list:
|
| 148 |
-
return pd.DataFrame(), {
|
| 149 |
-
"status": "error",
|
| 150 |
-
"message": "Please enter at least one SMILES.",
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
rows = add_ligand_ids_to_rows([compute_row(s) for s in smiles_list])
|
| 154 |
-
df = pd.DataFrame(rows)
|
| 155 |
-
|
| 156 |
-
response = {
|
| 157 |
-
"status": "completed",
|
| 158 |
-
"mode": "score_smiles_cpu_demo",
|
| 159 |
-
"docking_mode": "off",
|
| 160 |
-
"summary": {
|
| 161 |
-
"num_input": len(smiles_list),
|
| 162 |
-
"num_valid": int(df["valid"].sum()),
|
| 163 |
-
"num_returned": len(df),
|
| 164 |
-
},
|
| 165 |
-
"columns": list(df.columns),
|
| 166 |
-
"results": rows,
|
| 167 |
-
"warnings": [
|
| 168 |
-
"This Space is a CPU demo.",
|
| 169 |
-
"DeepPurpose pred_pAff_mean is not computed in this Space version.",
|
| 170 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 171 |
-
"Outputs are computational predictions only.",
|
| 172 |
-
],
|
| 173 |
-
}
|
| 174 |
-
csv_path = write_results_csv(df)
|
| 175 |
-
response["download_csv_file"] = csv_path
|
| 176 |
-
return df, response, csv_path
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
def get_example_table():
|
| 180 |
-
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 181 |
-
return pd.DataFrame(EXAMPLE_JSON["results"])
|
| 182 |
-
return pd.DataFrame([
|
| 183 |
-
{"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."}
|
| 184 |
-
])
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def get_example_json():
|
| 188 |
-
if EXAMPLE_JSON:
|
| 189 |
-
return EXAMPLE_JSON
|
| 190 |
-
return {
|
| 191 |
-
"status": "not_loaded",
|
| 192 |
-
"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
def get_api_spec():
|
| 197 |
-
return API_SPEC_TEXT or "API_RESPONSE_SPEC.md not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
def get_contract():
|
| 201 |
-
if BACKEND_CONTRACT_TEXT:
|
| 202 |
-
try:
|
| 203 |
-
return json.loads(BACKEND_CONTRACT_TEXT)
|
| 204 |
-
except Exception:
|
| 205 |
-
return {"raw": BACKEND_CONTRACT_TEXT}
|
| 206 |
-
|
| 207 |
-
return {
|
| 208 |
-
"status": "not_loaded",
|
| 209 |
-
"message": "backend_contract.json not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
def _write_reinvent_sampling_config(job_id, num_molecules):
|
| 213 |
-
if BUNDLE_ROOT is None:
|
| 214 |
-
raise RuntimeError("Bundle root is not loaded. Check HF_TOKEN and BUNDLE_REPO.")
|
| 215 |
-
|
| 216 |
-
checkpoint = Path(BUNDLE_ROOT) / "models" / "generator" / "egfr_generator.chkpt"
|
| 217 |
-
if not checkpoint.exists():
|
| 218 |
-
raise FileNotFoundError(f"Generator checkpoint not found: {checkpoint}")
|
| 219 |
-
|
| 220 |
-
output_dir = Path("outputs")
|
| 221 |
-
output_dir.mkdir(exist_ok=True)
|
| 222 |
-
|
| 223 |
-
raw_csv = output_dir / f"{job_id}_raw_sampling.csv"
|
| 224 |
-
log_file = output_dir / f"{job_id}_reinvent.log"
|
| 225 |
-
resolved_json = output_dir / f"{job_id}_sampling_resolved.json"
|
| 226 |
-
config_path = output_dir / f"{job_id}_sampling.toml"
|
| 227 |
-
|
| 228 |
-
config_text = f"""
|
| 229 |
-
run_type = "sampling"
|
| 230 |
-
device = "cpu"
|
| 231 |
-
|
| 232 |
-
json_out_config = "{resolved_json.resolve()}"
|
| 233 |
-
|
| 234 |
-
[parameters]
|
| 235 |
-
model_file = "{checkpoint.resolve()}"
|
| 236 |
-
output_file = "{raw_csv.resolve()}"
|
| 237 |
-
|
| 238 |
-
num_smiles = {int(num_molecules)}
|
| 239 |
-
unique_molecules = true
|
| 240 |
-
randomize_smiles = true
|
| 241 |
-
"""
|
| 242 |
-
|
| 243 |
-
config_path.write_text(config_text.strip() + "\n", encoding="utf-8")
|
| 244 |
-
return config_path, raw_csv, log_file
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
def _run_reinvent_sampling(job_id, num_molecules):
|
| 248 |
-
config_path, raw_csv, log_file = _write_reinvent_sampling_config(job_id, num_molecules)
|
| 249 |
-
|
| 250 |
-
cmd = [
|
| 251 |
-
"reinvent",
|
| 252 |
-
"-l",
|
| 253 |
-
str(log_file.resolve()),
|
| 254 |
-
str(config_path.resolve()),
|
| 255 |
-
]
|
| 256 |
-
|
| 257 |
-
proc = subprocess.run(
|
| 258 |
-
cmd,
|
| 259 |
-
text=True,
|
| 260 |
-
stdout=subprocess.PIPE,
|
| 261 |
-
stderr=subprocess.STDOUT,
|
| 262 |
-
timeout=300,
|
| 263 |
-
)
|
| 264 |
-
|
| 265 |
-
if proc.returncode != 0:
|
| 266 |
-
raise RuntimeError(
|
| 267 |
-
"REINVENT sampling failed.\n"
|
| 268 |
-
f"Command: {' '.join(cmd)}\n"
|
| 269 |
-
f"Output:\n{proc.stdout[-4000:]}"
|
| 270 |
-
)
|
| 271 |
-
|
| 272 |
-
if not raw_csv.exists():
|
| 273 |
-
raise RuntimeError(f"REINVENT finished but output CSV was not found: {raw_csv}")
|
| 274 |
-
|
| 275 |
-
return raw_csv, log_file
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
def _enrich_reinvent_csv(raw_csv, return_top_k):
|
| 279 |
-
raw_df = pd.read_csv(raw_csv)
|
| 280 |
-
|
| 281 |
-
if "SMILES" not in raw_df.columns:
|
| 282 |
-
raise ValueError(f"REINVENT output missing SMILES column. Columns: {list(raw_df.columns)}")
|
| 283 |
-
|
| 284 |
-
rows = []
|
| 285 |
-
for _, r in raw_df.iterrows():
|
| 286 |
-
row = compute_row(str(r["SMILES"]))
|
| 287 |
-
|
| 288 |
-
if "SMILES_state" in raw_df.columns:
|
| 289 |
-
try:
|
| 290 |
-
row["SMILES_state"] = int(r["SMILES_state"])
|
| 291 |
-
except Exception:
|
| 292 |
-
row["SMILES_state"] = None
|
| 293 |
-
else:
|
| 294 |
-
row["SMILES_state"] = None
|
| 295 |
-
|
| 296 |
-
if "NLL" in raw_df.columns:
|
| 297 |
-
try:
|
| 298 |
-
row["NLL"] = float(r["NLL"])
|
| 299 |
-
except Exception:
|
| 300 |
-
row["NLL"] = None
|
| 301 |
-
else:
|
| 302 |
-
row["NLL"] = None
|
| 303 |
-
|
| 304 |
-
# Keep these stable for frontend/backend filters.
|
| 305 |
-
row["pred_pAff_mean"] = None
|
| 306 |
-
row["docking_score"] = None
|
| 307 |
-
row["docking_status"] = "not_run"
|
| 308 |
-
row["docking_pose_file"] = None
|
| 309 |
-
|
| 310 |
-
rows.append(row)
|
| 311 |
-
|
| 312 |
-
df = pd.DataFrame(rows)
|
| 313 |
-
|
| 314 |
-
preferred_cols = [
|
| 315 |
-
"SMILES",
|
| 316 |
-
"SMILES_state",
|
| 317 |
-
"NLL",
|
| 318 |
-
"valid",
|
| 319 |
-
"canonical_smiles",
|
| 320 |
-
"mw",
|
| 321 |
-
"logp",
|
| 322 |
-
"tpsa",
|
| 323 |
-
"hbd",
|
| 324 |
-
"hba",
|
| 325 |
-
"rot_bonds",
|
| 326 |
-
"qed",
|
| 327 |
-
"sa_score",
|
| 328 |
-
"pred_pAff_mean",
|
| 329 |
-
"docking_score",
|
| 330 |
-
"docking_status",
|
| 331 |
-
"docking_pose_file",
|
| 332 |
-
"note",
|
| 333 |
-
]
|
| 334 |
-
|
| 335 |
-
cols = [c for c in preferred_cols if c in df.columns] + [c for c in df.columns if c not in preferred_cols]
|
| 336 |
-
df = df[cols]
|
| 337 |
-
|
| 338 |
-
return_top_k = int(return_top_k)
|
| 339 |
-
if return_top_k > 0:
|
| 340 |
-
df = df.head(return_top_k)
|
| 341 |
-
|
| 342 |
-
return df
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_backend):
|
| 346 |
-
"""
|
| 347 |
-
Generation endpoint for the Space.
|
| 348 |
-
|
| 349 |
-
Demo mode uses packaged example rows.
|
| 350 |
-
Live mode tries to run REINVENT CPU sampling from the packaged checkpoint.
|
| 351 |
-
"""
|
| 352 |
-
job_id = f"gen_{uuid.uuid4().hex[:8]}"
|
| 353 |
-
created_at = datetime.utcnow().isoformat() + "Z"
|
| 354 |
-
num_molecules = int(num_molecules)
|
| 355 |
-
return_top_k = int(return_top_k)
|
| 356 |
-
|
| 357 |
-
warnings = []
|
| 358 |
-
|
| 359 |
-
try:
|
| 360 |
-
if generation_backend == "live_reinvent_cpu":
|
| 361 |
-
raw_csv, log_file = _run_reinvent_sampling(job_id, num_molecules)
|
| 362 |
-
df = _enrich_reinvent_csv(raw_csv, return_top_k)
|
| 363 |
-
mode = "live_reinvent_cpu_sampling"
|
| 364 |
-
status = "completed"
|
| 365 |
-
warnings.extend([
|
| 366 |
-
"Live REINVENT CPU sampling was executed in this Space.",
|
| 367 |
-
"DeepPurpose scoring is not enabled in this Space version; pred_pAff_mean is null.",
|
| 368 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 369 |
-
])
|
| 370 |
-
|
| 371 |
-
else:
|
| 372 |
-
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 373 |
-
rows = EXAMPLE_JSON["results"]
|
| 374 |
-
else:
|
| 375 |
-
rows = [compute_row("CCO"), compute_row("c1ccccc1")]
|
| 376 |
-
|
| 377 |
-
selected = rows[: max(1, min(return_top_k, len(rows)))]
|
| 378 |
-
|
| 379 |
-
clean_rows = []
|
| 380 |
-
for row in selected:
|
| 381 |
-
clean = dict(row)
|
| 382 |
-
clean["docking_score"] = None
|
| 383 |
-
clean["docking_status"] = "not_run"
|
| 384 |
-
clean["docking_pose_file"] = None
|
| 385 |
-
if "pred_pAff_mean" not in clean:
|
| 386 |
-
clean["pred_pAff_mean"] = None
|
| 387 |
-
clean_rows.append(clean)
|
| 388 |
-
|
| 389 |
-
df = pd.DataFrame(clean_rows)
|
| 390 |
-
mode = "generate_job_cpu_demo"
|
| 391 |
-
status = "completed"
|
| 392 |
-
warnings.extend([
|
| 393 |
-
"This is a CPU demo generation response.",
|
| 394 |
-
"Live REINVENT generation was not executed because demo mode was selected.",
|
| 395 |
-
"The response shape matches the planned backend contract.",
|
| 396 |
-
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 397 |
-
])
|
| 398 |
-
|
| 399 |
-
except Exception as e:
|
| 400 |
-
df = pd.DataFrame()
|
| 401 |
-
mode = "live_reinvent_cpu_sampling"
|
| 402 |
-
status = "failed"
|
| 403 |
-
warnings.append(str(e))
|
| 404 |
-
|
| 405 |
-
df = add_ligand_ids_to_df(df)
|
| 406 |
-
|
| 407 |
-
output_dir = Path("outputs")
|
| 408 |
-
output_dir.mkdir(exist_ok=True)
|
| 409 |
-
csv_path = output_dir / f"{job_id}_results.csv"
|
| 410 |
-
df.to_csv(csv_path, index=False)
|
| 411 |
-
|
| 412 |
-
job_summary = {
|
| 413 |
-
"job_id": job_id,
|
| 414 |
-
"status": status,
|
| 415 |
-
"mode": mode,
|
| 416 |
-
"created_at": created_at,
|
| 417 |
-
"preset": "egfr_generator",
|
| 418 |
-
"docking_mode": docking_mode,
|
| 419 |
-
"num_requested": num_molecules,
|
| 420 |
-
"num_returned": int(len(df)),
|
| 421 |
-
"download_csv_file": str(csv_path),
|
| 422 |
-
"live_generation": generation_backend == "live_reinvent_cpu",
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
response = {
|
| 426 |
-
"job_id": job_id,
|
| 427 |
-
"status": status,
|
| 428 |
-
"mode": mode,
|
| 429 |
-
"created_at": created_at,
|
| 430 |
-
"preset": "egfr_generator",
|
| 431 |
-
"docking_mode": docking_mode,
|
| 432 |
-
"summary": {
|
| 433 |
-
"num_requested": num_molecules,
|
| 434 |
-
"num_returned": int(len(df)),
|
| 435 |
-
},
|
| 436 |
-
"columns": list(df.columns),
|
| 437 |
-
"results": df.to_dict(orient="records"),
|
| 438 |
-
"download_csv_file": str(csv_path),
|
| 439 |
-
"warnings": warnings,
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
return job_summary, df, response, str(csv_path)
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
def write_results_csv(df: pd.DataFrame, filename: str = "score_smiles_results.csv"):
|
| 446 |
-
out_dir = Path("outputs")
|
| 447 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 448 |
-
out_path = out_dir / filename
|
| 449 |
-
df.to_csv(out_path, index=False)
|
| 450 |
-
return str(out_path)
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
def smiles_to_3d_file(smiles: str, file_format: str = "sdf"):
|
| 454 |
-
smiles = str(smiles).strip()
|
| 455 |
-
file_format = str(file_format).lower().strip()
|
| 456 |
-
|
| 457 |
-
if file_format not in ["sdf", "mol", "pdb"]:
|
| 458 |
-
return None, {
|
| 459 |
-
"status": "error",
|
| 460 |
-
"message": "Unsupported format. Choose sdf, mol, or pdb.",
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
mol = Chem.MolFromSmiles(smiles)
|
| 464 |
-
if mol is None:
|
| 465 |
-
return None, {
|
| 466 |
-
"status": "error",
|
| 467 |
-
"input_smiles": smiles,
|
| 468 |
-
"message": "Invalid SMILES.",
|
| 469 |
-
}
|
| 470 |
-
|
| 471 |
-
canonical_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 472 |
-
|
| 473 |
-
mol = Chem.AddHs(mol)
|
| 474 |
-
|
| 475 |
-
params = AllChem.ETKDGv3()
|
| 476 |
-
params.randomSeed = 42
|
| 477 |
-
|
| 478 |
-
embed_status = AllChem.EmbedMolecule(mol, params)
|
| 479 |
-
if embed_status != 0:
|
| 480 |
-
return None, {
|
| 481 |
-
"status": "error",
|
| 482 |
-
"input_smiles": smiles,
|
| 483 |
-
"canonical_smiles": canonical_smiles,
|
| 484 |
-
"message": "3D embedding failed.",
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
try:
|
| 488 |
-
AllChem.UFFOptimizeMolecule(mol, maxIters=500)
|
| 489 |
-
optimization_status = "UFF optimized"
|
| 490 |
-
except Exception as e:
|
| 491 |
-
optimization_status = f"UFF optimization failed, using embedded geometry only: {e}"
|
| 492 |
-
|
| 493 |
-
out_dir = Path("outputs/ligands")
|
| 494 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 495 |
-
|
| 496 |
-
safe_name = "ligand_3d"
|
| 497 |
-
out_path = out_dir / f"{safe_name}.{file_format}"
|
| 498 |
-
|
| 499 |
-
if file_format == "sdf":
|
| 500 |
-
writer = Chem.SDWriter(str(out_path))
|
| 501 |
-
writer.write(mol)
|
| 502 |
-
writer.close()
|
| 503 |
-
elif file_format == "mol":
|
| 504 |
-
Chem.MolToMolFile(mol, str(out_path))
|
| 505 |
-
elif file_format == "pdb":
|
| 506 |
-
Chem.MolToPDBFile(mol, str(out_path))
|
| 507 |
-
|
| 508 |
-
meta = {
|
| 509 |
-
"status": "completed",
|
| 510 |
-
"input_smiles": smiles,
|
| 511 |
-
"canonical_smiles": canonical_smiles,
|
| 512 |
-
"file_format": file_format,
|
| 513 |
-
"file_path": str(out_path),
|
| 514 |
-
"optimization_status": optimization_status,
|
| 515 |
-
"note": "This is a generated 3D conformer from SMILES, not a docked protein-ligand pose.",
|
| 516 |
-
}
|
| 517 |
-
|
| 518 |
-
return str(out_path), meta
|
| 519 |
-
|
| 520 |
-
loaded_ok, loaded_msg = download_bundle_light()
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
with gr.Blocks(title="EGFR AI Demo") as demo:
|
| 524 |
-
gr.Markdown(
|
| 525 |
-
"""
|
| 526 |
-
# EGFR AI Demo
|
| 527 |
-
|
| 528 |
-
Private CPU demo for the EGFR AI backend workflow.
|
| 529 |
-
|
| 530 |
-
This demo shows the backend response shape for generation, scoring, filtering, and docking integration.
|
| 531 |
-
|
| 532 |
-
Real AutoDock-GPU docking is disabled in this CPU Space, but docking fields are present so backend/frontend teams can implement database columns and filters.
|
| 533 |
-
"""
|
| 534 |
-
)
|
| 535 |
-
|
| 536 |
-
gr.Markdown(f"Bundle load status: {loaded_msg}")
|
| 537 |
-
|
| 538 |
-
with gr.Tab("Score SMILES"):
|
| 539 |
-
gr.Markdown(
|
| 540 |
-
"""
|
| 541 |
-
Enter one or more SMILES.
|
| 542 |
-
|
| 543 |
-
This CPU demo calculates molecular properties and returns the same response columns expected by the backend.
|
| 544 |
-
|
| 545 |
-
Important:
|
| 546 |
-
- pred_pAff_mean is an integration field in this Space version.
|
| 547 |
-
- real docking is disabled in this CPU Space.
|
| 548 |
-
- docking_status is returned as not_run.
|
| 549 |
-
"""
|
| 550 |
-
)
|
| 551 |
-
|
| 552 |
-
smiles_input = gr.Textbox(
|
| 553 |
-
label="SMILES input",
|
| 554 |
-
value="CCO\nc1ccccc1",
|
| 555 |
-
lines=5,
|
| 556 |
-
)
|
| 557 |
-
|
| 558 |
-
score_btn = gr.Button("Score SMILES")
|
| 559 |
-
score_table = gr.Dataframe(label="Results table")
|
| 560 |
-
score_json = gr.JSON(label="Backend-style JSON response")
|
| 561 |
-
score_csv = gr.File(label="Download CSV")
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
score_btn.click(
|
| 565 |
-
score_smiles,
|
| 566 |
-
inputs=smiles_input,
|
| 567 |
-
outputs=[score_table, score_json, score_csv],
|
| 568 |
-
)
|
| 569 |
-
with gr.Tab("Generate Job Demo"):
|
| 570 |
-
gr.Markdown(
|
| 571 |
-
"""
|
| 572 |
-
This tab demonstrates generation job behavior.
|
| 573 |
-
|
| 574 |
-
Generation backend options:
|
| 575 |
-
|
| 576 |
-
- demo_rows: uses packaged example rows. Fast and stable.
|
| 577 |
-
- live_reinvent_cpu: tries to run real REINVENT sampling from the packaged checkpoint on CPU.
|
| 578 |
-
|
| 579 |
-
The output is split into:
|
| 580 |
-
|
| 581 |
-
1. Job summary: job_id, status, preset, mode, and metadata.
|
| 582 |
-
2. Generated molecule results table.
|
| 583 |
-
3. Full backend-style generation JSON.
|
| 584 |
-
4. Download CSV.
|
| 585 |
-
|
| 586 |
-
Notes:
|
| 587 |
-
- DeepPurpose live scoring is not enabled yet in this Space version.
|
| 588 |
-
- Real docking is disabled in this CPU Space, so docking_status is returned as not_run.
|
| 589 |
-
"""
|
| 590 |
-
)
|
| 591 |
-
|
| 592 |
-
generation_backend = gr.Dropdown(
|
| 593 |
-
label="generation_backend",
|
| 594 |
-
choices=["demo_rows", "live_reinvent_cpu"],
|
| 595 |
-
value="demo_rows",
|
| 596 |
-
)
|
| 597 |
-
|
| 598 |
-
gen_num_molecules = gr.Number(
|
| 599 |
-
label="num_molecules",
|
| 600 |
-
value=20,
|
| 601 |
-
precision=0,
|
| 602 |
-
)
|
| 603 |
-
|
| 604 |
-
gen_return_top_k = gr.Number(
|
| 605 |
-
label="return_top_k",
|
| 606 |
-
value=20,
|
| 607 |
-
precision=0,
|
| 608 |
-
)
|
| 609 |
-
|
| 610 |
-
gen_docking_mode = gr.Dropdown(
|
| 611 |
-
label="docking_mode",
|
| 612 |
-
choices=["off", "top_k", "all"],
|
| 613 |
-
value="off",
|
| 614 |
-
)
|
| 615 |
-
|
| 616 |
-
gen_btn = gr.Button("Run Generation Job")
|
| 617 |
-
|
| 618 |
-
gen_summary = gr.JSON(label="Job summary")
|
| 619 |
-
gen_table = gr.Dataframe(label="Generated molecule results table")
|
| 620 |
-
gen_json = gr.JSON(label="Full backend-style generation JSON")
|
| 621 |
-
gen_file = gr.File(label="Download CSV")
|
| 622 |
-
|
| 623 |
-
gen_btn.click(
|
| 624 |
-
generate_job_demo,
|
| 625 |
-
inputs=[gen_num_molecules, gen_return_top_k, gen_docking_mode, generation_backend],
|
| 626 |
-
outputs=[gen_summary, gen_table, gen_json, gen_file],
|
| 627 |
-
)
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
with gr.Tab("Example Generate Response"):
|
| 631 |
-
gr.Markdown(
|
| 632 |
-
"""
|
| 633 |
-
This example is loaded from the private Hugging Face Dataset bundle.
|
| 634 |
-
|
| 635 |
-
It shows the expected response shape after:
|
| 636 |
-
|
| 637 |
-
Generate -> Enrich -> Optional Docking
|
| 638 |
-
"""
|
| 639 |
-
)
|
| 640 |
-
|
| 641 |
-
example_btn = gr.Button("Load example response")
|
| 642 |
-
example_table = gr.Dataframe(label="Example results table")
|
| 643 |
-
example_json = gr.JSON(label="Example JSON response")
|
| 644 |
-
|
| 645 |
-
example_btn.click(
|
| 646 |
-
lambda: (get_example_table(), get_example_json()),
|
| 647 |
-
outputs=[example_table, example_json],
|
| 648 |
-
)
|
| 649 |
-
|
| 650 |
-
with gr.Tab("SMILES to 3D File"):
|
| 651 |
-
gr.Markdown(
|
| 652 |
-
"""
|
| 653 |
-
Convert a SMILES string into a downloadable 3D ligand file.
|
| 654 |
-
|
| 655 |
-
Supported formats:
|
| 656 |
-
|
| 657 |
-
- SDF
|
| 658 |
-
- MOL
|
| 659 |
-
- PDB
|
| 660 |
-
|
| 661 |
-
Important: this creates a 3D conformer from SMILES. It is not a docked pose inside the EGFR binding pocket.
|
| 662 |
-
"""
|
| 663 |
-
)
|
| 664 |
-
|
| 665 |
-
smiles_3d_input = gr.Textbox(
|
| 666 |
-
label="SMILES",
|
| 667 |
-
value="CCO",
|
| 668 |
-
lines=2,
|
| 669 |
-
)
|
| 670 |
-
|
| 671 |
-
format_input = gr.Radio(
|
| 672 |
-
choices=["sdf", "mol", "pdb"],
|
| 673 |
-
value="sdf",
|
| 674 |
-
label="Output file format",
|
| 675 |
-
)
|
| 676 |
-
|
| 677 |
-
make_3d_btn = gr.Button("Generate 3D ligand file")
|
| 678 |
-
ligand_file = gr.File(label="Download 3D ligand file")
|
| 679 |
-
ligand_meta = gr.JSON(label="3D file metadata")
|
| 680 |
-
|
| 681 |
-
make_3d_btn.click(
|
| 682 |
-
smiles_to_3d_file,
|
| 683 |
-
inputs=[smiles_3d_input, format_input],
|
| 684 |
-
outputs=[ligand_file, ligand_meta],
|
| 685 |
-
)
|
| 686 |
-
|
| 687 |
-
with gr.Tab("Docking Contract"):
|
| 688 |
-
gr.Markdown(
|
| 689 |
-
"""
|
| 690 |
-
## Docking modes
|
| 691 |
-
|
| 692 |
-
The full backend supports:
|
| 693 |
-
|
| 694 |
-
- off
|
| 695 |
-
- top_k
|
| 696 |
-
- all
|
| 697 |
-
|
| 698 |
-
In this CPU Space, real AutoDock-GPU docking is disabled.
|
| 699 |
-
|
| 700 |
-
Returned docking fields in CPU demo:
|
| 701 |
-
|
| 702 |
-
- docking_score: null
|
| 703 |
-
- docking_status: not_run
|
| 704 |
-
- docking_pose_file: null
|
| 705 |
-
|
| 706 |
-
Recommended database columns:
|
| 707 |
-
|
| 708 |
-
- docking_score FLOAT NULL
|
| 709 |
-
- docking_status VARCHAR(30) DEFAULT 'not_run'
|
| 710 |
-
- docking_pose_file TEXT NULL
|
| 711 |
-
- docking_mode VARCHAR(20) DEFAULT 'off'
|
| 712 |
-
- docking_error TEXT NULL
|
| 713 |
-
|
| 714 |
-
Recommended frontend and filter behavior:
|
| 715 |
-
|
| 716 |
-
- Show all valid molecules: valid == true
|
| 717 |
-
- Show docked only: docking_status == completed
|
| 718 |
-
- Show not-yet-docked molecules: docking_status == not_run
|
| 719 |
-
- Sort docking results ascending, because more negative docking scores are usually better
|
| 720 |
-
"""
|
| 721 |
-
)
|
| 722 |
-
|
| 723 |
-
contract_btn = gr.Button("Load backend contract")
|
| 724 |
-
contract_json = gr.JSON(label="backend_contract.json")
|
| 725 |
-
|
| 726 |
-
contract_btn.click(
|
| 727 |
-
get_contract,
|
| 728 |
-
outputs=contract_json,
|
| 729 |
-
)
|
| 730 |
-
|
| 731 |
-
with gr.Tab("API Spec"):
|
| 732 |
-
gr.Markdown(
|
| 733 |
-
"""
|
| 734 |
-
This tab loads the API response specification from the private artifact bundle.
|
| 735 |
-
|
| 736 |
-
It explains the expected backend/frontend response format for:
|
| 737 |
-
|
| 738 |
-
- generated molecules
|
| 739 |
-
- scored SMILES
|
| 740 |
-
- docking fields
|
| 741 |
-
- frontend filters
|
| 742 |
-
"""
|
| 743 |
-
)
|
| 744 |
-
|
| 745 |
-
spec_btn = gr.Button("Load API response spec")
|
| 746 |
-
spec_output = gr.Markdown(label="API_RESPONSE_SPEC.md")
|
| 747 |
-
|
| 748 |
-
spec_btn.click(
|
| 749 |
-
get_api_spec,
|
| 750 |
-
outputs=spec_output,
|
| 751 |
-
)
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/Helpers/OtpHelper.php
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Helpers;
|
| 4 |
+
|
| 5 |
+
use App\Models\User;
|
| 6 |
+
use App\Services\OtpService;
|
| 7 |
+
|
| 8 |
+
class OtpHelper
|
| 9 |
+
{
|
| 10 |
+
/**
|
| 11 |
+
* Send OTP by type for a user email
|
| 12 |
+
*
|
| 13 |
+
* @param string $email
|
| 14 |
+
* @param string $type ('email_verification' or 'password_reset')
|
| 15 |
+
* @param OtpService $otpService
|
| 16 |
+
* @return array
|
| 17 |
+
*/
|
| 18 |
+
public static function sendOtpByType(string $email, string $type, OtpService $otpService): array
|
| 19 |
+
{
|
| 20 |
+
$user = User::where('email', $email)->firstOrFail();
|
| 21 |
+
|
| 22 |
+
$otpService->resendOtp($user, $type);
|
| 23 |
+
|
| 24 |
+
return [
|
| 25 |
+
'email' => $user->email
|
| 26 |
+
];
|
| 27 |
+
}
|
| 28 |
+
}
|
app/Http/Controllers/Api/AdmetController.php
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Http\Request;
|
| 6 |
+
use App\Services\AdmetService;
|
| 7 |
+
|
| 8 |
+
class AdmetController extends BaseController
|
| 9 |
+
{
|
| 10 |
+
public function predict(Request $request, AdmetService $admetService)
|
| 11 |
+
{
|
| 12 |
+
try {
|
| 13 |
+
if ($request->hasFile('file')) {
|
| 14 |
+
$file = $request->file('file');
|
| 15 |
+
$extension = $file->getClientOriginalExtension();
|
| 16 |
+
$content = file_get_contents($file->getRealPath());
|
| 17 |
+
$result = $admetService->predictFromFile($content, $extension);
|
| 18 |
+
return $this->successResponse('ADMET predictions generated successfully', $result);
|
| 19 |
+
} elseif ($request->has('smiles') && !empty($request->input('smiles'))) {
|
| 20 |
+
$result = $admetService->predictBatchFromString($request->input('smiles'));
|
| 21 |
+
return $this->successResponse('ADMET predictions generated successfully', $result);
|
| 22 |
+
} else {
|
| 23 |
+
return $this->errorResponse('Please provide either a file or SMILES string', 400);
|
| 24 |
+
}
|
| 25 |
+
} catch (\Exception $e) {
|
| 26 |
+
return $this->errorResponse('Failed to generate ADMET predictions: ' . $e->getMessage(), 500);
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
app/Http/Controllers/Api/AiServicesIntegrationController.php
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Http\JsonResponse;
|
| 6 |
+
use Illuminate\Http\Request;
|
| 7 |
+
use Illuminate\Routing\Controller;
|
| 8 |
+
use Illuminate\Support\Facades\Http;
|
| 9 |
+
|
| 10 |
+
class AiServicesIntegrationController extends Controller
|
| 11 |
+
{
|
| 12 |
+
public function health(): JsonResponse
|
| 13 |
+
{
|
| 14 |
+
$checks = [
|
| 15 |
+
'admet' => config('services.admet.url'),
|
| 16 |
+
'chemical_rag' => config('services.chemical_ai.url'),
|
| 17 |
+
'drug_repurposing' => config('services.drug_repurposing.url'),
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
$results = [];
|
| 21 |
+
|
| 22 |
+
foreach ($checks as $name => $baseUrl) {
|
| 23 |
+
$results[$name] = $this->probeHealth($baseUrl);
|
| 24 |
+
usleep(200_000);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
$allHealthy = collect($results)->every(
|
| 28 |
+
fn (array $result) => ($result['status'] ?? '') === 'healthy'
|
| 29 |
+
);
|
| 30 |
+
|
| 31 |
+
return response()->json([
|
| 32 |
+
'success' => $allHealthy,
|
| 33 |
+
'services' => $results,
|
| 34 |
+
], $allHealthy ? 200 : 503);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
public function testAdmet(Request $request): JsonResponse
|
| 38 |
+
{
|
| 39 |
+
$baseUrl = rtrim((string) config('services.admet.url'), '/');
|
| 40 |
+
$smiles = (string) $request->input('smiles', 'c1ccccc1');
|
| 41 |
+
|
| 42 |
+
$response = Http::timeout(180)->post("{$baseUrl}/predict", [
|
| 43 |
+
'smiles' => $smiles,
|
| 44 |
+
]);
|
| 45 |
+
|
| 46 |
+
return $this->proxyResponse('admet', $response);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
public function testChemicalSearch(Request $request): JsonResponse
|
| 50 |
+
{
|
| 51 |
+
$baseUrl = rtrim((string) config('services.chemical_ai.url'), '/');
|
| 52 |
+
$smiles = (string) $request->input('smiles', 'CCO');
|
| 53 |
+
$topK = (int) $request->input('top_k', 3);
|
| 54 |
+
|
| 55 |
+
$response = Http::timeout(180)->post("{$baseUrl}/search/retrieval-only", [
|
| 56 |
+
'smiles' => $smiles,
|
| 57 |
+
'top_k' => $topK,
|
| 58 |
+
]);
|
| 59 |
+
|
| 60 |
+
return $this->proxyResponse('chemical_rag', $response);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
public function testDrugRepurposing(): JsonResponse
|
| 64 |
+
{
|
| 65 |
+
$baseUrl = rtrim((string) config('services.drug_repurposing.url'), '/');
|
| 66 |
+
|
| 67 |
+
$health = Http::timeout(60)->retry(3, 500)->withUserAgent('AILIXIR-Internal/1.0')->get("{$baseUrl}/health");
|
| 68 |
+
usleep(200_000);
|
| 69 |
+
$modelStatus = Http::timeout(60)->retry(3, 500)->withUserAgent('AILIXIR-Internal/1.0')->get("{$baseUrl}/api/v1/model-status");
|
| 70 |
+
|
| 71 |
+
$success = $health->successful() && $modelStatus->successful();
|
| 72 |
+
|
| 73 |
+
return response()->json([
|
| 74 |
+
'success' => $success,
|
| 75 |
+
'service' => 'drug_repurposing',
|
| 76 |
+
'health' => [
|
| 77 |
+
'status' => $health->status(),
|
| 78 |
+
'body' => $health->json(),
|
| 79 |
+
],
|
| 80 |
+
'model_status' => [
|
| 81 |
+
'status' => $modelStatus->status(),
|
| 82 |
+
'body' => $modelStatus->json(),
|
| 83 |
+
],
|
| 84 |
+
], $success ? 200 : 502);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
private function probeHealth(?string $baseUrl): array
|
| 88 |
+
{
|
| 89 |
+
if (empty($baseUrl)) {
|
| 90 |
+
return [
|
| 91 |
+
'status' => 'misconfigured',
|
| 92 |
+
'error' => 'URL not configured',
|
| 93 |
+
];
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
try {
|
| 97 |
+
$response = Http::timeout(60)->retry(3, 500)->withUserAgent('AILIXIR-Internal/1.0')->get(rtrim($baseUrl, '/').'/health');
|
| 98 |
+
|
| 99 |
+
return [
|
| 100 |
+
'status' => $response->successful() ? 'healthy' : 'unhealthy',
|
| 101 |
+
'http_status' => $response->status(),
|
| 102 |
+
'body' => $response->json() ?? $response->body(),
|
| 103 |
+
];
|
| 104 |
+
} catch (\Throwable $e) {
|
| 105 |
+
return [
|
| 106 |
+
'status' => 'unreachable',
|
| 107 |
+
'error' => $e->getMessage(),
|
| 108 |
+
];
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
private function proxyResponse(string $service, \Illuminate\Http\Client\Response $response): JsonResponse
|
| 113 |
+
{
|
| 114 |
+
return response()->json([
|
| 115 |
+
'success' => $response->successful(),
|
| 116 |
+
'service' => $service,
|
| 117 |
+
'upstream_status' => $response->status(),
|
| 118 |
+
'data' => $response->json(),
|
| 119 |
+
], $response->successful() ? 200 : 502);
|
| 120 |
+
}
|
| 121 |
+
}
|
app/Http/Controllers/Api/AuthController.php
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Api\BaseController;
|
| 6 |
+
use App\Http\Requests\User\LoginRequest;
|
| 7 |
+
use App\Http\Requests\User\RegisterRequest;
|
| 8 |
+
use App\Http\Requests\User\LoginGoogleRequest;
|
| 9 |
+
use App\Http\Requests\User\CheckEmailRequest;
|
| 10 |
+
use Illuminate\Http\Request;
|
| 11 |
+
use App\Services\UserService;
|
| 12 |
+
use App\Services\OtpService;
|
| 13 |
+
use App\Models\User;
|
| 14 |
+
|
| 15 |
+
class AuthController extends BaseController
|
| 16 |
+
{
|
| 17 |
+
|
| 18 |
+
protected $userService;
|
| 19 |
+
protected $otpService;
|
| 20 |
+
|
| 21 |
+
public function __construct(UserService $userService, OtpService $otpService)
|
| 22 |
+
{
|
| 23 |
+
$this->userService = $userService;
|
| 24 |
+
$this->otpService = $otpService;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/** ---------------- Register ---------------- */
|
| 28 |
+
|
| 29 |
+
public function register(RegisterRequest $request)
|
| 30 |
+
{
|
| 31 |
+
$user = $this->userService->registerUser($request->validated());
|
| 32 |
+
|
| 33 |
+
return $this->successResponse(
|
| 34 |
+
'Registered successfully. Please check your email for OTP verification code.',
|
| 35 |
+
['email' => $user->email]
|
| 36 |
+
);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
public function login(LoginRequest $request)
|
| 41 |
+
{
|
| 42 |
+
$result = $this->userService->loginUser($request->validated());
|
| 43 |
+
|
| 44 |
+
if (isset($result['error'])) {
|
| 45 |
+
return $this->errorResponse($result['error'], $result['code'] ?? 400);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
return $this->successResponse('Login successful', [
|
| 49 |
+
'token' => $result['token'],
|
| 50 |
+
'user' => $result['user']
|
| 51 |
+
]);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/** ---------------- Forgot Password ---------------- */
|
| 55 |
+
|
| 56 |
+
public function forgotPassword(CheckEmailRequest $request)
|
| 57 |
+
{
|
| 58 |
+
$user = User::where('email', $request->email)->first();
|
| 59 |
+
|
| 60 |
+
$this->otpService->sendOtp($user, 'password_reset');
|
| 61 |
+
|
| 62 |
+
return $this->successResponse(
|
| 63 |
+
'Password reset OTP has been sent to your email.',
|
| 64 |
+
['email' => $user->email]
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/** ---------------- Resend OTP ---------------- */
|
| 69 |
+
|
| 70 |
+
public function resendOtp(Request $request)
|
| 71 |
+
{
|
| 72 |
+
$request->validate([
|
| 73 |
+
'email' => 'required|email|exists:users,email',
|
| 74 |
+
'type' => 'required|in:email_verification,password_reset'
|
| 75 |
+
]);
|
| 76 |
+
|
| 77 |
+
$user = User::where('email', $request->email)->first();
|
| 78 |
+
|
| 79 |
+
try {
|
| 80 |
+
$this->otpService->resendOtp($user, $request->type);
|
| 81 |
+
|
| 82 |
+
return $this->successResponse(
|
| 83 |
+
'OTP has been resent to your email.',
|
| 84 |
+
['email' => $user->email]
|
| 85 |
+
);
|
| 86 |
+
} catch (\Illuminate\Validation\ValidationException $e) {
|
| 87 |
+
return $this->errorResponse($e->getMessage(), 429);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
public function logout(Request $request)
|
| 92 |
+
{
|
| 93 |
+
$request->user()->currentAccessToken()->delete();
|
| 94 |
+
|
| 95 |
+
return $this->successResponse('Logged out successfully');
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/** ---------------- Google OAuth ---------------- */
|
| 99 |
+
|
| 100 |
+
public function handleGoogleCallback(LoginGoogleRequest $request)
|
| 101 |
+
{
|
| 102 |
+
$result = $this->userService->loginGoogleUser(
|
| 103 |
+
$request->validated()['access_token']
|
| 104 |
+
);
|
| 105 |
+
|
| 106 |
+
if (isset($result['error'])) {
|
| 107 |
+
return $this->errorResponse($result['error'], $result['code'] ?? 400);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return $this->successResponse('Google login successful', [
|
| 111 |
+
'token' => $result['token'],
|
| 112 |
+
'user' => $result['user']
|
| 113 |
+
]);
|
| 114 |
+
}
|
| 115 |
+
}
|
app/Http/Controllers/Api/AwardController.php
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Api\BaseController;
|
| 6 |
+
use App\Http\Resources\ScientistResource;
|
| 7 |
+
use App\Http\Resources\AwardResource;
|
| 8 |
+
use App\Services\AwardService;
|
| 9 |
+
use Illuminate\Http\Request;
|
| 10 |
+
|
| 11 |
+
class AwardController extends BaseController
|
| 12 |
+
{
|
| 13 |
+
protected $service;
|
| 14 |
+
|
| 15 |
+
public function __construct(AwardService $service)
|
| 16 |
+
{
|
| 17 |
+
$this->service = $service;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
public function index(Request $request)
|
| 21 |
+
{
|
| 22 |
+
$perPage = min(100, max(1, (int)$request->get('per_page', 10)));
|
| 23 |
+
|
| 24 |
+
$awards = $this->service->getAwards($perPage);
|
| 25 |
+
|
| 26 |
+
return $this->paginatedResponse(
|
| 27 |
+
'Awards retrieved successfully',
|
| 28 |
+
$awards->through(fn($award) => new AwardResource($award)),
|
| 29 |
+
$awards
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
public function show($id)
|
| 34 |
+
{
|
| 35 |
+
$award = $this->service->getAward($id);
|
| 36 |
+
|
| 37 |
+
return $this->successResponse(
|
| 38 |
+
'Award retrieved successfully',
|
| 39 |
+
new AwardResource($award)
|
| 40 |
+
);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
public function getScientistsByAward($id)
|
| 44 |
+
{
|
| 45 |
+
$award = $this->service->getAward($id);
|
| 46 |
+
|
| 47 |
+
return $this->successResponse(
|
| 48 |
+
'Scientists retrieved successfully',
|
| 49 |
+
ScientistResource::collection($award->scientists)
|
| 50 |
+
);
|
| 51 |
+
}
|
| 52 |
+
}
|
app/Http/Controllers/Api/BaseController.php
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
| 6 |
+
use Illuminate\Foundation\Validation\ValidatesRequests;
|
| 7 |
+
use Illuminate\Routing\Controller;
|
| 8 |
+
use App\Traits\ApiResponseTrait;
|
| 9 |
+
|
| 10 |
+
class BaseController extends Controller
|
| 11 |
+
{
|
| 12 |
+
use AuthorizesRequests, ValidatesRequests, ApiResponseTrait;
|
| 13 |
+
}
|
app/Http/Controllers/Api/ChemicalSearchController.php
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use App\Http\Requests\ChemicalSearchRequest;
|
| 6 |
+
use App\Services\ChemicalSearchService;
|
| 7 |
+
use Illuminate\Http\JsonResponse;
|
| 8 |
+
use Illuminate\Support\Facades\Auth;
|
| 9 |
+
|
| 10 |
+
class ChemicalSearchController extends BaseController
|
| 11 |
+
{
|
| 12 |
+
public function __construct(
|
| 13 |
+
private ChemicalSearchService $searchService
|
| 14 |
+
) {}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* POST /api/chemical-search
|
| 18 |
+
* Retrieval Only - Synchronous
|
| 19 |
+
*/
|
| 20 |
+
public function store(ChemicalSearchRequest $request): JsonResponse
|
| 21 |
+
{
|
| 22 |
+
$user = Auth::user();
|
| 23 |
+
|
| 24 |
+
$result = $this->searchService->search(
|
| 25 |
+
smiles: $request->validated('smiles'),
|
| 26 |
+
topK: $request->input('top_k', 5),
|
| 27 |
+
userId: $user->id
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
if (!$result['success']) {
|
| 31 |
+
return response()->json([
|
| 32 |
+
'success' => false,
|
| 33 |
+
'message' => $result['error'] ?? 'Search failed',
|
| 34 |
+
], 500);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
return response()->json([
|
| 38 |
+
'success' => true,
|
| 39 |
+
'query' => [
|
| 40 |
+
'smiles' => $request->validated('smiles'),
|
| 41 |
+
'top_k' => $request->input('top_k', 5),
|
| 42 |
+
],
|
| 43 |
+
'compounds' => $result['compounds'],
|
| 44 |
+
'metadata' => $result['metadata'],
|
| 45 |
+
]);
|
| 46 |
+
}
|
| 47 |
+
public function fullRag(ChemicalSearchRequest $request): JsonResponse
|
| 48 |
+
{
|
| 49 |
+
$user = Auth::user();
|
| 50 |
+
|
| 51 |
+
$result = $this->searchService->fullRag(
|
| 52 |
+
smiles: $request->validated('smiles'),
|
| 53 |
+
topK: $request->input('top_k', 5),
|
| 54 |
+
userId: $user->id
|
| 55 |
+
);
|
| 56 |
+
|
| 57 |
+
if (!$result['success']) {
|
| 58 |
+
return response()->json([
|
| 59 |
+
'success' => false,
|
| 60 |
+
'message' => $result['error'] ?? 'Search failed',
|
| 61 |
+
], 500);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
return response()->json([
|
| 65 |
+
'success' => true,
|
| 66 |
+
'query' => [
|
| 67 |
+
'smiles' => $request->validated('smiles'),
|
| 68 |
+
'top_k' => $request->input('top_k', 5),
|
| 69 |
+
],
|
| 70 |
+
'compounds' => $result['compounds'],
|
| 71 |
+
'metadata' => $result['metadata'],
|
| 72 |
+
]);
|
| 73 |
+
}
|
| 74 |
+
}
|
app/Http/Controllers/Api/ChemistryController.php
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Http\Requests\ChemistryChatRequest;
|
| 7 |
+
use App\Http\Requests\ChemistryCompareRequest;
|
| 8 |
+
use App\Http\Requests\ChemistryDockingRequest;
|
| 9 |
+
use App\Http\Resources\ChemistryResponseResource;
|
| 10 |
+
use App\Models\ChemistryAnalysis;
|
| 11 |
+
use App\Models\ChemistryCsvJob;
|
| 12 |
+
use App\Models\ChemistryThread;
|
| 13 |
+
use App\Services\ChemistryApiService;
|
| 14 |
+
use Illuminate\Http\JsonResponse;
|
| 15 |
+
use Illuminate\Http\Request;
|
| 16 |
+
use Illuminate\Support\Facades\Storage;
|
| 17 |
+
use Illuminate\Support\Facades\Auth;
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ChemistryController extends BaseController
|
| 21 |
+
{
|
| 22 |
+
public function __construct(
|
| 23 |
+
protected ChemistryApiService $chemistryService
|
| 24 |
+
) {}
|
| 25 |
+
|
| 26 |
+
// ==================== Health ====================
|
| 27 |
+
public function health(): JsonResponse
|
| 28 |
+
{
|
| 29 |
+
$result = $this->chemistryService->healthCheck();
|
| 30 |
+
return response()->json(new ChemistryResponseResource($result));
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// ==================== Threads ====================
|
| 34 |
+
public function createThread(): JsonResponse
|
| 35 |
+
{
|
| 36 |
+
$user = Auth::user();
|
| 37 |
+
|
| 38 |
+
$result = $this->chemistryService->createThread();
|
| 39 |
+
|
| 40 |
+
if (!$result['success']) {
|
| 41 |
+
return response()->json(new ChemistryResponseResource($result), 500);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
$thread = ChemistryThread::create([
|
| 45 |
+
'user_id' => $user->id,
|
| 46 |
+
'thread_id' => $result['data']['thread_id'],
|
| 47 |
+
'title' => 'New Conversation',
|
| 48 |
+
'status' => 'active',
|
| 49 |
+
'last_used_at' => now(),
|
| 50 |
+
]);
|
| 51 |
+
|
| 52 |
+
return response()->json([
|
| 53 |
+
'success' => true,
|
| 54 |
+
'data' => [
|
| 55 |
+
'thread_id' => $thread->thread_id,
|
| 56 |
+
'id' => $thread->id,
|
| 57 |
+
'created_at' => $thread->created_at,
|
| 58 |
+
],
|
| 59 |
+
]);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
public function listThreads(): JsonResponse
|
| 63 |
+
{
|
| 64 |
+
$threads = Auth::user()->chemistryThreads()
|
| 65 |
+
->where('status', 'active')
|
| 66 |
+
->orderBy('last_used_at', 'desc')
|
| 67 |
+
->get(['id', 'thread_id', 'title', 'last_used_at', 'created_at']);
|
| 68 |
+
|
| 69 |
+
return response()->json([
|
| 70 |
+
'success' => true,
|
| 71 |
+
'data' => $threads,
|
| 72 |
+
]);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// ==================== Chat (معدل: title من أول سؤال) ====================
|
| 76 |
+
public function chat(ChemistryChatRequest $request): JsonResponse
|
| 77 |
+
{
|
| 78 |
+
$user = Auth::user();
|
| 79 |
+
$threadId = $request->input('thread_id');
|
| 80 |
+
|
| 81 |
+
if ($threadId) {
|
| 82 |
+
$thread = ChemistryThread::where('user_id', $user->id)
|
| 83 |
+
->where('thread_id', $threadId)
|
| 84 |
+
->first();
|
| 85 |
+
|
| 86 |
+
if (!$thread) {
|
| 87 |
+
return response()->json([
|
| 88 |
+
'success' => false,
|
| 89 |
+
'error' => 'Thread not found or access denied',
|
| 90 |
+
], 403);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// ← لو الـ title لسه "New Conversation"، غيريه لأول سؤال
|
| 94 |
+
if ($thread->title === 'New Conversation') {
|
| 95 |
+
$message = $request->input('message');
|
| 96 |
+
$newTitle = mb_strlen($message) > 50
|
| 97 |
+
? mb_substr($message, 0, 50) . '...'
|
| 98 |
+
: $message;
|
| 99 |
+
|
| 100 |
+
$thread->update([
|
| 101 |
+
'title' => $newTitle,
|
| 102 |
+
'last_used_at' => now(),
|
| 103 |
+
]);
|
| 104 |
+
} else {
|
| 105 |
+
$thread->update(['last_used_at' => now()]);
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
$result = $this->chemistryService->chat(
|
| 110 |
+
$request->input('message'),
|
| 111 |
+
$threadId
|
| 112 |
+
);
|
| 113 |
+
|
| 114 |
+
if (!$result['success']) {
|
| 115 |
+
return response()->json(new ChemistryResponseResource($result), $result['status']);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
ChemistryAnalysis::create([
|
| 119 |
+
'user_id' => $user->id,
|
| 120 |
+
'chemistry_thread_id' => $thread?->id,
|
| 121 |
+
'type' => 'chat',
|
| 122 |
+
'input_data' => $request->input('message'),
|
| 123 |
+
'response' => $result['data']['reply'] ?? null,
|
| 124 |
+
'processing_time_ms' => $result['data']['processing_time_ms'] ?? null,
|
| 125 |
+
'status' => 'success',
|
| 126 |
+
]);
|
| 127 |
+
|
| 128 |
+
return response()->json(new ChemistryResponseResource($result));
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// ==================== Analyze SMILES ====================
|
| 132 |
+
public function analyzeSmiles(Request $request): JsonResponse
|
| 133 |
+
{
|
| 134 |
+
$user = Auth::user();
|
| 135 |
+
|
| 136 |
+
$request->validate([
|
| 137 |
+
'smiles' => 'required|string|max:1000',
|
| 138 |
+
'thread_id' => 'nullable|string|max:255',
|
| 139 |
+
]);
|
| 140 |
+
|
| 141 |
+
$threadId = $request->input('thread_id');
|
| 142 |
+
$thread = $this->validateThread($user->id, $threadId);
|
| 143 |
+
|
| 144 |
+
$smiles = $request->input('smiles');
|
| 145 |
+
|
| 146 |
+
$result = $this->chemistryService->analyzeSmiles($smiles, $threadId);
|
| 147 |
+
|
| 148 |
+
return $this->saveAndRespond($result, $user->id, 'smiles', $smiles, $thread?->id);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// ==================== Compare ====================
|
| 152 |
+
public function compare(ChemistryCompareRequest $request): JsonResponse
|
| 153 |
+
{
|
| 154 |
+
$user = Auth::user();
|
| 155 |
+
$threadId = $request->input('thread_id');
|
| 156 |
+
$thread = $this->validateThread($user->id, $threadId);
|
| 157 |
+
|
| 158 |
+
$result = $this->chemistryService->compareMolecules(
|
| 159 |
+
$request->input('smiles'),
|
| 160 |
+
$threadId
|
| 161 |
+
);
|
| 162 |
+
|
| 163 |
+
$input = implode(', ', $request->input('smiles'));
|
| 164 |
+
return $this->saveAndRespond($result, $user->id, 'compare', $input, $thread?->id);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// ==================== Docking ====================
|
| 168 |
+
public function docking(ChemistryDockingRequest $request): JsonResponse
|
| 169 |
+
{
|
| 170 |
+
$user = Auth::user();
|
| 171 |
+
$threadId = $request->input('thread_id');
|
| 172 |
+
$thread = $this->validateThread($user->id, $threadId);
|
| 173 |
+
|
| 174 |
+
$result = $this->chemistryService->analyzeDocking(
|
| 175 |
+
$request->input('docking_data'),
|
| 176 |
+
$threadId
|
| 177 |
+
);
|
| 178 |
+
|
| 179 |
+
return $this->saveAndRespond($result, $user->id, 'docking', $request->input('docking_data'), $thread?->id);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// ==================== CSV (معدل: required thread_id) ====================
|
| 183 |
+
public function uploadCsv(Request $request): JsonResponse
|
| 184 |
+
{
|
| 185 |
+
$user = Auth::user();
|
| 186 |
+
|
| 187 |
+
$request->validate([
|
| 188 |
+
'file' => 'required|file|mimes:csv,txt|max:2048',
|
| 189 |
+
'analysis_type' => 'nullable|in:full,quick,admet,classify',
|
| 190 |
+
'thread_id' => 'required|string|max:255',
|
| 191 |
+
]);
|
| 192 |
+
|
| 193 |
+
$file = $request->file('file');
|
| 194 |
+
$analysisType = $request->input('analysis_type', 'full');
|
| 195 |
+
$threadId = $request->input('thread_id');
|
| 196 |
+
|
| 197 |
+
// Validate thread belongs to user
|
| 198 |
+
$thread = ChemistryThread::where('user_id', $user->id)
|
| 199 |
+
->where('thread_id', $threadId)
|
| 200 |
+
->first();
|
| 201 |
+
|
| 202 |
+
if (!$thread) {
|
| 203 |
+
return response()->json([
|
| 204 |
+
'success' => false,
|
| 205 |
+
'error' => 'Thread not found or access denied',
|
| 206 |
+
], 403);
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
$result = $this->chemistryService->uploadCsv($file, $analysisType);
|
| 210 |
+
|
| 211 |
+
if (!$result['success']) {
|
| 212 |
+
return response()->json(new ChemistryResponseResource($result), $result['status']);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
$apiJobId = $result['data']['job_id'] ?? null;
|
| 216 |
+
$totalRows = $result['data']['total_rows'] ?? 0;
|
| 217 |
+
|
| 218 |
+
if (!$apiJobId) {
|
| 219 |
+
return response()->json([
|
| 220 |
+
'success' => false,
|
| 221 |
+
'error' => 'Failed to get job ID from API',
|
| 222 |
+
], 500);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
$job = ChemistryCsvJob::create([
|
| 226 |
+
'user_id' => $user->id,
|
| 227 |
+
'chemistry_thread_id' => $thread->id,
|
| 228 |
+
'job_id' => $apiJobId,
|
| 229 |
+
'filename' => $file->getClientOriginalName(),
|
| 230 |
+
'analysis_type' => $analysisType,
|
| 231 |
+
'total_rows' => $totalRows,
|
| 232 |
+
'status' => 'queued',
|
| 233 |
+
]);
|
| 234 |
+
|
| 235 |
+
// Update thread last_used_at
|
| 236 |
+
$thread->update(['last_used_at' => now()]);
|
| 237 |
+
|
| 238 |
+
return response()->json([
|
| 239 |
+
'success' => true,
|
| 240 |
+
'data' => [
|
| 241 |
+
'job_id' => $job->job_id,
|
| 242 |
+
'id' => $job->id,
|
| 243 |
+
'status' => $job->status,
|
| 244 |
+
'total_rows' => $job->total_rows,
|
| 245 |
+
],
|
| 246 |
+
]);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
public function csvStatus(string $jobId): JsonResponse
|
| 250 |
+
{
|
| 251 |
+
$user = Auth::user();
|
| 252 |
+
|
| 253 |
+
$job = ChemistryCsvJob::where('user_id', $user->id)
|
| 254 |
+
->where('job_id', $jobId)
|
| 255 |
+
->first();
|
| 256 |
+
|
| 257 |
+
if (!$job) {
|
| 258 |
+
return response()->json([
|
| 259 |
+
'success' => false,
|
| 260 |
+
'error' => 'Job not found or access denied',
|
| 261 |
+
], 404);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
$result = $this->chemistryService->getCsvStatus($jobId);
|
| 265 |
+
|
| 266 |
+
if ($result['success'] && isset($result['data'])) {
|
| 267 |
+
$apiData = $result['data'];
|
| 268 |
+
|
| 269 |
+
$job->update([
|
| 270 |
+
'status' => $apiData['status'] ?? $job->status,
|
| 271 |
+
'completed_rows' => $apiData['completed'] ?? $job->completed_rows,
|
| 272 |
+
'failed_rows' => $apiData['failed_rows'] ?? $job->failed_rows,
|
| 273 |
+
'progress_percent' => $apiData['progress_percent'] ?? $job->progress_percent,
|
| 274 |
+
]);
|
| 275 |
+
|
| 276 |
+
if ($apiData['status'] === 'done' && !$job->completed_at) {
|
| 277 |
+
$job->update(['completed_at' => now(), 'status' => 'done']);
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
return response()->json([
|
| 282 |
+
'success' => true,
|
| 283 |
+
'data' => [
|
| 284 |
+
'job_id' => $job->job_id,
|
| 285 |
+
'status' => $job->status,
|
| 286 |
+
'total' => $job->total_rows,
|
| 287 |
+
'completed' => $job->completed_rows,
|
| 288 |
+
'failed' => $job->failed_rows,
|
| 289 |
+
'progress_percent' => $job->progress_percent,
|
| 290 |
+
],
|
| 291 |
+
]);
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
public function csvResults(string $jobId)
|
| 295 |
+
{
|
| 296 |
+
$user = Auth::user();
|
| 297 |
+
|
| 298 |
+
$job = ChemistryCsvJob::where('user_id', $user->id)
|
| 299 |
+
->where('job_id', $jobId)
|
| 300 |
+
->first();
|
| 301 |
+
|
| 302 |
+
if (!$job) {
|
| 303 |
+
return response()->json([
|
| 304 |
+
'success' => false,
|
| 305 |
+
'error' => 'Job not found or access denied',
|
| 306 |
+
], 404);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
if ($job->status !== 'done') {
|
| 310 |
+
return response()->json([
|
| 311 |
+
'success' => false,
|
| 312 |
+
'error' => 'Job not completed yet',
|
| 313 |
+
], 400);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// ← Return content from DB if available
|
| 317 |
+
if ($job->result_content) {
|
| 318 |
+
return response($job->result_content)
|
| 319 |
+
->header('Content-Type', 'text/csv')
|
| 320 |
+
->header('Content-Disposition', 'attachment; filename="results_' . $jobId . '.csv"');
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
$result = $this->chemistryService->getCsvResults($jobId);
|
| 324 |
+
|
| 325 |
+
if (is_string($result)) {
|
| 326 |
+
// ← Store content in DB instead of file path
|
| 327 |
+
$job->update([
|
| 328 |
+
'result_content' => $result,
|
| 329 |
+
'result_file_path' => null,
|
| 330 |
+
]);
|
| 331 |
+
|
| 332 |
+
return response($result)
|
| 333 |
+
->header('Content-Type', 'text/csv')
|
| 334 |
+
->header('Content-Disposition', 'attachment; filename="results_' . $jobId . '.csv"');
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
return response()->json(new ChemistryResponseResource($result));
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
public function listJobs(Request $request): JsonResponse
|
| 341 |
+
{
|
| 342 |
+
$user = Auth::user();
|
| 343 |
+
|
| 344 |
+
$query = $user->chemistryCsvJobs()
|
| 345 |
+
->orderBy('created_at', 'desc');
|
| 346 |
+
|
| 347 |
+
// Optional: filter by thread_id
|
| 348 |
+
if ($request->has('thread_id')) {
|
| 349 |
+
$thread = ChemistryThread::where('user_id', $user->id)
|
| 350 |
+
->where('thread_id', $request->input('thread_id'))
|
| 351 |
+
->first();
|
| 352 |
+
|
| 353 |
+
if ($thread) {
|
| 354 |
+
$query->where('chemistry_thread_id', $thread->id);
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
$jobs = $query->get([
|
| 359 |
+
'id',
|
| 360 |
+
'job_id',
|
| 361 |
+
'chemistry_thread_id',
|
| 362 |
+
'filename',
|
| 363 |
+
'analysis_type',
|
| 364 |
+
'status',
|
| 365 |
+
'total_rows',
|
| 366 |
+
'completed_rows',
|
| 367 |
+
'failed_rows',
|
| 368 |
+
'progress_percent',
|
| 369 |
+
'created_at'
|
| 370 |
+
]);
|
| 371 |
+
|
| 372 |
+
return response()->json([
|
| 373 |
+
'success' => true,
|
| 374 |
+
'data' => $jobs,
|
| 375 |
+
]);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
public function deleteJob(string $jobId): JsonResponse
|
| 379 |
+
{
|
| 380 |
+
$user = Auth::user();
|
| 381 |
+
|
| 382 |
+
$job = ChemistryCsvJob::where('user_id', $user->id)
|
| 383 |
+
->where('job_id', $jobId)
|
| 384 |
+
->first();
|
| 385 |
+
|
| 386 |
+
if (!$job) {
|
| 387 |
+
return response()->json([
|
| 388 |
+
'success' => false,
|
| 389 |
+
'error' => 'Job not found or access denied',
|
| 390 |
+
], 404);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
$result = $this->chemistryService->deleteCsvJob($jobId);
|
| 394 |
+
|
| 395 |
+
if ($job->result_file_path && Storage::exists($job->result_file_path)) {
|
| 396 |
+
Storage::delete($job->result_file_path);
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
$job->delete();
|
| 400 |
+
|
| 401 |
+
return response()->json([
|
| 402 |
+
'success' => true,
|
| 403 |
+
'message' => 'Job deleted successfully',
|
| 404 |
+
]);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
// ==================== User History ====================
|
| 408 |
+
public function userHistory(Request $request): JsonResponse
|
| 409 |
+
{
|
| 410 |
+
$user = Auth::user();
|
| 411 |
+
$type = $request->input('type');
|
| 412 |
+
|
| 413 |
+
$query = $user->chemistryAnalyses()
|
| 414 |
+
->with('thread:id,thread_id,title')
|
| 415 |
+
->orderBy('created_at', 'desc');
|
| 416 |
+
|
| 417 |
+
if ($type) {
|
| 418 |
+
$query->where('type', $type);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
$history = $query->paginate(20);
|
| 422 |
+
|
| 423 |
+
return response()->json([
|
| 424 |
+
'success' => true,
|
| 425 |
+
'data' => $history,
|
| 426 |
+
]);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
// ==================== Thread Messages (جديد) ====================
|
| 430 |
+
public function threadMessages(Request $request): JsonResponse
|
| 431 |
+
{
|
| 432 |
+
$user = Auth::user();
|
| 433 |
+
|
| 434 |
+
$request->validate([
|
| 435 |
+
'thread_id' => 'required|string|max:255',
|
| 436 |
+
]);
|
| 437 |
+
|
| 438 |
+
$threadId = $request->input('thread_id');
|
| 439 |
+
|
| 440 |
+
$thread = ChemistryThread::where('user_id', $user->id)
|
| 441 |
+
->where('thread_id', $threadId)
|
| 442 |
+
->first();
|
| 443 |
+
|
| 444 |
+
if (!$thread) {
|
| 445 |
+
return response()->json([
|
| 446 |
+
'success' => false,
|
| 447 |
+
'error' => 'Thread not found or access denied',
|
| 448 |
+
], 403);
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
// Chat/analysis messages
|
| 452 |
+
$messages = ChemistryAnalysis::where('user_id', $user->id)
|
| 453 |
+
->where('chemistry_thread_id', $thread->id)
|
| 454 |
+
->orderBy('created_at', 'asc')
|
| 455 |
+
->get(['id', 'type', 'input_data', 'response', 'status', 'created_at']);
|
| 456 |
+
|
| 457 |
+
// CSV jobs related to this thread
|
| 458 |
+
$csvJobs = ChemistryCsvJob::where('user_id', $user->id)
|
| 459 |
+
->where('chemistry_thread_id', $thread->id)
|
| 460 |
+
->orderBy('created_at', 'asc')
|
| 461 |
+
->get([
|
| 462 |
+
'id',
|
| 463 |
+
'job_id',
|
| 464 |
+
'filename',
|
| 465 |
+
'analysis_type',
|
| 466 |
+
'status',
|
| 467 |
+
'total_rows',
|
| 468 |
+
'completed_rows',
|
| 469 |
+
'failed_rows',
|
| 470 |
+
'progress_percent',
|
| 471 |
+
'result_content',
|
| 472 |
+
'created_at',
|
| 473 |
+
'completed_at'
|
| 474 |
+
]);
|
| 475 |
+
|
| 476 |
+
return response()->json([
|
| 477 |
+
'success' => true,
|
| 478 |
+
'data' => [
|
| 479 |
+
'thread_id' => $thread->thread_id,
|
| 480 |
+
'title' => $thread->title,
|
| 481 |
+
'messages' => $messages,
|
| 482 |
+
'csv_jobs' => $csvJobs,
|
| 483 |
+
],
|
| 484 |
+
]);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
// ==================== Helper Methods ====================
|
| 488 |
+
private function validateThread(int $userId, ?string $threadId): ?ChemistryThread
|
| 489 |
+
{
|
| 490 |
+
if (!$threadId) return null;
|
| 491 |
+
|
| 492 |
+
return ChemistryThread::where('user_id', $userId)
|
| 493 |
+
->where('thread_id', $threadId)
|
| 494 |
+
->first();
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
private function saveAndRespond(array $result, int $userId, string $type, string $input, ?int $threadId = null): JsonResponse
|
| 498 |
+
{
|
| 499 |
+
if (!$result['success']) {
|
| 500 |
+
ChemistryAnalysis::create([
|
| 501 |
+
'user_id' => $userId,
|
| 502 |
+
'chemistry_thread_id' => $threadId,
|
| 503 |
+
'type' => $type,
|
| 504 |
+
'input_data' => $input,
|
| 505 |
+
'status' => 'failed',
|
| 506 |
+
'error_message' => is_string($result['error']) ? $result['error'] : json_encode($result['error']),
|
| 507 |
+
]);
|
| 508 |
+
|
| 509 |
+
return response()->json(new ChemistryResponseResource($result), $result['status']);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
ChemistryAnalysis::create([
|
| 513 |
+
'user_id' => $userId,
|
| 514 |
+
'chemistry_thread_id' => $threadId,
|
| 515 |
+
'type' => $type,
|
| 516 |
+
'input_data' => $input,
|
| 517 |
+
'response' => $result['data']['reply'] ?? null,
|
| 518 |
+
'processing_time_ms' => $result['data']['processing_time_ms'] ?? null,
|
| 519 |
+
'status' => 'success',
|
| 520 |
+
]);
|
| 521 |
+
|
| 522 |
+
return response()->json(new ChemistryResponseResource($result));
|
| 523 |
+
}
|
| 524 |
+
}
|
app/Http/Controllers/Api/ConvertSmilesController.php
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use App\Models\DockingJob;
|
| 6 |
+
use App\Traits\ApiResponseTrait;
|
| 7 |
+
use Illuminate\Http\Request;
|
| 8 |
+
use Illuminate\Support\Facades\Log;
|
| 9 |
+
use Illuminate\Support\Facades\Process;
|
| 10 |
+
use Illuminate\Support\Facades\Validator;
|
| 11 |
+
use Illuminate\Support\Str;
|
| 12 |
+
|
| 13 |
+
class ConvertSmilesController
|
| 14 |
+
{
|
| 15 |
+
use ApiResponseTrait;
|
| 16 |
+
|
| 17 |
+
public function convert(Request $request)
|
| 18 |
+
{
|
| 19 |
+
$validator = Validator::make($request->all(), [
|
| 20 |
+
'ligand_smiles' => 'required|string|max:2000',
|
| 21 |
+
]);
|
| 22 |
+
|
| 23 |
+
if ($validator->fails()) {
|
| 24 |
+
return $this->errorResponse('Validation Error', 422, $validator->errors());
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
$conversion = $this->convertSmilesToPdbqt($request->ligand_smiles);
|
| 28 |
+
|
| 29 |
+
if ($conversion['status'] === 'error') {
|
| 30 |
+
return $this->errorResponse(
|
| 31 |
+
'SMILES conversion failed: ' . $conversion['message'],
|
| 32 |
+
422
|
| 33 |
+
);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Create a completed job record scoped to SMILES-only conversions
|
| 37 |
+
$job = DockingJob::create([
|
| 38 |
+
'user_id' => $request->user()->id,
|
| 39 |
+
'input_type' => 'smiles',
|
| 40 |
+
'smiles' => $request->ligand_smiles,
|
| 41 |
+
'protein_name' => null,
|
| 42 |
+
'ligand_name' => 'generated_from_smiles.pdbqt',
|
| 43 |
+
'protein_path' => '',
|
| 44 |
+
'ligand_path' => $conversion['output_file'],
|
| 45 |
+
'status' => 'completed',
|
| 46 |
+
'result_data' => [
|
| 47 |
+
'output_file' => $conversion['output_file'],
|
| 48 |
+
'smiles' => $request->ligand_smiles,
|
| 49 |
+
],
|
| 50 |
+
]);
|
| 51 |
+
|
| 52 |
+
return $this->successResponse('SMILES converted to PDBQT successfully', [
|
| 53 |
+
'job_id' => $job->id,
|
| 54 |
+
'download_url' => url('/api/convert-smiles/download/' . $job->id),
|
| 55 |
+
'smiles' => $request->ligand_smiles,
|
| 56 |
+
]);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Download the converted PDBQT file.
|
| 61 |
+
*/
|
| 62 |
+
public function download(Request $request, $id)
|
| 63 |
+
{
|
| 64 |
+
$job = DockingJob::where('id', $id)
|
| 65 |
+
->where('user_id', $request->user()->id)
|
| 66 |
+
->where('input_type', 'smiles')
|
| 67 |
+
->whereNull('protein_name') // ensure it's a convert-only job
|
| 68 |
+
->first();
|
| 69 |
+
|
| 70 |
+
if (! $job || $job->status !== 'completed') {
|
| 71 |
+
return $this->errorResponse('File not available or unauthorized', 404);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
$filePath = $job->result_data['output_file'] ?? $job->ligand_path ?? null;
|
| 75 |
+
|
| 76 |
+
if (! $filePath || ! file_exists($filePath)) {
|
| 77 |
+
return $this->errorResponse('File not found on server', 404);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
return response()->download($filePath, 'converted_ligand_' . $job->id . '.pdbqt');
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
* List all SMILES conversion jobs for the authenticated user.
|
| 85 |
+
*/
|
| 86 |
+
public function history(Request $request)
|
| 87 |
+
{
|
| 88 |
+
$perPage = min((int) $request->query('per_page', 15), 100);
|
| 89 |
+
|
| 90 |
+
$paginator = DockingJob::where('user_id', $request->user()->id)
|
| 91 |
+
->where('input_type', 'smiles')
|
| 92 |
+
->whereNull('protein_name')
|
| 93 |
+
->orderBy('created_at', 'desc')
|
| 94 |
+
->paginate($perPage);
|
| 95 |
+
|
| 96 |
+
$items = collect($paginator->items())->map(function ($job) {
|
| 97 |
+
$data = [
|
| 98 |
+
'job_id' => $job->id,
|
| 99 |
+
'status' => $job->status,
|
| 100 |
+
'smiles' => $job->smiles,
|
| 101 |
+
'created_at' => $job->created_at->toIso8601String(),
|
| 102 |
+
];
|
| 103 |
+
|
| 104 |
+
if ($job->status === 'completed' && $job->result_data) {
|
| 105 |
+
$data['results'] = [
|
| 106 |
+
'download_url' => url('/api/convert-smiles/download/' . $job->id),
|
| 107 |
+
];
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return $data;
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
return $this->successResponse('Conversion history retrieved successfully', [
|
| 114 |
+
'items' => $items,
|
| 115 |
+
'pagination' => [
|
| 116 |
+
'current_page' => $paginator->currentPage(),
|
| 117 |
+
'per_page' => $paginator->perPage(),
|
| 118 |
+
'total' => $paginator->total(),
|
| 119 |
+
'last_page' => $paginator->lastPage(),
|
| 120 |
+
'has_more' => $paginator->hasMorePages(),
|
| 121 |
+
],
|
| 122 |
+
]);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* Internal helper: run the SMILES → PDBQT Python conversion.
|
| 127 |
+
*
|
| 128 |
+
* @return array{status: string, output_file?: string, message?: string}
|
| 129 |
+
*/
|
| 130 |
+
private function convertSmilesToPdbqt(string $smiles): array
|
| 131 |
+
{
|
| 132 |
+
$pythonPath = env('DOCKING_PYTHON_PATH', base_path('vina_env/bin/python'));
|
| 133 |
+
$scriptPath = env('SMILES_SCRIPT_PATH', base_path('scripts/smiles_to_pdbqt.py'));
|
| 134 |
+
|
| 135 |
+
$outputFilename = Str::random(40) . '.pdbqt';
|
| 136 |
+
$outputDir = storage_path('app/private/docking/generated');
|
| 137 |
+
$outputPath = $outputDir . '/' . $outputFilename;
|
| 138 |
+
|
| 139 |
+
if (! is_dir($outputDir)) {
|
| 140 |
+
mkdir($outputDir, 0755, true);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
$cmd = sprintf(
|
| 144 |
+
'%s %s %s %s',
|
| 145 |
+
escapeshellarg($pythonPath),
|
| 146 |
+
escapeshellarg($scriptPath),
|
| 147 |
+
escapeshellarg($smiles),
|
| 148 |
+
escapeshellarg($outputPath)
|
| 149 |
+
);
|
| 150 |
+
|
| 151 |
+
try {
|
| 152 |
+
$result = Process::timeout(120)->run($cmd);
|
| 153 |
+
$fullOutput = $result->output();
|
| 154 |
+
$outputData = null;
|
| 155 |
+
|
| 156 |
+
if (preg_match('/\{"status":\s*"(success|error)".*\}/s', $fullOutput, $matches)) {
|
| 157 |
+
$outputData = json_decode($matches[0], true);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
if (! $result->successful()) {
|
| 161 |
+
$errorMsg = $outputData['message'] ?? $result->errorOutput() ?: 'Unknown conversion error';
|
| 162 |
+
Log::error('SMILES conversion failed', ['smiles' => $smiles, 'error' => $errorMsg]);
|
| 163 |
+
return ['status' => 'error', 'message' => $errorMsg];
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
if ($outputData && $outputData['status'] === 'success') {
|
| 167 |
+
return $outputData;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
if ($outputData && $outputData['status'] === 'error') {
|
| 171 |
+
return $outputData;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
return ['status' => 'error', 'message' => 'Unexpected output from conversion script'];
|
| 175 |
+
} catch (\Exception $e) {
|
| 176 |
+
Log::error('SMILES conversion exception', ['smiles' => $smiles, 'error' => $e->getMessage()]);
|
| 177 |
+
return ['status' => 'error', 'message' => $e->getMessage()];
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
}
|
app/Http/Controllers/Api/DockingController.php
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use App\Http\Requests\Docking\SubmitDockingRequest;
|
| 6 |
+
use App\Jobs\ConvertSmilesJob;
|
| 7 |
+
use App\Jobs\RunDockingJob;
|
| 8 |
+
use App\Models\DockingJob;
|
| 9 |
+
use App\Traits\ApiResponseTrait;
|
| 10 |
+
use Illuminate\Http\Request;
|
| 11 |
+
use Illuminate\Http\UploadedFile;
|
| 12 |
+
use Illuminate\Support\Facades\Log;
|
| 13 |
+
use Illuminate\Support\Facades\Process;
|
| 14 |
+
use Illuminate\Http\Exceptions\HttpResponseException;
|
| 15 |
+
use Illuminate\Support\Str;
|
| 16 |
+
use Laravel\Sanctum\PersonalAccessToken;
|
| 17 |
+
use Symfony\Component\HttpFoundation\StreamedResponse;
|
| 18 |
+
|
| 19 |
+
class DockingController
|
| 20 |
+
{
|
| 21 |
+
use ApiResponseTrait;
|
| 22 |
+
|
| 23 |
+
public function submit(SubmitDockingRequest $request)
|
| 24 |
+
{
|
| 25 |
+
$isSmiles = $request->filled('ligand_smiles');
|
| 26 |
+
|
| 27 |
+
if ($isSmiles) {
|
| 28 |
+
$ligandPath = null;
|
| 29 |
+
} else {
|
| 30 |
+
$ligandPath = $this->storeAsPdbqt($request->file('ligand_file'), true);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
$proteinPath = $this->storeAsPdbqt($request->file('protein_file'), false);
|
| 34 |
+
|
| 35 |
+
$job = DockingJob::create([
|
| 36 |
+
'user_id' => $request->user()->id,
|
| 37 |
+
'input_type' => $isSmiles ? 'smiles' : 'file',
|
| 38 |
+
'smiles' => $isSmiles ? $request->ligand_smiles : null,
|
| 39 |
+
'protein_name' => $request->protein_name,
|
| 40 |
+
'ligand_name' => $request->ligand_name,
|
| 41 |
+
'protein_path' => $proteinPath,
|
| 42 |
+
'ligand_path' => $ligandPath,
|
| 43 |
+
'status' => 'pending',
|
| 44 |
+
]);
|
| 45 |
+
|
| 46 |
+
$params = [
|
| 47 |
+
'center_x' => $request->center_x,
|
| 48 |
+
'center_y' => $request->center_y,
|
| 49 |
+
'center_z' => $request->center_z,
|
| 50 |
+
'box_size_x' => $request->box_size_x,
|
| 51 |
+
'box_size_y' => $request->box_size_y,
|
| 52 |
+
'box_size_z' => $request->box_size_z,
|
| 53 |
+
'exhaustiveness' => $request->exhaustiveness ?? 8,
|
| 54 |
+
'n_poses' => $request->n_poses ?? 5,
|
| 55 |
+
];
|
| 56 |
+
|
| 57 |
+
if ($isSmiles) {
|
| 58 |
+
ConvertSmilesJob::dispatch($job, $request->ligand_smiles, $params);
|
| 59 |
+
} else {
|
| 60 |
+
RunDockingJob::dispatch($job, $params);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return $this->successResponse('Docking Job Successfully Queued', [
|
| 64 |
+
'job_id' => $job->id,
|
| 65 |
+
'status' => $job->status,
|
| 66 |
+
]);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
public function history(Request $request)
|
| 70 |
+
{
|
| 71 |
+
$perPage = min((int) $request->query('per_page', 15), 100);
|
| 72 |
+
|
| 73 |
+
$paginator = DockingJob::dockingOnly()
|
| 74 |
+
->where('user_id', $request->user()->id)
|
| 75 |
+
->orderBy('created_at', 'desc')
|
| 76 |
+
->paginate($perPage)
|
| 77 |
+
->through(function ($job) {
|
| 78 |
+
return [
|
| 79 |
+
'id' => $job->id,
|
| 80 |
+
'status' => $job->status,
|
| 81 |
+
'protein' => $job->protein_name,
|
| 82 |
+
'ligand' => $job->ligand_name,
|
| 83 |
+
'created_at' => $job->created_at->toIso8601String(),
|
| 84 |
+
'download_url' => url('/api/docking/download/'.$job->id),
|
| 85 |
+
'scores' => $job->vina_scores ?? [],
|
| 86 |
+
'error' => $job->status === 'failed' ? ($job->result_data['error'] ?? null) : null,
|
| 87 |
+
];
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
return $this->paginatedResponse('Docking history retrieved successfully', $paginator);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
public function status(Request $request, $id)
|
| 94 |
+
{
|
| 95 |
+
$job = DockingJob::dockingOnly()
|
| 96 |
+
->where('id', $id)
|
| 97 |
+
->where('user_id', $request->user()->id)
|
| 98 |
+
->first();
|
| 99 |
+
|
| 100 |
+
if (! $job) {
|
| 101 |
+
return $this->errorResponse('Docking job not found or unauthorized', 404);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
return $this->successResponse('Job details retrieved successfully', [
|
| 105 |
+
'id' => $job->id,
|
| 106 |
+
'status' => $job->status,
|
| 107 |
+
'protein' => $job->protein_name,
|
| 108 |
+
'ligand' => $job->ligand_name,
|
| 109 |
+
'created_at' => $job->created_at->toIso8601String(),
|
| 110 |
+
'download_url' => url('/api/docking/download/'.$job->id),
|
| 111 |
+
'scores' => $job->vina_scores ?? [],
|
| 112 |
+
'error' => $job->status === 'failed' ? ($job->result_data['error'] ?? null) : null,
|
| 113 |
+
]);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
public function download(Request $request, $id)
|
| 117 |
+
{
|
| 118 |
+
$token = $request->bearerToken() ?? $request->query('token');
|
| 119 |
+
|
| 120 |
+
if (! $token) {
|
| 121 |
+
return $this->errorResponse('Unauthenticated', 401);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
$accessToken = PersonalAccessToken::findToken($token);
|
| 125 |
+
if (! $accessToken || ! $accessToken->tokenable) {
|
| 126 |
+
return $this->errorResponse('Invalid token', 401);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
$job = DockingJob::dockingOnly()
|
| 130 |
+
->where('id', $id)
|
| 131 |
+
->where('user_id', $accessToken->tokenable->id)
|
| 132 |
+
->first();
|
| 133 |
+
|
| 134 |
+
if (! $job || $job->status !== 'completed') {
|
| 135 |
+
return $this->errorResponse('Docking file not available or unauthorized', 404);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
$filePath = $job->result_data['output_file'] ?? null;
|
| 139 |
+
|
| 140 |
+
if (! $filePath || ! file_exists($filePath)) {
|
| 141 |
+
return $this->errorResponse('File not found on server', 404);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
return new StreamedResponse(function () use ($filePath) {
|
| 145 |
+
$stream = fopen($filePath, 'rb');
|
| 146 |
+
if ($stream) {
|
| 147 |
+
fpassthru($stream);
|
| 148 |
+
fclose($stream);
|
| 149 |
+
}
|
| 150 |
+
}, 200, [
|
| 151 |
+
'Content-Type' => 'application/octet-stream',
|
| 152 |
+
'Content-Disposition' => 'attachment; filename="docking_result_'.$job->id.'.pdbqt"',
|
| 153 |
+
'Content-Length' => filesize($filePath),
|
| 154 |
+
]);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
private function storeAsPdbqt(UploadedFile $file, bool $isLigand): string
|
| 158 |
+
{
|
| 159 |
+
$ext = strtolower($file->getClientOriginalExtension());
|
| 160 |
+
$filename = Str::random(40).'.pdbqt';
|
| 161 |
+
$destPath = storage_path('app/private/docking/'.$filename);
|
| 162 |
+
|
| 163 |
+
if ($ext === 'pdbqt') {
|
| 164 |
+
$file->storeAs('docking', $filename);
|
| 165 |
+
} elseif ($ext === 'pdb') {
|
| 166 |
+
$this->convertPdbToPdbqt($file->getRealPath(), $destPath, $isLigand);
|
| 167 |
+
} else {
|
| 168 |
+
throw new HttpResponseException(
|
| 169 |
+
$this->errorResponse('Unsupported file format: .'.$ext.'. Only .pdb and .pdbqt files are accepted.', 422)
|
| 170 |
+
);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
return $destPath;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
private function convertPdbToPdbqt(string $source, string $dest, bool $isLigand): void
|
| 177 |
+
{
|
| 178 |
+
$pythonPath = env('DOCKING_PYTHON_PATH');
|
| 179 |
+
$obabel = $pythonPath ? dirname($pythonPath).'/obabel' : 'obabel';
|
| 180 |
+
if (! file_exists($obabel)) {
|
| 181 |
+
$obabel = 'obabel';
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
$result = Process::timeout(60)->run([
|
| 185 |
+
$obabel,
|
| 186 |
+
'-ipdb', $source,
|
| 187 |
+
'-opdbqt',
|
| 188 |
+
'-O', $dest,
|
| 189 |
+
]);
|
| 190 |
+
|
| 191 |
+
if (! $result->successful()) {
|
| 192 |
+
Log::error('PDB-to-PDBQT conversion failed', [
|
| 193 |
+
'source' => $source,
|
| 194 |
+
'dest' => $dest,
|
| 195 |
+
'error' => $result->errorOutput(),
|
| 196 |
+
]);
|
| 197 |
+
throw new HttpResponseException(
|
| 198 |
+
$this->errorResponse('Failed to convert PDB file to PDBQT format.', 500)
|
| 199 |
+
);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
$content = file_get_contents($dest);
|
| 203 |
+
$lines = explode("\n", $content);
|
| 204 |
+
|
| 205 |
+
if ($isLigand) {
|
| 206 |
+
$lines = array_map(fn ($line) => str_starts_with($line, 'ATOM') ? 'HETATM'.substr($line, 6) : $line, $lines);
|
| 207 |
+
} else {
|
| 208 |
+
$lines = array_filter($lines, fn ($line) => preg_match('/^(ATOM|HETATM)/', $line));
|
| 209 |
+
$lines = array_values($lines);
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
file_put_contents($dest, implode("\n", $lines));
|
| 213 |
+
}
|
| 214 |
+
}
|
app/Http/Controllers/Api/EmailVerificationController.php
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
namespace App\Http\Controllers\Api;
|
| 4 |
+
|
| 5 |
+
use App\Http\Controllers\Controller;
|
| 6 |
+
use App\Http\Requests\VerifyOtpRequest;
|
| 7 |
+
use App\Services\OtpService;
|
| 8 |
+
use App\Models\User;
|
| 9 |
+
use Illuminate\Http\Request;
|
| 10 |
+
|
| 11 |
+
class EmailVerificationController extends BaseController
|
| 12 |
+
{
|
| 13 |
+
protected $otpService;
|
| 14 |
+
|
| 15 |
+
public function __construct(OtpService $otpService)
|
| 16 |
+
{
|
| 17 |
+
$this->otpService = $otpService;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Verify Email OTP
|
| 22 |
+
*/
|
| 23 |
+
public function verify(Request $request)
|
| 24 |
+
{
|
| 25 |
+
$request->validate([
|
| 26 |
+
'email' => 'required|email|exists:users,email',
|
| 27 |
+
'otp' => 'required|digits:6',
|
| 28 |
+
]);
|
| 29 |
+
|
| 30 |
+
$user = User::where('email', $request->email)->first();
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
$this->otpService->verifyOtp($user, 'email_verification', $request->otp);
|
| 34 |
+
|
| 35 |
+
$user->update([
|
| 36 |
+
'is_verified' => true,
|
| 37 |
+
'email_verification_otp' => null,
|
| 38 |
+
'email_verification_otp_expires_at' => null,
|
| 39 |
+
]);
|
| 40 |
+
|
| 41 |
+
$token = $user->createToken('auth_token')->plainTextToken;
|
| 42 |
+
|
| 43 |
+
return response()->json([
|
| 44 |
+
'success' => true,
|
| 45 |
+
'message' => 'Email verified successfully',
|
| 46 |
+
'data' => [
|
| 47 |
+
'token' => $token,
|
| 48 |
+
'user' => $user
|
| 49 |
+
]
|
| 50 |
+
]);
|
| 51 |
+
} catch (\Exception $e) {
|
| 52 |
+
return response()->json([
|
| 53 |
+
'success' => false,
|
| 54 |
+
'message' => $e->getMessage()
|
| 55 |
+
], 400);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Resend OTP
|
| 61 |
+
*/
|
| 62 |
+
public function resend(Request $request)
|
| 63 |
+
{
|
| 64 |
+
$request->validate([
|
| 65 |
+
'email' => 'required|email|exists:users,email'
|
| 66 |
+
]);
|
| 67 |
+
|
| 68 |
+
$user = User::where('email', $request->email)->first();
|
| 69 |
+
|
| 70 |
+
try {
|
| 71 |
+
$this->otpService->resendOtp($user, 'email_verification');
|
| 72 |
+
|
| 73 |
+
return response()->json([
|
| 74 |
+
'success' => true,
|
| 75 |
+
'message' => 'OTP resent successfully',
|
| 76 |
+
'data' => ['email' => $user->email]
|
| 77 |
+
]);
|
| 78 |
+
} catch (\Exception $e) {
|
| 79 |
+
return response()->json([
|
| 80 |
+
'success' => false,
|
| 81 |
+
'message' => $e->getMessage()
|
| 82 |
+
], 400);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|