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
12 changes: 9 additions & 3 deletions backend/app/logging/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,20 @@ def setup_logger(name: str) -> logging.Logger:
datefmt="%Y-%m-%d %H:%M:%S"
)

# Console Handler
# Console Handler with UTF-8 encoding for Windows support
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# Enable UTF-8 encoding to handle emoji and special characters on Windows
if hasattr(console_handler.stream, 'reconfigure'):
try:
console_handler.stream.reconfigure(encoding='utf-8')
except (AttributeError, ValueError):
pass
logger.addHandler(console_handler)

# File Handler
file_handler = logging.FileHandler("app.log")
# File Handler with UTF-8 encoding
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG) # Keep detailed logs in file
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/modules/bias_detection/check_bias.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def check_bias(text):
"content": (f"Give bias score to the following article \n\n{text}"),
},
],
model="gemma2-9b-it",
model="llama-3.1-8b-instant",
temperature=0.3,
max_tokens=512,
)
Expand Down
22 changes: 18 additions & 4 deletions backend/app/modules/chat/llm_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,23 @@ def build_context(docs):
return "\n".join(
f"{m['metadata'].get('explanation') or m['metadata'].get('reasoning', '')}"
for m in docs
)
).strip()


def ask_llm(question, docs, article_context=""):
rag_context = build_context(docs)
article_context = (article_context or "").strip()

if rag_context and article_context:
context = f"Article:\n{article_context}\n\nRetrieved Insights:\n{rag_context}"
else:
context = rag_context or article_context

if not context:
return (
"I don't have article context yet. Please analyze an article first and then ask me again."
)

def ask_llm(question, docs):
context = build_context(docs)
logger.debug(f"Generated context for LLM:\n{context}")
prompt = f"""You are an assistant that answers based on context.

Expand All @@ -55,11 +67,13 @@ def ask_llm(question, docs):
"""

response = client.chat.completions.create(
model="gemma2-9b-it",
model="llama-3.1-8b-instant",
messages=[
{"role": "system", "content": "Use only the context to answer."},
{"role": "user", "content": prompt},
],
)
logger.info("LLM response retrieved successfully.")
return response.choices[0].message.content


4 changes: 2 additions & 2 deletions backend/app/modules/facts_check/llm_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def run_claim_extractor_sdk(state):
),
},
],
model="gemma2-9b-it",
model="llama-3.1-8b-instant",
temperature=0.3,
max_tokens=512,
)
Expand Down Expand Up @@ -128,7 +128,7 @@ def run_fact_verifier_sdk(search_results):
),
},
],
model="gemma2-9b-it",
model="llama-3.1-8b-instant",
temperature=0.3,
max_tokens=256,
)
Expand Down
33 changes: 21 additions & 12 deletions backend/app/modules/facts_check/web_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@


def search_google(query):
results = requests.get(
f"https://www.googleapis.com/customsearch/v1?key={GOOGLE_SEARCH}&cx=f637ab77b5d8b4a3c&q={query}"
)
res = results.json()
first = {}
first["title"] = res["items"][0]["title"]
first["link"] = res["items"][0]["link"]
first["snippet"] = res["items"][0]["snippet"]

return [
first,
]
try:
results = requests.get(
f"https://www.googleapis.com/customsearch/v1?key={GOOGLE_SEARCH}&cx=f637ab77b5d8b4a3c&q={query}"
)
Comment on lines +32 to +34
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 | 🟡 Minor

Add a timeout to the HTTP request.

The requests.get call has no timeout, which can cause the request to hang indefinitely if the Google API is unresponsive. This is a reliability concern flagged by static analysis (S113).

🛡️ Proposed fix to add timeout
         results = requests.get(
-            f"https://www.googleapis.com/customsearch/v1?key={GOOGLE_SEARCH}&cx=f637ab77b5d8b4a3c&q={query}"
+            f"https://www.googleapis.com/customsearch/v1?key={GOOGLE_SEARCH}&cx=f637ab77b5d8b4a3c&q={query}",
+            timeout=10
         )
