routes.py 16 KB

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