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.
- solace_agent_mesh/agent/adk/alembic/README +74 -0
- solace_agent_mesh/agent/adk/alembic/env.py +77 -0
- solace_agent_mesh/agent/adk/alembic/script.py.mako +28 -0
- solace_agent_mesh/agent/adk/alembic/versions/e2902798564d_adk_session_db_upgrade.py +52 -0
- solace_agent_mesh/agent/adk/alembic.ini +112 -0
- solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
- solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +165 -1
- solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +163 -0
- solace_agent_mesh/agent/adk/callbacks.py +852 -109
- solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +234 -36
- solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +52 -5
- solace_agent_mesh/agent/adk/mcp_content_processor.py +1 -1
- solace_agent_mesh/agent/adk/models/lite_llm.py +77 -21
- solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +24 -137
- solace_agent_mesh/agent/adk/runner.py +85 -20
- solace_agent_mesh/agent/adk/schema_migration.py +88 -0
- solace_agent_mesh/agent/adk/services.py +94 -18
- solace_agent_mesh/agent/adk/setup.py +281 -65
- solace_agent_mesh/agent/adk/stream_parser.py +231 -37
- solace_agent_mesh/agent/adk/tool_wrapper.py +3 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +472 -137
- solace_agent_mesh/agent/proxies/a2a/app.py +3 -2
- solace_agent_mesh/agent/proxies/a2a/component.py +572 -75
- solace_agent_mesh/agent/proxies/a2a/config.py +80 -4
- solace_agent_mesh/agent/proxies/base/app.py +3 -2
- solace_agent_mesh/agent/proxies/base/component.py +188 -22
- solace_agent_mesh/agent/proxies/base/proxy_task_context.py +3 -1
- solace_agent_mesh/agent/sac/app.py +91 -3
- solace_agent_mesh/agent/sac/component.py +591 -157
- solace_agent_mesh/agent/sac/patch_adk.py +8 -16
- solace_agent_mesh/agent/sac/task_execution_context.py +146 -4
- solace_agent_mesh/agent/tools/__init__.py +3 -0
- solace_agent_mesh/agent/tools/audio_tools.py +3 -3
- solace_agent_mesh/agent/tools/builtin_artifact_tools.py +710 -171
- solace_agent_mesh/agent/tools/deep_research_tools.py +2161 -0
- solace_agent_mesh/agent/tools/dynamic_tool.py +2 -0
- solace_agent_mesh/agent/tools/peer_agent_tool.py +82 -15
- solace_agent_mesh/agent/tools/time_tools.py +126 -0
- solace_agent_mesh/agent/tools/tool_config_types.py +57 -2
- solace_agent_mesh/agent/tools/web_search_tools.py +279 -0
- solace_agent_mesh/agent/tools/web_tools.py +125 -17
- solace_agent_mesh/agent/utils/artifact_helpers.py +248 -6
- solace_agent_mesh/agent/utils/context_helpers.py +17 -0
- solace_agent_mesh/assets/docs/404.html +6 -6
- solace_agent_mesh/assets/docs/assets/css/{styles.906a1503.css → styles.8162edfb.css} +1 -1
- solace_agent_mesh/assets/docs/assets/js/05749d90.19ac4f35.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15ba94aa.e186750d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15e40e79.434bb30f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/17896441.e612dfb4.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/{17896441.a5e82f9b.js.LICENSE.txt → 2279.550aa580.js.LICENSE.txt} +6 -0
- solace_agent_mesh/assets/docs/assets/js/240a0364.83e37aa8.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2e32b5e0.2f0db237.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3a6c6137.7e61915d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ac1795d.7f7ab1c1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ff0015d.e53c9b78.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/41adc471.0e95b87c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4667dc50.bf2ad456.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/49eed117.493d6f99.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{509e993c.4c7a1a6d.js → 509e993c.a1fbf45a.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/547e15cc.8e6da617.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/55b7b518.29d6e75d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5b8d9c11.d4eb37b8.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.1ee87753.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/60702c0e.a8bdd79b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/64195356.09dbd087.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/66d4869e.30340bd3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6a520c9d.b6e3f2ce.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6aaedf65.7253541d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6d84eae0.fd23ba4a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/71da7b71.374b9d54.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/729898df.7249e9fd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7e294c01.7c5f6906.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8024126c.e3467286.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/81a99df0.7ed65d45.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/82fbfb93.161823a5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.975e428a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/94e8668d.16083b3f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9bb13469.4523ae20.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a7d42657.a956689d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a94703ab.3e5fbcb3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e563275.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/c93cbaa0.0e0d8baf.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cab03b5b.6a073091.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.07e170dd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/dd817ffc.c37a755e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/dd81e2b8.b682e9c2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/de915948.44a432bc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e04b235d.06d23db6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e1b6eeb4.deb2b62e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e3d9abda.1476f570.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.acc800d3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e92d0134.c147a429.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ee0c2fe7.94d0a351.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.cc97854c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.d634009f.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.27bb82a7.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +68 -68
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +50 -50
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +42 -42
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +55 -55
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +82 -68
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/image-tools/index.html +81 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +67 -50
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/research-tools/index.html +136 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +178 -144
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +43 -42
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +20 -18
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +23 -23
- solace_agent_mesh/assets/docs/docs/documentation/components/platform-service/index.html +33 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +45 -45
- solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +182 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/prompts/index.html +147 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +208 -125
- solace_agent_mesh/assets/docs/docs/documentation/components/speech/index.html +52 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +28 -49
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +29 -30
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +14 -14
- solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/index.html +47 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/kubernetes-deployment-guide/index.html +197 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +90 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +17 -16
- solace_agent_mesh/assets/docs/docs/documentation/deploying/proxy_configuration/index.html +49 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +38 -38
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +162 -171
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +67 -49
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +17 -17
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +22 -22
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +27 -27
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +135 -135
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +66 -66
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +50 -38
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +86 -86
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +24 -24
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +30 -30
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +44 -44
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/teams-integration/index.html +115 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +86 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +67 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +23 -19
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +40 -37
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/openapi-tools/index.html +324 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +112 -87
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +440 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +87 -64
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +62 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +44 -44
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +39 -37
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +30 -30
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +18 -18
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/vibe_coding/index.html +62 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +311 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +39 -42
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +14 -14
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +27 -25
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +69 -69
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +72 -72
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +251 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +88 -0
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +42 -42
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +20 -20
- solace_agent_mesh/assets/docs/docs/documentation/migrations/platform-service-split/index.html +85 -0
- solace_agent_mesh/assets/docs/lunr-index-1768329217460.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1768329217460.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/assets/docs/sitemap.xml +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/__init__.py +3 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -1
- solace_agent_mesh/cli/commands/add_cmd/proxy_cmd.py +100 -0
- solace_agent_mesh/cli/commands/docs_cmd.py +4 -1
- solace_agent_mesh/cli/commands/eval_cmd.py +1 -1
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +15 -0
- solace_agent_mesh/cli/commands/init_cmd/directory_step.py +1 -1
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +30 -3
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +3 -4
- solace_agent_mesh/cli/commands/init_cmd/platform_service_step.py +85 -0
- solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +16 -3
- solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +2 -1
- solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +1 -0
- solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +3 -3
- solace_agent_mesh/cli/commands/run_cmd.py +64 -49
- solace_agent_mesh/cli/commands/tools_cmd.py +315 -0
- solace_agent_mesh/cli/main.py +15 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-BTf6dqwp.js → authCallback-KnKMP_vb.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/client-DpBL2stg.js +25 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-Cd498TV2.js +435 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-rSf8Vu29.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-CGk8Suyh.js +565 -0
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
- solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
- solace_agent_mesh/client/webui/frontend/static/mockServiceWorker.js +336 -0
- solace_agent_mesh/client/webui/frontend/static/ui-version.json +6 -0
- solace_agent_mesh/common/a2a/events.py +2 -1
- solace_agent_mesh/common/a2a/protocol.py +5 -0
- solace_agent_mesh/common/a2a/types.py +2 -1
- solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +23 -6
- solace_agent_mesh/common/a2a_spec/schemas/feedback_event.json +51 -0
- solace_agent_mesh/common/agent_registry.py +38 -11
- solace_agent_mesh/common/data_parts.py +144 -4
- solace_agent_mesh/common/error_handlers.py +83 -0
- solace_agent_mesh/common/exceptions.py +24 -0
- solace_agent_mesh/common/oauth/__init__.py +17 -0
- solace_agent_mesh/common/oauth/oauth_client.py +408 -0
- solace_agent_mesh/common/oauth/utils.py +50 -0
- solace_agent_mesh/common/rag_dto.py +156 -0
- solace_agent_mesh/common/sac/sam_component_base.py +97 -19
- solace_agent_mesh/common/sam_events/event_service.py +2 -2
- solace_agent_mesh/common/services/employee_service.py +1 -1
- solace_agent_mesh/common/utils/embeds/constants.py +1 -0
- solace_agent_mesh/common/utils/embeds/converter.py +1 -8
- solace_agent_mesh/common/utils/embeds/modifiers.py +4 -28
- solace_agent_mesh/common/utils/embeds/resolver.py +152 -31
- solace_agent_mesh/common/utils/embeds/types.py +9 -0
- solace_agent_mesh/common/utils/log_formatters.py +20 -0
- solace_agent_mesh/common/utils/mime_helpers.py +12 -5
- solace_agent_mesh/common/utils/pydantic_utils.py +90 -3
- solace_agent_mesh/common/utils/rbac_utils.py +69 -0
- solace_agent_mesh/common/utils/templates/__init__.py +8 -0
- solace_agent_mesh/common/utils/templates/liquid_renderer.py +210 -0
- solace_agent_mesh/common/utils/templates/template_resolver.py +161 -0
- solace_agent_mesh/config_portal/backend/common.py +12 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-CljP4_mv.js +103 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/{components-Rk0n-9cK.js → components-CaC6hG8d.js} +22 -22
- solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-mvZjNKiz.js → entry.client-H_TM0YBt.js} +3 -3
- solace_agent_mesh/config_portal/frontend/static/client/assets/{index-DzNKzXrc.js → index-CnFykb2v.js} +16 -16
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-f8439d40.js +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-BIMqslJB.css +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-mJmTIdIk.js +10 -0
- solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
- solace_agent_mesh/core_a2a/service.py +3 -2
- solace_agent_mesh/gateway/adapter/__init__.py +1 -0
- solace_agent_mesh/gateway/adapter/base.py +170 -0
- solace_agent_mesh/gateway/adapter/types.py +230 -0
- solace_agent_mesh/gateway/base/app.py +39 -2
- solace_agent_mesh/gateway/base/auth_interface.py +103 -0
- solace_agent_mesh/gateway/base/component.py +1027 -151
- solace_agent_mesh/gateway/generic/__init__.py +1 -0
- solace_agent_mesh/gateway/generic/app.py +50 -0
- solace_agent_mesh/gateway/generic/component.py +894 -0
- solace_agent_mesh/gateway/http_sse/alembic/env.py +0 -7
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_project_users_table.py +72 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +109 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +26 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_projects_table.py +135 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251108_create_prompt_tables_with_sharing.py +154 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251115_add_parent_task_id.py +32 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251126_add_background_task_fields.py +47 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251202_add_versioned_fields_to_prompts.py +52 -0
- solace_agent_mesh/gateway/http_sse/alembic.ini +0 -36
- solace_agent_mesh/gateway/http_sse/app.py +40 -11
- solace_agent_mesh/gateway/http_sse/component.py +285 -160
- solace_agent_mesh/gateway/http_sse/dependencies.py +149 -114
- solace_agent_mesh/gateway/http_sse/main.py +68 -450
- solace_agent_mesh/gateway/http_sse/repository/__init__.py +19 -1
- solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +2 -2
- solace_agent_mesh/gateway/http_sse/repository/entities/project.py +81 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +47 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session.py +26 -3
- solace_agent_mesh/gateway/http_sse/repository/entities/task.py +7 -0
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +47 -0
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +114 -6
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +13 -0
- solace_agent_mesh/gateway/http_sse/repository/models/project_model.py +51 -0
- solace_agent_mesh/gateway/http_sse/repository/models/project_user_model.py +75 -0
- solace_agent_mesh/gateway/http_sse/repository/models/prompt_model.py +159 -0
- solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +8 -2
- solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +8 -1
- solace_agent_mesh/gateway/http_sse/repository/project_repository.py +172 -0
- solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +186 -0
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +177 -11
- solace_agent_mesh/gateway/http_sse/repository/task_repository.py +86 -2
- solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +38 -7
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +256 -58
- solace_agent_mesh/gateway/http_sse/routers/auth.py +168 -134
- solace_agent_mesh/gateway/http_sse/routers/config.py +302 -8
- solace_agent_mesh/gateway/http_sse/routers/dto/project_dto.py +69 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/prompt_dto.py +255 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +14 -1
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +31 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +5 -2
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/version_responses.py +31 -0
- solace_agent_mesh/gateway/http_sse/routers/feedback.py +133 -2
- solace_agent_mesh/gateway/http_sse/routers/people.py +2 -2
- solace_agent_mesh/gateway/http_sse/routers/projects.py +768 -0
- solace_agent_mesh/gateway/http_sse/routers/prompts.py +1416 -0
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +167 -7
- solace_agent_mesh/gateway/http_sse/routers/speech.py +355 -0
- solace_agent_mesh/gateway/http_sse/routers/sse.py +131 -8
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +670 -18
- solace_agent_mesh/gateway/http_sse/routers/users.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/version.py +343 -0
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +92 -9
- solace_agent_mesh/gateway/http_sse/services/audio_service.py +1227 -0
- solace_agent_mesh/gateway/http_sse/services/background_task_monitor.py +186 -0
- solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +1 -1
- solace_agent_mesh/gateway/http_sse/services/feedback_service.py +1 -1
- solace_agent_mesh/gateway/http_sse/services/project_service.py +930 -0
- solace_agent_mesh/gateway/http_sse/services/prompt_builder_assistant.py +303 -0
- solace_agent_mesh/gateway/http_sse/services/session_service.py +361 -12
- solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +354 -4
- solace_agent_mesh/gateway/http_sse/session_manager.py +15 -15
- solace_agent_mesh/gateway/http_sse/sse_manager.py +286 -166
- solace_agent_mesh/gateway/http_sse/utils/artifact_copy_utils.py +370 -0
- solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +41 -1
- solace_agent_mesh/services/__init__.py +0 -0
- solace_agent_mesh/services/platform/__init__.py +29 -0
- solace_agent_mesh/services/platform/alembic/env.py +85 -0
- solace_agent_mesh/services/platform/alembic/script.py.mako +28 -0
- solace_agent_mesh/services/platform/alembic.ini +109 -0
- solace_agent_mesh/services/platform/api/__init__.py +3 -0
- solace_agent_mesh/services/platform/api/dependencies.py +154 -0
- solace_agent_mesh/services/platform/api/main.py +314 -0
- solace_agent_mesh/services/platform/api/middleware.py +51 -0
- solace_agent_mesh/services/platform/api/routers/__init__.py +33 -0
- solace_agent_mesh/services/platform/api/routers/health_router.py +31 -0
- solace_agent_mesh/services/platform/app.py +215 -0
- solace_agent_mesh/services/platform/component.py +777 -0
- solace_agent_mesh/shared/__init__.py +14 -0
- solace_agent_mesh/shared/api/__init__.py +42 -0
- solace_agent_mesh/shared/auth/__init__.py +26 -0
- solace_agent_mesh/shared/auth/dependencies.py +204 -0
- solace_agent_mesh/shared/auth/middleware.py +347 -0
- solace_agent_mesh/shared/database/__init__.py +20 -0
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/base_repository.py +1 -1
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_exceptions.py +1 -1
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_helpers.py +1 -1
- solace_agent_mesh/shared/exceptions/__init__.py +36 -0
- solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exception_handlers.py +19 -5
- solace_agent_mesh/shared/utils/__init__.py +21 -0
- solace_agent_mesh/templates/logging_config_template.yaml +48 -0
- solace_agent_mesh/templates/main_orchestrator.yaml +12 -1
- solace_agent_mesh/templates/platform.yaml +49 -0
- solace_agent_mesh/templates/plugin_readme_template.md +3 -25
- solace_agent_mesh/templates/plugin_tool_config_template.yaml +109 -0
- solace_agent_mesh/templates/proxy_template.yaml +62 -0
- solace_agent_mesh/templates/webui.yaml +148 -6
- solace_agent_mesh/tools/web_search/__init__.py +18 -0
- solace_agent_mesh/tools/web_search/base.py +84 -0
- solace_agent_mesh/tools/web_search/google_search.py +247 -0
- solace_agent_mesh/tools/web_search/models.py +99 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/METADATA +31 -12
- solace_agent_mesh-1.13.2.dist-info/RECORD +591 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/WHEEL +1 -1
- solace_agent_mesh/agent/adk/adk_llm.txt +0 -232
- solace_agent_mesh/agent/adk/adk_llm_detail.txt +0 -566
- solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +0 -171
- solace_agent_mesh/agent/adk/models/models_llm.txt +0 -142
- solace_agent_mesh/agent/agent_llm.txt +0 -378
- solace_agent_mesh/agent/agent_llm_detail.txt +0 -1702
- solace_agent_mesh/agent/protocol/protocol_llm.txt +0 -81
- solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +0 -92
- solace_agent_mesh/agent/sac/sac_llm.txt +0 -189
- solace_agent_mesh/agent/sac/sac_llm_detail.txt +0 -200
- solace_agent_mesh/agent/testing/testing_llm.txt +0 -57
- solace_agent_mesh/agent/testing/testing_llm_detail.txt +0 -68
- solace_agent_mesh/agent/tools/tools_llm.txt +0 -263
- solace_agent_mesh/agent/tools/tools_llm_detail.txt +0 -274
- solace_agent_mesh/agent/utils/utils_llm.txt +0 -138
- solace_agent_mesh/agent/utils/utils_llm_detail.txt +0 -149
- solace_agent_mesh/assets/docs/assets/js/15ba94aa.932dd2db.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/240a0364.7eac6021.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/2e32b5e0.33f5d75b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3a6c6137.f5940cfa.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3ac1795d.76654dd9.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3ff0015d.2be20244.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/547e15cc.2cbb060a.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/55b7b518.f2b1d1ba.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/631738c7.a8b1ef8b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6a520c9d.ba015d81.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6d84eae0.4a5fbf39.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/8024126c.56e59919.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/81a99df0.07034dd9.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/82fbfb93.139a1a1f.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/94e8668d.b5ddb7a1.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/9bb13469.dd1c9b54.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e6dd091.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/c93cbaa0.eaff365e.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/da0b5bad.d08a9466.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/dd817ffc.0aa9630a.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/dd81e2b8.d590bc9e.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e3d9abda.6b9493d0.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e92d0134.cf6d6522.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/main.b12eac43.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.e268214e.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1761248203150.json +0 -1
- solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +0 -250
- solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +0 -365
- solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +0 -305
- solace_agent_mesh/client/webui/frontend/static/assets/client-CaY59VuC.js +0 -25
- solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +0 -342
- solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-BEmvJSYz.js +0 -405
- solace_agent_mesh/common/a2a/a2a_llm.txt +0 -182
- solace_agent_mesh/common/a2a/a2a_llm_detail.txt +0 -193
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +0 -407
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +0 -736
- solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +0 -313
- solace_agent_mesh/common/common_llm.txt +0 -251
- solace_agent_mesh/common/common_llm_detail.txt +0 -2562
- solace_agent_mesh/common/middleware/middleware_llm.txt +0 -174
- solace_agent_mesh/common/middleware/middleware_llm_detail.txt +0 -185
- solace_agent_mesh/common/sac/sac_llm.txt +0 -71
- solace_agent_mesh/common/sac/sac_llm_detail.txt +0 -82
- solace_agent_mesh/common/sam_events/sam_events_llm.txt +0 -104
- solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +0 -115
- solace_agent_mesh/common/services/providers/providers_llm.txt +0 -80
- solace_agent_mesh/common/services/services_llm.txt +0 -363
- solace_agent_mesh/common/services/services_llm_detail.txt +0 -459
- solace_agent_mesh/common/utils/embeds/embeds_llm.txt +0 -220
- solace_agent_mesh/common/utils/utils_llm.txt +0 -336
- solace_agent_mesh/common/utils/utils_llm_detail.txt +0 -572
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +0 -98
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-61038fc6.js +0 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-BWvk5-gF.js +0 -10
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +0 -1
- solace_agent_mesh/core_a2a/core_a2a_llm.txt +0 -90
- solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +0 -101
- solace_agent_mesh/gateway/base/base_llm.txt +0 -224
- solace_agent_mesh/gateway/base/base_llm_detail.txt +0 -235
- solace_agent_mesh/gateway/gateway_llm.txt +0 -373
- solace_agent_mesh/gateway/gateway_llm_detail.txt +0 -3885
- solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +0 -295
- solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +0 -155
- solace_agent_mesh/gateway/http_sse/components/components_llm.txt +0 -105
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +0 -299
- solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +0 -3278
- solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +0 -263
- solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +0 -266
- solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +0 -340
- solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +0 -346
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +0 -83
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +0 -107
- solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +0 -314
- solace_agent_mesh/gateway/http_sse/services/services_llm.txt +0 -297
- solace_agent_mesh/gateway/http_sse/shared/__init__.py +0 -146
- solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +0 -285
- solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +0 -47
- solace_agent_mesh/llm.txt +0 -228
- solace_agent_mesh/llm_detail.txt +0 -2835
- solace_agent_mesh/solace_agent_mesh_llm.txt +0 -362
- solace_agent_mesh/solace_agent_mesh_llm_detail.txt +0 -8599
- solace_agent_mesh/templates/logging_config_template.ini +0 -45
- solace_agent_mesh/templates/templates_llm.txt +0 -147
- solace_agent_mesh-1.6.1.dist-info/RECORD +0 -525
- /solace_agent_mesh/assets/docs/assets/js/{main.b12eac43.js.LICENSE.txt → main.d634009f.js.LICENSE.txt} +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/auth_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/pagination.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/response_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/error_dto.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exceptions.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/enums.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/timestamp_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/types.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/utils.py +0 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|