AILIXIR Bot commited on
Commit
907b200
·
1 Parent(s): c1aa524

Auto-sync: 201d269f287526269b8ffded5f450ff36b46c740

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .editorconfig +18 -0
  2. .env.docker +36 -0
  3. .env.example +16 -0
  4. .env.hf +37 -0
  5. .gitattributes +11 -35
  6. .github/workflows/api-tests-hf.yml +63 -0
  7. .github/workflows/api-tests.yml +135 -0
  8. .github/workflows/e2e-md-test.yml +139 -0
  9. .github/workflows/integration-test-hf.yml +492 -0
  10. .github/workflows/integration-test.yml +553 -0
  11. .github/workflows/keep-hf-alive.yml +21 -0
  12. .github/workflows/md-simulation-health.yml +117 -0
  13. .github/workflows/push-models-to-hf.yml +31 -0
  14. .github/workflows/sync-hf-all.yml +232 -0
  15. .gitignore +37 -0
  16. .scribe/.filehashes +4 -0
  17. .scribe/auth.md +3 -0
  18. .scribe/endpoints.cache/00.yaml +1503 -0
  19. .scribe/endpoints/00.yaml +1501 -0
  20. .scribe/endpoints/custom.0.yaml +53 -0
  21. .scribe/intro.md +13 -0
  22. API.md +0 -0
  23. ARCHITECTURE.md +224 -0
  24. CHANGELOG_SQLITE_HF.md +214 -0
  25. DOCKER.md +63 -0
  26. DOCKER_FIXES.md +231 -0
  27. DOCUMENTATION_STRUCTURE.md +449 -0
  28. Dockerfile +115 -0
  29. HF_SPACES_GUIDE.md +277 -0
  30. HUGGINGFACE_FLOW.md +186 -0
  31. README.md +16 -36
  32. README_HF.md +24 -0
  33. __pycache__/app.cpython-310.pyc +0 -0
  34. _ide_helper.php +0 -0
  35. _ide_helper_models.php +774 -0
  36. ailixir-api-tests.zip +0 -0
  37. app.py +0 -736
  38. app.py.backup_before_final_ui +0 -754
  39. app.py.backup_old_tabs +0 -754
  40. app/Helpers/OtpHelper.php +28 -0
  41. app/Http/Controllers/Api/AdmetController.php +29 -0
  42. app/Http/Controllers/Api/AiServicesIntegrationController.php +121 -0
  43. app/Http/Controllers/Api/AuthController.php +115 -0
  44. app/Http/Controllers/Api/AwardController.php +52 -0
  45. app/Http/Controllers/Api/BaseController.php +13 -0
  46. app/Http/Controllers/Api/ChemicalSearchController.php +74 -0
  47. app/Http/Controllers/Api/ChemistryController.php +524 -0
  48. app/Http/Controllers/Api/ConvertSmilesController.php +180 -0
  49. app/Http/Controllers/Api/DockingController.php +214 -0
  50. 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
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
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: EGFR AI Demo
3
- emoji: 🧪
4
  colorFrom: blue
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 6.13.0
8
- python_version: "3.10"
9
- app_file: app.py
10
- pinned: false
11
- license: other
12
  ---
13
 
14
- # EGFR AI Demo
15
 
16
- Private Gradio demo for the EGFR AI backend workflow.
17
 
18
- This Space is a CPU demo version.
 
 
19
 
20
- It demonstrates:
21
 
22
- - SMILES property calculation
23
- - response schema for generated molecules
24
- - `pred_pAff_mean` field placeholder / integration point
25
- - docking columns and backend contract
26
- - example response from the private artifact bundle
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
+ }