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,2360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Solace AI Connector Component to host the FastAPI backend for the Web UI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import queue
|
|
9
|
+
import re
|
|
10
|
+
import threading
|
|
11
|
+
import uuid
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import uvicorn
|
|
16
|
+
from fastapi import FastAPI, UploadFile
|
|
17
|
+
from fastapi import Request as FastAPIRequest
|
|
18
|
+
from solace_ai_connector.common.event import Event, EventType
|
|
19
|
+
from solace_ai_connector.components.inputs_outputs.broker_input import BrokerInput
|
|
20
|
+
from solace_ai_connector.flow.app import App as SACApp
|
|
21
|
+
|
|
22
|
+
from ...common.agent_registry import AgentRegistry
|
|
23
|
+
from ...core_a2a.service import CoreA2AService
|
|
24
|
+
from ...gateway.base.component import BaseGatewayComponent
|
|
25
|
+
from ...gateway.http_sse.session_manager import SessionManager
|
|
26
|
+
from ...gateway.http_sse.sse_manager import SSEManager
|
|
27
|
+
from . import dependencies
|
|
28
|
+
from .components import VisualizationForwarderComponent
|
|
29
|
+
from .components.task_logger_forwarder import TaskLoggerForwarderComponent
|
|
30
|
+
from .services.task_logger_service import TaskLoggerService
|
|
31
|
+
from .sse_event_buffer import SSEEventBuffer
|
|
32
|
+
|
|
33
|
+
log = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from google.adk.artifacts import BaseArtifactService
|
|
37
|
+
except ImportError:
|
|
38
|
+
|
|
39
|
+
class BaseArtifactService:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
from a2a.types import (
|
|
44
|
+
A2ARequest,
|
|
45
|
+
AgentCard,
|
|
46
|
+
JSONRPCError,
|
|
47
|
+
JSONRPCResponse,
|
|
48
|
+
Task,
|
|
49
|
+
TaskArtifactUpdateEvent,
|
|
50
|
+
TaskStatusUpdateEvent,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
from ...common import a2a
|
|
54
|
+
from ...common.a2a.types import ContentPart
|
|
55
|
+
from ...common.middleware.config_resolver import ConfigResolver
|
|
56
|
+
from ...common.utils.embeds import (
|
|
57
|
+
EARLY_EMBED_TYPES,
|
|
58
|
+
evaluate_embed,
|
|
59
|
+
resolve_embeds_in_string,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
info = {
|
|
63
|
+
"class_name": "WebUIBackendComponent",
|
|
64
|
+
"description": (
|
|
65
|
+
"Hosts the FastAPI backend server for the A2A Web UI, manages messaging via SAC, "
|
|
66
|
+
"and implements GDK abstract methods for Web UI interaction. "
|
|
67
|
+
"Configuration is derived from WebUIBackendApp's app_config."
|
|
68
|
+
),
|
|
69
|
+
"config_parameters": [
|
|
70
|
+
# Configuration parameters are defined and validated by WebUIBackendApp.app_schema.
|
|
71
|
+
],
|
|
72
|
+
"input_schema": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"description": "Not typically used; component reacts to events.",
|
|
75
|
+
"properties": {},
|
|
76
|
+
},
|
|
77
|
+
"output_schema": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"description": "Not typically used; component publishes results via FastAPI/SSE.",
|
|
80
|
+
"properties": {},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class WebUIBackendComponent(BaseGatewayComponent):
|
|
86
|
+
"""
|
|
87
|
+
Hosts the FastAPI backend, manages messaging via SAC, and bridges threads.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, **kwargs):
|
|
91
|
+
"""
|
|
92
|
+
Initializes the WebUIBackendComponent, inheriting from BaseGatewayComponent.
|
|
93
|
+
"""
|
|
94
|
+
component_config = kwargs.get("component_config", {})
|
|
95
|
+
app_config = component_config.get("app_config", {})
|
|
96
|
+
resolve_uris = app_config.get("resolve_artifact_uris_in_gateway", True)
|
|
97
|
+
|
|
98
|
+
# HTTP SSE gateway configuration:
|
|
99
|
+
# - supports_inline_artifact_resolution=True: Artifacts are converted to FileParts
|
|
100
|
+
# during embed resolution and rendered inline in the web UI
|
|
101
|
+
# - filter_tool_data_parts=False: Web UI displays all parts including tool execution details
|
|
102
|
+
super().__init__(
|
|
103
|
+
resolve_artifact_uris_in_gateway=resolve_uris,
|
|
104
|
+
supports_inline_artifact_resolution=True,
|
|
105
|
+
filter_tool_data_parts=False,
|
|
106
|
+
**kwargs
|
|
107
|
+
)
|
|
108
|
+
log.info("%s Initializing Web UI Backend Component...", self.log_identifier)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
self.namespace = self.get_config("namespace")
|
|
112
|
+
self.gateway_id = self.get_config("gateway_id")
|
|
113
|
+
if not self.gateway_id:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"Internal Error: Gateway ID missing after app initialization."
|
|
116
|
+
)
|
|
117
|
+
self.fastapi_host = self.get_config("fastapi_host", "127.0.0.1")
|
|
118
|
+
self.fastapi_port = self.get_config("fastapi_port", 8000)
|
|
119
|
+
self.fastapi_https_port = self.get_config("fastapi_https_port", 8443)
|
|
120
|
+
self.session_secret_key = self.get_config("session_secret_key")
|
|
121
|
+
self.cors_allowed_origins = self.get_config("cors_allowed_origins", ["*"])
|
|
122
|
+
self.ssl_keyfile = self.get_config("ssl_keyfile", "")
|
|
123
|
+
self.ssl_certfile = self.get_config("ssl_certfile", "")
|
|
124
|
+
self.ssl_keyfile_password = self.get_config("ssl_keyfile_password", "")
|
|
125
|
+
self.model_config = self.get_config("model", None)
|
|
126
|
+
|
|
127
|
+
log.info(
|
|
128
|
+
"%s WebUI-specific configuration retrieved (Host: %s, Port: %d).",
|
|
129
|
+
self.log_identifier,
|
|
130
|
+
self.fastapi_host,
|
|
131
|
+
self.fastapi_port,
|
|
132
|
+
)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
log.error("%s Failed to retrieve configuration: %s", self.log_identifier, e)
|
|
135
|
+
raise ValueError(f"Configuration retrieval error: {e}") from e
|
|
136
|
+
|
|
137
|
+
self.sse_max_queue_size = self.get_config("sse_max_queue_size", 200)
|
|
138
|
+
sse_buffer_max_age_seconds = self.get_config("sse_buffer_max_age_seconds", 600)
|
|
139
|
+
|
|
140
|
+
self.sse_event_buffer = SSEEventBuffer(
|
|
141
|
+
max_queue_size=self.sse_max_queue_size,
|
|
142
|
+
max_age_seconds=sse_buffer_max_age_seconds,
|
|
143
|
+
)
|
|
144
|
+
# SSE manager will be initialized after database setup
|
|
145
|
+
self.sse_manager = None
|
|
146
|
+
|
|
147
|
+
self._sse_cleanup_timer_id = f"sse_cleanup_{self.gateway_id}"
|
|
148
|
+
cleanup_interval_sec = self.get_config(
|
|
149
|
+
"sse_buffer_cleanup_interval_seconds", 300
|
|
150
|
+
)
|
|
151
|
+
self.add_timer(
|
|
152
|
+
delay_ms=cleanup_interval_sec * 1000,
|
|
153
|
+
timer_id=self._sse_cleanup_timer_id,
|
|
154
|
+
interval_ms=cleanup_interval_sec * 1000,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Set up health check timer for agent registry
|
|
158
|
+
from ...common.constants import HEALTH_CHECK_INTERVAL_SECONDS
|
|
159
|
+
|
|
160
|
+
self.health_check_timer_id = f"agent_health_check_{self.gateway_id}"
|
|
161
|
+
health_check_interval_seconds = self.get_config(
|
|
162
|
+
"agent_health_check_interval_seconds", HEALTH_CHECK_INTERVAL_SECONDS
|
|
163
|
+
)
|
|
164
|
+
if health_check_interval_seconds > 0:
|
|
165
|
+
log.info(
|
|
166
|
+
"%s Scheduling agent health check every %d seconds.",
|
|
167
|
+
self.log_identifier,
|
|
168
|
+
health_check_interval_seconds,
|
|
169
|
+
)
|
|
170
|
+
self.add_timer(
|
|
171
|
+
delay_ms=health_check_interval_seconds * 1000,
|
|
172
|
+
timer_id=self.health_check_timer_id,
|
|
173
|
+
interval_ms=health_check_interval_seconds * 1000,
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
log.warning(
|
|
177
|
+
"%s Agent health check interval not configured or invalid, health checks will not run periodically.",
|
|
178
|
+
self.log_identifier,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
session_config = self._resolve_session_config()
|
|
182
|
+
if session_config.get("type") == "sql":
|
|
183
|
+
# SQL type explicitly configured - database_url is required
|
|
184
|
+
database_url = session_config.get("database_url")
|
|
185
|
+
if not database_url:
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"{self.log_identifier} Session service type is 'sql' but no database_url provided. "
|
|
188
|
+
"Please provide a database_url in the session_service configuration or use type 'memory'."
|
|
189
|
+
)
|
|
190
|
+
self.database_url = database_url
|
|
191
|
+
else:
|
|
192
|
+
# Memory storage or no explicit configuration - no persistence service needed
|
|
193
|
+
self.database_url = None
|
|
194
|
+
|
|
195
|
+
# Validate that features requiring runtime database persistence are not enabled without database
|
|
196
|
+
if self.database_url is None:
|
|
197
|
+
task_logging_config = self.get_config("task_logging", {})
|
|
198
|
+
if task_logging_config.get("enabled", False):
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"{self.log_identifier} Task logging requires SQL session storage. "
|
|
201
|
+
"Either set session_service.type='sql' with a valid database_url, "
|
|
202
|
+
"or disable task_logging.enabled."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
feedback_config = self.get_config("feedback_publishing", {})
|
|
206
|
+
if feedback_config.get("enabled", False):
|
|
207
|
+
log.warning(
|
|
208
|
+
"%s Feedback publishing is enabled but database persistence is not configured. "
|
|
209
|
+
"Feedback will only be published to the broker, not stored locally.",
|
|
210
|
+
self.log_identifier,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
platform_config = self.get_config("platform_service", {})
|
|
214
|
+
self.platform_database_url = platform_config.get("database_url")
|
|
215
|
+
component_config = self.get_config("component_config", {})
|
|
216
|
+
app_config = component_config.get("app_config", {})
|
|
217
|
+
|
|
218
|
+
self.session_manager = SessionManager(
|
|
219
|
+
secret_key=self.session_secret_key,
|
|
220
|
+
app_config=app_config,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
self.fastapi_app: FastAPI | None = None
|
|
224
|
+
self.uvicorn_server: uvicorn.Server | None = None
|
|
225
|
+
self.fastapi_thread: threading.Thread | None = None
|
|
226
|
+
self.fastapi_event_loop: asyncio.AbstractEventLoop | None = None
|
|
227
|
+
|
|
228
|
+
self._visualization_internal_app: SACApp | None = None
|
|
229
|
+
self._visualization_broker_input: BrokerInput | None = None
|
|
230
|
+
self._visualization_message_queue: queue.Queue = queue.Queue(maxsize=200)
|
|
231
|
+
self._task_logger_queue: queue.Queue = queue.Queue(maxsize=200)
|
|
232
|
+
self._active_visualization_streams: dict[str, dict[str, Any]] = {}
|
|
233
|
+
self._visualization_locks: dict[asyncio.AbstractEventLoop, asyncio.Lock] = {}
|
|
234
|
+
self._visualization_locks_lock = threading.Lock()
|
|
235
|
+
self._global_visualization_subscriptions: dict[str, int] = {}
|
|
236
|
+
self._visualization_processor_task: asyncio.Task | None = None
|
|
237
|
+
|
|
238
|
+
self._task_logger_internal_app: SACApp | None = None
|
|
239
|
+
self._task_logger_broker_input: BrokerInput | None = None
|
|
240
|
+
self._task_logger_processor_task: asyncio.Task | None = None
|
|
241
|
+
self.task_logger_service: TaskLoggerService | None = None
|
|
242
|
+
|
|
243
|
+
# Background task monitor
|
|
244
|
+
self.background_task_monitor = None
|
|
245
|
+
self._background_task_monitor_timer_id = None
|
|
246
|
+
|
|
247
|
+
# Initialize SAM Events service for system events
|
|
248
|
+
from ...common.sam_events import SamEventService
|
|
249
|
+
|
|
250
|
+
self.sam_events = SamEventService(
|
|
251
|
+
namespace=self.namespace,
|
|
252
|
+
component_name=f"{self.name}_gateway",
|
|
253
|
+
publish_func=self.publish_a2a,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Initialize data retention service and timer
|
|
257
|
+
self.data_retention_service = None
|
|
258
|
+
self._data_retention_timer_id = None
|
|
259
|
+
data_retention_config = self.get_config("data_retention", {})
|
|
260
|
+
if data_retention_config.get("enabled", True):
|
|
261
|
+
log.info(
|
|
262
|
+
"%s Data retention is enabled. Initializing service and timer...",
|
|
263
|
+
self.log_identifier,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Import and initialize the DataRetentionService
|
|
267
|
+
from .services.data_retention_service import DataRetentionService
|
|
268
|
+
|
|
269
|
+
session_factory = None
|
|
270
|
+
if self.database_url:
|
|
271
|
+
# SessionLocal will be initialized later in setup_dependencies
|
|
272
|
+
# We'll pass a lambda that returns SessionLocal when called
|
|
273
|
+
session_factory = lambda: (
|
|
274
|
+
dependencies.SessionLocal() if dependencies.SessionLocal else None
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
self.data_retention_service = DataRetentionService(
|
|
278
|
+
session_factory=session_factory, config=data_retention_config
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Create and start the cleanup timer
|
|
282
|
+
cleanup_interval_hours = data_retention_config.get(
|
|
283
|
+
"cleanup_interval_hours", 24
|
|
284
|
+
)
|
|
285
|
+
cleanup_interval_ms = cleanup_interval_hours * 60 * 60 * 1000
|
|
286
|
+
self._data_retention_timer_id = f"data_retention_cleanup_{self.gateway_id}"
|
|
287
|
+
|
|
288
|
+
self.add_timer(
|
|
289
|
+
delay_ms=cleanup_interval_ms,
|
|
290
|
+
timer_id=self._data_retention_timer_id,
|
|
291
|
+
interval_ms=cleanup_interval_ms,
|
|
292
|
+
)
|
|
293
|
+
log.info(
|
|
294
|
+
"%s Data retention timer created with ID '%s' and interval %d hours.",
|
|
295
|
+
self.log_identifier,
|
|
296
|
+
self._data_retention_timer_id,
|
|
297
|
+
cleanup_interval_hours,
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
log.info(
|
|
301
|
+
"%s Data retention is disabled via configuration.", self.log_identifier
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
log.info("%s Web UI Backend Component initialized.", self.log_identifier)
|
|
305
|
+
|
|
306
|
+
def process_event(self, event: Event):
|
|
307
|
+
if event.event_type == EventType.TIMER:
|
|
308
|
+
timer_id = event.data.get("timer_id")
|
|
309
|
+
|
|
310
|
+
if timer_id == self._sse_cleanup_timer_id:
|
|
311
|
+
log.debug("%s SSE buffer cleanup timer triggered.", self.log_identifier)
|
|
312
|
+
self.sse_event_buffer.cleanup_stale_buffers()
|
|
313
|
+
return
|
|
314
|
+
elif event.data.get("timer_id") == self.health_check_timer_id:
|
|
315
|
+
log.debug("%s Agent health check timer triggered.", self.log_identifier)
|
|
316
|
+
self._check_agent_health()
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
if timer_id == self._data_retention_timer_id:
|
|
320
|
+
log.debug(
|
|
321
|
+
"%s Data retention cleanup timer triggered.", self.log_identifier
|
|
322
|
+
)
|
|
323
|
+
if self.data_retention_service:
|
|
324
|
+
try:
|
|
325
|
+
self.data_retention_service.cleanup_old_data()
|
|
326
|
+
except Exception as e:
|
|
327
|
+
log.error(
|
|
328
|
+
"%s Error during data retention cleanup: %s",
|
|
329
|
+
self.log_identifier,
|
|
330
|
+
e,
|
|
331
|
+
exc_info=True,
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
log.warning(
|
|
335
|
+
"%s Data retention timer fired but service is not initialized.",
|
|
336
|
+
self.log_identifier,
|
|
337
|
+
)
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
if timer_id == self._background_task_monitor_timer_id:
|
|
341
|
+
log.debug("%s Background task monitor timer triggered.", self.log_identifier)
|
|
342
|
+
if self.background_task_monitor:
|
|
343
|
+
loop = self.get_async_loop()
|
|
344
|
+
if loop and loop.is_running():
|
|
345
|
+
asyncio.run_coroutine_threadsafe(
|
|
346
|
+
self.background_task_monitor.check_timeouts(),
|
|
347
|
+
loop
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
log.warning(
|
|
351
|
+
"%s Async loop not available for background task monitor.",
|
|
352
|
+
self.log_identifier
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
log.warning(
|
|
356
|
+
"%s Background task monitor timer fired but service is not initialized.",
|
|
357
|
+
self.log_identifier,
|
|
358
|
+
)
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
super().process_event(event)
|
|
362
|
+
|
|
363
|
+
def _get_visualization_lock(self) -> asyncio.Lock:
|
|
364
|
+
"""Get or create a visualization lock for the current event loop."""
|
|
365
|
+
try:
|
|
366
|
+
current_loop = asyncio.get_running_loop()
|
|
367
|
+
except RuntimeError:
|
|
368
|
+
raise RuntimeError(
|
|
369
|
+
"Visualization lock methods must be called from within an async context"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
with self._visualization_locks_lock:
|
|
373
|
+
if current_loop not in self._visualization_locks:
|
|
374
|
+
self._visualization_locks[current_loop] = asyncio.Lock()
|
|
375
|
+
log.debug(
|
|
376
|
+
"%s Created new visualization lock for event loop %s",
|
|
377
|
+
self.log_identifier,
|
|
378
|
+
id(current_loop),
|
|
379
|
+
)
|
|
380
|
+
return self._visualization_locks[current_loop]
|
|
381
|
+
|
|
382
|
+
def _ensure_visualization_flow_is_running(self) -> None:
|
|
383
|
+
"""
|
|
384
|
+
Ensures the internal SAC flow for A2A message visualization is created and running.
|
|
385
|
+
This method is designed to be called once during component startup.
|
|
386
|
+
"""
|
|
387
|
+
log_id_prefix = f"{self.log_identifier}[EnsureVizFlow]"
|
|
388
|
+
if self._visualization_internal_app is not None:
|
|
389
|
+
log.debug("%s Visualization flow already running.", log_id_prefix)
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
log.info("%s Initializing internal A2A visualization flow...", log_id_prefix)
|
|
393
|
+
try:
|
|
394
|
+
main_app = self.get_app()
|
|
395
|
+
if not main_app or not main_app.connector:
|
|
396
|
+
log.error(
|
|
397
|
+
"%s Cannot get main app or connector instance. Visualization flow NOT started.",
|
|
398
|
+
log_id_prefix,
|
|
399
|
+
)
|
|
400
|
+
raise RuntimeError(
|
|
401
|
+
"Main app or connector not available for internal flow creation."
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
main_broker_config = main_app.app_info.get("broker", {})
|
|
405
|
+
if not main_broker_config:
|
|
406
|
+
log.error(
|
|
407
|
+
"%s Main app broker configuration not found. Visualization flow NOT started.",
|
|
408
|
+
log_id_prefix,
|
|
409
|
+
)
|
|
410
|
+
raise ValueError("Main app broker configuration is missing.")
|
|
411
|
+
|
|
412
|
+
broker_input_cfg = {
|
|
413
|
+
"component_module": "broker_input",
|
|
414
|
+
"component_name": f"{self.gateway_id}_viz_broker_input",
|
|
415
|
+
"broker_queue_name": f"{self.namespace.strip('/')}/q/gdk/viz/{self.gateway_id}",
|
|
416
|
+
"create_queue_on_start": True,
|
|
417
|
+
"component_config": {
|
|
418
|
+
"broker_url": main_broker_config.get("broker_url"),
|
|
419
|
+
"broker_username": main_broker_config.get("broker_username"),
|
|
420
|
+
"broker_password": main_broker_config.get("broker_password"),
|
|
421
|
+
"broker_vpn": main_broker_config.get("broker_vpn"),
|
|
422
|
+
"trust_store_path": main_broker_config.get("trust_store_path"),
|
|
423
|
+
"dev_mode": main_broker_config.get("dev_mode"),
|
|
424
|
+
"broker_subscriptions": [],
|
|
425
|
+
"reconnection_strategy": main_broker_config.get(
|
|
426
|
+
"reconnection_strategy"
|
|
427
|
+
),
|
|
428
|
+
"retry_interval": main_broker_config.get("retry_interval"),
|
|
429
|
+
"retry_count": main_broker_config.get("retry_count"),
|
|
430
|
+
"temporary_queue": main_broker_config.get("temporary_queue", True),
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
forwarder_cfg = {
|
|
435
|
+
"component_class": VisualizationForwarderComponent,
|
|
436
|
+
"component_name": f"{self.gateway_id}_viz_forwarder",
|
|
437
|
+
"component_config": {
|
|
438
|
+
"target_queue_ref": self._visualization_message_queue
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
flow_config = {
|
|
443
|
+
"name": f"{self.gateway_id}_viz_flow",
|
|
444
|
+
"components": [broker_input_cfg, forwarder_cfg],
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
internal_app_broker_config = main_broker_config.copy()
|
|
448
|
+
internal_app_broker_config["input_enabled"] = True
|
|
449
|
+
internal_app_broker_config["output_enabled"] = False
|
|
450
|
+
|
|
451
|
+
app_config_for_internal_flow = {
|
|
452
|
+
"name": f"{self.gateway_id}_viz_internal_app",
|
|
453
|
+
"flows": [flow_config],
|
|
454
|
+
"broker": internal_app_broker_config,
|
|
455
|
+
"app_config": {},
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
self._visualization_internal_app = main_app.connector.create_internal_app(
|
|
459
|
+
app_name=app_config_for_internal_flow["name"],
|
|
460
|
+
flows=app_config_for_internal_flow["flows"],
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if (
|
|
464
|
+
not self._visualization_internal_app
|
|
465
|
+
or not self._visualization_internal_app.flows
|
|
466
|
+
):
|
|
467
|
+
log.error(
|
|
468
|
+
"%s Failed to create internal visualization app/flow.",
|
|
469
|
+
log_id_prefix,
|
|
470
|
+
)
|
|
471
|
+
self._visualization_internal_app = None
|
|
472
|
+
raise RuntimeError("Internal visualization app/flow creation failed.")
|
|
473
|
+
|
|
474
|
+
self._visualization_internal_app.run()
|
|
475
|
+
log.info("%s Internal visualization app started.", log_id_prefix)
|
|
476
|
+
|
|
477
|
+
flow_instance = self._visualization_internal_app.flows[0]
|
|
478
|
+
if flow_instance.component_groups and flow_instance.component_groups[0]:
|
|
479
|
+
self._visualization_broker_input = flow_instance.component_groups[0][0]
|
|
480
|
+
if not isinstance(self._visualization_broker_input, BrokerInput):
|
|
481
|
+
log.error(
|
|
482
|
+
"%s First component in viz flow is not BrokerInput. Type: %s",
|
|
483
|
+
log_id_prefix,
|
|
484
|
+
type(self._visualization_broker_input).__name__,
|
|
485
|
+
)
|
|
486
|
+
self._visualization_broker_input = None
|
|
487
|
+
raise RuntimeError(
|
|
488
|
+
"Visualization flow setup error: BrokerInput not found."
|
|
489
|
+
)
|
|
490
|
+
log.debug(
|
|
491
|
+
"%s Obtained reference to internal BrokerInput component.",
|
|
492
|
+
log_id_prefix,
|
|
493
|
+
)
|
|
494
|
+
else:
|
|
495
|
+
log.error(
|
|
496
|
+
"%s Could not get BrokerInput instance from internal flow.",
|
|
497
|
+
log_id_prefix,
|
|
498
|
+
)
|
|
499
|
+
raise RuntimeError(
|
|
500
|
+
"Visualization flow setup error: BrokerInput instance not accessible."
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
except Exception as e:
|
|
504
|
+
log.exception(
|
|
505
|
+
"%s Failed to ensure visualization flow is running: %s",
|
|
506
|
+
log_id_prefix,
|
|
507
|
+
e,
|
|
508
|
+
)
|
|
509
|
+
if self._visualization_internal_app:
|
|
510
|
+
try:
|
|
511
|
+
self._visualization_internal_app.cleanup()
|
|
512
|
+
except Exception as cleanup_err:
|
|
513
|
+
log.error(
|
|
514
|
+
"%s Error during cleanup after viz flow init failure: %s",
|
|
515
|
+
log_id_prefix,
|
|
516
|
+
cleanup_err,
|
|
517
|
+
)
|
|
518
|
+
self._visualization_internal_app = None
|
|
519
|
+
self._visualization_broker_input = None
|
|
520
|
+
raise
|
|
521
|
+
|
|
522
|
+
def _ensure_task_logger_flow_is_running(self) -> None:
|
|
523
|
+
"""
|
|
524
|
+
Ensures the internal SAC flow for A2A task logging is created and running.
|
|
525
|
+
"""
|
|
526
|
+
log_id_prefix = f"{self.log_identifier}[EnsureTaskLogFlow]"
|
|
527
|
+
if self._task_logger_internal_app is not None:
|
|
528
|
+
log.debug("%s Task logger flow already running.", log_id_prefix)
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
log.info("%s Initializing internal A2A task logger flow...", log_id_prefix)
|
|
532
|
+
try:
|
|
533
|
+
main_app = self.get_app()
|
|
534
|
+
if not main_app or not main_app.connector:
|
|
535
|
+
raise RuntimeError(
|
|
536
|
+
"Main app or connector not available for internal flow creation."
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
main_broker_config = main_app.app_info.get("broker", {})
|
|
540
|
+
if not main_broker_config:
|
|
541
|
+
raise ValueError("Main app broker configuration is missing.")
|
|
542
|
+
|
|
543
|
+
# The task logger needs to see ALL messages.
|
|
544
|
+
subscriptions = [{"topic": f"{self.namespace.rstrip('/')}/a2a/>"}]
|
|
545
|
+
|
|
546
|
+
broker_input_cfg = {
|
|
547
|
+
"component_module": "broker_input",
|
|
548
|
+
"component_name": f"{self.gateway_id}_task_log_broker_input",
|
|
549
|
+
"broker_queue_name": f"{self.namespace.strip('/')}/q/gdk/task_log/{self.gateway_id}",
|
|
550
|
+
"create_queue_on_start": True,
|
|
551
|
+
"component_config": {
|
|
552
|
+
"broker_url": main_broker_config.get("broker_url"),
|
|
553
|
+
"broker_username": main_broker_config.get("broker_username"),
|
|
554
|
+
"broker_password": main_broker_config.get("broker_password"),
|
|
555
|
+
"broker_vpn": main_broker_config.get("broker_vpn"),
|
|
556
|
+
"trust_store_path": main_broker_config.get("trust_store_path"),
|
|
557
|
+
"dev_mode": main_broker_config.get("dev_mode"),
|
|
558
|
+
"broker_subscriptions": subscriptions,
|
|
559
|
+
"reconnection_strategy": main_broker_config.get(
|
|
560
|
+
"reconnection_strategy"
|
|
561
|
+
),
|
|
562
|
+
"retry_interval": main_broker_config.get("retry_interval"),
|
|
563
|
+
"retry_count": main_broker_config.get("retry_count"),
|
|
564
|
+
"temporary_queue": main_broker_config.get("temporary_queue", True),
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
forwarder_cfg = {
|
|
569
|
+
"component_class": TaskLoggerForwarderComponent,
|
|
570
|
+
"component_name": f"{self.gateway_id}_task_log_forwarder",
|
|
571
|
+
"component_config": {"target_queue_ref": self._task_logger_queue},
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
flow_config = {
|
|
575
|
+
"name": f"{self.gateway_id}_task_log_flow",
|
|
576
|
+
"components": [broker_input_cfg, forwarder_cfg],
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
internal_app_broker_config = main_broker_config.copy()
|
|
580
|
+
internal_app_broker_config["input_enabled"] = True
|
|
581
|
+
internal_app_broker_config["output_enabled"] = False
|
|
582
|
+
|
|
583
|
+
app_config_for_internal_flow = {
|
|
584
|
+
"name": f"{self.gateway_id}_task_log_internal_app",
|
|
585
|
+
"flows": [flow_config],
|
|
586
|
+
"broker": internal_app_broker_config,
|
|
587
|
+
"app_config": {},
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
self._task_logger_internal_app = main_app.connector.create_internal_app(
|
|
591
|
+
app_name=app_config_for_internal_flow["name"],
|
|
592
|
+
flows=app_config_for_internal_flow["flows"],
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if (
|
|
596
|
+
not self._task_logger_internal_app
|
|
597
|
+
or not self._task_logger_internal_app.flows
|
|
598
|
+
):
|
|
599
|
+
raise RuntimeError("Internal task logger app/flow creation failed.")
|
|
600
|
+
|
|
601
|
+
self._task_logger_internal_app.run()
|
|
602
|
+
log.info("%s Internal task logger app started.", log_id_prefix)
|
|
603
|
+
|
|
604
|
+
flow_instance = self._task_logger_internal_app.flows[0]
|
|
605
|
+
if flow_instance.component_groups and flow_instance.component_groups[0]:
|
|
606
|
+
self._task_logger_broker_input = flow_instance.component_groups[0][0]
|
|
607
|
+
if not isinstance(self._task_logger_broker_input, BrokerInput):
|
|
608
|
+
raise RuntimeError(
|
|
609
|
+
"Task logger flow setup error: BrokerInput not found."
|
|
610
|
+
)
|
|
611
|
+
log.info(
|
|
612
|
+
"%s Obtained reference to internal task logger BrokerInput component.",
|
|
613
|
+
log_id_prefix,
|
|
614
|
+
)
|
|
615
|
+
else:
|
|
616
|
+
raise RuntimeError(
|
|
617
|
+
"Task logger flow setup error: BrokerInput instance not accessible."
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
log.exception(
|
|
622
|
+
"%s Failed to ensure task logger flow is running: %s", log_id_prefix, e
|
|
623
|
+
)
|
|
624
|
+
if self._task_logger_internal_app:
|
|
625
|
+
try:
|
|
626
|
+
self._task_logger_internal_app.cleanup()
|
|
627
|
+
except Exception as cleanup_err:
|
|
628
|
+
log.error(
|
|
629
|
+
"%s Error during cleanup after task logger flow init failure: %s",
|
|
630
|
+
log_id_prefix,
|
|
631
|
+
cleanup_err,
|
|
632
|
+
)
|
|
633
|
+
self._task_logger_internal_app = None
|
|
634
|
+
self._task_logger_broker_input = None
|
|
635
|
+
raise
|
|
636
|
+
|
|
637
|
+
def _resolve_session_config(self) -> dict:
|
|
638
|
+
"""
|
|
639
|
+
Resolve session service configuration with backward compatibility.
|
|
640
|
+
|
|
641
|
+
Priority order:
|
|
642
|
+
1. Component-specific session_service config (new approach)
|
|
643
|
+
2. Shared default_session_service config (deprecated, with warning)
|
|
644
|
+
3. Hardcoded default (SQLite for Web UI)
|
|
645
|
+
"""
|
|
646
|
+
# Check component-specific session_service config first
|
|
647
|
+
component_session_config = self.get_config("session_service")
|
|
648
|
+
if component_session_config:
|
|
649
|
+
log.debug("Using component-specific session_service configuration")
|
|
650
|
+
return component_session_config
|
|
651
|
+
|
|
652
|
+
# Backward compatibility: check shared config
|
|
653
|
+
shared_session_config = self.get_config("default_session_service")
|
|
654
|
+
if shared_session_config:
|
|
655
|
+
log.warning(
|
|
656
|
+
"Using session_service from shared config is deprecated. "
|
|
657
|
+
"Move to component-specific configuration in app_config.session_service"
|
|
658
|
+
)
|
|
659
|
+
return shared_session_config
|
|
660
|
+
|
|
661
|
+
# Default configuration for Web UI (backward compatibility)
|
|
662
|
+
default_config = {"type": "memory", "default_behavior": "PERSISTENT"}
|
|
663
|
+
log.info(
|
|
664
|
+
"Using default memory session configuration for Web UI (backward compatibility)"
|
|
665
|
+
)
|
|
666
|
+
return default_config
|
|
667
|
+
|
|
668
|
+
async def _visualization_message_processor_loop(self) -> None:
|
|
669
|
+
"""
|
|
670
|
+
Asynchronously consumes messages from the _visualization_message_queue,
|
|
671
|
+
filters them, and forwards them to relevant SSE connections.
|
|
672
|
+
"""
|
|
673
|
+
log_id_prefix = f"{self.log_identifier}[VizMsgProcessor]"
|
|
674
|
+
log.info("%s Starting visualization message processor loop...", log_id_prefix)
|
|
675
|
+
loop = asyncio.get_running_loop()
|
|
676
|
+
|
|
677
|
+
while not self.stop_signal.is_set():
|
|
678
|
+
msg_data = None
|
|
679
|
+
try:
|
|
680
|
+
msg_data = await loop.run_in_executor(
|
|
681
|
+
None,
|
|
682
|
+
self._visualization_message_queue.get,
|
|
683
|
+
True,
|
|
684
|
+
1.0,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
if msg_data is None:
|
|
688
|
+
log.info(
|
|
689
|
+
"%s Received shutdown signal for viz processor loop.",
|
|
690
|
+
log_id_prefix,
|
|
691
|
+
)
|
|
692
|
+
break
|
|
693
|
+
|
|
694
|
+
current_size = self._visualization_message_queue.qsize()
|
|
695
|
+
max_size = self._visualization_message_queue.maxsize
|
|
696
|
+
if max_size > 0 and (current_size / max_size) > 0.90:
|
|
697
|
+
log.warning(
|
|
698
|
+
"%s Visualization message queue is over 90%% full. Current size: %d/%d",
|
|
699
|
+
log_id_prefix,
|
|
700
|
+
current_size,
|
|
701
|
+
max_size,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
topic = msg_data.get("topic")
|
|
705
|
+
payload_dict = msg_data.get("payload")
|
|
706
|
+
|
|
707
|
+
log.debug("%s [VIZ_DATA_RAW] Topic: %s", log_id_prefix, topic)
|
|
708
|
+
|
|
709
|
+
if "/a2a/v1/discovery/" in topic:
|
|
710
|
+
self._visualization_message_queue.task_done()
|
|
711
|
+
continue
|
|
712
|
+
|
|
713
|
+
event_details_for_owner = self._infer_visualization_event_details(
|
|
714
|
+
topic, payload_dict
|
|
715
|
+
)
|
|
716
|
+
task_id_for_context = event_details_for_owner.get("task_id")
|
|
717
|
+
message_owner_id = None
|
|
718
|
+
if task_id_for_context:
|
|
719
|
+
root_task_id = task_id_for_context.split(":", 1)[0]
|
|
720
|
+
context = self.task_context_manager.get_context(root_task_id)
|
|
721
|
+
if context and "user_identity" in context:
|
|
722
|
+
message_owner_id = context["user_identity"].get("id")
|
|
723
|
+
log.debug(
|
|
724
|
+
"%s Found owner '%s' for task %s via local context (root: %s).",
|
|
725
|
+
log_id_prefix,
|
|
726
|
+
message_owner_id,
|
|
727
|
+
task_id_for_context,
|
|
728
|
+
root_task_id,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
if not message_owner_id:
|
|
732
|
+
user_properties = msg_data.get("user_properties") or {}
|
|
733
|
+
|
|
734
|
+
if not user_properties:
|
|
735
|
+
log.warning(
|
|
736
|
+
"%s No user_properties found for task %s (root: %s). Cannot determine owner via message properties.",
|
|
737
|
+
log_id_prefix,
|
|
738
|
+
task_id_for_context,
|
|
739
|
+
root_task_id,
|
|
740
|
+
)
|
|
741
|
+
user_config = user_properties.get(
|
|
742
|
+
"a2aUserConfig"
|
|
743
|
+
) or user_properties.get("a2a_user_config")
|
|
744
|
+
|
|
745
|
+
if (
|
|
746
|
+
isinstance(user_config, dict)
|
|
747
|
+
and "user_profile" in user_config
|
|
748
|
+
and isinstance(user_config.get("user_profile"), dict)
|
|
749
|
+
):
|
|
750
|
+
message_owner_id = user_config["user_profile"].get("id")
|
|
751
|
+
if message_owner_id:
|
|
752
|
+
log.debug(
|
|
753
|
+
"%s Found owner '%s' for task %s via message properties.",
|
|
754
|
+
log_id_prefix,
|
|
755
|
+
message_owner_id,
|
|
756
|
+
task_id_for_context,
|
|
757
|
+
)
|
|
758
|
+
async with self._get_visualization_lock():
|
|
759
|
+
for (
|
|
760
|
+
stream_id,
|
|
761
|
+
stream_config,
|
|
762
|
+
) in self._active_visualization_streams.items():
|
|
763
|
+
sse_queue_for_stream = stream_config.get("sse_queue")
|
|
764
|
+
if not sse_queue_for_stream:
|
|
765
|
+
log.warning(
|
|
766
|
+
"%s SSE queue not found for stream %s. Skipping.",
|
|
767
|
+
log_id_prefix,
|
|
768
|
+
stream_id,
|
|
769
|
+
)
|
|
770
|
+
continue
|
|
771
|
+
|
|
772
|
+
is_permitted = False
|
|
773
|
+
stream_owner_id = stream_config.get("user_id")
|
|
774
|
+
abstract_targets = stream_config.get("abstract_targets", [])
|
|
775
|
+
|
|
776
|
+
for abstract_target in abstract_targets:
|
|
777
|
+
if abstract_target.status != "subscribed":
|
|
778
|
+
continue
|
|
779
|
+
|
|
780
|
+
if abstract_target.type == "my_a2a_messages":
|
|
781
|
+
if (
|
|
782
|
+
stream_owner_id
|
|
783
|
+
and message_owner_id
|
|
784
|
+
and stream_owner_id == message_owner_id
|
|
785
|
+
):
|
|
786
|
+
is_permitted = True
|
|
787
|
+
break
|
|
788
|
+
else:
|
|
789
|
+
subscribed_topics_for_stream = stream_config.get(
|
|
790
|
+
"solace_topics", set()
|
|
791
|
+
)
|
|
792
|
+
if any(
|
|
793
|
+
a2a.topic_matches_subscription(topic, pattern)
|
|
794
|
+
for pattern in subscribed_topics_for_stream
|
|
795
|
+
):
|
|
796
|
+
is_permitted = True
|
|
797
|
+
break
|
|
798
|
+
|
|
799
|
+
if is_permitted:
|
|
800
|
+
event_details = self._infer_visualization_event_details(
|
|
801
|
+
topic, payload_dict
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
sse_event_payload = {
|
|
805
|
+
"event_type": "a2a_message",
|
|
806
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
807
|
+
"solace_topic": topic,
|
|
808
|
+
"direction": event_details["direction"],
|
|
809
|
+
"source_entity": event_details["source_entity"],
|
|
810
|
+
"target_entity": event_details["target_entity"],
|
|
811
|
+
"message_id": event_details["message_id"],
|
|
812
|
+
"task_id": event_details["task_id"],
|
|
813
|
+
"payload_summary": event_details["payload_summary"],
|
|
814
|
+
"full_payload": payload_dict,
|
|
815
|
+
"debug_type": event_details["debug_type"],
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
try:
|
|
819
|
+
log.debug(
|
|
820
|
+
"%s Attempting to put message on SSE queue for stream %s. Queue size: %d",
|
|
821
|
+
log_id_prefix,
|
|
822
|
+
stream_id,
|
|
823
|
+
sse_queue_for_stream.qsize(),
|
|
824
|
+
)
|
|
825
|
+
sse_queue_for_stream.put_nowait(
|
|
826
|
+
{
|
|
827
|
+
"event": "a2a_message",
|
|
828
|
+
"data": json.dumps(sse_event_payload),
|
|
829
|
+
}
|
|
830
|
+
)
|
|
831
|
+
log.debug(
|
|
832
|
+
"%s [VIZ_DATA_SENT] Stream %s: Topic: %s, Direction: %s",
|
|
833
|
+
log_id_prefix,
|
|
834
|
+
stream_id,
|
|
835
|
+
topic,
|
|
836
|
+
event_details["direction"],
|
|
837
|
+
)
|
|
838
|
+
except asyncio.QueueFull:
|
|
839
|
+
# Check if this is a background task
|
|
840
|
+
is_background = False
|
|
841
|
+
if task_id_for_context and self.database_url:
|
|
842
|
+
try:
|
|
843
|
+
from .repository.task_repository import TaskRepository
|
|
844
|
+
db = dependencies.SessionLocal()
|
|
845
|
+
try:
|
|
846
|
+
repo = TaskRepository()
|
|
847
|
+
task = repo.find_by_id(db, task_id_for_context)
|
|
848
|
+
is_background = task and task.background_execution_enabled
|
|
849
|
+
finally:
|
|
850
|
+
db.close()
|
|
851
|
+
except Exception:
|
|
852
|
+
pass
|
|
853
|
+
|
|
854
|
+
if is_background:
|
|
855
|
+
log.debug(
|
|
856
|
+
"%s SSE queue full for stream %s. Dropping visualization message for background task %s.",
|
|
857
|
+
log_id_prefix,
|
|
858
|
+
stream_id,
|
|
859
|
+
task_id_for_context,
|
|
860
|
+
)
|
|
861
|
+
else:
|
|
862
|
+
log.warning(
|
|
863
|
+
"%s SSE queue full for stream %s. Visualization message dropped.",
|
|
864
|
+
log_id_prefix,
|
|
865
|
+
stream_id,
|
|
866
|
+
)
|
|
867
|
+
except Exception as send_err:
|
|
868
|
+
log.error(
|
|
869
|
+
"%s Error sending formatted message to SSE queue for stream %s: %s",
|
|
870
|
+
log_id_prefix,
|
|
871
|
+
stream_id,
|
|
872
|
+
send_err,
|
|
873
|
+
)
|
|
874
|
+
else:
|
|
875
|
+
pass
|
|
876
|
+
|
|
877
|
+
self._visualization_message_queue.task_done()
|
|
878
|
+
|
|
879
|
+
except queue.Empty:
|
|
880
|
+
continue
|
|
881
|
+
except asyncio.CancelledError:
|
|
882
|
+
log.info(
|
|
883
|
+
"%s Visualization message processor loop cancelled.", log_id_prefix
|
|
884
|
+
)
|
|
885
|
+
break
|
|
886
|
+
except Exception as e:
|
|
887
|
+
log.exception(
|
|
888
|
+
"%s Error in visualization message processor loop: %s",
|
|
889
|
+
log_id_prefix,
|
|
890
|
+
e,
|
|
891
|
+
)
|
|
892
|
+
if msg_data and self._visualization_message_queue:
|
|
893
|
+
self._visualization_message_queue.task_done()
|
|
894
|
+
await asyncio.sleep(1)
|
|
895
|
+
|
|
896
|
+
log.info("%s Visualization message processor loop finished.", log_id_prefix)
|
|
897
|
+
|
|
898
|
+
async def _task_logger_loop(self) -> None:
|
|
899
|
+
"""
|
|
900
|
+
Asynchronously consumes messages from the _task_logger_queue and
|
|
901
|
+
passes them to the TaskLoggerService for persistence.
|
|
902
|
+
"""
|
|
903
|
+
log_id_prefix = f"{self.log_identifier}[TaskLoggerLoop]"
|
|
904
|
+
log.info("%s Starting task logger loop...", log_id_prefix)
|
|
905
|
+
loop = asyncio.get_running_loop()
|
|
906
|
+
|
|
907
|
+
while not self.stop_signal.is_set():
|
|
908
|
+
msg_data = None
|
|
909
|
+
try:
|
|
910
|
+
msg_data = await loop.run_in_executor(
|
|
911
|
+
None,
|
|
912
|
+
self._task_logger_queue.get,
|
|
913
|
+
True,
|
|
914
|
+
1.0,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
if msg_data is None:
|
|
918
|
+
log.info(
|
|
919
|
+
"%s Received shutdown signal for task logger loop.",
|
|
920
|
+
log_id_prefix,
|
|
921
|
+
)
|
|
922
|
+
break
|
|
923
|
+
|
|
924
|
+
if self.task_logger_service:
|
|
925
|
+
self.task_logger_service.log_event(msg_data)
|
|
926
|
+
else:
|
|
927
|
+
log.warning(
|
|
928
|
+
"%s Task logger service not available. Cannot log event.",
|
|
929
|
+
log_id_prefix,
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
self._task_logger_queue.task_done()
|
|
933
|
+
|
|
934
|
+
except queue.Empty:
|
|
935
|
+
continue
|
|
936
|
+
except asyncio.CancelledError:
|
|
937
|
+
log.info("%s Task logger loop cancelled.", log_id_prefix)
|
|
938
|
+
break
|
|
939
|
+
except Exception as e:
|
|
940
|
+
log.exception(
|
|
941
|
+
"%s Error in task logger loop: %s",
|
|
942
|
+
log_id_prefix,
|
|
943
|
+
e,
|
|
944
|
+
)
|
|
945
|
+
if msg_data and self._task_logger_queue:
|
|
946
|
+
self._task_logger_queue.task_done()
|
|
947
|
+
await asyncio.sleep(1)
|
|
948
|
+
|
|
949
|
+
log.info("%s Task logger loop finished.", log_id_prefix)
|
|
950
|
+
|
|
951
|
+
async def _add_visualization_subscription(
|
|
952
|
+
self, topic_str: str, stream_id: str
|
|
953
|
+
) -> bool:
|
|
954
|
+
"""
|
|
955
|
+
Adds a Solace topic subscription to the internal BrokerInput for visualization.
|
|
956
|
+
Manages global subscription reference counts.
|
|
957
|
+
"""
|
|
958
|
+
log_id_prefix = f"{self.log_identifier}[AddVizSub:{stream_id}]"
|
|
959
|
+
log.debug(
|
|
960
|
+
"%s Attempting to add subscription to topic: %s", log_id_prefix, topic_str
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
if not self._visualization_broker_input:
|
|
964
|
+
log.error(
|
|
965
|
+
"%s Visualization BrokerInput is not initialized. Cannot add subscription.",
|
|
966
|
+
log_id_prefix,
|
|
967
|
+
)
|
|
968
|
+
return False
|
|
969
|
+
if (
|
|
970
|
+
not hasattr(self._visualization_broker_input, "messaging_service")
|
|
971
|
+
or not self._visualization_broker_input.messaging_service
|
|
972
|
+
):
|
|
973
|
+
log.error(
|
|
974
|
+
"%s Visualization BrokerInput's messaging_service not available or not initialized. Cannot add subscription.",
|
|
975
|
+
log_id_prefix,
|
|
976
|
+
)
|
|
977
|
+
return False
|
|
978
|
+
|
|
979
|
+
log.debug(
|
|
980
|
+
"%s Acquiring visualization stream lock for topic '%s'...",
|
|
981
|
+
log_id_prefix,
|
|
982
|
+
topic_str,
|
|
983
|
+
)
|
|
984
|
+
async with self._get_visualization_lock():
|
|
985
|
+
log.debug(
|
|
986
|
+
"%s Acquired visualization stream lock for topic '%s'.",
|
|
987
|
+
log_id_prefix,
|
|
988
|
+
topic_str,
|
|
989
|
+
)
|
|
990
|
+
self._global_visualization_subscriptions[topic_str] = (
|
|
991
|
+
self._global_visualization_subscriptions.get(topic_str, 0) + 1
|
|
992
|
+
)
|
|
993
|
+
log.debug(
|
|
994
|
+
"%s Global subscription count for topic '%s' is now %d.",
|
|
995
|
+
log_id_prefix,
|
|
996
|
+
topic_str,
|
|
997
|
+
self._global_visualization_subscriptions[topic_str],
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
if self._global_visualization_subscriptions[topic_str] == 1:
|
|
1001
|
+
log.info(
|
|
1002
|
+
"%s First global subscription for topic '%s'. Attempting to subscribe on broker.",
|
|
1003
|
+
log_id_prefix,
|
|
1004
|
+
topic_str,
|
|
1005
|
+
)
|
|
1006
|
+
try:
|
|
1007
|
+
if not hasattr(
|
|
1008
|
+
self._visualization_broker_input, "add_subscription"
|
|
1009
|
+
) or not callable(
|
|
1010
|
+
self._visualization_broker_input.add_subscription
|
|
1011
|
+
):
|
|
1012
|
+
log.error(
|
|
1013
|
+
"%s Visualization BrokerInput does not support dynamic 'add_subscription'. "
|
|
1014
|
+
"Please upgrade the 'solace-ai-connector' module. Cannot add subscription '%s'.",
|
|
1015
|
+
log_id_prefix,
|
|
1016
|
+
topic_str,
|
|
1017
|
+
)
|
|
1018
|
+
self._global_visualization_subscriptions[topic_str] -= 1
|
|
1019
|
+
if self._global_visualization_subscriptions[topic_str] == 0:
|
|
1020
|
+
del self._global_visualization_subscriptions[topic_str]
|
|
1021
|
+
return False
|
|
1022
|
+
|
|
1023
|
+
loop = asyncio.get_event_loop()
|
|
1024
|
+
add_result = await loop.run_in_executor(
|
|
1025
|
+
None,
|
|
1026
|
+
self._visualization_broker_input.add_subscription,
|
|
1027
|
+
topic_str,
|
|
1028
|
+
)
|
|
1029
|
+
if not add_result:
|
|
1030
|
+
log.error(
|
|
1031
|
+
"%s Failed to add subscription '%s' via BrokerInput.",
|
|
1032
|
+
log_id_prefix,
|
|
1033
|
+
topic_str,
|
|
1034
|
+
)
|
|
1035
|
+
self._global_visualization_subscriptions[topic_str] -= 1
|
|
1036
|
+
if self._global_visualization_subscriptions[topic_str] == 0:
|
|
1037
|
+
del self._global_visualization_subscriptions[topic_str]
|
|
1038
|
+
return False
|
|
1039
|
+
log.info(
|
|
1040
|
+
"%s Successfully added subscription '%s' via BrokerInput.",
|
|
1041
|
+
log_id_prefix,
|
|
1042
|
+
topic_str,
|
|
1043
|
+
)
|
|
1044
|
+
except Exception as e:
|
|
1045
|
+
log.exception(
|
|
1046
|
+
"%s Exception calling BrokerInput.add_subscription for topic '%s': %s",
|
|
1047
|
+
log_id_prefix,
|
|
1048
|
+
topic_str,
|
|
1049
|
+
e,
|
|
1050
|
+
)
|
|
1051
|
+
self._global_visualization_subscriptions[topic_str] -= 1
|
|
1052
|
+
if self._global_visualization_subscriptions[topic_str] == 0:
|
|
1053
|
+
del self._global_visualization_subscriptions[topic_str]
|
|
1054
|
+
return False
|
|
1055
|
+
else:
|
|
1056
|
+
log.debug(
|
|
1057
|
+
"%s Topic '%s' already globally subscribed. Skipping broker subscribe.",
|
|
1058
|
+
log_id_prefix,
|
|
1059
|
+
topic_str,
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
if stream_id in self._active_visualization_streams:
|
|
1063
|
+
self._active_visualization_streams[stream_id]["solace_topics"].add(
|
|
1064
|
+
topic_str
|
|
1065
|
+
)
|
|
1066
|
+
log.debug(
|
|
1067
|
+
"%s Topic '%s' added to active subscriptions for stream %s.",
|
|
1068
|
+
log_id_prefix,
|
|
1069
|
+
topic_str,
|
|
1070
|
+
stream_id,
|
|
1071
|
+
)
|
|
1072
|
+
else:
|
|
1073
|
+
log.warning(
|
|
1074
|
+
"%s Stream ID %s not found in active streams. Cannot add topic.",
|
|
1075
|
+
log_id_prefix,
|
|
1076
|
+
stream_id,
|
|
1077
|
+
)
|
|
1078
|
+
return False
|
|
1079
|
+
log.debug(
|
|
1080
|
+
"%s Releasing visualization stream lock after successful processing for topic '%s'.",
|
|
1081
|
+
log_id_prefix,
|
|
1082
|
+
topic_str,
|
|
1083
|
+
)
|
|
1084
|
+
return True
|
|
1085
|
+
|
|
1086
|
+
async def _remove_visualization_subscription_nolock(
|
|
1087
|
+
self, topic_str: str, stream_id: str
|
|
1088
|
+
) -> bool:
|
|
1089
|
+
"""
|
|
1090
|
+
Internal helper to remove a Solace topic subscription.
|
|
1091
|
+
Assumes _visualization_stream_lock is already held by the caller.
|
|
1092
|
+
Manages global subscription reference counts.
|
|
1093
|
+
"""
|
|
1094
|
+
log_id_prefix = f"{self.log_identifier}[RemoveVizSubNL:{stream_id}]"
|
|
1095
|
+
log.info(
|
|
1096
|
+
"%s Removing subscription (no-lock) from topic: %s",
|
|
1097
|
+
log_id_prefix,
|
|
1098
|
+
topic_str,
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
if not self._visualization_broker_input or not hasattr(
|
|
1102
|
+
self._visualization_broker_input, "messaging_service"
|
|
1103
|
+
):
|
|
1104
|
+
log.error(
|
|
1105
|
+
"%s Visualization BrokerInput or its messaging_service not available.",
|
|
1106
|
+
log_id_prefix,
|
|
1107
|
+
)
|
|
1108
|
+
return False
|
|
1109
|
+
|
|
1110
|
+
if topic_str not in self._global_visualization_subscriptions:
|
|
1111
|
+
log.warning(
|
|
1112
|
+
"%s Topic '%s' not found in global subscriptions. Cannot remove.",
|
|
1113
|
+
log_id_prefix,
|
|
1114
|
+
topic_str,
|
|
1115
|
+
)
|
|
1116
|
+
return False
|
|
1117
|
+
|
|
1118
|
+
self._global_visualization_subscriptions[topic_str] -= 1
|
|
1119
|
+
|
|
1120
|
+
if self._global_visualization_subscriptions[topic_str] == 0:
|
|
1121
|
+
del self._global_visualization_subscriptions[topic_str]
|
|
1122
|
+
try:
|
|
1123
|
+
if not hasattr(
|
|
1124
|
+
self._visualization_broker_input, "remove_subscription"
|
|
1125
|
+
) or not callable(self._visualization_broker_input.remove_subscription):
|
|
1126
|
+
log.error(
|
|
1127
|
+
"%s Visualization BrokerInput does not support dynamic 'remove_subscription'. "
|
|
1128
|
+
"Please upgrade the 'solace-ai-connector' module. Cannot remove subscription '%s'.",
|
|
1129
|
+
log_id_prefix,
|
|
1130
|
+
topic_str,
|
|
1131
|
+
)
|
|
1132
|
+
return False
|
|
1133
|
+
|
|
1134
|
+
loop = asyncio.get_event_loop()
|
|
1135
|
+
remove_result = await loop.run_in_executor(
|
|
1136
|
+
None,
|
|
1137
|
+
self._visualization_broker_input.remove_subscription,
|
|
1138
|
+
topic_str,
|
|
1139
|
+
)
|
|
1140
|
+
if not remove_result:
|
|
1141
|
+
log.error(
|
|
1142
|
+
"%s Failed to remove subscription '%s' via BrokerInput. Global count might be inaccurate.",
|
|
1143
|
+
log_id_prefix,
|
|
1144
|
+
topic_str,
|
|
1145
|
+
)
|
|
1146
|
+
else:
|
|
1147
|
+
log.info(
|
|
1148
|
+
"%s Successfully removed subscription '%s' via BrokerInput.",
|
|
1149
|
+
log_id_prefix,
|
|
1150
|
+
topic_str,
|
|
1151
|
+
)
|
|
1152
|
+
except Exception as e:
|
|
1153
|
+
log.exception(
|
|
1154
|
+
"%s Exception calling BrokerInput.remove_subscription for topic '%s': %s",
|
|
1155
|
+
log_id_prefix,
|
|
1156
|
+
topic_str,
|
|
1157
|
+
e,
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
if stream_id in self._active_visualization_streams:
|
|
1161
|
+
if (
|
|
1162
|
+
topic_str
|
|
1163
|
+
in self._active_visualization_streams[stream_id]["solace_topics"]
|
|
1164
|
+
):
|
|
1165
|
+
self._active_visualization_streams[stream_id]["solace_topics"].remove(
|
|
1166
|
+
topic_str
|
|
1167
|
+
)
|
|
1168
|
+
log.debug(
|
|
1169
|
+
"%s Topic '%s' removed from active subscriptions for stream %s.",
|
|
1170
|
+
log_id_prefix,
|
|
1171
|
+
topic_str,
|
|
1172
|
+
stream_id,
|
|
1173
|
+
)
|
|
1174
|
+
else:
|
|
1175
|
+
log.warning(
|
|
1176
|
+
"%s Topic '%s' not found in subscriptions for stream %s.",
|
|
1177
|
+
log_id_prefix,
|
|
1178
|
+
topic_str,
|
|
1179
|
+
stream_id,
|
|
1180
|
+
)
|
|
1181
|
+
else:
|
|
1182
|
+
log.warning(
|
|
1183
|
+
"%s Stream ID %s not found in active streams. Cannot remove topic.",
|
|
1184
|
+
log_id_prefix,
|
|
1185
|
+
stream_id,
|
|
1186
|
+
)
|
|
1187
|
+
return True
|
|
1188
|
+
|
|
1189
|
+
async def _remove_visualization_subscription(
|
|
1190
|
+
self, topic_str: str, stream_id: str
|
|
1191
|
+
) -> bool:
|
|
1192
|
+
"""
|
|
1193
|
+
Public method to remove a Solace topic subscription.
|
|
1194
|
+
Acquires the lock before calling the internal no-lock version.
|
|
1195
|
+
"""
|
|
1196
|
+
log_id_prefix = f"{self.log_identifier}[RemoveVizSubPub:{stream_id}]"
|
|
1197
|
+
log.debug(
|
|
1198
|
+
"%s Acquiring lock to remove subscription for topic: %s",
|
|
1199
|
+
log_id_prefix,
|
|
1200
|
+
topic_str,
|
|
1201
|
+
)
|
|
1202
|
+
async with self._get_visualization_lock():
|
|
1203
|
+
log.debug("%s Lock acquired for topic: %s", log_id_prefix, topic_str)
|
|
1204
|
+
result = await self._remove_visualization_subscription_nolock(
|
|
1205
|
+
topic_str, stream_id
|
|
1206
|
+
)
|
|
1207
|
+
log.debug("%s Releasing lock for topic: %s", log_id_prefix, topic_str)
|
|
1208
|
+
return result
|
|
1209
|
+
|
|
1210
|
+
async def _extract_initial_claims(
|
|
1211
|
+
self, external_event_data: Any
|
|
1212
|
+
) -> dict[str, Any] | None:
|
|
1213
|
+
"""
|
|
1214
|
+
Extracts initial identity claims from the incoming external event.
|
|
1215
|
+
For the WebUI, this means inspecting the FastAPIRequest.
|
|
1216
|
+
It prioritizes the authenticated user from `request.state.user`.
|
|
1217
|
+
"""
|
|
1218
|
+
log_id_prefix = f"{self.log_identifier}[ExtractClaims]"
|
|
1219
|
+
|
|
1220
|
+
if not isinstance(external_event_data, FastAPIRequest):
|
|
1221
|
+
log.warning(
|
|
1222
|
+
"%s Expected external_event_data to be a FastAPIRequest, but got %s.",
|
|
1223
|
+
log_id_prefix,
|
|
1224
|
+
type(external_event_data).__name__,
|
|
1225
|
+
)
|
|
1226
|
+
return None
|
|
1227
|
+
|
|
1228
|
+
request = external_event_data
|
|
1229
|
+
try:
|
|
1230
|
+
user_info = {}
|
|
1231
|
+
if hasattr(request.state, "user") and request.state.user:
|
|
1232
|
+
user_info = request.state.user
|
|
1233
|
+
username = user_info.get("username")
|
|
1234
|
+
if username:
|
|
1235
|
+
log.debug(
|
|
1236
|
+
"%s Extracted user '%s' from request.state.",
|
|
1237
|
+
log_id_prefix,
|
|
1238
|
+
username,
|
|
1239
|
+
)
|
|
1240
|
+
return {
|
|
1241
|
+
"id": username,
|
|
1242
|
+
"name": username,
|
|
1243
|
+
"email": username,
|
|
1244
|
+
"user_info": user_info,
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
log.debug(
|
|
1248
|
+
"%s No authenticated user in request.state, falling back to SessionManager.",
|
|
1249
|
+
log_id_prefix,
|
|
1250
|
+
)
|
|
1251
|
+
user_id = self.session_manager.get_a2a_client_id(request)
|
|
1252
|
+
log.debug(
|
|
1253
|
+
"%s Extracted user_id '%s' via SessionManager.", log_id_prefix, user_id
|
|
1254
|
+
)
|
|
1255
|
+
return {"id": user_id, "name": user_id, "user_info": user_info}
|
|
1256
|
+
|
|
1257
|
+
except Exception as e:
|
|
1258
|
+
log.error("%s Failed to extract user_id from request: %s", log_id_prefix, e)
|
|
1259
|
+
return None
|
|
1260
|
+
|
|
1261
|
+
def _start_fastapi_server(self):
|
|
1262
|
+
"""Starts the Uvicorn server in a separate thread."""
|
|
1263
|
+
log.info(
|
|
1264
|
+
"%s [_start_listener] Attempting to start FastAPI/Uvicorn server...",
|
|
1265
|
+
self.log_identifier,
|
|
1266
|
+
)
|
|
1267
|
+
if self.fastapi_thread and self.fastapi_thread.is_alive():
|
|
1268
|
+
log.warning(
|
|
1269
|
+
"%s FastAPI server thread already started.", self.log_identifier
|
|
1270
|
+
)
|
|
1271
|
+
return
|
|
1272
|
+
|
|
1273
|
+
try:
|
|
1274
|
+
from ...gateway.http_sse.main import app as fastapi_app_instance
|
|
1275
|
+
from ...gateway.http_sse.main import setup_dependencies
|
|
1276
|
+
|
|
1277
|
+
self.fastapi_app = fastapi_app_instance
|
|
1278
|
+
|
|
1279
|
+
setup_dependencies(self, self.database_url, self.platform_database_url)
|
|
1280
|
+
|
|
1281
|
+
# Instantiate services that depend on the database session factory.
|
|
1282
|
+
# This must be done *after* setup_dependencies has run.
|
|
1283
|
+
session_factory = dependencies.SessionLocal if self.database_url else None
|
|
1284
|
+
|
|
1285
|
+
# Initialize SSE manager with session factory for background task detection
|
|
1286
|
+
self.sse_manager = SSEManager(
|
|
1287
|
+
max_queue_size=self.sse_max_queue_size,
|
|
1288
|
+
event_buffer=self.sse_event_buffer,
|
|
1289
|
+
session_factory=session_factory
|
|
1290
|
+
)
|
|
1291
|
+
log.debug(
|
|
1292
|
+
"%s SSE manager initialized with database session factory.",
|
|
1293
|
+
self.log_identifier,
|
|
1294
|
+
)
|
|
1295
|
+
task_logging_config = self.get_config("task_logging", {})
|
|
1296
|
+
self.task_logger_service = TaskLoggerService(
|
|
1297
|
+
session_factory=session_factory, config=task_logging_config
|
|
1298
|
+
)
|
|
1299
|
+
log.debug(
|
|
1300
|
+
"%s Services dependent on database session factory have been initialized.",
|
|
1301
|
+
self.log_identifier,
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
# Initialize background task monitor if task logging is enabled
|
|
1305
|
+
if self.database_url and task_logging_config.get("enabled", False):
|
|
1306
|
+
from .services.background_task_monitor import BackgroundTaskMonitor
|
|
1307
|
+
from .services.task_service import TaskService
|
|
1308
|
+
|
|
1309
|
+
# Create task service for cancellation operations
|
|
1310
|
+
task_service = TaskService(
|
|
1311
|
+
core_a2a_service=self.core_a2a_service,
|
|
1312
|
+
publish_func=self.publish_a2a,
|
|
1313
|
+
namespace=self.namespace,
|
|
1314
|
+
gateway_id=self.gateway_id,
|
|
1315
|
+
sse_manager=self.sse_manager,
|
|
1316
|
+
task_context_map=self.task_context_manager._contexts,
|
|
1317
|
+
task_context_lock=self.task_context_manager._lock,
|
|
1318
|
+
app_name=self.name,
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
# Get timeout configuration
|
|
1322
|
+
background_config = self.get_config("background_tasks", {})
|
|
1323
|
+
default_timeout_ms = background_config.get("default_timeout_ms", 3600000) # 1 hour
|
|
1324
|
+
|
|
1325
|
+
self.background_task_monitor = BackgroundTaskMonitor(
|
|
1326
|
+
session_factory=session_factory,
|
|
1327
|
+
task_service=task_service,
|
|
1328
|
+
default_timeout_ms=default_timeout_ms,
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
# Create timer for periodic timeout checks
|
|
1332
|
+
monitor_interval_ms = background_config.get("monitor_interval_ms", 300000) # 5 minutes
|
|
1333
|
+
self._background_task_monitor_timer_id = f"background_task_monitor_{self.gateway_id}"
|
|
1334
|
+
|
|
1335
|
+
self.add_timer(
|
|
1336
|
+
delay_ms=monitor_interval_ms,
|
|
1337
|
+
timer_id=self._background_task_monitor_timer_id,
|
|
1338
|
+
interval_ms=monitor_interval_ms,
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1341
|
+
log.info(
|
|
1342
|
+
"%s Background task monitor initialized with %dms check interval and %dms default timeout",
|
|
1343
|
+
self.log_identifier,
|
|
1344
|
+
monitor_interval_ms,
|
|
1345
|
+
default_timeout_ms
|
|
1346
|
+
)
|
|
1347
|
+
else:
|
|
1348
|
+
log.info(
|
|
1349
|
+
"%s Background task monitor not initialized (task logging disabled or no database)",
|
|
1350
|
+
self.log_identifier
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
port = (
|
|
1354
|
+
self.fastapi_https_port
|
|
1355
|
+
if self.ssl_keyfile and self.ssl_certfile
|
|
1356
|
+
else self.fastapi_port
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
config = uvicorn.Config(
|
|
1360
|
+
app=self.fastapi_app,
|
|
1361
|
+
host=self.fastapi_host,
|
|
1362
|
+
port=port,
|
|
1363
|
+
log_level="warning",
|
|
1364
|
+
lifespan="on",
|
|
1365
|
+
ssl_keyfile=self.ssl_keyfile,
|
|
1366
|
+
ssl_certfile=self.ssl_certfile,
|
|
1367
|
+
ssl_keyfile_password=self.ssl_keyfile_password,
|
|
1368
|
+
log_config=None
|
|
1369
|
+
)
|
|
1370
|
+
self.uvicorn_server = uvicorn.Server(config)
|
|
1371
|
+
|
|
1372
|
+
@self.fastapi_app.on_event("startup")
|
|
1373
|
+
async def capture_event_loop():
|
|
1374
|
+
log.info(
|
|
1375
|
+
"%s [_start_listener] FastAPI startup event triggered.",
|
|
1376
|
+
self.log_identifier,
|
|
1377
|
+
)
|
|
1378
|
+
try:
|
|
1379
|
+
self.fastapi_event_loop = asyncio.get_running_loop()
|
|
1380
|
+
log.debug(
|
|
1381
|
+
"%s [_start_listener] Captured FastAPI event loop via startup event: %s",
|
|
1382
|
+
self.log_identifier,
|
|
1383
|
+
self.fastapi_event_loop,
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
if self.fastapi_event_loop:
|
|
1387
|
+
log.debug(
|
|
1388
|
+
"%s Ensuring visualization flow is running...",
|
|
1389
|
+
self.log_identifier,
|
|
1390
|
+
)
|
|
1391
|
+
self._ensure_visualization_flow_is_running()
|
|
1392
|
+
|
|
1393
|
+
if (
|
|
1394
|
+
self._visualization_processor_task is None
|
|
1395
|
+
or self._visualization_processor_task.done()
|
|
1396
|
+
):
|
|
1397
|
+
log.debug(
|
|
1398
|
+
"%s Starting visualization message processor task.",
|
|
1399
|
+
self.log_identifier,
|
|
1400
|
+
)
|
|
1401
|
+
self._visualization_processor_task = (
|
|
1402
|
+
self.fastapi_event_loop.create_task(
|
|
1403
|
+
self._visualization_message_processor_loop()
|
|
1404
|
+
)
|
|
1405
|
+
)
|
|
1406
|
+
else:
|
|
1407
|
+
log.debug(
|
|
1408
|
+
"%s Visualization message processor task already running.",
|
|
1409
|
+
self.log_identifier,
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
task_logging_config = self.get_config("task_logging", {})
|
|
1413
|
+
if task_logging_config.get("enabled", False):
|
|
1414
|
+
log.info(
|
|
1415
|
+
"%s Task logging is enabled. Ensuring flow is running...",
|
|
1416
|
+
self.log_identifier,
|
|
1417
|
+
)
|
|
1418
|
+
self._ensure_task_logger_flow_is_running()
|
|
1419
|
+
|
|
1420
|
+
if (
|
|
1421
|
+
self._task_logger_processor_task is None
|
|
1422
|
+
or self._task_logger_processor_task.done()
|
|
1423
|
+
):
|
|
1424
|
+
log.info(
|
|
1425
|
+
"%s Starting task logger processor task.",
|
|
1426
|
+
self.log_identifier,
|
|
1427
|
+
)
|
|
1428
|
+
self._task_logger_processor_task = (
|
|
1429
|
+
self.fastapi_event_loop.create_task(
|
|
1430
|
+
self._task_logger_loop()
|
|
1431
|
+
)
|
|
1432
|
+
)
|
|
1433
|
+
else:
|
|
1434
|
+
log.info(
|
|
1435
|
+
"%s Task logger processor task already running.",
|
|
1436
|
+
self.log_identifier,
|
|
1437
|
+
)
|
|
1438
|
+
else:
|
|
1439
|
+
log.info(
|
|
1440
|
+
"%s Task logging is disabled.", self.log_identifier
|
|
1441
|
+
)
|
|
1442
|
+
else:
|
|
1443
|
+
log.error(
|
|
1444
|
+
"%s FastAPI event loop not captured. Cannot start visualization processor.",
|
|
1445
|
+
self.log_identifier,
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
except Exception as startup_err:
|
|
1449
|
+
log.exception(
|
|
1450
|
+
"%s [_start_listener] Error during FastAPI startup event (capture_event_loop or viz setup): %s",
|
|
1451
|
+
self.log_identifier,
|
|
1452
|
+
startup_err,
|
|
1453
|
+
)
|
|
1454
|
+
self.stop_signal.set()
|
|
1455
|
+
|
|
1456
|
+
try:
|
|
1457
|
+
from solace_agent_mesh_enterprise.init_enterprise import (
|
|
1458
|
+
start_enterprise_background_tasks,
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
log.info(
|
|
1462
|
+
"%s Starting enterprise background tasks...",
|
|
1463
|
+
self.log_identifier,
|
|
1464
|
+
)
|
|
1465
|
+
await start_enterprise_background_tasks(self)
|
|
1466
|
+
log.info(
|
|
1467
|
+
"%s Enterprise background tasks started successfully",
|
|
1468
|
+
self.log_identifier,
|
|
1469
|
+
)
|
|
1470
|
+
except ImportError:
|
|
1471
|
+
log.debug(
|
|
1472
|
+
"%s Enterprise package not available - skipping background tasks",
|
|
1473
|
+
self.log_identifier,
|
|
1474
|
+
)
|
|
1475
|
+
except RuntimeError as enterprise_err:
|
|
1476
|
+
log.warning(
|
|
1477
|
+
"%s Enterprise background tasks disabled: %s - Community features will continue normally",
|
|
1478
|
+
self.log_identifier,
|
|
1479
|
+
enterprise_err,
|
|
1480
|
+
)
|
|
1481
|
+
except Exception as enterprise_err:
|
|
1482
|
+
log.error(
|
|
1483
|
+
"%s Failed to start enterprise background tasks: %s - Community features will continue normally",
|
|
1484
|
+
self.log_identifier,
|
|
1485
|
+
enterprise_err,
|
|
1486
|
+
exc_info=True,
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
@self.fastapi_app.on_event("shutdown")
|
|
1490
|
+
async def shutdown_event():
|
|
1491
|
+
log.info(
|
|
1492
|
+
"%s [_start_listener] FastAPI shutdown event triggered.",
|
|
1493
|
+
self.log_identifier,
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
try:
|
|
1497
|
+
from solace_agent_mesh_enterprise.init_enterprise import (
|
|
1498
|
+
stop_enterprise_background_tasks,
|
|
1499
|
+
)
|
|
1500
|
+
|
|
1501
|
+
log.info(
|
|
1502
|
+
"%s Stopping enterprise background tasks...",
|
|
1503
|
+
self.log_identifier,
|
|
1504
|
+
)
|
|
1505
|
+
await stop_enterprise_background_tasks()
|
|
1506
|
+
log.info(
|
|
1507
|
+
"%s Enterprise background tasks stopped", self.log_identifier
|
|
1508
|
+
)
|
|
1509
|
+
except ImportError:
|
|
1510
|
+
log.debug(
|
|
1511
|
+
"%s Enterprise package not available - no background tasks to stop",
|
|
1512
|
+
self.log_identifier,
|
|
1513
|
+
)
|
|
1514
|
+
except Exception as enterprise_err:
|
|
1515
|
+
log.error(
|
|
1516
|
+
"%s Failed to stop enterprise background tasks: %s",
|
|
1517
|
+
self.log_identifier,
|
|
1518
|
+
enterprise_err,
|
|
1519
|
+
exc_info=True,
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
self.fastapi_thread = threading.Thread(
|
|
1523
|
+
target=self.uvicorn_server.run, daemon=True, name="FastAPI_Thread"
|
|
1524
|
+
)
|
|
1525
|
+
self.fastapi_thread.start()
|
|
1526
|
+
protocol = "https" if self.ssl_keyfile and self.ssl_certfile else "http"
|
|
1527
|
+
log.info(
|
|
1528
|
+
"%s [_start_listener] FastAPI/Uvicorn server starting in background thread on %s://%s:%d",
|
|
1529
|
+
self.log_identifier,
|
|
1530
|
+
protocol,
|
|
1531
|
+
self.fastapi_host,
|
|
1532
|
+
port,
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
except Exception as e:
|
|
1536
|
+
log.error(
|
|
1537
|
+
"%s [_start_listener] Failed to start FastAPI/Uvicorn server: %s",
|
|
1538
|
+
self.log_identifier,
|
|
1539
|
+
e,
|
|
1540
|
+
)
|
|
1541
|
+
self.stop_signal.set()
|
|
1542
|
+
raise
|
|
1543
|
+
|
|
1544
|
+
def publish_a2a(
|
|
1545
|
+
self, topic: str, payload: dict, user_properties: dict | None = None
|
|
1546
|
+
):
|
|
1547
|
+
"""
|
|
1548
|
+
Publishes an A2A message using the SAC App's send_message method.
|
|
1549
|
+
This method can be called from FastAPI handlers (via dependency injection).
|
|
1550
|
+
It's thread-safe as it uses the SAC App instance.
|
|
1551
|
+
"""
|
|
1552
|
+
log.debug(f"[publish_a2a] Starting to publish message to topic: {topic}")
|
|
1553
|
+
log.debug(
|
|
1554
|
+
f"[publish_a2a] Payload type: {type(payload)}, size: {len(str(payload))} chars"
|
|
1555
|
+
)
|
|
1556
|
+
log.debug(f"[publish_a2a] User properties: {user_properties}")
|
|
1557
|
+
|
|
1558
|
+
try:
|
|
1559
|
+
super().publish_a2a_message(payload, topic, user_properties)
|
|
1560
|
+
log.debug(
|
|
1561
|
+
f"[publish_a2a] Successfully called super().publish_a2a_message for topic: {topic}"
|
|
1562
|
+
)
|
|
1563
|
+
except Exception as e:
|
|
1564
|
+
log.error(f"[publish_a2a] Exception in publish_a2a: {e}", exc_info=True)
|
|
1565
|
+
raise
|
|
1566
|
+
|
|
1567
|
+
def _cleanup_visualization_locks(self):
|
|
1568
|
+
"""Remove locks for closed event loops to prevent memory leaks."""
|
|
1569
|
+
with self._visualization_locks_lock:
|
|
1570
|
+
closed_loops = [
|
|
1571
|
+
loop for loop in self._visualization_locks if loop.is_closed()
|
|
1572
|
+
]
|
|
1573
|
+
for loop in closed_loops:
|
|
1574
|
+
del self._visualization_locks[loop]
|
|
1575
|
+
log.debug(
|
|
1576
|
+
"%s Cleaned up visualization lock for closed event loop %s",
|
|
1577
|
+
self.log_identifier,
|
|
1578
|
+
id(loop),
|
|
1579
|
+
)
|
|
1580
|
+
|
|
1581
|
+
def cleanup(self):
|
|
1582
|
+
"""Gracefully shuts down the component and the FastAPI server."""
|
|
1583
|
+
log.info("%s Cleaning up Web UI Backend Component...", self.log_identifier)
|
|
1584
|
+
|
|
1585
|
+
# Cancel timers
|
|
1586
|
+
self.cancel_timer(self._sse_cleanup_timer_id)
|
|
1587
|
+
if self._data_retention_timer_id:
|
|
1588
|
+
self.cancel_timer(self._data_retention_timer_id)
|
|
1589
|
+
log.info("%s Cancelled data retention cleanup timer.", self.log_identifier)
|
|
1590
|
+
|
|
1591
|
+
if self._background_task_monitor_timer_id:
|
|
1592
|
+
self.cancel_timer(self._background_task_monitor_timer_id)
|
|
1593
|
+
log.info("%s Cancelled background task monitor timer.", self.log_identifier)
|
|
1594
|
+
|
|
1595
|
+
# Clean up data retention service
|
|
1596
|
+
if self.data_retention_service:
|
|
1597
|
+
self.data_retention_service = None
|
|
1598
|
+
log.info("%s Data retention service cleaned up.", self.log_identifier)
|
|
1599
|
+
|
|
1600
|
+
# Clean up background task monitor
|
|
1601
|
+
if self.background_task_monitor:
|
|
1602
|
+
self.background_task_monitor = None
|
|
1603
|
+
log.info("%s Background task monitor cleaned up.", self.log_identifier)
|
|
1604
|
+
|
|
1605
|
+
self.cancel_timer(self.health_check_timer_id)
|
|
1606
|
+
log.info("%s Cleaning up visualization resources...", self.log_identifier)
|
|
1607
|
+
if self._visualization_message_queue:
|
|
1608
|
+
self._visualization_message_queue.put(None)
|
|
1609
|
+
if self._task_logger_queue:
|
|
1610
|
+
self._task_logger_queue.put(None)
|
|
1611
|
+
|
|
1612
|
+
if (
|
|
1613
|
+
self._visualization_processor_task
|
|
1614
|
+
and not self._visualization_processor_task.done()
|
|
1615
|
+
):
|
|
1616
|
+
log.info(
|
|
1617
|
+
"%s Cancelling visualization processor task...", self.log_identifier
|
|
1618
|
+
)
|
|
1619
|
+
self._visualization_processor_task.cancel()
|
|
1620
|
+
|
|
1621
|
+
if (
|
|
1622
|
+
self._task_logger_processor_task
|
|
1623
|
+
and not self._task_logger_processor_task.done()
|
|
1624
|
+
):
|
|
1625
|
+
log.info("%s Cancelling task logger processor task...", self.log_identifier)
|
|
1626
|
+
self._task_logger_processor_task.cancel()
|
|
1627
|
+
|
|
1628
|
+
if self._visualization_internal_app:
|
|
1629
|
+
log.info(
|
|
1630
|
+
"%s Cleaning up internal visualization app...", self.log_identifier
|
|
1631
|
+
)
|
|
1632
|
+
try:
|
|
1633
|
+
self._visualization_internal_app.cleanup()
|
|
1634
|
+
except Exception as e:
|
|
1635
|
+
log.error(
|
|
1636
|
+
"%s Error cleaning up internal visualization app: %s",
|
|
1637
|
+
self.log_identifier,
|
|
1638
|
+
e,
|
|
1639
|
+
)
|
|
1640
|
+
|
|
1641
|
+
if self._task_logger_internal_app:
|
|
1642
|
+
log.info("%s Cleaning up internal task logger app...", self.log_identifier)
|
|
1643
|
+
try:
|
|
1644
|
+
self._task_logger_internal_app.cleanup()
|
|
1645
|
+
except Exception as e:
|
|
1646
|
+
log.error(
|
|
1647
|
+
"%s Error cleaning up internal task logger app: %s",
|
|
1648
|
+
self.log_identifier,
|
|
1649
|
+
e,
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
self._active_visualization_streams.clear()
|
|
1653
|
+
self._global_visualization_subscriptions.clear()
|
|
1654
|
+
self._cleanup_visualization_locks()
|
|
1655
|
+
log.info("%s Visualization resources cleaned up.", self.log_identifier)
|
|
1656
|
+
|
|
1657
|
+
super().cleanup()
|
|
1658
|
+
|
|
1659
|
+
if self.fastapi_thread and self.fastapi_thread.is_alive():
|
|
1660
|
+
log.info(
|
|
1661
|
+
"%s Waiting for FastAPI server thread to exit...", self.log_identifier
|
|
1662
|
+
)
|
|
1663
|
+
self.fastapi_thread.join(timeout=10)
|
|
1664
|
+
if self.fastapi_thread.is_alive():
|
|
1665
|
+
log.warning(
|
|
1666
|
+
"%s FastAPI server thread did not exit gracefully.",
|
|
1667
|
+
self.log_identifier,
|
|
1668
|
+
)
|
|
1669
|
+
|
|
1670
|
+
if self.sse_manager:
|
|
1671
|
+
log.info(
|
|
1672
|
+
"%s Closing active SSE connections (best effort)...",
|
|
1673
|
+
self.log_identifier,
|
|
1674
|
+
)
|
|
1675
|
+
try:
|
|
1676
|
+
asyncio.run(self.sse_manager.close_all())
|
|
1677
|
+
except Exception as sse_close_err:
|
|
1678
|
+
log.error(
|
|
1679
|
+
"%s Error closing SSE connections during cleanup: %s",
|
|
1680
|
+
self.log_identifier,
|
|
1681
|
+
sse_close_err,
|
|
1682
|
+
)
|
|
1683
|
+
|
|
1684
|
+
log.info("%s Web UI Backend Component cleanup finished.", self.log_identifier)
|
|
1685
|
+
|
|
1686
|
+
def _infer_visualization_event_details(
|
|
1687
|
+
self, topic: str, payload: dict[str, Any]
|
|
1688
|
+
) -> dict[str, Any]:
|
|
1689
|
+
"""
|
|
1690
|
+
Infers details for the visualization SSE payload from the Solace topic and A2A message.
|
|
1691
|
+
This version is updated to parse the official A2A SDK message formats.
|
|
1692
|
+
"""
|
|
1693
|
+
details = {
|
|
1694
|
+
"direction": "unknown",
|
|
1695
|
+
"source_entity": "unknown",
|
|
1696
|
+
"target_entity": "unknown",
|
|
1697
|
+
"debug_type": "unknown",
|
|
1698
|
+
"message_id": payload.get("id"),
|
|
1699
|
+
"task_id": None,
|
|
1700
|
+
"payload_summary": {
|
|
1701
|
+
"method": payload.get("method", "N/A"),
|
|
1702
|
+
"params_preview": None,
|
|
1703
|
+
},
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
# --- Phase 1: Parse the payload to extract core info ---
|
|
1707
|
+
try:
|
|
1708
|
+
# Handle SAM Events (system events)
|
|
1709
|
+
event_type = payload.get("event_type")
|
|
1710
|
+
if event_type:
|
|
1711
|
+
details["direction"] = "system_event"
|
|
1712
|
+
details["debug_type"] = "sam_event"
|
|
1713
|
+
details["payload_summary"]["method"] = event_type
|
|
1714
|
+
details["source_entity"] = payload.get("source_component", "unknown")
|
|
1715
|
+
details["target_entity"] = "system"
|
|
1716
|
+
return details
|
|
1717
|
+
|
|
1718
|
+
# Try to parse as a JSON-RPC response first
|
|
1719
|
+
if "result" in payload or "error" in payload:
|
|
1720
|
+
rpc_response = JSONRPCResponse.model_validate(payload)
|
|
1721
|
+
result = a2a.get_response_result(rpc_response)
|
|
1722
|
+
error = a2a.get_response_error(rpc_response)
|
|
1723
|
+
details["message_id"] = a2a.get_response_id(rpc_response)
|
|
1724
|
+
|
|
1725
|
+
if result:
|
|
1726
|
+
kind = getattr(result, "kind", None)
|
|
1727
|
+
details["direction"] = kind or "response"
|
|
1728
|
+
details["task_id"] = getattr(result, "task_id", None) or getattr(
|
|
1729
|
+
result, "id", None
|
|
1730
|
+
)
|
|
1731
|
+
|
|
1732
|
+
if isinstance(result, TaskStatusUpdateEvent):
|
|
1733
|
+
details["source_entity"] = (
|
|
1734
|
+
result.metadata.get("agent_name")
|
|
1735
|
+
if result.metadata
|
|
1736
|
+
else None
|
|
1737
|
+
)
|
|
1738
|
+
message = a2a.get_message_from_status_update(result)
|
|
1739
|
+
if message:
|
|
1740
|
+
if not details["source_entity"]:
|
|
1741
|
+
details["source_entity"] = (
|
|
1742
|
+
message.metadata.get("agent_name")
|
|
1743
|
+
if message.metadata
|
|
1744
|
+
else None
|
|
1745
|
+
)
|
|
1746
|
+
data_parts = a2a.get_data_parts_from_message(message)
|
|
1747
|
+
if data_parts:
|
|
1748
|
+
details["debug_type"] = data_parts[0].data.get(
|
|
1749
|
+
"type", "unknown"
|
|
1750
|
+
)
|
|
1751
|
+
elif a2a.get_text_from_message(message):
|
|
1752
|
+
details["debug_type"] = "streaming_text"
|
|
1753
|
+
elif isinstance(result, Task):
|
|
1754
|
+
details["source_entity"] = (
|
|
1755
|
+
result.metadata.get("agent_name")
|
|
1756
|
+
if result.metadata
|
|
1757
|
+
else None
|
|
1758
|
+
)
|
|
1759
|
+
elif isinstance(result, TaskArtifactUpdateEvent):
|
|
1760
|
+
artifact = a2a.get_artifact_from_artifact_update(result)
|
|
1761
|
+
if artifact:
|
|
1762
|
+
details["source_entity"] = (
|
|
1763
|
+
artifact.metadata.get("agent_name")
|
|
1764
|
+
if artifact.metadata
|
|
1765
|
+
else None
|
|
1766
|
+
)
|
|
1767
|
+
elif error:
|
|
1768
|
+
details["direction"] = "error_response"
|
|
1769
|
+
details["task_id"] = (
|
|
1770
|
+
error.data.get("taskId")
|
|
1771
|
+
if isinstance(error.data, dict)
|
|
1772
|
+
else None
|
|
1773
|
+
)
|
|
1774
|
+
details["debug_type"] = "error"
|
|
1775
|
+
|
|
1776
|
+
# Try to parse as a JSON-RPC request
|
|
1777
|
+
elif "method" in payload:
|
|
1778
|
+
rpc_request = A2ARequest.model_validate(payload)
|
|
1779
|
+
method = a2a.get_request_method(rpc_request)
|
|
1780
|
+
details["direction"] = "request"
|
|
1781
|
+
details["payload_summary"]["method"] = method
|
|
1782
|
+
details["message_id"] = a2a.get_request_id(rpc_request)
|
|
1783
|
+
|
|
1784
|
+
if method in ["message/send", "message/stream"]:
|
|
1785
|
+
details["debug_type"] = method
|
|
1786
|
+
message = a2a.get_message_from_send_request(rpc_request)
|
|
1787
|
+
details["task_id"] = a2a.get_request_id(rpc_request)
|
|
1788
|
+
if message:
|
|
1789
|
+
details["target_entity"] = (
|
|
1790
|
+
message.metadata.get("agent_name")
|
|
1791
|
+
if message.metadata
|
|
1792
|
+
else None
|
|
1793
|
+
)
|
|
1794
|
+
elif method == "tasks/cancel":
|
|
1795
|
+
details["task_id"] = a2a.get_task_id_from_cancel_request(
|
|
1796
|
+
rpc_request
|
|
1797
|
+
)
|
|
1798
|
+
|
|
1799
|
+
# Handle Discovery messages (which are not JSON-RPC)
|
|
1800
|
+
elif "/a2a/v1/discovery/" in topic:
|
|
1801
|
+
agent_card = AgentCard.model_validate(payload)
|
|
1802
|
+
details["direction"] = "discovery"
|
|
1803
|
+
details["source_entity"] = agent_card.name
|
|
1804
|
+
details["target_entity"] = "broadcast"
|
|
1805
|
+
details["message_id"] = None # Discovery has no ID
|
|
1806
|
+
|
|
1807
|
+
except Exception as e:
|
|
1808
|
+
log.warning(
|
|
1809
|
+
"[%s] Failed to parse A2A payload for visualization details: %s",
|
|
1810
|
+
self.log_identifier,
|
|
1811
|
+
e,
|
|
1812
|
+
)
|
|
1813
|
+
|
|
1814
|
+
# --- Phase 2: Refine details using topic information as a fallback ---
|
|
1815
|
+
if details["direction"] == "unknown":
|
|
1816
|
+
if "request" in topic:
|
|
1817
|
+
details["direction"] = "request"
|
|
1818
|
+
elif "response" in topic:
|
|
1819
|
+
details["direction"] = "response"
|
|
1820
|
+
elif "status" in topic:
|
|
1821
|
+
details["direction"] = "status_update"
|
|
1822
|
+
# TEMP - add debug_type based on the type in the data
|
|
1823
|
+
details["debug_type"] = "unknown"
|
|
1824
|
+
|
|
1825
|
+
# --- Phase 3: Create a payload summary ---
|
|
1826
|
+
try:
|
|
1827
|
+
summary_source = (
|
|
1828
|
+
payload.get("result")
|
|
1829
|
+
or payload.get("params")
|
|
1830
|
+
or payload.get("error")
|
|
1831
|
+
or payload
|
|
1832
|
+
)
|
|
1833
|
+
summary_str = json.dumps(summary_source)
|
|
1834
|
+
details["payload_summary"]["params_preview"] = (
|
|
1835
|
+
(summary_str[:100] + "...") if len(summary_str) > 100 else summary_str
|
|
1836
|
+
)
|
|
1837
|
+
except Exception:
|
|
1838
|
+
details["payload_summary"][
|
|
1839
|
+
"params_preview"
|
|
1840
|
+
] = "[Could not serialize payload]"
|
|
1841
|
+
|
|
1842
|
+
return details
|
|
1843
|
+
|
|
1844
|
+
def _extract_involved_agents_for_viz(
|
|
1845
|
+
self, topic: str, payload_dict: dict[str, Any]
|
|
1846
|
+
) -> set[str]:
|
|
1847
|
+
"""
|
|
1848
|
+
Extracts agent names involved in a message from its topic and payload.
|
|
1849
|
+
"""
|
|
1850
|
+
agents: set[str] = set()
|
|
1851
|
+
log_id_prefix = f"{self.log_identifier}[ExtractAgentsViz]"
|
|
1852
|
+
|
|
1853
|
+
topic_agent_match = re.match(
|
|
1854
|
+
rf"^{re.escape(self.namespace)}/a2a/v1/agent/(?:request|response|status)/([^/]+)",
|
|
1855
|
+
topic,
|
|
1856
|
+
)
|
|
1857
|
+
if topic_agent_match:
|
|
1858
|
+
agents.add(topic_agent_match.group(1))
|
|
1859
|
+
log.debug(
|
|
1860
|
+
"%s Found agent '%s' in topic.",
|
|
1861
|
+
log_id_prefix,
|
|
1862
|
+
topic_agent_match.group(1),
|
|
1863
|
+
)
|
|
1864
|
+
|
|
1865
|
+
if isinstance(payload_dict, dict):
|
|
1866
|
+
if (
|
|
1867
|
+
"name" in payload_dict
|
|
1868
|
+
and "capabilities" in payload_dict
|
|
1869
|
+
and "skills" in payload_dict
|
|
1870
|
+
):
|
|
1871
|
+
try:
|
|
1872
|
+
card = AgentCard(**payload_dict)
|
|
1873
|
+
if card.name:
|
|
1874
|
+
agents.add(card.name)
|
|
1875
|
+
log.debug(
|
|
1876
|
+
"%s Found agent '%s' in AgentCard payload.",
|
|
1877
|
+
log_id_prefix,
|
|
1878
|
+
card.name,
|
|
1879
|
+
)
|
|
1880
|
+
except Exception:
|
|
1881
|
+
pass
|
|
1882
|
+
result = payload_dict.get("result")
|
|
1883
|
+
if isinstance(result, dict):
|
|
1884
|
+
status_info = result.get("status")
|
|
1885
|
+
if isinstance(status_info, dict):
|
|
1886
|
+
message_info = status_info.get("message")
|
|
1887
|
+
if isinstance(message_info, dict):
|
|
1888
|
+
metadata = message_info.get("metadata")
|
|
1889
|
+
if isinstance(metadata, dict) and "agent_name" in metadata:
|
|
1890
|
+
if metadata["agent_name"]:
|
|
1891
|
+
agents.add(metadata["agent_name"])
|
|
1892
|
+
log.debug(
|
|
1893
|
+
"%s Found agent '%s' in status.message.metadata.",
|
|
1894
|
+
log_id_prefix,
|
|
1895
|
+
metadata["agent_name"],
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
artifact_info = result.get("artifact")
|
|
1899
|
+
if isinstance(artifact_info, dict):
|
|
1900
|
+
metadata = artifact_info.get("metadata")
|
|
1901
|
+
if isinstance(metadata, dict) and "agent_name" in metadata:
|
|
1902
|
+
if metadata["agent_name"]:
|
|
1903
|
+
agents.add(metadata["agent_name"])
|
|
1904
|
+
log.debug(
|
|
1905
|
+
"%s Found agent '%s' in artifact.metadata.",
|
|
1906
|
+
log_id_prefix,
|
|
1907
|
+
metadata["agent_name"],
|
|
1908
|
+
)
|
|
1909
|
+
|
|
1910
|
+
params = payload_dict.get("params")
|
|
1911
|
+
if isinstance(params, dict):
|
|
1912
|
+
message_info = params.get("message")
|
|
1913
|
+
if isinstance(message_info, dict):
|
|
1914
|
+
metadata = message_info.get("metadata")
|
|
1915
|
+
if isinstance(metadata, dict) and "agent_name" in metadata:
|
|
1916
|
+
if metadata["agent_name"]:
|
|
1917
|
+
agents.add(metadata["agent_name"])
|
|
1918
|
+
log.debug(
|
|
1919
|
+
"%s Found agent '%s' in params.message.metadata.",
|
|
1920
|
+
log_id_prefix,
|
|
1921
|
+
metadata["agent_name"],
|
|
1922
|
+
)
|
|
1923
|
+
|
|
1924
|
+
if not agents:
|
|
1925
|
+
log.debug(
|
|
1926
|
+
"%s No specific agents identified from topic '%s' or payload.",
|
|
1927
|
+
log_id_prefix,
|
|
1928
|
+
topic,
|
|
1929
|
+
)
|
|
1930
|
+
return agents
|
|
1931
|
+
|
|
1932
|
+
def get_agent_registry(self) -> AgentRegistry:
|
|
1933
|
+
return self.agent_registry
|
|
1934
|
+
|
|
1935
|
+
def _check_agent_health(self):
|
|
1936
|
+
"""
|
|
1937
|
+
Checks the health of peer agents and de-registers unresponsive ones.
|
|
1938
|
+
This is called periodically by the health check timer.
|
|
1939
|
+
Uses TTL-based expiration to determine if an agent is unresponsive.
|
|
1940
|
+
"""
|
|
1941
|
+
|
|
1942
|
+
log.debug("%s Performing agent health check...", self.log_identifier)
|
|
1943
|
+
|
|
1944
|
+
# Get TTL from configuration or use default from constants
|
|
1945
|
+
from ...common.constants import (
|
|
1946
|
+
HEALTH_CHECK_INTERVAL_SECONDS,
|
|
1947
|
+
HEALTH_CHECK_TTL_SECONDS,
|
|
1948
|
+
)
|
|
1949
|
+
|
|
1950
|
+
ttl_seconds = self.get_config(
|
|
1951
|
+
"agent_health_check_ttl_seconds", HEALTH_CHECK_TTL_SECONDS
|
|
1952
|
+
)
|
|
1953
|
+
health_check_interval = self.get_config(
|
|
1954
|
+
"agent_health_check_interval_seconds", HEALTH_CHECK_INTERVAL_SECONDS
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
log.debug(
|
|
1958
|
+
"%s Health check configuration: interval=%d seconds, TTL=%d seconds",
|
|
1959
|
+
self.log_identifier,
|
|
1960
|
+
health_check_interval,
|
|
1961
|
+
ttl_seconds,
|
|
1962
|
+
)
|
|
1963
|
+
|
|
1964
|
+
# Validate configuration values
|
|
1965
|
+
if (
|
|
1966
|
+
ttl_seconds <= 0
|
|
1967
|
+
or health_check_interval <= 0
|
|
1968
|
+
or ttl_seconds < health_check_interval
|
|
1969
|
+
):
|
|
1970
|
+
log.error(
|
|
1971
|
+
"%s agent_health_check_ttl_seconds (%d) and agent_health_check_interval_seconds (%d) must be positive and TTL must be greater than interval.",
|
|
1972
|
+
self.log_identifier,
|
|
1973
|
+
ttl_seconds,
|
|
1974
|
+
health_check_interval,
|
|
1975
|
+
)
|
|
1976
|
+
raise ValueError(
|
|
1977
|
+
f"Invalid health check configuration. agent_health_check_ttl_seconds ({ttl_seconds}) and agent_health_check_interval_seconds ({health_check_interval}) must be positive and TTL must be greater than interval."
|
|
1978
|
+
)
|
|
1979
|
+
|
|
1980
|
+
# Get all agent names from the registry
|
|
1981
|
+
agent_names = self.agent_registry.get_agent_names()
|
|
1982
|
+
total_agents = len(agent_names)
|
|
1983
|
+
agents_to_deregister = []
|
|
1984
|
+
|
|
1985
|
+
log.debug(
|
|
1986
|
+
"%s Checking health of %d peer agents", self.log_identifier, total_agents
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
for agent_name in agent_names:
|
|
1990
|
+
# Check if the agent's TTL has expired
|
|
1991
|
+
is_expired, time_since_last_seen = self.agent_registry.check_ttl_expired(
|
|
1992
|
+
agent_name, ttl_seconds
|
|
1993
|
+
)
|
|
1994
|
+
|
|
1995
|
+
if is_expired:
|
|
1996
|
+
log.warning(
|
|
1997
|
+
"%s Agent '%s' TTL has expired. De-registering. Time since last seen: %d seconds (TTL: %d seconds)",
|
|
1998
|
+
self.log_identifier,
|
|
1999
|
+
agent_name,
|
|
2000
|
+
time_since_last_seen,
|
|
2001
|
+
ttl_seconds,
|
|
2002
|
+
)
|
|
2003
|
+
agents_to_deregister.append(agent_name)
|
|
2004
|
+
|
|
2005
|
+
# De-register unresponsive agents
|
|
2006
|
+
for agent_name in agents_to_deregister:
|
|
2007
|
+
self._deregister_agent(agent_name)
|
|
2008
|
+
|
|
2009
|
+
log.debug(
|
|
2010
|
+
"%s Agent health check completed. Total agents: %d, De-registered: %d",
|
|
2011
|
+
self.log_identifier,
|
|
2012
|
+
total_agents,
|
|
2013
|
+
len(agents_to_deregister),
|
|
2014
|
+
)
|
|
2015
|
+
|
|
2016
|
+
def _deregister_agent(self, agent_name: str):
|
|
2017
|
+
"""
|
|
2018
|
+
De-registers an agent from the registry and publishes a de-registration event.
|
|
2019
|
+
"""
|
|
2020
|
+
# Remove from registry
|
|
2021
|
+
self.agent_registry.remove_agent(agent_name)
|
|
2022
|
+
|
|
2023
|
+
def get_sse_manager(self) -> SSEManager:
|
|
2024
|
+
return self.sse_manager
|
|
2025
|
+
|
|
2026
|
+
def get_session_manager(self) -> SessionManager:
|
|
2027
|
+
return self.session_manager
|
|
2028
|
+
|
|
2029
|
+
def get_task_logger_service(self) -> TaskLoggerService | None:
|
|
2030
|
+
"""Returns the shared TaskLoggerService instance."""
|
|
2031
|
+
return self.task_logger_service
|
|
2032
|
+
|
|
2033
|
+
def get_namespace(self) -> str:
|
|
2034
|
+
return self.namespace
|
|
2035
|
+
|
|
2036
|
+
def get_gateway_id(self) -> str:
|
|
2037
|
+
"""Returns the unique identifier for this gateway instance."""
|
|
2038
|
+
return self.gateway_id
|
|
2039
|
+
|
|
2040
|
+
def get_cors_origins(self) -> list[str]:
|
|
2041
|
+
return self.cors_allowed_origins
|
|
2042
|
+
|
|
2043
|
+
def get_shared_artifact_service(self) -> BaseArtifactService | None:
|
|
2044
|
+
return self.shared_artifact_service
|
|
2045
|
+
|
|
2046
|
+
def get_embed_config(self) -> dict[str, Any]:
|
|
2047
|
+
"""Returns embed-related configuration needed by dependencies."""
|
|
2048
|
+
return {
|
|
2049
|
+
"enable_embed_resolution": self.enable_embed_resolution,
|
|
2050
|
+
"gateway_max_artifact_resolve_size_bytes": self.gateway_max_artifact_resolve_size_bytes,
|
|
2051
|
+
"gateway_recursive_embed_depth": self.gateway_recursive_embed_depth,
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
def get_core_a2a_service(self) -> CoreA2AService:
|
|
2055
|
+
"""Returns the CoreA2AService instance."""
|
|
2056
|
+
return self.core_a2a_service
|
|
2057
|
+
|
|
2058
|
+
def get_config_resolver(self) -> ConfigResolver:
|
|
2059
|
+
"""Returns the instance of the ConfigResolver."""
|
|
2060
|
+
return self._config_resolver
|
|
2061
|
+
|
|
2062
|
+
def _start_listener(self) -> None:
|
|
2063
|
+
"""
|
|
2064
|
+
GDK Hook: Starts the FastAPI/Uvicorn server.
|
|
2065
|
+
This method is called by BaseGatewayComponent.run().
|
|
2066
|
+
"""
|
|
2067
|
+
self._start_fastapi_server()
|
|
2068
|
+
|
|
2069
|
+
def _stop_listener(self) -> None:
|
|
2070
|
+
"""
|
|
2071
|
+
GDK Hook: Signals the Uvicorn server to shut down.
|
|
2072
|
+
This method is called by BaseGatewayComponent.cleanup().
|
|
2073
|
+
"""
|
|
2074
|
+
log.info(
|
|
2075
|
+
"%s _stop_listener called. Signaling Uvicorn server to exit.",
|
|
2076
|
+
self.log_identifier,
|
|
2077
|
+
)
|
|
2078
|
+
if self.uvicorn_server:
|
|
2079
|
+
self.uvicorn_server.should_exit = True
|
|
2080
|
+
pass
|
|
2081
|
+
|
|
2082
|
+
async def _translate_external_input(
|
|
2083
|
+
self, external_event_data: dict[str, Any]
|
|
2084
|
+
) -> tuple[str, list[ContentPart], dict[str, Any]]:
|
|
2085
|
+
"""
|
|
2086
|
+
Translates raw HTTP request data (from FastAPI form) into A2A task parameters.
|
|
2087
|
+
|
|
2088
|
+
Args:
|
|
2089
|
+
external_event_data: A dictionary containing data from the HTTP request,
|
|
2090
|
+
expected to have keys like 'agent_name', 'message',
|
|
2091
|
+
'files' (List[UploadFile]), 'client_id', 'a2a_session_id'.
|
|
2092
|
+
|
|
2093
|
+
Returns:
|
|
2094
|
+
A tuple containing:
|
|
2095
|
+
- target_agent_name (str): The name of the A2A agent to target.
|
|
2096
|
+
- a2a_parts (List[ContentPart]): A list of unwrapped A2A Part objects.
|
|
2097
|
+
- external_request_context (Dict[str, Any]): Context for TaskContextManager.
|
|
2098
|
+
"""
|
|
2099
|
+
log_id_prefix = f"{self.log_identifier}[TranslateInput]"
|
|
2100
|
+
log.debug(
|
|
2101
|
+
"%s Received external event data: %s",
|
|
2102
|
+
log_id_prefix,
|
|
2103
|
+
{k: type(v) for k, v in external_event_data.items()},
|
|
2104
|
+
)
|
|
2105
|
+
|
|
2106
|
+
target_agent_name: str = external_event_data.get("agent_name")
|
|
2107
|
+
user_message: str = external_event_data.get("message", "")
|
|
2108
|
+
files: list[UploadFile] | None = external_event_data.get("files")
|
|
2109
|
+
client_id: str = external_event_data.get("client_id")
|
|
2110
|
+
a2a_session_id: str = external_event_data.get("a2a_session_id")
|
|
2111
|
+
if not target_agent_name:
|
|
2112
|
+
raise ValueError("Target agent name is missing in external_event_data.")
|
|
2113
|
+
if not client_id or not a2a_session_id:
|
|
2114
|
+
raise ValueError(
|
|
2115
|
+
"Client ID or A2A Session ID is missing in external_event_data."
|
|
2116
|
+
)
|
|
2117
|
+
|
|
2118
|
+
a2a_parts: list[ContentPart] = []
|
|
2119
|
+
|
|
2120
|
+
if files:
|
|
2121
|
+
for upload_file in files:
|
|
2122
|
+
try:
|
|
2123
|
+
content_bytes = await upload_file.read()
|
|
2124
|
+
if not content_bytes:
|
|
2125
|
+
log.warning(
|
|
2126
|
+
"%s Skipping empty uploaded file: %s",
|
|
2127
|
+
log_id_prefix,
|
|
2128
|
+
upload_file.filename,
|
|
2129
|
+
)
|
|
2130
|
+
continue
|
|
2131
|
+
|
|
2132
|
+
# The BaseGatewayComponent will handle normalization based on policy.
|
|
2133
|
+
# Here, we just create the FilePart with inline bytes.
|
|
2134
|
+
file_part = a2a.create_file_part_from_bytes(
|
|
2135
|
+
content_bytes=content_bytes,
|
|
2136
|
+
name=upload_file.filename,
|
|
2137
|
+
mime_type=upload_file.content_type,
|
|
2138
|
+
)
|
|
2139
|
+
a2a_parts.append(file_part)
|
|
2140
|
+
log.info(
|
|
2141
|
+
"%s Created inline FilePart for uploaded file: %s (%d bytes)",
|
|
2142
|
+
log_id_prefix,
|
|
2143
|
+
upload_file.filename,
|
|
2144
|
+
len(content_bytes),
|
|
2145
|
+
)
|
|
2146
|
+
|
|
2147
|
+
except Exception as e:
|
|
2148
|
+
log.exception(
|
|
2149
|
+
"%s Error processing uploaded file %s: %s",
|
|
2150
|
+
log_id_prefix,
|
|
2151
|
+
upload_file.filename,
|
|
2152
|
+
e,
|
|
2153
|
+
)
|
|
2154
|
+
finally:
|
|
2155
|
+
await upload_file.close()
|
|
2156
|
+
|
|
2157
|
+
if user_message:
|
|
2158
|
+
a2a_parts.append(a2a.create_text_part(text=user_message))
|
|
2159
|
+
|
|
2160
|
+
external_request_context = {
|
|
2161
|
+
"app_name_for_artifacts": self.gateway_id,
|
|
2162
|
+
"user_id_for_artifacts": client_id,
|
|
2163
|
+
"a2a_session_id": a2a_session_id,
|
|
2164
|
+
"user_id_for_a2a": client_id,
|
|
2165
|
+
"target_agent_name": target_agent_name,
|
|
2166
|
+
}
|
|
2167
|
+
log.debug(
|
|
2168
|
+
"%s Translated input. Target: %s, Parts: %d, Context: %s",
|
|
2169
|
+
log_id_prefix,
|
|
2170
|
+
target_agent_name,
|
|
2171
|
+
len(a2a_parts),
|
|
2172
|
+
external_request_context,
|
|
2173
|
+
)
|
|
2174
|
+
return target_agent_name, a2a_parts, external_request_context
|
|
2175
|
+
|
|
2176
|
+
async def _send_update_to_external(
|
|
2177
|
+
self,
|
|
2178
|
+
external_request_context: dict[str, Any],
|
|
2179
|
+
event_data: TaskStatusUpdateEvent | TaskArtifactUpdateEvent,
|
|
2180
|
+
is_final_chunk_of_update: bool,
|
|
2181
|
+
) -> None:
|
|
2182
|
+
"""
|
|
2183
|
+
Sends an intermediate update (TaskStatusUpdateEvent or TaskArtifactUpdateEvent)
|
|
2184
|
+
to the external platform (Web UI via SSE) and stores agent messages in the database.
|
|
2185
|
+
"""
|
|
2186
|
+
log_id_prefix = f"{self.log_identifier}[SendUpdate]"
|
|
2187
|
+
sse_task_id = external_request_context.get("a2a_task_id_for_event")
|
|
2188
|
+
a2a_task_id = event_data.task_id
|
|
2189
|
+
|
|
2190
|
+
log.debug(
|
|
2191
|
+
"%s _send_update_to_external called with event_type: %s",
|
|
2192
|
+
log_id_prefix,
|
|
2193
|
+
type(event_data).__name__,
|
|
2194
|
+
)
|
|
2195
|
+
|
|
2196
|
+
if not sse_task_id:
|
|
2197
|
+
log.error(
|
|
2198
|
+
"%s Cannot send update: 'a2a_task_id_for_event' missing from external_request_context.",
|
|
2199
|
+
log_id_prefix,
|
|
2200
|
+
)
|
|
2201
|
+
return
|
|
2202
|
+
|
|
2203
|
+
try:
|
|
2204
|
+
from solace_agent_mesh_enterprise.auth.input_required import (
|
|
2205
|
+
handle_input_required_request,
|
|
2206
|
+
)
|
|
2207
|
+
|
|
2208
|
+
event_data = handle_input_required_request(event_data, sse_task_id, self)
|
|
2209
|
+
except ImportError:
|
|
2210
|
+
pass
|
|
2211
|
+
|
|
2212
|
+
log.debug(
|
|
2213
|
+
"%s Sending update for A2A Task ID %s to SSE Task ID %s. Final chunk: %s",
|
|
2214
|
+
log_id_prefix,
|
|
2215
|
+
a2a_task_id,
|
|
2216
|
+
sse_task_id,
|
|
2217
|
+
is_final_chunk_of_update,
|
|
2218
|
+
)
|
|
2219
|
+
|
|
2220
|
+
sse_event_type = "status_update"
|
|
2221
|
+
if isinstance(event_data, TaskArtifactUpdateEvent):
|
|
2222
|
+
sse_event_type = "artifact_update"
|
|
2223
|
+
|
|
2224
|
+
sse_payload_model = a2a.create_success_response(
|
|
2225
|
+
result=event_data, request_id=a2a_task_id
|
|
2226
|
+
)
|
|
2227
|
+
sse_payload = sse_payload_model.model_dump(by_alias=True, exclude_none=True)
|
|
2228
|
+
|
|
2229
|
+
try:
|
|
2230
|
+
await self.sse_manager.send_event(
|
|
2231
|
+
task_id=sse_task_id, event_data=sse_payload, event_type=sse_event_type
|
|
2232
|
+
)
|
|
2233
|
+
log.debug(
|
|
2234
|
+
"%s Successfully sent %s via SSE for A2A Task ID %s.",
|
|
2235
|
+
log_id_prefix,
|
|
2236
|
+
sse_event_type,
|
|
2237
|
+
a2a_task_id,
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
# Note: Agent message storage is handled in _send_final_response_to_external
|
|
2241
|
+
# to avoid duplicate storage of intermediate status updates
|
|
2242
|
+
|
|
2243
|
+
except Exception as e:
|
|
2244
|
+
log.exception(
|
|
2245
|
+
"%s Failed to send %s via SSE for A2A Task ID %s: %s",
|
|
2246
|
+
log_id_prefix,
|
|
2247
|
+
sse_event_type,
|
|
2248
|
+
a2a_task_id,
|
|
2249
|
+
e,
|
|
2250
|
+
)
|
|
2251
|
+
|
|
2252
|
+
async def _send_final_response_to_external(
|
|
2253
|
+
self, external_request_context: dict[str, Any], task_data: Task
|
|
2254
|
+
) -> None:
|
|
2255
|
+
"""
|
|
2256
|
+
Sends the final A2A Task result to the external platform (Web UI via SSE).
|
|
2257
|
+
"""
|
|
2258
|
+
log_id_prefix = f"{self.log_identifier}[SendFinalResponse]"
|
|
2259
|
+
sse_task_id = external_request_context.get("a2a_task_id_for_event")
|
|
2260
|
+
a2a_task_id = task_data.id
|
|
2261
|
+
|
|
2262
|
+
log.debug("%s _send_final_response_to_external called", log_id_prefix)
|
|
2263
|
+
|
|
2264
|
+
if not sse_task_id:
|
|
2265
|
+
log.error(
|
|
2266
|
+
"%s Cannot send final response: 'a2a_task_id_for_event' missing from external_request_context.",
|
|
2267
|
+
log_id_prefix,
|
|
2268
|
+
)
|
|
2269
|
+
return
|
|
2270
|
+
|
|
2271
|
+
log.info(
|
|
2272
|
+
"%s Sending final response for A2A Task ID %s to SSE Task ID %s.",
|
|
2273
|
+
log_id_prefix,
|
|
2274
|
+
a2a_task_id,
|
|
2275
|
+
sse_task_id,
|
|
2276
|
+
)
|
|
2277
|
+
|
|
2278
|
+
sse_payload_model = a2a.create_success_response(
|
|
2279
|
+
result=task_data, request_id=a2a_task_id
|
|
2280
|
+
)
|
|
2281
|
+
sse_payload = sse_payload_model.model_dump(by_alias=True, exclude_none=True)
|
|
2282
|
+
|
|
2283
|
+
try:
|
|
2284
|
+
await self.sse_manager.send_event(
|
|
2285
|
+
task_id=sse_task_id, event_data=sse_payload, event_type="final_response"
|
|
2286
|
+
)
|
|
2287
|
+
log.debug(
|
|
2288
|
+
"%s Successfully sent final_response via SSE for A2A Task ID %s.",
|
|
2289
|
+
log_id_prefix,
|
|
2290
|
+
a2a_task_id,
|
|
2291
|
+
)
|
|
2292
|
+
|
|
2293
|
+
except Exception as e:
|
|
2294
|
+
log.exception(
|
|
2295
|
+
"%s Failed to send final_response via SSE for A2A Task ID %s: %s",
|
|
2296
|
+
log_id_prefix,
|
|
2297
|
+
a2a_task_id,
|
|
2298
|
+
e,
|
|
2299
|
+
)
|
|
2300
|
+
finally:
|
|
2301
|
+
await self.sse_manager.close_all_for_task(sse_task_id)
|
|
2302
|
+
log.info(
|
|
2303
|
+
"%s Closed SSE connections for SSE Task ID %s.",
|
|
2304
|
+
log_id_prefix,
|
|
2305
|
+
sse_task_id,
|
|
2306
|
+
)
|
|
2307
|
+
|
|
2308
|
+
|
|
2309
|
+
async def _send_error_to_external(
|
|
2310
|
+
self, external_request_context: dict[str, Any], error_data: JSONRPCError
|
|
2311
|
+
) -> None:
|
|
2312
|
+
"""
|
|
2313
|
+
Sends an error notification to the external platform (Web UI via SSE).
|
|
2314
|
+
"""
|
|
2315
|
+
log_id_prefix = f"{self.log_identifier}[SendError]"
|
|
2316
|
+
sse_task_id = external_request_context.get("a2a_task_id_for_event")
|
|
2317
|
+
|
|
2318
|
+
if not sse_task_id:
|
|
2319
|
+
log.error(
|
|
2320
|
+
"%s Cannot send error: 'a2a_task_id_for_event' missing from external_request_context.",
|
|
2321
|
+
log_id_prefix,
|
|
2322
|
+
)
|
|
2323
|
+
return
|
|
2324
|
+
|
|
2325
|
+
log.debug(
|
|
2326
|
+
"%s Sending error to SSE Task ID %s. Error: %s",
|
|
2327
|
+
log_id_prefix,
|
|
2328
|
+
sse_task_id,
|
|
2329
|
+
error_data,
|
|
2330
|
+
)
|
|
2331
|
+
|
|
2332
|
+
sse_payload_model = a2a.create_error_response(
|
|
2333
|
+
error=error_data,
|
|
2334
|
+
request_id=external_request_context.get("original_rpc_id", sse_task_id),
|
|
2335
|
+
)
|
|
2336
|
+
sse_payload = sse_payload_model.model_dump(by_alias=True, exclude_none=True)
|
|
2337
|
+
|
|
2338
|
+
try:
|
|
2339
|
+
await self.sse_manager.send_event(
|
|
2340
|
+
task_id=sse_task_id, event_data=sse_payload, event_type="final_response"
|
|
2341
|
+
)
|
|
2342
|
+
log.info(
|
|
2343
|
+
"%s Successfully sent A2A error as 'final_response' via SSE for SSE Task ID %s.",
|
|
2344
|
+
log_id_prefix,
|
|
2345
|
+
sse_task_id,
|
|
2346
|
+
)
|
|
2347
|
+
except Exception as e:
|
|
2348
|
+
log.exception(
|
|
2349
|
+
"%s Failed to send error via SSE for SSE Task ID %s: %s",
|
|
2350
|
+
log_id_prefix,
|
|
2351
|
+
sse_task_id,
|
|
2352
|
+
e,
|
|
2353
|
+
)
|
|
2354
|
+
finally:
|
|
2355
|
+
await self.sse_manager.close_all_for_task(sse_task_id)
|
|
2356
|
+
log.info(
|
|
2357
|
+
"%s Closed SSE connections for SSE Task ID %s after error.",
|
|
2358
|
+
log_id_prefix,
|
|
2359
|
+
sse_task_id,
|
|
2360
|
+
)
|