routes.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import json
  2. from core_domain import ServiceHealth
  3. from core_shared import error_detail
  4. from fastapi import APIRouter, Depends, HTTPException, Query
  5. from fastapi.responses import StreamingResponse
  6. from sqlalchemy import text
  7. from sqlalchemy.orm import Session
  8. from app.application.services import AgentApplicationService, build_agent_application_service
  9. from app.bootstrap.settings import AgentServiceSettings
  10. from app.db.session import get_db
  11. from app.schemas.agent import (
  12. AgentConfigCreateRequest,
  13. AgentConfigListRequest,
  14. AgentConfigResponse,
  15. AgentCreateRequest,
  16. AgentDeleteRequest,
  17. AgentDetailRequest,
  18. AgentListRequest,
  19. AgentResponse,
  20. AgentRunCreateRequest,
  21. AgentRunDetailRequest,
  22. AgentRunExecutePostRequest,
  23. AgentRunExecuteRequest,
  24. AgentRunExecuteResponse,
  25. AgentRunListRequest,
  26. AgentRunResponse,
  27. AgentRunStatusPostRequest,
  28. AgentRunStatusUpdateRequest,
  29. AgentStatusUpdateRequest,
  30. AgentStatusPostRequest,
  31. AgentToolInvocationListRequest,
  32. AgentUpdateRequest,
  33. AgentToolInvocationResponse,
  34. AgentWorkerExecuteNextRequest,
  35. AgentWorkerExecuteNextResponse,
  36. DeleteData,
  37. )
  38. router = APIRouter()
  39. def json_dump(payload: dict[str, object]) -> str:
  40. return json.dumps(payload, ensure_ascii=False, default=str)
  41. def get_agent_service_settings() -> AgentServiceSettings:
  42. return AgentServiceSettings()
  43. def get_agent_application_service(
  44. db: Session = Depends(get_db),
  45. settings: AgentServiceSettings = Depends(get_agent_service_settings)) -> AgentApplicationService:
  46. return build_agent_application_service(db=db, settings=settings)
  47. @router.get("/health", response_model=ServiceHealth)
  48. def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
  49. db.execute(text("SELECT 1"))
  50. return ServiceHealth(service="agent-service", status="ok", database="ok")
  51. @router.post("", response_model=AgentResponse)
  52. def create_agent(
  53. payload: AgentCreateRequest,
  54. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
  55. entity = service.create_agent(payload)
  56. return AgentResponse.from_entity(entity)
  57. @router.get("", response_model=list[AgentResponse])
  58. def list_agents(
  59. service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentResponse]:
  60. return [AgentResponse.from_entity(item) for item in service.list_agents()]
  61. @router.post("/list", response_model=list[AgentResponse])
  62. def list_agents_post(
  63. payload: AgentListRequest,
  64. service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentResponse]:
  65. return [AgentResponse.from_entity(item) for item in service.list_agents()]
  66. @router.post("/detail", response_model=AgentResponse)
  67. def detail_agent(
  68. payload: AgentDetailRequest,
  69. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
  70. entity = service.get_agent(agent_id=payload.agent_id)
  71. if entity is None:
  72. raise HTTPException(status_code=404, detail=error_detail("error.agent.not_found", id=payload.agent_id))
  73. return AgentResponse.from_entity(entity)
  74. @router.post("/update", response_model=AgentResponse)
  75. def update_agent(
  76. payload: AgentUpdateRequest,
  77. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
  78. entity = service.update_agent(payload)
  79. if entity is None:
  80. raise HTTPException(status_code=404, detail=error_detail("error.agent.not_found", id=payload.agent_id))
  81. return AgentResponse.from_entity(entity)
  82. @router.patch("/{agent_id}/status", response_model=AgentResponse)
  83. def update_agent_status(
  84. agent_id: str,
  85. payload: AgentStatusUpdateRequest,
  86. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
  87. entity = service.update_agent_status(agent_id=agent_id, payload=payload)
  88. if entity is None:
  89. raise HTTPException(status_code=404, detail=error_detail("error.agent.not_found", id=agent_id))
  90. return AgentResponse.from_entity(entity)
  91. @router.post("/status", response_model=AgentResponse)
  92. def update_agent_status_post(
  93. payload: AgentStatusPostRequest,
  94. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
  95. entity = service.update_agent_status(
  96. agent_id=payload.agent_id,
  97. payload=AgentStatusUpdateRequest(status=payload.status))
  98. if entity is None:
  99. raise HTTPException(status_code=404, detail=error_detail("error.agent.not_found", id=payload.agent_id))
  100. return AgentResponse.from_entity(entity)
  101. @router.post("/delete", response_model=DeleteData)
  102. def delete_agent_post(
  103. payload: AgentDeleteRequest,
  104. service: AgentApplicationService = Depends(get_agent_application_service)) -> DeleteData:
  105. return DeleteData(
  106. deleted=service.delete_agent(agent_id=payload.agent_id),
  107. agent_id=payload.agent_id)
  108. @router.post("/configs/create", response_model=AgentConfigResponse)
  109. def create_agent_config(
  110. payload: AgentConfigCreateRequest,
  111. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentConfigResponse:
  112. try:
  113. entity = service.create_agent_config(payload)
  114. except ValueError as exc:
  115. raise HTTPException(status_code=422, detail=error_detail("error.validation", message=str(exc))) from exc
  116. return AgentConfigResponse.from_entity(entity)
  117. @router.post("/configs/list", response_model=list[AgentConfigResponse])
  118. def list_agent_configs(
  119. payload: AgentConfigListRequest,
  120. service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentConfigResponse]:
  121. return [
  122. AgentConfigResponse.from_entity(item)
  123. for item in service.list_agent_configs(agent_id=payload.agent_id)
  124. ]
  125. @router.post("/runs", response_model=AgentRunResponse)
  126. def create_agent_run(
  127. payload: AgentRunCreateRequest,
  128. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunResponse:
  129. try:
  130. entity = service.create_agent_run(payload)
  131. except ValueError as exc:
  132. raise HTTPException(status_code=422, detail=error_detail("error.validation", message=str(exc))) from exc
  133. return AgentRunResponse.from_entity(entity)
  134. @router.get("/runs", response_model=list[AgentRunResponse])
  135. def list_agent_runs(
  136. agent_id: str | None = Query(default=None),
  137. session_id: str | None = Query(default=None),
  138. service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentRunResponse]:
  139. return [
  140. AgentRunResponse.from_entity(item)
  141. for item in service.list_agent_runs(
  142. agent_id=agent_id,
  143. session_id=session_id)
  144. ]
  145. @router.post("/runs/list", response_model=list[AgentRunResponse])
  146. def list_agent_runs_post(
  147. payload: AgentRunListRequest,
  148. service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentRunResponse]:
  149. return [
  150. AgentRunResponse.from_entity(item)
  151. for item in service.list_agent_runs(
  152. agent_id=payload.agent_id,
  153. session_id=payload.session_id)
  154. ]
  155. @router.post("/runs/detail", response_model=AgentRunResponse)
  156. def get_agent_run(
  157. payload: AgentRunDetailRequest,
  158. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunResponse:
  159. entity = service.get_agent_run(payload)
  160. if entity is None:
  161. raise HTTPException(status_code=404, detail=error_detail("error.agent_run.not_found", id=payload.agent_run_id))
  162. return AgentRunResponse.from_entity(entity)
  163. @router.get(
  164. "/runs/{agent_run_id}/tool-invocations",
  165. response_model=list[AgentToolInvocationResponse])
  166. def list_agent_tool_invocations(
  167. agent_run_id: str,
  168. service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentToolInvocationResponse]:
  169. return [
  170. AgentToolInvocationResponse.from_entity(item)
  171. for item in service.list_agent_tool_invocations(
  172. agent_run_id=agent_run_id)
  173. ]
  174. @router.post(
  175. "/runs/tool-invocations/list",
  176. response_model=list[AgentToolInvocationResponse])
  177. def list_agent_tool_invocations_post(
  178. payload: AgentToolInvocationListRequest,
  179. service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentToolInvocationResponse]:
  180. return [
  181. AgentToolInvocationResponse.from_entity(item)
  182. for item in service.list_agent_tool_invocations(
  183. agent_run_id=payload.agent_run_id)
  184. ]
  185. @router.post("/runs/{agent_run_id}/status", response_model=AgentRunResponse)
  186. def update_agent_run_status(
  187. agent_run_id: str,
  188. payload: AgentRunStatusUpdateRequest,
  189. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunResponse:
  190. entity = service.update_agent_run_status(agent_run_id=agent_run_id, payload=payload)
  191. if entity is None:
  192. raise HTTPException(status_code=404, detail=error_detail("error.agent_run.not_found", id=agent_run_id))
  193. return AgentRunResponse.from_entity(entity)
  194. @router.post("/runs/status", response_model=AgentRunResponse)
  195. def update_agent_run_status_post(
  196. payload: AgentRunStatusPostRequest,
  197. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunResponse:
  198. entity = service.update_agent_run_status(
  199. agent_run_id=payload.agent_run_id,
  200. payload=AgentRunStatusUpdateRequest(
  201. status=payload.status,
  202. worker_key=payload.worker_key,
  203. output_text=payload.output_text,
  204. output_json=payload.output_json,
  205. error_code=payload.error_code,
  206. error_message=payload.error_message))
  207. if entity is None:
  208. raise HTTPException(status_code=404, detail=error_detail("error.agent_run.not_found", id=payload.agent_run_id))
  209. return AgentRunResponse.from_entity(entity)
  210. @router.post("/runs/{agent_run_id}/execute", response_model=AgentRunExecuteResponse)
  211. def execute_agent_run(
  212. agent_run_id: str,
  213. payload: AgentRunExecuteRequest,
  214. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunExecuteResponse:
  215. entity = service.execute_agent_run(agent_run_id=agent_run_id, payload=payload)
  216. if entity is None:
  217. raise HTTPException(status_code=404, detail=error_detail("error.agent_run.not_found", id=agent_run_id))
  218. output_json = entity.output_json or {}
  219. model_value = output_json.get("model")
  220. dry_run_value = output_json.get("dry_run")
  221. return AgentRunExecuteResponse(
  222. run=AgentRunResponse.from_entity(entity),
  223. model=model_value if isinstance(model_value, str) else None,
  224. dry_run=dry_run_value if isinstance(dry_run_value, bool) else False)
  225. @router.post("/runs/{agent_run_id}/execute-stream")
  226. def execute_agent_run_stream(
  227. agent_run_id: str,
  228. payload: AgentRunExecuteRequest,
  229. service: AgentApplicationService = Depends(get_agent_application_service)) -> StreamingResponse:
  230. if service.get_agent_run(AgentRunDetailRequest(agent_run_id=agent_run_id)) is None:
  231. raise HTTPException(status_code=404, detail=error_detail("error.agent_run.not_found", id=agent_run_id))
  232. def events():
  233. for item in service.execute_agent_run_stream(agent_run_id=agent_run_id, payload=payload):
  234. event = item.get("event")
  235. event_name = event if isinstance(event, str) else "message"
  236. data = {key: value for key, value in item.items() if key != "event"}
  237. yield f"event: {event_name}\ndata: {json_dump(data)}\n\n"
  238. return StreamingResponse(
  239. events(),
  240. media_type="text/event-stream",
  241. headers=_sse_headers())
  242. def _sse_headers() -> dict[str, str]:
  243. return {
  244. "Cache-Control": "no-cache",
  245. "X-Accel-Buffering": "no",
  246. }
  247. @router.post("/runs/execute", response_model=AgentRunExecuteResponse)
  248. def execute_agent_run_post(
  249. payload: AgentRunExecutePostRequest,
  250. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunExecuteResponse:
  251. entity = service.execute_agent_run(
  252. agent_run_id=payload.agent_run_id,
  253. payload=AgentRunExecuteRequest(
  254. worker_key=payload.worker_key,
  255. dry_run=payload.dry_run))
  256. if entity is None:
  257. raise HTTPException(status_code=404, detail=error_detail("error.agent_run.not_found", id=payload.agent_run_id))
  258. output_json = entity.output_json or {}
  259. model_value = output_json.get("model")
  260. dry_run_value = output_json.get("dry_run")
  261. return AgentRunExecuteResponse(
  262. run=AgentRunResponse.from_entity(entity),
  263. model=model_value if isinstance(model_value, str) else None,
  264. dry_run=dry_run_value if isinstance(dry_run_value, bool) else False)
  265. @router.post("/workers/execute-next", response_model=AgentWorkerExecuteNextResponse)
  266. def execute_next_worker_task(
  267. payload: AgentWorkerExecuteNextRequest,
  268. settings: AgentServiceSettings = Depends(get_agent_service_settings),
  269. service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentWorkerExecuteNextResponse:
  270. result = service.execute_next_claimed_agent_run(
  271. worker_key=payload.worker_key,
  272. lease_seconds=payload.lease_seconds or settings.worker_lease_seconds,
  273. dry_run=payload.dry_run if payload.dry_run is not None else settings.worker_dry_run)
  274. if result is None:
  275. raise HTTPException(status_code=404, detail=error_detail("error.agent_run.not_found_queued"))
  276. entity, released_lease_count = result
  277. output_json = entity.output_json or {}
  278. model_value = output_json.get("model")
  279. dry_run_value = output_json.get("dry_run")
  280. return AgentWorkerExecuteNextResponse(
  281. run=AgentRunResponse.from_entity(entity),
  282. model=model_value if isinstance(model_value, str) else None,
  283. dry_run=dry_run_value if isinstance(dry_run_value, bool) else False,
  284. released_lease_count=released_lease_count)