routes.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. from datetime import datetime
  2. from typing import Annotated, TypeVar
  3. from core_domain import ServiceHealth
  4. from core_shared.secrets import SecretCipher
  5. from fastapi import APIRouter, Depends, HTTPException, Query, Request
  6. from sqlalchemy import text
  7. from sqlalchemy.orm import Session
  8. from app.application.services import ToolApplicationService
  9. from app.bootstrap.settings import ToolServiceSettings
  10. from app.db.session import get_db
  11. from app.domain.repositories import (
  12. ToolBindingRepository,
  13. ToolCredentialRepository,
  14. ToolDefinitionRepository,
  15. ToolVersionRepository,
  16. )
  17. from app.schemas.tool import (
  18. ApiResponse,
  19. DeleteData,
  20. McpConnectData,
  21. McpConnectRequestDto,
  22. PageRequest,
  23. PageResult,
  24. ToolBindingCreateRequest,
  25. ToolBindingCreateRequestDto,
  26. ToolBindingDeleteRequestDto,
  27. ToolBindingDetailRequestDto,
  28. ToolBindingDetailResponse,
  29. ToolBindingDto,
  30. ToolBindingListRequestDto,
  31. ToolBindingResponse,
  32. ToolBindingUpdateRequestDto,
  33. ToolCreateRequest,
  34. ToolCreateRequestDto,
  35. ToolCredentialCreateRequest,
  36. ToolCredentialCreateRequestDto,
  37. ToolCredentialDeleteRequestDto,
  38. ToolCredentialDetailRequestDto,
  39. ToolCredentialDto,
  40. ToolCredentialResponse,
  41. ToolCredentialRevealDto,
  42. ToolCredentialRevealRequestDto,
  43. ToolCredentialRevealResponse,
  44. ToolCredentialUpdateRequestDto,
  45. ToolDeleteRequestDto,
  46. ToolDetailRequestDto,
  47. ToolDto,
  48. ToolResponse,
  49. ToolUpdateRequestDto,
  50. ToolVersionCreateRequest,
  51. ToolVersionCreateRequestDto,
  52. ToolVersionDeleteRequestDto,
  53. ToolVersionDetailRequestDto,
  54. ToolVersionDto,
  55. ToolVersionListRequestDto,
  56. ToolVersionResponse,
  57. ToolVersionUpdateRequestDto,
  58. )
  59. router = APIRouter()
  60. DbSession = Annotated[Session, Depends(get_db)]
  61. T = TypeVar("T")
  62. def get_tool_application_service(
  63. request: Request,
  64. db: DbSession) -> ToolApplicationService:
  65. settings: ToolServiceSettings = request.app.state.settings
  66. return ToolApplicationService(
  67. tool_definition_repository=ToolDefinitionRepository(db),
  68. tool_version_repository=ToolVersionRepository(db),
  69. tool_binding_repository=ToolBindingRepository(db),
  70. tool_credential_repository=ToolCredentialRepository(db),
  71. secret_cipher=SecretCipher(key=settings.credential_encryption_key))
  72. ToolServiceDep = Annotated[ToolApplicationService, Depends(get_tool_application_service)]
  73. def ok(data: T) -> ApiResponse[T]:
  74. return ApiResponse[T](
  75. data=data,
  76. requestId="",
  77. serverTime=datetime.utcnow())
  78. @router.get("/health", response_model=ServiceHealth)
  79. def health_check(db: DbSession) -> ServiceHealth:
  80. db.execute(text("SELECT 1"))
  81. return ServiceHealth(service="tool-service", status="ok", database="ok")
  82. @router.post("", response_model=ToolResponse)
  83. def create_tool_definition(
  84. payload: ToolCreateRequest,
  85. service: ToolServiceDep) -> ToolResponse:
  86. entity = service.create_tool_definition(payload)
  87. return ToolResponse.from_entity(entity)
  88. @router.get("", response_model=list[ToolResponse])
  89. def list_tool_definitions(
  90. service: ToolServiceDep) -> list[ToolResponse]:
  91. return [ToolResponse.from_entity(item) for item in service.list_tool_definitions()]
  92. @router.post("/versions", response_model=ToolVersionResponse)
  93. def create_tool_version(
  94. payload: ToolVersionCreateRequest,
  95. service: ToolServiceDep) -> ToolVersionResponse:
  96. entity = service.create_tool_version(payload)
  97. return ToolVersionResponse.from_entity(entity)
  98. @router.get("/versions", response_model=list[ToolVersionResponse])
  99. def list_tool_versions(
  100. service: ToolServiceDep,
  101. tool_id: str | None = Query(default=None)) -> list[ToolVersionResponse]:
  102. return [
  103. ToolVersionResponse.from_entity(item)
  104. for item in service.list_tool_versions(tool_id=tool_id)
  105. ]
  106. @router.post("/bindings", response_model=ToolBindingResponse)
  107. def create_tool_binding(
  108. payload: ToolBindingCreateRequest,
  109. service: ToolServiceDep) -> ToolBindingResponse:
  110. try:
  111. entity = service.create_tool_binding(payload)
  112. except ValueError as exc:
  113. raise HTTPException(status_code=422, detail=str(exc)) from exc
  114. return ToolBindingResponse.from_entity(entity)
  115. @router.get("/bindings", response_model=list[ToolBindingResponse])
  116. def list_tool_bindings(
  117. service: ToolServiceDep,
  118. app_id: str | None = Query(default=None)) -> list[ToolBindingResponse]:
  119. return [
  120. ToolBindingResponse.from_entity(item)
  121. for item in service.list_tool_bindings(app_id=app_id)
  122. ]
  123. @router.get("/bindings/{binding_id}", response_model=ToolBindingDetailResponse)
  124. def get_tool_binding_detail(
  125. binding_id: str,
  126. service: ToolServiceDep) -> ToolBindingDetailResponse:
  127. result = service.get_tool_binding_detail(binding_id=binding_id)
  128. if result is None:
  129. raise HTTPException(status_code=404, detail=f"tool_binding not found: {binding_id}")
  130. binding, tool_version, tool_definition = result
  131. return ToolBindingDetailResponse(
  132. binding=ToolBindingResponse.from_entity(binding),
  133. tool_version=ToolVersionResponse.from_entity(tool_version),
  134. tool_definition=ToolResponse.from_entity(tool_definition))
  135. @router.post("/credentials", response_model=ToolCredentialResponse)
  136. def create_tool_credential(
  137. payload: ToolCredentialCreateRequest,
  138. service: ToolServiceDep) -> ToolCredentialResponse:
  139. entity = service.create_tool_credential(payload)
  140. return ToolCredentialResponse.from_entity(entity)
  141. @router.get("/credentials", response_model=list[ToolCredentialResponse])
  142. def list_tool_credentials(
  143. service: ToolServiceDep) -> list[ToolCredentialResponse]:
  144. return [
  145. ToolCredentialResponse.from_entity(item)
  146. for item in service.list_tool_credentials()
  147. ]
  148. @router.post("/credentials/{credential_id}/reveal", response_model=ToolCredentialRevealResponse)
  149. def reveal_tool_credential(
  150. credential_id: str,
  151. service: ToolServiceDep) -> ToolCredentialRevealResponse:
  152. result = service.reveal_tool_credential(
  153. credential_id=credential_id)
  154. if result is None:
  155. raise HTTPException(status_code=404, detail=f"tool credential not found: {credential_id}")
  156. credential, secret_json = result
  157. return ToolCredentialRevealResponse(
  158. credential=ToolCredentialResponse.from_entity(credential),
  159. secret_json=secret_json)
  160. @router.post("/list", response_model=ApiResponse[PageResult[ToolDto]])
  161. def list_tools_contract(
  162. payload: PageRequest,
  163. service: ToolServiceDep) -> ApiResponse[PageResult[ToolDto]]:
  164. keyword = (payload.keyword or "").lower().strip()
  165. items = [
  166. item
  167. for item in service.list_tool_definitions()
  168. if not keyword
  169. or keyword in item.name.lower()
  170. or keyword in item.tool_type.lower()
  171. or keyword in (item.description or "").lower()
  172. ]
  173. page_items = items[payload.offset:payload.offset + payload.pageSize]
  174. return ok(
  175. PageResult[ToolDto].from_items(
  176. items=[ToolDto.from_entity(item) for item in page_items],
  177. total=len(items),
  178. page=payload.page,
  179. page_size=payload.pageSize))
  180. @router.post("/create", response_model=ApiResponse[ToolDto])
  181. def create_tool_contract(
  182. payload: ToolCreateRequestDto,
  183. service: ToolServiceDep) -> ApiResponse[ToolDto]:
  184. entity = service.create_tool_definition_from_contract(payload)
  185. return ok(ToolDto.from_entity(entity))
  186. @router.post("/detail", response_model=ApiResponse[ToolDto])
  187. def get_tool_contract(
  188. payload: ToolDetailRequestDto,
  189. service: ToolServiceDep) -> ApiResponse[ToolDto]:
  190. entity = service.get_tool_definition_from_contract(payload)
  191. if entity is None:
  192. raise HTTPException(status_code=404, detail=f"tool not found: {payload.toolId}")
  193. return ok(ToolDto.from_entity(entity))
  194. @router.post("/update", response_model=ApiResponse[ToolDto])
  195. def update_tool_contract(
  196. payload: ToolUpdateRequestDto,
  197. service: ToolServiceDep) -> ApiResponse[ToolDto]:
  198. entity = service.update_tool_definition_from_contract(payload)
  199. if entity is None:
  200. raise HTTPException(status_code=404, detail=f"tool not found: {payload.toolId}")
  201. return ok(ToolDto.from_entity(entity))
  202. @router.post("/delete", response_model=ApiResponse[DeleteData])
  203. def delete_tool_contract(
  204. payload: ToolDeleteRequestDto,
  205. service: ToolServiceDep) -> ApiResponse[DeleteData]:
  206. deleted = service.delete_tool_definition_from_contract(payload)
  207. return ok(DeleteData(deleted=deleted, toolId=payload.toolId))
  208. @router.post("/connections/list", response_model=ApiResponse[PageResult[ToolVersionDto]])
  209. def list_tool_connections_contract(
  210. payload: ToolVersionListRequestDto,
  211. service: ToolServiceDep) -> ApiResponse[PageResult[ToolVersionDto]]:
  212. items = service.list_tool_versions(tool_id=payload.toolId)
  213. page_items = items[payload.offset:payload.offset + payload.pageSize]
  214. return ok(
  215. PageResult[ToolVersionDto].from_items(
  216. items=[ToolVersionDto.from_entity(item) for item in page_items],
  217. total=len(items),
  218. page=payload.page,
  219. page_size=payload.pageSize))
  220. @router.post("/connections/create", response_model=ApiResponse[ToolVersionDto])
  221. def create_tool_connection_contract(
  222. payload: ToolVersionCreateRequestDto,
  223. service: ToolServiceDep) -> ApiResponse[ToolVersionDto]:
  224. entity = service.create_tool_version_from_contract(payload)
  225. return ok(ToolVersionDto.from_entity(entity))
  226. @router.post("/connections/detail", response_model=ApiResponse[ToolVersionDto])
  227. def get_tool_connection_contract(
  228. payload: ToolVersionDetailRequestDto,
  229. service: ToolServiceDep) -> ApiResponse[ToolVersionDto]:
  230. entity = service.get_tool_version_from_contract(payload)
  231. if entity is None:
  232. raise HTTPException(
  233. status_code=404,
  234. detail=f"tool connection not found: {payload.connectionId}")
  235. return ok(ToolVersionDto.from_entity(entity))
  236. @router.post("/connections/update", response_model=ApiResponse[ToolVersionDto])
  237. def update_tool_connection_contract(
  238. payload: ToolVersionUpdateRequestDto,
  239. service: ToolServiceDep) -> ApiResponse[ToolVersionDto]:
  240. entity = service.update_tool_version_from_contract(payload)
  241. if entity is None:
  242. raise HTTPException(
  243. status_code=404,
  244. detail=f"tool connection not found: {payload.connectionId}")
  245. return ok(ToolVersionDto.from_entity(entity))
  246. @router.post("/connections/delete", response_model=ApiResponse[DeleteData])
  247. def delete_tool_connection_contract(
  248. payload: ToolVersionDeleteRequestDto,
  249. service: ToolServiceDep) -> ApiResponse[DeleteData]:
  250. connection_id = payload.connectionId
  251. deleted = service.delete_tool_version(connection_id=connection_id)
  252. return ok(DeleteData(deleted=deleted, connectionId=connection_id))
  253. @router.post("/mcp/connect", response_model=ApiResponse[McpConnectData])
  254. def connect_mcp_server_contract(
  255. payload: McpConnectRequestDto,
  256. service: ToolServiceDep) -> ApiResponse[McpConnectData]:
  257. return ok(service.connect_mcp_server(payload))
  258. @router.post("/bindings/list", response_model=ApiResponse[PageResult[ToolBindingDto]])
  259. def list_tool_bindings_contract(
  260. payload: ToolBindingListRequestDto,
  261. service: ToolServiceDep) -> ApiResponse[PageResult[ToolBindingDto]]:
  262. items = service.list_tool_bindings(app_id=payload.appId)
  263. page_items = items[payload.offset:payload.offset + payload.pageSize]
  264. return ok(
  265. PageResult[ToolBindingDto].from_items(
  266. items=[ToolBindingDto.from_entity(item) for item in page_items],
  267. total=len(items),
  268. page=payload.page,
  269. page_size=payload.pageSize))
  270. @router.post("/bindings/create", response_model=ApiResponse[ToolBindingDto])
  271. def create_tool_binding_contract(
  272. payload: ToolBindingCreateRequestDto,
  273. service: ToolServiceDep) -> ApiResponse[ToolBindingDto]:
  274. try:
  275. entity = service.create_tool_binding_from_contract(payload)
  276. except ValueError as exc:
  277. raise HTTPException(status_code=422, detail=str(exc)) from exc
  278. return ok(ToolBindingDto.from_entity(entity))
  279. @router.post("/bindings/detail", response_model=ApiResponse[ToolBindingDto])
  280. def get_tool_binding_contract(
  281. payload: ToolBindingDetailRequestDto,
  282. service: ToolServiceDep) -> ApiResponse[ToolBindingDto]:
  283. entity = service.get_tool_binding_from_contract(payload)
  284. if entity is None:
  285. raise HTTPException(
  286. status_code=404,
  287. detail=f"tool binding not found: {payload.bindingId}")
  288. return ok(ToolBindingDto.from_entity(entity))
  289. @router.post("/bindings/update", response_model=ApiResponse[ToolBindingDto])
  290. def update_tool_binding_contract(
  291. payload: ToolBindingUpdateRequestDto,
  292. service: ToolServiceDep) -> ApiResponse[ToolBindingDto]:
  293. entity = service.update_tool_binding_from_contract(payload)
  294. if entity is None:
  295. raise HTTPException(
  296. status_code=404,
  297. detail=f"tool binding not found: {payload.bindingId}")
  298. return ok(ToolBindingDto.from_entity(entity))
  299. @router.post("/bindings/delete", response_model=ApiResponse[DeleteData])
  300. def delete_tool_binding_contract(
  301. payload: ToolBindingDeleteRequestDto,
  302. service: ToolServiceDep) -> ApiResponse[DeleteData]:
  303. deleted = service.delete_tool_binding(payload)
  304. return ok(DeleteData(deleted=deleted, bindingId=payload.bindingId))
  305. @router.post("/credentials/list", response_model=ApiResponse[PageResult[ToolCredentialDto]])
  306. def list_tool_credentials_contract(
  307. payload: PageRequest,
  308. service: ToolServiceDep) -> ApiResponse[PageResult[ToolCredentialDto]]:
  309. keyword = (payload.keyword or "").lower().strip()
  310. items = [
  311. item
  312. for item in service.list_tool_credentials()
  313. if not keyword
  314. or keyword in item.name.lower()
  315. or keyword in item.credential_type.lower()
  316. ]
  317. page_items = items[payload.offset:payload.offset + payload.pageSize]
  318. return ok(
  319. PageResult[ToolCredentialDto].from_items(
  320. items=[ToolCredentialDto.from_entity(item) for item in page_items],
  321. total=len(items),
  322. page=payload.page,
  323. page_size=payload.pageSize))
  324. @router.post("/credentials/create", response_model=ApiResponse[ToolCredentialDto])
  325. def create_tool_credential_contract(
  326. payload: ToolCredentialCreateRequestDto,
  327. service: ToolServiceDep) -> ApiResponse[ToolCredentialDto]:
  328. entity = service.create_tool_credential_from_contract(payload)
  329. return ok(ToolCredentialDto.from_entity(entity))
  330. @router.post("/credentials/detail", response_model=ApiResponse[ToolCredentialDto])
  331. def get_tool_credential_contract(
  332. payload: ToolCredentialDetailRequestDto,
  333. service: ToolServiceDep) -> ApiResponse[ToolCredentialDto]:
  334. entity = service.get_tool_credential_from_contract(payload)
  335. if entity is None:
  336. raise HTTPException(
  337. status_code=404,
  338. detail=f"tool credential not found: {payload.credentialId}")
  339. return ok(ToolCredentialDto.from_entity(entity))
  340. @router.post("/credentials/update", response_model=ApiResponse[ToolCredentialDto])
  341. def update_tool_credential_contract(
  342. payload: ToolCredentialUpdateRequestDto,
  343. service: ToolServiceDep) -> ApiResponse[ToolCredentialDto]:
  344. entity = service.update_tool_credential_from_contract(payload)
  345. if entity is None:
  346. raise HTTPException(
  347. status_code=404,
  348. detail=f"tool credential not found: {payload.credentialId}")
  349. return ok(ToolCredentialDto.from_entity(entity))
  350. @router.post("/credentials/delete", response_model=ApiResponse[DeleteData])
  351. def delete_tool_credential_contract(
  352. payload: ToolCredentialDeleteRequestDto,
  353. service: ToolServiceDep) -> ApiResponse[DeleteData]:
  354. deleted = service.delete_tool_credential(payload)
  355. return ok(DeleteData(deleted=deleted, credentialId=payload.credentialId))
  356. @router.post("/credentials/reveal", response_model=ApiResponse[ToolCredentialRevealDto])
  357. def reveal_tool_credential_contract(
  358. payload: ToolCredentialRevealRequestDto,
  359. service: ToolServiceDep) -> ApiResponse[ToolCredentialRevealDto]:
  360. result = service.reveal_tool_credential_from_contract(
  361. credential_id=payload.credentialId)
  362. if result is None:
  363. raise HTTPException(
  364. status_code=404,
  365. detail=f"tool credential not found: {payload.credentialId}")
  366. return ok(result)