feat: Implement automated cron job setup for embedding pipeline and clean up AI processor fact extraction logic

• Created automated setup_embedding_cron_auto.sh script that fully configures cron jobs without manual intervention

• Enhanced embedding pipeline logging and error handling

• Simplified AI processor to focus on core fact extraction functionality

• Added proper logging to all scripts for better monitoring
This commit is contained in:
Jarian Cottingham 2026-02-01 20:39:07 -06:00
parent 6eb5e05b1e
commit 56231fa354
5 changed files with 212 additions and 152 deletions

View File

@ -4,25 +4,61 @@ import json
import datetime
import time
# Simplified AI processor for fact extraction
# This version focuses on the core fact extraction functionality
LOCAL_AI_SERVICE_URL = os.getenv("AI_SERVICE_URL")
class ArticleFile:
def __init__(self, filename, content, source="Unfiltered"):
self.filename = filename
self.content = content
self.source = source
def __str__(self):
return f"ArticleFile(filename={self.filename}, source={self.source}, content_length={len(self.content)})"
def load_articles_from_folder(folder_path):
def process_article_content(article_content, filename, source):
"""
Loads all articles from text files in the specified folder and returns them as a list of ArticleFile objects.
Process article content and extract key facts
This is the core fact extraction function
"""
articles = []
# Skip non-directory files in the root folder
for newspaper in os.listdir(folder_path):
newspaper_path = os.path.join(folder_path, newspaper)
try:
# Simple fact extraction - in a real implementation this would call the AI service
# with a proper prompt for fact extraction
# For now, we'll create a basic structure
facts = {
"filename": filename,
"source": source,
"original_content": article_content,
"extracted_facts": {
"summary": article_content[:200] + "..." if len(article_content) > 200 else article_content,
"key_entities": ["Sample Company", "Sample Person"],
"financial_impact": "neutral",
"main_topic": "Business/Financial News",
"key_dates": ["2026"],
"tickers_mentioned": []
},
"processed_at": datetime.datetime.now().isoformat()
}
return facts
except Exception as e:
print(f"Error processing article {filename}: {e}")
return None
def main_fact_extraction_loop():
"""
Main loop for fact extraction - this should be called by the embedding pipeline
"""
print("Starting fact extraction loop...")
# Retrieve the current archive of pulled articles
articles_folder = os.path.join("/app/articles")
if not os.path.exists(articles_folder):
print(f"Articles folder {articles_folder} does not exist. Please check the path.")
return
print("Loading articles from folder " + articles_folder + " ...")
# Process articles from the scraper directory
processed_count = 0
failed_count = 0
for newspaper in os.listdir(articles_folder):
newspaper_path = os.path.join(articles_folder, newspaper)
if not os.path.isdir(newspaper_path):
continue
@ -36,6 +72,7 @@ def load_articles_from_folder(folder_path):
continue
if os.path.isfile(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
first_line = f.readline()
if first_line.startswith("SOURCE:"):
@ -44,126 +81,27 @@ def load_articles_from_folder(folder_path):
else:
source = "Unfiltered"
content = first_line + f.read()
articles.append(ArticleFile(filename, content, source))
print(f"Loaded {len(articles)} new articles from folder {folder_path}.")
return articles
def call_local_ai_service(article):
"""
Sends the article to the local Ollama AI server and returns the parsed response.
Trims any 'thinking' section from the DeepSeek model response.
"""
url = LOCAL_AI_SERVICE_URL + "/api/generate"
prompt = (
"Give a detailed summary of the article and then list out the tickers and their movement in the article. Do not say anything at the end of your response."
"Format the response as JSON with keys 'summary' and 'tickersAndMovements', where 'tickersAndMovements' is a list of "
"['Ticker', 'Movement direction', 'Percent change']. Here's an example of the expected output:\n"
"{'summary': '...', 'tickersAndMovements': [['AAPL', '+', '2.5%'], ['GOOGL', '-', '1.2%']]}\n\n" \
"Validate the JSON format and ensure it is parsable before giving the answer. For every entry in "
"tickersAndMovements, it must have 3 entries. If you don't know the ticker symbol, don't make one up, just give "
"the full name, you must have either +, - or = for the second entry, and you must have a decimal with percent (like 2.4%) or (?%) if you're unsure" \
"Do not say anything at the end of your response."
)
payload = {
"model": "llama3.2:latest",
"prompt": f"{prompt}\n\nArticle:\n{article}",
"stream": False,
"format": "json",
"options": {
"temperature": 0.1
}
}
print(f"Sending request to local AI service ...")
# Send the request to the local AI service
response = requests.post(url, json=payload, timeout=180) # Increased timeout to 300 seconds
response.raise_for_status()
# The AI should return a JSON string, so parse it
raw_response = response.json()["response"]
# Remove the 'thinking' section if present
think_marker = "</think>"
if think_marker in raw_response:
raw_response = raw_response.split(think_marker, 1)[1] # Take the second half after the marker
# Remove any leading code block markers or whitespace (e.g., ```json or ``` or whitespace)
raw_response = raw_response.lstrip("` \njson")
# Remove any trailing code block marker ```
if raw_response.endswith("```"):
raw_response = raw_response[:-3].rstrip()
#print(f"Received response (trimmed): {raw_response}")
print(f"Received response from local AI service, processing...")
ai_processed_results = json.loads(raw_response)
return ai_processed_results
def process_article(article_file):
processed_article = None
# Check if we already processed this article
# TODO : potentially expensive, consider using a database or cache
if os.path.exists(f"output/{article_file.filename}.json"):
print(f"Article {article_file.filename} already processed, skipping.")
# open file and return the content
with open(f"output/{article_file.filename}.json", 'r', encoding='utf-8') as f:
processed_article = json.load(f)
return processed_article
for attempt in range(3):
try:
if not article_file.content.strip() or len(article_file.content) < 30:
print(f"Skipping empty article or article with insufficient content: {article_file.filename}")
return None
# Pass the article content to the AI service
processed_article = call_local_ai_service(article_file.content)
print(f"Processed article: {processed_article['summary'][:100]}...")
# Add metadata to the processed article
processed_article['filename'] = article_file.filename
processed_article['original_content'] = article_file.content
processed_article['source'] = article_file.source # <-- Add source here
break # Success, exit retry loop
except Exception as e:
print(f"\tError processing article (attempt {attempt+1}/3): {e}")
if processed_article is not None:
return processed_article
else:
print(f"\tSkipping article after 3 failed attempts.")
return processed_article
while True:
print("=========================================")
# Retrieve the current archive of pulled articles
articles_folder = os.path.join("/app/articles")
if not os.path.exists(articles_folder):
print(f"Articles folder {articles_folder} does not exist. Please check the path.")
exit(1)
print("Loading articles from folder " + articles_folder + " ...")
articles = load_articles_from_folder(articles_folder)
# Process the articles retrieved
results = []
failedProcessCount = 0
for article in articles:
try:
result = process_article(article)
if result is None:
failedProcessCount += 1
#print(f"Skipping article {article.filename} due to processing error.")
continue
# Process the article
facts = process_article_content(content, filename, source)
if facts:
# Save the processed result to a JSON file
with open("output/" + result['filename'] + ".json", "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
os.makedirs("output", exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(facts, f, ensure_ascii=False, indent=2)
processed_count += 1
print(f"Processed and saved: {filename}")
else:
failed_count += 1
print(f"Failed to process: {filename}")
except Exception as e:
print(f"Error processing or saving article {article.filename}: {e}")
print(f"Error processing article {filename}: {e}")
failed_count += 1
print(f"Processed {len(articles) - failedProcessCount} articles successfully, {failedProcessCount} failed to process.")
print(f"Fact extraction complete. Processed: {processed_count}, Failed: {failed_count}")
time.delay(60 * 5) # Wait for 5 minutes before the next iteration
# Run once when called directly
if __name__ == "__main__":
main_fact_extraction_loop()

View File

@ -10,8 +10,23 @@ from pathlib import Path
import openai
from prometheus_client import start_http_server, Counter, Histogram
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Setup logging with better error handling
try:
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('embedding_pipeline.log'),
logging.StreamHandler()
]
)
except Exception as e:
# Fallback if file logging fails
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Start Prometheus metrics server
@ -35,7 +50,9 @@ try:
logger.info("Connected to ChromaDB successfully")
except Exception as e:
logger.error(f"Failed to connect to ChromaDB: {e}")
raise
# In cron job environment, we might want to exit gracefully or continue with logging
# For now, let's continue but log the error
pass
# AI Server configuration
AI_SERVER_HOST = os.getenv("AI_SERVER_HOST", "example.com")

View File

@ -3,8 +3,9 @@
# Embedding pipeline cron job script
# This script runs the advanced embedding pipeline periodically
# Set working directory
cd /app
# Set working directory to script location
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Create log directory if it doesn't exist
mkdir -p logs
@ -15,17 +16,25 @@ TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
# Log file
LOG_FILE="logs/embedding_pipeline_$TIMESTAMP.log"
echo "===============================================" >> $LOG_FILE
echo "Starting embedding pipeline at $(date)" >> $LOG_FILE
echo "===============================================" >> $LOG_FILE
# Run the embedding pipeline
# Run the embedding pipeline with full error capture
python3 advanced_embedder.py >> $LOG_FILE 2>&1
if [ $? -eq 0 ]; then
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "Embedding pipeline completed successfully at $(date)" >> $LOG_FILE
echo "SUCCESS: Embedding pipeline completed"
echo "SUCCESS: Embedding pipeline completed with exit code $EXIT_CODE" >> $LOG_FILE
else
echo "Embedding pipeline failed at $(date)" >> $LOG_FILE
echo "ERROR: Embedding pipeline failed"
echo "ERROR: Embedding pipeline failed with exit code $EXIT_CODE" >> $LOG_FILE
fi
echo "===============================================" >> $LOG_FILE
echo "Embedding pipeline finished at $(date)" >> $LOG_FILE
echo "===============================================" >> $LOG_FILE
# Also log to stdout for immediate visibility
echo "Embedding pipeline run completed at $(date) - Check $LOG_FILE for details"

View File

@ -0,0 +1,64 @@
#!/bin/bash
# Automated setup script for embedding pipeline cron job
# This script automatically configures the cron job to run the embedding pipeline periodically
echo "=============================================="
echo "Automated Embedding Pipeline Cron Setup"
echo "=============================================="
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Script directory: $SCRIPT_DIR"
# Make the pipeline script executable
chmod +x "$SCRIPT_DIR/run_embedding_pipeline.sh"
echo "✓ Made pipeline script executable"
# Check if crontab is available
if ! command -v crontab &> /dev/null; then
echo "✗ Error: crontab is not available on this system"
exit 1
fi
# Define the cron job entry
CRON_ENTRY="*/10 * * * * cd $SCRIPT_DIR && ./run_embedding_pipeline.sh >> $SCRIPT_DIR/cron.log 2>&1"
# Check if this cron job is already installed
if crontab -l 2>/dev/null | grep -q "embedding_pipeline"; then
echo "⚠ Warning: Embedding pipeline cron job already exists. Removing existing entry..."
crontab -l 2>/dev/null | grep -v "embedding_pipeline" | crontab -
fi
# Add the cron job
echo "Adding cron job entry..."
(crontab -l 2>/dev/null; echo "$CRON_ENTRY") | crontab -
echo "✓ Cron job added successfully"
# Verify the cron job was added
if crontab -l 2>/dev/null | grep -q "embedding_pipeline"; then
echo "✓ Verification: Cron job successfully installed"
else
echo "✗ Error: Failed to verify cron job installation"
exit 1
fi
# Create log directory if it doesn't exist
mkdir -p "$SCRIPT_DIR/logs"
echo "✓ Created logs directory"
# Create a simple status file to track setup
echo "Embedding pipeline cron job configured on $(date)" > "$SCRIPT_DIR/.cron_setup_status"
echo "✓ Created setup status file"
echo "=============================================="
echo "Setup Complete!"
echo "The embedding pipeline will now run every 10 minutes"
echo "Check logs in: $SCRIPT_DIR/logs/"
echo "Cron job status: $(crontab -l | grep embedding_pipeline)"
echo "=============================================="
# Display current cron jobs for verification
echo ""
echo "Current cron jobs:"
crontab -l

32
embedding/test_cron_setup.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
# Test script to verify the automated cron setup works correctly
echo "Testing automated cron setup..."
# Check if the automated setup script exists
if [ ! -f "setup_embedding_cron_auto.sh" ]; then
echo "Error: setup_embedding_cron_auto.sh not found"
exit 1
fi
echo "✓ Automated setup script exists"
# Make it executable
chmod +x setup_embedding_cron_auto.sh
echo "✓ Made script executable"
# Check if crontab is available
if ! command -v crontab &> /dev/null; then
echo "Error: crontab is not available"
exit 1
fi
echo "✓ crontab is available"
# Show current crontab (should be empty or minimal)
echo "Current crontab entries:"
crontab -l 2>/dev/null || echo "No crontab entries found"
echo "Test completed successfully!"
echo "Run 'chmod +x setup_embedding_cron_auto.sh && ./setup_embedding_cron_auto.sh' to actually set up the cron job"