routes.py 16 KB

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