aip-agents-binary 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. aip_agents/__init__.py +66 -0
  2. aip_agents/a2a/__init__.py +19 -0
  3. aip_agents/a2a/server/__init__.py +10 -0
  4. aip_agents/a2a/server/base_executor.py +1125 -0
  5. aip_agents/a2a/server/google_adk_executor.py +198 -0
  6. aip_agents/a2a/server/langflow_executor.py +180 -0
  7. aip_agents/a2a/server/langgraph_executor.py +270 -0
  8. aip_agents/a2a/types.py +232 -0
  9. aip_agents/a2ui/__init__.py +26 -0
  10. aip_agents/a2ui/catalog_provider.py +109 -0
  11. aip_agents/a2ui/examples/0.8/booking_form.json +1 -0
  12. aip_agents/a2ui/examples/0.8/confirmation.json +1 -0
  13. aip_agents/a2ui/examples/0.8/single_column_list.json +1 -0
  14. aip_agents/a2ui/examples/0.9/booking_form.json +1 -0
  15. aip_agents/a2ui/examples/0.9/confirmation.json +1 -0
  16. aip_agents/a2ui/examples/0.9/single_column_list.json +1 -0
  17. aip_agents/a2ui/middleware.py +754 -0
  18. aip_agents/a2ui/models.py +153 -0
  19. aip_agents/a2ui/public_url.py +138 -0
  20. aip_agents/a2ui/stream_filter.py +67 -0
  21. aip_agents/agent/__init__.py +72 -0
  22. aip_agents/agent/base_agent.py +1437 -0
  23. aip_agents/agent/base_langgraph_agent.py +4048 -0
  24. aip_agents/agent/google_adk_agent.py +932 -0
  25. aip_agents/agent/google_adk_constants.py +11 -0
  26. aip_agents/agent/graph_runtime.py +185 -0
  27. aip_agents/agent/hitl/__init__.py +24 -0
  28. aip_agents/agent/hitl/config.py +28 -0
  29. aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
  30. aip_agents/agent/hitl/manager.py +532 -0
  31. aip_agents/agent/hitl/models.py +18 -0
  32. aip_agents/agent/hitl/prompt/__init__.py +9 -0
  33. aip_agents/agent/hitl/prompt/base.py +42 -0
  34. aip_agents/agent/hitl/prompt/deferred.py +73 -0
  35. aip_agents/agent/hitl/registry.py +149 -0
  36. aip_agents/agent/interface.py +138 -0
  37. aip_agents/agent/interfaces.py +65 -0
  38. aip_agents/agent/langflow_agent.py +464 -0
  39. aip_agents/agent/langgraph_react_agent.py +3741 -0
  40. aip_agents/agent/model_invocation_adapter.py +346 -0
  41. aip_agents/agent/system_instruction_context.py +34 -0
  42. aip_agents/agent/tool_execution_engine.py +587 -0
  43. aip_agents/audio_interface/__init__.py +30 -0
  44. aip_agents/audio_interface/audio_agent_adapter.py +254 -0
  45. aip_agents/audio_interface/config.py +146 -0
  46. aip_agents/audio_interface/errors.py +15 -0
  47. aip_agents/audio_interface/livekit_audio_session.py +504 -0
  48. aip_agents/audio_interface/livekit_realtime_audio_session.py +142 -0
  49. aip_agents/audio_interface/session_factory.py +40 -0
  50. aip_agents/clients/__init__.py +10 -0
  51. aip_agents/clients/langflow/__init__.py +10 -0
  52. aip_agents/clients/langflow/client.py +477 -0
  53. aip_agents/clients/langflow/types.py +18 -0
  54. aip_agents/common/__init__.py +5 -0
  55. aip_agents/common/callable_introspection.py +17 -0
  56. aip_agents/constants.py +23 -0
  57. aip_agents/context/__init__.py +5 -0
  58. aip_agents/context/budget.py +237 -0
  59. aip_agents/context/compaction.py +339 -0
  60. aip_agents/context/metadata.py +169 -0
  61. aip_agents/context/offload.py +449 -0
  62. aip_agents/context/preparation.py +196 -0
  63. aip_agents/context/prompt.py +61 -0
  64. aip_agents/context/runtime.py +149 -0
  65. aip_agents/context/session_recall.py +69 -0
  66. aip_agents/context/state.py +48 -0
  67. aip_agents/context/utils.py +44 -0
  68. aip_agents/credentials/manager.py +132 -0
  69. aip_agents/examples/__init__.py +5 -0
  70. aip_agents/examples/audio_livekit_session.py +116 -0
  71. aip_agents/examples/compare_streaming_client.py +783 -0
  72. aip_agents/examples/compare_streaming_server.py +141 -0
  73. aip_agents/examples/gl_connectors_smoke.py +148 -0
  74. aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
  75. aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
  76. aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
  77. aip_agents/examples/hello_world_a2a_google_adk_server.py +78 -0
  78. aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
  79. aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
  80. aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
  81. aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
  82. aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
  83. aip_agents/examples/hello_world_a2a_langchain_reference_server.py +104 -0
  84. aip_agents/examples/hello_world_a2a_langchain_server.py +78 -0
  85. aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +77 -0
  86. aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
  87. aip_agents/examples/hello_world_a2a_langflow_server.py +80 -0
  88. aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
  89. aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
  90. aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +91 -0
  91. aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
  92. aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
  93. aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
  94. aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
  95. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
  96. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
  97. aip_agents/examples/hello_world_a2a_langgraph_server.py +83 -0
  98. aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +78 -0
  99. aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +131 -0
  100. aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
  101. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
  102. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +250 -0
  103. aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
  104. aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +79 -0
  105. aip_agents/examples/hello_world_filesystem.py +162 -0
  106. aip_agents/examples/hello_world_google_adk.py +41 -0
  107. aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
  108. aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
  109. aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
  110. aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
  111. aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
  112. aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
  113. aip_agents/examples/hello_world_google_adk_stream.py +44 -0
  114. aip_agents/examples/hello_world_langchain.py +28 -0
  115. aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
  116. aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
  117. aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
  118. aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
  119. aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
  120. aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
  121. aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
  122. aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
  123. aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
  124. aip_agents/examples/hello_world_langchain_stream.py +36 -0
  125. aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
  126. aip_agents/examples/hello_world_langflow_agent.py +163 -0
  127. aip_agents/examples/hello_world_langgraph.py +39 -0
  128. aip_agents/examples/hello_world_langgraph_gl_connector_twitter.py +44 -0
  129. aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
  130. aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
  131. aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
  132. aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
  133. aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
  134. aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
  135. aip_agents/examples/hello_world_langgraph_skills.py +62 -0
  136. aip_agents/examples/hello_world_langgraph_stream.py +43 -0
  137. aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
  138. aip_agents/examples/hello_world_model_switch_cli.py +210 -0
  139. aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
  140. aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
  141. aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
  142. aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
  143. aip_agents/examples/hello_world_multi_agent_tool_output_client.py +57 -0
  144. aip_agents/examples/hello_world_multi_agent_tool_output_server.py +131 -0
  145. aip_agents/examples/hello_world_pii_logger.py +21 -0
  146. aip_agents/examples/hello_world_ptc.py +49 -0
  147. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  148. aip_agents/examples/hello_world_sentry.py +133 -0
  149. aip_agents/examples/hello_world_step_limits.py +273 -0
  150. aip_agents/examples/hello_world_stock_a2a_server.py +100 -0
  151. aip_agents/examples/hello_world_tool_output_client.py +55 -0
  152. aip_agents/examples/hello_world_tool_output_server.py +113 -0
  153. aip_agents/examples/hitl_demo.py +724 -0
  154. aip_agents/examples/mcp_configs/configs.py +63 -0
  155. aip_agents/examples/mcp_servers/common.py +76 -0
  156. aip_agents/examples/mcp_servers/mcp_name.py +29 -0
  157. aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
  158. aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
  159. aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
  160. aip_agents/examples/mcp_servers/mcp_time.py +10 -0
  161. aip_agents/examples/pii_demo_langgraph_client.py +69 -0
  162. aip_agents/examples/pii_demo_langgraph_server.py +125 -0
  163. aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
  164. aip_agents/examples/pii_demo_multi_agent_server.py +246 -0
  165. aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
  166. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +87 -0
  167. aip_agents/examples/tools/__init__.py +27 -0
  168. aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
  169. aip_agents/examples/tools/adk_weather_tool.py +60 -0
  170. aip_agents/examples/tools/data_generator_tool.py +103 -0
  171. aip_agents/examples/tools/data_visualization_tool.py +312 -0
  172. aip_agents/examples/tools/image_artifact_tool.py +136 -0
  173. aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
  174. aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
  175. aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
  176. aip_agents/examples/tools/langchain_weather_tool.py +48 -0
  177. aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
  178. aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
  179. aip_agents/examples/tools/multiply_tool.py +43 -0
  180. aip_agents/examples/tools/pii_demo_tools.py +189 -0
  181. aip_agents/examples/tools/random_chart_tool.py +142 -0
  182. aip_agents/examples/tools/serper_tool.py +202 -0
  183. aip_agents/examples/tools/stock_tools.py +82 -0
  184. aip_agents/examples/tools/table_generator_tool.py +167 -0
  185. aip_agents/examples/tools/time_tool.py +82 -0
  186. aip_agents/examples/tools/weather_forecast_tool.py +38 -0
  187. aip_agents/guardrails/__init__.py +83 -0
  188. aip_agents/guardrails/engines/__init__.py +69 -0
  189. aip_agents/guardrails/engines/base.py +90 -0
  190. aip_agents/guardrails/engines/nemo.py +101 -0
  191. aip_agents/guardrails/engines/phrase_matcher.py +113 -0
  192. aip_agents/guardrails/exceptions.py +39 -0
  193. aip_agents/guardrails/manager.py +163 -0
  194. aip_agents/guardrails/middleware.py +199 -0
  195. aip_agents/guardrails/schemas.py +63 -0
  196. aip_agents/guardrails/utils.py +45 -0
  197. aip_agents/integration/__init__.py +43 -0
  198. aip_agents/integration/agent.py +58 -0
  199. aip_agents/integration/guardrails.py +52 -0
  200. aip_agents/integration/hitl.py +71 -0
  201. aip_agents/integration/ptc.py +95 -0
  202. aip_agents/integration/skills.py +37 -0
  203. aip_agents/integration/storage.py +81 -0
  204. aip_agents/integration/version.py +103 -0
  205. aip_agents/mcp/__init__.py +1 -0
  206. aip_agents/mcp/client/__init__.py +60 -0
  207. aip_agents/mcp/client/base_mcp_client.py +377 -0
  208. aip_agents/mcp/client/connection_manager.py +231 -0
  209. aip_agents/mcp/client/errors.py +119 -0
  210. aip_agents/mcp/client/google_adk/__init__.py +45 -0
  211. aip_agents/mcp/client/google_adk/client.py +383 -0
  212. aip_agents/mcp/client/langchain/__init__.py +11 -0
  213. aip_agents/mcp/client/langchain/client.py +265 -0
  214. aip_agents/mcp/client/persistent_session.py +704 -0
  215. aip_agents/mcp/client/session_pool.py +358 -0
  216. aip_agents/mcp/client/transports.py +288 -0
  217. aip_agents/mcp/utils/__init__.py +7 -0
  218. aip_agents/mcp/utils/config_validator.py +139 -0
  219. aip_agents/memory/__init__.py +14 -0
  220. aip_agents/memory/adapters/__init__.py +10 -0
  221. aip_agents/memory/adapters/base_adapter.py +815 -0
  222. aip_agents/memory/adapters/mem0.py +286 -0
  223. aip_agents/memory/base.py +84 -0
  224. aip_agents/memory/constants.py +49 -0
  225. aip_agents/memory/factory.py +86 -0
  226. aip_agents/memory/guidance.py +20 -0
  227. aip_agents/memory/simple_memory.py +47 -0
  228. aip_agents/middleware/__init__.py +65 -0
  229. aip_agents/middleware/backends/__init__.py +72 -0
  230. aip_agents/middleware/backends/in_memory.py +578 -0
  231. aip_agents/middleware/backends/local_disk.py +1203 -0
  232. aip_agents/middleware/backends/protocol.py +353 -0
  233. aip_agents/middleware/backends/sandbox.py +1431 -0
  234. aip_agents/middleware/backends/sandbox_files.py +113 -0
  235. aip_agents/middleware/backends/utils.py +293 -0
  236. aip_agents/middleware/base.py +239 -0
  237. aip_agents/middleware/filesystem.py +690 -0
  238. aip_agents/middleware/manager.py +291 -0
  239. aip_agents/middleware/memory.py +532 -0
  240. aip_agents/middleware/pii_middleware.py +192 -0
  241. aip_agents/middleware/schema.py +19 -0
  242. aip_agents/middleware/skills.py +872 -0
  243. aip_agents/middleware/task_delegation.py +1656 -0
  244. aip_agents/middleware/todolist.py +276 -0
  245. aip_agents/middleware/tools/__init__.py +23 -0
  246. aip_agents/middleware/tools/edit_file.py +70 -0
  247. aip_agents/middleware/tools/execute.py +428 -0
  248. aip_agents/middleware/tools/grep_file.py +79 -0
  249. aip_agents/middleware/tools/ls.py +71 -0
  250. aip_agents/middleware/tools/read_file.py +239 -0
  251. aip_agents/middleware/tools/skill_load.py +193 -0
  252. aip_agents/middleware/tools/skill_resource.py +338 -0
  253. aip_agents/middleware/tools/write_file.py +114 -0
  254. aip_agents/middleware/vision.py +322 -0
  255. aip_agents/ptc/__init__.py +87 -0
  256. aip_agents/ptc/custom_tools.py +511 -0
  257. aip_agents/ptc/custom_tools_payload.py +499 -0
  258. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  259. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +24 -0
  260. aip_agents/ptc/custom_tools_templates/custom_init.py.template +26 -0
  261. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +149 -0
  262. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +112 -0
  263. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  264. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +87 -0
  265. aip_agents/ptc/doc_gen.py +122 -0
  266. aip_agents/ptc/exceptions.py +57 -0
  267. aip_agents/ptc/executor.py +627 -0
  268. aip_agents/ptc/mcp/__init__.py +45 -0
  269. aip_agents/ptc/mcp/sandbox_bridge.py +679 -0
  270. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  271. aip_agents/ptc/mcp/templates/mcp_client.py.template +240 -0
  272. aip_agents/ptc/naming.py +430 -0
  273. aip_agents/ptc/payload.py +26 -0
  274. aip_agents/ptc/prompt_builder.py +674 -0
  275. aip_agents/ptc/ptc_helper.py +16 -0
  276. aip_agents/ptc/sandbox_bridge.py +297 -0
  277. aip_agents/ptc/template_utils.py +33 -0
  278. aip_agents/ptc/templates/__init__.py +1 -0
  279. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  280. aip_agents/ptc/tool_def_helpers.py +101 -0
  281. aip_agents/ptc/tool_enrichment.py +163 -0
  282. aip_agents/resilience/__init__.py +10 -0
  283. aip_agents/resilience/base.py +46 -0
  284. aip_agents/resilience/tool.py +1661 -0
  285. aip_agents/sandbox/__init__.py +62 -0
  286. aip_agents/sandbox/defaults.py +220 -0
  287. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1-datasaur.Dockerfile +120 -0
  288. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1.Dockerfile +100 -0
  289. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2-datasaur.Dockerfile +143 -0
  290. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2.Dockerfile +119 -0
  291. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3-datasaur.Dockerfile +173 -0
  292. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3.Dockerfile +130 -0
  293. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.Dockerfile +164 -0
  294. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.patch_js_context.py +41 -0
  295. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.requirements.txt +29 -0
  296. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2.Dockerfile +87 -0
  297. aip_agents/sandbox/e2b_runtime.py +27 -0
  298. aip_agents/sandbox/image_registry.py +98 -0
  299. aip_agents/sandbox/providers/__init__.py +76 -0
  300. aip_agents/sandbox/providers/base.py +392 -0
  301. aip_agents/sandbox/providers/bedrock.py +28 -0
  302. aip_agents/sandbox/providers/e2b.py +230 -0
  303. aip_agents/sandbox/providers/opensandbox.py +163 -0
  304. aip_agents/sandbox/sandbox_runtime.py +967 -0
  305. aip_agents/sandbox/template_builder.py +240 -0
  306. aip_agents/sandbox/types.py +141 -0
  307. aip_agents/sandbox/validation.py +50 -0
  308. aip_agents/schema/__init__.py +106 -0
  309. aip_agents/schema/a2a.py +58 -0
  310. aip_agents/schema/agent.py +117 -0
  311. aip_agents/schema/artifacts.py +75 -0
  312. aip_agents/schema/attachments.py +425 -0
  313. aip_agents/schema/hitl.py +157 -0
  314. aip_agents/schema/langgraph.py +47 -0
  315. aip_agents/schema/model_id.py +103 -0
  316. aip_agents/schema/model_usage.py +50 -0
  317. aip_agents/schema/runtime_metadata.py +78 -0
  318. aip_agents/schema/step_limit.py +108 -0
  319. aip_agents/schema/storage.py +40 -0
  320. aip_agents/schema/tool_payloads.py +68 -0
  321. aip_agents/sentry/__init__.py +11 -0
  322. aip_agents/sentry/sentry.py +267 -0
  323. aip_agents/skills/__init__.py +30 -0
  324. aip_agents/skills/errors.py +110 -0
  325. aip_agents/skills/installer.py +479 -0
  326. aip_agents/skills/models.py +769 -0
  327. aip_agents/skills/stager.py +224 -0
  328. aip_agents/skills/validation.py +53 -0
  329. aip_agents/storage/__init__.py +41 -0
  330. aip_agents/storage/base.py +85 -0
  331. aip_agents/storage/clients/__init__.py +12 -0
  332. aip_agents/storage/clients/minio_client.py +318 -0
  333. aip_agents/storage/config.py +62 -0
  334. aip_agents/storage/providers/__init__.py +15 -0
  335. aip_agents/storage/providers/base.py +106 -0
  336. aip_agents/storage/providers/memory.py +114 -0
  337. aip_agents/storage/providers/object_storage.py +214 -0
  338. aip_agents/tools/__init__.py +65 -0
  339. aip_agents/tools/agent_schedule_tool.py +392 -0
  340. aip_agents/tools/browser_use/__init__.py +83 -0
  341. aip_agents/tools/browser_use/action_parser.py +104 -0
  342. aip_agents/tools/browser_use/browser_use_tool.py +1118 -0
  343. aip_agents/tools/browser_use/llm_config.py +120 -0
  344. aip_agents/tools/browser_use/minio_storage.py +203 -0
  345. aip_agents/tools/browser_use/schemas.py +121 -0
  346. aip_agents/tools/browser_use/session.py +13 -0
  347. aip_agents/tools/browser_use/session_errors.py +132 -0
  348. aip_agents/tools/browser_use/steel_session_recording.py +321 -0
  349. aip_agents/tools/browser_use/streaming.py +263 -0
  350. aip_agents/tools/browser_use/structured_data_parser.py +257 -0
  351. aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
  352. aip_agents/tools/browser_use/types.py +37 -0
  353. aip_agents/tools/code_sandbox/__init__.py +48 -0
  354. aip_agents/tools/code_sandbox/code_sandbox_tool.py +350 -0
  355. aip_agents/tools/code_sandbox/constant.py +13 -0
  356. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +346 -0
  357. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +65 -0
  358. aip_agents/tools/computer_use/__init__.py +75 -0
  359. aip_agents/tools/computer_use/computer_use_tool.py +910 -0
  360. aip_agents/tools/computer_use/schemas.py +78 -0
  361. aip_agents/tools/computer_use/streaming.py +198 -0
  362. aip_agents/tools/computer_use/types.py +32 -0
  363. aip_agents/tools/constants.py +177 -0
  364. aip_agents/tools/date_range_tool.py +554 -0
  365. aip_agents/tools/document_loader/__init__.py +44 -0
  366. aip_agents/tools/document_loader/base_reader.py +302 -0
  367. aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
  368. aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
  369. aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
  370. aip_agents/tools/document_loader/pdf_splitter.py +169 -0
  371. aip_agents/tools/execute_ptc_code.py +582 -0
  372. aip_agents/tools/gl_connector/__init__.py +5 -0
  373. aip_agents/tools/gl_connector/tool.py +518 -0
  374. aip_agents/tools/gl_connector_tools.py +170 -0
  375. aip_agents/tools/memory_search/__init__.py +29 -0
  376. aip_agents/tools/memory_search/base.py +213 -0
  377. aip_agents/tools/memory_search/mem0.py +424 -0
  378. aip_agents/tools/memory_search/schema.py +81 -0
  379. aip_agents/tools/memory_search_tool.py +34 -0
  380. aip_agents/tools/streaming_common.py +577 -0
  381. aip_agents/tools/time_tool.py +117 -0
  382. aip_agents/tools/tool_config_injector.py +300 -0
  383. aip_agents/tools/web_search/__init__.py +15 -0
  384. aip_agents/tools/web_search/serper_tool.py +187 -0
  385. aip_agents/types/__init__.py +73 -0
  386. aip_agents/types/a2a_events.py +13 -0
  387. aip_agents/utils/__init__.py +91 -0
  388. aip_agents/utils/a2a_connector.py +1758 -0
  389. aip_agents/utils/artifact_helpers.py +542 -0
  390. aip_agents/utils/artifacts/__init__.py +20 -0
  391. aip_agents/utils/artifacts/tracker.py +367 -0
  392. aip_agents/utils/attachment_helpers.py +178 -0
  393. aip_agents/utils/constants.py +22 -0
  394. aip_agents/utils/datetime/__init__.py +34 -0
  395. aip_agents/utils/datetime/normalization.py +231 -0
  396. aip_agents/utils/datetime/timezone.py +206 -0
  397. aip_agents/utils/env_loader.py +27 -0
  398. aip_agents/utils/event_handler_registry.py +58 -0
  399. aip_agents/utils/file_prompt_utils.py +276 -0
  400. aip_agents/utils/file_watchers/__init__.py +33 -0
  401. aip_agents/utils/file_watchers/base.py +46 -0
  402. aip_agents/utils/file_watchers/e2b.py +128 -0
  403. aip_agents/utils/file_watchers/factory.py +39 -0
  404. aip_agents/utils/file_watchers/ls_diff.py +88 -0
  405. aip_agents/utils/file_watchers/sandbox.py +66 -0
  406. aip_agents/utils/file_watchers/watchdog.py +97 -0
  407. aip_agents/utils/final_response_builder.py +211 -0
  408. aip_agents/utils/formatter_llm_client.py +258 -0
  409. aip_agents/utils/langgraph/__init__.py +19 -0
  410. aip_agents/utils/langgraph/converter.py +399 -0
  411. aip_agents/utils/langgraph/delegation_context.py +22 -0
  412. aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
  413. aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
  414. aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
  415. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1329 -0
  416. aip_agents/utils/langgraph/tool_output_management.py +1047 -0
  417. aip_agents/utils/logger.py +195 -0
  418. aip_agents/utils/metadata/__init__.py +27 -0
  419. aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
  420. aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
  421. aip_agents/utils/metadata/activity_narrative/builder.py +822 -0
  422. aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
  423. aip_agents/utils/metadata/activity_narrative/context.py +49 -0
  424. aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
  425. aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
  426. aip_agents/utils/metadata/schemas/__init__.py +16 -0
  427. aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
  428. aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
  429. aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
  430. aip_agents/utils/metadata_helper.py +363 -0
  431. aip_agents/utils/name_preprocessor/__init__.py +17 -0
  432. aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
  433. aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
  434. aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
  435. aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
  436. aip_agents/utils/pii/__init__.py +29 -0
  437. aip_agents/utils/pii/pii_handler.py +555 -0
  438. aip_agents/utils/pii/pii_helper.py +225 -0
  439. aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
  440. aip_agents/utils/reference_helper.py +273 -0
  441. aip_agents/utils/sse_chunk_transformer.py +836 -0
  442. aip_agents/utils/step_limit_manager.py +265 -0
  443. aip_agents/utils/token_usage_helper.py +531 -0
  444. aip_agents_binary-0.0.0.dist-info/METADATA +825 -0
  445. aip_agents_binary-0.0.0.dist-info/RECORD +447 -0
  446. aip_agents_binary-0.0.0.dist-info/WHEEL +5 -0
  447. aip_agents_binary-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,532 @@
