diff --git a/README.md b/README.md index 814a993..2eca965 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

B0Bot - CyberSecurity News API

+

B0Bot - CyberSecurity News Intelligence Platform



Forks @@ -8,137 +8,89 @@

-B0Bot is a CyberSecurity News API tailored for automated bots on social media platforms. It is a cutting-edge Flask-based API that grants seamless access to the latest cybersecurity and hacker news. Users can effortlessly retrieve news articles either through specific keywords or without, streamlining the information acquisition process. -Once a user requests our API, it retrieves news data from our knowledge base and feeds it to the LLM. After the LLM processes the data, the API obtains the response and returns it in JSON format. The API is powered by LangChain and a Huggingface endpoint, ensuring that users receive accurate and up-to-date information. +B0Bot is a cybersecurity news intelligence platform built around a three-service architecture: an ingestion service that polls RSS feeds and enriches articles with CVE/severity metadata, an api-service that runs a LangGraph agent pipeline for search, analysis, and a grounded Ask AI chat, and a notification service that sends digest emails to subscribers.

+## Architecture -## App Screenshots - -| Home Page | LLM Page | News Page | News Keywords Page | -| :--------:| :-------:| :---------:| :-----------------:| -| ![Home Page](assets/home.png) | ![LLM Page](assets/llm.png) | ![News Page](assets/news.png) | ![News Keywords Page](assets/news_keywords.png) | - -## Setup -1. Install all necessary packages - -`pip install -r ./requirements.txt` - +![Architecture](assets/architecture.png) -2. Set up your Pinecone database -``` -https://www.pinecone.io/ -``` -Login to Pinecone and create a new index with the name `news-index`. Then, add the Pinecone API key in the `.env` file. +The project has three services, using PostgreSQL (with pgvector for embeddings) and Redis to share data and handle caching, sessions, and job queues: +- **ingestion-service** - polls RSS feeds loaded from the sources table (falls back to a hardcoded list if empty), extracts CVE/severity/affected-system metadata via LLM, computes embeddings, writes to Postgres +- **api-service** - Flask app serving the dashboard, chat, sources, and subscribe pages; runs every `/chat` request through a LangGraph agent pipeline +- **notification-service** - polls Postgres for subscribers due for a digest and sends via SMTP; subscriptions are created directly by api-service, no queue involved +All three run together via Docker Compose, alongside Postgres and Redis. +## Features -3. Set up your HuggingFace account -``` -https://huggingface.co/ -``` +- **Dashboard** - CVE Watchlist, Top News, and a filterable article feed (Newest / Critical / Frequent, by source) +- **Ask AI** - click into any article to ask questions grounded in that specific article's content, powered by a hosted Cohere model +- **Hybrid search** - chat queries combine keyword relevance and vector similarity search over article embeddings +- **Sentiment & trend analysis** - per-article sentiment (DistilBERT) and keyword/trend surfacing across search results +- **Sources management** - view and add RSS sources feeding the ingestion pipeline +- **Subscribe / Unsubscribe** - email digests by interest tag and frequency (daily/weekly), manageable via chat or the subscribe form. Chat-based subscribe can span multiple turns - if the email or interests aren't in the message, it asks as a follow-up instead of failing silently +## Setup -4. Set up your environment variables +1. Clone the repo and set up your environment file: -Copy `.env.example` to `.env` and fill in your keys: -``` +```bash cp .env.example .env ``` -Refer to [`.env.example`](.env.example) for all available keys and where to get them. - - -5. Enrich/Update news data into your database - -Run `./db_update/Update.py` as a worker on a cloud service (e.g. heroku). -Or, run `./db_update/Update.py` manually in local. - - -6. Run the flask app - -`flask --app app.py run` - -> By default, the home page will open. The routes have to be defined manually. - - -7. We have added support for the following routes: -``` -/llama # Loads the Meta-Llama-3-8B-Instruct -/gemma # Loads the Gemma-2b -/mistralai # Loads the Mistral-7B-Instruct-v0.2 -``` - -> [!NOTE] -> The Huggingface token you are using must have access to the LLama3 model listed above. -> You can do so by visiting this [link](https://huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct). +Fill in the values - see [`.env.example`](.env.example) for what each one is for. At minimum you'll need a [HuggingFace token](https://huggingface.co/settings/tokens) - used for the local embedding/sentiment models, and to authenticate HuggingFace's InferenceClient, which is how the app reaches the hosted Cohere model for summaries, intent classification, and Ask AI. +2. Bring up the full stack with Docker Compose: -8. Available url paths +```bash +docker compose up -d ``` -//news -//news_keywords?keywords=[Place news keywords here] -# Bypass LLM mapping forraw hybrid search results -/raw/news -/raw/news_keywords?keywords=[Place news keywords here] -``` +This starts Postgres (pgvector), Redis, and all three services. The api-service will be available at `http://localhost:5000`. -> [!IMPORTANT] -> The interface will only work if you specify one of the available paths above. +3. (Optional) Configure social connectors - see the [Social Connectors](#social-connectors) section below. +4. (Optional) Configure SMTP settings in `.env` if you want digest emails to actually send. Subscribing/unsubscribing itself doesn't depend on SMTP - that just updates the subscriber record. Without SMTP configured, the digest worker will fail to send and roll back that delivery attempt rather than crash, so it's safe to leave unset for local development. ## Social Connectors -**Layer 1 — RSS Feeds:** Pulls cybersecurity news from 7 curated RSS feeds (Reddit, KrebsOnSecurity, BleepingComputer, CISA, etc.) with no API key required. +**Layer 1 - RSS Feeds:** Pulls cybersecurity news from curated RSS feeds (KrebsOnSecurity, BleepingComputer, CISA, etc.) with no API key required. -**Layer 2 — Opt-in API Connectors:** Supports YouTube Data API v3 and NewsAPI.org for richer coverage. Both use free tiers and silently skip if keys are absent. See [`.env.example`](.env.example) for setup. +**Layer 2 - Opt-in API Connectors:** Supports YouTube Data API v3 and NewsAPI.org for additional coverage. Both use free tiers and silently skip if keys are absent. See [`.env.example`](.env.example) for setup. +## LangGraph Agent Pipeline -## High-Level Architecture Diagram +Every `/chat` request runs through a LangGraph pipeline of agents, each reading and updating a shared state object: -Our API lives inside a Flask API and is powered by LangChain and a Huggingface endpoint. +1. **PlannerAgent** - classifies intent (search, analyze, subscribe, chitchat, or grounded) via a hosted LLM, with keyword-based fallback if the LLM call fails or is unavailable +2. **ScraperAgent** - runs hybrid search (keyword + vector similarity) against PostgreSQL/pgvector to find matching articles +3. **AnalyzerAgent** - computes keyword frequency, trending topics, and per-article sentiment (DistilBERT SST-2) across retrieved articles +4. **ResponderAgent** - checks Redis for a cached response first (5 minute TTL), otherwise builds and caches the JSON response. For `grounded` intent (Ask AI), calls out to Cohere with the specific article's content instead of running the full search pipeline +5. **NotificationAgent** - triggered on subscribe intent; extracts email, frequency, and interest tags from the conversation (can span multiple turns if info is missing), creates the subscriber -In addition, to keep the knowledge base of news up to date, a scheduled script will be executed on a regular interval to retrieve the most recent cybersecurity news by scraping a list of target news websites and store them into the MongoDB Atlas Database. Everytime a user requests the API, news in the database will be read into LangChain's memory and fed to the LLM. Then, answers will be generated based on both the selected LLM and our knowledge base. +### Multi-turn Session Memory -![Architecture](assets/architecture.png) +Every `/chat` request accepts a `session_id`. Chat history for that session is stored in Redis with a 1 hour TTL and capped at 10 messages, so follow-up questions have context from previous turns. Ask AI grounding is single-turn only - it applies to the exact message sent right after clicking "Ask AI" on an article, not to later follow-ups in the same session. -![Knowledge Base](assets/db_arch.png) +## App Screenshots +**Landing page** +![Landing Page](assets/landing.png) -The API will continuely run as a serverless function (hosted on [Render](https://render.com/)) and it will record a successfull operation in a monitoring dashboard set up in [Better Uptime](https://betterstack.com/better-uptime). +**Dashboard - CVE watchlist, top news, and article feed** +![Dashboard](assets/dashboard.png) -## LangGraph Agent Pipeline +**Ask AI - grounded answers on a specific article** +![Ask AI](assets/chat.png) -The api-service runs every `/chat` request through a LangGraph pipeline of agents, each reading and updating a shared state object: - -1. **PlannerAgent** - classifies intent (search, analyze, subscribe) and extracts keywords from the user message -2. **ScraperAgent** - queries PostgreSQL for matching articles, falling back to recent articles if no keyword match is found -3. **AnalyzerAgent** - computes keyword frequency, trending topics, and sentiment (positive/negative/neutral) across the retrieved articles -4. **ResponderAgent** - checks Redis for a cached response first (5 minute TTL), otherwise builds the JSON response and caches it -5. **NotificationAgent** - triggered when intent is subscribe; extracts email and frequency from the user message, pushes an article.digest job to Redis for the notification-service to consume - -### Example /chat response - -```json -{ - "message": "Found 2 articles.", - "articles": [...], - "chat_history": [], - "analysis": { - "keyword_frequency": [["ransomware", 2], ["critical", 2]], - "trending_topics": ["ransomware", "critical"], - "sentiment": "negative", - "positive_signals": [], - "negative_signals": ["ransomware", "critical", "vulnerability"] - } -} -``` -### Multi-turn Session Memory +**Search - hybrid search with sentiment per article** +![Search](assets/chat-search.png) -Every `/chat` request accepts a `session_id`. Chat history for that session is stored in Redis with a 1 hour TTL and capped at 10 messages. Each request loads the history from Redis before invoking the pipeline and saves the updated history after, so follow-up questions have context from previous turns. +**Sources - manage RSS feeds powering ingestion** +![Sources](assets/sources.png) ## Licensing -The MIT License 2023 \ No newline at end of file +The MIT License 2023 diff --git a/api-service/Dockerfile b/api-service/Dockerfile index c7fbc8e..03e2f76 100644 --- a/api-service/Dockerfile +++ b/api-service/Dockerfile @@ -9,6 +9,7 @@ RUN pip install --upgrade pip \ COPY . . RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')" +RUN python -c "from transformers import pipeline; pipeline('text-classification', model='distilbert-base-uncased-finetuned-sst-2-english', truncation=True, max_length=128)" EXPOSE 5000 diff --git a/api-service/config/llm_config.json b/api-service/config/llm_config.json deleted file mode 100644 index 5c14f8b..0000000 --- a/api-service/config/llm_config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "mistralai": "mistralai/Mistral-7B-Instruct-v0.2", - "gemma": "google/gemma-2b", - "llama" : "meta-llama/Meta-Llama-3-8B-Instruct" - -} diff --git a/api-service/controllers/NewsController.py b/api-service/controllers/NewsController.py deleted file mode 100644 index 9417cd6..0000000 --- a/api-service/controllers/NewsController.py +++ /dev/null @@ -1,30 +0,0 @@ -from services.NewsService import NewsService - -class NewsController: - def __init__(self, model_name=None): - self.news_service = NewsService(model_name) - - """ - return news without considering keywords - """ - - def getNews(self, llm=True): - if self.news_service.model_name is None: - return self.news_service.getNews(llm=False) - return self.news_service.getNews(llm=llm) - - """ - return news based on certain keywords - """ - - def getNewsWithKeywords(self, user_keywords, llm=True): - if self.news_service.model_name is None: - return self.news_service.getNews(user_keywords, llm=False) - return self.news_service.getNews(user_keywords, llm=llm) - - """ - deal requests with wrong route - """ - - def notFound(self, error): - return self.news_service.notFound(error) diff --git a/api-service/cybernews/CyberNews.py b/api-service/cybernews/CyberNews.py deleted file mode 100644 index b381d28..0000000 --- a/api-service/cybernews/CyberNews.py +++ /dev/null @@ -1,74 +0,0 @@ -import json -import os -from cybernews.extractor import Extractor -from cybernews.social_connectors.rss_extractor import RSSExtractor -from cybernews.social_connectors.youtube_connector import YouTubeConnector -from cybernews.social_connectors.newsapi_connector import NewsAPIConnector - -class CyberNews: - def __init__(self) -> None: - self._extractor = Extractor() - self._rss_extractor = RSSExtractor() - self._youtube_connector = YouTubeConnector() - self._newsapi_connector = NewsAPIConnector() - - self._news_types = self.load_json_config('cybernews/news_types.json') - self._social_sources = {} - if os.path.exists('cybernews/social_sources.json'): - self._social_sources = self.load_json_config('cybernews/social_sources.json') - - def load_json_config(self, json_file): - with open(json_file, 'r', encoding='utf-8') as file: - return json.load(file) - - @property - def get_news_types(self) -> list: - return [news_type for news in self._news_types for news_type, _ in news.items()] - - def get_news(self, news) -> list: - combined_news = [] - - # 1. Fetch from standard web extractors - for news_type in self._news_types: - if news in news_type: - try: - combined_news.extend(self._extractor.data_extractor(news_type[news])) - except Exception as e: - print(f"Web extraction error for '{news}': {e}") - - # 2. Fetch from Social Extractors (only on relevant cybersecurity topics) - if news in ['general', 'cyberAttack', 'dataBreach', 'vulnerability', 'security', 'malware']: - # Get the category-specific query for API connectors - api_query = self._social_sources.get("api_queries", {}).get(news) - try: - # Layer 1: Free RSS feeds (Reddit, Krebs, BleepingComputer, CISA, etc.) - if "rss" in self._social_sources and "feeds" in self._social_sources["rss"]: - rss_data = self._rss_extractor.process_feeds(self._social_sources["rss"]["feeds"]) - print(f"[{news}] RSS feeds → {len(rss_data)} articles") - combined_news.extend(rss_data) - - # Layer 2: Optional YouTube Data API v3 (requires YOUTUBE_API_KEY) - youtube_data = self._youtube_connector.extract(query=api_query) - if youtube_data: - print(f"[{news}] YouTube API → {len(youtube_data)} videos (query: '{api_query}')") - combined_news.extend(youtube_data) - - # Layer 2: Optional NewsAPI.org (requires NEWSAPI_KEY) - newsapi_data = self._newsapi_connector.extract(query=api_query) - if newsapi_data: - print(f"[{news}] NewsAPI → {len(newsapi_data)} articles (query: '{api_query}')") - combined_news.extend(newsapi_data) - - except Exception as e: - print(f"Social extraction error: {e}") - - if not combined_news: - raise ValueError(f"News type '{news}' not found or yielded zero results") - - # Re-sort the combined list to interleave social news with web news chronologically - try: - from cybernews.sorting import Sorting - sorter = Sorting() - return sorter.ordering_news(combined_news) - except Exception: - return combined_news diff --git a/api-service/cybernews/__init__.py b/api-service/cybernews/__init__.py deleted file mode 100644 index 991aa1a..0000000 --- a/api-service/cybernews/__init__.py +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/api-service/cybernews/extractor.py b/api-service/cybernews/extractor.py deleted file mode 100644 index b2bc989..0000000 --- a/api-service/cybernews/extractor.py +++ /dev/null @@ -1,199 +0,0 @@ -""" - Class for Exception Handling and Extracting data out of complex strings -""" -import concurrent.futures - -import httpx -from bs4 import BeautifulSoup - -from .performance import Performance -from .sorting import Sorting - - -class Extractor(Performance): - def __init__(self): - """ - Initializing the Extractor class - """ - # Call the constructor of the parent class (Performance) - super().__init__() - # Initiate a session using requests - self.session = httpx.Client() - # Create an instance of the Sorting class - self.sorting = Sorting() - # Get the headers for the HTTP requests - self.headers = self.headers() - - # Extracting Author Name - def _author_name_extractor(self, name: str): - """ - Extract the author name from a given string. - - Args: - name (str): the name to extract from. - - Returns: - str: the extracted author name. - """ - # Use the '_pattern1' regular expression to remove unwanted characters - author_name = self.remove_symbols(self._pattern1.sub("", name)) - - if not self.is_valid_author_name(author_name): - return "N/A" - return self.format_author_name(author_name) - - # Checking is news or some random advertisement - def _check_ad(self, news_date: str): - """ - Check if a given date string represents an advertisement. - - Args: - news_date (str): the date string to check. - - Returns: - bool: True if the date string represents an advertisement, False otherwise. - """ - # Use the '_pattern4' regular expression to search for the advertisement indicator - return self._pattern4.search(news_date) is not None - - # Extracting NewsDate - def _news_date_extractor(self, date: str, news_date: str) -> str: - """ - Extract the news date from a given string. - - Args: - date (str): the string to extract the news date from. - news_date (str): an additional string to use in the extraction process. - - Returns: - str: the extracted news date, or 'N/A' if the date could not be extracted. - """ - # Use two regular expressions to remove unwanted characters - date = self._pattern3.sub("", self._pattern2.sub("", date)) - # Use the '_pattern5' regular expression to match the news date - return self._pattern5.match(date).group() if news_date != "" else "N/A" - - # Extracting Data From Single News - def _extract_data_from_single_news(self, url: str, value: dict): - """ - Extract data from a single news article. - - Args: - url (str): The URL of the news article. - value (dict): The CSS selectors for extracting news data. - - Returns: - list: A list of dictionaries with the extracted news data. - """ - news_data_from_single_news = [] - try: - response = self.session.get(url, timeout=20, headers=self.headers) - soup = BeautifulSoup(response.text, "lxml") - except (httpx.RequestError, httpx.TimeoutException) as e: - print(f"Request to {url} failed: {e}") - return [] - news_headlines = soup.select(value["headlines"]) - raw_news_author = ( - soup.select(value["author"]) if value["author"] is not None else "" - ) - news_full_news = soup.select(value["fullNews"]) - news_url = soup.select(value["newsURL"]) - news_img_url = soup.select(value["newsImg"]) if value["newsImg"] is not None else [] - raw_news_date = soup.select(value["date"]) if value["date"] is not None else "" - - for index in range(len(news_headlines)): - if raw_news_date and index < len(raw_news_date): - news_date = self._news_date_extractor( - raw_news_date[index].text.strip(), raw_news_date - ) - else: - news_date = "N/A" - - if raw_news_author and index < len(raw_news_author): - news_author = self._author_name_extractor( - raw_news_author[index].text.strip() - ) - else: - news_author = "N/A" - - if self._check_ad(news_date): - continue - - if index >= len(news_url) or not self.valid_url_check(news_url[index]["href"]): - continue - - full_news_text = news_full_news[index].text.strip() if index < len(news_full_news) else "" - if self.spam_content_check(news_headlines[index].text.strip() + " " + full_news_text): - continue - - complete_news = { - "id": self.sorting.ordering_date(news_date), - "headlines": news_headlines[index].text.strip(), - "author": news_author, - "fullNews": full_news_text, - "newsURL": news_url[index]["href"], - "newsImgURL": news_img_url[index].get("data-src") or news_img_url[index].get("src", "N/A") if index < len(news_img_url) else "N/A", - "newsDate": news_date, - } - news_data_from_single_news.append(complete_news) - - # Remove duplicates before sorting - unique_news_data_from_single_news = self._remove_duplicates(news_data_from_single_news) - return self.sorting.ordering_news(unique_news_data_from_single_news) - - # Extracting Data Using Tags - def data_extractor(self, news: list) -> list: - """ - Extract news data from a given list of news headers. - - Args: - news_header (list): a list of dictionaries containing news headers. - - Returns: - list: a list of dictionaries containing the extracted news data. - """ - - # Initialize an empty list to store the extracted news data - news_data = [] - - with concurrent.futures.ThreadPoolExecutor() as executor: - future_to_news = { - executor.submit(self._extract_data_from_single_news, url, value): ( - url, - value, - ) - for single_news in news - for url, value in single_news.items() - } - for future in concurrent.futures.as_completed(future_to_news): - url = future_to_news[future] - try: - news_data_from_single_news = future.result() - news_data.extend(news_data_from_single_news) - except Exception as exc: - print(f"{url} generated an exception: {exc}") - - # Remove duplicates before sorting - unique_news_data = self._remove_duplicates(news_data) - return self.sorting.ordering_news(unique_news_data) - - # Removing Duplicates - def _remove_duplicates(self, news_data: list) -> list: - """ - Remove duplicate news items based on specific criteria. - - Args: - news_data (list): A list of dictionaries containing news data. - - Returns: - list: A list with duplicate entries removed. - """ - seen = set() - unique_news_data = [] - for item in news_data: - # Use a tuple of fields that should be unique to identify duplicates - identifier = (item["headlines"], item["newsURL"], item["newsDate"]) - if identifier not in seen: - seen.add(identifier) - unique_news_data.append(item) - return unique_news_data \ No newline at end of file diff --git a/api-service/cybernews/news_types.json b/api-service/cybernews/news_types.json deleted file mode 100644 index f6086c8..0000000 --- a/api-service/cybernews/news_types.json +++ /dev/null @@ -1,252 +0,0 @@ -[ - { - "general": [ - { - "https://ciosea.economictimes.indiatimes.com/news/next-gen-technologies": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - }, - { - "https://telecom.economictimes.indiatimes.com/news/internet": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - } - ] - }, - { - "dataBreach": [ - { - "https://thehackernews.com/search/label/data%20breach": { - "headlines": "h2.home-title", - "author": ".item-label span", - "fullNews": ".home-desc", - "newsImg": ".img-ratio img", - "newsURL": "a.story-link", - "date": ".item-label" - } - }, - { - "https://ciso.economictimes.indiatimes.com/news/data-breaches": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - }, - { - "https://cyware.com/search?search=data%20breach": { - "headlines": "h1.cy-card__title", - "author": "a[href*='source_name']", - "fullNews": "div.cy-card__description", - "newsImg": null, - "newsURL": "a[href*='articles']", - "date": "span.cy-card__meta" - } - } - ] - }, - { - "cyberAttack": [ - { - "https://thehackernews.com/search/label/Cyber%20Attack": { - "headlines": "h2.home-title", - "author": ".item-label span", - "fullNews": ".home-desc", - "newsImg": ".img-ratio img", - "newsURL": "a.story-link", - "date": ".item-label" - } - }, - { - "https://ciso.economictimes.indiatimes.com/news/cybercrime-fraud": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - }, - { - "https://cyware.com/search?search=cyber%20attack": { - "headlines": "h1.cy-card__title", - "author": "a[href*='source_name']", - "fullNews": "div.cy-card__description", - "newsImg": null, - "newsURL": "a[href*='articles']", - "date": "span.cy-card__meta" - } - } - ] - }, - { - "vulnerability": [ - { - "https://thehackernews.com/search/label/Vulnerability": { - "headlines": "h2.home-title", - "author": ".item-label span", - "fullNews": ".home-desc", - "newsImg": ".img-ratio img", - "newsURL": "a.story-link", - "date": ".item-label" - } - }, - { - "https://ciso.economictimes.indiatimes.com/news/vulnerabilities-exploits": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - }, - { - "https://cyware.com/alerts/filter?alert_type=A&category_slug=malware-and-vulnerabilities-news": { - "headlines": "h1.cy-card__title", - "author": "a[href*='source_name']", - "fullNews": "div.cy-card__description", - "newsImg": null, - "newsURL": "a[href*='articles']", - "date": "span.cy-card__meta" - } - } - ] - }, - { - "malware": [ - { - "https://thehackernews.com/search/label/Malware": { - "headlines": "h2.home-title", - "author": ".item-label span", - "fullNews": ".home-desc", - "newsImg": ".img-ratio img", - "newsURL": "a.story-link", - "date": ".item-label" - } - }, - { - "https://www.infosecurity-magazine.com/malware/": { - "headlines": "h3.content-headline a", - "author": null, - "fullNews": "p.content-teaser", - "newsImg": "img.content-thumb", - "newsURL": "h3.content-headline a", - "date": "time" - } - }, - { - "https://www.bleepingcomputer.com/tag/malware/": { - "headlines": "h4 a", - "author": "li.bc_news_author a", - "fullNews": "div.bc_latest_news_text p", - "newsImg": "div.bc_latest_news_img img", - "newsURL": "h4 a", - "date": "li.bc_news_date" - } - } - ] - }, - { - "security": [ - { - "https://ciosea.economictimes.indiatimes.com/news/security": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - }, - { - "https://telecom.economictimes.indiatimes.com/tag/hacking": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - } - ] - }, - { - "cloud": [ - { - "https://ciosea.economictimes.indiatimes.com/news/cloud-computing": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - } - ] - }, - { - "tech": [ - { - "https://ciosea.economictimes.indiatimes.com/news/consumer-tech": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - } - ] - }, - { - "iot": [ - { - "https://ciosea.economictimes.indiatimes.com/news/internet-of-things": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - } - ] - }, - { - "bigData": [ - { - "https://ciosea.economictimes.indiatimes.com/news/big-data": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - }, - { - "https://ciosea.economictimes.indiatimes.com/news/data-center": { - "headlines": "article.desc div h3.heading", - "author": null, - "fullNews": "article.desc div p.desktop-view", - "newsImg": ".desc figure a img", - "newsURL": ".desc figure a", - "date": null - } - } - ] - } -] \ No newline at end of file diff --git a/api-service/cybernews/performance.py b/api-service/cybernews/performance.py deleted file mode 100644 index 90b2634..0000000 --- a/api-service/cybernews/performance.py +++ /dev/null @@ -1,120 +0,0 @@ -""" - Performance Class for better and fast extracting of data -""" -import re -from datetime import datetime - - -class Performance: - _pattern1 = re.compile(r"\ue804") - _pattern2 = re.compile(r"\ue802") - _pattern3 = re.compile(r"\ue804.+") - _pattern4 = re.compile( - r"([\w+]+\:\/\/)?([\w\d-]+\.)*[\w-]+[\.\:]\w+([\/\?\=\&\#.]?[\w-]+)*\/?" - ) - _pattern5 = re.compile(r"^[^\n]+") - symbol_regex = re.compile(r"[^\w\s]") - - def __init__(self): - """ - Headers For Performance - """ - self._headers = { - "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 " - "Safari/537.36", - "Content-Type": "application/json; charset=utf-8", - "server": "nginx/1.0.4", - "x-runtime": "148ms", - "etag": '"e1ca502697e5c9317743dc078f67693f"', - "Access-Control-Allow-Credentials": "true", - "Content-Encoding": "gzip", - } - self._spam_keywords = ["buy now", "click here", "subscribe", "limited offer"] - - def headers(self): - """ - Return the headers for HTTP requests. - """ - return self._headers - - def remove_symbols(self, text: str) -> str: - """ - Remove non-alphanumeric symbols from a given text. - - Args: - text (Optional[str]): The text to clean. - - Returns: - str: The cleaned text. - """ - if not text: - return "" - return re.sub(self.symbol_regex, "", text) - - def check_valid_date(self, date): - """ - Check if a given date string matches the expected format. - - Args: - date (str): The date string to check. - date_format (str): The expected date format. - - Returns: - bool: True if the date is valid, False otherwise. - """ - date_format = "%b %d %Y" - try: - datetime.strptime(date, date_format) - return True - except ValueError: - return False - - def is_valid_author_name(self, name: str) -> bool: - """ - Check if the given name is not a valid date, hence a valid author name. - - Args: - name (str): The name to check. - - Returns: - bool: True if the name is valid, False otherwise. - """ - return not self.check_valid_date(name) - - def format_author_name(self, name: str) -> str: - """ - Format the author name by stripping extra spaces. - - Args: - name (str): The name to format. - - Returns: - str: The formatted author name. - """ - return " ".join(name.strip().split()) - - def valid_url_check(self, url: str) -> bool: - """ - Check if the URL is valid. - - Args: - url (str): The URL to check. - - Returns: - bool: True if the URL is valid, False otherwise. - """ - return url.startswith("http://") or url.startswith("https://") - - def spam_content_check(self, content: str) -> bool: - """ - Check for spammy content in the text. - - Args: - content (str): The content to check. - - Returns: - bool: True if the content is spammy, False otherwise. - """ - return any(keyword in content.lower() for keyword in self._spam_keywords) - - diff --git a/api-service/cybernews/social_connectors/newsapi_connector.py b/api-service/cybernews/social_connectors/newsapi_connector.py deleted file mode 100644 index 6f50d94..0000000 --- a/api-service/cybernews/social_connectors/newsapi_connector.py +++ /dev/null @@ -1,109 +0,0 @@ -import os -from datetime import datetime -from cybernews.performance import Performance -from cybernews.sorting import Sorting - -# NOTE: Requires NEWSAPI_KEY in your .env -# Get a free key (100 requests/day) instantly at: https://newsapi.org/register -# No credit card required. - -class NewsAPIConnector(Performance): - """ - Optional connector that uses NewsAPI.org to fetch real cybersecurity - news articles from 150,000+ verified news sources. - - Activation: Set NEWSAPI_KEY in your .env file. - If the key is absent, this connector silently skips and returns an empty list. - """ - - FALLBACK_QUERY = "cybersecurity OR infosec OR CVE OR data breach OR malware" - MAX_RESULTS = 20 - - def __init__(self): - super().__init__() - self.sorting = Sorting() - self.is_configured = False - self._client = None - - api_key = os.environ.get("NEWSAPI_KEY") - if not api_key: - print("[NewsAPI Connector] Skipping: NEWSAPI_KEY not found in env.") - return - - try: - from newsapi import NewsApiClient - self._client = NewsApiClient(api_key=api_key) - self.is_configured = True - print("[NewsAPI Connector] Initialized successfully.") - except ImportError: - print("[NewsAPI Connector] Skipping: 'newsapi-python' not installed. Run: pip install newsapi-python") - except Exception as e: - print(f"[NewsAPI Connector] Initialization failed: {e}") - - def extract(self, query: str = None) -> list: - """ - Fetches recent cybersecurity articles from NewsAPI.org. - Args: - query: Category-specific search string from social_sources.json api_queries. - Falls back to FALLBACK_QUERY if not provided. - Returns a list of standardized news dictionaries. - """ - if not self.is_configured: - return [] - - search_query = query or self.FALLBACK_QUERY - news_data = [] - try: - response = self._client.get_everything( - q=search_query, - language="en", - sort_by="publishedAt", - page_size=self.MAX_RESULTS - ) - - for article in response.get("articles", []): - title = (article.get("title") or "").strip() - description = (article.get("description") or "").strip() - content = (article.get("content") or description).strip() - url = article.get("url", "") - source = article.get("source", {}).get("name", "NewsAPI") - author = article.get("author") or source - published_at = article.get("publishedAt", "") - image_url = article.get("urlToImage") or "N/A" - - if not title or not url: - continue - - # Remove "[+N chars]" truncation marker NewsAPI appends - if content and "[+" in content: - content = content[:content.rfind("[+")].strip() - - # Parse ISO 8601 date - date_str = "N/A" - try: - dt = datetime.fromisoformat(published_at.replace("Z", "+00:00")) - date_str = dt.strftime("%B %d, %Y") - except Exception: - pass - - if self.spam_content_check(title + " " + description): - continue - - body = content if content else description - news_data.append({ - "id": self.sorting.ordering_date(date_str) if date_str != "N/A" else 0, - "headlines": title, - "author": author, - "fullNews": body[:500] + "..." if len(body) > 500 else body, - "newsURL": url, - "newsImgURL": image_url, - "newsDate": date_str - }) - - except Exception as e: - print(f"[NewsAPI Connector] Extraction failed: {e}") - - # Deduplicate - seen = set() - unique = [item for item in news_data if not (item["newsURL"] in seen or seen.add(item["newsURL"]))] - return self.sorting.ordering_news(unique) diff --git a/api-service/cybernews/social_connectors/rss_extractor.py b/api-service/cybernews/social_connectors/rss_extractor.py deleted file mode 100644 index b2e8ba6..0000000 --- a/api-service/cybernews/social_connectors/rss_extractor.py +++ /dev/null @@ -1,62 +0,0 @@ -import feedparser -import warnings -from datetime import datetime -from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning -from cybernews.performance import Performance -from cybernews.sorting import Sorting - -# Suppress BeautifulSoup warning for URL-like strings from some feed summaries -warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning) - -class RSSExtractor(Performance): - def __init__(self): - super().__init__() - self.sorting = Sorting() - - def process_feeds(self, feeds: list) -> list: - news_data = [] - for feed_url in feeds: - try: - feed = feedparser.parse(feed_url) - for entry in feed.entries: - # Some feeds (e.g. Mastodon) have entries without title or link — skip them - title = entry.get('title', '').strip() - link = entry.get('link', '').strip() - if not title or not link: - continue - - # Clean up RSS bodies which often contain embedded HTML - soup = BeautifulSoup(entry.get('summary', '') or entry.get('description', ''), 'lxml') - body_text = soup.get_text(separator=' ', strip=True) - - if self.spam_content_check(title + " " + body_text): - continue - - # Try to parse published date, fallback to 'N/A' - date_str = "N/A" - if hasattr(entry, 'published_parsed') and entry.published_parsed: - dt = datetime(*entry.published_parsed[:6]) - date_str = dt.strftime("%B %d, %Y") - - item = { - "id": self.sorting.ordering_date(date_str) if date_str != "N/A" else 0, - "headlines": title, - "author": entry.get('author', 'N/A'), - "fullNews": body_text[:500] + "..." if len(body_text) > 500 else body_text, - "newsURL": link, - "newsImgURL": "N/A", - "newsDate": date_str - } - news_data.append(item) - except Exception as e: - print(f"[RSS Error] Failed to parse {feed_url}: {e}") - - # Basic deduplication across feeds - seen = set() - unique = [] - for item in news_data: - if item["newsURL"] not in seen: - seen.add(item["newsURL"]) - unique.append(item) - - return self.sorting.ordering_news(unique) diff --git a/api-service/cybernews/social_connectors/youtube_connector.py b/api-service/cybernews/social_connectors/youtube_connector.py deleted file mode 100644 index 95886f2..0000000 --- a/api-service/cybernews/social_connectors/youtube_connector.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -from datetime import datetime -from cybernews.performance import Performance -from cybernews.sorting import Sorting - -# NOTE: Requires YOUTUBE_API_KEY in your .env -# Get a free key (10,000 units/day) at: https://console.cloud.google.com -# Enable "YouTube Data API v3" in your Google Cloud project. - -class YouTubeConnector(Performance): - """ - Optional connector that uses the official YouTube Data API v3 to search - for recent cybersecurity-related videos. - - Activation: Set YOUTUBE_API_KEY in your .env file. - If the key is absent, this connector silently skips and returns an empty list. - """ - - FALLBACK_QUERY = "cybersecurity OR infosec OR CVE" - MAX_RESULTS = 15 - - def __init__(self): - super().__init__() - self.sorting = Sorting() - self.is_configured = False - self._youtube = None - - api_key = os.environ.get("YOUTUBE_API_KEY") - if not api_key: - print("[YouTube Connector] Skipping: YOUTUBE_API_KEY not found in env.") - return - - try: - from googleapiclient.discovery import build - self._youtube = build("youtube", "v3", developerKey=api_key) - self.is_configured = True - print("[YouTube Connector] Initialized successfully.") - except ImportError: - print("[YouTube Connector] Skipping: 'google-api-python-client' not installed. Run: pip install google-api-python-client") - except Exception as e: - print(f"[YouTube Connector] Initialization failed: {e}") - - def extract(self, query: str = None) -> list: - """ - Searches YouTube for recent cybersecurity videos. - Args: - query: Category-specific search string from social_sources.json api_queries. - Falls back to FALLBACK_QUERY if not provided. - Returns a list of standardized news dictionaries. - """ - if not self.is_configured: - return [] - - search_query = query or self.FALLBACK_QUERY - news_data = [] - try: - request = self._youtube.search().list( - part="snippet", - q=search_query, - type="video", - order="date", - maxResults=self.MAX_RESULTS, - relevanceLanguage="en" - ) - response = request.execute() - - for item in response.get("items", []): - snippet = item.get("snippet", {}) - video_id = item.get("id", {}).get("videoId", "") - if not video_id: - continue - - title = snippet.get("title", "").strip() - description = snippet.get("description", "").strip() - channel = snippet.get("channelTitle", "N/A") - published_at = snippet.get("publishedAt", "") - video_url = f"https://www.youtube.com/watch?v={video_id}" - - # Parse ISO 8601 date - date_str = "N/A" - try: - dt = datetime.fromisoformat(published_at.replace("Z", "+00:00")) - date_str = dt.strftime("%B %d, %Y") - except Exception: - pass - - if not title or self.spam_content_check(title + " " + description): - continue - - news_data.append({ - "id": self.sorting.ordering_date(date_str) if date_str != "N/A" else 0, - "headlines": f"[YouTube] {title}", - "author": channel, - "fullNews": description[:500] + "..." if len(description) > 500 else description, - "newsURL": video_url, - "newsImgURL": snippet.get("thumbnails", {}).get("high", {}).get("url", "N/A"), - "newsDate": date_str - }) - - except Exception as e: - print(f"[YouTube Connector] Extraction failed: {e}") - - # Deduplicate - seen = set() - unique = [item for item in news_data if not (item["newsURL"] in seen or seen.add(item["newsURL"]))] - return self.sorting.ordering_news(unique) diff --git a/api-service/cybernews/social_sources.json b/api-service/cybernews/social_sources.json deleted file mode 100644 index c2dc77b..0000000 --- a/api-service/cybernews/social_sources.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "rss": { - "feeds": [ - "https://www.reddit.com/r/cybersecurity.rss", - "https://www.reddit.com/r/netsec.rss", - "https://krebsonsecurity.com/feed/", - "https://feeds.feedburner.com/TheHackersNews", - "https://www.bleepingcomputer.com/feed/", - "https://www.cisa.gov/cybersecurity-advisories/all.xml", - "https://unit42.paloaltonetworks.com/feed/" - ] - }, - "api_queries": { - "general": "cybersecurity news infosec", - "cyberAttack": "cyber attack hacking breach intrusion", - "malware": "malware ransomware trojan spyware", - "vulnerability": "CVE vulnerability exploit patch zero-day", - "dataBreach": "data breach leak stolen credentials", - "security": "information security threat intelligence OSINT" - } -} \ No newline at end of file diff --git a/api-service/cybernews/sorting.py b/api-service/cybernews/sorting.py deleted file mode 100644 index 65143aa..0000000 --- a/api-service/cybernews/sorting.py +++ /dev/null @@ -1,75 +0,0 @@ -""" - Class for Sorting News According to the date -""" -import uuid - - -class Sorting: - """ - Months Dictionary - """ - - def __init__(self) -> None: - self._months = { - "january": "01", - "february": "02", - "march": "03", - "april": "04", - "may": "05", - "june": "06", - "july": "07", - "august": "08", - "september": "09", - "october": "10", - "november": "11", - "december": "12", - } - - """ - Giving UUID as _id for each news so that id is distinct - """ - - def _ordering_id(self, news): - for individual_news in news: - individual_news["id"] = uuid.uuid4().int - - return news - - """ - Ordering Date - """ - - def ordering_date(self, individual_news): - if individual_news == "N/A": - return 1 - - individual_news = individual_news.lower().replace(",", "").split(" ") - - try: - if individual_news[0].isnumeric(): - return int( - individual_news[2] - + self._months[individual_news[1]] - + individual_news[0] - ) - - return int( - individual_news[2] - + self._months[individual_news[0]] - + individual_news[1] - ) - - except Exception as _: - return 1 - - """ - Ordering News By Latest Date - """ - - def ordering_news(self, news): - data = sorted( - news, key=lambda individual_news: individual_news["id"], reverse=True - ) - - data = self._ordering_id(data) - return data diff --git a/api-service/db_update/Update.py b/api-service/db_update/Update.py deleted file mode 100644 index 4109397..0000000 --- a/api-service/db_update/Update.py +++ /dev/null @@ -1,133 +0,0 @@ -import sys -import os -from dotenv import dotenv_values -from pinecone import Pinecone, ServerlessSpec -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from cybernews.CyberNews import CyberNews - -PINECONE_API = dotenv_values(".env").get("PINECONE_API_KEY") - -# Configure client -pc = Pinecone(api_key=PINECONE_API) -index_name = str.lower(dotenv_values(".env").get("PINECONE_INDEX_NAME")) # pinecone index name must be in lowercase - - - - -# Different types of news -news = CyberNews() -newsBox = dict() -newsBox["general_news"] = news.get_news("general") -newsBox["cyber_attack_news"] = news.get_news("cyberAttack") -newsBox["vulnerability_news"] = news.get_news("vulnerability") -newsBox["malware_news"] = news.get_news("malware") -newsBox["security_news"] = news.get_news("security") -newsBox["data_breach_news"] = news.get_news("dataBreach") - -# Convert news articles to vectors and upsert into Pinecone -def update_database(overwrite=(len(sys.argv) > 1 and sys.argv[1] == '--overwrite')): - # Delete the index if overwrite is requested - if overwrite and index_name in pc.list_indexes().names(): - pc.delete_index(index_name) - print(f"Deleted existing index: {index_name}") - - # Create the hybrid index (metric='dotproduct' is recommended for hybrid) - if index_name not in pc.list_indexes().names(): - pc.create_index( - name=index_name, - dimension=384, - metric='dotproduct', - spec=ServerlessSpec( - cloud='aws', - region='us-east-1' - ) - ) - - # Connect to the index - index = pc.Index(index_name) - namespace = "c2si" - from sentence_transformers import SentenceTransformer, SparseEncoder - import numpy as np - - # Initialize native local embedding models - print("Loading local dense model (all-MiniLM-L6-v2)...") - dense_model = SentenceTransformer("all-MiniLM-L6-v2") - - print("Loading local sparse model (prithivida/Splade_PP_en_v2)...") - sparse_model = SparseEncoder("prithivida/Splade_PP_en_v2") - - # Track locally seen URLs to prevent processing duplicates across different sources - seen_urls = set() - all_records = [] - for news_type, articles in newsBox.items(): - if not articles: - continue - for article in articles: - url = str(article.get("newsURL", "")).strip() - - # Simple cross-source deduplication of Article URLs - if url in seen_urls: - print(f"Skipping duplicate article by URL: {url}") - continue - seen_urls.add(url) - - headlines = article.get("headlines") - full_news = article.get("fullNews") - if not headlines or not full_news: - continue - - text = str(headlines) + " " + str(full_news) - - try: - # 1. Generate Dense Vector locally - dense_vector = dense_model.encode(text).tolist() - - # 2. Generate Sparse Vector locally - emb = sparse_model.encode(text) - if hasattr(emb, 'to_dense'): - emb = emb.to_dense() - if hasattr(emb, 'cpu'): - emb = emb.cpu() - - emb_array = np.array(emb) - - # Flatten in case of batch dimension (1, vocab_size) - if len(emb_array.shape) == 2: - emb_array = emb_array[0] - - indices = np.nonzero(emb_array)[0].tolist() - values = [float(emb_array[i]) for i in indices] - sparse_vector = {"indices": indices, "values": values} - - # Construct record exactly as per Pinecone documentation - record = { - "id": str(article.get("id", "")), - "values": dense_vector, - "sparse_values": sparse_vector, - "metadata": { - "headlines": str(headlines), - "author": str(article.get("author", "Unknown")), - "fullNews": str(full_news), - "newsURL": str(article.get("newsURL", "")), - "newsImgURL": str(article.get("newsImgURL", "")), - "newsDate": str(article.get("newsDate", "")) - } - } - all_records.append(record) - print(f"Prepared article for hybrid indexing: {record['id']}") - - except Exception as e: - print(f"Error processing article {article.get('id')}: {e}") - - # Upsert all records in batches of 100 - if all_records: - batch_size = 100 - for i in range(0, len(all_records), batch_size): - batch = all_records[i : i + batch_size] - index.upsert(vectors=batch, namespace=namespace) - print(f"Successfully upserted batch {i//batch_size + 1}: {len(batch)} hybrid records into {index_name}") - print(f"Finished upserting all {len(all_records)} hybrid records.") - -if __name__ == "__main__": - update_database() diff --git a/api-service/db_update/__init__.py b/api-service/db_update/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api-service/db_update/tempCodeRunnerFile.py b/api-service/db_update/tempCodeRunnerFile.py deleted file mode 100644 index 8b13789..0000000 --- a/api-service/db_update/tempCodeRunnerFile.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/api-service/requirements.txt b/api-service/requirements.txt index 712dd84..4796ed8 100644 --- a/api-service/requirements.txt +++ b/api-service/requirements.txt @@ -1,17 +1,12 @@ python-dotenv==1.2.2 Flask==3.1.3 langchain-classic==1.0.8 -langchain-community==0.4.2 langchain-core==1.4.8 langgraph==1.2.6 huggingface_hub==1.24.0 beautifulsoup4==4.15.0 -lxml==6.1.1 httpx==0.28.1 requests==2.34.2 -feedparser==6.0.12 -google-api-python-client==2.198.0 -newsapi-python==0.2.7 redis==8.0.1 psycopg[binary]==3.3.4 pgvector==0.4.2 @@ -19,3 +14,5 @@ sentence-transformers==5.6.0 gunicorn==26.0.0 transformers==5.12.1 torch==2.12.1 +pytest==9.0.2 +pytest-mock==3.15.0 diff --git a/api-service/routes/NewsRoutes.py b/api-service/routes/NewsRoutes.py index 2147411..f3800cc 100644 --- a/api-service/routes/NewsRoutes.py +++ b/api-service/routes/NewsRoutes.py @@ -1,6 +1,5 @@ from flask import * import logging -from controllers.NewsController import NewsController from models.SubscriberModel import SubscriberDB from models.SourceModel import SourceDB from models.NewsModel import CybernewsDB @@ -48,60 +47,12 @@ def _save_history(session_id: str, history: list): def home_route(): return render_template("landing.html") -""" -set route for different LLM models -""" -@routes.route("/", methods=["GET"]) -def set_llm_route(llm_name): - if llm_name == "favicon.ico": - return "", 204 - g.news_controller = NewsController(llm_name) - return render_template("llm.html", llm_name=llm_name) - -""" -return news without considering keywords -""" -@routes.route("//news", methods=["GET"]) -def getNews_route(llm_name): - g.news_controller = NewsController(llm_name) - news = g.news_controller.getNews() - return render_template("news.html", data=news) - -""" -return news based on certain keywords -""" -@routes.route("//news_keywords", methods=["GET"]) -def getNewsWithKeywords_route(llm_name): - g.news_controller = NewsController(llm_name) - user_keywords = request.args.getlist("keywords") - data = g.news_controller.getNewsWithKeywords(user_keywords[0]) - return render_template("news_key.html", data=data, keyword=user_keywords[0]) - -""" -return news without considering keywords (NO LLM) -""" -@routes.route("/raw/news", methods=["GET"]) -def getNews_raw_route(): - g.news_controller = NewsController(None) - news = g.news_controller.getNews() - return render_template("news.html", data=news, llm_name="raw") - -""" -return news based on certain keywords (NO LLM) -""" -@routes.route("/raw/news_keywords", methods=["GET"]) -def getNewsWithKeywords_raw_route(): - g.news_controller = NewsController(None) - user_keywords = request.args.getlist("keywords") - data = g.news_controller.getNewsWithKeywords(user_keywords[0]) - return render_template("news_key.html", data=data, keyword=user_keywords[0], llm_name="raw") - """ deal requests with wrong route """ @routes.errorhandler(404) def notFound_route(error): - g.news_controller.notFound(error) + return jsonify({"error": "not found"}), 404 """ health check route diff --git a/api-service/services/NewsService.py b/api-service/services/NewsService.py deleted file mode 100644 index ab58494..0000000 --- a/api-service/services/NewsService.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import json -from dotenv import dotenv_values -from flask import jsonify -from langchain_classic.chains import LLMChain -from langchain_classic.prompts import PromptTemplate -from langchain_community.llms import HuggingFaceEndpoint - -from models.NewsModel import CybernewsDB -env_vars = dotenv_values(".env") -HUGGINGFACEHUB_API_TOKEN = env_vars.get("HUGGINGFACE_TOKEN") -# os.environ["HUGGINGFACEHUB_API_TOKEN"] = HUGGINGFACEHUB_API_TOKEN -class NewsService: - def __init__(self, model_name=None) -> None: - self.db = CybernewsDB() - self.llm = None - self.model_name = model_name - - # Only load the LLM configuration if a model_name is provided - if model_name: - with open('config/llm_config.json') as f: - llm_config = json.load(f) - - repo_id = llm_config.get(model_name) - - if not repo_id: - raise ValueError(f"Model '{model_name}' not found in llm_config.json") - - self.llm = HuggingFaceEndpoint( - repo_id=repo_id, temperature=0.5, token=HUGGINGFACEHUB_API_TOKEN - ) - self.news_format = "[title, source, date(DD/MM/YYYY), news url];" - self.news_number = 10 - - """ - Return news while checking if keyword has been specified or not - """ - - def getNews(self, user_keywords=None, llm=False): - # Fetch news data from db: - # Only fetch data with valid `author` and `newsDate` - # Drop field "id" from collection - - if user_keywords: - news_data = self.db.get_news_collections(is_keyword=True, keyword=user_keywords) - else: - news_data = self.db.get_news_collections() - - if not llm: - # Map raw database results to the standard format if LLM is skipped - return [ - { - "title": doc.get('headlines', 'No title provided'), - "source": doc.get('author', 'No source provided'), - "date": doc.get('newsDate', 'No date provided'), - "url": doc.get('newsURL', 'No URL provided'), - } - for doc in news_data[:self.news_number] - ] - - news_data = news_data[:50] # limit the number of news to 50 , as LLMs have a context limit - - template = """Question: {question} - Answer: Let's think step by step.""" - - prompt = PromptTemplate.from_template(template) - - # Determine which messages template to load - if user_keywords: - messages_template_path = 'prompts/withkey.json' - else: - messages_template_path = 'prompts/withoutkey.json' - - # Load the messages template from the JSON file - messages = self.load_json_file(messages_template_path) - - # Replace placeholders in the messages - for message in messages: - if message['role'] == 'user' and '' in message['content']: - message['content'] = message['content'].replace('', str(news_data)) - if user_keywords and message['role'] == 'user' and '' in message['content']: - message['content'] = message['content'].replace('', str(user_keywords)) - if message['role'] == 'user' and '{news_format}' in message['content']: - message['content'] = message['content'].replace('{news_format}', self.news_format) - if message['role'] == 'user' and '{news_number}' in message['content']: - message['content'] = message['content'].replace('{news_number}', str(self.news_number)) - - # Create the LLMChain with the prompt and llm - llm_chain = LLMChain(prompt=prompt, llm=self.llm) - output = llm_chain.invoke(messages) - - # Convert news data into JSON format - news_JSON = self.toJSON(output['text']) - - return news_JSON - - - """ - deal requests with wrong route - """ - - def notFound(self, error): - return jsonify({"error": error}), 404 - - """ - Load JSON file - """ - - def load_json_file(self, file_path): - with open(file_path, 'r', encoding='utf-8') as file: - data = json.load(file) - return data - - """ - Convert news given by Huggingface endpoint API into JSON format. - """ - - def toJSON(self, data: str): - if len(data) == 0: - return {} - news_list = data.split("\n") - news_list_json = [] - news_list.pop(0) - for item in news_list: - # Avoid dirty data - if len(item) == 0: - continue - # Remove leading and trailing square brackets and split by comma and strip extra spaces - data_list = [item.strip().strip('"') for item in item.strip('[').strip(']').split(',')] - data_list = [val.strip() for val in data_list] - - for i in data_list: - print(i) - print("----") - - print(data_list) - # Assign default values for missing elements - start_index = data_list[0].find('[') if len(data_list) > 0 else -1 - end_index = data_list[3].find(']') if len(data_list) > 3 else -1 - title = data_list[0][start_index+1:] if len(data_list) > 0 else "No title provided" - source = data_list[1] if len(data_list) > 1 else "No source provided" - date = data_list[2] if len(data_list) > 2 else "No date provided" - url = data_list[3][:end_index-1] if len(data_list) > 3 else "No URL provided" - - news_item = { - "title": title, - "source": source, - "date": date, - "url": url, - } - news_list_json.append(news_item) - - news_list_json.pop() - return news_list_json diff --git a/api-service/templates/llm.html b/api-service/templates/llm.html deleted file mode 100644 index 41eecfc..0000000 --- a/api-service/templates/llm.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - b0bot - - - - - -

