services.py 52 KB

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