api.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. from __future__ import annotations
  2. import os
  3. from fastapi import FastAPI, HTTPException, Query
  4. from fastapi.exceptions import RequestValidationError
  5. from fastapi.encoders import jsonable_encoder
  6. from fastapi.middleware.cors import CORSMiddleware
  7. from fastapi.responses import JSONResponse
  8. from content_agent.dashboard_service import DashboardService
  9. from content_agent.errors import ErrorCode, error_response, sanitize_error_detail
  10. from content_agent.run_service import RunService
  11. from content_agent.schemas import (
  12. ConfigFileResponse,
  13. ContentItemsResponse,
  14. DashboardResponse,
  15. JsonFileResponse,
  16. QueryListResponse,
  17. RecordsResponse,
  18. RunListResponse,
  19. RunStartRequest,
  20. RunStartResponse,
  21. RunSummaryResponse,
  22. RuntimeFileResponse,
  23. RuntimeFilesResponse,
  24. TimelineResponse,
  25. ValidationResponse,
  26. )
  27. app = FastAPI(title="Content Agent V1")
  28. service = RunService.from_env()
  29. _cors_origins = [
  30. origin.strip()
  31. for origin in os.environ.get(
  32. "CONTENT_AGENT_WEB_CORS_ORIGINS",
  33. "http://localhost:3000,http://127.0.0.1:3000,http://localhost:3010,http://127.0.0.1:3010",
  34. ).split(",")
  35. if origin.strip()
  36. ]
  37. if _cors_origins:
  38. app.add_middleware(
  39. CORSMiddleware,
  40. allow_origins=_cors_origins,
  41. allow_credentials=False,
  42. allow_methods=["GET", "POST", "OPTIONS"],
  43. allow_headers=["*"],
  44. )
  45. @app.exception_handler(RequestValidationError)
  46. async def validation_exception_handler(request, exc: RequestValidationError):
  47. return JSONResponse(
  48. status_code=422,
  49. content={
  50. "detail": error_response(
  51. ErrorCode.INVALID_REQUEST,
  52. "invalid request",
  53. {"errors": jsonable_encoder(sanitize_error_detail(exc.errors()))},
  54. )
  55. },
  56. )
  57. @app.post("/runs", response_model=RunStartResponse)
  58. def start_run(request: RunStartRequest) -> RunStartResponse:
  59. state = service.start_run(request)
  60. if state["status"] not in {"success", "partial_success"}:
  61. detail = error_response(
  62. state.get("error_code", ErrorCode.RUN_START_FAILED),
  63. state.get("error_message", "run failed"),
  64. state.get("error_detail", {"errors": state.get("errors", [])}),
  65. )
  66. raise HTTPException(status_code=state.get("http_status_code", 500), detail=detail)
  67. run_id = state["run_id"]
  68. return RunStartResponse(
  69. run_id=run_id,
  70. policy_run_id=state["policy_run_id"],
  71. status=state["status"],
  72. policy_bundle_id=state["policy_bundle_id"],
  73. strategy_version=state["strategy_version"],
  74. platform=state["platform"],
  75. platform_mode=state["platform_mode"],
  76. output_dir=str(service.runtime.run_dir(run_id)),
  77. )
  78. @app.get("/runs", response_model=RunListResponse)
  79. def list_runs(
  80. status: str | None = None,
  81. platform: str | None = None,
  82. platform_mode: str | None = None,
  83. strategy_version: str | None = None,
  84. validation_status: str | None = None,
  85. error_code: str | None = None,
  86. page: int = Query(default=1, ge=1),
  87. page_size: int = Query(default=20, ge=1, le=100),
  88. ) -> RunListResponse:
  89. return RunListResponse(
  90. **_dashboard_service().list_runs(
  91. status=status,
  92. platform=platform,
  93. platform_mode=platform_mode,
  94. strategy_version=strategy_version,
  95. validation_status=validation_status,
  96. error_code=error_code,
  97. page=page,
  98. page_size=page_size,
  99. )
  100. )
  101. @app.get("/runs/{run_id}", response_model=RunSummaryResponse)
  102. def get_run(run_id: str) -> RunSummaryResponse:
  103. _ensure_run_exists(run_id)
  104. return RunSummaryResponse(**service.get_summary(run_id))
  105. _CONFIG_FILES = {
  106. "rule-packs": "product_documents/规则包/douyin_rule_packs.v1.json",
  107. "walk-strategy": "product_documents/抖音游走策略/douyin_walk_strategy.v1.json",
  108. "query-prompts": "product_documents/配置/query_prompts.v1.json",
  109. }
  110. def _config_file_response(key: str) -> ConfigFileResponse:
  111. from pathlib import Path
  112. from content_agent.integrations import config_store
  113. path = Path(_CONFIG_FILES[key])
  114. if not path.exists():
  115. raise HTTPException(status_code=404, detail=error_response(
  116. ErrorCode.RUN_NOT_FOUND, f"config file missing: {key}", {"source_file": str(path)}
  117. ))
  118. data, _ = config_store.load_json(path)
  119. return ConfigFileResponse(source_file=str(path), data=data)
  120. @app.get("/config/rule-packs", response_model=ConfigFileResponse)
  121. def get_config_rule_packs() -> ConfigFileResponse:
  122. return _config_file_response("rule-packs")
  123. @app.get("/config/walk-strategy", response_model=ConfigFileResponse)
  124. def get_config_walk_strategy() -> ConfigFileResponse:
  125. return _config_file_response("walk-strategy")
  126. @app.get("/config/query-prompts", response_model=ConfigFileResponse)
  127. def get_config_query_prompts() -> ConfigFileResponse:
  128. return _config_file_response("query-prompts")
  129. @app.get("/runs/{run_id}/dashboard", response_model=DashboardResponse)
  130. def get_run_dashboard(run_id: str) -> DashboardResponse:
  131. _ensure_web_run_exists(run_id)
  132. return DashboardResponse(**_dashboard_service().dashboard(run_id))
  133. @app.get("/runs/{run_id}/queries", response_model=QueryListResponse)
  134. def get_run_queries(run_id: str) -> QueryListResponse:
  135. _ensure_web_run_exists(run_id)
  136. return QueryListResponse(**_dashboard_service().queries(run_id))
  137. @app.get("/runs/{run_id}/timeline", response_model=TimelineResponse)
  138. def get_run_timeline(run_id: str) -> TimelineResponse:
  139. _ensure_web_run_exists(run_id)
  140. return TimelineResponse(**_dashboard_service().timeline(run_id))
  141. @app.get("/runs/{run_id}/content-items", response_model=ContentItemsResponse)
  142. def get_run_content_items(run_id: str) -> ContentItemsResponse:
  143. _ensure_web_run_exists(run_id)
  144. return ContentItemsResponse(**_dashboard_service().content_items(run_id))
  145. @app.get("/runs/{run_id}/runtime-files", response_model=RuntimeFilesResponse)
  146. def get_run_runtime_files(run_id: str) -> RuntimeFilesResponse:
  147. _ensure_web_run_exists(run_id)
  148. return RuntimeFilesResponse(**_dashboard_service().runtime_files(run_id))
  149. @app.get("/runs/{run_id}/runtime-files/{filename}", response_model=RuntimeFileResponse)
  150. def get_run_runtime_file(
  151. run_id: str,
  152. filename: str,
  153. limit: int = Query(default=100, ge=1, le=500),
  154. offset: int = Query(default=0, ge=0),
  155. ) -> RuntimeFileResponse:
  156. _ensure_web_run_exists(run_id)
  157. try:
  158. return RuntimeFileResponse(
  159. **_dashboard_service().runtime_file(
  160. run_id,
  161. filename,
  162. limit=limit,
  163. offset=offset,
  164. )
  165. )
  166. except ValueError:
  167. raise HTTPException(
  168. status_code=400,
  169. detail=error_response(
  170. ErrorCode.INVALID_REQUEST,
  171. "runtime filename is not allowed",
  172. {"filename": filename},
  173. ),
  174. )
  175. @app.get("/runs/{run_id}/discovered-content-items", response_model=RecordsResponse)
  176. def get_discovered_content_items(run_id: str) -> RecordsResponse:
  177. return _jsonl_response(run_id, "discovered_content_items.jsonl")
  178. @app.get("/runs/{run_id}/rule-decisions", response_model=RecordsResponse)
  179. def get_rule_decisions(run_id: str) -> RecordsResponse:
  180. return _jsonl_response(run_id, "rule_decisions.jsonl")
  181. @app.get("/runs/{run_id}/source-path-records", response_model=RecordsResponse)
  182. def get_source_path_records(run_id: str) -> RecordsResponse:
  183. return _jsonl_response(run_id, "source_path_records.jsonl")
  184. @app.get("/runs/{run_id}/final-output", response_model=JsonFileResponse)
  185. def get_final_output(run_id: str) -> JsonFileResponse:
  186. return _json_response(run_id, "final_output.json")
  187. @app.get("/runs/{run_id}/strategy-review", response_model=JsonFileResponse)
  188. def get_strategy_review(run_id: str) -> JsonFileResponse:
  189. _ensure_run_exists(run_id)
  190. return JsonFileResponse(run_id=run_id, data=service.strategy_review(run_id))
  191. @app.post("/runs/{run_id}/strategy-review/regenerate", response_model=JsonFileResponse)
  192. def regenerate_strategy_review(run_id: str) -> JsonFileResponse:
  193. _ensure_run_exists(run_id)
  194. return JsonFileResponse(run_id=run_id, data=service.regenerate_strategy_review(run_id))
  195. @app.get("/runs/{run_id}/validation", response_model=ValidationResponse)
  196. def get_validation(run_id: str) -> ValidationResponse:
  197. _ensure_run_exists(run_id)
  198. return ValidationResponse(**service.validate_run(run_id))
  199. def _jsonl_response(run_id: str, filename: str) -> RecordsResponse:
  200. _ensure_run_exists(run_id)
  201. return RecordsResponse(run_id=run_id, records=service.read_jsonl(run_id, filename))
  202. def _json_response(run_id: str, filename: str) -> JsonFileResponse:
  203. _ensure_run_exists(run_id)
  204. return JsonFileResponse(run_id=run_id, data=service.read_json(run_id, filename))
  205. def _ensure_run_exists(run_id: str) -> None:
  206. if not service.runtime.run_dir(run_id).exists():
  207. raise HTTPException(
  208. status_code=404,
  209. detail=error_response(
  210. ErrorCode.RUN_NOT_FOUND,
  211. "run not found",
  212. {"run_id": run_id},
  213. ),
  214. )
  215. def _ensure_web_run_exists(run_id: str) -> None:
  216. if not _dashboard_service().run_exists(run_id):
  217. raise HTTPException(
  218. status_code=404,
  219. detail=error_response(
  220. ErrorCode.RUN_NOT_FOUND,
  221. "run not found",
  222. {"run_id": run_id},
  223. ),
  224. )
  225. def _dashboard_service() -> DashboardService:
  226. return DashboardService.from_runtime(service.runtime)