This fully implements the Stock Docs Project with full Docker Containerization support. This is a working prototype that is actively running on the Media Server. There's a few issues noted, including the following: - Support for some sites could be improved. Reuters has many articles behind an adblock and some websites present banners that don't need to be processed by our AI engine - Some caching could be smarter. As the size of files grows, it will get expensive to search through all files to be sure we've not scraped it, ai proccessed it or embedded it. - Logging could be improved to be much better than just print statements and telemetry could be sent for dashboard monitoring if this were ever to become a full service where we cared about reliability. - MCP server has been noted to return some poorly matching results. Would be better if it returned nothing at all. And should never really return banners or ads as that provides awful input for the model. Perhaps the model could be told to not care about this, but it's better to just never show irrelevant info to the model I think this is an overall really good jumping off point, and we've already gotten to see the max capabilities of our system so far. It's a major win to have the Scraper for instance running at all times getting articles from across the web. I look forward to expending this scraper in the near future for projects like scraping all local news websites in the US or general scraping and monitoring of websites. Co-authored-by: Jarian Cottingham <jariancottingham@dev-machine.local> Co-authored-by: jarianc <user@example.com> Reviewed-on: http://git.example.com/jarianc/StockDocs/pulls/2
136 lines
5.4 KiB
Python
136 lines
5.4 KiB
Python
import os
|
|
import json
|
|
import chromadb
|
|
import uuid
|
|
import time
|
|
import datetime
|
|
from sentence_transformers import SentenceTransformer
|
|
|
|
CHROMADB_HOST = os.getenv("CHROMADB_HOST")
|
|
CHROMADB_PORT = os.getenv("CHROMADB_PORT")
|
|
|
|
model = SentenceTransformer(
|
|
"Snowflake/snowflake-arctic-embed-m-long",
|
|
device="cpu", # <-- the only line that changes
|
|
trust_remote_code=True
|
|
)
|
|
|
|
|
|
import math
|
|
def embed_text(text, specific_context, max_tokens=2048, overlap=256):
|
|
"""
|
|
Embeds the given text using the SentenceTransformer model.
|
|
Splits the text into chunks if it exceeds max_tokens.
|
|
"""
|
|
if len(text) + len(specific_context) <= max_tokens:
|
|
return (text + specific_context, model.encode([text, specific_context], normalize_embeddings=True))
|
|
|
|
# Split text into chunks
|
|
chunks = []
|
|
start = 0
|
|
|
|
chunkNum = math.ceil(len(text) / (max_tokens - len(specific_context)))
|
|
# 1000 + 20 = 1020
|
|
# 3000 + 20 = 3020 -> 3000 / (2048 - 20) = 1.47 ~ 2
|
|
|
|
for i in range(chunkNum):
|
|
if start >= len(text):
|
|
break
|
|
# Calculate end index for the chunk
|
|
end = min(start + max_tokens - len(specific_context), len(text))
|
|
chunk = text[start:end] + specific_context
|
|
chunks.append(chunk)
|
|
start += (max_tokens - overlap) # Overlap for next chunk
|
|
|
|
return (chunks, model.encode(chunks, normalize_embeddings=True))
|
|
|
|
|
|
client = chromadb.HttpClient(host=CHROMADB_HOST, port=CHROMADB_PORT)
|
|
|
|
# client.delete_collection("news") # Replace "news" with your collection name
|
|
|
|
collection = client.get_or_create_collection("news")
|
|
|
|
# Load cache of previously embedded articles
|
|
cache_file = "embedded_articles_cache.json"
|
|
|
|
# Load all the processed articles from the output folder
|
|
output_folder = "/app/output"
|
|
|
|
while True:
|
|
print(f"Starting embedding run at {datetime.datetime.now()}")
|
|
|
|
embedded_cache = {}
|
|
if os.path.exists(cache_file):
|
|
with open(cache_file, 'r', encoding='utf-8') as f:
|
|
embedded_cache = json.load(f)
|
|
|
|
output_articles = []
|
|
processed_articles = []
|
|
# When embedding and upserting, use the source from the processed result
|
|
for filename in os.listdir(output_folder):
|
|
if filename.endswith(".json"):
|
|
# Check if already embedded
|
|
if filename in embedded_cache:
|
|
print(f"Article {filename} already embedded, skipping.")
|
|
continue
|
|
|
|
with open(os.path.join(output_folder, filename), 'r', encoding='utf-8') as f:
|
|
prev_proc = json.load(f)
|
|
output_articles.append(prev_proc)
|
|
|
|
def makeTickerMovement(ticker, movement, percent_change):
|
|
"""
|
|
Helper function to create a ticker movement entry.
|
|
"""
|
|
ticker = "" if ticker is None else ticker
|
|
movement = "" if movement is None else movement
|
|
percent_change = "" if percent_change is None else percent_change
|
|
return [ticker, movement, percent_change]
|
|
|
|
# Embed the article content
|
|
try:
|
|
specific_context = prev_proc['summary'] + ",".join([' '.join(makeTickerMovement(*x)) for x in prev_proc['tickersAndMovements']])
|
|
chunks, embedded_content = embed_text(prev_proc['original_content'], prev_proc['summary'])
|
|
|
|
print(f"Embedded content for {filename}: \n {embedded_content[:10]}...") # Print first 10 values for preview
|
|
|
|
collection.upsert(
|
|
ids = [ str(uuid.uuid4()) for d in chunks ], # Generate unique IDs for each embedded content
|
|
documents = chunks, # optional but nice for debugging
|
|
embeddings = embedded_content.tolist(), # Chroma expects List[List[float]]
|
|
metadatas =[{
|
|
"source": prev_proc.get('source', 'Unknown'), # Use the source from the processed result
|
|
"published": prev_proc.get('published', 'Unknown'),
|
|
"filename": prev_proc.get('filename', 'Unknown') # Add filename for traceability
|
|
} for d in chunks], # Metadata for each embedded content
|
|
)
|
|
|
|
# Add to processed list for cache update
|
|
processed_articles.append(filename)
|
|
print(f"Successfully embedded {filename}")
|
|
|
|
except Exception as e:
|
|
print(f"Error embedding content for {filename}: {e}")
|
|
continue
|
|
|
|
# Update cache with newly processed articles
|
|
for filename in processed_articles:
|
|
embedded_cache[filename] = {
|
|
"embedded_date": str(datetime.datetime.now()),
|
|
"status": "completed"
|
|
}
|
|
|
|
# Save updated cache
|
|
try:
|
|
with open(cache_file, 'w', encoding='utf-8') as f:
|
|
json.dump(embedded_cache, f, indent=2)
|
|
print(f"Updated cache with {len(processed_articles)} newly embedded articles.")
|
|
except Exception as e:
|
|
print(f"Error saving cache file: {e}")
|
|
|
|
print(f"Embedding process completed. Total articles in cache: {len(embedded_cache)}")
|
|
print("Waiting 5 minutes before next run...")
|
|
time.sleep(300) # Wait 5 minutes (300 seconds)
|
|
|