solace-agent-mesh 0.2.4__py3-none-any.whl → 1.0.1__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.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/adk/adk_llm.txt +93 -0
- solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
- solace_agent_mesh/agent/adk/callbacks.py +1694 -0
- solace_agent_mesh/agent/adk/filesystem_artifact_service.py +381 -0
- solace_agent_mesh/agent/adk/invocation_monitor.py +295 -0
- solace_agent_mesh/agent/adk/models/lite_llm.py +872 -0
- solace_agent_mesh/agent/adk/models/models_llm.txt +94 -0
- solace_agent_mesh/agent/adk/runner.py +353 -0
- solace_agent_mesh/agent/adk/services.py +240 -0
- solace_agent_mesh/agent/adk/setup.py +751 -0
- solace_agent_mesh/agent/adk/stream_parser.py +214 -0
- solace_agent_mesh/agent/adk/tool_wrapper.py +139 -0
- solace_agent_mesh/agent/agent_llm.txt +41 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +1469 -0
- solace_agent_mesh/agent/protocol/protocol_llm.txt +21 -0
- solace_agent_mesh/agent/sac/app.py +640 -0
- solace_agent_mesh/agent/sac/component.py +3388 -0
- solace_agent_mesh/agent/sac/patch_adk.py +111 -0
- solace_agent_mesh/agent/sac/sac_llm.txt +105 -0
- solace_agent_mesh/agent/sac/task_execution_context.py +176 -0
- solace_agent_mesh/agent/testing/__init__.py +3 -0
- solace_agent_mesh/agent/testing/debug_utils.py +135 -0
- solace_agent_mesh/agent/testing/testing_llm.txt +90 -0
- solace_agent_mesh/agent/tools/__init__.py +14 -0
- solace_agent_mesh/agent/tools/audio_tools.py +1622 -0
- solace_agent_mesh/agent/tools/builtin_artifact_tools.py +1954 -0
- solace_agent_mesh/agent/tools/builtin_data_analysis_tools.py +238 -0
- solace_agent_mesh/agent/tools/general_agent_tools.py +569 -0
- solace_agent_mesh/agent/tools/image_tools.py +1184 -0
- solace_agent_mesh/agent/tools/peer_agent_tool.py +289 -0
- solace_agent_mesh/agent/tools/registry.py +36 -0
- solace_agent_mesh/agent/tools/test_tools.py +135 -0
- solace_agent_mesh/agent/tools/tool_definition.py +45 -0
- solace_agent_mesh/agent/tools/tools_llm.txt +104 -0
- solace_agent_mesh/agent/tools/web_tools.py +381 -0
- solace_agent_mesh/agent/utils/artifact_helpers.py +927 -0
- solace_agent_mesh/agent/utils/config_parser.py +47 -0
- solace_agent_mesh/agent/utils/context_helpers.py +60 -0
- solace_agent_mesh/agent/utils/utils_llm.txt +153 -0
- solace_agent_mesh/assets/docs/404.html +16 -0
- solace_agent_mesh/assets/docs/assets/css/styles.906a1503.css +1 -0
- solace_agent_mesh/assets/docs/assets/images/Solace_AI_Framework_With_Broker-85f0a306a9bcdd20b390b7a949f6d862.png +0 -0
- solace_agent_mesh/assets/docs/assets/images/sac-flows-80d5b603c6aafd33e87945680ce0abf3.png +0 -0
- solace_agent_mesh/assets/docs/assets/images/sac_parts_of_a_component-cb3d0424b1d0c17734c5435cca6b4082.png +0 -0
- solace_agent_mesh/assets/docs/assets/js/04989206.674a8007.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/0e682baa.79f0ab22.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1001.0182a8bd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1023fc19.015679ca.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1039.0bd46aa1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/149.b797a808.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1523c6b4.91c7bc01.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/165.6a39807d.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/165.6a39807d.js.LICENSE.txt +9 -0
- solace_agent_mesh/assets/docs/assets/js/166ab619.7d97ccaf.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js.LICENSE.txt +7 -0
- solace_agent_mesh/assets/docs/assets/js/1c6e87d2.23bccffb.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2130.ab9fd314.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/21ceee5f.614fa8dd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2237.5e477fc6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2334.622a6395.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2a9cab12.8909df92.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3219.adc1d663.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/332e10b5.7a103f42.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3624.b524e433.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/375.708d48db.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3834.b6cd790e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3d406171.f722eaf5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4250.95455b28.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/42b3f8d8.36090198.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4356.d169ab5b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/442a8107.5ba94b65.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4458.518e66fa.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4488.c7cc3442.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4494.6ee23046.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4855.fc4444b6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4866.22daefc0.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4950.ca4caeda.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4c2787c2.66ee00e9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5388.7a136447.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/55f47984.c484bf96.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5607.081356f8.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5864.b0d0e9de.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5b4258a4.bda20761.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5e95c892.558d5167.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6143.0a1464c9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6395.e9c73649.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6796.51d2c9b7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6976.379be23b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6978.ee0b945c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7040.cb436723.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7195.412f418a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7280.3fb73bdb.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/768e31b0.a12673db.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7845.e33e7c4c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7900.69516146.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8356.8a379c04.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/85387663.6bf41934.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8567.4732c6b7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8573.cb04eda5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8577.1d54e766.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8591.d7c16be6.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/8591.d7c16be6.js.LICENSE.txt +61 -0
- solace_agent_mesh/assets/docs/assets/js/8709.7ecd4047.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8731.49e930c2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8908.f9d1b506.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9157.b4093d07.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9278.a4fd875d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/945fb41e.74d728aa.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9616.b75c2f6d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9793.c6d16376.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9eff14a2.1bf8f61c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a3a92b25.26ca071f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a7bd4aaa.2204d2f7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/aba21aa0.c42a534c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/aba87c2f.d3e2dcc3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ae4415af.8e279b5d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/b7006a3a.40b10c9d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/bac0be12.f50d9bac.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/bb2ef573.207e6990.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/c2c06897.63b76e9e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cc969b05.954186d4.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cd3d4052.ca6eed8c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ced92a13.fb92e7ca.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cee5d587.f5b73ca1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.ecc3d195.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f897a61a.2c2e152c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/fbfa3e75.aca209c9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.7ed3319f.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/main.7ed3319f.js.LICENSE.txt +81 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.d9520ae2.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +128 -0
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +91 -0
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +201 -0
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +91 -0
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +55 -0
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +82 -0
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +60 -0
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +48 -0
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +54 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +17 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +45 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +76 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +150 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +54 -0
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +267 -0
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +136 -0
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +116 -0
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +80 -0
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +164 -0
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +57 -0
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +72 -0
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +102 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +99 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +90 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +107 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +152 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +103 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +170 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +200 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +54 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +69 -0
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +59 -0
- solace_agent_mesh/assets/docs/img/Solace_AI_Framework_README.png +0 -0
- solace_agent_mesh/assets/docs/img/Solace_AI_Framework_With_Broker.png +0 -0
- solace_agent_mesh/assets/docs/img/logo.png +0 -0
- solace_agent_mesh/assets/docs/img/sac-flows.png +0 -0
- solace_agent_mesh/assets/docs/img/sac_parts_of_a_component.png +0 -0
- solace_agent_mesh/assets/docs/img/solace-logo.png +0 -0
- solace_agent_mesh/assets/docs/lunr-index-1753813536522.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -0
- solace_agent_mesh/assets/docs/search-doc-1753813536522.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -0
- solace_agent_mesh/assets/docs/sitemap.xml +1 -0
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/__init__.py +15 -0
- solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +250 -0
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +659 -0
- solace_agent_mesh/cli/commands/add_cmd/gateway_cmd.py +322 -0
- solace_agent_mesh/cli/commands/add_cmd/web_add_agent_step.py +93 -0
- solace_agent_mesh/cli/commands/add_cmd/web_add_gateway_step.py +118 -0
- solace_agent_mesh/cli/commands/docs_cmd.py +57 -0
- solace_agent_mesh/cli/commands/eval_cmd.py +64 -0
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +404 -0
- solace_agent_mesh/cli/commands/init_cmd/broker_step.py +201 -0
- solace_agent_mesh/cli/commands/init_cmd/directory_step.py +28 -0
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +197 -0
- solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +365 -0
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +387 -0
- solace_agent_mesh/cli/commands/init_cmd/project_files_step.py +38 -0
- solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +110 -0
- solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +183 -0
- solace_agent_mesh/cli/commands/plugin_cmd/__init__.py +18 -0
- solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +372 -0
- solace_agent_mesh/cli/commands/plugin_cmd/build_cmd.py +86 -0
- solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +138 -0
- solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +309 -0
- solace_agent_mesh/cli/commands/plugin_cmd/official_registry.py +174 -0
- solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +305 -0
- solace_agent_mesh/cli/commands/run_cmd.py +158 -0
- solace_agent_mesh/cli/main.py +17 -294
- solace_agent_mesh/cli/utils.py +135 -204
- solace_agent_mesh/client/webui/frontend/static/assets/authCallback-DvlO62me.js +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/client-bp6u3qVZ.js +49 -0
- solace_agent_mesh/client/webui/frontend/static/assets/favicon-BLgzUch9.ico +0 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-An0a5j5k.js +663 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-Bu5-4Bac.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +14 -0
- solace_agent_mesh/client/webui/frontend/static/index.html +15 -0
- solace_agent_mesh/common/__init__.py +1 -0
- solace_agent_mesh/common/a2a_protocol.py +564 -0
- solace_agent_mesh/common/agent_registry.py +42 -0
- solace_agent_mesh/common/client/__init__.py +4 -0
- solace_agent_mesh/common/client/card_resolver.py +21 -0
- solace_agent_mesh/common/client/client.py +85 -0
- solace_agent_mesh/common/client/client_llm.txt +133 -0
- solace_agent_mesh/common/common_llm.txt +144 -0
- solace_agent_mesh/common/constants.py +1 -14
- solace_agent_mesh/common/middleware/__init__.py +12 -0
- solace_agent_mesh/common/middleware/config_resolver.py +130 -0
- solace_agent_mesh/common/middleware/middleware_llm.txt +174 -0
- solace_agent_mesh/common/middleware/registry.py +125 -0
- solace_agent_mesh/common/server/__init__.py +4 -0
- solace_agent_mesh/common/server/server.py +122 -0
- solace_agent_mesh/common/server/server_llm.txt +169 -0
- solace_agent_mesh/common/server/task_manager.py +291 -0
- solace_agent_mesh/common/server/utils.py +28 -0
- solace_agent_mesh/common/services/__init__.py +4 -0
- solace_agent_mesh/common/services/employee_service.py +162 -0
- solace_agent_mesh/common/services/identity_service.py +129 -0
- solace_agent_mesh/common/services/providers/__init__.py +4 -0
- solace_agent_mesh/common/services/providers/local_file_identity_service.py +148 -0
- solace_agent_mesh/common/services/providers/providers_llm.txt +113 -0
- solace_agent_mesh/common/services/services_llm.txt +132 -0
- solace_agent_mesh/common/types.py +411 -0
- solace_agent_mesh/common/utils/__init__.py +7 -0
- solace_agent_mesh/common/utils/asyncio_macos_fix.py +86 -0
- solace_agent_mesh/common/utils/embeds/__init__.py +33 -0
- solace_agent_mesh/common/utils/embeds/constants.py +55 -0
- solace_agent_mesh/common/utils/embeds/converter.py +452 -0
- solace_agent_mesh/common/utils/embeds/embeds_llm.txt +124 -0
- solace_agent_mesh/common/utils/embeds/evaluators.py +394 -0
- solace_agent_mesh/common/utils/embeds/modifiers.py +816 -0
- solace_agent_mesh/common/utils/embeds/resolver.py +865 -0
- solace_agent_mesh/common/utils/embeds/types.py +14 -0
- solace_agent_mesh/common/utils/in_memory_cache.py +108 -0
- solace_agent_mesh/common/utils/log_formatters.py +44 -0
- solace_agent_mesh/common/utils/mime_helpers.py +106 -0
- solace_agent_mesh/common/utils/push_notification_auth.py +134 -0
- solace_agent_mesh/common/utils/utils_llm.txt +67 -0
- solace_agent_mesh/config_portal/backend/common.py +66 -24
- solace_agent_mesh/config_portal/backend/plugin_catalog/constants.py +23 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/models.py +49 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/registry_manager.py +160 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/scraper.py +525 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog_server.py +216 -0
- solace_agent_mesh/config_portal/backend/server.py +550 -181
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-DNxCwAGB.js +48 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/components-B7lKcHVY.js +140 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-DX1misIU.js → entry.client-CEumGClk.js} +3 -3
- solace_agent_mesh/config_portal/frontend/static/client/assets/index-DSo1AH_7.js +68 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-d2b54a97.js +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/{root-BApq5dPK.js → root-C4XmHinv.js} +2 -2
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
- solace_agent_mesh/core_a2a/__init__.py +1 -0
- solace_agent_mesh/core_a2a/core_a2a_llm.txt +88 -0
- solace_agent_mesh/core_a2a/service.py +331 -0
- solace_agent_mesh/evaluation/config_loader.py +657 -0
- solace_agent_mesh/evaluation/evaluator.py +667 -0
- solace_agent_mesh/evaluation/message_organizer.py +568 -0
- solace_agent_mesh/evaluation/report/benchmark_info.html +35 -0
- solace_agent_mesh/evaluation/report/chart_section.html +141 -0
- solace_agent_mesh/evaluation/report/detailed_breakdown.html +28 -0
- solace_agent_mesh/evaluation/report/modal.html +59 -0
- solace_agent_mesh/evaluation/report/modal_chart_functions.js +411 -0
- solace_agent_mesh/evaluation/report/modal_script.js +296 -0
- solace_agent_mesh/evaluation/report/modal_styles.css +340 -0
- solace_agent_mesh/evaluation/report/performance_metrics_styles.css +93 -0
- solace_agent_mesh/evaluation/report/templates/footer.html +2 -0
- solace_agent_mesh/evaluation/report/templates/header.html +340 -0
- solace_agent_mesh/evaluation/report_data_processor.py +972 -0
- solace_agent_mesh/evaluation/report_generator.py +613 -0
- solace_agent_mesh/evaluation/run.py +613 -0
- solace_agent_mesh/evaluation/subscriber.py +872 -0
- solace_agent_mesh/evaluation/summary_builder.py +775 -0
- solace_agent_mesh/evaluation/test_case_loader.py +714 -0
- solace_agent_mesh/gateway/base/__init__.py +1 -0
- solace_agent_mesh/gateway/base/app.py +266 -0
- solace_agent_mesh/gateway/base/base_llm.txt +119 -0
- solace_agent_mesh/gateway/base/component.py +1542 -0
- solace_agent_mesh/gateway/base/task_context.py +74 -0
- solace_agent_mesh/gateway/gateway_llm.txt +125 -0
- solace_agent_mesh/gateway/http_sse/app.py +190 -0
- solace_agent_mesh/gateway/http_sse/component.py +1602 -0
- solace_agent_mesh/gateway/http_sse/components/__init__.py +7 -0
- solace_agent_mesh/gateway/http_sse/components/components_llm.txt +65 -0
- solace_agent_mesh/gateway/http_sse/components/visualization_forwarder_component.py +108 -0
- solace_agent_mesh/gateway/http_sse/dependencies.py +316 -0
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +63 -0
- solace_agent_mesh/gateway/http_sse/main.py +442 -0
- solace_agent_mesh/gateway/http_sse/routers/__init__.py +4 -0
- solace_agent_mesh/gateway/http_sse/routers/agents.py +41 -0
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +821 -0
- solace_agent_mesh/gateway/http_sse/routers/auth.py +212 -0
- solace_agent_mesh/gateway/http_sse/routers/config.py +55 -0
- solace_agent_mesh/gateway/http_sse/routers/people.py +69 -0
- solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +37 -0
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +80 -0
- solace_agent_mesh/gateway/http_sse/routers/sse.py +138 -0
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +294 -0
- solace_agent_mesh/gateway/http_sse/routers/users.py +59 -0
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +1131 -0
- solace_agent_mesh/gateway/http_sse/services/__init__.py +4 -0
- solace_agent_mesh/gateway/http_sse/services/agent_service.py +69 -0
- solace_agent_mesh/gateway/http_sse/services/people_service.py +158 -0
- solace_agent_mesh/gateway/http_sse/services/services_llm.txt +179 -0
- solace_agent_mesh/gateway/http_sse/services/task_service.py +121 -0
- solace_agent_mesh/gateway/http_sse/session_manager.py +187 -0
- solace_agent_mesh/gateway/http_sse/sse_manager.py +328 -0
- solace_agent_mesh/llm.txt +228 -0
- solace_agent_mesh/llm_detail.txt +2835 -0
- solace_agent_mesh/templates/agent_template.yaml +53 -0
- solace_agent_mesh/templates/eval_backend_template.yaml +54 -0
- solace_agent_mesh/templates/gateway_app_template.py +73 -0
- solace_agent_mesh/templates/gateway_component_template.py +400 -0
- solace_agent_mesh/templates/gateway_config_template.yaml +43 -0
- solace_agent_mesh/templates/main_orchestrator.yaml +55 -0
- solace_agent_mesh/templates/plugin_agent_config_template.yaml +122 -0
- solace_agent_mesh/templates/plugin_custom_config_template.yaml +27 -0
- solace_agent_mesh/templates/plugin_custom_template.py +10 -0
- solace_agent_mesh/templates/plugin_gateway_config_template.yaml +63 -0
- solace_agent_mesh/templates/plugin_pyproject_template.toml +33 -0
- solace_agent_mesh/templates/plugin_readme_template.md +34 -0
- solace_agent_mesh/templates/plugin_tools_template.py +224 -0
- solace_agent_mesh/templates/shared_config.yaml +66 -0
- solace_agent_mesh/templates/templates_llm.txt +147 -0
- solace_agent_mesh/templates/webui.yaml +53 -0
- solace_agent_mesh-1.0.1.dist-info/METADATA +432 -0
- solace_agent_mesh-1.0.1.dist-info/RECORD +359 -0
- solace_agent_mesh-1.0.1.dist-info/entry_points.txt +3 -0
- {solace_agent_mesh-0.2.4.dist-info → solace_agent_mesh-1.0.1.dist-info}/licenses/LICENSE +1 -1
- solace_agent_mesh/agents/base_agent_component.py +0 -256
- solace_agent_mesh/agents/global/actions/agent_state_change.py +0 -54
- solace_agent_mesh/agents/global/actions/clear_history.py +0 -32
- solace_agent_mesh/agents/global/actions/convert_file_to_markdown.py +0 -160
- solace_agent_mesh/agents/global/actions/create_file.py +0 -70
- solace_agent_mesh/agents/global/actions/error_action.py +0 -45
- solace_agent_mesh/agents/global/actions/plantuml_diagram.py +0 -163
- solace_agent_mesh/agents/global/actions/plotly_graph.py +0 -152
- solace_agent_mesh/agents/global/actions/retrieve_file.py +0 -51
- solace_agent_mesh/agents/global/global_agent_component.py +0 -38
- solace_agent_mesh/agents/image_processing/actions/create_image.py +0 -75
- solace_agent_mesh/agents/image_processing/actions/describe_image.py +0 -115
- solace_agent_mesh/agents/image_processing/image_processing_agent_component.py +0 -23
- solace_agent_mesh/agents/slack/__init__.py +0 -1
- solace_agent_mesh/agents/slack/actions/__init__.py +0 -1
- solace_agent_mesh/agents/slack/actions/post_message.py +0 -177
- solace_agent_mesh/agents/slack/slack_agent_component.py +0 -59
- solace_agent_mesh/agents/web_request/actions/do_image_search.py +0 -84
- solace_agent_mesh/agents/web_request/actions/do_news_search.py +0 -47
- solace_agent_mesh/agents/web_request/actions/do_suggestion_search.py +0 -34
- solace_agent_mesh/agents/web_request/actions/do_web_request.py +0 -135
- solace_agent_mesh/agents/web_request/actions/download_file.py +0 -69
- solace_agent_mesh/agents/web_request/web_request_agent_component.py +0 -33
- solace_agent_mesh/assets/web-visualizer/assets/index-D0qORgkg.css +0 -1
- solace_agent_mesh/assets/web-visualizer/assets/index-DnDr1pnu.js +0 -109
- solace_agent_mesh/assets/web-visualizer/index.html +0 -14
- solace_agent_mesh/assets/web-visualizer/vite.svg +0 -1
- solace_agent_mesh/cli/commands/add/__init__.py +0 -3
- solace_agent_mesh/cli/commands/add/add.py +0 -88
- solace_agent_mesh/cli/commands/add/agent.py +0 -110
- solace_agent_mesh/cli/commands/add/copy_from_plugin.py +0 -92
- solace_agent_mesh/cli/commands/add/gateway.py +0 -374
- solace_agent_mesh/cli/commands/build.py +0 -670
- solace_agent_mesh/cli/commands/chat/__init__.py +0 -3
- solace_agent_mesh/cli/commands/chat/chat.py +0 -361
- solace_agent_mesh/cli/commands/config.py +0 -29
- solace_agent_mesh/cli/commands/init/__init__.py +0 -3
- solace_agent_mesh/cli/commands/init/ai_provider_step.py +0 -93
- solace_agent_mesh/cli/commands/init/broker_step.py +0 -99
- solace_agent_mesh/cli/commands/init/builtin_agent_step.py +0 -83
- solace_agent_mesh/cli/commands/init/check_if_already_done.py +0 -13
- solace_agent_mesh/cli/commands/init/create_config_file_step.py +0 -65
- solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +0 -147
- solace_agent_mesh/cli/commands/init/file_service_step.py +0 -73
- solace_agent_mesh/cli/commands/init/init.py +0 -92
- solace_agent_mesh/cli/commands/init/project_structure_step.py +0 -16
- solace_agent_mesh/cli/commands/init/web_init_step.py +0 -32
- solace_agent_mesh/cli/commands/plugin/__init__.py +0 -3
- solace_agent_mesh/cli/commands/plugin/add.py +0 -100
- solace_agent_mesh/cli/commands/plugin/build.py +0 -268
- solace_agent_mesh/cli/commands/plugin/create.py +0 -117
- solace_agent_mesh/cli/commands/plugin/plugin.py +0 -124
- solace_agent_mesh/cli/commands/plugin/remove.py +0 -73
- solace_agent_mesh/cli/commands/run.py +0 -68
- solace_agent_mesh/cli/commands/visualizer.py +0 -138
- solace_agent_mesh/cli/config.py +0 -85
- solace_agent_mesh/common/action.py +0 -91
- solace_agent_mesh/common/action_list.py +0 -37
- solace_agent_mesh/common/action_response.py +0 -340
- solace_agent_mesh/common/mysql_database.py +0 -40
- solace_agent_mesh/common/postgres_database.py +0 -85
- solace_agent_mesh/common/prompt_templates.py +0 -28
- solace_agent_mesh/common/stimulus_utils.py +0 -152
- solace_agent_mesh/common/time.py +0 -24
- solace_agent_mesh/common/utils.py +0 -712
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-a-zJ6rLx.js +0 -46
- solace_agent_mesh/config_portal/frontend/static/client/assets/components-ZIfdTbrV.js +0 -191
- solace_agent_mesh/config_portal/frontend/static/client/assets/index-BJHAE5s4.js +0 -17
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-44c41103.js +0 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-DX4gQ516.css +0 -1
- solace_agent_mesh/configs/agent_global.yaml +0 -74
- solace_agent_mesh/configs/agent_image_processing.yaml +0 -82
- solace_agent_mesh/configs/agent_slack.yaml +0 -64
- solace_agent_mesh/configs/agent_web_request.yaml +0 -75
- solace_agent_mesh/configs/conversation_to_file.yaml +0 -56
- solace_agent_mesh/configs/error_catcher.yaml +0 -56
- solace_agent_mesh/configs/monitor.yaml +0 -0
- solace_agent_mesh/configs/monitor_stim_and_errors_to_slack.yaml +0 -109
- solace_agent_mesh/configs/monitor_user_feedback.yaml +0 -58
- solace_agent_mesh/configs/orchestrator.yaml +0 -241
- solace_agent_mesh/configs/service_embedding.yaml +0 -81
- solace_agent_mesh/configs/service_llm.yaml +0 -265
- solace_agent_mesh/configs/visualize_websocket.yaml +0 -55
- solace_agent_mesh/gateway/components/gateway_base.py +0 -47
- solace_agent_mesh/gateway/components/gateway_input.py +0 -278
- solace_agent_mesh/gateway/components/gateway_output.py +0 -298
- solace_agent_mesh/gateway/identity/bamboohr_identity.py +0 -18
- solace_agent_mesh/gateway/identity/identity_base.py +0 -10
- solace_agent_mesh/gateway/identity/identity_provider.py +0 -60
- solace_agent_mesh/gateway/identity/no_identity.py +0 -9
- solace_agent_mesh/gateway/identity/passthru_identity.py +0 -9
- solace_agent_mesh/monitors/base_monitor_component.py +0 -26
- solace_agent_mesh/monitors/feedback/user_feedback_monitor.py +0 -75
- solace_agent_mesh/monitors/stim_and_errors/stim_and_error_monitor.py +0 -560
- solace_agent_mesh/orchestrator/__init__.py +0 -0
- solace_agent_mesh/orchestrator/action_manager.py +0 -237
- solace_agent_mesh/orchestrator/components/__init__.py +0 -0
- solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +0 -58
- solace_agent_mesh/orchestrator/components/orchestrator_action_response_component.py +0 -179
- solace_agent_mesh/orchestrator/components/orchestrator_register_component.py +0 -107
- solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +0 -527
- solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +0 -260
- solace_agent_mesh/orchestrator/orchestrator_main.py +0 -172
- solace_agent_mesh/orchestrator/orchestrator_prompt.py +0 -539
- solace_agent_mesh/services/__init__.py +0 -0
- solace_agent_mesh/services/authorization/providers/base_authorization_provider.py +0 -56
- solace_agent_mesh/services/bamboo_hr_service/__init__.py +0 -3
- solace_agent_mesh/services/bamboo_hr_service/bamboo_hr.py +0 -182
- solace_agent_mesh/services/common/__init__.py +0 -4
- solace_agent_mesh/services/common/auto_expiry.py +0 -45
- solace_agent_mesh/services/common/singleton.py +0 -18
- solace_agent_mesh/services/file_service/__init__.py +0 -14
- solace_agent_mesh/services/file_service/file_manager/__init__.py +0 -0
- solace_agent_mesh/services/file_service/file_manager/bucket_file_manager.py +0 -149
- solace_agent_mesh/services/file_service/file_manager/file_manager_base.py +0 -162
- solace_agent_mesh/services/file_service/file_manager/memory_file_manager.py +0 -64
- solace_agent_mesh/services/file_service/file_manager/volume_file_manager.py +0 -106
- solace_agent_mesh/services/file_service/file_service.py +0 -437
- solace_agent_mesh/services/file_service/file_service_constants.py +0 -54
- solace_agent_mesh/services/file_service/file_transformations.py +0 -141
- solace_agent_mesh/services/file_service/file_utils.py +0 -324
- solace_agent_mesh/services/file_service/transformers/__init__.py +0 -5
- solace_agent_mesh/services/history_service/__init__.py +0 -3
- solace_agent_mesh/services/history_service/history_providers/__init__.py +0 -0
- solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +0 -54
- solace_agent_mesh/services/history_service/history_providers/file_history_provider.py +0 -74
- solace_agent_mesh/services/history_service/history_providers/index.py +0 -40
- solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +0 -33
- solace_agent_mesh/services/history_service/history_providers/mongodb_history_provider.py +0 -66
- solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +0 -66
- solace_agent_mesh/services/history_service/history_providers/sql_history_provider.py +0 -93
- solace_agent_mesh/services/history_service/history_service.py +0 -413
- solace_agent_mesh/services/history_service/long_term_memory/__init__.py +0 -0
- solace_agent_mesh/services/history_service/long_term_memory/long_term_memory.py +0 -399
- solace_agent_mesh/services/llm_service/components/llm_request_component.py +0 -340
- solace_agent_mesh/services/llm_service/components/llm_service_component_base.py +0 -152
- solace_agent_mesh/services/middleware_service/__init__.py +0 -0
- solace_agent_mesh/services/middleware_service/middleware_service.py +0 -20
- solace_agent_mesh/templates/action.py +0 -38
- solace_agent_mesh/templates/agent.py +0 -29
- solace_agent_mesh/templates/agent.yaml +0 -70
- solace_agent_mesh/templates/gateway-config-template.yaml +0 -6
- solace_agent_mesh/templates/gateway-default-config.yaml +0 -28
- solace_agent_mesh/templates/gateway-flows.yaml +0 -78
- solace_agent_mesh/templates/gateway-header.yaml +0 -16
- solace_agent_mesh/templates/gateway_base.py +0 -15
- solace_agent_mesh/templates/gateway_input.py +0 -98
- solace_agent_mesh/templates/gateway_output.py +0 -71
- solace_agent_mesh/templates/plugin-gateway-default-config.yaml +0 -29
- solace_agent_mesh/templates/plugin-pyproject.toml +0 -30
- solace_agent_mesh/templates/rest-api-default-config.yaml +0 -31
- solace_agent_mesh/templates/rest-api-flows.yaml +0 -81
- solace_agent_mesh/templates/slack-default-config.yaml +0 -16
- solace_agent_mesh/templates/slack-flows.yaml +0 -81
- solace_agent_mesh/templates/solace-agent-mesh-default.yaml +0 -86
- solace_agent_mesh/templates/solace-agent-mesh-plugin-default.yaml +0 -8
- solace_agent_mesh/templates/web-default-config.yaml +0 -10
- solace_agent_mesh/templates/web-flows.yaml +0 -76
- solace_agent_mesh/tools/__init__.py +0 -0
- solace_agent_mesh/tools/components/__init__.py +0 -0
- solace_agent_mesh/tools/components/conversation_formatter.py +0 -111
- solace_agent_mesh/tools/components/file_resolver_component.py +0 -58
- solace_agent_mesh/tools/config/runtime_config.py +0 -26
- solace_agent_mesh-0.2.4.dist-info/METADATA +0 -176
- solace_agent_mesh-0.2.4.dist-info/RECORD +0 -193
- solace_agent_mesh-0.2.4.dist-info/entry_points.txt +0 -3
- /solace_agent_mesh/{agents → agent}/__init__.py +0 -0
- /solace_agent_mesh/{agents/global → agent/adk}/__init__.py +0 -0
- /solace_agent_mesh/{agents/global/actions → agent/protocol}/__init__.py +0 -0
- /solace_agent_mesh/{agents/image_processing → agent/sac}/__init__.py +0 -0
- /solace_agent_mesh/{agents/image_processing/actions → agent/utils}/__init__.py +0 -0
- /solace_agent_mesh/{agents/web_request → config_portal/backend/plugin_catalog}/__init__.py +0 -0
- /solace_agent_mesh/{agents/web_request/actions → evaluation}/__init__.py +0 -0
- /solace_agent_mesh/gateway/{components → http_sse}/__init__.py +0 -0
- {solace_agent_mesh-0.2.4.dist-info → solace_agent_mesh-1.0.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1694 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ADK Callbacks for the A2A Host Component.
|
|
3
|
+
Includes dynamic instruction injection, artifact metadata injection,
|
|
4
|
+
embed resolution, and logging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING, List
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
from google.adk.tools import BaseTool, ToolContext
|
|
15
|
+
from google.adk.artifacts import BaseArtifactService
|
|
16
|
+
from google.adk.agents.callback_context import CallbackContext
|
|
17
|
+
from google.adk.models.llm_request import LlmRequest
|
|
18
|
+
from google.adk.models.llm_response import LlmResponse
|
|
19
|
+
from ...common.a2a_protocol import (
|
|
20
|
+
A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY,
|
|
21
|
+
)
|
|
22
|
+
from google.genai import types as adk_types
|
|
23
|
+
from google.adk.tools.mcp_tool import MCPTool
|
|
24
|
+
from solace_ai_connector.common.log import log
|
|
25
|
+
|
|
26
|
+
from ...agent.utils.artifact_helpers import (
|
|
27
|
+
METADATA_SUFFIX,
|
|
28
|
+
format_metadata_for_llm,
|
|
29
|
+
)
|
|
30
|
+
from ...agent.utils.context_helpers import (
|
|
31
|
+
get_original_session_id,
|
|
32
|
+
get_session_from_callback_context,
|
|
33
|
+
)
|
|
34
|
+
from ..tools.tool_definition import BuiltinTool
|
|
35
|
+
|
|
36
|
+
from ...common.utils.embeds import (
|
|
37
|
+
EMBED_DELIMITER_OPEN,
|
|
38
|
+
EMBED_DELIMITER_CLOSE,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from ...common.utils.embeds import (
|
|
42
|
+
EMBED_CHAIN_DELIMITER,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from ...common.utils.embeds.modifiers import MODIFIER_IMPLEMENTATIONS
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from ..sac.component import SamAgentComponent
|
|
49
|
+
|
|
50
|
+
from ...common.types import (
|
|
51
|
+
TaskStatusUpdateEvent,
|
|
52
|
+
TaskStatus,
|
|
53
|
+
TaskState,
|
|
54
|
+
Message as A2AMessage,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
from ...agent.utils.artifact_helpers import (
|
|
58
|
+
save_artifact_with_metadata,
|
|
59
|
+
DEFAULT_SCHEMA_MAX_KEYS,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
METADATA_RESPONSE_KEY = "appended_artifact_metadata"
|
|
63
|
+
from ..tools.builtin_artifact_tools import _internal_create_artifact
|
|
64
|
+
from ...agent.adk.tool_wrapper import ADKToolWrapper
|
|
65
|
+
|
|
66
|
+
# Import the new parser and its events
|
|
67
|
+
from ...agent.adk.stream_parser import (
|
|
68
|
+
FencedBlockStreamParser,
|
|
69
|
+
BlockStartedEvent,
|
|
70
|
+
BlockProgressedEvent,
|
|
71
|
+
BlockCompletedEvent,
|
|
72
|
+
BlockInvalidatedEvent,
|
|
73
|
+
ARTIFACT_BLOCK_DELIMITER_OPEN,
|
|
74
|
+
ARTIFACT_BLOCK_DELIMITER_CLOSE,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def process_artifact_blocks_callback(
|
|
79
|
+
callback_context: CallbackContext,
|
|
80
|
+
llm_response: LlmResponse,
|
|
81
|
+
host_component: "SamAgentComponent",
|
|
82
|
+
) -> Optional[LlmResponse]:
|
|
83
|
+
"""
|
|
84
|
+
Orchestrates the parsing of fenced artifact blocks from an LLM stream
|
|
85
|
+
by delegating to a FencedBlockStreamParser instance.
|
|
86
|
+
This callback is stateful across streaming chunks within a single turn.
|
|
87
|
+
"""
|
|
88
|
+
log_identifier = "[Callback:ProcessArtifactBlocks]"
|
|
89
|
+
parser_state_key = "fenced_block_parser"
|
|
90
|
+
session = get_session_from_callback_context(callback_context)
|
|
91
|
+
|
|
92
|
+
parser: FencedBlockStreamParser = session.state.get(parser_state_key)
|
|
93
|
+
if parser is None:
|
|
94
|
+
log.debug("%s New turn. Creating new FencedBlockStreamParser.", log_identifier)
|
|
95
|
+
parser = FencedBlockStreamParser(progress_update_interval_bytes=250)
|
|
96
|
+
session.state[parser_state_key] = parser
|
|
97
|
+
session.state["completed_artifact_blocks_list"] = []
|
|
98
|
+
|
|
99
|
+
stream_chunks_were_processed = callback_context.state.get(
|
|
100
|
+
A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY, False
|
|
101
|
+
)
|
|
102
|
+
if llm_response.partial:
|
|
103
|
+
callback_context.state[A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY] = True
|
|
104
|
+
|
|
105
|
+
if llm_response.partial or not stream_chunks_were_processed:
|
|
106
|
+
processed_parts: List[adk_types.Part] = []
|
|
107
|
+
original_parts = llm_response.content.parts if llm_response.content else []
|
|
108
|
+
a2a_context = callback_context.state.get("a2a_context")
|
|
109
|
+
|
|
110
|
+
for part in original_parts:
|
|
111
|
+
if part.text is not None:
|
|
112
|
+
parser_result = parser.process_chunk(part.text)
|
|
113
|
+
|
|
114
|
+
if llm_response.partial:
|
|
115
|
+
if parser_result.user_facing_text:
|
|
116
|
+
processed_parts.append(
|
|
117
|
+
adk_types.Part(text=parser_result.user_facing_text)
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
processed_parts.append(part)
|
|
121
|
+
|
|
122
|
+
for event in parser_result.events:
|
|
123
|
+
if isinstance(event, BlockStartedEvent):
|
|
124
|
+
log.info(
|
|
125
|
+
"%s Event: BlockStarted. Params: %s",
|
|
126
|
+
log_identifier,
|
|
127
|
+
event.params,
|
|
128
|
+
)
|
|
129
|
+
filename = event.params.get("filename", "unknown_artifact")
|
|
130
|
+
if a2a_context:
|
|
131
|
+
await host_component._publish_agent_status_signal_update(
|
|
132
|
+
f"Receiving artifact `{filename}`...", a2a_context
|
|
133
|
+
)
|
|
134
|
+
params_str = " ".join(
|
|
135
|
+
[f'{k}="{v}"' for k, v in event.params.items()]
|
|
136
|
+
)
|
|
137
|
+
original_text = f"«««save_artifact: {params_str}\n"
|
|
138
|
+
session.state["artifact_block_original_text"] = original_text
|
|
139
|
+
|
|
140
|
+
elif isinstance(event, BlockProgressedEvent):
|
|
141
|
+
log.debug(
|
|
142
|
+
"%s Event: BlockProgressed. Size: %d",
|
|
143
|
+
log_identifier,
|
|
144
|
+
event.buffered_size,
|
|
145
|
+
)
|
|
146
|
+
params = parser._block_params
|
|
147
|
+
filename = params.get("filename", "unknown_artifact")
|
|
148
|
+
status_message = f"Creating artifact `{filename}` ({event.buffered_size}B saved)..."
|
|
149
|
+
if a2a_context:
|
|
150
|
+
await host_component._publish_agent_status_signal_update(
|
|
151
|
+
status_message, a2a_context
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
elif isinstance(event, BlockCompletedEvent):
|
|
155
|
+
log.info(
|
|
156
|
+
"%s Event: BlockCompleted. Content length: %d",
|
|
157
|
+
log_identifier,
|
|
158
|
+
len(event.content),
|
|
159
|
+
)
|
|
160
|
+
original_text = session.state.get(
|
|
161
|
+
"artifact_block_original_text", ""
|
|
162
|
+
)
|
|
163
|
+
original_text += event.content
|
|
164
|
+
original_text += "»»»"
|
|
165
|
+
|
|
166
|
+
tool_context_for_call = ToolContext(
|
|
167
|
+
callback_context._invocation_context
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
params = event.params
|
|
171
|
+
filename = params.get("filename")
|
|
172
|
+
if not filename or not filename.strip():
|
|
173
|
+
log.warning(
|
|
174
|
+
"%s Fenced artifact block is missing a valid 'filename'. Failing operation.",
|
|
175
|
+
log_identifier,
|
|
176
|
+
)
|
|
177
|
+
session.state["completed_artifact_blocks_list"].append(
|
|
178
|
+
{
|
|
179
|
+
"filename": (
|
|
180
|
+
"unknown_artifact"
|
|
181
|
+
if filename is None
|
|
182
|
+
else filename
|
|
183
|
+
),
|
|
184
|
+
"version": 0,
|
|
185
|
+
"status": "error",
|
|
186
|
+
"original_text": original_text,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
kwargs_for_call = {
|
|
192
|
+
"filename": filename,
|
|
193
|
+
"content": event.content,
|
|
194
|
+
"mime_type": params.get("mime_type"),
|
|
195
|
+
"description": params.get("description"),
|
|
196
|
+
"metadata_json": params.get("metadata"),
|
|
197
|
+
"tool_context": tool_context_for_call,
|
|
198
|
+
}
|
|
199
|
+
if "schema_max_keys" in params:
|
|
200
|
+
try:
|
|
201
|
+
kwargs_for_call["schema_max_keys"] = int(
|
|
202
|
+
params["schema_max_keys"]
|
|
203
|
+
)
|
|
204
|
+
except (ValueError, TypeError):
|
|
205
|
+
log.warning(
|
|
206
|
+
"%s Invalid 'schema_max_keys' value '%s'. Ignoring.",
|
|
207
|
+
log_identifier,
|
|
208
|
+
params["schema_max_keys"],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
wrapped_creator = ADKToolWrapper(
|
|
212
|
+
original_func=_internal_create_artifact,
|
|
213
|
+
tool_config=None, # No specific config for this internal tool
|
|
214
|
+
tool_name="_internal_create_artifact",
|
|
215
|
+
)
|
|
216
|
+
save_result = await wrapped_creator(**kwargs_for_call)
|
|
217
|
+
|
|
218
|
+
if save_result.get("status") in ["success", "partial_success"]:
|
|
219
|
+
status_for_tool = "success"
|
|
220
|
+
version_for_tool = save_result.get("data_version", 1)
|
|
221
|
+
try:
|
|
222
|
+
logical_task_id = a2a_context.get("logical_task_id")
|
|
223
|
+
if logical_task_id:
|
|
224
|
+
with host_component.active_tasks_lock:
|
|
225
|
+
task_context = host_component.active_tasks.get(
|
|
226
|
+
logical_task_id
|
|
227
|
+
)
|
|
228
|
+
if task_context:
|
|
229
|
+
task_context.register_produced_artifact(
|
|
230
|
+
filename, version_for_tool
|
|
231
|
+
)
|
|
232
|
+
log.info(
|
|
233
|
+
"%s Registered inline artifact '%s' v%d for task %s.",
|
|
234
|
+
log_identifier,
|
|
235
|
+
filename,
|
|
236
|
+
version_for_tool,
|
|
237
|
+
logical_task_id,
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
log.warning(
|
|
241
|
+
"%s No logical_task_id, cannot register inline artifact.",
|
|
242
|
+
log_identifier,
|
|
243
|
+
)
|
|
244
|
+
except Exception as e_track:
|
|
245
|
+
log.error(
|
|
246
|
+
"%s Failed to track inline artifact: %s",
|
|
247
|
+
log_identifier,
|
|
248
|
+
e_track,
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
status_for_tool = "error"
|
|
252
|
+
version_for_tool = 0
|
|
253
|
+
|
|
254
|
+
session.state["completed_artifact_blocks_list"].append(
|
|
255
|
+
{
|
|
256
|
+
"filename": filename,
|
|
257
|
+
"version": version_for_tool,
|
|
258
|
+
"status": status_for_tool,
|
|
259
|
+
"original_text": original_text,
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
elif isinstance(event, BlockInvalidatedEvent):
|
|
264
|
+
log.debug(
|
|
265
|
+
"%s Event: BlockInvalidated. Rolled back: '%s'",
|
|
266
|
+
log_identifier,
|
|
267
|
+
event.rolled_back_text,
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
processed_parts.append(part)
|
|
271
|
+
|
|
272
|
+
if llm_response.partial:
|
|
273
|
+
if llm_response.content:
|
|
274
|
+
llm_response.content.parts = processed_parts
|
|
275
|
+
elif processed_parts:
|
|
276
|
+
llm_response.content = adk_types.Content(parts=processed_parts)
|
|
277
|
+
else:
|
|
278
|
+
log.debug(
|
|
279
|
+
"%s Ignoring text content of final aggregated response because stream was already processed.",
|
|
280
|
+
log_identifier,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if not llm_response.partial and not llm_response.interrupted:
|
|
284
|
+
log.debug(
|
|
285
|
+
"%s Final, non-interrupted stream chunk received. Finalizing parser.",
|
|
286
|
+
log_identifier,
|
|
287
|
+
)
|
|
288
|
+
final_parser_result = parser.finalize()
|
|
289
|
+
|
|
290
|
+
for event in final_parser_result.events:
|
|
291
|
+
if isinstance(event, BlockCompletedEvent):
|
|
292
|
+
log.warning(
|
|
293
|
+
"%s Unterminated artifact block detected at end of turn.",
|
|
294
|
+
log_identifier,
|
|
295
|
+
)
|
|
296
|
+
params = event.params
|
|
297
|
+
filename = params.get("filename", "unknown_artifact")
|
|
298
|
+
if (
|
|
299
|
+
"completed_artifact_blocks_list" not in session.state
|
|
300
|
+
or session.state["completed_artifact_blocks_list"] is None
|
|
301
|
+
):
|
|
302
|
+
session.state["completed_artifact_blocks_list"] = []
|
|
303
|
+
session.state["completed_artifact_blocks_list"].append(
|
|
304
|
+
{
|
|
305
|
+
"filename": filename,
|
|
306
|
+
"version": 0,
|
|
307
|
+
"status": "error",
|
|
308
|
+
"original_text": session.state.get(
|
|
309
|
+
"artifact_block_original_text", ""
|
|
310
|
+
)
|
|
311
|
+
+ event.content,
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# If there was any rolled-back text from finalization, append it
|
|
316
|
+
if final_parser_result.user_facing_text:
|
|
317
|
+
if (
|
|
318
|
+
llm_response.content
|
|
319
|
+
and llm_response.content.parts
|
|
320
|
+
and llm_response.content.parts[-1].text is not None
|
|
321
|
+
):
|
|
322
|
+
llm_response.content.parts[
|
|
323
|
+
-1
|
|
324
|
+
].text += final_parser_result.user_facing_text
|
|
325
|
+
else:
|
|
326
|
+
if llm_response.content is None:
|
|
327
|
+
llm_response.content = adk_types.Content(parts=[])
|
|
328
|
+
elif llm_response.content.parts is None:
|
|
329
|
+
llm_response.content.parts = []
|
|
330
|
+
llm_response.content.parts.append(
|
|
331
|
+
adk_types.Part(text=final_parser_result.user_facing_text)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Check if any blocks were completed and need to be injected into the final response
|
|
335
|
+
completed_blocks_list = session.state.get("completed_artifact_blocks_list")
|
|
336
|
+
if completed_blocks_list:
|
|
337
|
+
log.info(
|
|
338
|
+
"%s Injecting info for %d saved artifact(s) into final LlmResponse.",
|
|
339
|
+
log_identifier,
|
|
340
|
+
len(completed_blocks_list),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
tool_call_parts = []
|
|
344
|
+
for block_info in completed_blocks_list:
|
|
345
|
+
notify_tool_call = adk_types.FunctionCall(
|
|
346
|
+
name="_notify_artifact_save",
|
|
347
|
+
args={
|
|
348
|
+
"filename": block_info["filename"],
|
|
349
|
+
"version": block_info["version"],
|
|
350
|
+
"status": block_info["status"],
|
|
351
|
+
},
|
|
352
|
+
id=f"host-notify-{uuid.uuid4()}",
|
|
353
|
+
)
|
|
354
|
+
tool_call_parts.append(adk_types.Part(function_call=notify_tool_call))
|
|
355
|
+
|
|
356
|
+
existing_parts = llm_response.content.parts if llm_response.content else []
|
|
357
|
+
final_existing_parts = existing_parts
|
|
358
|
+
|
|
359
|
+
if llm_response.content is None:
|
|
360
|
+
llm_response.content = adk_types.Content(parts=[])
|
|
361
|
+
|
|
362
|
+
llm_response.content.parts = tool_call_parts + final_existing_parts
|
|
363
|
+
|
|
364
|
+
llm_response.turn_complete = True
|
|
365
|
+
llm_response.partial = False
|
|
366
|
+
|
|
367
|
+
session.state[parser_state_key] = None
|
|
368
|
+
session.state["completed_artifact_blocks_list"] = None
|
|
369
|
+
session.state["artifact_block_original_text"] = None
|
|
370
|
+
log.debug("%s Cleaned up parser session state.", log_identifier)
|
|
371
|
+
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def create_dangling_tool_call_repair_content(
|
|
376
|
+
dangling_calls: List[adk_types.FunctionCall], error_message: str
|
|
377
|
+
) -> adk_types.Content:
|
|
378
|
+
"""
|
|
379
|
+
Creates a synthetic ADK Content object to repair a dangling tool call.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
dangling_calls: The list of FunctionCall objects that need a response.
|
|
383
|
+
error_message: The error message to include in the response.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
An ADK Content object with role='tool' containing the error response.
|
|
387
|
+
"""
|
|
388
|
+
error_response_parts = []
|
|
389
|
+
for fc in dangling_calls:
|
|
390
|
+
error_response_part = adk_types.Part.from_function_response(
|
|
391
|
+
name=fc.name,
|
|
392
|
+
response={"status": "error", "message": error_message},
|
|
393
|
+
)
|
|
394
|
+
error_response_part.function_response.id = fc.id
|
|
395
|
+
error_response_parts.append(error_response_part)
|
|
396
|
+
|
|
397
|
+
return adk_types.Content(role="tool", parts=error_response_parts)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def repair_history_callback(
|
|
401
|
+
callback_context: CallbackContext, llm_request: LlmRequest
|
|
402
|
+
) -> Optional[LlmResponse]:
|
|
403
|
+
"""
|
|
404
|
+
ADK before_model_callback to proactively check for and repair dangling
|
|
405
|
+
tool calls in the conversation history before it's sent to the LLM.
|
|
406
|
+
This acts as a "suspender" to catch any history corruption.
|
|
407
|
+
"""
|
|
408
|
+
log_identifier = "[Callback:RepairHistory]"
|
|
409
|
+
if not llm_request.contents:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
history_modified = False
|
|
413
|
+
i = 0
|
|
414
|
+
while i < len(llm_request.contents):
|
|
415
|
+
content = llm_request.contents[i]
|
|
416
|
+
function_calls = []
|
|
417
|
+
if content.role == "model" and content.parts:
|
|
418
|
+
function_calls = [p.function_call for p in content.parts if p.function_call]
|
|
419
|
+
|
|
420
|
+
if function_calls:
|
|
421
|
+
next_content_is_valid_response = False
|
|
422
|
+
if (i + 1) < len(llm_request.contents):
|
|
423
|
+
next_content = llm_request.contents[i + 1]
|
|
424
|
+
if (
|
|
425
|
+
next_content.role in ["user", "tool"]
|
|
426
|
+
and next_content.parts
|
|
427
|
+
and any(p.function_response for p in next_content.parts)
|
|
428
|
+
):
|
|
429
|
+
next_content_is_valid_response = True
|
|
430
|
+
|
|
431
|
+
if not next_content_is_valid_response:
|
|
432
|
+
log.warning(
|
|
433
|
+
"%s Found dangling tool call in history for tool(s): %s. Repairing.",
|
|
434
|
+
log_identifier,
|
|
435
|
+
[fc.name for fc in function_calls],
|
|
436
|
+
)
|
|
437
|
+
repair_content = create_dangling_tool_call_repair_content(
|
|
438
|
+
dangling_calls=function_calls,
|
|
439
|
+
error_message="The previous tool call did not complete successfully and was automatically repaired.",
|
|
440
|
+
)
|
|
441
|
+
llm_request.contents.insert(i + 1, repair_content)
|
|
442
|
+
history_modified = True
|
|
443
|
+
i += 1
|
|
444
|
+
i += 1
|
|
445
|
+
|
|
446
|
+
if history_modified:
|
|
447
|
+
log.info(
|
|
448
|
+
"%s History was modified to repair dangling tool calls.", log_identifier
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
async def _save_mcp_response_as_artifact(
|
|
455
|
+
tool: BaseTool,
|
|
456
|
+
tool_context: ToolContext,
|
|
457
|
+
host_component: "SamAgentComponent",
|
|
458
|
+
mcp_response_dict: Dict[str, Any],
|
|
459
|
+
original_tool_args: Dict[str, Any],
|
|
460
|
+
) -> Dict[str, Any]:
|
|
461
|
+
"""
|
|
462
|
+
Saves the full MCP tool response as a JSON artifact with associated metadata.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
tool: The MCPTool instance that generated the response.
|
|
466
|
+
tool_context: The ADK ToolContext.
|
|
467
|
+
host_component: The A2A_ADK_HostComponent instance for accessing config and services.
|
|
468
|
+
mcp_response_dict: The raw MCP tool response dictionary.
|
|
469
|
+
original_tool_args: The original arguments passed to the MCP tool.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
A dictionary containing details of the saved artifact (filename, version, etc.),
|
|
473
|
+
as returned by `save_artifact_with_metadata`.
|
|
474
|
+
"""
|
|
475
|
+
log_identifier = f"[CallbackHelper:{tool.name}]"
|
|
476
|
+
log.debug("%s Saving MCP response as artifact...", log_identifier)
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
a2a_context = tool_context.state.get("a2a_context", {})
|
|
480
|
+
logical_task_id = a2a_context.get("logical_task_id", "unknownTask")
|
|
481
|
+
task_id_suffix = logical_task_id[-6:]
|
|
482
|
+
random_suffix = uuid.uuid4().hex[:6]
|
|
483
|
+
filename = f"{task_id_suffix}_{tool.name}_{random_suffix}.json"
|
|
484
|
+
log.debug("%s Generated artifact filename: %s", log_identifier, filename)
|
|
485
|
+
|
|
486
|
+
content_bytes = json.dumps(mcp_response_dict, indent=2).encode("utf-8")
|
|
487
|
+
mime_type = "application/json"
|
|
488
|
+
artifact_timestamp = datetime.now(timezone.utc)
|
|
489
|
+
|
|
490
|
+
metadata_for_saving = {
|
|
491
|
+
"description": f"Full JSON response from MCP tool {tool.name}.",
|
|
492
|
+
"source_tool_name": tool.name,
|
|
493
|
+
"source_tool_args": original_tool_args,
|
|
494
|
+
}
|
|
495
|
+
log.debug("%s Prepared content and metadata for saving.", log_identifier)
|
|
496
|
+
|
|
497
|
+
artifact_service = host_component.artifact_service
|
|
498
|
+
if not artifact_service:
|
|
499
|
+
raise ValueError("ArtifactService is not available on host_component.")
|
|
500
|
+
|
|
501
|
+
app_name = host_component.agent_name
|
|
502
|
+
user_id = tool_context._invocation_context.user_id
|
|
503
|
+
session_id = get_original_session_id(tool_context._invocation_context)
|
|
504
|
+
schema_max_keys = host_component.get_config(
|
|
505
|
+
"schema_max_keys", DEFAULT_SCHEMA_MAX_KEYS
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
log.debug(
|
|
509
|
+
"%s Calling save_artifact_with_metadata with: app_name=%s, user_id=%s, session_id=%s, filename=%s, schema_max_keys=%d",
|
|
510
|
+
log_identifier,
|
|
511
|
+
app_name,
|
|
512
|
+
user_id,
|
|
513
|
+
session_id,
|
|
514
|
+
filename,
|
|
515
|
+
schema_max_keys,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
save_result = await save_artifact_with_metadata(
|
|
519
|
+
artifact_service=artifact_service,
|
|
520
|
+
app_name=app_name,
|
|
521
|
+
user_id=user_id,
|
|
522
|
+
session_id=session_id,
|
|
523
|
+
filename=filename,
|
|
524
|
+
content_bytes=content_bytes,
|
|
525
|
+
mime_type=mime_type,
|
|
526
|
+
metadata_dict=metadata_for_saving,
|
|
527
|
+
timestamp=artifact_timestamp,
|
|
528
|
+
schema_max_keys=schema_max_keys,
|
|
529
|
+
tool_context=tool_context,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
log.info(
|
|
533
|
+
"%s MCP response saved as artifact '%s' (version %s). Result: %s",
|
|
534
|
+
log_identifier,
|
|
535
|
+
save_result.get("data_filename", filename),
|
|
536
|
+
save_result.get("data_version", "N/A"),
|
|
537
|
+
save_result.get("status"),
|
|
538
|
+
)
|
|
539
|
+
return save_result
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
log.exception(
|
|
543
|
+
"%s Error in _save_mcp_response_as_artifact: %s", log_identifier, e
|
|
544
|
+
)
|
|
545
|
+
return {
|
|
546
|
+
"status": "error",
|
|
547
|
+
"data_filename": filename if "filename" in locals() else "unknown_filename",
|
|
548
|
+
"message": f"Failed to save MCP response as artifact: {e}",
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
async def manage_large_mcp_tool_responses_callback(
|
|
553
|
+
tool: BaseTool,
|
|
554
|
+
args: Dict[str, Any],
|
|
555
|
+
tool_context: ToolContext,
|
|
556
|
+
tool_response: Any,
|
|
557
|
+
host_component: "SamAgentComponent",
|
|
558
|
+
) -> Optional[Dict[str, Any]]:
|
|
559
|
+
"""
|
|
560
|
+
Manages large responses from MCP tools by conditionally saving them as artifacts
|
|
561
|
+
and/or truncating them before returning to the LLM.
|
|
562
|
+
The 'tool_response' is the direct output from the tool's run_async method.
|
|
563
|
+
"""
|
|
564
|
+
log_identifier = f"[Callback:ManageLargeMCPResponse:{tool.name}]"
|
|
565
|
+
log.info(
|
|
566
|
+
"%s Starting callback for tool response, type: %s",
|
|
567
|
+
log_identifier,
|
|
568
|
+
type(tool_response).__name__,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if tool_response is None:
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
if not isinstance(tool, MCPTool):
|
|
575
|
+
log.debug(
|
|
576
|
+
"%s Tool is not an MCPTool. Skipping large response handling.",
|
|
577
|
+
log_identifier,
|
|
578
|
+
)
|
|
579
|
+
return (
|
|
580
|
+
tool_response
|
|
581
|
+
if isinstance(tool_response, dict)
|
|
582
|
+
else {"result": tool_response}
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
log.debug(
|
|
586
|
+
"%s Tool is an MCPTool. Proceeding with large response handling.",
|
|
587
|
+
log_identifier,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
if hasattr(tool_response, "model_dump"):
|
|
591
|
+
mcp_response_dict = tool_response.model_dump(exclude_none=True)
|
|
592
|
+
log.debug("%s Converted MCPTool response object to dictionary.", log_identifier)
|
|
593
|
+
elif isinstance(tool_response, dict):
|
|
594
|
+
mcp_response_dict = tool_response
|
|
595
|
+
log.debug("%s MCPTool response is already a dictionary.", log_identifier)
|
|
596
|
+
else:
|
|
597
|
+
log.warning(
|
|
598
|
+
"%s MCPTool response is not a Pydantic model or dict (type: %s). Attempting to proceed, but serialization might fail.",
|
|
599
|
+
log_identifier,
|
|
600
|
+
type(tool_response),
|
|
601
|
+
)
|
|
602
|
+
mcp_response_dict = tool_response
|
|
603
|
+
|
|
604
|
+
try:
|
|
605
|
+
save_threshold = host_component.get_config(
|
|
606
|
+
"mcp_tool_response_save_threshold_bytes", 2048
|
|
607
|
+
)
|
|
608
|
+
llm_max_bytes = host_component.get_config("mcp_tool_llm_return_max_bytes", 4096)
|
|
609
|
+
log.debug(
|
|
610
|
+
"%s Config: save_threshold=%d bytes, llm_max_bytes=%d bytes.",
|
|
611
|
+
log_identifier,
|
|
612
|
+
save_threshold,
|
|
613
|
+
llm_max_bytes,
|
|
614
|
+
)
|
|
615
|
+
except Exception as e:
|
|
616
|
+
log.error(
|
|
617
|
+
"%s Error retrieving configuration: %s. Using defaults.", log_identifier, e
|
|
618
|
+
)
|
|
619
|
+
save_threshold = 2048
|
|
620
|
+
llm_max_bytes = 4096
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
serialized_original_response_str = json.dumps(mcp_response_dict)
|
|
624
|
+
original_response_bytes = len(serialized_original_response_str.encode("utf-8"))
|
|
625
|
+
log.debug(
|
|
626
|
+
"%s Original response size: %d bytes.",
|
|
627
|
+
log_identifier,
|
|
628
|
+
original_response_bytes,
|
|
629
|
+
)
|
|
630
|
+
except TypeError as e:
|
|
631
|
+
log.error(
|
|
632
|
+
"%s Failed to serialize original MCP tool response dictionary: %s. Returning original response object.",
|
|
633
|
+
log_identifier,
|
|
634
|
+
e,
|
|
635
|
+
)
|
|
636
|
+
return tool_response
|
|
637
|
+
|
|
638
|
+
needs_truncation_for_llm = original_response_bytes > llm_max_bytes
|
|
639
|
+
needs_saving_as_artifact = (
|
|
640
|
+
original_response_bytes > save_threshold
|
|
641
|
+
) or needs_truncation_for_llm
|
|
642
|
+
log.debug(
|
|
643
|
+
"%s Conditions: needs_truncation_for_llm=%s, needs_saving_as_artifact=%s",
|
|
644
|
+
log_identifier,
|
|
645
|
+
needs_truncation_for_llm,
|
|
646
|
+
needs_saving_as_artifact,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
saved_artifact_details = None
|
|
650
|
+
if needs_saving_as_artifact:
|
|
651
|
+
log.info(
|
|
652
|
+
"%s Original response (%d bytes) requires saving (save_threshold=%d, llm_max_bytes=%d). Saving as artifact.",
|
|
653
|
+
log_identifier,
|
|
654
|
+
original_response_bytes,
|
|
655
|
+
save_threshold,
|
|
656
|
+
llm_max_bytes,
|
|
657
|
+
)
|
|
658
|
+
saved_artifact_details = await _save_mcp_response_as_artifact(
|
|
659
|
+
tool, tool_context, host_component, mcp_response_dict, args
|
|
660
|
+
)
|
|
661
|
+
if not (
|
|
662
|
+
saved_artifact_details.get("status") == "success"
|
|
663
|
+
or saved_artifact_details.get("status") == "partial_success"
|
|
664
|
+
):
|
|
665
|
+
log.warning(
|
|
666
|
+
"%s Failed to save artifact: %s. Proceeding without saved artifact details.",
|
|
667
|
+
log_identifier,
|
|
668
|
+
saved_artifact_details.get("message"),
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
final_llm_response_dict: Dict[str, Any] = {}
|
|
672
|
+
message_parts_for_llm: list[str] = []
|
|
673
|
+
|
|
674
|
+
if needs_truncation_for_llm:
|
|
675
|
+
truncation_suffix = "... [Response truncated due to size limit.]"
|
|
676
|
+
adjusted_max_bytes = llm_max_bytes - len(truncation_suffix.encode("utf-8"))
|
|
677
|
+
if adjusted_max_bytes < 0:
|
|
678
|
+
adjusted_max_bytes = 0
|
|
679
|
+
|
|
680
|
+
truncated_bytes = serialized_original_response_str.encode("utf-8")[
|
|
681
|
+
:adjusted_max_bytes
|
|
682
|
+
]
|
|
683
|
+
truncated_preview_str = (
|
|
684
|
+
truncated_bytes.decode("utf-8", "ignore") + truncation_suffix
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
final_llm_response_dict["mcp_tool_output"] = {
|
|
688
|
+
"type": "truncated_json_string",
|
|
689
|
+
"content": truncated_preview_str,
|
|
690
|
+
}
|
|
691
|
+
message_parts_for_llm.append(
|
|
692
|
+
f"The response from tool '{tool.name}' was too large ({original_response_bytes} bytes) for direct display and has been truncated."
|
|
693
|
+
)
|
|
694
|
+
log.debug("%s MCP tool output truncated for LLM.", log_identifier)
|
|
695
|
+
else:
|
|
696
|
+
final_llm_response_dict["mcp_tool_output"] = mcp_response_dict
|
|
697
|
+
log.debug(
|
|
698
|
+
"%s MCP tool output is the full original response for LLM.", log_identifier
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
if needs_saving_as_artifact:
|
|
702
|
+
if saved_artifact_details and (
|
|
703
|
+
saved_artifact_details.get("status") == "success"
|
|
704
|
+
or saved_artifact_details.get("status") == "partial_success"
|
|
705
|
+
):
|
|
706
|
+
final_llm_response_dict["saved_mcp_response_artifact_details"] = (
|
|
707
|
+
saved_artifact_details
|
|
708
|
+
)
|
|
709
|
+
filename = saved_artifact_details.get(
|
|
710
|
+
"data_filename", "unknown_artifact.json"
|
|
711
|
+
)
|
|
712
|
+
version = saved_artifact_details.get("data_version", "N/A")
|
|
713
|
+
message_parts_for_llm.append(
|
|
714
|
+
f"The full response has been saved as artifact '{filename}' (version {version})."
|
|
715
|
+
)
|
|
716
|
+
log.debug(
|
|
717
|
+
"%s Added saved artifact details to LLM response.", log_identifier
|
|
718
|
+
)
|
|
719
|
+
else:
|
|
720
|
+
message_parts_for_llm.append(
|
|
721
|
+
"Saving the full response as an artifact failed."
|
|
722
|
+
)
|
|
723
|
+
final_llm_response_dict["saved_mcp_response_artifact_details"] = {
|
|
724
|
+
"status": "error",
|
|
725
|
+
"message": saved_artifact_details.get(
|
|
726
|
+
"message", "Artifact saving failed."
|
|
727
|
+
),
|
|
728
|
+
"filename": saved_artifact_details.get("data_filename", "unknown"),
|
|
729
|
+
}
|
|
730
|
+
log.warning(
|
|
731
|
+
"%s Artifact save failed, error details included in LLM response.",
|
|
732
|
+
log_identifier,
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
if needs_saving_as_artifact and (
|
|
736
|
+
saved_artifact_details.get("status") == "success"
|
|
737
|
+
or saved_artifact_details.get("status") == "partial_success"
|
|
738
|
+
):
|
|
739
|
+
if needs_truncation_for_llm:
|
|
740
|
+
final_llm_response_dict["status"] = "processed_saved_and_truncated"
|
|
741
|
+
else:
|
|
742
|
+
final_llm_response_dict["status"] = "processed_and_saved"
|
|
743
|
+
elif needs_saving_as_artifact:
|
|
744
|
+
if needs_truncation_for_llm:
|
|
745
|
+
final_llm_response_dict["status"] = "processed_truncated_save_failed"
|
|
746
|
+
else:
|
|
747
|
+
final_llm_response_dict["status"] = "processed_save_failed"
|
|
748
|
+
elif needs_truncation_for_llm:
|
|
749
|
+
final_llm_response_dict["status"] = "processed_truncated"
|
|
750
|
+
else:
|
|
751
|
+
final_llm_response_dict["status"] = "processed"
|
|
752
|
+
|
|
753
|
+
if not message_parts_for_llm:
|
|
754
|
+
message_parts_for_llm.append(f"Response from tool '{tool.name}' processed.")
|
|
755
|
+
final_llm_response_dict["message_to_llm"] = " ".join(message_parts_for_llm)
|
|
756
|
+
|
|
757
|
+
log.info(
|
|
758
|
+
"%s Returning processed response for LLM. Final status: %s",
|
|
759
|
+
log_identifier,
|
|
760
|
+
final_llm_response_dict.get("status", "unknown"),
|
|
761
|
+
)
|
|
762
|
+
return final_llm_response_dict
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _generate_fenced_artifact_instruction() -> str:
|
|
766
|
+
"""Generates the instruction text for using fenced artifact blocks."""
|
|
767
|
+
open_delim = ARTIFACT_BLOCK_DELIMITER_OPEN
|
|
768
|
+
close_delim = ARTIFACT_BLOCK_DELIMITER_CLOSE
|
|
769
|
+
return f"""\
|
|
770
|
+
**Creating Text-Based Artifacts:**
|
|
771
|
+
To create an artifact from content you generate (like code, a report, or a document), you MUST use a special `save_artifact` block. This is the only reliable way to ensure your content is saved correctly.
|
|
772
|
+
|
|
773
|
+
**Syntax:**
|
|
774
|
+
```
|
|
775
|
+
{open_delim}save_artifact: filename="your_filename.ext" mime_type="text/plain" description="A brief description."
|
|
776
|
+
The full content you want to save goes here.
|
|
777
|
+
It can span multiple lines.
|
|
778
|
+
{close_delim}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
- **Rules:**
|
|
782
|
+
- The parameters `filename` and `mime_type` are required. `description` is optional but recommended.
|
|
783
|
+
- All parameter values **MUST** be enclosed in double quotes.
|
|
784
|
+
- You **MUST NOT** use double quotes `"` inside the parameter values (e.g., within the description string). Use single quotes or rephrase instead.
|
|
785
|
+
|
|
786
|
+
The system will automatically save the content and give you a confirmation in the next turn."""
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _generate_embed_instruction(
|
|
790
|
+
include_artifact_content: bool,
|
|
791
|
+
log_identifier: str,
|
|
792
|
+
) -> Optional[str]:
|
|
793
|
+
"""Generates the instruction text for using embeds."""
|
|
794
|
+
open_delim = EMBED_DELIMITER_OPEN
|
|
795
|
+
close_delim = EMBED_DELIMITER_CLOSE
|
|
796
|
+
chain_delim = EMBED_CHAIN_DELIMITER
|
|
797
|
+
early_types = "`math`, `datetime`, `uuid`, `artifact_meta`"
|
|
798
|
+
modifier_list = ", ".join(
|
|
799
|
+
[f"`{prefix}`" for prefix in MODIFIER_IMPLEMENTATIONS.keys()]
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
base_instruction = f"""\
|
|
803
|
+
You can use dynamic embeds in your text responses and tool parameters using the syntax {open_delim}type:expression {chain_delim} format{close_delim}. This allows you to
|
|
804
|
+
always have correct information in your output. Specifically, make sure you always use embeds for math, even if it is simple. You will make mistakes if you try to do math yourself.
|
|
805
|
+
Use HTML entities to escape the delimiters.
|
|
806
|
+
This host resolves the following embed types *early* (before sending to the LLM or tool): {early_types}. This means the embed is replaced with its resolved value.
|
|
807
|
+
- `{open_delim}math:expression | .2f{close_delim}`: Evaluates the math expression using asteval - this must just be plain math (plus random(), randint() and uniform()), don't import anything. Optional format specifier follows Python's format(). Use this for all math calculations rather than doing it yourself. Don't give approximations.
|
|
808
|
+
- `{open_delim}datetime:format_or_keyword{close_delim}`: Inserts current date/time. Use Python strftime format (e.g., `%Y-%m-%d`) or keywords (`iso`, `timestamp`, `date`, `time`, `now`).
|
|
809
|
+
- `{open_delim}uuid:{close_delim}`: Inserts a random UUID.
|
|
810
|
+
- `{open_delim}artifact_meta:filename[:version]{close_delim}`: Inserts a summary of the artifact's metadata (latest version if unspecified).
|
|
811
|
+
- `{open_delim}status_update:Your message here{close_delim}`: Generates an immediate, distinct status message event that is displayed to the user (e.g., 'Thinking...', 'Searching database...'). This message appears in a status area, not as part of the main chat conversation. Use this to provide interim feedback during processing."""
|
|
812
|
+
|
|
813
|
+
artifact_content_instruction = f"""
|
|
814
|
+
- `{open_delim}artifact_content:filename[:version] {chain_delim} modifier1:value1 {chain_delim} ... {chain_delim} format:output_format{close_delim}`: Embeds artifact content after applying a chain of modifiers. This is resolved *late* (typically by a gateway before final display).
|
|
815
|
+
- Use `{chain_delim}` to separate the artifact identifier from the modifier steps and the final format step.
|
|
816
|
+
- Available modifiers: {modifier_list}.
|
|
817
|
+
- The `format:output_format` step *must* be the last step in the chain. Supported formats include `text`, `datauri`, `json`, `json_pretty`, `csv`. Formatting as datauri, will include the data URI prefix, so do not add it yourself.
|
|
818
|
+
- Use `artifact_meta` first to check size; embedding large files may fail.
|
|
819
|
+
- **Using `apply_to_template` Modifier:**
|
|
820
|
+
- This modifier renders a Mustache template artifact using the data from the previous step.
|
|
821
|
+
- **Data Context:**
|
|
822
|
+
- If the input data's original MIME type was `text/csv` or `application/csv`, it's automatically parsed into an object with two keys: `headers` (a list of column name strings) and `data_rows` (a list of lists, where each inner list contains the string values for a row). Example template usage: `<thead><tr>{{{{#headers}}}}<th>{{{{.}}}}</th>{{{{/headers}}}}</tr></thead><tbody>{{{{#data_rows}}}}<tr>{{{{#.}}}}<td>{{{{.}}}}</td>{{{{/.}}}}</tr>{{{{/data_rows}}}}</tbody>`. If CSV parsing fails, the raw string content is available under `text`.
|
|
823
|
+
- If the input data is a **list** (e.g., from `jsonpath` or a JSON array), it's available under `items`.
|
|
824
|
+
- If the input data is a **dictionary** (e.g., from a JSON object), its keys are directly available (e.g., `{{{{key1}}}}`).
|
|
825
|
+
- If the input data is a **plain string** (and not auto-parsed as CSV), it's available under `text`.
|
|
826
|
+
- The template filename can include a version (e.g., `template.mustache:2`). Defaults to latest.
|
|
827
|
+
- The template itself can contain `«artifact_content:...»` embeds, which will be resolved before rendering.
|
|
828
|
+
- Examples:
|
|
829
|
+
- `<img src="{open_delim}artifact_content:image.png {chain_delim} format:datauri{close_delim}`"> (Embed image as data URI - NOTE that this includes the datauri prefix. Do not add it yourself.)
|
|
830
|
+
- `{open_delim}artifact_content:data.json {chain_delim} jsonpath:$.items[*] {chain_delim} select_fields:name,status {chain_delim} format:json_pretty{close_delim}` (Extract and format JSON fields)
|
|
831
|
+
- `{open_delim}artifact_content:logs.txt {chain_delim} grep:ERROR {chain_delim} head:10 {chain_delim} format:text{close_delim}` (Get first 10 error lines)
|
|
832
|
+
- `{open_delim}artifact_content:products.csv {chain_delim} apply_to_template:product_table.html.mustache {chain_delim} format:text{close_delim}` (CSV is auto-parsed to `headers` and `data_rows` for the HTML template)
|
|
833
|
+
- `{open_delim}artifact_content:config.json {chain_delim} jsonpath:$.userPreferences.theme {chain_delim} format:text{close_delim}` (Extract a single value from a JSON artifact)
|
|
834
|
+
- `{open_delim}artifact_content:sensor_readings.csv {chain_delim} filter_rows_eq:status:critical {chain_delim} select_cols:timestamp,sensor_id,value {chain_delim} format:csv{close_delim}` (Filter critical sensor readings and select specific columns, output as CSV)
|
|
835
|
+
- `{open_delim}artifact_content:server.log {chain_delim} tail:100 {chain_delim} grep:WARN {chain_delim} format:text{close_delim}` (Get warning lines from the last 100 lines of a log file)"""
|
|
836
|
+
|
|
837
|
+
final_instruction = base_instruction
|
|
838
|
+
if include_artifact_content:
|
|
839
|
+
final_instruction += artifact_content_instruction
|
|
840
|
+
|
|
841
|
+
final_instruction += f"""
|
|
842
|
+
Ensure the syntax is exactly `{open_delim}type:expression{close_delim}` or `{open_delim}type:expression {chain_delim} ... {chain_delim} format:output_format{close_delim}` with no extra spaces around delimiters (`{open_delim}`, `{close_delim}`, `{chain_delim}`, `:`, `|`). Malformed directives will be ignored."""
|
|
843
|
+
|
|
844
|
+
return final_instruction
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _generate_tool_instructions_from_registry(
|
|
848
|
+
active_tools: List[BuiltinTool],
|
|
849
|
+
log_identifier: str,
|
|
850
|
+
) -> str:
|
|
851
|
+
"""Generates instruction text from a list of BuiltinTool definitions."""
|
|
852
|
+
if not active_tools:
|
|
853
|
+
return ""
|
|
854
|
+
|
|
855
|
+
instructions_by_category = defaultdict(list)
|
|
856
|
+
for tool in sorted(active_tools, key=lambda t: (t.category, t.name)):
|
|
857
|
+
param_parts = []
|
|
858
|
+
if tool.parameters and tool.parameters.properties:
|
|
859
|
+
for name, schema in tool.parameters.properties.items():
|
|
860
|
+
is_optional = name not in (tool.parameters.required or [])
|
|
861
|
+
type_name = "any"
|
|
862
|
+
if schema and hasattr(schema, "type") and schema.type:
|
|
863
|
+
type_name = schema.type.name.lower()
|
|
864
|
+
|
|
865
|
+
param_str = f"{name}: {type_name}"
|
|
866
|
+
if is_optional:
|
|
867
|
+
param_str = f"Optional[{param_str}]"
|
|
868
|
+
param_parts.append(param_str)
|
|
869
|
+
|
|
870
|
+
signature = f"`{tool.name}({', '.join(param_parts)})`"
|
|
871
|
+
description = tool.description or "No description available."
|
|
872
|
+
|
|
873
|
+
instructions_by_category[tool.category].append(f"- {signature}: {description}")
|
|
874
|
+
|
|
875
|
+
full_instruction_list = []
|
|
876
|
+
for category, tool_instructions in sorted(instructions_by_category.items()):
|
|
877
|
+
category_display_name = category.replace("_", " ").title()
|
|
878
|
+
full_instruction_list.append(
|
|
879
|
+
f"You have access to the following '{category_display_name}' tools:"
|
|
880
|
+
)
|
|
881
|
+
full_instruction_list.extend(tool_instructions)
|
|
882
|
+
|
|
883
|
+
return "\n".join(full_instruction_list)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def inject_dynamic_instructions_callback(
|
|
887
|
+
callback_context: CallbackContext,
|
|
888
|
+
llm_request: LlmRequest,
|
|
889
|
+
host_component: "SamAgentComponent",
|
|
890
|
+
active_builtin_tools: List[BuiltinTool],
|
|
891
|
+
) -> Optional[LlmResponse]:
|
|
892
|
+
"""
|
|
893
|
+
ADK before_model_callback to inject instructions based on host config.
|
|
894
|
+
Modifies the llm_request directly.
|
|
895
|
+
"""
|
|
896
|
+
log_identifier = "[Callback:InjectInstructions]"
|
|
897
|
+
log.debug("%s Running instruction injection callback...", log_identifier)
|
|
898
|
+
|
|
899
|
+
if not host_component:
|
|
900
|
+
log.error(
|
|
901
|
+
"%s Host component instance not provided. Cannot inject instructions.",
|
|
902
|
+
log_identifier,
|
|
903
|
+
)
|
|
904
|
+
return None
|
|
905
|
+
|
|
906
|
+
injected_instructions = []
|
|
907
|
+
|
|
908
|
+
planning_instruction = """\
|
|
909
|
+
Parallel Tool Calling:
|
|
910
|
+
The system is capable of calling multiple tools in parallel to speed up processing. Please try to run tools in parallel when they don't depend on each other. This saves money and time, providing faster results to the user.
|
|
911
|
+
|
|
912
|
+
Embeds in responses from agents:
|
|
913
|
+
To be efficient, agents may response with artifact_content embeds in their responses. These will not be resolved until they are sent back to a gateway. If it makes
|
|
914
|
+
sense, just carry that embed forward to your response to the user. For example, if you ask for an org chart from another agent and its response contains an embed like
|
|
915
|
+
`{open_delim}artifact_content:org_chart.md{close_delim}`, you can just include that embed in your response to the user. The gateway will resolve it and display the org chart.
|
|
916
|
+
|
|
917
|
+
When faced with a complex goal or request that involves multiple steps, data retrieval, or artifact summarization to produce a new report or document, you MUST first create a plan.
|
|
918
|
+
Simple, direct requests like 'create an image of a dog' or 'write an email to thank my boss' do not require a plan.
|
|
919
|
+
|
|
920
|
+
If a plan is created:
|
|
921
|
+
1. It should be a terse, hierarchical list describing the steps needed.
|
|
922
|
+
2. Use '☐' (empty checkbox emoji) for pending items and '☑' (checked checkbox emoji) for completed items.
|
|
923
|
+
3. If the plan changes significantly during execution, restate the updated plan.
|
|
924
|
+
4. As items are completed, update the plan to check them off.
|
|
925
|
+
|
|
926
|
+
"""
|
|
927
|
+
injected_instructions.append(planning_instruction)
|
|
928
|
+
log.debug("%s Added hardcoded planning instructions.", log_identifier)
|
|
929
|
+
fenced_artifact_instruction = _generate_fenced_artifact_instruction()
|
|
930
|
+
injected_instructions.append(fenced_artifact_instruction)
|
|
931
|
+
|
|
932
|
+
agent_instruction_str: Optional[str] = None
|
|
933
|
+
if host_component._agent_system_instruction_callback:
|
|
934
|
+
log.debug(
|
|
935
|
+
"%s Calling agent-provided system instruction callback.", log_identifier
|
|
936
|
+
)
|
|
937
|
+
try:
|
|
938
|
+
agent_instruction_str = host_component._agent_system_instruction_callback(
|
|
939
|
+
callback_context, llm_request
|
|
940
|
+
)
|
|
941
|
+
if agent_instruction_str and isinstance(agent_instruction_str, str):
|
|
942
|
+
injected_instructions.append(agent_instruction_str)
|
|
943
|
+
log.info(
|
|
944
|
+
"%s Injected instructions from agent callback.", log_identifier
|
|
945
|
+
)
|
|
946
|
+
elif agent_instruction_str:
|
|
947
|
+
log.warning(
|
|
948
|
+
"%s Agent instruction callback returned non-string type: %s. Ignoring.",
|
|
949
|
+
log_identifier,
|
|
950
|
+
type(agent_instruction_str),
|
|
951
|
+
)
|
|
952
|
+
except Exception as e_cb:
|
|
953
|
+
log.error(
|
|
954
|
+
"%s Error in agent-provided system instruction callback: %s. Skipping.",
|
|
955
|
+
log_identifier,
|
|
956
|
+
e_cb,
|
|
957
|
+
)
|
|
958
|
+
if host_component._agent_system_instruction_string:
|
|
959
|
+
log.debug(
|
|
960
|
+
"%s Using agent-provided static system instruction string.", log_identifier
|
|
961
|
+
)
|
|
962
|
+
agent_instruction_str = host_component._agent_system_instruction_string
|
|
963
|
+
if agent_instruction_str and isinstance(agent_instruction_str, str):
|
|
964
|
+
injected_instructions.append(agent_instruction_str)
|
|
965
|
+
log.info("%s Injected static instructions from agent.", log_identifier)
|
|
966
|
+
|
|
967
|
+
contents = llm_request.contents
|
|
968
|
+
if contents:
|
|
969
|
+
log.debug("\n\n### LLM Request Contents ###")
|
|
970
|
+
for content in contents:
|
|
971
|
+
if content.parts:
|
|
972
|
+
for part in content.parts:
|
|
973
|
+
if part.text:
|
|
974
|
+
log.debug("Content part: %s", part.text)
|
|
975
|
+
elif part.function_call:
|
|
976
|
+
log.debug("Function call: %s", part.function_call.name)
|
|
977
|
+
elif part.function_response:
|
|
978
|
+
log.debug("Function response: %s", part.function_response)
|
|
979
|
+
else:
|
|
980
|
+
log.debug("raw: %s", part)
|
|
981
|
+
log.debug("### End LLM Request Contents ###\n\n")
|
|
982
|
+
|
|
983
|
+
if host_component.get_config("enable_embed_resolution", True):
|
|
984
|
+
include_artifact_content_instr = host_component.get_config(
|
|
985
|
+
"enable_artifact_content_instruction", True
|
|
986
|
+
)
|
|
987
|
+
instruction = _generate_embed_instruction(
|
|
988
|
+
include_artifact_content_instr, log_identifier
|
|
989
|
+
)
|
|
990
|
+
if instruction:
|
|
991
|
+
injected_instructions.append(instruction)
|
|
992
|
+
log.debug(
|
|
993
|
+
"%s Prepared embed instructions (artifact_content included: %s).",
|
|
994
|
+
log_identifier,
|
|
995
|
+
include_artifact_content_instr,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
if active_builtin_tools:
|
|
999
|
+
instruction = _generate_tool_instructions_from_registry(
|
|
1000
|
+
active_builtin_tools, log_identifier
|
|
1001
|
+
)
|
|
1002
|
+
if instruction:
|
|
1003
|
+
injected_instructions.append(instruction)
|
|
1004
|
+
log.debug(
|
|
1005
|
+
"%s Prepared instructions for %d active built-in tools.",
|
|
1006
|
+
log_identifier,
|
|
1007
|
+
len(active_builtin_tools),
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
peer_instructions = callback_context.state.get("peer_tool_instructions")
|
|
1011
|
+
if peer_instructions and isinstance(peer_instructions, str):
|
|
1012
|
+
injected_instructions.append(peer_instructions)
|
|
1013
|
+
log.debug(
|
|
1014
|
+
"%s Injected peer discovery instructions from callback state.",
|
|
1015
|
+
log_identifier,
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
last_call_notification_message_added = False
|
|
1019
|
+
try:
|
|
1020
|
+
invocation_context = callback_context._invocation_context
|
|
1021
|
+
if invocation_context and invocation_context.run_config:
|
|
1022
|
+
current_llm_calls = (
|
|
1023
|
+
invocation_context._invocation_cost_manager._number_of_llm_calls
|
|
1024
|
+
)
|
|
1025
|
+
max_llm_calls = invocation_context.run_config.max_llm_calls
|
|
1026
|
+
|
|
1027
|
+
log.debug(
|
|
1028
|
+
"%s Checking for last LLM call: current_calls=%d, max_calls=%s",
|
|
1029
|
+
log_identifier,
|
|
1030
|
+
current_llm_calls,
|
|
1031
|
+
max_llm_calls,
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
if (
|
|
1035
|
+
max_llm_calls
|
|
1036
|
+
and max_llm_calls > 0
|
|
1037
|
+
and current_llm_calls >= (max_llm_calls - 1)
|
|
1038
|
+
):
|
|
1039
|
+
last_call_text = (
|
|
1040
|
+
"IMPORTANT: This is your final allowed interaction for the current request. "
|
|
1041
|
+
"Please inform the user that to continue this line of inquiry, they will need to "
|
|
1042
|
+
"make a new request or explicitly ask to continue if the interface supports it. "
|
|
1043
|
+
"Summarize your current findings and conclude your response."
|
|
1044
|
+
)
|
|
1045
|
+
if llm_request.contents is None:
|
|
1046
|
+
llm_request.contents = []
|
|
1047
|
+
|
|
1048
|
+
last_call_content = adk_types.Content(
|
|
1049
|
+
role="model",
|
|
1050
|
+
parts=[adk_types.Part(text=last_call_text)],
|
|
1051
|
+
)
|
|
1052
|
+
llm_request.contents.append(last_call_content)
|
|
1053
|
+
last_call_notification_message_added = True
|
|
1054
|
+
log.info(
|
|
1055
|
+
"%s Added 'last LLM call' notification as a 'model' message to llm_request.contents. Current calls (%d) reached max_llm_calls (%d).",
|
|
1056
|
+
log_identifier,
|
|
1057
|
+
current_llm_calls,
|
|
1058
|
+
max_llm_calls,
|
|
1059
|
+
)
|
|
1060
|
+
except Exception as e_last_call:
|
|
1061
|
+
log.error(
|
|
1062
|
+
"%s Error checking/injecting last LLM call notification message: %s",
|
|
1063
|
+
log_identifier,
|
|
1064
|
+
e_last_call,
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
if host_component.get_config("inject_current_time", True):
|
|
1068
|
+
current_time = datetime.now(timezone.utc).strftime("%A, %d %b %Y %H:%M:%S UTC")
|
|
1069
|
+
instruction = f"Current time {current_time}."
|
|
1070
|
+
injected_instructions.append(instruction)
|
|
1071
|
+
|
|
1072
|
+
if injected_instructions:
|
|
1073
|
+
combined_instructions = "\n\n---\n\n".join(injected_instructions)
|
|
1074
|
+
if llm_request.config is None:
|
|
1075
|
+
log.warning(
|
|
1076
|
+
"%s llm_request.config is None, cannot append system instructions.",
|
|
1077
|
+
log_identifier,
|
|
1078
|
+
)
|
|
1079
|
+
else:
|
|
1080
|
+
if llm_request.config.system_instruction is None:
|
|
1081
|
+
llm_request.config.system_instruction = ""
|
|
1082
|
+
|
|
1083
|
+
if llm_request.config.system_instruction:
|
|
1084
|
+
llm_request.config.system_instruction += (
|
|
1085
|
+
"\n\n---\n\n" + combined_instructions
|
|
1086
|
+
)
|
|
1087
|
+
else:
|
|
1088
|
+
llm_request.config.system_instruction = combined_instructions
|
|
1089
|
+
log.info(
|
|
1090
|
+
"%s Injected %d dynamic instruction block(s) into llm_request.config.system_instruction.",
|
|
1091
|
+
log_identifier,
|
|
1092
|
+
len(injected_instructions),
|
|
1093
|
+
)
|
|
1094
|
+
elif not last_call_notification_message_added:
|
|
1095
|
+
log.debug(
|
|
1096
|
+
"%s No dynamic instructions (system or last_call message) were injected based on config.",
|
|
1097
|
+
log_identifier,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
return None
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
async def after_tool_callback_inject_metadata(
|
|
1104
|
+
tool: BaseTool,
|
|
1105
|
+
args: Dict,
|
|
1106
|
+
tool_context: ToolContext,
|
|
1107
|
+
tool_response: Dict,
|
|
1108
|
+
host_component: "SamAgentComponent",
|
|
1109
|
+
) -> Optional[Dict]:
|
|
1110
|
+
"""
|
|
1111
|
+
ADK after_tool_callback to automatically load and inject metadata for
|
|
1112
|
+
newly created artifacts into the tool's response dictionary.
|
|
1113
|
+
"""
|
|
1114
|
+
log_identifier = f"[Callback:InjectMetadata:{tool.name}]"
|
|
1115
|
+
log.info(
|
|
1116
|
+
"%s Starting metadata injection for tool response, type: %s",
|
|
1117
|
+
log_identifier,
|
|
1118
|
+
type(tool_response).__name__,
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
if not host_component:
|
|
1122
|
+
log.error(
|
|
1123
|
+
"%s Host component instance not provided. Cannot proceed.",
|
|
1124
|
+
log_identifier,
|
|
1125
|
+
)
|
|
1126
|
+
return None
|
|
1127
|
+
|
|
1128
|
+
if not tool_context.actions.artifact_delta:
|
|
1129
|
+
log.debug(
|
|
1130
|
+
"%s No artifact delta found. Skipping metadata injection.", log_identifier
|
|
1131
|
+
)
|
|
1132
|
+
return None
|
|
1133
|
+
|
|
1134
|
+
artifact_service: Optional[BaseArtifactService] = (
|
|
1135
|
+
tool_context._invocation_context.artifact_service
|
|
1136
|
+
)
|
|
1137
|
+
if not artifact_service:
|
|
1138
|
+
log.error(
|
|
1139
|
+
"%s ArtifactService not available. Cannot load metadata.",
|
|
1140
|
+
log_identifier,
|
|
1141
|
+
)
|
|
1142
|
+
return None
|
|
1143
|
+
|
|
1144
|
+
app_name = tool_context._invocation_context.app_name
|
|
1145
|
+
user_id = tool_context._invocation_context.user_id
|
|
1146
|
+
session_id = get_original_session_id(tool_context._invocation_context)
|
|
1147
|
+
|
|
1148
|
+
metadata_texts = []
|
|
1149
|
+
|
|
1150
|
+
for filename, version in tool_context.actions.artifact_delta.items():
|
|
1151
|
+
if filename.endswith(METADATA_SUFFIX):
|
|
1152
|
+
log.debug(
|
|
1153
|
+
"%s Skipping metadata artifact '%s' itself.", log_identifier, filename
|
|
1154
|
+
)
|
|
1155
|
+
continue
|
|
1156
|
+
|
|
1157
|
+
metadata_filename = f"{filename}{METADATA_SUFFIX}"
|
|
1158
|
+
log.debug(
|
|
1159
|
+
"%s Found data artifact '%s' v%d. Attempting to load metadata '%s' v%d.",
|
|
1160
|
+
log_identifier,
|
|
1161
|
+
filename,
|
|
1162
|
+
version,
|
|
1163
|
+
metadata_filename,
|
|
1164
|
+
version,
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
try:
|
|
1168
|
+
metadata_part = await artifact_service.load_artifact(
|
|
1169
|
+
app_name=app_name,
|
|
1170
|
+
user_id=user_id,
|
|
1171
|
+
session_id=session_id,
|
|
1172
|
+
filename=metadata_filename,
|
|
1173
|
+
version=version,
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
if metadata_part and metadata_part.inline_data:
|
|
1177
|
+
try:
|
|
1178
|
+
metadata_dict = json.loads(
|
|
1179
|
+
metadata_part.inline_data.data.decode("utf-8")
|
|
1180
|
+
)
|
|
1181
|
+
metadata_dict["version"] = version
|
|
1182
|
+
formatted_text = format_metadata_for_llm(metadata_dict)
|
|
1183
|
+
metadata_texts.append(formatted_text)
|
|
1184
|
+
log.info(
|
|
1185
|
+
"%s Successfully loaded and formatted metadata for '%s' v%d.",
|
|
1186
|
+
log_identifier,
|
|
1187
|
+
filename,
|
|
1188
|
+
version,
|
|
1189
|
+
)
|
|
1190
|
+
except json.JSONDecodeError as json_err:
|
|
1191
|
+
log.warning(
|
|
1192
|
+
"%s Failed to parse metadata JSON for '%s' v%d: %s",
|
|
1193
|
+
log_identifier,
|
|
1194
|
+
metadata_filename,
|
|
1195
|
+
version,
|
|
1196
|
+
json_err,
|
|
1197
|
+
)
|
|
1198
|
+
except Exception as fmt_err:
|
|
1199
|
+
log.warning(
|
|
1200
|
+
"%s Failed to format metadata for '%s' v%d: %s",
|
|
1201
|
+
log_identifier,
|
|
1202
|
+
metadata_filename,
|
|
1203
|
+
version,
|
|
1204
|
+
fmt_err,
|
|
1205
|
+
)
|
|
1206
|
+
else:
|
|
1207
|
+
log.warning(
|
|
1208
|
+
"%s Companion metadata artifact '%s' v%d not found or empty.",
|
|
1209
|
+
log_identifier,
|
|
1210
|
+
metadata_filename,
|
|
1211
|
+
version,
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
except Exception as load_err:
|
|
1215
|
+
log.error(
|
|
1216
|
+
"%s Error loading companion metadata artifact '%s' v%d: %s",
|
|
1217
|
+
log_identifier,
|
|
1218
|
+
metadata_filename,
|
|
1219
|
+
version,
|
|
1220
|
+
load_err,
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
if metadata_texts:
|
|
1224
|
+
if not isinstance(tool_response, dict):
|
|
1225
|
+
log.error(
|
|
1226
|
+
"%s Tool response is not a dictionary. Cannot inject metadata. Type: %s",
|
|
1227
|
+
log_identifier,
|
|
1228
|
+
type(tool_response),
|
|
1229
|
+
)
|
|
1230
|
+
return None
|
|
1231
|
+
|
|
1232
|
+
combined_metadata_text = "\n\n".join(metadata_texts)
|
|
1233
|
+
tool_response[METADATA_RESPONSE_KEY] = combined_metadata_text
|
|
1234
|
+
log.info(
|
|
1235
|
+
"%s Injected metadata for %d artifact(s) into tool response key '%s'.",
|
|
1236
|
+
log_identifier,
|
|
1237
|
+
len(metadata_texts),
|
|
1238
|
+
METADATA_RESPONSE_KEY,
|
|
1239
|
+
)
|
|
1240
|
+
return tool_response
|
|
1241
|
+
else:
|
|
1242
|
+
log.debug(
|
|
1243
|
+
"%s No metadata loaded or formatted. Returning original tool response.",
|
|
1244
|
+
log_identifier,
|
|
1245
|
+
)
|
|
1246
|
+
return None
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
async def track_produced_artifacts_callback(
|
|
1250
|
+
tool: BaseTool,
|
|
1251
|
+
args: Dict,
|
|
1252
|
+
tool_context: ToolContext,
|
|
1253
|
+
tool_response: Dict,
|
|
1254
|
+
host_component: "SamAgentComponent",
|
|
1255
|
+
) -> Optional[Dict]:
|
|
1256
|
+
"""
|
|
1257
|
+
ADK after_tool_callback to automatically track all artifacts created by a tool.
|
|
1258
|
+
It inspects the artifact_delta and registers the created artifacts in the
|
|
1259
|
+
TaskExecutionContext.
|
|
1260
|
+
"""
|
|
1261
|
+
log_identifier = f"[Callback:TrackArtifacts:{tool.name}]"
|
|
1262
|
+
log.debug("%s Starting artifact tracking for tool response.", log_identifier)
|
|
1263
|
+
|
|
1264
|
+
if not tool_context.actions.artifact_delta:
|
|
1265
|
+
log.debug("%s No artifact delta found. Skipping tracking.", log_identifier)
|
|
1266
|
+
return None
|
|
1267
|
+
|
|
1268
|
+
if not host_component:
|
|
1269
|
+
log.error(
|
|
1270
|
+
"%s Host component instance not provided. Cannot proceed.", log_identifier
|
|
1271
|
+
)
|
|
1272
|
+
return None
|
|
1273
|
+
|
|
1274
|
+
try:
|
|
1275
|
+
a2a_context = tool_context.state.get("a2a_context", {})
|
|
1276
|
+
logical_task_id = a2a_context.get("logical_task_id")
|
|
1277
|
+
if not logical_task_id:
|
|
1278
|
+
log.warning(
|
|
1279
|
+
"%s Could not find logical_task_id in tool_context. Cannot track artifacts.",
|
|
1280
|
+
log_identifier,
|
|
1281
|
+
)
|
|
1282
|
+
return None
|
|
1283
|
+
|
|
1284
|
+
with host_component.active_tasks_lock:
|
|
1285
|
+
task_context = host_component.active_tasks.get(logical_task_id)
|
|
1286
|
+
|
|
1287
|
+
if not task_context:
|
|
1288
|
+
log.warning(
|
|
1289
|
+
"%s TaskExecutionContext not found for task %s. Cannot track artifacts.",
|
|
1290
|
+
log_identifier,
|
|
1291
|
+
logical_task_id,
|
|
1292
|
+
)
|
|
1293
|
+
return None
|
|
1294
|
+
|
|
1295
|
+
for filename, version in tool_context.actions.artifact_delta.items():
|
|
1296
|
+
if filename.endswith(METADATA_SUFFIX):
|
|
1297
|
+
continue
|
|
1298
|
+
log.info(
|
|
1299
|
+
"%s Registering produced artifact '%s' v%d for task %s.",
|
|
1300
|
+
log_identifier,
|
|
1301
|
+
filename,
|
|
1302
|
+
version,
|
|
1303
|
+
logical_task_id,
|
|
1304
|
+
)
|
|
1305
|
+
task_context.register_produced_artifact(filename, version)
|
|
1306
|
+
|
|
1307
|
+
except Exception as e:
|
|
1308
|
+
log.exception(
|
|
1309
|
+
"%s Error during artifact tracking callback: %s", log_identifier, e
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
return None
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def log_streaming_chunk_callback(
|
|
1316
|
+
callback_context: CallbackContext,
|
|
1317
|
+
llm_response: LlmResponse,
|
|
1318
|
+
host_component: "SamAgentComponent",
|
|
1319
|
+
) -> Optional[LlmResponse]:
|
|
1320
|
+
"""
|
|
1321
|
+
ADK after_model_callback to log the content of each LLM response chunk
|
|
1322
|
+
*after* potential modification by other callbacks (like embed resolution).
|
|
1323
|
+
"""
|
|
1324
|
+
log_identifier = "[Callback:LogChunk]"
|
|
1325
|
+
try:
|
|
1326
|
+
content_str = "None"
|
|
1327
|
+
is_partial = llm_response.partial
|
|
1328
|
+
is_final = llm_response.turn_complete
|
|
1329
|
+
if llm_response.content and llm_response.content.parts:
|
|
1330
|
+
texts = [p.text for p in llm_response.content.parts if p.text]
|
|
1331
|
+
content_str = '"' + "".join(texts) + '"' if texts else "[Non-text parts]"
|
|
1332
|
+
elif llm_response.error_message:
|
|
1333
|
+
content_str = f"[ERROR: {llm_response.error_message}]"
|
|
1334
|
+
|
|
1335
|
+
except Exception as e:
|
|
1336
|
+
log.error("%s Error logging LLM chunk: %s", log_identifier, e)
|
|
1337
|
+
|
|
1338
|
+
return None
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def solace_llm_invocation_callback(
|
|
1342
|
+
callback_context: CallbackContext,
|
|
1343
|
+
llm_request: LlmRequest,
|
|
1344
|
+
host_component: "SamAgentComponent",
|
|
1345
|
+
) -> Optional[LlmResponse]:
|
|
1346
|
+
"""
|
|
1347
|
+
ADK before_model_callback to send a Solace message when an LLM is invoked,
|
|
1348
|
+
using the host_component's process_and_publish_adk_event method.
|
|
1349
|
+
"""
|
|
1350
|
+
log_identifier = "[Callback:SolaceLLMInvocation]"
|
|
1351
|
+
log.debug(
|
|
1352
|
+
"%s Running Solace LLM invocation notification callback...", log_identifier
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
if not host_component:
|
|
1356
|
+
log.error(
|
|
1357
|
+
"%s Host component instance not provided. Cannot send Solace message.",
|
|
1358
|
+
log_identifier,
|
|
1359
|
+
)
|
|
1360
|
+
return None
|
|
1361
|
+
|
|
1362
|
+
try:
|
|
1363
|
+
from ...common.a2a_protocol import A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY
|
|
1364
|
+
|
|
1365
|
+
callback_context.state[A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY] = False
|
|
1366
|
+
log.debug(
|
|
1367
|
+
"%s Reset %s to False.", log_identifier, A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY
|
|
1368
|
+
)
|
|
1369
|
+
except Exception as e_flag_reset:
|
|
1370
|
+
log.error(
|
|
1371
|
+
"%s Error resetting %s: %s",
|
|
1372
|
+
log_identifier,
|
|
1373
|
+
A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY,
|
|
1374
|
+
e_flag_reset,
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
try:
|
|
1378
|
+
a2a_context = callback_context.state.get("a2a_context")
|
|
1379
|
+
if not a2a_context:
|
|
1380
|
+
log.error(
|
|
1381
|
+
"%s a2a_context not found in callback_context.state. Cannot send Solace message.",
|
|
1382
|
+
log_identifier,
|
|
1383
|
+
)
|
|
1384
|
+
return None
|
|
1385
|
+
|
|
1386
|
+
agent_name = host_component.get_config("agent_name", "unknown_agent")
|
|
1387
|
+
logical_task_id = a2a_context.get("logical_task_id")
|
|
1388
|
+
|
|
1389
|
+
llm_invocation_metadata = {
|
|
1390
|
+
"type": "llm_invocation",
|
|
1391
|
+
"data": llm_request.model_dump(exclude_none=True),
|
|
1392
|
+
}
|
|
1393
|
+
a2a_message = A2AMessage(
|
|
1394
|
+
role="agent",
|
|
1395
|
+
parts=[],
|
|
1396
|
+
metadata=llm_invocation_metadata,
|
|
1397
|
+
)
|
|
1398
|
+
task_status = TaskStatus(
|
|
1399
|
+
state=TaskState.WORKING,
|
|
1400
|
+
message=a2a_message,
|
|
1401
|
+
timestamp=datetime.now(timezone.utc),
|
|
1402
|
+
)
|
|
1403
|
+
status_update_event = TaskStatusUpdateEvent(
|
|
1404
|
+
id=logical_task_id,
|
|
1405
|
+
status=task_status,
|
|
1406
|
+
final=False,
|
|
1407
|
+
metadata={"agent_name": agent_name},
|
|
1408
|
+
)
|
|
1409
|
+
|
|
1410
|
+
loop = host_component.get_async_loop()
|
|
1411
|
+
if loop and loop.is_running():
|
|
1412
|
+
asyncio.run_coroutine_threadsafe(
|
|
1413
|
+
host_component._publish_status_update_with_buffer_flush(
|
|
1414
|
+
status_update_event,
|
|
1415
|
+
a2a_context,
|
|
1416
|
+
skip_buffer_flush=False,
|
|
1417
|
+
),
|
|
1418
|
+
loop,
|
|
1419
|
+
)
|
|
1420
|
+
log.debug(
|
|
1421
|
+
"%s Scheduled LLM invocation status update with buffer flush.",
|
|
1422
|
+
log_identifier,
|
|
1423
|
+
)
|
|
1424
|
+
else:
|
|
1425
|
+
log.error(
|
|
1426
|
+
"%s Async loop not available. Cannot publish LLM invocation status update.",
|
|
1427
|
+
log_identifier,
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
except Exception as e:
|
|
1431
|
+
log.error(
|
|
1432
|
+
"%s Error during Solace LLM invocation notification: %s", log_identifier, e
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
return None
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def solace_llm_response_callback(
|
|
1439
|
+
callback_context: CallbackContext,
|
|
1440
|
+
llm_response: LlmResponse,
|
|
1441
|
+
host_component: "SamAgentComponent",
|
|
1442
|
+
) -> Optional[LlmResponse]:
|
|
1443
|
+
"""
|
|
1444
|
+
ADK after_model_callback to send a Solace message with the LLM's response,
|
|
1445
|
+
using the host_component's process_and_publish_adk_event method.
|
|
1446
|
+
"""
|
|
1447
|
+
log_identifier = "[Callback:SolaceLLMResponse]"
|
|
1448
|
+
if llm_response.partial: # Don't send partial responses for this notification
|
|
1449
|
+
log.debug("%s Skipping partial response", log_identifier)
|
|
1450
|
+
return None
|
|
1451
|
+
|
|
1452
|
+
if not host_component:
|
|
1453
|
+
log.error(
|
|
1454
|
+
"%s Host component instance not provided. Cannot send Solace message.",
|
|
1455
|
+
log_identifier,
|
|
1456
|
+
)
|
|
1457
|
+
return None
|
|
1458
|
+
|
|
1459
|
+
try:
|
|
1460
|
+
a2a_context = callback_context.state.get("a2a_context")
|
|
1461
|
+
if not a2a_context:
|
|
1462
|
+
log.error(
|
|
1463
|
+
"%s a2a_context not found in callback_context.state. Cannot send Solace message.",
|
|
1464
|
+
log_identifier,
|
|
1465
|
+
)
|
|
1466
|
+
return None
|
|
1467
|
+
|
|
1468
|
+
agent_name = host_component.get_config("agent_name", "unknown_agent")
|
|
1469
|
+
logical_task_id = a2a_context.get("logical_task_id")
|
|
1470
|
+
|
|
1471
|
+
llm_response_metadata = {
|
|
1472
|
+
"type": "llm_response",
|
|
1473
|
+
"data": llm_response.model_dump(exclude_none=True),
|
|
1474
|
+
}
|
|
1475
|
+
a2a_message = A2AMessage(role="agent", parts=[], metadata=llm_response_metadata)
|
|
1476
|
+
task_status = TaskStatus(
|
|
1477
|
+
state=TaskState.WORKING,
|
|
1478
|
+
message=a2a_message,
|
|
1479
|
+
timestamp=datetime.now(timezone.utc),
|
|
1480
|
+
)
|
|
1481
|
+
status_update_event = TaskStatusUpdateEvent(
|
|
1482
|
+
id=logical_task_id,
|
|
1483
|
+
status=task_status,
|
|
1484
|
+
final=True,
|
|
1485
|
+
metadata={"agent_name": agent_name},
|
|
1486
|
+
)
|
|
1487
|
+
loop = host_component.get_async_loop()
|
|
1488
|
+
if loop and loop.is_running():
|
|
1489
|
+
asyncio.run_coroutine_threadsafe(
|
|
1490
|
+
host_component._publish_status_update_with_buffer_flush(
|
|
1491
|
+
status_update_event,
|
|
1492
|
+
a2a_context,
|
|
1493
|
+
skip_buffer_flush=False,
|
|
1494
|
+
),
|
|
1495
|
+
loop,
|
|
1496
|
+
)
|
|
1497
|
+
log.debug(
|
|
1498
|
+
"%s Scheduled LLM response status update with buffer flush (final_chunk=%s).",
|
|
1499
|
+
log_identifier,
|
|
1500
|
+
llm_response.turn_complete,
|
|
1501
|
+
)
|
|
1502
|
+
else:
|
|
1503
|
+
log.error(
|
|
1504
|
+
"%s Async loop not available. Cannot publish LLM response status update.",
|
|
1505
|
+
log_identifier,
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
except Exception as e:
|
|
1509
|
+
log.error(
|
|
1510
|
+
"%s Error during Solace LLM response notification: %s", log_identifier, e
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1513
|
+
return None
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
def notify_tool_invocation_start_callback(
|
|
1517
|
+
tool: BaseTool,
|
|
1518
|
+
args: Dict[str, Any],
|
|
1519
|
+
tool_context: ToolContext,
|
|
1520
|
+
host_component: "SamAgentComponent",
|
|
1521
|
+
) -> None:
|
|
1522
|
+
"""
|
|
1523
|
+
ADK before_tool_callback to send an A2A status message indicating
|
|
1524
|
+
that a tool is about to be invoked.
|
|
1525
|
+
"""
|
|
1526
|
+
log_identifier = f"[Callback:NotifyToolInvocationStart:{tool.name}]"
|
|
1527
|
+
log.debug(
|
|
1528
|
+
"%s Triggered for tool '%s' with args: %s", log_identifier, tool.name, args
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
if not host_component:
|
|
1532
|
+
log.error(
|
|
1533
|
+
"%s Host component instance not provided. Cannot send notification.",
|
|
1534
|
+
log_identifier,
|
|
1535
|
+
)
|
|
1536
|
+
return
|
|
1537
|
+
|
|
1538
|
+
a2a_context = tool_context.state.get("a2a_context")
|
|
1539
|
+
if not a2a_context:
|
|
1540
|
+
log.error(
|
|
1541
|
+
"%s a2a_context not found in tool_context.state. Cannot send notification.",
|
|
1542
|
+
log_identifier,
|
|
1543
|
+
)
|
|
1544
|
+
return
|
|
1545
|
+
|
|
1546
|
+
logical_task_id = a2a_context.get("logical_task_id")
|
|
1547
|
+
|
|
1548
|
+
try:
|
|
1549
|
+
serializable_args = {}
|
|
1550
|
+
for k, v in args.items():
|
|
1551
|
+
try:
|
|
1552
|
+
json.dumps(v)
|
|
1553
|
+
serializable_args[k] = v
|
|
1554
|
+
except TypeError:
|
|
1555
|
+
serializable_args[k] = str(v)
|
|
1556
|
+
|
|
1557
|
+
a2a_message_parts = []
|
|
1558
|
+
message_metadata = {
|
|
1559
|
+
"type": "tool_invocation_start",
|
|
1560
|
+
"data": {
|
|
1561
|
+
"tool_name": tool.name,
|
|
1562
|
+
"tool_args": serializable_args,
|
|
1563
|
+
"function_call_id": tool_context.function_call_id,
|
|
1564
|
+
},
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
a2a_message = A2AMessage(
|
|
1568
|
+
role="agent", parts=a2a_message_parts, metadata=message_metadata
|
|
1569
|
+
)
|
|
1570
|
+
|
|
1571
|
+
task_status = TaskStatus(
|
|
1572
|
+
state=TaskState.WORKING,
|
|
1573
|
+
message=a2a_message,
|
|
1574
|
+
timestamp=datetime.now(timezone.utc),
|
|
1575
|
+
)
|
|
1576
|
+
|
|
1577
|
+
status_update_event = TaskStatusUpdateEvent(
|
|
1578
|
+
id=logical_task_id,
|
|
1579
|
+
status=task_status,
|
|
1580
|
+
final=False,
|
|
1581
|
+
metadata={"agent_name": host_component.get_config("agent_name")},
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
loop = host_component.get_async_loop()
|
|
1585
|
+
if loop and loop.is_running():
|
|
1586
|
+
|
|
1587
|
+
asyncio.run_coroutine_threadsafe(
|
|
1588
|
+
host_component._publish_status_update_with_buffer_flush(
|
|
1589
|
+
status_update_event,
|
|
1590
|
+
a2a_context,
|
|
1591
|
+
skip_buffer_flush=False,
|
|
1592
|
+
),
|
|
1593
|
+
loop,
|
|
1594
|
+
)
|
|
1595
|
+
log.debug(
|
|
1596
|
+
"%s Scheduled tool_invocation_start notification with buffer flush.",
|
|
1597
|
+
log_identifier,
|
|
1598
|
+
)
|
|
1599
|
+
else:
|
|
1600
|
+
log.error(
|
|
1601
|
+
"%s Async loop not available. Cannot publish tool_invocation_start notification.",
|
|
1602
|
+
log_identifier,
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
except Exception as e:
|
|
1606
|
+
log.exception(
|
|
1607
|
+
"%s Error publishing tool_invocation_start status update: %s",
|
|
1608
|
+
log_identifier,
|
|
1609
|
+
e,
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
return None
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
def auto_continue_on_max_tokens_callback(
|
|
1616
|
+
callback_context: CallbackContext,
|
|
1617
|
+
llm_response: LlmResponse,
|
|
1618
|
+
host_component: "SamAgentComponent",
|
|
1619
|
+
) -> Optional[LlmResponse]:
|
|
1620
|
+
"""
|
|
1621
|
+
ADK after_model_callback to automatically continue an LLM response that
|
|
1622
|
+
was interrupted. This handles two interruption signals:
|
|
1623
|
+
1. The explicit `llm_response.interrupted` flag from the ADK.
|
|
1624
|
+
2. An implicit signal where the model itself calls a `_continue` tool.
|
|
1625
|
+
"""
|
|
1626
|
+
log_identifier = "[Callback:AutoContinue]"
|
|
1627
|
+
|
|
1628
|
+
if not host_component.get_config("enable_auto_continuation", True):
|
|
1629
|
+
log.debug("%s Auto-continuation is disabled. Skipping.", log_identifier)
|
|
1630
|
+
return None
|
|
1631
|
+
|
|
1632
|
+
# An interruption is signaled by either the explicit flag or an implicit tool call.
|
|
1633
|
+
was_explicitly_interrupted = llm_response.interrupted
|
|
1634
|
+
was_implicitly_interrupted = False
|
|
1635
|
+
if llm_response.content and llm_response.content.parts:
|
|
1636
|
+
if any(
|
|
1637
|
+
p.function_call and p.function_call.name == "_continue"
|
|
1638
|
+
for p in llm_response.content.parts
|
|
1639
|
+
):
|
|
1640
|
+
was_implicitly_interrupted = True
|
|
1641
|
+
|
|
1642
|
+
if not was_explicitly_interrupted and not was_implicitly_interrupted:
|
|
1643
|
+
return None
|
|
1644
|
+
|
|
1645
|
+
log.info(
|
|
1646
|
+
"%s Interruption signal detected (explicit: %s, implicit: %s). Triggering auto-continuation.",
|
|
1647
|
+
log_identifier,
|
|
1648
|
+
was_explicitly_interrupted,
|
|
1649
|
+
was_implicitly_interrupted,
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
# Get existing parts from the response, but filter out any `_continue` calls
|
|
1653
|
+
# the model might have added.
|
|
1654
|
+
existing_parts = []
|
|
1655
|
+
if llm_response.content and llm_response.content.parts:
|
|
1656
|
+
existing_parts = [
|
|
1657
|
+
p
|
|
1658
|
+
for p in llm_response.content.parts
|
|
1659
|
+
if not (p.function_call and p.function_call.name == "_continue")
|
|
1660
|
+
]
|
|
1661
|
+
if was_implicitly_interrupted:
|
|
1662
|
+
log.debug(
|
|
1663
|
+
"%s Removed implicit '_continue' tool call from response parts.",
|
|
1664
|
+
log_identifier,
|
|
1665
|
+
)
|
|
1666
|
+
|
|
1667
|
+
continue_tool_call = adk_types.FunctionCall(
|
|
1668
|
+
name="_continue_generation",
|
|
1669
|
+
args={},
|
|
1670
|
+
id=f"host-continue-{uuid.uuid4()}",
|
|
1671
|
+
)
|
|
1672
|
+
continue_part = adk_types.Part(function_call=continue_tool_call)
|
|
1673
|
+
|
|
1674
|
+
all_parts = existing_parts + [continue_part]
|
|
1675
|
+
|
|
1676
|
+
# If there was no text content in the interrupted part, add a space to ensure
|
|
1677
|
+
# the event is not filtered out by history processing logic.
|
|
1678
|
+
if not any(p.text for p in existing_parts):
|
|
1679
|
+
all_parts.insert(0, adk_types.Part(text=" "))
|
|
1680
|
+
log.debug(
|
|
1681
|
+
"%s Prepended empty text part to ensure event is preserved.", log_identifier
|
|
1682
|
+
)
|
|
1683
|
+
|
|
1684
|
+
# Create a new, non-interrupted LlmResponse containing all parts.
|
|
1685
|
+
# This ensures the partial text is saved to history and the tool call is executed.
|
|
1686
|
+
hijacked_response = LlmResponse(
|
|
1687
|
+
content=adk_types.Content(role="model", parts=all_parts),
|
|
1688
|
+
partial=False,
|
|
1689
|
+
custom_metadata={
|
|
1690
|
+
"was_interrupted": True,
|
|
1691
|
+
},
|
|
1692
|
+
)
|
|
1693
|
+
|
|
1694
|
+
return hijacked_response
|