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,930 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Business service for project-related operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
6
|
+
import logging
|
|
7
|
+
import json
|
|
8
|
+
import zipfile
|
|
9
|
+
from io import BytesIO
|
|
10
|
+
from fastapi import UploadFile
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
from ....agent.utils.artifact_helpers import get_artifact_info_list, save_artifact_with_metadata, get_artifact_counts_batch
|
|
14
|
+
|
|
15
|
+
# Default max upload size (50MB) - matches gateway_max_upload_size_bytes default
|
|
16
|
+
DEFAULT_MAX_UPLOAD_SIZE_BYTES = 52428800
|
|
17
|
+
# Default max ZIP upload size (100MB) - for project import ZIP files
|
|
18
|
+
DEFAULT_MAX_ZIP_UPLOAD_SIZE_BYTES = 104857600
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from google.adk.artifacts import BaseArtifactService
|
|
22
|
+
except ImportError:
|
|
23
|
+
|
|
24
|
+
class BaseArtifactService:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from ....common.a2a.types import ArtifactInfo
|
|
29
|
+
from ..repository.interfaces import IProjectRepository
|
|
30
|
+
from ..repository.entities.project import Project
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from ..component import WebUIBackendComponent
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProjectService:
|
|
37
|
+
"""Service layer for project business logic."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
component: "WebUIBackendComponent" = None,
|
|
42
|
+
):
|
|
43
|
+
self.component = component
|
|
44
|
+
self.artifact_service = component.get_shared_artifact_service() if component else None
|
|
45
|
+
self.app_name = component.get_config("name", "WebUIBackendApp") if component else "WebUIBackendApp"
|
|
46
|
+
self.logger = logging.getLogger(__name__)
|
|
47
|
+
# Get max upload size from component config, with fallback to default
|
|
48
|
+
# Ensure values are integers for proper formatting
|
|
49
|
+
max_upload_config = (
|
|
50
|
+
component.get_config("gateway_max_upload_size_bytes", DEFAULT_MAX_UPLOAD_SIZE_BYTES)
|
|
51
|
+
if component else DEFAULT_MAX_UPLOAD_SIZE_BYTES
|
|
52
|
+
)
|
|
53
|
+
self.max_upload_size_bytes = int(max_upload_config) if isinstance(max_upload_config, (int, float)) else DEFAULT_MAX_UPLOAD_SIZE_BYTES
|
|
54
|
+
|
|
55
|
+
# Get max ZIP upload size from component config, with fallback to default (100MB)
|
|
56
|
+
max_zip_config = (
|
|
57
|
+
component.get_config("gateway_max_zip_upload_size_bytes", DEFAULT_MAX_ZIP_UPLOAD_SIZE_BYTES)
|
|
58
|
+
if component else DEFAULT_MAX_ZIP_UPLOAD_SIZE_BYTES
|
|
59
|
+
)
|
|
60
|
+
self.max_zip_upload_size_bytes = int(max_zip_config) if isinstance(max_zip_config, (int, float)) else DEFAULT_MAX_ZIP_UPLOAD_SIZE_BYTES
|
|
61
|
+
|
|
62
|
+
self.logger.info(
|
|
63
|
+
"[ProjectService] Initialized with max_upload_size_bytes=%d (%.2f MB), "
|
|
64
|
+
"max_zip_upload_size_bytes=%d (%.2f MB)",
|
|
65
|
+
self.max_upload_size_bytes,
|
|
66
|
+
self.max_upload_size_bytes / (1024*1024),
|
|
67
|
+
self.max_zip_upload_size_bytes,
|
|
68
|
+
self.max_zip_upload_size_bytes / (1024*1024)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _get_repositories(self, db):
|
|
72
|
+
"""Create project repository for the given database session."""
|
|
73
|
+
from ..repository.project_repository import ProjectRepository
|
|
74
|
+
return ProjectRepository(db)
|
|
75
|
+
|
|
76
|
+
def is_persistence_enabled(self) -> bool:
|
|
77
|
+
"""Checks if the service is configured with a persistent backend."""
|
|
78
|
+
return self.component and self.component.database_url is not None
|
|
79
|
+
|
|
80
|
+
async def _validate_file_size(self, file: UploadFile, log_prefix: str = "") -> bytes:
|
|
81
|
+
"""
|
|
82
|
+
Validate file size and read content with size checking.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
file: The uploaded file to validate
|
|
86
|
+
log_prefix: Prefix for log messages
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bytes: The file content if validation passes
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If file exceeds maximum allowed size
|
|
93
|
+
"""
|
|
94
|
+
# Read file content in chunks to validate size
|
|
95
|
+
chunk_size = 1024 * 1024 # 1MB chunks
|
|
96
|
+
content_bytes = bytearray()
|
|
97
|
+
total_bytes_read = 0
|
|
98
|
+
|
|
99
|
+
while True:
|
|
100
|
+
chunk = await file.read(chunk_size)
|
|
101
|
+
if not chunk:
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
chunk_len = len(chunk)
|
|
105
|
+
total_bytes_read += chunk_len
|
|
106
|
+
|
|
107
|
+
# Validate size during reading (fail fast)
|
|
108
|
+
if total_bytes_read > self.max_upload_size_bytes:
|
|
109
|
+
error_msg = (
|
|
110
|
+
f"File '{file.filename}' rejected: size exceeds maximum "
|
|
111
|
+
f"{self.max_upload_size_bytes:,} bytes "
|
|
112
|
+
f"({self.max_upload_size_bytes / (1024*1024):.2f} MB). "
|
|
113
|
+
f"Read {total_bytes_read:,} bytes so far."
|
|
114
|
+
)
|
|
115
|
+
self.logger.warning(f"{log_prefix} {error_msg}")
|
|
116
|
+
raise ValueError(error_msg)
|
|
117
|
+
|
|
118
|
+
content_bytes.extend(chunk)
|
|
119
|
+
|
|
120
|
+
return bytes(content_bytes)
|
|
121
|
+
|
|
122
|
+
async def _validate_files(
|
|
123
|
+
self,
|
|
124
|
+
files: List[UploadFile],
|
|
125
|
+
log_prefix: str = ""
|
|
126
|
+
) -> List[tuple]:
|
|
127
|
+
"""
|
|
128
|
+
Validate multiple files and return their content.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
files: List of uploaded files to validate
|
|
132
|
+
log_prefix: Prefix for log messages
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of tuples: [(file, content_bytes), ...]
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If any file exceeds maximum allowed size
|
|
139
|
+
"""
|
|
140
|
+
validated_files = []
|
|
141
|
+
for file in files:
|
|
142
|
+
content_bytes = await self._validate_file_size(file, log_prefix)
|
|
143
|
+
validated_files.append((file, content_bytes))
|
|
144
|
+
self.logger.debug(
|
|
145
|
+
f"{log_prefix} Validated file '{file.filename}': {len(content_bytes):,} bytes"
|
|
146
|
+
)
|
|
147
|
+
return validated_files
|
|
148
|
+
|
|
149
|
+
async def create_project(
|
|
150
|
+
self,
|
|
151
|
+
db,
|
|
152
|
+
name: str,
|
|
153
|
+
user_id: str,
|
|
154
|
+
description: Optional[str] = None,
|
|
155
|
+
system_prompt: Optional[str] = None,
|
|
156
|
+
default_agent_id: Optional[str] = None,
|
|
157
|
+
files: Optional[List[UploadFile]] = None,
|
|
158
|
+
file_metadata: Optional[dict] = None,
|
|
159
|
+
) -> Project:
|
|
160
|
+
"""
|
|
161
|
+
Create a new project for a user.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
db: Database session
|
|
165
|
+
name: Project name
|
|
166
|
+
user_id: ID of the user creating the project
|
|
167
|
+
description: Optional project description
|
|
168
|
+
system_prompt: Optional system prompt
|
|
169
|
+
default_agent_id: Optional default agent ID for new chats
|
|
170
|
+
files: Optional list of files to associate with the project
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
DomainProject: The created project
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ValueError: If project name is invalid, user_id is missing, or file size exceeds limit
|
|
177
|
+
"""
|
|
178
|
+
log_prefix = f"[ProjectService:create_project] User {user_id}:"
|
|
179
|
+
self.logger.info(f"Creating new project '{name}' for user {user_id}")
|
|
180
|
+
|
|
181
|
+
# Business validation
|
|
182
|
+
if not name or not name.strip():
|
|
183
|
+
raise ValueError("Project name cannot be empty")
|
|
184
|
+
|
|
185
|
+
if not user_id:
|
|
186
|
+
raise ValueError("User ID is required to create a project")
|
|
187
|
+
|
|
188
|
+
# Validate file sizes before creating project
|
|
189
|
+
validated_files = []
|
|
190
|
+
if files:
|
|
191
|
+
self.logger.info(f"{log_prefix} Validating {len(files)} files before project creation")
|
|
192
|
+
validated_files = await self._validate_files(files, log_prefix)
|
|
193
|
+
self.logger.info(f"{log_prefix} All {len(files)} files passed size validation")
|
|
194
|
+
|
|
195
|
+
project_repository = self._get_repositories(db)
|
|
196
|
+
|
|
197
|
+
# Check for duplicate project name for this user
|
|
198
|
+
existing_projects = project_repository.get_user_projects(user_id)
|
|
199
|
+
if any(p.name.lower() == name.strip().lower() for p in existing_projects):
|
|
200
|
+
raise ValueError(f"A project with the name '{name.strip()}' already exists")
|
|
201
|
+
|
|
202
|
+
# Create the project
|
|
203
|
+
project_domain = project_repository.create_project(
|
|
204
|
+
name=name.strip(),
|
|
205
|
+
user_id=user_id,
|
|
206
|
+
description=description.strip() if description else None,
|
|
207
|
+
system_prompt=system_prompt.strip() if system_prompt else None,
|
|
208
|
+
default_agent_id=default_agent_id,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if validated_files and self.artifact_service:
|
|
212
|
+
self.logger.info(
|
|
213
|
+
f"Project {project_domain.id} created, now saving {len(validated_files)} artifacts."
|
|
214
|
+
)
|
|
215
|
+
project_session_id = f"project-{project_domain.id}"
|
|
216
|
+
for file, content_bytes in validated_files:
|
|
217
|
+
metadata = {"source": "project"}
|
|
218
|
+
if file_metadata and file.filename in file_metadata:
|
|
219
|
+
desc = file_metadata[file.filename]
|
|
220
|
+
if desc:
|
|
221
|
+
metadata["description"] = desc
|
|
222
|
+
|
|
223
|
+
await save_artifact_with_metadata(
|
|
224
|
+
artifact_service=self.artifact_service,
|
|
225
|
+
app_name=self.app_name,
|
|
226
|
+
user_id=project_domain.user_id,
|
|
227
|
+
session_id=project_session_id,
|
|
228
|
+
filename=file.filename,
|
|
229
|
+
content_bytes=content_bytes,
|
|
230
|
+
mime_type=file.content_type,
|
|
231
|
+
metadata_dict=metadata,
|
|
232
|
+
timestamp=datetime.now(timezone.utc),
|
|
233
|
+
)
|
|
234
|
+
self.logger.info(f"Saved {len(validated_files)} artifacts for project {project_domain.id}")
|
|
235
|
+
|
|
236
|
+
self.logger.info(
|
|
237
|
+
f"Successfully created project {project_domain.id} for user {user_id}"
|
|
238
|
+
)
|
|
239
|
+
return project_domain
|
|
240
|
+
|
|
241
|
+
def get_project(self, db, project_id: str, user_id: str) -> Optional[Project]:
|
|
242
|
+
"""
|
|
243
|
+
Get a project by ID, ensuring the user has access to it.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
db: Database session
|
|
247
|
+
project_id: The project ID
|
|
248
|
+
user_id: The requesting user ID
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Optional[Project]: The project if found and accessible, None otherwise
|
|
252
|
+
"""
|
|
253
|
+
project_repository = self._get_repositories(db)
|
|
254
|
+
return project_repository.get_by_id(project_id, user_id)
|
|
255
|
+
|
|
256
|
+
def get_user_projects(self, db, user_id: str) -> List[Project]:
|
|
257
|
+
"""
|
|
258
|
+
Get all projects owned by a specific user.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
db: Database session
|
|
262
|
+
user_id: The user ID
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
List[DomainProject]: List of user's projects
|
|
266
|
+
"""
|
|
267
|
+
self.logger.debug(f"Retrieving projects for user {user_id}")
|
|
268
|
+
project_repository = self._get_repositories(db)
|
|
269
|
+
db_projects = project_repository.get_user_projects(user_id)
|
|
270
|
+
return db_projects
|
|
271
|
+
|
|
272
|
+
async def get_user_projects_with_counts(self, db, user_id: str) -> List[tuple[Project, int]]:
|
|
273
|
+
"""
|
|
274
|
+
Get all projects owned by a specific user with artifact counts.
|
|
275
|
+
Uses batch counting for efficiency.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
db: Database session
|
|
279
|
+
user_id: The user ID
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
List[tuple[Project, int]]: List of tuples (project, artifact_count)
|
|
283
|
+
"""
|
|
284
|
+
self.logger.debug(f"Retrieving projects with artifact counts for user {user_id}")
|
|
285
|
+
projects = self.get_user_projects(db, user_id)
|
|
286
|
+
|
|
287
|
+
if not self.artifact_service or not projects:
|
|
288
|
+
# If no artifact service or no projects, return projects with 0 counts
|
|
289
|
+
return [(project, 0) for project in projects]
|
|
290
|
+
|
|
291
|
+
# Build list of session IDs for batch counting
|
|
292
|
+
session_ids = [f"project-{project.id}" for project in projects]
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Get all counts in a single batch operation
|
|
296
|
+
counts_by_session = await get_artifact_counts_batch(
|
|
297
|
+
artifact_service=self.artifact_service,
|
|
298
|
+
app_name=self.app_name,
|
|
299
|
+
user_id=user_id,
|
|
300
|
+
session_ids=session_ids,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Map counts back to projects
|
|
304
|
+
projects_with_counts = []
|
|
305
|
+
for project in projects:
|
|
306
|
+
storage_session_id = f"project-{project.id}"
|
|
307
|
+
artifact_count = counts_by_session.get(storage_session_id, 0)
|
|
308
|
+
projects_with_counts.append((project, artifact_count))
|
|
309
|
+
|
|
310
|
+
self.logger.debug(f"Retrieved artifact counts for {len(projects)} projects in batch")
|
|
311
|
+
return projects_with_counts
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.logger.error(f"Failed to get artifact counts in batch: {e}")
|
|
315
|
+
# Fallback to 0 counts on error
|
|
316
|
+
return [(project, 0) for project in projects]
|
|
317
|
+
|
|
318
|
+
async def get_project_artifacts(self, db, project_id: str, user_id: str) -> List[ArtifactInfo]:
|
|
319
|
+
"""
|
|
320
|
+
Get a list of artifacts for a given project.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
db: The database session
|
|
324
|
+
project_id: The project ID
|
|
325
|
+
user_id: The requesting user ID
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
List[ArtifactInfo]: A list of artifacts
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
ValueError: If project not found or access denied
|
|
332
|
+
"""
|
|
333
|
+
project = self.get_project(db, project_id, user_id)
|
|
334
|
+
if not project:
|
|
335
|
+
raise ValueError("Project not found or access denied")
|
|
336
|
+
|
|
337
|
+
if not self.artifact_service:
|
|
338
|
+
self.logger.warning(f"Attempted to get artifacts for project {project_id} but no artifact service is configured.")
|
|
339
|
+
return []
|
|
340
|
+
|
|
341
|
+
storage_user_id = project.user_id
|
|
342
|
+
storage_session_id = f"project-{project.id}"
|
|
343
|
+
|
|
344
|
+
self.logger.info(f"Fetching artifacts for project {project.id} with storage session {storage_session_id} and user {storage_user_id}")
|
|
345
|
+
|
|
346
|
+
artifacts = await get_artifact_info_list(
|
|
347
|
+
artifact_service=self.artifact_service,
|
|
348
|
+
app_name=self.app_name,
|
|
349
|
+
user_id=storage_user_id,
|
|
350
|
+
session_id=storage_session_id,
|
|
351
|
+
)
|
|
352
|
+
return artifacts
|
|
353
|
+
|
|
354
|
+
async def add_artifacts_to_project(
|
|
355
|
+
self,
|
|
356
|
+
db,
|
|
357
|
+
project_id: str,
|
|
358
|
+
user_id: str,
|
|
359
|
+
files: List[UploadFile],
|
|
360
|
+
file_metadata: Optional[dict] = None
|
|
361
|
+
) -> List[dict]:
|
|
362
|
+
"""
|
|
363
|
+
Add one or more artifacts to a project.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
db: The database session
|
|
367
|
+
project_id: The project ID
|
|
368
|
+
user_id: The requesting user ID
|
|
369
|
+
files: List of files to add
|
|
370
|
+
file_metadata: Optional dictionary of metadata (e.g., descriptions)
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
List[dict]: A list of results from the save operations
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValueError: If project not found, access denied, or file size exceeds limit
|
|
377
|
+
"""
|
|
378
|
+
log_prefix = f"[ProjectService:add_artifacts] Project {project_id}, User {user_id}:"
|
|
379
|
+
|
|
380
|
+
project = self.get_project(db, project_id, user_id)
|
|
381
|
+
if not project:
|
|
382
|
+
raise ValueError("Project not found or access denied")
|
|
383
|
+
|
|
384
|
+
if not self.artifact_service:
|
|
385
|
+
self.logger.warning(f"Attempted to add artifacts to project {project_id} but no artifact service is configured.")
|
|
386
|
+
raise ValueError("Artifact service is not configured")
|
|
387
|
+
|
|
388
|
+
if not files:
|
|
389
|
+
return []
|
|
390
|
+
|
|
391
|
+
# Validate file sizes before saving any artifacts
|
|
392
|
+
self.logger.info(f"{log_prefix} Validating {len(files)} files before adding to project")
|
|
393
|
+
validated_files = await self._validate_files(files, log_prefix)
|
|
394
|
+
self.logger.info(f"{log_prefix} All {len(files)} files passed size validation")
|
|
395
|
+
|
|
396
|
+
self.logger.info(f"Adding {len(validated_files)} artifacts to project {project_id} for user {user_id}")
|
|
397
|
+
storage_session_id = f"project-{project.id}"
|
|
398
|
+
results = []
|
|
399
|
+
|
|
400
|
+
for file, content_bytes in validated_files:
|
|
401
|
+
metadata = {"source": "project"}
|
|
402
|
+
if file_metadata and file.filename in file_metadata:
|
|
403
|
+
desc = file_metadata[file.filename]
|
|
404
|
+
if desc:
|
|
405
|
+
metadata["description"] = desc
|
|
406
|
+
|
|
407
|
+
result = await save_artifact_with_metadata(
|
|
408
|
+
artifact_service=self.artifact_service,
|
|
409
|
+
app_name=self.app_name,
|
|
410
|
+
user_id=project.user_id, # Always use project owner's ID for storage
|
|
411
|
+
session_id=storage_session_id,
|
|
412
|
+
filename=file.filename,
|
|
413
|
+
content_bytes=content_bytes,
|
|
414
|
+
mime_type=file.content_type,
|
|
415
|
+
metadata_dict=metadata,
|
|
416
|
+
timestamp=datetime.now(timezone.utc),
|
|
417
|
+
)
|
|
418
|
+
results.append(result)
|
|
419
|
+
|
|
420
|
+
self.logger.info(f"Finished adding {len(validated_files)} artifacts to project {project_id}")
|
|
421
|
+
return results
|
|
422
|
+
|
|
423
|
+
async def update_artifact_metadata(
|
|
424
|
+
self,
|
|
425
|
+
db,
|
|
426
|
+
project_id: str,
|
|
427
|
+
user_id: str,
|
|
428
|
+
filename: str,
|
|
429
|
+
description: Optional[str] = None
|
|
430
|
+
) -> bool:
|
|
431
|
+
"""
|
|
432
|
+
Update metadata (description) for a project artifact.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
db: The database session
|
|
436
|
+
project_id: The project ID
|
|
437
|
+
user_id: The requesting user ID
|
|
438
|
+
filename: The filename of the artifact to update
|
|
439
|
+
description: New description for the artifact
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
bool: True if update was successful, False if project not found
|
|
443
|
+
|
|
444
|
+
Raises:
|
|
445
|
+
ValueError: If user cannot modify the project or artifact service is missing
|
|
446
|
+
"""
|
|
447
|
+
project = self.get_project(db, project_id, user_id)
|
|
448
|
+
if not project:
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
if not self.artifact_service:
|
|
452
|
+
self.logger.warning(f"Attempted to update artifact metadata in project {project_id} but no artifact service is configured.")
|
|
453
|
+
raise ValueError("Artifact service is not configured")
|
|
454
|
+
|
|
455
|
+
storage_session_id = f"project-{project.id}"
|
|
456
|
+
|
|
457
|
+
self.logger.info(f"Updating metadata for artifact '{filename}' in project {project_id} for user {user_id}")
|
|
458
|
+
|
|
459
|
+
# Load the current artifact to get its content and existing metadata
|
|
460
|
+
try:
|
|
461
|
+
artifact_part = await self.artifact_service.load_artifact(
|
|
462
|
+
app_name=self.app_name,
|
|
463
|
+
user_id=project.user_id,
|
|
464
|
+
session_id=storage_session_id,
|
|
465
|
+
filename=filename,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if not artifact_part or not artifact_part.inline_data:
|
|
469
|
+
self.logger.warning(f"Artifact '{filename}' not found in project {project_id}")
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
# Prepare updated metadata
|
|
473
|
+
metadata = {"source": "project"}
|
|
474
|
+
if description is not None:
|
|
475
|
+
metadata["description"] = description
|
|
476
|
+
|
|
477
|
+
# Save the artifact with updated metadata
|
|
478
|
+
await save_artifact_with_metadata(
|
|
479
|
+
artifact_service=self.artifact_service,
|
|
480
|
+
app_name=self.app_name,
|
|
481
|
+
user_id=project.user_id,
|
|
482
|
+
session_id=storage_session_id,
|
|
483
|
+
filename=filename,
|
|
484
|
+
content_bytes=artifact_part.inline_data.data,
|
|
485
|
+
mime_type=artifact_part.inline_data.mime_type,
|
|
486
|
+
metadata_dict=metadata,
|
|
487
|
+
timestamp=datetime.now(timezone.utc),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
self.logger.info(f"Successfully updated metadata for artifact '{filename}' in project {project_id}")
|
|
491
|
+
return True
|
|
492
|
+
|
|
493
|
+
except Exception as e:
|
|
494
|
+
self.logger.error(f"Error updating artifact metadata: {e}")
|
|
495
|
+
raise
|
|
496
|
+
|
|
497
|
+
async def delete_artifact_from_project(self, db, project_id: str, user_id: str, filename: str) -> bool:
|
|
498
|
+
"""
|
|
499
|
+
Deletes an artifact from a project.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
db: The database session
|
|
503
|
+
project_id: The project ID
|
|
504
|
+
user_id: The requesting user ID
|
|
505
|
+
filename: The filename of the artifact to delete
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
bool: True if deletion was attempted, False if project not found
|
|
509
|
+
|
|
510
|
+
Raises:
|
|
511
|
+
ValueError: If user cannot modify the project or artifact service is missing
|
|
512
|
+
"""
|
|
513
|
+
project = self.get_project(db, project_id, user_id)
|
|
514
|
+
if not project:
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
if not self.artifact_service:
|
|
518
|
+
self.logger.warning(f"Attempted to delete artifact from project {project_id} but no artifact service is configured.")
|
|
519
|
+
raise ValueError("Artifact service is not configured")
|
|
520
|
+
|
|
521
|
+
storage_session_id = f"project-{project.id}"
|
|
522
|
+
|
|
523
|
+
self.logger.info(f"Deleting artifact '{filename}' from project {project_id} for user {user_id}")
|
|
524
|
+
|
|
525
|
+
await self.artifact_service.delete_artifact(
|
|
526
|
+
app_name=self.app_name,
|
|
527
|
+
user_id=project.user_id, # Always use project owner's ID for storage
|
|
528
|
+
session_id=storage_session_id,
|
|
529
|
+
filename=filename,
|
|
530
|
+
)
|
|
531
|
+
return True
|
|
532
|
+
|
|
533
|
+
def update_project(self, db, project_id: str, user_id: str,
|
|
534
|
+
name: Optional[str] = None, description: Optional[str] = None,
|
|
535
|
+
system_prompt: Optional[str] = None, default_agent_id: Optional[str] = ...) -> Optional[Project]:
|
|
536
|
+
"""
|
|
537
|
+
Update a project's details.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
db: Database session
|
|
541
|
+
project_id: The project ID
|
|
542
|
+
user_id: The requesting user ID
|
|
543
|
+
name: New project name (optional)
|
|
544
|
+
description: New project description (optional)
|
|
545
|
+
system_prompt: New system prompt (optional)
|
|
546
|
+
default_agent_id: New default agent ID (optional, use ... sentinel to indicate not provided)
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Optional[Project]: The updated project if successful, None otherwise
|
|
550
|
+
"""
|
|
551
|
+
# Validate business rules
|
|
552
|
+
if name is not None and name is not ... and not name.strip():
|
|
553
|
+
raise ValueError("Project name cannot be empty")
|
|
554
|
+
|
|
555
|
+
# Build update data
|
|
556
|
+
update_data = {}
|
|
557
|
+
if name is not None and name is not ...:
|
|
558
|
+
update_data["name"] = name.strip()
|
|
559
|
+
if description is not None and description is not ...:
|
|
560
|
+
update_data["description"] = description.strip() if description else None
|
|
561
|
+
if system_prompt is not None and system_prompt is not ...:
|
|
562
|
+
update_data["system_prompt"] = system_prompt.strip() if system_prompt else None
|
|
563
|
+
if default_agent_id is not ...:
|
|
564
|
+
update_data["default_agent_id"] = default_agent_id
|
|
565
|
+
|
|
566
|
+
if not update_data:
|
|
567
|
+
# Nothing to update - get existing project
|
|
568
|
+
return self.get_project(db, project_id, user_id)
|
|
569
|
+
|
|
570
|
+
project_repository = self._get_repositories(db)
|
|
571
|
+
self.logger.info(f"Updating project {project_id} for user {user_id}")
|
|
572
|
+
updated_project = project_repository.update(project_id, user_id, update_data)
|
|
573
|
+
|
|
574
|
+
if updated_project:
|
|
575
|
+
self.logger.info(f"Successfully updated project {project_id}")
|
|
576
|
+
|
|
577
|
+
return updated_project
|
|
578
|
+
|
|
579
|
+
def delete_project(self, db, project_id: str, user_id: str) -> bool:
|
|
580
|
+
"""
|
|
581
|
+
Delete a project.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
db: Database session
|
|
585
|
+
project_id: The project ID
|
|
586
|
+
user_id: The requesting user ID
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
bool: True if deleted successfully, False otherwise
|
|
590
|
+
"""
|
|
591
|
+
# First verify the project exists and user has access
|
|
592
|
+
existing_project = self.get_project(db, project_id, user_id)
|
|
593
|
+
if not existing_project:
|
|
594
|
+
return False
|
|
595
|
+
|
|
596
|
+
project_repository = self._get_repositories(db)
|
|
597
|
+
self.logger.info(f"Deleting project {project_id} for user {user_id}")
|
|
598
|
+
success = project_repository.delete(project_id, user_id)
|
|
599
|
+
|
|
600
|
+
if success:
|
|
601
|
+
self.logger.info(f"Successfully deleted project {project_id}")
|
|
602
|
+
|
|
603
|
+
return success
|
|
604
|
+
|
|
605
|
+
def soft_delete_project(self, db, project_id: str, user_id: str) -> bool:
|
|
606
|
+
"""
|
|
607
|
+
Soft delete a project (mark as deleted without removing from database).
|
|
608
|
+
Also cascades soft delete to all sessions associated with this project.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
db: Database session
|
|
612
|
+
project_id: The project ID
|
|
613
|
+
user_id: The requesting user ID
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
bool: True if soft deleted successfully, False otherwise
|
|
617
|
+
"""
|
|
618
|
+
# First verify the project exists and user has access
|
|
619
|
+
existing_project = self.get_project(db, project_id, user_id)
|
|
620
|
+
if not existing_project:
|
|
621
|
+
self.logger.warning(f"Attempted to soft delete non-existent project {project_id} by user {user_id}")
|
|
622
|
+
return False
|
|
623
|
+
|
|
624
|
+
self.logger.info(f"Soft deleting project {project_id} and its associated sessions for user {user_id}")
|
|
625
|
+
|
|
626
|
+
project_repository = self._get_repositories(db)
|
|
627
|
+
# Soft delete the project
|
|
628
|
+
success = project_repository.soft_delete(project_id, user_id)
|
|
629
|
+
|
|
630
|
+
if success:
|
|
631
|
+
from ..repository.session_repository import SessionRepository
|
|
632
|
+
session_repo = SessionRepository()
|
|
633
|
+
deleted_count = session_repo.soft_delete_by_project(db, project_id, user_id)
|
|
634
|
+
self.logger.info(f"Successfully soft deleted project {project_id} and {deleted_count} associated sessions")
|
|
635
|
+
|
|
636
|
+
return success
|
|
637
|
+
|
|
638
|
+
async def export_project_as_zip(
|
|
639
|
+
self, db, project_id: str, user_id: str
|
|
640
|
+
) -> BytesIO:
|
|
641
|
+
"""
|
|
642
|
+
Create ZIP file with project data and artifacts.
|
|
643
|
+
Returns in-memory ZIP file.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
db: Database session
|
|
647
|
+
project_id: The project ID
|
|
648
|
+
user_id: The requesting user ID
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
BytesIO: In-memory ZIP file
|
|
652
|
+
|
|
653
|
+
Raises:
|
|
654
|
+
ValueError: If project not found or access denied
|
|
655
|
+
"""
|
|
656
|
+
# Get project
|
|
657
|
+
project = self.get_project(db, project_id, user_id)
|
|
658
|
+
if not project:
|
|
659
|
+
raise ValueError("Project not found or access denied")
|
|
660
|
+
|
|
661
|
+
# Get artifacts
|
|
662
|
+
artifacts = await self.get_project_artifacts(db, project_id, user_id)
|
|
663
|
+
|
|
664
|
+
# Calculate total size
|
|
665
|
+
total_size = sum(artifact.size for artifact in artifacts)
|
|
666
|
+
|
|
667
|
+
# Create export metadata
|
|
668
|
+
from ..routers.dto.project_dto import (
|
|
669
|
+
ProjectExportFormat,
|
|
670
|
+
ProjectExportData,
|
|
671
|
+
ProjectExportMetadata,
|
|
672
|
+
ArtifactMetadata,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
export_data = ProjectExportFormat(
|
|
676
|
+
version="1.0",
|
|
677
|
+
exported_at=int(datetime.now(timezone.utc).timestamp() * 1000),
|
|
678
|
+
project=ProjectExportData(
|
|
679
|
+
name=project.name,
|
|
680
|
+
description=project.description,
|
|
681
|
+
system_prompt=project.system_prompt,
|
|
682
|
+
default_agent_id=project.default_agent_id,
|
|
683
|
+
metadata=ProjectExportMetadata(
|
|
684
|
+
original_created_at=project.created_at,
|
|
685
|
+
artifact_count=len(artifacts),
|
|
686
|
+
total_size_bytes=total_size,
|
|
687
|
+
),
|
|
688
|
+
),
|
|
689
|
+
artifacts=[
|
|
690
|
+
ArtifactMetadata(
|
|
691
|
+
filename=artifact.filename,
|
|
692
|
+
mime_type=artifact.mime_type or "application/octet-stream",
|
|
693
|
+
size=artifact.size,
|
|
694
|
+
metadata={
|
|
695
|
+
"description": artifact.description,
|
|
696
|
+
"source": artifact.source,
|
|
697
|
+
} if artifact.description or artifact.source else {},
|
|
698
|
+
)
|
|
699
|
+
for artifact in artifacts
|
|
700
|
+
],
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
# Create ZIP in memory
|
|
704
|
+
zip_buffer = BytesIO()
|
|
705
|
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
706
|
+
# Add project.json
|
|
707
|
+
project_json = export_data.model_dump(by_alias=True, mode='json')
|
|
708
|
+
zip_file.writestr('project.json', json.dumps(project_json, indent=2))
|
|
709
|
+
|
|
710
|
+
# Add artifacts
|
|
711
|
+
if self.artifact_service and artifacts:
|
|
712
|
+
storage_session_id = f"project-{project.id}"
|
|
713
|
+
for artifact in artifacts:
|
|
714
|
+
try:
|
|
715
|
+
# Load artifact content
|
|
716
|
+
artifact_part = await self.artifact_service.load_artifact(
|
|
717
|
+
app_name=self.app_name,
|
|
718
|
+
user_id=project.user_id,
|
|
719
|
+
session_id=storage_session_id,
|
|
720
|
+
filename=artifact.filename,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
if artifact_part and artifact_part.inline_data:
|
|
724
|
+
# Add to ZIP under artifacts/ directory
|
|
725
|
+
zip_file.writestr(
|
|
726
|
+
f'artifacts/{artifact.filename}',
|
|
727
|
+
artifact_part.inline_data.data
|
|
728
|
+
)
|
|
729
|
+
except Exception as e:
|
|
730
|
+
self.logger.warning(
|
|
731
|
+
f"Failed to add artifact {artifact.filename} to export: {e}"
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
zip_buffer.seek(0)
|
|
735
|
+
return zip_buffer
|
|
736
|
+
|
|
737
|
+
async def import_project_from_zip(
|
|
738
|
+
self, db, zip_file: UploadFile, user_id: str,
|
|
739
|
+
preserve_name: bool = False, custom_name: Optional[str] = None
|
|
740
|
+
) -> tuple[Project, int, List[str]]:
|
|
741
|
+
"""
|
|
742
|
+
Import project from ZIP file.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
db: Database session
|
|
746
|
+
zip_file: Uploaded ZIP file
|
|
747
|
+
user_id: The importing user ID
|
|
748
|
+
preserve_name: Whether to preserve original name
|
|
749
|
+
custom_name: Custom name to use (overrides preserve_name)
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
tuple: (created_project, artifacts_count, warnings)
|
|
753
|
+
|
|
754
|
+
Raises:
|
|
755
|
+
ValueError: If ZIP is invalid, import fails, or file size exceeds limit
|
|
756
|
+
"""
|
|
757
|
+
log_prefix = f"[ProjectService:import_project] User {user_id}:"
|
|
758
|
+
warnings = []
|
|
759
|
+
|
|
760
|
+
# Read ZIP file content with size validation
|
|
761
|
+
self.logger.info(f"{log_prefix} Reading ZIP file")
|
|
762
|
+
zip_content = await zip_file.read()
|
|
763
|
+
zip_size = len(zip_content)
|
|
764
|
+
self.logger.info(f"{log_prefix} ZIP file read: {zip_size:,} bytes")
|
|
765
|
+
|
|
766
|
+
# Validate ZIP file size (separate, larger limit than individual artifacts)
|
|
767
|
+
if zip_size > self.max_zip_upload_size_bytes:
|
|
768
|
+
max_size_mb = self.max_zip_upload_size_bytes / (1024 * 1024)
|
|
769
|
+
file_size_mb = zip_size / (1024 * 1024)
|
|
770
|
+
error_msg = (
|
|
771
|
+
f"ZIP file '{zip_file.filename}' rejected: size ({file_size_mb:.2f} MB) "
|
|
772
|
+
f"exceeds maximum allowed ({max_size_mb:.2f} MB)"
|
|
773
|
+
)
|
|
774
|
+
self.logger.warning(f"{log_prefix} {error_msg}")
|
|
775
|
+
raise ValueError(error_msg)
|
|
776
|
+
|
|
777
|
+
zip_buffer = BytesIO(zip_content)
|
|
778
|
+
|
|
779
|
+
try:
|
|
780
|
+
with zipfile.ZipFile(zip_buffer, 'r') as zip_ref:
|
|
781
|
+
# Validate ZIP structure
|
|
782
|
+
if 'project.json' not in zip_ref.namelist():
|
|
783
|
+
raise ValueError("Invalid project export: missing project.json")
|
|
784
|
+
|
|
785
|
+
# Parse project.json
|
|
786
|
+
project_json_content = zip_ref.read('project.json').decode('utf-8')
|
|
787
|
+
project_data = json.loads(project_json_content)
|
|
788
|
+
|
|
789
|
+
# Validate version
|
|
790
|
+
if project_data.get('version') != '1.0':
|
|
791
|
+
raise ValueError(
|
|
792
|
+
f"Unsupported export version: {project_data.get('version')}"
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
# Determine project name
|
|
796
|
+
original_name = project_data['project']['name']
|
|
797
|
+
if custom_name:
|
|
798
|
+
desired_name = custom_name
|
|
799
|
+
elif preserve_name:
|
|
800
|
+
desired_name = original_name
|
|
801
|
+
else:
|
|
802
|
+
desired_name = original_name
|
|
803
|
+
|
|
804
|
+
# Resolve name conflicts
|
|
805
|
+
final_name = self._resolve_project_name_conflict(db, desired_name, user_id)
|
|
806
|
+
if final_name != desired_name:
|
|
807
|
+
warnings.append(
|
|
808
|
+
f"Name conflict resolved: '{desired_name}' → '{final_name}'"
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
# Get default agent ID, but set to None if not provided
|
|
812
|
+
# The agent may not exist in the target environment
|
|
813
|
+
imported_agent_id = project_data['project'].get('defaultAgentId')
|
|
814
|
+
|
|
815
|
+
# Create project (agent validation happens in create_project if needed)
|
|
816
|
+
project = await self.create_project(
|
|
817
|
+
db=db,
|
|
818
|
+
name=final_name,
|
|
819
|
+
user_id=user_id,
|
|
820
|
+
description=project_data['project'].get('description'),
|
|
821
|
+
system_prompt=project_data['project'].get('systemPrompt'),
|
|
822
|
+
default_agent_id=imported_agent_id,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
# Add warning if agent was specified but may not exist
|
|
826
|
+
if imported_agent_id:
|
|
827
|
+
warnings.append(
|
|
828
|
+
f"Default agent '{imported_agent_id}' was imported. "
|
|
829
|
+
"Verify it exists in your environment."
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
# Import artifacts
|
|
833
|
+
artifacts_imported = 0
|
|
834
|
+
if self.artifact_service:
|
|
835
|
+
storage_session_id = f"project-{project.id}"
|
|
836
|
+
artifact_files = [
|
|
837
|
+
name for name in zip_ref.namelist()
|
|
838
|
+
if name.startswith('artifacts/') and name != 'artifacts/'
|
|
839
|
+
]
|
|
840
|
+
|
|
841
|
+
for artifact_path in artifact_files:
|
|
842
|
+
try:
|
|
843
|
+
filename = artifact_path.replace('artifacts/', '')
|
|
844
|
+
content_bytes = zip_ref.read(artifact_path)
|
|
845
|
+
|
|
846
|
+
# Skip oversized artifacts with a warning (don't fail the entire import)
|
|
847
|
+
if len(content_bytes) > self.max_upload_size_bytes:
|
|
848
|
+
max_size_mb = self.max_upload_size_bytes / (1024 * 1024)
|
|
849
|
+
file_size_mb = len(content_bytes) / (1024 * 1024)
|
|
850
|
+
skip_msg = (
|
|
851
|
+
f"Skipped '{filename}': size ({file_size_mb:.2f} MB) "
|
|
852
|
+
f"exceeds maximum allowed ({max_size_mb:.2f} MB)"
|
|
853
|
+
)
|
|
854
|
+
self.logger.warning(f"{log_prefix} {skip_msg}")
|
|
855
|
+
warnings.append(skip_msg)
|
|
856
|
+
continue # Skip this artifact, continue with others
|
|
857
|
+
|
|
858
|
+
# Find metadata from project.json
|
|
859
|
+
artifact_meta = next(
|
|
860
|
+
(a for a in project_data.get('artifacts', [])
|
|
861
|
+
if a['filename'] == filename),
|
|
862
|
+
None
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
metadata = artifact_meta.get('metadata', {}) if artifact_meta else {}
|
|
866
|
+
mime_type = artifact_meta.get('mimeType', 'application/octet-stream') if artifact_meta else 'application/octet-stream'
|
|
867
|
+
|
|
868
|
+
# Save artifact
|
|
869
|
+
from ....agent.utils.artifact_helpers import save_artifact_with_metadata
|
|
870
|
+
await save_artifact_with_metadata(
|
|
871
|
+
artifact_service=self.artifact_service,
|
|
872
|
+
app_name=self.app_name,
|
|
873
|
+
user_id=project.user_id,
|
|
874
|
+
session_id=storage_session_id,
|
|
875
|
+
filename=filename,
|
|
876
|
+
content_bytes=content_bytes,
|
|
877
|
+
mime_type=mime_type,
|
|
878
|
+
metadata_dict=metadata,
|
|
879
|
+
timestamp=datetime.now(timezone.utc),
|
|
880
|
+
)
|
|
881
|
+
artifacts_imported += 1
|
|
882
|
+
except Exception as e:
|
|
883
|
+
self.logger.warning(
|
|
884
|
+
f"Failed to import artifact {artifact_path}: {e}"
|
|
885
|
+
)
|
|
886
|
+
warnings.append(f"Failed to import artifact: {filename}")
|
|
887
|
+
|
|
888
|
+
self.logger.info(
|
|
889
|
+
f"Successfully imported project {project.id} with {artifacts_imported} artifacts"
|
|
890
|
+
)
|
|
891
|
+
return project, artifacts_imported, warnings
|
|
892
|
+
|
|
893
|
+
except zipfile.BadZipFile:
|
|
894
|
+
raise ValueError("Invalid ZIP file")
|
|
895
|
+
except json.JSONDecodeError:
|
|
896
|
+
raise ValueError("Invalid project.json format")
|
|
897
|
+
except KeyError as e:
|
|
898
|
+
raise ValueError(f"Missing required field in project.json: {e}")
|
|
899
|
+
|
|
900
|
+
def _resolve_project_name_conflict(
|
|
901
|
+
self, db, desired_name: str, user_id: str
|
|
902
|
+
) -> str:
|
|
903
|
+
"""
|
|
904
|
+
Resolve project name conflicts by appending (2), (3), etc.
|
|
905
|
+
Similar to prompt import conflict resolution.
|
|
906
|
+
|
|
907
|
+
Args:
|
|
908
|
+
db: Database session
|
|
909
|
+
desired_name: The desired project name
|
|
910
|
+
user_id: The user ID
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
str: A unique project name
|
|
914
|
+
"""
|
|
915
|
+
project_repository = self._get_repositories(db)
|
|
916
|
+
existing_projects = project_repository.get_user_projects(user_id)
|
|
917
|
+
existing_names = {p.name.lower() for p in existing_projects}
|
|
918
|
+
|
|
919
|
+
if desired_name.lower() not in existing_names:
|
|
920
|
+
return desired_name
|
|
921
|
+
|
|
922
|
+
# Try appending (2), (3), etc.
|
|
923
|
+
counter = 2
|
|
924
|
+
while True:
|
|
925
|
+
candidate = f"{desired_name} ({counter})"
|
|
926
|
+
if candidate.lower() not in existing_names:
|
|
927
|
+
return candidate
|
|
928
|
+
counter += 1
|
|
929
|
+
if counter > 100: # Safety limit
|
|
930
|
+
raise ValueError("Unable to resolve name conflict")
|