app.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. """FastAPI application — category tree API on port 8080."""
  2. from __future__ import annotations
  3. from contextlib import asynccontextmanager
  4. from pathlib import Path
  5. from fastapi import FastAPI, HTTPException, Query
  6. from fastapi.middleware.cors import CORSMiddleware
  7. from fastapi.staticfiles import StaticFiles
  8. from pydantic import BaseModel, Field
  9. from api.services.category_tree import build_category_tree
  10. from api.services.demand_belong_category import list_demand_belong_categories
  11. from api.services.demand_grade import list_demand_grades
  12. from api.services.demand_grade_videos import list_videos_for_demand_grade
  13. from api.services.demand_videos import list_videos_for_demand_belong
  14. from api.services.oss_logs import list_demand_belong_oss_logs
  15. from api.services.scheduler import (
  16. get_scheduler_job_run,
  17. list_triggerable_jobs,
  18. run_scheduler_job,
  19. run_supply_pipeline,
  20. )
  21. from api.services.video_discovery import (
  22. get_video_discovery_demand,
  23. list_video_discovery_demands,
  24. )
  25. from supply_infra.db import init_db
  26. from supply_infra.scheduler.app import get_scheduler_status, start_scheduler, stop_scheduler
  27. @asynccontextmanager
  28. async def lifespan(_app: FastAPI):
  29. init_db()
  30. start_scheduler()
  31. yield
  32. stop_scheduler()
  33. app = FastAPI(title="SupplyAgent API", version="0.1.0", lifespan=lifespan)
  34. app.add_middleware(
  35. CORSMiddleware,
  36. allow_origins=[
  37. "http://localhost:5173",
  38. "http://127.0.0.1:5173",
  39. "http://localhost:4173",
  40. "http://127.0.0.1:4173",
  41. ],
  42. allow_credentials=True,
  43. allow_methods=["*"],
  44. allow_headers=["*"],
  45. )
  46. @app.get("/health")
  47. def health() -> dict[str, str]:
  48. return {"status": "ok"}
  49. @app.get("/api/scheduler/status")
  50. def scheduler_status() -> dict:
  51. """Return scheduler enabled/running state and next run times."""
  52. return get_scheduler_status()
  53. class RunPipelineBody(BaseModel):
  54. biz_dt: str | None = Field(
  55. default=None,
  56. pattern=r"^\d{8}$",
  57. description="业务日 YYYYMMDD;省略则取当天",
  58. )
  59. class TriggerSchedulerJobBody(BaseModel):
  60. biz_dt: str | None = Field(
  61. default=None,
  62. pattern=r"^\d{8}$",
  63. description="业务日 YYYYMMDD;省略则取当天",
  64. )
  65. partition_date: str | None = Field(
  66. default=None,
  67. pattern=r"^\d{8}$",
  68. description="ODPS 分区日 YYYYMMDD;全局树默认取 biz_dt 前一日",
  69. )
  70. wait: bool = Field(
  71. default=False,
  72. description="是否同步等待执行完成;默认 false 表示后台执行",
  73. )
  74. @app.post("/api/pipeline/run", status_code=202)
  75. def run_pipeline(
  76. body: RunPipelineBody | None = None,
  77. biz_dt: str | None = Query(
  78. default=None,
  79. pattern=r"^\d{8}$",
  80. description="业务日 YYYYMMDD(与 body 二选一,body 优先)",
  81. ),
  82. ) -> dict:
  83. """
  84. 异步一键执行供给数据全流程,立即返回 run_id,后台串行执行:
  85. 全局树同步 → 需求池同步 → 需求分级 → 视频点位拓展 → find_agent 找片 → AIGC 发布。
  86. """
  87. resolved_biz_dt = body.biz_dt if body and body.biz_dt is not None else biz_dt
  88. return run_supply_pipeline(biz_dt=resolved_biz_dt)
  89. @app.get("/api/scheduler/jobs")
  90. def scheduler_jobs() -> dict:
  91. """Return jobs that can be triggered manually."""
  92. return {"items": list_triggerable_jobs()}
  93. @app.post("/api/scheduler/jobs/{job_id}/run")
  94. def trigger_scheduler_job(
  95. job_id: str,
  96. body: TriggerSchedulerJobBody | None = None,
  97. biz_dt: str | None = Query(
  98. default=None,
  99. pattern=r"^\d{8}$",
  100. description="业务日 YYYYMMDD(与 body 二选一,body 优先)",
  101. ),
  102. partition_date: str | None = Query(
  103. default=None,
  104. pattern=r"^\d{8}$",
  105. description="ODPS 分区日 YYYYMMDD(与 body 二选一,body 优先)",
  106. ),
  107. wait: bool = Query(
  108. default=False,
  109. description="是否同步等待执行完成",
  110. ),
  111. ) -> dict:
  112. """Manually trigger a scheduler job."""
  113. resolved_biz_dt = body.biz_dt if body and body.biz_dt is not None else biz_dt
  114. resolved_partition_date = (
  115. body.partition_date if body and body.partition_date is not None else partition_date
  116. )
  117. resolved_wait = body.wait if body is not None else wait
  118. try:
  119. return run_scheduler_job(
  120. job_id,
  121. biz_dt=resolved_biz_dt,
  122. partition_date=resolved_partition_date,
  123. wait=resolved_wait,
  124. )
  125. except KeyError:
  126. raise HTTPException(status_code=404, detail=f"scheduler job not found: {job_id}") from None
  127. @app.get("/api/scheduler/runs/{run_id}")
  128. def scheduler_job_run(run_id: str) -> dict:
  129. """Return manual job run status (in-memory; cleared on process restart)."""
  130. result = get_scheduler_job_run(run_id)
  131. if result is None:
  132. raise HTTPException(status_code=404, detail="scheduler run not found")
  133. return result
  134. @app.get("/api/category-tree")
  135. def category_tree(
  136. biz_dt: str | None = Query(
  137. default=None,
  138. description="业务日 YYYYMMDD;省略则取 category_tree_weight 最新一日",
  139. ),
  140. ) -> dict:
  141. """Return nested global_tree_category with per-dim avg score."""
  142. return build_category_tree(biz_dt=biz_dt)
  143. @app.get("/api/demand-belong-category")
  144. def demand_belong_category() -> dict:
  145. """Return all active demand_belong_category rows in one response."""
  146. items = list_demand_belong_categories()
  147. return {"items": items}
  148. @app.get("/api/demand-belong-category/{belong_id}/videos")
  149. def demand_belong_videos(belong_id: int) -> dict:
  150. """Return videos linked to a demand_belong_category row (vid + title + points JSON)."""
  151. result = list_videos_for_demand_belong(belong_id)
  152. if result is None:
  153. raise HTTPException(status_code=404, detail="demand_belong_category not found")
  154. return result
  155. @app.get("/api/demand-grade")
  156. def demand_grade(
  157. biz_dt: str | None = Query(
  158. default=None,
  159. description="业务日 YYYYMMDD;省略则取 demand_grade 最新一日",
  160. ),
  161. ) -> dict:
  162. """Return demand_grade rows (one per category_id) for the given/latest biz_dt."""
  163. items = list_demand_grades(biz_dt=biz_dt)
  164. return {"items": items}
  165. @app.get("/api/demand-grade/{demand_grade_id}/videos")
  166. def demand_grade_videos(demand_grade_id: int) -> dict:
  167. """Return expansion videos/points for a demand_grade row (by its biz_dt)."""
  168. result = list_videos_for_demand_grade(demand_grade_id)
  169. if result is None:
  170. raise HTTPException(status_code=404, detail="demand_grade not found")
  171. return result
  172. @app.get("/api/video-discovery/demands")
  173. def video_discovery_demands(
  174. biz_dt: str | None = Query(
  175. default=None,
  176. description="业务日 YYYYMMDD;省略则取 demand_grade 最新一日",
  177. ),
  178. ) -> dict:
  179. """Return demand-first video discovery cards for the selected/latest day."""
  180. return list_video_discovery_demands(biz_dt=biz_dt)
  181. @app.get("/api/video-discovery/demands/{demand_grade_id}")
  182. def video_discovery_demand(demand_grade_id: int) -> dict:
  183. """Return one demand and its videos/points resolved from the source tables."""
  184. result = get_video_discovery_demand(demand_grade_id)
  185. if result is None:
  186. raise HTTPException(status_code=404, detail="demand_grade not found")
  187. return result
  188. @app.get("/api/demand-belong-oss-logs")
  189. def demand_belong_oss_logs() -> dict:
  190. """Return demand_belong_category_agent oss_logs ordered by create_time desc."""
  191. items = list_demand_belong_oss_logs()
  192. return {"items": items}
  193. _web_dist = Path(__file__).resolve().parent.parent / "web" / "dist"
  194. if _web_dist.is_dir():
  195. app.mount("/", StaticFiles(directory=_web_dist, html=True), name="web")