1
+ """Memory middleware for automatic recall and persistence.
2
+
3
+ This module provides MemoryMiddleware, which implements the AgentMiddleware protocol
4
+ to handle memory operations (recall and persistence) through the middleware lifecycle
5
+ hooks instead of special graph nodes.
6
+
7
+ Authors:
8
+ Raymond Christopher (raymond.christopher@gdplabs.id)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import json
15
+ import re
16
+ from concurrent.futures import Future
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from langchain_core.messages import HumanMessage
20
+
21
+ from aip_agents.constants import TEXT_PREVIEW_LENGTH
22
+ from aip_agents.middleware.base import AgentMiddleware
23
+ from aip_agents.tools.memory_search import Mem0DeleteTool, Mem0SearchTool
24
+ from aip_agents.utils.logger import get_logger
25
+
26
+ if TYPE_CHECKING: # pragma: no cover
27
+ from aip_agents.memory import BaseMemory
28
+
29
+ logger = get_logger(__name__)
30
+
31
+ MAX_QUERY_LENGTH: int = 128
32
+ DEFAULT_MEMORY_LIMIT: int = 10
33
+ FORGET_WORD_PATTERN: str = r"\bforget\b"
34
+
35
+
36
+ class MemoryMiddleware(AgentMiddleware):
37
+ """Middleware for automatic memory recall and persistence.
38
+
39
+ This middleware replaces the dedicated memory graph entry node by using
40
+ middleware lifecycle hooks to:
41
+ 1. Recall relevant memories before the agent runs (abefore_run hook)
42
+ 2. Persist interactions after the agent completes (aafter_run hook)
43
+
44
+ The middleware handles memory retrieval logic directly, maintaining backward
45
+ compatibility with existing memory configuration.
46
+
47
+ Attributes:
48
+ tools: Empty list - memory middleware doesn't contribute tools.
49
+ system_prompt_additions: None - no prompt additions needed.
50
+ """
51
+
52
+ tools: list[Any] = []
53
+ system_prompt_additions: str | None = None
54
+
55
+ def __init__(
56
+ self,
57
+ memory: BaseMemory | None,
58
+ memory_agent_id: str,
59
+ save_interaction_to_memory: bool = True,
60
+ memory_retrieval_limit: int = DEFAULT_MEMORY_LIMIT,
61
+ ) -> None:
62
+ """Initialize the MemoryMiddleware.
63
+
64
+ Args:
65
+ memory: The memory backend instance (Mem0Memory or compatible).
66
+ memory_agent_id: The agent ID used for memory scoping.
67
+ save_interaction_to_memory: Whether to save interactions to memory.
68
+ memory_retrieval_limit: Maximum memories to retrieve per recall.
69
+ """
70
+ self._memory = memory
71
+ self._memory_agent_id = memory_agent_id
72
+ self._save_interaction_to_memory = save_interaction_to_memory
73
+ self._memory_retrieval_limit = max(memory_retrieval_limit, 1)
74
+
75
+ if memory is not None:
76
+ self._memory_search_tool = Mem0SearchTool(memory=memory, default_user_id=memory_agent_id)
77
+ self._memory_delete_tool = Mem0DeleteTool(memory=memory, default_user_id=memory_agent_id)
78
+ else:
79
+ self._memory_search_tool = None
80
+ self._memory_delete_tool = None
81
+
82
+ @property
83
+ def memory_enabled(self) -> bool:
84
+ """Check whether memory is enabled.
85
+
86
+ Returns:
87
+ True when a memory adapter is set.
88
+ """
89
+ return self._memory is not None
90
+
91
+ async def abefore_run(
92
+ self,
93
+ state: dict[str, Any],
94
+ config: dict[str, Any] | None,
95
+ ) -> dict[str, Any]:
96
+ """Execute memory recall before the agent run.
97
+
98
+ Extracts the user query from state messages, retrieves relevant memories
99
+ directly using the memory search tool, and enhances the query with
100
+ formatted memory context.
101
+
102
+ Args:
103
+ state: Initial agent state containing messages.
104
+ config: Optional run configuration.
105
+
106
+ Returns:
107
+ State updates with enhanced message if memories were found.
108
+ """
109
+ if not self.memory_enabled:
110
+ return {}
111
+
112
+ user_query = self._extract_user_query_from_messages(state.get("messages", []))
113
+ if not user_query:
114
+ return {}
115
+
116
+ self._store_original_query(config, user_query)
117
+
118
+ try:
119
+ memory_user_id = self._extract_memory_user_id(config)
120
+ effective_user_id = memory_user_id or self._memory_agent_id
121
+
122
+ if self._is_forget_request(user_query):
123
+ await self._delete_memories(
124
+ user_query=user_query, user_id=effective_user_id, state=state, config=config
125
+ )
126
+ return {"memory_action": "delete"}
127
+
128
+ memories = await self._retrieve_memories(user_query, effective_user_id, state, config)
129
+ if not memories:
130
+ return {}
131
+
132
+ tagged_memory = self._format_memories(memories)
133
+ enhanced_query = f"{user_query}\n\n{tagged_memory}".strip()
134
+ logger.info("MemoryMiddleware: enhanced query with %d memories", len(memories))
135
+
136
+ return {"messages": [HumanMessage(content=enhanced_query)]}
137
+
138
+ except Exception as e:
139
+ logger.warning("MemoryMiddleware: enhancement failed: %s", e)
140
+ return {}
141
+
142
+ async def _retrieve_memories(
143
+ self,
144
+ query: str,
145
+ user_id: str,
146
+ state: dict[str, Any],
147
+ config: dict[str, Any] | None,
148
+ ) -> list[dict[str, Any]]:
149
+ """Retrieve memories using the memory search tool."""
150
+ if self._memory_search_tool is None:
151
+ return [] # pragma: no cover
152
+
153
+ metadata = self._build_metadata(state, user_id)
154
+ search_args = {
155
+ "query": query[:MAX_QUERY_LENGTH] if query else None,
156
+ "limit": self._memory_retrieval_limit,
157
+ "metadata": metadata,
158
+ }
159
+ tool_config = self._create_tool_config(config, user_id)
160
+
161
+ try:
162
+ result = await self._memory_search_tool.ainvoke(search_args, config=tool_config)
163
+ memories = self._parse_memories_from_result(result)
164
+ if memories:
165
+ logger.info("MemoryMiddleware: retrieved %d memories", len(memories))
166
+ return memories
167
+ except Exception as e: # pragma: no cover
168
+ logger.warning("MemoryMiddleware: retrieval failed: %s", e) # pragma: no cover
169
+ return [] # pragma: no cover
170
+
171
+ def _create_tool_config(
172
+ self,
173
+ config: dict[str, Any] | None,
174
+ user_id: str,
175
+ ) -> dict[str, Any]:
176
+ """Create tool configuration with user_id."""
177
+ tool_config = dict(config) if config else {}
178
+ tool_config.setdefault("configurable", {})
179
+ tool_config["configurable"]["user_id"] = user_id
180
+ return tool_config
181
+
182
+ def _build_metadata(self, state: dict[str, Any], user_id: str) -> dict[str, Any]:
183
+ """Build metadata dict for memory tools, filtering internal keys."""
184
+ state_metadata = state.get("metadata", {})
185
+ metadata = {k: v for k, v in state_metadata.items() if k not in {"tool_configs", "configurable", "thread_id"}}
186
+ metadata["user_id"] = user_id
187
+ return metadata
188
+
189
+ def _parse_memories_from_result(self, result: Any) -> list[dict[str, Any]]:
190
+ """Parse memory list from tool result."""
191
+ if isinstance(result, list):
192
+ return [m for m in result if isinstance(m, dict)]
193
+ if isinstance(result, str):
194
+ try:
195
+ parsed = json.loads(result)
196
+ if isinstance(parsed, list):
197
+ return [m for m in parsed if isinstance(m, dict)]
198
+ except json.JSONDecodeError:
199
+ pass
200
+ return []
201
+
202
+ def _format_memories(self, memories: list[dict[str, Any]]) -> str:
203
+ """Format memory hits using the underlying tool formatter."""
204
+ if not memories:
205
+ return ""
206
+ if self._memory_search_tool is not None:
207
+ return self._memory_search_tool.format_hits(memories, with_tag=True)
208
+ lines = ["<MEMORY_CONTEXT>"]
209
+ for i, memory in enumerate(memories, 1):
210
+ content = memory.get("memory", str(memory))
211
+ lines.append(f"{i}. {content}")
212
+ lines.append("</MEMORY_CONTEXT>")
213
+ return "\n".join(lines)
214
+
215
+ def _is_forget_request(self, user_query: str) -> bool:
216
+ """Return True when the user request should delete/clear memories."""
217
+ normalized = user_query.strip().lower()
218
+ if not normalized:
219
+ return False
220
+
221
+ # Guard against reminder phrasing such as "don't forget to ...".
222
+ if self._is_non_destructive_forget_phrase(normalized):
223
+ return False
224
+
225
+ # Explicit forget intent (imperative/request form).
226
+ if re.search(r"^(please\s+)?((can|could|would)\s+you\s+)?forget\b", normalized):
227
+ return True
228
+
229
+ # Accept intent phrasing where "forget" is not sentence-initial.
230
+ if re.search(r"\bi\s+(want|need|would\s+like)\s+to\s+forget\b", normalized):
231
+ return True
232
+
233
+ # Allow explicit memory management phrasing.
234
+ if re.search(r"\b(clear|delete|remove)\b", normalized) and self._has_memory_context(normalized):
235
+ return True
236
+
237
+ return False
238
+
239
+ def _extract_delete_query(self, user_query: str) -> str:
240
+ """Extract the semantic target for deletion from a forget request."""
241
+ normalized = user_query.strip()
242
+
243
+ # Strip common prefixes so we delete the actual target content.
244
+ normalized = re.sub(r"(?i)^\s*(please\s+)?(forget|delete|remove|clear)\b\s*", "", normalized)
245
+ normalized = re.sub(r"(?i)^\s*(that\s+)?", "", normalized)
246
+ return normalized.strip() or user_query.strip()
247
+
248
+ @staticmethod
249
+ def _is_non_destructive_forget_phrase(normalized_query: str) -> bool:
250
+ """Return True for reminder phrasing that should not delete memory."""
251
+ non_destructive_patterns = (
252
+ r"\bdon['’]?t\s+forget\b",
253
+ r"\bdo\s+not\s+forget\b",
254
+ r"\bnever\s+forget\b",
255
+ r"\bforget\s+to\b",
256
+ )
257
+ return any(re.search(pattern, normalized_query) for pattern in non_destructive_patterns)
258
+
259
+ @staticmethod
260
+ def _has_memory_context(normalized_query: str) -> bool:
261
+ """Return True when the query refers to memory explicitly or implicitly."""
262
+ return bool(
263
+ re.search(
264
+ r"\bmemory\b|\bmemories\b|\byou\s+remember\b|\bremember\s+about\s+me\b|\bwhat\s+you\s+know\s+about\s+me\b",
265
+ normalized_query,
266
+ )
267
+ )
268
+
269
+ @staticmethod
270
+ def _is_scoped_forget_query(normalized_query: str) -> bool:
271
+ """Return True for scoped forget requests that should not map to delete_all."""
272
+ if not re.search(FORGET_WORD_PATTERN, normalized_query):
273
+ return False
274
+ if not re.search(r"\b(all|everything)\b", normalized_query):
275
+ return False
276
+ if re.search(r"\babout\s+me\b", normalized_query):
277
+ return False
278
+ return bool(re.search(r"\b(all|everything)\b(?:\s+\w+){0,5}\s+\b(about|regarding|on)\b", normalized_query))
279
+
280
+ def _is_delete_all_request(self, user_query: str) -> bool:
281
+ normalized = user_query.strip().lower()
282
+ if self._is_non_destructive_forget_phrase(normalized):
283
+ return False
284
+
285
+ has_delete_verb = bool(re.search(r"\b(forget|delete|clear|remove)\b", normalized))
286
+ has_all_scope = bool(re.search(r"\b(all|everything)\b", normalized))
287
+ if not (has_delete_verb and has_all_scope):
288
+ return False
289
+
290
+ if self._is_scoped_forget_query(normalized):
291
+ return False
292
+
293
+ if self._has_memory_context(normalized):
294
+ return True
295
+
296
+ # Keep concise global intent working (e.g. "forget everything").
297
+ return bool(re.search(FORGET_WORD_PATTERN, normalized))
298
+
299
+ async def _delete_memories(
300
+ self,
301
+ *,
302
+ user_query: str,
303
+ user_id: str,
304
+ state: dict[str, Any],
305
+ config: dict[str, Any] | None,
306
+ ) -> None:
307
+ """Best-effort deletion for forget/clear requests."""
308
+ if self._memory_delete_tool is None:
309
+ return # pragma: no cover
310
+
311
+ await self._flush_pending_memory_writes(timeout=30.0)
312
+
313
+ tool_config = self._create_tool_config(config, user_id)
314
+ metadata = self._build_metadata(state, user_id)
315
+
316
+ if self._is_delete_all_request(user_query):
317
+ args: dict[str, Any] = {"delete_all": True, "metadata": metadata}
318
+ else:
319
+ delete_query = self._extract_delete_query(user_query)[:MAX_QUERY_LENGTH]
320
+ args = {"query": delete_query, "metadata": metadata}
321
+
322
+ try:
323
+ await self._memory_delete_tool.ainvoke(args, config=tool_config)
324
+ except Exception as exc: # noqa: BLE001 # pragma: no cover
325
+ logger.warning("MemoryMiddleware: delete ignored error: %s", exc) # pragma: no cover
326
+
327
+ async def _flush_pending_memory_writes(self, timeout: float | None = None) -> None:
328
+ flush = getattr(self._memory, "flush_pending_writes", None)
329
+ if not callable(flush):
330
+ return
331
+ try:
332
+ await asyncio.to_thread(flush, timeout=timeout)
333
+ except Exception as exc: # noqa: BLE001
334
+ logger.debug("MemoryMiddleware: pending memory flush ignored error: %s", exc)
335
+
336
+ async def aafter_run(
337
+ self,
338
+ *,
339
+ final_state: dict[str, Any],
340
+ output: str | None,
341
+ config: dict[str, Any] | None,
342
+ error: Exception | None,
343
+ ) -> None:
344
+ """Execute memory persistence after the agent run."""
345
+ if error is not None:
346
+ return
347
+ if not self._should_save_interaction(final_state):
348
+ return
349
+ if not output:
350
+ return # pragma: no cover
351
+
352
+ user_query = self._extract_original_query(config=config, final_state=final_state)
353
+ if not user_query:
354
+ return
355
+
356
+ memory_user_id = self._extract_memory_user_id(config)
357
+ self._memory_save_interaction(
358
+ user_text=user_query,
359
+ ai_text=output,
360
+ memory_user_id=memory_user_id,
361
+ )
362
+
363
+ async def aon_final_response(self, content: str, context: dict[str, Any]) -> None:
364
+ """Persist memory early during streaming final response (best-effort)."""
365
+ if not self.memory_enabled or not self._save_interaction_to_memory:
366
+ return
367
+ if not isinstance(content, str) or not content:
368
+ return
369
+ if context.get("memory_persisted"):
370
+ return
371
+ final_state = context.get("final_state")
372
+ if isinstance(final_state, dict) and final_state.get("memory_action") == "delete":
373
+ context["memory_persisted"] = True
374
+ return
375
+ user_text = context.get("original_query")
376
+ if not isinstance(user_text, str) or not user_text:
377
+ return
378
+ config = context.get("config")
379
+ memory_user_id = self._extract_memory_user_id(config) if isinstance(config, dict) else None
380
+ if not memory_user_id:
381
+ memory_user_id = context.get("memory_user_id") if isinstance(context.get("memory_user_id"), str) else None
382
+ try:
383
+ self._memory_save_interaction(
384
+ user_text=user_text,
385
+ ai_text=content,
386
+ memory_user_id=memory_user_id,
387
+ )
388
+ finally:
389
+ context["memory_persisted"] = True
390
+
391
+ def _should_save_interaction(self, final_state: dict[str, Any] | None) -> bool:
392
+ """Return True when interaction should be saved to memory."""
393
+ if not self._save_interaction_to_memory or not self.memory_enabled:
394
+ return False
395
+ if isinstance(final_state, dict) and final_state.get("memory_persisted"):
396
+ return False # pragma: no cover
397
+ if isinstance(final_state, dict) and final_state.get("memory_action") == "delete":
398
+ return False # pragma: no cover
399
+ if self._contains_memory_delete_action(final_state):
400
+ return False
401
+ return True
402
+
403
+ @staticmethod
404
+ def _contains_memory_delete_action(final_state: dict[str, Any] | None) -> bool:
405
+ """Return True when final state includes a delete memory action block."""
406
+ if not isinstance(final_state, dict):
407
+ return False # pragma: no cover
408
+ messages = final_state.get("messages")
409
+ if not isinstance(messages, list):
410
+ return False # pragma: no cover
411
+ for message in messages:
412
+ content = getattr(message, "content", None)
413
+ if not isinstance(content, str):
414
+ continue
415
+ if "<MEMORY_ACTION>" in content and "action=delete" in content:
416
+ return True
417
+ return False
418
+
419
+ @staticmethod
420
+ def _extract_user_query_from_messages(messages: list[Any]) -> str | None:
421
+ """Get latest user query string from messages."""
422
+ if not messages:
423
+ return None
424
+ for msg in reversed(messages):
425
+ if isinstance(msg, HumanMessage) and hasattr(msg, "content"):
426
+ content = msg.content
427
+ if isinstance(content, str) and content.strip():
428
+ return content
429
+ return None
430
+
431
+ def _extract_memory_user_id(self, config: dict[str, Any] | None) -> str | None:
432
+ """Extract memory_user_id from config if present."""
433
+ if not isinstance(config, dict):
434
+ return None # pragma: no cover
435
+ configurable = config.get("configurable", {})
436
+ if isinstance(configurable, dict):
437
+ user_id = configurable.get("memory_user_id")
438
+ if user_id:
439
+ return user_id
440
+ metadata = config.get("metadata", {})
441
+ if isinstance(metadata, dict):
442
+ user_id = metadata.get("memory_user_id")
443
+ if user_id:
444
+ return user_id
445
+ return None
446
+
447
+ @staticmethod
448
+ def _store_original_query(config: dict[str, Any] | None, user_query: str) -> None:
449
+ """Store original user query in config metadata for persistence."""
450
+ if not isinstance(config, dict) or not isinstance(user_query, str) or not user_query.strip():
451
+ return
452
+ metadata = config.get("metadata")
453
+ if metadata is None:
454
+ config["metadata"] = {"original_query": user_query}
455
+ return
456
+ if not isinstance(metadata, dict):
457
+ return
458
+ existing = metadata.get("original_query")
459
+ if isinstance(existing, str) and existing.strip():
460
+ return
461
+ metadata["original_query"] = user_query
462
+
463
+ def _extract_original_query(
464
+ self,
465
+ *,
466
+ config: dict[str, Any] | None,
467
+ final_state: dict[str, Any] | None,
468
+ ) -> str | None:
469
+ """Extract original user query, preferring config metadata over state messages."""
470
+ if isinstance(config, dict):
471
+ metadata = config.get("metadata")
472
+ if isinstance(metadata, dict):
473
+ original_query = metadata.get("original_query")
474
+ if isinstance(original_query, str) and original_query.strip():
475
+ return original_query
476
+ if isinstance(final_state, dict):
477
+ state_original_query = final_state.get("original_query")
478
+ if isinstance(state_original_query, str) and state_original_query.strip():
479
+ return state_original_query
480
+ return self._extract_user_query_from_messages(final_state.get("messages", []))
481
+ return None
482
+
483
+ def _memory_save_interaction(
484
+ self,
485
+ user_text: str,
486
+ ai_text: str,
487
+ memory_user_id: str | None = None,
488
+ ) -> None:
489
+ """Persist the user/assistant pair (best-effort)."""
490
+ if not (self._save_interaction_to_memory and self.memory_enabled and user_text and ai_text):
491
+ return # pragma: no cover
492
+
493
+ try:
494
+ user_id = memory_user_id or self._memory_agent_id
495
+
496
+ logger.info("MemoryMiddleware: saving interaction for user_id='%s'", user_id)
497
+ logger.debug(
498
+ "MemoryMiddleware: interaction preview - User: '%s...' AI: '%s...'",
499
+ user_text[:TEXT_PREVIEW_LENGTH],
500
+ ai_text[:TEXT_PREVIEW_LENGTH],
501
+ )
502
+
503
+ save_async = getattr(self._memory, "save_interaction_async", None)
504
+ if callable(save_async):
505
+ future = save_async(user_text=str(user_text), ai_text=str(ai_text), user_id=user_id)
506
+ self._watch_memory_future(future, user_id)
507
+ elif hasattr(self._memory, "save_interaction"):
508
+ self._memory.save_interaction( # pragma: no cover
509
+ user_text=str(user_text),
510
+ ai_text=str(ai_text),
511
+ user_id=user_id,
512
+ )
513
+ else:
514
+ logger.warning( # pragma: no cover
515
+ "MemoryMiddleware: save_interaction not available on adapter type=%s",
516
+ type(self._memory).__name__,
517
+ )
518
+ except Exception as e:
519
+ logger.debug("MemoryMiddleware: save_interaction ignored error: %s", e)
520
+
521
+ @staticmethod
522
+ def _watch_memory_future(future: Any, user_id: str) -> None:
523
+ """Attach logging to async memory writes."""
524
+ if not isinstance(future, Future):
525
+ return
526
+
527
+ def _log_completion(done: Future) -> None:
528
+ exc = done.exception()
529
+ if exc: # pragma: no cover
530
+ logger.warning("MemoryMiddleware: async save failed for user_id='%s': %s", user_id, exc)
531
+
532
+ future.add_done_callback(_log_completion)