StockDocs/embedding/embedder.py
Jarian Cottingham c534c5a159 user/jarian/addingScraperDocker (#2)
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
2025-07-16 01:06:34 -05:00

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)