PrimoGreedy-Agent / src /core /memory.py
CiscsoPonce's picture
feat: LangGraph best-practices refactor (Sprint 8 Epics 2-3-4)
d5ba3a3
import json
import os
import time
import requests
from .logger import get_logger
logger = get_logger(__name__)
SEEN_TICKERS_FILE = "seen_tickers.json"
MEMORY_TTL_SECONDS = 30 * 24 * 60 * 60 # 30 days
# VPS Data API (optional — falls back to local JSON if not set)
VPS_API_URL = os.getenv("VPS_API_URL", "").rstrip("/")
VPS_API_KEY = os.getenv("VPS_API_KEY", "")
def _vps_headers() -> dict:
return {"X-API-Key": VPS_API_KEY, "Content-Type": "application/json"}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def load_seen_tickers() -> dict[str, float]:
"""Load the seen-tickers ledger, pruning entries older than 30 days.
Values are Unix timestamps (float).
Tries VPS API first, falls back to local JSON file.
"""
if VPS_API_URL:
try:
resp = requests.get(f"{VPS_API_URL}/seen-tickers", headers=_vps_headers(), timeout=5)
resp.raise_for_status()
data = resp.json()
logger.debug("Loaded %d seen tickers from VPS", len(data))
return data
except Exception as exc:
logger.warning("VPS seen-tickers unavailable, using local fallback: %s", exc)
return _load_local()
def mark_ticker_seen(ticker: str, region: str = "USA") -> None:
"""Record a ticker as recently analysed."""
if VPS_API_URL:
try:
resp = requests.post(
f"{VPS_API_URL}/seen-tickers",
headers=_vps_headers(),
json={"ticker": ticker, "region": region},
timeout=5,
)
resp.raise_for_status()
logger.debug("Marked %s as seen on VPS", ticker)
return
except Exception as exc:
logger.warning("VPS mark_ticker_seen failed, using local fallback: %s", exc)
# Local fallback
data = _load_local()
data[ticker] = time.time()
_save(data)
# ---------------------------------------------------------------------------
# Local JSON fallback (original behavior)
# ---------------------------------------------------------------------------
def _load_local() -> dict[str, float]:
if not os.path.exists(SEEN_TICKERS_FILE):
return {}
try:
with open(SEEN_TICKERS_FILE, "r") as f:
raw: dict = json.load(f)
now = time.time()
cleaned: dict[str, float] = {}
for ticker, ts in raw.items():
if isinstance(ts, str):
try:
from datetime import datetime, timezone
ts = datetime.fromisoformat(ts).timestamp()
except ValueError:
continue
if now - ts < MEMORY_TTL_SECONDS:
cleaned[ticker] = ts
_save(cleaned)
return cleaned
except (json.JSONDecodeError, OSError) as exc:
logger.warning("Could not read %s: %s", SEEN_TICKERS_FILE, exc)
return {}
def _save(data: dict) -> None:
try:
with open(SEEN_TICKERS_FILE, "w") as f:
json.dump(data, f, indent=2)
except OSError as exc:
logger.error("Failed to write %s: %s", SEEN_TICKERS_FILE, exc)