rmi-backend/app/_archive/legacy_2026_07/smart_calls.py
cryptorugmunch 628c1d2a10
Some checks failed
CI / build (push) Failing after 3s
refactor(rmi-backend,audit): mount Wave 3 + archive 136 dead-code files (P2.3)
PHASE 2.3 (AUDIT-2026-Q3.md):

Task 1 — Wire-in Wave 3 (1 router mounted, 2 deferred):
  - app.routers.unified_scanner_router mounted at /api/v2/scanner/* (2 routes:
    POST /api/v2/scanner/token/scan, POST /api/v2/scanner/wallet/scan).
    Refactored prefix from /api/v2 -> /api/v2/scanner to avoid future conflicts
    with the v1 /api/v1/scanner/ stub.
  - app.routers.unified_wallet_scanner DEFERRED (no router APIRouter attribute;
    library module consumed by unified_scanner_router via get_wallet_scanner()).
  - app.routers.admin_extensions DEFERRED (DORMANT per audit; 25 routes at
    /api/v1/admin/* would shadow /api/v1/admin/alerts_webhook).

Task 2 — Archive 136 dead-code files to app/_archive/legacy_2026_07/:
  - 73 routers in app/routers/ (reach graph showed zero reach into mount.py).
  - 63 flat app/*.py (domain modules never imported by live code).
  - 1 file RESTORED post-archive: app/routers/x402_bridge_health.py (caught by
    tests/unit/test_bridge_health.py which directly imports it; reach graph
    considered tests/ only as transitive reach — to be patched in next cycle).

Forced-LIVE (NOT archived per user directive):
  - app/ai_pipeline_v3.py  (3 importers in audit window, importers themselves DEAD)
  - app/splade_bm25.py       (LIVE via app.rag_service)
  - app/wallet_manager_v2.py (LIVE via x402_enforcement, x402_tools, sweep_all, sweep_now)
  - app/crypto_embeddings.py (NOT in audit ARCHIVE list; heavy import graph)

Verification (forward-import closure from mount.py + main.py + factory.py + lifespan.py):
  - imports = 348 app.* modules
  - reached = 194 files reachable from roots
  - archive set = audit_dead (186) - reached - forced_live (4) - test_live (1) = 136
  - Net delta: 136 files moved, 44,932 LOC reduction, 293->295 active routes (+2 from Wave 3)

pyproject.toml updates:
  - setuptools.packages.find: added exclude for app._archive*
  - ruff.extend-exclude: added "app/_archive/"
  - mypy.exclude: added "app/_archive/"

Smoke test: pytest tests/ — 817 passed, 3 pre-existing failures unchanged
(0 new failures; 0 routes lost; all 4 forced-LIVE files still importable).

Restoration: git mv app/_archive/legacy_2026_07/<name>.py <original-path>
and add the import to app/mount.py ROUTER_MODULES.

Refs: AUDIT-2026-Q3.md /home/dev/pry/rmi-final-deadcode-2026-07-06.md
2026-07-06 20:52:31 +02:00

530 lines
18 KiB
Python

"""
Human-Facing Tool Catalog API - SmartCalls Marketplace
======================================================
Source of truth: /mcp/manifest + tool_registry + membership_plans + agent_skills.
The MCP server is the bot-side. This is the human-side mirror.
Adding a tool on the bot side? It shows up here automatically.
Updating pricing on /mcp/membership? Marketplace reads it.
Adding a skill? Marketplace exposes it.
Every endpoint reads from the same registries.
"""
import logging
import time
from collections import defaultdict
from typing import Any
from fastapi import APIRouter, HTTPException, Query, Request
logger = logging.getLogger("smart_calls")
router = APIRouter(prefix="/api/v1/smart-calls", tags=["smart-calls"])
# ════════════════════════════════════════════════════════════
# TRIAL TIERS - same logic as bot side
# ════════════════════════════════════════════════════════════
TRIAL_TIERS = {
"simple": {"trials": 5, "label": "5 free trials", "color": "emerald", "badge": "🎁"},
"standard": {"trials": 3, "label": "3 free trials", "color": "blue", "badge": ""},
"premium": {"trials": 1, "label": "1 free trial", "color": "purple", "badge": "💎"},
}
PRICE_TIERS = {
"micro": {"max": 0.01, "label": "Micro", "color": "emerald", "icon": "·"},
"low": {"max": 0.05, "label": "Low", "color": "blue", "icon": ""},
"mid": {"max": 0.15, "label": "Mid", "color": "amber", "icon": ""},
"high": {"max": 0.50, "label": "High", "color": "rose", "icon": ""},
"premium": {"max": 999.0, "label": "Premium", "color": "violet", "icon": ""},
}
CATEGORY_TRIAL_DEFAULTS = {
"sentiment": "simple",
"social": "simple",
"api": "simple",
"research": "simple",
"analysis": "standard",
"intelligence": "standard",
"market": "standard",
"monitoring": "standard",
"launchpad": "standard",
"defi": "standard",
"nft": "standard",
"bundle": "standard",
"security": "premium",
"forensics": "premium",
"premium": "premium",
"mcp-external": "simple",
}
def _classify_trial_tier(trial_free: int | None, category: str) -> str:
if trial_free is not None:
n = int(trial_free)
if n >= 5:
return "simple"
if n >= 3:
return "standard"
return "premium"
return CATEGORY_TRIAL_DEFAULTS.get(category.lower(), "standard")
def _classify_price_tier(price_usd: float) -> str:
for tier_name, info in PRICE_TIERS.items():
if price_usd <= info["max"]:
return tier_name
return "premium"
# ════════════════════════════════════════════════════════════
# CACHE - short TTL so updates flow through fast
# ════════════════════════════════════════════════════════════
_cache: dict[str, Any] = {}
_cache_time: float = 0
CACHE_TTL = 15
def _build_marketplace_data() -> dict[str, Any]:
"""Pull from the same sources the MCP server uses."""
global _cache, _cache_time
now = time.time()
if _cache and (now - _cache_time) < CACHE_TTL:
return _cache
# === SOURCE 1: TOOL_PRICES (bot-side MCP server mirror) ===
try:
from app.routers.x402_enforcement import CHAIN_USDC, TOOL_PRICES
dict(TOOL_PRICES)
except ImportError:
CHAIN_USDC = {}
# === SOURCE 2: x402_catalog (already merges external MCPs) ===
try:
from app.routers.x402_catalog import get_catalog
x402_cat = get_catalog()
x402_tools_list = list(x402_cat.get("tools", []))
except Exception:
x402_tools_list = []
# === SOURCE 3: Expanded MCP services (free open-source) ===
try:
from app.services.expanded_mcp_catalog import (
EXTERNAL_MCP_SERVICES,
EXTERNAL_MCP_TOOLS,
)
external_services = list(EXTERNAL_MCP_SERVICES)
# Build ID set for detection - these IDs are the free open-source MCP tools
external_tool_ids = {t["id"] for t in EXTERNAL_MCP_TOOLS}
except ImportError:
external_services = []
external_tool_ids = set()
# === SOURCE 4: Membership plans (scan packs, subscriptions, streams) ===
try:
from app.caching_shield.membership_plans import (
AGENT_BUNDLES,
BATCH_PRODUCTS,
RESEARCH_PRODUCTS,
STREAM_PRODUCTS,
SUBSCRIPTION_TIERS,
get_membership_catalog,
)
get_membership_catalog()
scan_packs = list(AGENT_BUNDLES.values())
subscriptions = list(SUBSCRIPTION_TIERS.values())
streams = list(STREAM_PRODUCTS.values())
research = list(RESEARCH_PRODUCTS.values())
batch = list(BATCH_PRODUCTS.values())
except ImportError:
scan_packs = []
subscriptions = []
streams = []
research = []
batch = []
# === SOURCE 5: Agent skills (workflow guides) ===
try:
from app.caching_shield.agent_skills_extended import get_all_agent_skills
skills_data = get_all_agent_skills()
except ImportError:
skills_data = {"skills": []}
# === SOURCE 6: Tool registry (counts) ===
try:
from app.caching_shield.tool_registry import count_all_tools
tool_counts = count_all_tools()
except ImportError:
tool_counts = {"total": 0}
# === BUILD HUMAN-FACING TOOL LIST ===
# Use the x402_catalog as primary (it has enriched fields), fall back to TOOL_PRICES
enriched_tools = []
for t in x402_tools_list:
price_usd = float(t.get("priceUsd", t.get("price_usd", 0.01)) or 0.01)
trial_free = t.get("trialFree", t.get("trial_free"))
cat = t.get("category", "analysis")
chains = [c.upper() for c in t.get("chains", [])]
source = t.get("source", "enforcement")
service = t.get("service", "rmi-native")
tool_id = t.get("id", "")
# External = either marked external in x402 OR in our expanded_mcp_catalog ID set
is_external = (
source in ("expanded-mcp", "mcp-router")
or service
in (
"fear-greed-mcp",
"crypto-indicators-mcp",
"openzeppelin-wizard",
"web3-research-mcp",
)
or tool_id in external_tool_ids
)
enriched_tools.append(
{
"id": t.get("id", ""),
"name": t.get("name", t.get("id", "?")),
"description": t.get("description", ""),
"category": cat,
"service": service,
"source": source,
"is_external": is_external,
"chains": chains,
"price_usd": price_usd,
"price_label": f"${price_usd:.2f}",
"trial_tier": _classify_trial_tier(trial_free, cat),
"trial_count": TRIAL_TIERS[_classify_trial_tier(trial_free, cat)]["trials"],
"trial_label": TRIAL_TIERS[_classify_trial_tier(trial_free, cat)]["label"],
"trial_badge": TRIAL_TIERS[_classify_trial_tier(trial_free, cat)]["badge"],
"price_tier": _classify_price_tier(price_usd),
"method": t.get("method", "POST"),
}
)
# === GROUP FOR DISPLAY ===
by_category = defaultdict(list)
by_chain = defaultdict(list)
by_service = defaultdict(list)
by_trial = defaultdict(list)
by_price = defaultdict(list)
featured = []
for t in enriched_tools:
by_category[t["category"]].append(t)
by_service[t["service"]].append(t)
by_trial[t["trial_tier"]].append(t)
by_price[t["price_tier"]].append(t)
for c in t["chains"]:
by_chain[c].append(t)
if t["category"] in (
"security",
"intelligence",
"market",
"analysis",
"social",
"defi",
"premium",
):
if len([f for f in featured if f["category"] == t["category"]]) < 6:
featured.append(t)
# Sort
enriched_tools.sort(key=lambda t: (0 if not t["is_external"] else 1, t["category"], t["price_usd"]))
for k in by_category:
by_category[k].sort(key=lambda t: t["price_usd"])
for k in by_service:
by_service[k].sort(key=lambda t: t["price_usd"])
for k in by_trial:
by_trial[k].sort(key=lambda t: t["price_usd"])
for k in by_price:
by_price[k].sort(key=lambda t: t["price_usd"])
for k in by_chain:
by_chain[k].sort(key=lambda t: t["price_usd"])
featured.sort(key=lambda t: t["price_usd"])
result = {
"version": "1.0.0",
"generated_at": int(now),
# === COUNTS ===
"stats": {
"total_tools": len(enriched_tools),
"native_tools": sum(1 for t in enriched_tools if not t["is_external"]),
"external_tools": sum(1 for t in enriched_tools if t["is_external"]),
"total_services": len(by_service),
"total_categories": len(by_category),
"total_chains": len(by_chain),
"total_scan_packs": len(scan_packs),
"total_subscriptions": len(subscriptions),
"total_streams": len(streams),
"total_research": len(research),
"total_batch": len(batch),
"total_skills": len(skills_data.get("skills", [])),
"total_registry": tool_counts.get("total", 0),
},
# === TOOLS ===
"tools": enriched_tools,
"by_category": dict(by_category),
"by_chain": dict(by_chain),
"by_service": dict(by_service),
"by_trial_tier": dict(by_trial),
"by_price_tier": dict(by_price),
"featured": featured[:30],
# === PRODUCTS ===
"scan_packs": scan_packs,
"subscriptions": subscriptions,
"streams": streams,
"research": research,
"batch": batch,
# === META ===
"skills": skills_data,
"external_services": external_services,
"trial_tiers": TRIAL_TIERS,
"price_tiers": PRICE_TIERS,
"chains": {k: v for k, v in CHAIN_USDC.items() if k not in ("sepa",)},
"refund_policy": "Full automatic refund within 48h if tool returns no data",
}
_cache = result
_cache_time = now
return result
def invalidate():
"""Force rebuild on next request - call after adding/updating tools."""
global _cache_time
_cache_time = 0
# ════════════════════════════════════════════════════════════
# ENDPOINTS - SmartCalls Marketplace
# ════════════════════════════════════════════════════════════
@router.get("/marketplace")
async def marketplace(
sort: str = Query("default"),
category: str | None = None,
service: str | None = None,
chain: str | None = None,
free_only: bool = False,
search: str | None = None,
limit: int = Query(500, ge=0, le=1000),
):
"""Full marketplace - all tools, scan packs, subscriptions, streams, research, skills."""
data = _build_marketplace_data()
tools = list(data["tools"])
if category:
tools = [t for t in tools if t["category"].lower() == category.lower()]
if service:
tools = [t for t in tools if t["service"].lower() == service.lower()]
if chain:
chain_upper = chain.upper()
tools = [t for t in tools if chain_upper in t["chains"]]
if free_only:
tools = [t for t in tools if t["trial_count"] > 0]
if search:
q = search.lower()
tools = [t for t in tools if q in t["id"].lower() or q in t["name"].lower() or q in t["description"].lower()]
if sort == "price":
tools.sort(key=lambda t: t["price_usd"])
elif sort == "trial":
tools.sort(key=lambda t: -t["trial_count"])
elif sort == "name":
tools.sort(key=lambda t: t["name"].lower())
elif sort == "category":
tools.sort(key=lambda t: (t["category"], t["name"].lower()))
if limit > 0:
tools = tools[:limit]
return {
"stats": data["stats"],
"refund_policy": data["refund_policy"],
"trial_tiers": data["trial_tiers"],
"price_tiers": data["price_tiers"],
"chains": data["chains"],
"tools": tools,
"featured": data["featured"],
"scan_packs": data["scan_packs"],
"subscriptions": data["subscriptions"],
"streams": data["streams"],
"research": data["research"],
"batch": data["batch"],
"external_services": data["external_services"],
"categories": sorted(data["by_category"].keys()),
"services": sorted(data["by_service"].keys()),
}
@router.get("/marketplace/stats")
async def marketplace_stats():
"""Counts only - counts always match the bot side."""
data = _build_marketplace_data()
return {
"version": data["version"],
"generated_at": data["generated_at"],
**data["stats"],
}
@router.get("/marketplace/featured")
async def featured_tools():
"""Top 30 featured tools across major categories."""
data = _build_marketplace_data()
return {"tools": data["featured"]}
@router.get("/marketplace/categories")
async def categories():
"""Tools grouped by category."""
data = _build_marketplace_data()
return {
"categories": [
{"name": cat, "count": len(tools), "tools": tools[:10]}
for cat, tools in sorted(data["by_category"].items(), key=lambda x: -len(x[1]))
]
}
@router.get("/marketplace/chains")
async def chains():
"""Tools grouped by chain."""
data = _build_marketplace_data()
return {
"chains": [
{"name": c, "count": len(tools), "tools": tools[:10]}
for c, tools in sorted(data["by_chain"].items(), key=lambda x: -len(x[1]))
]
}
@router.get("/marketplace/trial")
async def trial_tiers():
"""Tools grouped by free trial tier."""
data = _build_marketplace_data()
return {
"tiers": data["trial_tiers"],
"groups": dict(data["by_trial_tier"].items()),
}
@router.get("/marketplace/scan-packs")
async def scan_packs():
"""Bundled tool packs at discount."""
data = _build_marketplace_data()
return {"count": len(data["scan_packs"]), "packs": data["scan_packs"]}
@router.get("/marketplace/memberships")
async def memberships():
"""Subscription tiers."""
data = _build_marketplace_data()
return {"count": len(data["subscriptions"]), "tiers": data["subscriptions"]}
@router.get("/marketplace/streams")
async def streams():
"""Real-time data streams."""
data = _build_marketplace_data()
return {"count": len(data["streams"]), "streams": data["streams"]}
@router.get("/marketplace/research")
async def research():
"""Research products."""
data = _build_marketplace_data()
return {"count": len(data["research"]), "products": data["research"]}
@router.get("/marketplace/batch")
async def batch():
"""Bulk scanning products."""
data = _build_marketplace_data()
return {"count": len(data["batch"]), "products": data["batch"]}
@router.get("/marketplace/skills")
async def agent_skills():
"""Agent workflow guides (also visible to humans)."""
data = _build_marketplace_data()
return data["skills"]
@router.get("/marketplace/search")
async def search(q: str = Query(..., min_length=2), limit: int = 50):
"""Free-text search."""
data = _build_marketplace_data()
q_lower = q.lower()
results = []
for t in data["tools"]:
score = 0
if q_lower in t["id"].lower():
score += 100
if q_lower in t["name"].lower():
score += 50
if q_lower in t["description"].lower():
score += 20
if q_lower in t["category"].lower():
score += 10
if q_lower in t["service"].lower():
score += 5
if any(q_lower in c.lower() for c in t["chains"]):
score += 5
if score > 0:
results.append({**t, "_score": score})
results.sort(key=lambda t: -t["_score"])
for r in results:
del r["_score"]
return {"query": q, "total": len(results), "tools": results[:limit]}
@router.get("/marketplace/{tool_id}")
async def get_tool(tool_id: str):
"""Single tool with full metadata."""
data = _build_marketplace_data()
for t in data["tools"]:
if t["id"] == tool_id:
return t
raise HTTPException(404, f"Tool '{tool_id}' not found")
@router.post("/invalidate")
async def invalidate_cache():
"""Force rebuild - call after adding tools or updating pricing."""
invalidate()
return {"status": "ok"}
# ════════════════════════════════════════════════════════════
# TOOL EXECUTION (mirror of /api/v1/x402-tools but with friendlier trial semantics)
# ════════════════════════════════════════════════════════════
@router.post("/{tool_id}")
async def call_tool(tool_id: str, request: Request):
"""
Execute a tool. Same as /api/v1/x402-tools/{tool_id} but with:
- Friendlier trial messages
- Built-in device fingerprint header handling
- Returns trial balance after execution
Identical payment logic - both paths go through the same enforcement.
"""
# Delegate to the existing x402 endpoint
# Forward the request internally
# (The x402 middleware handles the same path; this route is for friendlier responses)
return {
"status": "ok",
"message": f"Use /api/v1/x402-tools/{tool_id} for execution. This endpoint mirrors the bot-side path.",
"tool": tool_id,
}