📝 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
results = requests.get(
f"https://www.googleapis.com/customsearch/v1?key={GOOGLE_SEARCH}&cx=f637ab77b5d8b4a3c&q={query}"
)
results = requests.get(
f"https://www.googleapis.com/customsearch/v1?key={GOOGLE_SEARCH}&cx=f637ab77b5d8b4a3c&q={query}",
timeout=10
)
🧰 Tools
🪛 Ruff (0.15.6)

[error] 32-32: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/modules/facts_check/web_search.py` around lines 32 - 34, The HTTP
call assigning to results uses requests.get without a timeout; update the
requests.get call in web_search.py (the line that builds results =
requests.get(...)) to include a reasonable timeout (e.g., timeout=10 or a
configurable constant) so the request cannot hang indefinitely, and ensure any
surrounding error handling will catch requests.exceptions.Timeout if needed.

res = results.json()

# Check if the response contains 'items' (successful search)
if "items" not in res:
# Handle error responses from Google API
error_msg = res.get("error", {}).get("message", "Unknown error")
raise ValueError(f"Google API Error: {error_msg}")

first = {}
first["title"] = res["items"][0]["title"]
first["link"] = res["items"][0]["link"]
first["snippet"] = res["items"][0]["snippet"]

return [first]
except Exception as e:
print(f"Search Google Error: {e}")
raise
Comment on lines +49 to +51
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.

🛠️ Refactor suggestion | 🟠 Major

Use logger instead of print for error output.

The module uses print() for error logging while other modules in the codebase use the configured logger. This inconsistency affects log aggregation and monitoring.

♻️ Proposed fix
+from app.logging.logging_config import setup_logger
+
+logger = setup_logger(__name__)
+
 def search_google(query):
     try:
         # ... existing code ...
     except Exception as e:
-        print(f"Search Google Error: {e}")
+        logger.error(f"Search Google Error: {e}")
         raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/modules/facts_check/web_search.py` around lines 49 - 51, Replace
the print-based error reporting in the web_search module's exception handler
(the except Exception as e block that prints "Search Google Error: {e}") with
the project logger used elsewhere: call logger.exception or logger.error
(including exc_info) to log the exception and context instead of print, keeping
the same message text and the exception variable e so logs are captured by the
app's logging/monitoring pipeline.

2 changes: 1 addition & 1 deletion backend/app/modules/langgraph_nodes/judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

