base code added
This commit is contained in:
commit
9f87639b51
12 changed files with 360 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
env
|
||||
.env
|
||||
test.py
|
||||
/__pycache__/
|
||||
0
Dockerfile
Normal file
0
Dockerfile
Normal file
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
0
app/api/__init__.py
Normal file
0
app/api/__init__.py
Normal file
BIN
app/api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/fact_check.cpython-312.pyc
Normal file
BIN
app/api/__pycache__/fact_check.cpython-312.pyc
Normal file
Binary file not shown.
291
app/api/fact_check.py
Normal file
291
app/api/fact_check.py
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field, HttpUrl, validator, ConfigDict
|
||||
from typing import Dict, List, Optional, Union
|
||||
import requests
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
import json
|
||||
from app.config import GOOGLE_FACT_CHECK_API_KEY, GOOGLE_FACT_CHECK_BASE_URL
|
||||
|
||||
fact_check_router = APIRouter()
|
||||
|
||||
class CustomJSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
return super().default(obj)
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
detail: str
|
||||
error_code: str = Field(..., description="Unique error code for this type of error")
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
path: Optional[str] = Field(None, description="The endpoint path where error occurred")
|
||||
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"detail": "Error description",
|
||||
"error_code": "ERROR_CODE",
|
||||
"timestamp": "2024-12-09T16:49:30.905765",
|
||||
"path": "/check-facts"
|
||||
}
|
||||
})
|
||||
|
||||
class RequestValidationError(BaseModel):
|
||||
loc: List[str]
|
||||
msg: str
|
||||
type: str
|
||||
|
||||
class Publisher(BaseModel):
|
||||
name: str
|
||||
site: Optional[str] = Field(None, description="Publisher's website")
|
||||
|
||||
@validator('site')
|
||||
def validate_site(cls, v):
|
||||
if v and not (v.startswith('http://') or v.startswith('https://')):
|
||||
return f"https://{v}"
|
||||
return v
|
||||
|
||||
class ClaimReview(BaseModel):
|
||||
publisher: Publisher
|
||||
url: Optional[HttpUrl] = None
|
||||
title: Optional[str] = None
|
||||
reviewDate: Optional[str] = None
|
||||
textualRating: Optional[str] = None
|
||||
languageCode: str = Field(default="en-US")
|
||||
|
||||
class Claim(BaseModel):
|
||||
text: str
|
||||
claimant: Optional[str] = None
|
||||
claimDate: Optional[str] = None
|
||||
claimReview: List[ClaimReview]
|
||||
|
||||
class FactCheckResponse(BaseModel):
|
||||
query: str = Field(..., description="Original query that was fact-checked")
|
||||
total_claims_found: int = Field(..., ge=0)
|
||||
results: List[Claim] = Field(default_factory=list)
|
||||
summary: Dict[str, int] = Field(...)
|
||||
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"query": "Example claim",
|
||||
"total_claims_found": 1,
|
||||
"results": [{
|
||||
"text": "Example claim text",
|
||||
"claimant": "Source name",
|
||||
"claimReview": [{
|
||||
"publisher": {
|
||||
"name": "Fact Checker",
|
||||
"site": "factchecker.com"
|
||||
},
|
||||
"textualRating": "True"
|
||||
}]
|
||||
}],
|
||||
"summary": {
|
||||
"total_sources": 1,
|
||||
"fact_checking_sites_queried": 10
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
class SourceType(str, Enum):
|
||||
FACT_CHECKER = "fact_checker"
|
||||
NEWS_SITE = "news_site"
|
||||
|
||||
class FactCheckSource(BaseModel):
|
||||
domain: str
|
||||
type: SourceType
|
||||
priority: int = Field(default=1, ge=1, le=10)
|
||||
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"domain": "factcheck.org",
|
||||
"type": "fact_checker",
|
||||
"priority": 1
|
||||
}
|
||||
})
|
||||
|
||||
# Sources configuration with validation
|
||||
SOURCES = {
|
||||
"fact_checkers": [
|
||||
FactCheckSource(domain=domain, type=SourceType.FACT_CHECKER, priority=1)
|
||||
for domain in [
|
||||
"factcheck.org",
|
||||
"snopes.com",
|
||||
"politifact.com",
|
||||
"reuters.com",
|
||||
"bbc.com",
|
||||
"apnews.com",
|
||||
"usatoday.com",
|
||||
"nytimes.com",
|
||||
"washingtonpost.com",
|
||||
"afp.com",
|
||||
"fullfact.org",
|
||||
"truthorfiction.com",
|
||||
"leadstories.com",
|
||||
"altnews.in",
|
||||
"boomlive.in",
|
||||
"en.prothomalo.com"
|
||||
]
|
||||
],
|
||||
"news_sites": [
|
||||
FactCheckSource(domain=domain, type=SourceType.NEWS_SITE, priority=2)
|
||||
for domain in [
|
||||
"www.thedailystar.net",
|
||||
"www.thefinancialexpress.com.bd",
|
||||
"www.theindependentbd.com",
|
||||
"www.dhakatribune.com",
|
||||
"www.newagebd.net",
|
||||
"www.observerbd.com",
|
||||
"www.daily-sun.com",
|
||||
"www.tbsnews.net",
|
||||
"www.businesspostbd.com",
|
||||
"www.banglanews24.com/english",
|
||||
"www.bdnews24.com/english",
|
||||
"www.risingbd.com/english",
|
||||
"www.dailyindustry.news",
|
||||
"www.bangladeshpost.net",
|
||||
"www.daily-bangladesh.com/english"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
class FactCheckRequest(BaseModel):
|
||||
content: str = Field(
|
||||
...,
|
||||
min_length=10,
|
||||
max_length=1000,
|
||||
description="The claim to be fact-checked"
|
||||
)
|
||||
language: str = Field(default="en-US", pattern="^[a-z]{2}-[A-Z]{2}$")
|
||||
max_results_per_source: int = Field(default=10, ge=1, le=50)
|
||||
|
||||
@validator('content')
|
||||
def validate_content(cls, v):
|
||||
if not v.strip():
|
||||
raise ValueError("Content cannot be empty or just whitespace")
|
||||
return v.strip()
|
||||
|
||||
async def fetch_fact_checks(
|
||||
api_key: str,
|
||||
base_url: str,
|
||||
query: str,
|
||||
site: FactCheckSource
|
||||
) -> Dict:
|
||||
"""
|
||||
Fetch fact checks from a specific site using the Google Fact Check API
|
||||
"""
|
||||
try:
|
||||
if not api_key or not base_url:
|
||||
raise ValueError("API key or base URL not configured")
|
||||
|
||||
params = {
|
||||
"key": api_key,
|
||||
"query": query,
|
||||
"languageCode": "en-US",
|
||||
"reviewPublisherSiteFilter": site.domain,
|
||||
"pageSize": 10
|
||||
}
|
||||
|
||||
response = requests.get(base_url, params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail=ErrorResponse(
|
||||
detail=f"Error fetching from {site.domain}: {str(e)}",
|
||||
error_code="FACT_CHECK_SERVICE_ERROR",
|
||||
path="/check-facts"
|
||||
).dict()
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=ErrorResponse(
|
||||
detail=str(e),
|
||||
error_code="CONFIGURATION_ERROR",
|
||||
path="/check-facts"
|
||||
).dict()
|
||||
)
|
||||
|
||||
@fact_check_router.post(
|
||||
"/check-facts",
|
||||
response_model=FactCheckResponse,
|
||||
responses={
|
||||
400: {"model": ErrorResponse},
|
||||
404: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
503: {"model": ErrorResponse}
|
||||
}
|
||||
)
|
||||
async def check_facts(request: FactCheckRequest) -> FactCheckResponse:
|
||||
"""
|
||||
Check facts using multiple fact-checking sources
|
||||
"""
|
||||
all_results = []
|
||||
|
||||
# Validate configuration
|
||||
if not GOOGLE_FACT_CHECK_API_KEY or not GOOGLE_FACT_CHECK_BASE_URL:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=ErrorResponse(
|
||||
detail="API configuration is missing",
|
||||
error_code="CONFIGURATION_ERROR",
|
||||
path="/check-facts"
|
||||
).dict()
|
||||
)
|
||||
|
||||
# Check all sources in priority order
|
||||
all_sources = (
|
||||
SOURCES["fact_checkers"] +
|
||||
SOURCES["news_sites"]
|
||||
)
|
||||
all_sources.sort(key=lambda x: x.priority)
|
||||
|
||||
for source in all_sources:
|
||||
try:
|
||||
result = await fetch_fact_checks(
|
||||
GOOGLE_FACT_CHECK_API_KEY,
|
||||
GOOGLE_FACT_CHECK_BASE_URL,
|
||||
request.content,
|
||||
source
|
||||
)
|
||||
|
||||
if "claims" in result:
|
||||
# Validate each claim through Pydantic
|
||||
validated_claims = [
|
||||
Claim(**claim).dict()
|
||||
for claim in result["claims"]
|
||||
]
|
||||
all_results.extend(validated_claims)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Log the error but continue with other sources
|
||||
print(f"Error processing {source.domain}: {str(e)}")
|
||||
continue
|
||||
|
||||
if not all_results:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=ErrorResponse(
|
||||
detail="No fact check results found",
|
||||
error_code="NO_RESULTS_FOUND",
|
||||
path="/check-facts"
|
||||
).dict()
|
||||
)
|
||||
|
||||
# Create the response using Pydantic model
|
||||
response = FactCheckResponse(
|
||||
query=request.content,
|
||||
total_claims_found=len(all_results),
|
||||
results=all_results,
|
||||
summary={
|
||||
"total_sources": len(set(claim.get("claimReview", [{}])[0].get("publisher", {}).get("site", "")
|
||||
for claim in all_results if claim.get("claimReview"))),
|
||||
"fact_checking_sites_queried": len(all_sources)
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
10
app/config.py
Normal file
10
app/config.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
GOOGLE_FACT_CHECK_API_KEY = os.environ["GOOGLE_FACT_CHECK_API_KEY"]
|
||||
GOOGLE_FACT_CHECK_BASE_URL= os.environ["GOOGLE_FACT_CHECK_BASE_URL"]
|
||||
|
||||
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
|
||||
FRONTEND_URL = os.environ["FRONTEND_URL"]
|
||||
49
main.py
Normal file
49
main.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.api.fact_check import fact_check_router
|
||||
from app.config import FRONTEND_URL
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Your API Title",
|
||||
description="Your API Description",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# CORS configuration
|
||||
origins = [
|
||||
FRONTEND_URL,
|
||||
"http://localhost",
|
||||
"http://localhost:5173",
|
||||
"http://0.0.0.0",
|
||||
"http://0.0.0.0:5173",
|
||||
]
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Basic root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Welcome to your FastAPI application"}
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
app.include_router(fact_check_router, prefix="")
|
||||
|
||||
# Include routers (uncomment and modify as needed)
|
||||
# from routes import some_router
|
||||
# app.include_router(some_router, prefix="/your-prefix", tags=["your-tag"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
certifi==2024.8.30
|
||||
charset-normalizer==3.4.0
|
||||
idna==3.10
|
||||
python-dotenv==1.0.1
|
||||
requests==2.32.3
|
||||
urllib3==2.2.3
|
||||
Loading…
Add table
Reference in a new issue