alphax/app/web/routes_recommendations.py
2026-05-25 11:32:12 +08:00

259 lines
9.0 KiB
Python

from fastapi import APIRouter, Cookie
from app.db import auth_db
from app.db.analytics import (
get_all_recommendations,
get_cron_run_logs,
get_cron_run_summary,
get_observation_candidates,
get_pipeline_run_detail,
get_pipeline_runs,
get_review_stats,
get_screening_history,
get_stats,
)
from app.db.llm_insights import get_llm_insight_by_id, list_llm_insights
from app.db.recommendation_queries import get_active_recommendations, get_active_recommendations_deduped, get_opportunity_detail
from app.db.short_tf_signals import get_short_tf_signal_review
from app.config.config_loader import get_signal_weights
from app.web.shared import (
ObservationRequest,
PushRulesRequest,
WatchlistRequest,
require_api_user_with_subscription,
)
router = APIRouter()
def _friendly_llm_item(item):
content = item.get("content") or {}
payload = item.get("input") or {}
target_type = item.get("target_type") or ""
status = item.get("status") or ""
type_label = {
"recommendation": "推荐解释",
"sentiment": "舆情解读",
"review": "复盘 memo",
}.get(target_type, target_type or "未知任务")
status_label = {
"success": "成功",
"failed": "失败",
"skipped": "跳过",
}.get(status, status or "未知")
subject = payload.get("symbol") or payload.get("related_symbol") or payload.get("title") or payload.get("run_date") or item.get("target_id")
summary = content.get("summary") or content.get("memo") or content.get("why_now_or_not") or content.get("raw") or item.get("error") or ""
return {
"id": item.get("id"),
"type_label": type_label,
"status_label": status_label,
"status": status,
"subject": subject,
"summary": summary,
"model": item.get("model") or "",
"prompt_version": item.get("prompt_version") or "",
"target_type": target_type,
"target_id": item.get("target_id"),
"updated_at": item.get("updated_at"),
"error": item.get("error") or "",
"content": content,
"input": payload,
}
@router.get("/api/stats")
async def api_stats(altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
return get_stats()
@router.get("/api/recommendations")
async def api_recommendations(
limit: int = 50,
offset: int = 0,
decision_only: bool = False,
version: str = "",
archive_filter: str = "",
paged: bool = False,
compact: bool = False,
altcoin_session: str = Cookie(default=""),
):
require_api_user_with_subscription(altcoin_session)
return get_all_recommendations(
limit,
decision_only=decision_only,
version=version,
offset=offset,
with_meta=(paged or compact),
archive_filter=archive_filter,
)
@router.get("/api/recommendations/active")
async def api_recommendations_active(
dedup: bool = True,
actionable_only: bool = True,
version: str = "",
hours: float = 0,
limit: int = 0,
offset: int = 0,
paged: bool = False,
compact: bool = False,
altcoin_session: str = Cookie(default=""),
):
require_api_user_with_subscription(altcoin_session)
if dedup:
return get_active_recommendations_deduped(
actionable_only=actionable_only,
version=version,
hours=hours,
limit=limit,
offset=offset,
with_meta=(paged or compact),
)
return get_active_recommendations(actionable_only=actionable_only)
@router.get("/api/opportunity/detail")
async def api_opportunity_detail(symbol: str = "", rec_id: int = 0, altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
detail = get_opportunity_detail(symbol=symbol, rec_id=rec_id)
if not detail:
return {"error": "opportunity not found", "symbol": symbol, "rec_id": rec_id}
return detail
@router.get("/api/observations/active")
async def api_observations_active(
limit: int = 50,
altcoin_session: str = Cookie(default=""),
):
require_api_user_with_subscription(altcoin_session)
return get_observation_candidates(limit=limit)
@router.get("/api/personalization")
async def api_personalization(altcoin_session: str = Cookie(default="")):
user = require_api_user_with_subscription(altcoin_session)
return {
"watchlist": auth_db.get_watchlist_symbols(user["id"]),
"observations": auth_db.get_saved_observations(user["id"]),
"push_rules": auth_db.get_push_rules(user["id"]),
}
@router.post("/api/watchlist")
async def api_add_watchlist(req: WatchlistRequest, altcoin_session: str = Cookie(default="")):
user = require_api_user_with_subscription(altcoin_session)
auth_db.add_watchlist_symbol(user["id"], req.symbol)
return {"ok": True, "watchlist": auth_db.get_watchlist_symbols(user["id"])}
@router.delete("/api/watchlist/{symbol}")
async def api_remove_watchlist(symbol: str, altcoin_session: str = Cookie(default="")):
user = require_api_user_with_subscription(altcoin_session)
auth_db.remove_watchlist_symbol(user["id"], symbol)
return {"ok": True, "watchlist": auth_db.get_watchlist_symbols(user["id"])}
@router.post("/api/observations")
async def api_save_observation(req: ObservationRequest, altcoin_session: str = Cookie(default="")):
user = require_api_user_with_subscription(altcoin_session)
auth_db.save_observation(user["id"], req.rec_id, req.note)
return {"ok": True, "observations": auth_db.get_saved_observations(user["id"])}
@router.delete("/api/observations/{rec_id}")
async def api_remove_observation(rec_id: int, altcoin_session: str = Cookie(default="")):
user = require_api_user_with_subscription(altcoin_session)
auth_db.remove_observation(user["id"], rec_id)
return {"ok": True, "observations": auth_db.get_saved_observations(user["id"])}
@router.post("/api/push-rules")
async def api_update_push_rules(req: PushRulesRequest, altcoin_session: str = Cookie(default="")):
user = require_api_user_with_subscription(altcoin_session)
rules = auth_db.update_push_rules(user["id"], req.dict())
return {"ok": True, "push_rules": rules}
@router.get("/api/screening")
async def api_screening(hours: int = 24, limit: int = 100, altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
return get_screening_history(hours, limit)
@router.get("/api/screening/short-tf-review")
async def api_short_tf_signal_review(hours: int = 168, limit: int = 200, altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
return get_short_tf_signal_review(hours=hours, limit=limit)
@router.get("/api/review")
async def api_review(altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
return get_review_stats()
@router.get("/api/weights")
async def api_weights(altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
return get_signal_weights()
@router.get("/api/cron")
async def api_cron(limit: int = 50, job_name: str = "", altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
return get_cron_run_logs(limit=limit, job_name=job_name or None)
@router.get("/api/cron/summary")
async def api_cron_summary(hours: int = 24, altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
return get_cron_run_summary(hours=hours)
@router.get("/api/pipeline/runs")
async def api_pipeline_runs(limit: int = 30, hours: int = 24, offset: int = 0, altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
return get_pipeline_runs(limit=limit, hours=hours, offset=offset)
@router.get("/api/pipeline/runs/{run_id}")
async def api_pipeline_run_detail(run_id: int, altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
detail = get_pipeline_run_detail(run_id)
if not detail:
return {"error": "pipeline run not found", "run_id": run_id}
return detail
@router.get("/api/llm/insights")
async def api_llm_insights(
limit: int = 30,
offset: int = 0,
target_type: str = "",
status: str = "",
insight_type: str = "",
altcoin_session: str = Cookie(default=""),
):
require_api_user_with_subscription(altcoin_session)
data = list_llm_insights(
limit=limit,
offset=offset,
target_type=target_type or "",
status=status or "",
insight_type=insight_type or "",
)
data["items"] = [_friendly_llm_item(item) for item in data.get("items", [])]
return data
@router.get("/api/llm/insights/{insight_id}")
async def api_llm_insight_detail(insight_id: int, altcoin_session: str = Cookie(default="")):
require_api_user_with_subscription(altcoin_session)
item = get_llm_insight_by_id(insight_id)
if not item:
return {"error": "llm insight not found", "id": insight_id}
return _friendly_llm_item(item)