Ryan Christian D. Deniega commited on
Commit
954286a
·
1 Parent(s): 9724119

feat: Firebase integration — Firestore persistence, firebase.json hosting + Cloud Run rewrite

Browse files
.env.example CHANGED
@@ -12,7 +12,7 @@ REDIS_URL=redis://localhost:6379/0
12
  APP_ENV=development # development | production
13
  DEBUG=true
14
  LOG_LEVEL=INFO
15
- ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
16
 
17
  # ── Model Settings ────────────────────────────────────────────────────────────
18
  # Options: xlm-roberta-base | joelito/roberta-tagalog-base | bert-base-multilingual-cased
 
12
  APP_ENV=development # development | production
13
  DEBUG=true
14
  LOG_LEVEL=INFO
15
+ ALLOWED_ORIGINS=["http://localhost:3000","http://localhost:5173"]
16
 
17
  # ── Model Settings ────────────────────────────────────────────────────────────
18
  # Options: xlm-roberta-base | joelito/roberta-tagalog-base | bert-base-multilingual-cased
.firebaserc ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "projects": {},
3
+ "targets": {},
4
+ "etags": {}
5
+ }
.gitignore CHANGED
@@ -26,3 +26,5 @@ build/
26
  ml/models/*.pkl
27
  ml/models/*.bin
28
  ml/models/*.pt
 
 
 
26
  ml/models/*.pkl
27
  ml/models/*.bin
28
  ml/models/*.pt
29
+ serviceAccountKey.json
30
+ *.json.key
api/routes/history.py CHANGED
@@ -22,7 +22,7 @@ def record_verification(entry: dict) -> None:
22
  "",
23
  response_model=HistoryResponse,
24
  summary="Get verification history",
25
- description="Returns past verifications ordered by most recent. Supports pagination.",
26
  )
27
  async def get_history(
28
  page: int = Query(1, ge=1, description="Page number"),
@@ -31,14 +31,39 @@ async def get_history(
31
  ) -> HistoryResponse:
32
  logger.info("GET /history | page=%d limit=%d", page, limit)
33
 
34
- entries = list(reversed(_HISTORY)) # Most recent first
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  if verdict_filter:
36
  entries = [e for e in entries if e.get("verdict") == verdict_filter.value]
37
-
38
  total = len(entries)
39
  start = (page - 1) * limit
40
  paginated = entries[start : start + limit]
41
-
42
  return HistoryResponse(
43
  total=total,
44
  entries=[
 
22
  "",
23
  response_model=HistoryResponse,
24
  summary="Get verification history",
25
+ description="Returns past verifications ordered by most recent. Reads from Firestore when configured, falls back to in-memory store.",
26
  )
27
  async def get_history(
28
  page: int = Query(1, ge=1, description="Page number"),
 
31
  ) -> HistoryResponse:
32
  logger.info("GET /history | page=%d limit=%d", page, limit)
33
 
34
+ # Try Firestore first
35
+ try:
36
+ from firebase_client import get_verifications, get_verification_count
37
+ vf = verdict_filter.value if verdict_filter else None
38
+ offset = (page - 1) * limit
39
+ entries_raw = await get_verifications(limit=limit, offset=offset, verdict_filter=vf)
40
+ total = await get_verification_count(verdict_filter=vf)
41
+ if entries_raw or total > 0:
42
+ return HistoryResponse(
43
+ total=total,
44
+ entries=[
45
+ HistoryEntry(
46
+ id=e["id"],
47
+ timestamp=e["timestamp"],
48
+ input_type=e.get("input_type", "text"),
49
+ text_preview=e.get("text_preview", "")[:120],
50
+ verdict=Verdict(e["verdict"]),
51
+ confidence=e["confidence"],
52
+ final_score=e["final_score"],
53
+ )
54
+ for e in entries_raw
55
+ ],
56
+ )
57
+ except Exception as e:
58
+ logger.debug("Firestore history read failed (%s) — using in-memory store", e)
59
+
60
+ # In-memory fallback
61
+ entries = list(reversed(_HISTORY))
62
  if verdict_filter:
63
  entries = [e for e in entries if e.get("verdict") == verdict_filter.value]
 
64
  total = len(entries)
65
  start = (page - 1) * limit
66
  paginated = entries[start : start + limit]
 
67
  return HistoryResponse(
68
  total=total,
69
  entries=[
config.py CHANGED
@@ -28,10 +28,11 @@ class Settings(BaseSettings):
28
  app_env: str = "development"
29
  debug: bool = True
30
  log_level: str = "INFO"
31
- allowed_origins: list[str] = [
32
- "http://localhost:3000",
33
- "http://localhost:5173",
34
- ]
 
35
 
36
  # ── ML Models ─────────────────────────────────────────────────────────────
37
  ml_model_name: str = "xlm-roberta-base"
 
28
  app_env: str = "development"
29
  debug: bool = True
30
  log_level: str = "INFO"
31
+ allowed_origins: str = "http://localhost:3000,http://localhost:5173"
32
+
33
+ @property
34
+ def allowed_origins_list(self) -> list[str]:
35
+ return [o.strip() for o in self.allowed_origins.split(",") if o.strip()]
36
 
37
  # ── ML Models ─────────────────────────────────────────────────────────────
38
  ml_model_name: str = "xlm-roberta-base"
firebase.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "firestore": {
3
+ "rules": "firestore.rules",
4
+ "indexes": "firestore.indexes.json"
5
+ },
6
+ "hosting": {
7
+ "site": "philverify",
8
+ "public": "frontend/dist",
9
+ "ignore": [
10
+ "firebase.json",
11
+ "**/.*",
12
+ "**/node_modules/**"
13
+ ],
14
+ "rewrites": [
15
+ {
16
+ "source": "/api/**",
17
+ "run": {
18
+ "serviceId": "philverify-api",
19
+ "region": "asia-southeast1"
20
+ }
21
+ },
22
+ {
23
+ "source": "**",
24
+ "destination": "/index.html"
25
+ }
26
+ ],
27
+ "headers": [
28
+ {
29
+ "source": "/api/**",
30
+ "headers": [
31
+ {
32
+ "key": "Access-Control-Allow-Origin",
33
+ "value": "*"
34
+ }
35
+ ]
36
+ }
37
+ ]
38
+ }
39
+ }
firebase_client.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PhilVerify — Firebase / Firestore Client
3
+ Initializes firebase-admin SDK and provides typed helpers for persistence.
4
+
5
+ Setup:
6
+ 1. Go to Firebase Console → Project Settings → Service Accounts
7
+ 2. Click "Generate new private key" → save as `serviceAccountKey.json`
8
+ in the PhilVerify project root (already in .gitignore)
9
+ 3. Set FIREBASE_PROJECT_ID in .env
10
+
11
+ Collections:
12
+ verifications/ — one doc per verification run
13
+ trends/summary — aggregated entity/topic counters
14
+ """
15
+ import logging
16
+ import os
17
+ from functools import lru_cache
18
+ from pathlib import Path
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ _SERVICEACCOUNT_PATH = Path(__file__).parent / "serviceAccountKey.json"
23
+ _db = None # Firestore client singleton
24
+
25
+
26
+ def get_firestore():
27
+ """Return the Firestore client, or None if Firebase is not configured."""
28
+ global _db
29
+ if _db is not None:
30
+ return _db
31
+
32
+ try:
33
+ import firebase_admin
34
+ from firebase_admin import credentials, firestore
35
+
36
+ if firebase_admin._DEFAULT_APP_NAME in firebase_admin._apps:
37
+ _db = firestore.client()
38
+ return _db
39
+
40
+ if _SERVICEACCOUNT_PATH.exists():
41
+ # Service account key file available (local dev + CI)
42
+ cred = credentials.Certificate(str(_SERVICEACCOUNT_PATH))
43
+ firebase_admin.initialize_app(cred)
44
+ logger.info("Firebase initialized via service account key")
45
+ elif os.getenv("GOOGLE_APPLICATION_CREDENTIALS"):
46
+ # Cloud Run / GCE default credentials
47
+ cred = credentials.ApplicationDefault()
48
+ firebase_admin.initialize_app(cred)
49
+ logger.info("Firebase initialized via Application Default Credentials")
50
+ else:
51
+ logger.warning(
52
+ "Firebase not configured — no serviceAccountKey.json and no "
53
+ "GOOGLE_APPLICATION_CREDENTIALS env var. History will use in-memory store."
54
+ )
55
+ return None
56
+
57
+ _db = firestore.client()
58
+ return _db
59
+
60
+ except ImportError:
61
+ logger.warning("firebase-admin not installed — Firestore disabled")
62
+ return None
63
+ except Exception as e:
64
+ logger.error("Firebase init error: %s — falling back to in-memory store", e)
65
+ return None
66
+
67
+
68
+ async def save_verification(data: dict) -> bool:
69
+ """
70
+ Persist a verification result to Firestore.
71
+ Returns True on success, False if Firebase is unavailable.
72
+ """
73
+ db = get_firestore()
74
+ if db is None:
75
+ return False
76
+ try:
77
+ db.collection("verifications").document(data["id"]).set(data)
78
+ logger.debug("Verification %s saved to Firestore", data["id"])
79
+ return True
80
+ except Exception as e:
81
+ logger.error("Firestore write error: %s", e)
82
+ return False
83
+
84
+
85
+ async def get_verifications(
86
+ limit: int = 20,
87
+ offset: int = 0,
88
+ verdict_filter: str | None = None,
89
+ ) -> list[dict]:
90
+ """Fetch verification history from Firestore ordered by timestamp desc."""
91
+ db = get_firestore()
92
+ if db is None:
93
+ return []
94
+ try:
95
+ query = (
96
+ db.collection("verifications")
97
+ .order_by("timestamp", direction="DESCENDING")
98
+ )
99
+ if verdict_filter:
100
+ query = query.where("verdict", "==", verdict_filter)
101
+ docs = query.limit(limit + offset).stream()
102
+ results = [doc.to_dict() for doc in docs]
103
+ return results[offset : offset + limit]
104
+ except Exception as e:
105
+ logger.error("Firestore read error: %s", e)
106
+ return []
107
+
108
+
109
+ async def get_verification_count(verdict_filter: str | None = None) -> int:
110
+ """Return total count of verifications (with optional verdict filter)."""
111
+ db = get_firestore()
112
+ if db is None:
113
+ return 0
114
+ try:
115
+ query = db.collection("verifications")
116
+ if verdict_filter:
117
+ query = query.where("verdict", "==", verdict_filter)
118
+ # Use aggregation query (Firestore native count)
119
+ result = query.count().get()
120
+ return result[0][0].value
121
+ except Exception as e:
122
+ logger.error("Firestore count error: %s", e)
123
+ return 0
firestore.indexes.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ // Example (Standard Edition):
3
+ //
4
+ // "indexes": [
5
+ // {
6
+ // "collectionGroup": "widgets",
7
+ // "queryScope": "COLLECTION",
8
+ // "fields": [
9
+ // { "fieldPath": "foo", "arrayConfig": "CONTAINS" },
10
+ // { "fieldPath": "bar", "mode": "DESCENDING" }
11
+ // ]
12
+ // },
13
+ //
14
+ // "fieldOverrides": [
15
+ // {
16
+ // "collectionGroup": "widgets",
17
+ // "fieldPath": "baz",
18
+ // "indexes": [
19
+ // { "order": "ASCENDING", "queryScope": "COLLECTION" }
20
+ // ]
21
+ // },
22
+ // ]
23
+ // ]
24
+ //
25
+ // Example (Enterprise Edition):
26
+ //
27
+ // "indexes": [
28
+ // {
29
+ // "collectionGroup": "reviews",
30
+ // "queryScope": "COLLECTION_GROUP",
31
+ // "apiScope": "MONGODB_COMPATIBLE_API",
32
+ // "density": "DENSE",
33
+ // "multikey": false,
34
+ // "fields": [
35
+ // { "fieldPath": "baz", "mode": "ASCENDING" }
36
+ // ]
37
+ // },
38
+ // {
39
+ // "collectionGroup": "items",
40
+ // "queryScope": "COLLECTION_GROUP",
41
+ // "apiScope": "MONGODB_COMPATIBLE_API",
42
+ // "density": "SPARSE_ANY",
43
+ // "multikey": true,
44
+ // "fields": [
45
+ // { "fieldPath": "baz", "mode": "ASCENDING" }
46
+ // ]
47
+ // },
48
+ // ]
49
+ "indexes": [],
50
+ "fieldOverrides": []
51
+ }
firestore.rules ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ rules_version = '2';
2
+ service cloud.firestore {
3
+ match /databases/{database}/documents {
4
+ match /verifications/{docId} {
5
+ allow read: if true;
6
+ allow write: if false;
7
+ }
8
+ }
9
+ }
main.py CHANGED
@@ -72,7 +72,7 @@ app = FastAPI(
72
 
73
  app.add_middleware(
74
  CORSMiddleware,
75
- allow_origins=settings.allowed_origins,
76
  allow_credentials=True,
77
  allow_methods=["*"],
78
  allow_headers=["*"],
 
72
 
73
  app.add_middleware(
74
  CORSMiddleware,
75
+ allow_origins=settings.allowed_origins_list,
76
  allow_credentials=True,
77
  allow_methods=["*"],
78
  allow_headers=["*"],
requirements.txt CHANGED
@@ -5,6 +5,9 @@ python-multipart==0.0.17 # File upload support
5
  pydantic==2.9.2
6
  pydantic-settings==2.6.1
7
 
 
 
 
8
  # ── NLP & ML ──────────────────────────────────────────────────────────────────
9
  transformers==4.46.3
10
  torch==2.5.1
 
5
  pydantic==2.9.2
6
  pydantic-settings==2.6.1
7
 
8
+ # ── Firebase ──────────────────────────────────────────────────────────────────
9
+ firebase-admin==6.6.0 # Firestore + Admin SDK
10
+
11
  # ── NLP & ML ──────────────────────────────────────────────────────────────────
12
  transformers==4.46.3
13
  torch==2.5.1
scoring/engine.py CHANGED
@@ -192,21 +192,44 @@ async def run_verification(
192
  input_type=input_type,
193
  )
194
 
195
- # ── Record to history────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  try:
197
- from api.routes.history import record_verification
198
- record_verification({
199
- "id": str(uuid.uuid4()),
200
- "timestamp": datetime.now(timezone.utc).isoformat(),
201
- "input_type": input_type,
202
- "text_preview": text[:120],
203
- "verdict": verdict.value,
204
- "confidence": result.confidence,
205
- "final_score": final_score,
206
- "entities": ner_result.to_dict(),
207
- "claim_used": claim_result.claim,
208
- })
209
  except Exception as e:
210
  logger.warning("Failed to record history: %s", e)
 
 
 
 
 
211
 
212
  return result
 
192
  input_type=input_type,
193
  )
194
 
195
+ # ── Record to Firestore (falls back to in-memory if Firebase not configured)
196
+ history_entry = {
197
+ "id": str(uuid.uuid4()),
198
+ "timestamp": datetime.now(timezone.utc).isoformat(),
199
+ "input_type": input_type,
200
+ "text_preview": text[:120],
201
+ "verdict": verdict.value,
202
+ "confidence": result.confidence,
203
+ "final_score": final_score,
204
+ "entities": ner_result.to_dict(),
205
+ "claim_used": claim_result.claim,
206
+ "layer1": {
207
+ "verdict": layer1.verdict.value,
208
+ "confidence": layer1.confidence,
209
+ "triggered_features": layer1.triggered_features,
210
+ },
211
+ "layer2": {
212
+ "verdict": layer2.verdict.value,
213
+ "evidence_score": layer2.evidence_score,
214
+ "claim_used": layer2.claim_used,
215
+ },
216
+ "sentiment": sentiment_result.sentiment,
217
+ "emotion": sentiment_result.emotion,
218
+ "language": language.value,
219
+ }
220
  try:
221
+ from firebase_client import save_verification
222
+ saved = await save_verification(history_entry)
223
+ if not saved:
224
+ # Firestore unavailable — fall back to in-memory store
225
+ from api.routes.history import record_verification
226
+ record_verification(history_entry)
 
 
 
 
 
 
227
  except Exception as e:
228
  logger.warning("Failed to record history: %s", e)
229
+ try:
230
+ from api.routes.history import record_verification
231
+ record_verification(history_entry)
232
+ except Exception:
233
+ pass
234
 
235
  return result