rmi-backend/app/tools_integration.py

386 lines
15 KiB
Python

"""
RMI Tools Integration — Wires all installed open-source tools into the backend.
Tools wired: Ollama, Foundry, Slither, ccxt, web3, blogwatcher, LiteLLM, Vault.
"""
import json
import logging
import os
import subprocess
from datetime import UTC, datetime
import httpx
logger = logging.getLogger(__name__)
# ═══════════════════════════════════════════════════════════
# OLLAMA — Local LLM (port 11434, 2 models)
# ═══════════════════════════════════════════════════════════
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://172.17.0.1:11434")
async def ollama_chat(prompt: str, model: str = "phi3:mini", system: str = "") -> dict:
"""Use local Ollama for free AI inference."""
try:
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post(
f"{OLLAMA_URL}/api/chat",
json={
"model": model,
"messages": messages,
"stream": False,
"options": {"temperature": 0.3, "num_predict": 1024},
},
)
if r.status_code == 200:
data = r.json()
return {
"text": data.get("message", {}).get("content", ""),
"model": model,
"provider": "ollama-local",
"usage": {
"prompt_tokens": data.get("prompt_eval_count", 0),
"completion_tokens": data.get("eval_count", 0),
},
}
except Exception as e:
logger.debug(f"Ollama unavailable: {e}")
return {"error": "Ollama unavailable"}
async def ollama_list_models() -> list[str]:
"""Get available Ollama models."""
try:
async with httpx.AsyncClient(timeout=5) as c:
r = await c.get(f"{OLLAMA_URL}/api/tags")
if r.status_code == 200:
return [m["name"] for m in r.json().get("models", [])]
except Exception:
pass
return []
# ═══════════════════════════════════════════════════════════
# FOUNDRY — EVM smart contract analysis (cast)
# ═══════════════════════════════════════════════════════════
CAST_PATH = "/root/.foundry/bin/cast"
def cast_contract_info(address: str, chain: str = "ethereum") -> dict:
"""Get contract bytecode, ABI, and metadata using cast."""
rpcs = {
"ethereum": os.getenv("ETH_RPC_URL", "https://eth.llamarpc.com"),
"base": os.getenv("BASE_RPC_URL", "https://base.llamarpc.com"),
"arbitrum": os.getenv("ARB_RPC_URL", "https://arb1.arbitrum.io/rpc"),
"polygon": os.getenv("POLY_RPC_URL", "https://polygon.llamarpc.com"),
"bsc": os.getenv("BSC_RPC_URL", "https://bsc-dataseed.binance.org"),
"avalanche": os.getenv("AVAX_RPC_URL", "https://api.avax.network/ext/bc/C/rpc"),
}
rpc = rpcs.get(chain, rpcs["ethereum"])
try:
env = os.environ.copy()
env["ETH_RPC_URL"] = rpc
# Get bytecode
code = subprocess.run(
[CAST_PATH, "code", address, "--rpc-url", rpc],
capture_output=True,
text=True,
timeout=15,
)
bytecode = code.stdout.strip()
# Get nonce
nonce = subprocess.run(
[CAST_PATH, "nonce", address, "--rpc-url", rpc],
capture_output=True,
text=True,
timeout=10,
)
# Get balance
balance = subprocess.run(
[CAST_PATH, "balance", address, "--rpc-url", rpc],
capture_output=True,
text=True,
timeout=10,
)
return {
"address": address,
"chain": chain,
"has_bytecode": len(bytecode) > 4,
"bytecode_size": len(bytecode) // 2 if bytecode.startswith("0x") else 0,
"is_contract": len(bytecode) > 4,
"nonce": nonce.stdout.strip(),
"balance_wei": balance.stdout.strip(),
"balanced_checked": datetime.now(UTC).isoformat(),
}
except Exception as e:
return {
"address": address,
"chain": chain,
"error": str(e),
"cast_available": os.path.exists(CAST_PATH),
}
def cast_tx_decode(tx_hash: str, chain: str = "ethereum") -> dict:
"""Decode a transaction using cast."""
rpcs = {
"ethereum": "https://eth.llamarpc.com",
"base": "https://base.llamarpc.com",
"arbitrum": "https://arb1.arbitrum.io/rpc",
"bsc": "https://bsc-dataseed.binance.org",
}
rpc = rpcs.get(chain, rpcs["ethereum"])
try:
result = subprocess.run(
[CAST_PATH, "tx", tx_hash, "--rpc-url", rpc, "--json"],
capture_output=True,
text=True,
timeout=15,
)
if result.returncode == 0:
return json.loads(result.stdout)
except Exception:
pass
return {"error": "Decode failed", "tx_hash": tx_hash}
# ═══════════════════════════════════════════════════════════
# SLITHER — Solidity vulnerability scanner
# ═══════════════════════════════════════════════════════════
def slither_analyze(contract_address: str, chain: str = "ethereum") -> dict:
"""Run Slither on a verified contract."""
try:
result = subprocess.run(
["slither", contract_address, "--print", "human-summary"],
capture_output=True,
text=True,
timeout=60,
)
return {
"address": contract_address,
"chain": chain,
"analysis": result.stdout[:2000] if result.returncode == 0 else result.stderr[:500],
"success": result.returncode == 0,
}
except Exception as e:
return {"error": str(e), "slither_available": False}
# ═══════════════════════════════════════════════════════════
# CCXT — 100+ exchange price feeds
# ═══════════════════════════════════════════════════════════
def ccxt_get_prices(symbols: list[str] | None = None) -> dict:
"""Get real-time prices from multiple exchanges via ccxt."""
try:
import ccxt
exchanges = {
"binance": ccxt.binance(),
"kraken": ccxt.kraken(),
"coinbase": ccxt.coinbase(),
"bybit": ccxt.bybit(),
}
results = {}
default_symbols = [
"BTC/USDT",
"ETH/USDT",
"SOL/USDT",
"BNB/USDT",
"ARB/USDT",
"MATIC/USDT",
"AVAX/USDT",
]
for sym in symbols or default_symbols:
for ex_name, ex in exchanges.items():
try:
ticker = ex.fetch_ticker(sym)
if ticker and ticker.get("last"):
results.setdefault(sym, {})[ex_name] = {
"price": ticker["last"],
"change_24h": ticker.get("percentage", ticker.get("change", 0)),
"volume_24h": ticker.get("quoteVolume", ticker.get("baseVolume", 0)),
}
except Exception:
pass
return {
"prices": results,
"exchanges": list(exchanges.keys()),
"updated_at": datetime.now(UTC).isoformat(),
}
except ImportError:
return {"error": "ccxt not installed", "prices": {}}
except Exception as e:
return {"error": str(e), "prices": {}}
def ccxt_arbitrage(symbol: str = "BTC/USDT") -> dict:
"""Find arbitrage opportunities across exchanges."""
prices = ccxt_get_prices([symbol])
price_data = prices.get("prices", {}).get(symbol, {})
if len(price_data) >= 2:
prices_list = [(ex, p["price"]) for ex, p in price_data.items()]
lowest = min(prices_list, key=lambda x: x[1])
highest = max(prices_list, key=lambda x: x[1])
spread = ((highest[1] - lowest[1]) / lowest[1]) * 100
return {
"symbol": symbol,
"lowest": {"exchange": lowest[0], "price": lowest[1]},
"highest": {"exchange": highest[0], "price": highest[1]},
"spread_pct": round(spread, 4),
"profitable": spread > 0.5,
}
return {"symbol": symbol, "opportunities": 0}
# ═══════════════════════════════════════════════════════════
# WEB3.PY — Direct Ethereum interaction
# ═══════════════════════════════════════════════════════════
def web3_wallet_balance(address: str, chain: str = "ethereum") -> dict:
"""Get wallet balance and token holdings via web3."""
try:
from web3 import Web3
rpcs = {
"ethereum": "https://eth.llamarpc.com",
"base": "https://base.llamarpc.com",
"bsc": "https://bsc-dataseed.binance.org",
"polygon": "https://polygon.llamarpc.com",
"arbitrum": "https://arb1.arbitrum.io/rpc",
"avalanche": "https://api.avax.network/ext/bc/C/rpc",
}
rpc = rpcs.get(chain, rpcs["ethereum"])
w3 = Web3(Web3.HTTPProvider(rpc))
if not w3.is_connected():
return {"error": f"RPC unavailable for {chain}"}
checksum = w3.to_checksum_address(address)
balance = w3.eth.get_balance(checksum)
tx_count = w3.eth.get_transaction_count(checksum)
code = w3.eth.get_code(checksum)
return {
"address": address,
"chain": chain,
"balance_eth": round(w3.from_wei(balance, "ether"), 6),
"balance_wei": balance,
"transaction_count": tx_count,
"is_contract": len(code) > 0,
"rpc": rpc,
}
except ImportError:
return {"error": "web3 not installed"}
except Exception as e:
return {"error": str(e), "address": address, "chain": chain}
# ═══════════════════════════════════════════════════════════
# BLOGWATCHER — RSS/Atom feed monitoring
# ═══════════════════════════════════════════════════════════
BLOGWATCHER_PATH = "/root/go/bin/blogwatcher"
def blogwatcher_fetch(feed_url: str, limit: int = 10) -> dict:
"""Fetch RSS/Atom feed using blogwatcher."""
try:
result = subprocess.run(
[
BLOGWATCHER_PATH,
"fetch",
"--url",
feed_url,
"--limit",
str(limit),
"--output",
"json",
],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
posts = json.loads(result.stdout) if result.stdout.strip() else []
return {"feed": feed_url, "posts": posts, "count": len(posts)}
except Exception:
pass
return {"feed": feed_url, "posts": [], "error": "blogwatcher unavailable"}
# ═══════════════════════════════════════════════════════════
# LITELLM — Multi-provider LLM proxy routing
# ═══════════════════════════════════════════════════════════
LITELLM_CONFIG = "/srv/rugmuncher-backend/litellm/config.yaml"
def litellm_status() -> dict:
"""Check LiteLLM proxy availability."""
config_exists = os.path.exists(LITELLM_CONFIG)
return {
"available": config_exists,
"config_path": LITELLM_CONFIG,
"config_size": os.path.getsize(LITELLM_CONFIG) if config_exists else 0,
}
# ═══════════════════════════════════════════════════════════
# VAULT — Secrets management
# ═══════════════════════════════════════════════════════════
VAULT_ADDR = os.getenv("VAULT_ADDR", "http://172.17.0.1:8200")
def vault_get_secret(path: str) -> dict | None:
"""Retrieve a secret from HashiCorp Vault."""
try:
r = httpx.get(f"{VAULT_ADDR}/v1/{path}", headers={"X-Vault-Token": "root"}, timeout=5)
if r.status_code == 200:
return r.json().get("data", {}).get("data", {})
except Exception:
pass
return None
def vault_list_secrets(path: str = "secret") -> list[str]:
"""List secrets in Vault."""
try:
r = httpx.get(f"{VAULT_ADDR}/v1/{path}?list=true", headers={"X-Vault-Token": "root"}, timeout=5)
if r.status_code == 200:
return r.json().get("data", {}).get("keys", [])
except Exception:
pass
return []
# ═══════════════════════════════════════════════════════════
# TOOL STATUS — Health check for all integrated tools
# ═══════════════════════════════════════════════════════════
async def tools_health() -> dict:
"""Health check for all integrated tools."""
return {
"ollama": {
"available": bool(await ollama_list_models()),
"models": await ollama_list_models(),
"url": OLLAMA_URL,
},
"foundry": {"installed": os.path.exists(CAST_PATH), "path": CAST_PATH},
"slither": {"installed": os.path.exists("/root/.local/bin/slither")},
"ccxt": {
"available": True # pip installed
},
"web3": {"available": True},
"blogwatcher": {"installed": os.path.exists(BLOGWATCHER_PATH), "path": BLOGWATCHER_PATH},
"litellm": litellm_status(),
"vault": {"available": bool(vault_list_secrets()), "url": VAULT_ADDR},
"file_upload": {
"running": True, # port 8001
"url": "http://localhost:8001",
},
"timestamp": datetime.now(UTC).isoformat(),
}