services.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  1. from datetime import datetime, timedelta
  2. from typing import cast
  3. from sqlalchemy.orm import Session
  4. from core_events import EventPublishContract, EventServiceClient, EventServiceClientError
  5. from core_domain import (
  6. AgentSkillRefContract,
  7. AgentToolRefContract,
  8. ChatCompletionRequestContract,
  9. ChatMessageContract,
  10. MemoryCreateContract,
  11. MemoryScopeType,
  12. MemorySearchRequestContract,
  13. MemorySearchResultContract,
  14. )
  15. from core_shared import JSONValue
  16. from app.bootstrap.settings import AgentServiceSettings
  17. from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentVersion
  18. from app.domain.repositories import (
  19. AgentDefinitionRepository,
  20. AgentRunRepository,
  21. AgentToolInvocationRepository,
  22. AgentVersionRepository,
  23. )
  24. from app.infrastructure.model_gateway_client import ModelGatewayClient, ModelGatewayClientError
  25. from app.infrastructure.memory_client import MemoryClient, MemoryClientError
  26. from app.infrastructure.skill_client import SkillServiceClient, SkillServiceClientError
  27. from app.infrastructure.tool_client import ToolServiceClient, ToolServiceClientError
  28. from app.schemas.agent import (
  29. AgentCreateRequest,
  30. AgentRunCreateRequest,
  31. AgentRunExecuteRequest,
  32. AgentRunStatusUpdateRequest,
  33. AgentStatusUpdateRequest,
  34. AgentVersionCreateRequest,
  35. )
  36. class AgentApplicationService:
  37. def __init__(
  38. self,
  39. *,
  40. agent_repository: AgentDefinitionRepository,
  41. agent_version_repository: AgentVersionRepository,
  42. agent_run_repository: AgentRunRepository,
  43. agent_tool_invocation_repository: AgentToolInvocationRepository,
  44. model_gateway_client: ModelGatewayClient | None = None,
  45. memory_client: MemoryClient | None = None,
  46. tool_client: ToolServiceClient | None = None,
  47. skill_client: SkillServiceClient | None = None,
  48. event_client: EventServiceClient | None = None,
  49. ) -> None:
  50. self.agent_repository = agent_repository
  51. self.agent_version_repository = agent_version_repository
  52. self.agent_run_repository = agent_run_repository
  53. self.agent_tool_invocation_repository = agent_tool_invocation_repository
  54. self.model_gateway_client = model_gateway_client
  55. self.memory_client = memory_client
  56. self.tool_client = tool_client
  57. self.skill_client = skill_client
  58. self.event_client = event_client
  59. def create_agent(self, payload: AgentCreateRequest) -> AgentDefinition:
  60. return self.agent_repository.create(
  61. tenant_id=payload.tenant_id,
  62. code=payload.code,
  63. name=payload.name,
  64. description=payload.description,
  65. agent_type=payload.agent_type,
  66. owner_user_id=payload.owner_user_id,
  67. metadata_json=payload.metadata_json,
  68. )
  69. def list_agents(self, *, tenant_id: str) -> list[AgentDefinition]:
  70. return self.agent_repository.list_by_tenant(tenant_id=tenant_id)
  71. def update_agent_status(
  72. self,
  73. *,
  74. agent_id: str,
  75. payload: AgentStatusUpdateRequest,
  76. ) -> AgentDefinition | None:
  77. return self.agent_repository.update_status(
  78. tenant_id=payload.tenant_id,
  79. agent_id=agent_id,
  80. status=payload.status,
  81. )
  82. def create_agent_version(self, payload: AgentVersionCreateRequest) -> AgentVersion:
  83. agent = self.agent_repository.get_by_id(
  84. tenant_id=payload.tenant_id,
  85. agent_id=payload.agent_id,
  86. )
  87. if agent is None:
  88. raise ValueError(f"agent not found: {payload.agent_id}")
  89. return self.agent_version_repository.create(
  90. tenant_id=payload.tenant_id,
  91. agent_id=payload.agent_id,
  92. status=payload.status,
  93. role=payload.role,
  94. goal=payload.goal,
  95. system_prompt=payload.system_prompt,
  96. model_config_json=payload.model_config_data.model_dump(mode="json"),
  97. memory_policy_json=payload.memory_policy.model_dump(mode="json"),
  98. tool_refs_json=[item.model_dump(mode="json") for item in payload.tool_refs],
  99. skill_refs_json=[item.model_dump(mode="json") for item in payload.skill_refs],
  100. )
  101. def list_agent_versions(self, *, tenant_id: str, agent_id: str) -> list[AgentVersion]:
  102. return self.agent_version_repository.list_by_agent(tenant_id=tenant_id, agent_id=agent_id)
  103. def create_agent_run(self, payload: AgentRunCreateRequest) -> AgentRun:
  104. agent_version = self._resolve_agent_version(
  105. tenant_id=payload.tenant_id,
  106. agent_id=payload.agent_id,
  107. agent_version_id=payload.agent_version_id,
  108. )
  109. if agent_version is None:
  110. raise ValueError("published agent version not found")
  111. agent_run = self.agent_run_repository.create(
  112. tenant_id=payload.tenant_id,
  113. agent_id=payload.agent_id,
  114. agent_version_id=agent_version.id,
  115. session_id=payload.session_id,
  116. input_text=payload.input_text,
  117. input_json=payload.input_json,
  118. )
  119. self._publish_event(
  120. event_type="agent.run.created",
  121. agent_run=agent_run,
  122. payload_json={"agent_run_id": agent_run.id, "status": agent_run.status},
  123. )
  124. return agent_run
  125. def list_agent_runs(
  126. self,
  127. *,
  128. tenant_id: str,
  129. agent_id: str | None = None,
  130. session_id: str | None = None,
  131. ) -> list[AgentRun]:
  132. return self.agent_run_repository.list_by_scope(
  133. tenant_id=tenant_id,
  134. agent_id=agent_id,
  135. session_id=session_id,
  136. )
  137. def list_agent_tool_invocations(
  138. self,
  139. *,
  140. tenant_id: str,
  141. agent_run_id: str,
  142. ) -> list[AgentToolInvocation]:
  143. return self.agent_tool_invocation_repository.list_by_run(
  144. tenant_id=tenant_id,
  145. agent_run_id=agent_run_id,
  146. )
  147. def update_agent_run_status(
  148. self,
  149. *,
  150. agent_run_id: str,
  151. payload: AgentRunStatusUpdateRequest,
  152. ) -> AgentRun | None:
  153. entity = self.agent_run_repository.get_by_id(
  154. tenant_id=payload.tenant_id,
  155. agent_run_id=agent_run_id,
  156. )
  157. if entity is None:
  158. return None
  159. return self.agent_run_repository.update_status(
  160. agent_run_id=agent_run_id,
  161. status=payload.status,
  162. worker_key=payload.worker_key,
  163. output_text=payload.output_text,
  164. output_json=payload.output_json,
  165. error_code=payload.error_code,
  166. error_message=payload.error_message,
  167. )
  168. def execute_agent_run(
  169. self,
  170. *,
  171. agent_run_id: str,
  172. payload: AgentRunExecuteRequest,
  173. ) -> AgentRun | None:
  174. agent_run = self.agent_run_repository.get_by_id(
  175. tenant_id=payload.tenant_id,
  176. agent_run_id=agent_run_id,
  177. )
  178. if agent_run is None:
  179. return None
  180. agent_version = self.agent_version_repository.get_by_id(
  181. tenant_id=payload.tenant_id,
  182. agent_version_id=agent_run.agent_version_id,
  183. )
  184. if agent_version is None:
  185. return self.agent_run_repository.update_status(
  186. agent_run_id=agent_run.id,
  187. status="failed",
  188. worker_key=payload.worker_key,
  189. error_code="agent_version_missing",
  190. error_message=f"agent version not found: {agent_run.agent_version_id}",
  191. )
  192. self.agent_run_repository.update_status(
  193. agent_run_id=agent_run.id,
  194. status="running",
  195. worker_key=payload.worker_key,
  196. )
  197. memory_results, memory_metadata = self._read_relevant_memories(
  198. agent_run=agent_run,
  199. agent_version=agent_version,
  200. )
  201. selected_tools = self._select_tool_refs(agent_run=agent_run, agent_version=agent_version)
  202. selected_skills = self._select_skill_refs(agent_run=agent_run, agent_version=agent_version)
  203. if payload.dry_run:
  204. messages = self._build_chat_messages(
  205. agent_run=agent_run,
  206. agent_version=agent_version,
  207. memory_results=memory_results,
  208. capability_context=self._format_capability_plan(
  209. selected_tools=selected_tools,
  210. selected_skills=selected_skills,
  211. ),
  212. )
  213. completed_run = self.agent_run_repository.update_status(
  214. agent_run_id=agent_run.id,
  215. status="completed",
  216. worker_key=payload.worker_key,
  217. output_text=self._build_dry_run_output(
  218. agent_run=agent_run,
  219. agent_version=agent_version,
  220. ),
  221. output_json={
  222. "dry_run": True,
  223. "agent_version_id": agent_version.id,
  224. "message_count": len(messages),
  225. "messages": [message.model_dump(mode="json") for message in messages],
  226. "selected_tool_refs": [
  227. tool_ref.model_dump(mode="json") for tool_ref in selected_tools
  228. ],
  229. "selected_skill_refs": [
  230. skill_ref.model_dump(mode="json") for skill_ref in selected_skills
  231. ],
  232. **memory_metadata,
  233. },
  234. )
  235. if completed_run is not None:
  236. self._publish_event(
  237. event_type="agent.run.completed",
  238. agent_run=completed_run,
  239. payload_json={
  240. "agent_run_id": completed_run.id,
  241. "dry_run": True,
  242. "status": completed_run.status,
  243. },
  244. )
  245. return completed_run
  246. tool_invocations = self._invoke_selected_tools(
  247. agent_run=agent_run,
  248. agent_version=agent_version,
  249. selected_tools=selected_tools,
  250. )
  251. skill_invocations = self._invoke_selected_skills(
  252. agent_run=agent_run,
  253. selected_skills=selected_skills,
  254. worker_key=payload.worker_key,
  255. )
  256. messages = self._build_chat_messages(
  257. agent_run=agent_run,
  258. agent_version=agent_version,
  259. memory_results=memory_results,
  260. capability_context=self._format_capability_results(
  261. tool_invocations=tool_invocations,
  262. skill_invocations=skill_invocations,
  263. ),
  264. )
  265. if self.model_gateway_client is None:
  266. return self.agent_run_repository.update_status(
  267. agent_run_id=agent_run.id,
  268. status="failed",
  269. worker_key=payload.worker_key,
  270. error_code="model_gateway_missing",
  271. error_message="model gateway client is not configured",
  272. output_json={
  273. "tool_invocations": tool_invocations,
  274. "skill_invocations": skill_invocations,
  275. **memory_metadata,
  276. },
  277. )
  278. try:
  279. response = self.model_gateway_client.create_chat_completion(
  280. ChatCompletionRequestContract(
  281. model=self._read_optional_string(agent_version.model_config_json, "model"),
  282. temperature=self._read_optional_float(
  283. agent_version.model_config_json,
  284. "temperature",
  285. ),
  286. max_tokens=self._read_optional_int(
  287. agent_version.model_config_json,
  288. "max_tokens",
  289. ),
  290. messages=messages,
  291. metadata_json={
  292. "tenant_id": agent_run.tenant_id,
  293. "agent_id": agent_run.agent_id,
  294. "agent_version_id": agent_version.id,
  295. "agent_run_id": agent_run.id,
  296. },
  297. )
  298. )
  299. except ModelGatewayClientError as exc:
  300. return self.agent_run_repository.update_status(
  301. agent_run_id=agent_run.id,
  302. status="failed",
  303. worker_key=payload.worker_key,
  304. error_code="model_gateway_error",
  305. error_message=str(exc),
  306. )
  307. memory_write_metadata = self._write_interaction_memory(
  308. agent_run=agent_run,
  309. agent_version=agent_version,
  310. output_text=response.content,
  311. )
  312. completed_run = self.agent_run_repository.update_status(
  313. agent_run_id=agent_run.id,
  314. status="completed",
  315. worker_key=payload.worker_key,
  316. output_text=response.content,
  317. output_json={
  318. "dry_run": False,
  319. "agent_version_id": agent_version.id,
  320. "model": response.model,
  321. "finish_reason": response.finish_reason,
  322. "usage_json": response.usage_json,
  323. "raw_response_json": response.raw_response_json,
  324. "tool_invocations": tool_invocations,
  325. "skill_invocations": skill_invocations,
  326. **memory_metadata,
  327. **memory_write_metadata,
  328. },
  329. )
  330. if completed_run is not None:
  331. self._publish_event(
  332. event_type="agent.run.completed",
  333. agent_run=completed_run,
  334. payload_json={
  335. "agent_run_id": completed_run.id,
  336. "dry_run": False,
  337. "status": completed_run.status,
  338. },
  339. )
  340. return completed_run
  341. def _publish_event(
  342. self,
  343. *,
  344. event_type: str,
  345. agent_run: AgentRun,
  346. payload_json: dict[str, JSONValue],
  347. ) -> None:
  348. if self.event_client is None:
  349. return
  350. try:
  351. self.event_client.publish_event(
  352. EventPublishContract(
  353. tenant_id=agent_run.tenant_id,
  354. event_type=event_type,
  355. source_service="agent-service",
  356. aggregate_type="agent_run",
  357. aggregate_id=agent_run.id,
  358. correlation_id=agent_run.session_id,
  359. payload_json={
  360. **payload_json,
  361. "agent_id": agent_run.agent_id,
  362. "agent_version_id": agent_run.agent_version_id,
  363. },
  364. )
  365. )
  366. except EventServiceClientError:
  367. return
  368. def execute_next_claimed_agent_run(
  369. self,
  370. *,
  371. worker_key: str,
  372. lease_seconds: int,
  373. dry_run: bool,
  374. ) -> tuple[AgentRun, int] | None:
  375. released_lease_count = self.agent_run_repository.release_expired_leases(
  376. now_time=datetime.utcnow(),
  377. )
  378. claimed_agent_run = self.agent_run_repository.claim_next_queued(
  379. worker_key=worker_key,
  380. lease_expire_time=datetime.utcnow() + timedelta(seconds=lease_seconds),
  381. )
  382. if claimed_agent_run is None:
  383. return None
  384. result = self.execute_agent_run(
  385. agent_run_id=claimed_agent_run.id,
  386. payload=AgentRunExecuteRequest(
  387. tenant_id=claimed_agent_run.tenant_id,
  388. worker_key=worker_key,
  389. dry_run=dry_run,
  390. ),
  391. )
  392. if result is None:
  393. return None
  394. return result, released_lease_count
  395. def _resolve_agent_version(
  396. self,
  397. *,
  398. tenant_id: str,
  399. agent_id: str,
  400. agent_version_id: str | None,
  401. ) -> AgentVersion | None:
  402. if agent_version_id is not None:
  403. return self.agent_version_repository.get_by_id(
  404. tenant_id=tenant_id,
  405. agent_version_id=agent_version_id,
  406. )
  407. return self.agent_version_repository.get_latest_published(
  408. tenant_id=tenant_id,
  409. agent_id=agent_id,
  410. )
  411. def _build_chat_messages(
  412. self,
  413. *,
  414. agent_run: AgentRun,
  415. agent_version: AgentVersion,
  416. memory_results: list[MemorySearchResultContract] | None = None,
  417. capability_context: str | None = None,
  418. ) -> list[ChatMessageContract]:
  419. messages = [
  420. ChatMessageContract(role="system", content=agent_version.system_prompt),
  421. ]
  422. if agent_version.goal:
  423. messages.append(
  424. ChatMessageContract(role="system", content=f"Goal: {agent_version.goal}")
  425. )
  426. if memory_results:
  427. messages.append(
  428. ChatMessageContract(
  429. role="system",
  430. content=self._format_memory_context(memory_results),
  431. )
  432. )
  433. if capability_context:
  434. messages.append(ChatMessageContract(role="system", content=capability_context))
  435. if agent_run.input_text:
  436. messages.append(ChatMessageContract(role="user", content=agent_run.input_text))
  437. if agent_run.input_json:
  438. messages.append(
  439. ChatMessageContract(
  440. role="user",
  441. content=f"Structured input: {agent_run.input_json}",
  442. )
  443. )
  444. return messages
  445. def _select_tool_refs(
  446. self,
  447. *,
  448. agent_run: AgentRun,
  449. agent_version: AgentVersion,
  450. ) -> list[AgentToolRefContract]:
  451. input_preview = self._build_input_preview(agent_run)
  452. selected: list[AgentToolRefContract] = []
  453. for item in agent_version.tool_refs_json:
  454. ref = AgentToolRefContract.model_validate(item)
  455. if (
  456. ref.required
  457. or self._read_bool(ref.config_json, "auto_invoke", default=False)
  458. or self._matches_selection_keywords(ref.config_json, input_preview)
  459. ):
  460. selected.append(ref)
  461. return selected
  462. def _select_skill_refs(
  463. self,
  464. *,
  465. agent_run: AgentRun,
  466. agent_version: AgentVersion,
  467. ) -> list[AgentSkillRefContract]:
  468. input_preview = self._build_input_preview(agent_run)
  469. selected: list[AgentSkillRefContract] = []
  470. for item in agent_version.skill_refs_json:
  471. ref = AgentSkillRefContract.model_validate(item)
  472. auto_invoke = self._read_bool(ref.config_json, "auto_invoke", default=True)
  473. if auto_invoke or self._matches_selection_keywords(ref.config_json, input_preview):
  474. selected.append(ref)
  475. return selected
  476. def _invoke_selected_tools(
  477. self,
  478. *,
  479. agent_run: AgentRun,
  480. agent_version: AgentVersion,
  481. selected_tools: list[AgentToolRefContract],
  482. ) -> list[dict[str, JSONValue]]:
  483. invocations: list[dict[str, JSONValue]] = []
  484. for ref in selected_tools:
  485. invocation = self.agent_tool_invocation_repository.create(
  486. tenant_id=agent_run.tenant_id,
  487. agent_run_id=agent_run.id,
  488. agent_id=agent_run.agent_id,
  489. agent_version_id=agent_version.id,
  490. tool_code=ref.tool_code,
  491. tool_binding_id=ref.tool_binding_id,
  492. status="selected",
  493. input_json=agent_run.input_json or {},
  494. )
  495. if ref.tool_binding_id is None:
  496. self.agent_tool_invocation_repository.update_status(
  497. invocation_id=invocation.id,
  498. status="skipped",
  499. reason="tool_binding_id_missing",
  500. )
  501. invocations.append(
  502. {
  503. "status": "skipped",
  504. "reason": "tool_binding_id_missing",
  505. "tool_code": ref.tool_code,
  506. }
  507. )
  508. continue
  509. if self.tool_client is None:
  510. self.agent_tool_invocation_repository.update_status(
  511. invocation_id=invocation.id,
  512. status="failed",
  513. reason="tool_client_missing",
  514. error_message="tool client is not configured",
  515. )
  516. invocations.append(
  517. {
  518. "status": "failed",
  519. "reason": "tool_client_missing",
  520. "tool_binding_id": ref.tool_binding_id,
  521. }
  522. )
  523. continue
  524. try:
  525. self.agent_tool_invocation_repository.update_status(
  526. invocation_id=invocation.id,
  527. status="running",
  528. )
  529. detail = self.tool_client.get_tool_binding_detail(
  530. tenant_id=agent_run.tenant_id,
  531. binding_id=ref.tool_binding_id,
  532. )
  533. if not detail.binding.enabled:
  534. self.agent_tool_invocation_repository.update_status(
  535. invocation_id=invocation.id,
  536. status="failed",
  537. reason="tool_binding_disabled",
  538. error_message="tool binding is disabled",
  539. )
  540. invocations.append(
  541. {
  542. "status": "failed",
  543. "reason": "tool_binding_disabled",
  544. "tool_binding_id": ref.tool_binding_id,
  545. }
  546. )
  547. continue
  548. if detail.tool_definition.tool_type != "http":
  549. self.agent_tool_invocation_repository.update_status(
  550. invocation_id=invocation.id,
  551. status="skipped",
  552. reason="unsupported_tool_type",
  553. error_message=detail.tool_definition.tool_type,
  554. )
  555. invocations.append(
  556. {
  557. "status": "skipped",
  558. "reason": "unsupported_tool_type",
  559. "tool_type": detail.tool_definition.tool_type,
  560. "tool_binding_id": ref.tool_binding_id,
  561. }
  562. )
  563. continue
  564. output_text, output_json = self.tool_client.invoke_http_tool(
  565. detail=detail,
  566. input_json=agent_run.input_json or {},
  567. config_json=ref.config_json,
  568. )
  569. except ToolServiceClientError as exc:
  570. self.agent_tool_invocation_repository.update_status(
  571. invocation_id=invocation.id,
  572. status="failed",
  573. reason="tool_service_error",
  574. error_message=str(exc),
  575. )
  576. invocations.append(
  577. {
  578. "status": "failed",
  579. "reason": str(exc),
  580. "tool_binding_id": ref.tool_binding_id,
  581. }
  582. )
  583. continue
  584. self.agent_tool_invocation_repository.update_status(
  585. invocation_id=invocation.id,
  586. status="completed",
  587. output_text=output_text,
  588. output_json=output_json,
  589. )
  590. invocations.append(
  591. {
  592. "status": "completed",
  593. "tool_binding_id": ref.tool_binding_id,
  594. "tool_code": detail.tool_definition.code,
  595. "output_text": output_text,
  596. "output_json": output_json,
  597. }
  598. )
  599. return invocations
  600. def _invoke_selected_skills(
  601. self,
  602. *,
  603. agent_run: AgentRun,
  604. selected_skills: list[AgentSkillRefContract],
  605. worker_key: str | None,
  606. ) -> list[dict[str, JSONValue]]:
  607. invocations: list[dict[str, JSONValue]] = []
  608. for ref in selected_skills:
  609. if self.skill_client is None:
  610. invocations.append(
  611. {
  612. "status": "failed",
  613. "reason": "skill_client_missing",
  614. "skill_id": ref.skill_id,
  615. "skill_code": ref.skill_code,
  616. }
  617. )
  618. continue
  619. skill_id = ref.skill_id or self._resolve_skill_id_by_code(
  620. tenant_id=agent_run.tenant_id,
  621. skill_code=ref.skill_code,
  622. )
  623. if skill_id is None:
  624. invocations.append(
  625. {
  626. "status": "failed",
  627. "reason": "skill_id_missing",
  628. "skill_code": ref.skill_code,
  629. }
  630. )
  631. continue
  632. try:
  633. created_run = self.skill_client.create_skill_run(
  634. tenant_id=agent_run.tenant_id,
  635. skill_id=skill_id,
  636. skill_version_id=self._read_optional_string(
  637. ref.config_json,
  638. "skill_version_id",
  639. ),
  640. installation_id=self._read_optional_string(
  641. ref.config_json,
  642. "installation_id",
  643. ),
  644. input_json=self._build_skill_input_json(agent_run=agent_run, ref=ref),
  645. )
  646. executed_run = self.skill_client.execute_skill_run(
  647. tenant_id=agent_run.tenant_id,
  648. skill_run_id=created_run.id,
  649. worker_key=worker_key,
  650. )
  651. except SkillServiceClientError as exc:
  652. invocations.append(
  653. {
  654. "status": "failed",
  655. "reason": str(exc),
  656. "skill_id": skill_id,
  657. "skill_code": ref.skill_code,
  658. }
  659. )
  660. continue
  661. invocations.append(
  662. {
  663. "status": executed_run.status,
  664. "skill_id": skill_id,
  665. "skill_code": ref.skill_code,
  666. "skill_run_id": executed_run.id,
  667. "output_text": executed_run.output_text,
  668. "output_json": executed_run.output_json,
  669. "error_code": executed_run.error_code,
  670. "error_message": executed_run.error_message,
  671. }
  672. )
  673. return invocations
  674. def _format_capability_plan(
  675. self,
  676. *,
  677. selected_tools: list[AgentToolRefContract],
  678. selected_skills: list[AgentSkillRefContract],
  679. ) -> str:
  680. return (
  681. "Selected capability plan before model call:\n"
  682. f"Tools: {[item.model_dump(mode='json') for item in selected_tools]}\n"
  683. f"Skills: {[item.model_dump(mode='json') for item in selected_skills]}"
  684. )
  685. def _format_capability_results(
  686. self,
  687. *,
  688. tool_invocations: list[dict[str, JSONValue]],
  689. skill_invocations: list[dict[str, JSONValue]],
  690. ) -> str | None:
  691. if not tool_invocations and not skill_invocations:
  692. return None
  693. return (
  694. "Capability invocation results before model call:\n"
  695. f"Tools: {tool_invocations}\n"
  696. f"Skills: {skill_invocations}"
  697. )
  698. def _build_skill_input_json(
  699. self,
  700. *,
  701. agent_run: AgentRun,
  702. ref: AgentSkillRefContract,
  703. ) -> dict[str, JSONValue]:
  704. input_json: dict[str, JSONValue] = dict(agent_run.input_json or {})
  705. if agent_run.input_text:
  706. input_json.setdefault("input_text", agent_run.input_text)
  707. configured_input = ref.config_json.get("input_json")
  708. if isinstance(configured_input, dict):
  709. input_json.update(
  710. {str(item_key): item_value for item_key, item_value in configured_input.items()}
  711. )
  712. return input_json
  713. def _resolve_skill_id_by_code(self, *, tenant_id: str, skill_code: str | None) -> str | None:
  714. if skill_code is None or self.skill_client is None:
  715. return None
  716. try:
  717. skills = self.skill_client.list_skills(tenant_id=tenant_id)
  718. except SkillServiceClientError:
  719. return None
  720. for skill in skills:
  721. if skill.code == skill_code:
  722. return skill.id
  723. return None
  724. def _build_input_preview(self, agent_run: AgentRun) -> str:
  725. return f"{agent_run.input_text or ''} {agent_run.input_json or {}}".lower()
  726. def _matches_selection_keywords(
  727. self,
  728. config_json: dict[str, JSONValue],
  729. input_preview: str,
  730. ) -> bool:
  731. keywords = config_json.get("selection_keywords")
  732. if not isinstance(keywords, list):
  733. return False
  734. return any(
  735. isinstance(keyword, str) and keyword.lower() in input_preview
  736. for keyword in keywords
  737. )
  738. def _build_dry_run_output(self, *, agent_run: AgentRun, agent_version: AgentVersion) -> str:
  739. input_preview = agent_run.input_text or str(agent_run.input_json or {})
  740. return (
  741. f"[dry-run] Agent role={agent_version.role} "
  742. f"version={agent_version.version_no} received: {input_preview}"
  743. )
  744. def _read_optional_string(self, payload: dict[str, JSONValue], key: str) -> str | None:
  745. value = payload.get(key)
  746. if isinstance(value, str) and value:
  747. return value
  748. return None
  749. def _read_optional_float(self, payload: dict[str, JSONValue], key: str) -> float | None:
  750. value = payload.get(key)
  751. if isinstance(value, (int, float)) and not isinstance(value, bool):
  752. return float(value)
  753. return None
  754. def _read_optional_int(self, payload: dict[str, JSONValue], key: str) -> int | None:
  755. value = payload.get(key)
  756. if isinstance(value, int) and not isinstance(value, bool):
  757. return value
  758. return None
  759. def _read_relevant_memories(
  760. self,
  761. *,
  762. agent_run: AgentRun,
  763. agent_version: AgentVersion,
  764. ) -> tuple[list[MemorySearchResultContract], dict[str, JSONValue]]:
  765. if self.memory_client is None:
  766. return [], {"memory_read_enabled": False, "memory_read_reason": "client_missing"}
  767. if not self._read_bool(agent_version.memory_policy_json, "enabled", default=True):
  768. return [], {"memory_read_enabled": False, "memory_read_reason": "policy_disabled"}
  769. query = agent_run.input_text or str(agent_run.input_json or "")
  770. if not query:
  771. return [], {"memory_read_enabled": True, "memory_read_count": 0}
  772. scope = self._resolve_memory_scope(agent_run=agent_run, agent_version=agent_version)
  773. if scope is None:
  774. return [], {
  775. "memory_read_enabled": True,
  776. "memory_read_count": 0,
  777. "memory_read_reason": "scope_unavailable",
  778. }
  779. scope_type, scope_id = scope
  780. try:
  781. results = self.memory_client.search_memories(
  782. MemorySearchRequestContract(
  783. tenant_id=agent_run.tenant_id,
  784. query=query,
  785. scope_type=scope_type,
  786. scope_id=scope_id,
  787. owner_agent_id=agent_run.agent_id,
  788. session_id=agent_run.session_id,
  789. limit=self._read_int(
  790. agent_version.memory_policy_json,
  791. "read_top_k",
  792. default=8,
  793. ),
  794. )
  795. )
  796. except MemoryClientError as exc:
  797. return [], {
  798. "memory_read_enabled": True,
  799. "memory_read_count": 0,
  800. "memory_read_error": str(exc),
  801. }
  802. return results, {
  803. "memory_read_enabled": True,
  804. "memory_read_count": len(results),
  805. "memory_scope_type": scope_type,
  806. "memory_scope_id": scope_id,
  807. }
  808. def _write_interaction_memory(
  809. self,
  810. *,
  811. agent_run: AgentRun,
  812. agent_version: AgentVersion,
  813. output_text: str,
  814. ) -> dict[str, JSONValue]:
  815. if self.memory_client is None:
  816. return {"memory_write_enabled": False, "memory_write_reason": "client_missing"}
  817. if not self._read_bool(agent_version.memory_policy_json, "write_enabled", default=True):
  818. return {"memory_write_enabled": False, "memory_write_reason": "policy_disabled"}
  819. scope = self._resolve_memory_scope(agent_run=agent_run, agent_version=agent_version)
  820. if scope is None:
  821. return {"memory_write_enabled": True, "memory_write_reason": "scope_unavailable"}
  822. scope_type, scope_id = scope
  823. try:
  824. memory = self.memory_client.create_memory(
  825. MemoryCreateContract(
  826. tenant_id=agent_run.tenant_id,
  827. scope_type=scope_type,
  828. scope_id=scope_id,
  829. memory_type="conversation",
  830. content_text=self._format_interaction_memory(
  831. agent_run=agent_run,
  832. output_text=output_text,
  833. ),
  834. content_json={
  835. "agent_run_id": agent_run.id,
  836. "agent_version_id": agent_version.id,
  837. "input_text": agent_run.input_text,
  838. "output_text": output_text,
  839. },
  840. metadata_json={
  841. "source": "agent-service",
  842. "role": agent_version.role,
  843. "version_no": agent_version.version_no,
  844. },
  845. owner_agent_id=agent_run.agent_id,
  846. session_id=agent_run.session_id,
  847. source_ref=f"agent_run:{agent_run.id}",
  848. importance_score=self._read_nested_int(
  849. agent_version.memory_policy_json,
  850. "config_json",
  851. "write_importance_score",
  852. default=50,
  853. ),
  854. )
  855. )
  856. except MemoryClientError as exc:
  857. return {
  858. "memory_write_enabled": True,
  859. "memory_write_error": str(exc),
  860. }
  861. return {
  862. "memory_write_enabled": True,
  863. "memory_written_id": memory.id,
  864. "memory_scope_type": scope_type,
  865. "memory_scope_id": scope_id,
  866. }
  867. def _resolve_memory_scope(
  868. self,
  869. *,
  870. agent_run: AgentRun,
  871. agent_version: AgentVersion,
  872. ) -> tuple[MemoryScopeType, str] | None:
  873. scope_value = self._read_optional_string(
  874. agent_version.memory_policy_json,
  875. "memory_scope",
  876. ) or "session"
  877. if scope_value == "tenant":
  878. return "tenant", agent_run.tenant_id
  879. if scope_value == "agent":
  880. return "agent", agent_run.agent_id
  881. if scope_value == "session" and agent_run.session_id:
  882. return "session", agent_run.session_id
  883. if scope_value == "user":
  884. user_id = self._read_input_json_string(agent_run=agent_run, key="user_id")
  885. if user_id is not None:
  886. return "user", user_id
  887. if scope_value == "team":
  888. team_id = self._read_input_json_string(agent_run=agent_run, key="team_id")
  889. if team_id is not None:
  890. return "team", team_id
  891. return None
  892. def _format_memory_context(self, memory_results: list[MemorySearchResultContract]) -> str:
  893. lines = ["Relevant memories:"]
  894. for index, result in enumerate(memory_results, start=1):
  895. lines.append(f"{index}. {result.item.content_text}")
  896. return "\n".join(lines)
  897. def _format_interaction_memory(self, *, agent_run: AgentRun, output_text: str) -> str:
  898. input_text = agent_run.input_text or str(agent_run.input_json or {})
  899. return f"User input: {input_text}\nAgent output: {output_text}"
  900. def _read_bool(self, payload: dict[str, JSONValue], key: str, *, default: bool) -> bool:
  901. value = payload.get(key)
  902. if isinstance(value, bool):
  903. return value
  904. return default
  905. def _read_int(self, payload: dict[str, JSONValue], key: str, *, default: int) -> int:
  906. value = payload.get(key)
  907. if isinstance(value, int) and not isinstance(value, bool):
  908. return value
  909. return default
  910. def _read_nested_int(
  911. self,
  912. payload: dict[str, JSONValue],
  913. parent_key: str,
  914. child_key: str,
  915. *,
  916. default: int,
  917. ) -> int:
  918. parent_value = payload.get(parent_key)
  919. if not isinstance(parent_value, dict):
  920. return default
  921. return self._read_int(
  922. cast(dict[str, JSONValue], parent_value),
  923. child_key,
  924. default=default,
  925. )
  926. def _read_input_json_string(self, *, agent_run: AgentRun, key: str) -> str | None:
  927. if agent_run.input_json is None:
  928. return None
  929. value = agent_run.input_json.get(key)
  930. if isinstance(value, str) and value:
  931. return value
  932. return None
  933. def build_agent_application_service(
  934. *,
  935. db: Session,
  936. settings: AgentServiceSettings,
  937. ) -> AgentApplicationService:
  938. return AgentApplicationService(
  939. agent_repository=AgentDefinitionRepository(db),
  940. agent_version_repository=AgentVersionRepository(db),
  941. agent_run_repository=AgentRunRepository(db),
  942. agent_tool_invocation_repository=AgentToolInvocationRepository(db),
  943. model_gateway_client=ModelGatewayClient(
  944. base_url=settings.model_gateway_service_url,
  945. timeout_seconds=settings.model_gateway_timeout_seconds,
  946. ),
  947. memory_client=MemoryClient(
  948. base_url=settings.memory_service_url,
  949. timeout_seconds=settings.memory_service_timeout_seconds,
  950. ),
  951. tool_client=ToolServiceClient(
  952. base_url=settings.tool_service_url,
  953. timeout_seconds=settings.tool_service_timeout_seconds,
  954. ),
  955. skill_client=SkillServiceClient(
  956. base_url=settings.skill_service_url,
  957. timeout_seconds=settings.skill_service_timeout_seconds,
  958. ),
  959. event_client=EventServiceClient(
  960. base_url=settings.event_service_url,
  961. timeout_seconds=settings.event_service_timeout_seconds,
  962. ),
  963. )