Welcome to b0bot!

-

You are now using an unknown LLM as your base LLM

- -

Two available URL paths:

-
    -
  • /news
  • -
  • -
    - - - -
    -
  • -
- - - - - \ No newline at end of file diff --git a/api-service/templates/news.html b/api-service/templates/news.html deleted file mode 100644 index d366e84..0000000 --- a/api-service/templates/news.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - News Feed - - - - -
-

Latest News

-
- {% for item in data %} -
-
-
-
{{ item.title }}
-
{{ item.date }}
-

{{ item.source }}

- {% if item.url != 'N/A' %} - Read more - {% else %} - No URL provided - {% endif %} -
-
-
- {% endfor %} -
-
- - - - - diff --git a/api-service/templates/news_key.html b/api-service/templates/news_key.html deleted file mode 100644 index a0e96b1..0000000 --- a/api-service/templates/news_key.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - News Feed - {{ keyword }} - - - - -
-

Latest News related to {{ keyword }}

-
- {% for item in data %} -
-
-
-
{{ item.title }}
-
{{ item.date }}
-

{{ item.source }}

- {% if item.url != 'N/A' %} - Read more - {% else %} - No URL provided - {% endif %} -
-
-
- {% endfor %} -
-
- - - - - diff --git a/api-service/tests/unitTest_cybernews.py b/api-service/tests/unitTest_cybernews.py deleted file mode 100644 index 4162958..0000000 --- a/api-service/tests/unitTest_cybernews.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - Unittest class for CyberNews -""" -import unittest - -from cybernews.CyberNews import CyberNews - - -class TestCyberNews(unittest.TestCase): - def setUp(self): - self.news = CyberNews() - self.valid_news = self.news.get_news_types - self.invalid_news = ["", "Invalid"] - - def test_init(self): - self.assertIsNotNone(self.news.get_news_types) - - def test_get_news(self): - [self.assertIsNotNone(self.news.get_news(news)) for news in self.valid_news] - - def test_get_news_invalid_type(self): - for news in self.invalid_news: - with self.assertRaises(ValueError): - self.news.get_news(news) - - -if __name__ == "__main__": - unittest.main() diff --git a/app.py b/app.py deleted file mode 100644 index 09a0d3b..0000000 --- a/app.py +++ /dev/null @@ -1,20 +0,0 @@ -from dotenv import load_dotenv -from flask import * -from langchain_classic.prompts import PromptTemplate -from routes.NewsRoutes import routes - -# Load environment variables -load_dotenv() - -# `__name__` indicates the unique name of the current module -app = Flask(__name__) - -# Register routes -app.register_blueprint(routes) - - -if __name__ == "__main__": - # app.run(debug=True, host="0.0.0.0") - app.run(debug=True) - - diff --git a/assets/arch.png b/assets/arch.png deleted file mode 100644 index 4a424e9..0000000 Binary files a/assets/arch.png and /dev/null differ diff --git a/assets/chat-search.png b/assets/chat-search.png new file mode 100644 index 0000000..f43362c Binary files /dev/null and b/assets/chat-search.png differ diff --git a/assets/chat.png b/assets/chat.png new file mode 100644 index 0000000..0d2206a Binary files /dev/null and b/assets/chat.png differ diff --git a/assets/dashboard.png b/assets/dashboard.png new file mode 100644 index 0000000..adc7d64 Binary files /dev/null and b/assets/dashboard.png differ diff --git a/assets/db_arch.png b/assets/db_arch.png deleted file mode 100644 index 05932f3..0000000 Binary files a/assets/db_arch.png and /dev/null differ diff --git a/assets/home.png b/assets/home.png deleted file mode 100644 index a3c145b..0000000 Binary files a/assets/home.png and /dev/null differ diff --git a/assets/landing.png b/assets/landing.png new file mode 100644 index 0000000..3fe5ae2 Binary files /dev/null and b/assets/landing.png differ diff --git a/assets/llm.png b/assets/llm.png deleted file mode 100644 index b18996b..0000000 Binary files a/assets/llm.png and /dev/null differ diff --git a/assets/news.png b/assets/news.png deleted file mode 100644 index 4f4f5a7..0000000 Binary files a/assets/news.png and /dev/null differ diff --git a/assets/news_keywords.png b/assets/news_keywords.png deleted file mode 100644 index 1c5ffd6..0000000 Binary files a/assets/news_keywords.png and /dev/null differ diff --git a/assets/sources.png b/assets/sources.png new file mode 100644 index 0000000..d047398 Binary files /dev/null and b/assets/sources.png differ diff --git a/config/Database.py b/config/Database.py deleted file mode 100644 index dc9e5a0..0000000 --- a/config/Database.py +++ /dev/null @@ -1,11 +0,0 @@ -from dotenv import dotenv_values -from pinecone import Pinecone -import os -import sys - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -PINECONE_API = dotenv_values(".env").get("PINECONE_API_KEY") - -client = Pinecone(api_key=PINECONE_API) -index_name = "cybernews-index" \ No newline at end of file diff --git a/controllers/NewsController.py b/controllers/NewsController.py deleted file mode 100644 index 9417cd6..0000000 --- a/controllers/NewsController.py +++ /dev/null @@ -1,30 +0,0 @@ -from services.NewsService import NewsService - -class NewsController: - def __init__(self, model_name=None): - self.news_service = NewsService(model_name) - - """ - return news without considering keywords - """ - - def getNews(self, llm=True): - if self.news_service.model_name is None: - return self.news_service.getNews(llm=False) - return self.news_service.getNews(llm=llm) - - """ - return news based on certain keywords - """ - - def getNewsWithKeywords(self, user_keywords, llm=True): - if self.news_service.model_name is None: - return self.news_service.getNews(user_keywords, llm=False) - return self.news_service.getNews(user_keywords, llm=llm) - - """ - deal requests with wrong route - """ - - def notFound(self, error): - return self.news_service.notFound(error) diff --git a/db_update/Update.py b/db_update/Update.py deleted file mode 100644 index 4109397..0000000 --- a/db_update/Update.py +++ /dev/null @@ -1,133 +0,0 @@ -import sys -import os -from dotenv import dotenv_values -from pinecone import Pinecone, ServerlessSpec -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from cybernews.CyberNews import CyberNews - -PINECONE_API = dotenv_values(".env").get("PINECONE_API_KEY") - -# Configure client -pc = Pinecone(api_key=PINECONE_API) -index_name = str.lower(dotenv_values(".env").get("PINECONE_INDEX_NAME")) # pinecone index name must be in lowercase - - - - -# Different types of news -news = CyberNews() -newsBox = dict() -newsBox["general_news"] = news.get_news("general") -newsBox["cyber_attack_news"] = news.get_news("cyberAttack") -newsBox["vulnerability_news"] = news.get_news("vulnerability") -newsBox["malware_news"] = news.get_news("malware") -newsBox["security_news"] = news.get_news("security") -newsBox["data_breach_news"] = news.get_news("dataBreach") - -# Convert news articles to vectors and upsert into Pinecone -def update_database(overwrite=(len(sys.argv) > 1 and sys.argv[1] == '--overwrite')): - # Delete the index if overwrite is requested - if overwrite and index_name in pc.list_indexes().names(): - pc.delete_index(index_name) - print(f"Deleted existing index: {index_name}") - - # Create the hybrid index (metric='dotproduct' is recommended for hybrid) - if index_name not in pc.list_indexes().names(): - pc.create_index( - name=index_name, - dimension=384, - metric='dotproduct', - spec=ServerlessSpec( - cloud='aws', - region='us-east-1' - ) - ) - - # Connect to the index - index = pc.Index(index_name) - namespace = "c2si" - from sentence_transformers import SentenceTransformer, SparseEncoder - import numpy as np - - # Initialize native local embedding models - print("Loading local dense model (all-MiniLM-L6-v2)...") - dense_model = SentenceTransformer("all-MiniLM-L6-v2") - - print("Loading local sparse model (prithivida/Splade_PP_en_v2)...") - sparse_model = SparseEncoder("prithivida/Splade_PP_en_v2") - - # Track locally seen URLs to prevent processing duplicates across different sources - seen_urls = set() - all_records = [] - for news_type, articles in newsBox.items(): - if not articles: - continue - for article in articles: - url = str(article.get("newsURL", "")).strip() - - # Simple cross-source deduplication of Article URLs - if url in seen_urls: - print(f"Skipping duplicate article by URL: {url}") - continue - seen_urls.add(url) - - headlines = article.get("headlines") - full_news = article.get("fullNews") - if not headlines or not full_news: - continue - - text = str(headlines) + " " + str(full_news) - - try: - # 1. Generate Dense Vector locally - dense_vector = dense_model.encode(text).tolist() - - # 2. Generate Sparse Vector locally - emb = sparse_model.encode(text) - if hasattr(emb, 'to_dense'): - emb = emb.to_dense() - if hasattr(emb, 'cpu'): - emb = emb.cpu() - - emb_array = np.array(emb) - - # Flatten in case of batch dimension (1, vocab_size) - if len(emb_array.shape) == 2: - emb_array = emb_array[0] - - indices = np.nonzero(emb_array)[0].tolist() - values = [float(emb_array[i]) for i in indices] - sparse_vector = {"indices": indices, "values": values} - - # Construct record exactly as per Pinecone documentation - record = { - "id": str(article.get("id", "")), - "values": dense_vector, - "sparse_values": sparse_vector, - "metadata": { - "headlines": str(headlines), - "author": str(article.get("author", "Unknown")), - "fullNews": str(full_news), - "newsURL": str(article.get("newsURL", "")), - "newsImgURL": str(article.get("newsImgURL", "")), - "newsDate": str(article.get("newsDate", "")) - } - } - all_records.append(record) - print(f"Prepared article for hybrid indexing: {record['id']}") - - except Exception as e: - print(f"Error processing article {article.get('id')}: {e}") - - # Upsert all records in batches of 100 - if all_records: - batch_size = 100 - for i in range(0, len(all_records), batch_size): - batch = all_records[i : i + batch_size] - index.upsert(vectors=batch, namespace=namespace) - print(f"Successfully upserted batch {i//batch_size + 1}: {len(batch)} hybrid records into {index_name}") - print(f"Finished upserting all {len(all_records)} hybrid records.") - -if __name__ == "__main__": - update_database() diff --git a/docker-compose.yml b/docker-compose.yml index 924e7f1..5407cab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,7 +40,6 @@ services: DATABASE_URL: postgresql://${POSTGRES_USER:-b0bot}:${POSTGRES_PASSWORD:-b0bot}@postgres:5432/${POSTGRES_DB:-b0bot} REDIS_URL: redis://redis:6379/0 HUGGINGFACE_TOKEN: ${HUGGINGFACE_TOKEN:-} - HF_HUB_OFFLINE: "1" depends_on: postgres: condition: service_healthy diff --git a/models/NewsModel.py b/models/NewsModel.py deleted file mode 100644 index c859a3e..0000000 --- a/models/NewsModel.py +++ /dev/null @@ -1,169 +0,0 @@ -from config.Database import client -from datetime import datetime - -class CybernewsDB: - def __init__(self): - from sentence_transformers import SentenceTransformer, SparseEncoder - self.client = client - self.index_name = "cybernews-hybrid-test-2" - self.namespace = "c2si" - self.index = self.client.Index(self.index_name) - - # Initialize native local embedding models - self.dense_model = SentenceTransformer("all-MiniLM-L6-v2") - self.sparse_model = SparseEncoder("prithivida/Splade_PP_en_v2") - - @staticmethod - def parse_date(date_str): - if not date_str or str(date_str).strip() == "": - return datetime.min - - # Try formats: 'DD/MM/YYYY' or 'Month DD, YYYY' - for fmt in ('%d/%m/%Y', '%b %d, %Y'): - try: - return datetime.strptime(str(date_str), fmt) - except (ValueError, TypeError): - continue - return datetime.min - - def extract_metadata(self, nested_dict): - metadata_list = [] - for key, value in nested_dict.items(): - if isinstance(value, dict) and 'metadata' in value: - metadata = value['metadata'] - if isinstance(metadata, dict): - metadata_list.append(metadata) - return metadata_list - - def fetch_all_from_namespace(self, batch_size=100): - final_list = [] - - # Use list() generator to get all IDs page by page - for ids_page in self.index.list(namespace=self.namespace): - if not ids_page: - continue - - # Fetch metadata in batches for each page of IDs - for i in range(0, len(ids_page), batch_size): - batch_ids = ids_page[i:i + batch_size] - response = self.index.fetch(ids=batch_ids, namespace=self.namespace) - - vectors = response.vectors - for vid in batch_ids: - if vid in vectors: - final_list.append(vectors[vid].metadata) - - final_list.sort(key=lambda x: CybernewsDB.parse_date(x.get('newsDate')), reverse=True) - return final_list - - def fetch_keyword_from_namespace(self, keyword, alpha=0.5): - if not keyword: - return [] - - # 1. Generate Dense Vector locally - dense_vector = self.dense_model.encode(keyword).tolist() - dense_query_embedding = [{'values': dense_vector}] - - # 2. Generate Sparse Vector locally - import numpy as np - emb = self.sparse_model.encode(keyword) - if hasattr(emb, 'to_dense'): - emb = emb.to_dense() - if hasattr(emb, 'cpu'): - emb = emb.cpu() - - emb_array = np.array(emb) - if len(emb_array.shape) == 2: - emb_array = emb_array[0] - - indices = np.nonzero(emb_array)[0].tolist() - values = [float(emb_array[i]) for i in indices] - sparse_query_embedding = [{'sparse_indices': indices, 'sparse_values': values}] - - # 3. Execute Hybrid Query - final_list = [] - for d, s in zip(dense_query_embedding, sparse_query_embedding): - # Apply alpha weighting - scaled_dense = [v * alpha for v in d['values']] - scaled_sparse = { - "indices": s['sparse_indices'], - "values": [v * (1 - alpha) for v in s['sparse_values']] - } - - response = self.index.query( - namespace=self.namespace, - top_k=50, - vector=scaled_dense, - sparse_vector=scaled_sparse, - include_metadata=True, - include_values=False - ) - - # Map doc to metadata format - for match in response.matches: - metadata = match.metadata - # Add score for reference if needed - metadata['_score'] = match.score - final_list.append(metadata) - - if not final_list: - return [] - - final_list.sort(key=lambda x: (x.get('_score', 0), CybernewsDB.parse_date(x.get('newsDate'))), reverse=True) - return final_list - - def fetch_lexical_from_namespace(self, keyword): - if not keyword: - return [] - - # 1. Generate Sparse Vector locally - import numpy as np - emb = self.sparse_model.encode(keyword) - if hasattr(emb, 'to_dense'): - emb = emb.to_dense() - if hasattr(emb, 'cpu'): - emb = emb.cpu() - - emb_array = np.array(emb) - if len(emb_array.shape) == 2: - emb_array = emb_array[0] - - indices = np.nonzero(emb_array)[0].tolist() - values = [float(emb_array[i]) for i in indices] - sparse_query_embedding = [{'sparse_indices': indices, 'sparse_values': values}] - - # 2. Execute Lexical Query (Provide dummy zero vector to satisfy Pinecone requirement) - final_list = [] - dummy_dense = [0.0] * 384 # Match your index dimension - - for s in sparse_query_embedding: - response = self.index.query( - namespace=self.namespace, - top_k=50, - vector=dummy_dense, # Satisfy "dense vector required" rule - sparse_vector={ - "indices": s['sparse_indices'], - "values": s['sparse_values'] - }, - include_metadata=True, - include_values=False - ) - - for match in response.matches: - metadata = match.metadata - metadata['_score'] = match.score - final_list.append(metadata) - - if not final_list: - return [] - - final_list.sort(key=lambda x: (x.get('_score', 0), CybernewsDB.parse_date(x.get('newsDate'))), reverse=True) - return final_list - - def get_news_collections(self, is_keyword=False, keyword=None, search_type="hybrid", alpha=0.3): - if is_keyword and keyword: - if search_type == "lexical": - return self.fetch_lexical_from_namespace(keyword) - return self.fetch_keyword_from_namespace(keyword, alpha=alpha) - else: - return self.fetch_all_from_namespace() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7182092..0000000 --- a/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -python-dotenv -Flask -langchain -openai==0.27.8 -gunicorn==19.7.1 -pymongo==4.4.0 -dnspython==2.3.0 -huggingface_hub -beautifulsoup4==4.11.2 -lxml -httpx==0.23.3 -requests -pinecone -sentence-transformers -feedparser -google-api-python-client -newsapi-python -langchain-classic -langchain-community \ No newline at end of file diff --git a/routes/NewsRoutes.py b/routes/NewsRoutes.py deleted file mode 100644 index 7164eb1..0000000 --- a/routes/NewsRoutes.py +++ /dev/null @@ -1,75 +0,0 @@ -from flask import * -from controllers.NewsController import NewsController -routes = Blueprint("routes", __name__) -# news_controller = NewsController("mistralai") # default model name - -""" -home page route -""" -@routes.route("/", methods=["GET"]) -def home_route(): - return render_template("home.html") - - -""" -set route for different LLM models -""" -@routes.route("/", methods=["GET"]) -def set_llm_route(llm_name): - if llm_name == "favicon.ico": - return "", 204 # No Content response for favicon requests - g.news_controller = NewsController(llm_name) - return render_template("llm.html", llm_name=llm_name) - - -""" -return news without considering keywords -""" -@routes.route("//news", methods=["GET"]) -def getNews_route(llm_name): - g.news_controller = NewsController(llm_name) - news = g.news_controller.getNews() - return render_template("news.html", data=news) - - -""" -return news based on certain keywords -""" -@routes.route("//news_keywords", methods=["GET"]) -def getNewsWithKeywords_route(llm_name): - # get list of keywords as argument from User's request - g.news_controller = NewsController(llm_name) - user_keywords = request.args.getlist("keywords") - data = g.news_controller.getNewsWithKeywords(user_keywords[0]) - return render_template("news_key.html", data=data, keyword=user_keywords[0]) - - -""" -return news without considering keywords (NO LLM) -""" -@routes.route("/raw/news", methods=["GET"]) -def getNews_raw_route(): - # Instantiate without a model to bypass LLM initialization entirely - g.news_controller = NewsController(None) - news = g.news_controller.getNews() - return render_template("news.html", data=news, llm_name="raw") - - -""" -return news based on certain keywords (NO LLM) -""" -@routes.route("/raw/news_keywords", methods=["GET"]) -def getNewsWithKeywords_raw_route(): - # Instantiate without a model to bypass LLM initialization entirely - g.news_controller = NewsController(None) - user_keywords = request.args.getlist("keywords") - data = g.news_controller.getNewsWithKeywords(user_keywords[0]) - return render_template("news_key.html", data=data, keyword=user_keywords[0], llm_name="raw") - - -""" -deal requests with wrong route -""" -@routes.errorhandler(404) -def notFound_route(error): - g.news_controller.notFound(error) diff --git a/services/NewsService.py b/services/NewsService.py deleted file mode 100644 index ab58494..0000000 --- a/services/NewsService.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import json -from dotenv import dotenv_values -from flask import jsonify -from langchain_classic.chains import LLMChain -from langchain_classic.prompts import PromptTemplate -from langchain_community.llms import HuggingFaceEndpoint - -from models.NewsModel import CybernewsDB -env_vars = dotenv_values(".env") -HUGGINGFACEHUB_API_TOKEN = env_vars.get("HUGGINGFACE_TOKEN") -# os.environ["HUGGINGFACEHUB_API_TOKEN"] = HUGGINGFACEHUB_API_TOKEN -class NewsService: - def __init__(self, model_name=None) -> None: - self.db = CybernewsDB() - self.llm = None - self.model_name = model_name - - # Only load the LLM configuration if a model_name is provided - if model_name: - with open('config/llm_config.json') as f: - llm_config = json.load(f) - - repo_id = llm_config.get(model_name) - - if not repo_id: - raise ValueError(f"Model '{model_name}' not found in llm_config.json") - - self.llm = HuggingFaceEndpoint( - repo_id=repo_id, temperature=0.5, token=HUGGINGFACEHUB_API_TOKEN - ) - self.news_format = "[title, source, date(DD/MM/YYYY), news url];" - self.news_number = 10 - - """ - Return news while checking if keyword has been specified or not - """ - - def getNews(self, user_keywords=None, llm=False): - # Fetch news data from db: - # Only fetch data with valid `author` and `newsDate` - # Drop field "id" from collection - - if user_keywords: - news_data = self.db.get_news_collections(is_keyword=True, keyword=user_keywords) - else: - news_data = self.db.get_news_collections() - - if not llm: - # Map raw database results to the standard format if LLM is skipped - return [ - { - "title": doc.get('headlines', 'No title provided'), - "source": doc.get('author', 'No source provided'), - "date": doc.get('newsDate', 'No date provided'), - "url": doc.get('newsURL', 'No URL provided'), - } - for doc in news_data[:self.news_number] - ] - - news_data = news_data[:50] # limit the number of news to 50 , as LLMs have a context limit - - template = """Question: {question} - Answer: Let's think step by step.""" - - prompt = PromptTemplate.from_template(template) - - # Determine which messages template to load - if user_keywords: - messages_template_path = 'prompts/withkey.json' - else: - messages_template_path = 'prompts/withoutkey.json' - - # Load the messages template from the JSON file - messages = self.load_json_file(messages_template_path) - - # Replace placeholders in the messages - for message in messages: - if message['role'] == 'user' and '' in message['content']: - message['content'] = message['content'].replace('', str(news_data)) - if user_keywords and message['role'] == 'user' and '' in message['content']: - message['content'] = message['content'].replace('', str(user_keywords)) - if message['role'] == 'user' and '{news_format}' in message['content']: - message['content'] = message['content'].replace('{news_format}', self.news_format) - if message['role'] == 'user' and '{news_number}' in message['content']: - message['content'] = message['content'].replace('{news_number}', str(self.news_number)) - - # Create the LLMChain with the prompt and llm - llm_chain = LLMChain(prompt=prompt, llm=self.llm) - output = llm_chain.invoke(messages) - - # Convert news data into JSON format - news_JSON = self.toJSON(output['text']) - - return news_JSON - - - """ - deal requests with wrong route - """ - - def notFound(self, error): - return jsonify({"error": error}), 404 - - """ - Load JSON file - """ - - def load_json_file(self, file_path): - with open(file_path, 'r', encoding='utf-8') as file: - data = json.load(file) - return data - - """ - Convert news given by Huggingface endpoint API into JSON format. - """ - - def toJSON(self, data: str): - if len(data) == 0: - return {} - news_list = data.split("\n") - news_list_json = [] - news_list.pop(0) - for item in news_list: - # Avoid dirty data - if len(item) == 0: - continue - # Remove leading and trailing square brackets and split by comma and strip extra spaces - data_list = [item.strip().strip('"') for item in item.strip('[').strip(']').split(',')] - data_list = [val.strip() for val in data_list] - - for i in data_list: - print(i) - print("----") - - print(data_list) - # Assign default values for missing elements - start_index = data_list[0].find('[') if len(data_list) > 0 else -1 - end_index = data_list[3].find(']') if len(data_list) > 3 else -1 - title = data_list[0][start_index+1:] if len(data_list) > 0 else "No title provided" - source = data_list[1] if len(data_list) > 1 else "No source provided" - date = data_list[2] if len(data_list) > 2 else "No date provided" - url = data_list[3][:end_index-1] if len(data_list) > 3 else "No URL provided" - - news_item = { - "title": title, - "source": source, - "date": date, - "url": url, - } - news_list_json.append(news_item) - - news_list_json.pop() - return news_list_json diff --git a/static/js/content.js b/static/js/content.js deleted file mode 100644 index 300a98c..0000000 --- a/static/js/content.js +++ /dev/null @@ -1,35 +0,0 @@ -// static/js/content.js -function renderContent(baseLLM) { - const content = { - gemma: { - title: "Welcome to b0bot!", - subtitle: "You are now using Gemma-2b as your base LLM", - newsPath: "/gemma/news", - newsKeywordsPath: "/gemma/news_keywords?keywords=" - }, - llama: { - title: "Welcome to b0bot!", - subtitle: "You are now using Llama-3 as your base LLM", - newsPath: "/llama/news", - newsKeywordsPath: "/llama/news_keywords?keywords=" - }, - mistralai: { - title: "Welcome to b0bot!", - subtitle: "You are now using MistralAI as your base LLM", - newsPath: "/mistralai/news", - newsKeywordsPath: "/mistralai/news_keywords?keywords=" - } - }; - - const selectedContent = content[baseLLM]; - - if (selectedContent) { - document.getElementById('title').innerText = selectedContent.title; - document.getElementById('subtitle').innerText = selectedContent.subtitle; - document.getElementById('newsLink').href = selectedContent.newsPath; - document.getElementById('newsKeywordsForm').action = selectedContent.newsKeywordsPath; - } else { - document.getElementById('title').innerText = "Welcome to b0bot!"; - document.getElementById('subtitle').innerText = "Please provide a valid LLM."; - } -} diff --git a/templates/home.html b/templates/home.html deleted file mode 100644 index 5491358..0000000 --- a/templates/home.html +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - b0bot - - - - -

Welcome to b0bot!

-

Your CyberSecurity News API for Automated Bots

- -

Raw Database Search (Extremely Fast, No LLM):

- - -

3 Available LLMs to Choose From:

- - - - \ No newline at end of file diff --git a/templates/llm.html b/templates/llm.html deleted file mode 100644 index 41eecfc..0000000 --- a/templates/llm.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - b0bot - - - - - -

Welcome to b0bot!

-

You are now using an unknown LLM as your base LLM

- -

Two available URL paths:

-
    -
  • /news
  • -
  • -
    - - - -
    -
  • -
- - - - - \ No newline at end of file