solace-agent-mesh 0.2.4__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (518) hide show
  1. solace_agent_mesh/agent/adk/adk_llm.txt +93 -0
  2. solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
  3. solace_agent_mesh/agent/adk/callbacks.py +1694 -0
  4. solace_agent_mesh/agent/adk/filesystem_artifact_service.py +381 -0
  5. solace_agent_mesh/agent/adk/invocation_monitor.py +295 -0
  6. solace_agent_mesh/agent/adk/models/lite_llm.py +872 -0
  7. solace_agent_mesh/agent/adk/models/models_llm.txt +94 -0
  8. solace_agent_mesh/agent/adk/runner.py +353 -0
  9. solace_agent_mesh/agent/adk/services.py +240 -0
  10. solace_agent_mesh/agent/adk/setup.py +751 -0
  11. solace_agent_mesh/agent/adk/stream_parser.py +214 -0
  12. solace_agent_mesh/agent/adk/tool_wrapper.py +139 -0
  13. solace_agent_mesh/agent/agent_llm.txt +41 -0
  14. solace_agent_mesh/agent/protocol/event_handlers.py +1469 -0
  15. solace_agent_mesh/agent/protocol/protocol_llm.txt +21 -0
  16. solace_agent_mesh/agent/sac/app.py +640 -0
  17. solace_agent_mesh/agent/sac/component.py +3388 -0
  18. solace_agent_mesh/agent/sac/patch_adk.py +111 -0
  19. solace_agent_mesh/agent/sac/sac_llm.txt +105 -0
  20. solace_agent_mesh/agent/sac/task_execution_context.py +176 -0
  21. solace_agent_mesh/agent/testing/__init__.py +3 -0
  22. solace_agent_mesh/agent/testing/debug_utils.py +135 -0
  23. solace_agent_mesh/agent/testing/testing_llm.txt +90 -0
  24. solace_agent_mesh/agent/tools/__init__.py +14 -0
  25. solace_agent_mesh/agent/tools/audio_tools.py +1622 -0
  26. solace_agent_mesh/agent/tools/builtin_artifact_tools.py +1954 -0
  27. solace_agent_mesh/agent/tools/builtin_data_analysis_tools.py +238 -0
  28. solace_agent_mesh/agent/tools/general_agent_tools.py +569 -0
  29. solace_agent_mesh/agent/tools/image_tools.py +1184 -0
  30. solace_agent_mesh/agent/tools/peer_agent_tool.py +289 -0
  31. solace_agent_mesh/agent/tools/registry.py +36 -0
  32. solace_agent_mesh/agent/tools/test_tools.py +135 -0
  33. solace_agent_mesh/agent/tools/tool_definition.py +45 -0
  34. solace_agent_mesh/agent/tools/tools_llm.txt +104 -0
  35. solace_agent_mesh/agent/tools/web_tools.py +381 -0
  36. solace_agent_mesh/agent/utils/artifact_helpers.py +927 -0
  37. solace_agent_mesh/agent/utils/config_parser.py +47 -0
  38. solace_agent_mesh/agent/utils/context_helpers.py +60 -0
  39. solace_agent_mesh/agent/utils/utils_llm.txt +153 -0
  40. solace_agent_mesh/assets/docs/404.html +16 -0
  41. solace_agent_mesh/assets/docs/assets/css/styles.906a1503.css +1 -0
  42. solace_agent_mesh/assets/docs/assets/images/Solace_AI_Framework_With_Broker-85f0a306a9bcdd20b390b7a949f6d862.png +0 -0
  43. solace_agent_mesh/assets/docs/assets/images/sac-flows-80d5b603c6aafd33e87945680ce0abf3.png +0 -0
  44. solace_agent_mesh/assets/docs/assets/images/sac_parts_of_a_component-cb3d0424b1d0c17734c5435cca6b4082.png +0 -0
  45. solace_agent_mesh/assets/docs/assets/js/04989206.674a8007.js +1 -0
  46. solace_agent_mesh/assets/docs/assets/js/0e682baa.79f0ab22.js +1 -0
  47. solace_agent_mesh/assets/docs/assets/js/1001.0182a8bd.js +1 -0
  48. solace_agent_mesh/assets/docs/assets/js/1023fc19.015679ca.js +1 -0
  49. solace_agent_mesh/assets/docs/assets/js/1039.0bd46aa1.js +1 -0
  50. solace_agent_mesh/assets/docs/assets/js/149.b797a808.js +1 -0
  51. solace_agent_mesh/assets/docs/assets/js/1523c6b4.91c7bc01.js +1 -0
  52. solace_agent_mesh/assets/docs/assets/js/165.6a39807d.js +2 -0
  53. solace_agent_mesh/assets/docs/assets/js/165.6a39807d.js.LICENSE.txt +9 -0
  54. solace_agent_mesh/assets/docs/assets/js/166ab619.7d97ccaf.js +1 -0
  55. solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +2 -0
  56. solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js.LICENSE.txt +7 -0
  57. solace_agent_mesh/assets/docs/assets/js/1c6e87d2.23bccffb.js +1 -0
  58. solace_agent_mesh/assets/docs/assets/js/2130.ab9fd314.js +1 -0
  59. solace_agent_mesh/assets/docs/assets/js/21ceee5f.614fa8dd.js +1 -0
  60. solace_agent_mesh/assets/docs/assets/js/2237.5e477fc6.js +1 -0
  61. solace_agent_mesh/assets/docs/assets/js/2334.622a6395.js +1 -0
  62. solace_agent_mesh/assets/docs/assets/js/2a9cab12.8909df92.js +1 -0
  63. solace_agent_mesh/assets/docs/assets/js/3219.adc1d663.js +1 -0
  64. solace_agent_mesh/assets/docs/assets/js/332e10b5.7a103f42.js +1 -0
  65. solace_agent_mesh/assets/docs/assets/js/3624.b524e433.js +1 -0
  66. solace_agent_mesh/assets/docs/assets/js/375.708d48db.js +1 -0
  67. solace_agent_mesh/assets/docs/assets/js/3834.b6cd790e.js +1 -0
  68. solace_agent_mesh/assets/docs/assets/js/3d406171.f722eaf5.js +1 -0
  69. solace_agent_mesh/assets/docs/assets/js/4250.95455b28.js +1 -0
  70. solace_agent_mesh/assets/docs/assets/js/42b3f8d8.36090198.js +1 -0
  71. solace_agent_mesh/assets/docs/assets/js/4356.d169ab5b.js +1 -0
  72. solace_agent_mesh/assets/docs/assets/js/442a8107.5ba94b65.js +1 -0
  73. solace_agent_mesh/assets/docs/assets/js/4458.518e66fa.js +1 -0
  74. solace_agent_mesh/assets/docs/assets/js/4488.c7cc3442.js +1 -0
  75. solace_agent_mesh/assets/docs/assets/js/4494.6ee23046.js +1 -0
  76. solace_agent_mesh/assets/docs/assets/js/4855.fc4444b6.js +1 -0
  77. solace_agent_mesh/assets/docs/assets/js/4866.22daefc0.js +1 -0
  78. solace_agent_mesh/assets/docs/assets/js/4950.ca4caeda.js +1 -0
  79. solace_agent_mesh/assets/docs/assets/js/4c2787c2.66ee00e9.js +1 -0
  80. solace_agent_mesh/assets/docs/assets/js/5388.7a136447.js +1 -0
  81. solace_agent_mesh/assets/docs/assets/js/55f47984.c484bf96.js +1 -0
  82. solace_agent_mesh/assets/docs/assets/js/5607.081356f8.js +1 -0
  83. solace_agent_mesh/assets/docs/assets/js/5864.b0d0e9de.js +1 -0
  84. solace_agent_mesh/assets/docs/assets/js/5b4258a4.bda20761.js +1 -0
  85. solace_agent_mesh/assets/docs/assets/js/5e95c892.558d5167.js +1 -0
  86. solace_agent_mesh/assets/docs/assets/js/6143.0a1464c9.js +1 -0
  87. solace_agent_mesh/assets/docs/assets/js/6395.e9c73649.js +1 -0
  88. solace_agent_mesh/assets/docs/assets/js/6796.51d2c9b7.js +1 -0
  89. solace_agent_mesh/assets/docs/assets/js/6976.379be23b.js +1 -0
  90. solace_agent_mesh/assets/docs/assets/js/6978.ee0b945c.js +1 -0
  91. solace_agent_mesh/assets/docs/assets/js/7040.cb436723.js +1 -0
  92. solace_agent_mesh/assets/docs/assets/js/7195.412f418a.js +1 -0
  93. solace_agent_mesh/assets/docs/assets/js/7280.3fb73bdb.js +1 -0
  94. solace_agent_mesh/assets/docs/assets/js/768e31b0.a12673db.js +1 -0
  95. solace_agent_mesh/assets/docs/assets/js/7845.e33e7c4c.js +1 -0
  96. solace_agent_mesh/assets/docs/assets/js/7900.69516146.js +1 -0
  97. solace_agent_mesh/assets/docs/assets/js/8356.8a379c04.js +1 -0
  98. solace_agent_mesh/assets/docs/assets/js/85387663.6bf41934.js +1 -0
  99. solace_agent_mesh/assets/docs/assets/js/8567.4732c6b7.js +1 -0
  100. solace_agent_mesh/assets/docs/assets/js/8573.cb04eda5.js +1 -0
  101. solace_agent_mesh/assets/docs/assets/js/8577.1d54e766.js +1 -0
  102. solace_agent_mesh/assets/docs/assets/js/8591.d7c16be6.js +2 -0
  103. solace_agent_mesh/assets/docs/assets/js/8591.d7c16be6.js.LICENSE.txt +61 -0
  104. solace_agent_mesh/assets/docs/assets/js/8709.7ecd4047.js +1 -0
  105. solace_agent_mesh/assets/docs/assets/js/8731.49e930c2.js +1 -0
  106. solace_agent_mesh/assets/docs/assets/js/8908.f9d1b506.js +1 -0
  107. solace_agent_mesh/assets/docs/assets/js/9157.b4093d07.js +1 -0
  108. solace_agent_mesh/assets/docs/assets/js/9278.a4fd875d.js +1 -0
  109. solace_agent_mesh/assets/docs/assets/js/945fb41e.74d728aa.js +1 -0
  110. solace_agent_mesh/assets/docs/assets/js/9616.b75c2f6d.js +1 -0
  111. solace_agent_mesh/assets/docs/assets/js/9793.c6d16376.js +1 -0
  112. solace_agent_mesh/assets/docs/assets/js/9eff14a2.1bf8f61c.js +1 -0
  113. solace_agent_mesh/assets/docs/assets/js/a3a92b25.26ca071f.js +1 -0
  114. solace_agent_mesh/assets/docs/assets/js/a7bd4aaa.2204d2f7.js +1 -0
  115. solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +1 -0
  116. solace_agent_mesh/assets/docs/assets/js/aba21aa0.c42a534c.js +1 -0
  117. solace_agent_mesh/assets/docs/assets/js/aba87c2f.d3e2dcc3.js +1 -0
  118. solace_agent_mesh/assets/docs/assets/js/ae4415af.8e279b5d.js +1 -0
  119. solace_agent_mesh/assets/docs/assets/js/b7006a3a.40b10c9d.js +1 -0
  120. solace_agent_mesh/assets/docs/assets/js/bac0be12.f50d9bac.js +1 -0
  121. solace_agent_mesh/assets/docs/assets/js/bb2ef573.207e6990.js +1 -0
  122. solace_agent_mesh/assets/docs/assets/js/c2c06897.63b76e9e.js +1 -0
  123. solace_agent_mesh/assets/docs/assets/js/cc969b05.954186d4.js +1 -0
  124. solace_agent_mesh/assets/docs/assets/js/cd3d4052.ca6eed8c.js +1 -0
  125. solace_agent_mesh/assets/docs/assets/js/ced92a13.fb92e7ca.js +1 -0
  126. solace_agent_mesh/assets/docs/assets/js/cee5d587.f5b73ca1.js +1 -0
  127. solace_agent_mesh/assets/docs/assets/js/f284c35a.ecc3d195.js +1 -0
  128. solace_agent_mesh/assets/docs/assets/js/f897a61a.2c2e152c.js +1 -0
  129. solace_agent_mesh/assets/docs/assets/js/fbfa3e75.aca209c9.js +1 -0
  130. solace_agent_mesh/assets/docs/assets/js/main.7ed3319f.js +2 -0
  131. solace_agent_mesh/assets/docs/assets/js/main.7ed3319f.js.LICENSE.txt +81 -0
  132. solace_agent_mesh/assets/docs/assets/js/runtime~main.d9520ae2.js +1 -0
  133. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +128 -0
  134. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +91 -0
  135. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +201 -0
  136. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +91 -0
  137. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +55 -0
  138. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +82 -0
  139. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +60 -0
  140. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +48 -0
  141. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +54 -0
  142. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +17 -0
  143. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +45 -0
  144. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +76 -0
  145. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +150 -0
  146. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +54 -0
  147. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +267 -0
  148. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +136 -0
  149. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +116 -0
  150. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +80 -0
  151. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +164 -0
  152. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +57 -0
  153. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +72 -0
  154. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +102 -0
  155. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +99 -0
  156. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +90 -0
  157. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +107 -0
  158. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +152 -0
  159. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +103 -0
  160. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +170 -0
  161. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +200 -0
  162. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +54 -0
  163. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +69 -0
  164. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +59 -0
  165. solace_agent_mesh/assets/docs/img/Solace_AI_Framework_README.png +0 -0
  166. solace_agent_mesh/assets/docs/img/Solace_AI_Framework_With_Broker.png +0 -0
  167. solace_agent_mesh/assets/docs/img/logo.png +0 -0
  168. solace_agent_mesh/assets/docs/img/sac-flows.png +0 -0
  169. solace_agent_mesh/assets/docs/img/sac_parts_of_a_component.png +0 -0
  170. solace_agent_mesh/assets/docs/img/solace-logo.png +0 -0
  171. solace_agent_mesh/assets/docs/lunr-index-1753813536522.json +1 -0
  172. solace_agent_mesh/assets/docs/lunr-index.json +1 -0
  173. solace_agent_mesh/assets/docs/search-doc-1753813536522.json +1 -0
  174. solace_agent_mesh/assets/docs/search-doc.json +1 -0
  175. solace_agent_mesh/assets/docs/sitemap.xml +1 -0
  176. solace_agent_mesh/cli/__init__.py +1 -1
  177. solace_agent_mesh/cli/commands/add_cmd/__init__.py +15 -0
  178. solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +250 -0
  179. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +659 -0
  180. solace_agent_mesh/cli/commands/add_cmd/gateway_cmd.py +322 -0
  181. solace_agent_mesh/cli/commands/add_cmd/web_add_agent_step.py +93 -0
  182. solace_agent_mesh/cli/commands/add_cmd/web_add_gateway_step.py +118 -0
  183. solace_agent_mesh/cli/commands/docs_cmd.py +57 -0
  184. solace_agent_mesh/cli/commands/eval_cmd.py +64 -0
  185. solace_agent_mesh/cli/commands/init_cmd/__init__.py +404 -0
  186. solace_agent_mesh/cli/commands/init_cmd/broker_step.py +201 -0
  187. solace_agent_mesh/cli/commands/init_cmd/directory_step.py +28 -0
  188. solace_agent_mesh/cli/commands/init_cmd/env_step.py +197 -0
  189. solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +365 -0
  190. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +387 -0
  191. solace_agent_mesh/cli/commands/init_cmd/project_files_step.py +38 -0
  192. solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +110 -0
  193. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +183 -0
  194. solace_agent_mesh/cli/commands/plugin_cmd/__init__.py +18 -0
  195. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +372 -0
  196. solace_agent_mesh/cli/commands/plugin_cmd/build_cmd.py +86 -0
  197. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +138 -0
  198. solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +309 -0
  199. solace_agent_mesh/cli/commands/plugin_cmd/official_registry.py +174 -0
  200. solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +305 -0
  201. solace_agent_mesh/cli/commands/run_cmd.py +158 -0
  202. solace_agent_mesh/cli/main.py +17 -294
  203. solace_agent_mesh/cli/utils.py +135 -204
  204. solace_agent_mesh/client/webui/frontend/static/assets/authCallback-DvlO62me.js +1 -0
  205. solace_agent_mesh/client/webui/frontend/static/assets/client-bp6u3qVZ.js +49 -0
  206. solace_agent_mesh/client/webui/frontend/static/assets/favicon-BLgzUch9.ico +0 -0
  207. solace_agent_mesh/client/webui/frontend/static/assets/main-An0a5j5k.js +663 -0
  208. solace_agent_mesh/client/webui/frontend/static/assets/main-Bu5-4Bac.css +1 -0
  209. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +14 -0
  210. solace_agent_mesh/client/webui/frontend/static/index.html +15 -0
  211. solace_agent_mesh/common/__init__.py +1 -0
  212. solace_agent_mesh/common/a2a_protocol.py +564 -0
  213. solace_agent_mesh/common/agent_registry.py +42 -0
  214. solace_agent_mesh/common/client/__init__.py +4 -0
  215. solace_agent_mesh/common/client/card_resolver.py +21 -0
  216. solace_agent_mesh/common/client/client.py +85 -0
  217. solace_agent_mesh/common/client/client_llm.txt +133 -0
  218. solace_agent_mesh/common/common_llm.txt +144 -0
  219. solace_agent_mesh/common/constants.py +1 -14
  220. solace_agent_mesh/common/middleware/__init__.py +12 -0
  221. solace_agent_mesh/common/middleware/config_resolver.py +130 -0
  222. solace_agent_mesh/common/middleware/middleware_llm.txt +174 -0
  223. solace_agent_mesh/common/middleware/registry.py +125 -0
  224. solace_agent_mesh/common/server/__init__.py +4 -0
  225. solace_agent_mesh/common/server/server.py +122 -0
  226. solace_agent_mesh/common/server/server_llm.txt +169 -0
  227. solace_agent_mesh/common/server/task_manager.py +291 -0
  228. solace_agent_mesh/common/server/utils.py +28 -0
  229. solace_agent_mesh/common/services/__init__.py +4 -0
  230. solace_agent_mesh/common/services/employee_service.py +162 -0
  231. solace_agent_mesh/common/services/identity_service.py +129 -0
  232. solace_agent_mesh/common/services/providers/__init__.py +4 -0
  233. solace_agent_mesh/common/services/providers/local_file_identity_service.py +148 -0
  234. solace_agent_mesh/common/services/providers/providers_llm.txt +113 -0
  235. solace_agent_mesh/common/services/services_llm.txt +132 -0
  236. solace_agent_mesh/common/types.py +411 -0
  237. solace_agent_mesh/common/utils/__init__.py +7 -0
  238. solace_agent_mesh/common/utils/asyncio_macos_fix.py +86 -0
  239. solace_agent_mesh/common/utils/embeds/__init__.py +33 -0
  240. solace_agent_mesh/common/utils/embeds/constants.py +55 -0
  241. solace_agent_mesh/common/utils/embeds/converter.py +452 -0
  242. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +124 -0
  243. solace_agent_mesh/common/utils/embeds/evaluators.py +394 -0
  244. solace_agent_mesh/common/utils/embeds/modifiers.py +816 -0
  245. solace_agent_mesh/common/utils/embeds/resolver.py +865 -0
  246. solace_agent_mesh/common/utils/embeds/types.py +14 -0
  247. solace_agent_mesh/common/utils/in_memory_cache.py +108 -0
  248. solace_agent_mesh/common/utils/log_formatters.py +44 -0
  249. solace_agent_mesh/common/utils/mime_helpers.py +106 -0
  250. solace_agent_mesh/common/utils/push_notification_auth.py +134 -0
  251. solace_agent_mesh/common/utils/utils_llm.txt +67 -0
  252. solace_agent_mesh/config_portal/backend/common.py +66 -24
  253. solace_agent_mesh/config_portal/backend/plugin_catalog/constants.py +23 -0
  254. solace_agent_mesh/config_portal/backend/plugin_catalog/models.py +49 -0
  255. solace_agent_mesh/config_portal/backend/plugin_catalog/registry_manager.py +160 -0
  256. solace_agent_mesh/config_portal/backend/plugin_catalog/scraper.py +525 -0
  257. solace_agent_mesh/config_portal/backend/plugin_catalog_server.py +216 -0
  258. solace_agent_mesh/config_portal/backend/server.py +550 -181
  259. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-DNxCwAGB.js +48 -0
  260. solace_agent_mesh/config_portal/frontend/static/client/assets/components-B7lKcHVY.js +140 -0
  261. solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-DX1misIU.js → entry.client-CEumGClk.js} +3 -3
  262. solace_agent_mesh/config_portal/frontend/static/client/assets/index-DSo1AH_7.js +68 -0
  263. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-d2b54a97.js +1 -0
  264. solace_agent_mesh/config_portal/frontend/static/client/assets/{root-BApq5dPK.js → root-C4XmHinv.js} +2 -2
  265. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +1 -0
  266. solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
  267. solace_agent_mesh/core_a2a/__init__.py +1 -0
  268. solace_agent_mesh/core_a2a/core_a2a_llm.txt +88 -0
  269. solace_agent_mesh/core_a2a/service.py +331 -0
  270. solace_agent_mesh/evaluation/config_loader.py +657 -0
  271. solace_agent_mesh/evaluation/evaluator.py +667 -0
  272. solace_agent_mesh/evaluation/message_organizer.py +568 -0
  273. solace_agent_mesh/evaluation/report/benchmark_info.html +35 -0
  274. solace_agent_mesh/evaluation/report/chart_section.html +141 -0
  275. solace_agent_mesh/evaluation/report/detailed_breakdown.html +28 -0
  276. solace_agent_mesh/evaluation/report/modal.html +59 -0
  277. solace_agent_mesh/evaluation/report/modal_chart_functions.js +411 -0
  278. solace_agent_mesh/evaluation/report/modal_script.js +296 -0
  279. solace_agent_mesh/evaluation/report/modal_styles.css +340 -0
  280. solace_agent_mesh/evaluation/report/performance_metrics_styles.css +93 -0
  281. solace_agent_mesh/evaluation/report/templates/footer.html +2 -0
  282. solace_agent_mesh/evaluation/report/templates/header.html +340 -0
  283. solace_agent_mesh/evaluation/report_data_processor.py +972 -0
  284. solace_agent_mesh/evaluation/report_generator.py +613 -0
  285. solace_agent_mesh/evaluation/run.py +613 -0
  286. solace_agent_mesh/evaluation/subscriber.py +872 -0
  287. solace_agent_mesh/evaluation/summary_builder.py +775 -0
  288. solace_agent_mesh/evaluation/test_case_loader.py +714 -0
  289. solace_agent_mesh/gateway/base/__init__.py +1 -0
  290. solace_agent_mesh/gateway/base/app.py +266 -0
  291. solace_agent_mesh/gateway/base/base_llm.txt +119 -0
  292. solace_agent_mesh/gateway/base/component.py +1542 -0
  293. solace_agent_mesh/gateway/base/task_context.py +74 -0
  294. solace_agent_mesh/gateway/gateway_llm.txt +125 -0
  295. solace_agent_mesh/gateway/http_sse/app.py +190 -0
  296. solace_agent_mesh/gateway/http_sse/component.py +1602 -0
  297. solace_agent_mesh/gateway/http_sse/components/__init__.py +7 -0
  298. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +65 -0
  299. solace_agent_mesh/gateway/http_sse/components/visualization_forwarder_component.py +108 -0
  300. solace_agent_mesh/gateway/http_sse/dependencies.py +316 -0
  301. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +63 -0
  302. solace_agent_mesh/gateway/http_sse/main.py +442 -0
  303. solace_agent_mesh/gateway/http_sse/routers/__init__.py +4 -0
  304. solace_agent_mesh/gateway/http_sse/routers/agents.py +41 -0
  305. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +821 -0
  306. solace_agent_mesh/gateway/http_sse/routers/auth.py +212 -0
  307. solace_agent_mesh/gateway/http_sse/routers/config.py +55 -0
  308. solace_agent_mesh/gateway/http_sse/routers/people.py +69 -0
  309. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +37 -0
  310. solace_agent_mesh/gateway/http_sse/routers/sessions.py +80 -0
  311. solace_agent_mesh/gateway/http_sse/routers/sse.py +138 -0
  312. solace_agent_mesh/gateway/http_sse/routers/tasks.py +294 -0
  313. solace_agent_mesh/gateway/http_sse/routers/users.py +59 -0
  314. solace_agent_mesh/gateway/http_sse/routers/visualization.py +1131 -0
  315. solace_agent_mesh/gateway/http_sse/services/__init__.py +4 -0
  316. solace_agent_mesh/gateway/http_sse/services/agent_service.py +69 -0
  317. solace_agent_mesh/gateway/http_sse/services/people_service.py +158 -0
  318. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +179 -0
  319. solace_agent_mesh/gateway/http_sse/services/task_service.py +121 -0
  320. solace_agent_mesh/gateway/http_sse/session_manager.py +187 -0
  321. solace_agent_mesh/gateway/http_sse/sse_manager.py +328 -0
  322. solace_agent_mesh/llm.txt +228 -0
  323. solace_agent_mesh/llm_detail.txt +2835 -0
  324. solace_agent_mesh/templates/agent_template.yaml +53 -0
  325. solace_agent_mesh/templates/eval_backend_template.yaml +54 -0
  326. solace_agent_mesh/templates/gateway_app_template.py +73 -0
  327. solace_agent_mesh/templates/gateway_component_template.py +400 -0
  328. solace_agent_mesh/templates/gateway_config_template.yaml +43 -0
  329. solace_agent_mesh/templates/main_orchestrator.yaml +55 -0
  330. solace_agent_mesh/templates/plugin_agent_config_template.yaml +122 -0
  331. solace_agent_mesh/templates/plugin_custom_config_template.yaml +27 -0
  332. solace_agent_mesh/templates/plugin_custom_template.py +10 -0
  333. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +63 -0
  334. solace_agent_mesh/templates/plugin_pyproject_template.toml +33 -0
  335. solace_agent_mesh/templates/plugin_readme_template.md +34 -0
  336. solace_agent_mesh/templates/plugin_tools_template.py +224 -0
  337. solace_agent_mesh/templates/shared_config.yaml +66 -0
  338. solace_agent_mesh/templates/templates_llm.txt +147 -0
  339. solace_agent_mesh/templates/webui.yaml +53 -0
  340. solace_agent_mesh-1.0.1.dist-info/METADATA +432 -0
  341. solace_agent_mesh-1.0.1.dist-info/RECORD +359 -0
  342. solace_agent_mesh-1.0.1.dist-info/entry_points.txt +3 -0
  343. {solace_agent_mesh-0.2.4.dist-info → solace_agent_mesh-1.0.1.dist-info}/licenses/LICENSE +1 -1
  344. solace_agent_mesh/agents/base_agent_component.py +0 -256
  345. solace_agent_mesh/agents/global/actions/agent_state_change.py +0 -54
  346. solace_agent_mesh/agents/global/actions/clear_history.py +0 -32
  347. solace_agent_mesh/agents/global/actions/convert_file_to_markdown.py +0 -160
  348. solace_agent_mesh/agents/global/actions/create_file.py +0 -70
  349. solace_agent_mesh/agents/global/actions/error_action.py +0 -45
  350. solace_agent_mesh/agents/global/actions/plantuml_diagram.py +0 -163
  351. solace_agent_mesh/agents/global/actions/plotly_graph.py +0 -152
  352. solace_agent_mesh/agents/global/actions/retrieve_file.py +0 -51
  353. solace_agent_mesh/agents/global/global_agent_component.py +0 -38
  354. solace_agent_mesh/agents/image_processing/actions/create_image.py +0 -75
  355. solace_agent_mesh/agents/image_processing/actions/describe_image.py +0 -115
  356. solace_agent_mesh/agents/image_processing/image_processing_agent_component.py +0 -23
  357. solace_agent_mesh/agents/slack/__init__.py +0 -1
  358. solace_agent_mesh/agents/slack/actions/__init__.py +0 -1
  359. solace_agent_mesh/agents/slack/actions/post_message.py +0 -177
  360. solace_agent_mesh/agents/slack/slack_agent_component.py +0 -59
  361. solace_agent_mesh/agents/web_request/actions/do_image_search.py +0 -84
  362. solace_agent_mesh/agents/web_request/actions/do_news_search.py +0 -47
  363. solace_agent_mesh/agents/web_request/actions/do_suggestion_search.py +0 -34
  364. solace_agent_mesh/agents/web_request/actions/do_web_request.py +0 -135
  365. solace_agent_mesh/agents/web_request/actions/download_file.py +0 -69
  366. solace_agent_mesh/agents/web_request/web_request_agent_component.py +0 -33
  367. solace_agent_mesh/assets/web-visualizer/assets/index-D0qORgkg.css +0 -1
  368. solace_agent_mesh/assets/web-visualizer/assets/index-DnDr1pnu.js +0 -109
  369. solace_agent_mesh/assets/web-visualizer/index.html +0 -14
  370. solace_agent_mesh/assets/web-visualizer/vite.svg +0 -1
  371. solace_agent_mesh/cli/commands/add/__init__.py +0 -3
  372. solace_agent_mesh/cli/commands/add/add.py +0 -88
  373. solace_agent_mesh/cli/commands/add/agent.py +0 -110
  374. solace_agent_mesh/cli/commands/add/copy_from_plugin.py +0 -92
  375. solace_agent_mesh/cli/commands/add/gateway.py +0 -374
  376. solace_agent_mesh/cli/commands/build.py +0 -670
  377. solace_agent_mesh/cli/commands/chat/__init__.py +0 -3
  378. solace_agent_mesh/cli/commands/chat/chat.py +0 -361
  379. solace_agent_mesh/cli/commands/config.py +0 -29
  380. solace_agent_mesh/cli/commands/init/__init__.py +0 -3
  381. solace_agent_mesh/cli/commands/init/ai_provider_step.py +0 -93
  382. solace_agent_mesh/cli/commands/init/broker_step.py +0 -99
  383. solace_agent_mesh/cli/commands/init/builtin_agent_step.py +0 -83
  384. solace_agent_mesh/cli/commands/init/check_if_already_done.py +0 -13
  385. solace_agent_mesh/cli/commands/init/create_config_file_step.py +0 -65
  386. solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +0 -147
  387. solace_agent_mesh/cli/commands/init/file_service_step.py +0 -73
  388. solace_agent_mesh/cli/commands/init/init.py +0 -92
  389. solace_agent_mesh/cli/commands/init/project_structure_step.py +0 -16
  390. solace_agent_mesh/cli/commands/init/web_init_step.py +0 -32
  391. solace_agent_mesh/cli/commands/plugin/__init__.py +0 -3
  392. solace_agent_mesh/cli/commands/plugin/add.py +0 -100
  393. solace_agent_mesh/cli/commands/plugin/build.py +0 -268
  394. solace_agent_mesh/cli/commands/plugin/create.py +0 -117
  395. solace_agent_mesh/cli/commands/plugin/plugin.py +0 -124
  396. solace_agent_mesh/cli/commands/plugin/remove.py +0 -73
  397. solace_agent_mesh/cli/commands/run.py +0 -68
  398. solace_agent_mesh/cli/commands/visualizer.py +0 -138
  399. solace_agent_mesh/cli/config.py +0 -85
  400. solace_agent_mesh/common/action.py +0 -91
  401. solace_agent_mesh/common/action_list.py +0 -37
  402. solace_agent_mesh/common/action_response.py +0 -340
  403. solace_agent_mesh/common/mysql_database.py +0 -40
  404. solace_agent_mesh/common/postgres_database.py +0 -85
  405. solace_agent_mesh/common/prompt_templates.py +0 -28
  406. solace_agent_mesh/common/stimulus_utils.py +0 -152
  407. solace_agent_mesh/common/time.py +0 -24
  408. solace_agent_mesh/common/utils.py +0 -712
  409. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-a-zJ6rLx.js +0 -46
  410. solace_agent_mesh/config_portal/frontend/static/client/assets/components-ZIfdTbrV.js +0 -191
  411. solace_agent_mesh/config_portal/frontend/static/client/assets/index-BJHAE5s4.js +0 -17
  412. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-44c41103.js +0 -1
  413. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DX4gQ516.css +0 -1
  414. solace_agent_mesh/configs/agent_global.yaml +0 -74
  415. solace_agent_mesh/configs/agent_image_processing.yaml +0 -82
  416. solace_agent_mesh/configs/agent_slack.yaml +0 -64
  417. solace_agent_mesh/configs/agent_web_request.yaml +0 -75
  418. solace_agent_mesh/configs/conversation_to_file.yaml +0 -56
  419. solace_agent_mesh/configs/error_catcher.yaml +0 -56
  420. solace_agent_mesh/configs/monitor.yaml +0 -0
  421. solace_agent_mesh/configs/monitor_stim_and_errors_to_slack.yaml +0 -109
  422. solace_agent_mesh/configs/monitor_user_feedback.yaml +0 -58
  423. solace_agent_mesh/configs/orchestrator.yaml +0 -241
  424. solace_agent_mesh/configs/service_embedding.yaml +0 -81
  425. solace_agent_mesh/configs/service_llm.yaml +0 -265
  426. solace_agent_mesh/configs/visualize_websocket.yaml +0 -55
  427. solace_agent_mesh/gateway/components/gateway_base.py +0 -47
  428. solace_agent_mesh/gateway/components/gateway_input.py +0 -278
  429. solace_agent_mesh/gateway/components/gateway_output.py +0 -298
  430. solace_agent_mesh/gateway/identity/bamboohr_identity.py +0 -18
  431. solace_agent_mesh/gateway/identity/identity_base.py +0 -10
  432. solace_agent_mesh/gateway/identity/identity_provider.py +0 -60
  433. solace_agent_mesh/gateway/identity/no_identity.py +0 -9
  434. solace_agent_mesh/gateway/identity/passthru_identity.py +0 -9
  435. solace_agent_mesh/monitors/base_monitor_component.py +0 -26
  436. solace_agent_mesh/monitors/feedback/user_feedback_monitor.py +0 -75
  437. solace_agent_mesh/monitors/stim_and_errors/stim_and_error_monitor.py +0 -560
  438. solace_agent_mesh/orchestrator/__init__.py +0 -0
  439. solace_agent_mesh/orchestrator/action_manager.py +0 -237
  440. solace_agent_mesh/orchestrator/components/__init__.py +0 -0
  441. solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +0 -58
  442. solace_agent_mesh/orchestrator/components/orchestrator_action_response_component.py +0 -179
  443. solace_agent_mesh/orchestrator/components/orchestrator_register_component.py +0 -107
  444. solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +0 -527
  445. solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +0 -260
  446. solace_agent_mesh/orchestrator/orchestrator_main.py +0 -172
  447. solace_agent_mesh/orchestrator/orchestrator_prompt.py +0 -539
  448. solace_agent_mesh/services/__init__.py +0 -0
  449. solace_agent_mesh/services/authorization/providers/base_authorization_provider.py +0 -56
  450. solace_agent_mesh/services/bamboo_hr_service/__init__.py +0 -3
  451. solace_agent_mesh/services/bamboo_hr_service/bamboo_hr.py +0 -182
  452. solace_agent_mesh/services/common/__init__.py +0 -4
  453. solace_agent_mesh/services/common/auto_expiry.py +0 -45
  454. solace_agent_mesh/services/common/singleton.py +0 -18
  455. solace_agent_mesh/services/file_service/__init__.py +0 -14
  456. solace_agent_mesh/services/file_service/file_manager/__init__.py +0 -0
  457. solace_agent_mesh/services/file_service/file_manager/bucket_file_manager.py +0 -149
  458. solace_agent_mesh/services/file_service/file_manager/file_manager_base.py +0 -162
  459. solace_agent_mesh/services/file_service/file_manager/memory_file_manager.py +0 -64
  460. solace_agent_mesh/services/file_service/file_manager/volume_file_manager.py +0 -106
  461. solace_agent_mesh/services/file_service/file_service.py +0 -437
  462. solace_agent_mesh/services/file_service/file_service_constants.py +0 -54
  463. solace_agent_mesh/services/file_service/file_transformations.py +0 -141
  464. solace_agent_mesh/services/file_service/file_utils.py +0 -324
  465. solace_agent_mesh/services/file_service/transformers/__init__.py +0 -5
  466. solace_agent_mesh/services/history_service/__init__.py +0 -3
  467. solace_agent_mesh/services/history_service/history_providers/__init__.py +0 -0
  468. solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +0 -54
  469. solace_agent_mesh/services/history_service/history_providers/file_history_provider.py +0 -74
  470. solace_agent_mesh/services/history_service/history_providers/index.py +0 -40
  471. solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +0 -33
  472. solace_agent_mesh/services/history_service/history_providers/mongodb_history_provider.py +0 -66
  473. solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +0 -66
  474. solace_agent_mesh/services/history_service/history_providers/sql_history_provider.py +0 -93
  475. solace_agent_mesh/services/history_service/history_service.py +0 -413
  476. solace_agent_mesh/services/history_service/long_term_memory/__init__.py +0 -0
  477. solace_agent_mesh/services/history_service/long_term_memory/long_term_memory.py +0 -399
  478. solace_agent_mesh/services/llm_service/components/llm_request_component.py +0 -340
  479. solace_agent_mesh/services/llm_service/components/llm_service_component_base.py +0 -152
  480. solace_agent_mesh/services/middleware_service/__init__.py +0 -0
  481. solace_agent_mesh/services/middleware_service/middleware_service.py +0 -20
  482. solace_agent_mesh/templates/action.py +0 -38
  483. solace_agent_mesh/templates/agent.py +0 -29
  484. solace_agent_mesh/templates/agent.yaml +0 -70
  485. solace_agent_mesh/templates/gateway-config-template.yaml +0 -6
  486. solace_agent_mesh/templates/gateway-default-config.yaml +0 -28
  487. solace_agent_mesh/templates/gateway-flows.yaml +0 -78
  488. solace_agent_mesh/templates/gateway-header.yaml +0 -16
  489. solace_agent_mesh/templates/gateway_base.py +0 -15
  490. solace_agent_mesh/templates/gateway_input.py +0 -98
  491. solace_agent_mesh/templates/gateway_output.py +0 -71
  492. solace_agent_mesh/templates/plugin-gateway-default-config.yaml +0 -29
  493. solace_agent_mesh/templates/plugin-pyproject.toml +0 -30
  494. solace_agent_mesh/templates/rest-api-default-config.yaml +0 -31
  495. solace_agent_mesh/templates/rest-api-flows.yaml +0 -81
  496. solace_agent_mesh/templates/slack-default-config.yaml +0 -16
  497. solace_agent_mesh/templates/slack-flows.yaml +0 -81
  498. solace_agent_mesh/templates/solace-agent-mesh-default.yaml +0 -86
  499. solace_agent_mesh/templates/solace-agent-mesh-plugin-default.yaml +0 -8
  500. solace_agent_mesh/templates/web-default-config.yaml +0 -10
  501. solace_agent_mesh/templates/web-flows.yaml +0 -76
  502. solace_agent_mesh/tools/__init__.py +0 -0
  503. solace_agent_mesh/tools/components/__init__.py +0 -0
  504. solace_agent_mesh/tools/components/conversation_formatter.py +0 -111
  505. solace_agent_mesh/tools/components/file_resolver_component.py +0 -58
  506. solace_agent_mesh/tools/config/runtime_config.py +0 -26
  507. solace_agent_mesh-0.2.4.dist-info/METADATA +0 -176
  508. solace_agent_mesh-0.2.4.dist-info/RECORD +0 -193
  509. solace_agent_mesh-0.2.4.dist-info/entry_points.txt +0 -3
  510. /solace_agent_mesh/{agents → agent}/__init__.py +0 -0
  511. /solace_agent_mesh/{agents/global → agent/adk}/__init__.py +0 -0
  512. /solace_agent_mesh/{agents/global/actions → agent/protocol}/__init__.py +0 -0
  513. /solace_agent_mesh/{agents/image_processing → agent/sac}/__init__.py +0 -0
  514. /solace_agent_mesh/{agents/image_processing/actions → agent/utils}/__init__.py +0 -0
  515. /solace_agent_mesh/{agents/web_request → config_portal/backend/plugin_catalog}/__init__.py +0 -0
  516. /solace_agent_mesh/{agents/web_request/actions → evaluation}/__init__.py +0 -0
  517. /solace_agent_mesh/gateway/{components → http_sse}/__init__.py +0 -0
  518. {solace_agent_mesh-0.2.4.dist-info → solace_agent_mesh-1.0.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1622 @@
1
+ """
2
+ Collection of Python tools for audio processing and text-to-speech generation.
3
+ """
4
+
5
+ import asyncio
6
+ import inspect
7
+ import io
8
+ import json
9
+ import os
10
+ import random
11
+ import tempfile
12
+ import uuid
13
+ import wave
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ import httpx
18
+
19
+ from google import genai
20
+ from google.genai import types as adk_types
21
+ from google.adk.tools import ToolContext
22
+ from pydub import AudioSegment
23
+ from solace_ai_connector.common.log import log
24
+
25
+ from ...agent.utils.artifact_helpers import (
26
+ load_artifact_content_or_metadata,
27
+ save_artifact_with_metadata,
28
+ DEFAULT_SCHEMA_MAX_KEYS,
29
+ )
30
+ from ...agent.utils.context_helpers import get_original_session_id
31
+
32
+ from .tool_definition import BuiltinTool
33
+ from .registry import tool_registry
34
+
35
+
36
+ VOICE_TONE_MAPPING = {
37
+ "bright": ["Zephyr", "Autonoe"],
38
+ "upbeat": ["Puck", "Laomedeia"],
39
+ "informative": ["Charon", "Rasalgethi"],
40
+ "firm": ["Kore", "Orus", "Alnilam"],
41
+ "excitable": ["Fenrir"],
42
+ "youthful": ["Leda"],
43
+ "breezy": ["Aoede"],
44
+ "easy-going": ["Callirhoe", "Umbriel"],
45
+ "breathy": ["Enceladus"],
46
+ "clear": ["Iapetus", "Erinome"],
47
+ "smooth": ["Algieba", "Despina"],
48
+ "gravelly": ["Algenib"],
49
+ "soft": ["Achernar"],
50
+ "even": ["Schedar"],
51
+ "mature": ["Gacrux"],
52
+ "forward": ["Pulcherrima"],
53
+ "friendly": ["Achird"],
54
+ "casual": ["Zubenelgenubi"],
55
+ "gentle": ["Vindemiatrix"],
56
+ "lively": ["Sadachbia"],
57
+ "knowledgeable": ["Sadaltager"],
58
+ "warm": ["Sulafat"],
59
+ }
60
+
61
+ GENDER_TO_VOICE_MAPPING = {
62
+ "male": [
63
+ "Puck",
64
+ "Charon",
65
+ "Orus",
66
+ "Enceladus",
67
+ "Iapetus",
68
+ "Algieba",
69
+ "Algenib",
70
+ "Alnilam",
71
+ "Schedar",
72
+ "Achird",
73
+ "Zubenelgenubi",
74
+ "Sadachbia",
75
+ "Sadaltager",
76
+ ],
77
+ "female": [
78
+ "Zephyr",
79
+ "Kore",
80
+ "Leda",
81
+ "Aoede",
82
+ "Callirhoe",
83
+ "Autonoe",
84
+ "Despina",
85
+ "Erinome",
86
+ "Laomedeia",
87
+ "Achernar",
88
+ "Gacrux",
89
+ "Pulcherrima",
90
+ "Vindemiatrix",
91
+ "Sulafat",
92
+ ],
93
+ "neutral": [
94
+ "Fenrir",
95
+ "Umbriel",
96
+ "Rasalgethi",
97
+ ],
98
+ }
99
+
100
+ _all_voices_set = set(
101
+ voice for voices_in_tone in VOICE_TONE_MAPPING.values() for voice in voices_in_tone
102
+ )
103
+ _all_voices_set.update(
104
+ voice
105
+ for voices_in_gender in GENDER_TO_VOICE_MAPPING.values()
106
+ for voice in voices_in_gender
107
+ )
108
+ ALL_AVAILABLE_VOICES = list(_all_voices_set)
109
+
110
+ if not ALL_AVAILABLE_VOICES:
111
+ ALL_AVAILABLE_VOICES = [
112
+ "Kore",
113
+ "Puck",
114
+ "Zephyr",
115
+ "Charon",
116
+ "Rasalgethi",
117
+ "Alnilam",
118
+ "Fenrir",
119
+ "Leda",
120
+ "Aoede",
121
+ "Callirhoe",
122
+ "Umbriel",
123
+ "Enceladus",
124
+ "Iapetus",
125
+ "Erinome",
126
+ "Algieba",
127
+ "Despina",
128
+ "Algenib",
129
+ "Achernar",
130
+ "Schedar",
131
+ "Gacrux",
132
+ "Pulcherrima",
133
+ "Achird",
134
+ "Zubenelgenubi",
135
+ "Vindemiatrix",
136
+ "Sadachbia",
137
+ "Sadaltager",
138
+ "Sulafat",
139
+ "Autonoe",
140
+ "Laomedeia",
141
+ "Orus",
142
+ ]
143
+
144
+
145
+ SUPPORTED_LANGUAGES = {
146
+ "arabic": "ar-EG",
147
+ "arabic_egyptian": "ar-EG",
148
+ "german": "de-DE",
149
+ "english": "en-US",
150
+ "english_us": "en-US",
151
+ "english_india": "en-IN",
152
+ "spanish": "es-US",
153
+ "spanish_us": "es-US",
154
+ "french": "fr-FR",
155
+ "hindi": "hi-IN",
156
+ "indonesian": "id-ID",
157
+ "italian": "it-IT",
158
+ "japanese": "ja-JP",
159
+ "korean": "ko-KR",
160
+ "portuguese": "pt-BR",
161
+ "portuguese_brazil": "pt-BR",
162
+ "russian": "ru-RU",
163
+ "dutch": "nl-NL",
164
+ "polish": "pl-PL",
165
+ "thai": "th-TH",
166
+ "turkish": "tr-TR",
167
+ "vietnamese": "vi-VN",
168
+ "romanian": "ro-RO",
169
+ "ukrainian": "uk-UA",
170
+ "bengali": "bn-BD",
171
+ "marathi": "mr-IN",
172
+ "tamil": "ta-IN",
173
+ "telugu": "te-IN",
174
+ }
175
+
176
+ from typing import Set
177
+
178
+ DEFAULT_VOICE = "Kore"
179
+
180
+
181
+ def _get_effective_tone_voices(
182
+ tone: Optional[str], voice_tone_mapping: Optional[Dict[str, List[str]]] = None
183
+ ) -> Optional[List[str]]:
184
+ """Helper to get voices for a tone, considering aliases."""
185
+ if not tone:
186
+ return None
187
+ mapping = voice_tone_mapping or VOICE_TONE_MAPPING
188
+ tone_lower = tone.lower().strip()
189
+ tone_aliases = {
190
+ "professional": "firm",
191
+ "business": "firm",
192
+ "corporate": "firm",
193
+ "cheerful": "upbeat",
194
+ "happy": "upbeat",
195
+ "energetic": "excitable",
196
+ "calm": "soft",
197
+ "relaxed": "easy-going",
198
+ "serious": "informative",
199
+ "educational": "knowledgeable",
200
+ "teaching": "knowledgeable",
201
+ "conversational": "casual",
202
+ "natural": "friendly",
203
+ "welcoming": "warm",
204
+ }
205
+ effective_tone = tone_aliases.get(tone_lower, tone_lower)
206
+ return mapping.get(effective_tone)
207
+
208
+
209
+ def _get_gender_voices(
210
+ gender: Optional[str], gender_voice_mapping: Optional[Dict[str, List[str]]] = None
211
+ ) -> Optional[List[str]]:
212
+ """Helper to get voices for a gender."""
213
+ if not gender:
214
+ return None
215
+ mapping = gender_voice_mapping or GENDER_TO_VOICE_MAPPING
216
+ return mapping.get(gender.lower().strip())
217
+
218
+
219
+ def _get_voice_for_speaker(
220
+ gender: Optional[str],
221
+ tone: Optional[str],
222
+ used_voices_in_current_call: Set[str],
223
+ voice_tone_mapping: Optional[Dict[str, List[str]]] = None,
224
+ gender_voice_mapping: Optional[Dict[str, List[str]]] = None,
225
+ ) -> str:
226
+ """
227
+ Selects a voice based on desired gender and/or tone, prioritizing uniqueness.
228
+
229
+ Selection Priority:
230
+ 1. Unique voice matching both specified gender and tone.
231
+ 2. Unique voice matching specified gender (if tone match failed or tone not specified).
232
+ 3. Unique voice matching specified tone (if gender match failed or gender not specified).
233
+ 4. Any unique voice from the global pool.
234
+ 5. If all unique options exhausted (reuse necessary):
235
+ a. Voice matching both specified gender and tone.
236
+ b. Voice matching specified gender.
237
+ c. Voice matching specified tone.
238
+ d. Any voice from the global pool.
239
+ 6. Fallback to DEFAULT_VOICE ("Kore").
240
+
241
+ Args:
242
+ gender: The desired gender ("male", "female", "neutral").
243
+ tone: The desired tone (e.g., "friendly", "professional").
244
+ used_voices_in_current_call: A set of voice names already used.
245
+ voice_tone_mapping: Optional custom tone-to-voice mapping.
246
+ gender_voice_mapping: Optional custom gender-to-voice mapping.
247
+
248
+ Returns:
249
+ A voice name string.
250
+ """
251
+ log_identifier_select = "[AudioTools:_get_voice_for_speaker]"
252
+ log.debug(
253
+ "%s Selecting voice for gender='%s', tone='%s', used_voices=%s",
254
+ log_identifier_select,
255
+ gender,
256
+ tone,
257
+ used_voices_in_current_call,
258
+ )
259
+
260
+ candidate_pool = list(ALL_AVAILABLE_VOICES)
261
+
262
+ gender_specific_voices = None
263
+ if gender:
264
+ gender_specific_voices = _get_gender_voices(gender, gender_voice_mapping)
265
+ if gender_specific_voices:
266
+ candidate_pool = [v for v in candidate_pool if v in gender_specific_voices]
267
+ log.debug(
268
+ "%s Filtered by gender '%s'. Candidates: %s",
269
+ log_identifier_select,
270
+ gender,
271
+ candidate_pool,
272
+ )
273
+ else:
274
+ log.warning(
275
+ "%s Gender '%s' not found in mapping or has no voices. Gender filter ignored for now.",
276
+ log_identifier_select,
277
+ gender,
278
+ )
279
+ gender_specific_voices = None
280
+
281
+ tone_specific_voices = None
282
+ if tone:
283
+ voices_for_tone = _get_effective_tone_voices(tone, voice_tone_mapping)
284
+ if voices_for_tone:
285
+ tone_specific_voices = voices_for_tone
286
+ candidate_pool = [v for v in candidate_pool if v in voices_for_tone]
287
+ log.debug(
288
+ "%s Filtered by tone '%s'. Candidates: %s",
289
+ log_identifier_select,
290
+ tone,
291
+ candidate_pool,
292
+ )
293
+ else:
294
+ log.warning(
295
+ "%s Tone '%s' not found in mapping or has no voices. Tone filter ignored.",
296
+ log_identifier_select,
297
+ tone,
298
+ )
299
+ tone_specific_voices = None
300
+
301
+ available_unique_voices = [
302
+ v for v in candidate_pool if v not in used_voices_in_current_call
303
+ ]
304
+ if available_unique_voices:
305
+ selected = random.choice(available_unique_voices)
306
+ log.info(
307
+ "%s Selected unique voice '%s' from gender/tone filtered pool.",
308
+ log_identifier_select,
309
+ selected,
310
+ )
311
+ return selected
312
+
313
+ log.debug(
314
+ "%s No unique voice in primary filtered pool. Trying fallbacks for uniqueness.",
315
+ log_identifier_select,
316
+ )
317
+ if gender_specific_voices:
318
+ available_gender_unique = [
319
+ v for v in gender_specific_voices if v not in used_voices_in_current_call
320
+ ]
321
+ if available_gender_unique:
322
+ selected = random.choice(available_gender_unique)
323
+ log.info(
324
+ "%s Selected unique voice '%s' from gender-only pool (tone constraint relaxed).",
325
+ log_identifier_select,
326
+ selected,
327
+ )
328
+ return selected
329
+ if tone_specific_voices:
330
+ available_tone_unique = [
331
+ v for v in tone_specific_voices if v not in used_voices_in_current_call
332
+ ]
333
+ if available_tone_unique:
334
+ selected = random.choice(available_tone_unique)
335
+ log.info(
336
+ "%s Selected unique voice '%s' from tone-only pool (gender constraint relaxed or not specified).",
337
+ log_identifier_select,
338
+ selected,
339
+ )
340
+ return selected
341
+ globally_available_unique = [
342
+ v for v in ALL_AVAILABLE_VOICES if v not in used_voices_in_current_call
343
+ ]
344
+ if globally_available_unique:
345
+ selected = random.choice(globally_available_unique)
346
+ log.info(
347
+ "%s Selected unique voice '%s' from global pool (all constraints relaxed).",
348
+ log_identifier_select,
349
+ selected,
350
+ )
351
+ return selected
352
+
353
+ log.warning("%s All voices are used. Reusing a voice.", log_identifier_select)
354
+ if candidate_pool:
355
+ selected = random.choice(candidate_pool)
356
+ log.info(
357
+ "%s Reusing voice '%s' from gender/tone filtered pool.",
358
+ log_identifier_select,
359
+ selected,
360
+ )
361
+ return selected
362
+ if gender_specific_voices:
363
+ selected = random.choice(gender_specific_voices)
364
+ log.info(
365
+ "%s Reusing voice '%s' from gender-only pool.",
366
+ log_identifier_select,
367
+ selected,
368
+ )
369
+ return selected
370
+ if tone_specific_voices:
371
+ selected = random.choice(tone_specific_voices)
372
+ log.info(
373
+ "%s Reusing voice '%s' from tone-only pool.",
374
+ log_identifier_select,
375
+ selected,
376
+ )
377
+ return selected
378
+ if ALL_AVAILABLE_VOICES:
379
+ selected = random.choice(ALL_AVAILABLE_VOICES)
380
+ log.info(
381
+ "%s Reusing voice '%s' from global pool.", log_identifier_select, selected
382
+ )
383
+ return selected
384
+
385
+ log.error(
386
+ "%s No voices available in any mapping or pool. Using default '%s'.",
387
+ log_identifier_select,
388
+ DEFAULT_VOICE,
389
+ )
390
+ return DEFAULT_VOICE
391
+
392
+
393
+ def _get_language_code(language: str) -> str:
394
+ """
395
+ Get BCP-47 language code from language name or code.
396
+
397
+ Args:
398
+ language: Language name or BCP-47 code
399
+
400
+ Returns:
401
+ BCP-47 language code, defaults to "en-US"
402
+ """
403
+ if not language:
404
+ return "en-US"
405
+
406
+ language_lower = language.lower().strip()
407
+
408
+ if "-" in language_lower and len(language_lower) >= 5:
409
+ return language
410
+
411
+ if language_lower in SUPPORTED_LANGUAGES:
412
+ return SUPPORTED_LANGUAGES[language_lower]
413
+
414
+ log.warning(f"[AudioTools] Unknown language '{language}', using default 'en-US'")
415
+ return "en-US"
416
+
417
+
418
+ def _create_voice_config(voice_name: str) -> adk_types.VoiceConfig:
419
+ """Create a VoiceConfig for single-voice TTS."""
420
+ return adk_types.VoiceConfig(
421
+ prebuilt_voice_config=adk_types.PrebuiltVoiceConfig(voice_name=voice_name)
422
+ )
423
+
424
+
425
+ def _create_multi_speaker_config(
426
+ speaker_configs: List[Dict[str, str]],
427
+ ) -> adk_types.MultiSpeakerVoiceConfig:
428
+ """Create a MultiSpeakerVoiceConfig for multi-speaker TTS."""
429
+ speaker_voice_configs = []
430
+
431
+ for config in speaker_configs:
432
+ speaker_name = config.get("name", "Speaker")
433
+ voice_name = config.get("voice", "Kore")
434
+
435
+ speaker_voice_config = adk_types.SpeakerVoiceConfig(
436
+ speaker=speaker_name, voice_config=_create_voice_config(voice_name)
437
+ )
438
+ speaker_voice_configs.append(speaker_voice_config)
439
+
440
+ return adk_types.MultiSpeakerVoiceConfig(
441
+ speaker_voice_configs=speaker_voice_configs
442
+ )
443
+
444
+
445
+ async def _generate_audio_with_gemini(
446
+ client: genai.Client,
447
+ prompt: str,
448
+ speech_config: adk_types.SpeechConfig,
449
+ model: str = "gemini-2.5-flash-preview-tts",
450
+ language: str = "en-US",
451
+ ) -> bytes:
452
+ """
453
+ Shared function for generating audio using Gemini API.
454
+
455
+ Args:
456
+ client: Gemini client instance
457
+ prompt: Text prompt for TTS
458
+ speech_config: Speech configuration (single or multi-speaker)
459
+ model: Gemini model to use
460
+ language: BCP-47 language code
461
+
462
+ Returns:
463
+ Raw audio data as bytes
464
+ """
465
+ config = adk_types.GenerateContentConfig(
466
+ response_modalities=["AUDIO"],
467
+ speech_config=speech_config,
468
+ )
469
+
470
+ if hasattr(config, "language"):
471
+ config.language = language
472
+
473
+ response = await asyncio.to_thread(
474
+ client.models.generate_content, model=model, contents=prompt, config=config
475
+ )
476
+
477
+ if (
478
+ not response
479
+ or not response.candidates
480
+ or not response.candidates[0].content.parts
481
+ ):
482
+ raise ValueError("Gemini API did not return valid audio data")
483
+
484
+ audio_data = response.candidates[0].content.parts[0].inline_data.data
485
+ if not audio_data:
486
+ raise ValueError("No audio data received from Gemini API")
487
+
488
+ return audio_data
489
+
490
+
491
+ def _create_wav_file(
492
+ filename: str,
493
+ pcm_data: bytes,
494
+ channels: int = 1,
495
+ rate: int = 24000,
496
+ sample_width: int = 2,
497
+ ):
498
+ """Create a proper WAV file from PCM data (based on working example)."""
499
+ with wave.open(filename, "wb") as wf:
500
+ wf.setnchannels(channels)
501
+ wf.setsampwidth(sample_width)
502
+ wf.setframerate(rate)
503
+ wf.writeframes(pcm_data)
504
+
505
+
506
+ async def _convert_pcm_to_mp3(pcm_data: bytes) -> bytes:
507
+ """
508
+ Shared function for converting raw PCM data to MP3 format.
509
+
510
+ Args:
511
+ pcm_data: Raw PCM audio data from Gemini API
512
+
513
+ Returns:
514
+ MP3 audio data as bytes
515
+ """
516
+ wav_temp_path = None
517
+ mp3_temp_path = None
518
+
519
+ try:
520
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wav_temp:
521
+ wav_temp_path = wav_temp.name
522
+
523
+ await asyncio.to_thread(_create_wav_file, wav_temp_path, pcm_data)
524
+
525
+ with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as mp3_temp:
526
+ mp3_temp_path = mp3_temp.name
527
+
528
+ audio = await asyncio.to_thread(AudioSegment.from_wav, wav_temp_path)
529
+ await asyncio.to_thread(audio.export, mp3_temp_path, format="mp3")
530
+
531
+ with open(mp3_temp_path, "rb") as mp3_file:
532
+ mp3_data = mp3_file.read()
533
+
534
+ return mp3_data
535
+
536
+ finally:
537
+ if wav_temp_path:
538
+ try:
539
+ os.remove(wav_temp_path)
540
+ except OSError:
541
+ pass
542
+ if mp3_temp_path:
543
+ try:
544
+ os.remove(mp3_temp_path)
545
+ except OSError:
546
+ pass
547
+
548
+
549
+ async def _save_audio_artifact(
550
+ audio_data: bytes,
551
+ filename: str,
552
+ metadata: Dict[str, Any],
553
+ tool_context: ToolContext,
554
+ ) -> Dict[str, Any]:
555
+ """
556
+ Shared function for saving audio artifacts with metadata.
557
+
558
+ Args:
559
+ audio_data: Audio data to save
560
+ filename: Filename for the artifact
561
+ metadata: Metadata dictionary
562
+ tool_context: ADK tool context
563
+
564
+ Returns:
565
+ Save result dictionary
566
+ """
567
+ inv_context = tool_context._invocation_context
568
+ app_name = inv_context.app_name
569
+ user_id = inv_context.user_id
570
+ session_id = get_original_session_id(inv_context)
571
+ artifact_service = inv_context.artifact_service
572
+
573
+ if not artifact_service:
574
+ raise ValueError("ArtifactService is not available in the context")
575
+
576
+ save_result = await save_artifact_with_metadata(
577
+ artifact_service=artifact_service,
578
+ app_name=app_name,
579
+ user_id=user_id,
580
+ session_id=session_id,
581
+ filename=filename,
582
+ content_bytes=audio_data,
583
+ mime_type="audio/mpeg",
584
+ metadata_dict=metadata,
585
+ timestamp=datetime.now(timezone.utc),
586
+ schema_max_keys=DEFAULT_SCHEMA_MAX_KEYS,
587
+ tool_context=tool_context,
588
+ )
589
+
590
+ if save_result.get("status") == "error":
591
+ raise IOError(
592
+ f"Failed to save audio artifact: {save_result.get('message', 'Unknown error')}"
593
+ )
594
+
595
+ return save_result
596
+
597
+
598
+ async def select_voice(
599
+ gender: Optional[str] = None,
600
+ tone: Optional[str] = None,
601
+ exclude_voices: Optional[List[str]] = None,
602
+ tool_context: ToolContext = None,
603
+ tool_config: Optional[Dict[str, Any]] = None,
604
+ ) -> Dict[str, Any]:
605
+ """
606
+ Selects a suitable voice name based on criteria like gender and tone.
607
+ Use this to get a consistent voice name that can be passed to the `text_to_speech` tool for multiple calls.
608
+
609
+ Args:
610
+ gender: Optional desired gender for the voice ("male", "female", "neutral").
611
+ tone: Optional tone preference (e.g., "friendly", "professional", "warm").
612
+ exclude_voices: Optional list of voice names to exclude from the selection.
613
+ tool_context: ADK tool context.
614
+ tool_config: Configuration including voice mappings.
615
+
616
+ Returns:
617
+ Dictionary with status and the selected voice name.
618
+ """
619
+ log_identifier = "[AudioTools:select_voice]"
620
+ log.info(
621
+ f"{log_identifier} Selecting voice for gender='{gender}', tone='{tone}', excluding='{exclude_voices}'"
622
+ )
623
+
624
+ try:
625
+ config = tool_config or {}
626
+ voice_tone_mapping = config.get("voice_tone_mapping", VOICE_TONE_MAPPING)
627
+ gender_voice_mapping = config.get(
628
+ "gender_voice_mapping", GENDER_TO_VOICE_MAPPING
629
+ )
630
+
631
+ used_voices = set(exclude_voices) if exclude_voices else set()
632
+
633
+ selected_voice = _get_voice_for_speaker(
634
+ gender=gender,
635
+ tone=tone,
636
+ used_voices_in_current_call=used_voices,
637
+ voice_tone_mapping=voice_tone_mapping,
638
+ gender_voice_mapping=gender_voice_mapping,
639
+ )
640
+
641
+ log.info(f"{log_identifier} Selected voice: {selected_voice}")
642
+
643
+ return {
644
+ "status": "success",
645
+ "message": f"Successfully selected voice '{selected_voice}'.",
646
+ "voice_name": selected_voice,
647
+ }
648
+
649
+ except Exception as e:
650
+ log.exception(f"{log_identifier} Unexpected error in select_voice: {e}")
651
+ return {"status": "error", "message": f"An unexpected error occurred: {e}"}
652
+
653
+
654
+ async def text_to_speech(
655
+ text: str,
656
+ output_filename: Optional[str] = None,
657
+ voice_name: Optional[str] = None,
658
+ gender: Optional[str] = None,
659
+ tone: Optional[str] = None,
660
+ language: Optional[str] = None,
661
+ tool_context: ToolContext = None,
662
+ tool_config: Optional[Dict[str, Any]] = None,
663
+ ) -> Dict[str, Any]:
664
+ """
665
+ Converts text to speech using Gemini TTS API and saves as MP3 artifact.
666
+
667
+ Args:
668
+ text: The text to convert to speech.
669
+ output_filename: Optional filename for the output MP3.
670
+ voice_name: Optional specific voice name (e.g., "Kore", "Puck"). Overrides gender and tone.
671
+ gender: Optional desired gender for the voice ("male", "female", "neutral").
672
+ Used if 'voice_name' is not provided.
673
+ tone: Optional tone preference (e.g., "friendly", "professional", "warm").
674
+ Used if 'voice_name' is not provided, considered after 'gender'.
675
+ language: Optional language code (e.g., "en-US", "fr-FR", "ja-JP").
676
+ tool_context: ADK tool context.
677
+ tool_config: Configuration including API key, model settings, and voice mappings.
678
+
679
+ Returns:
680
+ Dictionary with status, output filename, version, and preview.
681
+ """
682
+ log_identifier = "[AudioTools:text_to_speech]"
683
+
684
+ if not tool_context:
685
+ return {"status": "error", "message": "ToolContext is missing"}
686
+
687
+ if not text or not text.strip():
688
+ return {"status": "error", "message": "Text input is required"}
689
+
690
+ try:
691
+ log.info(f"{log_identifier} Processing TTS request for text: '{text[:50]}...'")
692
+
693
+ config = tool_config or {}
694
+ api_key = config.get("gemini_api_key")
695
+ model = config.get("model", "gemini-2.5-flash-preview-tts")
696
+ default_voice = config.get("voice_name", DEFAULT_VOICE)
697
+ default_language = config.get("language", "en-US")
698
+ voice_tone_mapping = config.get("voice_tone_mapping", VOICE_TONE_MAPPING)
699
+ gender_voice_mapping = config.get(
700
+ "gender_voice_mapping", GENDER_TO_VOICE_MAPPING
701
+ )
702
+
703
+ if not api_key:
704
+ return {
705
+ "status": "error",
706
+ "message": "GEMINI_API_KEY is required in tool configuration",
707
+ }
708
+
709
+ final_voice = voice_name
710
+ if not final_voice:
711
+ final_voice = _get_voice_for_speaker(
712
+ gender=gender,
713
+ tone=tone,
714
+ used_voices_in_current_call=set(),
715
+ voice_tone_mapping=voice_tone_mapping,
716
+ gender_voice_mapping=gender_voice_mapping,
717
+ )
718
+ log.info(
719
+ f"{log_identifier} Selected voice '{final_voice}' for gender='{gender}', tone='{tone}'"
720
+ )
721
+ else:
722
+ log.info(
723
+ f"{log_identifier} Using specified voice_name '{final_voice}'. Gender/tone selection skipped."
724
+ )
725
+
726
+ if not final_voice:
727
+ final_voice = default_voice
728
+ log.warning(
729
+ f"{log_identifier} Voice selection resulted in None, using default '{default_voice}'."
730
+ )
731
+
732
+ final_language = _get_language_code(language or default_language)
733
+
734
+ client = genai.Client(api_key=api_key)
735
+
736
+ voice_config = _create_voice_config(final_voice)
737
+ speech_config = adk_types.SpeechConfig(voice_config=voice_config)
738
+
739
+ log.info(
740
+ f"{log_identifier} Generating audio with voice '{final_voice}' and language '{final_language}'"
741
+ )
742
+ wav_data = await _generate_audio_with_gemini(
743
+ client=client,
744
+ prompt=f"Say in a clear voice: {text}",
745
+ speech_config=speech_config,
746
+ model=model,
747
+ language=final_language,
748
+ )
749
+
750
+ log.info(f"{log_identifier} Converting audio to MP3 format")
751
+ mp3_data = await _convert_pcm_to_mp3(wav_data)
752
+
753
+ final_filename = output_filename
754
+ if not final_filename:
755
+ final_filename = f"tts_audio_{uuid.uuid4()}.mp3"
756
+ elif not final_filename.lower().endswith(".mp3"):
757
+ final_filename = f"{final_filename}.mp3"
758
+
759
+ metadata = {
760
+ "description": f"Text-to-speech audio generated from text input. Used voice: {final_voice}\nSource text: {text[:1500]}",
761
+ "source_text": text[:500],
762
+ "voice_name": final_voice,
763
+ "language": final_language,
764
+ "model": model,
765
+ "generation_tool": "text_to_speech",
766
+ "generation_timestamp": datetime.now(timezone.utc).isoformat(),
767
+ }
768
+
769
+ if voice_name:
770
+ metadata["requested_voice_name"] = voice_name
771
+ if gender:
772
+ metadata["requested_gender"] = gender
773
+ if tone:
774
+ metadata["requested_tone"] = tone
775
+ if language:
776
+ metadata["requested_language"] = language
777
+
778
+ log.info(f"{log_identifier} Saving audio artifact '{final_filename}'")
779
+ save_result = await _save_audio_artifact(
780
+ audio_data=mp3_data,
781
+ filename=final_filename,
782
+ metadata=metadata,
783
+ tool_context=tool_context,
784
+ )
785
+
786
+ log.info(
787
+ f"{log_identifier} Audio artifact saved successfully: {final_filename} v{save_result['data_version']}"
788
+ )
789
+
790
+ return {
791
+ "status": "success",
792
+ "message": f"Text-to-speech audio generated and saved successfully",
793
+ "output_filename": final_filename,
794
+ "output_version": save_result["data_version"],
795
+ "voice_used": final_voice,
796
+ "language_used": final_language,
797
+ "result_preview": f"Audio '{final_filename}' (v{save_result['data_version']}) created from text: \"{text[:100]}...\"",
798
+ }
799
+
800
+ except ValueError as ve:
801
+ log.error(f"{log_identifier} Value error: {ve}")
802
+ return {"status": "error", "message": str(ve)}
803
+ except IOError as ioe:
804
+ log.error(f"{log_identifier} IO error: {ioe}")
805
+ return {"status": "error", "message": str(ioe)}
806
+ except Exception as e:
807
+ log.exception(f"{log_identifier} Unexpected error in text_to_speech: {e}")
808
+ return {"status": "error", "message": f"An unexpected error occurred: {e}"}
809
+
810
+
811
+ async def multi_speaker_text_to_speech(
812
+ conversation_text: str,
813
+ output_filename: Optional[str] = None,
814
+ speaker_configs: Optional[List[Dict[str, str]]] = None,
815
+ language: Optional[str] = None,
816
+ tool_context: ToolContext = None,
817
+ tool_config: Optional[Dict[str, Any]] = None,
818
+ ) -> Dict[str, Any]:
819
+ """
820
+ Converts conversation text with speaker labels to speech using multiple voices.
821
+
822
+ Args:
823
+ conversation_text: Text with speaker labels (e.g., "Speaker1: Hello\\nSpeaker2: Hi there").
824
+ output_filename: Optional filename for the output MP3.
825
+ speaker_configs: Optional list of speaker configurations. Each item is a dictionary:
826
+ `{"name": "Speaker1", "voice": "Kore", "gender": "female", "tone": "firm"}`.
827
+ - `name` (str): The speaker's name as it appears in `conversation_text`.
828
+ - `voice` (Optional[str]): Specific voice name to use. Overrides gender/tone.
829
+ - `gender` (Optional[str]): Desired gender ("male", "female", "neutral").
830
+ - `tone` (Optional[str]): Desired tone (e.g., "friendly").
831
+ If only `gender` and/or `tone` are provided, a voice is selected.
832
+ If no config for a speaker in text, or if speaker_configs is empty,
833
+ default speakers from tool_config are used, or a default voice is assigned.
834
+ language: Optional language code (e.g., "en-US", "fr-FR", "ja-JP").
835
+ tool_context: ADK tool context.
836
+ tool_config: Configuration including API key, model, default speakers, and voice mappings.
837
+
838
+ Returns:
839
+ Dictionary with status, output filename, version, and preview.
840
+ """
841
+ log_identifier = "[AudioTools:multi_speaker_text_to_speech]"
842
+
843
+ if not tool_context:
844
+ return {"status": "error", "message": "ToolContext is missing"}
845
+
846
+ if not conversation_text or not conversation_text.strip():
847
+ return {"status": "error", "message": "Conversation text input is required"}
848
+
849
+ try:
850
+ log.info(
851
+ f"{log_identifier} Processing multi-speaker TTS request for text: '{conversation_text[:50]}...'"
852
+ )
853
+
854
+ config = tool_config or {}
855
+ api_key = config.get("gemini_api_key")
856
+ model = config.get("model", "gemini-2.5-flash-preview-tts")
857
+ default_language = config.get("language", "en-US")
858
+ default_speakers = config.get(
859
+ "default_speakers",
860
+ [
861
+ {"name": "Speaker1", "voice": "Kore"},
862
+ {
863
+ "name": "Speaker2",
864
+ "voice": "Puck",
865
+ "gender": "male",
866
+ "tone": "upbeat",
867
+ },
868
+ {
869
+ "name": "Speaker3",
870
+ "voice": "Zephyr",
871
+ "gender": "female",
872
+ "tone": "bright",
873
+ },
874
+ ],
875
+ )
876
+ voice_tone_mapping = config.get("voice_tone_mapping", VOICE_TONE_MAPPING)
877
+ gender_voice_mapping = config.get(
878
+ "gender_voice_mapping", GENDER_TO_VOICE_MAPPING
879
+ )
880
+
881
+ if not api_key:
882
+ return {
883
+ "status": "error",
884
+ "message": "GEMINI_API_KEY is required in tool configuration",
885
+ }
886
+
887
+ final_speaker_configs = []
888
+ used_voices_in_current_call: Set[str] = set()
889
+
890
+ configs_to_process = speaker_configs if speaker_configs else default_speakers
891
+ if not configs_to_process and conversation_text:
892
+ log.warning(
893
+ "%s No speaker_configs and no default_speakers. Creating a default speaker.",
894
+ log_identifier,
895
+ )
896
+ configs_to_process = [
897
+ {"name": "Speaker1", "gender": "neutral", "tone": "neutral"}
898
+ ]
899
+
900
+ for i, config_item in enumerate(configs_to_process):
901
+ speaker_name = config_item.get("name", f"Speaker{i+1}")
902
+ voice_name_from_config = config_item.get("voice")
903
+ gender_from_config = config_item.get("gender")
904
+ tone_from_config = config_item.get("tone")
905
+ final_voice_for_speaker = None
906
+
907
+ if voice_name_from_config:
908
+ final_voice_for_speaker = voice_name_from_config
909
+ log.info(
910
+ f"{log_identifier} Using specified voice '{final_voice_for_speaker}' for speaker '{speaker_name}'."
911
+ )
912
+ else:
913
+ final_voice_for_speaker = _get_voice_for_speaker(
914
+ gender=gender_from_config,
915
+ tone=tone_from_config,
916
+ used_voices_in_current_call=used_voices_in_current_call,
917
+ voice_tone_mapping=voice_tone_mapping,
918
+ gender_voice_mapping=gender_voice_mapping,
919
+ )
920
+ log.info(
921
+ f"{log_identifier} Selected voice '{final_voice_for_speaker}' for speaker '{speaker_name}' (gender='{gender_from_config}', tone='{tone_from_config}')."
922
+ )
923
+
924
+ if not final_voice_for_speaker:
925
+ final_voice_for_speaker = DEFAULT_VOICE
926
+ log.warning(
927
+ f"{log_identifier} Voice selection for speaker '{speaker_name}' resulted in None, using default '{DEFAULT_VOICE}'."
928
+ )
929
+
930
+ final_speaker_configs.append(
931
+ {"name": speaker_name, "voice": final_voice_for_speaker}
932
+ )
933
+ if final_voice_for_speaker:
934
+ used_voices_in_current_call.add(final_voice_for_speaker)
935
+
936
+ final_language = _get_language_code(language or default_language)
937
+
938
+ client = genai.Client(api_key=api_key)
939
+
940
+ multi_speaker_config = _create_multi_speaker_config(final_speaker_configs)
941
+ speech_config = adk_types.SpeechConfig(
942
+ multi_speaker_voice_config=multi_speaker_config
943
+ )
944
+
945
+ log.info(
946
+ f"{log_identifier} Generating multi-speaker audio with {len(final_speaker_configs)} speakers and language '{final_language}'"
947
+ )
948
+ wav_data = await _generate_audio_with_gemini(
949
+ client=client,
950
+ prompt=f"TTS the following conversation: {conversation_text}",
951
+ speech_config=speech_config,
952
+ model=model,
953
+ language=final_language,
954
+ )
955
+
956
+ log.info(f"{log_identifier} Converting audio to MP3 format")
957
+ mp3_data = await _convert_pcm_to_mp3(wav_data)
958
+
959
+ final_filename = output_filename
960
+ if not final_filename:
961
+ final_filename = f"multi_speaker_tts_{uuid.uuid4()}.mp3"
962
+ elif not final_filename.lower().endswith(".mp3"):
963
+ final_filename = f"{final_filename}.mp3"
964
+
965
+ voice_info = ", ".join(
966
+ [f"{s['name']} ({s['voice']})" for s in final_speaker_configs]
967
+ )
968
+
969
+ metadata = {
970
+ "description": f"Multi-speaker text-to-speech audio generated from conversation. Voices used: {voice_info}\nSource text: {conversation_text[:1500]}...",
971
+ "source_text": conversation_text[:500],
972
+ "language": final_language,
973
+ "model": model,
974
+ "generation_tool": "multi_speaker_text_to_speech",
975
+ "generation_timestamp": datetime.now(timezone.utc).isoformat(),
976
+ "speaker_count": len(final_speaker_configs),
977
+ "speakers_used_details": json.dumps(final_speaker_configs),
978
+ }
979
+
980
+ if language:
981
+ metadata["requested_language"] = language
982
+ if speaker_configs:
983
+ metadata["requested_speaker_configs"] = json.dumps(speaker_configs)
984
+
985
+ log.info(f"{log_identifier} Saving audio artifact '{final_filename}'")
986
+ save_result = await _save_audio_artifact(
987
+ audio_data=mp3_data,
988
+ filename=final_filename,
989
+ metadata=metadata,
990
+ tool_context=tool_context,
991
+ )
992
+
993
+ log.info(
994
+ f"{log_identifier} Multi-speaker audio artifact saved successfully: {final_filename} v{save_result['data_version']}"
995
+ )
996
+
997
+ speaker_summary = ", ".join(
998
+ [f"{s['name']} ({s['voice']})" for s in final_speaker_configs]
999
+ )
1000
+
1001
+ return {
1002
+ "status": "success",
1003
+ "message": f"Multi-speaker text-to-speech audio generated and saved successfully: Speakers: {speaker_summary}",
1004
+ "output_filename": final_filename,
1005
+ "output_version": save_result["data_version"],
1006
+ "speakers_used": speaker_summary,
1007
+ "language_used": final_language,
1008
+ "result_preview": f"Multi-speaker audio '{final_filename}' (v{save_result['data_version']}) created with {len(final_speaker_configs)} speakers from conversation: \"{conversation_text[:100]}...\"",
1009
+ }
1010
+
1011
+ except ValueError as ve:
1012
+ log.error(f"{log_identifier} Value error: {ve}")
1013
+ return {"status": "error", "message": str(ve)}
1014
+ except IOError as ioe:
1015
+ log.error(f"{log_identifier} IO error: {ioe}")
1016
+ return {"status": "error", "message": str(ioe)}
1017
+ except Exception as e:
1018
+ log.exception(
1019
+ f"{log_identifier} Unexpected error in multi_speaker_text_to_speech: {e}"
1020
+ )
1021
+ return {"status": "error", "message": f"An unexpected error occurred: {e}"}
1022
+
1023
+
1024
+ def _is_supported_audio_format_for_transcription(filename: str) -> bool:
1025
+ """Check if the audio format is supported for transcription."""
1026
+ ext = os.path.splitext(filename)[1].lower()
1027
+ supported_formats = {".wav", ".mp3"}
1028
+ return ext in supported_formats
1029
+
1030
+
1031
+ def _get_audio_mime_type(filename: str) -> str:
1032
+ """Get MIME type from audio file extension."""
1033
+ ext = os.path.splitext(filename)[1].lower()
1034
+ mime_mapping = {".wav": "audio/wav", ".mp3": "audio/mpeg"}
1035
+ return mime_mapping.get(ext, "audio/wav")
1036
+
1037
+
1038
+ async def concatenate_audio(
1039
+ clips_to_join: List[Dict[str, Any]],
1040
+ output_filename: Optional[str] = None,
1041
+ tool_context: ToolContext = None,
1042
+ tool_config: Optional[Dict[str, Any]] = None,
1043
+ ) -> Dict[str, Any]:
1044
+ """
1045
+ Combines multiple audio artifacts in a specified order into a single audio file.
1046
+ Allows for custom pause durations between each clip.
1047
+
1048
+ Args:
1049
+ clips_to_join: An ordered list of clip objects to be joined. Each object should contain:
1050
+ - `filename` (str): The artifact filename of the audio clip (with optional :version).
1051
+ - `pause_after_ms` (Optional[int]): The duration of silence, in milliseconds,
1052
+ to insert *after* this clip. If omitted, a default pause will be used. The gap between
1053
+ two people speaking in a conversation is typically around 250ms.
1054
+ output_filename: Optional. The desired filename for the final combined audio artifact.
1055
+ tool_context: The context provided by the ADK framework.
1056
+ tool_config: Configuration dictionary.
1057
+
1058
+ Returns:
1059
+ A dictionary with status and output artifact details.
1060
+ """
1061
+ log_identifier = "[AudioTools:concatenate_audio]"
1062
+ if not tool_context:
1063
+ return {"status": "error", "message": "ToolContext is missing"}
1064
+ if not clips_to_join:
1065
+ return {
1066
+ "status": "error",
1067
+ "message": "The 'clips_to_join' list cannot be empty.",
1068
+ }
1069
+
1070
+ try:
1071
+ inv_context = tool_context._invocation_context
1072
+ app_name = inv_context.app_name
1073
+ user_id = inv_context.user_id
1074
+ session_id = get_original_session_id(inv_context)
1075
+ artifact_service = inv_context.artifact_service
1076
+ host_component = getattr(inv_context.agent, "host_component", None)
1077
+
1078
+ if not artifact_service:
1079
+ raise ValueError("ArtifactService is not available in the context.")
1080
+
1081
+ config = tool_config or {}
1082
+ default_pause_ms = config.get("default_pause_ms", 250)
1083
+
1084
+ combined_audio = None
1085
+ source_filenames = []
1086
+
1087
+ for i, clip_info in enumerate(clips_to_join):
1088
+ clip_filename_with_version = clip_info.get("filename")
1089
+ if not clip_filename_with_version:
1090
+ raise ValueError(
1091
+ f"Clip at index {i} is missing the required 'filename' key."
1092
+ )
1093
+
1094
+ source_filenames.append(clip_filename_with_version)
1095
+
1096
+ parts = clip_filename_with_version.rsplit(":", 1)
1097
+ filename_base = parts[0]
1098
+ version_str = None
1099
+ if len(parts) > 1 and parts[1].isdigit():
1100
+ version_str = parts[1]
1101
+ version_to_load = int(version_str) if version_str else "latest"
1102
+
1103
+ load_result = await load_artifact_content_or_metadata(
1104
+ artifact_service=artifact_service,
1105
+ app_name=app_name,
1106
+ user_id=user_id,
1107
+ session_id=session_id,
1108
+ filename=filename_base,
1109
+ version=version_to_load,
1110
+ return_raw_bytes=True,
1111
+ component=host_component,
1112
+ log_identifier_prefix=f"{log_identifier}[LoadClip:{clip_filename_with_version}]",
1113
+ )
1114
+
1115
+ if load_result.get("status") != "success":
1116
+ raise FileNotFoundError(
1117
+ f"Failed to load audio clip '{clip_filename_with_version}': {load_result.get('message')}"
1118
+ )
1119
+
1120
+ audio_bytes = load_result.get("raw_bytes")
1121
+ mime_type = load_result.get("mime_type", "audio/mpeg")
1122
+
1123
+ audio_format = "mp3"
1124
+ if "wav" in mime_type:
1125
+ audio_format = "wav"
1126
+ elif "mpeg" in mime_type:
1127
+ audio_format = "mp3"
1128
+
1129
+ log.debug(
1130
+ f"{log_identifier} Loading clip '{clip_filename_with_version}' with format '{audio_format}'"
1131
+ )
1132
+
1133
+ clip_segment = await asyncio.to_thread(
1134
+ AudioSegment.from_file, io.BytesIO(audio_bytes), format=audio_format
1135
+ )
1136
+
1137
+ if combined_audio is None:
1138
+ combined_audio = clip_segment
1139
+ else:
1140
+ combined_audio += clip_segment
1141
+
1142
+ if i < len(clips_to_join) - 1:
1143
+ pause_ms = clip_info.get("pause_after_ms", default_pause_ms)
1144
+ if pause_ms > 0:
1145
+ pause_segment = AudioSegment.silent(duration=pause_ms)
1146
+ combined_audio += pause_segment
1147
+ log.debug(
1148
+ f"{log_identifier} Added {pause_ms}ms pause after '{clip_filename_with_version}'."
1149
+ )
1150
+
1151
+ if combined_audio is None:
1152
+ return {
1153
+ "status": "error",
1154
+ "message": "No audio clips were successfully processed.",
1155
+ }
1156
+
1157
+ output_buffer = io.BytesIO()
1158
+ await asyncio.to_thread(combined_audio.export, output_buffer, format="mp3")
1159
+ mp3_data = output_buffer.getvalue()
1160
+
1161
+ final_filename = output_filename
1162
+ if not final_filename:
1163
+ final_filename = f"concatenated_audio_{uuid.uuid4()}.mp3"
1164
+ elif not final_filename.lower().endswith(".mp3"):
1165
+ final_filename = f"{final_filename}.mp3"
1166
+
1167
+ metadata = {
1168
+ "description": f"Concatenated audio created from {len(clips_to_join)} clips.",
1169
+ "source_clips": source_filenames,
1170
+ "generation_tool": "concatenate_audio",
1171
+ "generation_timestamp": datetime.now(timezone.utc).isoformat(),
1172
+ }
1173
+
1174
+ log.info(
1175
+ f"{log_identifier} Saving concatenated audio artifact '{final_filename}'"
1176
+ )
1177
+ save_result = await _save_audio_artifact(
1178
+ audio_data=mp3_data,
1179
+ filename=final_filename,
1180
+ metadata=metadata,
1181
+ tool_context=tool_context,
1182
+ )
1183
+
1184
+ log.info(
1185
+ f"{log_identifier} Concatenated audio artifact saved successfully: {final_filename} v{save_result['data_version']}"
1186
+ )
1187
+
1188
+ return {
1189
+ "status": "success",
1190
+ "message": f"Audio clips concatenated and saved successfully.",
1191
+ "output_filename": final_filename,
1192
+ "output_version": save_result["data_version"],
1193
+ "result_preview": f"Concatenated audio '{final_filename}' (v{save_result['data_version']}) created from {len(clips_to_join)} clips.",
1194
+ }
1195
+
1196
+ except FileNotFoundError as e:
1197
+ log.warning(f"{log_identifier} File not found error: {e}")
1198
+ return {"status": "error", "message": str(e)}
1199
+ except ValueError as ve:
1200
+ log.error(f"{log_identifier} Value error: {ve}")
1201
+ return {"status": "error", "message": str(ve)}
1202
+ except IOError as ioe:
1203
+ log.error(f"{log_identifier} IO error: {ioe}")
1204
+ return {"status": "error", "message": str(ioe)}
1205
+ except Exception as e:
1206
+ log.exception(f"{log_identifier} Unexpected error in concatenate_audio: {e}")
1207
+ return {"status": "error", "message": f"An unexpected error occurred: {e}"}
1208
+
1209
+
1210
+ async def transcribe_audio(
1211
+ audio_filename: str,
1212
+ tool_context: ToolContext = None,
1213
+ tool_config: Optional[Dict[str, Any]] = None,
1214
+ ) -> Dict[str, Any]:
1215
+ """
1216
+ Transcribes an audio recording using an OpenAI-compatible audio transcription API.
1217
+
1218
+ Args:
1219
+ audio_filename: The filename (and optional :version) of the input audio artifact.
1220
+ tool_context: The context provided by the ADK framework.
1221
+ tool_config: Configuration dictionary containing model, api_base, api_key.
1222
+
1223
+ Returns:
1224
+ A dictionary containing:
1225
+ - "status": "success" or "error".
1226
+ - "message": A descriptive message about the outcome.
1227
+ - "transcription": The transcribed text from the API (if successful).
1228
+ - "audio_filename": The name of the input audio artifact (if successful).
1229
+ - "audio_version": The version of the input audio artifact (if successful).
1230
+ """
1231
+ log_identifier = f"[AudioTools:transcribe_audio:{audio_filename}]"
1232
+ if not tool_context:
1233
+ log.error(f"{log_identifier} ToolContext is missing.")
1234
+ return {"status": "error", "message": "ToolContext is missing."}
1235
+
1236
+ try:
1237
+ inv_context = tool_context._invocation_context
1238
+ if not inv_context:
1239
+ raise ValueError("InvocationContext is not available.")
1240
+
1241
+ app_name = getattr(inv_context, "app_name", None)
1242
+ user_id = getattr(inv_context, "user_id", None)
1243
+ session_id = get_original_session_id(inv_context)
1244
+ artifact_service = getattr(inv_context, "artifact_service", None)
1245
+
1246
+ if not all([app_name, user_id, session_id, artifact_service]):
1247
+ missing_parts = [
1248
+ part
1249
+ for part, val in [
1250
+ ("app_name", app_name),
1251
+ ("user_id", user_id),
1252
+ ("session_id", session_id),
1253
+ ("artifact_service", artifact_service),
1254
+ ]
1255
+ if not val
1256
+ ]
1257
+ raise ValueError(
1258
+ f"Missing required context parts: {', '.join(missing_parts)}"
1259
+ )
1260
+
1261
+ log.info(f"{log_identifier} Processing request for session {session_id}.")
1262
+
1263
+ current_tool_config = tool_config if tool_config is not None else {}
1264
+
1265
+ if not current_tool_config:
1266
+ log.warning(
1267
+ f"{log_identifier} Tool-specific configuration (tool_config) is empty."
1268
+ )
1269
+
1270
+ model_name = current_tool_config.get("model")
1271
+ api_key = current_tool_config.get("api_key")
1272
+ api_base = current_tool_config.get("api_base")
1273
+
1274
+ if not model_name:
1275
+ raise ValueError("'model' configuration is missing in tool_config.")
1276
+ if not api_key:
1277
+ raise ValueError("'api_key' configuration is missing in tool_config.")
1278
+ if not api_base:
1279
+ raise ValueError("'api_base' configuration is missing in tool_config.")
1280
+
1281
+ log.debug(f"{log_identifier} Using model: {model_name}, API base: {api_base}")
1282
+
1283
+ parts = audio_filename.split(":", 1)
1284
+ filename_base_for_load = parts[0]
1285
+ version_str = parts[1] if len(parts) > 1 else None
1286
+ version_to_load = int(version_str) if version_str else None
1287
+
1288
+ if not _is_supported_audio_format_for_transcription(filename_base_for_load):
1289
+ raise ValueError(f"Unsupported audio format. Supported formats: .wav, .mp3")
1290
+
1291
+ if version_to_load is None:
1292
+ list_versions_method = getattr(artifact_service, "list_versions")
1293
+ if inspect.iscoroutinefunction(list_versions_method):
1294
+ versions = await list_versions_method(
1295
+ app_name=app_name,
1296
+ user_id=user_id,
1297
+ session_id=session_id,
1298
+ filename=filename_base_for_load,
1299
+ )
1300
+ else:
1301
+ versions = await asyncio.to_thread(
1302
+ list_versions_method,
1303
+ app_name=app_name,
1304
+ user_id=user_id,
1305
+ session_id=session_id,
1306
+ filename=filename_base_for_load,
1307
+ )
1308
+ if not versions:
1309
+ raise FileNotFoundError(
1310
+ f"Audio artifact '{filename_base_for_load}' not found."
1311
+ )
1312
+ version_to_load = max(versions)
1313
+ log.debug(
1314
+ f"{log_identifier} Using latest version for input: {version_to_load}"
1315
+ )
1316
+
1317
+ load_artifact_method = getattr(artifact_service, "load_artifact")
1318
+ if inspect.iscoroutinefunction(load_artifact_method):
1319
+ audio_artifact_part = await load_artifact_method(
1320
+ app_name=app_name,
1321
+ user_id=user_id,
1322
+ session_id=session_id,
1323
+ filename=filename_base_for_load,
1324
+ version=version_to_load,
1325
+ )
1326
+ else:
1327
+ audio_artifact_part = await asyncio.to_thread(
1328
+ load_artifact_method,
1329
+ app_name=app_name,
1330
+ user_id=user_id,
1331
+ session_id=session_id,
1332
+ filename=filename_base_for_load,
1333
+ version=version_to_load,
1334
+ )
1335
+
1336
+ if not audio_artifact_part or not audio_artifact_part.inline_data:
1337
+ raise FileNotFoundError(
1338
+ f"Content for audio artifact '{filename_base_for_load}' v{version_to_load} not found."
1339
+ )
1340
+
1341
+ audio_bytes = audio_artifact_part.inline_data.data
1342
+ log.debug(f"{log_identifier} Loaded audio artifact: {len(audio_bytes)} bytes")
1343
+
1344
+ temp_file_path = None
1345
+ try:
1346
+ file_ext = os.path.splitext(filename_base_for_load)[1]
1347
+
1348
+ with tempfile.NamedTemporaryFile(
1349
+ suffix=file_ext, delete=False
1350
+ ) as temp_file:
1351
+ temp_file_path = temp_file.name
1352
+ temp_file.write(audio_bytes)
1353
+
1354
+ log.debug(f"{log_identifier} Created temporary file: {temp_file_path}")
1355
+
1356
+ api_url = f"{api_base.rstrip('/')}/v1/audio/transcriptions"
1357
+ headers = {"Authorization": f"Bearer {api_key}"}
1358
+
1359
+ mime_type = _get_audio_mime_type(filename_base_for_load)
1360
+
1361
+ with open(temp_file_path, "rb") as audio_file:
1362
+ files = {
1363
+ "file": (filename_base_for_load, audio_file, mime_type),
1364
+ "model": (None, model_name),
1365
+ }
1366
+
1367
+ log.debug(f"{log_identifier} Calling transcription API...")
1368
+
1369
+ async with httpx.AsyncClient(timeout=60.0) as client:
1370
+ response = await client.post(api_url, headers=headers, files=files)
1371
+ response.raise_for_status()
1372
+ response_data = response.json()
1373
+
1374
+ log.debug(f"{log_identifier} Transcription API response received.")
1375
+
1376
+ if not response_data.get("text"):
1377
+ raise ValueError("API response does not contain transcription text.")
1378
+
1379
+ transcription = response_data["text"]
1380
+
1381
+ log.info(
1382
+ f"{log_identifier} Audio transcribed successfully. Transcription length: {len(transcription)} characters"
1383
+ )
1384
+
1385
+ return {
1386
+ "status": "success",
1387
+ "message": "Audio transcribed successfully",
1388
+ "transcription": transcription,
1389
+ "audio_filename": filename_base_for_load,
1390
+ "audio_version": version_to_load,
1391
+ }
1392
+
1393
+ finally:
1394
+ if temp_file_path and os.path.exists(temp_file_path):
1395
+ try:
1396
+ os.remove(temp_file_path)
1397
+ log.debug(
1398
+ f"{log_identifier} Cleaned up temporary file: {temp_file_path}"
1399
+ )
1400
+ except OSError as e:
1401
+ log.warning(
1402
+ f"{log_identifier} Failed to clean up temporary file {temp_file_path}: {e}"
1403
+ )
1404
+
1405
+ except FileNotFoundError as e:
1406
+ log.warning(f"{log_identifier} File not found error: {e}")
1407
+ return {"status": "error", "message": str(e)}
1408
+ except ValueError as ve:
1409
+ log.error(f"{log_identifier} Value error: {ve}")
1410
+ return {"status": "error", "message": str(ve)}
1411
+ except httpx.HTTPStatusError as hse:
1412
+ log.error(
1413
+ f"{log_identifier} HTTP error calling transcription API: {hse.response.status_code} - {hse.response.text}"
1414
+ )
1415
+ return {"status": "error", "message": f"API error: {hse.response.status_code}"}
1416
+ except httpx.RequestError as re:
1417
+ log.error(f"{log_identifier} Request error calling transcription API: {re}")
1418
+ return {"status": "error", "message": f"Request error: {re}"}
1419
+ except json.JSONDecodeError as jde:
1420
+ log.error(f"{log_identifier} JSON decode error: {jde}")
1421
+ return {"status": "error", "message": "Invalid JSON response from API"}
1422
+ except Exception as e:
1423
+ log.exception(f"{log_identifier} Unexpected error in transcribe_audio: {e}")
1424
+ return {"status": "error", "message": f"An unexpected error occurred: {e}"}
1425
+
1426
+
1427
+ select_voice_tool_def = BuiltinTool(
1428
+ name="select_voice",
1429
+ implementation=select_voice,
1430
+ description="Selects a suitable voice name based on criteria like gender and tone. Use this to get a consistent voice name that can be passed to the `text_to_speech` tool for multiple calls.",
1431
+ category="audio",
1432
+ required_scopes=["tool:audio:tts"],
1433
+ parameters=adk_types.Schema(
1434
+ type=adk_types.Type.OBJECT,
1435
+ properties={
1436
+ "gender": adk_types.Schema(
1437
+ type=adk_types.Type.STRING,
1438
+ description="Optional desired gender for the voice ('male', 'female', 'neutral').",
1439
+ nullable=True,
1440
+ ),
1441
+ "tone": adk_types.Schema(
1442
+ type=adk_types.Type.STRING,
1443
+ description="Optional tone preference (e.g., 'friendly', 'professional').",
1444
+ nullable=True,
1445
+ ),
1446
+ "exclude_voices": adk_types.Schema(
1447
+ type=adk_types.Type.ARRAY,
1448
+ items=adk_types.Schema(type=adk_types.Type.STRING),
1449
+ description="Optional list of voice names to exclude from the selection.",
1450
+ nullable=True,
1451
+ ),
1452
+ },
1453
+ required=[],
1454
+ ),
1455
+ examples=[],
1456
+ )
1457
+
1458
+ text_to_speech_tool_def = BuiltinTool(
1459
+ name="text_to_speech",
1460
+ implementation=text_to_speech,
1461
+ description="Converts text to speech using Gemini TTS API and saves as MP3 artifact.",
1462
+ category="audio",
1463
+ required_scopes=["tool:audio:tts"],
1464
+ parameters=adk_types.Schema(
1465
+ type=adk_types.Type.OBJECT,
1466
+ properties={
1467
+ "text": adk_types.Schema(
1468
+ type=adk_types.Type.STRING, description="The text to convert to speech."
1469
+ ),
1470
+ "output_filename": adk_types.Schema(
1471
+ type=adk_types.Type.STRING,
1472
+ description="Optional filename for the output MP3.",
1473
+ nullable=True,
1474
+ ),
1475
+ "voice_name": adk_types.Schema(
1476
+ type=adk_types.Type.STRING,
1477
+ description="Optional specific voice name (e.g., 'Kore', 'Puck'). Overrides gender and tone.",
1478
+ nullable=True,
1479
+ ),
1480
+ "gender": adk_types.Schema(
1481
+ type=adk_types.Type.STRING,
1482
+ description="Optional desired gender for the voice ('male', 'female', 'neutral'). Used if 'voice_name' is not provided.",
1483
+ nullable=True,
1484
+ ),
1485
+ "tone": adk_types.Schema(
1486
+ type=adk_types.Type.STRING,
1487
+ description="Optional tone preference (e.g., 'friendly', 'professional'). Used if 'voice_name' is not provided.",
1488
+ nullable=True,
1489
+ ),
1490
+ "language": adk_types.Schema(
1491
+ type=adk_types.Type.STRING,
1492
+ description="Optional language code (e.g., 'en-US', 'fr-FR').",
1493
+ nullable=True,
1494
+ ),
1495
+ },
1496
+ required=["text"],
1497
+ ),
1498
+ examples=[],
1499
+ )
1500
+
1501
+ multi_speaker_text_to_speech_tool_def = BuiltinTool(
1502
+ name="multi_speaker_text_to_speech",
1503
+ implementation=multi_speaker_text_to_speech,
1504
+ description="Converts conversation text with speaker labels to speech using multiple voices.",
1505
+ category="audio",
1506
+ required_scopes=["tool:audio:tts"],
1507
+ parameters=adk_types.Schema(
1508
+ type=adk_types.Type.OBJECT,
1509
+ properties={
1510
+ "conversation_text": adk_types.Schema(
1511
+ type=adk_types.Type.STRING,
1512
+ description="Text with speaker labels (e.g., 'Speaker1: Hello\\nSpeaker2: Hi there').",
1513
+ ),
1514
+ "output_filename": adk_types.Schema(
1515
+ type=adk_types.Type.STRING,
1516
+ description="Optional filename for the output MP3.",
1517
+ nullable=True,
1518
+ ),
1519
+ "speaker_configs": adk_types.Schema(
1520
+ type=adk_types.Type.ARRAY,
1521
+ items=adk_types.Schema(
1522
+ type=adk_types.Type.OBJECT,
1523
+ description="Configuration for a single speaker.",
1524
+ properties={
1525
+ "name": adk_types.Schema(
1526
+ type=adk_types.Type.STRING,
1527
+ description="The speaker's name as it appears in conversation_text.",
1528
+ ),
1529
+ "voice": adk_types.Schema(
1530
+ type=adk_types.Type.STRING,
1531
+ description="Specific voice name to use. Overrides gender/tone.",
1532
+ nullable=True,
1533
+ ),
1534
+ "gender": adk_types.Schema(
1535
+ type=adk_types.Type.STRING,
1536
+ description="Desired gender ('male', 'female', 'neutral').",
1537
+ nullable=True,
1538
+ ),
1539
+ "tone": adk_types.Schema(
1540
+ type=adk_types.Type.STRING,
1541
+ description="Desired tone (e.g., 'friendly').",
1542
+ nullable=True,
1543
+ ),
1544
+ },
1545
+ ),
1546
+ description="Optional list of speaker configurations.",
1547
+ nullable=True,
1548
+ ),
1549
+ "language": adk_types.Schema(
1550
+ type=adk_types.Type.STRING,
1551
+ description="Optional language code (e.g., 'en-US', 'fr-FR').",
1552
+ nullable=True,
1553
+ ),
1554
+ },
1555
+ required=["conversation_text"],
1556
+ ),
1557
+ examples=[],
1558
+ )
1559
+
1560
+ concatenate_audio_tool_def = BuiltinTool(
1561
+ name="concatenate_audio",
1562
+ implementation=concatenate_audio,
1563
+ description="Combines multiple audio artifacts in a specified order into a single audio file. Allows for custom pause durations between each clip.",
1564
+ category="audio",
1565
+ required_scopes=["tool:audio:edit", "tool:artifact:create", "tool:artifact:load"],
1566
+ parameters=adk_types.Schema(
1567
+ type=adk_types.Type.OBJECT,
1568
+ properties={
1569
+ "clips_to_join": adk_types.Schema(
1570
+ type=adk_types.Type.ARRAY,
1571
+ description="An ordered list of clip objects to be joined.",
1572
+ items=adk_types.Schema(
1573
+ type=adk_types.Type.OBJECT,
1574
+ properties={
1575
+ "filename": adk_types.Schema(
1576
+ type=adk_types.Type.STRING,
1577
+ description="The artifact filename of the audio clip (with optional :version).",
1578
+ ),
1579
+ "pause_after_ms": adk_types.Schema(
1580
+ type=adk_types.Type.INTEGER,
1581
+ description="Optional duration of silence in milliseconds to insert *after* this clip. Defaults to 500ms.",
1582
+ nullable=True,
1583
+ ),
1584
+ },
1585
+ required=["filename"],
1586
+ ),
1587
+ ),
1588
+ "output_filename": adk_types.Schema(
1589
+ type=adk_types.Type.STRING,
1590
+ description="Optional. The desired filename for the final combined audio artifact.",
1591
+ nullable=True,
1592
+ ),
1593
+ },
1594
+ required=["clips_to_join"],
1595
+ ),
1596
+ examples=[],
1597
+ )
1598
+
1599
+ transcribe_audio_tool_def = BuiltinTool(
1600
+ name="transcribe_audio",
1601
+ implementation=transcribe_audio,
1602
+ description="Transcribes an audio recording using an OpenAI-compatible audio transcription API.",
1603
+ category="audio",
1604
+ required_scopes=["tool:audio:transcribe"],
1605
+ parameters=adk_types.Schema(
1606
+ type=adk_types.Type.OBJECT,
1607
+ properties={
1608
+ "audio_filename": adk_types.Schema(
1609
+ type=adk_types.Type.STRING,
1610
+ description="The filename (and optional :version) of the input audio artifact.",
1611
+ ),
1612
+ },
1613
+ required=["audio_filename"],
1614
+ ),
1615
+ examples=[],
1616
+ )
1617
+
1618
+ tool_registry.register(select_voice_tool_def)
1619
+ tool_registry.register(text_to_speech_tool_def)
1620
+ tool_registry.register(multi_speaker_text_to_speech_tool_def)
1621
+ tool_registry.register(concatenate_audio_tool_def)
1622
+ tool_registry.register(transcribe_audio_tool_def)