Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg-info/
dist/
build/
.eggs/
*.egg
.venv/
venv/
env/
.env

# Node / Frontend
node_modules/
.next/
out/
frontend/.next/
frontend/node_modules/
*.tsbuildinfo

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*

# Testing
.coverage
htmlcov/
.pytest_cache/
21 changes: 21 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ── BiasProbe Backend — Local Development Environment ──
# Copy this file to .env and fill in your values.
# This file should NOT be committed to git.

# Gemini API key for dynamic probe generation
GEMINI_API_KEY=

# Firebase project ID (from Firebase console)
FIREBASE_PROJECT_ID=

# Path to service account JSON file, or the JSON string itself
FIREBASE_SERVICE_ACCOUNT_JSON=

# Google Cloud Storage bucket for PDF reports (leave blank for local storage)
GCS_BUCKET_NAME=

# GCP project ID (used by google-cloud libraries)
GOOGLE_CLOUD_PROJECT=

# Set to true for development mode (relaxed CORS, local PDF storage)
LOCAL_DEV=true
113 changes: 113 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
BiasProbe — Application Configuration
Loads environment variables and initializes Firebase Admin SDK + Gemini client.
"""

import os
import json
import logging
from pathlib import Path
from dotenv import load_dotenv
import firebase_admin
from firebase_admin import credentials, firestore
from google import genai

load_dotenv()

logger = logging.getLogger("biasProbe")

# ── Environment Variables ──────────────────────────────────────────────────────

GEMINI_API_KEY: str = os.getenv("GEMINI_API_KEY", "")
FIREBASE_PROJECT_ID: str = os.getenv("FIREBASE_PROJECT_ID", "")
FIREBASE_SERVICE_ACCOUNT_JSON: str = os.getenv("FIREBASE_SERVICE_ACCOUNT_JSON", "")
GCS_BUCKET_NAME: str = os.getenv("GCS_BUCKET_NAME", "")
GOOGLE_CLOUD_PROJECT: str = os.getenv("GOOGLE_CLOUD_PROJECT", "")

# ── Dev Mode Detection ─────────────────────────────────────────────────────────

LOCAL_DEV: bool = os.getenv("LOCAL_DEV", "true").lower() in ("true", "1", "yes")

# Cross-platform local reports directory (project-local, not /tmp/)
REPORTS_DIR = Path(__file__).resolve().parent / "reports_out"
REPORTS_DIR.mkdir(exist_ok=True)


# ── Firebase Admin SDK ─────────────────────────────────────────────────────────

_firebase_app = None


def get_firebase_app():
"""Initialize Firebase Admin SDK (singleton)."""
global _firebase_app
if _firebase_app is not None:
return _firebase_app

if FIREBASE_SERVICE_ACCOUNT_JSON:
# Could be a file path or a JSON string
if os.path.isfile(FIREBASE_SERVICE_ACCOUNT_JSON):
cred = credentials.Certificate(FIREBASE_SERVICE_ACCOUNT_JSON)
else:
try:
cred = credentials.Certificate(json.loads(FIREBASE_SERVICE_ACCOUNT_JSON))
except json.JSONDecodeError as e:
logger.error("Failed to parse FIREBASE_SERVICE_ACCOUNT_JSON: %s", e)
raise
else:
# Fall back to Application Default Credentials (on Cloud Run)
cred = credentials.ApplicationDefault()

_firebase_app = firebase_admin.initialize_app(cred, {
"projectId": FIREBASE_PROJECT_ID,
"storageBucket": GCS_BUCKET_NAME,
})
return _firebase_app


def get_firestore_client():
"""Get a Firestore client instance."""
get_firebase_app()
return firestore.client()


# ── Gemini AI ──────────────────────────────────────────────────────────────────

_genai_client = None


def _get_genai_client() -> genai.Client:
"""Return a singleton google.genai Client."""
global _genai_client
if _genai_client is None:
if not GEMINI_API_KEY:
raise ValueError("GEMINI_API_KEY is not set. Add it to your .env file.")
_genai_client = genai.Client(api_key=GEMINI_API_KEY)
return _genai_client


class _GeminiModelWrapper:
"""Thin compatibility wrapper so callers can keep using model.generate_content(prompt)."""

def __init__(self, client: genai.Client, model_name: str):
self._client = client
self._model_name = model_name

def generate_content(self, prompt: str):
return self._client.models.generate_content(
model=self._model_name,
contents=prompt,
)


def get_gemini_model(model_name: str = "gemini-2.0-flash"):
"""Configure and return a Gemini generative model wrapper."""
client = _get_genai_client()
return _GeminiModelWrapper(client, model_name)


def get_gemini_embedding_model(model_name: str = "models/text-embedding-004"):
"""Return the model name string for embedding calls."""
# Ensure the client is configured (validates API key)
_get_genai_client()
return model_name
85 changes: 82 additions & 3 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,85 @@
# TODO: FastAPI entry point
"""
BiasProbe — FastAPI Application Entry Point
"""

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from config import get_firebase_app, LOCAL_DEV
from routers.audit import router as audit_router
from routers.report import router as report_router

# ── Logging Configuration ─────────────────────────────────────────────────────

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s │ %(levelname)-8s │ %(name)s │ %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("biasProbe")


@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize services on startup."""
# Initialize Firebase Admin SDK
try:
get_firebase_app()
logger.info("Firebase initialized successfully")
except Exception as e:
logger.warning("Firebase init failed (will retry on first request): %s", e)
yield


app = FastAPI(
title="BiasProbe API",
version="0.1.0",
description="LLM Bias Testing & Auditing Platform",
lifespan=lifespan,
)

# ── CORS Middleware ────────────────────────────────────────────────────────────
# In dev mode, allow all origins for easy local testing.
# In production, restrict to known domains.

_allowed_origins = [
"http://localhost:3000", # Next.js dev
"http://localhost:3001",
"http://127.0.0.1:3000",
"http://127.0.0.1:3001",
]

if not LOCAL_DEV:
_allowed_origins += [
"https://*.web.app", # Firebase Hosting
"https://*.firebaseapp.com",
]

app.add_middleware(
CORSMiddleware,
allow_origins=["*"] if LOCAL_DEV else _allowed_origins,
allow_credentials=not LOCAL_DEV, # credentials=True is incompatible with origins=["*"]
allow_methods=["*"],
allow_headers=["*"],
)

# ── Register Routers ──────────────────────────────────────────────────────────
app.include_router(audit_router)
app.include_router(report_router)


# ── Health Check ───────────────────────────────────────────────────────────────
@app.get("/health", tags=["system"])
async def health_check():
return {"status": "healthy", "service": "biasProbe-api", "version": "0.1.0"}

app = FastAPI(title="BiasProbe API", version="0.1.0")

# Routers will be included here
@app.get("/", tags=["system"])
async def root():
return {
"message": "BiasProbe API — LLM Bias Auditing Platform",
"docs": "/docs",
"health": "/health",
}
1 change: 1 addition & 0 deletions backend/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Models package
Loading