merge: chore/cleanup-remove-bloat-and-secrets into main

This commit is contained in:
Crypto Rug Munch 2026-07-02 01:24:22 +07:00
commit bde2f3a97d
1173 changed files with 437609 additions and 0 deletions

1
app/api/__init__.py Normal file
View file

@ -0,0 +1 @@
"""HTTP transport layer. Routes are thin: parse → call domain service → return."""

34
app/api/deps.py Normal file
View file

@ -0,0 +1,34 @@
"""Shared FastAPI dependencies.
Use these in route signatures to inject cross-cutting concerns:
from app.api.deps import get_redis, get_current_user, get_settings
Actual implementations live in `app/core/`. This module is a re-export
facade so route authors don't need to know which core module owns what.
"""
from __future__ import annotations
# Re-exports — actual implementations come from app/core/.
# Core modules are populated by the parallel DeepSeek tasks (DS-1..DS-10).
# Until then, these imports will fail; routes should not depend on them yet.
try:
from app.core.redis import get_redis
except ImportError:
get_redis = None # type: ignore[assignment]
try:
from app.core.db import get_db
except ImportError:
get_db = None # type: ignore[assignment]
try:
from app.core.auth import get_current_user, get_optional_user
except ImportError:
get_current_user = None # type: ignore[assignment]
get_optional_user = None # type: ignore[assignment]
try:
from app.core.config import settings
except ImportError:
pass # fallback until core.config lands

73
app/api/v1/__init__.py Normal file
View file

@ -0,0 +1,73 @@
"""V1 API router aggregator.
The strangle: new v1 routes are added here as domains migrate. The legacy
main.py still mounts all old routes; we ADD new v1 routes on top so they
co-exist until cutover.
To add a new domain:
1. Create app/api/v1/<group>/<domain>.py with APIRouter
2. Import and append it to `api_v1_router` below
3. Mount the route prefix in the domain's __init__.py
"""
from __future__ import annotations
from fastapi import APIRouter
# Aggregator list — populated as domains migrate.
# Each entry is an APIRouter from app/api/v1/<group>/<domain>.py.
api_v1_router: list[APIRouter] = []
# Aggregator router — single mount point for v1.
# When domains migrate, replace this with a real aggregator:
# from app.api.v1.public import router as public_router
# api_v1_router.append(public_router)
router = APIRouter(prefix="/api/v1", tags=["v1"])
# ── Migrated domains ───────────────────────────────────────────────────
# Each migrated domain is imported here. The router exposes endpoints
# at /api/v1/<domain>/* (path defined per-router).
#
# During strangelfig, the LEGACY /api/v1/<domain>/* endpoints remain
# mounted in main.py. The new v1 router is mounted at the same path
# (FastAPI handles prefix-based routing) — first match wins, so the
# legacy stays until we explicitly remove it.
from app.api.v1.auth.alerts import router as alerts_router # noqa: E402
api_v1_router.append(alerts_router)
from app.api.v1.public.wallet import router as wallet_router # noqa: E402
api_v1_router.append(wallet_router)
from app.api.v1.public.token import router as token_router # noqa: E402
api_v1_router.append(token_router)
from app.api.v1.public.scanner import router as scanner_router # noqa: E402
api_v1_router.append(scanner_router)
# x402 moved to app.domain.x402 (T34 v2)
# Old app/api/v1/x402/payments.py removed to avoid model conflicts
from app.api.v1.rag.search import router as rag_v2_router # noqa: E402
api_v1_router.append(rag_v2_router)
from app.api.v1.admin.alerts_webhook import router as admin_alerts_webhook_router # noqa: E402
api_v1_router.append(admin_alerts_webhook_router)
from app.api.v1.catalog import router as catalog_router # noqa: E402
api_v1_router.append(catalog_router)
def build_v1_router() -> APIRouter:
"""Construct the v1 aggregator with all migrated routes mounted."""
aggregated = APIRouter(prefix="/api/v1")
for r in api_v1_router:
aggregated.include_router(r)
return aggregated

View file

@ -0,0 +1,4 @@
"""Admin routes — admin role required.
Target: user management, system config, ops, bulletin moderation.
"""

View file

