rmi-backend/app/rugmaps_ai.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

358 lines
13 KiB
Python

"""
RugMaps AI Analysis - DeepSeek Flash powered token investigation.
Uses our own data: FAISS indexes, wallet labels, clustering engine, RAG cache.
"""
import hashlib
import json
import logging
import os
import time
logger = logging.getLogger(__name__)
# LLM config - DeepSeek Flash for fast, cheap analysis
DEEPSEEK_KEY = os.getenv("DEEPSEEK_API_KEY", "") or os.getenv("LLM_API_KEY", "") or os.getenv("OPENROUTER_API_KEY", "")
DEEPSEEK_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1/chat/completions")
AI_MODEL = os.getenv("LLM_MODEL", "deepseek-chat") # Flash by default
# Analysis cache (in-memory + Redis if available)
_analysis_cache: dict[str, dict] = {}
CACHE_TTL = 3600 # 1 hour
def _cache_key(token: str, chain: str) -> str:
return hashlib.sha256(f"{chain}:{token}".encode()).hexdigest()[:16]
async def get_cached_analysis(token: str, chain: str) -> dict | None:
"""Check if we have a cached analysis for this token."""
key = _cache_key(token, chain)
# In-memory cache
if key in _analysis_cache:
entry = _analysis_cache[key]
if time.time() - entry.get("_ts", 0) < CACHE_TTL:
logger.info(f"RugMaps cache HIT for {token[:12]}...")
return entry
return None
def set_cached_analysis(token: str, chain: str, analysis: dict):
"""Cache analysis result."""
key = _cache_key(token, chain)
analysis["_ts"] = time.time()
analysis["_cached"] = True
_analysis_cache[key] = analysis
# Prune old entries if memory grows
if len(_analysis_cache) > 500:
oldest = sorted(_analysis_cache.items(), key=lambda x: x[1].get("_ts", 0))[:100]
for k, _ in oldest:
del _analysis_cache[k]
async def get_token_holders(token_address: str, chain: str) -> list[dict]:
"""Get token holders from our data sources."""
try:
from app.bubble_maps import get_bubble_maps_pro
bm = get_bubble_maps_pro()
result = await bm.generate_map(center_wallet=token_address, depth=2, min_strength=0.1)
d = result.to_dict()
return d.get("nodes", [])
except Exception as e:
logger.warning(f"Holder fetch failed: {e}")
return []
async def get_wallet_labels(addresses: list[str]) -> dict[str, dict]:
"""Get wallet labels from our ClickHouse/FAISS database."""
labels = {}
try:
from app.bubble_maps import get_bubble_maps_pro
bm = get_bubble_maps_pro()
for addr in addresses[:50]:
try:
info = await bm._get_entity_info(addr)
risk, level = await bm._calculate_risk(addr)
labels[addr] = {
"entity": info or {},
"risk_score": risk,
"risk_level": level,
}
except Exception:
pass
except Exception as e:
logger.warning(f"Label fetch error: {e}")
return labels
async def search_similar_scams(token_address: str, chain: str) -> list[dict]:
"""Search FAISS indexes for similar scam patterns."""
similar = []
try:
from app.ann_index import get_ann_index
from app.crypto_embeddings import CryptoEmbedder
ann = get_ann_index()
embedder = CryptoEmbedder()
# Embed token info
query_text = f"token {token_address} on {chain} rug pull scam honeypot"
embedding = await embedder.embed_query(query_text)
# Search scam patterns index
if "scam_patterns" in ann.indexes:
results = ann.search("scam_patterns", embedding, k=5)
for r in results:
similar.append(
{
"pattern": r.get("metadata", {}).get("pattern", ""),
"description": r.get("metadata", {}).get("description", ""),
"similarity": float(r.get("score", 0)),
}
)
# Search known scams
if "known_scams" in ann.indexes:
results = ann.search("known_scams", embedding, k=5)
for r in results:
similar.append(
{
"scam_type": r.get("metadata", {}).get("type", ""),
"description": r.get("metadata", {}).get("description", ""),
"similarity": float(r.get("score", 0)),
}
)
# Search wallet profiles
if "wallet_profiles" in ann.indexes:
results = ann.search("wallet_profiles", embedding, k=5)
for r in results:
similar.append(
{
"wallet_type": r.get("metadata", {}).get("type", ""),
"description": r.get("metadata", {}).get("label", ""),
"similarity": float(r.get("score", 0)),
}
)
except Exception as e:
logger.warning(f"FAISS search failed: {e}")
# Deduplicate and sort by similarity
seen = set()
unique = []
for s in sorted(similar, key=lambda x: x.get("similarity", 0), reverse=True):
desc = s.get("description", "")
if desc and desc not in seen:
seen.add(desc)
unique.append(s)
return unique[:10]
async def generate_ai_analysis(
token_address: str,
chain: str,
graph_data: dict,
holders: list[dict],
labels: dict[str, dict],
similar_scams: list[dict],
) -> dict:
"""Generate AI analysis using DeepSeek Flash with our own data."""
import httpx
# Build a data-rich prompt from our own systems
holder_summary = []
for h in holders[:10]:
addr = h.get("address", "")[:12]
pct = h.get("supply_pct", h.get("pct", "?"))
label = labels.get(h.get("address", ""), {}).get("entity", {}).get("name", "")
risk = labels.get(h.get("address", ""), {}).get("risk_score", "?")
holder_summary.append(f" {addr}... - {pct}% supply - label: {label or 'unknown'} - risk: {risk}/100")
similar_summary = (
"\n".join(
[
f" - {s.get('description', '')[:100]} (similarity: {s.get('similarity', 0):.2f})"
for s in similar_scams[:5]
]
)
or "No similar scam patterns found in our database."
)
graph_stats = graph_data.get("stats", {})
node_count = graph_data.get("node_count", len(holders))
link_count = graph_data.get("link_count", 0)
risk_score = graph_data.get("risk_score", 0)
prompt = f"""You are RugMunch Intelligence's AI security analyst. Analyze this token using our proprietary on-chain data.
TOKEN: {token_address} on {chain}
GRAPH DATA:
- {node_count} connected wallets
- {link_count} transaction links
- Graph risk score: {risk_score}/100
- Graph stats: {json.dumps(graph_stats)}
TOP HOLDERS (from our systems):
{chr(10).join(holder_summary) if holder_summary else "No holder data available"}
SIMILAR SCAM PATTERNS (from our FAISS vector database):
{similar_summary}
WALLET LABELS (from our 393K-label database):
{json.dumps({a[:12]: line.get("entity", {}).get("name") or line.get("entity", {}).get("label") for a, line in list(labels.items())[:10]})}
Based on our data, provide a JSON analysis with these fields:
- "verdict": (SAFE / SUSPICIOUS / HIGH_RISK / SCAM)
- "risk_score": (0-100)
- "summary": (2-3 sentence analysis for users)
- "red_flags": (list of specific concerning findings, empty if none)
- "green_flags": (list of positive indicators, empty if none)
- "dev_wallets": (list of addresses likely belonging to developer/team)
- "bundle_wallets": (list of addresses in coordinated bundles)
- "recommendation": (one sentence: what should a user do?)
- "confidence": (0-100, how confident are you in this analysis?)
Return ONLY valid JSON, no markdown, no code blocks."""
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.post(
DEEPSEEK_URL,
headers={
"Authorization": f"Bearer {DEEPSEEK_KEY}",
"Content-Type": "application/json",
},
json={
"model": AI_MODEL,
"messages": [
{
"role": "system",
"content": "You are a crypto security expert. Return only valid JSON.",
},
{"role": "user", "content": prompt},
],
"temperature": 0.3,
"max_tokens": 800,
},
)
if resp.status_code == 200:
data = resp.json()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "{}")
# Try to parse the JSON response
try:
# Strip any markdown code blocks
if "```" in content:
content = content.split("```")[1]
if content.startswith("json"):
content = content[4:]
analysis = json.loads(content.strip())
return analysis
except json.JSONDecodeError:
# Fallback: return raw text as summary
return {
"verdict": "UNKNOWN",
"risk_score": risk_score,
"summary": content[:300],
"red_flags": [],
"green_flags": [],
"recommendation": "Manual review recommended - AI response was malformed.",
"confidence": 30,
}
logger.warning(f"DeepSeek API returned {resp.status_code}: {resp.text[:200]}")
except Exception as e:
logger.error(f"AI analysis failed: {e}")
# Fallback analysis from our own data (no AI needed)
return generate_fallback_analysis(risk_score, holders, labels, similar_scams, node_count, link_count)
def generate_fallback_analysis(
risk_score: int,
holders: list[dict],
labels: dict[str, dict],
similar_scams: list[dict],
node_count: int,
link_count: int,
) -> dict:
"""Generate analysis purely from our own data when AI is unavailable."""
red_flags = []
green_flags = []
dev_wallets = []
bundle_wallets = []
# Check for high concentration
top_holder_pct = sum(h.get("supply_pct", h.get("pct", 0)) for h in holders[:3])
if top_holder_pct > 50:
red_flags.append(f"Top 3 holders control {top_holder_pct:.0f}% of supply - extreme concentration risk")
elif top_holder_pct < 20:
green_flags.append(f"Well distributed: top 3 holders only control {top_holder_pct:.0f}%")
# Check labels
high_risk_count = 0
for addr, label_data in labels.items():
entity = label_data.get("entity", {})
risk = label_data.get("risk_score", 0)
if entity.get("type") in ("scammer", "phishing", "exploiter"):
red_flags.append(f"Known malicious entity: {entity.get('name', addr[:12])}")
high_risk_count += 1
if risk >= 70:
red_flags.append(f"High risk wallet ({risk}/100): {addr[:12]}... {entity.get('name', '')}")
high_risk_count += 1
dev_wallets.append(addr)
if entity.get("type") == "exchange":
green_flags.append(f"Exchange wallet detected: {entity.get('name', addr[:12])}")
# Similar scam patterns
if similar_scams:
top_match = similar_scams[0]
if top_match.get("similarity", 0) > 0.7:
red_flags.append(f"Matches known pattern: {top_match.get('description', '')[:80]}")
else:
green_flags.append("No matches in our scam pattern database")
# Risk assessment
if risk_score >= 80 or high_risk_count >= 3:
verdict = "SCAM"
elif risk_score >= 50 or high_risk_count >= 1:
verdict = "HIGH_RISK"
elif risk_score >= 25 or top_holder_pct > 40:
verdict = "SUSPICIOUS"
else:
verdict = "SAFE"
confidence = min(90, 40 + len(red_flags) * 10 + len(green_flags) * 5)
return {
"verdict": verdict,
"risk_score": risk_score,
"summary": f"Analysis based on {node_count} connected wallets across {link_count} transaction links. "
f"Top holders control {top_holder_pct:.0f}% of supply. "
f"{len(red_flags)} red flags, {len(green_flags)} green flags detected.",
"red_flags": red_flags,
"green_flags": green_flags,
"dev_wallets": dev_wallets[:5],
"bundle_wallets": bundle_wallets[:5],
"recommendation": (
"DO NOT INVEST - clear scam indicators"
if verdict == "SCAM"
else "Extreme caution - multiple high-risk signals"
if verdict == "HIGH_RISK"
else "Proceed with caution - some suspicious patterns"
if verdict == "SUSPICIOUS"
else "No major concerns detected from our analysis"
),
"confidence": confidence,
"source": "rmi_data_engine", # Indicates this came from our own data
}