solace-agent-mesh 1.6.1__py3-none-any.whl → 1.13.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (481) hide show
  1. solace_agent_mesh/agent/adk/alembic/README +74 -0
  2. solace_agent_mesh/agent/adk/alembic/env.py +77 -0
  3. solace_agent_mesh/agent/adk/alembic/script.py.mako +28 -0
  4. solace_agent_mesh/agent/adk/alembic/versions/e2902798564d_adk_session_db_upgrade.py +52 -0
  5. solace_agent_mesh/agent/adk/alembic.ini +112 -0
  6. solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
  7. solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +165 -1
  8. solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +163 -0
  9. solace_agent_mesh/agent/adk/callbacks.py +852 -109
  10. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +234 -36
  11. solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +52 -5
  12. solace_agent_mesh/agent/adk/mcp_content_processor.py +1 -1
  13. solace_agent_mesh/agent/adk/models/lite_llm.py +77 -21
  14. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +24 -137
  15. solace_agent_mesh/agent/adk/runner.py +85 -20
  16. solace_agent_mesh/agent/adk/schema_migration.py +88 -0
  17. solace_agent_mesh/agent/adk/services.py +94 -18
  18. solace_agent_mesh/agent/adk/setup.py +281 -65
  19. solace_agent_mesh/agent/adk/stream_parser.py +231 -37
  20. solace_agent_mesh/agent/adk/tool_wrapper.py +3 -0
  21. solace_agent_mesh/agent/protocol/event_handlers.py +472 -137
  22. solace_agent_mesh/agent/proxies/a2a/app.py +3 -2
  23. solace_agent_mesh/agent/proxies/a2a/component.py +572 -75
  24. solace_agent_mesh/agent/proxies/a2a/config.py +80 -4
  25. solace_agent_mesh/agent/proxies/base/app.py +3 -2
  26. solace_agent_mesh/agent/proxies/base/component.py +188 -22
  27. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +3 -1
  28. solace_agent_mesh/agent/sac/app.py +91 -3
  29. solace_agent_mesh/agent/sac/component.py +591 -157
  30. solace_agent_mesh/agent/sac/patch_adk.py +8 -16
  31. solace_agent_mesh/agent/sac/task_execution_context.py +146 -4
  32. solace_agent_mesh/agent/tools/__init__.py +3 -0
  33. solace_agent_mesh/agent/tools/audio_tools.py +3 -3
  34. solace_agent_mesh/agent/tools/builtin_artifact_tools.py +710 -171
  35. solace_agent_mesh/agent/tools/deep_research_tools.py +2161 -0
  36. solace_agent_mesh/agent/tools/dynamic_tool.py +2 -0
  37. solace_agent_mesh/agent/tools/peer_agent_tool.py +82 -15
  38. solace_agent_mesh/agent/tools/time_tools.py +126 -0
  39. solace_agent_mesh/agent/tools/tool_config_types.py +57 -2
  40. solace_agent_mesh/agent/tools/web_search_tools.py +279 -0
  41. solace_agent_mesh/agent/tools/web_tools.py +125 -17
  42. solace_agent_mesh/agent/utils/artifact_helpers.py +248 -6
  43. solace_agent_mesh/agent/utils/context_helpers.py +17 -0
  44. solace_agent_mesh/assets/docs/404.html +6 -6
  45. solace_agent_mesh/assets/docs/assets/css/{styles.906a1503.css → styles.8162edfb.css} +1 -1
  46. solace_agent_mesh/assets/docs/assets/js/05749d90.19ac4f35.js +1 -0
  47. solace_agent_mesh/assets/docs/assets/js/15ba94aa.e186750d.js +1 -0
  48. solace_agent_mesh/assets/docs/assets/js/15e40e79.434bb30f.js +1 -0
  49. solace_agent_mesh/assets/docs/assets/js/17896441.e612dfb4.js +1 -0
  50. solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js +2 -0
  51. solace_agent_mesh/assets/docs/assets/js/{17896441.a5e82f9b.js.LICENSE.txt → 2279.550aa580.js.LICENSE.txt} +6 -0
  52. solace_agent_mesh/assets/docs/assets/js/240a0364.83e37aa8.js +1 -0
  53. solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
  54. solace_agent_mesh/assets/docs/assets/js/2e32b5e0.2f0db237.js +1 -0
  55. solace_agent_mesh/assets/docs/assets/js/3a6c6137.7e61915d.js +1 -0
  56. solace_agent_mesh/assets/docs/assets/js/3ac1795d.7f7ab1c1.js +1 -0
  57. solace_agent_mesh/assets/docs/assets/js/3ff0015d.e53c9b78.js +1 -0
  58. solace_agent_mesh/assets/docs/assets/js/41adc471.0e95b87c.js +1 -0
  59. solace_agent_mesh/assets/docs/assets/js/4667dc50.bf2ad456.js +1 -0
  60. solace_agent_mesh/assets/docs/assets/js/49eed117.493d6f99.js +1 -0
  61. solace_agent_mesh/assets/docs/assets/js/{509e993c.4c7a1a6d.js → 509e993c.a1fbf45a.js} +1 -1
  62. solace_agent_mesh/assets/docs/assets/js/547e15cc.8e6da617.js +1 -0
  63. solace_agent_mesh/assets/docs/assets/js/55b7b518.29d6e75d.js +1 -0
  64. solace_agent_mesh/assets/docs/assets/js/5b8d9c11.d4eb37b8.js +1 -0
  65. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.1ee87753.js +1 -0
  66. solace_agent_mesh/assets/docs/assets/js/60702c0e.a8bdd79b.js +1 -0
  67. solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
  68. solace_agent_mesh/assets/docs/assets/js/64195356.09dbd087.js +1 -0
  69. solace_agent_mesh/assets/docs/assets/js/66d4869e.30340bd3.js +1 -0
  70. solace_agent_mesh/assets/docs/assets/js/6a520c9d.b6e3f2ce.js +1 -0
  71. solace_agent_mesh/assets/docs/assets/js/6aaedf65.7253541d.js +1 -0
  72. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
  73. solace_agent_mesh/assets/docs/assets/js/6d84eae0.fd23ba4a.js +1 -0
  74. solace_agent_mesh/assets/docs/assets/js/71da7b71.374b9d54.js +1 -0
  75. solace_agent_mesh/assets/docs/assets/js/729898df.7249e9fd.js +1 -0
  76. solace_agent_mesh/assets/docs/assets/js/7e294c01.7c5f6906.js +1 -0
  77. solace_agent_mesh/assets/docs/assets/js/8024126c.e3467286.js +1 -0
  78. solace_agent_mesh/assets/docs/assets/js/81a99df0.7ed65d45.js +1 -0
  79. solace_agent_mesh/assets/docs/assets/js/82fbfb93.161823a5.js +1 -0
  80. solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
  81. solace_agent_mesh/assets/docs/assets/js/924ffdeb.975e428a.js +1 -0
  82. solace_agent_mesh/assets/docs/assets/js/94e8668d.16083b3f.js +1 -0
  83. solace_agent_mesh/assets/docs/assets/js/9bb13469.4523ae20.js +1 -0
  84. solace_agent_mesh/assets/docs/assets/js/a7d42657.a956689d.js +1 -0
  85. solace_agent_mesh/assets/docs/assets/js/a94703ab.3e5fbcb3.js +1 -0
  86. solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e563275.js +1 -0
  87. solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
  88. solace_agent_mesh/assets/docs/assets/js/c93cbaa0.0e0d8baf.js +1 -0
  89. solace_agent_mesh/assets/docs/assets/js/cab03b5b.6a073091.js +1 -0
  90. solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.07e170dd.js +1 -0
  91. solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
  92. solace_agent_mesh/assets/docs/assets/js/dd817ffc.c37a755e.js +1 -0
  93. solace_agent_mesh/assets/docs/assets/js/dd81e2b8.b682e9c2.js +1 -0
  94. solace_agent_mesh/assets/docs/assets/js/de915948.44a432bc.js +1 -0
  95. solace_agent_mesh/assets/docs/assets/js/e04b235d.06d23db6.js +1 -0
  96. solace_agent_mesh/assets/docs/assets/js/e1b6eeb4.deb2b62e.js +1 -0
  97. solace_agent_mesh/assets/docs/assets/js/e3d9abda.1476f570.js +1 -0
  98. solace_agent_mesh/assets/docs/assets/js/e6f9706b.acc800d3.js +1 -0
  99. solace_agent_mesh/assets/docs/assets/js/e92d0134.c147a429.js +1 -0
  100. solace_agent_mesh/assets/docs/assets/js/ee0c2fe7.94d0a351.js +1 -0
  101. solace_agent_mesh/assets/docs/assets/js/f284c35a.cc97854c.js +1 -0
  102. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
  103. solace_agent_mesh/assets/docs/assets/js/main.d634009f.js +2 -0
  104. solace_agent_mesh/assets/docs/assets/js/runtime~main.27bb82a7.js +1 -0
  105. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +68 -68
  106. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +50 -50
  107. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +42 -42
  108. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +55 -55
  109. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +82 -68
  110. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/image-tools/index.html +81 -0
  111. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +67 -50
  112. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/research-tools/index.html +136 -0
  113. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +178 -144
  114. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +43 -42
  115. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +20 -18
  116. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +23 -23
  117. solace_agent_mesh/assets/docs/docs/documentation/components/platform-service/index.html +33 -0
  118. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +45 -45
  119. solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +182 -0
  120. solace_agent_mesh/assets/docs/docs/documentation/components/prompts/index.html +147 -0
  121. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +208 -125
  122. solace_agent_mesh/assets/docs/docs/documentation/components/speech/index.html +52 -0
  123. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +28 -49
  124. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +29 -30
  125. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +14 -14
  126. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/index.html +47 -0
  127. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/kubernetes-deployment-guide/index.html +197 -0
  128. solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +90 -0
  129. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +17 -16
  130. solace_agent_mesh/assets/docs/docs/documentation/deploying/proxy_configuration/index.html +49 -0
  131. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +38 -38
  132. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +162 -171
  133. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +67 -49
  134. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +17 -17
  135. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +51 -51
  136. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +22 -22
  137. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +27 -27
  138. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +135 -135
  139. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +66 -66
  140. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +51 -51
  141. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +50 -38
  142. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +86 -86
  143. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +51 -51
  144. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +24 -24
  145. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +30 -30
  146. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +44 -44
  147. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/teams-integration/index.html +115 -0
  148. solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +86 -0
  149. solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +67 -0
  150. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +23 -19
  151. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +40 -37
  152. solace_agent_mesh/assets/docs/docs/documentation/enterprise/openapi-tools/index.html +324 -0
  153. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +112 -87
  154. solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +440 -0
  155. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +87 -64
  156. solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +62 -0
  157. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +44 -44
  158. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +39 -37
  159. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +30 -30
  160. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +18 -18
  161. solace_agent_mesh/assets/docs/docs/documentation/getting-started/vibe_coding/index.html +62 -0
  162. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +311 -0
  163. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +39 -42
  164. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +14 -14
  165. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +27 -25
  166. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +69 -69
  167. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +72 -72
  168. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +251 -0
  169. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +88 -0
  170. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +42 -42
  171. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +20 -20
  172. solace_agent_mesh/assets/docs/docs/documentation/migrations/platform-service-split/index.html +85 -0
  173. solace_agent_mesh/assets/docs/lunr-index-1768329217460.json +1 -0
  174. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  175. solace_agent_mesh/assets/docs/search-doc-1768329217460.json +1 -0
  176. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  177. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  178. solace_agent_mesh/cli/__init__.py +1 -1
  179. solace_agent_mesh/cli/commands/add_cmd/__init__.py +3 -1
  180. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -1
  181. solace_agent_mesh/cli/commands/add_cmd/proxy_cmd.py +100 -0
  182. solace_agent_mesh/cli/commands/docs_cmd.py +4 -1
  183. solace_agent_mesh/cli/commands/eval_cmd.py +1 -1
  184. solace_agent_mesh/cli/commands/init_cmd/__init__.py +15 -0
  185. solace_agent_mesh/cli/commands/init_cmd/directory_step.py +1 -1
  186. solace_agent_mesh/cli/commands/init_cmd/env_step.py +30 -3
  187. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +3 -4
  188. solace_agent_mesh/cli/commands/init_cmd/platform_service_step.py +85 -0
  189. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +16 -3
  190. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +2 -1
  191. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +1 -0
  192. solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +3 -3
  193. solace_agent_mesh/cli/commands/run_cmd.py +64 -49
  194. solace_agent_mesh/cli/commands/tools_cmd.py +315 -0
  195. solace_agent_mesh/cli/main.py +15 -0
  196. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-BTf6dqwp.js → authCallback-KnKMP_vb.js} +1 -1
  197. solace_agent_mesh/client/webui/frontend/static/assets/client-DpBL2stg.js +25 -0
  198. solace_agent_mesh/client/webui/frontend/static/assets/main-Cd498TV2.js +435 -0
  199. solace_agent_mesh/client/webui/frontend/static/assets/main-rSf8Vu29.css +1 -0
  200. solace_agent_mesh/client/webui/frontend/static/assets/vendor-CGk8Suyh.js +565 -0
  201. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  202. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  203. solace_agent_mesh/client/webui/frontend/static/mockServiceWorker.js +336 -0
  204. solace_agent_mesh/client/webui/frontend/static/ui-version.json +6 -0
  205. solace_agent_mesh/common/a2a/events.py +2 -1
  206. solace_agent_mesh/common/a2a/protocol.py +5 -0
  207. solace_agent_mesh/common/a2a/types.py +2 -1
  208. solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +23 -6
  209. solace_agent_mesh/common/a2a_spec/schemas/feedback_event.json +51 -0
  210. solace_agent_mesh/common/agent_registry.py +38 -11
  211. solace_agent_mesh/common/data_parts.py +144 -4
  212. solace_agent_mesh/common/error_handlers.py +83 -0
  213. solace_agent_mesh/common/exceptions.py +24 -0
  214. solace_agent_mesh/common/oauth/__init__.py +17 -0
  215. solace_agent_mesh/common/oauth/oauth_client.py +408 -0
  216. solace_agent_mesh/common/oauth/utils.py +50 -0
  217. solace_agent_mesh/common/rag_dto.py +156 -0
  218. solace_agent_mesh/common/sac/sam_component_base.py +97 -19
  219. solace_agent_mesh/common/sam_events/event_service.py +2 -2
  220. solace_agent_mesh/common/services/employee_service.py +1 -1
  221. solace_agent_mesh/common/utils/embeds/constants.py +1 -0
  222. solace_agent_mesh/common/utils/embeds/converter.py +1 -8
  223. solace_agent_mesh/common/utils/embeds/modifiers.py +4 -28
  224. solace_agent_mesh/common/utils/embeds/resolver.py +152 -31
  225. solace_agent_mesh/common/utils/embeds/types.py +9 -0
  226. solace_agent_mesh/common/utils/log_formatters.py +20 -0
  227. solace_agent_mesh/common/utils/mime_helpers.py +12 -5
  228. solace_agent_mesh/common/utils/pydantic_utils.py +90 -3
  229. solace_agent_mesh/common/utils/rbac_utils.py +69 -0
  230. solace_agent_mesh/common/utils/templates/__init__.py +8 -0
  231. solace_agent_mesh/common/utils/templates/liquid_renderer.py +210 -0
  232. solace_agent_mesh/common/utils/templates/template_resolver.py +161 -0
  233. solace_agent_mesh/config_portal/backend/common.py +12 -0
  234. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-CljP4_mv.js +103 -0
  235. solace_agent_mesh/config_portal/frontend/static/client/assets/{components-Rk0n-9cK.js → components-CaC6hG8d.js} +22 -22
  236. solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-mvZjNKiz.js → entry.client-H_TM0YBt.js} +3 -3
  237. solace_agent_mesh/config_portal/frontend/static/client/assets/{index-DzNKzXrc.js → index-CnFykb2v.js} +16 -16
  238. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-f8439d40.js +1 -0
  239. solace_agent_mesh/config_portal/frontend/static/client/assets/root-BIMqslJB.css +1 -0
  240. solace_agent_mesh/config_portal/frontend/static/client/assets/root-mJmTIdIk.js +10 -0
  241. solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
  242. solace_agent_mesh/core_a2a/service.py +3 -2
  243. solace_agent_mesh/gateway/adapter/__init__.py +1 -0
  244. solace_agent_mesh/gateway/adapter/base.py +170 -0
  245. solace_agent_mesh/gateway/adapter/types.py +230 -0
  246. solace_agent_mesh/gateway/base/app.py +39 -2
  247. solace_agent_mesh/gateway/base/auth_interface.py +103 -0
  248. solace_agent_mesh/gateway/base/component.py +1027 -151
  249. solace_agent_mesh/gateway/generic/__init__.py +1 -0
  250. solace_agent_mesh/gateway/generic/app.py +50 -0
  251. solace_agent_mesh/gateway/generic/component.py +894 -0
  252. solace_agent_mesh/gateway/http_sse/alembic/env.py +0 -7
  253. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_project_users_table.py +72 -0
  254. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +109 -0
  255. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +26 -0
  256. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_projects_table.py +135 -0
  257. solace_agent_mesh/gateway/http_sse/alembic/versions/20251108_create_prompt_tables_with_sharing.py +154 -0
  258. solace_agent_mesh/gateway/http_sse/alembic/versions/20251115_add_parent_task_id.py +32 -0
  259. solace_agent_mesh/gateway/http_sse/alembic/versions/20251126_add_background_task_fields.py +47 -0
  260. solace_agent_mesh/gateway/http_sse/alembic/versions/20251202_add_versioned_fields_to_prompts.py +52 -0
  261. solace_agent_mesh/gateway/http_sse/alembic.ini +0 -36
  262. solace_agent_mesh/gateway/http_sse/app.py +40 -11
  263. solace_agent_mesh/gateway/http_sse/component.py +285 -160
  264. solace_agent_mesh/gateway/http_sse/dependencies.py +149 -114
  265. solace_agent_mesh/gateway/http_sse/main.py +68 -450
  266. solace_agent_mesh/gateway/http_sse/repository/__init__.py +19 -1
  267. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +2 -2
  268. solace_agent_mesh/gateway/http_sse/repository/entities/project.py +81 -0
  269. solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +47 -0
  270. solace_agent_mesh/gateway/http_sse/repository/entities/session.py +26 -3
  271. solace_agent_mesh/gateway/http_sse/repository/entities/task.py +7 -0
  272. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +47 -0
  273. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +114 -6
  274. solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +13 -0
  275. solace_agent_mesh/gateway/http_sse/repository/models/project_model.py +51 -0
  276. solace_agent_mesh/gateway/http_sse/repository/models/project_user_model.py +75 -0
  277. solace_agent_mesh/gateway/http_sse/repository/models/prompt_model.py +159 -0
  278. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +8 -2
  279. solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +8 -1
  280. solace_agent_mesh/gateway/http_sse/repository/project_repository.py +172 -0
  281. solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +186 -0
  282. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +177 -11
  283. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +86 -2
  284. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +38 -7
  285. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +256 -58
  286. solace_agent_mesh/gateway/http_sse/routers/auth.py +168 -134
  287. solace_agent_mesh/gateway/http_sse/routers/config.py +302 -8
  288. solace_agent_mesh/gateway/http_sse/routers/dto/project_dto.py +69 -0
  289. solace_agent_mesh/gateway/http_sse/routers/dto/prompt_dto.py +255 -0
  290. solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
  291. solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +14 -1
  292. solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +1 -1
  293. solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +31 -0
  294. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +5 -2
  295. solace_agent_mesh/gateway/http_sse/routers/dto/responses/version_responses.py +31 -0
  296. solace_agent_mesh/gateway/http_sse/routers/feedback.py +133 -2
  297. solace_agent_mesh/gateway/http_sse/routers/people.py +2 -2
  298. solace_agent_mesh/gateway/http_sse/routers/projects.py +768 -0
  299. solace_agent_mesh/gateway/http_sse/routers/prompts.py +1416 -0
  300. solace_agent_mesh/gateway/http_sse/routers/sessions.py +167 -7
  301. solace_agent_mesh/gateway/http_sse/routers/speech.py +355 -0
  302. solace_agent_mesh/gateway/http_sse/routers/sse.py +131 -8
  303. solace_agent_mesh/gateway/http_sse/routers/tasks.py +670 -18
  304. solace_agent_mesh/gateway/http_sse/routers/users.py +1 -1
  305. solace_agent_mesh/gateway/http_sse/routers/version.py +343 -0
  306. solace_agent_mesh/gateway/http_sse/routers/visualization.py +92 -9
  307. solace_agent_mesh/gateway/http_sse/services/audio_service.py +1227 -0
  308. solace_agent_mesh/gateway/http_sse/services/background_task_monitor.py +186 -0
  309. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +1 -1
  310. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +1 -1
  311. solace_agent_mesh/gateway/http_sse/services/project_service.py +930 -0
  312. solace_agent_mesh/gateway/http_sse/services/prompt_builder_assistant.py +303 -0
  313. solace_agent_mesh/gateway/http_sse/services/session_service.py +361 -12
  314. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +354 -4
  315. solace_agent_mesh/gateway/http_sse/session_manager.py +15 -15
  316. solace_agent_mesh/gateway/http_sse/sse_manager.py +286 -166
  317. solace_agent_mesh/gateway/http_sse/utils/artifact_copy_utils.py +370 -0
  318. solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +41 -1
  319. solace_agent_mesh/services/__init__.py +0 -0
  320. solace_agent_mesh/services/platform/__init__.py +29 -0
  321. solace_agent_mesh/services/platform/alembic/env.py +85 -0
  322. solace_agent_mesh/services/platform/alembic/script.py.mako +28 -0
  323. solace_agent_mesh/services/platform/alembic.ini +109 -0
  324. solace_agent_mesh/services/platform/api/__init__.py +3 -0
  325. solace_agent_mesh/services/platform/api/dependencies.py +154 -0
  326. solace_agent_mesh/services/platform/api/main.py +314 -0
  327. solace_agent_mesh/services/platform/api/middleware.py +51 -0
  328. solace_agent_mesh/services/platform/api/routers/__init__.py +33 -0
  329. solace_agent_mesh/services/platform/api/routers/health_router.py +31 -0
  330. solace_agent_mesh/services/platform/app.py +215 -0
  331. solace_agent_mesh/services/platform/component.py +777 -0
  332. solace_agent_mesh/shared/__init__.py +14 -0
  333. solace_agent_mesh/shared/api/__init__.py +42 -0
  334. solace_agent_mesh/shared/auth/__init__.py +26 -0
  335. solace_agent_mesh/shared/auth/dependencies.py +204 -0
  336. solace_agent_mesh/shared/auth/middleware.py +347 -0
  337. solace_agent_mesh/shared/database/__init__.py +20 -0
  338. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/base_repository.py +1 -1
  339. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_exceptions.py +1 -1
  340. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_helpers.py +1 -1
  341. solace_agent_mesh/shared/exceptions/__init__.py +36 -0
  342. solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exception_handlers.py +19 -5
  343. solace_agent_mesh/shared/utils/__init__.py +21 -0
  344. solace_agent_mesh/templates/logging_config_template.yaml +48 -0
  345. solace_agent_mesh/templates/main_orchestrator.yaml +12 -1
  346. solace_agent_mesh/templates/platform.yaml +49 -0
  347. solace_agent_mesh/templates/plugin_readme_template.md +3 -25
  348. solace_agent_mesh/templates/plugin_tool_config_template.yaml +109 -0
  349. solace_agent_mesh/templates/proxy_template.yaml +62 -0
  350. solace_agent_mesh/templates/webui.yaml +148 -6
  351. solace_agent_mesh/tools/web_search/__init__.py +18 -0
  352. solace_agent_mesh/tools/web_search/base.py +84 -0
  353. solace_agent_mesh/tools/web_search/google_search.py +247 -0
  354. solace_agent_mesh/tools/web_search/models.py +99 -0
  355. {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/METADATA +31 -12
  356. solace_agent_mesh-1.13.2.dist-info/RECORD +591 -0
  357. {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/WHEEL +1 -1
  358. solace_agent_mesh/agent/adk/adk_llm.txt +0 -232
  359. solace_agent_mesh/agent/adk/adk_llm_detail.txt +0 -566
  360. solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +0 -171
  361. solace_agent_mesh/agent/adk/models/models_llm.txt +0 -142
  362. solace_agent_mesh/agent/agent_llm.txt +0 -378
  363. solace_agent_mesh/agent/agent_llm_detail.txt +0 -1702
  364. solace_agent_mesh/agent/protocol/protocol_llm.txt +0 -81
  365. solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +0 -92
  366. solace_agent_mesh/agent/sac/sac_llm.txt +0 -189
  367. solace_agent_mesh/agent/sac/sac_llm_detail.txt +0 -200
  368. solace_agent_mesh/agent/testing/testing_llm.txt +0 -57
  369. solace_agent_mesh/agent/testing/testing_llm_detail.txt +0 -68
  370. solace_agent_mesh/agent/tools/tools_llm.txt +0 -263
  371. solace_agent_mesh/agent/tools/tools_llm_detail.txt +0 -274
  372. solace_agent_mesh/agent/utils/utils_llm.txt +0 -138
  373. solace_agent_mesh/agent/utils/utils_llm_detail.txt +0 -149
  374. solace_agent_mesh/assets/docs/assets/js/15ba94aa.932dd2db.js +0 -1
  375. solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +0 -2
  376. solace_agent_mesh/assets/docs/assets/js/240a0364.7eac6021.js +0 -1
  377. solace_agent_mesh/assets/docs/assets/js/2e32b5e0.33f5d75b.js +0 -1
  378. solace_agent_mesh/assets/docs/assets/js/3a6c6137.f5940cfa.js +0 -1
  379. solace_agent_mesh/assets/docs/assets/js/3ac1795d.76654dd9.js +0 -1
  380. solace_agent_mesh/assets/docs/assets/js/3ff0015d.2be20244.js +0 -1
  381. solace_agent_mesh/assets/docs/assets/js/547e15cc.2cbb060a.js +0 -1
  382. solace_agent_mesh/assets/docs/assets/js/55b7b518.f2b1d1ba.js +0 -1
  383. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +0 -1
  384. solace_agent_mesh/assets/docs/assets/js/631738c7.a8b1ef8b.js +0 -1
  385. solace_agent_mesh/assets/docs/assets/js/6a520c9d.ba015d81.js +0 -1
  386. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +0 -1
  387. solace_agent_mesh/assets/docs/assets/js/6d84eae0.4a5fbf39.js +0 -1
  388. solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +0 -1
  389. solace_agent_mesh/assets/docs/assets/js/8024126c.56e59919.js +0 -1
  390. solace_agent_mesh/assets/docs/assets/js/81a99df0.07034dd9.js +0 -1
  391. solace_agent_mesh/assets/docs/assets/js/82fbfb93.139a1a1f.js +0 -1
  392. solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +0 -1
  393. solace_agent_mesh/assets/docs/assets/js/94e8668d.b5ddb7a1.js +0 -1
  394. solace_agent_mesh/assets/docs/assets/js/9bb13469.dd1c9b54.js +0 -1
  395. solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +0 -1
  396. solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e6dd091.js +0 -1
  397. solace_agent_mesh/assets/docs/assets/js/c93cbaa0.eaff365e.js +0 -1
  398. solace_agent_mesh/assets/docs/assets/js/da0b5bad.d08a9466.js +0 -1
  399. solace_agent_mesh/assets/docs/assets/js/dd817ffc.0aa9630a.js +0 -1
  400. solace_agent_mesh/assets/docs/assets/js/dd81e2b8.d590bc9e.js +0 -1
  401. solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +0 -1
  402. solace_agent_mesh/assets/docs/assets/js/e3d9abda.6b9493d0.js +0 -1
  403. solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +0 -1
  404. solace_agent_mesh/assets/docs/assets/js/e92d0134.cf6d6522.js +0 -1
  405. solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +0 -1
  406. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +0 -1
  407. solace_agent_mesh/assets/docs/assets/js/main.b12eac43.js +0 -2
  408. solace_agent_mesh/assets/docs/assets/js/runtime~main.e268214e.js +0 -1
  409. solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +0 -1
  410. solace_agent_mesh/assets/docs/search-doc-1761248203150.json +0 -1
  411. solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +0 -250
  412. solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +0 -365
  413. solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +0 -305
  414. solace_agent_mesh/client/webui/frontend/static/assets/client-CaY59VuC.js +0 -25
  415. solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +0 -342
  416. solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +0 -1
  417. solace_agent_mesh/client/webui/frontend/static/assets/vendor-BEmvJSYz.js +0 -405
  418. solace_agent_mesh/common/a2a/a2a_llm.txt +0 -182
  419. solace_agent_mesh/common/a2a/a2a_llm_detail.txt +0 -193
  420. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +0 -407
  421. solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +0 -736
  422. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +0 -313
  423. solace_agent_mesh/common/common_llm.txt +0 -251
  424. solace_agent_mesh/common/common_llm_detail.txt +0 -2562
  425. solace_agent_mesh/common/middleware/middleware_llm.txt +0 -174
  426. solace_agent_mesh/common/middleware/middleware_llm_detail.txt +0 -185
  427. solace_agent_mesh/common/sac/sac_llm.txt +0 -71
  428. solace_agent_mesh/common/sac/sac_llm_detail.txt +0 -82
  429. solace_agent_mesh/common/sam_events/sam_events_llm.txt +0 -104
  430. solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +0 -115
  431. solace_agent_mesh/common/services/providers/providers_llm.txt +0 -80
  432. solace_agent_mesh/common/services/services_llm.txt +0 -363
  433. solace_agent_mesh/common/services/services_llm_detail.txt +0 -459
  434. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +0 -220
  435. solace_agent_mesh/common/utils/utils_llm.txt +0 -336
  436. solace_agent_mesh/common/utils/utils_llm_detail.txt +0 -572
  437. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +0 -98
  438. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-61038fc6.js +0 -1
  439. solace_agent_mesh/config_portal/frontend/static/client/assets/root-BWvk5-gF.js +0 -10
  440. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +0 -1
  441. solace_agent_mesh/core_a2a/core_a2a_llm.txt +0 -90
  442. solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +0 -101
  443. solace_agent_mesh/gateway/base/base_llm.txt +0 -224
  444. solace_agent_mesh/gateway/base/base_llm_detail.txt +0 -235
  445. solace_agent_mesh/gateway/gateway_llm.txt +0 -373
  446. solace_agent_mesh/gateway/gateway_llm_detail.txt +0 -3885
  447. solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +0 -295
  448. solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +0 -155
  449. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +0 -105
  450. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +0 -299
  451. solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +0 -3278
  452. solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +0 -263
  453. solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +0 -266
  454. solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +0 -340
  455. solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +0 -346
  456. solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +0 -83
  457. solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +0 -107
  458. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +0 -314
  459. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +0 -297
  460. solace_agent_mesh/gateway/http_sse/shared/__init__.py +0 -146
  461. solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +0 -285
  462. solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +0 -47
  463. solace_agent_mesh/llm.txt +0 -228
  464. solace_agent_mesh/llm_detail.txt +0 -2835
  465. solace_agent_mesh/solace_agent_mesh_llm.txt +0 -362
  466. solace_agent_mesh/solace_agent_mesh_llm_detail.txt +0 -8599
  467. solace_agent_mesh/templates/logging_config_template.ini +0 -45
  468. solace_agent_mesh/templates/templates_llm.txt +0 -147
  469. solace_agent_mesh-1.6.1.dist-info/RECORD +0 -525
  470. /solace_agent_mesh/assets/docs/assets/js/{main.b12eac43.js.LICENSE.txt → main.d634009f.js.LICENSE.txt} +0 -0
  471. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/auth_utils.py +0 -0
  472. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/pagination.py +0 -0
  473. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/response_utils.py +0 -0
  474. /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/error_dto.py +0 -0
  475. /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exceptions.py +0 -0
  476. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/enums.py +0 -0
  477. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/timestamp_utils.py +0 -0
  478. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/types.py +0 -0
  479. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/utils.py +0 -0
  480. {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/entry_points.txt +0 -0
  481. {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1227 @@
1
+ """
2
+ Audio Service for Speech-to-Text and Text-to-Speech operations.
3
+ Bridges gateway endpoints and external speech APIs.
4
+ """
5
+
6
+ import asyncio
7
+ import io
8
+ import tempfile
9
+ from typing import Any, AsyncGenerator, Dict, List, Optional
10
+ from fastapi import UploadFile, HTTPException
11
+ from solace_ai_connector.common.log import log
12
+
13
+ from ....agent.tools.audio_tools import ALL_AVAILABLE_VOICES
14
+
15
+
16
+ # AWS Polly Neural Voices (popular subset)
17
+ AWS_POLLY_NEURAL_VOICES = [
18
+ # US English - Neural
19
+ "Joanna",
20
+ "Matthew",
21
+ "Ruth",
22
+ "Stephen",
23
+ "Kendra",
24
+ "Joey",
25
+ "Kimberly",
26
+ "Salli",
27
+ "Ivy",
28
+ # UK English - Neural
29
+ "Amy",
30
+ "Emma",
31
+ "Brian",
32
+ "Arthur",
33
+ # Australian English
34
+ "Olivia",
35
+ # Canadian English
36
+ "Liam",
37
+ # Indian English
38
+ "Kajal",
39
+ "Aria",
40
+ ]
41
+
42
+ # Azure Neural Voices (popular subset with HD voices)
43
+ AZURE_NEURAL_VOICES = [
44
+ # US English - HD Voices
45
+ "en-US-Andrew:DragonHDLatestNeural",
46
+ "en-US-Ava:DragonHDLatestNeural",
47
+ "en-US-Brian:DragonHDLatestNeural",
48
+ "en-US-Emma:DragonHDLatestNeural",
49
+ # US English - Standard
50
+ "en-US-JennyNeural",
51
+ "en-US-GuyNeural",
52
+ "en-US-AriaNeural",
53
+ "en-US-DavisNeural",
54
+ "en-US-JaneNeural",
55
+ "en-US-JasonNeural",
56
+ "en-US-NancyNeural",
57
+ "en-US-TonyNeural",
58
+ "en-US-SaraNeural",
59
+ "en-US-AmberNeural",
60
+ "en-US-AnaNeural",
61
+ "en-US-AndrewNeural",
62
+ "en-US-AshleyNeural",
63
+ "en-US-BrandonNeural",
64
+ "en-US-ChristopherNeural",
65
+ "en-US-CoraNeural",
66
+ "en-US-ElizabethNeural",
67
+ "en-US-EricNeural",
68
+ "en-US-JacobNeural",
69
+ "en-US-MichelleNeural",
70
+ "en-US-MonicaNeural",
71
+ "en-US-RogerNeural",
72
+ "en-US-SteffanNeural",
73
+ # UK English
74
+ "en-GB-LibbyNeural",
75
+ "en-GB-RyanNeural",
76
+ "en-GB-SoniaNeural",
77
+ "en-GB-MiaNeural",
78
+ "en-GB-AlfieNeural",
79
+ "en-GB-BellaNeural",
80
+ "en-GB-ElliotNeural",
81
+ "en-GB-EthanNeural",
82
+ "en-GB-HollieNeural",
83
+ "en-GB-OliverNeural",
84
+ "en-GB-OliviaNeural",
85
+ "en-GB-ThomasNeural",
86
+ # Australian English
87
+ "en-AU-NatashaNeural",
88
+ "en-AU-WilliamNeural",
89
+ "en-AU-AnnetteNeural",
90
+ "en-AU-CarlyNeural",
91
+ "en-AU-DarrenNeural",
92
+ "en-AU-DuncanNeural",
93
+ "en-AU-ElsieNeural",
94
+ "en-AU-FreyaNeural",
95
+ "en-AU-JoanneNeural",
96
+ "en-AU-KenNeural",
97
+ "en-AU-KimNeural",
98
+ "en-AU-NeilNeural",
99
+ "en-AU-TimNeural",
100
+ "en-AU-TinaNeural",
101
+ # Canadian English
102
+ "en-CA-ClaraNeural",
103
+ "en-CA-LiamNeural",
104
+ # Indian English
105
+ "en-IN-NeerjaNeural",
106
+ "en-IN-PrabhatNeural",
107
+ ]
108
+
109
+
110
+ class TranscriptionResult:
111
+ """Result of audio transcription"""
112
+ def __init__(self, text: str, language: str = "en", duration: float = 0.0):
113
+ self.text = text
114
+ self.language = language
115
+ self.duration = duration
116
+
117
+ def to_dict(self) -> Dict[str, Any]:
118
+ return {
119
+ "text": self.text,
120
+ "language": self.language,
121
+ "duration": self.duration
122
+ }
123
+
124
+
125
+ class AudioService:
126
+ """
127
+ Service layer for audio operations.
128
+ Bridges gateway endpoints and agent audio tools.
129
+ """
130
+
131
+ def __init__(self, config: Dict[str, Any]):
132
+ """
133
+ Initialize AudioService with configuration.
134
+
135
+ Args:
136
+ config: Configuration dictionary containing speech settings
137
+ """
138
+ self.config = config
139
+ self.speech_config = config.get("speech", {})
140
+
141
+ async def transcribe_audio_openai(
142
+ self,
143
+ temp_path: str,
144
+ user_id: str,
145
+ session_id: str,
146
+ app_name: str = "webui",
147
+ language: Optional[str] = None
148
+ ) -> TranscriptionResult:
149
+ """
150
+ Transcribe audio using OpenAI Whisper API.
151
+
152
+ Args:
153
+ temp_path: Path to temporary audio file
154
+ user_id: User identifier
155
+ session_id: Session identifier
156
+ app_name: Application name
157
+ language: Optional language code (e.g., "en", "es", "fr")
158
+
159
+ Returns:
160
+ TranscriptionResult with transcribed text
161
+
162
+ Raises:
163
+ HTTPException: If transcription fails
164
+ """
165
+ try:
166
+ import httpx
167
+ import os
168
+
169
+ stt_config = self.speech_config.get("stt", {})
170
+ openai_config = stt_config.get("openai", stt_config) # Fallback to root for backward compat
171
+
172
+ api_url = openai_config.get("url", "https://api.openai.com/v1/audio/transcriptions")
173
+ api_key = openai_config.get("api_key", "")
174
+ model = openai_config.get("model", "whisper-1")
175
+
176
+ if not api_key:
177
+ raise HTTPException(500, "OpenAI STT API key not configured")
178
+
179
+ # Read the audio file
180
+ with open(temp_path, "rb") as audio_file:
181
+ audio_data = audio_file.read()
182
+
183
+ # Prepare multipart form data
184
+ files = {
185
+ "file": (os.path.basename(temp_path), audio_data, "audio/webm"),
186
+ }
187
+ data = {
188
+ "model": model,
189
+ }
190
+
191
+ # Add language parameter if provided (OpenAI expects ISO-639-1 code like "en", "es")
192
+ if language:
193
+ # Convert language code from "en-US" format to "en" format for OpenAI
194
+ lang_code = language.split("-")[0] if "-" in language else language
195
+ data["language"] = lang_code
196
+
197
+ headers = {
198
+ "Authorization": f"Bearer {api_key}"
199
+ }
200
+
201
+ async with httpx.AsyncClient(timeout=60.0) as client:
202
+ response = await client.post(api_url, headers=headers, files=files, data=data)
203
+ response.raise_for_status()
204
+ result = response.json()
205
+
206
+ transcription_text = result.get("text", "").strip()
207
+
208
+ if not transcription_text:
209
+ log.warning("[AudioService] Empty transcription - no speech detected in audio")
210
+ raise HTTPException(400, "No speech detected in audio. Please try speaking louder or longer.")
211
+
212
+ return TranscriptionResult(
213
+ text=transcription_text,
214
+ language="en",
215
+ duration=0.0
216
+ )
217
+
218
+ except HTTPException:
219
+ raise
220
+ except Exception as e:
221
+ log.exception("[AudioService] OpenAI STT error: %s", e)
222
+ raise HTTPException(500, f"OpenAI STT failed: {str(e)}")
223
+
224
+ async def transcribe_audio_azure(
225
+ self,
226
+ temp_path: str,
227
+ user_id: str,
228
+ session_id: str,
229
+ app_name: str = "webui",
230
+ language: Optional[str] = None
231
+ ) -> TranscriptionResult:
232
+ """
233
+ Transcribe audio using Azure Speech SDK.
234
+
235
+ Args:
236
+ temp_path: Path to temporary audio file
237
+ user_id: User identifier
238
+ session_id: Session identifier
239
+ app_name: Application name
240
+ language: Optional language code (e.g., "en-US", "es-ES")
241
+
242
+ Returns:
243
+ TranscriptionResult with transcribed text
244
+
245
+ Raises:
246
+ HTTPException: If transcription fails
247
+ """
248
+ wav_temp_path = None
249
+ try:
250
+ # Import Azure SDK
251
+ try:
252
+ import azure.cognitiveservices.speech as speechsdk
253
+ except ImportError:
254
+ raise HTTPException(
255
+ 500,
256
+ "Azure Speech SDK not installed. Run: pip install azure-cognitiveservices-speech"
257
+ )
258
+
259
+ # Get Azure configuration
260
+ stt_config = self.speech_config.get("stt", {})
261
+ azure_config = stt_config.get("azure", {})
262
+
263
+ api_key = azure_config.get("api_key", "")
264
+ region = azure_config.get("region", "")
265
+ # Use provided language or fall back to config
266
+ final_language = language or azure_config.get("language", "en-US")
267
+
268
+ if not api_key or not region:
269
+ raise HTTPException(
270
+ 500,
271
+ "Azure STT not configured. Please set speech.stt.azure.api_key and region, or speech.tts.azure if using shared config."
272
+ )
273
+
274
+ # Convert audio to WAV format (Azure SDK requires WAV)
275
+ # WebM/OGG/MP4 need to be converted
276
+ import os
277
+ from pydub import AudioSegment
278
+
279
+ file_ext = os.path.splitext(temp_path)[1].lower()
280
+
281
+ if file_ext not in ['.wav']:
282
+ # Convert to WAV
283
+ # Load audio file
284
+ if file_ext == '.webm':
285
+ audio = await asyncio.to_thread(AudioSegment.from_file, temp_path, format="webm")
286
+ elif file_ext == '.ogg':
287
+ audio = await asyncio.to_thread(AudioSegment.from_ogg, temp_path)
288
+ elif file_ext in ['.mp3', '.mp4']:
289
+ audio = await asyncio.to_thread(AudioSegment.from_file, temp_path)
290
+ else:
291
+ audio = await asyncio.to_thread(AudioSegment.from_file, temp_path)
292
+
293
+ # Create temp WAV file
294
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wav_temp:
295
+ wav_temp_path = wav_temp.name
296
+
297
+ # Export as WAV (16kHz, mono, 16-bit PCM - Azure's preferred format)
298
+ await asyncio.to_thread(
299
+ audio.set_frame_rate(16000).set_channels(1).export,
300
+ wav_temp_path,
301
+ format="wav"
302
+ )
303
+
304
+ audio_path = wav_temp_path
305
+ else:
306
+ audio_path = temp_path
307
+
308
+ # Create speech config
309
+ speech_config = speechsdk.SpeechConfig(
310
+ subscription=api_key,
311
+ region=region
312
+ )
313
+ speech_config.speech_recognition_language = final_language
314
+
315
+ # Create audio config from file
316
+ audio_config = speechsdk.AudioConfig(filename=audio_path)
317
+
318
+ # Create speech recognizer
319
+ recognizer = speechsdk.SpeechRecognizer(
320
+ speech_config=speech_config,
321
+ audio_config=audio_config
322
+ )
323
+
324
+ transcribed_texts = []
325
+ done = asyncio.Event()
326
+
327
+ def recognized_handler(evt):
328
+ if evt.result.reason == speechsdk.ResultReason.RecognizedSpeech:
329
+ text = evt.result.text.strip()
330
+ if text:
331
+ transcribed_texts.append(text)
332
+
333
+ def canceled_handler(evt):
334
+ if evt.cancellation_details.error_details:
335
+ log.error(f"[AudioService] Recognition error: {evt.cancellation_details.error_details}")
336
+ done.set()
337
+
338
+ def stopped_handler(evt):
339
+ done.set()
340
+
341
+ # Connect callbacks
342
+ recognizer.recognized.connect(recognized_handler)
343
+ recognizer.canceled.connect(canceled_handler)
344
+ recognizer.session_stopped.connect(stopped_handler)
345
+
346
+ # Start continuous recognition
347
+ await asyncio.to_thread(recognizer.start_continuous_recognition)
348
+
349
+ # Wait for recognition to complete
350
+ await done.wait()
351
+
352
+ # Stop recognition
353
+ await asyncio.to_thread(recognizer.stop_continuous_recognition)
354
+
355
+ # Combine all recognized text
356
+ if not transcribed_texts:
357
+ log.warning("[AudioService] No speech could be recognized")
358
+ raise HTTPException(400, "No speech detected in audio. Please try speaking louder or longer.")
359
+
360
+ full_transcription = " ".join(transcribed_texts).strip()
361
+
362
+ return TranscriptionResult(
363
+ text=full_transcription,
364
+ language=final_language,
365
+ duration=0.0 # Duration not available in continuous mode
366
+ )
367
+
368
+ except HTTPException:
369
+ raise
370
+ except Exception as e:
371
+ log.exception("[AudioService] Azure STT error: %s", e)
372
+ raise HTTPException(500, f"Azure STT failed: {str(e)}")
373
+ finally:
374
+ # Clean up WAV temp file if created
375
+ if wav_temp_path:
376
+ try:
377
+ import os
378
+ os.unlink(wav_temp_path)
379
+ except Exception as e:
380
+ log.warning("[AudioService] Failed to delete WAV temp file: %s", e)
381
+
382
+ async def transcribe_audio(
383
+ self,
384
+ audio_file: UploadFile,
385
+ user_id: str,
386
+ session_id: str,
387
+ app_name: str = "webui",
388
+ provider: Optional[str] = None,
389
+ language: Optional[str] = None
390
+ ) -> TranscriptionResult:
391
+ """
392
+ Transcribe audio file to text using configured STT service.
393
+ Routes to appropriate provider (OpenAI, Azure, etc.).
394
+
395
+ Args:
396
+ audio_file: Uploaded audio file
397
+ user_id: User identifier
398
+ session_id: Session identifier
399
+ app_name: Application name
400
+ provider: Optional provider override (openai, azure)
401
+ language: Optional language code (e.g., "en-US", "es-ES")
402
+
403
+ Returns:
404
+ TranscriptionResult with transcribed text
405
+
406
+ Raises:
407
+ HTTPException: If transcription fails
408
+ """
409
+
410
+ try:
411
+ # Validate file
412
+ if not audio_file.filename:
413
+ raise HTTPException(400, "No filename provided")
414
+
415
+ # Check file size before reading (max 25MB) to prevent OOM
416
+ if audio_file.size and audio_file.size > 25 * 1024 * 1024:
417
+ raise HTTPException(413, "Audio file too large (max 25MB)")
418
+
419
+ # Read content after size check
420
+ content = await audio_file.read()
421
+
422
+ # Double-check size after reading (in case size wasn't available before)
423
+ if len(content) > 25 * 1024 * 1024:
424
+ raise HTTPException(413, "Audio file too large (max 25MB)")
425
+
426
+ # Save to temporary file
427
+ with tempfile.NamedTemporaryFile(
428
+ suffix=self._get_file_extension(audio_file.filename),
429
+ delete=False
430
+ ) as temp_file:
431
+ temp_file.write(content)
432
+ temp_path = temp_file.name
433
+
434
+ try:
435
+ # Get STT configuration
436
+ stt_config = self.speech_config.get("stt", {})
437
+ if not stt_config:
438
+ raise HTTPException(
439
+ 500,
440
+ "STT not configured. Please add speech.stt configuration."
441
+ )
442
+
443
+ # Determine provider - use request provider if provided, otherwise use config
444
+ final_provider = provider or stt_config.get("provider", "openai")
445
+
446
+ log.info(
447
+ "[AudioService] Transcribing audio for user=%s, session=%s, provider=%s, language=%s",
448
+ user_id, session_id, final_provider, language
449
+ )
450
+
451
+ # Route to appropriate provider
452
+ if final_provider == "azure":
453
+ return await self.transcribe_audio_azure(
454
+ temp_path, user_id, session_id, app_name, language
455
+ )
456
+ elif final_provider == "openai":
457
+ return await self.transcribe_audio_openai(
458
+ temp_path, user_id, session_id, app_name, language
459
+ )
460
+ else:
461
+ raise HTTPException(500, f"Unknown STT provider: {final_provider}")
462
+
463
+ finally:
464
+ # Clean up temp file
465
+ import os
466
+ try:
467
+ os.unlink(temp_path)
468
+ except Exception as e:
469
+ log.warning("[AudioService] Failed to delete temp file: %s", e)
470
+
471
+ except HTTPException:
472
+ raise
473
+ except Exception as e:
474
+ log.exception("[AudioService] Transcription error: %s", e)
475
+ raise HTTPException(500, f"Transcription failed: {str(e)}")
476
+ def _generate_azure_ssml(self, text: str, voice: str) -> str:
477
+ """
478
+ Generate SSML for Azure TTS with proper XML escaping.
479
+ Handles both standard and HD voice formats.
480
+
481
+ Args:
482
+ text: Text to convert to speech
483
+ voice: Azure voice name (e.g., "en-US-JennyNeural" or "en-US-Ava:DragonHDLatestNeural")
484
+
485
+ Returns:
486
+ SSML string
487
+ """
488
+ # Escape XML special characters in text
489
+ escaped_text = (text
490
+ .replace("&", "&")
491
+ .replace("<", "&lt;")
492
+ .replace(">", "&gt;")
493
+ .replace('"', "&quot;")
494
+ .replace("'", "&apos;"))
495
+
496
+ # Escape XML special characters in voice name to prevent injection
497
+ escaped_voice = (voice
498
+ .replace("&", "&amp;")
499
+ .replace("<", "&lt;")
500
+ .replace(">", "&gt;")
501
+ .replace('"', "&quot;")
502
+ .replace("'", "&apos;"))
503
+
504
+ # For HD voices, the format is "locale-Name:DragonHDLatestNeural"
505
+ # Azure expects just the voice name in SSML, not the :DragonHDLatestNeural suffix
506
+ # The HD quality is specified via the voice name itself
507
+
508
+ return f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
509
+ <voice name="{escaped_voice}">
510
+ <prosody rate="medium" pitch="default">
511
+ {escaped_text}
512
+ </prosody>
513
+ </voice>
514
+ </speak>"""
515
+
516
+ async def generate_speech_azure(
517
+ self,
518
+ text: str,
519
+ voice: Optional[str],
520
+ user_id: str,
521
+ session_id: str,
522
+ app_name: str = "webui",
523
+ message_id: Optional[str] = None
524
+ ) -> bytes:
525
+ """
526
+ Generate speech using Azure Neural Voices.
527
+
528
+ Args:
529
+ text: Text to convert to speech
530
+ voice: Azure voice name (e.g., "en-US-JennyNeural")
531
+ user_id: User identifier
532
+ session_id: Session identifier
533
+ app_name: Application name
534
+ message_id: Optional message ID for caching
535
+
536
+ Returns:
537
+ Audio data as bytes (MP3 format)
538
+
539
+ Raises:
540
+ HTTPException: If generation fails
541
+ """
542
+
543
+ try:
544
+
545
+ # Import Azure SDK
546
+ try:
547
+ import azure.cognitiveservices.speech as speechsdk
548
+ except ImportError as e:
549
+ log.error(f"[AudioService] Azure SDK not installed: {e}")
550
+ raise HTTPException(
551
+ 500,
552
+ "Azure Speech SDK not installed. Run: pip install azure-cognitiveservices-speech"
553
+ )
554
+
555
+ # Get Azure configuration
556
+ tts_config = self.speech_config.get("tts", {})
557
+ azure_config = tts_config.get("azure", {})
558
+
559
+ api_key = azure_config.get("api_key", "")
560
+ region = azure_config.get("region", "")
561
+
562
+ if not api_key or not region:
563
+ log.error("[AudioService] Azure TTS missing api_key or region")
564
+ raise HTTPException(
565
+ 500,
566
+ "Azure TTS not configured. Please set speech.tts.azure.api_key and region."
567
+ )
568
+
569
+ # Set voice - use default if provided voice is not an Azure voice
570
+ requested_voice = voice or azure_config.get("default_voice", "en-US-JennyNeural")
571
+
572
+ # Check if requested voice is an Azure voice
573
+ # Azure voices contain "Neural" or "DragonHD" and have locale prefix (e.g., "en-US-")
574
+ is_azure_voice = (
575
+ ("Neural" in requested_voice or "DragonHD" in requested_voice)
576
+ and ("-" in requested_voice)
577
+ )
578
+
579
+ if is_azure_voice:
580
+ final_voice = requested_voice
581
+ else:
582
+ # Not an Azure voice, use default
583
+ final_voice = azure_config.get("default_voice", "en-US-JennyNeural")
584
+
585
+ # Create speech config
586
+ speech_config = speechsdk.SpeechConfig(
587
+ subscription=api_key,
588
+ region=region
589
+ )
590
+
591
+ # Set output format to MP3
592
+ speech_config.set_speech_synthesis_output_format(
593
+ speechsdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3
594
+ )
595
+
596
+ # Generate SSML
597
+ ssml = self._generate_azure_ssml(text, final_voice)
598
+
599
+ log.debug(f"[AudioService] Generated SSML: {ssml[:200]}...")
600
+
601
+ # Create synthesizer (None for in-memory output)
602
+ synthesizer = speechsdk.SpeechSynthesizer(
603
+ speech_config=speech_config,
604
+ audio_config=None
605
+ )
606
+
607
+ # Synthesize speech (run in thread pool to avoid blocking)
608
+ result = await asyncio.to_thread(
609
+ lambda: synthesizer.speak_ssml_async(ssml).get()
610
+ )
611
+
612
+ # Check result
613
+ if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
614
+ audio_data = result.audio_data
615
+ return audio_data
616
+ elif result.reason == speechsdk.ResultReason.Canceled:
617
+ cancellation = result.cancellation_details
618
+ error_msg = f"Azure TTS canceled: {cancellation.reason}"
619
+ if cancellation.error_details:
620
+ error_msg += f" - {cancellation.error_details}"
621
+ log.error(f"[AudioService] {error_msg}")
622
+ raise HTTPException(500, error_msg)
623
+ else:
624
+ raise HTTPException(500, f"Azure TTS failed with reason: {result.reason}")
625
+
626
+ except HTTPException:
627
+ raise
628
+ except Exception as e:
629
+ log.exception("[AudioService] Azure TTS generation error: %s", e)
630
+ raise HTTPException(500, f"Azure TTS generation failed: {str(e)}")
631
+
632
+ async def generate_speech_gemini(
633
+ self,
634
+ text: str,
635
+ voice: Optional[str],
636
+ user_id: str,
637
+ session_id: str,
638
+ app_name: str = "webui",
639
+ message_id: Optional[str] = None
640
+ ) -> bytes:
641
+ """
642
+ Generate speech using Gemini TTS (original implementation).
643
+
644
+ Args:
645
+ text: Text to convert to speech
646
+ voice: Voice name to use
647
+ user_id: User identifier
648
+ session_id: Session identifier
649
+ app_name: Application name
650
+ message_id: Optional message ID for caching
651
+
652
+ Returns:
653
+ Audio data as bytes (MP3 format)
654
+
655
+ Raises:
656
+ HTTPException: If generation fails
657
+ """
658
+
659
+ try:
660
+
661
+ # Get TTS configuration
662
+ tts_config = self.speech_config.get("tts", {})
663
+ gemini_config = tts_config.get("gemini", tts_config)
664
+
665
+ # Use direct Gemini API call
666
+ from google import genai
667
+ from google.genai import types as adk_types
668
+ import wave
669
+ from pydub import AudioSegment
670
+ import os
671
+
672
+ api_key = gemini_config.get("api_key", "")
673
+ model = gemini_config.get("model", "gemini-2.5-flash-preview-tts")
674
+ final_voice = voice or gemini_config.get("default_voice", "Kore")
675
+ # Gemini requires lowercase voice names
676
+ final_voice = final_voice.lower()
677
+ language = gemini_config.get("language", "en-US")
678
+
679
+ if not api_key:
680
+ log.error("[AudioService] No Gemini API key found")
681
+ raise HTTPException(500, "Gemini TTS API key not configured")
682
+
683
+
684
+ # Create Gemini client
685
+ client = genai.Client(api_key=api_key)
686
+
687
+ # Create voice config
688
+ voice_config = adk_types.VoiceConfig(
689
+ prebuilt_voice_config=adk_types.PrebuiltVoiceConfig(voice_name=final_voice)
690
+ )
691
+ speech_config = adk_types.SpeechConfig(voice_config=voice_config)
692
+
693
+ # Generate audio
694
+ config = adk_types.GenerateContentConfig(
695
+ response_modalities=["AUDIO"],
696
+ speech_config=speech_config,
697
+ )
698
+
699
+ # Retry logic for transient API failures
700
+ max_retries = 2
701
+ last_error = None
702
+
703
+ for attempt in range(max_retries):
704
+ try:
705
+ response = await asyncio.to_thread(
706
+ client.models.generate_content,
707
+ model=model,
708
+ contents=f"Say in a clear voice: {text}",
709
+ config=config
710
+ )
711
+
712
+ # Validate response structure
713
+ if not response:
714
+ raise ValueError("Gemini API returned empty response")
715
+
716
+ if not response.candidates or len(response.candidates) == 0:
717
+ raise ValueError("Gemini API returned no candidates")
718
+
719
+ candidate = response.candidates[0]
720
+
721
+ # Log candidate details for debugging
722
+ log.debug(f"[AudioService] Candidate finish_reason: {getattr(candidate, 'finish_reason', 'unknown')}")
723
+ log.debug(f"[AudioService] Candidate has content: {candidate.content is not None}")
724
+
725
+ if not candidate.content:
726
+ # Check if there's a finish_reason that explains why
727
+ finish_reason = getattr(candidate, 'finish_reason', None)
728
+ if finish_reason:
729
+ raise ValueError(f"Gemini API returned candidate with no content (finish_reason: {finish_reason})")
730
+ raise ValueError("Gemini API returned candidate with no content")
731
+
732
+ # Success - break retry loop
733
+ break
734
+
735
+ except ValueError as e:
736
+ last_error = e
737
+ if attempt < max_retries - 1:
738
+ log.warning(f"[AudioService] TTS attempt {attempt + 1} failed: {e}, retrying...")
739
+ await asyncio.sleep(0.5) # Brief delay before retry
740
+ else:
741
+ log.error(f"[AudioService] TTS failed after {max_retries} attempts: {e}")
742
+ raise HTTPException(500, f"TTS generation failed after {max_retries} attempts: {str(e)}")
743
+
744
+ if not candidate.content.parts or len(candidate.content.parts) == 0:
745
+ raise HTTPException(500, "Gemini API returned no audio parts")
746
+
747
+ part = candidate.content.parts[0]
748
+ if not hasattr(part, 'inline_data') or not part.inline_data:
749
+ raise HTTPException(500, "Gemini API returned part with no inline_data")
750
+
751
+ wav_data = part.inline_data.data
752
+ if not wav_data:
753
+ raise HTTPException(500, "No audio data received from Gemini API")
754
+
755
+ # Convert WAV to MP3
756
+ def create_wav_file(filename: str, pcm_data: bytes):
757
+ with wave.open(filename, "wb") as wf:
758
+ wf.setnchannels(1)
759
+ wf.setsampwidth(2)
760
+ wf.setframerate(24000)
761
+ wf.writeframes(pcm_data)
762
+
763
+ wav_temp_path = None
764
+ mp3_temp_path = None
765
+
766
+ try:
767
+ # Create temp WAV file
768
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wav_temp:
769
+ wav_temp_path = wav_temp.name
770
+
771
+ await asyncio.to_thread(create_wav_file, wav_temp_path, wav_data)
772
+
773
+ # Create temp MP3 file
774
+ with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as mp3_temp:
775
+ mp3_temp_path = mp3_temp.name
776
+
777
+ # Convert to MP3
778
+ audio = await asyncio.to_thread(AudioSegment.from_wav, wav_temp_path)
779
+ await asyncio.to_thread(audio.export, mp3_temp_path, format="mp3")
780
+
781
+ # Read MP3 data
782
+ with open(mp3_temp_path, "rb") as mp3_file:
783
+ mp3_data = mp3_file.read()
784
+
785
+ return mp3_data
786
+
787
+ finally:
788
+ # Clean up temp files
789
+ if wav_temp_path:
790
+ try:
791
+ os.remove(wav_temp_path)
792
+ except:
793
+ pass
794
+ if mp3_temp_path:
795
+ try:
796
+ os.remove(mp3_temp_path)
797
+ except:
798
+ pass
799
+
800
+ except HTTPException:
801
+ raise
802
+ except Exception as e:
803
+ log.exception("[AudioService] Gemini TTS generation error: %s", e)
804
+ raise HTTPException(500, f"Gemini TTS generation failed: {str(e)}")
805
+
806
+
807
+ async def generate_speech_polly(
808
+ self,
809
+ text: str,
810
+ voice: Optional[str],
811
+ user_id: str,
812
+ session_id: str,
813
+ app_name: str = "webui",
814
+ message_id: Optional[str] = None
815
+ ) -> bytes:
816
+ """
817
+ Generate speech using AWS Polly.
818
+
819
+ Args:
820
+ text: Text to convert to speech
821
+ voice: Polly voice ID (e.g., "Joanna", "Matthew")
822
+ user_id: User identifier
823
+ session_id: Session identifier
824
+ app_name: Application name
825
+ message_id: Optional message ID for caching
826
+
827
+ Returns:
828
+ Audio data as bytes (MP3 format)
829
+
830
+ Raises:
831
+ HTTPException: If generation fails
832
+ """
833
+
834
+ try:
835
+
836
+ # Import boto3
837
+ try:
838
+ import boto3
839
+ from botocore.exceptions import ClientError as BotoClientError, BotoCoreError
840
+ except ImportError as e:
841
+ log.error(f"[AudioService] boto3 not installed: {e}")
842
+ raise HTTPException(
843
+ 500,
844
+ "AWS boto3 SDK not installed. Run: pip install boto3"
845
+ )
846
+
847
+ # Get Polly configuration
848
+ tts_config = self.speech_config.get("tts", {})
849
+ polly_config = tts_config.get("polly", {})
850
+
851
+ aws_access_key_id = polly_config.get("aws_access_key_id", "")
852
+ aws_secret_access_key = polly_config.get("aws_secret_access_key", "")
853
+ region = polly_config.get("region", "us-east-1")
854
+ engine = polly_config.get("engine", "neural") # 'neural' or 'standard'
855
+
856
+ if not aws_access_key_id or not aws_secret_access_key:
857
+ log.error("[AudioService] AWS Polly missing credentials")
858
+ raise HTTPException(
859
+ 500,
860
+ "AWS Polly not configured. Please set speech.tts.polly.aws_access_key_id and aws_secret_access_key."
861
+ )
862
+
863
+ # Set voice - use default if provided voice is not a Polly voice
864
+ requested_voice = voice or polly_config.get("default_voice", "Joanna")
865
+
866
+ # Polly voices are simple names (e.g., "Joanna", "Matthew")
867
+ # Validate it's a reasonable voice name (alphanumeric)
868
+ is_polly_voice = requested_voice.isalpha()
869
+
870
+ if is_polly_voice:
871
+ final_voice = requested_voice
872
+ else:
873
+ # Not a valid Polly voice, use default
874
+ final_voice = polly_config.get("default_voice", "Joanna")
875
+ log.warning(f"[AudioService] Invalid Polly voice '{requested_voice}', using default '{final_voice}'")
876
+
877
+ # Create Polly client
878
+ try:
879
+ polly_client = boto3.client(
880
+ 'polly',
881
+ aws_access_key_id=aws_access_key_id,
882
+ aws_secret_access_key=aws_secret_access_key,
883
+ region_name=region
884
+ )
885
+ except Exception as e:
886
+ log.error(f"[AudioService] Failed to create Polly client: {e}")
887
+ raise HTTPException(500, f"Failed to create AWS Polly client: {str(e)}")
888
+
889
+ # Synthesize speech
890
+ log.debug(f"[AudioService] Synthesizing with voice={final_voice}, engine={engine}, text_len={len(text)}")
891
+
892
+ try:
893
+ response = await asyncio.to_thread(
894
+ polly_client.synthesize_speech,
895
+ Text=text,
896
+ OutputFormat='mp3',
897
+ VoiceId=final_voice,
898
+ Engine=engine
899
+ )
900
+
901
+ # Read audio stream
902
+ if 'AudioStream' in response:
903
+ audio_data = response['AudioStream'].read()
904
+ return audio_data
905
+ else:
906
+ raise HTTPException(500, "No audio stream in Polly response")
907
+
908
+ except BotoClientError as e:
909
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
910
+ error_msg = e.response.get('Error', {}).get('Message', str(e))
911
+ log.error(f"[AudioService] Polly API error: {error_code} - {error_msg}")
912
+
913
+ if error_code == 'InvalidParameterValue':
914
+ raise HTTPException(400, f"Invalid Polly parameter: {error_msg}")
915
+ elif error_code in ['AccessDeniedException', 'UnauthorizedException']:
916
+ raise HTTPException(403, f"AWS Polly authentication failed: {error_msg}")
917
+ else:
918
+ raise HTTPException(500, f"AWS Polly error ({error_code}): {error_msg}")
919
+
920
+ except BotoCoreError as e:
921
+ log.error(f"[AudioService] Boto core error: {e}")
922
+ raise HTTPException(500, f"AWS SDK error: {str(e)}")
923
+
924
+ except HTTPException:
925
+ raise
926
+ except Exception as e:
927
+ log.exception("[AudioService] AWS Polly TTS generation error: %s", e)
928
+ raise HTTPException(500, f"AWS Polly TTS generation failed: {str(e)}")
929
+
930
+ async def generate_speech(
931
+ self,
932
+ text: str,
933
+ voice: Optional[str],
934
+ user_id: str,
935
+ session_id: str,
936
+ app_name: str = "webui",
937
+ message_id: Optional[str] = None,
938
+ provider: Optional[str] = None # NEW: Allow provider override from request
939
+ ) -> bytes:
940
+ """
941
+ Generate speech audio from text using configured TTS service.
942
+ Routes to appropriate provider (Azure, Gemini, etc.).
943
+
944
+ Args:
945
+ text: Text to convert to speech
946
+ voice: Voice name to use
947
+ user_id: User identifier
948
+ session_id: Session identifier
949
+ app_name: Application name
950
+ message_id: Optional message ID for caching
951
+ provider: Optional provider override (azure, gemini)
952
+
953
+ Returns:
954
+ Audio data as bytes (MP3 format)
955
+
956
+ Raises:
957
+ HTTPException: If generation fails
958
+ """
959
+ log.info(
960
+ "[AudioService] Generating speech for user=%s, session=%s, voice=%s, text_len=%d, provider=%s",
961
+ user_id, session_id, voice, len(text), provider
962
+ )
963
+
964
+ try:
965
+ tts_config = self.speech_config.get("tts", {}) if self.speech_config else {}
966
+
967
+ if not tts_config:
968
+ log.error("[AudioService] TTS not configured in speech.tts")
969
+ log.error(f"[AudioService] Available config keys: {list(self.config.keys())}")
970
+ log.error(f"[AudioService] Speech config value: {self.speech_config}")
971
+ raise HTTPException(
972
+ 500,
973
+ "TTS not configured. Please add speech.tts configuration to gateway YAML under app_config."
974
+ )
975
+
976
+ # Determine provider - use request provider if provided, otherwise use config
977
+ final_provider = provider or tts_config.get("provider", "gemini")
978
+
979
+ # Route to appropriate provider
980
+ if final_provider == "azure":
981
+ return await self.generate_speech_azure(
982
+ text, voice, user_id, session_id, app_name, message_id
983
+ )
984
+ elif final_provider == "gemini":
985
+ return await self.generate_speech_gemini(
986
+ text, voice, user_id, session_id, app_name, message_id
987
+ )
988
+ elif final_provider == "polly":
989
+ return await self.generate_speech_polly(
990
+ text, voice, user_id, session_id, app_name, message_id
991
+ )
992
+ else:
993
+ raise HTTPException(500, f"Unknown TTS provider: {final_provider}")
994
+
995
+ except HTTPException:
996
+ raise
997
+ except Exception as e:
998
+ log.exception("[AudioService] TTS generation error: %s", e)
999
+ raise HTTPException(500, f"TTS generation failed: {str(e)}")
1000
+
1001
+ async def stream_speech(
1002
+ self,
1003
+ text: str,
1004
+ voice: Optional[str],
1005
+ user_id: str,
1006
+ session_id: str,
1007
+ app_name: str = "webui",
1008
+ provider: Optional[str] = None
1009
+ ) -> AsyncGenerator[bytes, None]:
1010
+ """
1011
+ Stream speech audio for long text with intelligent sentence-based chunking.
1012
+ Generates and yields audio chunks immediately for reduced latency.
1013
+
1014
+ Args:
1015
+ text: Text to convert to speech
1016
+ voice: Voice name to use
1017
+ user_id: User identifier
1018
+ session_id: Session identifier
1019
+ app_name: Application name
1020
+ provider: Optional provider override (azure, gemini, polly)
1021
+
1022
+ Yields:
1023
+ Audio data chunks as bytes
1024
+ """
1025
+
1026
+ # Split text into sentence-based chunks for more natural audio boundaries
1027
+ import re
1028
+
1029
+ # Split on sentence boundaries (., !, ?, newlines)
1030
+ sentences = re.split(r'(?<=[.!?\n])\s+', text)
1031
+
1032
+ # Group sentences into smaller chunks for faster initial playback
1033
+ MAX_CHUNK_SIZE = 300 # Reduced from 500 for faster first chunk
1034
+ chunks = []
1035
+ current_chunk = ""
1036
+
1037
+ for sentence in sentences:
1038
+ if len(current_chunk) + len(sentence) > MAX_CHUNK_SIZE and current_chunk:
1039
+ chunks.append(current_chunk.strip())
1040
+ current_chunk = sentence
1041
+ else:
1042
+ current_chunk += " " + sentence if current_chunk else sentence
1043
+
1044
+ if current_chunk:
1045
+ chunks.append(current_chunk.strip())
1046
+
1047
+
1048
+ # Generate and yield chunks immediately (no buffering)
1049
+ for i, chunk in enumerate(chunks):
1050
+ log.debug("[AudioService] Generating chunk %d/%d (len=%d)", i+1, len(chunks), len(chunk))
1051
+
1052
+ try:
1053
+ audio_data = await self.generate_speech(
1054
+ text=chunk,
1055
+ voice=voice,
1056
+ user_id=user_id,
1057
+ session_id=session_id,
1058
+ app_name=app_name,
1059
+ message_id=f"chunk_{i}",
1060
+ provider=provider # Pass provider to generate_speech
1061
+ )
1062
+
1063
+ if audio_data:
1064
+ log.debug("[AudioService] Yielding chunk %d (%d bytes)", i+1, len(audio_data))
1065
+ yield audio_data
1066
+
1067
+ except Exception as e:
1068
+ log.error("[AudioService] Error generating chunk %d: %s", i, e)
1069
+ # Continue with next chunk instead of failing completely
1070
+ continue
1071
+
1072
+ async def get_available_voices(self, provider: Optional[str] = None) -> List[str]:
1073
+ """
1074
+ Get list of available TTS voices from configuration.
1075
+
1076
+ Args:
1077
+ provider: Optional provider filter (azure, gemini)
1078
+
1079
+ Returns:
1080
+ List of voice names
1081
+ """
1082
+ tts_config = self.speech_config.get("tts", {})
1083
+ # Use provided provider or fall back to config
1084
+ final_provider = provider or tts_config.get("provider", "gemini")
1085
+
1086
+ if final_provider == "azure":
1087
+ azure_config = tts_config.get("azure", {})
1088
+ voices = azure_config.get("voices", AZURE_NEURAL_VOICES)
1089
+ elif final_provider == "gemini":
1090
+ gemini_config = tts_config.get("gemini", tts_config) # Fallback to root for backward compat
1091
+ voices = gemini_config.get("voices", ALL_AVAILABLE_VOICES)
1092
+ elif final_provider == "polly":
1093
+ polly_config = tts_config.get("polly", {})
1094
+ voices = polly_config.get("voices", AWS_POLLY_NEURAL_VOICES)
1095
+ else:
1096
+ voices = []
1097
+
1098
+ log.debug("[AudioService] Available voices for provider %s: %d", final_provider, len(voices))
1099
+ return voices
1100
+
1101
+ def _is_valid_api_key(self, value: Any) -> bool:
1102
+ """
1103
+ Check if a value is a valid API key (non-empty string that's not an unresolved env var).
1104
+
1105
+ Args:
1106
+ value: The value to check
1107
+
1108
+ Returns:
1109
+ True if the value appears to be a valid API key
1110
+ """
1111
+ if not value:
1112
+ return False
1113
+ if not isinstance(value, str):
1114
+ return False
1115
+ # Check if it's an unresolved environment variable placeholder
1116
+ if value.startswith("${") or value == "":
1117
+ return False
1118
+ return True
1119
+
1120
+ def get_speech_config(self) -> Dict[str, Any]:
1121
+ """
1122
+ Get speech configuration for frontend initialization.
1123
+
1124
+ Returns:
1125
+ Configuration dictionary
1126
+ """
1127
+ stt_config = self.speech_config.get("stt", {})
1128
+ tts_config = self.speech_config.get("tts", {})
1129
+ speech_tab = self.speech_config.get("speechTab", {})
1130
+
1131
+ # Check each STT provider individually
1132
+ stt_openai_valid = False
1133
+ stt_azure_valid = False
1134
+ if stt_config:
1135
+ # Check OpenAI - can be nested under 'openai' or at root level for backward compat
1136
+ openai_config = stt_config.get("openai", {})
1137
+ openai_api_key = openai_config.get("api_key") or stt_config.get("api_key")
1138
+ stt_openai_valid = self._is_valid_api_key(openai_api_key)
1139
+
1140
+ azure_config = stt_config.get("azure", {})
1141
+ stt_azure_valid = (
1142
+ self._is_valid_api_key(azure_config.get("api_key")) and
1143
+ self._is_valid_api_key(azure_config.get("region"))
1144
+ )
1145
+
1146
+ stt_configured = stt_openai_valid or stt_azure_valid
1147
+
1148
+ # Check each TTS provider individually
1149
+ tts_gemini_valid = False
1150
+ tts_azure_valid = False
1151
+ tts_polly_valid = False
1152
+ if tts_config:
1153
+ # Check Gemini - can be nested under 'gemini' or at root level for backward compat
1154
+ gemini_nested = tts_config.get("gemini", {})
1155
+ gemini_api_key = gemini_nested.get("api_key") or tts_config.get("api_key")
1156
+ tts_gemini_valid = self._is_valid_api_key(gemini_api_key)
1157
+
1158
+ azure_config = tts_config.get("azure", {})
1159
+ tts_azure_valid = (
1160
+ self._is_valid_api_key(azure_config.get("api_key")) and
1161
+ self._is_valid_api_key(azure_config.get("region"))
1162
+ )
1163
+
1164
+ polly_config = tts_config.get("polly", {})
1165
+ tts_polly_valid = (
1166
+ self._is_valid_api_key(polly_config.get("aws_access_key_id")) and
1167
+ self._is_valid_api_key(polly_config.get("aws_secret_access_key"))
1168
+ )
1169
+
1170
+ tts_configured = tts_gemini_valid or tts_azure_valid or tts_polly_valid
1171
+
1172
+ config = {
1173
+ "sttExternal": stt_configured,
1174
+ "ttsExternal": tts_configured,
1175
+ # Per-provider configuration status
1176
+ "sttProviders": {
1177
+ "openai": stt_openai_valid,
1178
+ "azure": stt_azure_valid,
1179
+ },
1180
+ "ttsProviders": {
1181
+ "gemini": tts_gemini_valid,
1182
+ "azure": tts_azure_valid,
1183
+ "polly": tts_polly_valid,
1184
+ },
1185
+ }
1186
+
1187
+ # Add speech tab settings if configured
1188
+ if speech_tab:
1189
+ config.update({
1190
+ "advancedMode": speech_tab.get("advancedMode", False),
1191
+ })
1192
+
1193
+ # STT settings
1194
+ stt_settings = speech_tab.get("speechToText", {})
1195
+ if stt_settings:
1196
+ config.update({
1197
+ "speechToText": stt_settings.get("speechToText", True),
1198
+ "engineSTT": stt_settings.get("engineSTT", "browser"),
1199
+ "languageSTT": stt_settings.get("languageSTT", "en-US"),
1200
+ "autoSendText": stt_settings.get("autoSendText", -1),
1201
+ "autoTranscribeAudio": stt_settings.get("autoTranscribeAudio", True),
1202
+ "decibelValue": stt_settings.get("decibelValue", -45),
1203
+ })
1204
+
1205
+ # TTS settings
1206
+ tts_settings = speech_tab.get("textToSpeech", {})
1207
+ if tts_settings:
1208
+ config.update({
1209
+ "textToSpeech": tts_settings.get("textToSpeech", True),
1210
+ "engineTTS": tts_settings.get("engineTTS", "browser"),
1211
+ "voice": tts_settings.get("voice", tts_config.get("default_voice", "Kore")),
1212
+ "playbackRate": tts_settings.get("playbackRate", 1.0),
1213
+ "automaticPlayback": tts_settings.get("automaticPlayback", False),
1214
+ "cacheTTS": tts_settings.get("cacheTTS", True),
1215
+ "cloudBrowserVoices": tts_settings.get("cloudBrowserVoices", False),
1216
+ })
1217
+
1218
+ # Conversation mode
1219
+ config["conversationMode"] = speech_tab.get("conversationMode", False)
1220
+
1221
+ log.debug("[AudioService] Speech config: %s", config.keys())
1222
+ return config
1223
+
1224
+ def _get_file_extension(self, filename: str) -> str:
1225
+ """Get file extension from filename"""
1226
+ import os
1227
+ return os.path.splitext(filename)[1] or ".wav"