| """ |
| v4 Complement API — expose v4 fallback routes and uptime probe on v2 primary Space. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import asyncio |
| import logging |
| import time |
| from datetime import datetime |
| from typing import Any, Dict, List, Optional |
|
|
| import httpx |
| from fastapi import APIRouter, Body, Query |
| from pydantic import BaseModel, Field |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router = APIRouter(tags=["v4 Complement"]) |
|
|
| V2_BASE = "https://really-amin-datasourceforcryptocurrency-2.hf.space" |
|
|
| CRITICAL_ENDPOINTS = [ |
| "/api/market", |
| "/api/coins/top", |
| "/api/market/gainers", |
| "/api/market/losers", |
| "/api/trading/ohlcv/BTC", |
| "/api/news/btc", |
| "/api/social/sentiment", |
| "/api/indicators/comprehensive?symbol=BTC", |
| "/api/resources/rotation", |
| "/api/models/list", |
| "/api/models/summary", |
| "/api/hf/run-sentiment", |
| ] |
|
|
|
|
| class SentimentRequest(BaseModel): |
| text: str = Field(..., min_length=1) |
| mode: str = Field("crypto", description="crypto | financial | social | news") |
| model: Optional[str] = None |
|
|
|
|
| @router.get("/api/complement/v4/status") |
| async def complement_v4_status(): |
| from backend.services.v4_complement_client import probe_v4, V4_SPACE_URL, V4_ENABLED |
| probe = await probe_v4() |
| return { |
| "success": True, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "v4_space_url": V4_SPACE_URL, |
| "enabled": V4_ENABLED, |
| "probe": probe, |
| } |
|
|
|
|
| @router.get("/api/complement/v4/models") |
| async def complement_v4_models(): |
| from backend.services.v4_complement_client import get_models_list_summary |
| result = await get_models_list_summary() |
| return {"success": result.get("success", False), **result} |
|
|
|
|
| @router.post("/api/sentiment") |
| async def sentiment_with_v4_fallback(payload: SentimentRequest): |
| """ |
| Unified sentiment: try v2 local HF models first, then v4 complement. |
| """ |
| text = payload.text.strip() |
| errors: List[str] = [] |
|
|
| |
| try: |
| from backend.services.hf_client import run_sentiment |
| local = run_sentiment([text], model=payload.model) |
| if local.get("enabled") and local.get("samples"): |
| return { |
| "success": True, |
| "sentiment": "Bullish" if local.get("vote", 0) > 0.1 else ( |
| "Bearish" if local.get("vote", 0) < -0.1 else "Neutral" |
| ), |
| "confidence": abs(local.get("vote", 0)), |
| "source": "v2_local", |
| "model": local.get("model"), |
| "samples": local.get("samples"), |
| } |
| except Exception as exc: |
| errors.append(f"v2_local: {exc}") |
|
|
| |
| try: |
| from ai_models import analyze_crypto_sentiment, analyze_financial_sentiment, analyze_social_sentiment |
| analyzers = { |
| "crypto": analyze_crypto_sentiment, |
| "financial": analyze_financial_sentiment, |
| "social": analyze_social_sentiment, |
| "news": analyze_financial_sentiment, |
| } |
| fn = analyzers.get(payload.mode, analyze_crypto_sentiment) |
| result = fn(text) |
| if result and result.get("available"): |
| return { |
| "success": True, |
| "sentiment": result.get("label", "Neutral"), |
| "confidence": result.get("confidence", 0), |
| "source": "v2_ai_models", |
| "model": result.get("model_key"), |
| "extra": result, |
| } |
| except Exception as exc: |
| errors.append(f"v2_ai_models: {exc}") |
|
|
| |
| from backend.services.v4_complement_client import analyze_sentiment as v4_sentiment |
| v4 = await v4_sentiment(text, mode=payload.mode, model=payload.model) |
| if v4.get("success"): |
| return {**v4, "fallback_chain": ["v2_local", "v2_ai_models", "v4_complement"]} |
|
|
| return { |
| "success": False, |
| "error": "All sentiment providers failed", |
| "attempts": errors + [v4.get("error", "v4_failed")], |
| "fallback_chain": ["v2_local", "v2_ai_models", "v4_complement"], |
| } |
|
|
|
|
| @router.get("/api/uptime/probe") |
| async def uptime_probe( |
| include_v4: bool = Query(True, description="Also probe v4 complement Space"), |
| base_url: Optional[str] = Query(None, description="Override v2 base for self-probe"), |
| ): |
| """Probe 12 critical v2 endpoints (+ optional v4) for uptime reporting.""" |
| v2 = (base_url or "").rstrip("/") or None |
|
|
| async def check_one(client: httpx.AsyncClient, base: str, path: str) -> Dict[str, Any]: |
| url = f"{base}{path}" |
| start = time.perf_counter() |
| try: |
| if path.endswith("/api/hf/run-sentiment"): |
| r = await client.post( |
| url, |
| json={"texts": ["Bitcoin market probe"]}, |
| timeout=45.0, |
| ) |
| else: |
| r = await client.get(url, timeout=30.0) |
| ms = round((time.perf_counter() - start) * 1000, 1) |
| return { |
| "path": path, |
| "status": r.status_code, |
| "ok": 200 <= r.status_code < 300, |
| "response_ms": ms, |
| } |
| except Exception as exc: |
| ms = round((time.perf_counter() - start) * 1000, 1) |
| return {"path": path, "status": 0, "ok": False, "response_ms": ms, "error": str(exc)[:120]} |
|
|
| |
| if v2 is None: |
| from fastapi import Request |
| |
| v2 = "https://really-amin-datasourceforcryptocurrency-2.hf.space" |
|
|
| async with httpx.AsyncClient(follow_redirects=True) as client: |
| v2_tasks = [check_one(client, v2, p) for p in CRITICAL_ENDPOINTS] |
| v2_results = await asyncio.gather(*v2_tasks) |
|
|
| v2_ok = sum(1 for r in v2_results if r["ok"]) |
| out: Dict[str, Any] = { |
| "success": True, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "v2_base": v2, |
| "v2_uptime_pct": round(100 * v2_ok / len(CRITICAL_ENDPOINTS), 1), |
| "v2_ok": v2_ok, |
| "v2_total": len(CRITICAL_ENDPOINTS), |
| "endpoints": v2_results, |
| } |
|
|
| if include_v4: |
| from backend.services.v4_complement_client import probe_v4, V4_SPACE_URL |
| v4_probe = await probe_v4() |
| out["v4_complement"] = { |
| "url": V4_SPACE_URL, |
| "uptime_pct": round( |
| 100 * v4_probe.get("endpoints_ok", 0) / max(v4_probe.get("endpoints_total", 1), 1), |
| 1, |
| ), |
| **v4_probe, |
| } |
|
|
| return out |
|
|