rmi-backend/app/rag_firehose.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

877 lines
35 KiB
Python

"""
RAG Firehose - Continuous Intelligence Ingestion Engine
========================================================
Self-feeding RAG pipeline that continuously pulls, filters, and ingests
crypto intelligence from multiple sources at different cadences.
Architecture:
┌─────────────────────────────────────────────────────────┐
│ FIREHOSE ENGINE │
│ │
│ Hourly (news/social) Daily (scams/wallets) Weekly │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ News RSS │ │ Etherscan│ │ FAISS │ │
│ │ CT Rundn │ │ Chainab. │ │ rebuild │ │
│ │ Social │ │ Rekt DB │ │ RAGAS │ │
│ │ Sentiment│ │ Solana │ │ eval │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────────────────┼─────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ SMART INGESTION PIPELINE │ │
│ │ Filter → Dedup → Extract │ │
│ │ → Classify → Embed → Store │ │
│ └────────────┬───────────────┘ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ RAG COLLECTIONS │ │
│ │ known_scams, news_articles │ │
│ │ forensic_reports, etc. │ │
│ └────────────┬───────────────┘ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ FEEDBACK LOOP │ │
│ │ Scanner hits → boost docs │ │
│ │ False positives → penalize │ │
│ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Feed Cadences:
- 15min: CT rundown, social sentiment, scam alerts (high-urgency)
- 1hr: News RSS (200+ feeds), market brief, fear & greed
- 6hr: X/Twitter profiles, KOL tracking, prediction markets
- 24hr: Etherscan labels, Solana registry, chainabuse, rekt DB
- 72hr: FAISS index rebuild, BM25 rebuild, RAGAS evaluation
- 168hr: Full pattern extraction from confirmed scams, quality audit
Smart Ingestion:
- Content hash dedup (Redis) - never ingest the same doc twice
- Quality scoring - skip low-signal content (<30 score)
- Entity extraction - pull addresses, chains, tokens, protocols
- Auto-classification - route to correct collection
- Batch embedding with rate limiting - never overload embedder
- Per-collection size caps - auto-evict oldest on overflow
"""
import asyncio
import contextlib
import hashlib
import logging
import os
import time
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any
import httpx
logger = logging.getLogger("rag.firehose")
# ──────────────────────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────────────────────
RAG_API = "http://localhost:8000/api/v1/rag"
# Per-collection size caps (auto-evict oldest on overflow)
COLLECTION_CAPS = {
"known_scams": 50000, # scam addresses - keep forever, large
"scam_patterns": 5000, # curated patterns - small, high quality
"forensic_reports": 10000, # hack reports - medium
"contract_audits": 5000, # code audits - medium
"wallet_profiles": 100000, # labeled wallets - large
"news_articles": 20000, # news - rolling window
"market_intel": 5000, # market data - medium
"token_analysis": 50000, # token data - large
"transaction_patterns": 10000, # on-chain patterns - medium
"social_sentiment": 10000, # social data - rolling
"general": 10000, # misc - catch-all
}
# Quality thresholds (skip docs below this score)
MIN_QUALITY_SCORE = 30 # 0-100
# Rate limits (docs per minute per collection)
RATE_LIMITS = {
"known_scams": 60,
"news_articles": 30,
"social_sentiment": 20,
"default": 30,
}
# Content hash TTL in Redis (7 days for news, 30 for scams)
HASH_TTL = {
"news_articles": 604800, # 7 days
"social_sentiment": 604800, # 7 days
"known_scams": 2592000, # 30 days
"default": 1209600, # 14 days
}
# ──────────────────────────────────────────────────────────────
# Data Structures
# ──────────────────────────────────────────────────────────────
@dataclass
class FeedSource:
"""A data source the firehose pulls from."""
name: str
collection: str
cadence_seconds: int
fetch_fn: Any = field(repr=False)
enabled: bool = True
description: str = ""
last_run: float = 0
runs: int = 0
docs_ingested: int = 0
docs_skipped: int = 0
errors: int = 0
@dataclass
class FirehoseStats:
"""Current firehose statistics."""
running: bool = False
uptime_seconds: float = 0
total_sources: int = 0
total_ingested: int = 0
total_skipped: int = 0
total_errors: int = 0
sources: dict[str, dict] = field(default_factory=dict)
last_activity: float = 0
# ──────────────────────────────────────────────────────────────
# Smart Ingestion Pipeline
# ──────────────────────────────────────────────────────────────
class IngestionPipeline:
"""Filters, deduplicates, and enriches documents before RAG storage."""
def __init__(self, client: httpx.AsyncClient):
self.client = client
self._rate_trackers: dict[str, list[float]] = {} # collection → recent ingest timestamps
def _check_rate(self, collection: str) -> bool:
"""Return True if we're under the rate limit for this collection."""
limit = RATE_LIMITS.get(collection, RATE_LIMITS["default"])
now = time.monotonic()
times = self._rate_trackers.setdefault(collection, [])
# Remove timestamps older than 60s
times[:] = [t for t in times if now - t < 60]
return len(times) < limit
def _record_ingest(self, collection: str):
"""Record an ingestion for rate limiting."""
self._rate_trackers.setdefault(collection, []).append(time.monotonic())
def make_content_hash(self, content: str) -> str:
"""Deterministic hash for dedup."""
return hashlib.sha256(content.encode()).hexdigest()[:16]
async def is_duplicate(self, content_hash: str, collection: str) -> bool:
"""Check if content hash already exists in Redis dedup set."""
try:
resp = await self.client.get(
f"{RAG_API}/dedup-check",
params={"hash": content_hash, "collection": collection},
timeout=5,
)
if resp.status_code == 200:
return resp.json().get("exists", False)
except Exception:
pass
return False
def score_quality(self, content: str, metadata: dict) -> int:
"""Score document quality 0-100. Skip low-signal content."""
score = 50 # Start neutral
# Length bonus (substantial content is better)
if len(content) > 500:
score += 15
elif len(content) > 200:
score += 10
elif len(content) < 50:
score -= 20
# Entity richness (more entities = more useful)
entity_count = 0
if metadata.get("address"):
entity_count += 1
if metadata.get("chain"):
entity_count += 1
if metadata.get("token"):
entity_count += 1
if metadata.get("protocol"):
entity_count += 1
score += entity_count * 5
# Source authority
high_authority = {"etherscan", "chainabuse", "rekt", "certik", "slowmist", "peckshield"}
if metadata.get("source", "").lower() in high_authority:
score += 20
# Severity boost (critical scams are more valuable)
if metadata.get("severity") == "critical":
score += 15
elif metadata.get("severity") == "high":
score += 10
# Penalize duplicates of very similar content
if metadata.get("is_variant"):
score -= 10
return max(0, min(100, score))
def extract_entities(self, content: str) -> dict:
"""Extract addresses, chains, tokens from content."""
import re
entities = {"addresses": [], "chains": [], "tokens": [], "protocols": []}
# EVM addresses (0x...)
evm_addrs = re.findall(r"0x[a-fA-F0-9]{40}", content)
entities["addresses"].extend(evm_addrs[:10])
# Solana addresses (base58, 32-44 chars)
sol_addrs = re.findall(r"[1-9A-HJ-NP-Za-km-z]{32,44}", content)
entities["addresses"].extend([a for a in sol_addrs[:10] if a not in entities["addresses"]])
# Known chains
known_chains = [
"ethereum",
"bsc",
"polygon",
"arbitrum",
"optimism",
"avalanche",
"solana",
"base",
"fantom",
"gnosis",
"celo",
"zksync",
"linea",
"scroll",
"mantle",
"sui",
"aptos",
"near",
"tron",
"bitcoin",
]
for chain in known_chains:
if chain.lower() in content.lower():
entities["chains"].append(chain)
# Token symbols ($TOKEN)
tokens = re.findall(r"\$([A-Z]{2,10})", content)
entities["tokens"].extend(tokens[:10])
# Known protocols
known_protocols = [
"uniswap",
"aave",
"curve",
"balancer",
"sushi",
"pancake",
"raydium",
"jupiter",
"orca",
"marinade",
"lido",
"eigenlayer",
"compound",
"maker",
"yearn",
"convex",
]
for proto in known_protocols:
if proto.lower() in content.lower():
entities["protocols"].append(proto)
return entities
async def ingest(self, collection: str, content: str, metadata: dict, doc_id: str | None = None) -> dict[str, Any]:
"""Run the full pipeline: dedup → quality → extract → classify → store."""
# 1. Hash and dedup
content_hash = self.make_content_hash(content)
if await self.is_duplicate(content_hash, collection):
return {"status": "duplicate", "hash": content_hash}
# 2. Quality filter
quality = self.score_quality(content, metadata)
if quality < MIN_QUALITY_SCORE:
return {"status": "skipped", "reason": "low_quality", "score": quality}
# 3. Entity extraction
entities = self.extract_entities(content)
metadata.update(
{
"entities": entities,
"quality_score": quality,
"content_hash": content_hash,
"ingested_at": datetime.now(UTC).isoformat(),
}
)
# 4. Rate limit check
if not self._check_rate(collection):
return {"status": "rate_limited", "collection": collection}
# 5. Ingest into RAG
try:
payload = {
"collection": collection,
"content": content,
"metadata": metadata,
}
if doc_id:
payload["doc_id"] = doc_id
resp = await self.client.post(f"{RAG_API}/ingest", json=payload, timeout=30)
self._record_ingest(collection)
if resp.status_code == 200:
result = resp.json()
# Track in dedup set
asyncio.create_task(self._mark_ingested(content_hash, collection))
return {
"status": "ingested",
"id": result.get("id"),
"quality": quality,
"entities": entities,
}
else:
return {"status": "error", "code": resp.status_code, "detail": resp.text[:200]}
except Exception as e:
return {"status": "error", "detail": str(e)[:200]}
async def _mark_ingested(self, content_hash: str, collection: str):
"""Mark content hash in Redis dedup set."""
try:
ttl = HASH_TTL.get(collection, HASH_TTL["default"])
await self.client.post(
f"{RAG_API}/dedup-mark",
json={"hash": content_hash, "collection": collection, "ttl": ttl},
timeout=5,
)
except Exception:
pass
async def ingest_batch(self, docs: list[dict]) -> dict[str, int]:
"""Ingest multiple documents with rate limiting."""
stats = {"ingested": 0, "duplicate": 0, "skipped": 0, "errors": 0}
for doc in docs:
collection = doc.get("collection", "general")
content = doc.get("content", "")
metadata = doc.get("metadata", {})
doc_id = doc.get("doc_id")
result = await self.ingest(collection, content, metadata, doc_id)
status = result.get("status", "error")
if status == "ingested":
stats["ingested"] += 1
elif status == "duplicate":
stats["duplicate"] += 1
elif status == "skipped":
stats["skipped"] += 1
else:
stats["errors"] += 1
# Small delay between docs to avoid overwhelming embedder
await asyncio.sleep(0.05)
return stats
# ──────────────────────────────────────────────────────────────
# Feed Sources - Pull Functions
# ──────────────────────────────────────────────────────────────
class FeedSources:
"""All data sources the firehose can pull from."""
def __init__(self, client: httpx.AsyncClient):
self.client = client
# ── Hourly: News ──
async def pull_news_rss(self) -> list[dict]:
"""Pull latest news from DataBus news provider (200+ RSS feeds)."""
try:
resp = await self.client.get(
"http://localhost:8000/api/v1/databus/fetch/news", params={"limit": 30}, timeout=30
)
if resp.status_code != 200:
return []
data = resp.json()
articles = data.get("articles", data.get("data", []))
docs = []
for article in (articles if isinstance(articles, list) else [])[:20]:
title = article.get("title", "")
desc = article.get("description", article.get("summary", ""))
if not title:
continue
content = f"News: {title}. {desc}"
docs.append(
{
"collection": "news_articles",
"content": content[:2000],
"metadata": {
"source": article.get("source", "news_rss"),
"url": article.get("url", ""),
"published": article.get("published_at", article.get("date", "")),
"category": article.get("category", "crypto"),
"title": title,
},
}
)
return docs
except Exception as e:
logger.warning(f"News RSS pull failed: {e}")
return []
async def pull_ct_rundown(self) -> list[dict]:
"""Pull CT Rundown stories."""
try:
resp = await self.client.get(
"http://localhost:8000/api/v1/databus/fetch/ct_rundown",
params={"limit": 10},
timeout=30,
)
if resp.status_code != 200:
return []
data = resp.json()
stories = data.get("stories", data.get("data", []))
docs = []
for story in (stories if isinstance(stories, list) else [])[:5]:
content = f"CT: {story.get('title', '')} - {story.get('summary', '')}"
docs.append(
{
"collection": "news_articles",
"content": content[:1500],
"metadata": {
"source": "ct_rundown",
"category": "crypto_twitter",
"handle": story.get("handle", ""),
"engagement": story.get("engagement", 0),
},
}
)
return docs
except Exception as e:
logger.warning(f"CT rundown pull failed: {e}")
return []
async def pull_social_sentiment(self) -> list[dict]:
"""Pull social sentiment and scam alerts."""
docs = []
try:
# Scam monitor
resp = await self.client.get("http://localhost:8000/api/v1/databus/fetch/scam_monitor", timeout=20)
if resp.status_code == 200:
data = resp.json()
alerts = data.get("alerts", data.get("data", []))
for alert in (alerts if isinstance(alerts, list) else [])[:10]:
docs.append(
{
"collection": "social_sentiment",
"content": f"Scam alert: {alert.get('title', '')} - {alert.get('description', '')}",
"metadata": {
"source": "scam_monitor",
"severity": alert.get("severity", "medium"),
"token": alert.get("token_address", ""),
"chain": alert.get("chain", ""),
},
}
)
# Social metrics
resp2 = await self.client.get("http://localhost:8000/api/v1/databus/fetch/social_metrics", timeout=20)
if resp2.status_code == 200:
data2 = resp2.json()
metrics = data2.get("metrics", data2.get("data", {}))
if metrics:
docs.append(
{
"collection": "social_sentiment",
"content": f"Social metrics: Sentiment {metrics.get('sentiment', '?')}, "
f"Trending: {metrics.get('trending_topics', [])}",
"metadata": {"source": "social_metrics", "type": "daily_summary"},
}
)
except Exception as e:
logger.warning(f"Social pull failed: {e}")
return docs
# ── Daily: Scam Databases ──
async def pull_etherscan_labels(self) -> list[dict]:
"""Pull etherscan labeled addresses (via existing CSV)."""
docs = []
csv_path = os.path.join(os.path.dirname(__file__), "..", "data", "etherscan_phish_hack.csv")
if not os.path.exists(csv_path):
return docs
import csv
try:
with open(csv_path, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
for row in rows:
addr = row.get("address", "").strip()
if not addr:
continue
content = (
f"Etherscan label: {addr} on {row.get('chain', 'Ethereum')}. "
f"Tag: {row.get('name_tag', '')}. Type: {row.get('label_type', 'scam')}."
)
docs.append(
{
"collection": "known_scams",
"content": content,
"metadata": {
"address": addr.lower(),
"chain": (row.get("chain", "Ethereum") or "Ethereum").lower(),
"label_type": row.get("label_type", "scam"),
"source": "etherscan",
"severity": "critical" if "phish" in row.get("label_type", "").lower() else "high",
},
}
)
except Exception as e:
logger.warning(f"Etherscan pull failed: {e}")
return docs
async def pull_solana_scams(self) -> list[dict]:
"""Pull Solana token registry flagged tokens."""
docs = []
try:
resp = await self.client.get(
"https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json",
timeout=30,
)
if resp.status_code == 200:
data = resp.json()
for token in data.get("tokens", []):
tags = [t.lower() for t in token.get("tags", [])]
if any(kw in str(tags) for kw in ["scam", "spam", "fake"]):
docs.append(
{
"collection": "known_scams",
"content": f"Solana scam token: {token.get('name', '')} ({token.get('symbol', '')}) "
f"at {token.get('address', '')}. Tags: {tags}.",
"metadata": {
"address": token.get("address", ""),
"name": token.get("name", ""),
"symbol": token.get("symbol", ""),
"chain": "solana",
"source": "solana_token_registry",
"tags": tags,
"severity": "high",
},
}
)
except Exception as e:
logger.warning(f"Solana pull failed: {e}")
return docs
async def pull_prediction_markets(self) -> list[dict]:
"""Pull Polymarket prediction data for market intel."""
docs = []
try:
resp = await self.client.get("http://localhost:8000/api/v1/databus/fetch/prediction_markets", timeout=20)
if resp.status_code == 200:
data = resp.json()
markets = data.get("markets", data.get("data", []))
for m in (markets if isinstance(markets, list) else [])[:10]:
docs.append(
{
"collection": "market_intel",
"content": f"Prediction market: {m.get('question', '')} - "
f"YES: {m.get('yes_price', '?')} NO: {m.get('no_price', '?')}",
"metadata": {"source": "polymarket", "type": "prediction"},
}
)
except Exception as e:
logger.warning(f"Prediction market pull failed: {e}")
return docs
# ── Weekly: Pattern Extraction ──
async def extract_scam_patterns(self) -> list[dict]:
"""Extract common patterns from confirmed scam documents."""
docs = []
try:
# Search for confirmed scams
resp = await self.client.get(
f"{RAG_API}/search",
params={
"q": "rug pull honeypot scam confirmed",
"collection": "known_scams",
"limit": 50,
},
timeout=30,
)
if resp.status_code == 200:
data = resp.json()
results = data.get("results", [])
if len(results) >= 10:
# Create a meta-pattern document
content_parts = []
for r in results[:20]:
c = r.get("content", "")[:200]
if c:
content_parts.append(c)
combined = "Common scam patterns observed: " + " | ".join(content_parts)
docs.append(
{
"collection": "scam_patterns",
"content": combined[:5000],
"metadata": {
"source": "pattern_extraction",
"pattern_count": len(results),
"extracted_at": datetime.now(UTC).isoformat(),
},
}
)
except Exception as e:
logger.warning(f"Pattern extraction failed: {e}")
return docs
# ──────────────────────────────────────────────────────────────
# Firehose Engine
# ──────────────────────────────────────────────────────────────
class FirehoseEngine:
"""The central continuous ingestion engine."""
def __init__(self):
self._running = False
self._task: asyncio.Task | None = None
self._client: httpx.AsyncClient | None = None
self._pipeline: IngestionPipeline | None = None
self._feeds: FeedSources | None = None
self._sources: list[FeedSource] = []
self._start_time: float = 0
self.stats = FirehoseStats()
self._lock = asyncio.Lock()
async def start(self):
"""Start the firehose engine."""
if self._running:
logger.info("Firehose already running")
return
self._client = httpx.AsyncClient(timeout=30, limits=httpx.Limits(max_connections=20))
self._pipeline = IngestionPipeline(self._client)
self._feeds = FeedSources(self._client)
self._start_time = time.monotonic()
# Define all feed sources with cadences
self._sources = [
# ── 15-minute cadence: High urgency ──
FeedSource(
"ct_rundown",
"news_articles",
900,
self._feeds.pull_ct_rundown,
True,
"CT Rundown stories",
),
FeedSource(
"social_sentiment",
"social_sentiment",
900,
self._feeds.pull_social_sentiment,
True,
"Social sentiment and scam alerts",
),
# ── 1-hour cadence: News ──
FeedSource(
"news_rss",
"news_articles",
3600,
self._feeds.pull_news_rss,
True,
"200+ RSS crypto news feeds",
),
# ── 6-hour cadence: Market data ──
FeedSource(
"prediction_markets",
"market_intel",
21600,
self._feeds.pull_prediction_markets,
True,
"Polymarket predictions",
),
# ── 24-hour cadence: Scam databases ──
FeedSource(
"etherscan_labels",
"known_scams",
86400,
self._feeds.pull_etherscan_labels,
True,
"Etherscan phish/hack labeled addresses",
),
FeedSource(
"solana_scams",
"known_scams",
86400,
self._feeds.pull_solana_scams,
True,
"Solana token registry flagged tokens",
),
# ── 72-hour cadence: Pattern extraction ──
FeedSource(
"scam_patterns",
"scam_patterns",
259200,
self._feeds.extract_scam_patterns,
True,
"Extract common patterns from confirmed scams",
),
]
self.stats.total_sources = len(self._sources)
self._running = True
self._task = asyncio.create_task(self._run_loop())
logger.info(f"Firehose started with {len(self._sources)} sources")
async def stop(self):
"""Stop the firehose engine."""
self._running = False
if self._task:
self._task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._task
if self._client:
await self._client.aclose()
logger.info("Firehose stopped")
async def _run_loop(self):
"""Main firehose loop - checks sources and runs those due."""
logger.info("Firehose loop started")
while self._running:
now = time.monotonic()
for source in self._sources:
if not source.enabled:
continue
if now - source.last_run < source.cadence_seconds:
continue
# Run this source
source.last_run = now
source.runs += 1
self.stats.last_activity = now
try:
logger.debug(f"Firehose: pulling {source.name}")
docs = await source.fetch_fn()
if docs:
stats = await self._pipeline.ingest_batch(docs)
source.docs_ingested += stats["ingested"]
source.docs_skipped += stats["duplicate"] + stats["skipped"]
source.errors += stats["errors"]
self.stats.total_ingested += stats["ingested"]
self.stats.total_skipped += stats["duplicate"] + stats["skipped"]
self.stats.total_errors += stats["errors"]
logger.info(
f"Firehose {source.name}: +{stats['ingested']} new, "
f"{stats['duplicate']} dup, {stats['skipped']} skip, "
f"{stats['errors']} err "
f"(total: {source.docs_ingested})"
)
except Exception as e:
logger.error(f"Firehose {source.name} failed: {e}")
source.errors += 1
self.stats.total_errors += 1
# Update source stats
async with self._lock:
self.stats.sources = {
s.name: {
"collection": s.collection,
"cadence_min": s.cadence_seconds // 60,
"runs": s.runs,
"ingested": s.docs_ingested,
"skipped": s.docs_skipped,
"errors": s.errors,
"last_run_ago": int(now - s.last_run) if s.last_run else -1,
}
for s in self._sources
}
# Check every 30 seconds
await asyncio.sleep(30)
async def feed_now(self, source_name: str) -> dict:
"""Manually trigger a specific feed source immediately."""
for source in self._sources:
if source.name == source_name:
try:
docs = await source.fetch_fn()
stats = await self._pipeline.ingest_batch(docs)
source.runs += 1
source.docs_ingested += stats["ingested"]
source.last_run = time.monotonic()
self.stats.total_ingested += stats["ingested"]
return {"source": source_name, "docs_fetched": len(docs), **stats}
except Exception as e:
return {"source": source_name, "error": str(e)}
return {"error": f"Source '{source_name}' not found"}
def get_status(self) -> dict:
"""Get current firehose status."""
return {
"running": self._running,
"uptime_seconds": int(time.monotonic() - self._start_time) if self._start_time else 0,
"total_sources": self.stats.total_sources,
"total_ingested": self.stats.total_ingested,
"total_skipped": self.stats.total_skipped,
"total_errors": self.stats.total_errors,
"sources": self.stats.sources,
}
# ──────────────────────────────────────────────────────────────
# Singleton
# ──────────────────────────────────────────────────────────────
_firehose: FirehoseEngine | None = None
def get_firehose() -> FirehoseEngine:
global _firehose
if _firehose is None:
_firehose = FirehoseEngine()
return _firehose