services.py 52 KB

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