rmi-backend/app/api/v1/catalog/router.py

155 lines
5.6 KiB
Python

"""T27B HTTP routes — CatalogService endpoints.
Per v4.0 §T27. The thin HTTP layer over app.catalog.service.CatalogService.
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from app.catalog.models import Chain
from app.catalog.service import get_catalog
router = APIRouter(prefix="/api/v1/catalog", tags=["catalog"])
# ── Request models ───────────────────────────────────────────────
class RagIngestRequest(BaseModel):
content: str = Field(..., min_length=1)
collection: str = "scam_intel"
doc_id: str | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
class RagSearchRequest(BaseModel):
query: str = Field(..., min_length=1)
collection: str = "scam_intel"
top_k: int = Field(default=5, ge=1, le=50)
class ResolveEntityRequest(BaseModel):
wallet_id: str
max_chains: int = Field(default=5, ge=1, le=20)
class FindRiskyTokensRequest(BaseModel):
min_rug_count: int = Field(default=1, ge=1)
chain: str | None = None
limit: int = Field(default=50, ge=1, le=200)
class AttachRagRequest(BaseModel):
chain: str
address: str
qdrant_point_id: str
# ── Health / introspection ───────────────────────────────────────
@router.get("/stats")
async def stats() -> dict:
"""Catalog stats: which stores are reachable + entity counts."""
return await get_catalog().stats()
@router.get("/probe")
async def probe() -> dict:
"""Probe which stores are reachable from this container."""
return await get_catalog().probe_stores()
# ── Token endpoints ─────────────────────────────────────────────
@router.get("/tokens/{chain}/{address}")
async def get_token(chain: str, address: str) -> dict:
"""Get a token by chain+address. Returns full Token model + provenance."""
try:
c = Chain(chain)
except ValueError:
raise HTTPException(400, f"unknown chain: {chain}")
tok = await get_catalog().get_token(c, address)
if not tok:
raise HTTPException(404, "token not found")
return tok.model_dump(mode="json")
@router.get("/tokens/{chain}/{address}/risk")
async def get_token_risk(chain: str, address: str) -> dict:
"""Recipe 3 — Real-time risk score. Composes Redis + Postgres + Neo4j."""
try:
c = Chain(chain)
except ValueError:
raise HTTPException(400, f"unknown chain: {chain}")
return await get_catalog().get_token_risk(c, address)
@router.post("/tokens/risky-by-deployer")
async def risky_tokens(req: FindRiskyTokensRequest) -> dict:
"""Recipe 1 — Find tokens deployed by wallets with rug history."""
chain_enum = None
if req.chain:
try:
chain_enum = Chain(req.chain)
except ValueError:
raise HTTPException(400, f"unknown chain: {req.chain}")
tokens = await get_catalog().find_tokens_by_deployer_history(
min_rug_count=req.min_rug_count, chain=chain_enum, limit=req.limit
)
return {
"count": len(tokens),
"tokens": [t.model_dump(mode="json") for t in tokens],
}
# ── Wallet endpoints ─────────────────────────────────────────────
@router.get("/wallets/{chain}/{address}")
async def get_wallet(chain: str, address: str) -> dict:
try:
c = Chain(chain)
except ValueError:
raise HTTPException(400, f"unknown chain: {chain}")
w = await get_catalog().get_wallet(c, address)
if not w:
raise HTTPException(404, "wallet not found")
return w.model_dump(mode="json")
# ── Entity resolution (Recipe 5) ────────────────────────────────
@router.post("/entities/resolve")
async def resolve_entity(req: ResolveEntityRequest) -> dict:
"""Cross-chain entity resolution via Neo4j Cypher."""
return await get_catalog().resolve_entity(req.wallet_id, req.max_chains)
# ── RAG bridge endpoints ────────────────────────────────────────
@router.post("/rag/search")
async def rag_search(req: RagSearchRequest) -> dict:
"""Search the RAG system. Returns ranked hits with RRF scores."""
hits = await get_catalog().rag_search(
query=req.query, collection=req.collection, top_k=req.top_k
)
return {"count": len(hits), "hits": hits}
@router.post("/rag/ingest")
async def rag_ingest(req: RagIngestRequest) -> dict:
"""Ingest content into RAG. Returns qdrant_point_id for cross-store linking."""
return await get_catalog().rag_ingest(
content=req.content,
collection=req.collection,
doc_id=req.doc_id,
metadata=req.metadata,
)
@router.post("/tokens/{chain}/{address}/attach-rag")
async def attach_rag(chain: str, address: str, req: AttachRagRequest) -> dict:
"""Link an existing RAG embedding (Qdrant point) to a Token row."""
try:
c = Chain(chain)
except ValueError:
raise HTTPException(400, f"unknown chain: {chain}")
ok = await get_catalog().attach_rag_to_token(c, address, req.qdrant_point_id)
if not ok:
raise HTTPException(404, "token not found or update failed")
return {"ok": True, "chain": chain, "address": address, "rag_embedding_id": req.qdrant_point_id}