Commit
·
c83cd7e
0
Parent(s):
initial commit
Browse files- .gitignore +56 -0
- README.md +108 -0
- backend/api/claims.py +79 -0
- backend/api/fact_check.py +216 -0
- backend/api/tone_intent.py +82 -0
- backend/api/verdict.py +56 -0
- backend/config.py +17 -0
- backend/langchain_tools.py +110 -0
- backend/langchain_tools1.py +114 -0
- backend/main.py +61 -0
- frontend/index.html +802 -0
- requirements.txt +0 -0
- test.py +7 -0
.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / cache
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
*.so
|
| 7 |
+
#assets
|
| 8 |
+
assets/
|
| 9 |
+
# Virtual environment
|
| 10 |
+
venv/
|
| 11 |
+
.env/
|
| 12 |
+
.venv/
|
| 13 |
+
ENV/
|
| 14 |
+
env/
|
| 15 |
+
*.env
|
| 16 |
+
|
| 17 |
+
# Environment & secrets
|
| 18 |
+
.env
|
| 19 |
+
*.env.*
|
| 20 |
+
*.secret
|
| 21 |
+
secrets/
|
| 22 |
+
|
| 23 |
+
# PyInstaller
|
| 24 |
+
*.spec
|
| 25 |
+
|
| 26 |
+
# Log files
|
| 27 |
+
*.log
|
| 28 |
+
logs/
|
| 29 |
+
|
| 30 |
+
# Unit test / coverage
|
| 31 |
+
.coverage
|
| 32 |
+
.cache/
|
| 33 |
+
.pytest_cache/
|
| 34 |
+
nosetests.xml
|
| 35 |
+
coverage.xml
|
| 36 |
+
*.cover
|
| 37 |
+
.hypothesis/
|
| 38 |
+
|
| 39 |
+
# IDEs and editors
|
| 40 |
+
.vscode/
|
| 41 |
+
.idea/
|
| 42 |
+
*.sublime-project
|
| 43 |
+
*.sublime-workspace
|
| 44 |
+
|
| 45 |
+
# OS-generated files
|
| 46 |
+
.DS_Store
|
| 47 |
+
Thumbs.db
|
| 48 |
+
ehthumbs.db
|
| 49 |
+
desktop.ini
|
| 50 |
+
|
| 51 |
+
# Python distribution
|
| 52 |
+
build/
|
| 53 |
+
dist/
|
| 54 |
+
*.egg-info/
|
| 55 |
+
*.egg
|
| 56 |
+
|
README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# Falcon: Fake News Analysis and Language Comprehension for Online Neutrality
|
| 3 |
+
|
| 4 |
+
**Falcon** is an advanced Gen AI-powered system designed to analyze, verify, and classify online claims to promote digital content neutrality. Leveraging state-of-the-art language models, it classifies user-submitted text, detects tone and intent, verifies facts using multiple data sources, and generates an informed verdict about claim credibility.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## Features
|
| 9 |
+
|
| 10 |
+
* **Claim Classification**: Classifies input text into categories like Factual Claim, Opinion, Irrelevant Talk, or Vague/Incomplete.
|
| 11 |
+
* **Tone and Intent Detection**: Analyzes the emotional tone and intent behind the claim (e.g., neutral, persuasive, humorous).
|
| 12 |
+
* **Fact Verification**: Verifies factual claims by searching reliable sources like Google (via Serper API) and Wikipedia.
|
| 13 |
+
* **Verdict Generation**: Produces a clear verdict on the claim’s truthfulness based on gathered evidence and reasoning.
|
| 14 |
+
* **Fallback Language Model**: Uses OpenAI’s GPT-4 as the primary LLM and automatically switches to **Deepseek** as a fallback model to ensure high availability and robustness.
|
| 15 |
+
* **Simple Web Interface**: Easy-to-use frontend using HTML, CSS, and JavaScript for seamless user interaction.
|
| 16 |
+
* **FastAPI Backend**: Fast, lightweight API server handling all processing with asynchronous endpoints.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## Tech Stack
|
| 21 |
+
|
| 22 |
+
* **Backend**: FastAPI (Python)
|
| 23 |
+
* **Frontend**: HTML, CSS, JavaScript
|
| 24 |
+
* **Language Models**: OpenAI GPT-4 (primary), Deepseek (fallback)
|
| 25 |
+
* **APIs**: Serper API (Google Search), Wikipedia API
|
| 26 |
+
* **Prompt Engineering**: Carefully crafted templates for accurate and reliable results
|
| 27 |
+
* **Deployment**: Docker containerization (optional)
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## Project Structure
|
| 32 |
+
|
| 33 |
+
```
|
| 34 |
+
falcon/
|
| 35 |
+
├── backend/
|
| 36 |
+
│ ├── api/
|
| 37 |
+
│ │ ├── claims.py # Claim classification logic
|
| 38 |
+
│ │ ├── fact_check.py # Fact verification logic
|
| 39 |
+
│ │ ├── tone_intent.py # Tone and intent detection logic
|
| 40 |
+
│ ├── langchain_tools.py # LangChain prompt templates and LLM wrappers
|
| 41 |
+
│ ├── config.py # API keys and config variables
|
| 42 |
+
│ └── main.py # FastAPI app entry point
|
| 43 |
+
├── frontend/
|
| 44 |
+
│ ├── index.html # Main UI page
|
| 45 |
+
│ ├── assets/ # CSS, JS, images
|
| 46 |
+
├── Dockerfile # Docker container definition (optional)
|
| 47 |
+
└── README.md # This file
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
## Setup Instructions
|
| 53 |
+
|
| 54 |
+
### Prerequisites
|
| 55 |
+
|
| 56 |
+
* Python 3.9+
|
| 57 |
+
* FastAPI
|
| 58 |
+
* Uvicorn (ASGI server)
|
| 59 |
+
* OpenAI API key
|
| 60 |
+
* Serper API key
|
| 61 |
+
* Deepseek API key (for fallback)
|
| 62 |
+
|
| 63 |
+
### Installation
|
| 64 |
+
|
| 65 |
+
1. Clone the repository:
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
git clone https://github.com/yourusername/falcon.git
|
| 69 |
+
cd falcon
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
2. Create and activate a virtual environment:
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
python -m venv venv
|
| 76 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
3. Install dependencies:
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
pip install -r requirements.txt
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
4. Configure API keys in `backend/config.py`:
|
| 86 |
+
|
| 87 |
+
```python
|
| 88 |
+
OPENAI_API_KEY = "your_openai_api_key"
|
| 89 |
+
SERPER_API_KEY = "your_serper_api_key"
|
| 90 |
+
DEEPSEEK_API_KEY = "your_deepseek_api_key"
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
5. Run the FastAPI server:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
uvicorn backend.main:app --port 8000
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
6. Open your browser and visit `http://localhost:8000` to use the Falcon app.
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## Usage
|
| 104 |
+
|
| 105 |
+
* Enter a claim or statement in the input form.
|
| 106 |
+
* The system classifies the claim, detects tone and intent, performs fact verification if applicable, and returns a detailed verdict with supporting evidence.
|
| 107 |
+
* The fallback model Deepseek is automatically used if GPT-4 is unavailable, ensuring consistent performance.
|
| 108 |
+
|
backend/api/claims.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.prompts import PromptTemplate
|
| 2 |
+
from backend.langchain_tools import llm, deepseek_tool
|
| 3 |
+
from langchain_core.runnables import RunnableSequence
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
|
| 7 |
+
# Prompt Template
|
| 8 |
+
claim_classification_prompt = PromptTemplate.from_template("""
|
| 9 |
+
You are an expert language analyst trained to classify text-based user claims. Your task is to analyze a given piece of text and classify it into one of the following precise categories, based on meaning, structure, and intention:
|
| 10 |
+
|
| 11 |
+
Classify the following text into one of these categories:
|
| 12 |
+
1. Factual Claim – A statement that can be verified or disproven using evidence.
|
| 13 |
+
2. Opinion – A personal belief or viewpoint that cannot be objectively proven.
|
| 14 |
+
3. Misleading Claim – A statement that could mislead or distort facts.
|
| 15 |
+
4. Exaggeration – A statement that overstates facts or makes things seem more dramatic.
|
| 16 |
+
5. Factoid – A trivial, unverifiable claim that seems factual but lacks evidence.
|
| 17 |
+
6. Question – A statement framed as a question, seeking information.
|
| 18 |
+
7. Joke/Hyperbole – A statement made in jest or exaggeration, not to be taken literally.
|
| 19 |
+
8. Testimonial/Personal Experience – A personal account or anecdote.
|
| 20 |
+
9. Propaganda/Manipulative Claim – A claim designed to manipulate public opinion.
|
| 21 |
+
|
| 22 |
+
Text: "{claim}"
|
| 23 |
+
|
| 24 |
+
Respond in JSON format:
|
| 25 |
+
{{
|
| 26 |
+
"category": "<one of the above categories>",
|
| 27 |
+
"reasoning": "<brief justification>"
|
| 28 |
+
}}
|
| 29 |
+
""")
|
| 30 |
+
|
| 31 |
+
# Convert template to runnable chain
|
| 32 |
+
claim_chain: RunnableSequence = claim_classification_prompt | llm
|
| 33 |
+
|
| 34 |
+
# Store the prompt template string for fallback use
|
| 35 |
+
prompt_template_str = claim_classification_prompt.template
|
| 36 |
+
|
| 37 |
+
# Classification Function with fallback to DeepSeek-V3
|
| 38 |
+
def classify_claim(claim_text: str) -> dict:
|
| 39 |
+
try:
|
| 40 |
+
# Try using the primary LLM model (OpenAI)
|
| 41 |
+
result = claim_chain.invoke({"claim": claim_text})
|
| 42 |
+
classification = json.loads(result.content.strip())
|
| 43 |
+
|
| 44 |
+
if 'category' not in classification or 'reasoning' not in classification:
|
| 45 |
+
raise ValueError("Invalid classification format received from the model.")
|
| 46 |
+
|
| 47 |
+
return classification
|
| 48 |
+
|
| 49 |
+
except Exception as e:
|
| 50 |
+
# If LLM fails, use DeepSeek-V3 as a fallback
|
| 51 |
+
try:
|
| 52 |
+
print(f"Error with LLM: {e}. Falling back to DeepSeek-V3.")
|
| 53 |
+
|
| 54 |
+
# Manually construct prompt text
|
| 55 |
+
deepseek_prompt = prompt_template_str.format(claim=claim_text)
|
| 56 |
+
|
| 57 |
+
# Call DeepSeek with input key "input"
|
| 58 |
+
deepseek_result = deepseek_tool.invoke({"input": deepseek_prompt})
|
| 59 |
+
print("Raw DeepSeek Output:", deepseek_result)
|
| 60 |
+
|
| 61 |
+
# Remove markdown-style code block if present
|
| 62 |
+
cleaned_output = re.sub(r"```(?:json)?\s*([\s\S]*?)\s*```", r"\1", deepseek_result.strip())
|
| 63 |
+
|
| 64 |
+
# Parse cleaned JSON
|
| 65 |
+
deepseek_classification = json.loads(cleaned_output)
|
| 66 |
+
|
| 67 |
+
if 'category' not in deepseek_classification or 'reasoning' not in deepseek_classification:
|
| 68 |
+
raise ValueError("Invalid classification format received from DeepSeek-V3.")
|
| 69 |
+
|
| 70 |
+
return deepseek_classification
|
| 71 |
+
|
| 72 |
+
except Exception as fallback_e:
|
| 73 |
+
return {"error": f"An error occurred with both LLM and DeepSeek-V3: {str(fallback_e)}"}
|
| 74 |
+
|
| 75 |
+
# Example usage
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
claim = "modi is prime minister"
|
| 78 |
+
result = classify_claim(claim)
|
| 79 |
+
print(result)
|
backend/api/fact_check.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import requests
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from backend.langchain_tools import llm, deepseek_tool
|
| 6 |
+
import re
|
| 7 |
+
from backend.api.claims import classify_claim
|
| 8 |
+
from backend.api.tone_intent import detect_tone_and_intent
|
| 9 |
+
from langchain.prompts import PromptTemplate
|
| 10 |
+
from langchain_core.runnables import RunnableSequence
|
| 11 |
+
import wikipedia
|
| 12 |
+
import logging
|
| 13 |
+
from bs4 import BeautifulSoup
|
| 14 |
+
from sumy.parsers.plaintext import PlaintextParser
|
| 15 |
+
from sumy.nlp.tokenizers import Tokenizer
|
| 16 |
+
from sumy.summarizers.lsa import LsaSummarizer
|
| 17 |
+
import nltk
|
| 18 |
+
|
| 19 |
+
load_dotenv()
|
| 20 |
+
|
| 21 |
+
SERPER_API_KEY = os.getenv("SERPER_API_KEY")
|
| 22 |
+
USER_AGENT = {"User-Agent": "Mozilla/5.0"}
|
| 23 |
+
|
| 24 |
+
# Initialising requests session to reuse connections
|
| 25 |
+
session = requests.Session()
|
| 26 |
+
session.headers.update(USER_AGENT)
|
| 27 |
+
|
| 28 |
+
# nltk data for tokenization once
|
| 29 |
+
nltk.download('punkt')
|
| 30 |
+
|
| 31 |
+
logging.basicConfig(level=logging.INFO)
|
| 32 |
+
|
| 33 |
+
fact_check_prompt = PromptTemplate.from_template("""
|
| 34 |
+
You are an expert fact-checker. Your task is to determine whether the following claim is true or false based on the information available from external sources (Wikipedia and Google search). The claim has already been classified into categories and analyzed for tone/intent.
|
| 35 |
+
|
| 36 |
+
Claim: "{claim}"
|
| 37 |
+
Classification: "{classification}"
|
| 38 |
+
Tone: "{tone}"
|
| 39 |
+
Intent: "{intent}"
|
| 40 |
+
Wikipedia Evidence: "{wikipedia_evidence}"
|
| 41 |
+
Serper Evidence: "{serper_evidence}"
|
| 42 |
+
|
| 43 |
+
Fact-checking Task:
|
| 44 |
+
- Based on the evidence from Wikipedia and Serper search results, classify the claim as:
|
| 45 |
+
1. True
|
| 46 |
+
2. False
|
| 47 |
+
3. Uncertain (when there is insufficient evidence)
|
| 48 |
+
|
| 49 |
+
Respond in JSON format:
|
| 50 |
+
{{
|
| 51 |
+
"claim": "{claim}",
|
| 52 |
+
"classification": "{classification}",
|
| 53 |
+
"tone": "{tone}",
|
| 54 |
+
"intent": "{intent}",
|
| 55 |
+
"fact_check_result": "<True/False/Uncertain/Misleading/Exaggerated/Partially True/Inconclusive:: >",
|
| 56 |
+
"evidence": "<evidence supporting or contradicting the claim>",
|
| 57 |
+
"sources": [
|
| 58 |
+
{{
|
| 59 |
+
"source": "Wikipedia",
|
| 60 |
+
"url": "<url>"
|
| 61 |
+
}},
|
| 62 |
+
{{
|
| 63 |
+
"source": "Search result",
|
| 64 |
+
"url": "<serper_url>"
|
| 65 |
+
}},
|
| 66 |
+
{{
|
| 67 |
+
"source": "LLM",
|
| 68 |
+
"url": "<llm_inferred_url>"
|
| 69 |
+
}},
|
| 70 |
+
...
|
| 71 |
+
],
|
| 72 |
+
"reasoning": "<reasoning for the fact-checking result>"
|
| 73 |
+
}}
|
| 74 |
+
""")
|
| 75 |
+
|
| 76 |
+
fact_check_chain = fact_check_prompt | llm
|
| 77 |
+
|
| 78 |
+
# Wikipedia Search Function
|
| 79 |
+
def search_wikipedia(query):
|
| 80 |
+
try:
|
| 81 |
+
results = wikipedia.search(query, results=5)
|
| 82 |
+
if not results:
|
| 83 |
+
return {"error": "No results found on Wikipedia."}
|
| 84 |
+
|
| 85 |
+
summaries = []
|
| 86 |
+
for title in results:
|
| 87 |
+
try:
|
| 88 |
+
page = wikipedia.page(title)
|
| 89 |
+
summaries.append({
|
| 90 |
+
"title": title,
|
| 91 |
+
"summary": page.summary,
|
| 92 |
+
"url": page.url
|
| 93 |
+
})
|
| 94 |
+
except wikipedia.exceptions.DisambiguationError as e:
|
| 95 |
+
summaries.append({"error": f"Disambiguation: {str(e)}"})
|
| 96 |
+
except Exception as e:
|
| 97 |
+
summaries.append({"error": str(e)})
|
| 98 |
+
return summaries
|
| 99 |
+
except Exception as e:
|
| 100 |
+
return {"error": str(e)}
|
| 101 |
+
|
| 102 |
+
# Fetch search results from Serper API
|
| 103 |
+
def fetch_search_results(query, sentences_count=3):
|
| 104 |
+
url = "https://google.serper.dev/search"
|
| 105 |
+
headers = {"X-API-KEY": SERPER_API_KEY}
|
| 106 |
+
data = {"q": query, "num": 10}
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
response = session.post(url, json=data, headers=headers)
|
| 110 |
+
response.raise_for_status()
|
| 111 |
+
results = response.json()
|
| 112 |
+
|
| 113 |
+
if not results.get("organic"):
|
| 114 |
+
return "Error: No search results found."
|
| 115 |
+
|
| 116 |
+
sources = []
|
| 117 |
+
for result in results.get("organic", [])[:5]:
|
| 118 |
+
source_url = result.get("url") or result.get("link")
|
| 119 |
+
sources.append(source_url)
|
| 120 |
+
|
| 121 |
+
summary = fetch_and_summarize(sources[0], sentences_count)
|
| 122 |
+
return summary, sources
|
| 123 |
+
except requests.exceptions.RequestException as e:
|
| 124 |
+
return f"Error fetching search results: {e}"
|
| 125 |
+
|
| 126 |
+
# Summarize content from a Webpage
|
| 127 |
+
def fetch_and_summarize(url, sentences_count=3):
|
| 128 |
+
try:
|
| 129 |
+
response = session.get(url, timeout=10)
|
| 130 |
+
response.raise_for_status()
|
| 131 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 132 |
+
paragraphs = soup.find_all("p")
|
| 133 |
+
text = "\n".join([p.get_text() for p in paragraphs])
|
| 134 |
+
|
| 135 |
+
if not text.strip():
|
| 136 |
+
return "Error: No readable content found on the page."
|
| 137 |
+
|
| 138 |
+
return summarize_text(text, sentences_count)
|
| 139 |
+
except requests.exceptions.RequestException as e:
|
| 140 |
+
return f"Error: Failed to fetch webpage ({str(e)})"
|
| 141 |
+
except Exception as e:
|
| 142 |
+
return f"Error: {str(e)}"
|
| 143 |
+
|
| 144 |
+
# Summarize text using LsaSummarizer
|
| 145 |
+
def summarize_text(text, sentences_count=3):
|
| 146 |
+
if not text.strip():
|
| 147 |
+
return "Error: No text provided for summarization."
|
| 148 |
+
|
| 149 |
+
parser = PlaintextParser.from_string(text, Tokenizer("english"))
|
| 150 |
+
summarizer = LsaSummarizer()
|
| 151 |
+
summary = summarizer(parser.document, sentences_count)
|
| 152 |
+
|
| 153 |
+
return " ".join(str(sentence) for sentence in summary)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def fact_check_claim(claim_text: str) -> dict:
|
| 158 |
+
try:
|
| 159 |
+
classification = classify_claim(claim_text)
|
| 160 |
+
tone_intent = detect_tone_and_intent(claim_text)
|
| 161 |
+
|
| 162 |
+
classification_type = classification.get("category", "Unknown")
|
| 163 |
+
tone = tone_intent.get("tone", "Unknown")
|
| 164 |
+
intent = tone_intent.get("intent", "Unknown")
|
| 165 |
+
|
| 166 |
+
wikipedia_results = search_wikipedia(claim_text)
|
| 167 |
+
serper_summary, serper_sources = fetch_search_results(claim_text)
|
| 168 |
+
|
| 169 |
+
wikipedia_evidence = " ".join([r["summary"] for r in wikipedia_results if "summary" in r])
|
| 170 |
+
|
| 171 |
+
sources = []
|
| 172 |
+
|
| 173 |
+
for r in wikipedia_results:
|
| 174 |
+
if "url" in r:
|
| 175 |
+
sources.append({"source": "Wikipedia", "url": r["url"]})
|
| 176 |
+
for url in serper_sources:
|
| 177 |
+
sources.append({"source": "Serper", "url": url})
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
fact_check_result = fact_check_chain.invoke({
|
| 181 |
+
"claim": claim_text,
|
| 182 |
+
"classification": classification_type,
|
| 183 |
+
"tone": tone,
|
| 184 |
+
"intent": intent,
|
| 185 |
+
"wikipedia_evidence": wikipedia_evidence,
|
| 186 |
+
"serper_evidence": serper_summary
|
| 187 |
+
})
|
| 188 |
+
|
| 189 |
+
result = json.loads(fact_check_result.content.strip())
|
| 190 |
+
result["sources"] = sources
|
| 191 |
+
return result
|
| 192 |
+
|
| 193 |
+
except Exception as primary_error:
|
| 194 |
+
logging.warning(f"Primary LLM failed: {primary_error}. Falling back to DeepSeek.")
|
| 195 |
+
|
| 196 |
+
deepseek_prompt = fact_check_prompt.template.format(
|
| 197 |
+
claim=claim_text,
|
| 198 |
+
classification=classification_type,
|
| 199 |
+
tone=tone,
|
| 200 |
+
intent=intent,
|
| 201 |
+
wikipedia_evidence=wikipedia_evidence,
|
| 202 |
+
serper_evidence=serper_summary
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
deepseek_result = deepseek_tool.invoke({"input": deepseek_prompt})
|
| 206 |
+
logging.info(f"Raw DeepSeek Output: {deepseek_result}")
|
| 207 |
+
|
| 208 |
+
cleaned_output = re.sub(r"```(?:json)?\s*([\s\S]*?)\s*```", r"\1", deepseek_result.strip())
|
| 209 |
+
|
| 210 |
+
result = json.loads(cleaned_output)
|
| 211 |
+
result["sources"] = sources
|
| 212 |
+
return result
|
| 213 |
+
|
| 214 |
+
except Exception as final_error:
|
| 215 |
+
logging.error(f"Total failure in fact_check_claim: {final_error}")
|
| 216 |
+
return {"error": f"Fact-checking failed due to: {str(final_error)}"}
|
backend/api/tone_intent.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.prompts import PromptTemplate
|
| 2 |
+
from backend.langchain_tools import llm, deepseek_tool
|
| 3 |
+
from langchain_core.runnables import RunnableSequence
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
logging.basicConfig(level=logging.INFO)
|
| 9 |
+
|
| 10 |
+
# Prompt Template
|
| 11 |
+
tone_intent_prompt = PromptTemplate.from_template("""
|
| 12 |
+
You are an expert language analyst trained to detect the tone and intent of a given piece of text. Your task is to analyze the text and determine both its tone and intent from the following predefined categories:
|
| 13 |
+
|
| 14 |
+
Tone Categories:
|
| 15 |
+
1. Neutral
|
| 16 |
+
2. Sarcastic
|
| 17 |
+
3. Clickbait
|
| 18 |
+
4. Propaganda
|
| 19 |
+
5. Aggressive / Toxic
|
| 20 |
+
6. Persuasive
|
| 21 |
+
7. Humorous
|
| 22 |
+
|
| 23 |
+
Intent Categories:
|
| 24 |
+
1. To Inform
|
| 25 |
+
2. To Persuade
|
| 26 |
+
3. To Deceive
|
| 27 |
+
4. To Express Emotion
|
| 28 |
+
5. To Incite Hate
|
| 29 |
+
6. To Promote Agenda
|
| 30 |
+
|
| 31 |
+
Text: "{text}"
|
| 32 |
+
|
| 33 |
+
Respond in JSON format:
|
| 34 |
+
{{
|
| 35 |
+
"tone": "<one of: Neutral, Sarcastic, Clickbait, Propaganda, Aggressive / Toxic, Persuasive, Humorous>",
|
| 36 |
+
"intent": "<one of: To Inform, To Persuade, To Deceive, To Express Emotion, To Incite Hate, To Promote Agenda>",
|
| 37 |
+
"reasoning": "<brief justification for tone and intent>"
|
| 38 |
+
}}
|
| 39 |
+
""")
|
| 40 |
+
|
| 41 |
+
# RunnableSequence
|
| 42 |
+
tone_intent_chain = tone_intent_prompt | llm
|
| 43 |
+
|
| 44 |
+
# Store the prompt string for fallback
|
| 45 |
+
prompt_template_str = tone_intent_prompt.template
|
| 46 |
+
|
| 47 |
+
# Detection function with fallback to DeepSeek-V3
|
| 48 |
+
def detect_tone_and_intent(text: str) -> dict:
|
| 49 |
+
try:
|
| 50 |
+
result = tone_intent_chain.invoke({"text": text})
|
| 51 |
+
detection = json.loads(result.content.strip())
|
| 52 |
+
|
| 53 |
+
if 'tone' not in detection or 'intent' not in detection or 'reasoning' not in detection:
|
| 54 |
+
logging.error(f"Unexpected response format: {result.content.strip()}")
|
| 55 |
+
return {"error": "Response format is incorrect. Missing required fields."}
|
| 56 |
+
|
| 57 |
+
return detection
|
| 58 |
+
|
| 59 |
+
except Exception as e:
|
| 60 |
+
logging.error(f"Error with primary model (LLM): {str(e)}. Falling back to DeepSeek-V3.")
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
# Create prompt manually for DeepSeek
|
| 64 |
+
deepseek_prompt = prompt_template_str.format(text=text)
|
| 65 |
+
|
| 66 |
+
deepseek_result = deepseek_tool.invoke({"input": deepseek_prompt})
|
| 67 |
+
logging.info(f"Raw DeepSeek Output: {deepseek_result}")
|
| 68 |
+
|
| 69 |
+
cleaned_output = re.sub(r"```(?:json)?\s*([\s\S]*?)\s*```", r"\1", deepseek_result.strip())
|
| 70 |
+
|
| 71 |
+
deepseek_detection = json.loads(cleaned_output)
|
| 72 |
+
|
| 73 |
+
if 'tone' not in deepseek_detection or 'intent' not in deepseek_detection or 'reasoning' not in deepseek_detection:
|
| 74 |
+
logging.error(f"Unexpected format from DeepSeek-V3: {cleaned_output}")
|
| 75 |
+
return {"error": "Response format from DeepSeek-V3 is incorrect. Missing required fields."}
|
| 76 |
+
|
| 77 |
+
return deepseek_detection
|
| 78 |
+
|
| 79 |
+
except Exception as fallback_e:
|
| 80 |
+
logging.error(f"Error with both LLM and DeepSeek-V3: {str(fallback_e)}")
|
| 81 |
+
return {"error": f"An error occurred with both LLM and DeepSeek-V3: {str(fallback_e)}"}
|
| 82 |
+
|
backend/api/verdict.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from backend.api.claims import classify_claim
|
| 2 |
+
from backend.api.tone_intent import detect_tone_and_intent
|
| 3 |
+
from backend.api.fact_check import fact_check_claim
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
# final verdict function
|
| 7 |
+
def get_final_verdict(claim_text: str) -> dict:
|
| 8 |
+
try:
|
| 9 |
+
# Classify the claim
|
| 10 |
+
classification = classify_claim(claim_text)
|
| 11 |
+
classification_type = classification.get("category", "Unknown")
|
| 12 |
+
|
| 13 |
+
# If the claim is not a factual claim, return an early verdict
|
| 14 |
+
if classification_type != "Factual Claim":
|
| 15 |
+
return {
|
| 16 |
+
"claim": claim_text,
|
| 17 |
+
"classification": classification_type,
|
| 18 |
+
"verdict": "Claim is not factual.",
|
| 19 |
+
"reasoning": "The claim does not fall under factual category. It is an opinion, irrelevant, or vague."
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# Detect the tone of the claim
|
| 23 |
+
tone_intent = detect_tone_and_intent(claim_text)
|
| 24 |
+
tone = tone_intent.get("tone", "Unknown")
|
| 25 |
+
|
| 26 |
+
# If the tone is positive (Neutral, Persuasive, Humorous), proceed to fact-check
|
| 27 |
+
if tone in ["Neutral", "Persuasive", "Humorous"]:
|
| 28 |
+
fact_check_result = fact_check_claim(claim_text)
|
| 29 |
+
|
| 30 |
+
# Fact-checking results
|
| 31 |
+
verdict = fact_check_result.get("fact_check_result", "Unknown")
|
| 32 |
+
evidence = fact_check_result.get("evidence", "No evidence available.")
|
| 33 |
+
sources = fact_check_result.get("sources", [])
|
| 34 |
+
reasoning = fact_check_result.get("reasoning", "No reasoning available.")
|
| 35 |
+
|
| 36 |
+
return {
|
| 37 |
+
"claim": claim_text,
|
| 38 |
+
"classification": classification_type,
|
| 39 |
+
"tone": tone,
|
| 40 |
+
"fact_check_result": verdict,
|
| 41 |
+
"evidence": evidence,
|
| 42 |
+
"sources": sources,
|
| 43 |
+
"reasoning": reasoning
|
| 44 |
+
}
|
| 45 |
+
else:
|
| 46 |
+
return {
|
| 47 |
+
"claim": claim_text,
|
| 48 |
+
"classification": classification_type,
|
| 49 |
+
"tone": tone,
|
| 50 |
+
"verdict": "Claim has a non-positive tone.",
|
| 51 |
+
"reasoning": "The claim's tone is considered negative or misleading, hence not processed for fact-checking."
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
except Exception as e:
|
| 55 |
+
return {"error": f"An error occurred while processing the claim: {str(e)}"}
|
| 56 |
+
|
backend/config.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 7 |
+
SERPER_API_KEY = os.getenv("SERPER_API_KEY")
|
| 8 |
+
TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
|
| 9 |
+
|
| 10 |
+
if not OPENAI_API_KEY:
|
| 11 |
+
raise ValueError("OPENAI_API_KEY is not set in environment variables or .env file")
|
| 12 |
+
if not SERPER_API_KEY:
|
| 13 |
+
raise ValueError("SERPER_API_KEY is not set in environment variables or .env file")
|
| 14 |
+
if not TOGETHER_API_KEY:
|
| 15 |
+
raise ValueError("TOGETHER_API_KEY is not set in environment variables or .env file")
|
| 16 |
+
|
| 17 |
+
|
backend/langchain_tools.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_openai import ChatOpenAI
|
| 2 |
+
from langchain.agents import Tool
|
| 3 |
+
from langchain_community.utilities import WikipediaAPIWrapper
|
| 4 |
+
import requests
|
| 5 |
+
import os
|
| 6 |
+
from backend.config import OPENAI_API_KEY, SERPER_API_KEY
|
| 7 |
+
|
| 8 |
+
llm = ChatOpenAI(
|
| 9 |
+
model="gpt-3.5-turbo",
|
| 10 |
+
temperature=0,
|
| 11 |
+
openai_api_key=OPENAI_API_KEY
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
import wikipedia
|
| 16 |
+
from langchain.tools import Tool
|
| 17 |
+
|
| 18 |
+
def search_wikipedia(query):
|
| 19 |
+
try:
|
| 20 |
+
results = wikipedia.search(query, results=10) # Fetch top 10 results
|
| 21 |
+
if not results:
|
| 22 |
+
return {"error": "No results found on Wikipedia."}
|
| 23 |
+
|
| 24 |
+
summaries = []
|
| 25 |
+
for title in results:
|
| 26 |
+
try:
|
| 27 |
+
page = wikipedia.page(title)
|
| 28 |
+
summary = page.summary
|
| 29 |
+
url = page.url
|
| 30 |
+
summaries.append({"title": title, "summary": summary, "url": url})
|
| 31 |
+
except wikipedia.exceptions.DisambiguationError as e:
|
| 32 |
+
summaries.append({"error": f"Disambiguation: {str(e)}"})
|
| 33 |
+
except wikipedia.exceptions.HTTPTimeoutError:
|
| 34 |
+
summaries.append({"error": "Wikipedia request timed out."})
|
| 35 |
+
except Exception as e:
|
| 36 |
+
summaries.append({"error": str(e)})
|
| 37 |
+
return summaries
|
| 38 |
+
except Exception as e:
|
| 39 |
+
return {"error": str(e)}
|
| 40 |
+
|
| 41 |
+
# Define the Wikipedia Tool
|
| 42 |
+
wiki_tool = Tool.from_function(
|
| 43 |
+
func=search_wikipedia,
|
| 44 |
+
name="WikipediaAPI",
|
| 45 |
+
description="Fetch summaries from Wikipedia based on search query"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
from langchain.tools import Tool
|
| 50 |
+
|
| 51 |
+
def fetch_search_results(query):
|
| 52 |
+
url = "https://google.serper.dev/search"
|
| 53 |
+
headers = {
|
| 54 |
+
"X-API-KEY": SERPER_API_KEY,
|
| 55 |
+
"Content-Type": "application/json"
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
data = {
|
| 59 |
+
"q": query,
|
| 60 |
+
"num": 10
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
response = requests.post(url, json=data, headers=headers)
|
| 65 |
+
if response.status_code == 200:
|
| 66 |
+
return response.json()
|
| 67 |
+
else:
|
| 68 |
+
return {"error": f"Serper API error: {response.status_code}"}
|
| 69 |
+
except requests.exceptions.RequestException as e:
|
| 70 |
+
return {"error": f"Request failed: {str(e)}"}
|
| 71 |
+
|
| 72 |
+
# Define the Serper Tool
|
| 73 |
+
serper_tool = Tool.from_function(
|
| 74 |
+
func=fetch_search_results,
|
| 75 |
+
name="SerperAPI",
|
| 76 |
+
description="Search the web using the Serper API"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
from together import Together
|
| 81 |
+
from backend.config import TOGETHER_API_KEY
|
| 82 |
+
|
| 83 |
+
client = Together(api_key=TOGETHER_API_KEY)
|
| 84 |
+
|
| 85 |
+
def deepseek_v3_query(query):
|
| 86 |
+
try:
|
| 87 |
+
response = client.chat.completions.create(
|
| 88 |
+
model="deepseek-ai/DeepSeek-V3", # DeepSeek-V3 model from Together
|
| 89 |
+
messages=[{"role": "user", "content": query}],
|
| 90 |
+
stream=True
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
result = ""
|
| 94 |
+
for token in response:
|
| 95 |
+
if hasattr(token, 'choices'):
|
| 96 |
+
result += token.choices[0].delta.content
|
| 97 |
+
return result
|
| 98 |
+
except Exception as e:
|
| 99 |
+
return {"error": f"DeepSeek-V3 API error: {str(e)}"}
|
| 100 |
+
|
| 101 |
+
# Define the DeepSeek-V3 Tool
|
| 102 |
+
deepseek_tool = Tool.from_function(
|
| 103 |
+
func=deepseek_v3_query,
|
| 104 |
+
name="DeepSeekV3",
|
| 105 |
+
description="Query DeepSeek-V3 AI model from Together for advanced text completions"
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# 5. Group tools together
|
| 110 |
+
search_tools = [wiki_tool, serper_tool, deepseek_tool]
|
backend/langchain_tools1.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_openai import ChatOpenAI # Corrected import
|
| 2 |
+
from langchain.agents import Tool
|
| 3 |
+
from langchain_community.utilities import WikipediaAPIWrapper
|
| 4 |
+
import requests
|
| 5 |
+
import os
|
| 6 |
+
from backend.config import OPENAI_API_KEY, SERPER_API_KEY
|
| 7 |
+
|
| 8 |
+
# 1. Setup OpenAI Chat Model (using GPT-4)
|
| 9 |
+
llm = ChatOpenAI(
|
| 10 |
+
model="gpt-3.5-turbo",
|
| 11 |
+
temperature=0,
|
| 12 |
+
openai_api_key=OPENAI_API_KEY
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# 2. Wikipedia Tool for Fact-Checking
|
| 16 |
+
import wikipedia
|
| 17 |
+
from langchain.tools import Tool
|
| 18 |
+
|
| 19 |
+
def search_wikipedia(query):
|
| 20 |
+
try:
|
| 21 |
+
results = wikipedia.search(query, results=10) # Fetch top 10 results
|
| 22 |
+
if not results:
|
| 23 |
+
return {"error": "No results found on Wikipedia."}
|
| 24 |
+
|
| 25 |
+
summaries = []
|
| 26 |
+
for title in results:
|
| 27 |
+
try:
|
| 28 |
+
page = wikipedia.page(title)
|
| 29 |
+
summary = page.summary
|
| 30 |
+
url = page.url
|
| 31 |
+
summaries.append({"title": title, "summary": summary, "url": url})
|
| 32 |
+
except wikipedia.exceptions.DisambiguationError as e:
|
| 33 |
+
summaries.append({"error": f"Disambiguation: {str(e)}"})
|
| 34 |
+
except wikipedia.exceptions.HTTPTimeoutError:
|
| 35 |
+
summaries.append({"error": "Wikipedia request timed out."})
|
| 36 |
+
except Exception as e:
|
| 37 |
+
summaries.append({"error": str(e)})
|
| 38 |
+
return summaries
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return {"error": str(e)}
|
| 41 |
+
|
| 42 |
+
# Define the Wikipedia Tool
|
| 43 |
+
wiki_tool = Tool.from_function(
|
| 44 |
+
func=search_wikipedia,
|
| 45 |
+
name="WikipediaAPI",
|
| 46 |
+
description="Fetch summaries from Wikipedia based on search query"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# 3. Serper Search Tool (Google-like Search)
|
| 51 |
+
from langchain.tools import Tool
|
| 52 |
+
|
| 53 |
+
def fetch_search_results(query):
|
| 54 |
+
url = "https://google.serper.dev/search"
|
| 55 |
+
headers = {
|
| 56 |
+
"X-API-KEY": SERPER_API_KEY,
|
| 57 |
+
"Content-Type": "application/json"
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
data = {
|
| 61 |
+
"q": query,
|
| 62 |
+
"num": 10 # Get 10 results
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
response = requests.post(url, json=data, headers=headers)
|
| 67 |
+
if response.status_code == 200:
|
| 68 |
+
return response.json()
|
| 69 |
+
else:
|
| 70 |
+
return {"error": f"Serper API error: {response.status_code}"}
|
| 71 |
+
except requests.exceptions.RequestException as e:
|
| 72 |
+
return {"error": f"Request failed: {str(e)}"}
|
| 73 |
+
|
| 74 |
+
# Define the Serper Tool
|
| 75 |
+
serper_tool = Tool.from_function(
|
| 76 |
+
func=fetch_search_results,
|
| 77 |
+
name="SerperAPI",
|
| 78 |
+
description="Search the web using the Serper API"
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# 4. DeepSeek-V3 Tool (Integration with Together API)
|
| 83 |
+
from together import Together
|
| 84 |
+
from backend.config import TOGETHER_API_KEY # Ensure you have this in your .env or config file
|
| 85 |
+
|
| 86 |
+
# Initialize Together client with the API key
|
| 87 |
+
client = Together(api_key=TOGETHER_API_KEY)
|
| 88 |
+
|
| 89 |
+
def deepseek_v3_query(query):
|
| 90 |
+
try:
|
| 91 |
+
response = client.chat.completions.create(
|
| 92 |
+
model="deepseek-ai/DeepSeek-V3", # DeepSeek-V3 model from Together
|
| 93 |
+
messages=[{"role": "user", "content": query}],
|
| 94 |
+
stream=True
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
result = ""
|
| 98 |
+
for token in response:
|
| 99 |
+
if hasattr(token, 'choices'):
|
| 100 |
+
result += token.choices[0].delta.content
|
| 101 |
+
return result
|
| 102 |
+
except Exception as e:
|
| 103 |
+
return {"error": f"DeepSeek-V3 API error: {str(e)}"}
|
| 104 |
+
|
| 105 |
+
# Define the DeepSeek-V3 Tool
|
| 106 |
+
deepseek_tool = Tool.from_function(
|
| 107 |
+
func=deepseek_v3_query,
|
| 108 |
+
name="DeepSeekV3",
|
| 109 |
+
description="Query DeepSeek-V3 AI model from Together for advanced text completions"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# 5. Group tools together
|
| 114 |
+
search_tools = [wiki_tool, serper_tool, deepseek_tool]
|
backend/main.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from fastapi import FastAPI, Form, Request
|
| 3 |
+
from fastapi.responses import HTMLResponse
|
| 4 |
+
from fastapi.templating import Jinja2Templates
|
| 5 |
+
from backend.api.fact_check import fact_check_claim
|
| 6 |
+
from backend.api.claims import classify_claim
|
| 7 |
+
from backend.api.tone_intent import detect_tone_and_intent
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
app = FastAPI()
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# Jinja2 templates for rendering HTML pages
|
| 15 |
+
templates = Jinja2Templates(directory="frontend")
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from fastapi.staticfiles import StaticFiles
|
| 18 |
+
|
| 19 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 20 |
+
STATIC_DIR = BASE_DIR.parent / "frontend" / "assets"
|
| 21 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@app.get("/", response_class=HTMLResponse)
|
| 25 |
+
async def read_root(request: Request):
|
| 26 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
| 27 |
+
|
| 28 |
+
@app.post("/process_claim")
|
| 29 |
+
async def process_claim(claim: str = Form(...)):
|
| 30 |
+
# Classify the claim
|
| 31 |
+
classification = classify_claim(claim)
|
| 32 |
+
classification_type = classification.get("category", "Unknown")
|
| 33 |
+
|
| 34 |
+
# Detect the tone and intent of the claim
|
| 35 |
+
tone_intent = detect_tone_and_intent(claim)
|
| 36 |
+
tone = tone_intent.get("tone", "Unknown")
|
| 37 |
+
intent = tone_intent.get("intent", "Unknown")
|
| 38 |
+
|
| 39 |
+
# fact-check the claim if it's factual and tone is positive
|
| 40 |
+
if classification_type in ["Factual Claim", "Misleading Claim", "Factoid"] and tone in ["Neutral", "Persuasive", "Humorous"]:
|
| 41 |
+
fact_check_result = fact_check_claim(claim)
|
| 42 |
+
verdict = fact_check_result.get("fact_check_result", "Unknown")
|
| 43 |
+
evidence = fact_check_result.get("evidence", "No evidence available.")
|
| 44 |
+
sources = fact_check_result.get("sources", [])
|
| 45 |
+
reasoning = fact_check_result.get("reasoning", "No reasoning available.")
|
| 46 |
+
else:
|
| 47 |
+
verdict = "Not Fact-Checked"
|
| 48 |
+
evidence = "Claim is either not factual or has a non-positive tone."
|
| 49 |
+
sources = []
|
| 50 |
+
reasoning = "The claim either isn’t factual or has a misleading tone, hence skipped from fact-checking."
|
| 51 |
+
|
| 52 |
+
return {
|
| 53 |
+
"claim": claim,
|
| 54 |
+
"classification": classification_type,
|
| 55 |
+
"tone": tone,
|
| 56 |
+
"intent": intent,
|
| 57 |
+
"fact_check_result": verdict,
|
| 58 |
+
"evidence": evidence,
|
| 59 |
+
"sources": sources,
|
| 60 |
+
"reasoning": reasoning
|
| 61 |
+
}
|
frontend/index.html
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<html lang="en">
|
| 2 |
+
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="description"
|
| 6 |
+
content="FALCON is an AI-powered tool for verifying the authenticity of claims. Check if a claim is factual, misleading, or opinion-based using trusted data sources.">
|
| 7 |
+
<meta name="keywords"
|
| 8 |
+
content="AI fact-checking, claim verification, fake news detection, NLP models, fact-checking tool, verify claims">
|
| 9 |
+
|
| 10 |
+
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
| 11 |
+
<title>FALCON - Fake news Analysis and Language Comprehension for Online Neutrality </title>
|
| 12 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 13 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet" />
|
| 14 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet" />
|
| 15 |
+
<style>
|
| 16 |
+
html {
|
| 17 |
+
scroll-behavior: smooth;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
font-family: "Inter", sans-serif;
|
| 22 |
+
background-color: #0c1a36;
|
| 23 |
+
/* dark blue background */
|
| 24 |
+
color: #ffffff;
|
| 25 |
+
/* white text */
|
| 26 |
+
min-height: 100vh;
|
| 27 |
+
display: flex;
|
| 28 |
+
flex-direction: column;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/* Fade-in animation */
|
| 32 |
+
.fade-in {
|
| 33 |
+
animation: fadeIn 0.8s ease forwards;
|
| 34 |
+
opacity: 0;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
@keyframes fadeIn {
|
| 38 |
+
to {
|
| 39 |
+
opacity: 1;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Stylish button hover and focus */
|
| 44 |
+
button,
|
| 45 |
+
a.button-link {
|
| 46 |
+
position: relative;
|
| 47 |
+
overflow: hidden;
|
| 48 |
+
transition: color 0.3s ease;
|
| 49 |
+
z-index: 0;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
button::before,
|
| 53 |
+
a.button-link::before {
|
| 54 |
+
content: "";
|
| 55 |
+
position: absolute;
|
| 56 |
+
top: 50%;
|
| 57 |
+
left: 50%;
|
| 58 |
+
width: 0;
|
| 59 |
+
height: 300%;
|
| 60 |
+
background: #2563eb;
|
| 61 |
+
transform: translate(-50%, -50%) rotate(45deg);
|
| 62 |
+
transition: width 0.4s ease;
|
| 63 |
+
z-index: -1;
|
| 64 |
+
border-radius: 12px;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
button:hover::before,
|
| 68 |
+
button:focus::before,
|
| 69 |
+
a.button-link:hover::before,
|
| 70 |
+
a.button-link:focus::before {
|
| 71 |
+
width: 250%;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
button:hover,
|
| 75 |
+
button:focus,
|
| 76 |
+
a.button-link:hover,
|
| 77 |
+
a.button-link:focus {
|
| 78 |
+
color: #dbeafe;
|
| 79 |
+
outline: none;
|
| 80 |
+
box-shadow: 0 0 12px #3b82f6;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/* Input and textarea focus glow */
|
| 84 |
+
input:focus,
|
| 85 |
+
textarea:focus {
|
| 86 |
+
box-shadow: 0 0 8px #3b82f6;
|
| 87 |
+
border-color: #3b82f6;
|
| 88 |
+
background-color: #142a5c;
|
| 89 |
+
color: #ffffff;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/* Scrollbar styling for result panel */
|
| 93 |
+
.result-scrollbar::-webkit-scrollbar {
|
| 94 |
+
height: 6px;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.result-scrollbar::-webkit-scrollbar-track {
|
| 98 |
+
background: #142a5c;
|
| 99 |
+
border-radius: 3px;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.result-scrollbar::-webkit-scrollbar-thumb {
|
| 103 |
+
background: #3b82f6;
|
| 104 |
+
border-radius: 3px;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* Override Tailwind default whites for backgrounds and borders */
|
| 108 |
+
header,
|
| 109 |
+
footer,
|
| 110 |
+
section.bg-white {
|
| 111 |
+
background-color: #142a5c;
|
| 112 |
+
/* darker blue slate */
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
header {
|
| 116 |
+
box-shadow: 0 2px 15px rgb(59 130 246 / 0.6);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
footer {
|
| 120 |
+
border-top-color: #1e3a8a;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
section.bg-white {
|
| 124 |
+
box-shadow: 0 4px 25px rgb(59 130 246 / 0.6);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
input,
|
| 128 |
+
button,
|
| 129 |
+
textarea {
|
| 130 |
+
background-color: #0c1a36;
|
| 131 |
+
border-color: #3b82f6;
|
| 132 |
+
color: #ffffff;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
input::placeholder,
|
| 136 |
+
textarea::placeholder {
|
| 137 |
+
color: #a5b4fc;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Result section styled like other sections */
|
| 141 |
+
#result {
|
| 142 |
+
background-color: #142a5c;
|
| 143 |
+
border-radius: 1rem;
|
| 144 |
+
box-shadow: 0 4px 25px rgb(59 130 246 / 0.6);
|
| 145 |
+
padding: 2.5rem 3rem;
|
| 146 |
+
max-width: 920px;
|
| 147 |
+
margin-left: auto;
|
| 148 |
+
margin-right: auto;
|
| 149 |
+
opacity: 0;
|
| 150 |
+
pointer-events: none;
|
| 151 |
+
transition: opacity 0.5s ease;
|
| 152 |
+
color: #dbeafe;
|
| 153 |
+
font-family: "Inter", sans-serif;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
#result.visible {
|
| 157 |
+
opacity: 1;
|
| 158 |
+
pointer-events: auto;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
#result h2 {
|
| 162 |
+
font-size: 2rem;
|
| 163 |
+
font-weight: 700;
|
| 164 |
+
margin-bottom: 1.5rem;
|
| 165 |
+
color: #bfdbfe;
|
| 166 |
+
text-align: center;
|
| 167 |
+
user-select: none;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
#result p {
|
| 171 |
+
font-size: 1rem;
|
| 172 |
+
line-height: 1.6;
|
| 173 |
+
margin-bottom: 1rem;
|
| 174 |
+
user-select: text;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
#result p span.font-semibold {
|
| 178 |
+
color: #93c5fd;
|
| 179 |
+
font-weight: 600;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* Link styling in result */
|
| 183 |
+
#sources a {
|
| 184 |
+
color: #bfdbfe;
|
| 185 |
+
font-weight: 600;
|
| 186 |
+
transition: color 0.3s ease;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
#sources a:hover,
|
| 190 |
+
#sources a:focus {
|
| 191 |
+
color: #60a5fa;
|
| 192 |
+
outline: none;
|
| 193 |
+
text-decoration: underline;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/* Header nav link underline on hover with animation */
|
| 197 |
+
nav a {
|
| 198 |
+
position: relative;
|
| 199 |
+
padding-bottom: 0.25rem;
|
| 200 |
+
color: #ffffff;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
transition: color 0.3s ease;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
nav a::after {
|
| 206 |
+
content: "";
|
| 207 |
+
position: absolute;
|
| 208 |
+
width: 0%;
|
| 209 |
+
height: 3px;
|
| 210 |
+
bottom: 0;
|
| 211 |
+
left: 0;
|
| 212 |
+
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
| 213 |
+
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 214 |
+
border-radius: 3px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
nav a:hover,
|
| 218 |
+
nav a:focus {
|
| 219 |
+
color: #60a5fa;
|
| 220 |
+
outline: none;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
nav a:hover::after,
|
| 224 |
+
nav a:focus::after {
|
| 225 |
+
width: 100%;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* Loading animation - rotating rings */
|
| 229 |
+
#loadingScreen {
|
| 230 |
+
background-color: #0c1a36;
|
| 231 |
+
display: flex;
|
| 232 |
+
justify-content: center;
|
| 233 |
+
align-items: center;
|
| 234 |
+
z-index: 50;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.loader {
|
| 238 |
+
position: relative;
|
| 239 |
+
width: 64px;
|
| 240 |
+
height: 64px;
|
| 241 |
+
animation: rotate 1.5s linear infinite;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.loader div {
|
| 245 |
+
position: absolute;
|
| 246 |
+
border: 4px solid transparent;
|
| 247 |
+
border-top-color: #3b82f6;
|
| 248 |
+
border-radius: 50%;
|
| 249 |
+
animation: spin 1s ease-in-out infinite;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.loader div:nth-child(1) {
|
| 253 |
+
width: 64px;
|
| 254 |
+
height: 64px;
|
| 255 |
+
top: 0;
|
| 256 |
+
left: 0;
|
| 257 |
+
animation-delay: -0.2s;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.loader div:nth-child(2) {
|
| 261 |
+
width: 48px;
|
| 262 |
+
height: 48px;
|
| 263 |
+
top: 8px;
|
| 264 |
+
left: 8px;
|
| 265 |
+
border-top-color: #60a5fa;
|
| 266 |
+
animation-delay: -0.4s;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.loader div:nth-child(3) {
|
| 270 |
+
width: 32px;
|
| 271 |
+
height: 32px;
|
| 272 |
+
top: 16px;
|
| 273 |
+
left: 16px;
|
| 274 |
+
border-top-color: #93c5fd;
|
| 275 |
+
animation-delay: -0.6s;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
@keyframes rotate {
|
| 279 |
+
100% {
|
| 280 |
+
transform: rotate(360deg);
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
@keyframes spin {
|
| 285 |
+
|
| 286 |
+
0%,
|
| 287 |
+
100% {
|
| 288 |
+
transform: rotate(0deg);
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
50% {
|
| 292 |
+
transform: rotate(180deg);
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/* Form error styling */
|
| 297 |
+
#error {
|
| 298 |
+
color: #f87171;
|
| 299 |
+
font-weight: 600;
|
| 300 |
+
user-select: none;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* Input and button width */
|
| 304 |
+
#formSection form input[type="text"] {
|
| 305 |
+
max-width: 900px;
|
| 306 |
+
width: 100%;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
#formSection form button {
|
| 310 |
+
max-width: 900px;
|
| 311 |
+
width: 100%;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
#formSection {
|
| 315 |
+
max-width: 920px;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/* Responsive adjustments */
|
| 319 |
+
@media (max-width: 1024px) {
|
| 320 |
+
#formSection {
|
| 321 |
+
max-width: 100%;
|
| 322 |
+
padding-left: 1.5rem;
|
| 323 |
+
padding-right: 1.5rem;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
#result {
|
| 327 |
+
max-width: 100%;
|
| 328 |
+
padding-left: 1.5rem;
|
| 329 |
+
padding-right: 1.5rem;
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
/* Loading overlay for analyze button */
|
| 334 |
+
#analyzeLoadingOverlay {
|
| 335 |
+
position: absolute;
|
| 336 |
+
inset: 0;
|
| 337 |
+
background: rgba(14, 42, 94, 0.7);
|
| 338 |
+
border-radius: 0.5rem;
|
| 339 |
+
display: flex;
|
| 340 |
+
justify-content: center;
|
| 341 |
+
align-items: center;
|
| 342 |
+
pointer-events: none;
|
| 343 |
+
opacity: 0;
|
| 344 |
+
transition: opacity 0.3s ease;
|
| 345 |
+
z-index: 10;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
#analyzeLoadingOverlay.visible {
|
| 349 |
+
opacity: 1;
|
| 350 |
+
pointer-events: auto;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
/* Spinner for analyze button */
|
| 354 |
+
.btn-spinner {
|
| 355 |
+
border: 3px solid transparent;
|
| 356 |
+
border-top-color: #60a5fa;
|
| 357 |
+
border-radius: 50%;
|
| 358 |
+
width: 24px;
|
| 359 |
+
height: 24px;
|
| 360 |
+
animation: spinBtn 1s linear infinite;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
@keyframes spinBtn {
|
| 364 |
+
0% {
|
| 365 |
+
transform: rotate(0deg);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
100% {
|
| 369 |
+
transform: rotate(360deg);
|
| 370 |
+
}
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
/* Position relative for button container */
|
| 374 |
+
.button-container {
|
| 375 |
+
position: relative;
|
| 376 |
+
display: inline-block;
|
| 377 |
+
width: 100%;
|
| 378 |
+
}
|
| 379 |
+
</style>
|
| 380 |
+
</head>
|
| 381 |
+
|
| 382 |
+
<body class="flex flex-col min-h-screen">
|
| 383 |
+
<!-- Loading Screen -->
|
| 384 |
+
<div aria-label="Loading animation" class="fixed inset-0 flex justify-center items-center" id="loadingScreen">
|
| 385 |
+
<div class="loader" aria-hidden="true">
|
| 386 |
+
<div></div>
|
| 387 |
+
<div></div>
|
| 388 |
+
<div></div>
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
<!-- Header -->
|
| 393 |
+
<header aria-label="Primary Navigation" class="w-full bg-[#142a5c] shadow-md sticky top-0 z-40" role="banner">
|
| 394 |
+
<nav aria-label="Main navigation"
|
| 395 |
+
class="max-w-7xl mx-auto px-6 sm:px-8 lg:px-12 flex items-center justify-between h-18">
|
| 396 |
+
<a aria-label="FALCON Home"
|
| 397 |
+
class="flex items-center space-x-3 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
| 398 |
+
href="#home">
|
| 399 |
+
<img alt="FALCON logo "
|
| 400 |
+
class="w-50 h-50 rounded-md" height="64" loading="lazy" src="/static/falcon-logo.svg" width="100" />
|
| 401 |
+
|
| 402 |
+
<span class="text-3xl font-extrabold select-none text-white tracking-wide">FALCON</span>
|
| 403 |
+
</a>
|
| 404 |
+
<button aria-controls="primary-navigation" aria-expanded="false"
|
| 405 |
+
class="sm:hidden text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
| 406 |
+
id="navToggle">
|
| 407 |
+
<svg aria-hidden="true" class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
| 408 |
+
xmlns="http://www.w3.org/2000/svg">
|
| 409 |
+
<path d="M4 8h16M4 16h16" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
|
| 410 |
+
</svg>
|
| 411 |
+
<span class="sr-only">Toggle navigation menu</span>
|
| 412 |
+
</button>
|
| 413 |
+
<ul class="hidden sm:flex space-x-10 font-medium text-lg" id="primary-navigation">
|
| 414 |
+
<li>
|
| 415 |
+
<a class="hover:text-[#60a5fa] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 416 |
+
href="#home">Home</a>
|
| 417 |
+
</li>
|
| 418 |
+
<li>
|
| 419 |
+
<a class="hover:text-[#60a5fa] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 420 |
+
href="#about">About Us</a>
|
| 421 |
+
</li>
|
| 422 |
+
<li>
|
| 423 |
+
<a class="hover:text-[#60a5fa] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 424 |
+
href="#contact">Contact Us</a>
|
| 425 |
+
</li>
|
| 426 |
+
</ul>
|
| 427 |
+
</nav>
|
| 428 |
+
<!-- Mobile menu -->
|
| 429 |
+
<div aria-label="Mobile navigation menu" class="sm:hidden bg-[#142a5c] border-t border-blue-900 hidden"
|
| 430 |
+
id="mobileMenu" role="menu">
|
| 431 |
+
<ul class="flex flex-col space-y-2 px-6 py-4 font-medium text-lg">
|
| 432 |
+
<li>
|
| 433 |
+
<a class="block py-2 hover:text-[#60a5fa] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 434 |
+
href="#home" role="menuitem">Home</a>
|
| 435 |
+
</li>
|
| 436 |
+
<li>
|
| 437 |
+
<a class="block py-2 hover:text-[#60a5fa] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 438 |
+
href="#about" role="menuitem">About Us</a>
|
| 439 |
+
</li>
|
| 440 |
+
<li>
|
| 441 |
+
<a class="block py-2 hover:text-[#60a5fa] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 442 |
+
href="#contact" role="menuitem">Contact Us</a>
|
| 443 |
+
</li>
|
| 444 |
+
</ul>
|
| 445 |
+
</div>
|
| 446 |
+
</header>
|
| 447 |
+
|
| 448 |
+
<!-- Main Content -->
|
| 449 |
+
<main class="flex-grow max-w-7xl mx-auto px-6 sm:px-8 lg:px-12 py-12 fade-in" id="mainContent" tabindex="-1">
|
| 450 |
+
<!-- Home Section -->
|
| 451 |
+
<section id="home" class="mb-20">
|
| 452 |
+
<div class="bg-[#142a5c] rounded-xl shadow-lg p-10 flex flex-col items-center text-center space-y-6">
|
| 453 |
+
<h1 class="text-5xl font-extrabold select-none leading-tight text-white drop-shadow-lg">
|
| 454 |
+
FALCON : AI-Powered Claim Fact-Checker
|
| 455 |
+
</h1>
|
| 456 |
+
<p class="max-w-3xl text-lg text-blue-200">
|
| 457 |
+
Instantly verify the accuracy of claims with FALCON, an advanced AI-powered fact-checking tool.
|
| 458 |
+
Powered by NLP and trusted data sources, FALCON helps detect factual, misleading, or opinion-based
|
| 459 |
+
content with ease.
|
| 460 |
+
</p><img alt="FALCON logo"
|
| 461 |
+
class=" rounded-md" height="64" loading="lazy" src="/static/falcon-logo.svg" width="200" />
|
| 462 |
+
<a href="#formSection"
|
| 463 |
+
class="button-link inline-block bg-transparent border-2 border-[#3b82f6] text-white font-semibold rounded-md py-3 px-8 transition focus:outline-none focus:ring-4 focus:ring-[#2563eb]"
|
| 464 |
+
title="Start fact-checking claims now">
|
| 465 |
+
Start Fact-Checking
|
| 466 |
+
</a>
|
| 467 |
+
</div>
|
| 468 |
+
</section>
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
<!-- Form Section -->
|
| 472 |
+
<section id="formSection" aria-labelledby="formTitle"
|
| 473 |
+
class="bg-[#142a5c] rounded-xl shadow-lg p-8 sm:p-10 mx-auto mb-20 max-w-[920px]">
|
| 474 |
+
<h2 class="sr-only" id="formTitle">Check your claim</h2>
|
| 475 |
+
<form class="space-y-6" id="claimForm" novalidate>
|
| 476 |
+
<label class="block text-white font-semibold text-lg mb-2" for="claim">Enter a Claim (max 350
|
| 477 |
+
characters):</label>
|
| 478 |
+
<input aria-describedby="charCount"
|
| 479 |
+
class="w-full rounded-md border border-[#3b82f6] px-4 py-3 text-white placeholder-blue-300 focus:outline-none focus:ring-2 focus:ring-[#60a5fa] focus:border-[#60a5fa] transition"
|
| 480 |
+
id="claim" maxlength="350" name="claim" placeholder="Type your claim here..." required type="text"
|
| 481 |
+
spellcheck="false" autocomplete="off" />
|
| 482 |
+
<p aria-atomic="true" aria-live="polite" class="text-sm text-blue-300 select-none" id="charCount">
|
| 483 |
+
350 characters remaining
|
| 484 |
+
</p>
|
| 485 |
+
<div class="button-container">
|
| 486 |
+
<button aria-label="Check Claim"
|
| 487 |
+
class="button-link w-full bg-transparent border-2 border-[#3b82f6] text-white font-semibold rounded-md py-3 transition focus:outline-none focus:ring-4 focus:ring-[#2563eb] flex justify-center items-center space-x-3"
|
| 488 |
+
type="submit">
|
| 489 |
+
<span>Check Claim</span>
|
| 490 |
+
</button>
|
| 491 |
+
<div id="analyzeLoadingOverlay" aria-hidden="true" class="rounded-md">
|
| 492 |
+
<div class="btn-spinner" aria-label="Loading"></div>
|
| 493 |
+
</div>
|
| 494 |
+
</div>
|
| 495 |
+
</form>
|
| 496 |
+
</section>
|
| 497 |
+
|
| 498 |
+
<!-- Result Section -->
|
| 499 |
+
<section aria-atomic="true" aria-live="polite" id="result" tabindex="0" aria-label="Fact check result output"
|
| 500 |
+
class="mb-20">
|
| 501 |
+
<h2>Result</h2>
|
| 502 |
+
<div class="space-y-4">
|
| 503 |
+
<p><span class="font-semibold">Claim:</span> <span id="claimText"></span></p>
|
| 504 |
+
<p><span class="font-semibold">Classification:</span> <span id="classification"></span></p>
|
| 505 |
+
<p><span class="font-semibold">Tone:</span> <span id="tone"></span></p>
|
| 506 |
+
<p><span class="font-semibold">Intent:</span> <span id="intent"></span></p>
|
| 507 |
+
<p><span class="font-semibold">Fact-Check Result:</span> <span id="factCheckResult"></span></p>
|
| 508 |
+
<p><span class="font-semibold">Evidence:</span> <span id="evidence"></span></p>
|
| 509 |
+
<p><span class="font-semibold">Sources:</span> <span id="sources" class="underline"></span></p>
|
| 510 |
+
<p><span class="font-semibold">Reasoning:</span> <span id="reasoning"></span></p>
|
| 511 |
+
</div>
|
| 512 |
+
</section>
|
| 513 |
+
|
| 514 |
+
<section id="about" class="bg-[#142a5c] rounded-xl shadow-lg p-10 max-w-4xl mx-auto mb-20"
|
| 515 |
+
aria-labelledby="aboutTitle">
|
| 516 |
+
<h2 id="aboutTitle" class="text-3xl font-semibold mb-6 text-center select-none text-white drop-shadow-md">
|
| 517 |
+
About Us</h2>
|
| 518 |
+
<div class="flex flex-col sm:flex-row sm:space-x-10 items-center sm:items-start">
|
| 519 |
+
<!-- Icons & Short Descriptions -->
|
| 520 |
+
<div class="flex space-x-6 mb-8 sm:mb-0 justify-center sm:justify-start w-full sm:w-auto">
|
| 521 |
+
<div class="flex flex-col items-center space-y-3 text-white drop-shadow-md max-w-xs">
|
| 522 |
+
<i class="fas fa-robot fa-3x"></i>
|
| 523 |
+
<p class="text-center">AI-powered fake news detection</p>
|
| 524 |
+
</div>
|
| 525 |
+
<div class="flex flex-col items-center space-y-3 text-white drop-shadow-md max-w-xs">
|
| 526 |
+
<i class="fas fa-shield-alt fa-3x"></i>
|
| 527 |
+
<p class="text-center">Reliable, trustworthy fact-checking</p>
|
| 528 |
+
</div>
|
| 529 |
+
<div class="flex flex-col items-center space-y-3 text-white drop-shadow-md max-w-xs">
|
| 530 |
+
<i class="fas fa-users fa-3x"></i>
|
| 531 |
+
<p class="text-center">Committed to transparency & accuracy</p>
|
| 532 |
+
</div>
|
| 533 |
+
</div>
|
| 534 |
+
<!-- Short Description Text -->
|
| 535 |
+
<div class="text-blue-100 max-w-xl text-center sm:text-left">
|
| 536 |
+
<p>
|
| 537 |
+
FALCON is an AI-powered fact-checking platform designed to quickly verify claims, detect
|
| 538 |
+
misinformation,
|
| 539 |
+
and provide users with reliable results. It uses AI-driven analysis to classify claims, assess
|
| 540 |
+
tone and intent,
|
| 541 |
+
and fact-check the information based on trusted sources.
|
| 542 |
+
</p>
|
| 543 |
+
<p class="mt-4">
|
| 544 |
+
Our goal is to empower users with accurate, timely information, helping to build a more
|
| 545 |
+
informed, responsible society
|
| 546 |
+
by combating fake news and misinformation effectively.
|
| 547 |
+
</p>
|
| 548 |
+
<p class="mt-4">
|
| 549 |
+
FALCON is developed by <strong>Saksham Pathak</strong>, a student currently pursuing his
|
| 550 |
+
Master's degree at IIIT Lucknow.
|
| 551 |
+
</p>
|
| 552 |
+
</div>
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
</div>
|
| 556 |
+
</section>
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
<!-- Contact Us Section -->
|
| 562 |
+
<section id="contact" class="bg-[#142a5c] rounded-xl shadow-lg p-10 max-w-3xl mx-auto mb-20"
|
| 563 |
+
aria-labelledby="contactTitle">
|
| 564 |
+
<h2 id="contactTitle" class="text-3xl font-semibold mb-6 text-center select-none text-white drop-shadow-md">
|
| 565 |
+
Contact Us
|
| 566 |
+
</h2>
|
| 567 |
+
<form id="contactForm" class="space-y-6" novalidate>
|
| 568 |
+
<div>
|
| 569 |
+
<label for="name" class="block mb-2 font-semibold text-white">Name</label>
|
| 570 |
+
<input type="text" id="name" name="name" placeholder="Your name" required
|
| 571 |
+
class="w-full rounded-md border border-[#3b82f6] px-4 py-3 text-white placeholder-blue-300 focus:outline-none focus:ring-2 focus:ring-[#60a5fa] focus:border-[#60a5fa] transition"
|
| 572 |
+
spellcheck="false" autocomplete="off" />
|
| 573 |
+
</div>
|
| 574 |
+
<div>
|
| 575 |
+
<label for="email" class="block mb-2 font-semibold text-white">Email</label>
|
| 576 |
+
<input type="email" id="email" name="email" placeholder="yourname@gmail.com" required
|
| 577 |
+
class="w-full rounded-md border border-[#3b82f6] px-4 py-3 text-white placeholder-blue-300 focus:outline-none focus:ring-2 focus:ring-[#60a5fa] focus:border-[#60a5fa] transition"
|
| 578 |
+
spellcheck="false" autocomplete="off" />
|
| 579 |
+
</div>
|
| 580 |
+
<div>
|
| 581 |
+
<label for="message" class="block mb-2 font-semibold text-white">Message</label>
|
| 582 |
+
<textarea id="message" name="message" rows="4" placeholder="Your message" required
|
| 583 |
+
class="w-full rounded-md border border-[#3b82f6] px-4 py-3 text-white placeholder-blue-300 focus:outline-none focus:ring-2 focus:ring-[#60a5fa] focus:border-[#60a5fa] transition resize-none"
|
| 584 |
+
spellcheck="false"></textarea>
|
| 585 |
+
</div>
|
| 586 |
+
<button type="submit"
|
| 587 |
+
class="button-link w-full bg-transparent border-2 border-[#3b82f6] text-white font-semibold rounded-md py-3 transition focus:outline-none focus:ring-4 focus:ring-[#2563eb]">
|
| 588 |
+
Send Message
|
| 589 |
+
</button>
|
| 590 |
+
</form>
|
| 591 |
+
<div class="mt-8 text-center text-blue-300 select-none drop-shadow-md">
|
| 592 |
+
<p>If you have any questions or inquiries about FALCON or need assistance with using the platform, feel
|
| 593 |
+
free to reach out to us.</p>
|
| 594 |
+
<p class="mt-4">Email: <a href="mailto:msa24021@iiit.ac.in"
|
| 595 |
+
class="text-[#60a5fa]">msa24021@iiit.ac.in</a></p>
|
| 596 |
+
<p>Location: Saksham Pathak, IIIT Lucknow, India</p>
|
| 597 |
+
</div>
|
| 598 |
+
</section>
|
| 599 |
+
</main>
|
| 600 |
+
|
| 601 |
+
<!-- Footer -->
|
| 602 |
+
<footer class="bg-[#142a5c] border-t border-blue-900 py-8 select-none" role="contentinfo">
|
| 603 |
+
<div
|
| 604 |
+
class="max-w-7xl mx-auto px-6 sm:px-8 lg:px-12 flex flex-col sm:flex-row justify-between items-center space-y-4 sm:space-y-0">
|
| 605 |
+
<p class="text-blue-300 text-sm drop-shadow-md">© FALCON : AI-Powered Fake News Detector </p>
|
| 606 |
+
<div class="flex space-x-6 text-blue-300 hover:text-[#60a5fa] transition-colors drop-shadow-md">
|
| 607 |
+
<a aria-label="LinkedIn"
|
| 608 |
+
class="text-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 609 |
+
href="https://www.linkedin.com/in/sakshampathak" rel="noopener noreferrer" target="_blank">
|
| 610 |
+
<i class="fab fa-linkedin-in"></i>
|
| 611 |
+
</a>
|
| 612 |
+
<a aria-label="GitHub"
|
| 613 |
+
class="text-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 614 |
+
href="https://github.com/parthmax2" rel="noopener noreferrer" target="_blank"><i
|
| 615 |
+
class="fab fa-github"></i></a>
|
| 616 |
+
<a aria-label="Twitter"
|
| 617 |
+
class="text-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 618 |
+
href="https://twitter.com/Parthamx__" rel="noopener noreferrer" target="_blank"><i
|
| 619 |
+
class="fab fa-twitter"></i></a>
|
| 620 |
+
<a aria-label="Instagram"
|
| 621 |
+
class="text-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 rounded"
|
| 622 |
+
href="https://instagram.com/parthmax_" rel="noopener noreferrer" target="_blank"><i
|
| 623 |
+
class="fab fa-instagram"></i></a>
|
| 624 |
+
</div>
|
| 625 |
+
</div>
|
| 626 |
+
<div class="text-blue-200 text-sm mt-4 text-center">
|
| 627 |
+
<p>Developed by <a href="https://instagram.com/parthmax_"
|
| 628 |
+
class="hover:text-[#60a5fa]"><strong>Parthamx</strong></a></p>
|
| 629 |
+
</div>
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
<p class="text-blue-300 text-sm mt-4 text-center">Follow us on social media for the latest updates!</p>
|
| 633 |
+
|
| 634 |
+
</footer>
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
<script>
|
| 640 |
+
// Mobile nav toggle
|
| 641 |
+
const navToggle = document.getElementById("navToggle");
|
| 642 |
+
const mobileMenu = document.getElementById("mobileMenu");
|
| 643 |
+
navToggle.addEventListener("click", () => {
|
| 644 |
+
const expanded =
|
| 645 |
+
navToggle.getAttribute("aria-expanded") === "true" || false;
|
| 646 |
+
navToggle.setAttribute("aria-expanded", !expanded);
|
| 647 |
+
mobileMenu.classList.toggle("hidden");
|
| 648 |
+
});
|
| 649 |
+
|
| 650 |
+
// Loading screen fade out after page load
|
| 651 |
+
window.addEventListener("load", () => {
|
| 652 |
+
const loadingScreen = document.getElementById("loadingScreen");
|
| 653 |
+
setTimeout(() => {
|
| 654 |
+
loadingScreen.style.opacity = "0";
|
| 655 |
+
loadingScreen.style.transition = "opacity 0.5s ease";
|
| 656 |
+
setTimeout(() => {
|
| 657 |
+
loadingScreen.style.display = "none";
|
| 658 |
+
// Focus main content for accessibility
|
| 659 |
+
document.getElementById("mainContent").focus();
|
| 660 |
+
}, 500);
|
| 661 |
+
}, 1200);
|
| 662 |
+
});
|
| 663 |
+
|
| 664 |
+
// Character count update
|
| 665 |
+
const claimInput = document.getElementById("claim");
|
| 666 |
+
const charCount = document.getElementById("charCount");
|
| 667 |
+
claimInput.addEventListener("input", () => {
|
| 668 |
+
const length = claimInput.value.length;
|
| 669 |
+
const remaining = 350 - length;
|
| 670 |
+
charCount.textContent = remaining + " characters remaining";
|
| 671 |
+
if (remaining <= 0) {
|
| 672 |
+
charCount.classList.add("text-red-600");
|
| 673 |
+
charCount.classList.remove("text-blue-300");
|
| 674 |
+
} else {
|
| 675 |
+
charCount.classList.remove("text-red-600");
|
| 676 |
+
charCount.classList.add("text-blue-300");
|
| 677 |
+
}
|
| 678 |
+
});
|
| 679 |
+
|
| 680 |
+
// Form submission and result display with loading overlay on button
|
| 681 |
+
const form = document.getElementById("claimForm");
|
| 682 |
+
const resultSection = document.getElementById("result");
|
| 683 |
+
const errorDiv = document.createElement("div");
|
| 684 |
+
errorDiv.id = "error";
|
| 685 |
+
errorDiv.setAttribute("role", "alert");
|
| 686 |
+
errorDiv.setAttribute("aria-live", "assertive");
|
| 687 |
+
errorDiv.className =
|
| 688 |
+
"mt-6 max-w-3xl mx-auto font-semibold text-center select-none text-red-600";
|
| 689 |
+
form.after(errorDiv);
|
| 690 |
+
errorDiv.style.display = "none";
|
| 691 |
+
|
| 692 |
+
const analyzeLoadingOverlay = document.getElementById("analyzeLoadingOverlay");
|
| 693 |
+
const submitBtn = form.querySelector("button[type=submit]");
|
| 694 |
+
|
| 695 |
+
form.addEventListener("submit", async (e) => {
|
| 696 |
+
e.preventDefault();
|
| 697 |
+
|
| 698 |
+
const claimText = claimInput.value.trim();
|
| 699 |
+
|
| 700 |
+
if (claimText.length === 0) {
|
| 701 |
+
errorDiv.textContent = "Please enter a claim.";
|
| 702 |
+
errorDiv.style.display = "block";
|
| 703 |
+
resultSection.classList.remove("visible");
|
| 704 |
+
return;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
if (claimText.length > 350) {
|
| 708 |
+
errorDiv.textContent = "Claim exceeds 350 characters!";
|
| 709 |
+
errorDiv.style.display = "block";
|
| 710 |
+
resultSection.classList.remove("visible");
|
| 711 |
+
return;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
errorDiv.style.display = "none";
|
| 715 |
+
|
| 716 |
+
// Show loading overlay on button
|
| 717 |
+
submitBtn.disabled = true;
|
| 718 |
+
analyzeLoadingOverlay.classList.add("visible");
|
| 719 |
+
submitBtn.querySelector("span").textContent = "Checking...";
|
| 720 |
+
|
| 721 |
+
try {
|
| 722 |
+
const response = await fetch("/process_claim", {
|
| 723 |
+
method: "POST",
|
| 724 |
+
body: new URLSearchParams({ claim: claimText }),
|
| 725 |
+
headers: {
|
| 726 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 727 |
+
"ngrok-skip-browser-warning": "true"
|
| 728 |
+
},
|
| 729 |
+
});
|
| 730 |
+
|
| 731 |
+
if (!response.ok) {
|
| 732 |
+
throw new Error("Network response was not ok");
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
const data = await response.json();
|
| 736 |
+
|
| 737 |
+
if (data.error) {
|
| 738 |
+
errorDiv.textContent = data.error;
|
| 739 |
+
errorDiv.style.display = "block";
|
| 740 |
+
resultSection.classList.remove("visible");
|
| 741 |
+
} else {
|
| 742 |
+
errorDiv.style.display = "none";
|
| 743 |
+
|
| 744 |
+
// Populate result fields
|
| 745 |
+
document.getElementById("claimText").textContent = data.claim || "N/A";
|
| 746 |
+
document.getElementById("classification").textContent =
|
| 747 |
+
data.classification || "N/A";
|
| 748 |
+
document.getElementById("tone").textContent = data.tone || "N/A";
|
| 749 |
+
document.getElementById("intent").textContent = data.intent || "N/A";
|
| 750 |
+
document.getElementById("factCheckResult").textContent =
|
| 751 |
+
data.fact_check_result || "N/A";
|
| 752 |
+
document.getElementById("evidence").textContent = data.evidence || "N/A";
|
| 753 |
+
|
| 754 |
+
if (Array.isArray(data.sources) && data.sources.length > 0) {
|
| 755 |
+
const sourcesContainer = document.getElementById("sources");
|
| 756 |
+
sourcesContainer.innerHTML = "";
|
| 757 |
+
data.sources.forEach((s, index) => {
|
| 758 |
+
const a = document.createElement("a");
|
| 759 |
+
a.href = s.url;
|
| 760 |
+
a.target = "_blank";
|
| 761 |
+
a.rel = "noopener noreferrer";
|
| 762 |
+
a.className = "hover:text-[#60a5fa] focus:outline-none focus:ring-2 focus:ring-indigo-500 rounded";
|
| 763 |
+
a.textContent = s.source;
|
| 764 |
+
sourcesContainer.appendChild(a);
|
| 765 |
+
if (index < data.sources.length - 1) {
|
| 766 |
+
sourcesContainer.appendChild(document.createTextNode(", "));
|
| 767 |
+
}
|
| 768 |
+
});
|
| 769 |
+
} else {
|
| 770 |
+
document.getElementById("sources").textContent = "N/A";
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
document.getElementById("reasoning").textContent = data.reasoning || "N/A";
|
| 774 |
+
|
| 775 |
+
// Show result with fade-in
|
| 776 |
+
resultSection.classList.add("visible");
|
| 777 |
+
resultSection.focus();
|
| 778 |
+
}
|
| 779 |
+
} catch (error) {
|
| 780 |
+
errorDiv.textContent = "An error occurred. Please try again later.";
|
| 781 |
+
errorDiv.style.display = "block";
|
| 782 |
+
resultSection.classList.remove("visible");
|
| 783 |
+
} finally {
|
| 784 |
+
submitBtn.disabled = false;
|
| 785 |
+
analyzeLoadingOverlay.classList.remove("visible");
|
| 786 |
+
submitBtn.querySelector("span").textContent = "Check Claim";
|
| 787 |
+
}
|
| 788 |
+
});
|
| 789 |
+
|
| 790 |
+
// Contact form submission (dummy)
|
| 791 |
+
const contactForm = document.getElementById("contactForm");
|
| 792 |
+
contactForm.addEventListener("submit", (e) => {
|
| 793 |
+
e.preventDefault();
|
| 794 |
+
alert(
|
| 795 |
+
"Thank you for reaching out! We will contact you soon. "
|
| 796 |
+
);
|
| 797 |
+
contactForm.reset();
|
| 798 |
+
});
|
| 799 |
+
</script>
|
| 800 |
+
</body>
|
| 801 |
+
|
| 802 |
+
</html>
|
requirements.txt
ADDED
|
Binary file (2.62 kB). View file
|
|
|
test.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from backend.api.fact_check import fact_check_claim
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
if __name__ == "__main__":
|
| 5 |
+
sample_text = "Modi is a great leader who changed India forever!"
|
| 6 |
+
result = fact_check_claim(sample_text)
|
| 7 |
+
print(result)
|