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.
- aip_agents/__init__.py +66 -0
- aip_agents/a2a/__init__.py +19 -0
- aip_agents/a2a/server/__init__.py +10 -0
- aip_agents/a2a/server/base_executor.py +1125 -0
- aip_agents/a2a/server/google_adk_executor.py +198 -0
- aip_agents/a2a/server/langflow_executor.py +180 -0
- aip_agents/a2a/server/langgraph_executor.py +270 -0
- aip_agents/a2a/types.py +232 -0
- aip_agents/a2ui/__init__.py +26 -0
- aip_agents/a2ui/catalog_provider.py +109 -0
- aip_agents/a2ui/examples/0.8/booking_form.json +1 -0
- aip_agents/a2ui/examples/0.8/confirmation.json +1 -0
- aip_agents/a2ui/examples/0.8/single_column_list.json +1 -0
- aip_agents/a2ui/examples/0.9/booking_form.json +1 -0
- aip_agents/a2ui/examples/0.9/confirmation.json +1 -0
- aip_agents/a2ui/examples/0.9/single_column_list.json +1 -0
- aip_agents/a2ui/middleware.py +754 -0
- aip_agents/a2ui/models.py +153 -0
- aip_agents/a2ui/public_url.py +138 -0
- aip_agents/a2ui/stream_filter.py +67 -0
- aip_agents/agent/__init__.py +72 -0
- aip_agents/agent/base_agent.py +1437 -0
- aip_agents/agent/base_langgraph_agent.py +4048 -0
- aip_agents/agent/google_adk_agent.py +932 -0
- aip_agents/agent/google_adk_constants.py +11 -0
- aip_agents/agent/graph_runtime.py +185 -0
- aip_agents/agent/hitl/__init__.py +24 -0
- aip_agents/agent/hitl/config.py +28 -0
- aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
- aip_agents/agent/hitl/manager.py +532 -0
- aip_agents/agent/hitl/models.py +18 -0
- aip_agents/agent/hitl/prompt/__init__.py +9 -0
- aip_agents/agent/hitl/prompt/base.py +42 -0
- aip_agents/agent/hitl/prompt/deferred.py +73 -0
- aip_agents/agent/hitl/registry.py +149 -0
- aip_agents/agent/interface.py +138 -0
- aip_agents/agent/interfaces.py +65 -0
- aip_agents/agent/langflow_agent.py +464 -0
- aip_agents/agent/langgraph_react_agent.py +3741 -0
- aip_agents/agent/model_invocation_adapter.py +346 -0
- aip_agents/agent/system_instruction_context.py +34 -0
- aip_agents/agent/tool_execution_engine.py +587 -0
- aip_agents/audio_interface/__init__.py +30 -0
- aip_agents/audio_interface/audio_agent_adapter.py +254 -0
- aip_agents/audio_interface/config.py +146 -0
- aip_agents/audio_interface/errors.py +15 -0
- aip_agents/audio_interface/livekit_audio_session.py +504 -0
- aip_agents/audio_interface/livekit_realtime_audio_session.py +142 -0
- aip_agents/audio_interface/session_factory.py +40 -0
- aip_agents/clients/__init__.py +10 -0
- aip_agents/clients/langflow/__init__.py +10 -0
- aip_agents/clients/langflow/client.py +477 -0
- aip_agents/clients/langflow/types.py +18 -0
- aip_agents/common/__init__.py +5 -0
- aip_agents/common/callable_introspection.py +17 -0
- aip_agents/constants.py +23 -0
- aip_agents/context/__init__.py +5 -0
- aip_agents/context/budget.py +237 -0
- aip_agents/context/compaction.py +339 -0
- aip_agents/context/metadata.py +169 -0
- aip_agents/context/offload.py +449 -0
- aip_agents/context/preparation.py +196 -0
- aip_agents/context/prompt.py +61 -0
- aip_agents/context/runtime.py +149 -0
- aip_agents/context/session_recall.py +69 -0
- aip_agents/context/state.py +48 -0
- aip_agents/context/utils.py +44 -0
- aip_agents/credentials/manager.py +132 -0
- aip_agents/examples/__init__.py +5 -0
- aip_agents/examples/audio_livekit_session.py +116 -0
- aip_agents/examples/compare_streaming_client.py +783 -0
- aip_agents/examples/compare_streaming_server.py +141 -0
- aip_agents/examples/gl_connectors_smoke.py +148 -0
- aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_google_adk_server.py +78 -0
- aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_server.py +104 -0
- aip_agents/examples/hello_world_a2a_langchain_server.py +78 -0
- aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +77 -0
- aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
- aip_agents/examples/hello_world_a2a_langflow_server.py +80 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +91 -0
- aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_server.py +83 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +78 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +131 -0
- aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +250 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +79 -0
- aip_agents/examples/hello_world_filesystem.py +162 -0
- aip_agents/examples/hello_world_google_adk.py +41 -0
- aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
- aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_stream.py +44 -0
- aip_agents/examples/hello_world_langchain.py +28 -0
- aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
- aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
- aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
- aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
- aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
- aip_agents/examples/hello_world_langchain_stream.py +36 -0
- aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
- aip_agents/examples/hello_world_langflow_agent.py +163 -0
- aip_agents/examples/hello_world_langgraph.py +39 -0
- aip_agents/examples/hello_world_langgraph_gl_connector_twitter.py +44 -0
- aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
- aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_skills.py +62 -0
- aip_agents/examples/hello_world_langgraph_stream.py +43 -0
- aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_model_switch_cli.py +210 -0
- aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
- aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
- aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
- aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
- aip_agents/examples/hello_world_multi_agent_tool_output_client.py +57 -0
- aip_agents/examples/hello_world_multi_agent_tool_output_server.py +131 -0
- aip_agents/examples/hello_world_pii_logger.py +21 -0
- aip_agents/examples/hello_world_ptc.py +49 -0
- aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
- aip_agents/examples/hello_world_sentry.py +133 -0
- aip_agents/examples/hello_world_step_limits.py +273 -0
- aip_agents/examples/hello_world_stock_a2a_server.py +100 -0
- aip_agents/examples/hello_world_tool_output_client.py +55 -0
- aip_agents/examples/hello_world_tool_output_server.py +113 -0
- aip_agents/examples/hitl_demo.py +724 -0
- aip_agents/examples/mcp_configs/configs.py +63 -0
- aip_agents/examples/mcp_servers/common.py +76 -0
- aip_agents/examples/mcp_servers/mcp_name.py +29 -0
- aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
- aip_agents/examples/mcp_servers/mcp_time.py +10 -0
- aip_agents/examples/pii_demo_langgraph_client.py +69 -0
- aip_agents/examples/pii_demo_langgraph_server.py +125 -0
- aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
- aip_agents/examples/pii_demo_multi_agent_server.py +246 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +87 -0
- aip_agents/examples/tools/__init__.py +27 -0
- aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
- aip_agents/examples/tools/adk_weather_tool.py +60 -0
- aip_agents/examples/tools/data_generator_tool.py +103 -0
- aip_agents/examples/tools/data_visualization_tool.py +312 -0
- aip_agents/examples/tools/image_artifact_tool.py +136 -0
- aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
- aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
- aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
- aip_agents/examples/tools/langchain_weather_tool.py +48 -0
- aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
- aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
- aip_agents/examples/tools/multiply_tool.py +43 -0
- aip_agents/examples/tools/pii_demo_tools.py +189 -0
- aip_agents/examples/tools/random_chart_tool.py +142 -0
- aip_agents/examples/tools/serper_tool.py +202 -0
- aip_agents/examples/tools/stock_tools.py +82 -0
- aip_agents/examples/tools/table_generator_tool.py +167 -0
- aip_agents/examples/tools/time_tool.py +82 -0
- aip_agents/examples/tools/weather_forecast_tool.py +38 -0
- aip_agents/guardrails/__init__.py +83 -0
- aip_agents/guardrails/engines/__init__.py +69 -0
- aip_agents/guardrails/engines/base.py +90 -0
- aip_agents/guardrails/engines/nemo.py +101 -0
- aip_agents/guardrails/engines/phrase_matcher.py +113 -0
- aip_agents/guardrails/exceptions.py +39 -0
- aip_agents/guardrails/manager.py +163 -0
- aip_agents/guardrails/middleware.py +199 -0
- aip_agents/guardrails/schemas.py +63 -0
- aip_agents/guardrails/utils.py +45 -0
- aip_agents/integration/__init__.py +43 -0
- aip_agents/integration/agent.py +58 -0
- aip_agents/integration/guardrails.py +52 -0
- aip_agents/integration/hitl.py +71 -0
- aip_agents/integration/ptc.py +95 -0
- aip_agents/integration/skills.py +37 -0
- aip_agents/integration/storage.py +81 -0
- aip_agents/integration/version.py +103 -0
- aip_agents/mcp/__init__.py +1 -0
- aip_agents/mcp/client/__init__.py +60 -0
- aip_agents/mcp/client/base_mcp_client.py +377 -0
- aip_agents/mcp/client/connection_manager.py +231 -0
- aip_agents/mcp/client/errors.py +119 -0
- aip_agents/mcp/client/google_adk/__init__.py +45 -0
- aip_agents/mcp/client/google_adk/client.py +383 -0
- aip_agents/mcp/client/langchain/__init__.py +11 -0
- aip_agents/mcp/client/langchain/client.py +265 -0
- aip_agents/mcp/client/persistent_session.py +704 -0
- aip_agents/mcp/client/session_pool.py +358 -0
- aip_agents/mcp/client/transports.py +288 -0
- aip_agents/mcp/utils/__init__.py +7 -0
- aip_agents/mcp/utils/config_validator.py +139 -0
- aip_agents/memory/__init__.py +14 -0
- aip_agents/memory/adapters/__init__.py +10 -0
- aip_agents/memory/adapters/base_adapter.py +815 -0
- aip_agents/memory/adapters/mem0.py +286 -0
- aip_agents/memory/base.py +84 -0
- aip_agents/memory/constants.py +49 -0
- aip_agents/memory/factory.py +86 -0
- aip_agents/memory/guidance.py +20 -0
- aip_agents/memory/simple_memory.py +47 -0
- aip_agents/middleware/__init__.py +65 -0
- aip_agents/middleware/backends/__init__.py +72 -0
- aip_agents/middleware/backends/in_memory.py +578 -0
- aip_agents/middleware/backends/local_disk.py +1203 -0
- aip_agents/middleware/backends/protocol.py +353 -0
- aip_agents/middleware/backends/sandbox.py +1431 -0
- aip_agents/middleware/backends/sandbox_files.py +113 -0
- aip_agents/middleware/backends/utils.py +293 -0
- aip_agents/middleware/base.py +239 -0
- aip_agents/middleware/filesystem.py +690 -0
- aip_agents/middleware/manager.py +291 -0
- aip_agents/middleware/memory.py +532 -0
- aip_agents/middleware/pii_middleware.py +192 -0
- aip_agents/middleware/schema.py +19 -0
- aip_agents/middleware/skills.py +872 -0
- aip_agents/middleware/task_delegation.py +1656 -0
- aip_agents/middleware/todolist.py +276 -0
- aip_agents/middleware/tools/__init__.py +23 -0
- aip_agents/middleware/tools/edit_file.py +70 -0
- aip_agents/middleware/tools/execute.py +428 -0
- aip_agents/middleware/tools/grep_file.py +79 -0
- aip_agents/middleware/tools/ls.py +71 -0
- aip_agents/middleware/tools/read_file.py +239 -0
- aip_agents/middleware/tools/skill_load.py +193 -0
- aip_agents/middleware/tools/skill_resource.py +338 -0
- aip_agents/middleware/tools/write_file.py +114 -0
- aip_agents/middleware/vision.py +322 -0
- aip_agents/ptc/__init__.py +87 -0
- aip_agents/ptc/custom_tools.py +511 -0
- aip_agents/ptc/custom_tools_payload.py +499 -0
- aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
- aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +24 -0
- aip_agents/ptc/custom_tools_templates/custom_init.py.template +26 -0
- aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +149 -0
- aip_agents/ptc/custom_tools_templates/custom_registry.py.template +112 -0
- aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
- aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +87 -0
- aip_agents/ptc/doc_gen.py +122 -0
- aip_agents/ptc/exceptions.py +57 -0
- aip_agents/ptc/executor.py +627 -0
- aip_agents/ptc/mcp/__init__.py +45 -0
- aip_agents/ptc/mcp/sandbox_bridge.py +679 -0
- aip_agents/ptc/mcp/templates/__init__.py +1 -0
- aip_agents/ptc/mcp/templates/mcp_client.py.template +240 -0
- aip_agents/ptc/naming.py +430 -0
- aip_agents/ptc/payload.py +26 -0
- aip_agents/ptc/prompt_builder.py +674 -0
- aip_agents/ptc/ptc_helper.py +16 -0
- aip_agents/ptc/sandbox_bridge.py +297 -0
- aip_agents/ptc/template_utils.py +33 -0
- aip_agents/ptc/templates/__init__.py +1 -0
- aip_agents/ptc/templates/ptc_helper.py.template +134 -0
- aip_agents/ptc/tool_def_helpers.py +101 -0
- aip_agents/ptc/tool_enrichment.py +163 -0
- aip_agents/resilience/__init__.py +10 -0
- aip_agents/resilience/base.py +46 -0
- aip_agents/resilience/tool.py +1661 -0
- aip_agents/sandbox/__init__.py +62 -0
- aip_agents/sandbox/defaults.py +220 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1-datasaur.Dockerfile +120 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1.Dockerfile +100 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2-datasaur.Dockerfile +143 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2.Dockerfile +119 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3-datasaur.Dockerfile +173 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3.Dockerfile +130 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.Dockerfile +164 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.patch_js_context.py +41 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.requirements.txt +29 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2.Dockerfile +87 -0
- aip_agents/sandbox/e2b_runtime.py +27 -0
- aip_agents/sandbox/image_registry.py +98 -0
- aip_agents/sandbox/providers/__init__.py +76 -0
- aip_agents/sandbox/providers/base.py +392 -0
- aip_agents/sandbox/providers/bedrock.py +28 -0
- aip_agents/sandbox/providers/e2b.py +230 -0
- aip_agents/sandbox/providers/opensandbox.py +163 -0
- aip_agents/sandbox/sandbox_runtime.py +967 -0
- aip_agents/sandbox/template_builder.py +240 -0
- aip_agents/sandbox/types.py +141 -0
- aip_agents/sandbox/validation.py +50 -0
- aip_agents/schema/__init__.py +106 -0
- aip_agents/schema/a2a.py +58 -0
- aip_agents/schema/agent.py +117 -0
- aip_agents/schema/artifacts.py +75 -0
- aip_agents/schema/attachments.py +425 -0
- aip_agents/schema/hitl.py +157 -0
- aip_agents/schema/langgraph.py +47 -0
- aip_agents/schema/model_id.py +103 -0
- aip_agents/schema/model_usage.py +50 -0
- aip_agents/schema/runtime_metadata.py +78 -0
- aip_agents/schema/step_limit.py +108 -0
- aip_agents/schema/storage.py +40 -0
- aip_agents/schema/tool_payloads.py +68 -0
- aip_agents/sentry/__init__.py +11 -0
- aip_agents/sentry/sentry.py +267 -0
- aip_agents/skills/__init__.py +30 -0
- aip_agents/skills/errors.py +110 -0
- aip_agents/skills/installer.py +479 -0
- aip_agents/skills/models.py +769 -0
- aip_agents/skills/stager.py +224 -0
- aip_agents/skills/validation.py +53 -0
- aip_agents/storage/__init__.py +41 -0
- aip_agents/storage/base.py +85 -0
- aip_agents/storage/clients/__init__.py +12 -0
- aip_agents/storage/clients/minio_client.py +318 -0
- aip_agents/storage/config.py +62 -0
- aip_agents/storage/providers/__init__.py +15 -0
- aip_agents/storage/providers/base.py +106 -0
- aip_agents/storage/providers/memory.py +114 -0
- aip_agents/storage/providers/object_storage.py +214 -0
- aip_agents/tools/__init__.py +65 -0
- aip_agents/tools/agent_schedule_tool.py +392 -0
- aip_agents/tools/browser_use/__init__.py +83 -0
- aip_agents/tools/browser_use/action_parser.py +104 -0
- aip_agents/tools/browser_use/browser_use_tool.py +1118 -0
- aip_agents/tools/browser_use/llm_config.py +120 -0
- aip_agents/tools/browser_use/minio_storage.py +203 -0
- aip_agents/tools/browser_use/schemas.py +121 -0
- aip_agents/tools/browser_use/session.py +13 -0
- aip_agents/tools/browser_use/session_errors.py +132 -0
- aip_agents/tools/browser_use/steel_session_recording.py +321 -0
- aip_agents/tools/browser_use/streaming.py +263 -0
- aip_agents/tools/browser_use/structured_data_parser.py +257 -0
- aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
- aip_agents/tools/browser_use/types.py +37 -0
- aip_agents/tools/code_sandbox/__init__.py +48 -0
- aip_agents/tools/code_sandbox/code_sandbox_tool.py +350 -0
- aip_agents/tools/code_sandbox/constant.py +13 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +346 -0
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +65 -0
- aip_agents/tools/computer_use/__init__.py +75 -0
- aip_agents/tools/computer_use/computer_use_tool.py +910 -0
- aip_agents/tools/computer_use/schemas.py +78 -0
- aip_agents/tools/computer_use/streaming.py +198 -0
- aip_agents/tools/computer_use/types.py +32 -0
- aip_agents/tools/constants.py +177 -0
- aip_agents/tools/date_range_tool.py +554 -0
- aip_agents/tools/document_loader/__init__.py +44 -0
- aip_agents/tools/document_loader/base_reader.py +302 -0
- aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
- aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
- aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
- aip_agents/tools/document_loader/pdf_splitter.py +169 -0
- aip_agents/tools/execute_ptc_code.py +582 -0
- aip_agents/tools/gl_connector/__init__.py +5 -0
- aip_agents/tools/gl_connector/tool.py +518 -0
- aip_agents/tools/gl_connector_tools.py +170 -0
- aip_agents/tools/memory_search/__init__.py +29 -0
- aip_agents/tools/memory_search/base.py +213 -0
- aip_agents/tools/memory_search/mem0.py +424 -0
- aip_agents/tools/memory_search/schema.py +81 -0
- aip_agents/tools/memory_search_tool.py +34 -0
- aip_agents/tools/streaming_common.py +577 -0
- aip_agents/tools/time_tool.py +117 -0
- aip_agents/tools/tool_config_injector.py +300 -0
- aip_agents/tools/web_search/__init__.py +15 -0
- aip_agents/tools/web_search/serper_tool.py +187 -0
- aip_agents/types/__init__.py +73 -0
- aip_agents/types/a2a_events.py +13 -0
- aip_agents/utils/__init__.py +91 -0
- aip_agents/utils/a2a_connector.py +1758 -0
- aip_agents/utils/artifact_helpers.py +542 -0
- aip_agents/utils/artifacts/__init__.py +20 -0
- aip_agents/utils/artifacts/tracker.py +367 -0
- aip_agents/utils/attachment_helpers.py +178 -0
- aip_agents/utils/constants.py +22 -0
- aip_agents/utils/datetime/__init__.py +34 -0
- aip_agents/utils/datetime/normalization.py +231 -0
- aip_agents/utils/datetime/timezone.py +206 -0
- aip_agents/utils/env_loader.py +27 -0
- aip_agents/utils/event_handler_registry.py +58 -0
- aip_agents/utils/file_prompt_utils.py +276 -0
- aip_agents/utils/file_watchers/__init__.py +33 -0
- aip_agents/utils/file_watchers/base.py +46 -0
- aip_agents/utils/file_watchers/e2b.py +128 -0
- aip_agents/utils/file_watchers/factory.py +39 -0
- aip_agents/utils/file_watchers/ls_diff.py +88 -0
- aip_agents/utils/file_watchers/sandbox.py +66 -0
- aip_agents/utils/file_watchers/watchdog.py +97 -0
- aip_agents/utils/final_response_builder.py +211 -0
- aip_agents/utils/formatter_llm_client.py +258 -0
- aip_agents/utils/langgraph/__init__.py +19 -0
- aip_agents/utils/langgraph/converter.py +399 -0
- aip_agents/utils/langgraph/delegation_context.py +22 -0
- aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
- aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
- aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1329 -0
- aip_agents/utils/langgraph/tool_output_management.py +1047 -0
- aip_agents/utils/logger.py +195 -0
- aip_agents/utils/metadata/__init__.py +27 -0
- aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
- aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
- aip_agents/utils/metadata/activity_narrative/builder.py +822 -0
- aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
- aip_agents/utils/metadata/activity_narrative/context.py +49 -0
- aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
- aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
- aip_agents/utils/metadata/schemas/__init__.py +16 -0
- aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
- aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
- aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
- aip_agents/utils/metadata_helper.py +363 -0
- aip_agents/utils/name_preprocessor/__init__.py +17 -0
- aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
- aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
- aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
- aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
- aip_agents/utils/pii/__init__.py +29 -0
- aip_agents/utils/pii/pii_handler.py +555 -0
- aip_agents/utils/pii/pii_helper.py +225 -0
- aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
- aip_agents/utils/reference_helper.py +273 -0
- aip_agents/utils/sse_chunk_transformer.py +836 -0
- aip_agents/utils/step_limit_manager.py +265 -0
- aip_agents/utils/token_usage_helper.py +531 -0
- aip_agents_binary-0.0.0.dist-info/METADATA +825 -0
- aip_agents_binary-0.0.0.dist-info/RECORD +447 -0
- aip_agents_binary-0.0.0.dist-info/WHEEL +5 -0
- 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)])
|