@ -0,0 +1,32 @@
"""Admin alerts webhook — /api/v1/admin/alerts/webhook.
Stub endpoint for receiving alert webhooks from external sources
(monitoring, observability platforms).
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/alerts", tags=["admin"])
class AlertWebhookPayload(BaseModel):
"""Generic webhook payload from external alert sources."""
source: str # "prometheus" | "grafana" | "sentry" | "custom"
severity: str # "info" | "warning" | "critical"
title: str
description: str | None = None
labels: dict[str, str] = {}
@router.post("/webhook")
async def receive_alert_webhook(payload: AlertWebhookPayload) -> dict[str, Any]:
"""Receive an alert webhook from external monitoring."""
raise HTTPException(
status_code=501,
detail="Alert webhook ingestion not yet implemented — pending T08 GlitchTip wiring",
)

View file

@ -0,0 +1,60 @@
"""T07 GlitchTip test endpoint.
POST /api/v1/_test/glitchtip
{"type": "error", "message": "test error"}
POST /api/v1/_test/exception
triggers a real exception, captured by GlitchTip
POST /api/v1/_test/message
captures an info-level message
Used for:
- Verifying the GlitchTip pipeline works
- Smoke testing after deploys
- Demonstrating the secret-scrubbing before_send hook
"""
from __future__ import annotations
import logging
from fastapi import APIRouter
from pydantic import BaseModel
log = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/_test", tags=["test"])
class GlitchtipTestRequest(BaseModel):
type: str = "error" # error | exception | message
message: str = "test event from RMI"
secret: str | None = None # should be REDACTED in Sentry
@router.post("/glitchtip")
async def test_glitchtip(req: GlitchtipTestRequest) -> dict:
"""Trigger a test event. Tests the secret-scrubbing before_send hook."""
if req.type == "exception":
try:
raise ValueError(req.message)
except Exception as e:
try:
from app.core.observability import capture_exception
capture_exception(e, secret=req.secret, route="/api/v1/_test/glitchtip")
except ImportError:
log.exception("test_exception_no_sentry")
return {"captured": "exception", "message": req.message}
if req.type == "message":
try:
from app.core.observability import capture_message
capture_message(req.message, level="warning", secret=req.secret)
except ImportError:
log.warning(f"test_message_no_sentry: {req.message}")
return {"captured": "message", "message": req.message}
# default: error log + capture
log.error(f"test_error: {req.message} (secret={req.secret})")
try:
from app.core.observability import capture_message
capture_message(req.message, level="error", secret=req.secret)
except ImportError:
pass
return {"captured": "error", "message": req.message, "secret_redacted_in_sentry": True}

View file

@ -0,0 +1,4 @@
"""Authenticated routes — JWT required.
Target: portfolio, alerts, intel feeds, profile, settings.
"""

65
app/api/v1/auth/alerts.py Normal file
View file

@ -0,0 +1,65 @@
"""Auth alerts router — /api/v1/alerts/*.
Stub implementation for the alerts domain. Real implementations
will wire up to user-configured alert rules and notification channels
(email, Telegram, webhook). For now, returns 501 Not Implemented
for actual alert operations, with version metadata.
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/alerts", tags=["alerts"])
class AlertRule(BaseModel):
"""Schema for an alert rule (creation/edit)."""
name: str
subject_type: str # "token" | "wallet" | "deployer"
subject_id: str
trigger: str # "risk_score_above" | "deployer_rug" | "news_mention"
threshold: float | None = None
channels: list[str] = [] # ["email", "telegram", "webhook"]
class AlertList(BaseModel):
"""Response for GET /api/v1/alerts."""
count: int
items: list[dict[str, Any]] = []
@router.get("", response_model=AlertList)
async def list_alerts() -> AlertList:
"""List all configured alert rules for the authenticated user.
TODO: wire up to Postgres once auth context is established.
Returns empty list as a stub so the factory can mount successfully.
"""
return AlertList(count=0, items=[])
@router.post("", status_code=501)
async def create_alert(rule: AlertRule) -> dict[str, str]:
"""Create a new alert rule.
Returns 501 until alert persistence is wired up. Stub so the
factory mounts this route without crashing.
"""
raise HTTPException(
status_code=501,
detail="Alert persistence not yet implemented — coming in v5.1",
)
@router.delete("/{rule_id}", status_code=501)
async def delete_alert(rule_id: str) -> dict[str, str]:
"""Delete an alert rule by ID."""
raise HTTPException(
status_code=501,
detail="Alert persistence not yet implemented — coming in v5.1",
)

View file

@ -0,0 +1,4 @@
"""Catalog v1 routes — thin HTTP layer."""
from .router import router
__all__ = ["router"]

View file

@ -0,0 +1,155 @@
"""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}

View file

@ -0,0 +1,4 @@
"""MCP v1 routes."""
from .router import router
__all__ = ["router"]

164
app/api/v1/mcp/router.py Normal file
View file

@ -0,0 +1,164 @@
"""T33 MCP Server — HTTP wrapper for SSE transport.
Per v4.0 §T33. Endpoints:
POST /mcp JSON-RPC 2.0 endpoint
GET /mcp/tools Tool catalog
POST /mcp/call/{tool_id} Direct tool execution (no JSON-RPC)
The server speaks the Model Context Protocol natively. Claude Desktop
and Cursor connect via:
{"mcpServers": {"rugmunch": {"url": "https://mcp.rugmunch.io/mcp", "transport": "sse"}}}
"""
from __future__ import annotations
import json
import logging
from typing import Any
from fastapi import APIRouter, Request
from pydantic import BaseModel
from app.mcp.server import (
MCP_PROTOCOL_VERSION,
MCP_SERVER_VERSION,
TOOL_CATALOG,
TOOL_DEPRECATED,
TOOL_SUCCESSORS,
TOOL_VERSIONS,
call_tool,
)
router = APIRouter(prefix="/mcp", tags=["mcp"])
log = logging.getLogger(__name__)
class JsonRpcRequest(BaseModel):
jsonrpc: str = "2.0"
method: str
params: dict[str, Any] = {}
id: int | str | None = None
class JsonRpcResponse(BaseModel):
jsonrpc: str = "2.0"
result: Any | None = None
error: dict | None = None
id: int | str | None = None
@router.post("")
async def jsonrpc_handler(req: JsonRpcRequest) -> dict:
"""JSON-RPC 2.0 endpoint for MCP clients.
Methods:
- initialize returns server info
- tools/list returns tool catalog
- tools/call dispatches to backend
- resources/list empty
- prompts/list empty
"""
if req.jsonrpc != "2.0":
return {"jsonrpc": "2.0", "error": {"code": -32600, "message": "invalid jsonrpc version"}, "id": req.id}
if req.method == "initialize":
return {
"jsonrpc": "2.0",
"result": {
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "rugmunch-intelligence",
"version": MCP_SERVER_VERSION,
"description": "Crypto intelligence platform — 13+ chains, 8 MCP tools, x402 paid tier",
},
"capabilities": {"tools": {}, "resources": {}, "prompts": {}},
},
"id": req.id,
}
if req.method == "tools/list":
return {
"jsonrpc": "2.0",
"result": {"tools": TOOL_CATALOG},
"id": req.id,
}
if req.method == "tools/call":
name = req.params.get("name", "")
arguments = req.params.get("arguments", {})
if not name:
return {"jsonrpc": "2.0", "error": {"code": -32602, "message": "tool name required"}, "id": req.id}
result = await call_tool(name, arguments)
return {
"jsonrpc": "2.0",
"result": {
"content": [{"type": "text", "text": json.dumps(result, default=str)[:50000]}],
"isError": "error" in result,
},
"id": req.id,
}
if req.method == "resources/list":
return {"jsonrpc": "2.0", "result": {"resources": []}, "id": req.id}
if req.method == "prompts/list":
return {"jsonrpc": "2.0", "result": {"prompts": []}, "id": req.id}
if req.method == "notifications/initialized":
return {"jsonrpc": "2.0", "result": {}, "id": req.id}
return {
"jsonrpc": "2.0",
"error": {"code": -32601, "message": f"method not found: {req.method}"},
"id": req.id,
}
@router.get("/tools")
async def list_tools() -> dict:
"""Plain JSON endpoint (for direct integration, no JSON-RPC)."""
return {"server": "rugmunch-intelligence", "version": MCP_SERVER_VERSION, "tools": TOOL_CATALOG}
@router.get("/info")
async def server_info() -> dict:
"""Server metadata: version, protocol, tool count, capabilities.
Use this to discover the MCP server's capabilities without listing all tools.
Equivalent to MCP initialize handshake.
"""
return {
"server": "rugmunch-intelligence",
"server_version": MCP_SERVER_VERSION,
"protocol_version": MCP_PROTOCOL_VERSION,
"tool_count": len(TOOL_CATALOG),
"tools_versioned": sum(1 for t in TOOL_CATALOG if t["name"] in TOOL_VERSIONS),
"tools_deprecated": list(TOOL_DEPRECATED),
"tools_with_successors": list(TOOL_SUCCESSORS.keys()),
"capabilities": ["tools", "resources", "prompts"],
"endpoints": {
"jsonrpc": "/mcp",
"tools_list": "/mcp/tools",
"server_info": "/mcp/info",
"direct_call": "/mcp/call/{tool_id}",
},
"auth": {
"free_tier_daily": 5,
"pro_tier": "x402 micropayment per call",
"x402_endpoint": "https://x402.rugmunch.io",
},
"links": {
"homepage": "https://rugmunch.io",
"mcp_endpoint": "https://mcp.rugmunch.io/mcp",
"status": "https://status.rugmunch.io",
"docs": "https://github.com/Rug-Munch-Media-LLC/rmi-docs",
},
}
@router.post("/call/{tool_id}")
async def direct_call(tool_id: str, request: Request) -> dict:
"""Direct tool execution (no JSON-RPC). For curl/scripts."""
body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
arguments = body.get("arguments", body) if isinstance(body, dict) else {}
result = await call_tool(tool_id, arguments)
return result

View file

@ -0,0 +1,4 @@
"""Public routes — no authentication required.
Target: scanner, wallet lookup, token info, pricing, health.
"""

View file

@ -0,0 +1,62 @@
"""Public scanner endpoints — /api/v1/scanner/*.
Stub for unauthenticated token scans. Real implementation will
trigger the scanner pipeline (honeypot detection, flash loan checks,
oracle manipulation analysis).
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
router = APIRouter(prefix="/scanner", tags=["scanner"])
class ScanRequest(BaseModel):
"""Request to scan a token or wallet."""
chain: str = "ethereum"
address: str
depth: str = "standard" # "quick" | "standard" | "deep"
class ScanResult(BaseModel):
"""Result of a scan."""
scan_id: str
status: str # "queued" | "running" | "completed" | "failed"
risk_score: int | None = None
risk_tier: str | None = None
findings: list[str] = []
@router.post("/scan", response_model=ScanResult)
async def scan(req: ScanRequest) -> ScanResult:
"""Queue a token/wallet scan."""
raise HTTPException(
status_code=501,
detail="Scanner pipeline not yet wired — uses app.domain.scanner (T06+)",
)
@router.get("/result/{scan_id}", response_model=ScanResult)
async def get_scan_result(scan_id: str) -> ScanResult:
"""Get the result of a previously queued scan."""
raise HTTPException(
status_code=501,
detail="Scan result retrieval not yet implemented",
)
@router.get("/quick")
async def quick_scan(
chain: str = Query("ethereum"),
address: str = Query(...),
) -> dict[str, Any]:
"""Quick scan (free tier, no persistence)."""
raise HTTPException(
status_code=501,
detail="Quick scan uses cached shield — see caching_shield module",
)

View file

@ -0,0 +1,65 @@
"""Public token endpoints — /api/v1/token/*.
Stub for unauthenticated token queries. Real implementation will
fetch token metadata, holders, liquidity, and risk score.
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
router = APIRouter(prefix="/token", tags=["token"])
class TokenSummary(BaseModel):
"""Basic token metadata."""
chain: str
address: str
name: str | None = None
symbol: str | None = None
decimals: int | None = None
deployed_at: str | None = None
deployer: str | None = None
class TokenRisk(BaseModel):
"""Token risk assessment."""
address: str
chain: str
risk_score: int
risk_tier: str
factors: list[str] = []
@router.get("/{address}", response_model=TokenSummary)
async def get_token(
address: str,
chain: str = Query("ethereum"),
) -> TokenSummary:
"""Fetch basic token metadata."""
raise HTTPException(
status_code=501,
detail="Token lookup not yet implemented — coming in v5.1",
)
@router.get("/{address}/risk", response_model=TokenRisk)
async def get_token_risk(address: str, chain: str = "ethereum") -> TokenRisk:
"""Compute the risk score for a token (uses Bayesian reputation + on-chain checks)."""
raise HTTPException(
status_code=501,
detail="Token risk uses T01 Bayesian + T02 scanner pipeline — pending wiring",
)
@router.get("/{address}/holders")
async def get_token_holders(address: str, chain: str = "ethereum", top: int = 50) -> dict[str, Any]:
"""Return top holders distribution for a token."""
raise HTTPException(
status_code=501,
detail="Holder distribution not yet implemented — uses Postgres + Neo4j",
)

View file

@ -0,0 +1,57 @@
"""Public wallet endpoints — /api/v1/wallet/*.
Stub for unauthenticated wallet queries. Real implementation will
resolve wallets, fetch labels, and return balance/history data.
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
router = APIRouter(prefix="/wallet", tags=["wallet"])
class WalletResolveResponse(BaseModel):
"""Response for wallet resolution."""
chain: str
address: str
labels: list[str] = []
entity: str | None = None
balance_usd: float | None = None
tx_count: int | None = None
@router.get("/{address}", response_model=WalletResolveResponse)
async def resolve_wallet(
address: str,
chain: str = Query("ethereum", description="Blockchain (ethereum, solana, base, etc.)"),
) -> WalletResolveResponse:
"""Resolve a wallet address to its labels + summary.
Returns 501 stub until label resolution is wired up.
"""
raise HTTPException(
status_code=501,
detail="Wallet resolution not yet implemented — coming in v5.1",
)
@router.get("/{address}/labels", response_model=list[dict[str, Any]])
async def get_wallet_labels(address: str, chain: str = "ethereum") -> list[dict[str, Any]]:
"""Return labels for a wallet from all federated sources."""
raise HTTPException(
status_code=501,
detail="Federated labels API pending — see T11",
)
@router.get("/{address}/history")
async def get_wallet_history(address: str, chain: str = "ethereum") -> dict[str, Any]:
"""Return transaction history summary for a wallet."""
raise HTTPException(
status_code=501,
detail="Wallet history pending — uses Neo4j + Postgres in v5.1",
)

81
app/api/v1/rag/search.py Normal file
View file

@ -0,0 +1,81 @@
"""V1 RAG route — thin HTTP layer over app.rag.
The RAG system is the most coupled module (14 legacy files). This
facade exposes the most-used operations: search, ingest, feedback.
"""
from __future__ import annotations
from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from app.rag import (
FeedbackRecord,
IngestRequest,
IngestResult,
RAGService,
SearchRequest,
SearchResponse,
)
from app.rag.engine import bulk_ingest as engine_bulk_ingest
from app.rag.engine import get_stats as engine_get_stats
router = APIRouter(prefix="/api/v1/rag/v2", tags=["rag"])
def _service() -> RAGService:
return RAGService()
class BulkIngestRequest(BaseModel):
collection: str = "scam_intel"
items: list[dict[str, Any]] = Field(default_factory=list)
@router.post("/search", response_model=SearchResponse)
async def search(
req: SearchRequest,
svc: Annotated[RAGService, Depends(_service)],
) -> SearchResponse:
"""RAG search. Returns Pydantic response with hits + scores."""
return await svc.search(req)
@router.post("/ingest", response_model=IngestResult)
async def ingest(
req: IngestRequest,
svc: Annotated[RAGService, Depends(_service)],
) -> IngestResult:
"""Ingest a document into the RAG system."""
return await svc.ingest(req)
@router.post("/feedback", response_model=IngestResult)
async def feedback(
record: FeedbackRecord,
svc: Annotated[RAGService, Depends(_service)],
) -> IngestResult:
"""Record scanner → RAG feedback. Ingests known scam into known_scams collection."""
ok = await svc.record_feedback(record)
return IngestResult(
doc_id=record.token_address,
collection="known_scams",
status="ok" if ok else "failed",
)
@router.get("/stats")
async def stats() -> dict:
"""Per-collection vector counts + active embedder backend."""
return engine_get_stats()
@router.post("/bulk-ingest")
async def bulk(req: BulkIngestRequest) -> dict:
"""Ingest many items into a collection sequentially (max 500 per call)."""
if not req.items:
raise HTTPException(status_code=400, detail="items must be non-empty")
if len(req.items) > 500:
raise HTTPException(status_code=400, detail="bulk limit 500 per call")
return await engine_bulk_ingest(items=req.items, collection=req.collection)

View file

@ -0,0 +1,4 @@
"""x402 paid routes — crypto micropayment gated.
Target: tools (split from legacy x402_tools.py), tokens, wallets, defi, security.
"""

4
app/api/ws/__init__.py Normal file
View file

@ -0,0 +1,4 @@
"""WebSocket endpoints.
Target: real-time alerts, scanner results, intel feeds.
"""