solace-agent-mesh 1.1.0__py3-none-any.whl → 1.3.0__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/runner.py +18 -12
- solace_agent_mesh/agent/adk/services.py +3 -3
- solace_agent_mesh/agent/protocol/event_handlers.py +27 -21
- solace_agent_mesh/agent/sac/app.py +1 -1
- solace_agent_mesh/agent/sac/component.py +0 -1
- solace_agent_mesh/assets/docs/404.html +2 -2
- solace_agent_mesh/assets/docs/assets/js/{main.a75ecc0d.js → main.08d30374.js} +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/migration-guides/a2a-upgrade-to-0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/migration-guides/a2a-upgrade-to-0.3.0/a2a-technical-migration-map/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +2 -2
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +2 -2
- solace_agent_mesh/assets/docs/lunr-index-1757433031159.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1757433031159.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +125 -48
- solace_agent_mesh/cli/commands/eval_cmd.py +14 -0
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +53 -31
- solace_agent_mesh/cli/commands/init_cmd/database_step.py +91 -0
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +19 -8
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +80 -25
- solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +32 -10
- solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +74 -15
- solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +0 -2
- solace_agent_mesh/cli/commands/run_cmd.py +5 -3
- solace_agent_mesh/cli/utils.py +68 -12
- solace_agent_mesh/client/webui/frontend/static/assets/authCallback-vY5eu2lI.js +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/client-BeBkzgWW.js +25 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-Bjys1KQs.js +339 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-C03yrETa.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-CE0AeXyK.js +395 -0
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -2
- solace_agent_mesh/client/webui/frontend/static/index.html +4 -3
- solace_agent_mesh/common/utils/embeds/resolver.py +1 -0
- solace_agent_mesh/config_portal/backend/common.py +2 -2
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-bFMKlzKf.js +98 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-d845808d.js → manifest-89db7c30.js} +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
- solace_agent_mesh/evaluation/message_organizer.py +35 -56
- solace_agent_mesh/evaluation/run.py +26 -5
- solace_agent_mesh/evaluation/subscriber.py +35 -10
- solace_agent_mesh/evaluation/summary_builder.py +27 -34
- solace_agent_mesh/gateway/http_sse/ARCHITECTURE_GUIDE.md +676 -0
- solace_agent_mesh/gateway/http_sse/alembic/env.py +85 -0
- solace_agent_mesh/gateway/http_sse/alembic/script.py.mako +28 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/b1c2d3e4f5g6_add_database_indexes.py +83 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/d5b3f8f2e9a0_create_initial_database.py +58 -0
- solace_agent_mesh/gateway/http_sse/alembic.ini +147 -0
- solace_agent_mesh/gateway/http_sse/api/__init__.py +11 -0
- solace_agent_mesh/gateway/http_sse/api/controllers/__init__.py +9 -0
- solace_agent_mesh/gateway/http_sse/api/controllers/session_controller.py +355 -0
- solace_agent_mesh/gateway/http_sse/api/controllers/task_controller.py +279 -0
- solace_agent_mesh/gateway/http_sse/api/controllers/user_controller.py +35 -0
- solace_agent_mesh/gateway/http_sse/api/dto/__init__.py +10 -0
- solace_agent_mesh/gateway/http_sse/api/dto/requests/__init__.py +37 -0
- solace_agent_mesh/gateway/http_sse/api/dto/requests/session_requests.py +49 -0
- solace_agent_mesh/gateway/http_sse/api/dto/requests/task_requests.py +66 -0
- solace_agent_mesh/gateway/http_sse/api/dto/responses/__init__.py +43 -0
- solace_agent_mesh/gateway/http_sse/api/dto/responses/session_responses.py +68 -0
- solace_agent_mesh/gateway/http_sse/api/dto/responses/task_responses.py +74 -0
- solace_agent_mesh/gateway/http_sse/app.py +31 -1
- solace_agent_mesh/gateway/http_sse/application/__init__.py +3 -0
- solace_agent_mesh/gateway/http_sse/application/services/__init__.py +3 -0
- solace_agent_mesh/gateway/http_sse/application/services/session_service.py +135 -0
- solace_agent_mesh/gateway/http_sse/component.py +224 -62
- solace_agent_mesh/gateway/http_sse/dependencies.py +142 -39
- solace_agent_mesh/gateway/http_sse/domain/entities/__init__.py +3 -0
- solace_agent_mesh/gateway/http_sse/domain/entities/session.py +90 -0
- solace_agent_mesh/gateway/http_sse/domain/repositories/__init__.py +3 -0
- solace_agent_mesh/gateway/http_sse/domain/repositories/session_repository.py +54 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/__init__.py +4 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/dependency_injection/__init__.py +3 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/dependency_injection/container.py +123 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence/__init__.py +4 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence/database_persistence_service.py +16 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence/database_service.py +119 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence/models.py +31 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence_service.py +12 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/repositories/__init__.py +3 -0
- solace_agent_mesh/gateway/http_sse/infrastructure/repositories/session_repository.py +174 -0
- solace_agent_mesh/gateway/http_sse/main.py +289 -85
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +121 -54
- solace_agent_mesh/gateway/http_sse/routers/config.py +3 -1
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +83 -2
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +7 -7
- solace_agent_mesh/gateway/http_sse/session_manager.py +64 -30
- solace_agent_mesh/gateway/http_sse/shared/__init__.py +9 -0
- solace_agent_mesh/gateway/http_sse/shared/auth_utils.py +29 -0
- solace_agent_mesh/gateway/http_sse/shared/enums.py +45 -0
- solace_agent_mesh/gateway/http_sse/shared/types.py +45 -0
- solace_agent_mesh/templates/shared_config.yaml +4 -5
- solace_agent_mesh/templates/webui.yaml +8 -10
- {solace_agent_mesh-1.1.0.dist-info → solace_agent_mesh-1.3.0.dist-info}/METADATA +5 -3
- {solace_agent_mesh-1.1.0.dist-info → solace_agent_mesh-1.3.0.dist-info}/RECORD +130 -91
- solace_agent_mesh/assets/docs/lunr-index-1756992446316.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1756992446316.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/authCallback-BmF2l6vg.js +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/client-D881Dttc.js +0 -49
- solace_agent_mesh/client/webui/frontend/static/assets/main-C0jZjYa8.js +0 -699
- solace_agent_mesh/client/webui/frontend/static/assets/main-CCeG324-.css +0 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-Bym6YkMd.js +0 -98
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +0 -85
- solace_agent_mesh/gateway/http_sse/routers/users.py +0 -59
- /solace_agent_mesh/assets/docs/assets/js/{main.a75ecc0d.js.LICENSE.txt → main.08d30374.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.1.0.dist-info → solace_agent_mesh-1.3.0.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.1.0.dist-info → solace_agent_mesh-1.3.0.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.1.0.dist-info → solace_agent_mesh-1.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,39 +3,40 @@ Defines FastAPI dependency injectors to access shared resources
|
|
|
3
3
|
managed by the WebUIBackendComponent.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
from typing import
|
|
8
|
-
TYPE_CHECKING,
|
|
9
|
-
Callable,
|
|
10
|
-
Dict,
|
|
11
|
-
Optional,
|
|
12
|
-
Any,
|
|
13
|
-
)
|
|
14
|
-
import os
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
15
8
|
|
|
9
|
+
from fastapi import Depends, HTTPException, Request, status
|
|
16
10
|
from solace_ai_connector.common.log import log
|
|
17
|
-
from ...gateway.http_sse.sse_manager import SSEManager
|
|
18
|
-
from ...gateway.http_sse.session_manager import SessionManager
|
|
19
|
-
from ...common.agent_registry import AgentRegistry
|
|
20
11
|
|
|
12
|
+
from ...common.agent_registry import AgentRegistry
|
|
13
|
+
from ...common.middleware.config_resolver import ConfigResolver
|
|
14
|
+
from ...common.services.identity_service import BaseIdentityService
|
|
15
|
+
from ...core_a2a.service import CoreA2AService
|
|
16
|
+
from ...gateway.base.task_context import TaskContextManager
|
|
21
17
|
from ...gateway.http_sse.services.agent_service import AgentService
|
|
22
|
-
from ...gateway.http_sse.services.task_service import TaskService
|
|
23
18
|
from ...gateway.http_sse.services.people_service import PeopleService
|
|
24
|
-
from ...gateway.
|
|
25
|
-
|
|
26
|
-
from ...
|
|
27
|
-
from
|
|
28
|
-
from
|
|
19
|
+
from ...gateway.http_sse.services.task_service import TaskService
|
|
20
|
+
from ...gateway.http_sse.session_manager import SessionManager
|
|
21
|
+
from ...gateway.http_sse.sse_manager import SSEManager
|
|
22
|
+
from .application.services.session_service import SessionService
|
|
23
|
+
from .infrastructure.persistence_service import PersistenceService
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
try:
|
|
26
|
+
from google.adk.artifacts import BaseArtifactService
|
|
27
|
+
except ImportError:
|
|
28
|
+
# Mock BaseArtifactService for environments without Google ADK
|
|
29
|
+
class BaseArtifactService:
|
|
30
|
+
pass
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from gateway.http_sse.component import WebUIBackendComponent
|
|
35
35
|
|
|
36
36
|
sac_component_instance: "WebUIBackendComponent" = None
|
|
37
|
+
persistence_service_instance: "PersistenceService" = None
|
|
37
38
|
|
|
38
|
-
api_config:
|
|
39
|
+
api_config: dict[str, Any] | None = None
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def set_component_instance(component: "WebUIBackendComponent"):
|
|
@@ -48,7 +49,30 @@ def set_component_instance(component: "WebUIBackendComponent"):
|
|
|
48
49
|
log.warning("[Dependencies] SAC Component instance already set.")
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def
|
|
52
|
+
def set_persistence_service(persistence_service: "PersistenceService"):
|
|
53
|
+
"""Called by the component during its startup to provide its instance."""
|
|
54
|
+
global persistence_service_instance
|
|
55
|
+
if persistence_service_instance is None:
|
|
56
|
+
persistence_service_instance = persistence_service
|
|
57
|
+
log.info("[Dependencies] Persistence Service instance provided.")
|
|
58
|
+
else:
|
|
59
|
+
log.warning("[Dependencies] Persistence Service instance already set.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_persistence_service() -> "PersistenceService":
|
|
63
|
+
"""FastAPI dependency to get the PersistenceService instance."""
|
|
64
|
+
if persistence_service_instance is None:
|
|
65
|
+
log.warning(
|
|
66
|
+
"[Dependencies] PersistenceService not available - running in compatibility mode"
|
|
67
|
+
)
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
70
|
+
detail="Persistence service not available in compatibility mode.",
|
|
71
|
+
)
|
|
72
|
+
return persistence_service_instance
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_api_config(config: dict[str, Any]):
|
|
52
76
|
"""Called during startup to provide API configuration."""
|
|
53
77
|
global api_config
|
|
54
78
|
if api_config is None:
|
|
@@ -71,7 +95,7 @@ def get_sac_component() -> "WebUIBackendComponent":
|
|
|
71
95
|
return sac_component_instance
|
|
72
96
|
|
|
73
97
|
|
|
74
|
-
def get_api_config() ->
|
|
98
|
+
def get_api_config() -> dict[str, Any]:
|
|
75
99
|
"""FastAPI dependency to get the API configuration."""
|
|
76
100
|
if api_config is None:
|
|
77
101
|
log.critical("[Dependencies] API configuration accessed before it was set!")
|
|
@@ -128,26 +152,43 @@ def get_user_id(
|
|
|
128
152
|
) -> str:
|
|
129
153
|
"""
|
|
130
154
|
FastAPI dependency that returns the user's identity.
|
|
131
|
-
|
|
132
|
-
|
|
155
|
+
When FRONTEND_USE_AUTHORIZATION is true: Fully relies on OAuth - user must be authenticated by AuthMiddleware.
|
|
156
|
+
When FRONTEND_USE_AUTHORIZATION is false: Uses development fallback user.
|
|
133
157
|
"""
|
|
134
158
|
log.debug("[Dependencies] Resolving user_id string")
|
|
135
159
|
|
|
160
|
+
# AuthMiddleware should always set user state for both auth enabled/disabled cases
|
|
136
161
|
if hasattr(request.state, "user") and request.state.user:
|
|
137
162
|
user_id = request.state.user.get("id")
|
|
138
163
|
if user_id:
|
|
139
164
|
log.debug(f"[Dependencies] Using user ID from AuthMiddleware: {user_id}")
|
|
140
165
|
return user_id
|
|
141
166
|
else:
|
|
142
|
-
log.
|
|
143
|
-
"[Dependencies] request.state.user exists but has no 'id' field.
|
|
167
|
+
log.error(
|
|
168
|
+
"[Dependencies] request.state.user exists but has no 'id' field: %s. This indicates a bug in AuthMiddleware.",
|
|
169
|
+
request.state.user,
|
|
144
170
|
)
|
|
145
171
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
# If we reach here, AuthMiddleware didn't set user state properly
|
|
173
|
+
use_authorization = session_manager.use_authorization
|
|
174
|
+
|
|
175
|
+
if use_authorization:
|
|
176
|
+
# When OAuth is enabled, we should never reach here - AuthMiddleware should have handled authentication
|
|
177
|
+
log.error(
|
|
178
|
+
"[Dependencies] OAuth is enabled but no authenticated user found. This indicates an authentication failure or middleware bug."
|
|
179
|
+
)
|
|
180
|
+
raise HTTPException(
|
|
181
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
182
|
+
detail="Authentication required but user not found",
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
# When auth is disabled, use development fallback user
|
|
186
|
+
fallback_id = "sam_dev_user"
|
|
187
|
+
log.info(
|
|
188
|
+
"[Dependencies] Authorization disabled and no user in request state, using fallback user: %s",
|
|
189
|
+
fallback_id,
|
|
190
|
+
)
|
|
191
|
+
return fallback_id
|
|
151
192
|
|
|
152
193
|
|
|
153
194
|
def ensure_session_id(
|
|
@@ -161,21 +202,21 @@ def ensure_session_id(
|
|
|
161
202
|
|
|
162
203
|
def get_identity_service(
|
|
163
204
|
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
164
|
-
) ->
|
|
205
|
+
) -> BaseIdentityService | None:
|
|
165
206
|
"""FastAPI dependency to get the configured IdentityService instance."""
|
|
166
207
|
log.debug("[Dependencies] get_identity_service called")
|
|
167
208
|
return component.identity_service
|
|
168
209
|
|
|
169
210
|
|
|
170
211
|
def get_people_service(
|
|
171
|
-
identity_service:
|
|
212
|
+
identity_service: BaseIdentityService | None = Depends(get_identity_service),
|
|
172
213
|
) -> PeopleService:
|
|
173
214
|
"""FastAPI dependency to get an instance of PeopleService."""
|
|
174
215
|
log.debug("[Dependencies] get_people_service called")
|
|
175
216
|
return PeopleService(identity_service=identity_service)
|
|
176
217
|
|
|
177
218
|
|
|
178
|
-
PublishFunc = Callable[[str,
|
|
219
|
+
PublishFunc = Callable[[str, dict, dict | None], None]
|
|
179
220
|
|
|
180
221
|
|
|
181
222
|
def get_publish_a2a_func(
|
|
@@ -212,7 +253,7 @@ def get_config_resolver(
|
|
|
212
253
|
|
|
213
254
|
def get_app_config(
|
|
214
255
|
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
215
|
-
) ->
|
|
256
|
+
) -> dict[str, Any]:
|
|
216
257
|
"""
|
|
217
258
|
FastAPI dependency to safely get the application configuration dictionary.
|
|
218
259
|
"""
|
|
@@ -225,8 +266,8 @@ async def get_user_config(
|
|
|
225
266
|
user_id: str = Depends(get_user_id),
|
|
226
267
|
config_resolver: ConfigResolver = Depends(get_config_resolver),
|
|
227
268
|
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
228
|
-
app_config:
|
|
229
|
-
) ->
|
|
269
|
+
app_config: dict[str, Any] = Depends(get_app_config),
|
|
270
|
+
) -> dict[str, Any]:
|
|
230
271
|
"""
|
|
231
272
|
FastAPI dependency to get the user-specific configuration.
|
|
232
273
|
"""
|
|
@@ -243,7 +284,7 @@ async def get_user_config(
|
|
|
243
284
|
|
|
244
285
|
def get_shared_artifact_service(
|
|
245
286
|
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
246
|
-
) ->
|
|
287
|
+
) -> BaseArtifactService | None:
|
|
247
288
|
"""FastAPI dependency to get the shared ArtifactService."""
|
|
248
289
|
log.debug("[Dependencies] get_shared_artifact_service called")
|
|
249
290
|
return component.get_shared_artifact_service()
|
|
@@ -251,7 +292,7 @@ def get_shared_artifact_service(
|
|
|
251
292
|
|
|
252
293
|
def get_embed_config(
|
|
253
294
|
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
254
|
-
) ->
|
|
295
|
+
) -> dict[str, Any]:
|
|
255
296
|
"""FastAPI dependency to get embed-related configuration."""
|
|
256
297
|
log.debug("[Dependencies] get_embed_config called")
|
|
257
298
|
return component.get_embed_config()
|
|
@@ -314,3 +355,65 @@ def get_task_service(
|
|
|
314
355
|
task_context_lock=task_context_manager._lock,
|
|
315
356
|
app_name=app_name,
|
|
316
357
|
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def get_session_service(
|
|
361
|
+
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
362
|
+
) -> SessionService:
|
|
363
|
+
log.debug("[Dependencies] get_session_service called")
|
|
364
|
+
|
|
365
|
+
if (
|
|
366
|
+
hasattr(component, "persistence_service")
|
|
367
|
+
and component.persistence_service is not None
|
|
368
|
+
):
|
|
369
|
+
log.debug("Using database-backed session service")
|
|
370
|
+
container = component.persistence_service.container
|
|
371
|
+
return container.get_session_service()
|
|
372
|
+
else:
|
|
373
|
+
log.debug("No database configured - session persistence not available")
|
|
374
|
+
raise HTTPException(
|
|
375
|
+
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
376
|
+
detail="Session management requires database configuration. Configure 'database_url' in your gateway configuration.",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def get_session_validator(
|
|
381
|
+
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
382
|
+
) -> Callable[[str, str], bool]:
|
|
383
|
+
"""
|
|
384
|
+
FastAPI dependency that returns a session validator function.
|
|
385
|
+
|
|
386
|
+
With database: Validates sessions against database
|
|
387
|
+
Without database: Validates sessions using basic checks (session exists in recent activity)
|
|
388
|
+
"""
|
|
389
|
+
log.debug("[Dependencies] get_session_validator called")
|
|
390
|
+
|
|
391
|
+
# Check if component has a persistence service (database-backed)
|
|
392
|
+
if (
|
|
393
|
+
hasattr(component, "persistence_service")
|
|
394
|
+
and component.persistence_service is not None
|
|
395
|
+
):
|
|
396
|
+
log.debug("Using database-backed session validation")
|
|
397
|
+
|
|
398
|
+
def validate_with_database(session_id: str, user_id: str) -> bool:
|
|
399
|
+
container = component.persistence_service.container
|
|
400
|
+
session_service = container.get_session_service()
|
|
401
|
+
session_domain = session_service.get_session(
|
|
402
|
+
session_id=session_id, user_id=user_id
|
|
403
|
+
)
|
|
404
|
+
return session_domain is not None
|
|
405
|
+
|
|
406
|
+
return validate_with_database
|
|
407
|
+
else:
|
|
408
|
+
log.debug("No database configured - using basic session validation")
|
|
409
|
+
|
|
410
|
+
def validate_without_database(session_id: str, user_id: str) -> bool:
|
|
411
|
+
"""Basic validation: check session ID format and user authentication"""
|
|
412
|
+
# Validate session ID format (should be web-session-* format from SessionManager)
|
|
413
|
+
if not session_id or not session_id.startswith("web-session-"):
|
|
414
|
+
return False
|
|
415
|
+
# If user_id is provided (authenticated), allow access
|
|
416
|
+
# This provides basic security while allowing filesystem-based artifacts
|
|
417
|
+
return bool(user_id)
|
|
418
|
+
|
|
419
|
+
return validate_without_database
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from ...shared.enums import MessageType, SenderType, SessionStatus
|
|
6
|
+
from ...shared.types import AgentId, MessageId, SessionId, UserId
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Session(BaseModel):
|
|
10
|
+
id: SessionId
|
|
11
|
+
user_id: UserId
|
|
12
|
+
name: str | None = None
|
|
13
|
+
agent_id: AgentId | None = None
|
|
14
|
+
status: SessionStatus = SessionStatus.ACTIVE
|
|
15
|
+
created_at: datetime
|
|
16
|
+
updated_at: datetime | None = None
|
|
17
|
+
last_activity: datetime | None = None
|
|
18
|
+
|
|
19
|
+
def update_name(self, new_name: str) -> None:
|
|
20
|
+
if not new_name or len(new_name.strip()) == 0:
|
|
21
|
+
raise ValueError("Session name cannot be empty")
|
|
22
|
+
if len(new_name) > 255:
|
|
23
|
+
raise ValueError("Session name cannot exceed 255 characters")
|
|
24
|
+
|
|
25
|
+
self.name = new_name.strip()
|
|
26
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
27
|
+
|
|
28
|
+
def mark_activity(self) -> None:
|
|
29
|
+
self.last_activity = datetime.now(timezone.utc)
|
|
30
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
31
|
+
|
|
32
|
+
def archive(self) -> None:
|
|
33
|
+
self.status = SessionStatus.ARCHIVED
|
|
34
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
35
|
+
|
|
36
|
+
def activate(self) -> None:
|
|
37
|
+
self.status = SessionStatus.ACTIVE
|
|
38
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
39
|
+
|
|
40
|
+
def can_be_deleted_by_user(self, user_id: UserId) -> bool:
|
|
41
|
+
return self.user_id == user_id
|
|
42
|
+
|
|
43
|
+
def can_be_accessed_by_user(self, user_id: UserId) -> bool:
|
|
44
|
+
return self.user_id == user_id
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Message(BaseModel):
|
|
48
|
+
id: MessageId
|
|
49
|
+
session_id: SessionId
|
|
50
|
+
message: str
|
|
51
|
+
sender_type: SenderType
|
|
52
|
+
sender_name: str
|
|
53
|
+
message_type: MessageType = MessageType.TEXT
|
|
54
|
+
created_at: datetime
|
|
55
|
+
|
|
56
|
+
def validate_message_content(self) -> None:
|
|
57
|
+
if not self.message or len(self.message.strip()) == 0:
|
|
58
|
+
raise ValueError("Message content cannot be empty")
|
|
59
|
+
if len(self.message) > 10_000_000:
|
|
60
|
+
raise ValueError("Message content exceeds maximum length (10MB)")
|
|
61
|
+
|
|
62
|
+
def is_from_user(self) -> bool:
|
|
63
|
+
return self.sender_type == SenderType.USER
|
|
64
|
+
|
|
65
|
+
def is_from_agent(self) -> bool:
|
|
66
|
+
return self.sender_type == SenderType.AGENT
|
|
67
|
+
|
|
68
|
+
def is_system_message(self) -> bool:
|
|
69
|
+
return self.sender_type == SenderType.SYSTEM
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SessionHistory(BaseModel):
|
|
73
|
+
session: Session
|
|
74
|
+
messages: list[Message] = []
|
|
75
|
+
total_message_count: int = 0
|
|
76
|
+
|
|
77
|
+
def add_message(self, message: Message) -> None:
|
|
78
|
+
if message.session_id != self.session.id:
|
|
79
|
+
raise ValueError("Message does not belong to this session")
|
|
80
|
+
|
|
81
|
+
message.validate_message_content()
|
|
82
|
+
self.messages.append(message)
|
|
83
|
+
self.total_message_count += 1
|
|
84
|
+
self.session.mark_activity()
|
|
85
|
+
|
|
86
|
+
def get_messages_by_sender_type(self, sender_type: SenderType) -> list[Message]:
|
|
87
|
+
return [msg for msg in self.messages if msg.sender_type == sender_type]
|
|
88
|
+
|
|
89
|
+
def get_latest_messages(self, count: int = 10) -> list[Message]:
|
|
90
|
+
return sorted(self.messages, key=lambda x: x.created_at, reverse=True)[:count]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from ...shared.types import PaginationInfo, SessionId, UserId
|
|
4
|
+
from ..entities.session import Message, Session
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ISessionRepository(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def get_by_id(self, session_id: SessionId) -> Session | None:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def get_by_user_id(
|
|
14
|
+
self, user_id: UserId, pagination: PaginationInfo | None = None
|
|
15
|
+
) -> list[Session]:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_user_session(
|
|
20
|
+
self, session_id: SessionId, user_id: UserId
|
|
21
|
+
) -> Session | None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def create(self, session: Session) -> Session:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def update(self, session: Session) -> Session | None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def delete(self, session_id: SessionId, user_id: UserId) -> bool:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def exists(self, session_id: SessionId) -> bool:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class IMessageRepository(ABC):
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get_by_session_id(
|
|
44
|
+
self, session_id: SessionId, pagination: PaginationInfo | None = None
|
|
45
|
+
) -> list[Message]:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def create(self, message: Message) -> Message:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def delete_by_session_id(self, session_id: SessionId) -> bool:
|
|
54
|
+
pass
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, TypeVar
|
|
3
|
+
|
|
4
|
+
from ...application.services.session_service import SessionService
|
|
5
|
+
from ...domain.repositories.session_repository import (
|
|
6
|
+
IMessageRepository,
|
|
7
|
+
ISessionRepository,
|
|
8
|
+
)
|
|
9
|
+
from ...infrastructure.persistence import database_service as db_service_module
|
|
10
|
+
from ...infrastructure.persistence.database_service import DatabaseService
|
|
11
|
+
from ...infrastructure.repositories.session_repository import (
|
|
12
|
+
MessageRepository,
|
|
13
|
+
SessionRepository,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DIContainer:
|
|
20
|
+
"""Dependency injection container."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._services: dict[str, Any] = {}
|
|
24
|
+
self._factories: dict[str, Callable] = {}
|
|
25
|
+
self._singletons: dict[str, Any] = {}
|
|
26
|
+
|
|
27
|
+
def register_singleton(self, service_type: type, instance: Any) -> None:
|
|
28
|
+
self._singletons[service_type.__name__] = instance
|
|
29
|
+
|
|
30
|
+
def register_factory(self, service_type: type, factory: Callable) -> None:
|
|
31
|
+
self._factories[service_type.__name__] = factory
|
|
32
|
+
|
|
33
|
+
def register_transient(self, service_type: type, implementation: type) -> None:
|
|
34
|
+
"""Register a transient service (new instance each time)."""
|
|
35
|
+
self._services[service_type.__name__] = implementation
|
|
36
|
+
|
|
37
|
+
def get(self, service_type: type) -> Any:
|
|
38
|
+
"""Get an instance of the requested service."""
|
|
39
|
+
service_name = service_type.__name__
|
|
40
|
+
|
|
41
|
+
# Check singletons first
|
|
42
|
+
if service_name in self._singletons:
|
|
43
|
+
return self._singletons[service_name]
|
|
44
|
+
|
|
45
|
+
# Check factories
|
|
46
|
+
if service_name in self._factories:
|
|
47
|
+
return self._factories[service_name]()
|
|
48
|
+
|
|
49
|
+
# Check transient services
|
|
50
|
+
if service_name in self._services:
|
|
51
|
+
implementation = self._services[service_name]
|
|
52
|
+
return self._create_instance(implementation)
|
|
53
|
+
|
|
54
|
+
raise ValueError(f"Service {service_name} is not registered")
|
|
55
|
+
|
|
56
|
+
def _create_instance(self, implementation: type) -> Any:
|
|
57
|
+
"""Create an instance with dependency injection."""
|
|
58
|
+
# This is a simplified implementation
|
|
59
|
+
# In a production system, you'd use a more sophisticated approach
|
|
60
|
+
# like inspecting constructor parameters and resolving them automatically
|
|
61
|
+
return implementation()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ApplicationContainer:
|
|
65
|
+
"""Application-specific dependency injection container."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, database_url: str | None = None):
|
|
68
|
+
self.container = DIContainer()
|
|
69
|
+
self.database_url = database_url
|
|
70
|
+
self.has_database = database_url is not None
|
|
71
|
+
self._setup_dependencies()
|
|
72
|
+
|
|
73
|
+
def _setup_dependencies(self) -> None:
|
|
74
|
+
if self.has_database:
|
|
75
|
+
database_service = DatabaseService(self.database_url)
|
|
76
|
+
self.container.register_singleton(DatabaseService, database_service)
|
|
77
|
+
db_service_module.database_service = database_service
|
|
78
|
+
|
|
79
|
+
session_repository = SessionRepository(database_service)
|
|
80
|
+
message_repository = MessageRepository(database_service)
|
|
81
|
+
|
|
82
|
+
self.container.register_singleton(ISessionRepository, session_repository)
|
|
83
|
+
self.container.register_singleton(IMessageRepository, message_repository)
|
|
84
|
+
|
|
85
|
+
def session_service_factory():
|
|
86
|
+
return SessionService(session_repository, message_repository)
|
|
87
|
+
|
|
88
|
+
self.container.register_factory(SessionService, session_service_factory)
|
|
89
|
+
|
|
90
|
+
def get_database_service(self) -> DatabaseService | None:
|
|
91
|
+
if not self.has_database:
|
|
92
|
+
return None
|
|
93
|
+
return self.container.get(DatabaseService)
|
|
94
|
+
|
|
95
|
+
def get_session_service(self) -> SessionService | None:
|
|
96
|
+
if not self.has_database:
|
|
97
|
+
return None
|
|
98
|
+
return self.container.get(SessionService)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Global container instance
|
|
102
|
+
_container: ApplicationContainer | None = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def initialize_container(database_url: str | None = None) -> ApplicationContainer:
|
|
106
|
+
global _container
|
|
107
|
+
_container = ApplicationContainer(database_url)
|
|
108
|
+
|
|
109
|
+
# Only create tables if database is available
|
|
110
|
+
if _container.has_database:
|
|
111
|
+
database_service = _container.get_database_service()
|
|
112
|
+
if database_service:
|
|
113
|
+
database_service.create_tables()
|
|
114
|
+
|
|
115
|
+
return _container
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_container() -> ApplicationContainer:
|
|
119
|
+
if _container is None:
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
"Container not initialized. Call initialize_container() first."
|
|
122
|
+
)
|
|
123
|
+
return _container
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from sqlalchemy.orm import sessionmaker
|
|
2
|
+
|
|
3
|
+
from .database_service import DatabaseService
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DatabasePersistenceService:
|
|
7
|
+
def __init__(self, db_url: str):
|
|
8
|
+
self.db_service = DatabaseService(db_url)
|
|
9
|
+
self._session_factory = sessionmaker(bind=self.db_service.engine)
|
|
10
|
+
|
|
11
|
+
def Session(self):
|
|
12
|
+
return self._session_factory()
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def engine(self):
|
|
16
|
+
return self.db_service.engine
|