Spaces:
Running
Running
Ryan Christian D. Deniega Claude Sonnet 4.6 commited on
Commit ·
2f3f71f
1
Parent(s): 9f6f344
Remove Firebase/Firestore from backend — use local JSON persistence only
Browse filesDeletes firebase_client.py and strips all Tier-1 Firestore code paths from
history.py, trends.py, and scoring/engine.py. Persistence now falls through
directly to the local JSON file (data/history.json) with in-memory fallback.
Eliminates the repeated "firebase-admin not installed" warnings in HF logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- api/routes/history.py +5 -43
- api/routes/trends.py +2 -11
- firebase_client.py +0 -143
- scoring/engine.py +2 -11
api/routes/history.py
CHANGED
|
@@ -3,9 +3,8 @@ PhilVerify — History Route
|
|
| 3 |
GET /history — Returns past verification logs with pagination.
|
| 4 |
|
| 5 |
Persistence tier order (best to worst):
|
| 6 |
-
1.
|
| 7 |
-
2.
|
| 8 |
-
3. In-memory list — last resort, resets on every restart
|
| 9 |
"""
|
| 10 |
import json
|
| 11 |
import logging
|
|
@@ -70,18 +69,7 @@ def record_verification(entry: dict) -> None:
|
|
| 70 |
async def get_history_entry(entry_id: str) -> dict:
|
| 71 |
logger.info("GET /history/%s", entry_id)
|
| 72 |
|
| 73 |
-
# Tier 1:
|
| 74 |
-
try:
|
| 75 |
-
from firebase_client import get_firestore
|
| 76 |
-
db = get_firestore()
|
| 77 |
-
if db:
|
| 78 |
-
doc = db.collection("verifications").document(entry_id).get()
|
| 79 |
-
if doc.exists:
|
| 80 |
-
return doc.to_dict()
|
| 81 |
-
except Exception as e:
|
| 82 |
-
logger.debug("Firestore detail unavailable (%s) — trying local file", e)
|
| 83 |
-
|
| 84 |
-
# Tier 2: Local JSON file
|
| 85 |
try:
|
| 86 |
records = _load_history_file()
|
| 87 |
for r in records:
|
|
@@ -102,7 +90,7 @@ async def get_history_entry(entry_id: str) -> dict:
|
|
| 102 |
"",
|
| 103 |
response_model=HistoryResponse,
|
| 104 |
summary="Get verification history",
|
| 105 |
-
description="Returns past verifications ordered by most recent. Reads from
|
| 106 |
)
|
| 107 |
async def get_history(
|
| 108 |
page: int = Query(1, ge=1, description="Page number"),
|
|
@@ -111,33 +99,7 @@ async def get_history(
|
|
| 111 |
) -> HistoryResponse:
|
| 112 |
logger.info("GET /history | page=%d limit=%d", page, limit)
|
| 113 |
|
| 114 |
-
# ── Tier 1:
|
| 115 |
-
try:
|
| 116 |
-
from firebase_client import get_verifications, get_verification_count
|
| 117 |
-
vf = verdict_filter.value if verdict_filter else None
|
| 118 |
-
offset = (page - 1) * limit
|
| 119 |
-
entries_raw = await get_verifications(limit=limit, offset=offset, verdict_filter=vf)
|
| 120 |
-
total = await get_verification_count(verdict_filter=vf)
|
| 121 |
-
if entries_raw or total > 0:
|
| 122 |
-
return HistoryResponse(
|
| 123 |
-
total=total,
|
| 124 |
-
entries=[
|
| 125 |
-
HistoryEntry(
|
| 126 |
-
id=e["id"],
|
| 127 |
-
timestamp=e["timestamp"],
|
| 128 |
-
input_type=e.get("input_type", "text"),
|
| 129 |
-
text_preview=e.get("text_preview", "")[:120],
|
| 130 |
-
verdict=Verdict(e["verdict"]),
|
| 131 |
-
confidence=e["confidence"],
|
| 132 |
-
final_score=e["final_score"],
|
| 133 |
-
)
|
| 134 |
-
for e in entries_raw
|
| 135 |
-
],
|
| 136 |
-
)
|
| 137 |
-
except Exception as e:
|
| 138 |
-
logger.debug("Firestore history unavailable (%s) — trying local file", e)
|
| 139 |
-
|
| 140 |
-
# ── Tier 2: Local JSON file ───────────────────────────────────────────────
|
| 141 |
# Load from file rather than in-memory list so data survives restarts.
|
| 142 |
file_entries = list(reversed(_load_history_file()))
|
| 143 |
if file_entries:
|
|
|
|
| 3 |
GET /history — Returns past verification logs with pagination.
|
| 4 |
|
| 5 |
Persistence tier order (best to worst):
|
| 6 |
+
1. Local JSON file — data/history.json, survives server restarts
|
| 7 |
+
2. In-memory list — last resort, resets on every restart
|
|
|
|
| 8 |
"""
|
| 9 |
import json
|
| 10 |
import logging
|
|
|
|
| 69 |
async def get_history_entry(entry_id: str) -> dict:
|
| 70 |
logger.info("GET /history/%s", entry_id)
|
| 71 |
|
| 72 |
+
# Tier 1: Local JSON file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
try:
|
| 74 |
records = _load_history_file()
|
| 75 |
for r in records:
|
|
|
|
| 90 |
"",
|
| 91 |
response_model=HistoryResponse,
|
| 92 |
summary="Get verification history",
|
| 93 |
+
description="Returns past verifications ordered by most recent. Reads from local JSON file, falls back to in-memory store.",
|
| 94 |
)
|
| 95 |
async def get_history(
|
| 96 |
page: int = Query(1, ge=1, description="Page number"),
|
|
|
|
| 99 |
) -> HistoryResponse:
|
| 100 |
logger.info("GET /history | page=%d limit=%d", page, limit)
|
| 101 |
|
| 102 |
+
# ── Tier 1: Local JSON file ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
# Load from file rather than in-memory list so data survives restarts.
|
| 104 |
file_entries = list(reversed(_load_history_file()))
|
| 105 |
if file_entries:
|
api/routes/trends.py
CHANGED
|
@@ -14,18 +14,9 @@ router = APIRouter(prefix="/trends", tags=["Trends"])
|
|
| 14 |
def _load_all_history() -> list[dict]:
|
| 15 |
"""
|
| 16 |
Return all history records from the best available source:
|
| 17 |
-
1.
|
| 18 |
"""
|
| 19 |
-
# Tier 1:
|
| 20 |
-
try:
|
| 21 |
-
from firebase_client import get_all_verifications_sync
|
| 22 |
-
records = get_all_verifications_sync()
|
| 23 |
-
if records:
|
| 24 |
-
return records
|
| 25 |
-
except Exception:
|
| 26 |
-
pass
|
| 27 |
-
|
| 28 |
-
# Tier 2: Local JSON file (persists across restarts)
|
| 29 |
try:
|
| 30 |
from api.routes.history import _load_history_file
|
| 31 |
records = _load_history_file()
|
|
|
|
| 14 |
def _load_all_history() -> list[dict]:
|
| 15 |
"""
|
| 16 |
Return all history records from the best available source:
|
| 17 |
+
1. Local JSON file 2. In-memory list (fallback)
|
| 18 |
"""
|
| 19 |
+
# Tier 1: Local JSON file (persists across restarts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
try:
|
| 21 |
from api.routes.history import _load_history_file
|
| 22 |
records = _load_history_file()
|
firebase_client.py
DELETED
|
@@ -1,143 +0,0 @@
|
|
| 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") or os.getenv("K_SERVICE"):
|
| 46 |
-
# Cloud Run (K_SERVICE is always set) or explicit ADC path
|
| 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 |
-
from google.cloud.firestore_v1.base_query import FieldFilter
|
| 96 |
-
query = (
|
| 97 |
-
db.collection("verifications")
|
| 98 |
-
.order_by("timestamp", direction="DESCENDING")
|
| 99 |
-
)
|
| 100 |
-
if verdict_filter:
|
| 101 |
-
query = query.where(filter=FieldFilter("verdict", "==", verdict_filter))
|
| 102 |
-
docs = query.limit(limit + offset).stream()
|
| 103 |
-
results = [doc.to_dict() for doc in docs]
|
| 104 |
-
return results[offset : offset + limit]
|
| 105 |
-
except Exception as e:
|
| 106 |
-
logger.error("Firestore read error: %s", e)
|
| 107 |
-
return []
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def get_all_verifications_sync() -> list[dict]:
|
| 111 |
-
"""Synchronously fetch ALL verification records from Firestore (used by trends aggregation)."""
|
| 112 |
-
db = get_firestore()
|
| 113 |
-
if db is None:
|
| 114 |
-
return []
|
| 115 |
-
try:
|
| 116 |
-
docs = (
|
| 117 |
-
db.collection("verifications")
|
| 118 |
-
.order_by("timestamp", direction="DESCENDING")
|
| 119 |
-
.limit(10_000) # hard cap — more than enough for trends analysis
|
| 120 |
-
.stream()
|
| 121 |
-
)
|
| 122 |
-
return [doc.to_dict() for doc in docs]
|
| 123 |
-
except Exception as e:
|
| 124 |
-
logger.error("Firestore get_all_verifications_sync error: %s", e)
|
| 125 |
-
return []
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
async def get_verification_count(verdict_filter: str | None = None) -> int:
|
| 129 |
-
"""Return total count of verifications (with optional verdict filter)."""
|
| 130 |
-
db = get_firestore()
|
| 131 |
-
if db is None:
|
| 132 |
-
return 0
|
| 133 |
-
try:
|
| 134 |
-
from google.cloud.firestore_v1.base_query import FieldFilter
|
| 135 |
-
query = db.collection("verifications")
|
| 136 |
-
if verdict_filter:
|
| 137 |
-
query = query.where(filter=FieldFilter("verdict", "==", verdict_filter))
|
| 138 |
-
# Use aggregation query (Firestore native count)
|
| 139 |
-
result = query.count().get()
|
| 140 |
-
return result[0][0].value
|
| 141 |
-
except Exception as e:
|
| 142 |
-
logger.error("Firestore count error: %s", e)
|
| 143 |
-
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scoring/engine.py
CHANGED
|
@@ -323,18 +323,9 @@ async def run_verification(
|
|
| 323 |
"language": language.value,
|
| 324 |
}
|
| 325 |
try:
|
| 326 |
-
from
|
| 327 |
-
|
| 328 |
-
if not saved:
|
| 329 |
-
# Firestore unavailable — fall back to in-memory store
|
| 330 |
-
from api.routes.history import record_verification
|
| 331 |
-
record_verification(history_entry)
|
| 332 |
except Exception as e:
|
| 333 |
logger.warning("Failed to record history: %s", e)
|
| 334 |
-
try:
|
| 335 |
-
from api.routes.history import record_verification
|
| 336 |
-
record_verification(history_entry)
|
| 337 |
-
except Exception:
|
| 338 |
-
pass
|
| 339 |
|
| 340 |
return result
|
|
|
|
| 323 |
"language": language.value,
|
| 324 |
}
|
| 325 |
try:
|
| 326 |
+
from api.routes.history import record_verification
|
| 327 |
+
record_verification(history_entry)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
except Exception as e:
|
| 329 |
logger.warning("Failed to record history: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
return result
|