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,1416 @@
1
+ """
2
+ Prompts API router for prompt library feature.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import uuid
7
+ from typing import List, Optional, Dict, Any, Literal
8
+ from fastapi import APIRouter, HTTPException, Depends, Query, status
9
+ from sqlalchemy.orm import Session
10
+ from sqlalchemy import or_, func
11
+
12
+ from ..services.prompt_builder_assistant import PromptBuilderAssistant
13
+
14
+ from ..dependencies import get_db, get_user_id, get_sac_component, get_api_config, get_user_display_name
15
+ from ..repository.models import PromptGroupModel, PromptModel, PromptGroupUserModel
16
+ from .dto.prompt_dto import (
17
+ PromptGroupCreate,
18
+ PromptGroupUpdate,
19
+ PromptGroupResponse,
20
+ PromptGroupListResponse,
21
+ PromptCreate,
22
+ PromptResponse,
23
+ PromptBuilderChatRequest,
24
+ PromptBuilderChatResponse,
25
+ PromptExportResponse,
26
+ PromptExportData,
27
+ PromptExportMetadata,
28
+ PromptImportRequest,
29
+ PromptImportResponse,
30
+ )
31
+ from solace_agent_mesh.shared.utils.timestamp_utils import now_epoch_ms
32
+ from solace_ai_connector.common.log import log
33
+
34
+ from typing import TYPE_CHECKING
35
+
36
+ if TYPE_CHECKING:
37
+ from ..component import WebUIBackendComponent
38
+
39
+ router = APIRouter()
40
+
41
+
42
+ # ============================================================================
43
+ # Permission Helper Functions
44
+ # ============================================================================
45
+
46
+ def get_user_role(db: Session, group_id: str, user_id: str) -> Optional[Literal["owner", "editor", "viewer"]]:
47
+ """
48
+ Get the user's role for a prompt group.
49
+ Returns 'owner' if user owns the group, or their assigned role from prompt_group_users.
50
+ Returns None if user has no access.
51
+ """
52
+ # Check if user is the owner
53
+ group = db.query(PromptGroupModel).filter(
54
+ PromptGroupModel.id == group_id,
55
+ PromptGroupModel.user_id == user_id
56
+ ).first()
57
+
58
+ if group:
59
+ return "owner"
60
+
61
+ # Check if user has shared access
62
+ share = db.query(PromptGroupUserModel).filter(
63
+ PromptGroupUserModel.prompt_group_id == group_id,
64
+ PromptGroupUserModel.user_id == user_id
65
+ ).first()
66
+
67
+ if share:
68
+ return share.role
69
+
70
+ return None
71
+
72
+
73
+ def check_permission(
74
+ db: Session,
75
+ group_id: str,
76
+ user_id: str,
77
+ required_permission: Literal["read", "write", "delete"]
78
+ ) -> None:
79
+ """
80
+ Check if user has the required permission for a prompt group.
81
+ Raises HTTPException if permission is denied.
82
+
83
+ Permission levels:
84
+ - owner: read, write, delete
85
+ - editor: read, write, delete
86
+ - viewer: read only
87
+ """
88
+ role = get_user_role(db, group_id, user_id)
89
+
90
+ if role is None:
91
+ raise HTTPException(
92
+ status_code=status.HTTP_404_NOT_FOUND,
93
+ detail="Prompt group not found"
94
+ )
95
+
96
+ # Check permissions based on role
97
+ if required_permission == "read":
98
+ # All roles can read
99
+ return
100
+
101
+ if required_permission in ("write", "delete"):
102
+ if role == "viewer":
103
+ raise HTTPException(
104
+ status_code=status.HTTP_403_FORBIDDEN,
105
+ detail=f"Viewer role cannot {required_permission} this prompt group"
106
+ )
107
+ # owner and editor can write and delete
108
+ return
109
+
110
+
111
+ def check_prompts_enabled(
112
+ component: "WebUIBackendComponent" = Depends(get_sac_component),
113
+ api_config: Dict[str, Any] = Depends(get_api_config),
114
+ ) -> None:
115
+ """
116
+ Dependency to check if prompts feature is enabled.
117
+ Raises HTTPException if prompts are disabled.
118
+ """
119
+ # Check if persistence is enabled (required for prompts)
120
+ persistence_enabled = api_config.get("persistence_enabled", False)
121
+ if not persistence_enabled:
122
+ log.warning("Prompts API called but persistence is not enabled")
123
+ raise HTTPException(
124
+ status_code=status.HTTP_501_NOT_IMPLEMENTED,
125
+ detail="Prompts feature requires persistence to be enabled. Please configure session_service.type as 'sql'."
126
+ )
127
+
128
+ # Check explicit prompt_library config
129
+ prompt_library_config = component.get_config("prompt_library", {})
130
+ if isinstance(prompt_library_config, dict):
131
+ prompts_explicitly_enabled = prompt_library_config.get("enabled", True)
132
+ if not prompts_explicitly_enabled:
133
+ log.warning("Prompts API called but prompt library is explicitly disabled in config")
134
+ raise HTTPException(
135
+ status_code=status.HTTP_501_NOT_IMPLEMENTED,
136
+ detail="Prompt library feature is disabled. Please enable it in the configuration."
137
+ )
138
+
139
+ # Check frontend_feature_enablement override
140
+ feature_flags = component.get_config("frontend_feature_enablement", {})
141
+ if "promptLibrary" in feature_flags:
142
+ prompts_flag = feature_flags.get("promptLibrary", True)
143
+ if not prompts_flag:
144
+ log.warning("Prompts API called but prompts are disabled via feature flag")
145
+ raise HTTPException(
146
+ status_code=status.HTTP_501_NOT_IMPLEMENTED,
147
+ detail="Prompt library feature is disabled via feature flag."
148
+ )
149
+
150
+
151
+ # ============================================================================
152
+ # Prompt Groups Endpoints
153
+ # ============================================================================
154
+
155
+ @router.get("/groups/all", response_model=List[PromptGroupResponse])
156
+ async def get_all_prompt_groups(
157
+ db: Session = Depends(get_db),
158
+ user_id: str = Depends(get_user_id),
159
+ _: None = Depends(check_prompts_enabled),
160
+ ):
161
+ """
162
+ Get all prompt groups for quick access (used by "/" command).
163
+ Returns all groups owned by user or shared with them.
164
+ """
165
+ try:
166
+ # Get groups owned by user
167
+ owned_groups = db.query(PromptGroupModel).filter(
168
+ PromptGroupModel.user_id == user_id
169
+ ).all()
170
+
171
+ # Get groups shared with user
172
+ shared_group_ids = db.query(PromptGroupUserModel.prompt_group_id).filter(
173
+ PromptGroupUserModel.user_id == user_id
174
+ ).all()
175
+ shared_group_ids = [gid[0] for gid in shared_group_ids]
176
+
177
+ shared_groups = []
178
+ if shared_group_ids:
179
+ shared_groups = db.query(PromptGroupModel).filter(
180
+ PromptGroupModel.id.in_(shared_group_ids)
181
+ ).all()
182
+
183
+ # Combine and sort
184
+ all_groups = owned_groups + shared_groups
185
+ groups = sorted(
186
+ all_groups,
187
+ key=lambda g: (not g.is_pinned, -g.created_at)
188
+ )
189
+
190
+ # Fetch production prompts for each group
191
+ result = []
192
+ for group in groups:
193
+ try:
194
+ # Truncate fields that exceed max length to prevent validation errors
195
+ name = group.name[:255] if group.name and len(group.name) > 255 else group.name
196
+ description = group.description[:1000] if group.description and len(group.description) > 1000 else group.description
197
+ category = group.category[:100] if group.category and len(group.category) > 100 else group.category
198
+ command = group.command[:50] if group.command and len(group.command) > 50 else group.command
199
+ author_name = group.author_name[:255] if group.author_name and len(group.author_name) > 255 else group.author_name
200
+
201
+ group_dict = {
202
+ "id": group.id,
203
+ "name": name,
204
+ "description": description,
205
+ "category": category,
206
+ "command": command,
207
+ "user_id": group.user_id,
208
+ "author_name": author_name,
209
+ "production_prompt_id": group.production_prompt_id,
210
+ "is_shared": group.is_shared,
211
+ "is_pinned": group.is_pinned,
212
+ "created_at": group.created_at,
213
+ "updated_at": group.updated_at,
214
+ "production_prompt": None,
215
+ }
216
+
217
+ if group.production_prompt_id:
218
+ prod_prompt = db.query(PromptModel).filter(
219
+ PromptModel.id == group.production_prompt_id
220
+ ).first()
221
+ if prod_prompt:
222
+ group_dict["production_prompt"] = {
223
+ "id": prod_prompt.id,
224
+ "prompt_text": prod_prompt.prompt_text,
225
+ "group_id": prod_prompt.group_id,
226
+ "user_id": prod_prompt.user_id,
227
+ "version": prod_prompt.version,
228
+ "name": prod_prompt.name,
229
+ "description": prod_prompt.description,
230
+ "category": prod_prompt.category,
231
+ "command": prod_prompt.command,
232
+ "created_at": prod_prompt.created_at,
233
+ "updated_at": prod_prompt.updated_at,
234
+ }
235
+
236
+ result.append(PromptGroupResponse(**group_dict))
237
+ except Exception as e:
238
+ # Log the error but continue processing other groups
239
+ log.warning(f"Skipping invalid prompt group {group.id}: {e}")
240
+ continue
241
+
242
+ return result
243
+ except Exception as e:
244
+ log.error(f"Error fetching all prompt groups: {e}")
245
+ raise HTTPException(
246
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
247
+ detail="Failed to fetch prompt groups"
248
+ )
249
+
250
+
251
+ @router.get("/groups", response_model=PromptGroupListResponse)
252
+ async def list_prompt_groups(
253
+ skip: int = Query(0, ge=0),
254
+ limit: int = Query(100, ge=1, le=100),
255
+ category: Optional[str] = None,
256
+ search: Optional[str] = None,
257
+ db: Session = Depends(get_db),
258
+ user_id: str = Depends(get_user_id),
259
+ _: None = Depends(check_prompts_enabled),
260
+ ):
261
+ """
262
+ List all prompt groups accessible to the user (owned or shared).
263
+ Supports pagination, category filtering, and text search.
264
+ """
265
+ try:
266
+ # Get shared group IDs
267
+ shared_group_ids = db.query(PromptGroupUserModel.prompt_group_id).filter(
268
+ PromptGroupUserModel.user_id == user_id
269
+ ).all()
270
+ shared_group_ids = [gid[0] for gid in shared_group_ids]
271
+
272
+ # Build query for owned or shared groups
273
+ query = db.query(PromptGroupModel).filter(
274
+ or_(
275
+ PromptGroupModel.user_id == user_id,
276
+ PromptGroupModel.id.in_(shared_group_ids) if shared_group_ids else False
277
+ )
278
+ )
279
+
280
+ if category:
281
+ query = query.filter(PromptGroupModel.category == category)
282
+
283
+ if search:
284
+ search_pattern = f"%{search}%"
285
+ query = query.filter(
286
+ or_(
287
+ PromptGroupModel.name.ilike(search_pattern),
288
+ PromptGroupModel.description.ilike(search_pattern),
289
+ PromptGroupModel.command.ilike(search_pattern)
290
+ )
291
+ )
292
+
293
+ total = query.count()
294
+ groups = query.order_by(
295
+ PromptGroupModel.is_pinned.desc(), # Pinned first
296
+ PromptGroupModel.created_at.desc()
297
+ ).offset(skip).limit(limit).all()
298
+
299
+ # Fetch production prompts for each group
300
+ result_groups = []
301
+ for group in groups:
302
+ try:
303
+ # Truncate fields that exceed max length to prevent validation errors
304
+ name = group.name[:255] if group.name and len(group.name) > 255 else group.name
305
+ description = group.description[:1000] if group.description and len(group.description) > 1000 else group.description
306
+ category = group.category[:100] if group.category and len(group.category) > 100 else group.category
307
+ command = group.command[:50] if group.command and len(group.command) > 50 else group.command
308
+ author_name = group.author_name[:255] if group.author_name and len(group.author_name) > 255 else group.author_name
309
+
310
+ group_dict = {
311
+ "id": group.id,
312
+ "name": name,
313
+ "description": description,
314
+ "category": category,
315
+ "command": command,
316
+ "user_id": group.user_id,
317
+ "author_name": author_name,
318
+ "production_prompt_id": group.production_prompt_id,
319
+ "is_shared": group.is_shared,
320
+ "is_pinned": group.is_pinned,
321
+ "created_at": group.created_at,
322
+ "updated_at": group.updated_at,
323
+ "production_prompt": None,
324
+ }
325
+
326
+ if group.production_prompt_id:
327
+ prod_prompt = db.query(PromptModel).filter(
328
+ PromptModel.id == group.production_prompt_id
329
+ ).first()
330
+ if prod_prompt:
331
+ group_dict["production_prompt"] = {
332
+ "id": prod_prompt.id,
333
+ "prompt_text": prod_prompt.prompt_text,
334
+ "group_id": prod_prompt.group_id,
335
+ "user_id": prod_prompt.user_id,
336
+ "version": prod_prompt.version,
337
+ "name": prod_prompt.name,
338
+ "description": prod_prompt.description,
339
+ "category": prod_prompt.category,
340
+ "command": prod_prompt.command,
341
+ "created_at": prod_prompt.created_at,
342
+ "updated_at": prod_prompt.updated_at,
343
+ }
344
+
345
+ result_groups.append(PromptGroupResponse(**group_dict))
346
+ except Exception as e:
347
+ # Log the error but continue processing other groups
348
+ log.warning(f"Skipping invalid prompt group {group.id}: {e}")
349
+ continue
350
+
351
+ return PromptGroupListResponse(
352
+ groups=result_groups,
353
+ total=total,
354
+ skip=skip,
355
+ limit=limit,
356
+ )
357
+ except Exception as e:
358
+ log.error(f"Error listing prompt groups: {e}")
359
+ raise HTTPException(
360
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
361
+ detail="Failed to list prompt groups"
362
+ )
363
+
364
+
365
+ @router.get("/groups/{group_id}", response_model=PromptGroupResponse)
366
+ async def get_prompt_group(
367
+ group_id: str,
368
+ db: Session = Depends(get_db),
369
+ user_id: str = Depends(get_user_id),
370
+ _: None = Depends(check_prompts_enabled),
371
+ ):
372
+ """Get a specific prompt group by ID (requires read permission)."""
373
+ try:
374
+ # Check read permission (works for owner, editor, viewer)
375
+ check_permission(db, group_id, user_id, "read")
376
+
377
+ group = db.query(PromptGroupModel).filter(
378
+ PromptGroupModel.id == group_id
379
+ ).first()
380
+
381
+ if not group:
382
+ raise HTTPException(
383
+ status_code=status.HTTP_404_NOT_FOUND,
384
+ detail="Prompt group not found"
385
+ )
386
+
387
+ group_dict = {
388
+ "id": group.id,
389
+ "name": group.name,
390
+ "description": group.description,
391
+ "category": group.category,
392
+ "command": group.command,
393
+ "user_id": group.user_id,
394
+ "author_name": group.author_name,
395
+ "production_prompt_id": group.production_prompt_id,
396
+ "is_shared": group.is_shared,
397
+ "is_pinned": group.is_pinned,
398
+ "created_at": group.created_at,
399
+ "updated_at": group.updated_at,
400
+ "production_prompt": None,
401
+ }
402
+
403
+ if group.production_prompt_id:
404
+ prod_prompt = db.query(PromptModel).filter(
405
+ PromptModel.id == group.production_prompt_id
406
+ ).first()
407
+ if prod_prompt:
408
+ group_dict["production_prompt"] = {
409
+ "id": prod_prompt.id,
410
+ "prompt_text": prod_prompt.prompt_text,
411
+ "group_id": prod_prompt.group_id,
412
+ "user_id": prod_prompt.user_id,
413
+ "version": prod_prompt.version,
414
+ "name": prod_prompt.name,
415
+ "description": prod_prompt.description,
416
+ "category": prod_prompt.category,
417
+ "command": prod_prompt.command,
418
+ "created_at": prod_prompt.created_at,
419
+ "updated_at": prod_prompt.updated_at,
420
+ }
421
+
422
+ return PromptGroupResponse(**group_dict)
423
+ except HTTPException:
424
+ raise
425
+ except Exception as e:
426
+ log.error(f"Error fetching prompt group {group_id}: {e}")
427
+ raise HTTPException(
428
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
429
+ detail="Failed to fetch prompt group"
430
+ )
431
+
432
+
433
+ @router.post("/groups", response_model=PromptGroupResponse, status_code=status.HTTP_201_CREATED)
434
+ async def create_prompt_group(
435
+ group_data: PromptGroupCreate,
436
+ db: Session = Depends(get_db),
437
+ user_id: str = Depends(get_user_id),
438
+ user_display_name: str = Depends(get_user_display_name),
439
+ _: None = Depends(check_prompts_enabled),
440
+ ):
441
+ """
442
+ Create a new prompt group with an initial prompt.
443
+ The initial prompt is automatically set as the production version.
444
+ """
445
+ try:
446
+ # Check if command already exists
447
+ if group_data.command:
448
+ existing = db.query(PromptGroupModel).filter(
449
+ PromptGroupModel.command == group_data.command,
450
+ PromptGroupModel.user_id == user_id,
451
+ ).first()
452
+ if existing:
453
+ raise HTTPException(
454
+ status_code=status.HTTP_400_BAD_REQUEST,
455
+ detail=f"Command '/{group_data.command}' already exists"
456
+ )
457
+
458
+ # Create prompt group
459
+ group_id = str(uuid.uuid4())
460
+ now_ms = now_epoch_ms()
461
+
462
+ new_group = PromptGroupModel(
463
+ id=group_id,
464
+ name=group_data.name,
465
+ description=group_data.description,
466
+ category=group_data.category,
467
+ command=group_data.command,
468
+ user_id=user_id,
469
+ author_name=user_display_name,
470
+ production_prompt_id=None,
471
+ is_shared=False,
472
+ is_pinned=False,
473
+ created_at=now_ms,
474
+ updated_at=now_ms,
475
+ )
476
+ db.add(new_group)
477
+ db.flush()
478
+
479
+ # Create initial prompt with versioned metadata
480
+ prompt_id = str(uuid.uuid4())
481
+ new_prompt = PromptModel(
482
+ id=prompt_id,
483
+ prompt_text=group_data.initial_prompt,
484
+ name=group_data.name,
485
+ description=group_data.description,
486
+ category=group_data.category,
487
+ command=group_data.command,
488
+ group_id=group_id,
489
+ user_id=user_id,
490
+ version=1,
491
+ created_at=now_ms,
492
+ updated_at=now_ms,
493
+ )
494
+ db.add(new_prompt)
495
+ db.flush()
496
+
497
+ # Set production prompt reference
498
+ new_group.production_prompt_id = prompt_id
499
+ new_group.updated_at = now_epoch_ms()
500
+
501
+ db.commit()
502
+ db.refresh(new_group)
503
+
504
+ # Build response
505
+ return PromptGroupResponse(
506
+ id=new_group.id,
507
+ name=new_group.name,
508
+ description=new_group.description,
509
+ category=new_group.category,
510
+ command=new_group.command,
511
+ user_id=new_group.user_id,
512
+ author_name=new_group.author_name,
513
+ production_prompt_id=new_group.production_prompt_id,
514
+ is_shared=new_group.is_shared,
515
+ is_pinned=new_group.is_pinned,
516
+ created_at=new_group.created_at,
517
+ updated_at=new_group.updated_at,
518
+ production_prompt=PromptResponse(
519
+ id=new_prompt.id,
520
+ prompt_text=new_prompt.prompt_text,
521
+ group_id=new_prompt.group_id,
522
+ user_id=new_prompt.user_id,
523
+ version=new_prompt.version,
524
+ name=new_prompt.name,
525
+ description=new_prompt.description,
526
+ category=new_prompt.category,
527
+ command=new_prompt.command,
528
+ created_at=new_prompt.created_at,
529
+ updated_at=new_prompt.updated_at,
530
+ ),
531
+ )
532
+ except HTTPException:
533
+ db.rollback()
534
+ raise
535
+ except Exception as e:
536
+ db.rollback()
537
+ log.error(f"Error creating prompt group: {e}")
538
+ raise HTTPException(
539
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
540
+ detail="Failed to create prompt group"
541
+ )
542
+
543
+
544
+ @router.patch("/groups/{group_id}", response_model=PromptGroupResponse)
545
+ async def update_prompt_group(
546
+ group_id: str,
547
+ group_data: PromptGroupUpdate,
548
+ db: Session = Depends(get_db),
549
+ user_id: str = Depends(get_user_id),
550
+ _: None = Depends(check_prompts_enabled),
551
+ ):
552
+ """Update a prompt group's metadata (requires write permission - owner or editor only)."""
553
+ try:
554
+ # Check write permission (owner or editor only, not viewer)
555
+ check_permission(db, group_id, user_id, "write")
556
+
557
+ group = db.query(PromptGroupModel).filter(
558
+ PromptGroupModel.id == group_id
559
+ ).first()
560
+
561
+ if not group:
562
+ raise HTTPException(
563
+ status_code=status.HTTP_404_NOT_FOUND,
564
+ detail="Prompt group not found"
565
+ )
566
+
567
+ # Check command uniqueness if being updated
568
+ if group_data.command and group_data.command != group.command:
569
+ existing = db.query(PromptGroupModel).filter(
570
+ PromptGroupModel.command == group_data.command,
571
+ PromptGroupModel.user_id == user_id,
572
+ PromptGroupModel.id != group_id,
573
+ ).first()
574
+ if existing:
575
+ raise HTTPException(
576
+ status_code=status.HTTP_400_BAD_REQUEST,
577
+ detail=f"Command '/{group_data.command}' already exists"
578
+ )
579
+
580
+ # Update fields (excluding initial_prompt which is handled separately)
581
+ update_data = group_data.dict(exclude_unset=True, exclude={'initial_prompt'})
582
+ for field, value in update_data.items():
583
+ setattr(group, field, value)
584
+
585
+ # If initial_prompt is provided, always create a new version
586
+ # This happens when user clicks "Save New Version" button
587
+ if hasattr(group_data, 'initial_prompt') and group_data.initial_prompt:
588
+ # Get next version number
589
+ max_version_result = db.query(func.max(PromptModel.version)).filter(
590
+ PromptModel.group_id == group_id
591
+ ).scalar()
592
+
593
+ next_version = (max_version_result + 1) if max_version_result else 1
594
+
595
+ # Create new prompt version
596
+ prompt_id = str(uuid.uuid4())
597
+ now_ms = now_epoch_ms()
598
+
599
+ # Create new prompt version with current metadata
600
+ new_prompt = PromptModel(
601
+ id=prompt_id,
602
+ prompt_text=group_data.initial_prompt,
603
+ name=group_data.name if group_data.name else group.name,
604
+ description=group_data.description if group_data.description is not None else group.description,
605
+ category=group_data.category if group_data.category is not None else group.category,
606
+ command=group_data.command if group_data.command is not None else group.command,
607
+ group_id=group_id,
608
+ user_id=user_id,
609
+ version=next_version,
610
+ created_at=now_ms,
611
+ updated_at=now_ms,
612
+ )
613
+ db.add(new_prompt)
614
+ db.flush()
615
+
616
+ # Update production prompt reference
617
+ group.production_prompt_id = prompt_id
618
+
619
+ group.updated_at = now_epoch_ms()
620
+
621
+ db.commit()
622
+ db.refresh(group)
623
+
624
+ # Build response
625
+ group_dict = {
626
+ "id": group.id,
627
+ "name": group.name,
628
+ "description": group.description,
629
+ "category": group.category,
630
+ "command": group.command,
631
+ "user_id": group.user_id,
632
+ "author_name": group.author_name,
633
+ "production_prompt_id": group.production_prompt_id,
634
+ "is_shared": group.is_shared,
635
+ "is_pinned": group.is_pinned,
636
+ "created_at": group.created_at,
637
+ "updated_at": group.updated_at,
638
+ "production_prompt": None,
639
+ }
640
+
641
+ if group.production_prompt_id:
642
+ prod_prompt = db.query(PromptModel).filter(
643
+ PromptModel.id == group.production_prompt_id
644
+ ).first()
645
+ if prod_prompt:
646
+ group_dict["production_prompt"] = {
647
+ "id": prod_prompt.id,
648
+ "prompt_text": prod_prompt.prompt_text,
649
+ "group_id": prod_prompt.group_id,
650
+ "user_id": prod_prompt.user_id,
651
+ "version": prod_prompt.version,
652
+ "name": prod_prompt.name,
653
+ "description": prod_prompt.description,
654
+ "category": prod_prompt.category,
655
+ "command": prod_prompt.command,
656
+ "created_at": prod_prompt.created_at,
657
+ "updated_at": prod_prompt.updated_at,
658
+ }
659
+
660
+ return PromptGroupResponse(**group_dict)
661
+ except HTTPException:
662
+ db.rollback()
663
+ raise
664
+ except Exception as e:
665
+ db.rollback()
666
+ log.error(f"Error updating prompt group {group_id}: {e}")
667
+ raise HTTPException(
668
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
669
+ detail="Failed to update prompt group"
670
+ )
671
+
672
+
673
+ @router.patch("/groups/{group_id}/pin", response_model=PromptGroupResponse)
674
+ async def toggle_pin_prompt(
675
+ group_id: str,
676
+ db: Session = Depends(get_db),
677
+ user_id: str = Depends(get_user_id),
678
+ _: None = Depends(check_prompts_enabled),
679
+ ):
680
+ """Toggle pin status for a prompt group (requires write permission)."""
681
+ try:
682
+ # Check write permission
683
+ check_permission(db, group_id, user_id, "write")
684
+
685
+ group = db.query(PromptGroupModel).filter(
686
+ PromptGroupModel.id == group_id
687
+ ).first()
688
+
689
+ if not group:
690
+ raise HTTPException(
691
+ status_code=status.HTTP_404_NOT_FOUND,
692
+ detail="Prompt group not found"
693
+ )
694
+
695
+ # Toggle pin status
696
+ group.is_pinned = not group.is_pinned
697
+ group.updated_at = now_epoch_ms()
698
+
699
+ db.commit()
700
+ db.refresh(group)
701
+
702
+ # Build response
703
+ group_dict = {
704
+ "id": group.id,
705
+ "name": group.name,
706
+ "description": group.description,
707
+ "category": group.category,
708
+ "command": group.command,
709
+ "user_id": group.user_id,
710
+ "author_name": group.author_name,
711
+ "production_prompt_id": group.production_prompt_id,
712
+ "is_shared": group.is_shared,
713
+ "is_pinned": group.is_pinned,
714
+ "created_at": group.created_at,
715
+ "updated_at": group.updated_at,
716
+ "production_prompt": None,
717
+ }
718
+
719
+ if group.production_prompt_id:
720
+ prod_prompt = db.query(PromptModel).filter(
721
+ PromptModel.id == group.production_prompt_id
722
+ ).first()
723
+ if prod_prompt:
724
+ group_dict["production_prompt"] = {
725
+ "id": prod_prompt.id,
726
+ "prompt_text": prod_prompt.prompt_text,
727
+ "group_id": prod_prompt.group_id,
728
+ "user_id": prod_prompt.user_id,
729
+ "version": prod_prompt.version,
730
+ "name": prod_prompt.name,
731
+ "description": prod_prompt.description,
732
+ "category": prod_prompt.category,
733
+ "command": prod_prompt.command,
734
+ "created_at": prod_prompt.created_at,
735
+ "updated_at": prod_prompt.updated_at,
736
+ }
737
+
738
+ return PromptGroupResponse(**group_dict)
739
+ except HTTPException:
740
+ db.rollback()
741
+ raise
742
+ except Exception as e:
743
+ db.rollback()
744
+ log.error(f"Error toggling pin for prompt group {group_id}: {e}")
745
+ raise HTTPException(
746
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
747
+ detail="Failed to toggle pin status"
748
+ )
749
+
750
+
751
+ @router.delete("/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
752
+ async def delete_prompt_group(
753
+ group_id: str,
754
+ db: Session = Depends(get_db),
755
+ user_id: str = Depends(get_user_id),
756
+ _: None = Depends(check_prompts_enabled),
757
+ ):
758
+ """Delete a prompt group and all its prompts (requires delete permission - owner or editor only)."""
759
+ try:
760
+ # Check delete permission
761
+ check_permission(db, group_id, user_id, "delete")
762
+
763
+ group = db.query(PromptGroupModel).filter(
764
+ PromptGroupModel.id == group_id
765
+ ).first()
766
+
767
+ if not group:
768
+ raise HTTPException(
769
+ status_code=status.HTTP_404_NOT_FOUND,
770
+ detail="Prompt group not found"
771
+ )
772
+
773
+ # Delete all prompts in the group (cascade should handle this)
774
+ db.delete(group)
775
+ db.commit()
776
+
777
+ return None
778
+ except HTTPException:
779
+ db.rollback()
780
+ raise
781
+ except Exception as e:
782
+ db.rollback()
783
+ log.error(f"Error deleting prompt group {group_id}: {e}")
784
+ raise HTTPException(
785
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
786
+ detail="Failed to delete prompt group"
787
+ )
788
+
789
+
790
+ # ============================================================================
791
+ # Prompts Endpoints
792
+ # ============================================================================
793
+
794
+ @router.get("/groups/{group_id}/prompts", response_model=List[PromptResponse])
795
+ async def list_prompts_in_group(
796
+ group_id: str,
797
+ db: Session = Depends(get_db),
798
+ user_id: str = Depends(get_user_id),
799
+ _: None = Depends(check_prompts_enabled),
800
+ ):
801
+ """List all prompt versions in a group (requires read permission)."""
802
+ try:
803
+ # Check read permission
804
+ check_permission(db, group_id, user_id, "read")
805
+
806
+ group = db.query(PromptGroupModel).filter(
807
+ PromptGroupModel.id == group_id
808
+ ).first()
809
+
810
+ if not group:
811
+ raise HTTPException(
812
+ status_code=status.HTTP_404_NOT_FOUND,
813
+ detail="Prompt group not found"
814
+ )
815
+
816
+ prompts = db.query(PromptModel).filter(
817
+ PromptModel.group_id == group_id
818
+ ).order_by(PromptModel.created_at.desc()).all()
819
+
820
+ return [
821
+ PromptResponse(
822
+ id=p.id,
823
+ prompt_text=p.prompt_text,
824
+ group_id=p.group_id,
825
+ user_id=p.user_id,
826
+ version=p.version,
827
+ name=p.name,
828
+ description=p.description,
829
+ category=p.category,
830
+ command=p.command,
831
+ created_at=p.created_at,
832
+ updated_at=p.updated_at,
833
+ )
834
+ for p in prompts
835
+ ]
836
+ except HTTPException:
837
+ raise
838
+ except Exception as e:
839
+ log.error(f"Error listing prompts in group {group_id}: {e}")
840
+ raise HTTPException(
841
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
842
+ detail="Failed to list prompts"
843
+ )
844
+
845
+
846
+ @router.post("/groups/{group_id}/prompts", response_model=PromptResponse, status_code=status.HTTP_201_CREATED)
847
+ async def create_prompt_version(
848
+ group_id: str,
849
+ prompt_data: PromptCreate,
850
+ db: Session = Depends(get_db),
851
+ user_id: str = Depends(get_user_id),
852
+ _: None = Depends(check_prompts_enabled),
853
+ ):
854
+ """Create a new prompt version in a group (requires write permission)."""
855
+ try:
856
+ # Check write permission
857
+ check_permission(db, group_id, user_id, "write")
858
+
859
+ group = db.query(PromptGroupModel).filter(
860
+ PromptGroupModel.id == group_id
861
+ ).first()
862
+
863
+ if not group:
864
+ raise HTTPException(
865
+ status_code=status.HTTP_404_NOT_FOUND,
866
+ detail="Prompt group not found"
867
+ )
868
+
869
+ # Get next version number
870
+ max_version_result = db.query(func.max(PromptModel.version)).filter(
871
+ PromptModel.group_id == group_id
872
+ ).scalar()
873
+
874
+ next_version = (max_version_result + 1) if max_version_result else 1
875
+
876
+ # Create new prompt
877
+ prompt_id = str(uuid.uuid4())
878
+ now_ms = now_epoch_ms()
879
+
880
+ # Get current group metadata for the new version
881
+ new_prompt = PromptModel(
882
+ id=prompt_id,
883
+ prompt_text=prompt_data.prompt_text,
884
+ name=group.name,
885
+ description=group.description,
886
+ category=group.category,
887
+ command=group.command,
888
+ group_id=group_id,
889
+ user_id=user_id,
890
+ version=next_version,
891
+ created_at=now_ms,
892
+ updated_at=now_ms,
893
+ )
894
+ db.add(new_prompt)
895
+ db.commit()
896
+ db.refresh(new_prompt)
897
+
898
+ return PromptResponse(
899
+ id=new_prompt.id,
900
+ prompt_text=new_prompt.prompt_text,
901
+ group_id=new_prompt.group_id,
902
+ user_id=new_prompt.user_id,
903
+ version=new_prompt.version,
904
+ name=new_prompt.name,
905
+ description=new_prompt.description,
906
+ category=new_prompt.category,
907
+ command=new_prompt.command,
908
+ created_at=new_prompt.created_at,
909
+ updated_at=new_prompt.updated_at,
910
+ )
911
+ except HTTPException:
912
+ db.rollback()
913
+ raise
914
+ except Exception as e:
915
+ db.rollback()
916
+ log.error(f"Error creating prompt version in group {group_id}: {e}")
917
+ raise HTTPException(
918
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
919
+ detail="Failed to create prompt version"
920
+ )
921
+
922
+
923
+ @router.patch("/{prompt_id}", response_model=PromptResponse)
924
+ async def update_prompt(
925
+ prompt_id: str,
926
+ prompt_data: PromptCreate,
927
+ db: Session = Depends(get_db),
928
+ user_id: str = Depends(get_user_id),
929
+ _: None = Depends(check_prompts_enabled),
930
+ ):
931
+ """Update an existing prompt's content (requires write permission)."""
932
+ try:
933
+ prompt = db.query(PromptModel).filter(
934
+ PromptModel.id == prompt_id
935
+ ).first()
936
+
937
+ if not prompt:
938
+ raise HTTPException(
939
+ status_code=status.HTTP_404_NOT_FOUND,
940
+ detail="Prompt not found"
941
+ )
942
+
943
+ # Check write permission on the group
944
+ check_permission(db, prompt.group_id, user_id, "write")
945
+
946
+ # Update prompt text
947
+ prompt.prompt_text = prompt_data.prompt_text
948
+ prompt.updated_at = now_epoch_ms()
949
+
950
+ db.commit()
951
+ db.refresh(prompt)
952
+
953
+ return PromptResponse(
954
+ id=prompt.id,
955
+ prompt_text=prompt.prompt_text,
956
+ group_id=prompt.group_id,
957
+ user_id=prompt.user_id,
958
+ version=prompt.version,
959
+ name=prompt.name,
960
+ description=prompt.description,
961
+ category=prompt.category,
962
+ command=prompt.command,
963
+ created_at=prompt.created_at,
964
+ updated_at=prompt.updated_at,
965
+ )
966
+ except HTTPException:
967
+ db.rollback()
968
+ raise
969
+ except Exception as e:
970
+ db.rollback()
971
+ log.error(f"Error updating prompt {prompt_id}: {e}")
972
+ raise HTTPException(
973
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
974
+ detail="Failed to update prompt"
975
+ )
976
+
977
+
978
+ @router.patch("/{prompt_id}/make-production", response_model=PromptResponse)
979
+ async def make_prompt_production(
980
+ prompt_id: str,
981
+ db: Session = Depends(get_db),
982
+ user_id: str = Depends(get_user_id),
983
+ _: None = Depends(check_prompts_enabled),
984
+ ):
985
+ """Set a prompt as the production version for its group (requires write permission)."""
986
+ try:
987
+ prompt = db.query(PromptModel).filter(
988
+ PromptModel.id == prompt_id
989
+ ).first()
990
+
991
+ if not prompt:
992
+ raise HTTPException(
993
+ status_code=status.HTTP_404_NOT_FOUND,
994
+ detail="Prompt not found"
995
+ )
996
+
997
+ # Check write permission on the group
998
+ check_permission(db, prompt.group_id, user_id, "write")
999
+
1000
+ # Update group's production prompt
1001
+ group = db.query(PromptGroupModel).filter(
1002
+ PromptGroupModel.id == prompt.group_id
1003
+ ).first()
1004
+
1005
+ if group:
1006
+ group.production_prompt_id = prompt_id
1007
+ group.updated_at = now_epoch_ms()
1008
+ db.commit()
1009
+ db.refresh(prompt)
1010
+
1011
+ return PromptResponse(
1012
+ id=prompt.id,
1013
+ prompt_text=prompt.prompt_text,
1014
+ group_id=prompt.group_id,
1015
+ user_id=prompt.user_id,
1016
+ version=prompt.version,
1017
+ name=prompt.name,
1018
+ description=prompt.description,
1019
+ category=prompt.category,
1020
+ command=prompt.command,
1021
+ created_at=prompt.created_at,
1022
+ updated_at=prompt.updated_at,
1023
+ )
1024
+ except HTTPException:
1025
+ db.rollback()
1026
+ raise
1027
+ except Exception as e:
1028
+ db.rollback()
1029
+ log.error(f"Error making prompt {prompt_id} production: {e}")
1030
+ raise HTTPException(
1031
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1032
+ detail="Failed to update production prompt"
1033
+ )
1034
+
1035
+
1036
+ @router.delete("/{prompt_id}", status_code=status.HTTP_204_NO_CONTENT)
1037
+ async def delete_prompt(
1038
+ prompt_id: str,
1039
+ db: Session = Depends(get_db),
1040
+ user_id: str = Depends(get_user_id),
1041
+ _: None = Depends(check_prompts_enabled),
1042
+ ):
1043
+ """Delete a specific prompt version (requires delete permission)."""
1044
+ try:
1045
+ prompt = db.query(PromptModel).filter(
1046
+ PromptModel.id == prompt_id
1047
+ ).first()
1048
+
1049
+ if not prompt:
1050
+ raise HTTPException(
1051
+ status_code=status.HTTP_404_NOT_FOUND,
1052
+ detail="Prompt not found"
1053
+ )
1054
+
1055
+ # Check delete permission on the group
1056
+ check_permission(db, prompt.group_id, user_id, "delete")
1057
+
1058
+ # Check if this is the only prompt in the group
1059
+ group = db.query(PromptGroupModel).filter(
1060
+ PromptGroupModel.id == prompt.group_id
1061
+ ).first()
1062
+
1063
+ prompt_count = db.query(PromptModel).filter(
1064
+ PromptModel.group_id == prompt.group_id
1065
+ ).count()
1066
+
1067
+ if prompt_count == 1:
1068
+ # Delete the entire group if this is the last prompt
1069
+ db.delete(group)
1070
+ else:
1071
+ # If this was the production prompt, set another as production
1072
+ if group and group.production_prompt_id == prompt_id:
1073
+ other_prompt = db.query(PromptModel).filter(
1074
+ PromptModel.group_id == prompt.group_id,
1075
+ PromptModel.id != prompt_id,
1076
+ ).order_by(PromptModel.created_at.desc()).first()
1077
+
1078
+ if other_prompt:
1079
+ group.production_prompt_id = other_prompt.id
1080
+ group.updated_at = now_epoch_ms()
1081
+
1082
+ db.delete(prompt)
1083
+
1084
+ db.commit()
1085
+ return None
1086
+ except HTTPException:
1087
+ db.rollback()
1088
+ raise
1089
+ except Exception as e:
1090
+ db.rollback()
1091
+ log.error(f"Error deleting prompt {prompt_id}: {e}")
1092
+ raise HTTPException(
1093
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1094
+ detail="Failed to delete prompt"
1095
+ )
1096
+
1097
+
1098
+ # ============================================================================
1099
+ # AI-Assisted Prompt Builder Endpoints
1100
+ # ============================================================================
1101
+
1102
+
1103
+ @router.get("/chat/init")
1104
+ async def init_prompt_builder_chat(
1105
+ db: Session = Depends(get_db),
1106
+ component: "WebUIBackendComponent" = Depends(get_sac_component),
1107
+ ):
1108
+ """Initialize the prompt template builder chat"""
1109
+ model_config = component.get_config("model", {})
1110
+ assistant = PromptBuilderAssistant(db=db, model_config=model_config)
1111
+ greeting = assistant.get_initial_greeting()
1112
+ return {
1113
+ "message": greeting.message,
1114
+ "confidence": greeting.confidence
1115
+ }
1116
+
1117
+
1118
+ @router.post("/chat", response_model=PromptBuilderChatResponse)
1119
+ async def prompt_builder_chat(
1120
+ request: PromptBuilderChatRequest,
1121
+ db: Session = Depends(get_db),
1122
+ user_id: str = Depends(get_user_id),
1123
+ component: "WebUIBackendComponent" = Depends(get_sac_component),
1124
+ _: None = Depends(check_prompts_enabled),
1125
+ ):
1126
+ """
1127
+ Handle conversational prompt template building using LLM.
1128
+
1129
+ Uses LLM to:
1130
+ 1. Analyze user's description or example transcript
1131
+ 2. Identify variable elements vs fixed instructions
1132
+ 3. Generate template structure
1133
+ 4. Suggest variable names and descriptions
1134
+ 5. Avoid command conflicts with existing prompts
1135
+ """
1136
+ try:
1137
+ # Get model configuration from component
1138
+ model_config = component.get_config("model", {})
1139
+
1140
+ # Initialize the assistant with database session and model config
1141
+ assistant = PromptBuilderAssistant(db=db, model_config=model_config)
1142
+
1143
+ # Process the message using real LLM with conflict checking
1144
+ response = await assistant.process_message(
1145
+ user_message=request.message,
1146
+ conversation_history=[msg.dict() for msg in request.conversation_history],
1147
+ current_template=request.current_template or {},
1148
+ user_id=user_id
1149
+ )
1150
+
1151
+ return PromptBuilderChatResponse(
1152
+ message=response.message,
1153
+ template_updates=response.template_updates,
1154
+ confidence=response.confidence,
1155
+ ready_to_save=response.ready_to_save
1156
+ )
1157
+
1158
+ except Exception as e:
1159
+ log.error(f"Error in prompt builder chat: {e}")
1160
+ raise HTTPException(
1161
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1162
+ detail="Failed to process chat message"
1163
+ )
1164
+
1165
+
1166
+ # ============================================================================
1167
+ # Export/Import Endpoints
1168
+ # ============================================================================
1169
+
1170
+ @router.get("/groups/{group_id}/export", response_model=PromptExportResponse)
1171
+ async def export_prompt_group(
1172
+ group_id: str,
1173
+ db: Session = Depends(get_db),
1174
+ user_id: str = Depends(get_user_id),
1175
+ user_display_name: str = Depends(get_user_display_name),
1176
+ _: None = Depends(check_prompts_enabled),
1177
+ ):
1178
+ """
1179
+ Export a prompt group's active/production version as a JSON file.
1180
+ Returns a downloadable JSON file containing the prompt data.
1181
+ Requires read permission on the prompt group.
1182
+ """
1183
+ try:
1184
+ # Check read permission
1185
+ check_permission(db, group_id, user_id, "read")
1186
+
1187
+ # Fetch the prompt group
1188
+ group = db.query(PromptGroupModel).filter(
1189
+ PromptGroupModel.id == group_id
1190
+ ).first()
1191
+
1192
+ if not group:
1193
+ raise HTTPException(
1194
+ status_code=status.HTTP_404_NOT_FOUND,
1195
+ detail="Prompt group not found"
1196
+ )
1197
+
1198
+ # Fetch the production prompt
1199
+ if not group.production_prompt_id:
1200
+ raise HTTPException(
1201
+ status_code=status.HTTP_400_BAD_REQUEST,
1202
+ detail="No active prompt version to export"
1203
+ )
1204
+
1205
+ prod_prompt = db.query(PromptModel).filter(
1206
+ PromptModel.id == group.production_prompt_id
1207
+ ).first()
1208
+
1209
+ if not prod_prompt:
1210
+ raise HTTPException(
1211
+ status_code=status.HTTP_404_NOT_FOUND,
1212
+ detail="Active prompt version not found"
1213
+ )
1214
+
1215
+ # Build export data
1216
+ # Use author_name if available, otherwise use current user's display name as fallback
1217
+ author_name = group.author_name or user_display_name
1218
+
1219
+ export_data = PromptExportResponse(
1220
+ version="1.0",
1221
+ exported_at=now_epoch_ms(),
1222
+ prompt=PromptExportData(
1223
+ name=group.name,
1224
+ description=group.description,
1225
+ category=group.category,
1226
+ command=group.command,
1227
+ prompt_text=prod_prompt.prompt_text,
1228
+ metadata=PromptExportMetadata(
1229
+ author_name=author_name,
1230
+ original_version=prod_prompt.version,
1231
+ original_created_at=prod_prompt.created_at
1232
+ )
1233
+ )
1234
+ )
1235
+
1236
+ return export_data
1237
+
1238
+ except HTTPException:
1239
+ raise
1240
+ except Exception as e:
1241
+ log.error(f"Error exporting prompt group {group_id}: {e}")
1242
+ raise HTTPException(
1243
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1244
+ detail="Failed to export prompt"
1245
+ )
1246
+
1247
+
1248
+ @router.post("/import", response_model=PromptImportResponse, status_code=status.HTTP_201_CREATED)
1249
+ async def import_prompt(
1250
+ import_request: PromptImportRequest,
1251
+ db: Session = Depends(get_db),
1252
+ user_id: str = Depends(get_user_id),
1253
+ user_display_name: str = Depends(get_user_display_name),
1254
+ _: None = Depends(check_prompts_enabled),
1255
+ ):
1256
+ """
1257
+ Import a prompt from exported JSON data.
1258
+ Creates a new prompt group with the imported data.
1259
+ Handles command conflicts automatically by generating alternative commands.
1260
+ """
1261
+ try:
1262
+ prompt_data = import_request.prompt_data
1263
+ options = import_request.options or PromptImportOptions()
1264
+ warnings = []
1265
+
1266
+ # Validate export format version
1267
+ export_version = prompt_data.get("version", "1.0")
1268
+ if export_version != "1.0":
1269
+ raise HTTPException(
1270
+ status_code=status.HTTP_400_BAD_REQUEST,
1271
+ detail=f"Unsupported export format version: {export_version}"
1272
+ )
1273
+
1274
+ # Extract prompt data
1275
+ prompt_info = prompt_data.get("prompt")
1276
+ if not prompt_info:
1277
+ raise HTTPException(
1278
+ status_code=status.HTTP_400_BAD_REQUEST,
1279
+ detail="Invalid export format: missing 'prompt' field"
1280
+ )
1281
+
1282
+ # Validate required fields
1283
+ required_fields = ["name", "prompt_text"]
1284
+ for field in required_fields:
1285
+ if field not in prompt_info or not prompt_info[field]:
1286
+ raise HTTPException(
1287
+ status_code=status.HTTP_400_BAD_REQUEST,
1288
+ detail=f"Invalid export format: missing required field '{field}'"
1289
+ )
1290
+
1291
+ # Extract fields with validation
1292
+ name = prompt_info["name"]
1293
+ # Truncate name if it exceeds max length (255 chars)
1294
+ if len(name) > 255:
1295
+ original_name = name
1296
+ name = name[:252] + "..."
1297
+ warnings.append(
1298
+ f"Name was truncated from {len(original_name)} to 255 characters"
1299
+ )
1300
+
1301
+ description = prompt_info.get("description")
1302
+ # Truncate description if it exceeds max length (1000 chars)
1303
+ if description and len(description) > 1000:
1304
+ description = description[:997] + "..."
1305
+ warnings.append("Description was truncated to 1000 characters")
1306
+
1307
+ category = prompt_info.get("category") if options.preserve_category else None
1308
+ # Truncate category if it exceeds max length (100 chars)
1309
+ if category and len(category) > 100:
1310
+ category = category[:97] + "..."
1311
+ warnings.append("Category was truncated to 100 characters")
1312
+
1313
+ command = prompt_info.get("command") if options.preserve_command else None
1314
+ # Truncate command if it exceeds max length (50 chars)
1315
+ if command and len(command) > 50:
1316
+ command = command[:50]
1317
+ warnings.append("Command was truncated to 50 characters")
1318
+
1319
+ prompt_text = prompt_info["prompt_text"]
1320
+ # Truncate prompt_text if it exceeds max length (10000 chars)
1321
+ if len(prompt_text) > 10000:
1322
+ prompt_text = prompt_text[:9997] + "..."
1323
+ warnings.append("Prompt text was truncated to 10000 characters")
1324
+
1325
+ # Handle command conflicts
1326
+ if command:
1327
+ original_command = command
1328
+ existing = db.query(PromptGroupModel).filter(
1329
+ PromptGroupModel.command == command,
1330
+ PromptGroupModel.user_id == user_id,
1331
+ ).first()
1332
+
1333
+ if existing:
1334
+ # Generate alternative command
1335
+ counter = 2
1336
+ while True:
1337
+ new_command = f"{original_command}-{counter}"
1338
+ existing_alt = db.query(PromptGroupModel).filter(
1339
+ PromptGroupModel.command == new_command,
1340
+ PromptGroupModel.user_id == user_id,
1341
+ ).first()
1342
+ if not existing_alt:
1343
+ command = new_command
1344
+ warnings.append(
1345
+ f"Command '/{original_command}' already exists, using '/{command}' instead"
1346
+ )
1347
+ break
1348
+ counter += 1
1349
+ if counter > 100: # Safety limit
1350
+ command = None
1351
+ warnings.append(
1352
+ f"Could not generate unique command, imported without command"
1353
+ )
1354
+ break
1355
+
1356
+ # Create new prompt group
1357
+ group_id = str(uuid.uuid4())
1358
+ now_ms = now_epoch_ms()
1359
+
1360
+ new_group = PromptGroupModel(
1361
+ id=group_id,
1362
+ name=name,
1363
+ description=description,
1364
+ category=category,
1365
+ command=command,
1366
+ user_id=user_id,
1367
+ author_name=user_display_name, # Set to importing user, not original author
1368
+ production_prompt_id=None,
1369
+ is_shared=False,
1370
+ is_pinned=False,
1371
+ created_at=now_ms,
1372
+ updated_at=now_ms,
1373
+ )
1374
+ db.add(new_group)
1375
+ db.flush()
1376
+
1377
+ # Create prompt version with versioned metadata
1378
+ prompt_id = str(uuid.uuid4())
1379
+ new_prompt = PromptModel(
1380
+ id=prompt_id,
1381
+ prompt_text=prompt_text,
1382
+ name=name,
1383
+ description=description,
1384
+ category=category,
1385
+ command=command,
1386
+ group_id=group_id,
1387
+ user_id=user_id,
1388
+ version=1, # Start at version 1 for imported prompts
1389
+ created_at=now_ms,
1390
+ updated_at=now_ms,
1391
+ )
1392
+ db.add(new_prompt)
1393
+ db.flush()
1394
+
1395
+ # Set as production prompt
1396
+ new_group.production_prompt_id = prompt_id
1397
+ new_group.updated_at = now_epoch_ms()
1398
+
1399
+ db.commit()
1400
+
1401
+ return PromptImportResponse(
1402
+ success=True,
1403
+ prompt_group_id=group_id,
1404
+ warnings=warnings
1405
+ )
1406
+
1407
+ except HTTPException:
1408
+ db.rollback()
1409
+ raise
1410
+ except Exception as e:
1411
+ db.rollback()
1412
+ log.error(f"Error importing prompt: {e}")
1413
+ raise HTTPException(
1414
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1415
+ detail=f"Failed to import prompt: {str(e)}"
1416
+ )