From 614bb60ddbc1c5796dfe2136ab856a5d4ee9d2cf Mon Sep 17 00:00:00 2001 From: kanikashreesivakumar Date: Mon, 20 Oct 2025 16:09:44 +0530 Subject: [PATCH] Add :[AI-ML] factreal --- Domains/AI-ML/MiniProjects/factreal | 1 - .../AI-ML/MiniProjects/factreal/.dockerignore | 16 +++ .../AI-ML/MiniProjects/factreal/.env.example | 6 + .../factreal/.github/workflows/ci.yml | 33 +++++ .../AI-ML/MiniProjects/factreal/.gitignore | 45 +++++++ .../AI-ML/MiniProjects/factreal/Dockerfile | 27 +++++ Domains/AI-ML/MiniProjects/factreal/README.md | 113 ++++++++++++++++++ .../MiniProjects/factreal/backend/__init__.py | 1 + .../factreal/backend/controllers/__init__.py | 2 + .../controllers/classify_controller.py | 39 ++++++ .../backend/controllers/explain_controller.py | 24 ++++ .../backend/data/sample_dataset.jsonl | 3 + .../MiniProjects/factreal/backend/main.py | 32 +++++ .../factreal/backend/models/__init__.py | 1 + .../factreal/backend/models/bias_model.py | 77 ++++++++++++ .../factreal/backend/models/fact_checker.py | 90 ++++++++++++++ .../backend/models/reasoning_model.py | 72 +++++++++++ .../postman/FactReal.postman_collection.json | 36 ++++++ .../factreal/backend/routes/__init__.py | 2 + .../backend/routes/classify_router.py | 20 ++++ .../factreal/backend/routes/explain_router.py | 20 ++++ .../factreal/backend/scripts/run_server.ps1 | 31 +++++ .../backend/scripts/sample_requests.py | 33 +++++ .../factreal/backend/services/__init__.py | 1 + .../factreal/backend/services/api_service.py | 57 +++++++++ .../services/classification_service.py | 31 +++++ .../services/explainability_service.py | 21 ++++ .../backend/services/preprocessing_service.py | 22 ++++ .../factreal/backend/utils/config.py | 39 ++++++ .../factreal/backend/utils/logger.py | 26 ++++ .../MiniProjects/factreal/requirements.txt | 10 ++ .../MiniProjects/factreal/tests/test_smoke.py | 33 +++++ 32 files changed, 963 insertions(+), 1 deletion(-) delete mode 160000 Domains/AI-ML/MiniProjects/factreal create mode 100644 Domains/AI-ML/MiniProjects/factreal/.dockerignore create mode 100644 Domains/AI-ML/MiniProjects/factreal/.env.example create mode 100644 Domains/AI-ML/MiniProjects/factreal/.github/workflows/ci.yml create mode 100644 Domains/AI-ML/MiniProjects/factreal/.gitignore create mode 100644 Domains/AI-ML/MiniProjects/factreal/Dockerfile create mode 100644 Domains/AI-ML/MiniProjects/factreal/README.md create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/__init__.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/controllers/__init__.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/controllers/classify_controller.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/controllers/explain_controller.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/data/sample_dataset.jsonl create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/main.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/models/__init__.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/models/bias_model.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/models/fact_checker.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/models/reasoning_model.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/postman/FactReal.postman_collection.json create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/routes/__init__.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/routes/classify_router.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/routes/explain_router.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/scripts/run_server.ps1 create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/scripts/sample_requests.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/services/__init__.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/services/api_service.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/services/classification_service.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/services/explainability_service.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/services/preprocessing_service.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/utils/config.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/backend/utils/logger.py create mode 100644 Domains/AI-ML/MiniProjects/factreal/requirements.txt create mode 100644 Domains/AI-ML/MiniProjects/factreal/tests/test_smoke.py diff --git a/Domains/AI-ML/MiniProjects/factreal b/Domains/AI-ML/MiniProjects/factreal deleted file mode 160000 index 5da0c7d9..00000000 --- a/Domains/AI-ML/MiniProjects/factreal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5da0c7d91a5dc163a2abcbaec4735edb53e675db diff --git a/Domains/AI-ML/MiniProjects/factreal/.dockerignore b/Domains/AI-ML/MiniProjects/factreal/.dockerignore new file mode 100644 index 00000000..03aada82 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/.dockerignore @@ -0,0 +1,16 @@ +.git +.gitignore +__pycache__ +*.pyc +.venv +.env +.vscode +.idea +backend/data/*.csv +backend/data/*.tsv +backend/data/*.json +backend/data/*.jsonl +backend/scripts/*.sh +backend/scripts/*.bat +backend/scripts/*.ps1 +backend/postman diff --git a/Domains/AI-ML/MiniProjects/factreal/.env.example b/Domains/AI-ML/MiniProjects/factreal/.env.example new file mode 100644 index 00000000..f2e2a2c4 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/.env.example @@ -0,0 +1,6 @@ + +MODEL_BIAS=facebook/bart-large-mnli +MODEL_REASONING=google/flan-t5-base +THRESHOLD_BIAS=0.55 +ENABLE_FACT_CHECK=false +# NEWS_API_KEY=your_news_api_key_here diff --git a/Domains/AI-ML/MiniProjects/factreal/.github/workflows/ci.yml b/Domains/AI-ML/MiniProjects/factreal/.github/workflows/ci.yml new file mode 100644 index 00000000..200c4193 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + paths: + - 'k4niz/factreal/**' + pull_request: + paths: + - 'k4niz/factreal/**' + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: k4niz/factreal + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest + + - name: Run tests + run: pytest -q diff --git a/Domains/AI-ML/MiniProjects/factreal/.gitignore b/Domains/AI-ML/MiniProjects/factreal/.gitignore new file mode 100644 index 00000000..532baa1e --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/.gitignore @@ -0,0 +1,45 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +.venv/ +env/ +venv/ +ENV/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ + +# IDE/editor +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Project +.env +*.log +__pycache__/ +.pytest_cache/ +.venv/ +env/ +venv/ +.env +*.pyc +*.pyo +*.pyd +.DS_Store diff --git a/Domains/AI-ML/MiniProjects/factreal/Dockerfile b/Domains/AI-ML/MiniProjects/factreal/Dockerfile new file mode 100644 index 00000000..62d94e75 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/Dockerfile @@ -0,0 +1,27 @@ +# Lightweight production-like image for FactReal +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + HF_HOME=/root/.cache/huggingface + +WORKDIR /app + +# System deps (optional; add git if models need it) +RUN apt-get update -y && apt-get install -y --no-install-recommends \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install Python deps first for better cache +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy backend code +COPY backend ./backend + +# Expose port +EXPOSE 8000 + +# Default command +CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Domains/AI-ML/MiniProjects/factreal/README.md b/Domains/AI-ML/MiniProjects/factreal/README.md new file mode 100644 index 00000000..0b25b76a --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/README.md @@ -0,0 +1,113 @@ +**Contributor:** k4niz + + + +FactReal — AI Backend for Bias/Misinformation Detection +======================================================= + +FactReal is an AI-powered backend system that detects bias, misinformation, and propaganda in text (e.g., social posts, articles). It returns classifications with confidence scores and explainable reasoning, and optionally performs lightweight fact checking. + +- Tech: FastAPI, Hugging Face Transformers +- Targets: Hacktoberfest-friendly, modular, easy to extend + + +Features +-------- +- Bias detection: neutral, biased, misleading, propaganda +- Explainable reasoning: natural language explanation +- Optional fact verification: Wikipedia / NewsAPI (stubs included) +- Modular layers: models → services → controllers → routes + +Project Structure +----------------- +``` +backend/ + models/ + bias_model.py # Transformer zero-shot bias classifier (with offline fallback) + reasoning_model.py # Text-to-text reasoning generator (with offline fallback) + fact_checker.py # Optional fact check (Wikipedia/NewsAPI) + services/ + preprocessing_service.py + classification_service.py + explainability_service.py + api_service.py + controllers/ + classify_controller.py + explain_controller.py + routes/ + classify_router.py + explain_router.py + utils/ + logger.py + config.py + data/ + sample_dataset.jsonl # Small sample data to try + scripts/ + sample_requests.py # Example client calls + run_server.ps1 # Windows: start dev server + main.py # FastAPI app entry + +requirements.txt # Python dependencies +README.md # You are here +``` + +Cleanup +------- +If you see stray files like `controllers.py` or `models.py` in this folder root, they are not used by the modular backend and can be safely deleted. + +Quickstart +---------- +1) Create and activate a virtual environment (recommended). + +2) Install requirements: +```powershell +pip install -r requirements.txt +``` + +3) Run the API server (Windows PowerShell): +```powershell +./backend/scripts/run_server.ps1 +``` +This starts Uvicorn with auto-reload at http://127.0.0.1:8000. + +4) Try example requests in a new terminal: +```powershell +python ./backend/scripts/sample_requests.py +``` +Or use the interactive docs at: +- Swagger UI: http://127.0.0.1:8000/docs +- ReDoc: http://127.0.0.1:8000/redoc + +Environment Configuration +------------------------- +Environment variables (optional) are loaded via `utils/config.py`: +- MODEL_BIAS (default: "facebook/bart-large-mnli") +- MODEL_REASONING (default: "google/flan-t5-base") +- THRESHOLD_BIAS (default: 0.55) +- ENABLE_FACT_CHECK (default: false) +- NEWS_API_KEY (optional) + +Create a `.env` file next to `requirements.txt` if desired: +``` +MODEL_BIAS=facebook/bart-large-mnli +MODEL_REASONING=google/flan-t5-base +THRESHOLD_BIAS=0.6 +ENABLE_FACT_CHECK=true +NEWS_API_KEY=... +``` + +Hacktoberfest Contribution Guide +-------------------------------- +- Good first issues: implement new models, improve heuristics, add datasets, expand fact-checkers +- Coding style: type hints, docstrings, small functions, tests where reasonable +- Folder-first design: add new models in `backend/models/`, new services in `backend/services/`, and wire via controllers/routes +- Please add comments to every function you create; keep public APIs stable + +Data & Models +------------- +- The code uses Hugging Face pipelines. If models can’t be downloaded (e.g., offline), the code falls back to safe heuristic baselines so the API keeps working for demos and local development. +- To change models, set env vars or edit `utils/config.py`. + +License +------- +This project follows the repository’s LICENSE. diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/__init__.py b/Domains/AI-ML/MiniProjects/factreal/backend/__init__.py new file mode 100644 index 00000000..48249d01 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/__init__.py @@ -0,0 +1 @@ +"""Backend package initializer for FactReal.""" diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/controllers/__init__.py b/Domains/AI-ML/MiniProjects/factreal/backend/controllers/__init__.py new file mode 100644 index 00000000..1aaf13b8 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/controllers/__init__.py @@ -0,0 +1,2 @@ +"""Controllers package for FastAPI request handling.""" +"""Controllers for FactReal API.""" diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/controllers/classify_controller.py b/Domains/AI-ML/MiniProjects/factreal/backend/controllers/classify_controller.py new file mode 100644 index 00000000..49d35182 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/controllers/classify_controller.py @@ -0,0 +1,39 @@ +"""Controllers for classification endpoints. + +Defines request/response schemas close to HTTP layer and calls services. +""" +from __future__ import annotations + +from pydantic import BaseModel, Field + +from ..services.api_service import ApiService + + +class ClassifyRequest(BaseModel): + text: str = Field(..., description="Input text to analyze") + + +class ClassifyResponse(BaseModel): + label: str + confidence: float + scores: dict + flagged: bool + explanation: str + fact_check: dict | None = None + + +class ClassifyController: + def __init__(self, api: ApiService | None = None) -> None: + self.api = api or ApiService() + + def classify(self, req: ClassifyRequest) -> ClassifyResponse: + analysis = self.api.analyze(req.text) + payload = analysis.to_json() + return ClassifyResponse( + label=payload["classification"]["label"], + confidence=payload["classification"]["confidence"], + scores=payload["classification"]["scores"], + flagged=payload["classification"]["flagged"], + explanation=payload["explanation"], + fact_check=payload.get("fact_check"), + ) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/controllers/explain_controller.py b/Domains/AI-ML/MiniProjects/factreal/backend/controllers/explain_controller.py new file mode 100644 index 00000000..2e2cb221 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/controllers/explain_controller.py @@ -0,0 +1,24 @@ +"""Controllers for explanation endpoints.""" +from __future__ import annotations + +from pydantic import BaseModel, Field + +from ..services.explainability_service import ExplainabilityService + + +class ExplainRequest(BaseModel): + text: str = Field(..., description="Input text to explain") + label: str = Field(..., description="Label to explain, e.g., biased") + + +class ExplainResponse(BaseModel): + explanation: str + + +class ExplainController: + def __init__(self, service: ExplainabilityService | None = None) -> None: + self.service = service or ExplainabilityService() + + def explain(self, req: ExplainRequest) -> ExplainResponse: + res = self.service.explain(req.text, req.label) + return ExplainResponse(explanation=res.text) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/data/sample_dataset.jsonl b/Domains/AI-ML/MiniProjects/factreal/backend/data/sample_dataset.jsonl new file mode 100644 index 00000000..75403599 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/data/sample_dataset.jsonl @@ -0,0 +1,3 @@ +{"text": "Elections are always rigged and the media hides the truth."} +{"text": "The sky is blue on a clear day."} +{"text": "Experts claim this vaccine is a hoax without any real evidence."} diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/main.py b/Domains/AI-ML/MiniProjects/factreal/backend/main.py new file mode 100644 index 00000000..0a28e852 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/main.py @@ -0,0 +1,32 @@ +"""FastAPI application entry point for FactReal.""" +from __future__ import annotations + +from fastapi import FastAPI +from .controllers.classify_controller import ( + ClassifyController, + ClassifyRequest, + ClassifyResponse, +) + +from .routes.classify_router import router as classify_router +from .routes.explain_router import router as explain_router + +app = FastAPI(title="FactReal", version="0.1.0") + + +@app.get("/") +def health() -> dict: + return {"status": "ok", "service": "FactReal"} + + +app.include_router(classify_router) +app.include_router(explain_router) + + +# Convenience: allow POST / to behave like /classify for ease of testing +_root_classify_controller = ClassifyController() + + +@app.post("/", response_model=ClassifyResponse) +def classify_root(req: ClassifyRequest) -> ClassifyResponse: + return _root_classify_controller.classify(req) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/models/__init__.py b/Domains/AI-ML/MiniProjects/factreal/backend/models/__init__.py new file mode 100644 index 00000000..03ef8fe8 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/models/__init__.py @@ -0,0 +1 @@ +"""Model wrappers for FactReal.""" diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/models/bias_model.py b/Domains/AI-ML/MiniProjects/factreal/backend/models/bias_model.py new file mode 100644 index 00000000..b0587e94 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/models/bias_model.py @@ -0,0 +1,77 @@ +"""Bias detection model wrapper. + +Uses a Hugging Face zero-shot classification pipeline with a model like +"facebook/bart-large-mnli". If transformers or model weights are not available, +falls back to a simple keyword heuristic so the API remains usable. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List + +from ..utils.config import settings +from ..utils.logger import get_logger + +logger = get_logger(__name__) + + +@dataclass +class BiasPrediction: + label: str + score: float + details: Dict[str, float] + + +class BiasModel: + """Encapsulates a zero-shot bias classification pipeline. + + The candidate labels are fixed for now but can be extended easily by + contributors. The class exposes a simple `predict(text)` method. + """ + + CANDIDATE_LABELS = ["neutral", "biased", "misleading", "propaganda"] + + def __init__(self, model_name: str | None = None) -> None: + self.model_name = model_name or settings.MODEL_BIAS + self.pipeline = None + + try: + from transformers import pipeline # defer import for faster startup + + logger.info("Loading zero-shot classification model: %s", self.model_name) + self.pipeline = pipeline( + "zero-shot-classification", model=self.model_name, device_map="auto" + ) + except Exception as e: # noqa: BLE001 broad but safe for offline demo + logger.warning( + "Falling back to heuristic bias detection (reason: %s)", str(e) + ) + self.pipeline = None + + def predict(self, text: str) -> BiasPrediction: + """Return the most likely label with confidence and per-label scores. + + If the transformer pipeline is unavailable, a keyword-based heuristic is used. + """ + if not text.strip(): + return BiasPrediction(label="neutral", score=1.0, details={"neutral": 1.0}) + + if self.pipeline is None: + # Simple heuristic: look for charged words to flag as biased/propaganda + charged = ["fake", "hoax", "never", "always", "disaster", "enemy", "traitor"] + score = 0.7 if any(w in text.lower() for w in charged) else 0.2 + label = "propaganda" if score >= 0.65 else "neutral" + return BiasPrediction(label=label, score=score, details={label: score}) + + res: Dict[str, Any] = self.pipeline( + sequences=[text], candidate_labels=self.CANDIDATE_LABELS + ) + # transformers returns a dict when single input but we passed list; unwrap + if isinstance(res, list): + res = res[0] + labels: List[str] = list(res.get("labels", [])) + scores: List[float] = list(res.get("scores", [])) + best_label = labels[0] if labels else "neutral" + best_score = float(scores[0]) if scores else 0.0 + details = {lbl: float(scr) for lbl, scr in zip(labels, scores)} + return BiasPrediction(label=best_label, score=best_score, details=details) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/models/fact_checker.py b/Domains/AI-ML/MiniProjects/factreal/backend/models/fact_checker.py new file mode 100644 index 00000000..739d3b6a --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/models/fact_checker.py @@ -0,0 +1,90 @@ +"""Optional fact checker using Wikipedia and/or NewsAPI. + +This module contains lightweight stubs that contributors can extend. By default, +it attempts a simple Wikipedia summary presence check and returns references. +If disabled or offline, it safely returns a neutral response. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + +from ..utils.config import settings +from ..utils.logger import get_logger + +logger = get_logger(__name__) + + +@dataclass +class FactCheckResult: + veracity: str # e.g., "unknown", "supported", "disputed" + sources: List[str] + notes: Optional[str] = None + + +class FactChecker: + def __init__(self) -> None: + self.enabled = settings.ENABLE_FACT_CHECK + self.news_api_key = settings.NEWS_API_KEY + + def check(self, text: str) -> FactCheckResult: + """Perform a minimal fact check of the input text. + + - If disabled, returns unknown. + - Try Wikipedia search/summary presence as a naive signal. + - NewsAPI can be wired later using self.news_api_key. + """ + if not self.enabled: + return FactCheckResult(veracity="unknown", sources=[], notes="disabled") + + sources: List[str] = [] + + # 1) Try NewsAPI if configured + if self.news_api_key: + try: + import requests + + # Very naive query: take first 5 tokens; contributors can improve NLP later + terms = [t for t in text.split() if t.isalpha()][:5] + query = " ".join(terms) or text[:64] + resp = requests.get( + "https://newsapi.org/v2/everything", + params={"q": query, "pageSize": 3, "sortBy": "relevancy"}, + headers={"X-Api-Key": self.news_api_key}, + timeout=6, + ) + if resp.status_code == 200: + data = resp.json() + for art in data.get("articles", [])[:3]: + url = art.get("url") + if url: + sources.append(url) + if sources: + return FactCheckResult(veracity="supported", sources=sources) + else: + logger.info("NewsAPI non-200: %s", resp.status_code) + except Exception as e: # noqa: BLE001 + logger.info("NewsAPI unavailable: %s", str(e)) + + # 2) Fallback to Wikipedia signal + try: + import wikipedia + + # Very naive: search for a keyword from text and fetch a summary + terms = text.split() + if not terms: + return FactCheckResult(veracity="unknown", sources=[], notes="empty") + query = terms[0] + results = wikipedia.search(query) + if results: + title = results[0] + try: + page = wikipedia.page(title, auto_suggest=False) + sources.append(page.url) + return FactCheckResult(veracity="supported", sources=sources) + except Exception: # noqa: BLE001 for robustness + pass + except Exception as e: # noqa: BLE001 + logger.info("Wikipedia check unavailable: %s", str(e)) + + return FactCheckResult(veracity="unknown", sources=sources) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/models/reasoning_model.py b/Domains/AI-ML/MiniProjects/factreal/backend/models/reasoning_model.py new file mode 100644 index 00000000..0b978128 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/models/reasoning_model.py @@ -0,0 +1,72 @@ +"""Reasoning (explanation) model wrapper. + +Uses a text-to-text model (e.g., FLAN-T5) to generate short natural language +explanations for a given text and predicted label. If transformers/models are +unavailable, falls back to a template-based heuristic explanation. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from ..utils.config import settings +from ..utils.logger import get_logger + +logger = get_logger(__name__) + + +@dataclass +class ReasoningResult: + explanation: str + + +class ReasoningModel: + def __init__(self, model_name: Optional[str] = None) -> None: + self.model_name = model_name or settings.MODEL_REASONING + self.pipeline = None + try: + from transformers import pipeline + + logger.info("Loading reasoning model: %s", self.model_name) + self.pipeline = pipeline( + "text2text-generation", + model=self.model_name, + device_map="auto", + ) + except Exception as e: # noqa: BLE001 + logger.warning( + "Falling back to template-based reasoning (reason: %s)", str(e) + ) + self.pipeline = None + + def explain(self, text: str, label: str) -> ReasoningResult: + """Generate a concise explanation for a given text and predicted label.""" + prompt = ( + "Explain in 1-2 sentences why the following text may be {label}:\n" + "Text: {text}\nExplanation:" + ).format(label=label, text=text) + + if self.pipeline is None: + # Fallback: simple template + explanation = ( + f"The text is labeled as '{label}' based on its wording and context. " + f"This is a heuristic explanation because the full model is unavailable." + ) + return ReasoningResult(explanation=explanation) + + try: + outs = self.pipeline(prompt, max_new_tokens=80, num_return_sequences=1) + if isinstance(outs, list) and outs: + # Different models return 'generated_text' or 'summary_text' + generated = ( + outs[0].get("generated_text") + or outs[0].get("summary_text") + or "" + ) + explanation = generated.strip() + else: + explanation = "No explanation generated." + except Exception as e: # noqa: BLE001 + logger.error("Reasoning generation error: %s", str(e)) + explanation = "Explanation generation failed due to an internal error." + return ReasoningResult(explanation=explanation) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/postman/FactReal.postman_collection.json b/Domains/AI-ML/MiniProjects/factreal/backend/postman/FactReal.postman_collection.json new file mode 100644 index 00000000..9abb5269 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/postman/FactReal.postman_collection.json @@ -0,0 +1,36 @@ +{ + "info": { + "name": "FactReal", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Classify", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "http://127.0.0.1:8000/classify", "protocol": "http", "host": ["127","0","0","1"], "port": "8000", "path": ["classify"] }, + "body": { + "mode": "raw", + "raw": "{\n \"text\": \"Elections are always rigged and the media hides the truth.\"\n}" + } + } + }, + { + "name": "Explain", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "http://127.0.0.1:8000/explain", "protocol": "http", "host": ["127","0","0","1"], "port": "8000", "path": ["explain"] }, + "body": { + "mode": "raw", + "raw": "{\n \"text\": \"Elections are always rigged and the media hides the truth.\",\n \"label\": \"propaganda\"\n}" + } + } + } + ] +} diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/routes/__init__.py b/Domains/AI-ML/MiniProjects/factreal/backend/routes/__init__.py new file mode 100644 index 00000000..0426fde9 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/routes/__init__.py @@ -0,0 +1,2 @@ +"""API routers package.""" +"""FastAPI routers for FactReal.""" diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/routes/classify_router.py b/Domains/AI-ML/MiniProjects/factreal/backend/routes/classify_router.py new file mode 100644 index 00000000..fc288dbf --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/routes/classify_router.py @@ -0,0 +1,20 @@ +"""FastAPI router for classification endpoints.""" +from __future__ import annotations + +from fastapi import APIRouter + +from ..controllers.classify_controller import ( + ClassifyController, + ClassifyRequest, + ClassifyResponse, +) + + +router = APIRouter(prefix="/classify", tags=["classify"]) +controller = ClassifyController() + + +@router.post("", response_model=ClassifyResponse) +def classify(req: ClassifyRequest) -> ClassifyResponse: + """Classify input text and return label, scores, and explanation.""" + return controller.classify(req) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/routes/explain_router.py b/Domains/AI-ML/MiniProjects/factreal/backend/routes/explain_router.py new file mode 100644 index 00000000..77db539c --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/routes/explain_router.py @@ -0,0 +1,20 @@ +"""FastAPI router for explanation endpoints.""" +from __future__ import annotations + +from fastapi import APIRouter + +from ..controllers.explain_controller import ( + ExplainController, + ExplainRequest, + ExplainResponse, +) + + +router = APIRouter(prefix="/explain", tags=["explain"]) +controller = ExplainController() + + +@router.post("", response_model=ExplainResponse) +def explain(req: ExplainRequest) -> ExplainResponse: + """Generate an explanation for a text and label.""" + return controller.explain(req) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/scripts/run_server.ps1 b/Domains/AI-ML/MiniProjects/factreal/backend/scripts/run_server.ps1 new file mode 100644 index 00000000..69c8eebf --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/scripts/run_server.ps1 @@ -0,0 +1,31 @@ +$ErrorActionPreference = "Stop" + +# Always run Uvicorn from the project root so imports work +# Script location: /backend/scripts/run_server.ps1 +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$backendDir = Split-Path -Parent $scriptDir +$projectRoot = Split-Path -Parent $backendDir +Set-Location $projectRoot + +# Resolve Python interpreter preference order: +# 1) Local venv at .venv\Scripts\python.exe if present +# 2) uvicorn on PATH +# 3) system 'python -m uvicorn' + +$venvPython = Join-Path $projectRoot ".venv\Scripts\python.exe" +Write-Host "[FactReal] Project root:" $projectRoot +if (Test-Path $venvPython) { + Write-Host "[FactReal] Using venv interpreter:" $venvPython + & $venvPython -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload +} +elseif (Get-Command uvicorn -ErrorAction SilentlyContinue) { + $uvPath = (Get-Command uvicorn).Source + Write-Host "[FactReal] Using uvicorn on PATH:" $uvPath + uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload +} +else { + $py = (Get-Command python -ErrorAction SilentlyContinue) + if ($py) { Write-Host "[FactReal] Using system python:" $py.Source } + else { Write-Host "[FactReal] 'python' not found on PATH" } + python -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload +} diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/scripts/sample_requests.py b/Domains/AI-ML/MiniProjects/factreal/backend/scripts/sample_requests.py new file mode 100644 index 00000000..8e44cf70 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/scripts/sample_requests.py @@ -0,0 +1,33 @@ +"""Minimal client to exercise the FactReal API endpoints.""" +from __future__ import annotations + +import json +import time +from typing import Any + +import requests + + +BASE = "http://127.0.0.1:8000" + + +def pretty(obj: Any) -> str: + return json.dumps(obj, indent=2, ensure_ascii=False) + + +def main() -> None: + # Wait briefly in case server just started + time.sleep(0.3) + + text = "Elections are always rigged and the media hides the truth." + r = requests.post(f"{BASE}/classify", json={"text": text}) + print("/classify =>", r.status_code) + print(pretty(r.json())) + + r2 = requests.post(f"{BASE}/explain", json={"text": text, "label": "propaganda"}) + print("/explain =>", r2.status_code) + print(pretty(r2.json())) + + +if __name__ == "__main__": + main() diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/services/__init__.py b/Domains/AI-ML/MiniProjects/factreal/backend/services/__init__.py new file mode 100644 index 00000000..137a5d59 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/services/__init__.py @@ -0,0 +1 @@ +"""Service layer for FactReal.""" diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/services/api_service.py b/Domains/AI-ML/MiniProjects/factreal/backend/services/api_service.py new file mode 100644 index 00000000..b0b0c940 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/services/api_service.py @@ -0,0 +1,57 @@ +"""High-level API service orchestrating preprocessing, classification, +explanation, and optional fact checking. +""" +from __future__ import annotations + +from dataclasses import dataclass, asdict +from typing import Any, Dict, Optional + +from .preprocessing_service import preprocess_text +from .classification_service import ClassificationService, ClassificationResult +from .explainability_service import ExplainabilityService +from ..models.fact_checker import FactChecker, FactCheckResult +from ..utils.config import settings + + +@dataclass +class FullAnalysis: + classification: ClassificationResult + explanation: str + fact_check: Dict[str, Any] | None + + def to_json(self) -> Dict[str, Any]: + payload = { + "classification": asdict(self.classification), + "explanation": self.explanation, + } + if self.fact_check is not None: + payload["fact_check"] = self.fact_check + return payload + + +class ApiService: + def __init__( + self, + classifier: Optional[ClassificationService] = None, + explainer: Optional[ExplainabilityService] = None, + fact_checker: Optional[FactChecker] = None, + ) -> None: + self.classifier = classifier or ClassificationService() + self.explainer = explainer or ExplainabilityService() + self.fact_checker = fact_checker or FactChecker() + + def analyze(self, text: str) -> FullAnalysis: + pre = preprocess_text(text) + cls: ClassificationResult = self.classifier.classify(pre.text) + exp = self.explainer.explain(pre.text, cls.label) + + fc: Dict[str, Any] | None = None + if settings.ENABLE_FACT_CHECK: + fr: FactCheckResult = self.fact_checker.check(pre.text) + fc = { + "veracity": fr.veracity, + "sources": fr.sources, + "notes": fr.notes, + } + + return FullAnalysis(classification=cls, explanation=exp.text, fact_check=fc) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/services/classification_service.py b/Domains/AI-ML/MiniProjects/factreal/backend/services/classification_service.py new file mode 100644 index 00000000..7fa295c2 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/services/classification_service.py @@ -0,0 +1,31 @@ +"""Classification service that wraps the BiasModel. + +Responsible for applying thresholds, shaping the response, and logging. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Optional + +from ..models.bias_model import BiasModel, BiasPrediction +from ..utils.config import settings + + +@dataclass +class ClassificationResult: + label: str + confidence: float + scores: Dict[str, float] + flagged: bool + + +class ClassificationService: + def __init__(self, model: Optional[BiasModel] = None) -> None: + self.model = model or BiasModel() + + def classify(self, text: str) -> ClassificationResult: + pred: BiasPrediction = self.model.predict(text) + flagged = pred.score >= settings.THRESHOLD_BIAS and pred.label != "neutral" + return ClassificationResult( + label=pred.label, confidence=pred.score, scores=pred.details, flagged=flagged + ) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/services/explainability_service.py b/Domains/AI-ML/MiniProjects/factreal/backend/services/explainability_service.py new file mode 100644 index 00000000..279de33b --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/services/explainability_service.py @@ -0,0 +1,21 @@ +"""Explainability service using ReasoningModel.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from ..models.reasoning_model import ReasoningModel, ReasoningResult + + +@dataclass +class Explanation: + text: str + + +class ExplainabilityService: + def __init__(self, model: Optional[ReasoningModel] = None) -> None: + self.model = model or ReasoningModel() + + def explain(self, text: str, label: str) -> Explanation: + res: ReasoningResult = self.model.explain(text, label) + return Explanation(text=res.explanation) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/services/preprocessing_service.py b/Domains/AI-ML/MiniProjects/factreal/backend/services/preprocessing_service.py new file mode 100644 index 00000000..b30ca99c --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/services/preprocessing_service.py @@ -0,0 +1,22 @@ +"""Text preprocessing service. + +Keeps preprocessing minimal and transparent. Contributors can add more advanced +normalization, token cleaning, or language detection here. +""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class PreprocessResult: + text: str + + +def preprocess_text(text: str) -> PreprocessResult: + """Trim and normalize whitespace for now. + + Notes: Minimal by design to avoid altering semantics before classification. + """ + cleaned = " ".join(text.split()) + return PreprocessResult(text=cleaned) diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/utils/config.py b/Domains/AI-ML/MiniProjects/factreal/backend/utils/config.py new file mode 100644 index 00000000..dc62d509 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/utils/config.py @@ -0,0 +1,39 @@ +"""Application configuration using Pydantic Settings (v2). + +This centralizes environment-based settings for model names, +thresholds, and optional feature toggles. Safe defaults are provided +to keep the app usable even without external configuration. +""" +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + # Model identifiers (Hugging Face) + MODEL_BIAS: str = Field( + default="facebook/bart-large-mnli", + description="Zero-shot classification model for bias detection.", + ) + MODEL_REASONING: str = Field( + default="google/flan-t5-base", + description="Text-to-text model for explanation generation.", + ) + + # Thresholds + THRESHOLD_BIAS: float = Field( + default=0.55, description="Confidence threshold to mark text as flagged." + ) + + # Feature toggles + ENABLE_FACT_CHECK: bool = Field( + default=False, description="Enable optional external fact checking." + ) + + # External APIs + NEWS_API_KEY: str | None = Field(default=None, description="NewsAPI key.") + + # Pydantic v2 settings configuration + model_config = SettingsConfigDict(env_file=".env", case_sensitive=False) + + +settings = Settings() diff --git a/Domains/AI-ML/MiniProjects/factreal/backend/utils/logger.py b/Domains/AI-ML/MiniProjects/factreal/backend/utils/logger.py new file mode 100644 index 00000000..594c4350 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/backend/utils/logger.py @@ -0,0 +1,26 @@ +"""Project-wide logger configuration. + +Provides get_logger() to get a configured logger with a concise format. +""" +import logging +import sys + + +def get_logger(name: str | None = None) -> logging.Logger: + """Return a logger with stream handler and a simple formatter. + + Parameters + ---------- + name: Optional logger name. Use module __name__ by default. + """ + logger = logging.getLogger(name or __name__) + if not logger.handlers: + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter( + fmt="%(asctime)s | %(levelname)s | %(name)s | %(message)s", + datefmt="%H:%M:%S", + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + return logger diff --git a/Domains/AI-ML/MiniProjects/factreal/requirements.txt b/Domains/AI-ML/MiniProjects/factreal/requirements.txt new file mode 100644 index 00000000..a530bc07 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/requirements.txt @@ -0,0 +1,10 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +pydantic>=2.7.0 +pydantic-settings>=2.3.0 +python-dotenv>=1.0.1 +transformers>=4.44.0 +torch>=2.3.0 +accelerate>=0.33.0 +requests>=2.32.3 +wikipedia>=1.4.0 \ No newline at end of file diff --git a/Domains/AI-ML/MiniProjects/factreal/tests/test_smoke.py b/Domains/AI-ML/MiniProjects/factreal/tests/test_smoke.py new file mode 100644 index 00000000..1b529505 --- /dev/null +++ b/Domains/AI-ML/MiniProjects/factreal/tests/test_smoke.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient + +from backend.main import app + + +client = TestClient(app) + + +def test_health(): + r = client.get("/") + assert r.status_code == 200 + data = r.json() + assert data.get("status") == "ok" + + +def test_classify(): + r = client.post("/classify", json={"text": "The media hides the truth; elections are always rigged."}) + assert r.status_code == 200 + data = r.json() + assert "label" in data and "confidence" in data and "explanation" in data + + +def test_explain(): + r = client.post( + "/explain", + json={ + "text": "The media hides the truth; elections are always rigged.", + "label": "propaganda", + }, + ) + assert r.status_code == 200 + data = r.json() + assert "explanation" in data