services.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. from core_domain import ChatCompletionRequestContract, ChatMessageContract
  2. from core_shared import JSONValue
  3. from app.db.models import AgentDefinition, AgentRun, AgentVersion
  4. from app.domain.repositories import (
  5. AgentDefinitionRepository,
  6. AgentRunRepository,
  7. AgentVersionRepository,
  8. )
  9. from app.infrastructure.model_gateway_client import ModelGatewayClient, ModelGatewayClientError
  10. from app.schemas.agent import (
  11. AgentCreateRequest,
  12. AgentRunCreateRequest,
  13. AgentRunExecuteRequest,
  14. AgentRunStatusUpdateRequest,
  15. AgentStatusUpdateRequest,
  16. AgentVersionCreateRequest,
  17. )
  18. class AgentApplicationService:
  19. def __init__(
  20. self,
  21. *,
  22. agent_repository: AgentDefinitionRepository,
  23. agent_version_repository: AgentVersionRepository,
  24. agent_run_repository: AgentRunRepository,
  25. model_gateway_client: ModelGatewayClient | None = None,
  26. ) -> None:
  27. self.agent_repository = agent_repository
  28. self.agent_version_repository = agent_version_repository
  29. self.agent_run_repository = agent_run_repository
  30. self.model_gateway_client = model_gateway_client
  31. def create_agent(self, payload: AgentCreateRequest) -> AgentDefinition:
  32. return self.agent_repository.create(
  33. tenant_id=payload.tenant_id,
  34. code=payload.code,
  35. name=payload.name,
  36. description=payload.description,
  37. agent_type=payload.agent_type,
  38. owner_user_id=payload.owner_user_id,
  39. metadata_json=payload.metadata_json,
  40. )
  41. def list_agents(self, *, tenant_id: str) -> list[AgentDefinition]:
  42. return self.agent_repository.list_by_tenant(tenant_id=tenant_id)
  43. def update_agent_status(
  44. self,
  45. *,
  46. agent_id: str,
  47. payload: AgentStatusUpdateRequest,
  48. ) -> AgentDefinition | None:
  49. return self.agent_repository.update_status(
  50. tenant_id=payload.tenant_id,
  51. agent_id=agent_id,
  52. status=payload.status,
  53. )
  54. def create_agent_version(self, payload: AgentVersionCreateRequest) -> AgentVersion:
  55. agent = self.agent_repository.get_by_id(
  56. tenant_id=payload.tenant_id,
  57. agent_id=payload.agent_id,
  58. )
  59. if agent is None:
  60. raise ValueError(f"agent not found: {payload.agent_id}")
  61. return self.agent_version_repository.create(
  62. tenant_id=payload.tenant_id,
  63. agent_id=payload.agent_id,
  64. status=payload.status,
  65. role=payload.role,
  66. goal=payload.goal,
  67. system_prompt=payload.system_prompt,
  68. model_config_json=payload.model_config_data.model_dump(mode="json"),
  69. memory_policy_json=payload.memory_policy.model_dump(mode="json"),
  70. tool_refs_json=[item.model_dump(mode="json") for item in payload.tool_refs],
  71. skill_refs_json=[item.model_dump(mode="json") for item in payload.skill_refs],
  72. )
  73. def list_agent_versions(self, *, tenant_id: str, agent_id: str) -> list[AgentVersion]:
  74. return self.agent_version_repository.list_by_agent(tenant_id=tenant_id, agent_id=agent_id)
  75. def create_agent_run(self, payload: AgentRunCreateRequest) -> AgentRun:
  76. agent_version = self._resolve_agent_version(
  77. tenant_id=payload.tenant_id,
  78. agent_id=payload.agent_id,
  79. agent_version_id=payload.agent_version_id,
  80. )
  81. if agent_version is None:
  82. raise ValueError("published agent version not found")
  83. return self.agent_run_repository.create(
  84. tenant_id=payload.tenant_id,
  85. agent_id=payload.agent_id,
  86. agent_version_id=agent_version.id,
  87. session_id=payload.session_id,
  88. input_text=payload.input_text,
  89. input_json=payload.input_json,
  90. )
  91. def list_agent_runs(
  92. self,
  93. *,
  94. tenant_id: str,
  95. agent_id: str | None = None,
  96. session_id: str | None = None,
  97. ) -> list[AgentRun]:
  98. return self.agent_run_repository.list_by_scope(
  99. tenant_id=tenant_id,
  100. agent_id=agent_id,
  101. session_id=session_id,
  102. )
  103. def update_agent_run_status(
  104. self,
  105. *,
  106. agent_run_id: str,
  107. payload: AgentRunStatusUpdateRequest,
  108. ) -> AgentRun | None:
  109. entity = self.agent_run_repository.get_by_id(
  110. tenant_id=payload.tenant_id,
  111. agent_run_id=agent_run_id,
  112. )
  113. if entity is None:
  114. return None
  115. return self.agent_run_repository.update_status(
  116. agent_run_id=agent_run_id,
  117. status=payload.status,
  118. worker_key=payload.worker_key,
  119. output_text=payload.output_text,
  120. output_json=payload.output_json,
  121. error_code=payload.error_code,
  122. error_message=payload.error_message,
  123. )
  124. def execute_agent_run(
  125. self,
  126. *,
  127. agent_run_id: str,
  128. payload: AgentRunExecuteRequest,
  129. ) -> AgentRun | None:
  130. agent_run = self.agent_run_repository.get_by_id(
  131. tenant_id=payload.tenant_id,
  132. agent_run_id=agent_run_id,
  133. )
  134. if agent_run is None:
  135. return None
  136. agent_version = self.agent_version_repository.get_by_id(
  137. tenant_id=payload.tenant_id,
  138. agent_version_id=agent_run.agent_version_id,
  139. )
  140. if agent_version is None:
  141. return self.agent_run_repository.update_status(
  142. agent_run_id=agent_run.id,
  143. status="failed",
  144. worker_key=payload.worker_key,
  145. error_code="agent_version_missing",
  146. error_message=f"agent version not found: {agent_run.agent_version_id}",
  147. )
  148. self.agent_run_repository.update_status(
  149. agent_run_id=agent_run.id,
  150. status="running",
  151. worker_key=payload.worker_key,
  152. )
  153. messages = self._build_chat_messages(agent_run=agent_run, agent_version=agent_version)
  154. if payload.dry_run:
  155. return self.agent_run_repository.update_status(
  156. agent_run_id=agent_run.id,
  157. status="completed",
  158. worker_key=payload.worker_key,
  159. output_text=self._build_dry_run_output(
  160. agent_run=agent_run,
  161. agent_version=agent_version,
  162. ),
  163. output_json={
  164. "dry_run": True,
  165. "agent_version_id": agent_version.id,
  166. "message_count": len(messages),
  167. "messages": [message.model_dump(mode="json") for message in messages],
  168. },
  169. )
  170. if self.model_gateway_client is None:
  171. return self.agent_run_repository.update_status(
  172. agent_run_id=agent_run.id,
  173. status="failed",
  174. worker_key=payload.worker_key,
  175. error_code="model_gateway_missing",
  176. error_message="model gateway client is not configured",
  177. )
  178. try:
  179. response = self.model_gateway_client.create_chat_completion(
  180. ChatCompletionRequestContract(
  181. model=self._read_optional_string(agent_version.model_config_json, "model"),
  182. temperature=self._read_optional_float(
  183. agent_version.model_config_json,
  184. "temperature",
  185. ),
  186. max_tokens=self._read_optional_int(agent_version.model_config_json, "max_tokens"),
  187. messages=messages,
  188. metadata_json={
  189. "tenant_id": agent_run.tenant_id,
  190. "agent_id": agent_run.agent_id,
  191. "agent_version_id": agent_version.id,
  192. "agent_run_id": agent_run.id,
  193. },
  194. )
  195. )
  196. except ModelGatewayClientError as exc:
  197. return self.agent_run_repository.update_status(
  198. agent_run_id=agent_run.id,
  199. status="failed",
  200. worker_key=payload.worker_key,
  201. error_code="model_gateway_error",
  202. error_message=str(exc),
  203. )
  204. return self.agent_run_repository.update_status(
  205. agent_run_id=agent_run.id,
  206. status="completed",
  207. worker_key=payload.worker_key,
  208. output_text=response.content,
  209. output_json={
  210. "dry_run": False,
  211. "agent_version_id": agent_version.id,
  212. "model": response.model,
  213. "finish_reason": response.finish_reason,
  214. "usage_json": response.usage_json,
  215. "raw_response_json": response.raw_response_json,
  216. },
  217. )
  218. def _resolve_agent_version(
  219. self,
  220. *,
  221. tenant_id: str,
  222. agent_id: str,
  223. agent_version_id: str | None,
  224. ) -> AgentVersion | None:
  225. if agent_version_id is not None:
  226. return self.agent_version_repository.get_by_id(
  227. tenant_id=tenant_id,
  228. agent_version_id=agent_version_id,
  229. )
  230. return self.agent_version_repository.get_latest_published(
  231. tenant_id=tenant_id,
  232. agent_id=agent_id,
  233. )
  234. def _build_chat_messages(
  235. self,
  236. *,
  237. agent_run: AgentRun,
  238. agent_version: AgentVersion,
  239. ) -> list[ChatMessageContract]:
  240. messages = [
  241. ChatMessageContract(role="system", content=agent_version.system_prompt),
  242. ]
  243. if agent_version.goal:
  244. messages.append(ChatMessageContract(role="system", content=f"Goal: {agent_version.goal}"))
  245. if agent_run.input_text:
  246. messages.append(ChatMessageContract(role="user", content=agent_run.input_text))
  247. if agent_run.input_json:
  248. messages.append(
  249. ChatMessageContract(
  250. role="user",
  251. content=f"Structured input: {agent_run.input_json}",
  252. )
  253. )
  254. return messages
  255. def _build_dry_run_output(self, *, agent_run: AgentRun, agent_version: AgentVersion) -> str:
  256. input_preview = agent_run.input_text or str(agent_run.input_json or {})
  257. return (
  258. f"[dry-run] Agent role={agent_version.role} "
  259. f"version={agent_version.version_no} received: {input_preview}"
  260. )
  261. def _read_optional_string(self, payload: dict[str, JSONValue], key: str) -> str | None:
  262. value = payload.get(key)
  263. if isinstance(value, str) and value:
  264. return value
  265. return None
  266. def _read_optional_float(self, payload: dict[str, JSONValue], key: str) -> float | None:
  267. value = payload.get(key)
  268. if isinstance(value, (int, float)) and not isinstance(value, bool):
  269. return float(value)
  270. return None
  271. def _read_optional_int(self, payload: dict[str, JSONValue], key: str) -> int | None:
  272. value = payload.get(key)
  273. if isinstance(value, int) and not isinstance(value, bool):
  274. return value
  275. return None