solace-agent-mesh 1.6.3__py3-none-any.whl → 1.7.1__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/adk_llm.txt +12 -18
- solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +1 -1
- solace_agent_mesh/agent/adk/callbacks.py +138 -20
- solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +2 -0
- solace_agent_mesh/agent/adk/models/lite_llm.py +38 -5
- solace_agent_mesh/agent/adk/models/models_llm.txt +82 -35
- solace_agent_mesh/agent/adk/runner.py +9 -0
- solace_agent_mesh/agent/adk/stream_parser.py +6 -1
- solace_agent_mesh/agent/adk/tool_wrapper.py +3 -0
- solace_agent_mesh/agent/agent_llm.txt +61 -70
- solace_agent_mesh/agent/protocol/event_handlers.py +29 -1
- solace_agent_mesh/agent/protocol/protocol_llm.txt +1 -1
- solace_agent_mesh/agent/proxies/a2a/a2a_llm.txt +190 -0
- solace_agent_mesh/agent/proxies/base/base_llm.txt +148 -0
- solace_agent_mesh/agent/proxies/proxies_llm.txt +283 -0
- solace_agent_mesh/agent/sac/app.py +22 -0
- solace_agent_mesh/agent/sac/component.py +76 -40
- solace_agent_mesh/agent/sac/sac_llm.txt +1 -1
- solace_agent_mesh/agent/sac/task_execution_context.py +21 -0
- solace_agent_mesh/agent/testing/testing_llm.txt +2 -1
- solace_agent_mesh/agent/tools/builtin_artifact_tools.py +13 -148
- solace_agent_mesh/agent/tools/dynamic_tool.py +2 -0
- solace_agent_mesh/agent/tools/tools_llm.txt +93 -80
- solace_agent_mesh/agent/tools/tools_llm_detail.txt +3 -2
- solace_agent_mesh/agent/utils/artifact_helpers.py +4 -0
- solace_agent_mesh/agent/utils/utils_llm.txt +16 -2
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/05749d90.c70b2be9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15ba94aa.92fea363.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15e40e79.36003774.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ac1795d.e4870a49.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ff0015d.b63ee53a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/547e15cc.2f7790c1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.45b32c2b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/64195356.c498c4d0.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6a520c9d.b6e3f2ce.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/71da7b71.374b9d54.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8024126c.fa0e7186.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/94e8668d.09ed9234.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{ab9708a8.3e6dd091.js → ab9708a8.245ae0ef.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.f902fad8.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/db5d6442.3daf1696.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.c9c50c7b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e3d9abda.d11c67a7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{e6f9706b.e74a984d.js → e6f9706b.045d0fa1.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/e92d0134.3bda61dd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.5099c51e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.f213fe0c.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.d9606d6a.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +18 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +196 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +6 -7
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes-deployment/index.html +47 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +160 -169
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +59 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +62 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +10 -6
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +4 -4
- 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 +27 -4
- 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 +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +5 -4
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +290 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +9 -9
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +4 -4
- 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 +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1762283454666.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1762283454666.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/docs_cmd.py +4 -1
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-D4_RMYRh.js → authCallback-tcIFZLis.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{client-UZ3qU6Bq.js → client-CRYdKo2Q.js} +3 -3
- solace_agent_mesh/client/webui/frontend/static/assets/main-CojeY_1w.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-ILja9MCG.js +353 -0
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-CINwxvwV.js +470 -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/common/a2a/a2a_llm.txt +13 -20
- solace_agent_mesh/common/a2a/protocol.py +5 -0
- solace_agent_mesh/common/a2a/types.py +1 -0
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +49 -11
- 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/a2a_spec/schemas/schemas_llm.txt +26 -9
- solace_agent_mesh/common/common_llm.txt +13 -34
- solace_agent_mesh/common/data_parts.py +20 -4
- solace_agent_mesh/common/middleware/middleware_llm.txt +1 -1
- solace_agent_mesh/common/sac/sac_llm.txt +1 -1
- solace_agent_mesh/common/sam_events/sam_events_llm.txt +1 -1
- solace_agent_mesh/common/services/employee_service.py +1 -1
- solace_agent_mesh/common/services/providers/providers_llm.txt +3 -2
- solace_agent_mesh/common/services/services_llm.txt +9 -4
- solace_agent_mesh/common/utils/embeds/constants.py +1 -0
- solace_agent_mesh/common/utils/embeds/embeds_llm.txt +1 -1
- solace_agent_mesh/common/utils/embeds/modifiers.py +2 -1
- solace_agent_mesh/common/utils/embeds/resolver.py +58 -6
- solace_agent_mesh/common/utils/embeds/types.py +8 -0
- solace_agent_mesh/common/utils/utils_llm.txt +5 -6
- solace_agent_mesh/core_a2a/core_a2a_llm.txt +1 -1
- solace_agent_mesh/gateway/adapter/__init__.py +1 -0
- solace_agent_mesh/gateway/adapter/base.py +143 -0
- solace_agent_mesh/gateway/adapter/types.py +221 -0
- solace_agent_mesh/gateway/base/app.py +29 -2
- solace_agent_mesh/gateway/base/base_llm.txt +10 -8
- solace_agent_mesh/gateway/base/component.py +573 -142
- solace_agent_mesh/gateway/gateway_llm.txt +55 -59
- 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 +650 -0
- solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +99 -49
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_fulltext_search_indexes.py +92 -0
- 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 +150 -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/versions_llm.txt +26 -20
- solace_agent_mesh/gateway/http_sse/app.py +0 -14
- solace_agent_mesh/gateway/http_sse/component.py +17 -56
- solace_agent_mesh/gateway/http_sse/components/components_llm.txt +1 -1
- solace_agent_mesh/gateway/http_sse/dependencies.py +21 -3
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +8 -8
- solace_agent_mesh/gateway/http_sse/main.py +23 -5
- solace_agent_mesh/gateway/http_sse/repository/__init__.py +19 -1
- solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +56 -98
- 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 +23 -1
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +47 -0
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +112 -4
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +9 -1
- solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +51 -60
- 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/session_model.py +7 -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/repository_llm.txt +125 -157
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +269 -8
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +143 -51
- solace_agent_mesh/gateway/http_sse/routers/config.py +69 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +198 -94
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +68 -18
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +13 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +30 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +51 -35
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +2 -0
- solace_agent_mesh/gateway/http_sse/routers/feedback.py +133 -2
- solace_agent_mesh/gateway/http_sse/routers/projects.py +542 -0
- solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +9 -11
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +154 -3
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +296 -4
- solace_agent_mesh/gateway/http_sse/services/project_service.py +403 -0
- solace_agent_mesh/gateway/http_sse/services/services_llm.txt +16 -10
- solace_agent_mesh/gateway/http_sse/services/session_service.py +178 -6
- solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +2 -3
- solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +48 -14
- solace_agent_mesh/solace_agent_mesh_llm.txt +1 -1
- {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.1.dist-info}/METADATA +3 -5
- {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.1.dist-info}/RECORD +218 -175
- solace_agent_mesh/assets/docs/assets/js/15ba94aa.932dd2db.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/5c2bd65f.eda4bcb2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/631738c7.7c4594c9.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/71da7b71.ddbdfbe2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/8024126c.56e59919.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/94e8668d.3b883666.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/e92d0134.4f395c6b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.720d2ef2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/main.ed05b14d.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.a8a75e0b.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1761744323675.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1761744323675.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main--3yJYl7S.css +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-DojKHS49.js +0 -342
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-DSqhjwq_.js +0 -405
- /solace_agent_mesh/assets/docs/assets/js/{main.ed05b14d.js.LICENSE.txt → main.f213fe0c.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.1.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.1.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Business service for project-related operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
6
|
+
import logging
|
|
7
|
+
from fastapi import UploadFile
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
from ....agent.utils.artifact_helpers import get_artifact_info_list, save_artifact_with_metadata
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from google.adk.artifacts import BaseArtifactService
|
|
14
|
+
except ImportError:
|
|
15
|
+
|
|
16
|
+
class BaseArtifactService:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
from ....common.a2a.types import ArtifactInfo
|
|
21
|
+
from ..repository.interfaces import IProjectRepository
|
|
22
|
+
from ..repository.entities.project import Project
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from ..component import WebUIBackendComponent
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProjectService:
|
|
29
|
+
"""Service layer for project business logic."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
component: "WebUIBackendComponent" = None,
|
|
34
|
+
):
|
|
35
|
+
self.component = component
|
|
36
|
+
self.artifact_service = component.get_shared_artifact_service() if component else None
|
|
37
|
+
self.app_name = component.get_config("name", "WebUIBackendApp") if component else "WebUIBackendApp"
|
|
38
|
+
self.logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
def _get_repositories(self, db):
|
|
41
|
+
"""Create project repository for the given database session."""
|
|
42
|
+
from ..repository.project_repository import ProjectRepository
|
|
43
|
+
return ProjectRepository(db)
|
|
44
|
+
|
|
45
|
+
def is_persistence_enabled(self) -> bool:
|
|
46
|
+
"""Checks if the service is configured with a persistent backend."""
|
|
47
|
+
return self.component and self.component.database_url is not None
|
|
48
|
+
|
|
49
|
+
async def create_project(
|
|
50
|
+
self,
|
|
51
|
+
db,
|
|
52
|
+
name: str,
|
|
53
|
+
user_id: str,
|
|
54
|
+
description: Optional[str] = None,
|
|
55
|
+
system_prompt: Optional[str] = None,
|
|
56
|
+
default_agent_id: Optional[str] = None,
|
|
57
|
+
files: Optional[List[UploadFile]] = None,
|
|
58
|
+
file_metadata: Optional[dict] = None,
|
|
59
|
+
) -> Project:
|
|
60
|
+
"""
|
|
61
|
+
Create a new project for a user.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
db: Database session
|
|
65
|
+
name: Project name
|
|
66
|
+
user_id: ID of the user creating the project
|
|
67
|
+
description: Optional project description
|
|
68
|
+
system_prompt: Optional system prompt
|
|
69
|
+
default_agent_id: Optional default agent ID for new chats
|
|
70
|
+
files: Optional list of files to associate with the project
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
DomainProject: The created project
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If project name is invalid or user_id is missing
|
|
77
|
+
"""
|
|
78
|
+
self.logger.info(f"Creating new project '{name}' for user {user_id}")
|
|
79
|
+
|
|
80
|
+
# Business validation
|
|
81
|
+
if not name or not name.strip():
|
|
82
|
+
raise ValueError("Project name cannot be empty")
|
|
83
|
+
|
|
84
|
+
if not user_id:
|
|
85
|
+
raise ValueError("User ID is required to create a project")
|
|
86
|
+
|
|
87
|
+
project_repository = self._get_repositories(db)
|
|
88
|
+
|
|
89
|
+
# Check for duplicate project name for this user
|
|
90
|
+
existing_projects = project_repository.get_user_projects(user_id)
|
|
91
|
+
if any(p.name.lower() == name.strip().lower() for p in existing_projects):
|
|
92
|
+
raise ValueError(f"A project with the name '{name.strip()}' already exists")
|
|
93
|
+
|
|
94
|
+
# Create the project
|
|
95
|
+
project_domain = project_repository.create_project(
|
|
96
|
+
name=name.strip(),
|
|
97
|
+
user_id=user_id,
|
|
98
|
+
description=description.strip() if description else None,
|
|
99
|
+
system_prompt=system_prompt.strip() if system_prompt else None,
|
|
100
|
+
default_agent_id=default_agent_id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if files and self.artifact_service:
|
|
104
|
+
self.logger.info(
|
|
105
|
+
f"Project {project_domain.id} created, now saving {len(files)} artifacts."
|
|
106
|
+
)
|
|
107
|
+
project_session_id = f"project-{project_domain.id}"
|
|
108
|
+
for file in files:
|
|
109
|
+
content_bytes = await file.read()
|
|
110
|
+
metadata = {"source": "project"}
|
|
111
|
+
if file_metadata and file.filename in file_metadata:
|
|
112
|
+
desc = file_metadata[file.filename]
|
|
113
|
+
if desc:
|
|
114
|
+
metadata["description"] = desc
|
|
115
|
+
|
|
116
|
+
await save_artifact_with_metadata(
|
|
117
|
+
artifact_service=self.artifact_service,
|
|
118
|
+
app_name=self.app_name,
|
|
119
|
+
user_id=project_domain.user_id,
|
|
120
|
+
session_id=project_session_id,
|
|
121
|
+
filename=file.filename,
|
|
122
|
+
content_bytes=content_bytes,
|
|
123
|
+
mime_type=file.content_type,
|
|
124
|
+
metadata_dict=metadata,
|
|
125
|
+
timestamp=datetime.now(timezone.utc),
|
|
126
|
+
)
|
|
127
|
+
self.logger.info(f"Saved {len(files)} artifacts for project {project_domain.id}")
|
|
128
|
+
|
|
129
|
+
self.logger.info(
|
|
130
|
+
f"Successfully created project {project_domain.id} for user {user_id}"
|
|
131
|
+
)
|
|
132
|
+
return project_domain
|
|
133
|
+
|
|
134
|
+
def get_project(self, db, project_id: str, user_id: str) -> Optional[Project]:
|
|
135
|
+
"""
|
|
136
|
+
Get a project by ID, ensuring the user has access to it.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
db: Database session
|
|
140
|
+
project_id: The project ID
|
|
141
|
+
user_id: The requesting user ID
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Optional[Project]: The project if found and accessible, None otherwise
|
|
145
|
+
"""
|
|
146
|
+
project_repository = self._get_repositories(db)
|
|
147
|
+
return project_repository.get_by_id(project_id, user_id)
|
|
148
|
+
|
|
149
|
+
def get_user_projects(self, db, user_id: str) -> List[Project]:
|
|
150
|
+
"""
|
|
151
|
+
Get all projects owned by a specific user.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
db: Database session
|
|
155
|
+
user_id: The user ID
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List[DomainProject]: List of user's projects
|
|
159
|
+
"""
|
|
160
|
+
self.logger.debug(f"Retrieving projects for user {user_id}")
|
|
161
|
+
project_repository = self._get_repositories(db)
|
|
162
|
+
db_projects = project_repository.get_user_projects(user_id)
|
|
163
|
+
return db_projects
|
|
164
|
+
|
|
165
|
+
async def get_project_artifacts(self, db, project_id: str, user_id: str) -> List[ArtifactInfo]:
|
|
166
|
+
"""
|
|
167
|
+
Get a list of artifacts for a given project.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
db: The database session
|
|
171
|
+
project_id: The project ID
|
|
172
|
+
user_id: The requesting user ID
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List[ArtifactInfo]: A list of artifacts
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
ValueError: If project not found or access denied
|
|
179
|
+
"""
|
|
180
|
+
project = self.get_project(db, project_id, user_id)
|
|
181
|
+
if not project:
|
|
182
|
+
raise ValueError("Project not found or access denied")
|
|
183
|
+
|
|
184
|
+
if not self.artifact_service:
|
|
185
|
+
self.logger.warning(f"Attempted to get artifacts for project {project_id} but no artifact service is configured.")
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
storage_user_id = project.user_id
|
|
189
|
+
storage_session_id = f"project-{project.id}"
|
|
190
|
+
|
|
191
|
+
self.logger.info(f"Fetching artifacts for project {project.id} with storage session {storage_session_id} and user {storage_user_id}")
|
|
192
|
+
|
|
193
|
+
artifacts = await get_artifact_info_list(
|
|
194
|
+
artifact_service=self.artifact_service,
|
|
195
|
+
app_name=self.app_name,
|
|
196
|
+
user_id=storage_user_id,
|
|
197
|
+
session_id=storage_session_id,
|
|
198
|
+
)
|
|
199
|
+
return artifacts
|
|
200
|
+
|
|
201
|
+
async def add_artifacts_to_project(
|
|
202
|
+
self,
|
|
203
|
+
db,
|
|
204
|
+
project_id: str,
|
|
205
|
+
user_id: str,
|
|
206
|
+
files: List[UploadFile],
|
|
207
|
+
file_metadata: Optional[dict] = None
|
|
208
|
+
) -> List[dict]:
|
|
209
|
+
"""
|
|
210
|
+
Add one or more artifacts to a project.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
db: The database session
|
|
214
|
+
project_id: The project ID
|
|
215
|
+
user_id: The requesting user ID
|
|
216
|
+
files: List of files to add
|
|
217
|
+
file_metadata: Optional dictionary of metadata (e.g., descriptions)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List[dict]: A list of results from the save operations
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
ValueError: If project not found or access denied
|
|
224
|
+
"""
|
|
225
|
+
project = self.get_project(db, project_id, user_id)
|
|
226
|
+
if not project:
|
|
227
|
+
raise ValueError("Project not found or access denied")
|
|
228
|
+
|
|
229
|
+
if not self.artifact_service:
|
|
230
|
+
self.logger.warning(f"Attempted to add artifacts to project {project_id} but no artifact service is configured.")
|
|
231
|
+
raise ValueError("Artifact service is not configured")
|
|
232
|
+
|
|
233
|
+
if not files:
|
|
234
|
+
return []
|
|
235
|
+
|
|
236
|
+
self.logger.info(f"Adding {len(files)} artifacts to project {project_id} for user {user_id}")
|
|
237
|
+
storage_session_id = f"project-{project.id}"
|
|
238
|
+
results = []
|
|
239
|
+
|
|
240
|
+
for file in files:
|
|
241
|
+
content_bytes = await file.read()
|
|
242
|
+
metadata = {"source": "project"}
|
|
243
|
+
if file_metadata and file.filename in file_metadata:
|
|
244
|
+
desc = file_metadata[file.filename]
|
|
245
|
+
if desc:
|
|
246
|
+
metadata["description"] = desc
|
|
247
|
+
|
|
248
|
+
result = await save_artifact_with_metadata(
|
|
249
|
+
artifact_service=self.artifact_service,
|
|
250
|
+
app_name=self.app_name,
|
|
251
|
+
user_id=project.user_id, # Always use project owner's ID for storage
|
|
252
|
+
session_id=storage_session_id,
|
|
253
|
+
filename=file.filename,
|
|
254
|
+
content_bytes=content_bytes,
|
|
255
|
+
mime_type=file.content_type,
|
|
256
|
+
metadata_dict=metadata,
|
|
257
|
+
timestamp=datetime.now(timezone.utc),
|
|
258
|
+
)
|
|
259
|
+
results.append(result)
|
|
260
|
+
|
|
261
|
+
self.logger.info(f"Finished adding {len(files)} artifacts to project {project_id}")
|
|
262
|
+
return results
|
|
263
|
+
|
|
264
|
+
async def delete_artifact_from_project(self, db, project_id: str, user_id: str, filename: str) -> bool:
|
|
265
|
+
"""
|
|
266
|
+
Deletes an artifact from a project.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
db: The database session
|
|
270
|
+
project_id: The project ID
|
|
271
|
+
user_id: The requesting user ID
|
|
272
|
+
filename: The filename of the artifact to delete
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
bool: True if deletion was attempted, False if project not found
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ValueError: If user cannot modify the project or artifact service is missing
|
|
279
|
+
"""
|
|
280
|
+
project = self.get_project(db, project_id, user_id)
|
|
281
|
+
if not project:
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
if not self.artifact_service:
|
|
285
|
+
self.logger.warning(f"Attempted to delete artifact from project {project_id} but no artifact service is configured.")
|
|
286
|
+
raise ValueError("Artifact service is not configured")
|
|
287
|
+
|
|
288
|
+
storage_session_id = f"project-{project.id}"
|
|
289
|
+
|
|
290
|
+
self.logger.info(f"Deleting artifact '{filename}' from project {project_id} for user {user_id}")
|
|
291
|
+
|
|
292
|
+
await self.artifact_service.delete_artifact(
|
|
293
|
+
app_name=self.app_name,
|
|
294
|
+
user_id=project.user_id, # Always use project owner's ID for storage
|
|
295
|
+
session_id=storage_session_id,
|
|
296
|
+
filename=filename,
|
|
297
|
+
)
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
def update_project(self, db, project_id: str, user_id: str,
|
|
301
|
+
name: Optional[str] = None, description: Optional[str] = None,
|
|
302
|
+
system_prompt: Optional[str] = None, default_agent_id: Optional[str] = ...) -> Optional[Project]:
|
|
303
|
+
"""
|
|
304
|
+
Update a project's details.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
db: Database session
|
|
308
|
+
project_id: The project ID
|
|
309
|
+
user_id: The requesting user ID
|
|
310
|
+
name: New project name (optional)
|
|
311
|
+
description: New project description (optional)
|
|
312
|
+
system_prompt: New system prompt (optional)
|
|
313
|
+
default_agent_id: New default agent ID (optional, use ... sentinel to indicate not provided)
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Optional[Project]: The updated project if successful, None otherwise
|
|
317
|
+
"""
|
|
318
|
+
# Validate business rules
|
|
319
|
+
if name is not None and name is not ... and not name.strip():
|
|
320
|
+
raise ValueError("Project name cannot be empty")
|
|
321
|
+
|
|
322
|
+
# Build update data
|
|
323
|
+
update_data = {}
|
|
324
|
+
if name is not None and name is not ...:
|
|
325
|
+
update_data["name"] = name.strip()
|
|
326
|
+
if description is not None and description is not ...:
|
|
327
|
+
update_data["description"] = description.strip() if description else None
|
|
328
|
+
if system_prompt is not None and system_prompt is not ...:
|
|
329
|
+
update_data["system_prompt"] = system_prompt.strip() if system_prompt else None
|
|
330
|
+
if default_agent_id is not ...:
|
|
331
|
+
update_data["default_agent_id"] = default_agent_id
|
|
332
|
+
|
|
333
|
+
if not update_data:
|
|
334
|
+
# Nothing to update - get existing project
|
|
335
|
+
return self.get_project(db, project_id, user_id)
|
|
336
|
+
|
|
337
|
+
project_repository = self._get_repositories(db)
|
|
338
|
+
self.logger.info(f"Updating project {project_id} for user {user_id}")
|
|
339
|
+
updated_project = project_repository.update(project_id, user_id, update_data)
|
|
340
|
+
|
|
341
|
+
if updated_project:
|
|
342
|
+
self.logger.info(f"Successfully updated project {project_id}")
|
|
343
|
+
|
|
344
|
+
return updated_project
|
|
345
|
+
|
|
346
|
+
def delete_project(self, db, project_id: str, user_id: str) -> bool:
|
|
347
|
+
"""
|
|
348
|
+
Delete a project.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
db: Database session
|
|
352
|
+
project_id: The project ID
|
|
353
|
+
user_id: The requesting user ID
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
bool: True if deleted successfully, False otherwise
|
|
357
|
+
"""
|
|
358
|
+
# First verify the project exists and user has access
|
|
359
|
+
existing_project = self.get_project(db, project_id, user_id)
|
|
360
|
+
if not existing_project:
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
project_repository = self._get_repositories(db)
|
|
364
|
+
self.logger.info(f"Deleting project {project_id} for user {user_id}")
|
|
365
|
+
success = project_repository.delete(project_id, user_id)
|
|
366
|
+
|
|
367
|
+
if success:
|
|
368
|
+
self.logger.info(f"Successfully deleted project {project_id}")
|
|
369
|
+
|
|
370
|
+
return success
|
|
371
|
+
|
|
372
|
+
def soft_delete_project(self, db, project_id: str, user_id: str) -> bool:
|
|
373
|
+
"""
|
|
374
|
+
Soft delete a project (mark as deleted without removing from database).
|
|
375
|
+
Also cascades soft delete to all sessions associated with this project.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
db: Database session
|
|
379
|
+
project_id: The project ID
|
|
380
|
+
user_id: The requesting user ID
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
bool: True if soft deleted successfully, False otherwise
|
|
384
|
+
"""
|
|
385
|
+
# First verify the project exists and user has access
|
|
386
|
+
existing_project = self.get_project(db, project_id, user_id)
|
|
387
|
+
if not existing_project:
|
|
388
|
+
self.logger.warning(f"Attempted to soft delete non-existent project {project_id} by user {user_id}")
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
self.logger.info(f"Soft deleting project {project_id} and its associated sessions for user {user_id}")
|
|
392
|
+
|
|
393
|
+
project_repository = self._get_repositories(db)
|
|
394
|
+
# Soft delete the project
|
|
395
|
+
success = project_repository.soft_delete(project_id, user_id)
|
|
396
|
+
|
|
397
|
+
if success:
|
|
398
|
+
from ..repository.session_repository import SessionRepository
|
|
399
|
+
session_repo = SessionRepository()
|
|
400
|
+
deleted_count = session_repo.soft_delete_by_project(db, project_id, user_id)
|
|
401
|
+
self.logger.info(f"Successfully soft deleted project {project_id} and {deleted_count} associated sessions")
|
|
402
|
+
|
|
403
|
+
return success
|
|
@@ -58,7 +58,7 @@ else:
|
|
|
58
58
|
- `cleanup_old_data() -> None` - Main orchestration method for cleaning up old data, calls cleanup methods for tasks and feedback
|
|
59
59
|
|
|
60
60
|
**Constants/Variables:**
|
|
61
|
-
- `MIN_RETENTION_DAYS: int` - Minimum retention period (
|
|
61
|
+
- `MIN_RETENTION_DAYS: int` - Minimum retention period (1 day)
|
|
62
62
|
- `MIN_CLEANUP_INTERVAL_HOURS: int` - Minimum cleanup interval (1 hour)
|
|
63
63
|
- `MIN_BATCH_SIZE: int` - Minimum batch size for deletion (1)
|
|
64
64
|
- `MAX_BATCH_SIZE: int` - Maximum batch size for deletion (10000)
|
|
@@ -160,16 +160,16 @@ asyncio.run(search_users())
|
|
|
160
160
|
- `is_persistence_enabled() -> bool` - Checks if the service is configured with a persistent backend
|
|
161
161
|
- `get_user_sessions(db: DbSession, user_id: UserId, pagination: PaginationParams | None = None) -> PaginatedResponse[Session]` - Retrieves paginated sessions for a user
|
|
162
162
|
- `get_session_details(db: DbSession, session_id: SessionId, user_id: UserId) -> Session | None` - Gets session details for a specific session
|
|
163
|
-
- `get_session_history(db: DbSession, session_id: SessionId, user_id: UserId, pagination: PaginationInfo | None = None) -> SessionHistory | None` - Gets session with messages
|
|
164
163
|
- `create_session(db: DbSession, user_id: UserId, name: str | None = None, agent_id: str | None = None, session_id: str | None = None) -> Optional[Session]` - Creates a new session
|
|
165
164
|
- `update_session_name(db: DbSession, session_id: SessionId, user_id: UserId, name: str) -> Session | None` - Updates session name
|
|
166
165
|
- `delete_session_with_notifications(db: DbSession, session_id: SessionId, user_id: UserId) -> bool` - Deletes session and notifies agents
|
|
167
|
-
- `
|
|
166
|
+
- `save_task(db: DbSession, task_id: str, session_id: str, user_id: str, user_message: Optional[str], message_bubbles: str, task_metadata: Optional[str] = None) -> ChatTask` - Saves a complete task interaction
|
|
167
|
+
- `get_session_tasks(db: DbSession, session_id: str, user_id: str) -> List[ChatTask]` - Gets all tasks for a session
|
|
168
|
+
- `get_session_messages_from_tasks(db: DbSession, session_id: str, user_id: str) -> List[Dict[str, Any]]` - Gets session messages by flattening task message_bubbles for backward compatibility
|
|
168
169
|
|
|
169
170
|
**Usage Examples:**
|
|
170
171
|
```python
|
|
171
172
|
from solace_agent_mesh.gateway.http_sse.services.session_service import SessionService
|
|
172
|
-
from solace_agent_mesh.gateway.http_sse.shared.enums import SenderType, MessageType
|
|
173
173
|
from sqlalchemy.orm import Session as DbSession
|
|
174
174
|
|
|
175
175
|
# Initialize with component
|
|
@@ -186,14 +186,20 @@ with your_session_factory() as db:
|
|
|
186
186
|
agent_id="assistant-agent"
|
|
187
187
|
)
|
|
188
188
|
|
|
189
|
-
#
|
|
190
|
-
|
|
189
|
+
# Save a task with message bubbles
|
|
190
|
+
import json
|
|
191
|
+
message_bubbles = json.dumps([
|
|
192
|
+
{"type": "user", "text": "Hello", "id": "msg1"},
|
|
193
|
+
{"type": "agent", "text": "Hi there!", "id": "msg2"}
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
task = session_service.save_task(
|
|
191
197
|
db=db,
|
|
198
|
+
task_id="task-123",
|
|
192
199
|
session_id=session.id,
|
|
193
200
|
user_id="user123",
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
sender_name="John Doe"
|
|
201
|
+
user_message="Hello",
|
|
202
|
+
message_bubbles=message_bubbles
|
|
197
203
|
)
|
|
198
204
|
|
|
199
205
|
# Get user's sessions with pagination
|
|
@@ -294,4 +300,4 @@ async def cancel_task_example():
|
|
|
294
300
|
asyncio.run(cancel_task_example())
|
|
295
301
|
```
|
|
296
302
|
|
|
297
|
-
# content_hash:
|
|
303
|
+
# content_hash: 09bc3008d9a6c148b0b9d7477d20b26514f7fc4ee72f36eda5348f8f7d51f2ea
|
|
@@ -42,13 +42,19 @@ class SessionService:
|
|
|
42
42
|
self,
|
|
43
43
|
db: DbSession,
|
|
44
44
|
user_id: UserId,
|
|
45
|
-
pagination: PaginationParams | None = None
|
|
45
|
+
pagination: PaginationParams | None = None,
|
|
46
|
+
project_id: str | None = None
|
|
46
47
|
) -> PaginatedResponse[Session]:
|
|
47
48
|
"""
|
|
48
|
-
Get paginated sessions for a user with full metadata.
|
|
49
|
-
|
|
49
|
+
Get paginated sessions for a user with full metadata including project names.
|
|
50
50
|
Uses default pagination if none provided (page 1, size 20).
|
|
51
51
|
Returns paginated response with pageNumber, pageSize, nextPage, totalPages, totalCount.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
db: Database session
|
|
55
|
+
user_id: User ID to filter sessions by
|
|
56
|
+
pagination: Pagination parameters
|
|
57
|
+
project_id: Optional project ID to filter sessions by (for project-specific views)
|
|
52
58
|
"""
|
|
53
59
|
if not user_id or user_id.strip() == "":
|
|
54
60
|
raise ValueError("User ID cannot be empty")
|
|
@@ -56,9 +62,24 @@ class SessionService:
|
|
|
56
62
|
pagination = get_pagination_or_default(pagination)
|
|
57
63
|
session_repository = self._get_repositories(db)
|
|
58
64
|
|
|
59
|
-
#
|
|
60
|
-
sessions = session_repository.find_by_user(db, user_id, pagination)
|
|
61
|
-
total_count = session_repository.count_by_user(db, user_id)
|
|
65
|
+
# Fetch sessions with optional project filtering
|
|
66
|
+
sessions = session_repository.find_by_user(db, user_id, pagination, project_id=project_id)
|
|
67
|
+
total_count = session_repository.count_by_user(db, user_id, project_id=project_id)
|
|
68
|
+
|
|
69
|
+
# Enrich sessions with project names
|
|
70
|
+
# Collect unique project IDs
|
|
71
|
+
project_ids = [s.project_id for s in sessions if s.project_id]
|
|
72
|
+
|
|
73
|
+
if project_ids:
|
|
74
|
+
# Fetch all projects in one query
|
|
75
|
+
from ..repository.models import ProjectModel
|
|
76
|
+
projects = db.query(ProjectModel).filter(ProjectModel.id.in_(project_ids)).all()
|
|
77
|
+
project_map = {p.id: p.name for p in projects}
|
|
78
|
+
|
|
79
|
+
# Map project names to sessions
|
|
80
|
+
for session in sessions:
|
|
81
|
+
if session.project_id:
|
|
82
|
+
session.project_name = project_map.get(session.project_id)
|
|
62
83
|
|
|
63
84
|
return PaginatedResponse.create(sessions, total_count, pagination)
|
|
64
85
|
|
|
@@ -78,6 +99,7 @@ class SessionService:
|
|
|
78
99
|
name: str | None = None,
|
|
79
100
|
agent_id: str | None = None,
|
|
80
101
|
session_id: str | None = None,
|
|
102
|
+
project_id: str | None = None,
|
|
81
103
|
) -> Optional[Session]:
|
|
82
104
|
if not self.is_persistence_enabled():
|
|
83
105
|
log.debug("Persistence is not enabled. Skipping session creation in DB.")
|
|
@@ -95,6 +117,7 @@ class SessionService:
|
|
|
95
117
|
user_id=user_id,
|
|
96
118
|
name=name,
|
|
97
119
|
agent_id=agent_id,
|
|
120
|
+
project_id=project_id,
|
|
98
121
|
created_time=now_ms,
|
|
99
122
|
updated_time=now_ms,
|
|
100
123
|
)
|
|
@@ -166,6 +189,155 @@ class SessionService:
|
|
|
166
189
|
|
|
167
190
|
return True
|
|
168
191
|
|
|
192
|
+
def soft_delete_session(
|
|
193
|
+
self, db: DbSession, session_id: SessionId, user_id: UserId
|
|
194
|
+
) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
Soft delete a session (mark as deleted without removing from database).
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
db: Database session
|
|
200
|
+
session_id: Session ID to soft delete
|
|
201
|
+
user_id: User ID performing the deletion
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
bool: True if soft deleted successfully, False otherwise
|
|
205
|
+
"""
|
|
206
|
+
if not self._is_valid_session_id(session_id):
|
|
207
|
+
raise ValueError("Invalid session ID")
|
|
208
|
+
|
|
209
|
+
session_repository = self._get_repositories(db)
|
|
210
|
+
session = session_repository.find_user_session(db, session_id, user_id)
|
|
211
|
+
if not session:
|
|
212
|
+
log.warning(
|
|
213
|
+
"Attempted to soft delete non-existent session %s by user %s",
|
|
214
|
+
session_id,
|
|
215
|
+
user_id,
|
|
216
|
+
)
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
if not session.can_be_deleted_by_user(user_id):
|
|
220
|
+
log.warning(
|
|
221
|
+
"User %s not authorized to soft delete session %s", user_id, session_id
|
|
222
|
+
)
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
deleted = session_repository.soft_delete(db, session_id, user_id)
|
|
226
|
+
if not deleted:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
log.info("Session %s soft deleted successfully by user %s", session_id, user_id)
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
def move_session_to_project(
|
|
233
|
+
self, db: DbSession, session_id: SessionId, user_id: UserId, new_project_id: str | None
|
|
234
|
+
) -> Session | None:
|
|
235
|
+
"""
|
|
236
|
+
Move a session to a different project.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
db: Database session
|
|
240
|
+
session_id: Session ID to move
|
|
241
|
+
user_id: User ID performing the move
|
|
242
|
+
new_project_id: New project ID (or None to remove from project)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Session: Updated session if successful, None otherwise
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
ValueError: If session or project validation fails
|
|
249
|
+
"""
|
|
250
|
+
if not self._is_valid_session_id(session_id):
|
|
251
|
+
raise ValueError("Invalid session ID")
|
|
252
|
+
|
|
253
|
+
# Validate project exists and user has access if project_id is provided
|
|
254
|
+
if new_project_id:
|
|
255
|
+
from ..repository.models import ProjectModel
|
|
256
|
+
project = db.query(ProjectModel).filter(
|
|
257
|
+
ProjectModel.id == new_project_id,
|
|
258
|
+
ProjectModel.user_id == user_id,
|
|
259
|
+
ProjectModel.deleted_at.is_(None)
|
|
260
|
+
).first()
|
|
261
|
+
|
|
262
|
+
if not project:
|
|
263
|
+
raise ValueError(f"Project {new_project_id} not found or access denied")
|
|
264
|
+
|
|
265
|
+
session_repository = self._get_repositories(db)
|
|
266
|
+
updated_session = session_repository.move_to_project(db, session_id, user_id, new_project_id)
|
|
267
|
+
|
|
268
|
+
if not updated_session:
|
|
269
|
+
log.warning(
|
|
270
|
+
"Failed to move session %s to project %s for user %s",
|
|
271
|
+
session_id,
|
|
272
|
+
new_project_id,
|
|
273
|
+
user_id,
|
|
274
|
+
)
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
log.info(
|
|
278
|
+
"Session %s moved to project %s by user %s",
|
|
279
|
+
session_id,
|
|
280
|
+
new_project_id or "None",
|
|
281
|
+
user_id,
|
|
282
|
+
)
|
|
283
|
+
return updated_session
|
|
284
|
+
|
|
285
|
+
def search_sessions(
|
|
286
|
+
self,
|
|
287
|
+
db: DbSession,
|
|
288
|
+
user_id: UserId,
|
|
289
|
+
query: str,
|
|
290
|
+
pagination: PaginationParams | None = None,
|
|
291
|
+
project_id: str | None = None
|
|
292
|
+
) -> PaginatedResponse[Session]:
|
|
293
|
+
"""
|
|
294
|
+
Search sessions by name or content.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
db: Database session
|
|
298
|
+
user_id: User ID to filter sessions by
|
|
299
|
+
query: Search query string
|
|
300
|
+
pagination: Pagination parameters
|
|
301
|
+
project_id: Optional project ID to filter sessions by
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
PaginatedResponse[Session]: Paginated search results
|
|
305
|
+
"""
|
|
306
|
+
if not user_id or user_id.strip() == "":
|
|
307
|
+
raise ValueError("User ID cannot be empty")
|
|
308
|
+
|
|
309
|
+
if not query or query.strip() == "":
|
|
310
|
+
raise ValueError("Search query cannot be empty")
|
|
311
|
+
|
|
312
|
+
pagination = get_pagination_or_default(pagination)
|
|
313
|
+
session_repository = self._get_repositories(db)
|
|
314
|
+
|
|
315
|
+
# Search sessions
|
|
316
|
+
sessions = session_repository.search(db, user_id, query.strip(), pagination, project_id)
|
|
317
|
+
total_count = session_repository.count_search_results(db, user_id, query.strip(), project_id)
|
|
318
|
+
|
|
319
|
+
# Enrich sessions with project names
|
|
320
|
+
project_ids = [s.project_id for s in sessions if s.project_id]
|
|
321
|
+
|
|
322
|
+
if project_ids:
|
|
323
|
+
from ..repository.models import ProjectModel
|
|
324
|
+
projects = db.query(ProjectModel).filter(ProjectModel.id.in_(project_ids)).all()
|
|
325
|
+
project_map = {p.id: p.name for p in projects}
|
|
326
|
+
|
|
327
|
+
for session in sessions:
|
|
328
|
+
if session.project_id:
|
|
329
|
+
session.project_name = project_map.get(session.project_id)
|
|
330
|
+
|
|
331
|
+
log.info(
|
|
332
|
+
"Search for '%s' by user %s returned %d results (total: %d)",
|
|
333
|
+
query,
|
|
334
|
+
user_id,
|
|
335
|
+
len(sessions),
|
|
336
|
+
total_count,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return PaginatedResponse.create(sessions, total_count, pagination)
|
|
340
|
+
|
|
169
341
|
def save_task(
|
|
170
342
|
self,
|
|
171
343
|
db: DbSession,
|