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.
Files changed (624) hide show
  1. solace_agent_mesh/__init__.py +0 -0
  2. solace_agent_mesh/agent/__init__.py +0 -0
  3. solace_agent_mesh/agent/adk/__init__.py +0 -0
  4. solace_agent_mesh/agent/adk/adk_llm.txt +226 -0
  5. solace_agent_mesh/agent/adk/adk_llm_detail.txt +566 -0
  6. solace_agent_mesh/agent/adk/alembic/README +74 -0
  7. solace_agent_mesh/agent/adk/alembic/env.py +77 -0
  8. solace_agent_mesh/agent/adk/alembic/script.py.mako +28 -0
  9. solace_agent_mesh/agent/adk/alembic/versions/e2902798564d_adk_session_db_upgrade.py +52 -0
  10. solace_agent_mesh/agent/adk/alembic.ini +112 -0
  11. solace_agent_mesh/agent/adk/app_llm_agent.py +52 -0
  12. solace_agent_mesh/agent/adk/artifacts/__init__.py +1 -0
  13. solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +171 -0
  14. solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +545 -0
  15. solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +609 -0
  16. solace_agent_mesh/agent/adk/callbacks.py +2318 -0
  17. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +406 -0
  18. solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +415 -0
  19. solace_agent_mesh/agent/adk/mcp_content_processor.py +666 -0
  20. solace_agent_mesh/agent/adk/models/lite_llm.py +1026 -0
  21. solace_agent_mesh/agent/adk/models/models_llm.txt +189 -0
  22. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +132 -0
  23. solace_agent_mesh/agent/adk/runner.py +390 -0
  24. solace_agent_mesh/agent/adk/schema_migration.py +88 -0
  25. solace_agent_mesh/agent/adk/services.py +468 -0
  26. solace_agent_mesh/agent/adk/setup.py +1325 -0
  27. solace_agent_mesh/agent/adk/stream_parser.py +415 -0
  28. solace_agent_mesh/agent/adk/tool_wrapper.py +165 -0
  29. solace_agent_mesh/agent/agent_llm.txt +369 -0
  30. solace_agent_mesh/agent/agent_llm_detail.txt +1702 -0
  31. solace_agent_mesh/agent/protocol/__init__.py +0 -0
  32. solace_agent_mesh/agent/protocol/event_handlers.py +2041 -0
  33. solace_agent_mesh/agent/protocol/protocol_llm.txt +81 -0
  34. solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +92 -0
  35. solace_agent_mesh/agent/proxies/__init__.py +0 -0
  36. solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
  37. solace_agent_mesh/agent/proxies/a2a/a2a_llm.txt +190 -0
  38. solace_agent_mesh/agent/proxies/a2a/app.py +56 -0
  39. solace_agent_mesh/agent/proxies/a2a/component.py +1585 -0
  40. solace_agent_mesh/agent/proxies/a2a/config.py +216 -0
  41. solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
  42. solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
  43. solace_agent_mesh/agent/proxies/base/app.py +100 -0
  44. solace_agent_mesh/agent/proxies/base/base_llm.txt +148 -0
  45. solace_agent_mesh/agent/proxies/base/component.py +816 -0
  46. solace_agent_mesh/agent/proxies/base/config.py +85 -0
  47. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +19 -0
  48. solace_agent_mesh/agent/proxies/proxies_llm.txt +283 -0
  49. solace_agent_mesh/agent/sac/__init__.py +0 -0
  50. solace_agent_mesh/agent/sac/app.py +595 -0
  51. solace_agent_mesh/agent/sac/component.py +3668 -0
  52. solace_agent_mesh/agent/sac/patch_adk.py +103 -0
  53. solace_agent_mesh/agent/sac/sac_llm.txt +189 -0
  54. solace_agent_mesh/agent/sac/sac_llm_detail.txt +200 -0
  55. solace_agent_mesh/agent/sac/task_execution_context.py +415 -0
  56. solace_agent_mesh/agent/testing/__init__.py +3 -0
  57. solace_agent_mesh/agent/testing/debug_utils.py +135 -0
  58. solace_agent_mesh/agent/testing/testing_llm.txt +58 -0
  59. solace_agent_mesh/agent/testing/testing_llm_detail.txt +68 -0
  60. solace_agent_mesh/agent/tools/__init__.py +16 -0
  61. solace_agent_mesh/agent/tools/audio_tools.py +1740 -0
  62. solace_agent_mesh/agent/tools/builtin_artifact_tools.py +2500 -0
  63. solace_agent_mesh/agent/tools/builtin_data_analysis_tools.py +244 -0
  64. solace_agent_mesh/agent/tools/dynamic_tool.py +396 -0
  65. solace_agent_mesh/agent/tools/general_agent_tools.py +572 -0
  66. solace_agent_mesh/agent/tools/image_tools.py +1185 -0
  67. solace_agent_mesh/agent/tools/peer_agent_tool.py +363 -0
  68. solace_agent_mesh/agent/tools/registry.py +38 -0
  69. solace_agent_mesh/agent/tools/test_tools.py +136 -0
  70. solace_agent_mesh/agent/tools/time_tools.py +126 -0
  71. solace_agent_mesh/agent/tools/tool_config_types.py +93 -0
  72. solace_agent_mesh/agent/tools/tool_definition.py +53 -0
  73. solace_agent_mesh/agent/tools/tools_llm.txt +276 -0
  74. solace_agent_mesh/agent/tools/tools_llm_detail.txt +275 -0
  75. solace_agent_mesh/agent/tools/web_tools.py +392 -0
  76. solace_agent_mesh/agent/utils/__init__.py +0 -0
  77. solace_agent_mesh/agent/utils/artifact_helpers.py +1353 -0
  78. solace_agent_mesh/agent/utils/config_parser.py +49 -0
  79. solace_agent_mesh/agent/utils/context_helpers.py +77 -0
  80. solace_agent_mesh/agent/utils/utils_llm.txt +152 -0
  81. solace_agent_mesh/agent/utils/utils_llm_detail.txt +149 -0
  82. solace_agent_mesh/assets/docs/404.html +16 -0
  83. solace_agent_mesh/assets/docs/assets/css/styles.8162edfb.css +1 -0
  84. solace_agent_mesh/assets/docs/assets/images/Solace_AI_Framework_With_Broker-85f0a306a9bcdd20b390b7a949f6d862.png +0 -0
  85. solace_agent_mesh/assets/docs/assets/images/sam-enterprise-credentials-b269f095349473118b2b33bdfcc40122.png +0 -0
  86. solace_agent_mesh/assets/docs/assets/js/032c2d61.f3d37824.js +1 -0
  87. solace_agent_mesh/assets/docs/assets/js/05749d90.19ac4f35.js +1 -0
  88. solace_agent_mesh/assets/docs/assets/js/0bcf40b7.c019ad46.js +1 -0
  89. solace_agent_mesh/assets/docs/assets/js/1001.0182a8bd.js +1 -0
  90. solace_agent_mesh/assets/docs/assets/js/1039.0bd46aa1.js +1 -0
  91. solace_agent_mesh/assets/docs/assets/js/149.b797a808.js +1 -0
  92. solace_agent_mesh/assets/docs/assets/js/15ba94aa.92fea363.js +1 -0
  93. solace_agent_mesh/assets/docs/assets/js/15e40e79.434bb30f.js +1 -0
  94. solace_agent_mesh/assets/docs/assets/js/165.6a39807d.js +2 -0
  95. solace_agent_mesh/assets/docs/assets/js/165.6a39807d.js.LICENSE.txt +9 -0
  96. solace_agent_mesh/assets/docs/assets/js/17896441.e612dfb4.js +1 -0
  97. solace_agent_mesh/assets/docs/assets/js/2130.ab9fd314.js +1 -0
  98. solace_agent_mesh/assets/docs/assets/js/2131ec11.5c7a1f6e.js +1 -0
  99. solace_agent_mesh/assets/docs/assets/js/2237.5e477fc6.js +1 -0
  100. solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js +2 -0
  101. solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js.LICENSE.txt +13 -0
  102. solace_agent_mesh/assets/docs/assets/js/2334.1cf50a20.js +1 -0
  103. solace_agent_mesh/assets/docs/assets/js/240a0364.9ad94d1b.js +1 -0
  104. solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
  105. solace_agent_mesh/assets/docs/assets/js/2e32b5e0.33f5d75b.js +1 -0
  106. solace_agent_mesh/assets/docs/assets/js/3219.adc1d663.js +1 -0
  107. solace_agent_mesh/assets/docs/assets/js/341393d4.0fac2613.js +1 -0
  108. solace_agent_mesh/assets/docs/assets/js/3624.0eaa1fd0.js +1 -0
  109. solace_agent_mesh/assets/docs/assets/js/375.708d48db.js +1 -0
  110. solace_agent_mesh/assets/docs/assets/js/3834.b6cd790e.js +1 -0
  111. solace_agent_mesh/assets/docs/assets/js/3a6c6137.f5940cfa.js +1 -0
  112. solace_agent_mesh/assets/docs/assets/js/3ac1795d.28b7c67b.js +1 -0
  113. solace_agent_mesh/assets/docs/assets/js/3ff0015d.2ddc75c0.js +1 -0
  114. solace_agent_mesh/assets/docs/assets/js/41adc471.48b12a4e.js +1 -0
  115. solace_agent_mesh/assets/docs/assets/js/4250.95455b28.js +1 -0
  116. solace_agent_mesh/assets/docs/assets/js/4356.d169ab5b.js +1 -0
  117. solace_agent_mesh/assets/docs/assets/js/4458.518e66fa.js +1 -0
  118. solace_agent_mesh/assets/docs/assets/js/4488.c7cc3442.js +1 -0
  119. solace_agent_mesh/assets/docs/assets/js/4494.6ee23046.js +1 -0
  120. solace_agent_mesh/assets/docs/assets/js/4855.fc4444b6.js +1 -0
  121. solace_agent_mesh/assets/docs/assets/js/4866.22daefc0.js +1 -0
  122. solace_agent_mesh/assets/docs/assets/js/4950.ca4caeda.js +1 -0
  123. solace_agent_mesh/assets/docs/assets/js/509e993c.a1fbf45a.js +1 -0
  124. solace_agent_mesh/assets/docs/assets/js/5388.7a136447.js +1 -0
  125. solace_agent_mesh/assets/docs/assets/js/547e15cc.2f7790c1.js +1 -0
  126. solace_agent_mesh/assets/docs/assets/js/55b7b518.29d6e75d.js +1 -0
  127. solace_agent_mesh/assets/docs/assets/js/5607.081356f8.js +1 -0
  128. solace_agent_mesh/assets/docs/assets/js/5864.b0d0e9de.js +1 -0
  129. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.90a87880.js +1 -0
  130. solace_agent_mesh/assets/docs/assets/js/5e95c892.558d5167.js +1 -0
  131. solace_agent_mesh/assets/docs/assets/js/6063ff4c.ef84f702.js +1 -0
  132. solace_agent_mesh/assets/docs/assets/js/60702c0e.a8bdd79b.js +1 -0
  133. solace_agent_mesh/assets/docs/assets/js/6143.0a1464c9.js +1 -0
  134. solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
  135. solace_agent_mesh/assets/docs/assets/js/6395.e9c73649.js +1 -0
  136. solace_agent_mesh/assets/docs/assets/js/64195356.c498c4d0.js +1 -0
  137. solace_agent_mesh/assets/docs/assets/js/66d4869e.b77431fc.js +1 -0
  138. solace_agent_mesh/assets/docs/assets/js/6796.51d2c9b7.js +1 -0
  139. solace_agent_mesh/assets/docs/assets/js/6976.379be23b.js +1 -0
  140. solace_agent_mesh/assets/docs/assets/js/6978.ee0b945c.js +1 -0
  141. solace_agent_mesh/assets/docs/assets/js/6a520c9d.b6e3f2ce.js +1 -0
  142. solace_agent_mesh/assets/docs/assets/js/6aaedf65.7253541d.js +1 -0
  143. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
  144. solace_agent_mesh/assets/docs/assets/js/6d84eae0.fd23ba4a.js +1 -0
  145. solace_agent_mesh/assets/docs/assets/js/6fdfefc7.99de744e.js +1 -0
  146. solace_agent_mesh/assets/docs/assets/js/7040.cb436723.js +1 -0
  147. solace_agent_mesh/assets/docs/assets/js/7195.412f418a.js +1 -0
  148. solace_agent_mesh/assets/docs/assets/js/71da7b71.374b9d54.js +1 -0
  149. solace_agent_mesh/assets/docs/assets/js/722f809d.965da774.js +1 -0
  150. solace_agent_mesh/assets/docs/assets/js/7280.3fb73bdb.js +1 -0
  151. solace_agent_mesh/assets/docs/assets/js/742f027b.46c07808.js +1 -0
  152. solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.js +1 -0
  153. solace_agent_mesh/assets/docs/assets/js/7845.e33e7c4c.js +1 -0
  154. solace_agent_mesh/assets/docs/assets/js/7900.69516146.js +1 -0
  155. solace_agent_mesh/assets/docs/assets/js/8024126c.fa0e7186.js +1 -0
  156. solace_agent_mesh/assets/docs/assets/js/81a99df0.2484b8d9.js +1 -0
  157. solace_agent_mesh/assets/docs/assets/js/82fbfb93.161823a5.js +1 -0
  158. solace_agent_mesh/assets/docs/assets/js/8356.8a379c04.js +1 -0
  159. solace_agent_mesh/assets/docs/assets/js/8567.4732c6b7.js +1 -0
  160. solace_agent_mesh/assets/docs/assets/js/8573.cb04eda5.js +1 -0
  161. solace_agent_mesh/assets/docs/assets/js/8577.1d54e766.js +1 -0
  162. solace_agent_mesh/assets/docs/assets/js/8591.5d015485.js +2 -0
  163. solace_agent_mesh/assets/docs/assets/js/8591.5d015485.js.LICENSE.txt +61 -0
  164. solace_agent_mesh/assets/docs/assets/js/8709.7ecd4047.js +1 -0
  165. solace_agent_mesh/assets/docs/assets/js/8731.6c1dbf0c.js +1 -0
  166. solace_agent_mesh/assets/docs/assets/js/8908.f9d1b506.js +1 -0
  167. solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
  168. solace_agent_mesh/assets/docs/assets/js/9157.b4093d07.js +1 -0
  169. solace_agent_mesh/assets/docs/assets/js/924ffdeb.975e428a.js +1 -0
  170. solace_agent_mesh/assets/docs/assets/js/9278.a4fd875d.js +1 -0
  171. solace_agent_mesh/assets/docs/assets/js/945fb41e.6f4cdffd.js +1 -0
  172. solace_agent_mesh/assets/docs/assets/js/94e8668d.16083b3f.js +1 -0
  173. solace_agent_mesh/assets/docs/assets/js/9616.b75c2f6d.js +1 -0
  174. solace_agent_mesh/assets/docs/assets/js/9793.c6d16376.js +1 -0
  175. solace_agent_mesh/assets/docs/assets/js/9bb13469.b2333011.js +1 -0
  176. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
  177. solace_agent_mesh/assets/docs/assets/js/a7bd4aaa.2204d2f7.js +1 -0
  178. solace_agent_mesh/assets/docs/assets/js/a94703ab.3e5fbcb3.js +1 -0
  179. solace_agent_mesh/assets/docs/assets/js/ab9708a8.245ae0ef.js +1 -0
  180. solace_agent_mesh/assets/docs/assets/js/aba21aa0.c42a534c.js +1 -0
  181. solace_agent_mesh/assets/docs/assets/js/ad71b5ed.af3ecfd1.js +1 -0
  182. solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
  183. solace_agent_mesh/assets/docs/assets/js/c198a0dc.8f31f867.js +1 -0
  184. solace_agent_mesh/assets/docs/assets/js/c93cbaa0.0e0d8baf.js +1 -0
  185. solace_agent_mesh/assets/docs/assets/js/cab03b5b.6a073091.js +1 -0
  186. solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.07e170dd.js +1 -0
  187. solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
  188. solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
  189. solace_agent_mesh/assets/docs/assets/js/db5d6442.3daf1696.js +1 -0
  190. solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
  191. solace_agent_mesh/assets/docs/assets/js/dd817ffc.c37a755e.js +1 -0
  192. solace_agent_mesh/assets/docs/assets/js/dd81e2b8.b682e9c2.js +1 -0
  193. solace_agent_mesh/assets/docs/assets/js/de5f4c65.e8241890.js +1 -0
  194. solace_agent_mesh/assets/docs/assets/js/de915948.44a432bc.js +1 -0
  195. solace_agent_mesh/assets/docs/assets/js/e04b235d.52cb25ed.js +1 -0
  196. solace_agent_mesh/assets/docs/assets/js/e1b6eeb4.b1068f9b.js +1 -0
  197. solace_agent_mesh/assets/docs/assets/js/e3d9abda.1476f570.js +1 -0
  198. solace_agent_mesh/assets/docs/assets/js/e6f9706b.4488e34c.js +1 -0
  199. solace_agent_mesh/assets/docs/assets/js/e92d0134.3bda61dd.js +1 -0
  200. solace_agent_mesh/assets/docs/assets/js/f284c35a.250993bf.js +1 -0
  201. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
  202. solace_agent_mesh/assets/docs/assets/js/main.7acf7ace.js +2 -0
  203. solace_agent_mesh/assets/docs/assets/js/main.7acf7ace.js.LICENSE.txt +81 -0
  204. solace_agent_mesh/assets/docs/assets/js/runtime~main.9e0813a2.js +1 -0
  205. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +154 -0
  206. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +99 -0
  207. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +90 -0
  208. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +107 -0
  209. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +166 -0
  210. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +101 -0
  211. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +219 -0
  212. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +92 -0
  213. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +29 -0
  214. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +55 -0
  215. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +110 -0
  216. solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +182 -0
  217. solace_agent_mesh/assets/docs/docs/documentation/components/prompts/index.html +147 -0
  218. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +345 -0
  219. solace_agent_mesh/assets/docs/docs/documentation/components/speech/index.html +52 -0
  220. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +83 -0
  221. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +84 -0
  222. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +25 -0
  223. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes-deployment/index.html +47 -0
  224. solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +85 -0
  225. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +60 -0
  226. solace_agent_mesh/assets/docs/docs/documentation/deploying/proxy_configuration/index.html +49 -0
  227. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +144 -0
  228. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +191 -0
  229. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +128 -0
  230. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +54 -0
  231. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
  232. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +34 -0
  233. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +55 -0
  234. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +267 -0
  235. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +142 -0
  236. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +116 -0
  237. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +86 -0
  238. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +164 -0
  239. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +140 -0
  240. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +57 -0
  241. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +72 -0
  242. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +102 -0
  243. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/teams-integration/index.html +115 -0
  244. solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +86 -0
  245. solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +67 -0
  246. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +37 -0
  247. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +86 -0
  248. solace_agent_mesh/assets/docs/docs/documentation/enterprise/openapi-tools/index.html +324 -0
  249. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +247 -0
  250. solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +440 -0
  251. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +184 -0
  252. solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +62 -0
  253. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +75 -0
  254. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +54 -0
  255. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +85 -0
  256. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +41 -0
  257. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +290 -0
  258. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +78 -0
  259. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +25 -0
  260. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +78 -0
  261. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +160 -0
  262. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +142 -0
  263. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +251 -0
  264. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +88 -0
  265. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +100 -0
  266. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +52 -0
  267. solace_agent_mesh/assets/docs/img/Solace_AI_Framework_With_Broker.png +0 -0
  268. solace_agent_mesh/assets/docs/img/logo.png +0 -0
  269. solace_agent_mesh/assets/docs/img/sac-flows.png +0 -0
  270. solace_agent_mesh/assets/docs/img/sac_parts_of_a_component.png +0 -0
  271. solace_agent_mesh/assets/docs/img/sam-enterprise-credentials.png +0 -0
  272. solace_agent_mesh/assets/docs/img/solace-logo-text.svg +18 -0
  273. solace_agent_mesh/assets/docs/img/solace-logo.png +0 -0
  274. solace_agent_mesh/assets/docs/lunr-index-1765810064709.json +1 -0
  275. solace_agent_mesh/assets/docs/lunr-index.json +1 -0
  276. solace_agent_mesh/assets/docs/search-doc-1765810064709.json +1 -0
  277. solace_agent_mesh/assets/docs/search-doc.json +1 -0
  278. solace_agent_mesh/assets/docs/sitemap.xml +1 -0
  279. solace_agent_mesh/cli/__init__.py +1 -0
  280. solace_agent_mesh/cli/commands/__init__.py +0 -0
  281. solace_agent_mesh/cli/commands/add_cmd/__init__.py +15 -0
  282. solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +250 -0
  283. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +729 -0
  284. solace_agent_mesh/cli/commands/add_cmd/gateway_cmd.py +322 -0
  285. solace_agent_mesh/cli/commands/add_cmd/web_add_agent_step.py +102 -0
  286. solace_agent_mesh/cli/commands/add_cmd/web_add_gateway_step.py +114 -0
  287. solace_agent_mesh/cli/commands/docs_cmd.py +60 -0
  288. solace_agent_mesh/cli/commands/eval_cmd.py +46 -0
  289. solace_agent_mesh/cli/commands/init_cmd/__init__.py +439 -0
  290. solace_agent_mesh/cli/commands/init_cmd/broker_step.py +201 -0
  291. solace_agent_mesh/cli/commands/init_cmd/database_step.py +91 -0
  292. solace_agent_mesh/cli/commands/init_cmd/directory_step.py +28 -0
  293. solace_agent_mesh/cli/commands/init_cmd/env_step.py +238 -0
  294. solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +365 -0
  295. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +464 -0
  296. solace_agent_mesh/cli/commands/init_cmd/project_files_step.py +38 -0
  297. solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +119 -0
  298. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +215 -0
  299. solace_agent_mesh/cli/commands/plugin_cmd/__init__.py +20 -0
  300. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +137 -0
  301. solace_agent_mesh/cli/commands/plugin_cmd/build_cmd.py +86 -0
  302. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +144 -0
  303. solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +306 -0
  304. solace_agent_mesh/cli/commands/plugin_cmd/install_cmd.py +283 -0
  305. solace_agent_mesh/cli/commands/plugin_cmd/official_registry.py +175 -0
  306. solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +305 -0
  307. solace_agent_mesh/cli/commands/run_cmd.py +215 -0
  308. solace_agent_mesh/cli/main.py +52 -0
  309. solace_agent_mesh/cli/utils.py +262 -0
  310. solace_agent_mesh/client/webui/frontend/static/assets/authCallback-Dj3JtK42.js +1 -0
  311. solace_agent_mesh/client/webui/frontend/static/assets/client-ZKk9kEJ5.js +25 -0
  312. solace_agent_mesh/client/webui/frontend/static/assets/favicon-BLgzUch9.ico +0 -0
  313. solace_agent_mesh/client/webui/frontend/static/assets/main-BcUaNZ-Q.css +1 -0
  314. solace_agent_mesh/client/webui/frontend/static/assets/main-vjch4RYc.js +435 -0
  315. solace_agent_mesh/client/webui/frontend/static/assets/vendor-BNV4kZN0.js +535 -0
  316. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +15 -0
  317. solace_agent_mesh/client/webui/frontend/static/index.html +16 -0
  318. solace_agent_mesh/client/webui/frontend/static/mockServiceWorker.js +336 -0
  319. solace_agent_mesh/client/webui/frontend/static/ui-version.json +6 -0
  320. solace_agent_mesh/common/__init__.py +1 -0
  321. solace_agent_mesh/common/a2a/__init__.py +241 -0
  322. solace_agent_mesh/common/a2a/a2a_llm.txt +175 -0
  323. solace_agent_mesh/common/a2a/a2a_llm_detail.txt +193 -0
  324. solace_agent_mesh/common/a2a/artifact.py +368 -0
  325. solace_agent_mesh/common/a2a/events.py +213 -0
  326. solace_agent_mesh/common/a2a/message.py +375 -0
  327. solace_agent_mesh/common/a2a/protocol.py +689 -0
  328. solace_agent_mesh/common/a2a/task.py +127 -0
  329. solace_agent_mesh/common/a2a/translation.py +655 -0
  330. solace_agent_mesh/common/a2a/types.py +55 -0
  331. solace_agent_mesh/common/a2a_spec/a2a.json +2576 -0
  332. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +445 -0
  333. solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +736 -0
  334. solace_agent_mesh/common/a2a_spec/schemas/agent_progress_update.json +18 -0
  335. solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +48 -0
  336. solace_agent_mesh/common/a2a_spec/schemas/feedback_event.json +51 -0
  337. solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +41 -0
  338. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +330 -0
  339. solace_agent_mesh/common/a2a_spec/schemas/tool_invocation_start.json +26 -0
  340. solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +48 -0
  341. solace_agent_mesh/common/agent_registry.py +122 -0
  342. solace_agent_mesh/common/common_llm.txt +230 -0
  343. solace_agent_mesh/common/common_llm_detail.txt +2562 -0
  344. solace_agent_mesh/common/constants.py +6 -0
  345. solace_agent_mesh/common/data_parts.py +150 -0
  346. solace_agent_mesh/common/exceptions.py +49 -0
  347. solace_agent_mesh/common/middleware/__init__.py +12 -0
  348. solace_agent_mesh/common/middleware/config_resolver.py +132 -0
  349. solace_agent_mesh/common/middleware/middleware_llm.txt +174 -0
  350. solace_agent_mesh/common/middleware/middleware_llm_detail.txt +185 -0
  351. solace_agent_mesh/common/middleware/registry.py +127 -0
  352. solace_agent_mesh/common/oauth/__init__.py +17 -0
  353. solace_agent_mesh/common/oauth/oauth_client.py +408 -0
  354. solace_agent_mesh/common/oauth/utils.py +50 -0
  355. solace_agent_mesh/common/sac/__init__.py +0 -0
  356. solace_agent_mesh/common/sac/sac_llm.txt +71 -0
  357. solace_agent_mesh/common/sac/sac_llm_detail.txt +82 -0
  358. solace_agent_mesh/common/sac/sam_component_base.py +730 -0
  359. solace_agent_mesh/common/sam_events/__init__.py +9 -0
  360. solace_agent_mesh/common/sam_events/event_service.py +208 -0
  361. solace_agent_mesh/common/sam_events/sam_events_llm.txt +104 -0
  362. solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +115 -0
  363. solace_agent_mesh/common/services/__init__.py +4 -0
  364. solace_agent_mesh/common/services/employee_service.py +164 -0
  365. solace_agent_mesh/common/services/identity_service.py +134 -0
  366. solace_agent_mesh/common/services/providers/__init__.py +4 -0
  367. solace_agent_mesh/common/services/providers/local_file_identity_service.py +151 -0
  368. solace_agent_mesh/common/services/providers/providers_llm.txt +81 -0
  369. solace_agent_mesh/common/services/services_llm.txt +368 -0
  370. solace_agent_mesh/common/services/services_llm_detail.txt +459 -0
  371. solace_agent_mesh/common/utils/__init__.py +7 -0
  372. solace_agent_mesh/common/utils/artifact_utils.py +31 -0
  373. solace_agent_mesh/common/utils/asyncio_macos_fix.py +88 -0
  374. solace_agent_mesh/common/utils/embeds/__init__.py +33 -0
  375. solace_agent_mesh/common/utils/embeds/constants.py +56 -0
  376. solace_agent_mesh/common/utils/embeds/converter.py +447 -0
  377. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +220 -0
  378. solace_agent_mesh/common/utils/embeds/evaluators.py +395 -0
  379. solace_agent_mesh/common/utils/embeds/modifiers.py +793 -0
  380. solace_agent_mesh/common/utils/embeds/resolver.py +967 -0
  381. solace_agent_mesh/common/utils/embeds/types.py +23 -0
  382. solace_agent_mesh/common/utils/in_memory_cache.py +108 -0
  383. solace_agent_mesh/common/utils/initializer.py +52 -0
  384. solace_agent_mesh/common/utils/log_formatters.py +64 -0
  385. solace_agent_mesh/common/utils/message_utils.py +80 -0
  386. solace_agent_mesh/common/utils/mime_helpers.py +172 -0
  387. solace_agent_mesh/common/utils/push_notification_auth.py +135 -0
  388. solace_agent_mesh/common/utils/pydantic_utils.py +159 -0
  389. solace_agent_mesh/common/utils/rbac_utils.py +69 -0
  390. solace_agent_mesh/common/utils/templates/__init__.py +8 -0
  391. solace_agent_mesh/common/utils/templates/liquid_renderer.py +210 -0
  392. solace_agent_mesh/common/utils/templates/template_resolver.py +161 -0
  393. solace_agent_mesh/common/utils/type_utils.py +28 -0
  394. solace_agent_mesh/common/utils/utils_llm.txt +335 -0
  395. solace_agent_mesh/common/utils/utils_llm_detail.txt +572 -0
  396. solace_agent_mesh/config_portal/__init__.py +0 -0
  397. solace_agent_mesh/config_portal/backend/__init__.py +0 -0
  398. solace_agent_mesh/config_portal/backend/common.py +77 -0
  399. solace_agent_mesh/config_portal/backend/plugin_catalog/__init__.py +0 -0
  400. solace_agent_mesh/config_portal/backend/plugin_catalog/constants.py +24 -0
  401. solace_agent_mesh/config_portal/backend/plugin_catalog/models.py +49 -0
  402. solace_agent_mesh/config_portal/backend/plugin_catalog/registry_manager.py +166 -0
  403. solace_agent_mesh/config_portal/backend/plugin_catalog/scraper.py +521 -0
  404. solace_agent_mesh/config_portal/backend/plugin_catalog_server.py +217 -0
  405. solace_agent_mesh/config_portal/backend/server.py +644 -0
  406. solace_agent_mesh/config_portal/frontend/static/client/Solace_community_logo.png +0 -0
  407. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-DiOiAjzL.js +103 -0
  408. solace_agent_mesh/config_portal/frontend/static/client/assets/components-Rk0n-9cK.js +140 -0
  409. solace_agent_mesh/config_portal/frontend/static/client/assets/entry.client-mvZjNKiz.js +19 -0
  410. solace_agent_mesh/config_portal/frontend/static/client/assets/index-DzNKzXrc.js +68 -0
  411. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-ba77705e.js +1 -0
  412. solace_agent_mesh/config_portal/frontend/static/client/assets/root-B17tZKK7.css +1 -0
  413. solace_agent_mesh/config_portal/frontend/static/client/assets/root-V2BeTIUc.js +10 -0
  414. solace_agent_mesh/config_portal/frontend/static/client/favicon.ico +0 -0
  415. solace_agent_mesh/config_portal/frontend/static/client/index.html +7 -0
  416. solace_agent_mesh/core_a2a/__init__.py +1 -0
  417. solace_agent_mesh/core_a2a/core_a2a_llm.txt +90 -0
  418. solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +101 -0
  419. solace_agent_mesh/core_a2a/service.py +307 -0
  420. solace_agent_mesh/evaluation/__init__.py +0 -0
  421. solace_agent_mesh/evaluation/evaluator.py +691 -0
  422. solace_agent_mesh/evaluation/message_organizer.py +553 -0
  423. solace_agent_mesh/evaluation/report/benchmark_info.html +35 -0
  424. solace_agent_mesh/evaluation/report/chart_section.html +141 -0
  425. solace_agent_mesh/evaluation/report/detailed_breakdown.html +28 -0
  426. solace_agent_mesh/evaluation/report/modal.html +59 -0
  427. solace_agent_mesh/evaluation/report/modal_chart_functions.js +411 -0
  428. solace_agent_mesh/evaluation/report/modal_script.js +296 -0
  429. solace_agent_mesh/evaluation/report/modal_styles.css +340 -0
  430. solace_agent_mesh/evaluation/report/performance_metrics_styles.css +93 -0
  431. solace_agent_mesh/evaluation/report/templates/footer.html +2 -0
  432. solace_agent_mesh/evaluation/report/templates/header.html +340 -0
  433. solace_agent_mesh/evaluation/report_data_processor.py +970 -0
  434. solace_agent_mesh/evaluation/report_generator.py +607 -0
  435. solace_agent_mesh/evaluation/run.py +954 -0
  436. solace_agent_mesh/evaluation/shared/__init__.py +92 -0
  437. solace_agent_mesh/evaluation/shared/constants.py +47 -0
  438. solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
  439. solace_agent_mesh/evaluation/shared/helpers.py +35 -0
  440. solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
  441. solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
  442. solace_agent_mesh/evaluation/subscriber.py +776 -0
  443. solace_agent_mesh/evaluation/summary_builder.py +880 -0
  444. solace_agent_mesh/gateway/__init__.py +0 -0
  445. solace_agent_mesh/gateway/adapter/__init__.py +1 -0
  446. solace_agent_mesh/gateway/adapter/base.py +143 -0
  447. solace_agent_mesh/gateway/adapter/types.py +221 -0
  448. solace_agent_mesh/gateway/base/__init__.py +1 -0
  449. solace_agent_mesh/gateway/base/app.py +345 -0
  450. solace_agent_mesh/gateway/base/base_llm.txt +226 -0
  451. solace_agent_mesh/gateway/base/base_llm_detail.txt +235 -0
  452. solace_agent_mesh/gateway/base/component.py +2030 -0
  453. solace_agent_mesh/gateway/base/task_context.py +75 -0
  454. solace_agent_mesh/gateway/gateway_llm.txt +369 -0
  455. solace_agent_mesh/gateway/gateway_llm_detail.txt +3885 -0
  456. solace_agent_mesh/gateway/generic/__init__.py +1 -0
  457. solace_agent_mesh/gateway/generic/app.py +50 -0
  458. solace_agent_mesh/gateway/generic/component.py +727 -0
  459. solace_agent_mesh/gateway/http_sse/__init__.py +0 -0
  460. solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +345 -0
  461. solace_agent_mesh/gateway/http_sse/alembic/env.py +87 -0
  462. solace_agent_mesh/gateway/http_sse/alembic/script.py.mako +28 -0
  463. solace_agent_mesh/gateway/http_sse/alembic/versions/20250910_d5b3f8f2e9a0_create_initial_database.py +58 -0
  464. solace_agent_mesh/gateway/http_sse/alembic/versions/20250911_b1c2d3e4f5g6_add_database_indexes.py +83 -0
  465. solace_agent_mesh/gateway/http_sse/alembic/versions/20250916_f6e7d8c9b0a1_convert_timestamps_to_epoch_and_align_columns.py +412 -0
  466. solace_agent_mesh/gateway/http_sse/alembic/versions/20251006_98882922fa59_add_tasks_events_feedback_chat_tasks.py +190 -0
  467. solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
  468. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_project_users_table.py +72 -0
  469. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +109 -0
  470. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +26 -0
  471. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_projects_table.py +135 -0
  472. solace_agent_mesh/gateway/http_sse/alembic/versions/20251108_create_prompt_tables_with_sharing.py +154 -0
  473. solace_agent_mesh/gateway/http_sse/alembic/versions/20251115_add_parent_task_id.py +32 -0
  474. solace_agent_mesh/gateway/http_sse/alembic/versions/20251126_add_background_task_fields.py +47 -0
  475. solace_agent_mesh/gateway/http_sse/alembic/versions/20251202_add_versioned_fields_to_prompts.py +52 -0
  476. solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +161 -0
  477. solace_agent_mesh/gateway/http_sse/alembic.ini +109 -0
  478. solace_agent_mesh/gateway/http_sse/app.py +351 -0
  479. solace_agent_mesh/gateway/http_sse/component.py +2360 -0
  480. solace_agent_mesh/gateway/http_sse/components/__init__.py +7 -0
  481. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +105 -0
  482. solace_agent_mesh/gateway/http_sse/components/task_logger_forwarder.py +109 -0
  483. solace_agent_mesh/gateway/http_sse/components/visualization_forwarder_component.py +110 -0
  484. solace_agent_mesh/gateway/http_sse/dependencies.py +653 -0
  485. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +299 -0
  486. solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +3278 -0
  487. solace_agent_mesh/gateway/http_sse/main.py +789 -0
  488. solace_agent_mesh/gateway/http_sse/repository/__init__.py +46 -0
  489. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +102 -0
  490. solace_agent_mesh/gateway/http_sse/repository/entities/__init__.py +11 -0
  491. solace_agent_mesh/gateway/http_sse/repository/entities/chat_task.py +75 -0
  492. solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +221 -0
  493. solace_agent_mesh/gateway/http_sse/repository/entities/feedback.py +20 -0
  494. solace_agent_mesh/gateway/http_sse/repository/entities/project.py +81 -0
  495. solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +47 -0
  496. solace_agent_mesh/gateway/http_sse/repository/entities/session.py +66 -0
  497. solace_agent_mesh/gateway/http_sse/repository/entities/session_history.py +0 -0
  498. solace_agent_mesh/gateway/http_sse/repository/entities/task.py +32 -0
  499. solace_agent_mesh/gateway/http_sse/repository/entities/task_event.py +21 -0
  500. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +125 -0
  501. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +239 -0
  502. solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +34 -0
  503. solace_agent_mesh/gateway/http_sse/repository/models/base.py +7 -0
  504. solace_agent_mesh/gateway/http_sse/repository/models/chat_task_model.py +31 -0
  505. solace_agent_mesh/gateway/http_sse/repository/models/feedback_model.py +21 -0
  506. solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +257 -0
  507. solace_agent_mesh/gateway/http_sse/repository/models/project_model.py +51 -0
  508. solace_agent_mesh/gateway/http_sse/repository/models/project_user_model.py +75 -0
  509. solace_agent_mesh/gateway/http_sse/repository/models/prompt_model.py +159 -0
  510. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +53 -0
  511. solace_agent_mesh/gateway/http_sse/repository/models/task_event_model.py +25 -0
  512. solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +39 -0
  513. solace_agent_mesh/gateway/http_sse/repository/project_repository.py +172 -0
  514. solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +186 -0
  515. solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +308 -0
  516. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +268 -0
  517. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +248 -0
  518. solace_agent_mesh/gateway/http_sse/routers/__init__.py +4 -0
  519. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +74 -0
  520. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +1137 -0
  521. solace_agent_mesh/gateway/http_sse/routers/auth.py +311 -0
  522. solace_agent_mesh/gateway/http_sse/routers/config.py +371 -0
  523. solace_agent_mesh/gateway/http_sse/routers/dto/__init__.py +10 -0
  524. solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +450 -0
  525. solace_agent_mesh/gateway/http_sse/routers/dto/project_dto.py +69 -0
  526. solace_agent_mesh/gateway/http_sse/routers/dto/prompt_dto.py +255 -0
  527. solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +15 -0
  528. solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
  529. solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +133 -0
  530. solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +33 -0
  531. solace_agent_mesh/gateway/http_sse/routers/dto/requests/task_requests.py +58 -0
  532. solace_agent_mesh/gateway/http_sse/routers/dto/responses/__init__.py +18 -0
  533. solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +42 -0
  534. solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +31 -0
  535. solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +123 -0
  536. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +33 -0
  537. solace_agent_mesh/gateway/http_sse/routers/dto/responses/task_responses.py +30 -0
  538. solace_agent_mesh/gateway/http_sse/routers/dto/responses/version_responses.py +31 -0
  539. solace_agent_mesh/gateway/http_sse/routers/feedback.py +168 -0
  540. solace_agent_mesh/gateway/http_sse/routers/people.py +38 -0
  541. solace_agent_mesh/gateway/http_sse/routers/projects.py +767 -0
  542. solace_agent_mesh/gateway/http_sse/routers/prompts.py +1415 -0
  543. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +312 -0
  544. solace_agent_mesh/gateway/http_sse/routers/sessions.py +634 -0
  545. solace_agent_mesh/gateway/http_sse/routers/speech.py +355 -0
  546. solace_agent_mesh/gateway/http_sse/routers/sse.py +230 -0
  547. solace_agent_mesh/gateway/http_sse/routers/tasks.py +1089 -0
  548. solace_agent_mesh/gateway/http_sse/routers/users.py +83 -0
  549. solace_agent_mesh/gateway/http_sse/routers/version.py +343 -0
  550. solace_agent_mesh/gateway/http_sse/routers/visualization.py +1220 -0
  551. solace_agent_mesh/gateway/http_sse/services/__init__.py +4 -0
  552. solace_agent_mesh/gateway/http_sse/services/agent_card_service.py +71 -0
  553. solace_agent_mesh/gateway/http_sse/services/audio_service.py +1227 -0
  554. solace_agent_mesh/gateway/http_sse/services/background_task_monitor.py +186 -0
  555. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +273 -0
  556. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +250 -0
  557. solace_agent_mesh/gateway/http_sse/services/people_service.py +78 -0
  558. solace_agent_mesh/gateway/http_sse/services/project_service.py +930 -0
  559. solace_agent_mesh/gateway/http_sse/services/prompt_builder_assistant.py +303 -0
  560. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +303 -0
  561. solace_agent_mesh/gateway/http_sse/services/session_service.py +702 -0
  562. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +593 -0
  563. solace_agent_mesh/gateway/http_sse/services/task_service.py +119 -0
  564. solace_agent_mesh/gateway/http_sse/session_manager.py +219 -0
  565. solace_agent_mesh/gateway/http_sse/shared/__init__.py +146 -0
  566. solace_agent_mesh/gateway/http_sse/shared/auth_utils.py +29 -0
  567. solace_agent_mesh/gateway/http_sse/shared/base_repository.py +252 -0
  568. solace_agent_mesh/gateway/http_sse/shared/database_exceptions.py +274 -0
  569. solace_agent_mesh/gateway/http_sse/shared/database_helpers.py +43 -0
  570. solace_agent_mesh/gateway/http_sse/shared/enums.py +40 -0
  571. solace_agent_mesh/gateway/http_sse/shared/error_dto.py +107 -0
  572. solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +217 -0
  573. solace_agent_mesh/gateway/http_sse/shared/exceptions.py +192 -0
  574. solace_agent_mesh/gateway/http_sse/shared/pagination.py +138 -0
  575. solace_agent_mesh/gateway/http_sse/shared/response_utils.py +134 -0
  576. solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +319 -0
  577. solace_agent_mesh/gateway/http_sse/shared/timestamp_utils.py +97 -0
  578. solace_agent_mesh/gateway/http_sse/shared/types.py +50 -0
  579. solace_agent_mesh/gateway/http_sse/shared/utils.py +22 -0
  580. solace_agent_mesh/gateway/http_sse/sse_event_buffer.py +88 -0
  581. solace_agent_mesh/gateway/http_sse/sse_manager.py +491 -0
  582. solace_agent_mesh/gateway/http_sse/utils/__init__.py +1 -0
  583. solace_agent_mesh/gateway/http_sse/utils/artifact_copy_utils.py +370 -0
  584. solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +72 -0
  585. solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +47 -0
  586. solace_agent_mesh/llm.txt +228 -0
  587. solace_agent_mesh/llm_detail.txt +2835 -0
  588. solace_agent_mesh/services/__init__.py +0 -0
  589. solace_agent_mesh/services/platform/__init__.py +18 -0
  590. solace_agent_mesh/services/platform/alembic/env.py +85 -0
  591. solace_agent_mesh/services/platform/alembic/script.py.mako +28 -0
  592. solace_agent_mesh/services/platform/alembic.ini +109 -0
  593. solace_agent_mesh/services/platform/api/__init__.py +3 -0
  594. solace_agent_mesh/services/platform/api/dependencies.py +147 -0
  595. solace_agent_mesh/services/platform/api/main.py +280 -0
  596. solace_agent_mesh/services/platform/api/middleware.py +51 -0
  597. solace_agent_mesh/services/platform/api/routers/__init__.py +24 -0
  598. solace_agent_mesh/services/platform/app.py +114 -0
  599. solace_agent_mesh/services/platform/component.py +235 -0
  600. solace_agent_mesh/solace_agent_mesh_llm.txt +362 -0
  601. solace_agent_mesh/solace_agent_mesh_llm_detail.txt +8599 -0
  602. solace_agent_mesh/templates/agent_template.yaml +53 -0
  603. solace_agent_mesh/templates/eval_backend_template.yaml +54 -0
  604. solace_agent_mesh/templates/gateway_app_template.py +75 -0
  605. solace_agent_mesh/templates/gateway_component_template.py +484 -0
  606. solace_agent_mesh/templates/gateway_config_template.yaml +38 -0
  607. solace_agent_mesh/templates/logging_config_template.yaml +48 -0
  608. solace_agent_mesh/templates/main_orchestrator.yaml +66 -0
  609. solace_agent_mesh/templates/plugin_agent_config_template.yaml +122 -0
  610. solace_agent_mesh/templates/plugin_custom_config_template.yaml +27 -0
  611. solace_agent_mesh/templates/plugin_custom_template.py +10 -0
  612. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +60 -0
  613. solace_agent_mesh/templates/plugin_pyproject_template.toml +32 -0
  614. solace_agent_mesh/templates/plugin_readme_template.md +12 -0
  615. solace_agent_mesh/templates/plugin_tool_config_template.yaml +109 -0
  616. solace_agent_mesh/templates/plugin_tools_template.py +224 -0
  617. solace_agent_mesh/templates/shared_config.yaml +112 -0
  618. solace_agent_mesh/templates/templates_llm.txt +147 -0
  619. solace_agent_mesh/templates/webui.yaml +177 -0
  620. solace_agent_mesh-1.11.2.dist-info/METADATA +504 -0
  621. solace_agent_mesh-1.11.2.dist-info/RECORD +624 -0
  622. solace_agent_mesh-1.11.2.dist-info/WHEEL +4 -0
  623. solace_agent_mesh-1.11.2.dist-info/entry_points.txt +3 -0
  624. solace_agent_mesh-1.11.2.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,2318 @@
