Compare commits
10 Commits
876cfddce7
...
8030ef8efe
| Author | SHA1 | Date | |
|---|---|---|---|
| 8030ef8efe | |||
| 1271f0b21b | |||
| ef51ef635c | |||
| 930189ac4f | |||
| db7bbf5907 | |||
| 6b8a87f9ef | |||
| e9785b3ea2 | |||
| b41a43bf89 | |||
| 0fa94d3ee3 | |||
| 271ede7845 |
157
.gitea/workflows/ci.yml
Normal file
157
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://git.example.com
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: gitea-job-image
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
run: |
|
||||||
|
rm -rf $GITHUB_WORKSPACE/*
|
||||||
|
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
||||||
|
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Run ruff (Python lint)
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ -f pyproject.toml ]]; then
|
||||||
|
pip3 install ruff
|
||||||
|
ruff check .
|
||||||
|
else
|
||||||
|
echo "No Python project detected, skipping ruff"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run npm lint (JS/TS)
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ -f package.json ]]; then
|
||||||
|
npm ci
|
||||||
|
npm run lint --if-present || true
|
||||||
|
else
|
||||||
|
echo "No Node.js project detected, skipping npm lint"
|
||||||
|
fi
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: gitea-job-image
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
run: |
|
||||||
|
rm -rf $GITHUB_WORKSPACE/*
|
||||||
|
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
||||||
|
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Run pytest (Python)
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ -f pyproject.toml ]]; then
|
||||||
|
python3 -m pip install --upgrade pip
|
||||||
|
pip3 install -e ".[dev]" 2>/dev/null || pip3 install -e . 2>/dev/null || true
|
||||||
|
pip3 install pytest
|
||||||
|
pytest tests/ -v --tb=short 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "No Python project detected, skipping pytest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run npm test (JS/TS)
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ -f package.json ]]; then
|
||||||
|
npm ci
|
||||||
|
npm run test --if-present || true
|
||||||
|
else
|
||||||
|
echo "No Node.js project detected, skipping npm test"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Go tests
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ -f go.mod ]]; then
|
||||||
|
go test ./...
|
||||||
|
else
|
||||||
|
echo "No Go project detected, skipping go test"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
run: |
|
||||||
|
rm -rf $GITHUB_WORKSPACE/*
|
||||||
|
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
||||||
|
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Build Docker images
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
DOCKERFILES=$(find . -type f \( -name "Dockerfile" -o -name "dockerfile" \) 2>/dev/null)
|
||||||
|
if [ -z "$DOCKERFILES" ]; then
|
||||||
|
echo "No Dockerfile found, skipping docker build"
|
||||||
|
else
|
||||||
|
echo "Found Dockerfiles:"
|
||||||
|
echo "$DOCKERFILES"
|
||||||
|
FAIL=0
|
||||||
|
while IFS= read -r df; do
|
||||||
|
DIR=$(dirname "$df")
|
||||||
|
NAME=$(basename "$DIR")
|
||||||
|
echo "Building $DIR/dockerfile as ${NAME}:test..."
|
||||||
|
docker build -t "${NAME}:test" "$DIR/" || FAIL=1
|
||||||
|
done <<< "$DOCKERFILES"
|
||||||
|
if [ $FAIL -ne 0 ]; then
|
||||||
|
echo "One or more Docker builds failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "All Docker images built successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: gitea-job-image
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
run: |
|
||||||
|
rm -rf $GITHUB_WORKSPACE/*
|
||||||
|
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
||||||
|
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Run bandit (Python SAST)
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ -f pyproject.toml ]]; then
|
||||||
|
pip3 install bandit
|
||||||
|
bandit -r . --severity-level high --confidence-level high --exclude tests/,test_*
|
||||||
|
else
|
||||||
|
echo "No Python project detected, skipping bandit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run npm audit (JS/TS)
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ -f package.json ]]; then
|
||||||
|
npm ci
|
||||||
|
npm audit --audit-level=high 2>/dev/null || echo "npm audit: vulnerabilities found (non-blocking)"
|
||||||
|
else
|
||||||
|
echo "No Node.js project detected, skipping npm audit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-result:
|
||||||
|
needs: [lint, test, docker-build, security]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: gitea-job-image
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Summary
|
||||||
|
run: echo "All CI checks completed"
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -24,3 +24,9 @@ nohup.out
|
|||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
pyvenv.cfg
|
||||||
|
|
||||||
|
processed_articles_cache.json
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Jarian Cottingham
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import chromadb
|
import chromadb
|
||||||
from flask import Flask, request, jsonify, send_from_directory
|
from flask import Flask, request, jsonify, send_from_directory
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
12
README.md
12
README.md
@ -131,9 +131,19 @@ docker run -p 5008:5008 stockdocs-article-server
|
|||||||
# Continue for other components...
|
# Continue for other components...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r scraper/requirements_clean.txt pytest
|
||||||
|
pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Unit tests cover the scraper's article-processing cache: load/save
|
||||||
|
roundtrips, processing status tracking, and progress computation.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.6+
|
- Python 3.9+
|
||||||
- Flask 2.3.3
|
- Flask 2.3.3
|
||||||
- Various NLP and ML libraries
|
- Various NLP and ML libraries
|
||||||
- Docker (for containerized deployment)
|
- Docker (for containerized deployment)
|
||||||
|
|||||||
277
agent
277
agent
@ -1,277 +0,0 @@
|
|||||||
# StockDocs Project Overview
|
|
||||||
|
|
||||||
This document provides a comprehensive overview of the StockDocs financial news analysis platform, including detailed information about each project component, file structures, and how to begin working with the system.
|
|
||||||
|
|
||||||
## Project Architecture
|
|
||||||
|
|
||||||
StockDocs is a sophisticated system designed to collect, process, and analyze financial news from multiple sources. The platform consists of several interconnected components that work together to transform raw news content into valuable market intelligence.
|
|
||||||
|
|
||||||
```
|
|
||||||
+--------------+ +--------------+ +-------------------+
|
|
||||||
| Scraper | | Article | | AI |
|
|
||||||
| (RSS Feeds) |--> | Server |--> | Processor |
|
|
||||||
| | | | | |
|
|
||||||
+--------------+ +--------------+ +-------------------+
|
|
||||||
| |
|
|
||||||
v v
|
|
||||||
+--------------+ +-------------------+
|
|
||||||
| Embedding | | MCPServer |
|
|
||||||
| Service | | (Financial Data) |
|
|
||||||
| | | |
|
|
||||||
+--------------+ +-------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Components
|
|
||||||
|
|
||||||
### 1. Scraper
|
|
||||||
**Location:** `scraper/`
|
|
||||||
|
|
||||||
The Scraper component is responsible for collecting financial news and articles from multiple sources including major news outlets, financial publications, and market analysis services. It uses RSS feeds to gather content and stores the articles in a structured directory hierarchy for easy access by other components of the system.
|
|
||||||
|
|
||||||
#### Key Features:
|
|
||||||
- RSS Feed Integration: Supports multiple financial news sources through RSS feeds
|
|
||||||
- Automated Scraping: Regularly fetches and processes new articles from configured feeds
|
|
||||||
- Structured Storage: Organizes articles in a directory structure by news outlet
|
|
||||||
- Duplicate Detection: Prevents re-processing of already collected articles
|
|
||||||
- Caching Mechanism: Maintains a cache of processed articles to optimize performance
|
|
||||||
|
|
||||||
#### File Structure:
|
|
||||||
```
|
|
||||||
scraper/
|
|
||||||
├── rss_feeds.json # Configuration file with RSS feed URLs
|
|
||||||
├── scraper.py # Main scraping logic
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── dockerfile # Docker configuration
|
|
||||||
├── dockerfile-selenium # Dockerfile for selenium-based scraping
|
|
||||||
├── articles/ # Directory where scraped articles are stored
|
|
||||||
│ ├── Reuters – Business News/
|
|
||||||
│ │ ├── article1.txt
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── Associated Press – Business/
|
|
||||||
│ │ ├── article1.txt
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── ...
|
|
||||||
├── processed_articles_cache.json # Cache of already processed articles
|
|
||||||
└── pyvenv.cfg # Python virtual environment configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Key Files:
|
|
||||||
- `scraper.py`: Main scraping logic with parallel processing capabilities
|
|
||||||
- `rss_feeds.json`: Configuration file with RSS feed URLs for 60+ news outlets
|
|
||||||
- `articles/`: Directory structure for storing scraped articles organized by news source
|
|
||||||
|
|
||||||
### 2. Article Server
|
|
||||||
**Location:** `articleServer/`
|
|
||||||
|
|
||||||
A Flask-based HTTP server that provides access to news articles stored in a directory structure, with time-based filtering and outlet-specific querying capabilities.
|
|
||||||
|
|
||||||
#### Key Features:
|
|
||||||
- Time-based filtering: Query articles by hour, day, week, or month
|
|
||||||
- Outlet-specific querying: Filter articles by news source
|
|
||||||
- Content retrieval: Get full article content by file path
|
|
||||||
- RESTful API: Clean interfaces for integration with external systems
|
|
||||||
|
|
||||||
#### File Structure:
|
|
||||||
```
|
|
||||||
articleServer/
|
|
||||||
├── app.py # Main Flask application
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── run_server.py # Server startup script
|
|
||||||
├── pyvenv.cfg # Python virtual environment configuration
|
|
||||||
└── README.md # Documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
#### API Endpoints:
|
|
||||||
- `GET /articles` - Get articles within time range
|
|
||||||
- `GET /article/content` - Get full article content by path
|
|
||||||
- `GET /outlets` - List all available news outlets
|
|
||||||
- `GET /health` - Health check endpoint
|
|
||||||
|
|
||||||
### 3. AI Processor
|
|
||||||
**Location:** `ai_processor/`
|
|
||||||
|
|
||||||
An AI-powered analytics engine designed to process financial news articles and extract meaningful insights, sentiment analysis, and market indicators from collected content.
|
|
||||||
|
|
||||||
#### Key Features:
|
|
||||||
- Sentiment Analysis: Determine positive, negative, or neutral sentiment of news articles
|
|
||||||
- Topic Classification: Categorize articles by financial topics
|
|
||||||
- Entity Extraction: Identify key entities mentioned in articles (stocks, companies, people, organizations)
|
|
||||||
- Market Indicator Detection: Extract quantitative indicators that may affect stock prices
|
|
||||||
- Insight Generation: Automated generation of actionable intelligence from news content
|
|
||||||
- Batch Processing: Process large volumes of articles efficiently
|
|
||||||
|
|
||||||
#### File Structure:
|
|
||||||
```
|
|
||||||
ai_processor/
|
|
||||||
├── ai_processor.py # Main AI processing application
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── README.md # Documentation
|
|
||||||
├── models/ # Machine learning models and NLP components
|
|
||||||
│ ├── sentiment_analyzer.py # Sentiment analysis module
|
|
||||||
│ ├── topic_classifier.py # News categorization module
|
|
||||||
│ └── entity_extractor.py # Named entity recognition
|
|
||||||
├── processors/ # Article processing pipelines
|
|
||||||
│ ├── text_processor.py # Text cleaning and preprocessing
|
|
||||||
│ └── analysis_pipeline.py # Full analysis pipeline
|
|
||||||
└── output/ # Processed data storage
|
|
||||||
├── insights/
|
|
||||||
└── reports/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### API Endpoints:
|
|
||||||
- `POST /api/analyze/article` - Analyze a single article for insights
|
|
||||||
- `POST /api/analyze/batch` - Process multiple articles in batch mode
|
|
||||||
- `GET /api/insights/latest` - Get latest analysis insights
|
|
||||||
- `GET /api/models` - List available AI models
|
|
||||||
|
|
||||||
### 4. Embedding Service
|
|
||||||
**Location:** `embedding/`
|
|
||||||
|
|
||||||
An embedding service that converts text content into numerical vectors for machine learning and data analysis purposes. This component transforms financial news articles into vector representations that can be used for similarity comparisons, clustering, and other AI tasks.
|
|
||||||
|
|
||||||
#### Key Features:
|
|
||||||
- Multiple Model Support: Integrates with various embedding models including BERT, Sentence-BERT, and other transformer-based models
|
|
||||||
- Batch Processing: Efficient processing of large volumes of articles
|
|
||||||
- Caching Mechanism: Caches generated embeddings to avoid reprocessing
|
|
||||||
- Real-time Generation: Generates embeddings on-demand for new content
|
|
||||||
- Vector Similarity: Computes similarity between different pieces of content
|
|
||||||
- Storage Management: Organizes and stores embeddings efficiently
|
|
||||||
|
|
||||||
#### File Structure:
|
|
||||||
```
|
|
||||||
embedding/
|
|
||||||
├── embedder.py # Main embedding application
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── README.md # Documentation
|
|
||||||
├── models/ # Pre-trained embedding models
|
|
||||||
│ ├── sentence_transformer.py # Sentence transformer implementation
|
|
||||||
│ └── model_loader.py # Model loading utilities
|
|
||||||
├── processors/ # Text processing pipeline
|
|
||||||
│ ├── text_cleaner.py # Text cleaning and preprocessing
|
|
||||||
│ └── embedding_generator.py # Embedding generation
|
|
||||||
└── data/ # Processed embeddings storage
|
|
||||||
├── cache/
|
|
||||||
└── outputs/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### API Endpoints:
|
|
||||||
- `POST /api/embeddings/generate` - Generate embeddings for text content
|
|
||||||
- `POST /api/embeddings/batch` - Generate embeddings for multiple texts in batch
|
|
||||||
- `GET /api/embeddings/similarity` - Calculate similarity between two pieces of text/content
|
|
||||||
|
|
||||||
### 5. MCPServer
|
|
||||||
**Location:** `MCPServer/`
|
|
||||||
|
|
||||||
A Flask-based server that provides an API for accessing and analyzing financial data, with integration for stock analysis and news processing. Serves as the backend service for processing financial information and making it available through HTTP endpoints.
|
|
||||||
|
|
||||||
#### Key Features:
|
|
||||||
- Stock Analysis: Financial metrics calculation and market data processing
|
|
||||||
- News Integration: APIs for retrieving and processing financial news
|
|
||||||
- Data Aggregation: Consolidation of multiple data sources into unified responses
|
|
||||||
- RESTful API: Clean HTTP interface for external services to consume data
|
|
||||||
|
|
||||||
#### File Structure:
|
|
||||||
```
|
|
||||||
MCPServer/
|
|
||||||
├── server.py # Main Flask application
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── openapi.json # API specification
|
|
||||||
├── README.md # Documentation
|
|
||||||
└── api/ # API endpoints and handlers
|
|
||||||
├── stock_analysis.py # Stock analysis functions
|
|
||||||
└── news_processing.py # News processing functions
|
|
||||||
```
|
|
||||||
|
|
||||||
#### API Endpoints:
|
|
||||||
- `POST /query` - Query the vector database with a question
|
|
||||||
- `GET /health` - Server health check
|
|
||||||
- `GET /info` - Returns model and service metadata
|
|
||||||
- `GET /tools` - List available endpoints and their descriptions
|
|
||||||
|
|
||||||
## Setup and Installation
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Python 3.6+
|
|
||||||
- Docker (for containerized deployment)
|
|
||||||
- Internet connection for RSS feed access
|
|
||||||
|
|
||||||
### Installation Steps
|
|
||||||
|
|
||||||
1. **Clone the repository:**
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd StockDocs
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set up each component:**
|
|
||||||
```bash
|
|
||||||
# For each component, follow specific installation instructions
|
|
||||||
cd scraper && pip install -r requirements.txt
|
|
||||||
cd articleServer && pip install -r requirements.txt
|
|
||||||
cd ai_processor && pip install -r requirements.txt
|
|
||||||
cd embedding && pip install -r requirements.txt
|
|
||||||
cd MCPServer && pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Configure environment variables as needed for each component**
|
|
||||||
|
|
||||||
4. **Run individual services:**
|
|
||||||
```bash
|
|
||||||
python scraper/scraper.py # Start scraping
|
|
||||||
python articleServer/run_server.py # Start article server
|
|
||||||
python ai_processor/app.py # Start AI processor
|
|
||||||
python embedding/app.py # Start embedding service
|
|
||||||
python MCPServer/app.py # Start MCP server
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
Each component can be run independently or containerized using the provided Dockerfiles:
|
|
||||||
```bash
|
|
||||||
# Build and run each component in Docker
|
|
||||||
docker build -t stockdocs-scraper ./scraper
|
|
||||||
docker run -p 5000:5000 stockdocs-scraper
|
|
||||||
|
|
||||||
docker build -t stockdocs-article-server ./articleServer
|
|
||||||
docker run -p 5008:5008 stockdocs-article-server
|
|
||||||
|
|
||||||
# Continue for other components...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Technical Details
|
|
||||||
|
|
||||||
### Data Flow
|
|
||||||
1. **Scraper** collects articles from RSS feeds and stores them in `scraper/articles/`
|
|
||||||
2. **Article Server** provides API access to these articles
|
|
||||||
3. **AI Processor** analyzes articles and generates insights in `ai_processor/output/`
|
|
||||||
4. **Embedding Service** converts article content into vector representations and stores in ChromaDB
|
|
||||||
5. **MCPServer** provides API access to the vector database for querying
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
Each component may require specific environment variables:
|
|
||||||
- `ARTICLE_DIR`: Path to article directory
|
|
||||||
- `AI_SERVICE_URL`: URL for local AI service
|
|
||||||
- `CHROMADB_HOST` and `CHROMADB_PORT`: ChromaDB connection settings
|
|
||||||
- `FEED_FILE`: Path to RSS feed configuration file
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
- `scraper/articles/`: Stores raw scraped articles organized by news source
|
|
||||||
- `ai_processor/output/`: Stores processed AI analysis results
|
|
||||||
- `embedding/data/cache/`: Stores cached embeddings
|
|
||||||
- `MCPServer/`: Contains server configuration and API endpoints
|
|
||||||
|
|
||||||
## Getting Started Guide
|
|
||||||
|
|
||||||
To begin working with the StockDocs platform:
|
|
||||||
|
|
||||||
1. **Start the Scraper** to collect news articles
|
|
||||||
2. **Run the Article Server** to make articles accessible via API
|
|
||||||
3. **Launch the AI Processor** to analyze articles and generate insights
|
|
||||||
4. **Initialize the Embedding Service** to create vector representations
|
|
||||||
5. **Start MCPServer** to query the vector database for insights
|
|
||||||
|
|
||||||
Each component can be run independently or as part of a complete pipeline for comprehensive financial news analysis.
|
|
||||||
304
agent.md
304
agent.md
@ -1,304 +0,0 @@
|
|||||||
# StockDocs Project Overview
|
|
||||||
|
|
||||||
This document provides a comprehensive overview of the StockDocs repository, which contains multiple interconnected projects for processing, scraping, and serving financial articles and embeddings.
|
|
||||||
|
|
||||||
## Repository Structure
|
|
||||||
|
|
||||||
The repository contains 5 main projects:
|
|
||||||
|
|
||||||
1. **ai_processor/** - AI processing component
|
|
||||||
2. **articleServer/** - Article serving component
|
|
||||||
3. **embedding/** - Embedding functionality
|
|
||||||
4. **MCPServer/** - MCP server component
|
|
||||||
5. **scraper/** - Web scraping component
|
|
||||||
|
|
||||||
## Project Details
|
|
||||||
|
|
||||||
### 1. ai_processor/
|
|
||||||
The AI processing component handles artificial intelligence operations for processing articles and generating insights.
|
|
||||||
|
|
||||||
**Key Files:**
|
|
||||||
- `ai_processor.py` - Main AI processing logic
|
|
||||||
- `requirements.txt` - Python dependencies
|
|
||||||
- `Dockerfile` - Container configuration
|
|
||||||
|
|
||||||
**Purpose:** Processes articles using AI models to extract key information, generate summaries, and create embeddings.
|
|
||||||
|
|
||||||
**Detailed Structure:**
|
|
||||||
```
|
|
||||||
ai_processor/
|
|
||||||
├── app.py # Main AI processing application
|
|
||||||
├── config.py # Configuration settings
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── README.md # This file
|
|
||||||
├── models/ # Machine learning models and NLP components
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── sentiment_analyzer.py # Sentiment analysis module
|
|
||||||
│ ├── topic_classifier.py # News categorization module
|
|
||||||
│ └── entity_extractor.py # Named entity recognition
|
|
||||||
├── processors/ # Article processing pipelines
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── text_processor.py # Text cleaning and preprocessing
|
|
||||||
│ └── analysis_pipeline.py # Full analysis pipeline
|
|
||||||
└── data/ # Processed data storage
|
|
||||||
├── insights/
|
|
||||||
└── reports/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Endpoints:**
|
|
||||||
- POST `/api/analyze/article` - Analyze a single article for insights
|
|
||||||
- POST `/api/analyze/batch` - Process multiple articles in batch mode
|
|
||||||
- GET `/api/analyze/status/{task_id}` - Check processing status
|
|
||||||
- GET `/api/insights/latest` - Get latest analysis insights
|
|
||||||
- GET `/api/insights/articles/{article_path}` - Get insights for specific article
|
|
||||||
- GET `/api/reports/generate` - Generate comprehensive market analysis report
|
|
||||||
- GET `/api/models` - List available AI models
|
|
||||||
- POST `/api/models/update` - Update or retrain models with new data
|
|
||||||
|
|
||||||
### 2. articleServer/
|
|
||||||
The article serving component provides an API for accessing processed articles.
|
|
||||||
|
|
||||||
**Key Files:**
|
|
||||||
- `app.py` - Main Flask application
|
|
||||||
- `run_server.py` - Server startup script
|
|
||||||
- `requirements.txt` - Python dependencies
|
|
||||||
- `Dockerfile` - Container configuration
|
|
||||||
|
|
||||||
**Purpose:** Exposes processed articles through a REST API for client applications to consume.
|
|
||||||
|
|
||||||
**Detailed Structure:**
|
|
||||||
```
|
|
||||||
articleServer/
|
|
||||||
├── app.py # Main Flask application
|
|
||||||
├── run_server.py # Server startup script
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── README.md # This file
|
|
||||||
└── templates/ # HTML templates (if any)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Endpoints:**
|
|
||||||
- GET `/articles` - Get articles within time range
|
|
||||||
- GET `/article/content` - Get full article content by path
|
|
||||||
- GET `/outlets` - List all available news outlets
|
|
||||||
- GET `/health` - Health check endpoint
|
|
||||||
|
|
||||||
### 3. embedding/
|
|
||||||
The embedding functionality handles vector embeddings for articles and documents.
|
|
||||||
|
|
||||||
**Key Files:**
|
|
||||||
- `embedder.py` - Embedding generation logic
|
|
||||||
- `requirements.txt` - Python dependencies
|
|
||||||
- `Dockerfile` - Container configuration
|
|
||||||
|
|
||||||
**Purpose:** Converts articles into vector embeddings for semantic search and similarity operations.
|
|
||||||
|
|
||||||
**Detailed Structure:**
|
|
||||||
```
|
|
||||||
embedding/
|
|
||||||
├── app.py # Main embedding application
|
|
||||||
├── config.py # Configuration settings
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── README.md # This file
|
|
||||||
├── models/ # Pre-trained embedding models
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── sentence_transformer.py # Sentence transformer implementation
|
|
||||||
│ └── model_loader.py # Model loading utilities
|
|
||||||
├── processors/ # Text processing pipeline
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── text_cleaner.py # Text cleaning and preprocessing
|
|
||||||
│ └── embedding_generator.py # Embedding generation
|
|
||||||
└── data/ # Processed embeddings storage
|
|
||||||
├── cache/
|
|
||||||
└── outputs/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Endpoints:**
|
|
||||||
- POST `/api/embeddings/generate` - Generate embeddings for text content
|
|
||||||
- POST `/api/embeddings/batch` - Generate embeddings for multiple texts in batch
|
|
||||||
- GET `/api/embeddings/similarity` - Calculate similarity between two pieces of text/content
|
|
||||||
- GET `/api/embeddings/status` - Check service status
|
|
||||||
- GET `/api/embeddings/models` - List available embedding models
|
|
||||||
- DELETE `/api/embeddings/cache/clear` - Clear the embedding cache
|
|
||||||
|
|
||||||
### 4. MCPServer/
|
|
||||||
The MCP (Model Context Protocol) server component provides external API access.
|
|
||||||
|
|
||||||
**Key Files:**
|
|
||||||
- `server.py` - Main MCP server implementation
|
|
||||||
- `openapi.json` - API specification
|
|
||||||
- `requirements.txt` - Python dependencies
|
|
||||||
- `Dockerfile` - Container configuration
|
|
||||||
|
|
||||||
**Purpose:** Exposes functionality through the Model Context Protocol for integration with other systems.
|
|
||||||
|
|
||||||
**Detailed Structure:**
|
|
||||||
```
|
|
||||||
MCPServer/
|
|
||||||
├── server.py # Main Flask application
|
|
||||||
├── config.py # Configuration settings
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── README.md # This file
|
|
||||||
└── api/ # API endpoints and handlers
|
|
||||||
├── __init__.py
|
|
||||||
├── stock_analysis.py # Stock analysis functions
|
|
||||||
└── news_processing.py # News processing functions
|
|
||||||
```
|
|
||||||
|
|
||||||
**Endpoints:**
|
|
||||||
- GET `/api/stock/metrics` - Get financial metrics for a stock
|
|
||||||
- GET `/api/stock/history` - Get historical price data
|
|
||||||
- POST `/api/stock/analyze` - Perform comprehensive stock analysis
|
|
||||||
- GET `/api/news` - Retrieve news articles related to stocks
|
|
||||||
- GET `/api/news/outlets` - List available news sources
|
|
||||||
- POST `/api/news/process` - Process and categorize news content
|
|
||||||
- GET `/api/data/refresh` - Refresh data from sources
|
|
||||||
- GET `/api/status` - Server health check
|
|
||||||
|
|
||||||
### 5. scraper/
|
|
||||||
The web scraping component collects articles from various sources.
|
|
||||||
|
|
||||||
**Key Files:**
|
|
||||||
- `scraper.py` - Main scraping logic
|
|
||||||
- `rss_feeds.json` - RSS feed configuration
|
|
||||||
- `requirements.txt` - Python dependencies
|
|
||||||
- `Dockerfile` - Container configuration
|
|
||||||
|
|
||||||
**Purpose:** Collects financial articles from various sources including RSS feeds and web scraping.
|
|
||||||
|
|
||||||
**Detailed Structure:**
|
|
||||||
```
|
|
||||||
scraper/
|
|
||||||
├── rss_feeds.json # Configuration file with RSS feed URLs
|
|
||||||
├── scraper.py # Main scraping logic
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── dockerfile # Docker configuration
|
|
||||||
├── dockerfile-selenium # Dockerfile for selenium-based scraping
|
|
||||||
├── articles/ # Directory where scraped articles are stored
|
|
||||||
│ ├── Reuters – Business News/
|
|
||||||
│ │ ├── article1.txt
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── Associated Press – Business/
|
|
||||||
│ │ ├── article1.txt
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── ...
|
|
||||||
├── processed_articles_cache.json # Cache of already processed articles
|
|
||||||
└── pyvenv.cfg # Python virtual environment configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- RSS Feed Integration: Supports multiple financial news sources through RSS feeds
|
|
||||||
- Automated Scraping: Regularly fetches and processes new articles from configured feeds
|
|
||||||
- Structured Storage: Organizes articles in a directory structure by news outlet
|
|
||||||
- Duplicate Detection: Prevents re-processing of already collected articles
|
|
||||||
- Caching Mechanism: Maintains a cache of processed articles to optimize performance
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Docker installed
|
|
||||||
- Python 3.8+
|
|
||||||
- Git
|
|
||||||
|
|
||||||
### Setup Instructions
|
|
||||||
|
|
||||||
1. **Clone the repository:**
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd StockDocs
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Build and run containers:**
|
|
||||||
```bash
|
|
||||||
docker-compose up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Project-specific setup:**
|
|
||||||
- Each project has its own `README.md` with detailed setup instructions
|
|
||||||
- Check individual project directories for specific requirements
|
|
||||||
|
|
||||||
## Project Dependencies
|
|
||||||
|
|
||||||
### Common Dependencies
|
|
||||||
- Python 3.8+
|
|
||||||
- Docker
|
|
||||||
- Various Python packages (listed in requirements.txt files)
|
|
||||||
|
|
||||||
### Inter-project Relationships
|
|
||||||
- `scraper/` feeds articles to `ai_processor/`
|
|
||||||
- `ai_processor/` generates embeddings that `embedding/` processes
|
|
||||||
- `articleServer/` serves articles processed by `ai_processor/`
|
|
||||||
- `MCPServer/` provides API access to all components
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. **Start all services:**
|
|
||||||
```bash
|
|
||||||
docker-compose up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Work with individual projects:**
|
|
||||||
- Navigate to project directory
|
|
||||||
- Check `README.md` for specific instructions
|
|
||||||
- Make changes and rebuild as needed
|
|
||||||
|
|
||||||
3. **Testing:**
|
|
||||||
- Each project includes its own testing setup
|
|
||||||
- Integration tests may be needed for cross-project functionality
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### articleServer/
|
|
||||||
- `/articles` - Get all articles
|
|
||||||
- `/articles/<id>` - Get specific article
|
|
||||||
- `/search` - Search articles by query
|
|
||||||
|
|
||||||
### MCPServer/
|
|
||||||
- Exposes various endpoints through Model Context Protocol
|
|
||||||
- See `openapi.json` for complete specification
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
Each project may require specific environment variables. Check individual `README.md` files for details.
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
The system now supports API key authentication for connections to the centralized AI service at `http://example.com:4000`. To enable authentication:
|
|
||||||
|
|
||||||
1. Set the `AI_SERVICE_API_KEY` environment variable with your API key
|
|
||||||
2. All connections to the AI service will automatically include the `Authorization: Bearer {api_key}` header
|
|
||||||
|
|
||||||
### Data Storage
|
|
||||||
- Articles are stored in the scraper component
|
|
||||||
- Processed data flows through the ai_processor
|
|
||||||
- Embeddings are generated and stored in the embedding component
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
1. **Docker build failures:** Ensure Docker is running and check `Dockerfile` syntax
|
|
||||||
2. **Python dependency issues:** Run `pip install -r requirements.txt` in each project
|
|
||||||
3. **Port conflicts:** Check `docker-compose.yml` for port mappings
|
|
||||||
4. **Service startup issues:** Check individual project logs
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
- View logs with `docker-compose logs <service-name>`
|
|
||||||
- Check individual project logs for detailed error information
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create feature branch
|
|
||||||
3. Make changes
|
|
||||||
4. Test thoroughly
|
|
||||||
5. Submit pull request
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues or questions, please check:
|
|
||||||
- Individual project README.md files
|
|
||||||
- Docker logs for runtime errors
|
|
||||||
- GitHub issues for known problems
|
|
||||||
@ -60,6 +60,7 @@ class ArticleProcessor:
|
|||||||
scraper_dir = alt_path
|
scraper_dir = alt_path
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
logger.error("No valid articles directory found")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Log cache state before scanning
|
# Log cache state before scanning
|
||||||
@ -68,7 +69,7 @@ class ArticleProcessor:
|
|||||||
f"Cache state before scanning: {cache_stats['processed_files']} files marked as processed"
|
f"Cache state before scanning: {cache_stats['processed_files']} files marked as processed"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Directory exists, walking through files...")
|
logger.info("Directory exists, walking through files...")
|
||||||
file_count = 0
|
file_count = 0
|
||||||
article_file_count = 0
|
article_file_count = 0
|
||||||
already_processed_count = 0
|
already_processed_count = 0
|
||||||
@ -113,10 +114,12 @@ class ArticleProcessor:
|
|||||||
f"If cache should be empty, check cache file: {self.cache_manager.cache_file}"
|
f"If cache should be empty, check cache file: {self.cache_manager.cache_file}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"Found {len(unprocessed_articles)} unprocessed articles to process")
|
||||||
return unprocessed_articles
|
return unprocessed_articles
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error finding unprocessed articles: {e}")
|
logger.error(f"Error finding unprocessed articles: {e}")
|
||||||
|
logger.error(f"Error type: {type(e).__name__}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def process_article_file(self, file_path: str, filename: str) -> dict:
|
def process_article_file(self, file_path: str, filename: str) -> dict:
|
||||||
@ -131,9 +134,16 @@ class ArticleProcessor:
|
|||||||
Dictionary containing the extracted facts or None if failed
|
Dictionary containing the extracted facts or None if failed
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
logger.debug(f"Processing article file: {filename}")
|
||||||
|
logger.debug(f"File path: {file_path}")
|
||||||
|
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
article_data = json.load(f)
|
article_data = json.load(f)
|
||||||
|
|
||||||
|
logger.debug(f"Loaded article data for: {filename}")
|
||||||
|
logger.debug(f"Article title: {article_data.get('title', 'No title')}")
|
||||||
|
logger.debug(f"Article content length: {len(article_data.get('original_content', ''))}")
|
||||||
|
|
||||||
# Extract facts from the article
|
# Extract facts from the article
|
||||||
facts = self.fact_extractor.extract_facts_from_article(
|
facts = self.fact_extractor.extract_facts_from_article(
|
||||||
article_data.get("original_content", ""),
|
article_data.get("original_content", ""),
|
||||||
@ -152,8 +162,15 @@ class ArticleProcessor:
|
|||||||
logger.info(f"Successfully processed article: {filename}")
|
logger.info(f"Successfully processed article: {filename}")
|
||||||
return facts
|
return facts
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"JSON decode error processing article {filename}: {e}")
|
||||||
|
logger.error(f"File path: {file_path}")
|
||||||
|
metrics_collector.increment_articles_failed()
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing article {filename}: {e}")
|
logger.error(f"Error processing article {filename}: {e}")
|
||||||
|
logger.error(f"Error type: {type(e).__name__}")
|
||||||
|
logger.error(f"File path: {file_path}")
|
||||||
metrics_collector.increment_articles_failed()
|
metrics_collector.increment_articles_failed()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -171,18 +188,23 @@ class ArticleProcessor:
|
|||||||
failed = 0
|
failed = 0
|
||||||
|
|
||||||
logger.info(f"Processing batch of {len(articles_batch)} articles")
|
logger.info(f"Processing batch of {len(articles_batch)} articles")
|
||||||
|
logger.debug(f"Batch contents: {[filename for _, filename in articles_batch]}")
|
||||||
|
|
||||||
for file_path, filename in articles_batch:
|
for file_path, filename in articles_batch:
|
||||||
try:
|
try:
|
||||||
|
logger.debug(f"Processing individual article: {filename}")
|
||||||
facts = self.process_article_file(file_path, filename)
|
facts = self.process_article_file(file_path, filename)
|
||||||
if facts:
|
if facts:
|
||||||
successful += 1
|
successful += 1
|
||||||
metrics_collector.increment_articles_processed()
|
metrics_collector.increment_articles_processed()
|
||||||
|
logger.debug(f"Successfully processed: {filename}")
|
||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
metrics_collector.increment_articles_failed()
|
metrics_collector.increment_articles_failed()
|
||||||
|
logger.warning(f"Failed to process: {filename}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing batch item {filename}: {e}")
|
logger.error(f"Error processing batch item {filename}: {e}")
|
||||||
|
logger.error(f"Error type: {type(e).__name__}")
|
||||||
failed += 1
|
failed += 1
|
||||||
metrics_collector.increment_articles_failed()
|
metrics_collector.increment_articles_failed()
|
||||||
|
|
||||||
@ -226,6 +248,7 @@ class ArticleProcessor:
|
|||||||
# Process articles in batches
|
# Process articles in batches
|
||||||
for i in range(0, len(unprocessed_articles), self.batch_size):
|
for i in range(0, len(unprocessed_articles), self.batch_size):
|
||||||
batch = unprocessed_articles[i : i + self.batch_size]
|
batch = unprocessed_articles[i : i + self.batch_size]
|
||||||
|
logger.info(f"Processing batch {i//self.batch_size + 1} with {len(batch)} articles")
|
||||||
successful, failed = self.process_batch(batch)
|
successful, failed = self.process_batch(batch)
|
||||||
total_processed += successful
|
total_processed += successful
|
||||||
total_failed += failed
|
total_failed += failed
|
||||||
@ -251,6 +274,7 @@ class ArticleProcessor:
|
|||||||
|
|
||||||
logger.info(f"Processing completed in {duration:.2f} seconds")
|
logger.info(f"Processing completed in {duration:.2f} seconds")
|
||||||
logger.info(f"Total processed: {total_processed}, Total failed: {total_failed}")
|
logger.info(f"Total processed: {total_processed}, Total failed: {total_failed}")
|
||||||
|
logger.info(f"Processing stats: {stats}")
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
@ -265,4 +289,5 @@ class ArticleProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with processing statistics
|
Dictionary with processing statistics
|
||||||
"""
|
"""
|
||||||
|
logger.info("Starting real-time processing of new articles")
|
||||||
return self.process_all_articles(scraper_dir)
|
return self.process_all_articles(scraper_dir)
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List
|
||||||
|
|
||||||
from config import CACHE_FILE
|
from config import CACHE_FILE
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ class CacheManager:
|
|||||||
def __init__(self, cache_file: str = CACHE_FILE):
|
def __init__(self, cache_file: str = CACHE_FILE):
|
||||||
self.cache_file = cache_file
|
self.cache_file = cache_file
|
||||||
self.cache = self._load_cache()
|
self.cache = self._load_cache()
|
||||||
|
logger.info(f"Cache manager initialized with cache file: {self.cache_file}")
|
||||||
|
|
||||||
def _load_cache(self) -> Dict:
|
def _load_cache(self) -> Dict:
|
||||||
"""Load cache from file."""
|
"""Load cache from file."""
|
||||||
@ -29,6 +30,8 @@ class CacheManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logger.debug(f"Attempting to load cache from: {self.cache_file}")
|
||||||
|
|
||||||
if not os.path.exists(self.cache_file):
|
if not os.path.exists(self.cache_file):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Cache file does not exist: {self.cache_file}. Creating new empty cache."
|
f"Cache file does not exist: {self.cache_file}. Creating new empty cache."
|
||||||
@ -77,13 +80,14 @@ class CacheManager:
|
|||||||
cache_data["processed_files"] = {}
|
cache_data["processed_files"] = {}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Loaded cache with {len(cache_data['processed_files'])} processed files"
|
f"Loaded cache with {len(cache_data['processed_files'])} processed files from {self.cache_file}"
|
||||||
)
|
)
|
||||||
|
logger.debug(f"Cache file size: {file_size} bytes")
|
||||||
return cache_data
|
return cache_data
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.warning(
|
logger.error(
|
||||||
f"Cache file contains invalid JSON: {e}. Treating as fresh cache."
|
f"Cache file contains invalid JSON: {e}. Creating fresh cache."
|
||||||
)
|
)
|
||||||
return empty_cache
|
return empty_cache
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -94,16 +98,20 @@ class CacheManager:
|
|||||||
def _save_cache(self) -> None:
|
def _save_cache(self) -> None:
|
||||||
"""Save cache to file."""
|
"""Save cache to file."""
|
||||||
try:
|
try:
|
||||||
|
logger.debug(f"Saving cache to file: {self.cache_file}")
|
||||||
# Create directory if it doesn't exist
|
# Create directory if it doesn't exist
|
||||||
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
|
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
|
||||||
|
|
||||||
with open(self.cache_file, "w", encoding="utf-8") as f:
|
with open(self.cache_file, "w", encoding="utf-8") as f:
|
||||||
json.dump(self.cache, f, indent=2, ensure_ascii=False)
|
json.dump(self.cache, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
logger.debug(f"Successfully saved cache to {self.cache_file}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving cache file {self.cache_file}: {e}")
|
logger.error(f"Error saving cache file {self.cache_file}: {e}")
|
||||||
|
|
||||||
def is_processed(self, file_path: str) -> bool:
|
def is_processed(self, file_path: str) -> bool:
|
||||||
"""Check if a file has been processed."""
|
"""Check if a file has been processed."""
|
||||||
|
logger.debug(f"Checking if file is processed: {file_path}")
|
||||||
processed_files = self.cache.get("processed_files")
|
processed_files = self.cache.get("processed_files")
|
||||||
|
|
||||||
# Handle case where processed_files is None or not a dict
|
# Handle case where processed_files is None or not a dict
|
||||||
@ -113,10 +121,13 @@ class CacheManager:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return file_path in processed_files
|
result = file_path in processed_files
|
||||||
|
logger.debug(f"File {file_path} processed status: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
def mark_processed(self, file_path: str, status: str = "processed") -> None:
|
def mark_processed(self, file_path: str, status: str = "processed") -> None:
|
||||||
"""Mark a file as processed."""
|
"""Mark a file as processed."""
|
||||||
|
logger.debug(f"Marking file as processed: {file_path}")
|
||||||
# Ensure we're using the correct cache structure
|
# Ensure we're using the correct cache structure
|
||||||
if "processed_files" not in self.cache:
|
if "processed_files" not in self.cache:
|
||||||
self.cache["processed_files"] = {}
|
self.cache["processed_files"] = {}
|
||||||
@ -126,21 +137,27 @@ class CacheManager:
|
|||||||
"last_updated": datetime.now().isoformat(),
|
"last_updated": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
self._save_cache()
|
self._save_cache()
|
||||||
|
logger.info(f"Successfully marked file as processed: {file_path}")
|
||||||
|
|
||||||
def get_processed_files(self) -> List[str]:
|
def get_processed_files(self) -> List[str]:
|
||||||
"""Get list of all processed files."""
|
"""Get list of all processed files."""
|
||||||
return list(self.cache.get("processed_files", {}).keys())
|
files = list(self.cache.get("processed_files", {}).keys())
|
||||||
|
logger.debug(f"Retrieved {len(files)} processed files from cache")
|
||||||
|
return files
|
||||||
|
|
||||||
def get_cache_stats(self) -> Dict:
|
def get_cache_stats(self) -> Dict:
|
||||||
"""Get cache statistics."""
|
"""Get cache statistics."""
|
||||||
return {
|
stats = {
|
||||||
"total_files": len(self.cache.get("processed_files", {})),
|
"total_files": len(self.cache.get("processed_files", {})),
|
||||||
"processed_files": len(self.cache.get("processed_files", {})),
|
"processed_files": len(self.cache.get("processed_files", {})),
|
||||||
"cache_file": self.cache_file,
|
"cache_file": self.cache_file,
|
||||||
}
|
}
|
||||||
|
logger.debug(f"Cache stats: {stats}")
|
||||||
|
return stats
|
||||||
|
|
||||||
def clear_cache(self) -> None:
|
def clear_cache(self) -> None:
|
||||||
"""Clear the entire cache."""
|
"""Clear the entire cache."""
|
||||||
|
logger.info("Clearing entire cache")
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
self._save_cache()
|
self._save_cache()
|
||||||
logger.info("Cache cleared")
|
logger.info("Cache cleared successfully")
|
||||||
|
|||||||
@ -6,7 +6,7 @@ Uses the gpt-oss model via the centralized AI service.
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any
|
||||||
|
|
||||||
from config import AI_SERVER_URL, AI_SERVICE_API_KEY, FACT_EXTRACTION_MODEL
|
from config import AI_SERVER_URL, AI_SERVICE_API_KEY, FACT_EXTRACTION_MODEL
|
||||||
from metrics_collector import metrics_collector
|
from metrics_collector import metrics_collector
|
||||||
@ -71,6 +71,11 @@ class FactExtractor:
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Log the request details for debugging
|
||||||
|
logger.debug(f"Preparing AI request for article: {title}")
|
||||||
|
logger.debug(f"AI Server URL: {extraction_url}")
|
||||||
|
logger.debug(f"Request payload preview: {str({'model': self.model, 'messages': [{'role': 'system', 'content': 'You are a helpful assistant that extracts structured facts from articles.'}, {'role': 'user', 'content': prompt[:200]}]}[:300])}...")
|
||||||
|
|
||||||
# Call the AI service with gpt-oss model for fact extraction
|
# Call the AI service with gpt-oss model for fact extraction
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
@ -87,9 +92,18 @@ class FactExtractor:
|
|||||||
headers=self._get_headers(),
|
headers=self._get_headers(),
|
||||||
timeout=60
|
timeout=60
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Log response details for debugging
|
||||||
|
logger.debug(f"AI service response status: {response.status_code}")
|
||||||
|
logger.debug(f"AI service response headers: {dict(response.headers)}")
|
||||||
|
logger.debug(f"AI service response text preview: {response.text[:500]}...")
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.error(f"Request failed for article '{title}': {e}")
|
logger.error(f"REQUEST FAILED for article '{title}' - URL: {extraction_url}")
|
||||||
|
logger.error(f"Request error details: {e}")
|
||||||
|
logger.error(f"Article content preview: {article_content[:200]}...")
|
||||||
|
logger.error(f"Response text (if available): {response.text[:500] if 'response' in locals() else 'No response available'}")
|
||||||
# Return basic structure if request fails
|
# Return basic structure if request fails
|
||||||
return self._create_basic_fact_structure(article_content, title)
|
return self._create_basic_fact_structure(article_content, title)
|
||||||
|
|
||||||
@ -97,22 +111,35 @@ class FactExtractor:
|
|||||||
try:
|
try:
|
||||||
result = response.json()
|
result = response.json()
|
||||||
extracted_text = result['choices'][0]['message']['content'].strip()
|
extracted_text = result['choices'][0]['message']['content'].strip()
|
||||||
|
logger.debug(f"Successfully parsed JSON response for article '{title}'")
|
||||||
|
logger.debug(f"Extracted text preview: {extracted_text[:300]}...")
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"Failed to parse JSON response from AI service for article '{title}': {e}")
|
logger.error(f"JSON PARSING FAILED for article '{title}'")
|
||||||
|
logger.error(f"Response status: {response.status_code}")
|
||||||
|
logger.error(f"Response text (full): {response.text}")
|
||||||
|
logger.error(f"JSON parsing error: {e}")
|
||||||
|
logger.error(f"Article content preview: {article_content[:200]}...")
|
||||||
# Return basic structure if response parsing fails
|
# Return basic structure if response parsing fails
|
||||||
return self._create_basic_fact_structure(article_content, title)
|
return self._create_basic_fact_structure(article_content, title)
|
||||||
|
|
||||||
# Check if the response is empty or invalid
|
# Check if the response is empty or invalid
|
||||||
if not extracted_text or extracted_text.strip() == "":
|
if not extracted_text or extracted_text.strip() == "":
|
||||||
logger.warning(f"Empty response from AI service for article '{title}'. Creating basic structure.")
|
logger.warning(f"Empty response from AI service for article '{title}'")
|
||||||
|
logger.warning(f"Response status: {response.status_code}")
|
||||||
|
logger.warning(f"Response text preview: {response.text[:300]}...")
|
||||||
|
logger.warning(f"Article content preview: {article_content[:200]}...")
|
||||||
facts = self._create_basic_fact_structure(article_content, title)
|
facts = self._create_basic_fact_structure(article_content, title)
|
||||||
else:
|
else:
|
||||||
# Try to parse the JSON from the response
|
# Try to parse the JSON from the response
|
||||||
try:
|
try:
|
||||||
facts = json.loads(extracted_text)
|
facts = json.loads(extracted_text)
|
||||||
|
logger.debug(f"Successfully parsed extracted JSON for article '{title}'")
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
# If JSON parsing fails, create a basic structure
|
# If JSON parsing fails, create a basic structure
|
||||||
logger.warning(f"Failed to parse JSON from AI response for article '{title}': {e}. Creating basic structure.")
|
logger.error(f"Failed to parse JSON from AI response for article '{title}': {e}")
|
||||||
|
logger.error(f"Extracted text that failed to parse: {extracted_text[:500]}...")
|
||||||
|
logger.error(f"Response status: {response.status_code}")
|
||||||
|
logger.error(f"Response text (full): {response.text}")
|
||||||
facts = self._create_basic_fact_structure(article_content, title)
|
facts = self._create_basic_fact_structure(article_content, title)
|
||||||
|
|
||||||
# Ensure all required fields are present
|
# Ensure all required fields are present
|
||||||
@ -123,7 +150,9 @@ class FactExtractor:
|
|||||||
return facts
|
return facts
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error extracting facts from article '{title}': {e}")
|
logger.error(f"UNEXPECTED ERROR extracting facts from article '{title}': {e}")
|
||||||
|
logger.error(f"Error type: {type(e).__name__}")
|
||||||
|
logger.error(f"Article content preview: {article_content[:200]}...")
|
||||||
# Return basic structure if extraction fails
|
# Return basic structure if extraction fails
|
||||||
return self._create_basic_fact_structure(article_content, title)
|
return self._create_basic_fact_structure(article_content, title)
|
||||||
|
|
||||||
|
|||||||
@ -6,31 +6,20 @@ Handles the orchestration of article processing and fact extraction.
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Setup logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[
|
|
||||||
logging.FileHandler('ai_processor.log'),
|
|
||||||
logging.StreamHandler()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def setup_logging():
|
|
||||||
"""Setup logging configuration."""
|
|
||||||
# Ensure log directory exists
|
|
||||||
log_dir = os.path.dirname('ai_processor.log')
|
|
||||||
if log_dir:
|
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
|
||||||
|
|
||||||
from article_processor import ArticleProcessor
|
from article_processor import ArticleProcessor
|
||||||
from metrics_collector import metrics_collector
|
from metrics_collector import metrics_collector
|
||||||
from cache_manager import CacheManager
|
from config import CACHE_FILE, LOG_FILE, LOG_LEVEL
|
||||||
from config import CACHE_FILE, LOG_FILE
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, LOG_LEVEL.upper(), logging.INFO),
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
home = /usr/bin
|
|
||||||
include-system-site-packages = false
|
|
||||||
version = 3.12.3
|
|
||||||
executable = /usr/bin/python3.12
|
|
||||||
command = /usr/bin/python3 -m venv /home/user/StockDocs/articleServer
|
|
||||||
@ -2,11 +2,9 @@ import os
|
|||||||
import json
|
import json
|
||||||
import chromadb
|
import chromadb
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
|
||||||
import datetime
|
import datetime
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
|
||||||
from prometheus_client import start_http_server, Counter, Histogram
|
from prometheus_client import start_http_server, Counter, Histogram
|
||||||
|
|
||||||
# Setup logging with better error handling
|
# Setup logging with better error handling
|
||||||
@ -19,7 +17,7 @@ try:
|
|||||||
logging.StreamHandler()
|
logging.StreamHandler()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# Fallback if file logging fails
|
# Fallback if file logging fails
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import chromadb
|
import chromadb
|
||||||
@ -15,8 +16,6 @@ model = SentenceTransformer(
|
|||||||
trust_remote_code=True
|
trust_remote_code=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
import math
|
|
||||||
def embed_text(text, specific_context, max_tokens=2048, overlap=256):
|
def embed_text(text, specific_context, max_tokens=2048, overlap=256):
|
||||||
"""
|
"""
|
||||||
Embeds the given text using the SentenceTransformer model.
|
Embeds the given text using the SentenceTransformer model.
|
||||||
|
|||||||
@ -100,7 +100,7 @@ def check_scraper_directory():
|
|||||||
health_status.labels(component='scraper_dir').set(1)
|
health_status.labels(component='scraper_dir').set(1)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠ Scraper directory exists but no JSON files found")
|
logger.warning("⚠ Scraper directory exists but no JSON files found")
|
||||||
health_status.labels(component='scraper_dir').set(1) # Directory exists, just no files yet
|
health_status.labels(component='scraper_dir').set(1) # Directory exists, just no files yet
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,6 @@ This shows how to connect and query your ChromaDB database
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import chromadb
|
import chromadb
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from chromadb.config import Settings
|
from chromadb.config import Settings
|
||||||
|
|
||||||
def connect_to_chromadb():
|
def connect_to_chromadb():
|
||||||
|
|||||||
@ -5,8 +5,6 @@ Test script to demonstrate end-to-end pipeline with a single article
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Add the current directory to Python path to import our modules
|
# Add the current directory to Python path to import our modules
|
||||||
import sys
|
import sys
|
||||||
@ -14,10 +12,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||||||
|
|
||||||
from advanced_embedder import (
|
from advanced_embedder import (
|
||||||
extract_facts_from_article,
|
extract_facts_from_article,
|
||||||
get_embedding,
|
get_embedding
|
||||||
create_collections,
|
|
||||||
embed_and_store_facts,
|
|
||||||
process_article_file
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_end_to_end():
|
def test_end_to_end():
|
||||||
|
|||||||
48
pyproject.toml
Normal file
48
pyproject.toml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
[project]
|
||||||
|
name = "stockdocs"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Financial news analysis platform: RSS scraping, AI processing, embeddings, and MCP server"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
authors = [{ name = "Jarian Cottingham", email = "jarianc@proton.me" }]
|
||||||
|
keywords = ["finance", "news", "scraping", "nlp", "embeddings", "mcp"]
|
||||||
|
classifiers = [
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"feedparser>=6.0,<7.0",
|
||||||
|
"requests>=2.31,<3.0",
|
||||||
|
"beautifulsoup4>=4.12,<5.0",
|
||||||
|
"lxml>=5.0",
|
||||||
|
"nltk>=3.8",
|
||||||
|
"newspaper4k>=0.2.8,<0.3",
|
||||||
|
"selenium>=4.15",
|
||||||
|
"pandas>=2.0",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"tqdm>=4.64",
|
||||||
|
"tldextract>=5.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
|
target-version = "py39"
|
||||||
|
exclude = [".git", "articles", "ai_processor/ai_processor"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "W"]
|
||||||
|
ignore = ["E501"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
BIN
scraper/.DS_Store
vendored
BIN
scraper/.DS_Store
vendored
Binary file not shown.
@ -12,7 +12,7 @@ import time
|
|||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
import random
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
@ -30,13 +30,24 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Rotating User-Agents to bypass bot detection (Reuters, etc.)
|
||||||
|
USER_AGENTS = [
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_random_ua():
|
||||||
|
return random.choice(USER_AGENTS)
|
||||||
|
|
||||||
# Robust file path handling - try multiple locations
|
# Robust file path handling - try multiple locations
|
||||||
def get_feed_file_path():
|
def get_feed_file_path():
|
||||||
"""Get the RSS feed file path, trying multiple locations."""
|
"""Get the RSS feed file path, trying multiple locations."""
|
||||||
possible_paths = [
|
possible_paths = [
|
||||||
"./rss_feeds.json", # Current directory
|
"./rss_feeds.json", # Current directory
|
||||||
"../rss_feeds.json", # Parent directory
|
"../rss_feeds.json", # Parent directory
|
||||||
"/home/user/StockDocs/scraper/rss_feeds.json", # Explicit path
|
|
||||||
"/app/rss_feeds.json", # Docker path
|
"/app/rss_feeds.json", # Docker path
|
||||||
"./scraper/rss_feeds.json" # Scraper subdirectory
|
"./scraper/rss_feeds.json" # Scraper subdirectory
|
||||||
]
|
]
|
||||||
@ -165,19 +176,22 @@ def save_article_to_file(article, filename, source="Unfiltered"):
|
|||||||
|
|
||||||
def get_article_with_selenium(url):
|
def get_article_with_selenium(url):
|
||||||
"""
|
"""
|
||||||
Gets article text using Selenium Firefox driver with proper error handling
|
Gets article text using Selenium Firefox driver with proper error handling,
|
||||||
and cleanup.
|
cleanup, and bot-detection evasion.
|
||||||
"""
|
"""
|
||||||
driver = None
|
driver = None
|
||||||
try:
|
try:
|
||||||
# Configure Firefox options
|
# Configure Firefox options with bot-detection evasion
|
||||||
options = FirefoxOptions()
|
options = FirefoxOptions()
|
||||||
options.add_argument("--headless")
|
options.add_argument("--headless")
|
||||||
options.set_preference("dom.ipc.processCount", 1) # Reduce process count
|
options.set_preference("dom.ipc.processCount", 1)
|
||||||
|
options.set_preference("general.useragent.override", get_random_ua())
|
||||||
|
options.set_preference("permissions.default.image", 2)
|
||||||
|
options.set_preference("dom.webnotifications.enabled", False)
|
||||||
|
|
||||||
# Initialize driver with timeout
|
# Initialize driver with timeout
|
||||||
driver = webdriver.Firefox(options=options)
|
driver = webdriver.Firefox(options=options)
|
||||||
driver.set_page_load_timeout(30) # 30 seconds timeout
|
driver.set_page_load_timeout(30)
|
||||||
|
|
||||||
# Navigate to URL
|
# Navigate to URL
|
||||||
driver.get(url)
|
driver.get(url)
|
||||||
@ -187,10 +201,10 @@ def get_article_with_selenium(url):
|
|||||||
WebDriverWait(driver, 15).until(
|
WebDriverWait(driver, 15).until(
|
||||||
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
||||||
)
|
)
|
||||||
except:
|
except Exception:
|
||||||
pass # Continue even if wait times out
|
pass
|
||||||
|
|
||||||
time.sleep(2) # Brief additional wait
|
time.sleep(random.uniform(1, 3))
|
||||||
|
|
||||||
html = driver.page_source
|
html = driver.page_source
|
||||||
|
|
||||||
@ -204,42 +218,45 @@ def get_article_with_selenium(url):
|
|||||||
logger.error(f"Selenium failed for {url}: {str(e)}")
|
logger.error(f"Selenium failed for {url}: {str(e)}")
|
||||||
return ""
|
return ""
|
||||||
finally:
|
finally:
|
||||||
# Always quit the driver
|
|
||||||
if driver:
|
if driver:
|
||||||
try:
|
try:
|
||||||
driver.quit()
|
driver.quit()
|
||||||
except:
|
except Exception:
|
||||||
pass # Ignore errors in cleanup
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_article_with_playwright(url):
|
def get_article_with_playwright(url):
|
||||||
"""
|
"""
|
||||||
Gets article text using Playwright with proper error handling.
|
Gets article text using Playwright with proper bot-detection evasion.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
# Use Chromium instead of Firefox for better compatibility
|
|
||||||
browser = p.chromium.launch(headless=True, timeout=30000)
|
browser = p.chromium.launch(headless=True, timeout=30000)
|
||||||
page = browser.new_page()
|
context = browser.new_context(
|
||||||
|
user_agent=get_random_ua(),
|
||||||
# Set user agent to avoid bot detection
|
viewport={"width": 1920, "height": 1080},
|
||||||
page.set_extra_http_headers(
|
locale="en-US",
|
||||||
{
|
timezone_id="America/New_York",
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
page.goto(url, wait_until="load")
|
page.set_extra_http_headers({
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
})
|
||||||
|
|
||||||
# Wait for content to load
|
page.goto(url, wait_until="domcontentloaded", timeout=30000)
|
||||||
time.sleep(3)
|
time.sleep(random.uniform(2, 4))
|
||||||
|
|
||||||
html = page.content()
|
html = page.content()
|
||||||
|
context.close()
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
||||||
# Parse with Newspaper4k
|
|
||||||
article = newspaper.article(url, input_html=html, language="en")
|
article = newspaper.article(url, input_html=html, language="en")
|
||||||
article.nlp()
|
article.nlp()
|
||||||
logger.info(f"Successfully extracted article with Playwright from {url}")
|
logger.info(f"Successfully extracted article with Playwright from {url}")
|
||||||
@ -265,11 +282,15 @@ def pull_article(link, source, title=None, save_to_file=True):
|
|||||||
) as f:
|
) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
# Random delay before fetching to avoid rate-limiting / bot detection
|
||||||
|
time.sleep(random.uniform(0.5, 2))
|
||||||
|
|
||||||
text = ""
|
text = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try newspaper4k first
|
# Try newspaper4k first with proper User-Agent to bypass bot detection
|
||||||
article = newspaper.article(link)
|
ua = get_random_ua()
|
||||||
|
article = newspaper.article(link, browser_user_agent=ua)
|
||||||
article.download()
|
article.download()
|
||||||
article.parse()
|
article.parse()
|
||||||
text = article.text
|
text = article.text
|
||||||
@ -290,7 +311,7 @@ def pull_article(link, source, title=None, save_to_file=True):
|
|||||||
logger.info(f"Successfully pulled article from {link} with Playwright")
|
logger.info(f"Successfully pulled article from {link} with Playwright")
|
||||||
|
|
||||||
if not text or len(text) < 200:
|
if not text or len(text) < 200:
|
||||||
logger.warning(f"Playwright article too short, falling back to Selenium.")
|
logger.warning("Playwright article too short, falling back to Selenium.")
|
||||||
# Fallback to Selenium with better error handling
|
# Fallback to Selenium with better error handling
|
||||||
text = get_article_with_selenium(link)
|
text = get_article_with_selenium(link)
|
||||||
logger.info(f"Successfully pulled article from {link} with Selenium")
|
logger.info(f"Successfully pulled article from {link} with Selenium")
|
||||||
@ -439,7 +460,7 @@ def main():
|
|||||||
with open("errors.txt", "w", encoding="utf-8") as f:
|
with open("errors.txt", "w", encoding="utf-8") as f:
|
||||||
for error in errors:
|
for error in errors:
|
||||||
f.write(str(error) + "\n")
|
f.write(str(error) + "\n")
|
||||||
logger.info(f"Errors logged to errors.txt")
|
logger.info("Errors logged to errors.txt")
|
||||||
|
|
||||||
# Print all results to a log file
|
# Print all results to a log file
|
||||||
with open("results.txt", "w", encoding="utf-8") as f:
|
with open("results.txt", "w", encoding="utf-8") as f:
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
home = /usr/bin
|
|
||||||
include-system-site-packages = false
|
|
||||||
version = 3.12.3
|
|
||||||
executable = /usr/bin/python3.12
|
|
||||||
command = /usr/bin/python3 -m venv /home/user/StockDocs/scraper
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"rss_feeds": {
|
"rss_feeds": {
|
||||||
"Reuters – Business News": {
|
"Reuters – Business News": {
|
||||||
"source_website": "reuters.com",
|
"source_website": "reuters.com",
|
||||||
"rss_url": "https://news.google.com/rss/search?q=site:reuters.com+business&hl=en-US&gl=US&ceid=US:en"
|
"rss_url": "https://www.reutersagency.com/feed/"
|
||||||
},
|
},
|
||||||
"Associated Press – Business": {
|
"Associated Press – Business": {
|
||||||
"source_website": "apnews.com",
|
"source_website": "apnews.com",
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import feedparser
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
||||||
@ -27,6 +28,18 @@ MAX_FEED_WORKERS = int(os.getenv("MAX_FEED_WORKERS", "10"))
|
|||||||
MAX_ARTICLE_WORKERS = int(os.getenv("MAX_ARTICLE_WORKERS", "10"))
|
MAX_ARTICLE_WORKERS = int(os.getenv("MAX_ARTICLE_WORKERS", "10"))
|
||||||
BATCH_SIZE = int(os.getenv("BATCH_SIZE", "50")) # Batch processing size
|
BATCH_SIZE = int(os.getenv("BATCH_SIZE", "50")) # Batch processing size
|
||||||
|
|
||||||
|
# Rotating User-Agents to bypass bot detection (Reuters, etc.)
|
||||||
|
USER_AGENTS = [
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_random_ua():
|
||||||
|
return random.choice(USER_AGENTS)
|
||||||
|
|
||||||
# Ensure necessary NLTK resources are downloaded
|
# Ensure necessary NLTK resources are downloaded
|
||||||
d = Downloader()
|
d = Downloader()
|
||||||
if not d.is_installed("punkt_tab"):
|
if not d.is_installed("punkt_tab"):
|
||||||
@ -237,15 +250,18 @@ def save_article_to_file(article, filename, source="Unfiltered"):
|
|||||||
|
|
||||||
def get_article_with_selenium(url):
|
def get_article_with_selenium(url):
|
||||||
"""
|
"""
|
||||||
Gets article text using Selenium Firefox driver with proper error handling
|
Gets article text using Selenium Firefox driver with proper error handling,
|
||||||
and cleanup.
|
cleanup, and bot-detection evasion.
|
||||||
"""
|
"""
|
||||||
driver = None
|
driver = None
|
||||||
try:
|
try:
|
||||||
# Configure Firefox options
|
# Configure Firefox options with bot-detection evasion
|
||||||
options = FirefoxOptions()
|
options = FirefoxOptions()
|
||||||
options.add_argument("--headless")
|
options.add_argument("--headless")
|
||||||
options.set_preference("dom.ipc.processCount", 1) # Reduce process count
|
options.set_preference("dom.ipc.processCount", 1)
|
||||||
|
options.set_preference("general.useragent.override", get_random_ua())
|
||||||
|
options.set_preference("permissions.default.image", 2) # Block images for speed
|
||||||
|
options.set_preference("dom.webnotifications.enabled", False)
|
||||||
|
|
||||||
# Try to initialize driver with explicit path to Firefox
|
# Try to initialize driver with explicit path to Firefox
|
||||||
try:
|
try:
|
||||||
@ -269,10 +285,10 @@ def get_article_with_selenium(url):
|
|||||||
WebDriverWait(driver, 15).until(
|
WebDriverWait(driver, 15).until(
|
||||||
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
||||||
)
|
)
|
||||||
except:
|
except Exception:
|
||||||
pass # Continue even if wait times out
|
pass # Continue even if wait times out
|
||||||
|
|
||||||
time.sleep(2) # Brief additional wait
|
time.sleep(random.uniform(1, 3)) # Random wait to mimic human behavior
|
||||||
|
|
||||||
html = driver.page_source
|
html = driver.page_source
|
||||||
|
|
||||||
@ -296,35 +312,44 @@ def get_article_with_selenium(url):
|
|||||||
if driver:
|
if driver:
|
||||||
try:
|
try:
|
||||||
driver.quit()
|
driver.quit()
|
||||||
except:
|
except Exception:
|
||||||
pass # Ignore errors in cleanup
|
pass # Ignore errors in cleanup
|
||||||
|
|
||||||
|
|
||||||
def get_article_with_playwright(url):
|
def get_article_with_playwright(url):
|
||||||
"""
|
"""
|
||||||
Gets article text using Playwright with proper error handling.
|
Gets article text using Playwright with proper bot-detection evasion.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
# Use Chromium instead of Firefox for better compatibility
|
# Use Chromium with full browser context for UA spoofing
|
||||||
browser = p.chromium.launch(headless=True, timeout=30000)
|
browser = p.chromium.launch(headless=True, timeout=30000)
|
||||||
page = browser.new_page()
|
context = browser.new_context(
|
||||||
|
user_agent=get_random_ua(),
|
||||||
# Set user agent to avoid bot detection
|
viewport={"width": 1920, "height": 1080},
|
||||||
page.set_extra_http_headers(
|
locale="en-US",
|
||||||
{
|
timezone_id="America/New_York",
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
page.goto(url, wait_until="load")
|
# Additional headers for legitimacy
|
||||||
|
page.set_extra_http_headers({
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
page.goto(url, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
|
||||||
# Wait for content to load
|
# Wait for content to load
|
||||||
time.sleep(3)
|
time.sleep(random.uniform(2, 4))
|
||||||
|
|
||||||
html = page.content()
|
html = page.content()
|
||||||
|
context.close()
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
||||||
# Parse with Newspaper4k
|
# Parse with Newspaper4k
|
||||||
@ -353,11 +378,15 @@ def pull_article(link, source, title=None, save_to_file=True):
|
|||||||
) as f:
|
) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
# Random delay before fetching to avoid rate-limiting / bot detection
|
||||||
|
time.sleep(random.uniform(0.5, 2))
|
||||||
|
|
||||||
text = ""
|
text = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try newspaper4k first
|
# Try newspaper4k first with proper User-Agent to bypass bot detection
|
||||||
article = newspaper.article(link)
|
ua = get_random_ua()
|
||||||
|
article = newspaper.article(link, browser_user_agent=ua)
|
||||||
article.download()
|
article.download()
|
||||||
article.parse()
|
article.parse()
|
||||||
text = article.text
|
text = article.text
|
||||||
@ -378,7 +407,7 @@ def pull_article(link, source, title=None, save_to_file=True):
|
|||||||
logger.info(f"Successfully pulled article from {link} with Playwright")
|
logger.info(f"Successfully pulled article from {link} with Playwright")
|
||||||
|
|
||||||
if not text or len(text) < 200:
|
if not text or len(text) < 200:
|
||||||
logger.warning(f"Playwright article too short, falling back to Selenium.")
|
logger.warning("Playwright article too short, falling back to Selenium.")
|
||||||
# Fallback to Selenium with better error handling
|
# Fallback to Selenium with better error handling
|
||||||
text = get_article_with_selenium(link)
|
text = get_article_with_selenium(link)
|
||||||
logger.info(f"Successfully pulled article from {link} with Selenium")
|
logger.info(f"Successfully pulled article from {link} with Selenium")
|
||||||
@ -511,7 +540,7 @@ def main():
|
|||||||
with open("errors.txt", "w", encoding="utf-8") as f:
|
with open("errors.txt", "w", encoding="utf-8") as f:
|
||||||
for error in errors:
|
for error in errors:
|
||||||
f.write(str(error) + "\n")
|
f.write(str(error) + "\n")
|
||||||
logger.info(f"Errors logged to errors.txt")
|
logger.info("Errors logged to errors.txt")
|
||||||
|
|
||||||
# Print all results to a log file
|
# Print all results to a log file
|
||||||
with open("results.txt", "w", encoding="utf-8") as f:
|
with open("results.txt", "w", encoding="utf-8") as f:
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script to verify the enhanced cache system implementation
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import datetime
|
|
||||||
from scraper.scraper import load_processed_cache, save_processed_cache, get_processing_progress
|
|
||||||
|
|
||||||
def test_cache_system():
|
|
||||||
"""Test the enhanced cache system"""
|
|
||||||
print("Testing enhanced cache system...")
|
|
||||||
|
|
||||||
# Test loading cache (should work even if file doesn't exist)
|
|
||||||
cache = load_processed_cache()
|
|
||||||
print(f"Initial cache loaded: {len(cache)} entries")
|
|
||||||
|
|
||||||
# Test saving cache
|
|
||||||
test_entry = {
|
|
||||||
"test_article_path": {
|
|
||||||
"processed_date": datetime.datetime.now().isoformat(),
|
|
||||||
"status": "completed",
|
|
||||||
"embedding_status": "pending",
|
|
||||||
"last_updated": datetime.datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
save_processed_cache(test_entry)
|
|
||||||
print("Cache saved successfully")
|
|
||||||
|
|
||||||
# Test loading again
|
|
||||||
cache = load_processed_cache()
|
|
||||||
print(f"Cache loaded after save: {len(cache)} entries")
|
|
||||||
|
|
||||||
# Test progress tracking
|
|
||||||
progress = get_processing_progress()
|
|
||||||
print(f"Progress tracking: {progress}")
|
|
||||||
|
|
||||||
print("Cache system test completed successfully!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_cache_system()
|
|
||||||
61
tests/test_scraper_cache.py
Normal file
61
tests/test_scraper_cache.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""Tests for the scraper article-processing cache."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
|
from scraper.scraper import ( # noqa: E402
|
||||||
|
get_processing_progress,
|
||||||
|
load_processed_cache,
|
||||||
|
mark_article_processed,
|
||||||
|
save_processed_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cache_dir(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
(tmp_path / "articles").mkdir()
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_missing_cache_returns_empty(cache_dir):
|
||||||
|
assert load_processed_cache() == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_and_load_roundtrip(cache_dir):
|
||||||
|
save_processed_cache({"a.html": {"status": "completed"}})
|
||||||
|
cache = load_processed_cache()
|
||||||
|
assert cache["a.html"]["status"] == "completed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mark_article_processed(cache_dir):
|
||||||
|
mark_article_processed("articles/x/article.html")
|
||||||
|
cache = load_processed_cache()
|
||||||
|
entry = cache["articles/x/article.html"]
|
||||||
|
assert entry["status"] == "completed"
|
||||||
|
assert entry["embedding_status"] == "pending"
|
||||||
|
assert "processed_date" in entry
|
||||||
|
assert "last_updated" in entry
|
||||||
|
|
||||||
|
|
||||||
|
def test_processing_progress_empty(cache_dir):
|
||||||
|
progress = get_processing_progress()
|
||||||
|
assert progress["total_articles"] == 0
|
||||||
|
assert progress["completed_articles"] == 0
|
||||||
|
assert progress["embedded_articles"] == 0
|
||||||
|
assert progress["completion_rate"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_processing_progress_mixed(cache_dir):
|
||||||
|
mark_article_processed("a.html")
|
||||||
|
mark_article_processed("b.html", status="failed")
|
||||||
|
mark_article_processed("c.html", embedding_status="completed")
|
||||||
|
progress = get_processing_progress()
|
||||||
|
assert progress["total_articles"] == 3
|
||||||
|
assert progress["completed_articles"] == 2
|
||||||
|
assert progress["embedded_articles"] == 1
|
||||||
|
assert progress["completion_rate"] == pytest.approx(2 / 3 * 100)
|
||||||
Loading…
x
Reference in New Issue
Block a user