services.py 54 KB

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