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
66 changes: 43 additions & 23 deletions backend/app/routes/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
- ask_llm: Generates a natural language answer using retrieved context.
"""


from fastapi import APIRouter
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, HttpUrl, Field, validator
from app.modules.pipeline import run_scraper_pipeline
from app.modules.pipeline import run_langgraph_workflow
from app.modules.bias_detection.check_bias import check_bias
Expand All @@ -46,12 +46,17 @@
router = APIRouter()


class URlRequest(BaseModel):
url: str

class URLRequest(BaseModel):
url: HttpUrl # Validates URL format automatically, returns 422 for invalid URLs

class ChatQuery(BaseModel):
message: str
message: str = Field(..., min_length=1, strip_whitespace=True) # Rejects empty/whitespace

@validator('message')
def message_must_not_be_blank(cls, v):
if not v.strip():
raise ValueError('Message cannot be empty or whitespace')
return v


@router.get("/")
Expand All @@ -60,26 +65,41 @@ async def home():


@router.post("/bias")
async def bias_detection(request: URlRequest):
content = await asyncio.to_thread(run_scraper_pipeline, (request.url))
bias_score = await asyncio.to_thread(check_bias, (content))
logger.info(f"Bias detection result: {bias_score}")
return bias_score
async def bias_detection(request: URLRequest):
try:
content = await asyncio.to_thread(run_scraper_pipeline, (str(request.url)))
bias_score = await asyncio.to_thread(check_bias, (content))
logger.info(f"Bias detection result: {bias_score}")
return bias_score
Comment on lines +69 to +73
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify failure semantics in scraper/pipeline code paths used by /bias and /process.
rg -n -C3 'def run_scraper_pipeline|def extract\(|def _fetch_html|return ""|return \{"url": self\.url, "text": "", "error": "Failed to extract article\."\}|raise ValueError' backend/app/modules/pipeline.py backend/app/modules/scraper/extractor.py

Repository: AOSSIE-Org/Perspective

Length of output: 2427


🏁 Script executed:

# Check the full run_scraper_pipeline function
cat -n backend/app/modules/pipeline.py | head -80

Repository: AOSSIE-Org/Perspective

Length of output: 2975


🏁 Script executed:

# Check the routes.py file to see error handlers and context
cat -n backend/app/routes/routes.py

Repository: AOSSIE-Org/Perspective

Length of output: 4566


🏁 Script executed:

# Check what check_bias expects
rg -n -A10 'def check_bias' backend/

Repository: AOSSIE-Org/Perspective

Length of output: 1009


🏁 Script executed:

# Check what run_langgraph_workflow expects
rg -n -A10 'def run_langgraph_workflow' backend/

Repository: AOSSIE-Org/Perspective

Length of output: 462


🏁 Script executed:

# Check clean_extracted_text function
rg -n -A15 'def clean_extracted_text' backend/

Repository: AOSSIE-Org/Perspective

Length of output: 1238


🏁 Script executed:

# Check full check_bias function logic
cat -n backend/app/modules/bias_detection/check_bias.py | head -60

Repository: AOSSIE-Org/Perspective

Length of output: 2362


Add explicit validation for empty article content to ensure deterministic 400 handling.

The scraper pipeline can return {"cleaned_text": "", "keywords": []} on failure. Since check_bias() receives the entire dict and only checks if not text: (dict truthiness), an empty cleaned_text value bypasses validation and causes inconsistent behavior. Both endpoints must validate that cleaned_text is actually populated.

Also remove unnecessary tuple wrapping when passing str(request.url) to asyncio.to_thread().

Suggested fix
 `@router.post`("/bias")
 async def bias_detection(request: URLRequest):
     try:
-        content = await asyncio.to_thread(run_scraper_pipeline, (str(request.url)))
+        content = await asyncio.to_thread(run_scraper_pipeline, str(request.url))
+        if not content.get("cleaned_text"):
+            raise ValueError("Unable to extract article content from URL")
         bias_score = await asyncio.to_thread(check_bias, (content))
         logger.info(f"Bias detection result: {bias_score}")
         return bias_score
@@
 `@router.post`("/process")
 async def run_pipelines(request: URLRequest):
     try:
-        article_text = await asyncio.to_thread(run_scraper_pipeline, (str(request.url)))
+        article_text = await asyncio.to_thread(run_scraper_pipeline, str(request.url))
+        if not article_text.get("cleaned_text"):
+            raise ValueError("Unable to extract article content from URL")
         logger.debug(f"Scraper output: {json.dumps(article_text, indent=2, ensure_ascii=False)}")
         data = await asyncio.to_thread(run_langgraph_workflow, (article_text))
         return data
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/routes/routes.py` around lines 69 - 73, The scraper can return
{"cleaned_text": "", "keywords": []} so update the route handler around the
run_scraper_pipeline and check_bias calls to (1) call asyncio.to_thread without
tuple-wrapping the argument (use asyncio.to_thread(run_scraper_pipeline,
str(request.url))) and (2) explicitly validate the returned dict's cleaned_text
(e.g., ensure result.get("cleaned_text") is a non-empty string) before calling
check_bias; if cleaned_text is empty/missing, return a deterministic 400
response. Reference run_scraper_pipeline, check_bias, and request.url when
making these changes.

except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception:
raise # Let global handler in main.py catch it with trace_id


@router.post("/process")
async def run_pipelines(request: URlRequest):
article_text = await asyncio.to_thread(run_scraper_pipeline, (request.url))
logger.debug(f"Scraper output: {json.dumps(article_text, indent=2, ensure_ascii=False)}")
data = await asyncio.to_thread(run_langgraph_workflow, (article_text))
return data
async def run_pipelines(request: URLRequest):
try:
article_text = await asyncio.to_thread(run_scraper_pipeline, (str(request.url)))
logger.debug(f"Scraper output: {json.dumps(article_text, indent=2, ensure_ascii=False)}")
data = await asyncio.to_thread(run_langgraph_workflow, (article_text))
return data
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception:
raise # Let global handler in main.py catch it with trace_id


@router.post("/chat")
async def answer_query(request: ChatQuery):
query = request.message
results = search_pinecone(query)
answer = ask_llm(query, results)
logger.info(f"Chat answer generated: {answer}")

return {"answer": answer}
try:
query = request.message
results = search_pinecone(query)
answer = ask_llm(query, results)
logger.info(f"Chat answer generated: {answer}")
return {"answer": answer}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception:
raise

78 changes: 48 additions & 30 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,52 @@
app (FastAPI): The FastAPI application instance.
"""

