services.py 35 KB

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