services.py 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309
  1. import json
  2. from datetime import datetime, timedelta
  3. from typing import cast
  4. from sqlalchemy.orm import Session
  5. from core_events import EventPublishContract, EventServiceClient, EventServiceClientError
  6. from core_domain import (
  7. AgentSkillRefContract,
  8. AgentToolRefContract,
  9. ChatCompletionRequestContract,
  10. ChatCompletionResponseContract,
  11. ChatMessageContract,
  12. MemoryCreateContract,
  13. MemoryScopeType,
  14. MemorySearchRequestContract,
  15. MemorySearchResultContract)
  16. from core_shared import JSONValue, try_build_redis_client
  17. from core_shared.task_queue import TaskQueuePublisher
  18. from app.bootstrap.settings import AgentServiceSettings
  19. from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentVersion
  20. from app.domain.repositories import (
  21. AgentDefinitionRepository,
  22. AgentRunRepository,
  23. AgentToolInvocationRepository,
  24. AgentVersionRepository)
  25. from app.infrastructure.model_gateway_client import ModelGatewayClient, ModelGatewayClientError
  26. from app.infrastructure.memory_client import MemoryClient, MemoryClientError
  27. from app.infrastructure.skill_client import SkillServiceClient, SkillServiceClientError
  28. from app.infrastructure.tool_client import ToolServiceClient, ToolServiceClientError
  29. from app.schemas.agent import (
  30. AgentCreateRequest,
  31. AgentRunCreateRequest,
  32. AgentRunExecuteRequest,
  33. AgentRunStatusUpdateRequest,
  34. AgentStatusUpdateRequest,
  35. AgentVersionCreateRequest)
  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. task_queue_publisher: TaskQueuePublisher | None = None,
  50. react_max_steps: int = 5,
  51. react_max_tool_calls: int = 10,
  52. react_tool_retry_count: int = 1) -> None:
  53. self.agent_repository = agent_repository
  54. self.agent_version_repository = agent_version_repository
  55. self.agent_run_repository = agent_run_repository
  56. self.agent_tool_invocation_repository = agent_tool_invocation_repository
  57. self.model_gateway_client = model_gateway_client
  58. self.memory_client = memory_client
  59. self.tool_client = tool_client
  60. self.skill_client = skill_client
  61. self.event_client = event_client
  62. self.task_queue_publisher = task_queue_publisher
  63. self.react_max_steps = react_max_steps
  64. self.react_max_tool_calls = react_max_tool_calls
  65. self.react_tool_retry_count = react_tool_retry_count
  66. def create_agent(self, payload: AgentCreateRequest) -> AgentDefinition:
  67. return self.agent_repository.create(
  68. code=payload.code,
  69. name=payload.name,
  70. description=payload.description,
  71. agent_type=payload.agent_type,
  72. owner_user_id=payload.owner_user_id,
  73. metadata_json=payload.metadata_json)
  74. def list_agents(self) -> list[AgentDefinition]:
  75. return self.agent_repository.list_all()
  76. def update_agent_status(
  77. self,
  78. *,
  79. agent_id: str,
  80. payload: AgentStatusUpdateRequest) -> AgentDefinition | None:
  81. return self.agent_repository.update_status(
  82. agent_id=agent_id,
  83. status=payload.status)
  84. def create_agent_version(self, payload: AgentVersionCreateRequest) -> AgentVersion:
  85. agent = self.agent_repository.get_by_id(
  86. agent_id=payload.agent_id)
  87. if agent is None:
  88. raise ValueError(f"agent not found: {payload.agent_id}")
  89. return self.agent_version_repository.create(
  90. agent_id=payload.agent_id,
  91. status=payload.status,
  92. role=payload.role,
  93. goal=payload.goal,
  94. system_prompt=payload.system_prompt,
  95. model_config_json=payload.model_config_data.model_dump(mode="json"),
  96. memory_policy_json=payload.memory_policy.model_dump(mode="json"),
  97. tool_refs_json=[item.model_dump(mode="json") for item in payload.tool_refs],
  98. skill_refs_json=[item.model_dump(mode="json") for item in payload.skill_refs])
  99. def list_agent_versions(self, *, agent_id: str) -> list[AgentVersion]:
  100. return self.agent_version_repository.list_by_agent(agent_id=agent_id)
  101. def create_agent_run(self, payload: AgentRunCreateRequest) -> AgentRun:
  102. agent_version = self._resolve_agent_version(
  103. agent_id=payload.agent_id,
  104. agent_version_id=payload.agent_version_id)
  105. if agent_version is None:
  106. raise ValueError("published agent version not found")
  107. agent_run = self.agent_run_repository.create(
  108. agent_id=payload.agent_id,
  109. agent_version_id=agent_version.id,
  110. session_id=payload.session_id,
  111. input_text=payload.input_text,
  112. input_json=payload.input_json)
  113. self._publish_event(
  114. event_type="agent.run.created",
  115. agent_run=agent_run,
  116. payload_json={"agent_run_id": agent_run.id, "status": agent_run.status})
  117. if self.task_queue_publisher is not None:
  118. self.task_queue_publisher.publish_agent_run(
  119. agent_run_id=agent_run.id)
  120. return agent_run
  121. def list_agent_runs(
  122. self,
  123. *,
  124. agent_id: str | None = None,
  125. session_id: str | None = None) -> list[AgentRun]:
  126. return self.agent_run_repository.list_by_scope(
  127. agent_id=agent_id,
  128. session_id=session_id)
  129. def list_agent_tool_invocations(
  130. self,
  131. *,
  132. agent_run_id: str) -> list[AgentToolInvocation]:
  133. return self.agent_tool_invocation_repository.list_by_run(
  134. agent_run_id=agent_run_id)
  135. def update_agent_run_status(
  136. self,
  137. *,
  138. agent_run_id: str,
  139. payload: AgentRunStatusUpdateRequest) -> AgentRun | None:
  140. entity = self.agent_run_repository.get_by_id(
  141. agent_run_id=agent_run_id)
  142. if entity is None:
  143. return None
  144. return self.agent_run_repository.update_status(
  145. agent_run_id=agent_run_id,
  146. status=payload.status,
  147. worker_key=payload.worker_key,
  148. output_text=payload.output_text,
  149. output_json=payload.output_json,
  150. error_code=payload.error_code,
  151. error_message=payload.error_message)
  152. def execute_agent_run(
  153. self,
  154. *,
  155. agent_run_id: str,
  156. payload: AgentRunExecuteRequest) -> AgentRun | None:
  157. agent_run = self.agent_run_repository.get_by_id(
  158. agent_run_id=agent_run_id)
  159. if agent_run is None:
  160. return None
  161. agent_version = self.agent_version_repository.get_by_id(
  162. agent_version_id=agent_run.agent_version_id)
  163. if agent_version is None:
  164. return self.agent_run_repository.update_status(
  165. agent_run_id=agent_run.id,
  166. status="failed",
  167. worker_key=payload.worker_key,
  168. error_code="agent_version_missing",
  169. error_message=f"agent version not found: {agent_run.agent_version_id}")
  170. self.agent_run_repository.update_status(
  171. agent_run_id=agent_run.id,
  172. status="running",
  173. worker_key=payload.worker_key)
  174. memory_results, memory_metadata = self._read_relevant_memories(
  175. agent_run=agent_run,
  176. agent_version=agent_version)
  177. selected_tools = self._select_tool_refs(agent_run=agent_run, agent_version=agent_version)
  178. selected_skills = self._select_skill_refs(agent_run=agent_run, agent_version=agent_version)
  179. if payload.dry_run:
  180. messages = self._build_chat_messages(
  181. agent_run=agent_run,
  182. agent_version=agent_version,
  183. memory_results=memory_results,
  184. capability_context=self._format_capability_plan(
  185. selected_tools=selected_tools,
  186. selected_skills=selected_skills))
  187. completed_run = self.agent_run_repository.update_status(
  188. agent_run_id=agent_run.id,
  189. status="completed",
  190. worker_key=payload.worker_key,
  191. output_text=self._build_dry_run_output(
  192. agent_run=agent_run,
  193. agent_version=agent_version),
  194. output_json={
  195. "dry_run": True,
  196. "agent_version_id": agent_version.id,
  197. "message_count": len(messages),
  198. "messages": [message.model_dump(mode="json") for message in messages],
  199. "selected_tool_refs": [
  200. tool_ref.model_dump(mode="json") for tool_ref in selected_tools
  201. ],
  202. "selected_skill_refs": [
  203. skill_ref.model_dump(mode="json") for skill_ref in selected_skills
  204. ],
  205. **memory_metadata,
  206. })
  207. if completed_run is not None:
  208. self._publish_event(
  209. event_type="agent.run.completed",
  210. agent_run=completed_run,
  211. payload_json={
  212. "agent_run_id": completed_run.id,
  213. "dry_run": True,
  214. "status": completed_run.status,
  215. })
  216. return completed_run
  217. if self._read_bool(agent_version.model_config_json, "react_enabled", default=False):
  218. return self._execute_react_agent_run(
  219. agent_run=agent_run,
  220. agent_version=agent_version,
  221. payload=payload,
  222. memory_results=memory_results,
  223. memory_metadata=memory_metadata,
  224. selected_tools=selected_tools,
  225. selected_skills=selected_skills)
  226. tool_invocations = self._invoke_selected_tools(
  227. agent_run=agent_run,
  228. agent_version=agent_version,
  229. selected_tools=selected_tools)
  230. skill_invocations = self._invoke_selected_skills(
  231. agent_run=agent_run,
  232. selected_skills=selected_skills,
  233. worker_key=payload.worker_key)
  234. messages = self._build_chat_messages(
  235. agent_run=agent_run,
  236. agent_version=agent_version,
  237. memory_results=memory_results,
  238. capability_context=self._format_capability_results(
  239. tool_invocations=tool_invocations,
  240. skill_invocations=skill_invocations))
  241. if self.model_gateway_client is None:
  242. return self.agent_run_repository.update_status(
  243. agent_run_id=agent_run.id,
  244. status="failed",
  245. worker_key=payload.worker_key,
  246. error_code="model_gateway_missing",
  247. error_message="model gateway client is not configured",
  248. output_json={
  249. "tool_invocations": tool_invocations,
  250. "skill_invocations": skill_invocations,
  251. **memory_metadata,
  252. })
  253. try:
  254. response = self.model_gateway_client.create_chat_completion(
  255. ChatCompletionRequestContract(
  256. model=self._read_optional_string(agent_version.model_config_json, "model"),
  257. temperature=self._read_optional_float(
  258. agent_version.model_config_json,
  259. "temperature"),
  260. max_tokens=self._read_optional_int(
  261. agent_version.model_config_json,
  262. "max_tokens"),
  263. messages=messages,
  264. metadata_json={
  265. "agent_id": agent_run.agent_id,
  266. "agent_version_id": agent_version.id,
  267. "agent_run_id": agent_run.id,
  268. })
  269. )
  270. except ModelGatewayClientError as exc:
  271. return self.agent_run_repository.update_status(
  272. agent_run_id=agent_run.id,
  273. status="failed",
  274. worker_key=payload.worker_key,
  275. error_code="model_gateway_error",
  276. error_message=str(exc))
  277. memory_write_metadata = self._write_interaction_memory(
  278. agent_run=agent_run,
  279. agent_version=agent_version,
  280. output_text=response.content)
  281. completed_run = self.agent_run_repository.update_status(
  282. agent_run_id=agent_run.id,
  283. status="completed",
  284. worker_key=payload.worker_key,
  285. output_text=response.content,
  286. output_json={
  287. "dry_run": False,
  288. "agent_version_id": agent_version.id,
  289. "model": response.model,
  290. "finish_reason": response.finish_reason,
  291. "usage_json": response.usage_json,
  292. "raw_response_json": response.raw_response_json,
  293. "tool_invocations": tool_invocations,
  294. "skill_invocations": skill_invocations,
  295. **memory_metadata,
  296. **memory_write_metadata,
  297. })
  298. if completed_run is not None:
  299. self._publish_event(
  300. event_type="agent.run.completed",
  301. agent_run=completed_run,
  302. payload_json={
  303. "agent_run_id": completed_run.id,
  304. "dry_run": False,
  305. "status": completed_run.status,
  306. })
  307. return completed_run
  308. def _publish_event(
  309. self,
  310. *,
  311. event_type: str,
  312. agent_run: AgentRun,
  313. payload_json: dict[str, JSONValue]) -> None:
  314. if self.event_client is None:
  315. return
  316. try:
  317. self.event_client.publish_event(
  318. EventPublishContract(
  319. event_type=event_type,
  320. source_service="agent-service",
  321. aggregate_type="agent_run",
  322. aggregate_id=agent_run.id,
  323. correlation_id=agent_run.session_id,
  324. payload_json={
  325. **payload_json,
  326. "agent_id": agent_run.agent_id,
  327. "agent_version_id": agent_run.agent_version_id,
  328. })
  329. )
  330. except EventServiceClientError:
  331. return
  332. def _execute_react_agent_run(
  333. self,
  334. *,
  335. agent_run: AgentRun,
  336. agent_version: AgentVersion,
  337. payload: AgentRunExecuteRequest,
  338. memory_results: list[MemorySearchResultContract],
  339. memory_metadata: dict[str, JSONValue],
  340. selected_tools: list[AgentToolRefContract],
  341. selected_skills: list[AgentSkillRefContract]) -> AgentRun | None:
  342. if self.model_gateway_client is None:
  343. return self.agent_run_repository.update_status(
  344. agent_run_id=agent_run.id,
  345. status="failed",
  346. worker_key=payload.worker_key,
  347. error_code="model_gateway_missing",
  348. error_message="model gateway client is not configured")
  349. skill_invocations = self._invoke_selected_skills(
  350. agent_run=agent_run,
  351. selected_skills=selected_skills,
  352. worker_key=payload.worker_key)
  353. messages = self._build_chat_messages(
  354. agent_run=agent_run,
  355. agent_version=agent_version,
  356. memory_results=memory_results,
  357. capability_context=self._format_react_instruction(
  358. agent_run=agent_run,
  359. selected_tools=selected_tools,
  360. skill_invocations=skill_invocations))
  361. react_steps: list[dict[str, JSONValue]] = []
  362. tool_invocations: list[dict[str, JSONValue]] = []
  363. final_answer: str | None = None
  364. tool_call_count = 0
  365. max_steps = self._read_int(
  366. agent_version.model_config_json,
  367. "react_max_steps",
  368. default=self.react_max_steps)
  369. for step_index in range(max(max_steps, 1)):
  370. try:
  371. response = self.model_gateway_client.create_chat_completion(
  372. self._build_chat_completion_request(
  373. agent_run=agent_run,
  374. agent_version=agent_version,
  375. messages=messages,
  376. selected_tools=selected_tools)
  377. )
  378. except ModelGatewayClientError as exc:
  379. return self.agent_run_repository.update_status(
  380. agent_run_id=agent_run.id,
  381. status="failed",
  382. worker_key=payload.worker_key,
  383. error_code="model_gateway_error",
  384. error_message=str(exc),
  385. output_json={
  386. "react_steps": react_steps,
  387. "tool_invocations": tool_invocations,
  388. "skill_invocations": skill_invocations,
  389. **memory_metadata,
  390. })
  391. action = self._parse_react_action_from_response(response)
  392. react_step: dict[str, JSONValue] = {
  393. "step_index": step_index,
  394. "model_content": response.content,
  395. "action": action,
  396. }
  397. react_steps.append(react_step)
  398. if action.get("action") == "finish":
  399. answer_value = action.get("answer")
  400. final_answer = answer_value if isinstance(answer_value, str) else response.content
  401. break
  402. if action.get("action") != "tool":
  403. final_answer = response.content
  404. break
  405. max_tool_calls = self._read_int(
  406. agent_version.model_config_json,
  407. "react_max_tool_calls",
  408. default=self.react_max_tool_calls)
  409. if tool_call_count >= max(max_tool_calls, 0):
  410. final_answer = "Tool call budget exhausted."
  411. react_step["observation"] = final_answer
  412. break
  413. tool_code = action.get("tool_code")
  414. matching_tools = [
  415. item for item in selected_tools if item.tool_code == tool_code
  416. ]
  417. if not matching_tools:
  418. observation = f"tool not available: {tool_code}"
  419. react_step["observation"] = observation
  420. messages.append(ChatMessageContract(role="assistant", content=response.content))
  421. messages.append(ChatMessageContract(role="user", content=observation))
  422. continue
  423. tool_input = action.get("input_json")
  424. original_input_json = agent_run.input_json
  425. if isinstance(tool_input, dict):
  426. agent_run.input_json = {
  427. str(item_key): item_value for item_key, item_value in tool_input.items()
  428. }
  429. current_invocations = self._invoke_react_tool_with_retry(
  430. agent_run=agent_run,
  431. agent_version=agent_version,
  432. tool_ref=matching_tools[0])
  433. tool_call_count += len(current_invocations)
  434. agent_run.input_json = original_input_json
  435. tool_invocations.extend(current_invocations)
  436. observation = self._format_react_observation(current_invocations)
  437. react_step["observation"] = observation
  438. messages.append(ChatMessageContract(role="assistant", content=response.content))
  439. messages.append(ChatMessageContract(role="user", content=observation))
  440. if final_answer is None:
  441. final_answer = "ReAct loop reached max steps without a final answer."
  442. memory_write_metadata = self._write_interaction_memory(
  443. agent_run=agent_run,
  444. agent_version=agent_version,
  445. output_text=final_answer)
  446. completed_run = self.agent_run_repository.update_status(
  447. agent_run_id=agent_run.id,
  448. status="completed",
  449. worker_key=payload.worker_key,
  450. output_text=final_answer,
  451. output_json={
  452. "dry_run": False,
  453. "agent_version_id": agent_version.id,
  454. "react_enabled": True,
  455. "react_steps": react_steps,
  456. "react_tool_call_count": tool_call_count,
  457. "tool_invocations": tool_invocations,
  458. "skill_invocations": skill_invocations,
  459. **memory_metadata,
  460. **memory_write_metadata,
  461. })
  462. if completed_run is not None:
  463. self._publish_event(
  464. event_type="agent.run.completed",
  465. agent_run=completed_run,
  466. payload_json={
  467. "agent_run_id": completed_run.id,
  468. "react_enabled": True,
  469. "status": completed_run.status,
  470. })
  471. return completed_run
  472. def execute_next_claimed_agent_run(
  473. self,
  474. *,
  475. worker_key: str,
  476. lease_seconds: int,
  477. dry_run: bool,
  478. redis_client: object | None = None) -> tuple[AgentRun, int] | None:
  479. released_lease_count = self.agent_run_repository.release_expired_leases(
  480. now_time=datetime.utcnow())
  481. claimed_agent_run = self.agent_run_repository.claim_next_queued(
  482. worker_key=worker_key,
  483. lease_expire_time=datetime.utcnow() + timedelta(seconds=lease_seconds))
  484. if claimed_agent_run is None:
  485. return None
  486. if redis_client is not None:
  487. from core_shared.redis_primitives import DistributedLock, IdempotencyStore
  488. lock = DistributedLock(
  489. client=redis_client,
  490. name=f"agent-run:{claimed_agent_run.id}:lock",
  491. ttl_seconds=lease_seconds)
  492. if not lock.acquire():
  493. return None
  494. idempotency_store = IdempotencyStore(
  495. client=redis_client,
  496. prefix="agent-run-idempotency")
  497. if not idempotency_store.begin(key=claimed_agent_run.id):
  498. lock.release()
  499. return None
  500. else:
  501. lock = None
  502. idempotency_store = None
  503. try:
  504. result = self.execute_agent_run(
  505. agent_run_id=claimed_agent_run.id,
  506. payload=AgentRunExecuteRequest(
  507. worker_key=worker_key,
  508. dry_run=dry_run))
  509. if idempotency_store is not None and result is not None:
  510. idempotency_store.complete(
  511. key=claimed_agent_run.id,
  512. result={"status": result.status, "agent_run_id": result.id})
  513. finally:
  514. if lock is not None:
  515. lock.release()
  516. if result is None:
  517. return None
  518. return result, released_lease_count
  519. def _resolve_agent_version(
  520. self,
  521. *,
  522. agent_id: str,
  523. agent_version_id: str | None) -> AgentVersion | None:
  524. if agent_version_id is not None:
  525. return self.agent_version_repository.get_by_id(
  526. agent_version_id=agent_version_id)
  527. return self.agent_version_repository.get_latest_published(
  528. agent_id=agent_id)
  529. def _build_chat_messages(
  530. self,
  531. *,
  532. agent_run: AgentRun,
  533. agent_version: AgentVersion,
  534. memory_results: list[MemorySearchResultContract] | None = None,
  535. capability_context: str | None = None) -> list[ChatMessageContract]:
  536. messages = [
  537. ChatMessageContract(role="system", content=agent_version.system_prompt),
  538. ]
  539. if agent_version.goal:
  540. messages.append(
  541. ChatMessageContract(role="system", content=f"Goal: {agent_version.goal}")
  542. )
  543. if memory_results:
  544. messages.append(
  545. ChatMessageContract(
  546. role="system",
  547. content=self._format_memory_context(memory_results))
  548. )
  549. if capability_context:
  550. messages.append(ChatMessageContract(role="system", content=capability_context))
  551. if agent_run.input_text:
  552. messages.append(ChatMessageContract(role="user", content=agent_run.input_text))
  553. if agent_run.input_json:
  554. messages.append(
  555. ChatMessageContract(
  556. role="user",
  557. content=f"Structured input: {agent_run.input_json}")
  558. )
  559. return messages
  560. def _select_tool_refs(
  561. self,
  562. *,
  563. agent_run: AgentRun,
  564. agent_version: AgentVersion) -> list[AgentToolRefContract]:
  565. input_preview = self._build_input_preview(agent_run)
  566. selected: list[AgentToolRefContract] = []
  567. for item in agent_version.tool_refs_json:
  568. ref = AgentToolRefContract.model_validate(item)
  569. if (
  570. ref.required
  571. or self._read_bool(ref.config_json, "auto_invoke", default=False)
  572. or self._matches_selection_keywords(ref.config_json, input_preview)
  573. ):
  574. selected.append(ref)
  575. return selected
  576. def _select_skill_refs(
  577. self,
  578. *,
  579. agent_run: AgentRun,
  580. agent_version: AgentVersion) -> list[AgentSkillRefContract]:
  581. input_preview = self._build_input_preview(agent_run)
  582. selected: list[AgentSkillRefContract] = []
  583. for item in agent_version.skill_refs_json:
  584. ref = AgentSkillRefContract.model_validate(item)
  585. auto_invoke = self._read_bool(ref.config_json, "auto_invoke", default=True)
  586. if auto_invoke or self._matches_selection_keywords(ref.config_json, input_preview):
  587. selected.append(ref)
  588. return selected
  589. def _invoke_selected_tools(
  590. self,
  591. *,
  592. agent_run: AgentRun,
  593. agent_version: AgentVersion,
  594. selected_tools: list[AgentToolRefContract]) -> list[dict[str, JSONValue]]:
  595. invocations: list[dict[str, JSONValue]] = []
  596. for ref in selected_tools:
  597. invocation = self.agent_tool_invocation_repository.create(
  598. agent_run_id=agent_run.id,
  599. agent_id=agent_run.agent_id,
  600. agent_version_id=agent_version.id,
  601. tool_code=ref.tool_code,
  602. tool_binding_id=ref.tool_binding_id,
  603. status="selected",
  604. input_json=agent_run.input_json or {})
  605. if ref.tool_binding_id is None:
  606. self.agent_tool_invocation_repository.update_status(
  607. invocation_id=invocation.id,
  608. status="skipped",
  609. reason="tool_binding_id_missing")
  610. invocations.append(
  611. {
  612. "status": "skipped",
  613. "reason": "tool_binding_id_missing",
  614. "tool_code": ref.tool_code,
  615. }
  616. )
  617. continue
  618. if self.tool_client is None:
  619. self.agent_tool_invocation_repository.update_status(
  620. invocation_id=invocation.id,
  621. status="failed",
  622. reason="tool_client_missing",
  623. error_message="tool client is not configured")
  624. invocations.append(
  625. {
  626. "status": "failed",
  627. "reason": "tool_client_missing",
  628. "tool_binding_id": ref.tool_binding_id,
  629. }
  630. )
  631. continue
  632. try:
  633. self.agent_tool_invocation_repository.update_status(
  634. invocation_id=invocation.id,
  635. status="running")
  636. detail = self.tool_client.get_tool_binding_detail(
  637. binding_id=ref.tool_binding_id)
  638. if not detail.binding.enabled:
  639. self.agent_tool_invocation_repository.update_status(
  640. invocation_id=invocation.id,
  641. status="failed",
  642. reason="tool_binding_disabled",
  643. error_message="tool binding is disabled")
  644. invocations.append(
  645. {
  646. "status": "failed",
  647. "reason": "tool_binding_disabled",
  648. "tool_binding_id": ref.tool_binding_id,
  649. }
  650. )
  651. continue
  652. if detail.tool_definition.tool_type != "http":
  653. self.agent_tool_invocation_repository.update_status(
  654. invocation_id=invocation.id,
  655. status="skipped",
  656. reason="unsupported_tool_type",
  657. error_message=detail.tool_definition.tool_type)
  658. invocations.append(
  659. {
  660. "status": "skipped",
  661. "reason": "unsupported_tool_type",
  662. "tool_type": detail.tool_definition.tool_type,
  663. "tool_binding_id": ref.tool_binding_id,
  664. }
  665. )
  666. continue
  667. output_text, output_json = self.tool_client.invoke_http_tool(
  668. detail=detail,
  669. input_json=agent_run.input_json or {},
  670. config_json=ref.config_json)
  671. except ToolServiceClientError as exc:
  672. self.agent_tool_invocation_repository.update_status(
  673. invocation_id=invocation.id,
  674. status="failed",
  675. reason="tool_service_error",
  676. error_message=str(exc))
  677. invocations.append(
  678. {
  679. "status": "failed",
  680. "reason": str(exc),
  681. "tool_binding_id": ref.tool_binding_id,
  682. }
  683. )
  684. continue
  685. self.agent_tool_invocation_repository.update_status(
  686. invocation_id=invocation.id,
  687. status="completed",
  688. output_text=output_text,
  689. output_json=output_json)
  690. invocations.append(
  691. {
  692. "status": "completed",
  693. "tool_binding_id": ref.tool_binding_id,
  694. "tool_code": detail.tool_definition.code,
  695. "output_text": output_text,
  696. "output_json": output_json,
  697. }
  698. )
  699. return invocations
  700. def _invoke_selected_skills(
  701. self,
  702. *,
  703. agent_run: AgentRun,
  704. selected_skills: list[AgentSkillRefContract],
  705. worker_key: str | None) -> list[dict[str, JSONValue]]:
  706. invocations: list[dict[str, JSONValue]] = []
  707. for ref in selected_skills:
  708. if self.skill_client is None:
  709. invocations.append(
  710. {
  711. "status": "failed",
  712. "reason": "skill_client_missing",
  713. "skill_id": ref.skill_id,
  714. "skill_code": ref.skill_code,
  715. }
  716. )
  717. continue
  718. skill_id = ref.skill_id or self._resolve_skill_id_by_code(
  719. skill_code=ref.skill_code)
  720. if skill_id is None:
  721. invocations.append(
  722. {
  723. "status": "failed",
  724. "reason": "skill_id_missing",
  725. "skill_code": ref.skill_code,
  726. }
  727. )
  728. continue
  729. try:
  730. created_run = self.skill_client.create_skill_run(
  731. skill_id=skill_id,
  732. skill_version_id=self._read_optional_string(
  733. ref.config_json,
  734. "skill_version_id"),
  735. installation_id=self._read_optional_string(
  736. ref.config_json,
  737. "installation_id"),
  738. input_json=self._build_skill_input_json(agent_run=agent_run, ref=ref))
  739. executed_run = self.skill_client.execute_skill_run(
  740. skill_run_id=created_run.id,
  741. worker_key=worker_key)
  742. except SkillServiceClientError as exc:
  743. invocations.append(
  744. {
  745. "status": "failed",
  746. "reason": str(exc),
  747. "skill_id": skill_id,
  748. "skill_code": ref.skill_code,
  749. }
  750. )
  751. continue
  752. invocations.append(
  753. {
  754. "status": executed_run.status,
  755. "skill_id": skill_id,
  756. "skill_code": ref.skill_code,
  757. "skill_run_id": executed_run.id,
  758. "output_text": executed_run.output_text,
  759. "output_json": executed_run.output_json,
  760. "error_code": executed_run.error_code,
  761. "error_message": executed_run.error_message,
  762. }
  763. )
  764. return invocations
  765. def _format_capability_plan(
  766. self,
  767. *,
  768. selected_tools: list[AgentToolRefContract],
  769. selected_skills: list[AgentSkillRefContract]) -> str:
  770. return (
  771. "Selected capability plan before model call:\n"
  772. f"Tools: {[item.model_dump(mode='json') for item in selected_tools]}\n"
  773. f"Skills: {[item.model_dump(mode='json') for item in selected_skills]}"
  774. )
  775. def _format_capability_results(
  776. self,
  777. *,
  778. tool_invocations: list[dict[str, JSONValue]],
  779. skill_invocations: list[dict[str, JSONValue]]) -> str | None:
  780. if not tool_invocations and not skill_invocations:
  781. return None
  782. return (
  783. "Capability invocation results before model call:\n"
  784. f"Tools: {tool_invocations}\n"
  785. f"Skills: {skill_invocations}"
  786. )
  787. def _format_react_instruction(
  788. self,
  789. *,
  790. agent_run: AgentRun,
  791. selected_tools: list[AgentToolRefContract],
  792. skill_invocations: list[dict[str, JSONValue]]) -> str:
  793. tool_schemas = self._build_react_tool_schemas(
  794. agent_run=agent_run,
  795. selected_tools=selected_tools)
  796. return (
  797. "Use ReAct JSON only. Respond with one JSON object per turn.\n"
  798. "To call a tool: "
  799. '{"action":"tool","tool_code":"code","input_json":{...}}\n'
  800. "To finish: "
  801. '{"action":"finish","answer":"final answer"}\n'
  802. f"Available tools: {tool_schemas}\n"
  803. f"Pre-run skill results: {skill_invocations}"
  804. )
  805. def _build_react_tool_schemas(
  806. self,
  807. *,
  808. agent_run: AgentRun,
  809. selected_tools: list[AgentToolRefContract]) -> list[dict[str, JSONValue]]:
  810. schemas: list[dict[str, JSONValue]] = []
  811. for ref in selected_tools:
  812. schema: dict[str, JSONValue] = {
  813. "tool_code": ref.tool_code,
  814. "tool_binding_id": ref.tool_binding_id,
  815. "required": ref.required,
  816. "config_json": ref.config_json,
  817. }
  818. if ref.tool_binding_id is not None and self.tool_client is not None:
  819. try:
  820. detail = self.tool_client.get_tool_binding_detail(
  821. binding_id=ref.tool_binding_id)
  822. schema.update(
  823. {
  824. "name": detail.tool_definition.name,
  825. "description": detail.tool_definition.description,
  826. "tool_type": detail.tool_definition.tool_type,
  827. "input_schema_json": detail.tool_version.input_schema_json or {},
  828. "output_schema_json": detail.tool_version.output_schema_json or {},
  829. "timeout_ms": detail.tool_version.timeout_ms,
  830. }
  831. )
  832. except ToolServiceClientError as exc:
  833. schema["schema_error"] = str(exc)
  834. schemas.append(schema)
  835. return schemas
  836. def _invoke_react_tool_with_retry(
  837. self,
  838. *,
  839. agent_run: AgentRun,
  840. agent_version: AgentVersion,
  841. tool_ref: AgentToolRefContract) -> list[dict[str, JSONValue]]:
  842. retry_count = self._read_int(
  843. agent_version.model_config_json,
  844. "react_tool_retry_count",
  845. default=self.react_tool_retry_count)
  846. attempts: list[dict[str, JSONValue]] = []
  847. for attempt_index in range(max(retry_count, 0) + 1):
  848. current = self._invoke_selected_tools(
  849. agent_run=agent_run,
  850. agent_version=agent_version,
  851. selected_tools=[tool_ref])
  852. for item in current:
  853. item["attempt_index"] = attempt_index
  854. attempts.extend(current)
  855. if current and current[-1].get("status") == "completed":
  856. break
  857. return attempts
  858. def _format_react_observation(
  859. self,
  860. tool_invocations: list[dict[str, JSONValue]]) -> str:
  861. return f"Observation: {tool_invocations}"
  862. def _parse_react_action(self, content: str) -> dict[str, JSONValue]:
  863. try:
  864. value = json.loads(content)
  865. except json.JSONDecodeError:
  866. start_index = content.find("{")
  867. end_index = content.rfind("}")
  868. if start_index < 0 or end_index <= start_index:
  869. return {"action": "finish", "answer": content}
  870. try:
  871. value = json.loads(content[start_index : end_index + 1])
  872. except json.JSONDecodeError:
  873. return {"action": "finish", "answer": content}
  874. if not isinstance(value, dict):
  875. return {"action": "finish", "answer": content}
  876. return {str(item_key): item_value for item_key, item_value in value.items()}
  877. def _parse_react_action_from_response(
  878. self,
  879. response: ChatCompletionResponseContract) -> dict[str, JSONValue]:
  880. if response.tool_calls_json:
  881. action = self._parse_openai_tool_call(response.tool_calls_json[0])
  882. if action is not None:
  883. return action
  884. return self._parse_react_action(response.content)
  885. def _parse_openai_tool_call(
  886. self,
  887. tool_call: dict[str, JSONValue]) -> dict[str, JSONValue] | None:
  888. function_value = tool_call.get("function")
  889. if not isinstance(function_value, dict):
  890. return None
  891. tool_code = function_value.get("name")
  892. if not isinstance(tool_code, str) or not tool_code:
  893. return None
  894. raw_arguments = function_value.get("arguments")
  895. input_json: dict[str, JSONValue] = {}
  896. if isinstance(raw_arguments, str) and raw_arguments:
  897. try:
  898. decoded = json.loads(raw_arguments)
  899. except json.JSONDecodeError:
  900. decoded = {"raw_arguments": raw_arguments}
  901. if isinstance(decoded, dict):
  902. input_json = {str(item_key): item_value for item_key, item_value in decoded.items()}
  903. elif isinstance(raw_arguments, dict):
  904. input_json = {str(item_key): item_value for item_key, item_value in raw_arguments.items()}
  905. tool_call_id = tool_call.get("id")
  906. return {
  907. "action": "tool",
  908. "tool_code": tool_code,
  909. "input_json": input_json,
  910. "tool_call_id": tool_call_id if isinstance(tool_call_id, str) else None,
  911. "tool_call_protocol": "openai",
  912. }
  913. def _build_chat_completion_request(
  914. self,
  915. *,
  916. agent_run: AgentRun,
  917. agent_version: AgentVersion,
  918. messages: list[ChatMessageContract],
  919. selected_tools: list[AgentToolRefContract] | None = None) -> ChatCompletionRequestContract:
  920. function_calling_enabled = self._read_bool(
  921. agent_version.model_config_json,
  922. "function_calling_enabled",
  923. default=False) or self._read_bool(
  924. agent_version.model_config_json,
  925. "tool_calling_enabled",
  926. default=False)
  927. return ChatCompletionRequestContract(
  928. model=self._read_optional_string(agent_version.model_config_json, "model"),
  929. temperature=self._read_optional_float(
  930. agent_version.model_config_json,
  931. "temperature"),
  932. max_tokens=self._read_optional_int(
  933. agent_version.model_config_json,
  934. "max_tokens"),
  935. messages=messages,
  936. tools_json=(
  937. self._build_openai_tool_schemas(
  938. agent_run=agent_run,
  939. selected_tools=selected_tools or [])
  940. if function_calling_enabled
  941. else []
  942. ),
  943. tool_choice="auto" if function_calling_enabled and selected_tools else None,
  944. metadata_json={
  945. "agent_id": agent_run.agent_id,
  946. "agent_version_id": agent_version.id,
  947. "agent_run_id": agent_run.id,
  948. })
  949. def _build_openai_tool_schemas(
  950. self,
  951. *,
  952. agent_run: AgentRun,
  953. selected_tools: list[AgentToolRefContract]) -> list[dict[str, JSONValue]]:
  954. tool_schemas: list[dict[str, JSONValue]] = []
  955. for schema in self._build_react_tool_schemas(
  956. agent_run=agent_run,
  957. selected_tools=selected_tools):
  958. tool_code = schema.get("tool_code")
  959. if not isinstance(tool_code, str) or not tool_code:
  960. continue
  961. description = schema.get("description")
  962. input_schema = schema.get("input_schema_json")
  963. tool_schemas.append(
  964. {
  965. "type": "function",
  966. "function": {
  967. "name": tool_code,
  968. "description": description if isinstance(description, str) else "",
  969. "parameters": input_schema if isinstance(input_schema, dict) else {},
  970. },
  971. }
  972. )
  973. return tool_schemas
  974. def _build_skill_input_json(
  975. self,
  976. *,
  977. agent_run: AgentRun,
  978. ref: AgentSkillRefContract) -> dict[str, JSONValue]:
  979. input_json: dict[str, JSONValue] = dict(agent_run.input_json or {})
  980. if agent_run.input_text:
  981. input_json.setdefault("input_text", agent_run.input_text)
  982. configured_input = ref.config_json.get("input_json")
  983. if isinstance(configured_input, dict):
  984. input_json.update(
  985. {str(item_key): item_value for item_key, item_value in configured_input.items()}
  986. )
  987. return input_json
  988. def _resolve_skill_id_by_code(self, *, skill_code: str | None) -> str | None:
  989. if skill_code is None or self.skill_client is None:
  990. return None
  991. try:
  992. skills = self.skill_client.list_skills()
  993. except SkillServiceClientError:
  994. return None
  995. for skill in skills:
  996. if skill.code == skill_code:
  997. return skill.id
  998. return None
  999. def _build_input_preview(self, agent_run: AgentRun) -> str:
  1000. return f"{agent_run.input_text or ''} {agent_run.input_json or {}}".lower()
  1001. def _matches_selection_keywords(
  1002. self,
  1003. config_json: dict[str, JSONValue],
  1004. input_preview: str) -> bool:
  1005. keywords = config_json.get("selection_keywords")
  1006. if not isinstance(keywords, list):
  1007. return False
  1008. return any(
  1009. isinstance(keyword, str) and keyword.lower() in input_preview
  1010. for keyword in keywords
  1011. )
  1012. def _build_dry_run_output(self, *, agent_run: AgentRun, agent_version: AgentVersion) -> str:
  1013. input_preview = agent_run.input_text or str(agent_run.input_json or {})
  1014. return (
  1015. f"[dry-run] Agent role={agent_version.role} "
  1016. f"version={agent_version.version_no} received: {input_preview}"
  1017. )
  1018. def _read_optional_string(self, payload: dict[str, JSONValue], key: str) -> str | None:
  1019. value = payload.get(key)
  1020. if isinstance(value, str) and value:
  1021. return value
  1022. return None
  1023. def _read_optional_float(self, payload: dict[str, JSONValue], key: str) -> float | None:
  1024. value = payload.get(key)
  1025. if isinstance(value, (int, float)) and not isinstance(value, bool):
  1026. return float(value)
  1027. return None
  1028. def _read_optional_int(self, payload: dict[str, JSONValue], key: str) -> int | None:
  1029. value = payload.get(key)
  1030. if isinstance(value, int) and not isinstance(value, bool):
  1031. return value
  1032. return None
  1033. def _read_relevant_memories(
  1034. self,
  1035. *,
  1036. agent_run: AgentRun,
  1037. agent_version: AgentVersion) -> tuple[list[MemorySearchResultContract], dict[str, JSONValue]]:
  1038. if self.memory_client is None:
  1039. return [], {"memory_read_enabled": False, "memory_read_reason": "client_missing"}
  1040. if not self._read_bool(agent_version.memory_policy_json, "enabled", default=True):
  1041. return [], {"memory_read_enabled": False, "memory_read_reason": "policy_disabled"}
  1042. query = agent_run.input_text or str(agent_run.input_json or "")
  1043. if not query:
  1044. return [], {"memory_read_enabled": True, "memory_read_count": 0}
  1045. scope = self._resolve_memory_scope(agent_run=agent_run, agent_version=agent_version)
  1046. if scope is None:
  1047. return [], {
  1048. "memory_read_enabled": True,
  1049. "memory_read_count": 0,
  1050. "memory_read_reason": "scope_unavailable",
  1051. }
  1052. scope_type, scope_id = scope
  1053. try:
  1054. results = self.memory_client.search_memories(
  1055. MemorySearchRequestContract(
  1056. query=query,
  1057. scope_type=scope_type,
  1058. scope_id=scope_id,
  1059. owner_agent_id=agent_run.agent_id,
  1060. session_id=agent_run.session_id,
  1061. limit=self._read_int(
  1062. agent_version.memory_policy_json,
  1063. "read_top_k",
  1064. default=8))
  1065. )
  1066. except MemoryClientError as exc:
  1067. return [], {
  1068. "memory_read_enabled": True,
  1069. "memory_read_count": 0,
  1070. "memory_read_error": str(exc),
  1071. }
  1072. return results, {
  1073. "memory_read_enabled": True,
  1074. "memory_read_count": len(results),
  1075. "memory_scope_type": scope_type,
  1076. "memory_scope_id": scope_id,
  1077. }
  1078. def _write_interaction_memory(
  1079. self,
  1080. *,
  1081. agent_run: AgentRun,
  1082. agent_version: AgentVersion,
  1083. output_text: str) -> dict[str, JSONValue]:
  1084. if self.memory_client is None:
  1085. return {"memory_write_enabled": False, "memory_write_reason": "client_missing"}
  1086. if not self._read_bool(agent_version.memory_policy_json, "write_enabled", default=True):
  1087. return {"memory_write_enabled": False, "memory_write_reason": "policy_disabled"}
  1088. scope = self._resolve_memory_scope(agent_run=agent_run, agent_version=agent_version)
  1089. if scope is None:
  1090. return {"memory_write_enabled": True, "memory_write_reason": "scope_unavailable"}
  1091. scope_type, scope_id = scope
  1092. try:
  1093. memory = self.memory_client.create_memory(
  1094. MemoryCreateContract(
  1095. scope_type=scope_type,
  1096. scope_id=scope_id,
  1097. memory_type="conversation",
  1098. content_text=self._format_interaction_memory(
  1099. agent_run=agent_run,
  1100. output_text=output_text),
  1101. content_json={
  1102. "agent_run_id": agent_run.id,
  1103. "agent_version_id": agent_version.id,
  1104. "input_text": agent_run.input_text,
  1105. "output_text": output_text,
  1106. },
  1107. metadata_json={
  1108. "source": "agent-service",
  1109. "role": agent_version.role,
  1110. "version_no": agent_version.version_no,
  1111. },
  1112. owner_agent_id=agent_run.agent_id,
  1113. session_id=agent_run.session_id,
  1114. source_ref=f"agent_run:{agent_run.id}",
  1115. importance_score=self._read_nested_int(
  1116. agent_version.memory_policy_json,
  1117. "config_json",
  1118. "write_importance_score",
  1119. default=50))
  1120. )
  1121. except MemoryClientError as exc:
  1122. return {
  1123. "memory_write_enabled": True,
  1124. "memory_write_error": str(exc),
  1125. }
  1126. return {
  1127. "memory_write_enabled": True,
  1128. "memory_written_id": memory.id,
  1129. "memory_scope_type": scope_type,
  1130. "memory_scope_id": scope_id,
  1131. }
  1132. def _resolve_memory_scope(
  1133. self,
  1134. *,
  1135. agent_run: AgentRun,
  1136. agent_version: AgentVersion) -> tuple[MemoryScopeType, str] | None:
  1137. scope_value = self._read_optional_string(
  1138. agent_version.memory_policy_json,
  1139. "memory_scope") or "session"
  1140. if scope_value == "global":
  1141. return "global", "global"
  1142. if scope_value == "agent":
  1143. return "agent", agent_run.agent_id
  1144. if scope_value == "session" and agent_run.session_id:
  1145. return "session", agent_run.session_id
  1146. if scope_value == "user":
  1147. user_id = self._read_input_json_string(agent_run=agent_run, key="user_id")
  1148. if user_id is not None:
  1149. return "user", user_id
  1150. if scope_value == "team":
  1151. team_id = self._read_input_json_string(agent_run=agent_run, key="team_id")
  1152. if team_id is not None:
  1153. return "team", team_id
  1154. return None
  1155. def _format_memory_context(self, memory_results: list[MemorySearchResultContract]) -> str:
  1156. lines = ["Relevant memories:"]
  1157. for index, result in enumerate(memory_results, start=1):
  1158. lines.append(f"{index}. {result.item.content_text}")
  1159. return "\n".join(lines)
  1160. def _format_interaction_memory(self, *, agent_run: AgentRun, output_text: str) -> str:
  1161. input_text = agent_run.input_text or str(agent_run.input_json or {})
  1162. return f"User input: {input_text}\nAgent output: {output_text}"
  1163. def _read_bool(self, payload: dict[str, JSONValue], key: str, *, default: bool) -> bool:
  1164. value = payload.get(key)
  1165. if isinstance(value, bool):
  1166. return value
  1167. return default
  1168. def _read_int(self, payload: dict[str, JSONValue], key: str, *, default: int) -> int:
  1169. value = payload.get(key)
  1170. if isinstance(value, int) and not isinstance(value, bool):
  1171. return value
  1172. return default
  1173. def _read_nested_int(
  1174. self,
  1175. payload: dict[str, JSONValue],
  1176. parent_key: str,
  1177. child_key: str,
  1178. *,
  1179. default: int) -> int:
  1180. parent_value = payload.get(parent_key)
  1181. if not isinstance(parent_value, dict):
  1182. return default
  1183. return self._read_int(
  1184. cast(dict[str, JSONValue], parent_value),
  1185. child_key,
  1186. default=default)
  1187. def _read_input_json_string(self, *, agent_run: AgentRun, key: str) -> str | None:
  1188. if agent_run.input_json is None:
  1189. return None
  1190. value = agent_run.input_json.get(key)
  1191. if isinstance(value, str) and value:
  1192. return value
  1193. return None
  1194. def build_agent_application_service(
  1195. *,
  1196. db: Session,
  1197. settings: AgentServiceSettings) -> AgentApplicationService:
  1198. redis_client = try_build_redis_client(settings.redis_url)
  1199. return AgentApplicationService(
  1200. agent_repository=AgentDefinitionRepository(db),
  1201. agent_version_repository=AgentVersionRepository(db),
  1202. agent_run_repository=AgentRunRepository(db),
  1203. agent_tool_invocation_repository=AgentToolInvocationRepository(db),
  1204. model_gateway_client=ModelGatewayClient(
  1205. base_url=settings.model_gateway_service_url,
  1206. timeout_seconds=settings.model_gateway_timeout_seconds),
  1207. memory_client=MemoryClient(
  1208. base_url=settings.memory_service_url,
  1209. timeout_seconds=settings.memory_service_timeout_seconds),
  1210. tool_client=ToolServiceClient(
  1211. base_url=settings.tool_service_url,
  1212. timeout_seconds=settings.tool_service_timeout_seconds),
  1213. skill_client=SkillServiceClient(
  1214. base_url=settings.skill_service_url,
  1215. timeout_seconds=settings.skill_service_timeout_seconds),
  1216. event_client=EventServiceClient(
  1217. base_url=settings.event_service_url,
  1218. timeout_seconds=settings.event_service_timeout_seconds),
  1219. task_queue_publisher=(
  1220. TaskQueuePublisher(client=redis_client) if redis_client is not None else None
  1221. ),
  1222. react_max_steps=settings.react_max_steps)