solace-agent-mesh 1.7.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 (447) 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/artifacts/filesystem_artifact_service.py +164 -0
  7. solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +163 -0
  8. solace_agent_mesh/agent/adk/callbacks.py +752 -127
  9. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +99 -7
  10. solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +52 -5
  11. solace_agent_mesh/agent/adk/mcp_content_processor.py +1 -1
  12. solace_agent_mesh/agent/adk/models/lite_llm.py +34 -16
  13. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +24 -137
  14. solace_agent_mesh/agent/adk/runner.py +66 -8
  15. solace_agent_mesh/agent/adk/schema_migration.py +88 -0
  16. solace_agent_mesh/agent/adk/services.py +41 -1
  17. solace_agent_mesh/agent/adk/setup.py +220 -32
  18. solace_agent_mesh/agent/adk/stream_parser.py +229 -40
  19. solace_agent_mesh/agent/protocol/event_handlers.py +219 -33
  20. solace_agent_mesh/agent/proxies/a2a/component.py +572 -75
  21. solace_agent_mesh/agent/proxies/a2a/config.py +80 -4
  22. solace_agent_mesh/agent/proxies/base/component.py +188 -22
  23. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +3 -1
  24. solace_agent_mesh/agent/sac/app.py +37 -12
  25. solace_agent_mesh/agent/sac/component.py +322 -52
  26. solace_agent_mesh/agent/sac/patch_adk.py +8 -16
  27. solace_agent_mesh/agent/sac/task_execution_context.py +90 -0
  28. solace_agent_mesh/agent/tools/__init__.py +3 -0
  29. solace_agent_mesh/agent/tools/audio_tools.py +3 -3
  30. solace_agent_mesh/agent/tools/builtin_artifact_tools.py +698 -24
  31. solace_agent_mesh/agent/tools/deep_research_tools.py +2161 -0
  32. solace_agent_mesh/agent/tools/peer_agent_tool.py +82 -15
  33. solace_agent_mesh/agent/tools/time_tools.py +126 -0
  34. solace_agent_mesh/agent/tools/tool_config_types.py +54 -2
  35. solace_agent_mesh/agent/tools/web_search_tools.py +279 -0
  36. solace_agent_mesh/agent/tools/web_tools.py +125 -17
  37. solace_agent_mesh/agent/utils/artifact_helpers.py +243 -5
  38. solace_agent_mesh/agent/utils/context_helpers.py +17 -0
  39. solace_agent_mesh/assets/docs/404.html +6 -6
  40. solace_agent_mesh/assets/docs/assets/css/{styles.906a1503.css → styles.8162edfb.css} +1 -1
  41. solace_agent_mesh/assets/docs/assets/js/05749d90.19ac4f35.js +1 -0
  42. solace_agent_mesh/assets/docs/assets/js/15ba94aa.e186750d.js +1 -0
  43. solace_agent_mesh/assets/docs/assets/js/15e40e79.434bb30f.js +1 -0
  44. solace_agent_mesh/assets/docs/assets/js/17896441.e612dfb4.js +1 -0
  45. solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js +2 -0
  46. solace_agent_mesh/assets/docs/assets/js/{17896441.a5e82f9b.js.LICENSE.txt → 2279.550aa580.js.LICENSE.txt} +6 -0
  47. solace_agent_mesh/assets/docs/assets/js/240a0364.83e37aa8.js +1 -0
  48. solace_agent_mesh/assets/docs/assets/js/2e32b5e0.2f0db237.js +1 -0
  49. solace_agent_mesh/assets/docs/assets/js/3a6c6137.7e61915d.js +1 -0
  50. solace_agent_mesh/assets/docs/assets/js/3ac1795d.7f7ab1c1.js +1 -0
  51. solace_agent_mesh/assets/docs/assets/js/3ff0015d.e53c9b78.js +1 -0
  52. solace_agent_mesh/assets/docs/assets/js/41adc471.0e95b87c.js +1 -0
  53. solace_agent_mesh/assets/docs/assets/js/4667dc50.bf2ad456.js +1 -0
  54. solace_agent_mesh/assets/docs/assets/js/49eed117.493d6f99.js +1 -0
  55. solace_agent_mesh/assets/docs/assets/js/{509e993c.4c7a1a6d.js → 509e993c.a1fbf45a.js} +1 -1
  56. solace_agent_mesh/assets/docs/assets/js/547e15cc.8e6da617.js +1 -0
  57. solace_agent_mesh/assets/docs/assets/js/55b7b518.29d6e75d.js +1 -0
  58. solace_agent_mesh/assets/docs/assets/js/5b8d9c11.d4eb37b8.js +1 -0
  59. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.1ee87753.js +1 -0
  60. solace_agent_mesh/assets/docs/assets/js/60702c0e.a8bdd79b.js +1 -0
  61. solace_agent_mesh/assets/docs/assets/js/64195356.09dbd087.js +1 -0
  62. solace_agent_mesh/assets/docs/assets/js/66d4869e.30340bd3.js +1 -0
  63. solace_agent_mesh/assets/docs/assets/js/6aaedf65.7253541d.js +1 -0
  64. solace_agent_mesh/assets/docs/assets/js/6d84eae0.fd23ba4a.js +1 -0
  65. solace_agent_mesh/assets/docs/assets/js/729898df.7249e9fd.js +1 -0
  66. solace_agent_mesh/assets/docs/assets/js/7e294c01.7c5f6906.js +1 -0
  67. solace_agent_mesh/assets/docs/assets/js/8024126c.e3467286.js +1 -0
  68. solace_agent_mesh/assets/docs/assets/js/81a99df0.7ed65d45.js +1 -0
  69. solace_agent_mesh/assets/docs/assets/js/82fbfb93.161823a5.js +1 -0
  70. solace_agent_mesh/assets/docs/assets/js/924ffdeb.975e428a.js +1 -0
  71. solace_agent_mesh/assets/docs/assets/js/94e8668d.16083b3f.js +1 -0
  72. solace_agent_mesh/assets/docs/assets/js/9bb13469.4523ae20.js +1 -0
  73. solace_agent_mesh/assets/docs/assets/js/a7d42657.a956689d.js +1 -0
  74. solace_agent_mesh/assets/docs/assets/js/a94703ab.3e5fbcb3.js +1 -0
  75. solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e563275.js +1 -0
  76. solace_agent_mesh/assets/docs/assets/js/c93cbaa0.0e0d8baf.js +1 -0
  77. solace_agent_mesh/assets/docs/assets/js/cab03b5b.6a073091.js +1 -0
  78. solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.07e170dd.js +1 -0
  79. solace_agent_mesh/assets/docs/assets/js/e04b235d.06d23db6.js +1 -0
  80. solace_agent_mesh/assets/docs/assets/js/e1b6eeb4.deb2b62e.js +1 -0
  81. solace_agent_mesh/assets/docs/assets/js/e3d9abda.1476f570.js +1 -0
  82. solace_agent_mesh/assets/docs/assets/js/e6f9706b.acc800d3.js +1 -0
  83. solace_agent_mesh/assets/docs/assets/js/e92d0134.c147a429.js +1 -0
  84. solace_agent_mesh/assets/docs/assets/js/ee0c2fe7.94d0a351.js +1 -0
  85. solace_agent_mesh/assets/docs/assets/js/f284c35a.cc97854c.js +1 -0
  86. solace_agent_mesh/assets/docs/assets/js/main.d634009f.js +2 -0
  87. solace_agent_mesh/assets/docs/assets/js/runtime~main.27bb82a7.js +1 -0
  88. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +68 -68
  89. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +50 -50
  90. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +42 -42
  91. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +55 -55
  92. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +75 -75
  93. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/image-tools/index.html +81 -0
  94. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +67 -50
  95. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/research-tools/index.html +136 -0
  96. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +178 -144
  97. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +43 -42
  98. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +20 -18
  99. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +23 -23
  100. solace_agent_mesh/assets/docs/docs/documentation/components/platform-service/index.html +33 -0
  101. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +45 -45
  102. solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +98 -112
  103. solace_agent_mesh/assets/docs/docs/documentation/components/prompts/index.html +147 -0
  104. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +208 -125
  105. solace_agent_mesh/assets/docs/docs/documentation/components/speech/index.html +52 -0
  106. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +28 -28
  107. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +29 -29
  108. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +14 -14
  109. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/index.html +47 -0
  110. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/kubernetes-deployment-guide/index.html +197 -0
  111. solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +67 -53
  112. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +17 -17
  113. solace_agent_mesh/assets/docs/docs/documentation/deploying/proxy_configuration/index.html +49 -0
  114. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +38 -38
  115. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +87 -87
  116. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +67 -49
  117. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +17 -17
  118. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +51 -51
  119. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +22 -22
  120. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +27 -27
  121. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +135 -135
  122. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +66 -66
  123. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +51 -51
  124. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +50 -38
  125. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +86 -86
  126. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +51 -51
  127. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +24 -24
  128. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +30 -30
  129. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +44 -44
  130. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/teams-integration/index.html +115 -0
  131. solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +50 -23
  132. solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +29 -24
  133. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +21 -21
  134. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +40 -37
  135. solace_agent_mesh/assets/docs/docs/documentation/enterprise/openapi-tools/index.html +324 -0
  136. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +96 -66
  137. solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +181 -181
  138. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +75 -75
  139. solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +27 -27
  140. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +44 -44
  141. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +39 -38
  142. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +30 -30
  143. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +18 -18
  144. solace_agent_mesh/assets/docs/docs/documentation/getting-started/vibe_coding/index.html +62 -0
  145. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +135 -114
  146. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +37 -37
  147. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +14 -14
  148. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +27 -25
  149. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +69 -69
  150. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +72 -72
  151. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +112 -112
  152. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +28 -28
  153. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +42 -42
  154. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +20 -20
  155. solace_agent_mesh/assets/docs/docs/documentation/migrations/platform-service-split/index.html +85 -0
  156. solace_agent_mesh/assets/docs/lunr-index-1768329217460.json +1 -0
  157. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  158. solace_agent_mesh/assets/docs/search-doc-1768329217460.json +1 -0
  159. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  160. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  161. solace_agent_mesh/cli/__init__.py +1 -1
  162. solace_agent_mesh/cli/commands/add_cmd/__init__.py +3 -1
  163. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -1
  164. solace_agent_mesh/cli/commands/add_cmd/proxy_cmd.py +100 -0
  165. solace_agent_mesh/cli/commands/eval_cmd.py +1 -1
  166. solace_agent_mesh/cli/commands/init_cmd/__init__.py +15 -0
  167. solace_agent_mesh/cli/commands/init_cmd/directory_step.py +1 -1
  168. solace_agent_mesh/cli/commands/init_cmd/env_step.py +30 -3
  169. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +3 -4
  170. solace_agent_mesh/cli/commands/init_cmd/platform_service_step.py +85 -0
  171. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +16 -3
  172. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +2 -1
  173. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +1 -0
  174. solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +3 -3
  175. solace_agent_mesh/cli/commands/run_cmd.py +64 -49
  176. solace_agent_mesh/cli/commands/tools_cmd.py +315 -0
  177. solace_agent_mesh/cli/main.py +15 -0
  178. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-tcIFZLis.js → authCallback-KnKMP_vb.js} +1 -1
  179. solace_agent_mesh/client/webui/frontend/static/assets/client-DpBL2stg.js +25 -0
  180. solace_agent_mesh/client/webui/frontend/static/assets/main-Cd498TV2.js +435 -0
  181. solace_agent_mesh/client/webui/frontend/static/assets/main-rSf8Vu29.css +1 -0
  182. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-CINwxvwV.js → vendor-CGk8Suyh.js} +189 -94
  183. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  184. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  185. solace_agent_mesh/client/webui/frontend/static/mockServiceWorker.js +336 -0
  186. solace_agent_mesh/client/webui/frontend/static/ui-version.json +6 -0
  187. solace_agent_mesh/common/a2a/types.py +1 -1
  188. solace_agent_mesh/common/agent_registry.py +38 -11
  189. solace_agent_mesh/common/data_parts.py +124 -0
  190. solace_agent_mesh/common/error_handlers.py +83 -0
  191. solace_agent_mesh/common/exceptions.py +24 -0
  192. solace_agent_mesh/common/oauth/__init__.py +17 -0
  193. solace_agent_mesh/common/oauth/oauth_client.py +408 -0
  194. solace_agent_mesh/common/oauth/utils.py +50 -0
  195. solace_agent_mesh/common/rag_dto.py +156 -0
  196. solace_agent_mesh/common/sac/sam_component_base.py +73 -1
  197. solace_agent_mesh/common/sam_events/event_service.py +2 -2
  198. solace_agent_mesh/common/utils/embeds/converter.py +1 -8
  199. solace_agent_mesh/common/utils/embeds/modifiers.py +2 -27
  200. solace_agent_mesh/common/utils/embeds/resolver.py +94 -25
  201. solace_agent_mesh/common/utils/embeds/types.py +1 -0
  202. solace_agent_mesh/common/utils/log_formatters.py +20 -0
  203. solace_agent_mesh/common/utils/mime_helpers.py +12 -5
  204. solace_agent_mesh/common/utils/rbac_utils.py +69 -0
  205. solace_agent_mesh/common/utils/templates/__init__.py +8 -0
  206. solace_agent_mesh/common/utils/templates/liquid_renderer.py +210 -0
  207. solace_agent_mesh/common/utils/templates/template_resolver.py +161 -0
  208. solace_agent_mesh/config_portal/backend/common.py +12 -0
  209. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-CljP4_mv.js +103 -0
  210. solace_agent_mesh/config_portal/frontend/static/client/assets/{components-Rk0n-9cK.js → components-CaC6hG8d.js} +22 -22
  211. solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-mvZjNKiz.js → entry.client-H_TM0YBt.js} +3 -3
  212. solace_agent_mesh/config_portal/frontend/static/client/assets/{index-DzNKzXrc.js → index-CnFykb2v.js} +16 -16
  213. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-f8439d40.js +1 -0
  214. solace_agent_mesh/config_portal/frontend/static/client/assets/root-BIMqslJB.css +1 -0
  215. solace_agent_mesh/config_portal/frontend/static/client/assets/root-mJmTIdIk.js +10 -0
  216. solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
  217. solace_agent_mesh/core_a2a/service.py +3 -2
  218. solace_agent_mesh/gateway/adapter/base.py +28 -1
  219. solace_agent_mesh/gateway/adapter/types.py +9 -0
  220. solace_agent_mesh/gateway/base/app.py +10 -0
  221. solace_agent_mesh/gateway/base/auth_interface.py +103 -0
  222. solace_agent_mesh/gateway/base/component.py +451 -10
  223. solace_agent_mesh/gateway/generic/component.py +274 -30
  224. solace_agent_mesh/gateway/http_sse/alembic/env.py +0 -7
  225. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +2 -43
  226. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +2 -2
  227. solace_agent_mesh/gateway/http_sse/alembic/versions/20251108_create_prompt_tables_with_sharing.py +154 -0
  228. solace_agent_mesh/gateway/http_sse/alembic/versions/20251115_add_parent_task_id.py +32 -0
  229. solace_agent_mesh/gateway/http_sse/alembic/versions/20251126_add_background_task_fields.py +47 -0
  230. solace_agent_mesh/gateway/http_sse/alembic/versions/20251202_add_versioned_fields_to_prompts.py +52 -0
  231. solace_agent_mesh/gateway/http_sse/alembic.ini +0 -36
  232. solace_agent_mesh/gateway/http_sse/app.py +23 -6
  233. solace_agent_mesh/gateway/http_sse/component.py +158 -73
  234. solace_agent_mesh/gateway/http_sse/dependencies.py +50 -57
  235. solace_agent_mesh/gateway/http_sse/main.py +58 -482
  236. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +2 -2
  237. solace_agent_mesh/gateway/http_sse/repository/entities/project.py +1 -1
  238. solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +1 -1
  239. solace_agent_mesh/gateway/http_sse/repository/entities/session.py +3 -2
  240. solace_agent_mesh/gateway/http_sse/repository/entities/task.py +7 -0
  241. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +2 -2
  242. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +2 -2
  243. solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +5 -0
  244. solace_agent_mesh/gateway/http_sse/repository/models/prompt_model.py +159 -0
  245. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +1 -1
  246. solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +8 -1
  247. solace_agent_mesh/gateway/http_sse/repository/project_repository.py +1 -1
  248. solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +1 -1
  249. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +12 -107
  250. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +86 -2
  251. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +38 -7
  252. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +113 -7
  253. solace_agent_mesh/gateway/http_sse/routers/auth.py +69 -132
  254. solace_agent_mesh/gateway/http_sse/routers/config.py +235 -10
  255. solace_agent_mesh/gateway/http_sse/routers/dto/project_dto.py +69 -0
  256. solace_agent_mesh/gateway/http_sse/routers/dto/prompt_dto.py +255 -0
  257. solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +1 -1
  258. solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +1 -1
  259. solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +1 -0
  260. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +3 -2
  261. solace_agent_mesh/gateway/http_sse/routers/dto/responses/version_responses.py +31 -0
  262. solace_agent_mesh/gateway/http_sse/routers/feedback.py +2 -2
  263. solace_agent_mesh/gateway/http_sse/routers/people.py +2 -2
  264. solace_agent_mesh/gateway/http_sse/routers/projects.py +250 -24
  265. solace_agent_mesh/gateway/http_sse/routers/prompts.py +1416 -0
  266. solace_agent_mesh/gateway/http_sse/routers/sessions.py +14 -5
  267. solace_agent_mesh/gateway/http_sse/routers/speech.py +355 -0
  268. solace_agent_mesh/gateway/http_sse/routers/sse.py +117 -4
  269. solace_agent_mesh/gateway/http_sse/routers/tasks.py +509 -149
  270. solace_agent_mesh/gateway/http_sse/routers/users.py +1 -1
  271. solace_agent_mesh/gateway/http_sse/routers/version.py +343 -0
  272. solace_agent_mesh/gateway/http_sse/routers/visualization.py +2 -1
  273. solace_agent_mesh/gateway/http_sse/services/audio_service.py +1227 -0
  274. solace_agent_mesh/gateway/http_sse/services/background_task_monitor.py +186 -0
  275. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +1 -1
  276. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +1 -1
  277. solace_agent_mesh/gateway/http_sse/services/project_service.py +539 -12
  278. solace_agent_mesh/gateway/http_sse/services/prompt_builder_assistant.py +303 -0
  279. solace_agent_mesh/gateway/http_sse/services/session_service.py +198 -21
  280. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +354 -4
  281. solace_agent_mesh/gateway/http_sse/sse_manager.py +280 -169
  282. solace_agent_mesh/gateway/http_sse/utils/artifact_copy_utils.py +370 -0
  283. solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +41 -1
  284. solace_agent_mesh/services/__init__.py +0 -0
  285. solace_agent_mesh/services/platform/__init__.py +29 -0
  286. solace_agent_mesh/services/platform/alembic/env.py +85 -0
  287. solace_agent_mesh/services/platform/alembic/script.py.mako +28 -0
  288. solace_agent_mesh/services/platform/alembic.ini +109 -0
  289. solace_agent_mesh/services/platform/api/__init__.py +3 -0
  290. solace_agent_mesh/services/platform/api/dependencies.py +154 -0
  291. solace_agent_mesh/services/platform/api/main.py +314 -0
  292. solace_agent_mesh/services/platform/api/middleware.py +51 -0
  293. solace_agent_mesh/services/platform/api/routers/__init__.py +33 -0
  294. solace_agent_mesh/services/platform/api/routers/health_router.py +31 -0
  295. solace_agent_mesh/services/platform/app.py +215 -0
  296. solace_agent_mesh/services/platform/component.py +777 -0
  297. solace_agent_mesh/shared/__init__.py +14 -0
  298. solace_agent_mesh/shared/api/__init__.py +42 -0
  299. solace_agent_mesh/shared/auth/__init__.py +26 -0
  300. solace_agent_mesh/shared/auth/dependencies.py +204 -0
  301. solace_agent_mesh/shared/auth/middleware.py +347 -0
  302. solace_agent_mesh/shared/database/__init__.py +20 -0
  303. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/base_repository.py +1 -1
  304. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_exceptions.py +1 -1
  305. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_helpers.py +1 -1
  306. solace_agent_mesh/shared/exceptions/__init__.py +36 -0
  307. solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exception_handlers.py +1 -1
  308. solace_agent_mesh/shared/utils/__init__.py +21 -0
  309. solace_agent_mesh/templates/logging_config_template.yaml +48 -0
  310. solace_agent_mesh/templates/main_orchestrator.yaml +12 -1
  311. solace_agent_mesh/templates/platform.yaml +49 -0
  312. solace_agent_mesh/templates/plugin_readme_template.md +3 -25
  313. solace_agent_mesh/templates/plugin_tool_config_template.yaml +109 -0
  314. solace_agent_mesh/templates/proxy_template.yaml +62 -0
  315. solace_agent_mesh/templates/webui.yaml +148 -6
  316. solace_agent_mesh/tools/web_search/__init__.py +18 -0
  317. solace_agent_mesh/tools/web_search/base.py +84 -0
  318. solace_agent_mesh/tools/web_search/google_search.py +247 -0
  319. solace_agent_mesh/tools/web_search/models.py +99 -0
  320. {solace_agent_mesh-1.7.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/METADATA +29 -8
  321. {solace_agent_mesh-1.7.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/RECORD +334 -313
  322. {solace_agent_mesh-1.7.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/WHEEL +1 -1
  323. solace_agent_mesh/agent/adk/adk_llm.txt +0 -226
  324. solace_agent_mesh/agent/adk/adk_llm_detail.txt +0 -566
  325. solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +0 -171
  326. solace_agent_mesh/agent/adk/models/models_llm.txt +0 -189
  327. solace_agent_mesh/agent/agent_llm.txt +0 -369
  328. solace_agent_mesh/agent/agent_llm_detail.txt +0 -1702
  329. solace_agent_mesh/agent/protocol/protocol_llm.txt +0 -81
  330. solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +0 -92
  331. solace_agent_mesh/agent/proxies/a2a/a2a_llm.txt +0 -190
  332. solace_agent_mesh/agent/proxies/base/base_llm.txt +0 -148
  333. solace_agent_mesh/agent/proxies/proxies_llm.txt +0 -283
  334. solace_agent_mesh/agent/sac/sac_llm.txt +0 -189
  335. solace_agent_mesh/agent/sac/sac_llm_detail.txt +0 -200
  336. solace_agent_mesh/agent/testing/testing_llm.txt +0 -58
  337. solace_agent_mesh/agent/testing/testing_llm_detail.txt +0 -68
  338. solace_agent_mesh/agent/tools/tools_llm.txt +0 -276
  339. solace_agent_mesh/agent/tools/tools_llm_detail.txt +0 -275
  340. solace_agent_mesh/agent/utils/utils_llm.txt +0 -152
  341. solace_agent_mesh/agent/utils/utils_llm_detail.txt +0 -149
  342. solace_agent_mesh/assets/docs/assets/js/05749d90.c70b2be9.js +0 -1
  343. solace_agent_mesh/assets/docs/assets/js/15ba94aa.92fea363.js +0 -1
  344. solace_agent_mesh/assets/docs/assets/js/15e40e79.36003774.js +0 -1
  345. solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +0 -2
  346. solace_agent_mesh/assets/docs/assets/js/240a0364.c39f8388.js +0 -1
  347. solace_agent_mesh/assets/docs/assets/js/2e32b5e0.33f5d75b.js +0 -1
  348. solace_agent_mesh/assets/docs/assets/js/3a6c6137.f5940cfa.js +0 -1
  349. solace_agent_mesh/assets/docs/assets/js/3ac1795d.e4870a49.js +0 -1
  350. solace_agent_mesh/assets/docs/assets/js/3ff0015d.b63ee53a.js +0 -1
  351. solace_agent_mesh/assets/docs/assets/js/547e15cc.2f7790c1.js +0 -1
  352. solace_agent_mesh/assets/docs/assets/js/55b7b518.f2b1d1ba.js +0 -1
  353. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.45b32c2b.js +0 -1
  354. solace_agent_mesh/assets/docs/assets/js/64195356.c498c4d0.js +0 -1
  355. solace_agent_mesh/assets/docs/assets/js/66d4869e.830d443f.js +0 -1
  356. solace_agent_mesh/assets/docs/assets/js/6d84eae0.4a5fbf39.js +0 -1
  357. solace_agent_mesh/assets/docs/assets/js/8024126c.fa0e7186.js +0 -1
  358. solace_agent_mesh/assets/docs/assets/js/81a99df0.07034dd9.js +0 -1
  359. solace_agent_mesh/assets/docs/assets/js/82fbfb93.139a1a1f.js +0 -1
  360. solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +0 -1
  361. solace_agent_mesh/assets/docs/assets/js/94e8668d.09ed9234.js +0 -1
  362. solace_agent_mesh/assets/docs/assets/js/9bb13469.dd1c9b54.js +0 -1
  363. solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +0 -1
  364. solace_agent_mesh/assets/docs/assets/js/ab9708a8.245ae0ef.js +0 -1
  365. solace_agent_mesh/assets/docs/assets/js/c93cbaa0.eaff365e.js +0 -1
  366. solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.f902fad8.js +0 -1
  367. solace_agent_mesh/assets/docs/assets/js/db5d6442.3daf1696.js +0 -1
  368. solace_agent_mesh/assets/docs/assets/js/e04b235d.c9c50c7b.js +0 -1
  369. solace_agent_mesh/assets/docs/assets/js/e3d9abda.d11c67a7.js +0 -1
  370. solace_agent_mesh/assets/docs/assets/js/e6f9706b.045d0fa1.js +0 -1
  371. solace_agent_mesh/assets/docs/assets/js/e92d0134.3bda61dd.js +0 -1
  372. solace_agent_mesh/assets/docs/assets/js/f284c35a.5099c51e.js +0 -1
  373. solace_agent_mesh/assets/docs/assets/js/main.f213fe0c.js +0 -2
  374. solace_agent_mesh/assets/docs/assets/js/runtime~main.d9606d6a.js +0 -1
  375. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes-deployment/index.html +0 -47
  376. solace_agent_mesh/assets/docs/lunr-index-1762283454666.json +0 -1
  377. solace_agent_mesh/assets/docs/search-doc-1762283454666.json +0 -1
  378. solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +0 -250
  379. solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +0 -365
  380. solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +0 -305
  381. solace_agent_mesh/client/webui/frontend/static/assets/client-CRYdKo2Q.js +0 -25
  382. solace_agent_mesh/client/webui/frontend/static/assets/main-CojeY_1w.css +0 -1
  383. solace_agent_mesh/client/webui/frontend/static/assets/main-ILja9MCG.js +0 -353
  384. solace_agent_mesh/common/a2a/a2a_llm.txt +0 -175
  385. solace_agent_mesh/common/a2a/a2a_llm_detail.txt +0 -193
  386. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +0 -445
  387. solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +0 -736
  388. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +0 -330
  389. solace_agent_mesh/common/common_llm.txt +0 -230
  390. solace_agent_mesh/common/common_llm_detail.txt +0 -2562
  391. solace_agent_mesh/common/middleware/middleware_llm.txt +0 -174
  392. solace_agent_mesh/common/middleware/middleware_llm_detail.txt +0 -185
  393. solace_agent_mesh/common/sac/sac_llm.txt +0 -71
  394. solace_agent_mesh/common/sac/sac_llm_detail.txt +0 -82
  395. solace_agent_mesh/common/sam_events/sam_events_llm.txt +0 -104
  396. solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +0 -115
  397. solace_agent_mesh/common/services/providers/providers_llm.txt +0 -81
  398. solace_agent_mesh/common/services/services_llm.txt +0 -368
  399. solace_agent_mesh/common/services/services_llm_detail.txt +0 -459
  400. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +0 -220
  401. solace_agent_mesh/common/utils/utils_llm.txt +0 -335
  402. solace_agent_mesh/common/utils/utils_llm_detail.txt +0 -572
  403. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +0 -98
  404. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-61038fc6.js +0 -1
  405. solace_agent_mesh/config_portal/frontend/static/client/assets/root-BWvk5-gF.js +0 -10
  406. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +0 -1
  407. solace_agent_mesh/core_a2a/core_a2a_llm.txt +0 -90
  408. solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +0 -101
  409. solace_agent_mesh/gateway/base/base_llm.txt +0 -226
  410. solace_agent_mesh/gateway/base/base_llm_detail.txt +0 -235
  411. solace_agent_mesh/gateway/gateway_llm.txt +0 -369
  412. solace_agent_mesh/gateway/gateway_llm_detail.txt +0 -3885
  413. solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +0 -345
  414. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_fulltext_search_indexes.py +0 -92
  415. solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +0 -161
  416. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +0 -105
  417. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +0 -299
  418. solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +0 -3278
  419. solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +0 -221
  420. solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +0 -257
  421. solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +0 -308
  422. solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +0 -450
  423. solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +0 -133
  424. solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +0 -123
  425. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +0 -312
  426. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +0 -303
  427. solace_agent_mesh/gateway/http_sse/shared/__init__.py +0 -146
  428. solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +0 -319
  429. solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +0 -47
  430. solace_agent_mesh/llm.txt +0 -228
  431. solace_agent_mesh/llm_detail.txt +0 -2835
  432. solace_agent_mesh/solace_agent_mesh_llm.txt +0 -362
  433. solace_agent_mesh/solace_agent_mesh_llm_detail.txt +0 -8599
  434. solace_agent_mesh/templates/logging_config_template.ini +0 -45
  435. solace_agent_mesh/templates/templates_llm.txt +0 -147
  436. /solace_agent_mesh/assets/docs/assets/js/{main.f213fe0c.js.LICENSE.txt → main.d634009f.js.LICENSE.txt} +0 -0
  437. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/auth_utils.py +0 -0
  438. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/pagination.py +0 -0
  439. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/response_utils.py +0 -0
  440. /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/error_dto.py +0 -0
  441. /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exceptions.py +0 -0
  442. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/enums.py +0 -0
  443. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/timestamp_utils.py +0 -0
  444. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/types.py +0 -0
  445. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/utils.py +0 -0
  446. {solace_agent_mesh-1.7.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/entry_points.txt +0 -0
  447. {solace_agent_mesh-1.7.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
+ )