1
+ """
2
+ ADK Callbacks for the A2A Host Component.
3
+ Includes dynamic instruction injection, artifact metadata injection,
4
+ embed resolution, and logging.
5
+ """
6
+
7
+ import logging
8
+ import json
9
+ import asyncio
10
+ import uuid
11
+ from typing import Any, Dict, Optional, TYPE_CHECKING, List
12
+ from collections import defaultdict
13
+
14
+ from google.adk.tools import BaseTool, ToolContext
15
+ from google.adk.artifacts import BaseArtifactService
16
+ from google.adk.agents.callback_context import CallbackContext
17
+ from google.adk.models.llm_request import LlmRequest
18
+ from google.adk.models.llm_response import LlmResponse
19
+ from google.genai import types as adk_types
20
+ from google.adk.tools.mcp_tool import MCPTool
21
+
22
+ from .intelligent_mcp_callbacks import (
23
+ save_mcp_response_as_artifact_intelligent,
24
+ McpSaveStatus,
25
+ )
26
+
27
+ from ...agent.utils.artifact_helpers import (
28
+ METADATA_SUFFIX,
29
+ format_metadata_for_llm,
30
+ )
31
+ from ...agent.utils.context_helpers import (
32
+ get_original_session_id,
33
+ get_session_from_callback_context,
34
+ )
35
+ from ..tools.tool_definition import BuiltinTool
36
+
37
+ from ...common.utils.embeds import (
38
+ EMBED_DELIMITER_OPEN,
39
+ EMBED_DELIMITER_CLOSE,
40
+ EMBED_CHAIN_DELIMITER,
41
+ EARLY_EMBED_TYPES,
42
+ evaluate_embed,
43
+ resolve_embeds_in_string,
44
+ )
45
+ from ...common.utils.embeds.types import ResolutionMode
46
+
47
+ from ...common.utils.embeds.modifiers import MODIFIER_IMPLEMENTATIONS
48
+
49
+ from ...common import a2a
50
+ from ...common.data_parts import (
51
+ AgentProgressUpdateData,
52
+ ArtifactCreationProgressData,
53
+ LlmInvocationData,
54
+ ToolInvocationStartData,
55
+ ToolResultData,
56
+ TemplateBlockData,
57
+ )
58
+
59
+
60
+ METADATA_RESPONSE_KEY = "appended_artifact_metadata"
61
+ from ..tools.builtin_artifact_tools import _internal_create_artifact
62
+ from ...agent.adk.tool_wrapper import ADKToolWrapper
63
+
64
+ # Import the new parser and its events
65
+ from pydantic import BaseModel
66
+ from ...agent.adk.stream_parser import (
67
+ FencedBlockStreamParser,
68
+ BlockStartedEvent,
69
+ BlockProgressedEvent,
70
+ BlockCompletedEvent,
71
+ BlockInvalidatedEvent,
72
+ TemplateBlockStartedEvent,
73
+ TemplateBlockCompletedEvent,
74
+ ARTIFACT_BLOCK_DELIMITER_OPEN,
75
+ ARTIFACT_BLOCK_DELIMITER_CLOSE,
76
+ )
77
+
78
+ log = logging.getLogger(__name__)
79
+
80
+ A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY = "temp:llm_stream_chunks_processed"
81
+
82
+ if TYPE_CHECKING:
83
+ from ..sac.component import SamAgentComponent
84
+
85
+
86
+ async def _publish_data_part_status_update(
87
+ host_component: "SamAgentComponent",
88
+ a2a_context: Dict[str, Any],
89
+ data_part_model: BaseModel,
90
+ ):
91
+ """Helper to construct and publish a TaskStatusUpdateEvent with a DataPart."""
92
+ logical_task_id = a2a_context.get("logical_task_id")
93
+ context_id = a2a_context.get("contextId")
94
+
95
+ status_update_event = a2a.create_data_signal_event(
96
+ task_id=logical_task_id,
97
+ context_id=context_id,
98
+ signal_data=data_part_model,
99
+ agent_name=host_component.agent_name,
100
+ )
101
+
102
+ loop = host_component.get_async_loop()
103
+ if loop and loop.is_running():
104
+ asyncio.run_coroutine_threadsafe(
105
+ host_component._publish_status_update_with_buffer_flush(
106
+ status_update_event,
107
+ a2a_context,
108
+ skip_buffer_flush=False,
109
+ ),
110
+ loop,
111
+ )
112
+ else:
113
+ log.error(
114
+ "%s Async loop not available. Cannot publish status update.",
115
+ host_component.log_identifier,
116
+ )
117
+
118
+
119
+ async def _resolve_early_embeds_in_chunk(
120
+ chunk: str,
121
+ callback_context: CallbackContext,
122
+ host_component: "SamAgentComponent",
123
+ log_identifier: str,
124
+ ) -> str:
125
+ """
126
+ Resolves early embeds in an artifact chunk before streaming to the browser.
127
+
128
+ Args:
129
+ chunk: The text chunk containing potential embeds
130
+ callback_context: The ADK callback context with services
131
+ host_component: The host component instance
132
+ log_identifier: Identifier for logging
133
+
134
+ Returns:
135
+ The chunk with early embeds resolved
136
+ """
137
+ if not chunk or EMBED_DELIMITER_OPEN not in chunk:
138
+ return chunk
139
+
140
+ try:
141
+ # Build resolution context from callback_context (pattern from EmbedResolvingMCPToolset)
142
+ invocation_context = callback_context._invocation_context
143
+ if not invocation_context:
144
+ log.warning("%s No invocation context available for embed resolution", log_identifier)
145
+ return chunk
146
+
147
+ session_context = invocation_context.session
148
+ if not session_context:
149
+ log.warning("%s No session context available for embed resolution", log_identifier)
150
+ return chunk
151
+
152
+ resolution_context = {
153
+ "artifact_service": invocation_context.artifact_service,
154
+ "session_context": {
155
+ "session_id": get_original_session_id(invocation_context),
156
+ "user_id": session_context.user_id,
157
+ "app_name": session_context.app_name,
158
+ },
159
+ }
160
+
161
+ # Resolve only early embeds (math, datetime, uuid, artifact_meta)
162
+ resolved_text, processed_until, _ = await resolve_embeds_in_string(
163
+ text=chunk,
164
+ context=resolution_context,
165
+ resolver_func=evaluate_embed,
166
+ types_to_resolve=EARLY_EMBED_TYPES, # Only resolve early embeds
167
+ resolution_mode=ResolutionMode.ARTIFACT_STREAMING, # New mode
168
+ log_identifier=log_identifier,
169
+ config=None, # Could pass host_component config if needed
170
+ )
171
+
172
+ # SAFETY CHECK: If resolver buffered something, parser has a bug
173
+ if processed_until < len(chunk):
174
+ log.error(
175
+ "%s PARSER BUG DETECTED: Resolver buffered partial embed. "
176
+ "Chunk ends with: %r. Returning unresolved chunk to avoid corruption.",
177
+ log_identifier,
178
+ chunk[-50:] if len(chunk) > 50 else chunk,
179
+ )
180
+ # Fallback: return original unresolved chunk (degraded but not corrupted)
181
+ return chunk
182
+
183
+ return resolved_text
184
+
185
+ except Exception as e:
186
+ log.error("%s Error resolving embeds in chunk: %s", log_identifier, e, exc_info=True)
187
+ return chunk # Return original chunk on error
188
+
189
+
190
+ async def process_artifact_blocks_callback(
191
+ callback_context: CallbackContext,
192
+ llm_response: LlmResponse,
193
+ host_component: "SamAgentComponent",
194
+ ) -> Optional[LlmResponse]:
195
+ """
196
+ Orchestrates the parsing of fenced artifact blocks from an LLM stream
197
+ by delegating to a FencedBlockStreamParser instance.
198
+ This callback is stateful across streaming chunks within a single turn.
199
+ """
200
+ log_identifier = "[Callback:ProcessArtifactBlocks]"
201
+ parser_state_key = "fenced_block_parser"
202
+ session = get_session_from_callback_context(callback_context)
203
+
204
+ parser: FencedBlockStreamParser = session.state.get(parser_state_key)
205
+ if parser is None:
206
+ log.debug("%s New turn. Creating new FencedBlockStreamParser.", log_identifier)
207
+ parser = FencedBlockStreamParser(progress_update_interval_bytes=50)
208
+ session.state[parser_state_key] = parser
209
+ session.state["completed_artifact_blocks_list"] = []
210
+ session.state["completed_template_blocks_list"] = []
211
+ session.state["artifact_chars_sent"] = 0 # Reset character tracking for new turn
212
+
213
+ stream_chunks_were_processed = callback_context.state.get(
214
+ A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY, False
215
+ )
216
+ if llm_response.partial:
217
+ callback_context.state[A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY] = True
218
+
219
+ if llm_response.partial or not stream_chunks_were_processed:
220
+ processed_parts: List[adk_types.Part] = []
221
+ original_parts = llm_response.content.parts if llm_response.content else []
222
+ a2a_context = callback_context.state.get("a2a_context")
223
+
224
+ for part in original_parts:
225
+ if part.text is not None:
226
+ parser_result = parser.process_chunk(part.text)
227
+
228
+ if llm_response.partial:
229
+ if parser_result.user_facing_text:
230
+ processed_parts.append(
231
+ adk_types.Part(text=parser_result.user_facing_text)
232
+ )
233
+ else:
234
+ processed_parts.append(part)
235
+
236
+ for event in parser_result.events:
237
+ if isinstance(event, BlockStartedEvent):
238
+ log.info(
239
+ "%s Event: BlockStarted. Params: %s",
240
+ log_identifier,
241
+ event.params,
242
+ )
243
+ # Reset character tracking for this new artifact block
244
+ session.state["artifact_chars_sent"] = 0
245
+
246
+ filename = event.params.get("filename", "unknown_artifact")
247
+ if filename == "unknown_artifact":
248
+ log.warning(
249
+ "%s Fenced artifact block started without a 'filename' parameter.",
250
+ log_identifier,
251
+ )
252
+ description = event.params.get("description")
253
+ if filename == "unknown_artifact":
254
+ log.warning(
255
+ "%s Fenced artifact block started without a 'filename' parameter.",
256
+ log_identifier,
257
+ )
258
+ if a2a_context:
259
+ status_text = f"Receiving artifact `{filename}`..."
260
+ if description:
261
+ status_text = (
262
+ f"Receiving artifact `{filename}`: {description}"
263
+ )
264
+ progress_data = AgentProgressUpdateData(
265
+ status_text=status_text
266
+ )
267
+ await _publish_data_part_status_update(
268
+ host_component, a2a_context, progress_data
269
+ )
270
+ # Also send an initial in-progress event to create the UI bubble
271
+ artifact_progress_data = ArtifactCreationProgressData(
272
+ filename=filename,
273
+ description=description,
274
+ status="in-progress",
275
+ bytes_transferred=0,
276
+ artifact_chunk=None,
277
+ )
278
+
279
+ await _publish_data_part_status_update(
280
+ host_component, a2a_context, artifact_progress_data
281
+ )
282
+ params_str = " ".join(
283
+ [f'{k}="{v}"' for k, v in event.params.items()]
284
+ )
285
+ original_text = f"{ARTIFACT_BLOCK_DELIMITER_OPEN}save_artifact: {params_str}\n"
286
+ session.state["artifact_block_original_text"] = original_text
287
+
288
+ elif isinstance(event, BlockProgressedEvent):
289
+ log.debug(
290
+ "%s Event: BlockProgressed. Size: %d",
291
+ log_identifier,
292
+ event.buffered_size,
293
+ )
294
+ params = event.params
295
+ filename = params.get("filename", "unknown_artifact")
296
+ if filename == "unknown_artifact":
297
+ log.warning(
298
+ "%s Fenced artifact block progressed without a 'filename' parameter.",
299
+ log_identifier,
300
+ )
301
+ if a2a_context:
302
+ # Resolve early embeds in the chunk before streaming
303
+ resolved_chunk = await _resolve_early_embeds_in_chunk(
304
+ chunk=event.chunk,
305
+ callback_context=callback_context,
306
+ host_component=host_component,
307
+ log_identifier=f"{log_identifier}[ResolveChunk]",
308
+ )
309
+
310
+ progress_data = ArtifactCreationProgressData(
311
+ filename=filename,
312
+ description=params.get("description"),
313
+ status="in-progress",
314
+ bytes_transferred=event.buffered_size,
315
+ artifact_chunk=resolved_chunk, # Resolved chunk
316
+ )
317
+
318
+ # Track the cumulative character count of what we've sent
319
+ # We need character count (not bytes) to slice correctly later
320
+ previous_char_count = session.state.get("artifact_chars_sent", 0)
321
+ new_char_count = previous_char_count + len(event.chunk)
322
+ session.state["artifact_chars_sent"] = new_char_count
323
+
324
+ await _publish_data_part_status_update(
325
+ host_component, a2a_context, progress_data
326
+ )
327
+
328
+ elif isinstance(event, BlockCompletedEvent):
329
+ log.debug(
330
+ "%s Event: BlockCompleted. Content length: %d",
331
+ log_identifier,
332
+ len(event.content),
333
+ )
334
+ original_text = session.state.get(
335
+ "artifact_block_original_text", ""
336
+ )
337
+ original_text += event.content
338
+ original_text += "»»»"
339
+
340
+ tool_context_for_call = ToolContext(
341
+ callback_context._invocation_context
342
+ )
343
+
344
+ params = event.params
345
+ filename = params.get("filename")
346
+ if not filename or not filename.strip():
347
+ log.warning(
348
+ "%s Fenced artifact block is missing a valid 'filename'. Failing operation.",
349
+ log_identifier,
350
+ )
351
+ session.state["completed_artifact_blocks_list"].append(
352
+ {
353
+ "filename": (
354
+ "unknown_artifact"
355
+ if filename is None
356
+ else filename
357
+ ),
358
+ "version": 0,
359
+ "status": "error",
360
+ "original_text": original_text,
361
+ }
362
+ )
363
+ if a2a_context:
364
+ if not filename or not filename.strip():
365
+ filename = "unknown_artifact"
366
+ progress_data = ArtifactCreationProgressData(
367
+ filename=filename or "unknown_artifact",
368
+ description=params.get("description"),
369
+ status="failed",
370
+ bytes_transferred=0,
371
+ )
372
+ await _publish_data_part_status_update(
373
+ host_component, a2a_context, progress_data
374
+ )
375
+ continue
376
+
377
+ kwargs_for_call = {
378
+ "filename": filename,
379
+ "content": event.content,
380
+ "mime_type": params.get("mime_type"),
381
+ "description": params.get("description"),
382
+ "metadata_json": params.get("metadata"),
383
+ "tool_context": tool_context_for_call,
384
+ }
385
+ if "schema_max_keys" in params:
386
+ try:
387
+ kwargs_for_call["schema_max_keys"] = int(
388
+ params["schema_max_keys"]
389
+ )
390
+ except (ValueError, TypeError):
391
+ log.warning(
392
+ "%s Invalid 'schema_max_keys' value '%s'. Ignoring.",
393
+ log_identifier,
394
+ params["schema_max_keys"],
395
+ )
396
+ wrapped_creator = ADKToolWrapper(
397
+ original_func=_internal_create_artifact,
398
+ tool_config=None, # No specific config for this internal tool
399
+ tool_name="_internal_create_artifact",
400
+ origin="internal",
401
+ resolution_type="early",
402
+ )
403
+ save_result = await wrapped_creator(**kwargs_for_call)
404
+
405
+ if save_result.get("status") in ["success", "partial_success"]:
406
+ status_for_tool = "success"
407
+ version_for_tool = save_result.get("data_version", 1)
408
+ try:
409
+ logical_task_id = a2a_context.get("logical_task_id")
410
+ if logical_task_id:
411
+ with host_component.active_tasks_lock:
412
+ task_context = host_component.active_tasks.get(
413
+ logical_task_id
414
+ )
415
+ if task_context:
416
+ task_context.register_produced_artifact(
417
+ filename, version_for_tool
418
+ )
419
+ log.info(
420
+ "%s Registered inline artifact '%s' v%d for task %s.",
421
+ log_identifier,
422
+ filename,
423
+ version_for_tool,
424
+ logical_task_id,
425
+ )
426
+ else:
427
+ log.warning(
428
+ "%s No logical_task_id, cannot register inline artifact.",
429
+ log_identifier,
430
+ )
431
+ except Exception as e_track:
432
+ log.error(
433
+ "%s Failed to track inline artifact: %s",
434
+ log_identifier,
435
+ e_track,
436
+ )
437
+
438
+ # Send final progress update with any remaining content not yet sent
439
+ if a2a_context:
440
+ # Check if there's unsent content (content after last progress event)
441
+ total_bytes = len(event.content.encode("utf-8"))
442
+ chars_already_sent = session.state.get("artifact_chars_sent", 0)
443
+
444
+ if chars_already_sent < len(event.content):
445
+ # There's unsent content - send it as a final progress update
446
+ final_chunk = event.content[chars_already_sent:]
447
+
448
+ # Resolve embeds in final chunk
449
+ resolved_final_chunk = await _resolve_early_embeds_in_chunk(
450
+ chunk=final_chunk,
451
+ callback_context=callback_context,
452
+ host_component=host_component,
453
+ log_identifier=f"{log_identifier}[ResolveFinalChunk]",
454
+ )
455
+
456
+ final_progress_data = ArtifactCreationProgressData(
457
+ filename=filename,
458
+ description=params.get("description"),
459
+ status="in-progress",
460
+ bytes_transferred=total_bytes,
461
+ artifact_chunk=resolved_final_chunk, # Resolved final chunk
462
+ )
463
+
464
+ await _publish_data_part_status_update(
465
+ host_component, a2a_context, final_progress_data
466
+ )
467
+
468
+ # Publish completion status immediately via SSE
469
+ if a2a_context:
470
+ progress_data = ArtifactCreationProgressData(
471
+ filename=filename,
472
+ description=params.get("description"),
473
+ status="completed",
474
+ bytes_transferred=len(event.content),
475
+ mime_type=params.get("mime_type"),
476
+ version=version_for_tool,
477
+ )
478
+
479
+ await _publish_data_part_status_update(
480
+ host_component, a2a_context, progress_data
481
+ )
482
+ else:
483
+ status_for_tool = "error"
484
+ version_for_tool = 0
485
+ # Publish failure status immediately via SSE
486
+ if a2a_context:
487
+ progress_data = ArtifactCreationProgressData(
488
+ filename=filename,
489
+ description=params.get("description"),
490
+ status="failed",
491
+ bytes_transferred=len(event.content),
492
+ )
493
+ await _publish_data_part_status_update(
494
+ host_component, a2a_context, progress_data
495
+ )
496
+
497
+ session.state["completed_artifact_blocks_list"].append(
498
+ {
499
+ "filename": filename,
500
+ "version": version_for_tool,
501
+ "status": status_for_tool,
502
+ "original_text": original_text,
503
+ }
504
+ )
505
+
506
+ elif isinstance(event, TemplateBlockStartedEvent):
507
+ log.debug(
508
+ "%s Event: TemplateBlockStarted. Params: %s",
509
+ log_identifier,
510
+ event.params,
511
+ )
512
+
513
+ elif isinstance(event, TemplateBlockCompletedEvent):
514
+ log.debug(
515
+ "%s Event: TemplateBlockCompleted. Template length: %d",
516
+ log_identifier,
517
+ len(event.template_content),
518
+ )
519
+
520
+ # Create a TemplateBlockData message to send to the gateway
521
+ template_id = str(uuid.uuid4())
522
+ params = event.params
523
+
524
+ data_artifact = params.get("data")
525
+ if not data_artifact:
526
+ log.warning(
527
+ "%s Template block is missing 'data' parameter. Skipping.",
528
+ log_identifier,
529
+ )
530
+ continue
531
+
532
+ template_data = TemplateBlockData(
533
+ template_id=template_id,
534
+ data_artifact=data_artifact,
535
+ jsonpath=params.get("jsonpath"),
536
+ limit=(
537
+ int(params.get("limit"))
538
+ if params.get("limit")
539
+ else None
540
+ ),
541
+ template_content=event.template_content,
542
+ )
543
+
544
+ # Publish A2A status update with template metadata
545
+ if a2a_context:
546
+ await _publish_data_part_status_update(
547
+ host_component, a2a_context, template_data
548
+ )
549
+ log.info(
550
+ "%s Published TemplateBlockData with ID: %s",
551
+ log_identifier,
552
+ template_id,
553
+ )
554
+
555
+ # Store template_id in session for potential future use
556
+ # (Gateway will handle the actual resolution)
557
+ if (
558
+ "completed_template_blocks_list" not in session.state
559
+ or session.state["completed_template_blocks_list"] is None
560
+ ):
561
+ session.state["completed_template_blocks_list"] = []
562
+ session.state["completed_template_blocks_list"].append(
563
+ {
564
+ "template_id": template_id,
565
+ "data_artifact": data_artifact,
566
+ }
567
+ )
568
+
569
+ elif isinstance(event, BlockInvalidatedEvent):
570
+ log.debug(
571
+ "%s Event: BlockInvalidated. Rolled back: '%s'",
572
+ log_identifier,
573
+ event.rolled_back_text,
574
+ )
575
+ else:
576
+ processed_parts.append(part)
577
+
578
+ if llm_response.partial:
579
+ if llm_response.content:
580
+ llm_response.content.parts = processed_parts
581
+ elif processed_parts:
582
+ llm_response.content = adk_types.Content(parts=processed_parts)
583
+ else:
584
+ log.debug(
585
+ "%s Ignoring text content of final aggregated response because stream was already processed.",
586
+ log_identifier,
587
+ )
588
+
589
+ if not llm_response.partial and not llm_response.interrupted:
590
+ log.debug(
591
+ "%s Final, non-interrupted stream chunk received. Finalizing parser.",
592
+ log_identifier,
593
+ )
594
+ final_parser_result = parser.finalize()
595
+
596
+ for event in final_parser_result.events:
597
+ if isinstance(event, BlockCompletedEvent):
598
+ log.warning(
599
+ "%s Unterminated artifact block detected at end of turn.",
600
+ log_identifier,
601
+ )
602
+ params = event.params
603
+ filename = params.get("filename", "unknown_artifact")
604
+ if filename == "unknown_artifact":
605
+ log.warning(
606
+ "%s Unterminated fenced artifact block is missing a valid 'filename'. Failing operation.",
607
+ log_identifier,
608
+ )
609
+ if (
610
+ "completed_artifact_blocks_list" not in session.state
611
+ or session.state["completed_artifact_blocks_list"] is None
612
+ ):
613
+ session.state["completed_artifact_blocks_list"] = []
614
+ session.state["completed_artifact_blocks_list"].append(
615
+ {
616
+ "filename": filename,
617
+ "version": 0,
618
+ "status": "error",
619
+ "original_text": session.state.get(
620
+ "artifact_block_original_text", ""
621
+ )
622
+ + event.content,
623
+ }
624
+ )
625
+
626
+ # If there was any rolled-back text from finalization, append it
627
+ if final_parser_result.user_facing_text:
628
+ if (
629
+ llm_response.content
630
+ and llm_response.content.parts
631
+ and llm_response.content.parts[-1].text is not None
632
+ ):
633
+ llm_response.content.parts[
634
+ -1
635
+ ].text += final_parser_result.user_facing_text
636
+ else:
637
+ if llm_response.content is None:
638
+ llm_response.content = adk_types.Content(parts=[])
639
+ elif llm_response.content.parts is None:
640
+ llm_response.content.parts = []
641
+ llm_response.content.parts.append(
642
+ adk_types.Part(text=final_parser_result.user_facing_text)
643
+ )
644
+
645
+ # Check if any blocks were completed and need to be injected into the final response
646
+ completed_blocks_list = session.state.get("completed_artifact_blocks_list")
647
+ if completed_blocks_list:
648
+ log.info(
649
+ "%s Injecting info for %d saved artifact(s) into final LlmResponse.",
650
+ log_identifier,
651
+ len(completed_blocks_list),
652
+ )
653
+
654
+ tool_call_parts = []
655
+ for block_info in completed_blocks_list:
656
+ notify_tool_call = adk_types.FunctionCall(
657
+ name="_notify_artifact_save",
658
+ args={
659
+ "filename": block_info["filename"],
660
+ "version": block_info["version"],
661
+ "status": block_info["status"],
662
+ },
663
+ id=f"host-notify-{uuid.uuid4()}",
664
+ )
665
+ tool_call_parts.append(adk_types.Part(function_call=notify_tool_call))
666
+
667
+ existing_parts = llm_response.content.parts if llm_response.content else []
668
+ final_existing_parts = existing_parts
669
+
670
+ if llm_response.content is None:
671
+ llm_response.content = adk_types.Content(parts=[])
672
+
673
+ llm_response.content.parts = tool_call_parts + final_existing_parts
674
+
675
+ llm_response.turn_complete = True
676
+ llm_response.partial = False
677
+
678
+ session.state[parser_state_key] = None
679
+ session.state["completed_artifact_blocks_list"] = None
680
+ session.state["artifact_block_original_text"] = None
681
+ session.state["completed_template_blocks_list"] = None
682
+ log.debug("%s Cleaned up parser session state.", log_identifier)
683
+
684
+ return None
685
+
686
+
687
+ def create_dangling_tool_call_repair_content(
688
+ dangling_calls: List[adk_types.FunctionCall], error_message: str
689
+ ) -> adk_types.Content:
690
+ """
691
+ Creates a synthetic ADK Content object to repair a dangling tool call.
692
+
693
+ Args:
694
+ dangling_calls: The list of FunctionCall objects that need a response.
695
+ error_message: The error message to include in the response.
696
+
697
+ Returns:
698
+ An ADK Content object with role='tool' containing the error response.
699
+ """
700
+ error_response_parts = []
701
+ for fc in dangling_calls:
702
+ error_response_part = adk_types.Part.from_function_response(
703
+ name=fc.name,
704
+ response={"status": "error", "message": error_message},
705
+ )
706
+ error_response_part.function_response.id = fc.id
707
+ error_response_parts.append(error_response_part)
708
+
709
+ return adk_types.Content(role="tool", parts=error_response_parts)
710
+
711
+
712
+ def repair_history_callback(
713
+ callback_context: CallbackContext, llm_request: LlmRequest
714
+ ) -> Optional[LlmResponse]:
715
+ """
716
+ ADK before_model_callback to proactively check for and repair dangling
717
+ tool calls in the conversation history before it's sent to the LLM.
718
+ This acts as a "suspender" to catch any history corruption.
719
+ """
720
+ log_identifier = "[Callback:RepairHistory]"
721
+ if not llm_request.contents:
722
+ return None
723
+
724
+ history_modified = False
725
+ i = 0
726
+ while i < len(llm_request.contents):
727
+ content = llm_request.contents[i]
728
+ function_calls = []
729
+ if content.role == "model" and content.parts:
730
+ function_calls = [p.function_call for p in content.parts if p.function_call]
731
+
732
+ if function_calls:
733
+ next_content_is_valid_response = False
734
+ if (i + 1) < len(llm_request.contents):
735
+ next_content = llm_request.contents[i + 1]
736
+ if (
737
+ next_content.role in ["user", "tool"]
738
+ and next_content.parts
739
+ and any(p.function_response for p in next_content.parts)
740
+ ):
741
+ next_content_is_valid_response = True
742
+
743
+ if not next_content_is_valid_response:
744
+ log.warning(
745
+ "%s Found dangling tool call in history for tool(s): %s. Repairing.",
746
+ log_identifier,
747
+ [fc.name for fc in function_calls],
748
+ )
749
+ repair_content = create_dangling_tool_call_repair_content(
750
+ dangling_calls=function_calls,
751
+ error_message="The previous tool call did not complete successfully and was automatically repaired.",
752
+ )
753
+ llm_request.contents.insert(i + 1, repair_content)
754
+ history_modified = True
755
+ i += 1
756
+ i += 1
757
+
758
+ if history_modified:
759
+ log.info(
760
+ "%s History was modified to repair dangling tool calls.", log_identifier
761
+ )
762
+
763
+ return None
764
+
765
+
766
+ def _recursively_clean_pydantic_types(data: Any) -> Any:
767
+ """
768
+ Recursively traverses a data structure (dicts, lists) and converts
769
+ Pydantic-specific types like AnyUrl to their primitive string representation
770
+ to ensure JSON serializability.
771
+ """
772
+ if isinstance(data, dict):
773
+ return {
774
+ key: _recursively_clean_pydantic_types(value) for key, value in data.items()
775
+ }
776
+ elif isinstance(data, list):
777
+ return [_recursively_clean_pydantic_types(item) for item in data]
778
+ # Check for Pydantic's AnyUrl without a direct import to avoid dependency issues.
779
+ elif type(data).__name__ == "AnyUrl" and hasattr(data, "__str__"):
780
+ return str(data)
781
+ return data
782
+
783
+
784
+ def _mcp_response_contains_non_text(mcp_response_dict: Dict[str, Any]) -> bool:
785
+ """
786
+ Checks if the 'content' list in an MCP response dictionary contains any
787
+ items that are not of type 'text'.
788
+ """
789
+ if not isinstance(mcp_response_dict, dict):
790
+ return False
791
+
792
+ content_list = mcp_response_dict.get("content")
793
+ if not isinstance(content_list, list):
794
+ return False
795
+
796
+ for item in content_list:
797
+ if isinstance(item, dict) and item.get("type") != "text":
798
+ return True
799
+ return False
800
+
801
+
802
+ async def manage_large_mcp_tool_responses_callback(
803
+ tool: BaseTool,
804
+ args: Dict[str, Any],
805
+ tool_context: ToolContext,
806
+ tool_response: Any,
807
+ host_component: "SamAgentComponent",
808
+ ) -> Optional[Dict[str, Any]]:
809
+ """
810
+ Manages large or non-textual responses from MCP tools.
811
+
812
+ This callback intercepts the response from an MCPTool. Based on the response's
813
+ size and content type, it performs one or more of the following actions:
814
+ 1. **Saves as Artifact:** If the response size exceeds a configured threshold,
815
+ or if it contains non-textual content (like images), it calls the
816
+ `save_mcp_response_as_artifact_intelligent` function to save the
817
+ response as one or more typed artifacts.
818
+ 2. **Truncates for LLM:** If the response size exceeds a configured limit for
819
+ the LLM, it truncates the content to a preview string.
820
+ 3. **Constructs Final Response:** It builds a new dictionary to be returned
821
+ to the LLM, which includes:
822
+ - A `message_to_llm` summarizing what was done (e.g., saved, truncated).
823
+ - `saved_mcp_response_artifact_details` with the result of the save operation.
824
+ - `mcp_tool_output` containing either the original response or the truncated preview.
825
+ - A `status` field indicating the outcome (e.g., 'processed_and_saved').
826
+
827
+ The `tool_response` is the direct output from the tool's `run_async` method.
828
+ """
829
+ log_identifier = f"[Callback:ManageLargeMCPResponse:{tool.name}]"
830
+ log.info(
831
+ "%s Starting callback for tool response, type: %s",
832
+ log_identifier,
833
+ type(tool_response).__name__,
834
+ )
835
+
836
+ if tool_response is None:
837
+ return None
838
+
839
+ if not isinstance(tool, MCPTool):
840
+ log.debug(
841
+ "%s Tool is not an MCPTool. Skipping large response handling.",
842
+ log_identifier,
843
+ )
844
+ return (
845
+ tool_response
846
+ if isinstance(tool_response, dict)
847
+ else {"result": tool_response}
848
+ )
849
+
850
+ log.debug(
851
+ "%s Tool is an MCPTool. Proceeding with large response handling.",
852
+ log_identifier,
853
+ )
854
+
855
+ if hasattr(tool_response, "model_dump"):
856
+ mcp_response_dict = tool_response.model_dump(exclude_none=True)
857
+ log.debug("%s Converted MCPTool response object to dictionary.", log_identifier)
858
+ elif isinstance(tool_response, dict):
859
+ mcp_response_dict = tool_response
860
+ log.debug("%s MCPTool response is already a dictionary.", log_identifier)
861
+ else:
862
+ log.warning(
863
+ "%s MCPTool response is not a Pydantic model or dict (type: %s). Attempting to proceed, but serialization might fail.",
864
+ log_identifier,
865
+ type(tool_response),
866
+ )
867
+ mcp_response_dict = tool_response
868
+
869
+ # Clean any Pydantic-specific types before serialization
870
+ mcp_response_dict = _recursively_clean_pydantic_types(mcp_response_dict)
871
+ cleaned_args = _recursively_clean_pydantic_types(args)
872
+
873
+ try:
874
+ save_threshold = host_component.get_config(
875
+ "mcp_tool_response_save_threshold_bytes", 2048
876
+ )
877
+ llm_max_bytes = host_component.get_config("mcp_tool_llm_return_max_bytes", 4096)
878
+ log.debug(
879
+ "%s Config: save_threshold=%d bytes, llm_max_bytes=%d bytes.",
880
+ log_identifier,
881
+ save_threshold,
882
+ llm_max_bytes,
883
+ )
884
+ except Exception as e:
885
+ log.error(
886
+ "%s Error retrieving configuration: %s. Using defaults.", log_identifier, e
887
+ )
888
+ save_threshold = 2048
889
+ llm_max_bytes = 4096
890
+
891
+ contains_non_text_content = _mcp_response_contains_non_text(mcp_response_dict)
892
+ if not contains_non_text_content:
893
+ try:
894
+ serialized_original_response_str = json.dumps(mcp_response_dict)
895
+ original_response_bytes = len(
896
+ serialized_original_response_str.encode("utf-8")
897
+ )
898
+ log.debug(
899
+ "%s Original response size: %d bytes.",
900
+ log_identifier,
901
+ original_response_bytes,
902
+ )
903
+ except TypeError as e:
904
+ log.error(
905
+ "%s Failed to serialize original MCP tool response dictionary: %s. Returning original response object.",
906
+ log_identifier,
907
+ e,
908
+ )
909
+ return tool_response
910
+ needs_truncation_for_llm = original_response_bytes > llm_max_bytes
911
+ needs_saving_as_artifact = (
912
+ original_response_bytes > save_threshold
913
+ ) or needs_truncation_for_llm
914
+ else:
915
+ needs_truncation_for_llm = False
916
+ needs_saving_as_artifact = True
917
+
918
+ save_result = None
919
+ if needs_saving_as_artifact:
920
+ save_result = await save_mcp_response_as_artifact_intelligent(
921
+ tool, tool_context, host_component, mcp_response_dict, cleaned_args
922
+ )
923
+ if save_result.status == McpSaveStatus.ERROR:
924
+ log.warning(
925
+ "%s Failed to save artifact: %s. Proceeding without saved artifact details.",
926
+ log_identifier,
927
+ save_result.message,
928
+ )
929
+
930
+ final_llm_response_dict: Dict[str, Any] = {}
931
+ message_parts_for_llm: list[str] = []
932
+
933
+ if needs_truncation_for_llm:
934
+ truncation_suffix = "... [Response truncated due to size limit.]"
935
+ adjusted_max_bytes = llm_max_bytes - len(truncation_suffix.encode("utf-8"))
936
+ if adjusted_max_bytes < 0:
937
+ adjusted_max_bytes = 0
938
+
939
+ truncated_bytes = serialized_original_response_str.encode("utf-8")[
940
+ :adjusted_max_bytes
941
+ ]
942
+ truncated_preview_str = (
943
+ truncated_bytes.decode("utf-8", "ignore") + truncation_suffix
944
+ )
945
+
946
+ final_llm_response_dict["mcp_tool_output"] = {
947
+ "type": "truncated_json_string",
948
+ "content": truncated_preview_str,
949
+ }
950
+ message_parts_for_llm.append(
951
+ f"The response from tool '{tool.name}' was too large ({original_response_bytes} bytes) for direct display and has been truncated."
952
+ )
953
+ log.debug("%s MCP tool output truncated for LLM.", log_identifier)
954
+
955
+ if needs_saving_as_artifact:
956
+ if save_result and save_result.status in [
957
+ McpSaveStatus.SUCCESS,
958
+ McpSaveStatus.PARTIAL_SUCCESS,
959
+ ]:
960
+ final_llm_response_dict["saved_mcp_response_artifact_details"] = (
961
+ save_result.model_dump(exclude_none=True)
962
+ )
963
+
964
+ total_artifacts = len(save_result.artifacts_saved)
965
+ if total_artifacts > 0:
966
+ first_artifact = save_result.artifacts_saved[0]
967
+ filename = first_artifact.data_filename
968
+ version = first_artifact.data_version
969
+ if total_artifacts > 1:
970
+ message_parts_for_llm.append(
971
+ f"The full response has been saved as {total_artifacts} artifacts, starting with '{filename}' (version {version})."
972
+ )
973
+ else:
974
+ message_parts_for_llm.append(
975
+ f"The full response has been saved as artifact '{filename}' (version {version})."
976
+ )
977
+ elif save_result.fallback_artifact:
978
+ filename = save_result.fallback_artifact.data_filename
979
+ version = save_result.fallback_artifact.data_version
980
+ message_parts_for_llm.append(
981
+ f"The full response has been saved as artifact '{filename}' (version {version})."
982
+ )
983
+
984
+ log.debug(
985
+ "%s Added saved artifact details to LLM response.", log_identifier
986
+ )
987
+ else:
988
+ message_parts_for_llm.append(
989
+ "Saving the full response as an artifact failed."
990
+ )
991
+ if save_result:
992
+ final_llm_response_dict["saved_mcp_response_artifact_details"] = (
993
+ save_result.model_dump(exclude_none=True)
994
+ )
995
+ log.warning(
996
+ "%s Artifact save failed, error details included in LLM response.",
997
+ log_identifier,
998
+ )
999
+ else:
1000
+ final_llm_response_dict["mcp_tool_output"] = mcp_response_dict
1001
+
1002
+ if needs_saving_as_artifact and (
1003
+ save_result
1004
+ and save_result.status in [McpSaveStatus.SUCCESS, McpSaveStatus.PARTIAL_SUCCESS]
1005
+ ):
1006
+ if needs_truncation_for_llm:
1007
+ final_llm_response_dict["status"] = "processed_saved_and_truncated"
1008
+ else:
1009
+ final_llm_response_dict["status"] = "processed_and_saved"
1010
+ elif needs_saving_as_artifact:
1011
+ if needs_truncation_for_llm:
1012
+ final_llm_response_dict["status"] = "processed_truncated_save_failed"
1013
+ else:
1014
+ final_llm_response_dict["status"] = "processed_save_failed"
1015
+ elif needs_truncation_for_llm:
1016
+ final_llm_response_dict["status"] = "processed_truncated"
1017
+ else:
1018
+ final_llm_response_dict["status"] = "processed"
1019
+
1020
+ if not message_parts_for_llm:
1021
+ message_parts_for_llm.append(f"Response from tool '{tool.name}' processed.")
1022
+ final_llm_response_dict["message_to_llm"] = " ".join(message_parts_for_llm)
1023
+
1024
+ log.info(
1025
+ "%s Returning processed response for LLM. Final status: %s",
1026
+ log_identifier,
1027
+ final_llm_response_dict.get("status", "unknown"),
1028
+ )
1029
+ return final_llm_response_dict
1030
+
1031
+
1032
+ def _generate_fenced_block_syntax_rules() -> str:
1033
+ """Generates the shared syntax rules for all fenced blocks."""
1034
+ open_delim = ARTIFACT_BLOCK_DELIMITER_OPEN
1035
+ close_delim = ARTIFACT_BLOCK_DELIMITER_CLOSE
1036
+ return f"""
1037
+ **Fenced Block Syntax Rules (Applies to `save_artifact` and `template_liquid`):**
1038
+ To create content blocks, you MUST use the EXACT syntax shown below.
1039
+
1040
+ **EXACT SYNTAX (copy this pattern exactly):**
1041
+ {open_delim}keyword: parameter="value" ...
1042
+ The content for the block goes here.
1043
+ It can span multiple lines.
1044
+ {close_delim}
1045
+
1046
+ **CRITICAL FORMATTING RULES:**
1047
+ 1. The opening delimiter MUST be EXACTLY `{open_delim}`.
1048
+ 2. Immediately after the delimiter, write the keyword (`save_artifact` or `template_liquid`) followed by a colon, with NO space before the colon (e.g., `{open_delim}save_artifact:`).
1049
+ 3. All parameters (like `filename`, `data`, `mime_type`) must be on the SAME line as the opening delimiter.
1050
+ 4. All parameter values **MUST** be enclosed in double quotes (e.g., `filename="example.txt"`).
1051
+ 5. You **MUST NOT** use double quotes `"` inside parameter values. Use single quotes or rephrase instead.
1052
+ 6. The block's content begins on the line immediately following the parameters.
1053
+ 7. Close the block with EXACTLY `{close_delim}` (three angle brackets) on its own line.
1054
+ 8. Do NOT surround the block with triple backticks (```). The `{open_delim}` and `{close_delim}` delimiters are sufficient.
1055
+
1056
+ **COMMON ERRORS TO AVOID:**
1057
+ ❌ WRONG: `{open_delim[0:1]}template_liquid:` (only 1 angle brackets)
1058
+ ❌ WRONG: `{open_delim[0:2]}save_artifact:` (only 2 angle brackets)
1059
+ ❌ WRONG: `{open_delim}save_artifact` (missing colon)
1060
+ ✅ CORRECT: `{open_delim}save_artifact: filename="test.txt" mime_type="text/plain"`
1061
+ """
1062
+
1063
+
1064
+ def _generate_fenced_artifact_instruction() -> str:
1065
+ """Generates the instruction text for using fenced artifact blocks."""
1066
+ open_delim = ARTIFACT_BLOCK_DELIMITER_OPEN
1067
+ return f"""\
1068
+ **Creating Text-Based Artifacts (`{open_delim}save_artifact: ...`):**
1069
+
1070
+ When to Create Artifacts:
1071
+ Create an artifact when the content provides value as a standalone file, such as:
1072
+ - Content with special formatting (HTML, Markdown, CSS).
1073
+ - Documents intended for use outside the conversation (reports, emails).
1074
+ - Structured reference content (schedules, guides, templates).
1075
+ - Substantial text documents or technical documentation.
1076
+
1077
+ When NOT to Create Artifacts:
1078
+ - Simple answers, explanations, or conversational responses.
1079
+ - Brief advice, opinions, or short lists.
1080
+
1081
+ Behavior of Created Artifacts:
1082
+ - They are sent to the user as an interactive file component.
1083
+ - The user can see the content, so there is no need to return or embed it again.
1084
+
1085
+ Parameters for `{open_delim}save_artifact: ...`:
1086
+ - `filename="your_filename.ext"` (REQUIRED)
1087
+ - `mime_type="text/plain"` (optional, defaults to text/plain)
1088
+ - `description="A brief description."` (optional)
1089
+
1090
+ The system will automatically save the content and confirm it in the next turn.
1091
+ """
1092
+
1093
+
1094
+ def _generate_inline_template_instruction() -> str:
1095
+ """Generates the instruction text for using inline Liquid templates."""
1096
+ open_delim = ARTIFACT_BLOCK_DELIMITER_OPEN
1097
+ close_delim = ARTIFACT_BLOCK_DELIMITER_CLOSE
1098
+ return f"""\
1099
+ **Inline Liquid Templates (`{open_delim}template_liquid: ...`):**
1100
+
1101
+ Use inline Liquid templates to dynamically render data from artifacts for user-friendly display. This is faster and more accurate than reading the artifact and reformatting it yourself.
1102
+
1103
+ IMPORTANT: Template Format
1104
+ - Templates use Liquid template syntax (same as Shopify templates - NOTE that Jekyll extensions are NOT supported).
1105
+
1106
+ When to Use Inline Templates:
1107
+ - Formatting CSV, JSON, or YAML data into tables or lists.
1108
+ - Applying simple transformations (filtering, limiting rows).
1109
+
1110
+ Parameters for `{open_delim}template_liquid: ...`:
1111
+ - `data="filename.ext"` (REQUIRED): The data artifact to render. Can include version: `data="file.csv:2"`.
1112
+ - `jsonpath="$.expression"` (optional): JSONPath to extract a subset of JSON/YAML data.
1113
+ - `limit="N"` (optional): Limit to the first N rows (CSV) or items (JSON/YAML arrays).
1114
+
1115
+ Data Context for Liquid Templates:
1116
+ - CSV data: Available as `headers` (array of column names) and `data_rows` (array of row arrays).
1117
+ - JSON/YAML arrays: Available as `items`.
1118
+ - JSON/YAML objects: Keys are directly available (e.g., `name`, `email`).
1119
+
1120
+ Example - CSV Table:
1121
+ {open_delim}template_liquid: data="sales_data.csv" limit="5"
1122
+ | {{% for h in headers %}}{{{{ h }}}} | {{% endfor %}}
1123
+ |{{% for h in headers %}}---|{{% endfor %}}
1124
+ {{% for row in data_rows %}}| {{% for cell in row %}}{{{{ cell }}}} | {{% endfor %}}{{% endfor %}}
1125
+ {close_delim}
1126
+
1127
+ Negative Examples
1128
+ Use {{ issues.size }} instead of {{ issues|length }}
1129
+ Use {{ forloop.index }} instead of {{ loop.index }} (Liquid uses forloop not loop)
1130
+ Use {{ issue.fields.description | truncate: 200 }} instead of slicing with [:200]
1131
+ Do not use Jekyll-specific tags or filters (e.g., `{{% assign %}}`, `{{% capture %}}`, `where`, `sort`, `where_exp`, etc.)
1132
+
1133
+ The rendered output will appear inline in your response automatically.
1134
+ """
1135
+
1136
+
1137
+ def _generate_artifact_creation_instruction() -> str:
1138
+ return """
1139
+ **Creating Text-Based Artifacts:**
1140
+
1141
+ When to Create Text-based Artifacts:
1142
+ Create an artifact when the content provides value as a standalone file:
1143
+ - Content with special formatting (HTML, Markdown, CSS, structured markup) that requires proper rendering
1144
+ - Content explicitly intended for use outside this conversation (reports, emails, presentations, reference documents)
1145
+ - Structured reference content users will save or follow (schedules, guides, templates)
1146
+ - Content that will be edited, expanded, or reused
1147
+ - Substantial text documents
1148
+ - Technical documentation meant as reference material
1149
+
1150
+ When NOT to Create Text-based Artifacts:
1151
+ - Simple answers, explanations, or conversational responses
1152
+ - Brief advice, opinions, or quick information
1153
+ - Short lists, summaries, or single paragraphs
1154
+ - Temporary content only relevant to the immediate conversation
1155
+ - Basic explanations that don't require reference material
1156
+ """
1157
+
1158
+
1159
+ def _generate_examples_instruction() -> str:
1160
+ open_delim = ARTIFACT_BLOCK_DELIMITER_OPEN
1161
+ close_delim = ARTIFACT_BLOCK_DELIMITER_CLOSE
1162
+ embed_open_delim = EMBED_DELIMITER_OPEN
1163
+ embed_close_delim = EMBED_DELIMITER_CLOSE
1164
+
1165
+ return (
1166
+ f"""\
1167
+ Example 1:
1168
+ - User: "Create a markdown file with your two csv files as tables."
1169
+ <note>There are two csv files already uploaded: data1.csv and data2.csv</note>
1170
+ - OrchestratorAgent:
1171
+ {embed_open_delim}status_update:Creating Markdown tables from CSV files...{embed_close_delim}
1172
+ {open_delim}save_artifact: filename="data_tables.md" mime_type="text/markdown" description="Markdown tables from CSV files"
1173
+ # Data Tables
1174
+ ## Data 1
1175
+ {open_delim}template_liquid: data="data1.csv"
1176
+ """
1177
+ + """| {% for h in headers %}{{ h }} | {% endfor %}
1178
+ |{% for h in headers %}---|{% endfor %}
1179
+ {% for row in data_rows %}| {% for cell in row %}{{ cell }} | {% endfor %}{% endfor %}
1180
+ """
1181
+ + f"""{close_delim}
1182
+ ## Data 2
1183
+ {open_delim}template_liquid: data="data2.csv"
1184
+ """
1185
+ + """| {% for h in headers %}{{ h }} | {% endfor %}
1186
+ |{% for h in headers %}---|{% endfor %}
1187
+ {% for row in data_rows %}| {% for cell in row %}{{ cell }} | {% endfor %}{% endfor %}
1188
+ """
1189
+ + f"""{close_delim}
1190
+ {close_delim}
1191
+ Example 2:
1192
+ - User: "Create a text file with the result of sqrt(12345) + sqrt(67890) + sqrt(13579) + sqrt(24680)."
1193
+ - OrchestratorAgent:
1194
+ {embed_open_delim}status_update:Calculating and creating text file...{embed_close_delim}
1195
+ {open_delim}save_artifact: filename="math.txt" mime_type="text/plain" description="Result of sqrt(12345) + sqrt(67890) + sqrt(13579) + sqrt(24680)"
1196
+ result = {embed_open_delim}math: sqrt(12345) + sqrt(67890) + sqrt(13579) + sqrt(24680) | .2f{embed_close_delim}
1197
+ {close_delim}
1198
+
1199
+ Example 3:
1200
+ - User: "Show me the first 10 entries from data1.csv"
1201
+ - OrchestratorAgent:
1202
+ {embed_open_delim}status_update:Loading CSV data...{embed_close_delim}
1203
+ {open_delim}template_liquid: data="data1.csv" limit="10"
1204
+ """
1205
+ + """| {% for h in headers %}{{ h }} | {% endfor %}
1206
+ |{% for h in headers %}---|{% endfor %}
1207
+ {% for row in data_rows %}| {% for cell in row %}{{ cell }} | {% endfor %}{% endfor %}
1208
+ """
1209
+ + f"""{close_delim}
1210
+
1211
+ Example 4:
1212
+ - User: "Search the database for all orders from last month"
1213
+ - OrchestratorAgent:
1214
+ {embed_open_delim}status_update:Querying order database...{embed_close_delim}
1215
+ [calls search_database tool with no visible text]
1216
+ [After getting results:]
1217
+ Found 247 orders from last month totaling $45,231.
1218
+
1219
+ Example 5:
1220
+ - User: "Create an HTML with the chart image you just generated with the customer data."
1221
+ - OrchestratorAgent:
1222
+ {embed_open_delim}status_update:Generating HTML report with chart...{embed_close_delim}
1223
+ {open_delim}save_artifact: filename="customer_analysis.html" mime_type="text/html" description="Interactive customer analysis dashboard"
1224
+ <!DOCTYPE html>
1225
+ <html>
1226
+ <head>
1227
+ <title>Customer Chart - {embed_open_delim}datetime:%Y-%m-%d{embed_close_delim}</title>
1228
+ """
1229
+ + """
1230
+ <style>
1231
+ body { font-family: Arial, sans-serif; margin: 20px; }
1232
+ .metric { background: #f0f0f0; padding: 10px; margin: 10px 0; }
1233
+ img { max-width: 100%; height: auto; }
1234
+ """
1235
+ + f""" </style>
1236
+ </head>
1237
+ <body>
1238
+ <h1>Customer Analysis Report</h1>
1239
+ <p>Generated: {embed_open_delim}datetime:iso{embed_close_delim}</p>
1240
+
1241
+ <h2>Customer Distribution Chart</h2>
1242
+ <img src="{embed_open_delim}artifact_content:customer_chart.png >>> format:datauri{embed_close_delim}" alt="Customer Distribution">
1243
+
1244
+ </body>
1245
+ </html>
1246
+ {close_delim}
1247
+
1248
+ """
1249
+ )
1250
+
1251
+
1252
+ def _generate_embed_instruction(
1253
+ include_artifact_content: bool,
1254
+ log_identifier: str,
1255
+ ) -> Optional[str]:
1256
+ """Generates the instruction text for using embeds."""
1257
+ open_delim = EMBED_DELIMITER_OPEN
1258
+ close_delim = EMBED_DELIMITER_CLOSE
1259
+ chain_delim = EMBED_CHAIN_DELIMITER
1260
+ early_types = "`math`, `datetime`, `uuid`, `artifact_meta`"
1261
+
1262
+ modifier_list = MODIFIER_IMPLEMENTATIONS.keys()
1263
+ # Remove apply_to_template from the modifier list as it's been deprecated
1264
+ if "apply_to_template" in modifier_list:
1265
+ modifier_list = list(modifier_list)
1266
+ modifier_list.remove("apply_to_template")
1267
+ modifier_list = ", ".join([f"`{prefix}`" for prefix in modifier_list])
1268
+
1269
+ base_instruction = f"""\
1270
+ **Using Dynamic Embeds in Responses:**
1271
+
1272
+ You can use dynamic embeds in your text responses and tool parameters using the syntax {open_delim}type:expression {chain_delim} format{close_delim}. NOTE that this differs from 'save_artifact', which has different delimiters. This allows you to
1273
+ always have correct information in your output. Specifically, make sure you always use embeds for math, even if it is simple. You will make mistakes if you try to do math yourself.
1274
+ Use HTML entities to escape the delimiters.
1275
+ This host resolves the following embed types *early* (before sending to the LLM or tool): {early_types}. This means the embed is replaced with its resolved value.
1276
+ - `{open_delim}math:expression | .2f{close_delim}`: Evaluates the math expression using asteval - this must just be plain math (plus random(), randint() and uniform()), don't import anything. Optional format specifier follows Python's format(). Use this for all math calculations rather than doing it yourself. Don't give approximations.
1277
+ - `{open_delim}datetime:format_or_keyword{close_delim}`: Inserts current date/time. Use Python strftime format (e.g., `%Y-%m-%d`) or keywords (`iso`, `timestamp`, `date`, `time`, `now`).
1278
+ - `{open_delim}uuid:{close_delim}`: Inserts a random UUID.
1279
+ - `{open_delim}artifact_meta:filename[:version]{close_delim}`: Inserts a summary of the artifact's metadata (latest version if unspecified).
1280
+ - `{open_delim}status_update:Your message here{close_delim}`: Generates an immediate, distinct status message event that is displayed to the user (e.g., 'Thinking...', 'Searching database...'). This message appears in a status area, not as part of the main chat conversation. Use this to provide interim feedback during processing.
1281
+
1282
+ Examples:
1283
+ - `{open_delim}status_update:Analyzing data...{close_delim}` (Shows 'Analyzing data...' as a status update)
1284
+ - `The result of 23.5 * 4.2 is {open_delim}math:23.5 * 4.2 | .2f{close_delim}` (Embeds calculated result with 2 decimal places)
1285
+
1286
+ The following embeds are resolved *late* (by the gateway before final display):
1287
+ - `{open_delim}artifact_return:filename[:version]{close_delim}`: This is the primary way to return an artifact to the user. It attaches the specified artifact to the message. The embed itself is removed from the text. Use this instead of describing a file and expecting the user to download it. Note: artifact_return is not necessary if the artifact was just created by you in this same response, since newly created artifacts are automatically attached to your message.
1288
+ """
1289
+
1290
+ artifact_content_instruction = f"""
1291
+ - `{open_delim}artifact_content:filename[:version] {chain_delim} modifier1:value1 {chain_delim} ... {chain_delim} format:output_format{close_delim}`: Embeds artifact content after applying a chain of modifiers. This is resolved *late* (typically by a gateway before final display).
1292
+ - If this embed resolves to binary content (like an image), it will be automatically converted into an attached file, similar to `artifact_return`.
1293
+ - Use `{chain_delim}` to separate the artifact identifier from the modifier steps and the final format step.
1294
+ - Available modifiers: {modifier_list}.
1295
+ - The `format:output_format` step *must* be the last step in the chain. Supported formats include `text`, `datauri`, `json`, `json_pretty`, `csv`. Formatting as datauri, will include the data URI prefix, so do not add it yourself.
1296
+ - Use `artifact_meta` first to check size; embedding large files may fail.
1297
+ - Efficient workflows for large artifacts:
1298
+ - To extract specific line ranges: `load_artifact(filename, version, include_line_numbers=True)` to identify lines, then use `slice_lines:start:end` modifier to extract that range.
1299
+ - To fill templates with many placeholders: use `artifact_search_and_replace_regex` with `replacements` array (single atomic operation instead of multiple calls).
1300
+ - Line numbers are display-only; `slice_lines` always operates on original content.
1301
+ - Examples:
1302
+ - `<img src="{open_delim}artifact_content:image.png {chain_delim} format:datauri{close_delim}`"> (Embed image as data URI - NOTE that this includes the datauri prefix. Do not add it yourself.)
1303
+ - `{open_delim}artifact_content:data.json {chain_delim} jsonpath:$.items[*] {chain_delim} select_fields:name,status {chain_delim} format:json_pretty{close_delim}` (Extract and format JSON fields)
1304
+ - `{open_delim}artifact_content:logs.txt {chain_delim} grep:ERROR {chain_delim} head:10 {chain_delim} format:text{close_delim}` (Get first 10 error lines)
1305
+ - `{open_delim}artifact_content:config.json {chain_delim} jsonpath:$.userPreferences.theme {chain_delim} format:text{close_delim}` (Extract a single value from a JSON artifact)
1306
+ - `{open_delim}artifact_content:server.log {chain_delim} tail:100 {chain_delim} grep:WARN {chain_delim} format:text{close_delim}` (Get warning lines from the last 100 lines of a log file)
1307
+ - `{open_delim}artifact_content:template.html {chain_delim} slice_lines:10:50 {chain_delim} format:text{close_delim}` (Extract lines 10-50 from a large file)
1308
+ - `<img src="{open_delim}artifact_content:diagram.png {chain_delim} format:datauri{close_delim}`"> (Embed an PNG diagram as a data URI)`
1309
+ """
1310
+
1311
+ final_instruction = base_instruction
1312
+ if include_artifact_content:
1313
+ final_instruction += artifact_content_instruction
1314
+
1315
+ final_instruction += f"""
1316
+ Ensure the syntax is exactly `{open_delim}type:expression{close_delim}` or `{open_delim}type:expression {chain_delim} ... {chain_delim} format:output_format{close_delim}` with no extra spaces around delimiters (`{open_delim}`, `{close_delim}`, `{chain_delim}`, `:`, `|`). Malformed directives will be ignored."""
1317
+
1318
+ return final_instruction
1319
+
1320
+
1321
+ def _generate_conversation_flow_instruction() -> str:
1322
+ """Generates instruction text for conversation flow and response formatting."""
1323
+ open_delim = EMBED_DELIMITER_OPEN
1324
+ close_delim = EMBED_DELIMITER_CLOSE
1325
+ return f"""\
1326
+ **Conversation Flow and Response Formatting:**
1327
+
1328
+ **CRITICAL: Minimize Narration - Maximize Results**
1329
+
1330
+ You do NOT need to produce visible text on every turn. Many turns should contain ONLY status updates and tool calls, with NO visible text at all.
1331
+ Only produce visible text when you have actual results, answers, or insights to share with the user.
1332
+
1333
+ Response Content Rules:
1334
+ 1. Visible responses should contain ONLY:
1335
+ - Direct answers to the user's question
1336
+ - Analysis and insights derived from tool results
1337
+ - Final results and data
1338
+ - Follow-up questions when needed
1339
+ - Plans for complex multi-step tasks
1340
+
1341
+ 2. DO NOT include visible text for:
1342
+ - Process narration ("Let me...", "I'll...", "Now I will...")
1343
+ - Acknowledgments of tool calls ("I'm calling...", "Searching...")
1344
+ - Descriptions of what you're about to do
1345
+ - Play-by-play commentary on your actions
1346
+ - Transitional phrases between tool calls
1347
+
1348
+ 3. Use invisible status_update embeds for ALL process updates:
1349
+ - "Searching for..."
1350
+ - "Analyzing..."
1351
+ - "Creating..."
1352
+ - "Querying..."
1353
+ - "Calling agent X..."
1354
+
1355
+ 4. NEVER mix process narration with status updates - if you use a status_update embed, do NOT repeat that information in visible text.
1356
+
1357
+ Examples:
1358
+
1359
+ **Excellent (no visible text, just status and tools):**
1360
+ "{open_delim}status_update:Retrieving sales data...{close_delim}" [then calls tool, no visible text]
1361
+
1362
+ **Good (visible text only contains results):**
1363
+ "{open_delim}status_update:Analyzing Q4 sales...{close_delim}" [calls tool]
1364
+ "Sales increased 23% in Q4, driven primarily by enterprise accounts."
1365
+
1366
+ **Bad (unnecessary narration):**
1367
+ "Let me retrieve the sales data for you." [then calls tool]
1368
+
1369
+ **Bad (narration mixed with results):**
1370
+ "I've analyzed the data and found that sales increased 23% in Q4."
1371
+
1372
+ **Bad (play-by-play commentary):**
1373
+ "Now I'll search for the information. After that I'll analyze it."
1374
+
1375
+ Remember: The user can see status updates and tool calls. You don't need to announce them in visible text.
1376
+ """
1377
+
1378
+
1379
+ def _generate_tool_instructions_from_registry(
1380
+ active_tools: List[BuiltinTool],
1381
+ log_identifier: str,
1382
+ ) -> str:
1383
+ """Generates instruction text from a list of BuiltinTool definitions."""
1384
+ if not active_tools:
1385
+ return ""
1386
+
1387
+ instructions_by_category = defaultdict(list)
1388
+ for tool in sorted(active_tools, key=lambda t: (t.category, t.name)):
1389
+ # Skip internal tools (those starting with underscore)
1390
+ if tool.name.startswith("_"):
1391
+ continue
1392
+
1393
+ param_parts = []
1394
+ if tool.parameters and tool.parameters.properties:
1395
+ for name, schema in tool.parameters.properties.items():
1396
+ is_optional = name not in (tool.parameters.required or [])
1397
+ type_name = "any"
1398
+ if schema and hasattr(schema, "type") and schema.type:
1399
+ type_name = schema.type.name.lower()
1400
+
1401
+ param_str = f"{name}: {type_name}"
1402
+ if is_optional:
1403
+ param_str = f"Optional[{param_str}]"
1404
+ param_parts.append(param_str)
1405
+
1406
+ signature = f"`{tool.name}({', '.join(param_parts)})`"
1407
+ description = tool.description or "No description available."
1408
+
1409
+ instructions_by_category[tool.category].append(f"- {signature}: {description}")
1410
+
1411
+ full_instruction_list = []
1412
+ for category, tool_instructions in sorted(instructions_by_category.items()):
1413
+ category_display_name = category.replace("_", " ").title()
1414
+ full_instruction_list.append(
1415
+ f"You have access to the following '{category_display_name}' tools:"
1416
+ )
1417
+ full_instruction_list.extend(tool_instructions)
1418
+
1419
+ return "\n".join(full_instruction_list)
1420
+
1421
+
1422
+ def inject_dynamic_instructions_callback(
1423
+ callback_context: CallbackContext,
1424
+ llm_request: LlmRequest,
1425
+ host_component: "SamAgentComponent",
1426
+ active_builtin_tools: List[BuiltinTool],
1427
+ ) -> Optional[LlmResponse]:
1428
+ """
1429
+ ADK before_model_callback to inject instructions based on host config.
1430
+ Modifies the llm_request directly.
1431
+ """
1432
+ log_identifier = "[Callback:InjectInstructions]"
1433
+ log.debug("%s Running instruction injection callback...", log_identifier)
1434
+
1435
+ if not host_component:
1436
+ log.error(
1437
+ "%s Host component instance not provided. Cannot inject instructions.",
1438
+ log_identifier,
1439
+ )
1440
+ return None
1441
+
1442
+ injected_instructions = []
1443
+
1444
+ planning_instruction = """\
1445
+ Parallel Tool Calling:
1446
+ The system is capable of calling multiple tools in parallel to speed up processing. Please try to run tools in parallel when they don't depend on each other. This saves money and time, providing faster results to the user.
1447
+
1448
+ **Response Formatting - CRITICAL**:
1449
+ In most cases when calling tools, you should produce NO visible text at all - only status_update embeds and the tool calls themselves.
1450
+ The user can see your tool calls and status updates, so narrating your actions is redundant and creates noise.
1451
+
1452
+ If you do include visible text:
1453
+ - It must contain actual results, insights, or answers - NOT process narration
1454
+ - Do NOT end with a colon (":") before tool calls, as this leaves it hanging
1455
+ - Prefer ending with a period (".") if you must include visible text
1456
+
1457
+ Examples:
1458
+ - BEST: "{open_delim}status_update:Searching database...{close_delim}" [then calls tool, NO visible text]
1459
+ - BAD: "Let me search for that information." [then calls tool]
1460
+ - BAD: "Searching for information..." [then calls tool]
1461
+
1462
+ Embeds in responses from agents:
1463
+ To be efficient, peer agents may respond with artifact_content in their responses. These will not be resolved until they are sent back to a gateway. If it makes
1464
+ sense, just carry that embed forward to your response to the user. For example, if you ask for an org chart from another agent and its response contains an embed like
1465
+ `{open_delim}artifact_content:org_chart.md{close_delim}`, you can just include that embed in your response to the user. The gateway will resolve it and display the org chart.
1466
+
1467
+ Similarly, template_liquid blocks in peer agent responses can be carried forward to your response to the user for resolution by the gateway.
1468
+
1469
+ When faced with a complex goal or request that involves multiple steps, data retrieval, or artifact summarization to produce a new report or document, you MUST first create a plan.
1470
+ Simple, direct requests like 'create an image of a dog' or 'write an email to thank my boss' do not require a plan.
1471
+
1472
+ If a plan is created:
1473
+ 1. It should be a terse, hierarchical list describing the steps needed, with each checkbox item on its own line.
1474
+ 2. Use '⬜' for pending items, '✅' for completed items, and '❌' for cancelled items.
1475
+ 3. If the plan changes significantly during execution, restate the updated plan.
1476
+ 4. As items are completed, update the plan to check them off.
1477
+
1478
+ """
1479
+ injected_instructions.append(planning_instruction)
1480
+
1481
+ # Add the consolidated block instructions
1482
+ injected_instructions.append(_generate_fenced_artifact_instruction())
1483
+ injected_instructions.append(_generate_inline_template_instruction())
1484
+ injected_instructions.append(_generate_fenced_block_syntax_rules())
1485
+
1486
+ agent_instruction_str: Optional[str] = None
1487
+ if host_component._agent_system_instruction_callback:
1488
+ log.debug(
1489
+ "%s Calling agent-provided system instruction callback.", log_identifier
1490
+ )
1491
+ try:
1492
+ agent_instruction_str = host_component._agent_system_instruction_callback(
1493
+ callback_context, llm_request
1494
+ )
1495
+ if agent_instruction_str and isinstance(agent_instruction_str, str):
1496
+ injected_instructions.append(agent_instruction_str)
1497
+ log.info(
1498
+ "%s Injected instructions from agent callback.", log_identifier
1499
+ )
1500
+ elif agent_instruction_str:
1501
+ log.warning(
1502
+ "%s Agent instruction callback returned non-string type: %s. Ignoring.",
1503
+ log_identifier,
1504
+ type(agent_instruction_str),
1505
+ )
1506
+ except Exception as e_cb:
1507
+ log.error(
1508
+ "%s Error in agent-provided system instruction callback: %s. Skipping.",
1509
+ log_identifier,
1510
+ e_cb,
1511
+ )
1512
+ if host_component._agent_system_instruction_string:
1513
+ log.debug(
1514
+ "%s Using agent-provided static system instruction string.", log_identifier
1515
+ )
1516
+ agent_instruction_str = host_component._agent_system_instruction_string
1517
+ if agent_instruction_str and isinstance(agent_instruction_str, str):
1518
+ injected_instructions.append(agent_instruction_str)
1519
+ log.info("%s Injected static instructions from agent.", log_identifier)
1520
+
1521
+ contents = llm_request.contents
1522
+ if contents:
1523
+ log.debug("\n\n### LLM Request Contents ###")
1524
+ for content in contents:
1525
+ if content.parts:
1526
+ for part in content.parts:
1527
+ if part.text:
1528
+ log.debug("Content part: %s", part.text)
1529
+ elif part.function_call:
1530
+ log.debug("Function call: %s", part.function_call.name)
1531
+ elif part.function_response:
1532
+ log.debug("Function response: %s", part.function_response)
1533
+ else:
1534
+ log.debug("raw: %s", part)
1535
+ log.debug("### End LLM Request Contents ###\n\n")
1536
+
1537
+ if host_component.get_config("enable_embed_resolution", True):
1538
+ include_artifact_content_instr = host_component.get_config(
1539
+ "enable_artifact_content_instruction", True
1540
+ )
1541
+ instruction = _generate_embed_instruction(
1542
+ include_artifact_content_instr, log_identifier
1543
+ )
1544
+ if instruction:
1545
+ injected_instructions.append(instruction)
1546
+ log.debug(
1547
+ "%s Prepared embed instructions (artifact_content included: %s).",
1548
+ log_identifier,
1549
+ include_artifact_content_instr,
1550
+ )
1551
+
1552
+ instruction = _generate_conversation_flow_instruction()
1553
+ if instruction:
1554
+ injected_instructions.append(instruction)
1555
+ log.debug("%s Prepared conversation flow instructions.", log_identifier)
1556
+
1557
+ if active_builtin_tools:
1558
+ instruction = _generate_tool_instructions_from_registry(
1559
+ active_builtin_tools, log_identifier
1560
+ )
1561
+ if instruction:
1562
+ injected_instructions.append(instruction)
1563
+ log.debug(
1564
+ "%s Prepared instructions for %d active built-in tools.",
1565
+ log_identifier,
1566
+ len(active_builtin_tools),
1567
+ )
1568
+
1569
+ peer_instructions = callback_context.state.get("peer_tool_instructions")
1570
+ if peer_instructions and isinstance(peer_instructions, str):
1571
+ injected_instructions.append(peer_instructions)
1572
+ log.debug(
1573
+ "%s Injected peer discovery instructions from callback state.",
1574
+ log_identifier,
1575
+ )
1576
+
1577
+ last_call_notification_message_added = False
1578
+ try:
1579
+ invocation_context = callback_context._invocation_context
1580
+ if invocation_context and invocation_context.run_config:
1581
+ current_llm_calls = (
1582
+ invocation_context._invocation_cost_manager._number_of_llm_calls
1583
+ )
1584
+ max_llm_calls = invocation_context.run_config.max_llm_calls
1585
+
1586
+ log.debug(
1587
+ "%s Checking for last LLM call: current_calls=%d, max_calls=%s",
1588
+ log_identifier,
1589
+ current_llm_calls,
1590
+ max_llm_calls,
1591
+ )
1592
+
1593
+ if (
1594
+ max_llm_calls
1595
+ and max_llm_calls > 0
1596
+ and current_llm_calls >= (max_llm_calls - 1)
1597
+ ):
1598
+ last_call_text = (
1599
+ "IMPORTANT: This is your final allowed interaction for the current request. "
1600
+ "Please inform the user that to continue this line of inquiry, they will need to "
1601
+ "make a new request or explicitly ask to continue if the interface supports it. "
1602
+ "Summarize your current findings and conclude your response."
1603
+ )
1604
+ if llm_request.contents is None:
1605
+ llm_request.contents = []
1606
+
1607
+ last_call_content = adk_types.Content(
1608
+ role="model",
1609
+ parts=[adk_types.Part(text=last_call_text)],
1610
+ )
1611
+ llm_request.contents.append(last_call_content)
1612
+ last_call_notification_message_added = True
1613
+ log.info(
1614
+ "%s Added 'last LLM call' notification as a 'model' message to llm_request.contents. Current calls (%d) reached max_llm_calls (%d).",
1615
+ log_identifier,
1616
+ current_llm_calls,
1617
+ max_llm_calls,
1618
+ )
1619
+ except Exception as e_last_call:
1620
+ log.error(
1621
+ "%s Error checking/injecting last LLM call notification message: %s",
1622
+ log_identifier,
1623
+ e_last_call,
1624
+ )
1625
+
1626
+ injected_instructions.append(_generate_examples_instruction())
1627
+
1628
+ if injected_instructions:
1629
+ combined_instructions = "\n\n---\n\n".join(injected_instructions)
1630
+ if llm_request.config is None:
1631
+ log.warning(
1632
+ "%s llm_request.config is None, cannot append system instructions.",
1633
+ log_identifier,
1634
+ )
1635
+ else:
1636
+ if llm_request.config.system_instruction is None:
1637
+ llm_request.config.system_instruction = ""
1638
+
1639
+ if llm_request.config.system_instruction:
1640
+ llm_request.config.system_instruction += (
1641
+ "\n\n---\n\n" + combined_instructions
1642
+ )
1643
+ else:
1644
+ llm_request.config.system_instruction = combined_instructions
1645
+ log.info(
1646
+ "%s Injected %d dynamic instruction block(s) into llm_request.config.system_instruction.",
1647
+ log_identifier,
1648
+ len(injected_instructions),
1649
+ )
1650
+ elif not last_call_notification_message_added:
1651
+ log.debug(
1652
+ "%s No dynamic instructions (system or last_call message) were injected based on config.",
1653
+ log_identifier,
1654
+ )
1655
+
1656
+ return None
1657
+
1658
+
1659
+ async def after_tool_callback_inject_metadata(
1660
+ tool: BaseTool,
1661
+ args: Dict,
1662
+ tool_context: ToolContext,
1663
+ tool_response: Dict,
1664
+ host_component: "SamAgentComponent",
1665
+ ) -> Optional[Dict]:
1666
+ """
1667
+ ADK after_tool_callback to automatically load and inject metadata for
1668
+ newly created artifacts into the tool's response dictionary.
1669
+ """
1670
+ log_identifier = f"[Callback:InjectMetadata:{tool.name}]"
1671
+ log.info(
1672
+ "%s Starting metadata injection for tool response, type: %s",
1673
+ log_identifier,
1674
+ type(tool_response).__name__,
1675
+ )
1676
+
1677
+ if not host_component:
1678
+ log.error(
1679
+ "%s Host component instance not provided. Cannot proceed.",
1680
+ log_identifier,
1681
+ )
1682
+ return None
1683
+
1684
+ if not tool_context.actions.artifact_delta:
1685
+ log.debug(
1686
+ "%s No artifact delta found. Skipping metadata injection.", log_identifier
1687
+ )
1688
+ return None
1689
+
1690
+ artifact_service: Optional[BaseArtifactService] = (
1691
+ tool_context._invocation_context.artifact_service
1692
+ )
1693
+ if not artifact_service:
1694
+ log.error(
1695
+ "%s ArtifactService not available. Cannot load metadata.",
1696
+ log_identifier,
1697
+ )
1698
+ return None
1699
+
1700
+ app_name = tool_context._invocation_context.app_name
1701
+ user_id = tool_context._invocation_context.user_id
1702
+ session_id = get_original_session_id(tool_context._invocation_context)
1703
+
1704
+ metadata_texts = []
1705
+
1706
+ for filename, version in tool_context.actions.artifact_delta.items():
1707
+ if filename.endswith(METADATA_SUFFIX):
1708
+ log.debug(
1709
+ "%s Skipping metadata artifact '%s' itself.", log_identifier, filename
1710
+ )
1711
+ continue
1712
+
1713
+ metadata_filename = f"{filename}{METADATA_SUFFIX}"
1714
+ log.debug(
1715
+ "%s Found data artifact '%s' v%d. Attempting to load metadata '%s' v%d.",
1716
+ log_identifier,
1717
+ filename,
1718
+ version,
1719
+ metadata_filename,
1720
+ version,
1721
+ )
1722
+
1723
+ try:
1724
+ metadata_part = await artifact_service.load_artifact(
1725
+ app_name=app_name,
1726
+ user_id=user_id,
1727
+ session_id=session_id,
1728
+ filename=metadata_filename,
1729
+ version=version,
1730
+ )
1731
+
1732
+ if metadata_part and metadata_part.inline_data:
1733
+ try:
1734
+ metadata_dict = json.loads(
1735
+ metadata_part.inline_data.data.decode("utf-8")
1736
+ )
1737
+ metadata_dict["version"] = version
1738
+ metadata_dict["filename"] = filename
1739
+ formatted_text = format_metadata_for_llm(metadata_dict)
1740
+ metadata_texts.append(formatted_text)
1741
+ log.info(
1742
+ "%s Successfully loaded and formatted metadata for '%s' v%d.",
1743
+ log_identifier,
1744
+ filename,
1745
+ version,
1746
+ )
1747
+ except json.JSONDecodeError as json_err:
1748
+ log.warning(
1749
+ "%s Failed to parse metadata JSON for '%s' v%d: %s",
1750
+ log_identifier,
1751
+ metadata_filename,
1752
+ version,
1753
+ json_err,
1754
+ )
1755
+ except Exception as fmt_err:
1756
+ log.warning(
1757
+ "%s Failed to format metadata for '%s' v%d: %s",
1758
+ log_identifier,
1759
+ metadata_filename,
1760
+ version,
1761
+ fmt_err,
1762
+ )
1763
+ else:
1764
+ log.warning(
1765
+ "%s Companion metadata artifact '%s' v%d not found or empty.",
1766
+ log_identifier,
1767
+ metadata_filename,
1768
+ version,
1769
+ )
1770
+
1771
+ except Exception as load_err:
1772
+ log.error(
1773
+ "%s Error loading companion metadata artifact '%s' v%d: %s",
1774
+ log_identifier,
1775
+ metadata_filename,
1776
+ version,
1777
+ load_err,
1778
+ )
1779
+
1780
+ if metadata_texts:
1781
+ if not isinstance(tool_response, dict):
1782
+ log.error(
1783
+ "%s Tool response is not a dictionary. Cannot inject metadata. Type: %s",
1784
+ log_identifier,
1785
+ type(tool_response),
1786
+ )
1787
+ return None
1788
+
1789
+ combined_metadata_text = "\n\n".join(metadata_texts)
1790
+ tool_response[METADATA_RESPONSE_KEY] = combined_metadata_text
1791
+ log.info(
1792
+ "%s Injected metadata for %d artifact(s) into tool response key '%s'.",
1793
+ log_identifier,
1794
+ len(metadata_texts),
1795
+ METADATA_RESPONSE_KEY,
1796
+ )
1797
+ return tool_response
1798
+ else:
1799
+ log.debug(
1800
+ "%s No metadata loaded or formatted. Returning original tool response.",
1801
+ log_identifier,
1802
+ )
1803
+ return None
1804
+
1805
+
1806
+ async def track_produced_artifacts_callback(
1807
+ tool: BaseTool,
1808
+ args: Dict,
1809
+ tool_context: ToolContext,
1810
+ tool_response: Dict,
1811
+ host_component: "SamAgentComponent",
1812
+ ) -> Optional[Dict]:
1813
+ """
1814
+ ADK after_tool_callback to automatically track all artifacts created by a tool.
1815
+ It inspects the artifact_delta and registers the created artifacts in the
1816
+ TaskExecutionContext.
1817
+ """
1818
+ log_identifier = f"[Callback:TrackArtifacts:{tool.name}]"
1819
+ log.debug("%s Starting artifact tracking for tool response.", log_identifier)
1820
+
1821
+ if not tool_context.actions.artifact_delta:
1822
+ log.debug("%s No artifact delta found. Skipping tracking.", log_identifier)
1823
+ return None
1824
+
1825
+ if not host_component:
1826
+ log.error(
1827
+ "%s Host component instance not provided. Cannot proceed.", log_identifier
1828
+ )
1829
+ return None
1830
+
1831
+ try:
1832
+ a2a_context = tool_context.state.get("a2a_context", {})
1833
+ logical_task_id = a2a_context.get("logical_task_id")
1834
+ if not logical_task_id:
1835
+ log.warning(
1836
+ "%s Could not find logical_task_id in tool_context. Cannot track artifacts.",
1837
+ log_identifier,
1838
+ )
1839
+ return None
1840
+
1841
+ with host_component.active_tasks_lock:
1842
+ task_context = host_component.active_tasks.get(logical_task_id)
1843
+
1844
+ if not task_context:
1845
+ log.warning(
1846
+ "%s TaskExecutionContext not found for task %s. Cannot track artifacts.",
1847
+ log_identifier,
1848
+ logical_task_id,
1849
+ )
1850
+ return None
1851
+
1852
+ for filename, version in tool_context.actions.artifact_delta.items():
1853
+ if filename.endswith(METADATA_SUFFIX):
1854
+ continue
1855
+ log.info(
1856
+ "%s Registering produced artifact '%s' v%d for task %s.",
1857
+ log_identifier,
1858
+ filename,
1859
+ version,
1860
+ logical_task_id,
1861
+ )
1862
+ task_context.register_produced_artifact(filename, version)
1863
+
1864
+ except Exception as e:
1865
+ log.exception(
1866
+ "%s Error during artifact tracking callback: %s", log_identifier, e
1867
+ )
1868
+
1869
+ return None
1870
+
1871
+
1872
+ def log_streaming_chunk_callback(
1873
+ callback_context: CallbackContext,
1874
+ llm_response: LlmResponse,
1875
+ host_component: "SamAgentComponent",
1876
+ ) -> Optional[LlmResponse]:
1877
+ """
1878
+ ADK after_model_callback to log the content of each LLM response chunk
1879
+ *after* potential modification by other callbacks (like embed resolution).
1880
+ """
1881
+ log_identifier = "[Callback:LogChunk]"
1882
+ try:
1883
+ content_str = "None"
1884
+ is_partial = llm_response.partial
1885
+ is_final = llm_response.turn_complete
1886
+ if llm_response.content and llm_response.content.parts:
1887
+ texts = [p.text for p in llm_response.content.parts if p.text]
1888
+ content_str = '"' + "".join(texts) + '"' if texts else "[Non-text parts]"
1889
+ elif llm_response.error_message:
1890
+ content_str = f"[ERROR: {llm_response.error_message}]"
1891
+
1892
+ except Exception as e:
1893
+ log.error("%s Error logging LLM chunk: %s", log_identifier, e)
1894
+
1895
+ return None
1896
+
1897
+
1898
+ def solace_llm_invocation_callback(
1899
+ callback_context: CallbackContext,
1900
+ llm_request: LlmRequest,
1901
+ host_component: "SamAgentComponent",
1902
+ ) -> Optional[LlmResponse]:
1903
+ """
1904
+ ADK before_model_callback to send a Solace message when an LLM is invoked,
1905
+ using the host_component's process_and_publish_adk_event method.
1906
+ """
1907
+ log_identifier = "[Callback:SolaceLLMInvocation]"
1908
+ log.debug(
1909
+ "%s Running Solace LLM invocation notification callback...", log_identifier
1910
+ )
1911
+
1912
+ if not host_component:
1913
+ log.error(
1914
+ "%s Host component instance not provided. Cannot send Solace message.",
1915
+ log_identifier,
1916
+ )
1917
+ return None
1918
+
1919
+ callback_context.state[A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY] = False
1920
+ log.debug(
1921
+ "%s Reset %s to False.", log_identifier, A2A_LLM_STREAM_CHUNKS_PROCESSED_KEY
1922
+ )
1923
+
1924
+ try:
1925
+ a2a_context = callback_context.state.get("a2a_context")
1926
+ if not a2a_context:
1927
+ log.error(
1928
+ "%s a2a_context not found in callback_context.state. Cannot send Solace message.",
1929
+ log_identifier,
1930
+ )
1931
+ return None
1932
+
1933
+ logical_task_id = a2a_context.get("logical_task_id")
1934
+ context_id = a2a_context.get("contextId")
1935
+
1936
+ # Store model name in callback state for later use in response callback
1937
+ model_name = host_component.model_config
1938
+ if isinstance(model_name, dict):
1939
+ model_name = model_name.get("model", "unknown")
1940
+ callback_context.state["model_name"] = model_name
1941
+
1942
+ llm_data = LlmInvocationData(request=llm_request.model_dump(exclude_none=True))
1943
+ status_update_event = a2a.create_data_signal_event(
1944
+ task_id=logical_task_id,
1945
+ context_id=context_id,
1946
+ signal_data=llm_data,
1947
+ agent_name=host_component.agent_name,
1948
+ )
1949
+
1950
+ loop = host_component.get_async_loop()
1951
+ if loop and loop.is_running():
1952
+ asyncio.run_coroutine_threadsafe(
1953
+ host_component._publish_status_update_with_buffer_flush(
1954
+ status_update_event,
1955
+ a2a_context,
1956
+ skip_buffer_flush=False,
1957
+ ),
1958
+ loop,
1959
+ )
1960
+ log.debug(
1961
+ "%s Scheduled LLM invocation status update with buffer flush.",
1962
+ log_identifier,
1963
+ )
1964
+ else:
1965
+ log.error(
1966
+ "%s Async loop not available. Cannot publish LLM invocation status update.",
1967
+ log_identifier,
1968
+ )
1969
+
1970
+ except Exception as e:
1971
+ log.error(
1972
+ "%s Error during Solace LLM invocation notification: %s", log_identifier, e
1973
+ )
1974
+
1975
+ return None
1976
+
1977
+
1978
+ def solace_llm_response_callback(
1979
+ callback_context: CallbackContext,
1980
+ llm_response: LlmResponse,
1981
+ host_component: "SamAgentComponent",
1982
+ ) -> Optional[LlmResponse]:
1983
+ """
1984
+ ADK after_model_callback to send a Solace message with the LLM's response
1985
+ and token usage information.
1986
+ """
1987
+ log_identifier = "[Callback:SolaceLLMResponse]"
1988
+ if llm_response.partial: # Don't send partial responses for this notification
1989
+ log.debug("%s Skipping partial response", log_identifier)
1990
+ return None
1991
+
1992
+ if not host_component:
1993
+ log.error(
1994
+ "%s Host component instance not provided. Cannot send Solace message.",
1995
+ log_identifier,
1996
+ )
1997
+ return None
1998
+
1999
+ try:
2000
+ a2a_context = callback_context.state.get("a2a_context")
2001
+ if not a2a_context:
2002
+ log.error(
2003
+ "%s a2a_context not found in callback_context.state. Cannot send Solace message.",
2004
+ log_identifier,
2005
+ )
2006
+ return None
2007
+
2008
+ agent_name = host_component.get_config("agent_name", "unknown_agent")
2009
+ logical_task_id = a2a_context.get("logical_task_id")
2010
+
2011
+ llm_response_data = {
2012
+ "type": "llm_response",
2013
+ "data": llm_response.model_dump(exclude_none=True),
2014
+ }
2015
+
2016
+ # Extract and record token usage
2017
+ if llm_response.usage_metadata:
2018
+ usage = llm_response.usage_metadata
2019
+ model_name = callback_context.state.get("model_name", "unknown")
2020
+
2021
+ usage_dict = {
2022
+ "input_tokens": usage.prompt_token_count,
2023
+ "output_tokens": usage.candidates_token_count,
2024
+ "model": model_name,
2025
+ }
2026
+
2027
+ # Check for cached tokens (provider-specific)
2028
+ cached_tokens = 0
2029
+ if hasattr(usage, "prompt_tokens_details") and usage.prompt_tokens_details:
2030
+ cached_tokens = getattr(usage.prompt_tokens_details, "cached_tokens", 0)
2031
+ if cached_tokens > 0:
2032
+ usage_dict["cached_input_tokens"] = cached_tokens
2033
+
2034
+ # Add to response data
2035
+ llm_response_data["usage"] = usage_dict
2036
+
2037
+ # Record in task context for aggregation
2038
+ with host_component.active_tasks_lock:
2039
+ task_context = host_component.active_tasks.get(logical_task_id)
2040
+
2041
+ if task_context:
2042
+ task_context.record_token_usage(
2043
+ input_tokens=usage.prompt_token_count,
2044
+ output_tokens=usage.candidates_token_count,
2045
+ model=model_name,
2046
+ source="agent",
2047
+ cached_input_tokens=cached_tokens,
2048
+ )
2049
+ log.debug(
2050
+ "%s Recorded token usage: input=%d, output=%d, cached=%d, model=%s",
2051
+ log_identifier,
2052
+ usage.prompt_token_count,
2053
+ usage.candidates_token_count,
2054
+ cached_tokens,
2055
+ model_name,
2056
+ )
2057
+
2058
+ # This signal doesn't have a dedicated Pydantic model, so we create the
2059
+ # DataPart directly and use the lower-level helpers.
2060
+ data_part = a2a.create_data_part(data=llm_response_data)
2061
+ a2a_message = a2a.create_agent_parts_message(
2062
+ parts=[data_part],
2063
+ task_id=logical_task_id,
2064
+ context_id=a2a_context.get("contextId"),
2065
+ )
2066
+ status_update_event = a2a.create_status_update(
2067
+ task_id=logical_task_id,
2068
+ context_id=a2a_context.get("contextId"),
2069
+ message=a2a_message,
2070
+ is_final=False,
2071
+ metadata={"agent_name": agent_name},
2072
+ )
2073
+ loop = host_component.get_async_loop()
2074
+ if loop and loop.is_running():
2075
+ asyncio.run_coroutine_threadsafe(
2076
+ host_component._publish_status_update_with_buffer_flush(
2077
+ status_update_event,
2078
+ a2a_context,
2079
+ skip_buffer_flush=False,
2080
+ ),
2081
+ loop,
2082
+ )
2083
+ log.debug(
2084
+ "%s Scheduled LLM response status update with buffer flush (final_chunk=%s).",
2085
+ log_identifier,
2086
+ llm_response.turn_complete,
2087
+ )
2088
+ else:
2089
+ log.error(
2090
+ "%s Async loop not available. Cannot publish LLM response status update.",
2091
+ log_identifier,
2092
+ )
2093
+
2094
+ except Exception as e:
2095
+ log.error(
2096
+ "%s Error during Solace LLM response notification: %s", log_identifier, e
2097
+ )
2098
+
2099
+ return None
2100
+
2101
+
2102
+ def notify_tool_invocation_start_callback(
2103
+ tool: BaseTool,
2104
+ args: Dict[str, Any],
2105
+ tool_context: ToolContext,
2106
+ host_component: "SamAgentComponent",
2107
+ ) -> None:
2108
+ """
2109
+ ADK before_tool_callback to send an A2A status message indicating
2110
+ that a tool is about to be invoked.
2111
+ """
2112
+ log_identifier = f"[Callback:NotifyToolInvocationStart:{tool.name}]"
2113
+ log.debug(
2114
+ "%s Triggered for tool '%s' with args: %s", log_identifier, tool.name, args
2115
+ )
2116
+
2117
+ if not host_component:
2118
+ log.error(
2119
+ "%s Host component instance not provided. Cannot send notification.",
2120
+ log_identifier,
2121
+ )
2122
+ return
2123
+
2124
+ a2a_context = tool_context.state.get("a2a_context")
2125
+ if not a2a_context:
2126
+ log.error(
2127
+ "%s a2a_context not found in tool_context.state. Cannot send notification.",
2128
+ log_identifier,
2129
+ )
2130
+ return
2131
+
2132
+ try:
2133
+ serializable_args = {}
2134
+ for k, v in args.items():
2135
+ try:
2136
+ json.dumps(v)
2137
+ serializable_args[k] = v
2138
+ except TypeError:
2139
+ serializable_args[k] = str(v)
2140
+
2141
+ tool_data = ToolInvocationStartData(
2142
+ tool_name=tool.name,
2143
+ tool_args=serializable_args,
2144
+ function_call_id=tool_context.function_call_id,
2145
+ )
2146
+ asyncio.run_coroutine_threadsafe(
2147
+ _publish_data_part_status_update(host_component, a2a_context, tool_data),
2148
+ host_component.get_async_loop(),
2149
+ )
2150
+ log.debug(
2151
+ "%s Scheduled tool_invocation_start notification.",
2152
+ log_identifier,
2153
+ )
2154
+
2155
+ except Exception as e:
2156
+ log.exception(
2157
+ "%s Error publishing tool_invocation_start status update: %s",
2158
+ log_identifier,
2159
+ e,
2160
+ )
2161
+
2162
+ return None
2163
+
2164
+
2165
+ def notify_tool_execution_result_callback(
2166
+ tool: BaseTool,
2167
+ args: Dict[str, Any],
2168
+ tool_context: ToolContext,
2169
+ tool_response: Any,
2170
+ host_component: "SamAgentComponent",
2171
+ ) -> None:
2172
+ """
2173
+ ADK after_tool_callback to send an A2A status message with the result
2174
+ of a tool's execution.
2175
+ """
2176
+ log_identifier = f"[Callback:NotifyToolResult:{tool.name}]"
2177
+ log.debug("%s Triggered for tool '%s'", log_identifier, tool.name)
2178
+
2179
+ if not host_component:
2180
+ log.error(
2181
+ "%s Host component instance not provided. Cannot send notification.",
2182
+ log_identifier,
2183
+ )
2184
+ return
2185
+
2186
+ a2a_context = tool_context.state.get("a2a_context")
2187
+ if not a2a_context:
2188
+ log.error(
2189
+ "%s a2a_context not found in tool_context.state. Cannot send notification.",
2190
+ log_identifier,
2191
+ )
2192
+ return
2193
+
2194
+ if tool.is_long_running and not tool_response:
2195
+ log.debug(
2196
+ "%s Tool is long-running and is not yet complete. Don't notify its completion",
2197
+ log_identifier,
2198
+ )
2199
+ return
2200
+
2201
+ try:
2202
+ # Attempt to make the response JSON serializable
2203
+ serializable_response = tool_response
2204
+ if hasattr(tool_response, "model_dump"):
2205
+ serializable_response = tool_response.model_dump(exclude_none=True)
2206
+ else:
2207
+ try:
2208
+ # A simple check to see if it can be dumped.
2209
+ # This isn't perfect but catches many non-serializable types.
2210
+ json.dumps(tool_response)
2211
+ except (TypeError, OverflowError):
2212
+ serializable_response = str(tool_response)
2213
+
2214
+ tool_data = ToolResultData(
2215
+ tool_name=tool.name,
2216
+ result_data=serializable_response,
2217
+ function_call_id=tool_context.function_call_id,
2218
+ )
2219
+ asyncio.run_coroutine_threadsafe(
2220
+ _publish_data_part_status_update(host_component, a2a_context, tool_data),
2221
+ host_component.get_async_loop(),
2222
+ )
2223
+ log.debug(
2224
+ "%s Scheduled tool_result notification for function call ID %s.",
2225
+ log_identifier,
2226
+ tool_context.function_call_id,
2227
+ )
2228
+
2229
+ except Exception as e:
2230
+ log.exception(
2231
+ "%s Error publishing tool_result status update: %s",
2232
+ log_identifier,
2233
+ e,
2234
+ )
2235
+
2236
+ return None
2237
+
2238
+
2239
+ def auto_continue_on_max_tokens_callback(
2240
+ callback_context: CallbackContext,
2241
+ llm_response: LlmResponse,
2242
+ host_component: "SamAgentComponent",
2243
+ ) -> Optional[LlmResponse]:
2244
+ """
2245
+ ADK after_model_callback to automatically continue an LLM response that
2246
+ was interrupted. This handles two interruption signals:
2247
+ 1. The explicit `llm_response.interrupted` flag from the ADK.
2248
+ 2. An implicit signal where the model itself calls a `_continue` tool.
2249
+ """
2250
+ log_identifier = "[Callback:AutoContinue]"
2251
+
2252
+ if not host_component.get_config("enable_auto_continuation", True):
2253
+ log.debug("%s Auto-continuation is disabled. Skipping.", log_identifier)
2254
+ return None
2255
+
2256
+ # An interruption is signaled by either the explicit flag or an implicit tool call.
2257
+ was_explicitly_interrupted = llm_response.interrupted
2258
+ was_implicitly_interrupted = False
2259
+ if llm_response.content and llm_response.content.parts:
2260
+ if any(
2261
+ p.function_call and p.function_call.name == "_continue"
2262
+ for p in llm_response.content.parts
2263
+ ):
2264
+ was_implicitly_interrupted = True
2265
+
2266
+ if not was_explicitly_interrupted and not was_implicitly_interrupted:
2267
+ return None
2268
+
2269
+ log.info(
2270
+ "%s Interruption signal detected (explicit: %s, implicit: %s). Triggering auto-continuation.",
2271
+ log_identifier,
2272
+ was_explicitly_interrupted,
2273
+ was_implicitly_interrupted,
2274
+ )
2275
+
2276
+ # Get existing parts from the response, but filter out any `_continue` calls
2277
+ # the model might have added.
2278
+ existing_parts = []
2279
+ if llm_response.content and llm_response.content.parts:
2280
+ existing_parts = [
2281
+ p
2282
+ for p in llm_response.content.parts
2283
+ if not (p.function_call and p.function_call.name == "_continue")
2284
+ ]
2285
+ if was_implicitly_interrupted:
2286
+ log.debug(
2287
+ "%s Removed implicit '_continue' tool call from response parts.",
2288
+ log_identifier,
2289
+ )
2290
+
2291
+ continue_tool_call = adk_types.FunctionCall(
2292
+ name="_continue_generation",
2293
+ args={},
2294
+ id=f"host-continue-{uuid.uuid4()}",
2295
+ )
2296
+ continue_part = adk_types.Part(function_call=continue_tool_call)
2297
+
2298
+ all_parts = existing_parts + [continue_part]
2299
+
2300
+ # If there was no text content in the interrupted part, add a space to ensure
2301
+ # the event is not filtered out by history processing logic.
2302
+ if not any(p.text for p in existing_parts):
2303
+ all_parts.insert(0, adk_types.Part(text=" "))
2304
+ log.debug(
2305
+ "%s Prepended empty text part to ensure event is preserved.", log_identifier
2306
+ )
2307
+
2308
+ # Create a new, non-interrupted LlmResponse containing all parts.
2309
+ # This ensures the partial text is saved to history and the tool call is executed.
2310
+ hijacked_response = LlmResponse(
2311
+ content=adk_types.Content(role="model", parts=all_parts),
2312
+ partial=False,
2313
+ custom_metadata={
2314
+ "was_interrupted": True,
2315
+ },
2316
+ )
2317
+
2318
+ return hijacked_response