solace-agent-mesh 1.11.2__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.
- solace_agent_mesh/__init__.py +0 -0
- solace_agent_mesh/agent/__init__.py +0 -0
- solace_agent_mesh/agent/adk/__init__.py +0 -0
- solace_agent_mesh/agent/adk/adk_llm.txt +226 -0
- solace_agent_mesh/agent/adk/adk_llm_detail.txt +566 -0
- solace_agent_mesh/agent/adk/alembic/README +74 -0
- solace_agent_mesh/agent/adk/alembic/env.py +77 -0
- solace_agent_mesh/agent/adk/alembic/script.py.mako +28 -0
- solace_agent_mesh/agent/adk/alembic/versions/e2902798564d_adk_session_db_upgrade.py +52 -0
- solace_agent_mesh/agent/adk/alembic.ini +112 -0
- solace_agent_mesh/agent/adk/app_llm_agent.py +52 -0
- solace_agent_mesh/agent/adk/artifacts/__init__.py +1 -0
- solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +171 -0
- solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +545 -0
- solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +609 -0
- solace_agent_mesh/agent/adk/callbacks.py +2318 -0
- solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +406 -0
- solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +415 -0
- solace_agent_mesh/agent/adk/mcp_content_processor.py +666 -0
- solace_agent_mesh/agent/adk/models/lite_llm.py +1026 -0
- solace_agent_mesh/agent/adk/models/models_llm.txt +189 -0
- solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +132 -0
- solace_agent_mesh/agent/adk/runner.py +390 -0
- solace_agent_mesh/agent/adk/schema_migration.py +88 -0
- solace_agent_mesh/agent/adk/services.py +468 -0
- solace_agent_mesh/agent/adk/setup.py +1325 -0
- solace_agent_mesh/agent/adk/stream_parser.py +415 -0
- solace_agent_mesh/agent/adk/tool_wrapper.py +165 -0
- solace_agent_mesh/agent/agent_llm.txt +369 -0
- solace_agent_mesh/agent/agent_llm_detail.txt +1702 -0
- solace_agent_mesh/agent/protocol/__init__.py +0 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +2041 -0
- solace_agent_mesh/agent/protocol/protocol_llm.txt +81 -0
- solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +92 -0
- solace_agent_mesh/agent/proxies/__init__.py +0 -0
- solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
- solace_agent_mesh/agent/proxies/a2a/a2a_llm.txt +190 -0
- solace_agent_mesh/agent/proxies/a2a/app.py +56 -0
- solace_agent_mesh/agent/proxies/a2a/component.py +1585 -0
- solace_agent_mesh/agent/proxies/a2a/config.py +216 -0
- solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
- solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
- solace_agent_mesh/agent/proxies/base/app.py +100 -0
- solace_agent_mesh/agent/proxies/base/base_llm.txt +148 -0
- solace_agent_mesh/agent/proxies/base/component.py +816 -0
- solace_agent_mesh/agent/proxies/base/config.py +85 -0
- solace_agent_mesh/agent/proxies/base/proxy_task_context.py +19 -0
- solace_agent_mesh/agent/proxies/proxies_llm.txt +283 -0
- solace_agent_mesh/agent/sac/__init__.py +0 -0
- solace_agent_mesh/agent/sac/app.py +595 -0
- solace_agent_mesh/agent/sac/component.py +3668 -0
- solace_agent_mesh/agent/sac/patch_adk.py +103 -0
- solace_agent_mesh/agent/sac/sac_llm.txt +189 -0
- solace_agent_mesh/agent/sac/sac_llm_detail.txt +200 -0
- solace_agent_mesh/agent/sac/task_execution_context.py +415 -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 +58 -0
- solace_agent_mesh/agent/testing/testing_llm_detail.txt +68 -0
- solace_agent_mesh/agent/tools/__init__.py +16 -0
- solace_agent_mesh/agent/tools/audio_tools.py +1740 -0
- solace_agent_mesh/agent/tools/builtin_artifact_tools.py +2500 -0
- solace_agent_mesh/agent/tools/builtin_data_analysis_tools.py +244 -0
- solace_agent_mesh/agent/tools/dynamic_tool.py +396 -0
- solace_agent_mesh/agent/tools/general_agent_tools.py +572 -0
- solace_agent_mesh/agent/tools/image_tools.py +1185 -0
- solace_agent_mesh/agent/tools/peer_agent_tool.py +363 -0
- solace_agent_mesh/agent/tools/registry.py +38 -0
- solace_agent_mesh/agent/tools/test_tools.py +136 -0
- solace_agent_mesh/agent/tools/time_tools.py +126 -0
- solace_agent_mesh/agent/tools/tool_config_types.py +93 -0
- solace_agent_mesh/agent/tools/tool_definition.py +53 -0
- solace_agent_mesh/agent/tools/tools_llm.txt +276 -0
- solace_agent_mesh/agent/tools/tools_llm_detail.txt +275 -0
- solace_agent_mesh/agent/tools/web_tools.py +392 -0
- solace_agent_mesh/agent/utils/__init__.py +0 -0
- solace_agent_mesh/agent/utils/artifact_helpers.py +1353 -0
- solace_agent_mesh/agent/utils/config_parser.py +49 -0
- solace_agent_mesh/agent/utils/context_helpers.py +77 -0
- solace_agent_mesh/agent/utils/utils_llm.txt +152 -0
- solace_agent_mesh/agent/utils/utils_llm_detail.txt +149 -0
- solace_agent_mesh/assets/docs/404.html +16 -0
- solace_agent_mesh/assets/docs/assets/css/styles.8162edfb.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/sam-enterprise-credentials-b269f095349473118b2b33bdfcc40122.png +0 -0
- solace_agent_mesh/assets/docs/assets/js/032c2d61.f3d37824.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/05749d90.19ac4f35.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/0bcf40b7.c019ad46.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1001.0182a8bd.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/15ba94aa.92fea363.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15e40e79.434bb30f.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/17896441.e612dfb4.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2130.ab9fd314.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2131ec11.5c7a1f6e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2237.5e477fc6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js.LICENSE.txt +13 -0
- solace_agent_mesh/assets/docs/assets/js/2334.1cf50a20.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/240a0364.9ad94d1b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2e32b5e0.33f5d75b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3219.adc1d663.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/341393d4.0fac2613.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3624.0eaa1fd0.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/3a6c6137.f5940cfa.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ac1795d.28b7c67b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ff0015d.2ddc75c0.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/41adc471.48b12a4e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4250.95455b28.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4356.d169ab5b.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/509e993c.a1fbf45a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5388.7a136447.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/547e15cc.2f7790c1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/55b7b518.29d6e75d.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/5c2bd65f.90a87880.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5e95c892.558d5167.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6063ff4c.ef84f702.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/60702c0e.a8bdd79b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6143.0a1464c9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6395.e9c73649.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/64195356.c498c4d0.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/66d4869e.b77431fc.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/6a520c9d.b6e3f2ce.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6aaedf65.7253541d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6d84eae0.fd23ba4a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6fdfefc7.99de744e.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/71da7b71.374b9d54.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/722f809d.965da774.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7280.3fb73bdb.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/742f027b.46c07808.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.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/8024126c.fa0e7186.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/81a99df0.2484b8d9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/82fbfb93.161823a5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8356.8a379c04.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.5d015485.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/8591.5d015485.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.6c1dbf0c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8908.f9d1b506.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9157.b4093d07.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.975e428a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9278.a4fd875d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/945fb41e.6f4cdffd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/94e8668d.16083b3f.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/9bb13469.b2333011.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a7bd4aaa.2204d2f7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a94703ab.3e5fbcb3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ab9708a8.245ae0ef.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/aba21aa0.c42a534c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ad71b5ed.af3ecfd1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/c198a0dc.8f31f867.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/c93cbaa0.0e0d8baf.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cab03b5b.6a073091.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.07e170dd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/db5d6442.3daf1696.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/dd817ffc.c37a755e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/dd81e2b8.b682e9c2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/de5f4c65.e8241890.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/de915948.44a432bc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e04b235d.52cb25ed.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e1b6eeb4.b1068f9b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e3d9abda.1476f570.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.4488e34c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e92d0134.3bda61dd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.250993bf.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.7acf7ace.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/main.7acf7ace.js.LICENSE.txt +81 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.9e0813a2.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +154 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +99 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +90 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +107 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +166 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +101 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +219 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +92 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +29 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +55 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +110 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +182 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/prompts/index.html +147 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +345 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/speech/index.html +52 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +83 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +84 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +25 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes-deployment/index.html +47 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +85 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +60 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/proxy_configuration/index.html +49 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +144 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +191 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +128 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +54 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +34 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +55 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +267 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +142 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +116 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +86 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +164 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +140 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +57 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +72 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +102 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/teams-integration/index.html +115 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +86 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +67 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +37 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +86 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/openapi-tools/index.html +324 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +247 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +440 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +184 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +62 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +75 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +54 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +85 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +41 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +290 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +78 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +25 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +78 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +160 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +142 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +251 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +88 -0
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +100 -0
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +52 -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/sam-enterprise-credentials.png +0 -0
- solace_agent_mesh/assets/docs/img/solace-logo-text.svg +18 -0
- solace_agent_mesh/assets/docs/img/solace-logo.png +0 -0
- solace_agent_mesh/assets/docs/lunr-index-1765810064709.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -0
- solace_agent_mesh/assets/docs/search-doc-1765810064709.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 -0
- solace_agent_mesh/cli/commands/__init__.py +0 -0
- 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 +729 -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 +102 -0
- solace_agent_mesh/cli/commands/add_cmd/web_add_gateway_step.py +114 -0
- solace_agent_mesh/cli/commands/docs_cmd.py +60 -0
- solace_agent_mesh/cli/commands/eval_cmd.py +46 -0
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +439 -0
- solace_agent_mesh/cli/commands/init_cmd/broker_step.py +201 -0
- solace_agent_mesh/cli/commands/init_cmd/database_step.py +91 -0
- solace_agent_mesh/cli/commands/init_cmd/directory_step.py +28 -0
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +238 -0
- solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +365 -0
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +464 -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 +119 -0
- solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +215 -0
- solace_agent_mesh/cli/commands/plugin_cmd/__init__.py +20 -0
- solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +137 -0
- solace_agent_mesh/cli/commands/plugin_cmd/build_cmd.py +86 -0
- solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +144 -0
- solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +306 -0
- solace_agent_mesh/cli/commands/plugin_cmd/install_cmd.py +283 -0
- solace_agent_mesh/cli/commands/plugin_cmd/official_registry.py +175 -0
- solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +305 -0
- solace_agent_mesh/cli/commands/run_cmd.py +215 -0
- solace_agent_mesh/cli/main.py +52 -0
- solace_agent_mesh/cli/utils.py +262 -0
- solace_agent_mesh/client/webui/frontend/static/assets/authCallback-Dj3JtK42.js +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/client-ZKk9kEJ5.js +25 -0
- solace_agent_mesh/client/webui/frontend/static/assets/favicon-BLgzUch9.ico +0 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-BcUaNZ-Q.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-vjch4RYc.js +435 -0
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-BNV4kZN0.js +535 -0
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +15 -0
- solace_agent_mesh/client/webui/frontend/static/index.html +16 -0
- solace_agent_mesh/client/webui/frontend/static/mockServiceWorker.js +336 -0
- solace_agent_mesh/client/webui/frontend/static/ui-version.json +6 -0
- solace_agent_mesh/common/__init__.py +1 -0
- solace_agent_mesh/common/a2a/__init__.py +241 -0
- solace_agent_mesh/common/a2a/a2a_llm.txt +175 -0
- solace_agent_mesh/common/a2a/a2a_llm_detail.txt +193 -0
- solace_agent_mesh/common/a2a/artifact.py +368 -0
- solace_agent_mesh/common/a2a/events.py +213 -0
- solace_agent_mesh/common/a2a/message.py +375 -0
- solace_agent_mesh/common/a2a/protocol.py +689 -0
- solace_agent_mesh/common/a2a/task.py +127 -0
- solace_agent_mesh/common/a2a/translation.py +655 -0
- solace_agent_mesh/common/a2a/types.py +55 -0
- solace_agent_mesh/common/a2a_spec/a2a.json +2576 -0
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +445 -0
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +736 -0
- solace_agent_mesh/common/a2a_spec/schemas/agent_progress_update.json +18 -0
- solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +48 -0
- solace_agent_mesh/common/a2a_spec/schemas/feedback_event.json +51 -0
- solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +41 -0
- solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +330 -0
- solace_agent_mesh/common/a2a_spec/schemas/tool_invocation_start.json +26 -0
- solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +48 -0
- solace_agent_mesh/common/agent_registry.py +122 -0
- solace_agent_mesh/common/common_llm.txt +230 -0
- solace_agent_mesh/common/common_llm_detail.txt +2562 -0
- solace_agent_mesh/common/constants.py +6 -0
- solace_agent_mesh/common/data_parts.py +150 -0
- solace_agent_mesh/common/exceptions.py +49 -0
- solace_agent_mesh/common/middleware/__init__.py +12 -0
- solace_agent_mesh/common/middleware/config_resolver.py +132 -0
- solace_agent_mesh/common/middleware/middleware_llm.txt +174 -0
- solace_agent_mesh/common/middleware/middleware_llm_detail.txt +185 -0
- solace_agent_mesh/common/middleware/registry.py +127 -0
- solace_agent_mesh/common/oauth/__init__.py +17 -0
- solace_agent_mesh/common/oauth/oauth_client.py +408 -0
- solace_agent_mesh/common/oauth/utils.py +50 -0
- solace_agent_mesh/common/sac/__init__.py +0 -0
- solace_agent_mesh/common/sac/sac_llm.txt +71 -0
- solace_agent_mesh/common/sac/sac_llm_detail.txt +82 -0
- solace_agent_mesh/common/sac/sam_component_base.py +730 -0
- solace_agent_mesh/common/sam_events/__init__.py +9 -0
- solace_agent_mesh/common/sam_events/event_service.py +208 -0
- solace_agent_mesh/common/sam_events/sam_events_llm.txt +104 -0
- solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +115 -0
- solace_agent_mesh/common/services/__init__.py +4 -0
- solace_agent_mesh/common/services/employee_service.py +164 -0
- solace_agent_mesh/common/services/identity_service.py +134 -0
- solace_agent_mesh/common/services/providers/__init__.py +4 -0
- solace_agent_mesh/common/services/providers/local_file_identity_service.py +151 -0
- solace_agent_mesh/common/services/providers/providers_llm.txt +81 -0
- solace_agent_mesh/common/services/services_llm.txt +368 -0
- solace_agent_mesh/common/services/services_llm_detail.txt +459 -0
- solace_agent_mesh/common/utils/__init__.py +7 -0
- solace_agent_mesh/common/utils/artifact_utils.py +31 -0
- solace_agent_mesh/common/utils/asyncio_macos_fix.py +88 -0
- solace_agent_mesh/common/utils/embeds/__init__.py +33 -0
- solace_agent_mesh/common/utils/embeds/constants.py +56 -0
- solace_agent_mesh/common/utils/embeds/converter.py +447 -0
- solace_agent_mesh/common/utils/embeds/embeds_llm.txt +220 -0
- solace_agent_mesh/common/utils/embeds/evaluators.py +395 -0
- solace_agent_mesh/common/utils/embeds/modifiers.py +793 -0
- solace_agent_mesh/common/utils/embeds/resolver.py +967 -0
- solace_agent_mesh/common/utils/embeds/types.py +23 -0
- solace_agent_mesh/common/utils/in_memory_cache.py +108 -0
- solace_agent_mesh/common/utils/initializer.py +52 -0
- solace_agent_mesh/common/utils/log_formatters.py +64 -0
- solace_agent_mesh/common/utils/message_utils.py +80 -0
- solace_agent_mesh/common/utils/mime_helpers.py +172 -0
- solace_agent_mesh/common/utils/push_notification_auth.py +135 -0
- solace_agent_mesh/common/utils/pydantic_utils.py +159 -0
- solace_agent_mesh/common/utils/rbac_utils.py +69 -0
- solace_agent_mesh/common/utils/templates/__init__.py +8 -0
- solace_agent_mesh/common/utils/templates/liquid_renderer.py +210 -0
- solace_agent_mesh/common/utils/templates/template_resolver.py +161 -0
- solace_agent_mesh/common/utils/type_utils.py +28 -0
- solace_agent_mesh/common/utils/utils_llm.txt +335 -0
- solace_agent_mesh/common/utils/utils_llm_detail.txt +572 -0
- solace_agent_mesh/config_portal/__init__.py +0 -0
- solace_agent_mesh/config_portal/backend/__init__.py +0 -0
- solace_agent_mesh/config_portal/backend/common.py +77 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/__init__.py +0 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/constants.py +24 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/models.py +49 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/registry_manager.py +166 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog/scraper.py +521 -0
- solace_agent_mesh/config_portal/backend/plugin_catalog_server.py +217 -0
- solace_agent_mesh/config_portal/backend/server.py +644 -0
- solace_agent_mesh/config_portal/frontend/static/client/Solace_community_logo.png +0 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-DiOiAjzL.js +103 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/components-Rk0n-9cK.js +140 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/entry.client-mvZjNKiz.js +19 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/index-DzNKzXrc.js +68 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-ba77705e.js +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-B17tZKK7.css +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-V2BeTIUc.js +10 -0
- solace_agent_mesh/config_portal/frontend/static/client/favicon.ico +0 -0
- solace_agent_mesh/config_portal/frontend/static/client/index.html +7 -0
- solace_agent_mesh/core_a2a/__init__.py +1 -0
- solace_agent_mesh/core_a2a/core_a2a_llm.txt +90 -0
- solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +101 -0
- solace_agent_mesh/core_a2a/service.py +307 -0
- solace_agent_mesh/evaluation/__init__.py +0 -0
- solace_agent_mesh/evaluation/evaluator.py +691 -0
- solace_agent_mesh/evaluation/message_organizer.py +553 -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 +970 -0
- solace_agent_mesh/evaluation/report_generator.py +607 -0
- solace_agent_mesh/evaluation/run.py +954 -0
- solace_agent_mesh/evaluation/shared/__init__.py +92 -0
- solace_agent_mesh/evaluation/shared/constants.py +47 -0
- solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
- solace_agent_mesh/evaluation/shared/helpers.py +35 -0
- solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
- solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
- solace_agent_mesh/evaluation/subscriber.py +776 -0
- solace_agent_mesh/evaluation/summary_builder.py +880 -0
- solace_agent_mesh/gateway/__init__.py +0 -0
- solace_agent_mesh/gateway/adapter/__init__.py +1 -0
- solace_agent_mesh/gateway/adapter/base.py +143 -0
- solace_agent_mesh/gateway/adapter/types.py +221 -0
- solace_agent_mesh/gateway/base/__init__.py +1 -0
- solace_agent_mesh/gateway/base/app.py +345 -0
- solace_agent_mesh/gateway/base/base_llm.txt +226 -0
- solace_agent_mesh/gateway/base/base_llm_detail.txt +235 -0
- solace_agent_mesh/gateway/base/component.py +2030 -0
- solace_agent_mesh/gateway/base/task_context.py +75 -0
- solace_agent_mesh/gateway/gateway_llm.txt +369 -0
- solace_agent_mesh/gateway/gateway_llm_detail.txt +3885 -0
- solace_agent_mesh/gateway/generic/__init__.py +1 -0
- solace_agent_mesh/gateway/generic/app.py +50 -0
- solace_agent_mesh/gateway/generic/component.py +727 -0
- solace_agent_mesh/gateway/http_sse/__init__.py +0 -0
- solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +345 -0
- solace_agent_mesh/gateway/http_sse/alembic/env.py +87 -0
- solace_agent_mesh/gateway/http_sse/alembic/script.py.mako +28 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20250910_d5b3f8f2e9a0_create_initial_database.py +58 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20250911_b1c2d3e4f5g6_add_database_indexes.py +83 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20250916_f6e7d8c9b0a1_convert_timestamps_to_epoch_and_align_columns.py +412 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251006_98882922fa59_add_tasks_events_feedback_chat_tasks.py +190 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_project_users_table.py +72 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +109 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +26 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_projects_table.py +135 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251108_create_prompt_tables_with_sharing.py +154 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251115_add_parent_task_id.py +32 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251126_add_background_task_fields.py +47 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251202_add_versioned_fields_to_prompts.py +52 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +161 -0
- solace_agent_mesh/gateway/http_sse/alembic.ini +109 -0
- solace_agent_mesh/gateway/http_sse/app.py +351 -0
- solace_agent_mesh/gateway/http_sse/component.py +2360 -0
- solace_agent_mesh/gateway/http_sse/components/__init__.py +7 -0
- solace_agent_mesh/gateway/http_sse/components/components_llm.txt +105 -0
- solace_agent_mesh/gateway/http_sse/components/task_logger_forwarder.py +109 -0
- solace_agent_mesh/gateway/http_sse/components/visualization_forwarder_component.py +110 -0
- solace_agent_mesh/gateway/http_sse/dependencies.py +653 -0
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +299 -0
- solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +3278 -0
- solace_agent_mesh/gateway/http_sse/main.py +789 -0
- solace_agent_mesh/gateway/http_sse/repository/__init__.py +46 -0
- solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +102 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/__init__.py +11 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/chat_task.py +75 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +221 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/feedback.py +20 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/project.py +81 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +47 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session.py +66 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session_history.py +0 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/task.py +32 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/task_event.py +21 -0
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +125 -0
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +239 -0
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +34 -0
- solace_agent_mesh/gateway/http_sse/repository/models/base.py +7 -0
- solace_agent_mesh/gateway/http_sse/repository/models/chat_task_model.py +31 -0
- solace_agent_mesh/gateway/http_sse/repository/models/feedback_model.py +21 -0
- solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +257 -0
- solace_agent_mesh/gateway/http_sse/repository/models/project_model.py +51 -0
- solace_agent_mesh/gateway/http_sse/repository/models/project_user_model.py +75 -0
- solace_agent_mesh/gateway/http_sse/repository/models/prompt_model.py +159 -0
- solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +53 -0
- solace_agent_mesh/gateway/http_sse/repository/models/task_event_model.py +25 -0
- solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +39 -0
- solace_agent_mesh/gateway/http_sse/repository/project_repository.py +172 -0
- solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +186 -0
- solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +308 -0
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +268 -0
- solace_agent_mesh/gateway/http_sse/repository/task_repository.py +248 -0
- solace_agent_mesh/gateway/http_sse/routers/__init__.py +4 -0
- solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +74 -0
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +1137 -0
- solace_agent_mesh/gateway/http_sse/routers/auth.py +311 -0
- solace_agent_mesh/gateway/http_sse/routers/config.py +371 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/__init__.py +10 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +450 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/project_dto.py +69 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/prompt_dto.py +255 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +15 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +133 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +33 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/task_requests.py +58 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/__init__.py +18 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +42 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +31 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +123 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +33 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/task_responses.py +30 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/version_responses.py +31 -0
- solace_agent_mesh/gateway/http_sse/routers/feedback.py +168 -0
- solace_agent_mesh/gateway/http_sse/routers/people.py +38 -0
- solace_agent_mesh/gateway/http_sse/routers/projects.py +767 -0
- solace_agent_mesh/gateway/http_sse/routers/prompts.py +1415 -0
- solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +312 -0
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +634 -0
- solace_agent_mesh/gateway/http_sse/routers/speech.py +355 -0
- solace_agent_mesh/gateway/http_sse/routers/sse.py +230 -0
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +1089 -0
- solace_agent_mesh/gateway/http_sse/routers/users.py +83 -0
- solace_agent_mesh/gateway/http_sse/routers/version.py +343 -0
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +1220 -0
- solace_agent_mesh/gateway/http_sse/services/__init__.py +4 -0
- solace_agent_mesh/gateway/http_sse/services/agent_card_service.py +71 -0
- solace_agent_mesh/gateway/http_sse/services/audio_service.py +1227 -0
- solace_agent_mesh/gateway/http_sse/services/background_task_monitor.py +186 -0
- solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +273 -0
- solace_agent_mesh/gateway/http_sse/services/feedback_service.py +250 -0
- solace_agent_mesh/gateway/http_sse/services/people_service.py +78 -0
- solace_agent_mesh/gateway/http_sse/services/project_service.py +930 -0
- solace_agent_mesh/gateway/http_sse/services/prompt_builder_assistant.py +303 -0
- solace_agent_mesh/gateway/http_sse/services/services_llm.txt +303 -0
- solace_agent_mesh/gateway/http_sse/services/session_service.py +702 -0
- solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +593 -0
- solace_agent_mesh/gateway/http_sse/services/task_service.py +119 -0
- solace_agent_mesh/gateway/http_sse/session_manager.py +219 -0
- solace_agent_mesh/gateway/http_sse/shared/__init__.py +146 -0
- solace_agent_mesh/gateway/http_sse/shared/auth_utils.py +29 -0
- solace_agent_mesh/gateway/http_sse/shared/base_repository.py +252 -0
- solace_agent_mesh/gateway/http_sse/shared/database_exceptions.py +274 -0
- solace_agent_mesh/gateway/http_sse/shared/database_helpers.py +43 -0
- solace_agent_mesh/gateway/http_sse/shared/enums.py +40 -0
- solace_agent_mesh/gateway/http_sse/shared/error_dto.py +107 -0
- solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +217 -0
- solace_agent_mesh/gateway/http_sse/shared/exceptions.py +192 -0
- solace_agent_mesh/gateway/http_sse/shared/pagination.py +138 -0
- solace_agent_mesh/gateway/http_sse/shared/response_utils.py +134 -0
- solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +319 -0
- solace_agent_mesh/gateway/http_sse/shared/timestamp_utils.py +97 -0
- solace_agent_mesh/gateway/http_sse/shared/types.py +50 -0
- solace_agent_mesh/gateway/http_sse/shared/utils.py +22 -0
- solace_agent_mesh/gateway/http_sse/sse_event_buffer.py +88 -0
- solace_agent_mesh/gateway/http_sse/sse_manager.py +491 -0
- solace_agent_mesh/gateway/http_sse/utils/__init__.py +1 -0
- solace_agent_mesh/gateway/http_sse/utils/artifact_copy_utils.py +370 -0
- solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +72 -0
- solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +47 -0
- solace_agent_mesh/llm.txt +228 -0
- solace_agent_mesh/llm_detail.txt +2835 -0
- solace_agent_mesh/services/__init__.py +0 -0
- solace_agent_mesh/services/platform/__init__.py +18 -0
- solace_agent_mesh/services/platform/alembic/env.py +85 -0
- solace_agent_mesh/services/platform/alembic/script.py.mako +28 -0
- solace_agent_mesh/services/platform/alembic.ini +109 -0
- solace_agent_mesh/services/platform/api/__init__.py +3 -0
- solace_agent_mesh/services/platform/api/dependencies.py +147 -0
- solace_agent_mesh/services/platform/api/main.py +280 -0
- solace_agent_mesh/services/platform/api/middleware.py +51 -0
- solace_agent_mesh/services/platform/api/routers/__init__.py +24 -0
- solace_agent_mesh/services/platform/app.py +114 -0
- solace_agent_mesh/services/platform/component.py +235 -0
- solace_agent_mesh/solace_agent_mesh_llm.txt +362 -0
- solace_agent_mesh/solace_agent_mesh_llm_detail.txt +8599 -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 +75 -0
- solace_agent_mesh/templates/gateway_component_template.py +484 -0
- solace_agent_mesh/templates/gateway_config_template.yaml +38 -0
- solace_agent_mesh/templates/logging_config_template.yaml +48 -0
- solace_agent_mesh/templates/main_orchestrator.yaml +66 -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 +60 -0
- solace_agent_mesh/templates/plugin_pyproject_template.toml +32 -0
- solace_agent_mesh/templates/plugin_readme_template.md +12 -0
- solace_agent_mesh/templates/plugin_tool_config_template.yaml +109 -0
- solace_agent_mesh/templates/plugin_tools_template.py +224 -0
- solace_agent_mesh/templates/shared_config.yaml +112 -0
- solace_agent_mesh/templates/templates_llm.txt +147 -0
- solace_agent_mesh/templates/webui.yaml +177 -0
- solace_agent_mesh-1.11.2.dist-info/METADATA +504 -0
- solace_agent_mesh-1.11.2.dist-info/RECORD +624 -0
- solace_agent_mesh-1.11.2.dist-info/WHEEL +4 -0
- solace_agent_mesh-1.11.2.dist-info/entry_points.txt +3 -0
- solace_agent_mesh-1.11.2.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,1585 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concrete implementation of a proxy for standard A2A-over-HTTPS agents.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from a2a.client import (
|
|
15
|
+
A2ACardResolver,
|
|
16
|
+
Client,
|
|
17
|
+
ClientConfig,
|
|
18
|
+
ClientFactory,
|
|
19
|
+
A2AClientHTTPError,
|
|
20
|
+
AuthInterceptor,
|
|
21
|
+
InMemoryContextCredentialStore,
|
|
22
|
+
)
|
|
23
|
+
from a2a.client.errors import A2AClientJSONRPCError
|
|
24
|
+
from .oauth_token_cache import OAuth2TokenCache
|
|
25
|
+
from a2a.types import (
|
|
26
|
+
A2ARequest,
|
|
27
|
+
AgentCard,
|
|
28
|
+
Artifact,
|
|
29
|
+
CancelTaskRequest,
|
|
30
|
+
DataPart,
|
|
31
|
+
Message,
|
|
32
|
+
SendMessageRequest,
|
|
33
|
+
SendStreamingMessageRequest,
|
|
34
|
+
Task,
|
|
35
|
+
TaskArtifactUpdateEvent,
|
|
36
|
+
TaskState,
|
|
37
|
+
TaskStatus,
|
|
38
|
+
TaskStatusUpdateEvent,
|
|
39
|
+
TextPart,
|
|
40
|
+
TransportProtocol,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
from solace_ai_connector.common.log import log
|
|
44
|
+
|
|
45
|
+
from datetime import datetime, timezone
|
|
46
|
+
|
|
47
|
+
from ....common import a2a
|
|
48
|
+
from ....common.oauth import OAuth2Client, validate_https_url
|
|
49
|
+
from ....common.data_parts import AgentProgressUpdateData
|
|
50
|
+
from ....agent.utils.artifact_helpers import format_artifact_uri
|
|
51
|
+
from ..base.component import BaseProxyComponent
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from ..base.proxy_task_context import ProxyTaskContext
|
|
55
|
+
|
|
56
|
+
info = {
|
|
57
|
+
"class_name": "A2AProxyComponent",
|
|
58
|
+
"description": "A proxy for standard A2A-over-HTTPS agents.",
|
|
59
|
+
"config_parameters": [],
|
|
60
|
+
"input_schema": {},
|
|
61
|
+
"output_schema": {},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class A2AProxyComponent(BaseProxyComponent):
|
|
66
|
+
"""
|
|
67
|
+
Concrete proxy component for standard A2A-over-HTTPS agents.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, **kwargs: Any):
|
|
71
|
+
super().__init__(**kwargs)
|
|
72
|
+
# Cache Client instances per (agent_name, session_id, is_streaming) to ensure
|
|
73
|
+
# each session gets its own client with session-specific credentials and streaming mode
|
|
74
|
+
self._a2a_clients: Dict[Tuple[str, str, bool], Client] = {}
|
|
75
|
+
self._credential_store: InMemoryContextCredentialStore = (
|
|
76
|
+
InMemoryContextCredentialStore()
|
|
77
|
+
)
|
|
78
|
+
self._auth_interceptor: AuthInterceptor = AuthInterceptor(
|
|
79
|
+
self._credential_store
|
|
80
|
+
)
|
|
81
|
+
# OAuth 2.0 token cache for client credentials flow
|
|
82
|
+
# Why use asyncio.Lock: Ensures thread-safe access to the token cache
|
|
83
|
+
# when multiple concurrent requests target the same agent
|
|
84
|
+
self._oauth_token_cache: OAuth2TokenCache = OAuth2TokenCache()
|
|
85
|
+
|
|
86
|
+
# OAuth 2.0 client for protocol operations (no retry for A2A)
|
|
87
|
+
self._oauth_client = OAuth2Client()
|
|
88
|
+
|
|
89
|
+
# Index agent configs by name for O(1) lookup (performance optimization)
|
|
90
|
+
self._agent_config_by_name: Dict[str, Dict[str, Any]] = {
|
|
91
|
+
agent["name"]: agent for agent in self.proxied_agents_config
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# NEW: OAuth 2.0 authorization code support (enterprise feature)
|
|
95
|
+
# Stores paused tasks waiting for user authorization
|
|
96
|
+
self._paused_a2a_oauth2_tasks: Dict[str, Dict[str, Any]] = {}
|
|
97
|
+
# Caches CredentialManagerWithDiscovery instances per agent
|
|
98
|
+
self._a2a_oauth2_credential_managers: Dict[str, Any] = {}
|
|
99
|
+
|
|
100
|
+
# NEW: Initialize enterprise features for OAuth2 support
|
|
101
|
+
try:
|
|
102
|
+
from solace_agent_mesh_enterprise.init_enterprise_component import (
|
|
103
|
+
init_enterprise_proxy_features
|
|
104
|
+
)
|
|
105
|
+
init_enterprise_proxy_features(self)
|
|
106
|
+
except ImportError:
|
|
107
|
+
pass # Enterprise not installed
|
|
108
|
+
|
|
109
|
+
# OAuth 2.0 configuration is now validated by Pydantic models at app initialization
|
|
110
|
+
# No need for separate _validate_oauth_config() method
|
|
111
|
+
|
|
112
|
+
def _get_agent_config(self, agent_name: str) -> Optional[Dict[str, Any]]:
|
|
113
|
+
"""
|
|
114
|
+
O(1) lookup of agent configuration by name.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
agent_name: The name of the agent to look up.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The agent configuration dictionary, or None if not found.
|
|
121
|
+
"""
|
|
122
|
+
return self._agent_config_by_name.get(agent_name)
|
|
123
|
+
|
|
124
|
+
async def _build_headers(
|
|
125
|
+
self,
|
|
126
|
+
agent_name: str,
|
|
127
|
+
agent_config: Dict[str, Any],
|
|
128
|
+
custom_headers_key: str,
|
|
129
|
+
use_auth: bool = True,
|
|
130
|
+
) -> Dict[str, str]:
|
|
131
|
+
"""
|
|
132
|
+
Builds HTTP headers for requests, applying authentication and custom headers.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
agent_name: The name of the agent.
|
|
136
|
+
agent_config: The agent configuration dictionary.
|
|
137
|
+
custom_headers_key: Key to look up custom headers in config ('agent_card_headers' or 'task_headers').
|
|
138
|
+
use_auth: Whether to apply authentication headers.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Dictionary of HTTP headers. Custom headers are applied after auth headers.
|
|
142
|
+
Note: For task invocations, the A2A SDK's AuthInterceptor may further
|
|
143
|
+
modify authentication headers after these are set.
|
|
144
|
+
"""
|
|
145
|
+
headers: Dict[str, str] = {}
|
|
146
|
+
|
|
147
|
+
# Step 1: Add authentication headers if requested
|
|
148
|
+
if use_auth:
|
|
149
|
+
auth_config = agent_config.get("authentication")
|
|
150
|
+
if auth_config:
|
|
151
|
+
auth_type = auth_config.get("type")
|
|
152
|
+
|
|
153
|
+
# Determine auth type (with backward compatibility)
|
|
154
|
+
if not auth_type:
|
|
155
|
+
scheme = auth_config.get("scheme", "bearer")
|
|
156
|
+
auth_type = "static_bearer" if scheme == "bearer" else "static_apikey"
|
|
157
|
+
|
|
158
|
+
# Apply authentication based on type
|
|
159
|
+
if auth_type == "static_bearer":
|
|
160
|
+
token = auth_config.get("token")
|
|
161
|
+
if token:
|
|
162
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
163
|
+
elif auth_type == "static_apikey":
|
|
164
|
+
token = auth_config.get("token")
|
|
165
|
+
if token:
|
|
166
|
+
headers["X-API-Key"] = token
|
|
167
|
+
elif auth_type == "oauth2_client_credentials":
|
|
168
|
+
# Fetch OAuth token
|
|
169
|
+
try:
|
|
170
|
+
access_token = await self._fetch_oauth2_token(agent_name, auth_config)
|
|
171
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
172
|
+
except Exception as e:
|
|
173
|
+
log.error(
|
|
174
|
+
"%s Failed to obtain OAuth 2.0 token for headers: %s",
|
|
175
|
+
self.log_identifier,
|
|
176
|
+
e,
|
|
177
|
+
)
|
|
178
|
+
# Continue without auth header - let the request fail downstream
|
|
179
|
+
|
|
180
|
+
# Step 2: Add custom headers (these override auth headers)
|
|
181
|
+
custom_headers_list = agent_config.get(custom_headers_key)
|
|
182
|
+
if custom_headers_list:
|
|
183
|
+
for header_config in custom_headers_list:
|
|
184
|
+
header_name = header_config.get("name")
|
|
185
|
+
header_value = header_config.get("value")
|
|
186
|
+
if header_name and header_value:
|
|
187
|
+
headers[header_name] = header_value
|
|
188
|
+
|
|
189
|
+
return headers
|
|
190
|
+
|
|
191
|
+
async def _fetch_agent_card(
|
|
192
|
+
self, agent_config: Dict[str, Any]
|
|
193
|
+
) -> Optional[AgentCard]:
|
|
194
|
+
"""
|
|
195
|
+
Fetches the AgentCard from a downstream A2A agent via HTTPS.
|
|
196
|
+
|
|
197
|
+
Applies authentication and custom headers based on configuration:
|
|
198
|
+
- If use_auth_for_agent_card=true, applies the configured authentication
|
|
199
|
+
- Custom agent_card_headers override authentication headers
|
|
200
|
+
"""
|
|
201
|
+
agent_name = agent_config.get("name")
|
|
202
|
+
agent_url = agent_config.get("url")
|
|
203
|
+
agent_card_path = agent_config.get("agent_card_path", "/agent/card.json")
|
|
204
|
+
log_identifier = f"{self.log_identifier}[FetchCard:{agent_name}]"
|
|
205
|
+
|
|
206
|
+
if not agent_url:
|
|
207
|
+
log.error("%s No URL configured for agent.", log_identifier)
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
# Build headers based on configuration
|
|
212
|
+
use_auth = agent_config.get("use_auth_for_agent_card", False)
|
|
213
|
+
headers = await self._build_headers(
|
|
214
|
+
agent_name=agent_name,
|
|
215
|
+
agent_config=agent_config,
|
|
216
|
+
custom_headers_key="agent_card_headers",
|
|
217
|
+
use_auth=use_auth,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if headers:
|
|
221
|
+
log.debug(
|
|
222
|
+
"%s Fetching agent card with %d custom header(s) (auth=%s)",
|
|
223
|
+
log_identifier,
|
|
224
|
+
len(headers),
|
|
225
|
+
use_auth,
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
log.debug("%s Fetching agent card without authentication", log_identifier)
|
|
229
|
+
|
|
230
|
+
log.info("%s Fetching agent card from %s", log_identifier, agent_url)
|
|
231
|
+
async with httpx.AsyncClient(headers=headers) as client:
|
|
232
|
+
resolver = A2ACardResolver(httpx_client=client, base_url=agent_url)
|
|
233
|
+
agent_card = await resolver.get_agent_card()
|
|
234
|
+
return agent_card
|
|
235
|
+
except A2AClientHTTPError as e:
|
|
236
|
+
log.error(
|
|
237
|
+
"%s HTTP error fetching agent card from %s: %s",
|
|
238
|
+
log_identifier,
|
|
239
|
+
agent_url,
|
|
240
|
+
e,
|
|
241
|
+
)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
log.exception(
|
|
244
|
+
"%s Unexpected error fetching agent card from %s: %s",
|
|
245
|
+
log_identifier,
|
|
246
|
+
agent_url,
|
|
247
|
+
e,
|
|
248
|
+
)
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
async def _forward_request(
|
|
252
|
+
self, task_context: ProxyTaskContext, request: A2ARequest, agent_name: str
|
|
253
|
+
) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Forwards an A2A request to a downstream A2A-over-HTTPS agent.
|
|
256
|
+
|
|
257
|
+
Implements automatic retry logic for OAuth 2.0 authentication failures.
|
|
258
|
+
If a 401 Unauthorized response is received and the agent uses OAuth 2.0,
|
|
259
|
+
the cached token is invalidated and the request is retried once with a
|
|
260
|
+
fresh token.
|
|
261
|
+
"""
|
|
262
|
+
log_identifier = (
|
|
263
|
+
f"{self.log_identifier}[ForwardRequest:{task_context.task_id}:{agent_name}]"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Store original request for potential resumption (OAuth2 authorization code flow)
|
|
267
|
+
task_context.original_request = request
|
|
268
|
+
|
|
269
|
+
# Step 1: Initialize retry counter
|
|
270
|
+
# Why only retry once: Prevents infinite loops on persistent auth failures.
|
|
271
|
+
# First 401 may be due to token expiration between cache check and request;
|
|
272
|
+
# second 401 indicates a configuration or authorization issue (not transient).
|
|
273
|
+
max_auth_retries: int = 1
|
|
274
|
+
auth_retry_count: int = 0
|
|
275
|
+
|
|
276
|
+
# Step 2: Check for OAuth2 authorization code flow
|
|
277
|
+
# This auth type requires user interaction and can pause the task,
|
|
278
|
+
# so we check it before attempting normal request flow
|
|
279
|
+
agent_config = self._get_agent_config(agent_name)
|
|
280
|
+
auth_config = agent_config.get("authentication") if agent_config else None
|
|
281
|
+
auth_type = auth_config.get("type") if auth_config else None
|
|
282
|
+
|
|
283
|
+
if auth_type == "oauth2_authorization_code":
|
|
284
|
+
try:
|
|
285
|
+
from solace_agent_mesh_enterprise.auth.a2a import (
|
|
286
|
+
check_authorization_required,
|
|
287
|
+
request_authorization,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Check if user authorization is needed
|
|
291
|
+
needs_auth = await check_authorization_required(
|
|
292
|
+
component=self,
|
|
293
|
+
agent_name=agent_name,
|
|
294
|
+
task_context=task_context,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if needs_auth:
|
|
298
|
+
# Pause task and request authorization
|
|
299
|
+
log.info(
|
|
300
|
+
"%s User authorization required for agent '%s'. Pausing task.",
|
|
301
|
+
log_identifier,
|
|
302
|
+
agent_name,
|
|
303
|
+
)
|
|
304
|
+
await request_authorization(
|
|
305
|
+
component=self,
|
|
306
|
+
agent_name=agent_name,
|
|
307
|
+
task_context=task_context,
|
|
308
|
+
)
|
|
309
|
+
return # Exit - task paused, will resume after OAuth callback
|
|
310
|
+
|
|
311
|
+
except ImportError:
|
|
312
|
+
log.error(
|
|
313
|
+
"%s Agent '%s' requires OAuth2 authorization code flow, "
|
|
314
|
+
"but solace-agent-mesh-enterprise is not installed.",
|
|
315
|
+
log_identifier,
|
|
316
|
+
agent_name,
|
|
317
|
+
)
|
|
318
|
+
raise ValueError(
|
|
319
|
+
f"Agent '{agent_name}' requires OAuth2 authorization code flow, "
|
|
320
|
+
"but solace-agent-mesh-enterprise is not installed."
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Step 3: Normal request flow for all other auth types
|
|
324
|
+
# (static_bearer, static_apikey, oauth2_client_credentials, or authorized oauth2_authorization_code)
|
|
325
|
+
while auth_retry_count <= max_auth_retries:
|
|
326
|
+
try:
|
|
327
|
+
# Get or create A2AClient
|
|
328
|
+
client = await self._get_or_create_a2a_client(agent_name, task_context)
|
|
329
|
+
if not client:
|
|
330
|
+
raise ValueError(
|
|
331
|
+
f"Could not create A2A client for agent '{agent_name}'"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Create context with sessionId (camelCase!) so AuthInterceptor can look up credentials
|
|
335
|
+
from a2a.client.middleware import ClientCallContext
|
|
336
|
+
|
|
337
|
+
session_id = task_context.a2a_context.get(
|
|
338
|
+
"session_id", "default_session"
|
|
339
|
+
)
|
|
340
|
+
call_context = ClientCallContext(state={"sessionId": session_id})
|
|
341
|
+
|
|
342
|
+
# Forward the request with context
|
|
343
|
+
if isinstance(
|
|
344
|
+
request, (SendStreamingMessageRequest, SendMessageRequest)
|
|
345
|
+
):
|
|
346
|
+
# Extract the Message from the request params
|
|
347
|
+
message_to_send = request.params.message
|
|
348
|
+
|
|
349
|
+
# Check if this is a RUN_BASED request by inspecting message metadata
|
|
350
|
+
# For RUN_BASED requests, omit context_id to indicate independent tasks
|
|
351
|
+
if message_to_send.metadata:
|
|
352
|
+
session_behavior = message_to_send.metadata.get("sessionBehavior")
|
|
353
|
+
if session_behavior:
|
|
354
|
+
session_behavior = str(session_behavior).upper()
|
|
355
|
+
if session_behavior == "RUN_BASED" and message_to_send.context_id:
|
|
356
|
+
# For RUN_BASED requests, omit context_id entirely
|
|
357
|
+
# Each request is independent with no logical grouping
|
|
358
|
+
log.debug(
|
|
359
|
+
"%s RUN_BASED request detected. Omitting context_id "
|
|
360
|
+
"(independent task)",
|
|
361
|
+
log_identifier,
|
|
362
|
+
)
|
|
363
|
+
message_to_send = message_to_send.model_copy(
|
|
364
|
+
update={"context_id": None}
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# WORKAROUND: The A2A SDK has a bug in ClientTaskManager that breaks streaming.
|
|
368
|
+
# For streaming requests, we bypass the Client.send_message() method and call
|
|
369
|
+
# the transport directly to avoid the buggy ClientTaskManager.
|
|
370
|
+
# Non-streaming requests work fine with the normal client method.
|
|
371
|
+
# TODO: Remove this workaround once SDK bug is fixed upstream.
|
|
372
|
+
if task_context.a2a_context.get("is_streaming", True):
|
|
373
|
+
# Access transport directly (private API) to bypass ClientTaskManager
|
|
374
|
+
log.debug(
|
|
375
|
+
"%s Using transport directly for streaming request (SDK bug workaround)",
|
|
376
|
+
log_identifier,
|
|
377
|
+
)
|
|
378
|
+
async for raw_event in client._transport.send_message_streaming(
|
|
379
|
+
request.params, context=call_context
|
|
380
|
+
):
|
|
381
|
+
# Process raw events directly without ClientTaskManager
|
|
382
|
+
await self._process_downstream_response(
|
|
383
|
+
raw_event, task_context, client, agent_name
|
|
384
|
+
)
|
|
385
|
+
else:
|
|
386
|
+
# Non-streaming: use normal client method (works fine)
|
|
387
|
+
log.debug(
|
|
388
|
+
"%s Using normal client method for non-streaming request",
|
|
389
|
+
log_identifier,
|
|
390
|
+
)
|
|
391
|
+
async for event in client.send_message(
|
|
392
|
+
message_to_send, context=call_context
|
|
393
|
+
):
|
|
394
|
+
await self._process_downstream_response(
|
|
395
|
+
event, task_context, client, agent_name
|
|
396
|
+
)
|
|
397
|
+
elif isinstance(request, CancelTaskRequest):
|
|
398
|
+
# Forward cancel request to downstream agent using the downstream task ID
|
|
399
|
+
# The request.params.id contains SAM's task ID, but we need to send
|
|
400
|
+
# the downstream agent's task ID for the cancel to work
|
|
401
|
+
|
|
402
|
+
if not task_context.downstream_task_id:
|
|
403
|
+
log.error(
|
|
404
|
+
"%s Cannot forward cancel request: downstream task ID not yet captured for SAM task %s",
|
|
405
|
+
log_identifier,
|
|
406
|
+
task_context.task_id,
|
|
407
|
+
)
|
|
408
|
+
# Create an error response
|
|
409
|
+
from a2a.types import InvalidRequestError
|
|
410
|
+
error = InvalidRequestError(
|
|
411
|
+
message=f"Cannot cancel task {task_context.task_id}: downstream task ID not available",
|
|
412
|
+
data={"taskId": task_context.task_id}
|
|
413
|
+
)
|
|
414
|
+
# Publish error response
|
|
415
|
+
await self._publish_error_response(error, task_context.a2a_context)
|
|
416
|
+
break
|
|
417
|
+
|
|
418
|
+
log.info(
|
|
419
|
+
"%s Forwarding cancel request for task %s (SAM ID: %s, downstream ID: %s) to downstream agent.",
|
|
420
|
+
log_identifier,
|
|
421
|
+
task_context.downstream_task_id,
|
|
422
|
+
task_context.task_id,
|
|
423
|
+
task_context.downstream_task_id,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Create new params with the downstream task ID
|
|
427
|
+
from a2a.types import TaskIdParams
|
|
428
|
+
downstream_params = TaskIdParams(id=task_context.downstream_task_id)
|
|
429
|
+
|
|
430
|
+
# Use the modern client's cancel_task method with the downstream task ID
|
|
431
|
+
result = await client.cancel_task(
|
|
432
|
+
downstream_params, context=call_context
|
|
433
|
+
)
|
|
434
|
+
# Publish the canceled task response
|
|
435
|
+
await self._publish_task_response(result, task_context.a2a_context)
|
|
436
|
+
else:
|
|
437
|
+
log.warning(
|
|
438
|
+
"%s Unhandled request type for forwarding: %s",
|
|
439
|
+
log_identifier,
|
|
440
|
+
type(request),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Step 5: Success - break out of retry loop
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
except RuntimeError as e:
|
|
447
|
+
# WORKAROUND: The A2A SDK raises StopAsyncIteration for connection failures,
|
|
448
|
+
# which Python 3.7+ automatically converts to RuntimeError (PEP 479).
|
|
449
|
+
# We catch this here to provide a more meaningful error message.
|
|
450
|
+
# This should be fixed upstream in the A2A SDK to raise proper connection exceptions.
|
|
451
|
+
if "StopAsyncIteration" in str(e):
|
|
452
|
+
error_msg = (
|
|
453
|
+
f"Failed to connect to agent '{agent_name}': "
|
|
454
|
+
"Connection refused or agent unreachable"
|
|
455
|
+
)
|
|
456
|
+
log.error(
|
|
457
|
+
"%s Connection error (SDK raised StopAsyncIteration): %s",
|
|
458
|
+
log_identifier,
|
|
459
|
+
error_msg,
|
|
460
|
+
)
|
|
461
|
+
# Raise a more descriptive error that will be caught by the outer handler
|
|
462
|
+
raise ConnectionError(error_msg) from e
|
|
463
|
+
else:
|
|
464
|
+
# Some other RuntimeError - re-raise it
|
|
465
|
+
raise
|
|
466
|
+
|
|
467
|
+
except A2AClientJSONRPCError as e:
|
|
468
|
+
# Handle JSON-RPC protocol errors
|
|
469
|
+
|
|
470
|
+
# Special case: Task already in terminal state (canceled/completed/failed)
|
|
471
|
+
# This is not a fatal error - the cancellation is effectively a no-op
|
|
472
|
+
if (e.error.code == -32002 and
|
|
473
|
+
"cannot be canceled" in e.error.message.lower() and
|
|
474
|
+
isinstance(request, CancelTaskRequest)):
|
|
475
|
+
log.warning(
|
|
476
|
+
"%s Task %s is already in terminal state: %s. Treating as successful cancellation.",
|
|
477
|
+
log_identifier,
|
|
478
|
+
task_context.downstream_task_id,
|
|
479
|
+
e.error.message,
|
|
480
|
+
)
|
|
481
|
+
# Task is already done - return success (cancellation is effectively complete)
|
|
482
|
+
# We don't need to publish a response because the task already sent its final response
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
log.error(
|
|
486
|
+
"%s JSON-RPC error from agent '%s': %s",
|
|
487
|
+
log_identifier,
|
|
488
|
+
agent_name,
|
|
489
|
+
e.error,
|
|
490
|
+
)
|
|
491
|
+
# TODO: Publish error response to Solace
|
|
492
|
+
# Do not retry - this is a protocol-level error
|
|
493
|
+
raise
|
|
494
|
+
|
|
495
|
+
except ConnectionError as e:
|
|
496
|
+
# Connection errors (including those converted from RuntimeError above)
|
|
497
|
+
log.error(
|
|
498
|
+
"%s Connection error forwarding request to agent '%s': %s",
|
|
499
|
+
log_identifier,
|
|
500
|
+
agent_name,
|
|
501
|
+
e,
|
|
502
|
+
)
|
|
503
|
+
raise
|
|
504
|
+
|
|
505
|
+
except A2AClientHTTPError as e:
|
|
506
|
+
# Step 4: Add specific handling for 401 Unauthorized errors
|
|
507
|
+
# The error might be wrapped in an SSE parsing error, so we need to check
|
|
508
|
+
# if the underlying cause is a 401
|
|
509
|
+
is_401_error = False
|
|
510
|
+
|
|
511
|
+
# Check if this is directly a 401
|
|
512
|
+
if hasattr(e, "status_code") and e.status_code == 401:
|
|
513
|
+
is_401_error = True
|
|
514
|
+
# Check if this is an SSE parsing error caused by a 401 response
|
|
515
|
+
elif "401" in str(e) or "Unauthorized" in str(e):
|
|
516
|
+
is_401_error = True
|
|
517
|
+
# Check if the error message mentions application/json content type
|
|
518
|
+
# (which is what 401 responses typically return)
|
|
519
|
+
elif "application/json" in str(e) and "text/event-stream" in str(e):
|
|
520
|
+
# This is likely an SSE parsing error caused by a 401 JSON response
|
|
521
|
+
is_401_error = True
|
|
522
|
+
|
|
523
|
+
if is_401_error and auth_retry_count < max_auth_retries:
|
|
524
|
+
log.warning(
|
|
525
|
+
"%s Received 401 Unauthorized from agent '%s' (detected from error: %s). Attempting token refresh (retry %d/%d).",
|
|
526
|
+
log_identifier,
|
|
527
|
+
agent_name,
|
|
528
|
+
str(e)[:100],
|
|
529
|
+
auth_retry_count + 1,
|
|
530
|
+
max_auth_retries,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
should_retry = await self._handle_auth_error(
|
|
534
|
+
agent_name, task_context
|
|
535
|
+
)
|
|
536
|
+
if should_retry:
|
|
537
|
+
auth_retry_count += 1
|
|
538
|
+
continue # Retry with fresh token
|
|
539
|
+
|
|
540
|
+
# Not a retryable auth error, or max retries exceeded
|
|
541
|
+
log.exception(
|
|
542
|
+
"%s HTTP error forwarding request: %s",
|
|
543
|
+
log_identifier,
|
|
544
|
+
e,
|
|
545
|
+
)
|
|
546
|
+
raise
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
log.exception(
|
|
550
|
+
"%s Unexpected error forwarding request: %s",
|
|
551
|
+
log_identifier,
|
|
552
|
+
e,
|
|
553
|
+
)
|
|
554
|
+
# Let base class exception handler in _handle_a2a_request catch this
|
|
555
|
+
# and publish an error response.
|
|
556
|
+
raise
|
|
557
|
+
|
|
558
|
+
async def _handle_auth_error(
|
|
559
|
+
self, agent_name: str, task_context: ProxyTaskContext
|
|
560
|
+
) -> bool:
|
|
561
|
+
"""
|
|
562
|
+
Handles authentication errors by invalidating cached tokens and clients.
|
|
563
|
+
|
|
564
|
+
This method is called when a 401 Unauthorized response is received from
|
|
565
|
+
a downstream agent. It checks if the agent uses OAuth 2.0 authentication,
|
|
566
|
+
and if so, invalidates the cached token and removes ALL cached clients
|
|
567
|
+
for this agent/session combination (both streaming and non-streaming).
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
agent_name: The name of the agent that returned 401.
|
|
571
|
+
task_context: The current task context.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
True if token was invalidated and retry should be attempted.
|
|
575
|
+
False if no retry should be attempted (e.g., static token).
|
|
576
|
+
"""
|
|
577
|
+
log_identifier = f"{self.log_identifier}[AuthError:{agent_name}]"
|
|
578
|
+
|
|
579
|
+
# Step 1: Retrieve agent configuration using O(1) lookup
|
|
580
|
+
agent_config = self._get_agent_config(agent_name)
|
|
581
|
+
|
|
582
|
+
if not agent_config:
|
|
583
|
+
log.warning(
|
|
584
|
+
"%s Agent configuration not found. Cannot handle auth error.",
|
|
585
|
+
log_identifier,
|
|
586
|
+
)
|
|
587
|
+
return False
|
|
588
|
+
|
|
589
|
+
# Step 2: Check authentication type
|
|
590
|
+
auth_config = agent_config.get("authentication")
|
|
591
|
+
if not auth_config:
|
|
592
|
+
log.debug(
|
|
593
|
+
"%s No authentication configured for agent. No retry needed.",
|
|
594
|
+
log_identifier,
|
|
595
|
+
)
|
|
596
|
+
return False
|
|
597
|
+
|
|
598
|
+
auth_type = auth_config.get("type")
|
|
599
|
+
if not auth_type:
|
|
600
|
+
# Legacy config - infer from scheme
|
|
601
|
+
scheme = auth_config.get("scheme", "bearer")
|
|
602
|
+
auth_type = "static_bearer" if scheme == "bearer" else "static_apikey"
|
|
603
|
+
|
|
604
|
+
if auth_type != "oauth2_client_credentials":
|
|
605
|
+
log.debug(
|
|
606
|
+
"%s Agent uses '%s' authentication (not OAuth 2.0). No retry for static tokens.",
|
|
607
|
+
log_identifier,
|
|
608
|
+
auth_type,
|
|
609
|
+
)
|
|
610
|
+
return False
|
|
611
|
+
|
|
612
|
+
# Step 3: Invalidate cached OAuth token
|
|
613
|
+
log.info(
|
|
614
|
+
"%s Invalidating cached OAuth 2.0 token for agent '%s'.",
|
|
615
|
+
log_identifier,
|
|
616
|
+
agent_name,
|
|
617
|
+
)
|
|
618
|
+
await self._oauth_token_cache.invalidate(agent_name)
|
|
619
|
+
|
|
620
|
+
# Step 4: Remove ALL cached Clients for this agent/session combination
|
|
621
|
+
# We clear both streaming and non-streaming clients because:
|
|
622
|
+
# 1. Both share the same session_id in the credential store
|
|
623
|
+
# 2. Both would have been created with the same expired token
|
|
624
|
+
# 3. We want fresh tokens for any subsequent requests
|
|
625
|
+
# The cache key is a 3-tuple: (agent_name, session_id, is_streaming)
|
|
626
|
+
session_id = task_context.a2a_context.get("session_id", "default_session")
|
|
627
|
+
|
|
628
|
+
clients_removed = 0
|
|
629
|
+
for is_streaming in [True, False]:
|
|
630
|
+
cache_key = (agent_name, session_id, is_streaming)
|
|
631
|
+
if cache_key in self._a2a_clients:
|
|
632
|
+
self._a2a_clients.pop(cache_key)
|
|
633
|
+
clients_removed += 1
|
|
634
|
+
log.info(
|
|
635
|
+
"%s Removed cached Client for agent '%s' session '%s' streaming=%s.",
|
|
636
|
+
log_identifier,
|
|
637
|
+
agent_name,
|
|
638
|
+
session_id,
|
|
639
|
+
is_streaming,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
if clients_removed == 0:
|
|
643
|
+
log.warning(
|
|
644
|
+
"%s No cached Clients found for agent '%s' session '%s'. This is unexpected.",
|
|
645
|
+
log_identifier,
|
|
646
|
+
agent_name,
|
|
647
|
+
session_id,
|
|
648
|
+
)
|
|
649
|
+
else:
|
|
650
|
+
log.info(
|
|
651
|
+
"%s Removed %d cached Client(s). Will create fresh client(s) with new token on retry.",
|
|
652
|
+
log_identifier,
|
|
653
|
+
clients_removed,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Step 5: Return True to signal retry should be attempted
|
|
657
|
+
log.info(
|
|
658
|
+
"%s Auth error handling complete. Retry will be attempted with fresh token.",
|
|
659
|
+
log_identifier,
|
|
660
|
+
)
|
|
661
|
+
return True
|
|
662
|
+
|
|
663
|
+
async def _fetch_oauth2_token(
|
|
664
|
+
self, agent_name: str, auth_config: Dict[str, Any]
|
|
665
|
+
) -> str:
|
|
666
|
+
"""
|
|
667
|
+
Fetches an OAuth 2.0 access token using the client credentials flow.
|
|
668
|
+
|
|
669
|
+
This method implements token caching to avoid unnecessary token requests.
|
|
670
|
+
Tokens are cached per agent and automatically expire based on the configured
|
|
671
|
+
cache duration (default: 55 minutes).
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
agent_name: The name of the agent (used as cache key).
|
|
675
|
+
auth_config: Authentication configuration dictionary containing:
|
|
676
|
+
- token_url: OAuth 2.0 token endpoint (required)
|
|
677
|
+
- client_id: OAuth 2.0 client identifier (required)
|
|
678
|
+
- client_secret: OAuth 2.0 client secret (required)
|
|
679
|
+
- scope: (optional) Space-separated scope string
|
|
680
|
+
- token_cache_duration_seconds: (optional) Cache duration in seconds
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
A valid OAuth 2.0 access token (string).
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
ValueError: If required OAuth parameters are missing or invalid.
|
|
687
|
+
httpx.HTTPStatusError: If token request returns non-2xx status.
|
|
688
|
+
httpx.RequestError: If network error occurs.
|
|
689
|
+
"""
|
|
690
|
+
log_identifier = f"{self.log_identifier}[OAuth2:{agent_name}]"
|
|
691
|
+
|
|
692
|
+
# Step 1: Check cache first
|
|
693
|
+
cached_token = await self._oauth_token_cache.get(agent_name)
|
|
694
|
+
if cached_token:
|
|
695
|
+
log.debug("%s Using cached OAuth token.", log_identifier)
|
|
696
|
+
return cached_token
|
|
697
|
+
|
|
698
|
+
# Step 2: Validate required parameters
|
|
699
|
+
token_url = auth_config.get("token_url")
|
|
700
|
+
client_id = auth_config.get("client_id")
|
|
701
|
+
client_secret = auth_config.get("client_secret")
|
|
702
|
+
|
|
703
|
+
if not all([token_url, client_id, client_secret]):
|
|
704
|
+
raise ValueError(
|
|
705
|
+
f"{log_identifier} OAuth 2.0 client credentials flow requires "
|
|
706
|
+
"'token_url', 'client_id', and 'client_secret'."
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
# SECURITY: Enforce HTTPS for token URL using common utility
|
|
710
|
+
validate_https_url(token_url)
|
|
711
|
+
|
|
712
|
+
# Step 3: Extract optional parameters
|
|
713
|
+
scope = auth_config.get("scope", "")
|
|
714
|
+
# Why 3300 seconds (55 minutes): Provides a 5-minute safety margin before
|
|
715
|
+
# typical 60-minute token expiration, preventing token expiration mid-request
|
|
716
|
+
cache_duration = auth_config.get("token_cache_duration_seconds", 3300)
|
|
717
|
+
|
|
718
|
+
# Step 4: Log token acquisition attempt
|
|
719
|
+
# SECURITY: Never log client_secret or access_token to prevent credential leakage
|
|
720
|
+
log.info(
|
|
721
|
+
"%s Fetching new OAuth 2.0 token from %s (scope: %s)",
|
|
722
|
+
log_identifier,
|
|
723
|
+
token_url,
|
|
724
|
+
scope or "default",
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
# Step 5: Fetch token using common OAuth client (no retry for A2A)
|
|
729
|
+
token_data = await self._oauth_client.fetch_client_credentials_token(
|
|
730
|
+
token_url=token_url,
|
|
731
|
+
client_id=client_id,
|
|
732
|
+
client_secret=client_secret,
|
|
733
|
+
scope=scope,
|
|
734
|
+
verify=True,
|
|
735
|
+
timeout=30.0,
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
access_token = token_data["access_token"]
|
|
739
|
+
|
|
740
|
+
# Step 6: Cache the token
|
|
741
|
+
await self._oauth_token_cache.set(
|
|
742
|
+
agent_name, access_token, cache_duration
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# Step 7: Log success
|
|
746
|
+
log.info(
|
|
747
|
+
"%s Successfully obtained OAuth 2.0 token (cached for %ds)",
|
|
748
|
+
log_identifier,
|
|
749
|
+
cache_duration,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Step 8: Return access token
|
|
753
|
+
return access_token
|
|
754
|
+
|
|
755
|
+
except httpx.HTTPStatusError as e:
|
|
756
|
+
log.error(
|
|
757
|
+
"%s OAuth 2.0 token request failed with status %d: %s",
|
|
758
|
+
log_identifier,
|
|
759
|
+
e.response.status_code,
|
|
760
|
+
e.response.text,
|
|
761
|
+
)
|
|
762
|
+
raise
|
|
763
|
+
except httpx.RequestError as e:
|
|
764
|
+
log.error(
|
|
765
|
+
"%s OAuth 2.0 token request failed: %s",
|
|
766
|
+
log_identifier,
|
|
767
|
+
e,
|
|
768
|
+
)
|
|
769
|
+
raise
|
|
770
|
+
except Exception as e:
|
|
771
|
+
log.exception(
|
|
772
|
+
"%s Unexpected error fetching OAuth 2.0 token: %s",
|
|
773
|
+
log_identifier,
|
|
774
|
+
e,
|
|
775
|
+
)
|
|
776
|
+
raise
|
|
777
|
+
|
|
778
|
+
async def _get_or_create_a2a_client(
|
|
779
|
+
self, agent_name: str, task_context: ProxyTaskContext
|
|
780
|
+
) -> Optional[Client]:
|
|
781
|
+
"""
|
|
782
|
+
Gets a cached Client or creates a new one for the given agent, session, and streaming mode.
|
|
783
|
+
|
|
784
|
+
Caches clients per (agent_name, session_id, is_streaming) to ensure each session gets its
|
|
785
|
+
own client with session-specific credentials and the correct streaming mode. This is necessary because:
|
|
786
|
+
1. The A2A SDK's AuthInterceptor uses session-based credential lookup
|
|
787
|
+
2. The Client's streaming mode is set at creation time and cannot be changed
|
|
788
|
+
|
|
789
|
+
Supports multiple authentication types:
|
|
790
|
+
- static_bearer: Static bearer token authentication
|
|
791
|
+
- static_apikey: Static API key authentication
|
|
792
|
+
- oauth2_client_credentials: OAuth 2.0 Client Credentials flow with automatic token refresh
|
|
793
|
+
- oauth2_authorization_code: OAuth 2.0 Authorization Code flow
|
|
794
|
+
|
|
795
|
+
For backward compatibility, legacy configurations without a 'type' field
|
|
796
|
+
will have their type inferred from the 'scheme' field.
|
|
797
|
+
|
|
798
|
+
The client's streaming mode is determined by the original request type from
|
|
799
|
+
the gateway (message/send vs message/stream).
|
|
800
|
+
"""
|
|
801
|
+
session_id = task_context.a2a_context.get("session_id", "default_session")
|
|
802
|
+
is_streaming = task_context.a2a_context.get("is_streaming", True)
|
|
803
|
+
cache_key = (agent_name, session_id, is_streaming)
|
|
804
|
+
|
|
805
|
+
if cache_key in self._a2a_clients:
|
|
806
|
+
return self._a2a_clients[cache_key]
|
|
807
|
+
|
|
808
|
+
# Use O(1) lookup for agent configuration
|
|
809
|
+
agent_config = self._get_agent_config(agent_name)
|
|
810
|
+
if not agent_config:
|
|
811
|
+
log.error(f"No configuration found for proxied agent '{agent_name}'")
|
|
812
|
+
return None
|
|
813
|
+
|
|
814
|
+
agent_card = self.agent_registry.get_agent(agent_name)
|
|
815
|
+
if not agent_card:
|
|
816
|
+
log.error(f"Agent card not found for '{agent_name}' in registry.")
|
|
817
|
+
return None
|
|
818
|
+
|
|
819
|
+
# Check if we should use the configured URL or the agent card URL
|
|
820
|
+
use_agent_card_url = agent_config.get("use_agent_card_url", True)
|
|
821
|
+
if not use_agent_card_url:
|
|
822
|
+
# Override the agent card URL with the configured URL
|
|
823
|
+
configured_url = agent_config.get("url")
|
|
824
|
+
log.info(
|
|
825
|
+
"%s Overriding agent card URL with configured URL for agent '%s': %s",
|
|
826
|
+
self.log_identifier,
|
|
827
|
+
agent_name,
|
|
828
|
+
configured_url,
|
|
829
|
+
)
|
|
830
|
+
# Create a modified copy of the agent card with the configured URL
|
|
831
|
+
agent_card = agent_card.model_copy(update={"url": configured_url})
|
|
832
|
+
|
|
833
|
+
# Resolve timeout - ensure we always have a valid timeout value
|
|
834
|
+
default_timeout = self.get_config("default_request_timeout_seconds", 300)
|
|
835
|
+
agent_timeout = agent_config.get("request_timeout_seconds")
|
|
836
|
+
if agent_timeout is None:
|
|
837
|
+
agent_timeout = default_timeout
|
|
838
|
+
log.info("Using timeout of %ss for agent '%s'.", agent_timeout, agent_name)
|
|
839
|
+
|
|
840
|
+
# Build custom headers for task invocation
|
|
841
|
+
# Note: We build headers here but apply them via the httpx client
|
|
842
|
+
# The A2A SDK's AuthInterceptor will add auth headers via the credential store,
|
|
843
|
+
# but we want custom headers to override those, so we apply them at the client level
|
|
844
|
+
task_headers = await self._build_headers(
|
|
845
|
+
agent_name=agent_name,
|
|
846
|
+
agent_config=agent_config,
|
|
847
|
+
custom_headers_key="task_headers",
|
|
848
|
+
use_auth=False, # Auth will be handled by AuthInterceptor below
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
# Create a new httpx client with the specific timeout and custom headers for this agent
|
|
852
|
+
# httpx.Timeout requires explicit values for connect, read, write, and pool
|
|
853
|
+
httpx_client_for_agent = httpx.AsyncClient(
|
|
854
|
+
timeout=httpx.Timeout(
|
|
855
|
+
connect=agent_timeout,
|
|
856
|
+
read=agent_timeout,
|
|
857
|
+
write=agent_timeout,
|
|
858
|
+
pool=agent_timeout,
|
|
859
|
+
),
|
|
860
|
+
headers=task_headers if task_headers else None,
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
if task_headers:
|
|
864
|
+
log.info(
|
|
865
|
+
"%s Applied %d custom task header(s) for agent '%s'",
|
|
866
|
+
self.log_identifier,
|
|
867
|
+
len(task_headers),
|
|
868
|
+
agent_name,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
# Setup authentication if configured
|
|
872
|
+
auth_config = agent_config.get("authentication")
|
|
873
|
+
if auth_config:
|
|
874
|
+
auth_type = auth_config.get("type")
|
|
875
|
+
|
|
876
|
+
# Determine auth type (with backward compatibility)
|
|
877
|
+
if not auth_type:
|
|
878
|
+
# Legacy config: infer type from 'scheme' field
|
|
879
|
+
scheme = auth_config.get("scheme", "bearer")
|
|
880
|
+
if scheme == "bearer":
|
|
881
|
+
auth_type = "static_bearer"
|
|
882
|
+
elif scheme == "apikey":
|
|
883
|
+
auth_type = "static_apikey"
|
|
884
|
+
else:
|
|
885
|
+
raise ValueError(
|
|
886
|
+
f"Unknown legacy authentication scheme '{scheme}' for agent '{agent_name}'. "
|
|
887
|
+
f"Supported schemes: 'bearer', 'apikey'."
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
log.warning(
|
|
891
|
+
"%s Using legacy authentication config for agent '%s'. "
|
|
892
|
+
"Consider migrating to 'type' field.",
|
|
893
|
+
self.log_identifier,
|
|
894
|
+
agent_name,
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
log.info(
|
|
898
|
+
"%s Configuring authentication type '%s' for agent '%s'",
|
|
899
|
+
self.log_identifier,
|
|
900
|
+
auth_type,
|
|
901
|
+
agent_name,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# Route to appropriate handler
|
|
905
|
+
if auth_type == "static_bearer":
|
|
906
|
+
token = auth_config.get("token")
|
|
907
|
+
if not token:
|
|
908
|
+
raise ValueError(
|
|
909
|
+
f"Authentication type 'static_bearer' requires 'token' for agent '{agent_name}'"
|
|
910
|
+
)
|
|
911
|
+
await self._credential_store.set_credentials(
|
|
912
|
+
session_id, "bearer", token
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
elif auth_type == "static_apikey":
|
|
916
|
+
token = auth_config.get("token")
|
|
917
|
+
if not token:
|
|
918
|
+
raise ValueError(
|
|
919
|
+
f"Authentication type 'static_apikey' requires 'token' for agent '{agent_name}'"
|
|
920
|
+
)
|
|
921
|
+
await self._credential_store.set_credentials(
|
|
922
|
+
session_id, "apikey", token
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
elif auth_type == "oauth2_client_credentials":
|
|
926
|
+
# NEW: OAuth 2.0 Client Credentials Flow
|
|
927
|
+
try:
|
|
928
|
+
access_token = await self._fetch_oauth2_token(
|
|
929
|
+
agent_name, auth_config
|
|
930
|
+
)
|
|
931
|
+
await self._credential_store.set_credentials(
|
|
932
|
+
session_id, "bearer", access_token
|
|
933
|
+
)
|
|
934
|
+
except Exception as e:
|
|
935
|
+
log.error(
|
|
936
|
+
"%s Failed to obtain OAuth 2.0 token for agent '%s': %s",
|
|
937
|
+
self.log_identifier,
|
|
938
|
+
agent_name,
|
|
939
|
+
e,
|
|
940
|
+
)
|
|
941
|
+
raise
|
|
942
|
+
|
|
943
|
+
elif auth_type == "oauth2_authorization_code":
|
|
944
|
+
# NEW: OAuth 2.0 Authorization Code Flow (enterprise feature)
|
|
945
|
+
# At this point, user has already authorized (checked in _forward_request)
|
|
946
|
+
# We just need to get the access token from enterprise helpers
|
|
947
|
+
try:
|
|
948
|
+
from solace_agent_mesh_enterprise.auth.a2a import get_access_token
|
|
949
|
+
|
|
950
|
+
# Get access token (enterprise handles refresh if needed)
|
|
951
|
+
access_token = await get_access_token(
|
|
952
|
+
component=self,
|
|
953
|
+
agent_name=agent_name,
|
|
954
|
+
task_context=task_context,
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
if not access_token:
|
|
958
|
+
raise ValueError(
|
|
959
|
+
f"No OAuth2 credential found for agent '{agent_name}'. "
|
|
960
|
+
"User authorization should have completed in _forward_request()."
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
# Find the OAuth2 authorization code scheme name from agent card
|
|
964
|
+
oauth_scheme_name = None
|
|
965
|
+
if agent_card and agent_card.security_schemes:
|
|
966
|
+
for scheme_name, scheme_wrapper in agent_card.security_schemes.items():
|
|
967
|
+
scheme = scheme_wrapper.root
|
|
968
|
+
if (hasattr(scheme, 'type') and scheme.type.lower() == 'oauth2' and
|
|
969
|
+
hasattr(scheme, 'flows') and scheme.flows and
|
|
970
|
+
scheme.flows.authorization_code):
|
|
971
|
+
oauth_scheme_name = scheme_name
|
|
972
|
+
break
|
|
973
|
+
|
|
974
|
+
# Fallback if not found
|
|
975
|
+
if not oauth_scheme_name:
|
|
976
|
+
oauth_scheme_name = "oauth2_authorization_code"
|
|
977
|
+
log.warning(
|
|
978
|
+
"%s No OAuth2 authorization code scheme found in agent card, using default name",
|
|
979
|
+
self.log_identifier
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
# Store in credential store for AuthInterceptor
|
|
983
|
+
await self._credential_store.set_credentials(
|
|
984
|
+
session_id, oauth_scheme_name, access_token
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
except ImportError:
|
|
988
|
+
log.error(
|
|
989
|
+
"%s OAuth2 authorization code requires solace-agent-mesh-enterprise package",
|
|
990
|
+
self.log_identifier,
|
|
991
|
+
)
|
|
992
|
+
raise ValueError(
|
|
993
|
+
"OAuth2 authorization code requires solace-agent-mesh-enterprise package"
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
else:
|
|
997
|
+
raise ValueError(
|
|
998
|
+
f"Unsupported authentication type '{auth_type}' for agent '{agent_name}'. "
|
|
999
|
+
f"Supported types: static_bearer, static_apikey, oauth2_client_credentials, oauth2_authorization_code."
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
# Create ClientConfig for the modern client
|
|
1003
|
+
# Use the streaming mode from the original request
|
|
1004
|
+
config = ClientConfig(
|
|
1005
|
+
streaming=is_streaming,
|
|
1006
|
+
polling=False,
|
|
1007
|
+
httpx_client=httpx_client_for_agent,
|
|
1008
|
+
supported_transports=[TransportProtocol.jsonrpc],
|
|
1009
|
+
accepted_output_modes=[],
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
# Create client using ClientFactory
|
|
1013
|
+
factory = ClientFactory(config)
|
|
1014
|
+
client = factory.create(
|
|
1015
|
+
agent_card,
|
|
1016
|
+
consumers=None,
|
|
1017
|
+
interceptors=[self._auth_interceptor],
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
self._a2a_clients[cache_key] = client
|
|
1021
|
+
return client
|
|
1022
|
+
|
|
1023
|
+
async def _handle_outbound_artifacts(
|
|
1024
|
+
self,
|
|
1025
|
+
response: Any,
|
|
1026
|
+
task_context: ProxyTaskContext,
|
|
1027
|
+
agent_name: str,
|
|
1028
|
+
) -> List[Dict[str, Any]]:
|
|
1029
|
+
"""
|
|
1030
|
+
Finds artifacts with byte content, saves them to the proxy's artifact store,
|
|
1031
|
+
and mutates the response object to replace bytes with a URI.
|
|
1032
|
+
It also uses TextParts within an artifact as a description for the saved file.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
A list of dictionaries, each representing a saved artifact with its filename and version.
|
|
1036
|
+
"""
|
|
1037
|
+
from ....agent.utils.artifact_helpers import save_artifact_with_metadata
|
|
1038
|
+
|
|
1039
|
+
log_identifier = (
|
|
1040
|
+
f"{self.log_identifier}[HandleOutboundArtifacts:{task_context.task_id}]"
|
|
1041
|
+
)
|
|
1042
|
+
saved_artifacts_manifest = []
|
|
1043
|
+
|
|
1044
|
+
artifacts_to_process: List[Artifact] = []
|
|
1045
|
+
if isinstance(response, Task) and response.artifacts:
|
|
1046
|
+
artifacts_to_process = response.artifacts
|
|
1047
|
+
elif isinstance(response, TaskArtifactUpdateEvent):
|
|
1048
|
+
artifacts_to_process = [response.artifact]
|
|
1049
|
+
|
|
1050
|
+
if not artifacts_to_process:
|
|
1051
|
+
return saved_artifacts_manifest
|
|
1052
|
+
|
|
1053
|
+
if not self.artifact_service:
|
|
1054
|
+
log.warning(
|
|
1055
|
+
"%s Artifact service not configured. Cannot save outbound artifacts.",
|
|
1056
|
+
log_identifier,
|
|
1057
|
+
)
|
|
1058
|
+
return saved_artifacts_manifest
|
|
1059
|
+
|
|
1060
|
+
for artifact in artifacts_to_process:
|
|
1061
|
+
contextual_description = "\n".join(
|
|
1062
|
+
[
|
|
1063
|
+
a2a.get_text_from_text_part(part.root)
|
|
1064
|
+
for part in artifact.parts
|
|
1065
|
+
if a2a.is_text_part(part)
|
|
1066
|
+
]
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
for i, part_container in enumerate(artifact.parts):
|
|
1070
|
+
part = part_container.root
|
|
1071
|
+
if (
|
|
1072
|
+
a2a.is_file_part(part_container)
|
|
1073
|
+
and a2a.is_file_part_bytes(part)
|
|
1074
|
+
and a2a.get_bytes_from_file_part(part)
|
|
1075
|
+
):
|
|
1076
|
+
file_part = part
|
|
1077
|
+
file_content = file_part.file
|
|
1078
|
+
log.info(
|
|
1079
|
+
"%s Found outbound artifact '%s' with byte content. Saving...",
|
|
1080
|
+
log_identifier,
|
|
1081
|
+
file_content.name,
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
metadata_to_save = artifact.metadata or {}
|
|
1085
|
+
if artifact.description:
|
|
1086
|
+
metadata_to_save["description"] = artifact.description
|
|
1087
|
+
elif contextual_description:
|
|
1088
|
+
metadata_to_save["description"] = contextual_description
|
|
1089
|
+
else:
|
|
1090
|
+
metadata_to_save["description"] = (
|
|
1091
|
+
f"Artifact created by {agent_name}"
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
metadata_to_save["proxied_from_artifact_id"] = artifact.artifact_id
|
|
1095
|
+
user_id = task_context.a2a_context.get("user_id", "default_user")
|
|
1096
|
+
session_id = task_context.a2a_context.get("session_id")
|
|
1097
|
+
|
|
1098
|
+
# Get file content using facade helpers
|
|
1099
|
+
content_bytes = a2a.get_bytes_from_file_part(file_part)
|
|
1100
|
+
filename = a2a.get_filename_from_file_part(file_part)
|
|
1101
|
+
mime_type = a2a.get_mimetype_from_file_part(file_part)
|
|
1102
|
+
|
|
1103
|
+
save_result = await save_artifact_with_metadata(
|
|
1104
|
+
artifact_service=self.artifact_service,
|
|
1105
|
+
app_name=agent_name,
|
|
1106
|
+
user_id=user_id,
|
|
1107
|
+
session_id=session_id,
|
|
1108
|
+
filename=filename,
|
|
1109
|
+
content_bytes=content_bytes,
|
|
1110
|
+
mime_type=mime_type,
|
|
1111
|
+
metadata_dict=metadata_to_save,
|
|
1112
|
+
timestamp=datetime.now(timezone.utc),
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
if save_result.get("status") in ["success", "partial_success"]:
|
|
1116
|
+
data_version = save_result.get("data_version")
|
|
1117
|
+
saved_uri = format_artifact_uri(
|
|
1118
|
+
app_name=agent_name,
|
|
1119
|
+
user_id=user_id,
|
|
1120
|
+
session_id=session_id,
|
|
1121
|
+
filename=filename,
|
|
1122
|
+
version=data_version,
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
new_file_part = a2a.create_file_part_from_uri(
|
|
1126
|
+
uri=saved_uri,
|
|
1127
|
+
name=filename,
|
|
1128
|
+
mime_type=mime_type,
|
|
1129
|
+
metadata=a2a.get_metadata_from_part(file_part),
|
|
1130
|
+
)
|
|
1131
|
+
from a2a.types import Part
|
|
1132
|
+
|
|
1133
|
+
artifact.parts[i] = Part(root=new_file_part)
|
|
1134
|
+
|
|
1135
|
+
saved_artifacts_manifest.append(
|
|
1136
|
+
{"filename": filename, "version": data_version}
|
|
1137
|
+
)
|
|
1138
|
+
log.info(
|
|
1139
|
+
"%s Saved artifact '%s' as version %d. URI: %s",
|
|
1140
|
+
log_identifier,
|
|
1141
|
+
filename,
|
|
1142
|
+
data_version,
|
|
1143
|
+
saved_uri,
|
|
1144
|
+
)
|
|
1145
|
+
else:
|
|
1146
|
+
log.error(
|
|
1147
|
+
"%s Failed to save artifact '%s': %s",
|
|
1148
|
+
log_identifier,
|
|
1149
|
+
filename,
|
|
1150
|
+
save_result.get("message"),
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
return saved_artifacts_manifest
|
|
1154
|
+
|
|
1155
|
+
async def _process_downstream_response(
|
|
1156
|
+
self,
|
|
1157
|
+
event: Union[
|
|
1158
|
+
tuple, Message, Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent
|
|
1159
|
+
],
|
|
1160
|
+
task_context: ProxyTaskContext,
|
|
1161
|
+
client: Client,
|
|
1162
|
+
agent_name: str,
|
|
1163
|
+
) -> None:
|
|
1164
|
+
"""
|
|
1165
|
+
Processes a single event from the downstream agent.
|
|
1166
|
+
|
|
1167
|
+
When using the normal client (non-streaming), events are:
|
|
1168
|
+
- A ClientEvent tuple: (Task, Optional[UpdateEvent])
|
|
1169
|
+
- A Message object (for direct responses)
|
|
1170
|
+
|
|
1171
|
+
When using transport directly (streaming workaround), events are raw:
|
|
1172
|
+
- Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, or Message objects
|
|
1173
|
+
"""
|
|
1174
|
+
log_identifier = (
|
|
1175
|
+
f"{self.log_identifier}[ProcessResponse:{task_context.task_id}]"
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
# Use facade helpers to determine event type
|
|
1179
|
+
event_payload = None
|
|
1180
|
+
|
|
1181
|
+
# Handle raw transport events (from streaming workaround)
|
|
1182
|
+
if isinstance(event, (Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent)):
|
|
1183
|
+
event_payload = event
|
|
1184
|
+
log.debug(
|
|
1185
|
+
"%s Received raw transport event: %s",
|
|
1186
|
+
log_identifier,
|
|
1187
|
+
type(event).__name__,
|
|
1188
|
+
)
|
|
1189
|
+
elif a2a.is_client_event(event):
|
|
1190
|
+
# Unpack the ClientEvent tuple
|
|
1191
|
+
task, update_event = a2a.unpack_client_event(event)
|
|
1192
|
+
# If there's an update event, that's what we should process
|
|
1193
|
+
# The task is just context; the update is the actual event
|
|
1194
|
+
if update_event is not None:
|
|
1195
|
+
event_payload = update_event
|
|
1196
|
+
log.debug(
|
|
1197
|
+
"%s Received ClientEvent with update: %s (task state: %s)",
|
|
1198
|
+
log_identifier,
|
|
1199
|
+
type(update_event).__name__,
|
|
1200
|
+
task.status.state if task.status else "unknown",
|
|
1201
|
+
)
|
|
1202
|
+
else:
|
|
1203
|
+
# No update event means this is the final task state
|
|
1204
|
+
event_payload = task
|
|
1205
|
+
log.debug(
|
|
1206
|
+
"%s Received ClientEvent with final task state: %s",
|
|
1207
|
+
log_identifier,
|
|
1208
|
+
task.status.state if task.status else "unknown",
|
|
1209
|
+
)
|
|
1210
|
+
elif a2a.is_message_object(event):
|
|
1211
|
+
# Direct Message response
|
|
1212
|
+
event_payload = event
|
|
1213
|
+
log.debug(
|
|
1214
|
+
"%s Received direct Message response",
|
|
1215
|
+
log_identifier,
|
|
1216
|
+
)
|
|
1217
|
+
else:
|
|
1218
|
+
log.warning(
|
|
1219
|
+
"%s Received unexpected event type: %s",
|
|
1220
|
+
log_identifier,
|
|
1221
|
+
type(event).__name__,
|
|
1222
|
+
)
|
|
1223
|
+
return
|
|
1224
|
+
|
|
1225
|
+
if not event_payload:
|
|
1226
|
+
log.warning(
|
|
1227
|
+
"%s Received an event with no processable payload: %s",
|
|
1228
|
+
log_identifier,
|
|
1229
|
+
event,
|
|
1230
|
+
)
|
|
1231
|
+
return
|
|
1232
|
+
|
|
1233
|
+
produced_artifacts = await self._handle_outbound_artifacts(
|
|
1234
|
+
event_payload, task_context, agent_name
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
# Add produced_artifacts to metadata if any artifacts were processed
|
|
1238
|
+
if produced_artifacts and isinstance(
|
|
1239
|
+
event_payload, (Task, TaskStatusUpdateEvent)
|
|
1240
|
+
):
|
|
1241
|
+
if not event_payload.metadata:
|
|
1242
|
+
event_payload.metadata = {}
|
|
1243
|
+
event_payload.metadata["produced_artifacts"] = produced_artifacts
|
|
1244
|
+
log.info(
|
|
1245
|
+
"%s Added manifest of %d produced artifacts to %s metadata.",
|
|
1246
|
+
log_identifier,
|
|
1247
|
+
len(produced_artifacts),
|
|
1248
|
+
type(event_payload).__name__,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
# Add agent_name to metadata for all response types
|
|
1252
|
+
if isinstance(
|
|
1253
|
+
event_payload, (Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent)
|
|
1254
|
+
):
|
|
1255
|
+
if not event_payload.metadata:
|
|
1256
|
+
event_payload.metadata = {}
|
|
1257
|
+
event_payload.metadata["agent_name"] = agent_name
|
|
1258
|
+
log.debug(
|
|
1259
|
+
"%s Added agent_name '%s' to %s metadata.",
|
|
1260
|
+
log_identifier,
|
|
1261
|
+
agent_name,
|
|
1262
|
+
type(event_payload).__name__,
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
# Convert TextParts to AgentProgressUpdateData for intermediate status updates if configured
|
|
1266
|
+
# Only convert non-final status updates; final status updates are used to construct the final Task
|
|
1267
|
+
if isinstance(event_payload, TaskStatusUpdateEvent) and not event_payload.final:
|
|
1268
|
+
agent_config = self._get_agent_config(agent_name)
|
|
1269
|
+
convert_progress = agent_config.get("convert_progress_updates", True) if agent_config else True
|
|
1270
|
+
|
|
1271
|
+
# DEBUG: Log config lookup results
|
|
1272
|
+
log.info(
|
|
1273
|
+
"%s DEBUG convert_progress_updates: agent_name='%s', agent_config_name='%s', agent_config_keys=%s, convert_progress_value=%s, convert_progress=%s",
|
|
1274
|
+
log_identifier,
|
|
1275
|
+
agent_name,
|
|
1276
|
+
agent_config.get('name') if agent_config else None,
|
|
1277
|
+
list(agent_config.keys()) if agent_config else None,
|
|
1278
|
+
agent_config.get("convert_progress_updates") if agent_config else None,
|
|
1279
|
+
convert_progress,
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
if convert_progress and event_payload.status and event_payload.status.message:
|
|
1283
|
+
message = event_payload.status.message
|
|
1284
|
+
original_parts = a2a.get_parts_from_message(message)
|
|
1285
|
+
|
|
1286
|
+
if original_parts:
|
|
1287
|
+
converted_parts = []
|
|
1288
|
+
text_parts_converted = 0
|
|
1289
|
+
|
|
1290
|
+
for part in original_parts:
|
|
1291
|
+
if isinstance(part, TextPart) and part.text:
|
|
1292
|
+
# Convert TextPart to DataPart with AgentProgressUpdateData
|
|
1293
|
+
progress_data = AgentProgressUpdateData(
|
|
1294
|
+
type="agent_progress_update",
|
|
1295
|
+
status_text=part.text
|
|
1296
|
+
)
|
|
1297
|
+
data_part = DataPart(
|
|
1298
|
+
kind="data",
|
|
1299
|
+
data=progress_data.model_dump(),
|
|
1300
|
+
metadata=part.metadata
|
|
1301
|
+
)
|
|
1302
|
+
converted_parts.append(data_part)
|
|
1303
|
+
text_parts_converted += 1
|
|
1304
|
+
else:
|
|
1305
|
+
# Keep non-text parts as-is
|
|
1306
|
+
converted_parts.append(part)
|
|
1307
|
+
|
|
1308
|
+
if text_parts_converted > 0:
|
|
1309
|
+
# Update the message with converted parts
|
|
1310
|
+
event_payload.status.message = a2a.update_message_parts(
|
|
1311
|
+
message, converted_parts
|
|
1312
|
+
)
|
|
1313
|
+
log.debug(
|
|
1314
|
+
"%s Converted %d TextPart(s) to AgentProgressUpdateData in status update",
|
|
1315
|
+
log_identifier,
|
|
1316
|
+
text_parts_converted,
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
# Capture the downstream task ID before we replace it
|
|
1320
|
+
# This is needed for forwarding cancellation requests to the downstream agent
|
|
1321
|
+
downstream_id = None
|
|
1322
|
+
if hasattr(event_payload, "task_id") and event_payload.task_id:
|
|
1323
|
+
downstream_id = event_payload.task_id
|
|
1324
|
+
elif hasattr(event_payload, "id") and event_payload.id:
|
|
1325
|
+
downstream_id = event_payload.id
|
|
1326
|
+
|
|
1327
|
+
# Store the downstream task ID in the context if we haven't already
|
|
1328
|
+
if downstream_id and not task_context.downstream_task_id:
|
|
1329
|
+
task_context.downstream_task_id = downstream_id
|
|
1330
|
+
log.debug(
|
|
1331
|
+
"%s Captured downstream task ID: %s (SAM task ID: %s)",
|
|
1332
|
+
log_identifier,
|
|
1333
|
+
downstream_id,
|
|
1334
|
+
task_context.task_id,
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
# Replace the downstream task ID with SAM's task ID for upstream responses
|
|
1338
|
+
original_task_id = task_context.task_id
|
|
1339
|
+
if hasattr(event_payload, "task_id") and event_payload.task_id:
|
|
1340
|
+
event_payload.task_id = original_task_id
|
|
1341
|
+
elif hasattr(event_payload, "id") and event_payload.id:
|
|
1342
|
+
event_payload.id = original_task_id
|
|
1343
|
+
|
|
1344
|
+
if isinstance(event_payload, Task) and event_payload.artifacts:
|
|
1345
|
+
text_only_artifacts_content = []
|
|
1346
|
+
remaining_artifacts = []
|
|
1347
|
+
for artifact in event_payload.artifacts:
|
|
1348
|
+
if a2a.is_text_only_artifact(artifact):
|
|
1349
|
+
text_only_artifacts_content.extend(
|
|
1350
|
+
a2a.get_text_content_from_artifact(artifact)
|
|
1351
|
+
)
|
|
1352
|
+
else:
|
|
1353
|
+
remaining_artifacts.append(artifact)
|
|
1354
|
+
|
|
1355
|
+
if text_only_artifacts_content:
|
|
1356
|
+
log.info(
|
|
1357
|
+
"%s Consolidating %d text-only artifacts into status message.",
|
|
1358
|
+
log_identifier,
|
|
1359
|
+
len(event_payload.artifacts) - len(remaining_artifacts),
|
|
1360
|
+
)
|
|
1361
|
+
event_payload.artifacts = (
|
|
1362
|
+
remaining_artifacts if remaining_artifacts else None
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
consolidated_text = "\n".join(text_only_artifacts_content)
|
|
1366
|
+
summary_message_part = TextPart(
|
|
1367
|
+
text=(
|
|
1368
|
+
"The following text-only artifacts were returned and have been consolidated into this message:\n\n---\n\n"
|
|
1369
|
+
f"{consolidated_text}"
|
|
1370
|
+
)
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
if not event_payload.status.message:
|
|
1374
|
+
from a2a.types import Part
|
|
1375
|
+
|
|
1376
|
+
event_payload.status.message = Message(
|
|
1377
|
+
message_id=str(uuid.uuid4()),
|
|
1378
|
+
role="agent",
|
|
1379
|
+
parts=[Part(root=summary_message_part)],
|
|
1380
|
+
)
|
|
1381
|
+
else:
|
|
1382
|
+
from a2a.types import Part
|
|
1383
|
+
|
|
1384
|
+
event_payload.status.message.parts.append(
|
|
1385
|
+
Part(root=summary_message_part)
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
# Convert text-only TaskArtifactUpdateEvents to TaskStatusUpdateEvents
|
|
1389
|
+
# Some A2A agents send text content as artifacts, which SAM expects as status updates
|
|
1390
|
+
if isinstance(event_payload, TaskArtifactUpdateEvent):
|
|
1391
|
+
artifact = event_payload.artifact
|
|
1392
|
+
if a2a.is_text_only_artifact(artifact):
|
|
1393
|
+
log.info(
|
|
1394
|
+
"%s Converting text-only artifact to status update",
|
|
1395
|
+
log_identifier,
|
|
1396
|
+
)
|
|
1397
|
+
# Extract text from text-only artifact
|
|
1398
|
+
text_content = "\n".join(a2a.get_text_content_from_artifact(artifact))
|
|
1399
|
+
|
|
1400
|
+
# Convert to status update
|
|
1401
|
+
text_message = a2a.create_agent_text_message(
|
|
1402
|
+
text=text_content,
|
|
1403
|
+
task_id=event_payload.task_id,
|
|
1404
|
+
context_id=event_payload.context_id,
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
status_event = TaskStatusUpdateEvent(
|
|
1408
|
+
task_id=event_payload.task_id,
|
|
1409
|
+
context_id=event_payload.context_id,
|
|
1410
|
+
kind="status-update",
|
|
1411
|
+
status=TaskStatus(state=TaskState.working, message=text_message),
|
|
1412
|
+
final=False,
|
|
1413
|
+
metadata=event_payload.metadata,
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
# Replace event_payload with the converted status update
|
|
1417
|
+
event_payload = status_event
|
|
1418
|
+
log.info(
|
|
1419
|
+
"%s Converted text-only artifact (length: %d bytes) to status update",
|
|
1420
|
+
log_identifier,
|
|
1421
|
+
len(text_content.encode("utf-8")),
|
|
1422
|
+
)
|
|
1423
|
+
|
|
1424
|
+
# Determine if this is a terminal event requiring cleanup
|
|
1425
|
+
should_cleanup_task = False
|
|
1426
|
+
|
|
1427
|
+
# Route based on event type
|
|
1428
|
+
if isinstance(event_payload, Task):
|
|
1429
|
+
# Discard initial Task events (non-completed states)
|
|
1430
|
+
# The final Task will be constructed from the final status update
|
|
1431
|
+
if event_payload.status.state != TaskState.completed:
|
|
1432
|
+
log.debug(
|
|
1433
|
+
"%s Discarding Task event with state=%s (not completed). Final Task will be constructed from final status update.",
|
|
1434
|
+
log_identifier,
|
|
1435
|
+
event_payload.status.state,
|
|
1436
|
+
)
|
|
1437
|
+
# Don't publish, don't cleanup - wait for final status update
|
|
1438
|
+
return
|
|
1439
|
+
|
|
1440
|
+
# Forward completed Task to reply topic
|
|
1441
|
+
await self._publish_task_response(event_payload, task_context.a2a_context)
|
|
1442
|
+
|
|
1443
|
+
# Completed Task is terminal - cleanup
|
|
1444
|
+
should_cleanup_task = True
|
|
1445
|
+
log.debug(
|
|
1446
|
+
"%s Task in terminal state: %s",
|
|
1447
|
+
log_identifier,
|
|
1448
|
+
event_payload.status.state,
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
elif isinstance(event_payload, TaskStatusUpdateEvent):
|
|
1452
|
+
# Forward status update to status topic
|
|
1453
|
+
await self._publish_status_update(event_payload, task_context.a2a_context)
|
|
1454
|
+
|
|
1455
|
+
# Check if final event - construct and send Task
|
|
1456
|
+
if event_payload.final:
|
|
1457
|
+
log.info(
|
|
1458
|
+
"%s Received final status update (final=true). Constructing completed Task.",
|
|
1459
|
+
log_identifier,
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
# Construct Task from final status update
|
|
1463
|
+
# Copy the status but ensure state is "completed"
|
|
1464
|
+
final_task_status = TaskStatus(
|
|
1465
|
+
state=TaskState.completed,
|
|
1466
|
+
message=event_payload.status.message if event_payload.status else None,
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
final_task = Task(
|
|
1470
|
+
id=event_payload.task_id,
|
|
1471
|
+
context_id=event_payload.context_id,
|
|
1472
|
+
status=final_task_status,
|
|
1473
|
+
artifacts=None, # Artifacts come via separate events
|
|
1474
|
+
metadata=event_payload.metadata,
|
|
1475
|
+
)
|
|
1476
|
+
|
|
1477
|
+
# Add produced_artifacts metadata if any artifacts were processed
|
|
1478
|
+
if produced_artifacts:
|
|
1479
|
+
if not final_task.metadata:
|
|
1480
|
+
final_task.metadata = {}
|
|
1481
|
+
final_task.metadata["produced_artifacts"] = produced_artifacts
|
|
1482
|
+
log.info(
|
|
1483
|
+
"%s Added manifest of %d produced artifacts to final Task metadata.",
|
|
1484
|
+
log_identifier,
|
|
1485
|
+
len(produced_artifacts),
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
# Publish the constructed Task
|
|
1489
|
+
await self._publish_task_response(final_task, task_context.a2a_context)
|
|
1490
|
+
|
|
1491
|
+
should_cleanup_task = True
|
|
1492
|
+
log.debug(
|
|
1493
|
+
"%s Published final Task constructed from status update",
|
|
1494
|
+
log_identifier,
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
elif isinstance(event_payload, TaskArtifactUpdateEvent):
|
|
1498
|
+
# Forward artifact update to status topic
|
|
1499
|
+
await self._publish_artifact_update(event_payload, task_context.a2a_context)
|
|
1500
|
+
|
|
1501
|
+
elif isinstance(event_payload, Message):
|
|
1502
|
+
# Wrap Message in Task for gateway compatibility
|
|
1503
|
+
log.info(
|
|
1504
|
+
"%s Received direct Message response. Wrapping in completed Task.",
|
|
1505
|
+
log_identifier,
|
|
1506
|
+
)
|
|
1507
|
+
final_task = Task(
|
|
1508
|
+
id=task_context.task_id,
|
|
1509
|
+
context_id=task_context.a2a_context.get("session_id"),
|
|
1510
|
+
status=TaskStatus(state=TaskState.completed, message=event_payload),
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1513
|
+
# Add produced_artifacts metadata if any artifacts were processed
|
|
1514
|
+
if produced_artifacts:
|
|
1515
|
+
final_task.metadata = {"produced_artifacts": produced_artifacts}
|
|
1516
|
+
log.info(
|
|
1517
|
+
"%s Added manifest of %d produced artifacts to wrapped Task metadata.",
|
|
1518
|
+
log_identifier,
|
|
1519
|
+
len(produced_artifacts),
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
await self._publish_task_response(final_task, task_context.a2a_context)
|
|
1523
|
+
should_cleanup_task = True
|
|
1524
|
+
|
|
1525
|
+
else:
|
|
1526
|
+
log.warning(
|
|
1527
|
+
"%s Received unhandled response payload type: %s",
|
|
1528
|
+
log_identifier,
|
|
1529
|
+
type(event_payload).__name__,
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
# Cleanup task state if terminal event detected
|
|
1533
|
+
if should_cleanup_task:
|
|
1534
|
+
log.info(
|
|
1535
|
+
"%s Terminal event detected for task %s. Cleaning up state.",
|
|
1536
|
+
log_identifier,
|
|
1537
|
+
task_context.task_id,
|
|
1538
|
+
)
|
|
1539
|
+
self._cleanup_task_state(task_context.task_id)
|
|
1540
|
+
|
|
1541
|
+
def clear_client_cache(self):
|
|
1542
|
+
"""
|
|
1543
|
+
Clears all cached A2A clients and OAuth tokens.
|
|
1544
|
+
This is useful for testing when authentication configuration changes.
|
|
1545
|
+
"""
|
|
1546
|
+
num_clients = len(self._a2a_clients)
|
|
1547
|
+
self._a2a_clients.clear()
|
|
1548
|
+
log.info(
|
|
1549
|
+
"%s Cleared all cached A2A clients (%d clients removed).",
|
|
1550
|
+
self.log_identifier,
|
|
1551
|
+
num_clients,
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
def cleanup(self):
|
|
1555
|
+
"""Cleans up resources on component shutdown."""
|
|
1556
|
+
log.info("%s Cleaning up A2A proxy component resources...", self.log_identifier)
|
|
1557
|
+
|
|
1558
|
+
# Token cache cleanup:
|
|
1559
|
+
# - OAuth2TokenCache is automatically garbage collected
|
|
1560
|
+
# - No persistent state to clean up
|
|
1561
|
+
# - Tokens are lost on component restart (by design)
|
|
1562
|
+
|
|
1563
|
+
async def _async_cleanup():
|
|
1564
|
+
# Close all created clients using public API
|
|
1565
|
+
for cache_key, client in self._a2a_clients.items():
|
|
1566
|
+
agent_name, session_id = cache_key
|
|
1567
|
+
log.info(
|
|
1568
|
+
"%s Closing client for agent '%s' session '%s'",
|
|
1569
|
+
self.log_identifier,
|
|
1570
|
+
agent_name,
|
|
1571
|
+
session_id,
|
|
1572
|
+
)
|
|
1573
|
+
await client.close()
|
|
1574
|
+
self._a2a_clients.clear()
|
|
1575
|
+
|
|
1576
|
+
if self._async_loop and self._async_loop.is_running():
|
|
1577
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
1578
|
+
_async_cleanup(), self._async_loop
|
|
1579
|
+
)
|
|
1580
|
+
try:
|
|
1581
|
+
future.result(timeout=5)
|
|
1582
|
+
except Exception as e:
|
|
1583
|
+
log.error("%s Error during async cleanup: %s", self.log_identifier, e)
|
|
1584
|
+
|
|
1585
|
+
super().cleanup()
|