services.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. from string import Template
  2. from core_shared import JSONValue
  3. from app.db.models import SkillDefinition, SkillInstallation, SkillRun, SkillVersion
  4. from app.domain.repositories import (
  5. SkillDefinitionRepository,
  6. SkillInstallationRepository,
  7. SkillRunRepository,
  8. SkillVersionRepository,
  9. )
  10. from app.schemas.skill import (
  11. SkillCreateRequest,
  12. SkillInstallRequest,
  13. SkillInstallationStatusUpdateRequest,
  14. SkillRunCreateRequest,
  15. SkillRunExecuteRequest,
  16. SkillStatusUpdateRequest,
  17. SkillVersionCreateRequest,
  18. )
  19. class SkillApplicationService:
  20. def __init__(
  21. self,
  22. *,
  23. skill_repository: SkillDefinitionRepository,
  24. skill_version_repository: SkillVersionRepository,
  25. installation_repository: SkillInstallationRepository,
  26. skill_run_repository: SkillRunRepository,
  27. ) -> None:
  28. self.skill_repository = skill_repository
  29. self.skill_version_repository = skill_version_repository
  30. self.installation_repository = installation_repository
  31. self.skill_run_repository = skill_run_repository
  32. def create_skill(self, payload: SkillCreateRequest) -> SkillDefinition:
  33. return self.skill_repository.create(
  34. tenant_id=payload.tenant_id,
  35. code=payload.code,
  36. name=payload.name,
  37. skill_type=payload.skill_type,
  38. description=payload.description,
  39. owner_user_id=payload.owner_user_id,
  40. metadata_json=payload.metadata_json,
  41. )
  42. def list_skills(self, *, tenant_id: str) -> list[SkillDefinition]:
  43. return self.skill_repository.list_by_tenant(tenant_id=tenant_id)
  44. def update_skill_status(
  45. self,
  46. *,
  47. skill_id: str,
  48. payload: SkillStatusUpdateRequest,
  49. ) -> SkillDefinition | None:
  50. return self.skill_repository.update_status(
  51. tenant_id=payload.tenant_id,
  52. skill_id=skill_id,
  53. status=payload.status,
  54. )
  55. def create_skill_version(self, payload: SkillVersionCreateRequest) -> SkillVersion:
  56. skill = self.skill_repository.get_by_id(tenant_id=payload.tenant_id, skill_id=payload.skill_id)
  57. if skill is None:
  58. raise ValueError(f"skill not found: {payload.skill_id}")
  59. return self.skill_version_repository.create(
  60. tenant_id=payload.tenant_id,
  61. skill_id=payload.skill_id,
  62. status=payload.status,
  63. runtime_type=payload.runtime_type,
  64. entrypoint=payload.entrypoint,
  65. parameter_schema_json=payload.parameter_schema_json,
  66. output_schema_json=payload.output_schema_json,
  67. implementation_json=payload.implementation_json,
  68. )
  69. def list_skill_versions(self, *, tenant_id: str, skill_id: str) -> list[SkillVersion]:
  70. return self.skill_version_repository.list_by_skill(tenant_id=tenant_id, skill_id=skill_id)
  71. def install_skill(self, payload: SkillInstallRequest) -> SkillInstallation:
  72. version = self._resolve_skill_version(
  73. tenant_id=payload.tenant_id,
  74. skill_id=payload.skill_id,
  75. skill_version_id=payload.skill_version_id,
  76. )
  77. if version is None:
  78. raise ValueError("published skill version not found")
  79. return self.installation_repository.create(
  80. tenant_id=payload.tenant_id,
  81. skill_id=payload.skill_id,
  82. skill_version_id=version.id,
  83. install_scope=payload.install_scope,
  84. scope_id=payload.scope_id,
  85. config_json=payload.config_json,
  86. installed_by=payload.installed_by,
  87. )
  88. def list_installations(
  89. self,
  90. *,
  91. tenant_id: str,
  92. install_scope: str | None = None,
  93. scope_id: str | None = None,
  94. ) -> list[SkillInstallation]:
  95. return self.installation_repository.list_by_scope(
  96. tenant_id=tenant_id,
  97. install_scope=install_scope,
  98. scope_id=scope_id,
  99. )
  100. def update_installation_status(
  101. self,
  102. *,
  103. installation_id: str,
  104. payload: SkillInstallationStatusUpdateRequest,
  105. ) -> SkillInstallation | None:
  106. return self.installation_repository.update_status(
  107. tenant_id=payload.tenant_id,
  108. installation_id=installation_id,
  109. status=payload.status,
  110. )
  111. def create_skill_run(self, payload: SkillRunCreateRequest) -> SkillRun:
  112. version = self._resolve_skill_version(
  113. tenant_id=payload.tenant_id,
  114. skill_id=payload.skill_id,
  115. skill_version_id=payload.skill_version_id,
  116. )
  117. if version is None:
  118. raise ValueError("published skill version not found")
  119. return self.skill_run_repository.create(
  120. tenant_id=payload.tenant_id,
  121. skill_id=payload.skill_id,
  122. skill_version_id=version.id,
  123. installation_id=payload.installation_id,
  124. input_json=payload.input_json,
  125. )
  126. def execute_skill_run(
  127. self,
  128. *,
  129. skill_run_id: str,
  130. payload: SkillRunExecuteRequest,
  131. ) -> SkillRun | None:
  132. run = self.skill_run_repository.get_by_id(
  133. tenant_id=payload.tenant_id,
  134. skill_run_id=skill_run_id,
  135. )
  136. if run is None:
  137. return None
  138. version = self.skill_version_repository.get_by_id(
  139. tenant_id=payload.tenant_id,
  140. skill_version_id=run.skill_version_id,
  141. )
  142. if version is None:
  143. return self.skill_run_repository.update_status(
  144. skill_run_id=run.id,
  145. status="failed",
  146. worker_key=payload.worker_key,
  147. error_code="skill_version_missing",
  148. error_message=f"skill version not found: {run.skill_version_id}",
  149. )
  150. self.skill_run_repository.update_status(
  151. skill_run_id=run.id,
  152. status="running",
  153. worker_key=payload.worker_key,
  154. )
  155. try:
  156. output_text, output_json = self._execute_version(version=version, input_json=run.input_json)
  157. except ValueError as exc:
  158. return self.skill_run_repository.update_status(
  159. skill_run_id=run.id,
  160. status="failed",
  161. worker_key=payload.worker_key,
  162. error_code="skill_execution_error",
  163. error_message=str(exc),
  164. )
  165. return self.skill_run_repository.update_status(
  166. skill_run_id=run.id,
  167. status="completed",
  168. worker_key=payload.worker_key,
  169. output_text=output_text,
  170. output_json=output_json,
  171. )
  172. def _resolve_skill_version(
  173. self,
  174. *,
  175. tenant_id: str,
  176. skill_id: str,
  177. skill_version_id: str | None,
  178. ) -> SkillVersion | None:
  179. if skill_version_id is not None:
  180. return self.skill_version_repository.get_by_id(
  181. tenant_id=tenant_id,
  182. skill_version_id=skill_version_id,
  183. )
  184. return self.skill_version_repository.get_latest_published(
  185. tenant_id=tenant_id,
  186. skill_id=skill_id,
  187. )
  188. def _execute_version(
  189. self,
  190. *,
  191. version: SkillVersion,
  192. input_json: dict[str, JSONValue],
  193. ) -> tuple[str | None, dict[str, JSONValue]]:
  194. if version.runtime_type != "template":
  195. raise ValueError(f"unsupported skill runtime_type: {version.runtime_type}")
  196. template_value = version.implementation_json.get("template")
  197. if not isinstance(template_value, str):
  198. raise ValueError("template skill requires implementation_json.template")
  199. substitutions = {
  200. key: str(value)
  201. for key, value in input_json.items()
  202. if isinstance(value, (str, int, float, bool))
  203. }
  204. output_text = Template(template_value).safe_substitute(substitutions)
  205. return output_text, {
  206. "runtime_type": version.runtime_type,
  207. "entrypoint": version.entrypoint,
  208. "result": output_text,
  209. }