routes.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. import asyncio
  2. from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
  3. from sqlalchemy import text
  4. from sqlalchemy.orm import Session
  5. from core_domain import ServiceDescriptor, ServiceHealth
  6. from app.bootstrap.settings import ApiGatewaySettings
  7. from app.db.session import get_db
  8. from app.domain.repositories import ApiKeyRepository, GatewayRequestAuditRepository
  9. from app.infrastructure.api_keys import generate_api_key, get_api_key_prefix, hash_api_key
  10. from app.infrastructure.proxy import ProxyServiceName, ProxyTarget, ServiceProxy
  11. from app.schemas.gateway import (
  12. ApiKeyCreateRequest,
  13. ApiKeyCreateResponse,
  14. ApiKeyResponse,
  15. ApiKeyStatusUpdateRequest,
  16. GatewayRequestAuditResponse,
  17. GatewayAuditServiceStats,
  18. GatewayAuditStatsResponse,
  19. GatewayServicesHealthResponse,
  20. )
  21. router = APIRouter()
  22. @router.get("/health", response_model=ServiceDescriptor)
  23. def health_check(db: Session = Depends(get_db)) -> ServiceDescriptor:
  24. db.execute(text("SELECT 1"))
  25. return ServiceDescriptor(name="api-gateway")
  26. @router.get("/ready", response_model=ServiceHealth)
  27. def readiness_check(db: Session = Depends(get_db)) -> ServiceHealth:
  28. db.execute(text("SELECT 1"))
  29. return ServiceHealth(service="api-gateway", status="ok", database="ok")
  30. @router.post("/gateway/api-keys", response_model=ApiKeyCreateResponse)
  31. def create_api_key(
  32. payload: ApiKeyCreateRequest,
  33. db: Session = Depends(get_db),
  34. ) -> ApiKeyCreateResponse:
  35. api_key = generate_api_key()
  36. entity = ApiKeyRepository(db).create(
  37. tenant_id=payload.tenant_id,
  38. name=payload.name,
  39. key_prefix=get_api_key_prefix(api_key),
  40. key_hash=hash_api_key(api_key),
  41. scopes=payload.scopes,
  42. expires_time=payload.expires_time,
  43. )
  44. return ApiKeyCreateResponse(
  45. id=entity.id,
  46. tenant_id=entity.tenant_id,
  47. name=entity.name,
  48. key_prefix=entity.key_prefix,
  49. api_key=api_key,
  50. status=entity.status,
  51. scopes=entity.scopes,
  52. expires_time=entity.expires_time,
  53. created_time=entity.created_time,
  54. )
  55. @router.get("/gateway/api-keys", response_model=list[ApiKeyResponse])
  56. def list_api_keys(
  57. tenant_id: str = Query(...),
  58. db: Session = Depends(get_db),
  59. ) -> list[ApiKeyResponse]:
  60. return [
  61. ApiKeyResponse.from_entity(item)
  62. for item in ApiKeyRepository(db).list_by_tenant(tenant_id=tenant_id)
  63. ]
  64. @router.patch("/gateway/api-keys/{api_key_id}/status", response_model=ApiKeyResponse)
  65. def update_api_key_status(
  66. api_key_id: str,
  67. payload: ApiKeyStatusUpdateRequest,
  68. db: Session = Depends(get_db),
  69. ) -> ApiKeyResponse:
  70. entity = ApiKeyRepository(db).update_status(
  71. tenant_id=payload.tenant_id,
  72. api_key_id=api_key_id,
  73. status=payload.status,
  74. )
  75. if entity is None:
  76. raise HTTPException(status_code=404, detail=f"api key not found: {api_key_id}")
  77. return ApiKeyResponse.from_entity(entity)
  78. @router.get("/gateway/audits", response_model=list[GatewayRequestAuditResponse])
  79. def list_gateway_audits(
  80. tenant_id: str = Query(...),
  81. request_id: str | None = Query(default=None),
  82. target_service: str | None = Query(default=None),
  83. limit: int = Query(default=100, ge=1, le=500),
  84. db: Session = Depends(get_db),
  85. ) -> list[GatewayRequestAuditResponse]:
  86. items = GatewayRequestAuditRepository(db).list_by_scope(
  87. tenant_id=tenant_id,
  88. request_id=request_id,
  89. target_service=target_service,
  90. limit=limit,
  91. )
  92. return [GatewayRequestAuditResponse.from_entity(item) for item in items]
  93. @router.get("/gateway/audits/stats", response_model=GatewayAuditStatsResponse)
  94. def gateway_audit_stats(
  95. tenant_id: str = Query(...),
  96. db: Session = Depends(get_db),
  97. ) -> GatewayAuditStatsResponse:
  98. rows = GatewayRequestAuditRepository(db).stats_by_service(tenant_id=tenant_id)
  99. services = [
  100. GatewayAuditServiceStats(
  101. target_service=target_service,
  102. request_count=request_count,
  103. error_count=error_count,
  104. average_duration_ms=round(average_duration_ms, 2),
  105. )
  106. for target_service, request_count, error_count, average_duration_ms in rows
  107. ]
  108. return GatewayAuditStatsResponse(
  109. tenant_id=tenant_id,
  110. total_request_count=sum(item.request_count for item in services),
  111. total_error_count=sum(item.error_count for item in services),
  112. services=services,
  113. )
  114. def get_gateway_settings() -> ApiGatewaySettings:
  115. return ApiGatewaySettings()
  116. def get_service_proxy(settings: ApiGatewaySettings = Depends(get_gateway_settings)) -> ServiceProxy:
  117. return ServiceProxy(timeout_seconds=settings.proxy_timeout_seconds)
  118. def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName, ProxyTarget]:
  119. return {
  120. "workflow-service": ProxyTarget(
  121. service_name="workflow-service",
  122. base_url=settings.workflow_service_url,
  123. path_prefix="/workflows",
  124. health_path="/workflows/health",
  125. ),
  126. "session-service": ProxyTarget(
  127. service_name="session-service",
  128. base_url=settings.session_service_url,
  129. path_prefix="/sessions",
  130. health_path="/sessions/health",
  131. ),
  132. "runtime-service": ProxyTarget(
  133. service_name="runtime-service",
  134. base_url=settings.runtime_service_url,
  135. path_prefix="/runtime",
  136. health_path="/runtime/health",
  137. ),
  138. "tool-service": ProxyTarget(
  139. service_name="tool-service",
  140. base_url=settings.tool_service_url,
  141. path_prefix="/tools",
  142. health_path="/tools/health",
  143. ),
  144. "model-gateway-service": ProxyTarget(
  145. service_name="model-gateway-service",
  146. base_url=settings.model_gateway_service_url,
  147. path_prefix="/models",
  148. health_path="/models/health",
  149. ),
  150. "code-runner-service": ProxyTarget(
  151. service_name="code-runner-service",
  152. base_url=settings.code_runner_service_url,
  153. path_prefix="/code",
  154. health_path="/code/health",
  155. ),
  156. "agent-service": ProxyTarget(
  157. service_name="agent-service",
  158. base_url=settings.agent_service_url,
  159. path_prefix="/agents",
  160. health_path="/agents/health",
  161. ),
  162. "memory-service": ProxyTarget(
  163. service_name="memory-service",
  164. base_url=settings.memory_service_url,
  165. path_prefix="/memories",
  166. health_path="/memories/health",
  167. ),
  168. "team-service": ProxyTarget(
  169. service_name="team-service",
  170. base_url=settings.team_service_url,
  171. path_prefix="/teams",
  172. health_path="/teams/health",
  173. ),
  174. "skill-service": ProxyTarget(
  175. service_name="skill-service",
  176. base_url=settings.skill_service_url,
  177. path_prefix="/skills",
  178. health_path="/skills/health",
  179. ),
  180. "human-service": ProxyTarget(
  181. service_name="human-service",
  182. base_url=settings.human_service_url,
  183. path_prefix="/human",
  184. health_path="/human/health",
  185. ),
  186. "knowledge-service": ProxyTarget(
  187. service_name="knowledge-service",
  188. base_url=settings.knowledge_service_url,
  189. path_prefix="/knowledge",
  190. health_path="/knowledge/health",
  191. ),
  192. "event-service": ProxyTarget(
  193. service_name="event-service",
  194. base_url=settings.event_service_url,
  195. path_prefix="/events",
  196. health_path="/events/health",
  197. ),
  198. "auth-service": ProxyTarget(
  199. service_name="auth-service",
  200. base_url=settings.auth_service_url,
  201. path_prefix="/auth",
  202. health_path="/auth/health",
  203. ),
  204. "scheduler-service": ProxyTarget(
  205. service_name="scheduler-service",
  206. base_url=settings.scheduler_service_url,
  207. path_prefix="/scheduler",
  208. health_path="/scheduler/health",
  209. ),
  210. }
  211. @router.get("/gateway/services/health", response_model=GatewayServicesHealthResponse)
  212. async def downstream_health_check(
  213. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  214. ) -> GatewayServicesHealthResponse:
  215. targets = build_proxy_targets(settings)
  216. health_proxy = ServiceProxy(timeout_seconds=settings.downstream_health_timeout_seconds)
  217. downstream_services = await asyncio.gather(
  218. *[health_proxy.check_health(target) for target in targets.values()]
  219. )
  220. status = "ok" if all(item.status == "ok" for item in downstream_services) else "degraded"
  221. return GatewayServicesHealthResponse(
  222. status=status,
  223. downstream_services=downstream_services,
  224. )
  225. @router.api_route(
  226. "/gateway/workflows",
  227. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  228. )
  229. @router.api_route(
  230. "/gateway/workflows/{path:path}",
  231. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  232. )
  233. async def proxy_workflow_service(
  234. request: Request,
  235. path: str = "",
  236. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  237. proxy: ServiceProxy = Depends(get_service_proxy),
  238. ) -> Response:
  239. return await proxy.forward(
  240. request=request,
  241. target=build_proxy_targets(settings)["workflow-service"],
  242. path=path,
  243. )
  244. @router.api_route(
  245. "/gateway/sessions",
  246. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  247. )
  248. @router.api_route(
  249. "/gateway/sessions/{path:path}",
  250. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  251. )
  252. async def proxy_session_service(
  253. request: Request,
  254. path: str = "",
  255. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  256. proxy: ServiceProxy = Depends(get_service_proxy),
  257. ) -> Response:
  258. return await proxy.forward(
  259. request=request,
  260. target=build_proxy_targets(settings)["session-service"],
  261. path=path,
  262. )
  263. @router.api_route(
  264. "/gateway/runtime",
  265. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  266. )
  267. @router.api_route(
  268. "/gateway/runtime/{path:path}",
  269. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  270. )
  271. async def proxy_runtime_service(
  272. request: Request,
  273. path: str = "",
  274. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  275. proxy: ServiceProxy = Depends(get_service_proxy),
  276. ) -> Response:
  277. return await proxy.forward(
  278. request=request,
  279. target=build_proxy_targets(settings)["runtime-service"],
  280. path=path,
  281. )
  282. @router.api_route(
  283. "/gateway/agents",
  284. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  285. )
  286. @router.api_route(
  287. "/gateway/agents/{path:path}",
  288. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  289. )
  290. async def proxy_agent_service(
  291. request: Request,
  292. path: str = "",
  293. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  294. proxy: ServiceProxy = Depends(get_service_proxy),
  295. ) -> Response:
  296. return await proxy.forward(
  297. request=request,
  298. target=build_proxy_targets(settings)["agent-service"],
  299. path=path,
  300. )
  301. @router.api_route(
  302. "/gateway/memories",
  303. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  304. )
  305. @router.api_route(
  306. "/gateway/memories/{path:path}",
  307. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  308. )
  309. async def proxy_memory_service(
  310. request: Request,
  311. path: str = "",
  312. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  313. proxy: ServiceProxy = Depends(get_service_proxy),
  314. ) -> Response:
  315. return await proxy.forward(
  316. request=request,
  317. target=build_proxy_targets(settings)["memory-service"],
  318. path=path,
  319. )
  320. @router.api_route(
  321. "/gateway/teams",
  322. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  323. )
  324. @router.api_route(
  325. "/gateway/teams/{path:path}",
  326. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  327. )
  328. async def proxy_team_service(
  329. request: Request,
  330. path: str = "",
  331. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  332. proxy: ServiceProxy = Depends(get_service_proxy),
  333. ) -> Response:
  334. return await proxy.forward(
  335. request=request,
  336. target=build_proxy_targets(settings)["team-service"],
  337. path=path,
  338. )
  339. @router.api_route(
  340. "/gateway/skills",
  341. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  342. )
  343. @router.api_route(
  344. "/gateway/skills/{path:path}",
  345. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  346. )
  347. async def proxy_skill_service(
  348. request: Request,
  349. path: str = "",
  350. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  351. proxy: ServiceProxy = Depends(get_service_proxy),
  352. ) -> Response:
  353. return await proxy.forward(
  354. request=request,
  355. target=build_proxy_targets(settings)["skill-service"],
  356. path=path,
  357. )
  358. @router.api_route(
  359. "/gateway/human",
  360. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  361. )
  362. @router.api_route(
  363. "/gateway/human/{path:path}",
  364. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  365. )
  366. async def proxy_human_service(
  367. request: Request,
  368. path: str = "",
  369. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  370. proxy: ServiceProxy = Depends(get_service_proxy),
  371. ) -> Response:
  372. return await proxy.forward(
  373. request=request,
  374. target=build_proxy_targets(settings)["human-service"],
  375. path=path,
  376. )
  377. @router.api_route(
  378. "/gateway/knowledge",
  379. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  380. )
  381. @router.api_route(
  382. "/gateway/knowledge/{path:path}",
  383. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  384. )
  385. async def proxy_knowledge_service(
  386. request: Request,
  387. path: str = "",
  388. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  389. proxy: ServiceProxy = Depends(get_service_proxy),
  390. ) -> Response:
  391. return await proxy.forward(
  392. request=request,
  393. target=build_proxy_targets(settings)["knowledge-service"],
  394. path=path,
  395. )
  396. @router.api_route(
  397. "/gateway/events",
  398. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  399. )
  400. @router.api_route(
  401. "/gateway/events/{path:path}",
  402. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  403. )
  404. async def proxy_event_service(
  405. request: Request,
  406. path: str = "",
  407. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  408. proxy: ServiceProxy = Depends(get_service_proxy),
  409. ) -> Response:
  410. return await proxy.forward(
  411. request=request,
  412. target=build_proxy_targets(settings)["event-service"],
  413. path=path,
  414. )
  415. @router.api_route(
  416. "/gateway/auth",
  417. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  418. )
  419. @router.api_route(
  420. "/gateway/auth/{path:path}",
  421. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  422. )
  423. async def proxy_auth_service(
  424. request: Request,
  425. path: str = "",
  426. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  427. proxy: ServiceProxy = Depends(get_service_proxy),
  428. ) -> Response:
  429. return await proxy.forward(
  430. request=request,
  431. target=build_proxy_targets(settings)["auth-service"],
  432. path=path,
  433. )
  434. @router.api_route(
  435. "/gateway/scheduler",
  436. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  437. )
  438. @router.api_route(
  439. "/gateway/scheduler/{path:path}",
  440. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  441. )
  442. async def proxy_scheduler_service(
  443. request: Request,
  444. path: str = "",
  445. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  446. proxy: ServiceProxy = Depends(get_service_proxy),
  447. ) -> Response:
  448. return await proxy.forward(
  449. request=request,
  450. target=build_proxy_targets(settings)["scheduler-service"],
  451. path=path,
  452. )
  453. @router.api_route(
  454. "/gateway/tools",
  455. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  456. )
  457. @router.api_route(
  458. "/gateway/tools/{path:path}",
  459. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  460. )
  461. async def proxy_tool_service(
  462. request: Request,
  463. path: str = "",
  464. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  465. proxy: ServiceProxy = Depends(get_service_proxy),
  466. ) -> Response:
  467. return await proxy.forward(
  468. request=request,
  469. target=build_proxy_targets(settings)["tool-service"],
  470. path=path,
  471. )
  472. @router.api_route(
  473. "/gateway/models",
  474. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  475. )
  476. @router.api_route(
  477. "/gateway/models/{path:path}",
  478. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  479. )
  480. async def proxy_model_gateway_service(
  481. request: Request,
  482. path: str = "",
  483. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  484. proxy: ServiceProxy = Depends(get_service_proxy),
  485. ) -> Response:
  486. return await proxy.forward(
  487. request=request,
  488. target=build_proxy_targets(settings)["model-gateway-service"],
  489. path=path,
  490. )
  491. @router.api_route(
  492. "/gateway/code",
  493. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  494. )
  495. @router.api_route(
  496. "/gateway/code/{path:path}",
  497. methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
  498. )
  499. async def proxy_code_runner_service(
  500. request: Request,
  501. path: str = "",
  502. settings: ApiGatewaySettings = Depends(get_gateway_settings),
  503. proxy: ServiceProxy = Depends(get_service_proxy),
  504. ) -> Response:
  505. return await proxy.forward(
  506. request=request,
  507. target=build_proxy_targets(settings)["code-runner-service"],
  508. path=path,
  509. )