# Init once
groq_llm = ChatGroq(
model="gemma2-9b-it",
model="llama-3.1-8b-instant",
temperature=0.0,
max_tokens=10,
)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/modules/langgraph_nodes/sentiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def run_sentiment_sdk(state):
),
},
],
model="gemma2-9b-it",
model="llama-3.1-8b-instant",
temperature=0.2,
max_tokens=3,
)
Expand Down
26 changes: 19 additions & 7 deletions backend/app/routes/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"""


from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.modules.pipeline import run_scraper_pipeline
from app.modules.pipeline import run_langgraph_workflow
Expand All @@ -52,6 +52,7 @@ class URlRequest(BaseModel):

class ChatQuery(BaseModel):
message: str
article_context: str | None = None


@router.get("/")
Expand All @@ -77,9 +78,20 @@ async def run_pipelines(request: URlRequest):

@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.strip()
if not query:
raise HTTPException(status_code=400, detail="Message cannot be empty.")

article_context = (request.article_context or "").strip()

results = search_pinecone(query)
answer = ask_llm(query, results, article_context)
Comment on lines +88 to +89
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
set -euo pipefail

sed -n '79,97p' backend/app/routes/routes.py

printf '\n--- search_pinecone definition ---\n'
rg -n -C3 '^(async\s+def|def)\s+search_pinecone\b' backend/app/modules/chat

printf '\n--- ask_llm definition ---\n'
rg -n -C3 '^(async\s+def|def)\s+ask_llm\b' backend/app/modules/chat

Repository: AOSSIE-Org/Perspective

Length of output: 1755


Wrap synchronous I/O calls in asyncio.to_thread() to prevent event loop blocking.

Lines 88–89 invoke search_pinecone() and ask_llm() synchronously inside an async route. Both perform remote I/O (Pinecone queries and LLM calls) and will block the event loop, delaying other requests.

Suggested fix
-        results = search_pinecone(query)
-        answer = ask_llm(query, results, article_context)
+        results = await asyncio.to_thread(search_pinecone, query)
+        answer = await asyncio.to_thread(ask_llm, query, results, article_context)
📝 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
results = search_pinecone(query)
answer = ask_llm(query, results, article_context)
results = await asyncio.to_thread(search_pinecone, query)
answer = await asyncio.to_thread(ask_llm, query, results, article_context)
🤖 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 88 - 89, The calls to
search_pinecone and ask_llm are synchronous and will block the async route; wrap
them in asyncio.to_thread (or loop.run_in_executor) and await the results so the
event loop isn't blocked. Specifically, replace direct calls to
search_pinecone(query) and ask_llm(query, results, article_context) in the async
handler with awaited asyncio.to_thread(...) invocations (e.g., results = await
asyncio.to_thread(search_pinecone, query); answer = await
asyncio.to_thread(ask_llm, query, results, article_context)), preserving
existing error handling and variable names.

logger.info("Chat answer generated successfully.")

return {"answer": answer}
except HTTPException:
raise
except Exception as e:
logger.exception(f"Chat request failed: {e}")
raise HTTPException(status_code=500, detail="Failed to generate chat response.")
15 changes: 9 additions & 6 deletions backend/app/utils/fact_check_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,36 +45,39 @@ def run_fact_check_pipeline(state):
result = run_claim_extractor_sdk(state)

if state.get("status") != "success":
logger.error("Claim extraction failed.")
logger.error("Claim extraction failed.")
return [], "Claim extraction failed."
Comment on lines 47 to 49
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

Bug: Checking wrong variable for extraction status.

Line 47 checks state.get("status") but should check result.get("status") since result is the return value from run_claim_extractor_sdk. The input state won't have a status field, so this condition will never be true when extraction fails.

🐛 Proposed fix
-    if state.get("status") != "success":
+    if result.get("status") != "success":
         logger.error("Claim extraction failed.")
         return [], "Claim extraction failed."
📝 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
if state.get("status") != "success":
logger.error("❌ Claim extraction failed.")
logger.error("Claim extraction failed.")
return [], "Claim extraction failed."
if result.get("status") != "success":
logger.error("Claim extraction failed.")
return [], "Claim extraction failed."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/utils/fact_check_utils.py` around lines 47 - 49, In the claim
extraction path inside fact_check_utils (where run_claim_extractor_sdk is
called), change the conditional that checks the extraction outcome to inspect
result.get("status") instead of state.get("status"); update the error branch
that currently logs "Claim extraction failed." and returns to use result (e.g.,
include result or its error field) so failures from run_claim_extractor_sdk are
correctly detected and reported (look for the block referencing
run_claim_extractor_sdk, the local variable result, and the current check using
state.get("status")).


# Step 1: Extract claims
raw_output = result.get("verifiable_claims", "")
claims = re.findall(r"^[\*\-•]\s+(.*)", raw_output, re.MULTILINE)
claims = [claim.strip() for claim in claims if claim.strip()]
logger.info(f"🧠 Extracted claims: {claims}")
logger.info(f"Extracted claims: {claims}")

if not claims:
return [], "No verifiable claims found."

# Step 2: Search each claim with polite delay
search_results = []
for claim in claims:
logger.info(f"\n🔍 Searching for claim: {claim}")
logger.info(f"Searching for claim: {claim}")
try:
results = search_google(claim)
if results:
results[0]["claim"] = claim
search_results.append(results[0])
logger.info(f"Found result: {results[0]['title']}")
logger.info(f"Found result: {results[0]['title']}")
else:
logger.warning(f"⚠️ No search result for: {claim}")
logger.warning(f"No search result for: {claim}")
except Exception as e:
logger.error(f"Search failed for: {claim} -> {e}")
logger.error(f"Search failed for: {claim} -> {e}")

if not search_results:
logger.error("All claim searches failed or returned no results.")
return [], "All claim searches failed or returned no results."

# Step 3: Verify facts using LLM
logger.info(f"Verifying {len(search_results)} claims using LLM...")
final = run_fact_verifier_sdk(search_results)
logger.info("Fact-checking pipeline completed successfully.")
return final.get("verifications", []), None
6 changes: 3 additions & 3 deletions frontend/app/analyze/loading/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import ThemeToggle from "@/components/theme-toggle";
import axios from "axios";

// const backend_url = process.env.NEXT_PUBLIC_API_URL;
const backend_url = process.env.NEXT_PUBLIC_API_URL;



Expand Down Expand Up @@ -74,10 +74,10 @@ export default function LoadingPage() {

try {
const [processRes, biasRes] = await Promise.all([
axios.post("https://thunder1245-perspective-backend.hf.space/api/process", {
axios.post(`${backend_url}/api/process`, {
url: storedUrl,
}),
axios.post("https://thunder1245-perspective-backend.hf.space/api/bias", {
axios.post(`${backend_url}/api/bias`, {
url: storedUrl,
}),
Comment on lines 76 to 82
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
set -euo pipefail

printf 'Loading page requests:\n'
sed -n '75,83p' frontend/app/analyze/loading/page.tsx

printf '\nChat request config for comparison:\n'
sed -n '92,100p' frontend/app/analyze/results/page.tsx

Repository: AOSSIE-Org/Perspective

Length of output: 686


Add a timeout to the analysis requests.

The POST calls to /api/process and /api/bias on lines 77–80 lack timeout configuration. If either request hangs, Promise.all never settles and the page stays on the spinner indefinitely because the redirect only runs after axios rejects. The codebase already uses a 45s timeout for the chat request in results/page.tsx (line 97); apply the same timeout here for consistency and to make this failure mode recoverable.

Suggested fix
+          const requestConfig = { timeout: 45000 };
           const [processRes, biasRes] = await Promise.all([
-            axios.post(`${backend_url}/api/process`, {
-              url: storedUrl,
-            }),
-            axios.post(`${backend_url}/api/bias`, {
-              url: storedUrl,
-            }),
+            axios.post(`${backend_url}/api/process`, { url: storedUrl }, requestConfig),
+            axios.post(`${backend_url}/api/bias`, { url: storedUrl }, requestConfig),
           ]);
📝 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
const [processRes, biasRes] = await Promise.all([
axios.post("https://thunder1245-perspective-backend.hf.space/api/process", {
axios.post(`${backend_url}/api/process`, {
url: storedUrl,
}),
axios.post("https://thunder1245-perspective-backend.hf.space/api/bias", {
axios.post(`${backend_url}/api/bias`, {
url: storedUrl,
}),
const requestConfig = { timeout: 45000 };
const [processRes, biasRes] = await Promise.all([
axios.post(`${backend_url}/api/process`, { url: storedUrl }, requestConfig),
axios.post(`${backend_url}/api/bias`, { url: storedUrl }, requestConfig),
]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` around lines 76 - 82, The two axios
POST calls to /api/process and /api/bias (the Promise.all block creating
processRes and biasRes) lack a request timeout and can hang indefinitely; update
both axios.post calls to pass a timeout option of 45000 ms (match the existing
chat request timeout used in results/page.tsx) so each request will reject after
45s and allow Promise.all to settle and the page to recover.

]);
Expand Down
Loading