services.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. from string import Template
  2. from core_shared import JSONValue
  3. from app.db.models import SkillDefinition, SkillInstallation, SkillRun
  4. from app.domain.repositories import (
  5. SkillDefinitionRepository,
  6. SkillInstallationRepository,
  7. SkillRunRepository)
  8. from app.schemas.skill import (
  9. SkillCreateRequest,
  10. SkillCreateRequestDto,
  11. SkillDeleteRequestDto,
  12. SkillInstallRequest,
  13. SkillInstallRequestDto,
  14. SkillInstallationListRequestDto,
  15. SkillInstallationStatusRequestDto,
  16. SkillInstallationStatusUpdateRequest,
  17. SkillListRequestDto,
  18. SkillRunCreateRequest,
  19. SkillRunCreateRequestDto,
  20. SkillRunExecuteRequest,
  21. SkillRunExecuteRequestDto,
  22. SkillStatusRequestDto,
  23. SkillStatusUpdateRequest,
  24. SkillUpdateRequestDto)
  25. class SkillApplicationService:
  26. def __init__(
  27. self,
  28. *,
  29. skill_repository: SkillDefinitionRepository,
  30. installation_repository: SkillInstallationRepository,
  31. skill_run_repository: SkillRunRepository) -> None:
  32. self.skill_repository = skill_repository
  33. self.installation_repository = installation_repository
  34. self.skill_run_repository = skill_run_repository
  35. def create_skill(self, payload: SkillCreateRequest) -> SkillDefinition:
  36. return self.skill_repository.create(
  37. code=payload.code,
  38. name=payload.name,
  39. skill_type=payload.skill_type,
  40. description=payload.description,
  41. owner_user_id=payload.owner_user_id,
  42. runtime_type=payload.runtime_type,
  43. entrypoint=payload.entrypoint,
  44. parameter_schema_json=payload.parameter_schema_json,
  45. output_schema_json=payload.output_schema_json,
  46. implementation_json=payload.implementation_json,
  47. metadata_json=payload.metadata_json)
  48. def create_skill_from_contract(self, payload: SkillCreateRequestDto) -> SkillDefinition:
  49. metadata = {
  50. **payload.metadata,
  51. "category": payload.category,
  52. "instruction": payload.instruction,
  53. "toolIds": payload.toolIds,
  54. }
  55. skill = self.create_skill(SkillCreateRequest(
  56. code=self._build_skill_code(payload.name),
  57. name=payload.name,
  58. skill_type=payload.skillType,
  59. description=payload.description,
  60. runtime_type=payload.runtimeType,
  61. entrypoint=payload.entrypoint,
  62. parameter_schema_json=payload.parameterSchema,
  63. output_schema_json=payload.outputSchema,
  64. implementation_json=payload.implementation
  65. or {"template": payload.instruction or payload.description or payload.name},
  66. owner_user_id=payload.ownerUserId,
  67. metadata_json=metadata))
  68. return skill
  69. def list_skills(self) -> list[SkillDefinition]:
  70. return self.skill_repository.list_all()
  71. def list_skills_contract(self, payload: SkillListRequestDto) -> tuple[list[SkillDefinition], int]:
  72. return self.skill_repository.list_filtered(
  73. keyword=payload.keyword,
  74. status=payload.status,
  75. skill_type=payload.skillType,
  76. category=payload.category,
  77. offset=payload.offset,
  78. limit=payload.pageSize)
  79. def update_skill_status(
  80. self,
  81. *,
  82. skill_id: str,
  83. payload: SkillStatusUpdateRequest) -> SkillDefinition | None:
  84. return self.skill_repository.update_status(
  85. skill_id=skill_id,
  86. status=payload.status)
  87. def update_skill_status_contract(self, payload: SkillStatusRequestDto) -> SkillDefinition | None:
  88. return self.update_skill_status(
  89. skill_id=payload.skillId,
  90. payload=SkillStatusUpdateRequest(status=payload.status))
  91. def update_skill(self, payload: SkillUpdateRequestDto) -> SkillDefinition | None:
  92. current = self.skill_repository.get_by_id(skill_id=payload.skillId)
  93. if current is None:
  94. return None
  95. metadata = dict(current.metadata_json or {})
  96. if payload.metadata is not None:
  97. metadata.update(payload.metadata)
  98. if payload.category is not None:
  99. metadata["category"] = payload.category
  100. if payload.instruction is not None:
  101. metadata["instruction"] = payload.instruction
  102. if payload.toolIds is not None:
  103. metadata["toolIds"] = payload.toolIds
  104. implementation_json = payload.implementation
  105. if implementation_json is None and payload.instruction is not None:
  106. implementation_json = {"template": payload.instruction}
  107. return self.skill_repository.update(
  108. skill_id=payload.skillId,
  109. name=payload.name,
  110. skill_type=payload.skillType,
  111. description=payload.description,
  112. owner_user_id=payload.ownerUserId,
  113. runtime_type=payload.runtimeType,
  114. entrypoint=payload.entrypoint,
  115. parameter_schema_json=payload.parameterSchema,
  116. output_schema_json=payload.outputSchema,
  117. implementation_json=implementation_json,
  118. metadata_json=metadata)
  119. def delete_skill(self, payload: SkillDeleteRequestDto) -> SkillDefinition | None:
  120. return self.skill_repository.update_status(skill_id=payload.skillId, status="archived")
  121. def install_skill(self, payload: SkillInstallRequest) -> SkillInstallation:
  122. skill = self.skill_repository.get_by_id(skill_id=payload.skill_id)
  123. if skill is None:
  124. raise ValueError("skill not found")
  125. return self.installation_repository.create(
  126. skill_id=payload.skill_id,
  127. install_scope=payload.install_scope,
  128. scope_id=payload.scope_id,
  129. config_json=payload.config_json,
  130. installed_by=payload.installed_by)
  131. def install_skill_from_contract(self, payload: SkillInstallRequestDto) -> SkillInstallation:
  132. return self.install_skill(SkillInstallRequest(
  133. skill_id=payload.skillId,
  134. install_scope=payload.installScope,
  135. scope_id=payload.scopeId,
  136. config_json=payload.config,
  137. installed_by=payload.installedBy))
  138. def list_installations(
  139. self,
  140. *,
  141. install_scope: str | None = None,
  142. scope_id: str | None = None) -> list[SkillInstallation]:
  143. return self.installation_repository.list_by_scope(
  144. install_scope=install_scope,
  145. scope_id=scope_id)
  146. def list_installations_contract(
  147. self,
  148. payload: SkillInstallationListRequestDto) -> tuple[list[SkillInstallation], int]:
  149. return self.installation_repository.list_filtered(
  150. install_scope=payload.installScope,
  151. scope_id=payload.scopeId,
  152. status=payload.status,
  153. offset=payload.offset,
  154. limit=payload.pageSize)
  155. def update_installation_status(
  156. self,
  157. *,
  158. installation_id: str,
  159. payload: SkillInstallationStatusUpdateRequest) -> SkillInstallation | None:
  160. return self.installation_repository.update_status(
  161. installation_id=installation_id,
  162. status=payload.status)
  163. def update_installation_status_contract(
  164. self,
  165. payload: SkillInstallationStatusRequestDto) -> SkillInstallation | None:
  166. return self.update_installation_status(
  167. installation_id=payload.installationId,
  168. payload=SkillInstallationStatusUpdateRequest(status=payload.status))
  169. def create_skill_run(self, payload: SkillRunCreateRequest) -> SkillRun:
  170. skill = self.skill_repository.get_by_id(skill_id=payload.skill_id)
  171. if skill is None:
  172. raise ValueError("skill not found")
  173. return self.skill_run_repository.create(
  174. skill_id=payload.skill_id,
  175. installation_id=payload.installation_id,
  176. input_json=payload.input_json)
  177. def create_skill_run_from_contract(self, payload: SkillRunCreateRequestDto) -> SkillRun:
  178. return self.create_skill_run(SkillRunCreateRequest(
  179. skill_id=payload.skillId,
  180. installation_id=payload.installationId,
  181. input_json=payload.input))
  182. def execute_skill_run_from_contract(self, payload: SkillRunExecuteRequestDto) -> SkillRun | None:
  183. return self.execute_skill_run(
  184. skill_run_id=payload.skillRunId,
  185. payload=SkillRunExecuteRequest(
  186. skillRunId=payload.skillRunId,
  187. worker_key=payload.workerKey))
  188. def execute_skill_run(
  189. self,
  190. *,
  191. skill_run_id: str,
  192. payload: SkillRunExecuteRequest) -> SkillRun | None:
  193. run = self.skill_run_repository.get_by_id(
  194. skill_run_id=skill_run_id)
  195. if run is None:
  196. return None
  197. skill = self.skill_repository.get_by_id(skill_id=run.skill_id)
  198. if skill is None:
  199. return self.skill_run_repository.update_status(
  200. skill_run_id=run.id,
  201. status="failed",
  202. worker_key=payload.worker_key,
  203. error_code="skill_missing",
  204. error_message=f"skill not found: {run.skill_id}")
  205. self.skill_run_repository.update_status(
  206. skill_run_id=run.id,
  207. status="running",
  208. worker_key=payload.worker_key)
  209. try:
  210. output_text, output_json = self._execute_skill(skill=skill, input_json=run.input_json)
  211. except ValueError as exc:
  212. return self.skill_run_repository.update_status(
  213. skill_run_id=run.id,
  214. status="failed",
  215. worker_key=payload.worker_key,
  216. error_code="skill_execution_error",
  217. error_message=str(exc))
  218. return self.skill_run_repository.update_status(
  219. skill_run_id=run.id,
  220. status="completed",
  221. worker_key=payload.worker_key,
  222. output_text=output_text,
  223. output_json=output_json)
  224. def _execute_skill(
  225. self,
  226. *,
  227. skill: SkillDefinition,
  228. input_json: dict[str, JSONValue]) -> tuple[str | None, dict[str, JSONValue]]:
  229. if skill.runtime_type != "template":
  230. raise ValueError(f"unsupported skill runtime_type: {skill.runtime_type}")
  231. template_value = skill.implementation_json.get("template")
  232. if not isinstance(template_value, str):
  233. raise ValueError("template skill requires implementation_json.template")
  234. substitutions = {
  235. key: str(value)
  236. for key, value in input_json.items()
  237. if isinstance(value, (str, int, float, bool))
  238. }
  239. output_text = Template(template_value).safe_substitute(substitutions)
  240. return output_text, {
  241. "runtime_type": skill.runtime_type,
  242. "entrypoint": skill.entrypoint,
  243. "result": output_text,
  244. }
  245. def _build_skill_code(self, name: str) -> str:
  246. base = "".join(
  247. char.lower() if char.isalnum() else "_"
  248. for char in name
  249. ).strip("_") or "skill"
  250. return base[:64]