from fastapi import FastAPI
from app.routes.routes import router as article_router
from fastapi.middleware.cors import CORSMiddleware
from app.logging.logging_config import setup_logger
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
import logging
import uuid

app = FastAPI()

logger = logging.getLogger(__name__)

Comment on lines +28 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Register the APIRouter; otherwise all API endpoints return 404.

backend/app/routes/routes.py defines the handlers on router, but this app instance never includes that router. In the current state, the API surface is effectively unreachable.

🔧 Proposed fix
+from app.routes.routes import router
+
 app = FastAPI()
+app.include_router(router)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app = FastAPI()
logger = logging.getLogger(__name__)
from app.routes.routes import router
app = FastAPI()
app.include_router(router)
logger = logging.getLogger(__name__)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/main.py` around lines 28 - 31, The FastAPI app instance is created
but never mounts the routes router, so endpoints defined on router in
backend/app/routes/routes.py return 404; import the router (named router) from
that module and call app.include_router(router) on the FastAPI instance (app)
after logger creation to register the endpoints, ensuring any prefix or tags
used in routes.py are preserved when including the router.

# Handle HTTPExceptions (400, 404, etc.)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": exc.detail,
"code": f"HTTP_{exc.status_code}"
}
)

# Handle ALL unhandled exceptions (500s)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
trace_id = str(uuid.uuid4()) # Unique ID for debugging
logger.error(f"[{trace_id}] Unhandled exception: {exc}", exc_info=True)

# Setup logger for this module
logger = setup_logger(__name__)

app = FastAPI(
title="Perspective API",
version="1.0.0",
description=("An API to generate alternative perspectives on biased articles"),
)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(article_router, prefix="/api", tags=["Articles"])

if __name__ == "__main__":
import uvicorn
import os

port = int(os.environ.get("PORT", 7860))
logger.info(f" Server is running on http://localhost:{port}")
uvicorn.run(app, host="0.0.0.0", port=port)
return JSONResponse(
status_code=500,
content={
"error": "An internal server error occurred",
"code": "INTERNAL_SERVER_ERROR",
"trace_id": trace_id # Return to frontend for easier debugging
}
)

from fastapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = exc.errors()
return JSONResponse(
status_code=400,
content={
"error": errors[0]['msg'] if errors else "Invalid input",
"code": "VALIDATION_ERROR",
"details": errors
}
)