133 lines
5.9 KiB
Python
133 lines
5.9 KiB
Python
import os
|
|
import requests
|
|
import json
|
|
|
|
LOCAL_AI_SERVICE_URL = "http://192.168.8.124:11434"
|
|
|
|
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):
|
|
"""
|
|
Loads all articles from text files in the specified folder and returns them as a list of ArticleFile objects.
|
|
"""
|
|
articles = []
|
|
for newspaper in os.listdir(folder_path):
|
|
for filename in os.listdir(os.path.join(folder_path, newspaper)):
|
|
file_path = os.path.join(folder_path, newspaper, filename)
|
|
if os.path.isfile(file_path):
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
first_line = f.readline()
|
|
if first_line.startswith("SOURCE:"):
|
|
source = first_line[len("SOURCE:"):].strip()
|
|
content = f.read()
|
|
else:
|
|
source = "Unfiltered"
|
|
content = first_line + f.read()
|
|
articles.append(ArticleFile(filename, content, source))
|
|
|
|
print(f"Loaded {len(articles)} 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
|
|
# TODO: In the future, we should force the response to be json and adjust the temperature
|
|
}
|
|
|
|
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 : protentially 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:
|
|
# 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
|
|
|
|
# Retrieve the current archive of pulled articles
|
|
articles_folder = os.path.join(os.path.dirname(__file__), "../scraper/articles")
|
|
print("Loading articles from folder " + articles_folder + " ...")
|
|
articles = load_articles_from_folder(articles_folder)
|
|
|
|
# Process the articles retrieved
|
|
results = []
|
|
for article in articles:
|
|
try:
|
|
result = process_article(article)
|
|
|
|
# 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)
|
|
except Exception as e:
|
|
print(f"Error processing or saving article {article.filename}: {e}")
|