solace-agent-mesh 1.3.1__py3-none-any.whl → 1.3.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/protocol/event_handlers.py +91 -0
- solace_agent_mesh/agent/sac/app.py +2 -0
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/483cef9a.03d5dceb.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{main.1c79039d.js → main.4adc477a.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/{runtime~main.858117b7.js → runtime~main.cf0229ea.js} +1 -1
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1757704179464.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1757704179464.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{main-C1k9E0aC.js → main-DjoMeldu.js} +8 -8
- solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
- solace_agent_mesh/common/a2a/__init__.py +4 -0
- solace_agent_mesh/common/a2a/protocol.py +20 -0
- solace_agent_mesh/common/sac/sam_component_base.py +29 -9
- solace_agent_mesh/common/sam_events/__init__.py +9 -0
- solace_agent_mesh/common/sam_events/event_service.py +207 -0
- solace_agent_mesh/gateway/http_sse/alembic/env.py +1 -1
- solace_agent_mesh/gateway/http_sse/component.py +45 -35
- solace_agent_mesh/gateway/http_sse/dependencies.py +123 -60
- solace_agent_mesh/gateway/http_sse/main.py +20 -33
- solace_agent_mesh/gateway/http_sse/repository/__init__.py +37 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/__init__.py +9 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/message.py +41 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session.py +45 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session_history.py +16 -0
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +64 -0
- solace_agent_mesh/gateway/http_sse/repository/message_repository.py +78 -0
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +9 -0
- solace_agent_mesh/gateway/http_sse/repository/models/base.py +7 -0
- solace_agent_mesh/gateway/http_sse/repository/models/message_model.py +27 -0
- solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +27 -0
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +139 -0
- solace_agent_mesh/gateway/http_sse/routers/config.py +1 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +20 -0
- solace_agent_mesh/gateway/http_sse/{api → routers}/dto/requests/session_requests.py +1 -8
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/__init__.py +16 -0
- solace_agent_mesh/gateway/http_sse/{api → routers}/dto/responses/session_responses.py +3 -30
- solace_agent_mesh/gateway/http_sse/{api/controllers/session_controller.py → routers/sessions.py} +20 -77
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +42 -49
- solace_agent_mesh/gateway/http_sse/{api/controllers/user_controller.py → routers/users.py} +1 -1
- solace_agent_mesh/gateway/http_sse/services/session_service.py +245 -0
- solace_agent_mesh/gateway/http_sse/session_manager.py +0 -3
- solace_agent_mesh/gateway/http_sse/shared/enums.py +0 -5
- {solace_agent_mesh-1.3.1.dist-info → solace_agent_mesh-1.3.2.dist-info}/METADATA +1 -1
- {solace_agent_mesh-1.3.1.dist-info → solace_agent_mesh-1.3.2.dist-info}/RECORD +90 -98
- solace_agent_mesh/assets/docs/assets/js/483cef9a.4e972867.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1757531604543.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1757531604543.json +0 -1
- solace_agent_mesh/gateway/http_sse/ARCHITECTURE_GUIDE.md +0 -676
- solace_agent_mesh/gateway/http_sse/api/__init__.py +0 -11
- solace_agent_mesh/gateway/http_sse/api/controllers/__init__.py +0 -9
- solace_agent_mesh/gateway/http_sse/api/controllers/task_controller.py +0 -279
- solace_agent_mesh/gateway/http_sse/api/dto/requests/__init__.py +0 -37
- solace_agent_mesh/gateway/http_sse/api/dto/requests/task_requests.py +0 -66
- solace_agent_mesh/gateway/http_sse/api/dto/responses/__init__.py +0 -43
- solace_agent_mesh/gateway/http_sse/api/dto/responses/task_responses.py +0 -74
- solace_agent_mesh/gateway/http_sse/application/__init__.py +0 -3
- solace_agent_mesh/gateway/http_sse/application/services/__init__.py +0 -3
- solace_agent_mesh/gateway/http_sse/application/services/session_service.py +0 -135
- solace_agent_mesh/gateway/http_sse/domain/entities/__init__.py +0 -3
- solace_agent_mesh/gateway/http_sse/domain/entities/session.py +0 -90
- solace_agent_mesh/gateway/http_sse/domain/repositories/__init__.py +0 -3
- solace_agent_mesh/gateway/http_sse/domain/repositories/session_repository.py +0 -54
- solace_agent_mesh/gateway/http_sse/infrastructure/__init__.py +0 -4
- solace_agent_mesh/gateway/http_sse/infrastructure/dependency_injection/__init__.py +0 -3
- solace_agent_mesh/gateway/http_sse/infrastructure/dependency_injection/container.py +0 -123
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence/__init__.py +0 -4
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence/database_persistence_service.py +0 -16
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence/database_service.py +0 -119
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence/models.py +0 -31
- solace_agent_mesh/gateway/http_sse/infrastructure/persistence_service.py +0 -12
- solace_agent_mesh/gateway/http_sse/infrastructure/repositories/__init__.py +0 -3
- solace_agent_mesh/gateway/http_sse/infrastructure/repositories/session_repository.py +0 -174
- /solace_agent_mesh/assets/docs/assets/js/{main.1c79039d.js.LICENSE.txt → main.4adc477a.js.LICENSE.txt} +0 -0
- /solace_agent_mesh/gateway/http_sse/{api → routers}/dto/__init__.py +0 -0
- {solace_agent_mesh-1.3.1.dist-info → solace_agent_mesh-1.3.2.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.3.1.dist-info → solace_agent_mesh-1.3.2.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.3.1.dist-info → solace_agent_mesh-1.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,11 +3,14 @@ Defines FastAPI dependency injectors to access shared resources
|
|
|
3
3
|
managed by the WebUIBackendComponent.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from collections.abc import Callable
|
|
6
|
+
from collections.abc import Callable, Generator
|
|
7
|
+
from contextlib import contextmanager
|
|
7
8
|
from typing import TYPE_CHECKING, Any
|
|
8
9
|
|
|
9
10
|
from fastapi import Depends, HTTPException, Request, status
|
|
10
11
|
from solace_ai_connector.common.log import log
|
|
12
|
+
from sqlalchemy import create_engine
|
|
13
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
11
14
|
|
|
12
15
|
from ...common.agent_registry import AgentRegistry
|
|
13
16
|
from ...common.middleware.config_resolver import ConfigResolver
|
|
@@ -19,8 +22,12 @@ from ...gateway.http_sse.services.people_service import PeopleService
|
|
|
19
22
|
from ...gateway.http_sse.services.task_service import TaskService
|
|
20
23
|
from ...gateway.http_sse.session_manager import SessionManager
|
|
21
24
|
from ...gateway.http_sse.sse_manager import SSEManager
|
|
22
|
-
from .
|
|
23
|
-
from .
|
|
25
|
+
from .services.session_service import SessionService
|
|
26
|
+
from .repository import (
|
|
27
|
+
Message,
|
|
28
|
+
MessageRepository,
|
|
29
|
+
SessionRepository,
|
|
30
|
+
)
|
|
24
31
|
|
|
25
32
|
try:
|
|
26
33
|
from google.adk.artifacts import BaseArtifactService
|
|
@@ -34,7 +41,7 @@ if TYPE_CHECKING:
|
|
|
34
41
|
from gateway.http_sse.component import WebUIBackendComponent
|
|
35
42
|
|
|
36
43
|
sac_component_instance: "WebUIBackendComponent" = None
|
|
37
|
-
|
|
44
|
+
SessionLocal: sessionmaker = None
|
|
38
45
|
|
|
39
46
|
api_config: dict[str, Any] | None = None
|
|
40
47
|
|
|
@@ -49,27 +56,15 @@ def set_component_instance(component: "WebUIBackendComponent"):
|
|
|
49
56
|
log.warning("[Dependencies] SAC Component instance already set.")
|
|
50
57
|
|
|
51
58
|
|
|
52
|
-
def
|
|
53
|
-
"""
|
|
54
|
-
global
|
|
55
|
-
if
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
def init_database(database_url: str):
|
|
60
|
+
"""Initialize database with direct sessionmaker."""
|
|
61
|
+
global SessionLocal
|
|
62
|
+
if SessionLocal is None:
|
|
63
|
+
engine = create_engine(database_url)
|
|
64
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
65
|
+
log.info("[Dependencies] Database initialized.")
|
|
58
66
|
else:
|
|
59
|
-
log.warning("[Dependencies]
|
|
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
|
|
67
|
+
log.warning("[Dependencies] Database already initialized.")
|
|
73
68
|
|
|
74
69
|
|
|
75
70
|
def set_api_config(config: dict[str, Any]):
|
|
@@ -357,63 +352,131 @@ def get_task_service(
|
|
|
357
352
|
)
|
|
358
353
|
|
|
359
354
|
|
|
360
|
-
def
|
|
361
|
-
|
|
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")
|
|
355
|
+
def get_db() -> Generator[Session, None, None]:
|
|
356
|
+
if SessionLocal is None:
|
|
374
357
|
raise HTTPException(
|
|
375
358
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
376
|
-
detail="Session management requires database configuration.
|
|
359
|
+
detail="Session management requires database configuration.",
|
|
377
360
|
)
|
|
361
|
+
db = SessionLocal()
|
|
362
|
+
try:
|
|
363
|
+
yield db
|
|
364
|
+
db.commit()
|
|
365
|
+
except Exception:
|
|
366
|
+
db.rollback()
|
|
367
|
+
raise
|
|
368
|
+
finally:
|
|
369
|
+
db.close()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def get_session_business_service(
|
|
374
|
+
db: Session = Depends(get_db),
|
|
375
|
+
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
376
|
+
) -> SessionService:
|
|
377
|
+
log.debug("[Dependencies] get_session_business_service called")
|
|
378
|
+
|
|
379
|
+
session_repository = SessionRepository(db)
|
|
380
|
+
message_repository = MessageRepository(db)
|
|
381
|
+
return SessionService(session_repository, message_repository, component)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@contextmanager
|
|
385
|
+
def create_session_service_with_transaction():
|
|
386
|
+
"""Create session data access service with its own transaction for non-HTTP contexts."""
|
|
387
|
+
if SessionLocal is None:
|
|
388
|
+
raise RuntimeError("Database not configured")
|
|
389
|
+
|
|
390
|
+
db = SessionLocal()
|
|
391
|
+
try:
|
|
392
|
+
session_repository = SessionRepository(db)
|
|
393
|
+
message_repository = MessageRepository(db)
|
|
394
|
+
|
|
395
|
+
# Create a simple data access object for transaction contexts
|
|
396
|
+
# This provides the basic repository operations without business logic
|
|
397
|
+
class SessionDataAccess:
|
|
398
|
+
def __init__(self, session_repo, message_repo):
|
|
399
|
+
self.session_repository = session_repo
|
|
400
|
+
self.message_repository = message_repo
|
|
401
|
+
|
|
402
|
+
def add_message_to_session(self, session_id, user_id, message, sender_type, sender_name, agent_id=None):
|
|
403
|
+
# Simple data access - just save the message
|
|
404
|
+
from uuid import uuid4
|
|
405
|
+
from datetime import datetime, timezone
|
|
406
|
+
from .shared.enums import MessageType
|
|
407
|
+
|
|
408
|
+
message_entity = Message(
|
|
409
|
+
id=str(uuid4()),
|
|
410
|
+
session_id=session_id,
|
|
411
|
+
message=message,
|
|
412
|
+
sender_type=sender_type,
|
|
413
|
+
sender_name=sender_name,
|
|
414
|
+
message_type=MessageType.TEXT,
|
|
415
|
+
created_at=datetime.now(timezone.utc),
|
|
416
|
+
)
|
|
417
|
+
return self.message_repository.save(message_entity)
|
|
418
|
+
|
|
419
|
+
def get_session(self, session_id, user_id):
|
|
420
|
+
# Use the session repository to find the session
|
|
421
|
+
return self.session_repository.find_user_session(session_id, user_id)
|
|
422
|
+
|
|
423
|
+
def create_session(self, user_id, name=None, agent_id=None, session_id=None):
|
|
424
|
+
# Create a new session using the session repository
|
|
425
|
+
from uuid import uuid4
|
|
426
|
+
from datetime import datetime, timezone
|
|
427
|
+
from .repository.entities import Session
|
|
428
|
+
|
|
429
|
+
if not session_id:
|
|
430
|
+
session_id = str(uuid4())
|
|
431
|
+
|
|
432
|
+
# Leave name as None/empty - frontend will generate display name if needed
|
|
433
|
+
|
|
434
|
+
now = datetime.now(timezone.utc)
|
|
435
|
+
session = Session(
|
|
436
|
+
id=session_id,
|
|
437
|
+
user_id=user_id,
|
|
438
|
+
name=name,
|
|
439
|
+
agent_id=agent_id,
|
|
440
|
+
created_at=now,
|
|
441
|
+
updated_at=now,
|
|
442
|
+
last_activity=now,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return self.session_repository.save(session)
|
|
446
|
+
|
|
447
|
+
session_service = SessionDataAccess(session_repository, message_repository)
|
|
448
|
+
yield session_service, db
|
|
449
|
+
db.commit()
|
|
450
|
+
except Exception:
|
|
451
|
+
db.rollback()
|
|
452
|
+
raise
|
|
453
|
+
finally:
|
|
454
|
+
db.close()
|
|
378
455
|
|
|
379
456
|
|
|
380
457
|
def get_session_validator(
|
|
381
458
|
component: "WebUIBackendComponent" = Depends(get_sac_component),
|
|
382
459
|
) -> 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
460
|
log.debug("[Dependencies] get_session_validator called")
|
|
390
461
|
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
hasattr(component, "persistence_service")
|
|
394
|
-
and component.persistence_service is not None
|
|
395
|
-
):
|
|
462
|
+
if SessionLocal:
|
|
396
463
|
log.debug("Using database-backed session validation")
|
|
397
464
|
|
|
398
465
|
def validate_with_database(session_id: str, user_id: str) -> bool:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
466
|
+
try:
|
|
467
|
+
with create_session_service_with_transaction() as (session_service, db):
|
|
468
|
+
session_domain = session_service.get_session(session_id, user_id)
|
|
469
|
+
return session_domain is not None
|
|
470
|
+
except:
|
|
471
|
+
return False
|
|
405
472
|
|
|
406
473
|
return validate_with_database
|
|
407
474
|
else:
|
|
408
475
|
log.debug("No database configured - using basic session validation")
|
|
409
476
|
|
|
410
477
|
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
478
|
if not session_id or not session_id.startswith("web-session-"):
|
|
414
479
|
return False
|
|
415
|
-
# If user_id is provided (authenticated), allow access
|
|
416
|
-
# This provides basic security while allowing filesystem-based artifacts
|
|
417
480
|
return bool(user_id)
|
|
418
481
|
|
|
419
482
|
return validate_without_database
|
|
@@ -8,8 +8,9 @@ from a2a.types import InternalError, JSONRPCError
|
|
|
8
8
|
from a2a.types import JSONRPCResponse as A2AJSONRPCResponse
|
|
9
9
|
from alembic import command
|
|
10
10
|
from alembic.config import Config
|
|
11
|
-
from fastapi import FastAPI, HTTPException
|
|
11
|
+
from fastapi import FastAPI, HTTPException
|
|
12
12
|
from fastapi import Request as FastAPIRequest
|
|
13
|
+
from fastapi import status
|
|
13
14
|
from fastapi.exceptions import RequestValidationError
|
|
14
15
|
from fastapi.middleware.cors import CORSMiddleware
|
|
15
16
|
from fastapi.responses import JSONResponse
|
|
@@ -31,10 +32,9 @@ from ...gateway.http_sse.routers import (
|
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
# Import persistence-aware controllers
|
|
34
|
-
from .
|
|
35
|
-
from .
|
|
36
|
-
from .
|
|
37
|
-
from .infrastructure.persistence.database_service import DatabaseService
|
|
35
|
+
from .routers.sessions import router as session_router
|
|
36
|
+
from .routers.tasks import router as task_router
|
|
37
|
+
from .routers.users import router as user_router
|
|
38
38
|
|
|
39
39
|
if TYPE_CHECKING:
|
|
40
40
|
from gateway.http_sse.component import WebUIBackendComponent
|
|
@@ -46,39 +46,25 @@ app = FastAPI(
|
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def setup_dependencies(component: "WebUIBackendComponent",
|
|
49
|
+
def setup_dependencies(component: "WebUIBackendComponent", database_url: str = None):
|
|
50
50
|
"""
|
|
51
|
-
This function initializes the
|
|
51
|
+
This function initializes the simplified architecture while maintaining full
|
|
52
52
|
backward compatibility with existing API contracts.
|
|
53
53
|
|
|
54
|
-
If
|
|
54
|
+
If database_url is None, runs in compatibility mode with in-memory sessions.
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
|
-
if persistence_service:
|
|
58
|
-
database_url = persistence_service.engine.url.__str__()
|
|
59
|
-
global database_service
|
|
60
|
-
database_service = DatabaseService(database_url)
|
|
61
|
-
log.info("Database service initialized")
|
|
62
|
-
|
|
63
|
-
from .infrastructure.dependency_injection.container import initialize_container
|
|
64
|
-
|
|
65
|
-
initialize_container(database_url)
|
|
66
|
-
log.info("Persistence enabled - sessions will be stored in database")
|
|
67
|
-
else:
|
|
68
|
-
from .infrastructure.dependency_injection.container import initialize_container
|
|
69
|
-
|
|
70
|
-
initialize_container()
|
|
71
|
-
log.warning(
|
|
72
|
-
"No persistence service provided - using in-memory session storage (data not persisted across restarts)"
|
|
73
|
-
)
|
|
74
|
-
log.info("This maintains backward compatibility for existing SAM installations")
|
|
75
|
-
|
|
76
57
|
dependencies.set_component_instance(component)
|
|
77
58
|
|
|
78
|
-
if
|
|
59
|
+
if database_url:
|
|
60
|
+
dependencies.init_database(database_url)
|
|
61
|
+
log.info("Persistence enabled - sessions will be stored in database")
|
|
62
|
+
|
|
79
63
|
log.info("Checking database migrations...")
|
|
80
64
|
try:
|
|
81
|
-
|
|
65
|
+
from sqlalchemy import create_engine
|
|
66
|
+
engine = create_engine(database_url)
|
|
67
|
+
inspector = sa.inspect(engine)
|
|
82
68
|
existing_tables = inspector.get_table_names()
|
|
83
69
|
|
|
84
70
|
if not existing_tables or "sessions" not in existing_tables:
|
|
@@ -108,10 +94,11 @@ def setup_dependencies(component: "WebUIBackendComponent", persistence_service=N
|
|
|
108
94
|
log.info("Database migrations complete.")
|
|
109
95
|
except Exception as migration_error:
|
|
110
96
|
log.warning(f"Migration failed but continuing: {migration_error}")
|
|
111
|
-
|
|
112
|
-
dependencies.set_persistence_service(persistence_service)
|
|
113
97
|
else:
|
|
114
|
-
log.
|
|
98
|
+
log.warning(
|
|
99
|
+
"No database URL provided - using in-memory session storage (data not persisted across restarts)"
|
|
100
|
+
)
|
|
101
|
+
log.info("This maintains backward compatibility for existing SAM installations")
|
|
115
102
|
|
|
116
103
|
webui_app = component.get_app()
|
|
117
104
|
app_config = {}
|
|
@@ -137,7 +124,7 @@ def setup_dependencies(component: "WebUIBackendComponent", persistence_service=N
|
|
|
137
124
|
"frontend_redirect_url": app_config.get(
|
|
138
125
|
"frontend_redirect_url", "http://localhost:3000"
|
|
139
126
|
),
|
|
140
|
-
"persistence_enabled":
|
|
127
|
+
"persistence_enabled": database_url is not None,
|
|
141
128
|
}
|
|
142
129
|
|
|
143
130
|
dependencies.set_api_config(api_config_dict)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository layer containing all data access logic organized by entity type.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Interfaces
|
|
6
|
+
from .interfaces import IMessageRepository, ISessionRepository
|
|
7
|
+
|
|
8
|
+
# Implementations
|
|
9
|
+
from .message_repository import MessageRepository
|
|
10
|
+
from .session_repository import SessionRepository
|
|
11
|
+
|
|
12
|
+
# Entities (re-exported for convenience)
|
|
13
|
+
from .entities.session import Session
|
|
14
|
+
from .entities.message import Message
|
|
15
|
+
from .entities.session_history import SessionHistory
|
|
16
|
+
|
|
17
|
+
# Models (re-exported for convenience)
|
|
18
|
+
from .models.base import Base
|
|
19
|
+
from .models.session_model import SessionModel
|
|
20
|
+
from .models.message_model import MessageModel
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# Interfaces
|
|
24
|
+
"IMessageRepository",
|
|
25
|
+
"ISessionRepository",
|
|
26
|
+
# Implementations
|
|
27
|
+
"MessageRepository",
|
|
28
|
+
"SessionRepository",
|
|
29
|
+
# Entities
|
|
30
|
+
"Message",
|
|
31
|
+
"Session",
|
|
32
|
+
"SessionHistory",
|
|
33
|
+
# Models
|
|
34
|
+
"Base",
|
|
35
|
+
"MessageModel",
|
|
36
|
+
"SessionModel",
|
|
37
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message domain entity.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from ...shared.enums import MessageType, SenderType
|
|
10
|
+
from ...shared.types import MessageId, SessionId
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Message(BaseModel):
|
|
14
|
+
"""Message domain entity with business logic."""
|
|
15
|
+
|
|
16
|
+
id: MessageId
|
|
17
|
+
session_id: SessionId
|
|
18
|
+
message: str
|
|
19
|
+
sender_type: SenderType
|
|
20
|
+
sender_name: str
|
|
21
|
+
message_type: MessageType = MessageType.TEXT
|
|
22
|
+
created_at: datetime
|
|
23
|
+
|
|
24
|
+
def validate_message_content(self) -> None:
|
|
25
|
+
"""Validate message content."""
|
|
26
|
+
if not self.message or len(self.message.strip()) == 0:
|
|
27
|
+
raise ValueError("Message content cannot be empty")
|
|
28
|
+
if len(self.message) > 10_000_000:
|
|
29
|
+
raise ValueError("Message content exceeds maximum length (10MB)")
|
|
30
|
+
|
|
31
|
+
def is_from_user(self) -> bool:
|
|
32
|
+
"""Check if message is from a user."""
|
|
33
|
+
return self.sender_type == SenderType.USER
|
|
34
|
+
|
|
35
|
+
def is_from_agent(self) -> bool:
|
|
36
|
+
"""Check if message is from an agent."""
|
|
37
|
+
return self.sender_type == SenderType.AGENT
|
|
38
|
+
|
|
39
|
+
def is_system_message(self) -> bool:
|
|
40
|
+
"""Check if message is a system message."""
|
|
41
|
+
return self.sender_type == SenderType.SYSTEM
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session domain entity.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from ...shared.types import AgentId, SessionId, UserId
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Session(BaseModel):
|
|
13
|
+
"""Session domain entity with business logic."""
|
|
14
|
+
|
|
15
|
+
id: SessionId
|
|
16
|
+
user_id: UserId
|
|
17
|
+
name: str | None = None
|
|
18
|
+
agent_id: AgentId | None = None
|
|
19
|
+
created_at: datetime
|
|
20
|
+
updated_at: datetime | None = None
|
|
21
|
+
last_activity: datetime | None = None
|
|
22
|
+
|
|
23
|
+
def update_name(self, new_name: str) -> None:
|
|
24
|
+
"""Update session name with validation."""
|
|
25
|
+
if not new_name or len(new_name.strip()) == 0:
|
|
26
|
+
raise ValueError("Session name cannot be empty")
|
|
27
|
+
if len(new_name) > 255:
|
|
28
|
+
raise ValueError("Session name cannot exceed 255 characters")
|
|
29
|
+
|
|
30
|
+
self.name = new_name.strip()
|
|
31
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
32
|
+
|
|
33
|
+
def mark_activity(self) -> None:
|
|
34
|
+
"""Mark session as having recent activity."""
|
|
35
|
+
self.last_activity = datetime.now(timezone.utc)
|
|
36
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def can_be_deleted_by_user(self, user_id: UserId) -> bool:
|
|
40
|
+
"""Check if user can delete this session."""
|
|
41
|
+
return self.user_id == user_id
|
|
42
|
+
|
|
43
|
+
def can_be_accessed_by_user(self, user_id: UserId) -> bool:
|
|
44
|
+
"""Check if user can access this session."""
|
|
45
|
+
return self.user_id == user_id
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session history composite entity.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from .message import Message
|
|
8
|
+
from .session import Session
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SessionHistory(BaseModel):
|
|
12
|
+
"""Composite entity representing a session with its messages."""
|
|
13
|
+
|
|
14
|
+
session: Session
|
|
15
|
+
messages: list[Message] = []
|
|
16
|
+
total_message_count: int = 0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository interfaces defining contracts for data access.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from ..shared.types import PaginationInfo, SessionId, UserId
|
|
8
|
+
from .entities import Message, Session
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ISessionRepository(ABC):
|
|
12
|
+
"""Interface for session data access operations."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def find_by_user(
|
|
16
|
+
self, user_id: UserId, pagination: PaginationInfo | None = None
|
|
17
|
+
) -> list[Session]:
|
|
18
|
+
"""Find all sessions for a specific user."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def find_user_session(
|
|
23
|
+
self, session_id: SessionId, user_id: UserId
|
|
24
|
+
) -> Session | None:
|
|
25
|
+
"""Find a specific session belonging to a user."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def save(self, session: Session) -> Session:
|
|
30
|
+
"""Save or update a session."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def delete(self, session_id: SessionId, user_id: UserId) -> bool:
|
|
35
|
+
"""Delete a session belonging to a user."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def find_user_session_with_messages(
|
|
40
|
+
self, session_id: SessionId, user_id: UserId, pagination: PaginationInfo | None = None
|
|
41
|
+
) -> tuple[Session, list[Message]] | None:
|
|
42
|
+
"""Find a session with its messages."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class IMessageRepository(ABC):
|
|
47
|
+
"""Interface for message data access operations."""
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def find_by_session(
|
|
51
|
+
self, session_id: SessionId, pagination: PaginationInfo | None = None
|
|
52
|
+
) -> list[Message]:
|
|
53
|
+
"""Find all messages in a session."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def save(self, message: Message) -> Message:
|
|
58
|
+
"""Save or update a message."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def delete_by_session(self, session_id: SessionId) -> bool:
|
|
63
|
+
"""Delete all messages in a session."""
|
|
64
|
+
pass
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message repository implementation using SQLAlchemy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.orm import Session as DBSession
|
|
6
|
+
|
|
7
|
+
from ..shared.enums import MessageType, SenderType
|
|
8
|
+
from ..shared.types import PaginationInfo, SessionId
|
|
9
|
+
from .entities import Message
|
|
10
|
+
from .interfaces import IMessageRepository
|
|
11
|
+
from .models import MessageModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessageRepository(IMessageRepository):
|
|
15
|
+
"""SQLAlchemy implementation of message repository."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, db: DBSession):
|
|
18
|
+
self.db = db
|
|
19
|
+
|
|
20
|
+
def find_by_session(
|
|
21
|
+
self, session_id: SessionId, pagination: PaginationInfo | None = None
|
|
22
|
+
) -> list[Message]:
|
|
23
|
+
"""Find all messages in a session."""
|
|
24
|
+
query = self.db.query(MessageModel).filter(MessageModel.session_id == session_id)
|
|
25
|
+
|
|
26
|
+
if pagination:
|
|
27
|
+
offset = (pagination.page - 1) * pagination.page_size
|
|
28
|
+
query = query.offset(offset).limit(pagination.page_size)
|
|
29
|
+
|
|
30
|
+
models = query.order_by(MessageModel.created_at.asc()).all()
|
|
31
|
+
return [self._model_to_entity(model) for model in models]
|
|
32
|
+
|
|
33
|
+
def save(self, message: Message) -> Message:
|
|
34
|
+
"""Save or update a message."""
|
|
35
|
+
model = self.db.query(MessageModel).filter(MessageModel.id == message.id).first()
|
|
36
|
+
|
|
37
|
+
if model:
|
|
38
|
+
# Update existing
|
|
39
|
+
model.message = message.message
|
|
40
|
+
model.sender_type = message.sender_type.value
|
|
41
|
+
model.sender_name = message.sender_name
|
|
42
|
+
else:
|
|
43
|
+
# Create new
|
|
44
|
+
model = MessageModel(
|
|
45
|
+
id=message.id,
|
|
46
|
+
session_id=message.session_id,
|
|
47
|
+
message=message.message,
|
|
48
|
+
sender_type=message.sender_type.value,
|
|
49
|
+
sender_name=message.sender_name,
|
|
50
|
+
created_at=message.created_at,
|
|
51
|
+
)
|
|
52
|
+
self.db.add(model)
|
|
53
|
+
|
|
54
|
+
self.db.commit()
|
|
55
|
+
self.db.refresh(model)
|
|
56
|
+
return self._model_to_entity(model)
|
|
57
|
+
|
|
58
|
+
def delete_by_session(self, session_id: SessionId) -> bool:
|
|
59
|
+
"""Delete all messages in a session."""
|
|
60
|
+
result = (
|
|
61
|
+
self.db.query(MessageModel)
|
|
62
|
+
.filter(MessageModel.session_id == session_id)
|
|
63
|
+
.delete()
|
|
64
|
+
)
|
|
65
|
+
self.db.commit()
|
|
66
|
+
return result > 0
|
|
67
|
+
|
|
68
|
+
def _model_to_entity(self, model: MessageModel) -> Message:
|
|
69
|
+
"""Convert SQLAlchemy model to domain entity."""
|
|
70
|
+
return Message(
|
|
71
|
+
id=model.id,
|
|
72
|
+
session_id=model.session_id,
|
|
73
|
+
message=model.message,
|
|
74
|
+
sender_type=SenderType(model.sender_type),
|
|
75
|
+
sender_name=model.sender_name,
|
|
76
|
+
message_type=MessageType.TEXT, # Default for now
|
|
77
|
+
created_at=model.created_at,
|
|
78
|
+
)
|