=Apyhtml20 Claude Sonnet 4.6 commited on
Commit
45fa780
Β·
1 Parent(s): 5309c0a

Add RAG module: PostgreSQL + pgvector in same container

Browse files

- PostgreSQL 16 + pgvector installed in Docker image
- start.sh initializes DB and starts FastAPI on port 7860
- Full RAG pipeline: TF-IDF/SVD embeddings + pgvector search
- NVIDIA NIM justification (requires NVIDIA_API_KEY secret)
- Multi-model inference: logistic, lgbm, xgb
- train.json (16MB) tracked via git-lfs
- Angular SPA served from FastAPI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (9) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +16 -1
  3. api/inference.py +96 -14
  4. api/main.py +62 -11
  5. api/rag.py +230 -0
  6. api/schema.py +13 -1
  7. data/raw/train.json +3 -0
  8. requirements.txt +10 -19
  9. start.sh +22 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* 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
 
 
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
36
+ data/raw/*.json filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,5 +1,17 @@
1
  FROM python:3.11-slim
2
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  WORKDIR /app
4
 
5
  COPY requirements.txt .
@@ -7,8 +19,11 @@ RUN pip install --no-cache-dir -r requirements.txt
7
 
8
  COPY api/ api/
9
  COPY models/ models/
 
10
  COPY frontend/dist/frontend/browser/ frontend/dist/frontend/browser/
 
 
11
 
12
  EXPOSE 7860
13
 
14
- CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
  FROM python:3.11-slim
2
 
3
+ RUN apt-get update && apt-get install -y \
4
+ postgresql \
5
+ postgresql-contrib \
6
+ libpq-dev \
7
+ build-essential \
8
+ git \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ RUN git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git /pgvector \
12
+ && cd /pgvector && make && make install \
13
+ && rm -rf /pgvector
14
+
15
  WORKDIR /app
16
 
17
  COPY requirements.txt .
 
19
 
20
  COPY api/ api/
21
  COPY models/ models/
22
+ COPY data/ data/
23
  COPY frontend/dist/frontend/browser/ frontend/dist/frontend/browser/
24
+ COPY start.sh /start.sh
25
+ RUN chmod +x /start.sh
26
 
27
  EXPOSE 7860
28
 
29
+ CMD ["/start.sh"]
api/inference.py CHANGED
@@ -1,22 +1,104 @@
1
  import joblib
 
 
2
 
3
- model = joblib.load(
4
- "models/best_logistic.pkl"
5
- )
6
 
7
- vectorizer = joblib.load(
8
- "models/tfidf_vectorizer.pkl"
9
- )
10
 
11
 
12
- def predict_claim(text):
 
13
 
14
- vector = vectorizer.transform(
15
- [text]
16
- )
17
 
18
- prediction = model.predict(
19
- vector
20
- )
21
 
22
- return str(prediction[0])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import joblib
2
+ import os
3
+ from fastapi import HTTPException
4
 
5
+ MODELS_DIR = os.path.join(os.path.dirname(__file__), '..', 'models')
 
 
6
 
7
+ _cache: dict = {}
 
 
8
 
9
 
10
+ def _path(filename: str) -> str:
11
+ return os.path.join(MODELS_DIR, filename)
12
 
 
 
 
13
 
14
+ def _available(filename: str) -> bool:
15
+ return os.path.exists(_path(filename))
 
16
 
17
+
18
+ def _load(filename: str, key: str) -> bool:
19
+ if not _available(filename):
20
+ return False
21
+ try:
22
+ _cache[key] = joblib.load(_path(filename))
23
+ return True
24
+ except Exception as e:
25
+ print(f"WARNING: could not load {filename}: {e}")
26
+ return False
27
+
28
+
29
+ def get_available_models() -> list[str]:
30
+ available = []
31
+ if 'logistic' in _cache: available.append('logistic')
32
+ if 'lgbm' in _cache: available.append('lgbm')
33
+ if 'xgb' in _cache: available.append('xgb')
34
+ return available
35
+
36
+
37
+ def load_models() -> None:
38
+ if 'vectorizer' not in _cache:
39
+ if not _available('tfidf_vectorizer.pkl'):
40
+ raise RuntimeError(
41
+ "tfidf_vectorizer.pkl not found in models/. "
42
+ "Run scripts/train_all_models.py first."
43
+ )
44
+ _cache['vectorizer'] = joblib.load(_path('tfidf_vectorizer.pkl'))
45
+ print("Vectorizer loaded.")
46
+
47
+ if 'logistic' not in _cache:
48
+ if _load('best_logistic.pkl', 'logistic'):
49
+ print("Logistic model loaded.")
50
+
51
+ if 'lgbm' not in _cache:
52
+ if _load('best_lgbm.pkl', 'lgbm'):
53
+ print("LightGBM model loaded.")
54
+
55
+ if 'xgb' not in _cache:
56
+ ok1 = _load('best_xgb.pkl', 'xgb')
57
+ ok2 = _load('xgb_encoder.pkl', 'xgb_encoder')
58
+ if ok1 and ok2:
59
+ print("XGBoost model loaded.")
60
+ elif ok1:
61
+ del _cache['xgb']
62
+
63
+ available = get_available_models()
64
+ if not available:
65
+ raise RuntimeError(
66
+ "No models loaded. Run scripts/train_all_models.py first."
67
+ )
68
+ print(f"Models ready: {available}")
69
+
70
+
71
+ def predict_claim(text: str, model_id: str = 'logistic') -> dict:
72
+ if 'vectorizer' not in _cache:
73
+ raise HTTPException(status_code=503, detail="Models not loaded.")
74
+
75
+ vectorizer = _cache['vectorizer']
76
+ vector = vectorizer.transform([text])
77
+
78
+ if model_id == 'lgbm':
79
+ if 'lgbm' not in _cache:
80
+ raise HTTPException(status_code=503, detail="LightGBM model not available.")
81
+ model = _cache['lgbm']
82
+ prediction = str(model.predict(vector)[0])
83
+ proba_values = model.predict_proba(vector)[0]
84
+ probabilities = {str(c): float(p) for c, p in zip(model.classes_, proba_values)}
85
+
86
+ elif model_id == 'xgb':
87
+ if 'xgb' not in _cache:
88
+ raise HTTPException(status_code=503, detail="XGBoost model not available.")
89
+ model = _cache['xgb']
90
+ encoder = _cache['xgb_encoder']
91
+ pred_enc = model.predict(vector)[0]
92
+ prediction = str(encoder.inverse_transform([pred_enc])[0])
93
+ proba_values = model.predict_proba(vector)[0]
94
+ probabilities = {str(c): float(p) for c, p in zip(encoder.classes_, proba_values)}
95
+
96
+ else: # logistic (default)
97
+ if 'logistic' not in _cache:
98
+ raise HTTPException(status_code=503, detail="Logistic model not available.")
99
+ model = _cache['logistic']
100
+ prediction = str(model.predict(vector)[0])
101
+ proba_values = model.predict_proba(vector)[0]
102
+ probabilities = {str(c): float(p) for c, p in zip(model.classes_, proba_values)}
103
+
104
+ return {"prediction": prediction, "probabilities": probabilities}
api/main.py CHANGED
@@ -1,29 +1,80 @@
1
- from fastapi import FastAPI
2
  from fastapi.responses import FileResponse
3
  from fastapi.staticfiles import StaticFiles
4
- from api.inference import predict_claim
5
- from api.schema import ClaimRequest, PredictionResponse
 
 
 
 
6
  import os
 
7
 
8
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
 
11
  @app.get("/health")
12
  def health():
13
- return {"status": "ok"}
 
 
 
 
 
14
 
15
 
16
  @app.post("/predict")
17
- def predict_claim_endpoint(claim_request: ClaimRequest):
18
- prediction = predict_claim(claim_request.claim)
19
- return PredictionResponse(prediction=prediction)
 
 
20
 
 
 
21
 
22
- STATIC_DIR = "frontend/dist/frontend/browser"
 
 
23
 
24
- if os.path.exists(STATIC_DIR):
25
- app.mount("/assets", StaticFiles(directory=STATIC_DIR), name="assets")
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  @app.get("/{full_path:path}")
28
  async def serve_spa(full_path: str):
29
  file_path = os.path.join(STATIC_DIR, full_path)
 
1
+ from fastapi import FastAPI, HTTPException
2
  from fastapi.responses import FileResponse
3
  from fastapi.staticfiles import StaticFiles
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from api.inference import predict_claim, load_models, get_available_models
6
+ from api.schema import ClaimRequest, PredictionResponse, EvidenceItem
7
+ from api.rag import load_rag_data, get_rag_result
8
+ from dotenv import load_dotenv
9
+ import asyncio
10
  import os
11
+ from contextlib import asynccontextmanager
12
 
13
+ load_dotenv()
14
+
15
+ STATIC_DIR = "frontend/dist/frontend/browser"
16
+
17
+
18
+ @asynccontextmanager
19
+ async def lifespan(app: FastAPI):
20
+ print("Loading models...")
21
+ load_models()
22
+ print("Building RAG index...")
23
+ load_rag_data()
24
+ print("Startup complete.")
25
+ yield
26
+
27
+
28
+ app = FastAPI(lifespan=lifespan)
29
+
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
 
38
 
39
  @app.get("/health")
40
  def health():
41
+ return {"status": "ok", "available_models": get_available_models()}
42
+
43
+
44
+ @app.get("/models")
45
+ def list_models():
46
+ return {"available_models": get_available_models()}
47
 
48
 
49
  @app.post("/predict")
50
+ async def predict_claim_endpoint(claim_request: ClaimRequest):
51
+ try:
52
+ claim = claim_request.claim.strip()
53
+ if not claim:
54
+ raise HTTPException(status_code=400, detail="Claim text cannot be empty")
55
 
56
+ result = await asyncio.to_thread(predict_claim, claim, claim_request.model)
57
+ predicted_label = result["prediction"]
58
 
59
+ evidence_raw, justification = await asyncio.to_thread(
60
+ get_rag_result, claim, predicted_label
61
+ )
62
 
63
+ evidence = [EvidenceItem(**e) for e in evidence_raw] if evidence_raw else None
 
64
 
65
+ return PredictionResponse(
66
+ prediction=predicted_label,
67
+ probabilities=result.get("probabilities"),
68
+ evidence=evidence,
69
+ justification=justification,
70
+ )
71
+ except HTTPException:
72
+ raise
73
+ except Exception as e:
74
+ raise HTTPException(status_code=500, detail=f"Error processing claim: {str(e)}")
75
+
76
+
77
+ if os.path.exists(STATIC_DIR):
78
  @app.get("/{full_path:path}")
79
  async def serve_spa(full_path: str):
80
  file_path = os.path.join(STATIC_DIR, full_path)
api/rag.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import numpy as np
4
+ from sklearn.feature_extraction.text import TfidfVectorizer
5
+ from sklearn.decomposition import TruncatedSVD
6
+ from sklearn.pipeline import Pipeline
7
+
8
+ DATA_PATH = os.path.join(os.path.dirname(__file__), '..', 'data', 'raw', 'train.json')
9
+ RAG_DIMS = 512
10
+
11
+ _pipeline: Pipeline | None = None
12
+ _pgvector_ready = False
13
+
14
+
15
+ # ── helpers ──────────────────────────────────────────────────────────────────
16
+
17
+ def _database_url() -> str:
18
+ return os.environ.get(
19
+ 'DATABASE_URL',
20
+ 'postgresql://mlflow:mlflow123@localhost:5432/mlflow_db'
21
+ )
22
+
23
+
24
+ def _connect():
25
+ import psycopg2
26
+ from pgvector.psycopg2 import register_vector
27
+ conn = psycopg2.connect(_database_url())
28
+ register_vector(conn)
29
+ return conn
30
+
31
+
32
+ def _fit_pipeline(texts: list[str]) -> Pipeline:
33
+ pipe = Pipeline([
34
+ ('tfidf', TfidfVectorizer(max_features=5000, stop_words='english', ngram_range=(1, 2))),
35
+ ('svd', TruncatedSVD(n_components=RAG_DIMS, random_state=42)),
36
+ ])
37
+ pipe.fit(texts)
38
+ return pipe
39
+
40
+
41
+ def _embed(pipe: Pipeline, texts: list[str]) -> np.ndarray:
42
+ vecs = pipe.transform(texts)
43
+ norms = np.linalg.norm(vecs, axis=1, keepdims=True)
44
+ norms[norms == 0] = 1.0
45
+ return (vecs / norms).astype(np.float32)
46
+
47
+
48
+ # ── startup ───────────────────────────────────────────────────────────────────
49
+
50
+ def load_rag_data(_main_vectorizer=None) -> None:
51
+ global _pipeline, _pgvector_ready
52
+
53
+ if not os.path.exists(DATA_PATH):
54
+ print(f"RAG: {DATA_PATH} not found β€” retrieval disabled.")
55
+ return
56
+
57
+ try:
58
+ with open(DATA_PATH, 'r', encoding='utf-8') as f:
59
+ data = json.load(f)
60
+ except Exception as e:
61
+ print(f"RAG: failed to load data β€” {e}")
62
+ return
63
+
64
+ statements = [str(item.get('statement', '')) for item in data]
65
+ labels = [str(item.get('label', '')) for item in data]
66
+ reasons = [str(item.get('reason', '')) for item in data]
67
+
68
+ # Always (re)fit the pipeline β€” fast enough on 10K rows
69
+ print("RAG: fitting LSA pipeline (TF-IDF 5 000 β†’ SVD 512)…")
70
+ _pipeline = _fit_pipeline(statements)
71
+
72
+ db_url = _database_url()
73
+ print(f"RAG: connecting to {db_url.split('@')[-1]}…") # hide credentials
74
+
75
+ try:
76
+ conn = _connect()
77
+ cur = conn.cursor()
78
+
79
+ cur.execute("SELECT COUNT(*) FROM rag_claims;")
80
+ count = cur.fetchone()[0]
81
+
82
+ if count == 0:
83
+ print(f"RAG: inserting {len(statements)} claims into pgvector…")
84
+ vectors = _embed(_pipeline, statements)
85
+
86
+ from psycopg2.extras import execute_values
87
+ rows = [
88
+ (statements[i], labels[i], reasons[i][:500], vectors[i])
89
+ for i in range(len(statements))
90
+ ]
91
+ execute_values(
92
+ cur,
93
+ "INSERT INTO rag_claims (statement, label, reason, embedding) VALUES %s",
94
+ rows,
95
+ template="(%s, %s, %s, %s)",
96
+ )
97
+ conn.commit()
98
+ print(f"RAG: {len(rows)} claims indexed in pgvector βœ“")
99
+ else:
100
+ print(f"RAG: {count} claims already in pgvector β€” skipping insert.")
101
+
102
+ cur.close()
103
+ conn.close()
104
+ _pgvector_ready = True
105
+ print("RAG: ready βœ“")
106
+
107
+ except Exception as e:
108
+ print(f"RAG ERROR: {e}")
109
+ print("RAG: retrieval disabled β€” run 'docker compose down -v && docker compose up --build' to reset.")
110
+ _pgvector_ready = False
111
+
112
+
113
+ # ── retrieval ─────────────────────────────────────────────────────────────────
114
+
115
+ def retrieve_similar(claim: str, predicted_label: str, top_k: int = 3) -> list:
116
+ if not _pgvector_ready or _pipeline is None:
117
+ return []
118
+
119
+ try:
120
+ query_vec = _embed(_pipeline, [claim])[0]
121
+
122
+ conn = _connect()
123
+ cur = conn.cursor()
124
+ cur.execute(
125
+ """
126
+ SELECT statement, label, reason,
127
+ 1 - (embedding <=> %s) AS similarity
128
+ FROM rag_claims
129
+ WHERE label = %s
130
+ ORDER BY embedding <=> %s
131
+ LIMIT %s
132
+ """,
133
+ (query_vec, predicted_label, query_vec, top_k),
134
+ )
135
+ rows = cur.fetchall()
136
+ cur.close()
137
+ conn.close()
138
+
139
+ return [
140
+ {
141
+ 'statement': row[0],
142
+ 'label': row[1],
143
+ 'reason': row[2],
144
+ 'similarity': float(row[3]),
145
+ }
146
+ for row in rows
147
+ ]
148
+ except Exception as e:
149
+ print(f"RAG: retrieval error β€” {e}")
150
+ return []
151
+
152
+
153
+ # ── generation (NVIDIA NIM) ───────────────────────────────────────────────────
154
+
155
+ _NIM_BASE_URL = "https://integrate.api.nvidia.com/v1"
156
+ _NIM_MODEL = "meta/llama-3.1-8b-instruct"
157
+
158
+
159
+ def _generate_justification(claim: str, predicted_label: str, similar: list) -> str | None:
160
+ if not similar:
161
+ return None
162
+
163
+ nim_key = os.environ.get('NVIDIA_API_KEY')
164
+ if not nim_key:
165
+ print("RAG: NVIDIA_API_KEY not set β€” justification disabled.")
166
+ return None
167
+
168
+ try:
169
+ from openai import OpenAI
170
+ client = OpenAI(base_url=_NIM_BASE_URL, api_key=nim_key)
171
+ except ImportError:
172
+ return None
173
+
174
+ label_fr = {
175
+ 'true': 'VRAI',
176
+ 'mostly-true': 'MAJORITAIREMENT VRAI',
177
+ 'half-true': 'PARTIELLEMENT VRAI',
178
+ 'barely-true': 'Γ€ PEINE VRAI',
179
+ 'false': 'FAUX',
180
+ 'pants-fire': 'TOTALEMENT FAUX',
181
+ }.get(predicted_label, predicted_label.upper())
182
+
183
+ examples = []
184
+ for i, s in enumerate(similar[:3], 1):
185
+ examples.append(
186
+ f"Exemple {i} (similaritΓ©={s['similarity']:.2f}) :\n"
187
+ f" DΓ©claration : {s['statement']}\n"
188
+ f" Raison : {(s['reason'] or '')[:250]}"
189
+ )
190
+
191
+ prompt = (
192
+ f"Tu es un assistant de vΓ©rification des faits.\n"
193
+ f"DΓ©claration : Β« {claim} Β»\n"
194
+ f"Classification : {label_fr} ({predicted_label})\n\n"
195
+ f"Exemples similaires (mΓͺme classification) dans la base LIAR :\n\n"
196
+ + '\n\n'.join(examples) +
197
+ f"\n\nEn 2-3 phrases concises en franΓ§ais, justifie pourquoi cette dΓ©claration est"
198
+ f" classΓ©e Β« {predicted_label} Β» en t'appuyant sur les similitudes avec ces exemples."
199
+ " RΓ©ponds directement sans titre ni introduction."
200
+ )
201
+
202
+ try:
203
+ resp = client.chat.completions.create(
204
+ model=_NIM_MODEL,
205
+ messages=[{"role": "user", "content": prompt}],
206
+ max_tokens=350,
207
+ temperature=0.3,
208
+ )
209
+ return resp.choices[0].message.content.strip()
210
+ except Exception as exc:
211
+ print(f"RAG NIM generation error: {exc}")
212
+ return None
213
+
214
+
215
+ # ── public API ────────────────────────────────────────────────────────────────
216
+
217
+ def get_rag_result(claim: str, predicted_label: str, _vectorizer=None) -> tuple[list, str | None]:
218
+ similar = retrieve_similar(claim, predicted_label)
219
+ justification = _generate_justification(claim, predicted_label, similar)
220
+
221
+ evidence = [
222
+ {
223
+ 'statement': s['statement'],
224
+ 'label': s['label'],
225
+ 'similarity': round(s['similarity'], 3),
226
+ 'reason': (s['reason'] or '')[:300] or None,
227
+ }
228
+ for s in similar
229
+ ]
230
+ return evidence, justification
api/schema.py CHANGED
@@ -1,9 +1,21 @@
1
  from pydantic import BaseModel
 
2
 
3
 
4
  class ClaimRequest(BaseModel):
5
  claim: str
 
 
 
 
 
 
 
 
6
 
7
 
8
  class PredictionResponse(BaseModel):
9
- prediction: str
 
 
 
 
1
  from pydantic import BaseModel
2
+ from typing import Dict, List, Optional
3
 
4
 
5
  class ClaimRequest(BaseModel):
6
  claim: str
7
+ model: str = 'logistic'
8
+
9
+
10
+ class EvidenceItem(BaseModel):
11
+ statement: str
12
+ label: str
13
+ similarity: float
14
+ reason: Optional[str] = None
15
 
16
 
17
  class PredictionResponse(BaseModel):
18
+ prediction: str
19
+ probabilities: Optional[Dict[str, float]] = None
20
+ evidence: Optional[List[EvidenceItem]] = None
21
+ justification: Optional[str] = None
data/raw/train.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:821995c30a67c5dfd22e1f0f2b4d90a6f72358b26ee83621c8b451d6a828f951
3
+ size 15789816
requirements.txt CHANGED
@@ -1,23 +1,14 @@
1
- pandas==2.3.0
2
- numpy==2.3.1
3
-
4
- scikit-learn==1.7.0
5
-
6
- xgboost==3.0.2
7
- lightgbm==4.6.0
8
-
9
- optuna==4.4.0
10
- mlflow==3.1.1
11
-
12
- matplotlib==3.10.3
13
- seaborn==0.13.2
14
-
15
- jupyter==1.1.1
16
- notebook==7.4.4
17
-
18
  fastapi==0.116.0
19
  uvicorn==0.35.0
20
  aiofiles==24.1.0
21
-
22
  joblib==1.5.1
23
- pytest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  fastapi==0.116.0
2
  uvicorn==0.35.0
3
  aiofiles==24.1.0
 
4
  joblib==1.5.1
5
+ scikit-learn==1.7.0
6
+ numpy==2.3.1
7
+ lightgbm==4.6.0
8
+ xgboost==3.0.2
9
+ pandas==2.3.0
10
+ openai>=1.30.0
11
+ python-dotenv>=1.0.0
12
+ psycopg2-binary>=2.9.9
13
+ pgvector>=0.3.0
14
+ scipy>=1.13.0
start.sh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ service postgresql start
5
+
6
+ for i in $(seq 1 20); do
7
+ if pg_isready -U postgres -q; then
8
+ echo "PostgreSQL ready."
9
+ break
10
+ fi
11
+ echo "Waiting for PostgreSQL... ($i/20)"
12
+ sleep 1
13
+ done
14
+
15
+ su -c "psql -c \"DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='mlflow') THEN CREATE USER mlflow WITH PASSWORD 'mlflow123'; END IF; END \$\$;\"" postgres
16
+ su -c "psql -c \"SELECT 'CREATE DATABASE mlflow_db OWNER mlflow' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname='mlflow_db')\gexec\"" postgres
17
+ su -c "psql -d mlflow_db -c \"CREATE EXTENSION IF NOT EXISTS vector;\"" postgres
18
+ su -c "psql -d mlflow_db -c \"CREATE TABLE IF NOT EXISTS rag_claims (id SERIAL PRIMARY KEY, statement TEXT, label TEXT, reason TEXT, embedding vector(512));\"" postgres
19
+
20
+ export DATABASE_URL="postgresql://mlflow:mlflow123@localhost:5432/mlflow_db"
21
+
22
+ exec uvicorn api.main:app --host 0.0.0.0 --port 7860