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,754 @@
1
+ """A2UI middleware for protocol-driven prompt enrichment and validation.
2
+
3
+ Authors:
4
+ Reinhart Linanda (reinhart.linanda@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ import json
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import Any, cast
14
+
15
+ import jsonschema
16
+ import requests
17
+ from a2ui.basic_catalog.provider import BasicCatalog
18
+ from a2ui.schema.catalog import CatalogConfig
19
+ from a2ui.schema.common_modifiers import remove_strict_validation
20
+ from a2ui.schema.constants import A2UI_CLOSE_TAG, A2UI_OPEN_TAG, VERSION_0_8, VERSION_0_9
21
+ from a2ui.schema.manager import A2uiSchemaManager
22
+
23
+ from aip_agents.a2ui.catalog_provider import (
24
+ UrlCatalogProvider,
25
+ materialize_example_urls,
26
+ release_materialized_example_dirs,
27
+ )
28
+ from aip_agents.a2ui.models import (
29
+ STANDARD_SCHEMA_VERSION,
30
+ A2UIActionContext,
31
+ A2UIActionStatus,
32
+ A2UIActionValidationResult,
33
+ A2UIContentMetadata,
34
+ A2UIContentMetadataStatus,
35
+ A2UIContext,
36
+ _normalize_iso_timestamp,
37
+ )
38
+ from aip_agents.middleware.base import AgentMiddleware, ModelRequest
39
+ from aip_agents.types import JsonValue
40
+ from aip_agents.utils.logger import get_logger
41
+
42
+ logger = get_logger(__name__)
43
+
44
+ _REQUIRED_ACTION_FIELDS = ("name", "surfaceId", "sourceComponentId", "timestamp", "context")
45
+ _A2UI_REPAIR_ATTEMPT_LIMIT = 1
46
+
47
+ # Canonical per-version message action keys in protocol stream order. v0.8 and v0.9 use different
48
+ # message structures, so keep the version scope explicit at each constant name.
49
+ _A2UI_V0_8_MESSAGE_ACTION_KEYS = ("beginRendering", "surfaceUpdate", "dataModelUpdate", "deleteSurface")
50
+ _A2UI_V0_9_MESSAGE_ACTION_KEYS = ("createSurface", "updateComponents", "updateDataModel", "deleteSurface")
51
+
52
+ # v0.9 messages carry `version` as a required sibling field next to the single action key (e.g.
53
+ # ``{"version": "v0.9", "deleteSurface": {...}}``); v0.8 messages have no such top-level field.
54
+ _A2UI_V0_9_SIBLING_FIELDS = ("version",)
55
+
56
+ _A2UI_MESSAGE_ACTION_KEYS_BY_VERSION: dict[str, tuple[str, ...]] = {
57
+ VERSION_0_8: _A2UI_V0_8_MESSAGE_ACTION_KEYS,
58
+ VERSION_0_9: _A2UI_V0_9_MESSAGE_ACTION_KEYS,
59
+ }
60
+ _A2UI_SIBLING_FIELDS_BY_VERSION: dict[str, tuple[str, ...]] = {
61
+ VERSION_0_9: _A2UI_V0_9_SIBLING_FIELDS,
62
+ }
63
+
64
+ _A2UI_BUNDLED_EXAMPLE_VERSIONS = frozenset({VERSION_0_8, VERSION_0_9})
65
+
66
+
67
+ def _default_examples_path(schema_version: str) -> str | None:
68
+ """Return the repo-owned default examples directory for the given schema version."""
69
+ if schema_version not in _A2UI_BUNDLED_EXAMPLE_VERSIONS:
70
+ return None
71
+ examples_dir = Path(__file__).resolve().parent / "examples" / schema_version
72
+ if not examples_dir.is_dir():
73
+ return None
74
+ return str(examples_dir)
75
+
76
+
77
+ def _build_catalog_configs(
78
+ context: A2UIContext, schema_version: str
79
+ ) -> tuple[list[CatalogConfig], list[str], requests.Session | None]:
80
+ """Resolve bundled or URL-backed catalogs for the schema manager."""
81
+ materialized_example_dirs: list[str] = []
82
+ if not context.catalog_configs:
83
+ return (
84
+ [
85
+ BasicCatalog.get_config(
86
+ version=schema_version,
87
+ examples_path=_default_examples_path(schema_version),
88
+ )
89
+ ],
90
+ materialized_example_dirs,
91
+ None,
92
+ )
93
+
94
+ session = requests.Session()
95
+ catalog_configs: list[CatalogConfig] = []
96
+ try:
97
+ for index, entry in enumerate(context.catalog_configs):
98
+ examples_path = None
99
+ if entry.example_urls:
100
+ examples_path = materialize_example_urls(entry.example_urls, session=session)
101
+ materialized_example_dirs.append(examples_path)
102
+ catalog_configs.append(
103
+ CatalogConfig(
104
+ name=f"custom-{index}",
105
+ provider=UrlCatalogProvider(entry.catalog_url, session=session),
106
+ examples_path=examples_path,
107
+ )
108
+ )
109
+ return catalog_configs, materialized_example_dirs, session
110
+ except Exception:
111
+ release_materialized_example_dirs(materialized_example_dirs)
112
+ session.close()
113
+ raise
114
+
115
+
116
+ class A2UIMiddleware(AgentMiddleware):
117
+ """Middleware for A2UI protocol responsibilities.
118
+
119
+ The current implementation stores minimal state required by callers and
120
+ provides protocol-defined hooks with default no-op behavior.
121
+ """
122
+
123
+ tools: list[Any] = []
124
+ system_prompt_additions: str | None = None
125
+
126
+ def __init__(self, context: A2UIContext | None = None) -> None:
127
+ """Create a middleware instance.
128
+
129
+ Args:
130
+ context: Optional A2UI context resolved at agent creation.
131
+ """
132
+ self.context = context or A2UIContext()
133
+ self.schema_version = self.context.schema_version or STANDARD_SCHEMA_VERSION
134
+
135
+ self._materialized_example_dirs: list[str] = []
136
+ self._catalog_session: requests.Session | None = None
137
+ catalog_configs, self._materialized_example_dirs, self._catalog_session = _build_catalog_configs(
138
+ self.context, self.schema_version
139
+ )
140
+
141
+ try:
142
+ self._include_examples = any(catalog_config.examples_path is not None for catalog_config in catalog_configs)
143
+ self.schema_manager = A2uiSchemaManager(
144
+ version=self.schema_version,
145
+ catalogs=catalog_configs,
146
+ # remove_strict_validation has no return type annotation; cast pins the callable
147
+ # signature so schema_modifiers receives a correctly-typed function.
148
+ schema_modifiers=[cast(Callable[[dict[str, Any]], dict[str, Any]], remove_strict_validation)],
149
+ )
150
+ except Exception:
151
+ self.close()
152
+ raise
153
+
154
+ def __del__(self) -> None:
155
+ """Cleanup resources on middleware instance destruction."""
156
+ try:
157
+ self.close()
158
+ except Exception:
159
+ pass
160
+
161
+ def close(self) -> None:
162
+ """Release downloaded example fixture directories and catalog HTTP session."""
163
+ if self._materialized_example_dirs:
164
+ release_materialized_example_dirs(self._materialized_example_dirs)
165
+ self._materialized_example_dirs = []
166
+ if self._catalog_session is not None:
167
+ self._catalog_session.close()
168
+ self._catalog_session = None
169
+
170
+ def before_model(self, state: dict[str, Any]) -> dict[str, Any]:
171
+ """No-op hook prior to model call in first implementation pass."""
172
+ return {}
173
+
174
+ async def abefore_model(self, state: dict[str, Any]) -> dict[str, Any]:
175
+ """Asynchronous mirror of before_model."""
176
+ return self.before_model(state)
177
+
178
+ def modify_model_request(self, request: ModelRequest, state: dict[str, Any]) -> ModelRequest:
179
+ """Append A2UI prompt instructions when enabled."""
180
+ if not self.context.enabled:
181
+ return request
182
+
183
+ prompt = request.get("system_prompt") or ""
184
+ prompt = prompt.rstrip("\n")
185
+ include_examples = self._include_examples
186
+ prompt = self.schema_manager.generate_system_prompt(
187
+ role_description=prompt,
188
+ include_schema=True,
189
+ include_examples=include_examples,
190
+ validate_examples=include_examples,
191
+ )
192
+ request["system_prompt"] = prompt
193
+ return request
194
+
195
+ def after_model(self, state: dict[str, Any]) -> dict[str, Any]:
196
+ """No-op post-model hook."""
197
+ return {}
198
+
199
+ async def aafter_model(self, state: dict[str, Any]) -> dict[str, Any]:
200
+ """Asynchronous mirror of after_model."""
201
+ return self.after_model(state)
202
+
203
+ def on_final_response(self, content: str, context: dict[str, Any]) -> None:
204
+ """Capture final response and attach validated A2UI metadata."""
205
+ if not self.context.enabled:
206
+ return
207
+
208
+ a2ui_content, sanitized_content = self._extract_a2ui_content(content)
209
+ self._apply_a2ui_content(context, a2ui_content, sanitized_content)
210
+
211
+ async def aon_final_response(self, content: str, context: dict[str, Any]) -> None:
212
+ """Capture final response and repair invalid A2UI content once when configured."""
213
+ if not self.context.enabled:
214
+ return
215
+
216
+ a2ui_content, sanitized_content = self._extract_a2ui_content(content)
217
+ if self._should_attempt_repair(content, context, a2ui_content):
218
+ repaired = await self._repair_a2ui_content(content, context, a2ui_content)
219
+ if repaired is not None:
220
+ repaired_a2ui_content, repaired_sanitized_content = self._extract_a2ui_content(repaired)
221
+ if repaired_a2ui_content.is_valid:
222
+ a2ui_content = repaired_a2ui_content
223
+ sanitized_content = repaired_sanitized_content
224
+
225
+ self._apply_a2ui_content(context, a2ui_content, sanitized_content)
226
+
227
+ def _apply_a2ui_content(
228
+ self, context: dict[str, Any], a2ui_content: A2UIContentMetadata, sanitized_content: str
229
+ ) -> None:
230
+ """Store A2UI metadata and sanitized visible content in hook context."""
231
+ context["a2ui_content"] = a2ui_content.model_dump(exclude_none=True)
232
+ final_state = context.get("final_state")
233
+ if isinstance(final_state, dict):
234
+ final_state["a2ui_content"] = a2ui_content.model_dump(exclude_none=True)
235
+
236
+ context["final_content"] = sanitized_content
237
+ event = context.get("event")
238
+ if isinstance(event, dict) and "content" in event:
239
+ event["content"] = sanitized_content
240
+
241
+ def _should_attempt_repair(self, content: str, context: dict[str, Any], a2ui_content: A2UIContentMetadata) -> bool:
242
+ """Return True when invalid marked A2UI content is eligible for one repair attempt."""
243
+ has_validation_errors = bool(
244
+ isinstance(a2ui_content.validation, dict) and a2ui_content.validation.get("errors")
245
+ )
246
+ if a2ui_content.is_valid and not has_validation_errors:
247
+ return False
248
+ if not isinstance(content, str) or not content:
249
+ return False
250
+ if A2UI_OPEN_TAG not in content and A2UI_CLOSE_TAG not in content:
251
+ return False
252
+ return self._get_repair_handler(context) is not None
253
+
254
+ async def _repair_a2ui_content(
255
+ self, content: str, context: dict[str, Any], a2ui_content: A2UIContentMetadata
256
+ ) -> str | None:
257
+ """Invoke the configured A2UI repair handler once and return repaired content."""
258
+ repair_handler = self._get_repair_handler(context)
259
+ if repair_handler is None:
260
+ return None
261
+
262
+ try:
263
+ logger.info("Attempting A2UI final-response repair attempt 1/%d", _A2UI_REPAIR_ATTEMPT_LIMIT)
264
+ result = repair_handler(
265
+ content=content,
266
+ context=context,
267
+ validation=a2ui_content.validation,
268
+ schema_version=self.schema_version,
269
+ )
270
+ if inspect.isawaitable(result):
271
+ result = await result
272
+ except Exception as exc:
273
+ logger.warning("A2UI final-response repair failed: %s", exc, exc_info=True)
274
+ return None
275
+
276
+ if isinstance(result, str) and result:
277
+ return result
278
+ logger.info("A2UI final-response repair returned no content")
279
+ return None
280
+
281
+ def _get_repair_handler(self, context: dict[str, Any]) -> Any:
282
+ """Resolve a repair handler from hook context or A2UI context extras."""
283
+ repair_handler = context.get("a2ui_repair_handler")
284
+ if repair_handler is None:
285
+ repair_handler = getattr(self.context, "repair_handler", None)
286
+ if callable(repair_handler):
287
+ return repair_handler
288
+ return None
289
+
290
+ def _extract_a2ui_content(self, content: str) -> tuple[A2UIContentMetadata, str]:
291
+ """Extract validated A2UI metadata and sanitized final text."""
292
+ if not content or not isinstance(content, str):
293
+ return (
294
+ A2UIContentMetadata(
295
+ schema_version=self.schema_version,
296
+ messages=[],
297
+ validation=self._build_validation_block(
298
+ status=A2UIContentMetadataStatus.FALLBACK,
299
+ fallback_reason="missing_content",
300
+ errors=["missing_content"],
301
+ ),
302
+ ),
303
+ "",
304
+ )
305
+
306
+ if not self._contains_a2ui_payload(content):
307
+ return (
308
+ A2UIContentMetadata(
309
+ schema_version=self.schema_version,
310
+ messages=[],
311
+ validation=self._build_validation_block(
312
+ status=A2UIContentMetadataStatus.FALLBACK,
313
+ fallback_reason="missing_content",
314
+ errors=["missing_content"],
315
+ ),
316
+ ),
317
+ content,
318
+ )
319
+
320
+ messages: list[dict[str, Any]] = []
321
+ selected_catalog = self.schema_manager.get_selected_catalog()
322
+ errors: list[str] = []
323
+ repairs: list[dict[str, Any]] = []
324
+
325
+ try:
326
+ response_text_parts, payload_chunks = self._split_a2ui_response(content)
327
+ except ValueError as e:
328
+ return (
329
+ A2UIContentMetadata(
330
+ schema_version=self.schema_version,
331
+ messages=[],
332
+ validation=self._build_validation_block(
333
+ status=A2UIContentMetadataStatus.FALLBACK,
334
+ fallback_reason="invalid_content",
335
+ errors=[str(e)],
336
+ ),
337
+ ),
338
+ content,
339
+ )
340
+
341
+ for payload_chunk in payload_chunks:
342
+ try:
343
+ raw_messages = self._parse_payload_chunk(payload_chunk)
344
+ normalized_messages, chunk_repairs = self._split_merged_messages(raw_messages)
345
+ selected_catalog.validator.validate(normalized_messages)
346
+ messages.extend(cast(list[dict[str, Any]], normalized_messages))
347
+ repairs.extend(chunk_repairs)
348
+ except (ValueError, jsonschema.exceptions.ValidationError) as e:
349
+ errors.append(str(e))
350
+ logger.warning("Skipping invalid A2UI block: %s", e)
351
+
352
+ if not messages:
353
+ return (
354
+ A2UIContentMetadata(
355
+ schema_version=self.schema_version,
356
+ messages=[],
357
+ validation=self._build_validation_block(
358
+ status=A2UIContentMetadataStatus.FALLBACK,
359
+ fallback_reason="invalid_content",
360
+ errors=errors or ["invalid_content"],
361
+ ),
362
+ ),
363
+ "".join(response_text_parts),
364
+ )
365
+
366
+ semantic_errors = self._validate_semantic_renderability(messages)
367
+ if semantic_errors:
368
+ return (
369
+ A2UIContentMetadata(
370
+ schema_version=self.schema_version,
371
+ messages=messages,
372
+ validation=self._build_validation_block(
373
+ status=A2UIContentMetadataStatus.FALLBACK,
374
+ fallback_reason="invalid_content",
375
+ errors=errors + semantic_errors,
376
+ ),
377
+ ),
378
+ "".join(response_text_parts),
379
+ )
380
+
381
+ return (
382
+ A2UIContentMetadata(
383
+ schema_version=self.schema_version,
384
+ messages=messages,
385
+ validation=self._build_validation_block(
386
+ status=A2UIContentMetadataStatus.VALID,
387
+ errors=errors or None,
388
+ repairs=repairs or None,
389
+ ),
390
+ ),
391
+ "".join(response_text_parts),
392
+ )
393
+
394
+ def _extract_surface_message(self, message: dict[str, Any]) -> tuple[str | None, dict[str, Any] | None]:
395
+ """Return the message type and dict payload for semantic renderability checks."""
396
+ if not isinstance(message, dict) or not message:
397
+ return None, None
398
+
399
+ message_type, payload = next(iter(message.items()))
400
+ if not isinstance(payload, dict):
401
+ return None, None
402
+
403
+ return message_type, payload
404
+
405
+ def _collect_component_ids(self, surface_update: dict[str, Any]) -> set[str]:
406
+ """Collect component ids from one surface update payload."""
407
+ component_ids: set[str] = set()
408
+ components = surface_update.get("components")
409
+ if not isinstance(components, list):
410
+ return component_ids
411
+
412
+ for component in components:
413
+ if not isinstance(component, dict):
414
+ continue
415
+ component_id = component.get("id")
416
+ if isinstance(component_id, str) and component_id:
417
+ component_ids.add(component_id)
418
+
419
+ return component_ids
420
+
421
+ def _collect_surface_renderability_state(
422
+ self, messages: list[dict[str, Any]]
423
+ ) -> tuple[dict[str, list[dict[str, Any]]], dict[str, list[dict[str, Any]]], dict[str, set[str]]]:
424
+ """Collect beginRendering, surfaceUpdate, and component-id state by surface."""
425
+ begin_renderings: dict[str, list[dict[str, Any]]] = {}
426
+ surface_updates: dict[str, list[dict[str, Any]]] = {}
427
+ component_ids_by_surface: dict[str, set[str]] = {}
428
+
429
+ for message in messages:
430
+ message_type, payload = self._extract_surface_message(message)
431
+ if payload is None:
432
+ continue
433
+
434
+ surface_id = payload.get("surfaceId")
435
+ if not isinstance(surface_id, str) or not surface_id:
436
+ continue
437
+
438
+ if message_type == "beginRendering":
439
+ # Keep the full list because final-response aggregation can still surface
440
+ # repeated beginRendering payloads for one surface, even though this pass
441
+ # only validates the first root until duplicate semantics are defined.
442
+ begin_renderings.setdefault(surface_id, []).append(payload)
443
+ continue
444
+
445
+ if message_type == "surfaceUpdate":
446
+ surface_updates.setdefault(surface_id, []).append(payload)
447
+ component_ids_by_surface.setdefault(surface_id, set()).update(self._collect_component_ids(payload))
448
+
449
+ return begin_renderings, surface_updates, component_ids_by_surface
450
+
451
+ def _iter_layout_child_reference_ids(self, component_payload: dict[str, Any]) -> list[str]:
452
+ """Collect v0.8 layout child ids from explicitList and template.componentId only."""
453
+ children = component_payload.get("children")
454
+ if not isinstance(children, dict):
455
+ return []
456
+
457
+ child_ids = [
458
+ child_id for child_id in children.get("explicitList", []) if isinstance(child_id, str) and child_id
459
+ ]
460
+
461
+ template = children.get("template")
462
+ if isinstance(template, dict):
463
+ template_component_id = template.get("componentId")
464
+ if isinstance(template_component_id, str) and template_component_id:
465
+ child_ids.append(template_component_id)
466
+
467
+ return child_ids
468
+
469
+ def _iter_child_reference_ids(self, node: Any) -> list[str]:
470
+ """Collect v0.8 child component ids referenced by a rendered component payload."""
471
+ if not isinstance(node, dict) or not node:
472
+ return []
473
+
474
+ component_type, payload = next(iter(node.items()))
475
+ if not isinstance(payload, dict):
476
+ return []
477
+
478
+ # This helper intentionally tracks only the v0.8 standard-catalog component
479
+ # shapes validated by BasicCatalog. Unknown/third-party components are rejected
480
+ # by the selected schema before this semantic pass runs today.
481
+
482
+ if component_type in {"Row", "Column", "List"}:
483
+ return self._iter_layout_child_reference_ids(payload)
484
+
485
+ if component_type in {"Card", "Button"}:
486
+ child_id = payload.get("child")
487
+ return [child_id] if isinstance(child_id, str) and child_id else []
488
+
489
+ if component_type == "Tabs":
490
+ tab_items = payload.get("tabItems")
491
+ if not isinstance(tab_items, list):
492
+ return []
493
+
494
+ return [
495
+ child_id
496
+ for tab_item in tab_items
497
+ if isinstance(tab_item, dict)
498
+ for child_id in [tab_item.get("child")]
499
+ if isinstance(child_id, str) and child_id
500
+ ]
501
+
502
+ if component_type == "Modal":
503
+ return [
504
+ child_id
505
+ for child_id in [payload.get("entryPointChild"), payload.get("contentChild")]
506
+ if isinstance(child_id, str) and child_id
507
+ ]
508
+
509
+ return []
510
+
511
+ def _validate_explicit_child_references(
512
+ self, surface_id: str, surface_update: dict[str, Any], component_ids: set[str]
513
+ ) -> list[str]:
514
+ """Check child references against component ids in one update."""
515
+ components = surface_update.get("components")
516
+ if not isinstance(components, list):
517
+ return []
518
+
519
+ errors: list[str] = []
520
+ for component in components:
521
+ if not isinstance(component, dict):
522
+ continue
523
+
524
+ component_name = component.get("id")
525
+ rendered_component = component.get("component")
526
+ if not isinstance(rendered_component, dict):
527
+ continue
528
+
529
+ missing_child_ids = sorted(
530
+ {
531
+ child_id
532
+ for child_id in self._iter_child_reference_ids(rendered_component)
533
+ if child_id not in component_ids
534
+ }
535
+ )
536
+ if missing_child_ids:
537
+ component_label = component_name if isinstance(component_name, str) and component_name else "<unknown>"
538
+ errors.append(
539
+ "surfaceUpdate.surfaceId "
540
+ f"'{surface_id}' component '{component_label}' references missing child ids: "
541
+ f"{', '.join(missing_child_ids)}"
542
+ )
543
+
544
+ return errors
545
+
546
+ def _validate_surface_renderability(
547
+ self,
548
+ *,
549
+ surface_id: str,
550
+ updates: list[dict[str, Any]],
551
+ begin_renderings: list[dict[str, Any]],
552
+ component_ids: set[str],
553
+ ) -> list[str]:
554
+ """Validate one surface's begin/update/root/child relationships."""
555
+ if not begin_renderings:
556
+ return [f"surfaceUpdate.surfaceId '{surface_id}' requires a matching beginRendering.surfaceId"]
557
+
558
+ errors: list[str] = []
559
+ root_id = begin_renderings[0].get("root")
560
+ if isinstance(root_id, str) and root_id and root_id not in component_ids:
561
+ errors.append(
562
+ f"beginRendering.surfaceId '{surface_id}' root '{root_id}' is missing from surfaceUpdate.components ids"
563
+ )
564
+
565
+ for update in updates:
566
+ errors.extend(self._validate_explicit_child_references(surface_id, update, component_ids))
567
+
568
+ return errors
569
+
570
+ def _validate_semantic_renderability(self, messages: list[dict[str, Any]]) -> list[str]:
571
+ """Return deterministic errors for schema-valid but non-renderable v0.8 message sequences."""
572
+ if self.schema_version != VERSION_0_8:
573
+ return []
574
+
575
+ begin_renderings, surface_updates, component_ids_by_surface = self._collect_surface_renderability_state(
576
+ messages
577
+ )
578
+ errors: list[str] = []
579
+
580
+ for surface_id, updates in surface_updates.items():
581
+ errors.extend(
582
+ self._validate_surface_renderability(
583
+ surface_id=surface_id,
584
+ updates=updates,
585
+ begin_renderings=begin_renderings.get(surface_id, []),
586
+ component_ids=component_ids_by_surface.get(surface_id, set()),
587
+ )
588
+ )
589
+
590
+ return list(dict.fromkeys(errors))
591
+
592
+ def _split_a2ui_response(self, content: str) -> tuple[list[str], list[str]]:
593
+ """Split tagged A2UI payloads from surrounding visible text."""
594
+ text_parts: list[str] = []
595
+ payload_chunks: list[str] = []
596
+ cursor = 0
597
+
598
+ while True:
599
+ open_index = content.find(A2UI_OPEN_TAG, cursor)
600
+ if open_index == -1:
601
+ text_parts.append(content[cursor:])
602
+ break
603
+
604
+ text_parts.append(content[cursor:open_index])
605
+ payload_start = open_index + len(A2UI_OPEN_TAG)
606
+ close_index = content.find(A2UI_CLOSE_TAG, payload_start)
607
+ if close_index == -1:
608
+ raise ValueError("Missing closing A2UI tag")
609
+
610
+ payload_chunks.append(content[payload_start:close_index])
611
+ cursor = close_index + len(A2UI_CLOSE_TAG)
612
+
613
+ return text_parts, payload_chunks
614
+
615
+ @staticmethod
616
+ def _parse_payload_chunk(payload_chunk: str) -> list[JsonValue]:
617
+ """Parse one raw A2UI JSON chunk into a flat list of messages."""
618
+ parsed = json.loads(payload_chunk)
619
+ if isinstance(parsed, dict):
620
+ return [parsed]
621
+ if not isinstance(parsed, list):
622
+ raise ValueError("A2UI payload must decode to a message object or list of messages")
623
+ return cast(list[JsonValue], parsed)
624
+
625
+ def _contains_a2ui_payload(self, content: str) -> bool:
626
+ """Return True when the response contains an A2UI payload marker."""
627
+ return A2UI_OPEN_TAG in content and A2UI_CLOSE_TAG in content
628
+
629
+ def _split_merged_messages(self, raw_messages: list[JsonValue]) -> tuple[list[JsonValue], list[dict[str, Any]]]:
630
+ """Canonicalize merged A2UI messages into one message per action key.
631
+
632
+ The A2UI protocol requires each streamed message to carry exactly one action key, but the
633
+ schema does not enforce it, so a model may emit one object that fuses several action keys
634
+ (e.g. v0.8 ``beginRendering`` + ``surfaceUpdate`` + ``dataModelUpdate``). Such an object is
635
+ rejected by the renderer even though it passes schema validation. Each action key has its
636
+ own disjoint body (no shared top-level fields other than the v0.9 sibling fields below), so
637
+ fanning a merged object out into one message per action key is lossless by construction:
638
+ every key's value is re-emitted unchanged, in canonical stream order, before validation.
639
+ v0.9 sibling fields (currently just ``version``) are not action keys and are re-attached to
640
+ every split message so each one stays independently valid.
641
+
642
+ Unknown top-level keys are rejected rather than dropped. This middleware validates with
643
+ ``remove_strict_validation`` applied (see ``__init__``), which strips
644
+ ``additionalProperties: false`` from the schema, so the validator will not reject an
645
+ unrecognized key on its own -- this helper is the only gate for it. Dropping the key would
646
+ let a likely-malformed payload pass as ``valid`` with no downstream backstop (the H-1
647
+ regression), so we raise and route it to ``validation.errors`` instead. An unknown key is
648
+ also a useful signal -- a hallucinated field or a mismatched schema version -- worth
649
+ surfacing rather than silently discarding. The allowed action-key and sibling-field sets are
650
+ selected from ``self.schema_version``; a message built for one version (e.g. a v0.9 message
651
+ carrying ``version``) is rejected under another version's middleware instance rather than
652
+ silently accepted.
653
+
654
+ ``raw_messages`` is the direct `json.loads(...)` output before schema validation, so list
655
+ elements may still be arbitrary JSON values rather than message dicts. Returns the
656
+ normalized raw payload plus a list of non-fatal repair notes describing any split performed.
657
+ """
658
+ action_key_set = _A2UI_MESSAGE_ACTION_KEYS_BY_VERSION.get(self.schema_version)
659
+ if action_key_set is None:
660
+ raise ValueError(f"Unsupported A2UI schema version for message normalization: {self.schema_version}")
661
+ sibling_field_set = _A2UI_SIBLING_FIELDS_BY_VERSION.get(self.schema_version, ())
662
+
663
+ normalized: list[JsonValue] = []
664
+ repairs: list[dict[str, Any]] = []
665
+
666
+ for index, raw_message in enumerate(raw_messages):
667
+ if not isinstance(raw_message, dict):
668
+ normalized.append(raw_message)
669
+ continue
670
+
671
+ action_keys = [key for key in action_key_set if key in raw_message]
672
+ sibling_fields = {field: raw_message[field] for field in sibling_field_set if field in raw_message}
673
+ unknown_keys = [key for key in raw_message if key not in action_key_set and key not in sibling_field_set]
674
+
675
+ if "version" in sibling_fields and sibling_fields["version"] != f"v{self.schema_version}":
676
+ raise ValueError(
677
+ f"A2UI message version {sibling_fields['version']!r} does not match schema version v{self.schema_version}"
678
+ )
679
+
680
+ if unknown_keys:
681
+ # Strict validation is disabled for this schema (remove_strict_validation strips
682
+ # additionalProperties: false), so the validator will not reject these keys -- this
683
+ # raise is the only gate. Dropping them would let a malformed payload pass as
684
+ # `valid`; reject so it routes to validation.errors instead. See the docstring.
685
+ unknown_keys_text = ", ".join(sorted(unknown_keys))
686
+ raise ValueError(f"A2UI message contains unknown top-level keys: {unknown_keys_text}")
687
+
688
+ if not action_keys:
689
+ raise ValueError("A2UI message must contain exactly one action key")
690
+ if len(action_keys) == 1:
691
+ normalized.append(raw_message)
692
+ continue
693
+
694
+ normalized.extend({**sibling_fields, key: raw_message[key]} for key in action_keys)
695
+ repairs.append({"type": "split_merged_message", "from_index": index, "produced": len(action_keys)})
696
+
697
+ return normalized, repairs
698
+
699
+ def _build_validation_block(
700
+ self,
701
+ status: str,
702
+ fallback_reason: str | None = None,
703
+ errors: list[str] | None = None,
704
+ repairs: list[dict[str, Any]] | None = None,
705
+ ) -> dict[str, Any]:
706
+ """Build normalized validation result object.
707
+
708
+ ``repairs`` records non-fatal deterministic normalizations (e.g. merged-message splits) as
709
+ sidecar telemetry; it never changes ``status`` and is kept separate from ``errors`` so a
710
+ valid-but-repaired payload is not mistaken for a failed one.
711
+ """
712
+ block: dict[str, Any] = {"status": status}
713
+ if errors is not None:
714
+ block["errors"] = errors
715
+ if repairs:
716
+ block["repairs"] = repairs
717
+ if fallback_reason is not None and status == A2UIContentMetadataStatus.FALLBACK:
718
+ block["fallback_reason"] = fallback_reason
719
+ return block
720
+
721
+ def normalize_user_action(self, action: dict[str, Any]) -> A2UIActionValidationResult:
722
+ """Normalize raw renderer action payloads to canonical structure."""
723
+ if not self.context.enabled or not self.context.allow_user_actions:
724
+ return A2UIActionValidationResult(status=A2UIActionStatus.INVALID, errors=["user_actions_disabled"])
725
+
726
+ if not isinstance(action, dict):
727
+ return A2UIActionValidationResult(status=A2UIActionStatus.INVALID, errors=["action_payload_must_be_object"])
728
+
729
+ wrapped_action = action.get("userAction")
730
+ if wrapped_action is not None and not isinstance(wrapped_action, dict):
731
+ return A2UIActionValidationResult(status=A2UIActionStatus.INVALID, errors=["missing_userAction_payload"])
732
+ raw_action = wrapped_action if isinstance(wrapped_action, dict) else action
733
+
734
+ missing_fields = []
735
+ for field in _REQUIRED_ACTION_FIELDS:
736
+ if field not in raw_action:
737
+ missing_fields.append(f"missing_{field}")
738
+ if missing_fields:
739
+ return A2UIActionValidationResult(status=A2UIActionStatus.INVALID, errors=missing_fields)
740
+
741
+ if not isinstance(raw_action["context"], dict):
742
+ return A2UIActionValidationResult(status=A2UIActionStatus.INVALID, errors=["context_must_be_object"])
743
+
744
+ try:
745
+ normalized_action = A2UIActionContext.from_normalized_payload(
746
+ name=str(raw_action["name"]),
747
+ surface_id=str(raw_action["surfaceId"]),
748
+ source_component_id=str(raw_action["sourceComponentId"]),
749
+ timestamp=_normalize_iso_timestamp(str(raw_action["timestamp"])),
750
+ context=raw_action["context"],
751
+ )
752
+ return A2UIActionValidationResult(status=A2UIActionStatus.VALID, normalized_action=normalized_action)
753
+ except ValueError as exc:
754
+ return A2UIActionValidationResult(status=A2UIActionStatus.INVALID, errors=[str(exc)])