Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4f7ed34
(chore): upgrade package newspaper3k -> newspaper4k [compatibility] p…
khushal1512 Dec 28, 2025
eec9a7e
(feat): Replace GoogleSearch Tool with LangGraph Native DuckDuckGo Se…
khushal1512 Dec 29, 2025
b48af96
docs: minor fix in env variables needed to setup
khushal1512 Dec 29, 2025
f963b97
(refactor): Now Graph runs asynchronously due to parallel executing n…
khushal1512 Dec 29, 2025
0bc84cc
fix: update chunk_rag_data to handle dynamic keys and prevent crashes
khushal1512 Dec 29, 2025
ef1f0bc
fix: updated schema and fact parsing in generate_perspective
khushal1512 Dec 29, 2025
32beeb8
chore(backend): update dependencies for NLTK and FastAPI compatibility
khushal1512 Jan 25, 2026
938853a
chore(config): update tailwind config and add static assets
khushal1512 Jan 25, 2026
fd52659
feat(ai): refine prompt templates for perspective generation
khushal1512 Jan 25, 2026
c15403c
fix(backend): optimize fact check utility functions for thread safety
khushal1512 Jan 25, 2026
27efddf
feat(ai): implement core perspective generation node logic
khushal1512 Jan 25, 2026
cad3477
feat(ai): update graph builder to include new perspective nodes
khushal1512 Jan 25, 2026
a568e48
feat(landing): implement navbar, footer, and hero components
khushal1512 Jan 26, 2026
8710410
feat(landing): add features showcase with isometric card design
khushal1512 Jan 26, 2026
71d432d
feat(landing): implement stats counter and final call-to-action section
khushal1512 Jan 26, 2026
c550c7e
feat(search): implement search bar validation and routing logic
khushal1512 Jan 26, 2026
47d5753
refactor(frontend): extract api logic to usePerspective custom hook
khushal1512 Jan 26, 2026
6cb458a
feat(ui): add modular bias gauge and sidebar components
khushal1512 Jan 26, 2026
3a526a4
fix(frontend): connect perspective page to backend and fix mobile res…
khushal1512 Jan 26, 2026
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ GROQ_API_KEY= <groq_api_key>
PINECONE_API_KEY = <your_pinecone_API_KEY>
PORT = 8000
SEARCH_KEY = <your_Google_custom_search_engine_API_key>
HF_TOKEN = <your_huggingface_access_token>
```

*Run backend:*
Expand Down
4 changes: 4 additions & 0 deletions backend/app/llm_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os

# Default to a stable model
LLM_MODEL = os.getenv("LLM_MODEL", "llama-3.3-70b-versatile")
3 changes: 2 additions & 1 deletion backend/app/modules/bias_detection/check_bias.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from dotenv import load_dotenv
import json
from app.logging.logging_config import setup_logger
from app.llm_config import LLM_MODEL

logger = setup_logger(__name__)

Expand Down Expand Up @@ -61,7 +62,7 @@ def check_bias(text):
"content": (f"Give bias score to the following article \n\n{text}"),
},
],
model="gemma2-9b-it",
model=LLM_MODEL,
temperature=0.3,
max_tokens=512,
)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/modules/chat/get_rag_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

load_dotenv()

pc = Pinecone(os.getenv("PINECONE_API_KEY"))
pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY"))
index = pc.Index("perspective")


Expand Down
3 changes: 2 additions & 1 deletion backend/app/modules/chat/llm_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from groq import Groq
from dotenv import load_dotenv
from app.logging.logging_config import setup_logger
from app.llm_config import LLM_MODEL

logger = setup_logger(__name__)

Expand Down Expand Up @@ -55,7 +56,7 @@ def ask_llm(question, docs):
"""

response = client.chat.completions.create(
model="gemma2-9b-it",
model=LLM_MODEL,
messages=[
{"role": "system", "content": "Use only the context to answer."},
{"role": "user", "content": prompt},
Expand Down
166 changes: 166 additions & 0 deletions backend/app/modules/fact_check_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""
fact checking tool node implementation to replace the google search

"""

import os
import json
import asyncio
from groq import Groq
from langchain_community.tools import DuckDuckGoSearchRun
from app.logging.logging_config import setup_logger
from dotenv import load_dotenv
from app.llm_config import LLM_MODEL

load_dotenv()

client = Groq(api_key=os.getenv("GROQ_API_KEY"))
search_tool = DuckDuckGoSearchRun()

logger = setup_logger(__name__)

async def extract_claims_node(state):
logger.info("--- Fact Check Step 1: Extracting Claims ---")
try:
text = state.get("cleaned_text", "")

response = await asyncio.to_thread(
client.chat.completions.create,
messages=[
{
"role": "system",
"content": "Extract specific, verifiable factual claims from the text. Ignore opinions. Return a simple list of strings, one per line."
},
{"role": "user", "content": text[:4000]}
],
model=LLM_MODEL,
temperature=0.0
)

raw_content = response.choices[0].message.content

claims = [line.strip("- *") for line in raw_content.split("\n") if len(line.strip()) > 10]

logger.info(f"Extracted {len(claims)} claims.")
return {"claims": claims}

except Exception as e:
logger.error(f"Error extraction claims: {e}")
return {"claims": []}

async def plan_searches_node(state):
logger.info("--- Fact Check Step 2: Planning Searches ---")
claims = state.get("claims", [])

if not claims:
return {"search_queries": []}

claims_text = "\n".join([f"{i}. {c}" for i, c in enumerate(claims)])

prompt = f"""
You are a search query generator.
For each claim, generate a search query to verify it.

Output MUST be valid JSON in this format:
{{
"searches": [
{{"query": "search query 1", "claim_id": 0}},
{{"query": "search query 2", "claim_id": 1}}
]
}}

Claims:
{claims_text}
"""

try:
response = await asyncio.to_thread(
client.chat.completions.create,
messages=[{"role": "user", "content": prompt}],
model=LLM_MODEL,
temperature=0.0,
response_format={"type": "json_object"}
)

plan_json = json.loads(response.choices[0].message.content)
queries = plan_json.get("searches", [])

return {"search_queries": queries}

except Exception as e:
logger.error(f"Failed to plan searches: {e}")
return {"search_queries": []}

async def execute_searches_node(state):
logger.info("--- Fact Check Step 3: Executing Parallel Searches ---")
queries = state.get("search_queries", [])

if not queries:
return {"search_results": []}

async def run_one_search(q):
try:
query_str = q.get("query")
c_id = q.get("claim_id")

res = await asyncio.to_thread(search_tool.invoke, query_str)
logger.info(f"Search Result for Claim {c_id}: {res[:200]}...")
return {"claim_id": c_id, "result": res}
except Exception as e:
return {"claim_id": q.get("claim_id"), "result": "Search failed"}
Comment on lines +101 to +110
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if file exists and read lines 101-110
if [ -f "backend/app/modules/fact_check_tool.py" ]; then
  echo "=== Lines 101-110 ==="
  sed -n '101,110p' backend/app/modules/fact_check_tool.py
  echo ""
  echo "=== Context around lines 101-110 (95-115) ==="
  sed -n '95,115p' backend/app/modules/fact_check_tool.py
else
  echo "File not found at backend/app/modules/fact_check_tool.py"
  find . -name "fact_check_tool.py" -type f
fi

Repository: AOSSIE-Org/Perspective

Length of output: 1382


Remove unused exception variable in search execution.

The exception variable e is bound but never used in the except clause. Drop the binding:

Minimal fix
-        except Exception as e:
+        except Exception:
             return {"claim_id": q.get("claim_id"), "result": "Search failed"}
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 108-108: Consider moving this statement to an else block

(TRY300)


[warning] 109-109: Do not catch blind exception: Exception

(BLE001)


[error] 109-109: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🤖 Prompt for AI Agents
In `@backend/app/modules/fact_check_tool.py` around lines 101 - 110, In
run_one_search, remove the unused exception binding by changing the except
clause from "except Exception as e:" to "except Exception:" so the variable e is
not declared; locate the async def run_one_search(q) function and update only
the except line to drop the unused exception variable while preserving the
existing return behavior.


results = await asyncio.gather(*[run_one_search(q) for q in queries])

logger.info(f"Completed {len(results)} searches.")
return {"search_results": results}

async def verify_facts_node(state):
logger.info("--- Fact Check Step 4: Verifying Facts ---")
claims = state.get("claims", [])
results = state.get("search_results", [])

if not claims:
return {"facts": [], "fact_check_done": True}

context = "Verify these claims based on the search results:\n"
for item in results:
c_id = item["claim_id"]
if c_id < len(claims):
context += f"\nClaim: {claims[c_id]}\nEvidence: {item['result']}\n"

try:
response = await asyncio.to_thread(
client.chat.completions.create,
messages=[
{
"role": "system",
"content": "You are a strict fact checker. Return a JSON list of objects with keys: 'claim', 'status' (True/False/Unverified), and 'reason'."
},
{"role": "user", "content": context}
],
model=LLM_MODEL,
temperature=0.0,
response_format={"type": "json_object"}
Comment on lines +125 to +143
Copy link
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:

# Check which client is being imported in the file
head -50 backend/app/modules/fact_check_tool.py | grep -E "^import|^from"

Repository: AOSSIE-Org/Perspective

Length of output: 302


🏁 Script executed:

# Look for the client initialization to confirm it's OpenAI
grep -n "client\s*=" backend/app/modules/fact_check_tool.py | head -5

Repository: AOSSIE-Org/Perspective

Length of output: 117


🏁 Script executed:

# Check the imports at the top of the file to see which library is used
cat backend/app/modules/fact_check_tool.py | head -30

Repository: AOSSIE-Org/Perspective

Length of output: 805


🌐 Web query:

OpenAI API response_format json_object top-level array support JSON schema

💡 Result:

  • response_format: { "type": "json_object" } (“JSON mode”) is the older mode that OpenAI documents as a JSON object response format and discusses in terms of producing/validating a “JSON object” (including edge cases where it may be an incomplete JSON object). Practically: don’t rely on it for top‑level arrays; treat it as “top-level object expected.” [1], [2]

  • If you need a top-level array, use Structured Outputs:
    response_format: { "type": "json_schema", "json_schema": { ... } }. With this you can set your schema’s root to { "type": "array", ... } (or anything else allowed), and with strict: true the model is constrained to match the supplied schema (noting only a subset of JSON Schema is supported in strict mode). [1], [3]

Sources:
[1] OpenAI API Reference — Responses: response_format (json_object vs json_schema) (platform.openai.com)
[2] OpenAI Help Center — JSON mode notes (response_format: json_object) (help.openai.com)
[3] OpenAI — “Introducing Structured Outputs in the API” (JSON Schema via response_format, strict: true, limitations) (openai.com)

Citations:


🌐 Web query:

Groq API response_format json_object specification documentation

💡 Result:

Groq API response_format: {"type":"json_object"} (JSON Object Mode)

Where it’s documented

  • Groq Chat Completions API (POST /openai/v1/chat/completions) supports a response_format object; setting it to {"type":"json_object"} enables the “older JSON mode,” which ensures the model’s message content is valid JSON syntax. [1]
  • Groq’s “Structured Outputs” docs describe JSON Object Mode as JSON-syntax validation only (no schema enforcement) and recommend json_schema when you need schema guarantees. [2]
  • Groq Community FAQ reiterates usage: response_format={"type":"json_object"}. [3]

Specification / behavior

  • Request shape:
    response_format is an object with at least:
    • type: "json_object" [1][2]
  • Guarantee: valid JSON syntax (not schema adherence). [1][2]
  • Recommendation: prefer {"type":"json_schema", "json_schema": {...}} on supported models for schema-conformant outputs. [1][2]
  • Important requirement: include explicit instructions in your prompt telling the model to output JSON (e.g., “Respond with a JSON object only”). [2]

Minimal example (Chat Completions)

{
  "model": "llama-3.3-70b-versatile",
  "messages": [
    { "role": "system", "content": "Return ONLY a JSON object with keys: answer (string)." },
    { "role": "user", "content": "What is 2+2?" }
  ],
  "response_format": { "type": "json_object" }
}

Sources
[1] Groq API Reference (Chat Completions response_format) (console.groq.com)
[2] Groq Docs — Structured Outputs (JSON Object Mode section) (console.groq.com)
[3] Groq Community FAQ — JSON mode (community.groq.com)

Citations:


🌐 Web query:

Groq API json_object top-level array response support

💡 Result:

Groq’s response_format: { "type": "json_object" } (“JSON mode”) is intended to produce a top-level JSON object, not a bare array—so a response like [...] is not supported/guaranteed in json_object mode. (console.groq.com)

If you need a top-level array, use Structured Outputs (response_format.type = "json_schema") and set the root schema to { "type": "array", ... } (arrays are supported by Structured Outputs). (console.groq.com)

Workaround if you must stay on json_object: wrap the array in an object, e.g. { "items": [ ... ] }. (console.groq.com)

Citations:


Align prompt with response_format to avoid invalid JSON.

The prompt asks for a top‑level list, but response_format={"type":"json_object"} requires an object. Groq's JSON mode does not support bare arrays at the top level. Wrap the array in an object with a facts key, or use json_schema format if available.

✅ Prompt aligned to object response
                 {
                     "role": "system", 
-                    "content": "You are a strict fact checker. Return a JSON list of objects with keys: 'claim', 'status' (True/False/Unverified), and 'reason'."
+                    "content": "You are a strict fact checker. Return a JSON object with a 'facts' array. Each item must have keys: 'claim', 'status' (True/False/Unverified), and 'reason'."
                 },
🤖 Prompt for AI Agents
In `@backend/app/modules/fact_check_tool.py` around lines 125 - 143, The prompt
asks the model to return a top-level JSON list but the call to
client.chat.completions.create uses response_format={"type":"json_object"},
which cannot parse bare arrays; update the prompt or the response_format so they
match: either change the system/user prompt (the context string built in this
block and sent to client.chat.completions.create) to request an object like
{"facts": [...]} instead of a bare list, or switch response_format to a
schema-based JSON format (e.g., json_schema) that allows an array top-level;
make this change where the completion is invoked (client.chat.completions.create
/ LLM_MODEL / response_format) so the model output and parser align.

)

final_verdict_str = response.choices[0].message.content

data = json.loads(final_verdict_str)

facts_list = []
if isinstance(data, dict):
# Look for common keys if wrapped
if "facts" in data:
facts_list = data["facts"]
elif "verified_claims" in data:
facts_list = data["verified_claims"]
else:
facts_list = [data]
elif isinstance(data, list):
facts_list = data

return {"facts": facts_list, "fact_check_done": True}

except Exception as e:
logger.error(f"Verification failed: {e}")
return {"facts": [], "fact_check_done": True}
5 changes: 3 additions & 2 deletions backend/app/modules/facts_check/llm_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import json
import re
from app.logging.logging_config import setup_logger
from app.llm_config import LLM_MODEL

logger = setup_logger(__name__)

Expand Down Expand Up @@ -63,7 +64,7 @@ def run_claim_extractor_sdk(state):
),
},
],
model="gemma2-9b-it",
model=LLM_MODEL,
temperature=0.3,
max_tokens=512,
)
Expand Down Expand Up @@ -128,7 +129,7 @@ def run_fact_verifier_sdk(search_results):
),
},
],
model="gemma2-9b-it",
model=LLM_MODEL,
temperature=0.3,
max_tokens=256,
)
Expand Down
39 changes: 19 additions & 20 deletions backend/app/modules/langgraph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
and retry logic.

Workflow:
1. Sentiment analysis on the cleaned text.
2. Fact-checking detected claims.
3. Generating a counter-perspective.
4. Judging the quality of the generated perspective.
5. Storing results and sending them downstream.
6. Error handling at any step if failures occur.
1. Parallel analysis: sentiment analysis and fact checking tool pipeline
(extract_claims -> plan_searches -> execute_searches -> verify_facts)
2. Generating a counter-perspective.
3. Judging the quality of the generated perspective.
4. Storing results and sending.
5. Error handling at any step if failures occur.

Core Features:
- Uses a TypedDict (`MyState`) to define the shape of the pipeline's
Expand All @@ -31,50 +31,48 @@
"""


from typing import List, Any
from langgraph.graph import StateGraph
from typing_extensions import TypedDict

from app.modules.langgraph_nodes import (
sentiment,
fact_check,
generate_perspective,
judge,
store_and_send,
error_handler,
)

from typing_extensions import TypedDict


class MyState(TypedDict):
cleaned_text: str
facts: list[dict]
sentiment: str
perspective: str
short_title: str
score: int
retries: int
status: str
claims: List[str]
search_queries: List[Any]
search_results: List[Any]


def build_langgraph():
graph = StateGraph(MyState)

graph.add_node("sentiment_analysis", sentiment.run_sentiment_sdk)
graph.add_node("fact_checking", fact_check.run_fact_check)
# parallel analysis runs sentiment and fact_check tool pipeline in parallel
graph.add_node("parallel_analysis", sentiment.run_parallel_analysis)

graph.add_node("generate_perspective", generate_perspective.generate_perspective)
graph.add_node("judge_perspective", judge.judge_perspective)
graph.add_node("store_and_send", store_and_send.store_and_send)
graph.add_node("error_handler", error_handler.error_handler)

graph.set_entry_point(
"sentiment_analysis",
)
graph.set_entry_point("parallel_analysis")

graph.add_conditional_edges(
"sentiment_analysis",
lambda x: ("error_handler" if x.get("status") == "error" else "fact_checking"),
)

graph.add_conditional_edges(
"fact_checking",
"parallel_analysis",
lambda x: (
"error_handler" if x.get("status") == "error" else "generate_perspective"
),
Expand All @@ -101,6 +99,7 @@ def build_langgraph():
else "store_and_send"
),
)

graph.add_conditional_edges(
"store_and_send",
lambda x: ("error_handler" if x.get("status") == "error" else "__end__"),
Expand Down
31 changes: 17 additions & 14 deletions backend/app/modules/langgraph_nodes/generate_perspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,24 @@


from app.utils.prompt_templates import generation_prompt
from app.llm_config import LLM_MODEL
from langchain_groq import ChatGroq
from pydantic import BaseModel, Field
from app.logging.logging_config import setup_logger

from typing import List
logger = setup_logger(__name__)


prompt = generation_prompt


class PerspectiveOutput(BaseModel):
reasoning: str = Field(..., description="Chain-of-thought reasoning steps")
short_title: str = Field(..., description="A catchy, concise title for this analysis (max 10 words)")
reasoning: List[str] = Field(description="Chain-of-thought reasoning steps", alias="reasoning_steps")
perspective: str = Field(..., description="Generated opposite perspective")


my_llm = "llama-3.3-70b-versatile"
my_llm = LLM_MODEL

llm = ChatGroq(model=my_llm, temperature=0.7)

Expand All @@ -56,17 +58,18 @@ def generate_perspective(state):

if not text:
raise ValueError("Missing or empty 'cleaned_text' in state")
elif not facts:
raise ValueError("Missing or empty 'facts' in state")

facts_str = "\n".join(
[
f"Claim: {f['original_claim']}\n"
"Verdict: {f['verdict']}\nExplanation: "
"{f['explanation']}"
for f in state["facts"]
]
)
if not facts:
logger.warning("No facts found in state. Generating perspective based on text only.")
facts_str = "No specific claims verified."
else:
facts_str = "\n".join(
[
f"Claim: {f.get('claim', f.get('original_claim', 'Unknown Claim'))}\n"
f"Verdict: {f.get('status', f.get('verdict', 'Unknown Verdict'))}\n"
f"Explanation: {f.get('reason', f.get('explanation', 'No explanation'))}"
for f in facts
]
)

result = chain.invoke(
{
Expand Down
Loading