- 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>
245 lines
7.1 KiB
Python
245 lines
7.1 KiB
Python
"""
|
|
RMI AI Pipeline v3 - Full Production
|
|
=====================================
|
|
Redis caching, FastAPI endpoints, usage tracking, retry logic.
|
|
"""
|
|
|
|
import contextlib
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
import urllib.request
|
|
from datetime import UTC, datetime
|
|
|
|
logger = logging.getLogger("rmi.ai_v3")
|
|
OLLAMA_KEY = os.getenv("OLLAMA_API_KEY", "")
|
|
OLLAMA_URL = "https://ollama.com/v1/chat/completions"
|
|
MODEL = "deepseek-v4-flash"
|
|
|
|
# ── Redis Cache (survives restarts) ──
|
|
REDIS_AVAILABLE = False
|
|
try:
|
|
import redis
|
|
|
|
_redis = redis.Redis(
|
|
host=os.getenv("REDIS_HOST", "rmi-redis"),
|
|
port=int(os.getenv("REDIS_PORT", "6379")),
|
|
password=os.getenv("REDIS_PASSWORD", ""),
|
|
db=1,
|
|
socket_connect_timeout=2,
|
|
)
|
|
_redis.ping()
|
|
REDIS_AVAILABLE = True
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _cache_get(key: str) -> str | None:
|
|
if REDIS_AVAILABLE:
|
|
try:
|
|
return _redis.get(f"rmi:ai:{key}")
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def _cache_set(key: str, value: str, ttl: int = 300):
|
|
if REDIS_AVAILABLE:
|
|
with contextlib.suppress(BaseException):
|
|
_redis.setex(f"rmi:ai:{key}", ttl, value)
|
|
|
|
|
|
# ── Usage Tracking ──
|
|
_usage = {"total_calls": 0, "total_tokens": 0, "total_cost": 0.0}
|
|
|
|
|
|
def _track(prompt_tokens: int, completion_tokens: int, cost: float):
|
|
_usage["total_calls"] += 1
|
|
_usage["total_tokens"] += prompt_tokens + completion_tokens
|
|
_usage["total_cost"] += cost
|
|
|
|
|
|
def usage_stats() -> dict:
|
|
return {**_usage, "timestamp": datetime.now(UTC).isoformat()}
|
|
|
|
|
|
# ── Retry with Exponential Backoff ──
|
|
def _call_ollama(system: str, prompt: str, max_tokens: int = 250, temp: float = 0.3, cache_ttl: int = 300) -> str:
|
|
cache_key = hashlib.md5(f"{system[:60]}|{prompt[:120]}".encode()).hexdigest()
|
|
cached = _cache_get(cache_key)
|
|
if cached:
|
|
val = cached.decode() if isinstance(cached, bytes) else cached
|
|
if isinstance(val, str):
|
|
return val
|
|
|
|
for attempt in range(3):
|
|
try:
|
|
body = json.dumps(
|
|
{
|
|
"model": MODEL,
|
|
"messages": [
|
|
{"role": "system", "content": system},
|
|
{"role": "user", "content": prompt},
|
|
],
|
|
"max_tokens": max_tokens,
|
|
"temperature": temp,
|
|
}
|
|
).encode()
|
|
req = urllib.request.Request(
|
|
OLLAMA_URL,
|
|
data=body,
|
|
headers={
|
|
"Authorization": f"Bearer {OLLAMA_KEY}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
)
|
|
resp = urllib.request.urlopen(req, timeout=12)
|
|
data = json.loads(resp.read())
|
|
result = data["choices"][0]["message"]["content"].strip()
|
|
usage = data.get("usage", {})
|
|
_track(usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0), 0.000001)
|
|
_cache_set(cache_key, result, cache_ttl)
|
|
return result
|
|
except Exception as e:
|
|
if attempt < 2:
|
|
time.sleep(2**attempt)
|
|
else:
|
|
logger.warning(f"Ollama failed after 3 retries: {e}")
|
|
return ""
|
|
|
|
|
|
# ── ALL 12 MODULES (Unified) ──
|
|
|
|
|
|
def explain_risks(scan: dict) -> str:
|
|
s = scan.get("safety_score", 50)
|
|
f = scan.get("risk_flags", [])
|
|
g = scan.get("green_flags", [])
|
|
n = scan.get("name", scan.get("symbol", "token"))
|
|
r = _call_ollama(
|
|
"Explain token risk to non-technical user. 3-4 sentences. Start with safety score. Use <b>bold</b>. End with DYOR.",
|
|
f"Token:{n} Score:{s}/100 Risks:{', '.join(f[:5]) or 'none'} Green:{', '.join(g[:3]) or 'none'}",
|
|
150,
|
|
0.2,
|
|
600,
|
|
)
|
|
return r or f"<b>Safety: {s}/100</b>. Risk flags: {', '.join(f[:3])}. Always DYOR."
|
|
|
|
|
|
def classify_news(title: str, content: str = "") -> str:
|
|
r = _call_ollama(
|
|
"Classify crypto news: SCAM MARKET REGULATION SECURITY DEFI MEMECOIN GENERAL. Reply ONE word.",
|
|
f"{title} {content[:200]}",
|
|
8,
|
|
0.1,
|
|
3600,
|
|
)
|
|
for cat in ["SCAM", "MARKET", "REGULATION", "SECURITY", "DEFI", "MEMECOIN"]:
|
|
if cat in r.upper():
|
|
return cat
|
|
t = (title + content).lower()
|
|
if any(w in t for w in ["hack", "exploit", "rug", "scam", "drain"]):
|
|
return "SCAM"
|
|
if any(w in t for w in ["price", "btc", "eth", "bull", "bear"]):
|
|
return "MARKET"
|
|
return "GENERAL"
|
|
|
|
|
|
def profile_wallet(tx: dict) -> str:
|
|
return (
|
|
_call_ollama(
|
|
"Classify wallet persona: PERSONA|conf. DayTrader Whale BotFarm Insider ScamDeployer AirdropHunter DiamondHands DegenGambler Unknown",
|
|
json.dumps(tx)[:1000],
|
|
25,
|
|
)
|
|
or "Unknown|0"
|
|
)
|
|
|
|
|
|
def enrich_rag(query: str, docs: str) -> str:
|
|
return (
|
|
_call_ollama("Reformat RAG chunks into 2-3 sentence answer.", f"Q:{query}\nD:{docs[:2000]}", 200) or docs[:400]
|
|
)
|
|
|
|
|
|
def rank_alerts(alerts: list) -> list:
|
|
s = "\n".join(f"{a.get('id', '?')}|{a.get('severity', '?')}|{str(a.get('title', ''))[:80]}" for a in alerts[:10])
|
|
r = _call_ollama("Rank by urgency. Reply: id1,id2,id3...", s, 50)
|
|
return [x.strip() for x in r.split(",") if x.strip()] if r else []
|
|
|
|
|
|
def briefing(data: dict) -> str:
|
|
return (
|
|
_call_ollama(
|
|
"3-para crypto market briefing. P1:volume P2:risks P3:watch. <b>bold</b>. 250 words.",
|
|
json.dumps(data)[:2000],
|
|
350,
|
|
0.5,
|
|
1800,
|
|
)
|
|
or "Briefing unavailable."
|
|
)
|
|
|
|
|
|
def post_mortem(incident: dict) -> str:
|
|
return (
|
|
_call_ollama(
|
|
"Forensic post-mortem: What→How→RedFlags→Protection. <b>bold</b>. 200 words.",
|
|
json.dumps(incident)[:1500],
|
|
300,
|
|
0.4,
|
|
3600,
|
|
)
|
|
or "Autopsy unavailable."
|
|
)
|
|
|
|
|
|
def analyze_submission(sub: dict) -> str:
|
|
return (
|
|
_call_ollama("Analyze suspicious token. Verdict+2-3 concerns.", json.dumps(sub)[:1500], 200)
|
|
or "Analysis unavailable."
|
|
)
|
|
|
|
|
|
def cross_chain(wallets: dict) -> str:
|
|
return (
|
|
_call_ollama(
|
|
"Same entity across chains? MATCH|conf|reason or NO_MATCH|reason",
|
|
json.dumps(wallets)[:1500],
|
|
80,
|
|
)
|
|
or "Unknown"
|
|
)
|
|
|
|
|
|
def blog_draft(topic: str, data: dict) -> str:
|
|
return (
|
|
_call_ollama(
|
|
"Blog post: Title|Hook|Body|Takeaways|CTA. Markdown.",
|
|
f"Topic:{topic}\n{json.dumps(data)[:2000]}",
|
|
500,
|
|
0.6,
|
|
3600,
|
|
)
|
|
or f"# {topic}\n\nDraft unavailable."
|
|
)
|
|
|
|
|
|
def social_post(incident: dict) -> str:
|
|
return (
|
|
_call_ollama("Tweet(<280)+Telegram(<500). Hook first.", json.dumps(incident)[:1000], 200, 0.7)
|
|
or "Post unavailable."
|
|
)
|
|
|
|
|
|
def compare_tokens(a: dict, b: dict) -> str:
|
|
return (
|
|
_call_ollama(
|
|
"Compare 2 tokens: SAFER name REASON SCORE_DIFF KEY_DIFFERENCES",
|
|
f"A:{json.dumps(a)[:800]}\nB:{json.dumps(b)[:800]}",
|
|
200,
|
|
)
|
|
or "Comparison unavailable."
|
|
)
|