solace-agent-mesh 1.4.7__py3-none-any.whl → 1.4.8__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.

Files changed (117) hide show
  1. solace_agent_mesh/agent/sac/component.py +2 -3
  2. solace_agent_mesh/assets/docs/404.html +3 -3
  3. solace_agent_mesh/assets/docs/assets/js/{0e682baa.da822665.js → 0e682baa.d054e1d8.js} +1 -1
  4. solace_agent_mesh/assets/docs/assets/js/166ab619.e27886d9.js +1 -0
  5. solace_agent_mesh/assets/docs/assets/js/453a82a6.3c6bb61d.js +1 -0
  6. solace_agent_mesh/assets/docs/assets/js/{75384d09.1e7d7cb7.js → 75384d09.c19e8b51.js} +1 -1
  7. solace_agent_mesh/assets/docs/assets/js/a3a92b25.af35e313.js +1 -0
  8. solace_agent_mesh/assets/docs/assets/js/{bac0be12.bf0181cf.js → bac0be12.17de4316.js} +1 -1
  9. solace_agent_mesh/assets/docs/assets/js/d6a81ee7.829198f1.js +1 -0
  10. solace_agent_mesh/assets/docs/assets/js/f284c35a.ed8dd236.js +1 -0
  11. solace_agent_mesh/assets/docs/assets/js/{main.11f9f9f3.js → main.86924c42.js} +2 -2
  12. solace_agent_mesh/assets/docs/assets/js/runtime~main.0d2ff2b6.js +1 -0
  13. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +4 -4
  14. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/rbac-setup-guilde/index.html +4 -4
  15. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +4 -4
  16. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +4 -4
  17. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +4 -4
  18. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +4 -4
  19. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +4 -4
  20. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +4 -4
  21. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +4 -4
  22. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +4 -4
  23. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +4 -4
  24. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
  25. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +4 -4
  26. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
  27. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +4 -4
  28. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +11 -12
  29. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/litellm_models/index.html +49 -0
  30. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +4 -4
  31. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +4 -4
  32. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +4 -4
  33. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
  34. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +4 -4
  35. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +5 -5
  36. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +5 -5
  37. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
  38. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +6 -6
  39. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +4 -4
  40. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +4 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +4 -4
  44. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +4 -4
  45. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +4 -4
  51. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
  52. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
  53. solace_agent_mesh/assets/docs/lunr-index-1759246102819.json +1 -0
  54. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  55. solace_agent_mesh/assets/docs/search-doc-1759246102819.json +1 -0
  56. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  57. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  58. solace_agent_mesh/cli/__init__.py +1 -1
  59. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -3
  60. solace_agent_mesh/cli/commands/init_cmd/__init__.py +3 -3
  61. solace_agent_mesh/cli/commands/init_cmd/broker_step.py +1 -1
  62. solace_agent_mesh/cli/commands/init_cmd/env_step.py +1 -1
  63. solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +4 -4
  64. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +12 -1
  65. solace_agent_mesh/cli/commands/plugin_cmd/install_cmd.py +5 -5
  66. solace_agent_mesh/client/webui/frontend/static/assets/main-B0PHV3hm.js +339 -0
  67. solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
  68. solace_agent_mesh/config_portal/backend/common.py +1 -1
  69. solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-bFMKlzKf.js → _index-BNuqpWDc.js} +1 -1
  70. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-89db7c30.js → manifest-44d62be6.js} +1 -1
  71. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  72. solace_agent_mesh/gateway/http_sse/component.py +21 -15
  73. solace_agent_mesh/gateway/http_sse/dependencies.py +84 -88
  74. solace_agent_mesh/gateway/http_sse/main.py +8 -2
  75. solace_agent_mesh/gateway/http_sse/repository/entities/message.py +3 -1
  76. solace_agent_mesh/gateway/http_sse/repository/entities/session.py +3 -1
  77. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +5 -0
  78. solace_agent_mesh/gateway/http_sse/repository/message_repository.py +25 -23
  79. solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +12 -4
  80. solace_agent_mesh/gateway/http_sse/repository/models/message_model.py +19 -1
  81. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +19 -1
  82. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +46 -42
  83. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +199 -59
  84. solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +1 -6
  85. solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +3 -17
  86. solace_agent_mesh/gateway/http_sse/routers/people.py +4 -37
  87. solace_agent_mesh/gateway/http_sse/routers/sessions.py +33 -68
  88. solace_agent_mesh/gateway/http_sse/routers/tasks.py +54 -28
  89. solace_agent_mesh/gateway/http_sse/services/session_service.py +60 -28
  90. solace_agent_mesh/gateway/http_sse/shared/__init__.py +122 -1
  91. solace_agent_mesh/gateway/http_sse/shared/base_repository.py +278 -0
  92. solace_agent_mesh/gateway/http_sse/shared/database_exceptions.py +274 -0
  93. solace_agent_mesh/gateway/http_sse/shared/database_helpers.py +43 -0
  94. solace_agent_mesh/gateway/http_sse/shared/error_dto.py +107 -0
  95. solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +192 -0
  96. solace_agent_mesh/gateway/http_sse/shared/exceptions.py +192 -0
  97. solace_agent_mesh/gateway/http_sse/shared/pagination.py +138 -0
  98. solace_agent_mesh/gateway/http_sse/shared/response_utils.py +134 -0
  99. solace_agent_mesh/gateway/http_sse/shared/utils.py +22 -0
  100. solace_agent_mesh/templates/plugin_agent_config_template.yaml +1 -1
  101. solace_agent_mesh/templates/plugin_custom_config_template.yaml +1 -1
  102. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +1 -1
  103. solace_agent_mesh/templates/shared_config.yaml +1 -1
  104. {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.8.dist-info}/METADATA +34 -35
  105. {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.8.dist-info}/RECORD +109 -98
  106. solace_agent_mesh/assets/docs/assets/js/166ab619.bdddc63a.js +0 -1
  107. solace_agent_mesh/assets/docs/assets/js/a3a92b25.6def8980.js +0 -1
  108. solace_agent_mesh/assets/docs/assets/js/beecea0d.ce915979.js +0 -1
  109. solace_agent_mesh/assets/docs/assets/js/f284c35a.525933db.js +0 -1
  110. solace_agent_mesh/assets/docs/assets/js/runtime~main.5922bcf0.js +0 -1
  111. solace_agent_mesh/assets/docs/lunr-index-1759151175744.json +0 -1
  112. solace_agent_mesh/assets/docs/search-doc-1759151175744.json +0 -1
  113. solace_agent_mesh/client/webui/frontend/static/assets/main-BKIoiLSu.js +0 -339
  114. /solace_agent_mesh/assets/docs/assets/js/{main.11f9f9f3.js.LICENSE.txt → main.86924c42.js.LICENSE.txt} +0 -0
  115. {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.8.dist-info}/WHEEL +0 -0
  116. {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.8.dist-info}/entry_points.txt +0 -0
  117. {solace_agent_mesh-1.4.7.dist-info → solace_agent_mesh-1.4.8.dist-info}/licenses/LICENSE +0 -0
@@ -1690,23 +1690,29 @@ class WebUIBackendComponent(BaseGatewayComponent):
1690
1690
  message_text += part.text
1691
1691
 
1692
1692
  if message_text and session_id and user_id:
1693
- from .dependencies import (
1694
- create_session_service_with_transaction,
1695
- )
1693
+ from .dependencies import SessionLocal, get_session_business_service
1696
1694
  from ...gateway.http_sse.shared.enums import SenderType
1697
1695
 
1698
- with create_session_service_with_transaction() as (
1699
- session_service,
1700
- db,
1701
- ):
1702
- session_service.add_message_to_session(
1703
- session_id=session_id,
1704
- user_id=user_id,
1705
- message=message_text,
1706
- sender_type=SenderType.AGENT,
1707
- sender_name=agent_name,
1708
- agent_id=agent_name,
1709
- )
1696
+ # For background processing, create simple session wrapper
1697
+ if SessionLocal:
1698
+ db = SessionLocal()
1699
+ try:
1700
+ session_service = get_session_business_service()
1701
+ session_service.add_message_to_session(
1702
+ db=db,
1703
+ session_id=session_id,
1704
+ user_id=user_id,
1705
+ message=message_text,
1706
+ sender_type=SenderType.AGENT,
1707
+ sender_name=agent_name,
1708
+ agent_id=agent_name,
1709
+ )
1710
+ db.commit()
1711
+ except Exception:
1712
+ db.rollback()
1713
+ raise
1714
+ finally:
1715
+ db.close()
1710
1716
  log.info(
1711
1717
  "%s Final agent response stored in session %s",
1712
1718
  log_id_prefix,
@@ -273,6 +273,54 @@ async def get_user_config(
273
273
  )
274
274
 
275
275
 
276
+ class ValidatedUserConfig:
277
+ """
278
+ FastAPI dependency class for validating user scopes and returning user config.
279
+
280
+ This class creates a callable dependency that validates a user has the required
281
+ scopes before allowing access to protected endpoints.
282
+
283
+ Args:
284
+ required_scopes: List of scope strings required for authorization
285
+
286
+ Raises:
287
+ HTTPException: 403 if user lacks required scopes
288
+
289
+ Example:
290
+ @router.get("/artifacts")
291
+ async def list_artifacts(
292
+ user_config: dict = Depends(ValidatedUserConfig(["tool:artifact:list"])),
293
+ ):
294
+ """
295
+
296
+ def __init__(self, required_scopes: list[str]):
297
+ self.required_scopes = required_scopes
298
+
299
+ async def __call__(
300
+ self,
301
+ request: Request,
302
+ config_resolver: ConfigResolver = Depends(get_config_resolver),
303
+ user_config: dict[str, Any] = Depends(get_user_config),
304
+ ) -> dict[str, Any]:
305
+ user_id = user_config.get("user_profile", {}).get("id")
306
+
307
+ log.debug(f"[Dependencies] ValidatedUserConfig called for user_id: {user_id} with required scopes: {self.required_scopes}")
308
+
309
+ # Validate scopes
310
+ if not config_resolver.is_feature_enabled(
311
+ user_config, {"tool_metadata": {"required_scopes": self.required_scopes}}, {}
312
+ ):
313
+ log.warning(
314
+ f"[Dependencies] Authorization denied for user '{user_id}'. Required scopes: {self.required_scopes}"
315
+ )
316
+ raise HTTPException(
317
+ status_code=status.HTTP_403_FORBIDDEN,
318
+ detail=f"Not authorized. Required scopes: {self.required_scopes}"
319
+ )
320
+
321
+ return user_config
322
+
323
+
276
324
  def get_shared_artifact_service(
277
325
  component: "WebUIBackendComponent" = Depends(get_sac_component),
278
326
  ) -> BaseArtifactService | None:
@@ -366,98 +414,15 @@ def get_db() -> Generator[Session, None, None]:
366
414
 
367
415
 
368
416
  def get_session_business_service(
369
- db: Session = Depends(get_db),
370
417
  component: "WebUIBackendComponent" = Depends(get_sac_component),
371
418
  ) -> SessionService:
372
419
  log.debug("[Dependencies] get_session_business_service called")
373
420
 
374
- session_repository = SessionRepository(db)
375
- message_repository = MessageRepository(db)
376
- return SessionService(session_repository, message_repository, component)
377
-
421
+ # Note: Session and message repositories will be created per request
422
+ # when the SessionService methods receive the db parameter
423
+ return SessionService(component=component)
378
424
 
379
- @contextmanager
380
- def create_session_service_with_transaction():
381
- """Create session data access service with its own transaction for non-HTTP contexts."""
382
- if SessionLocal is None:
383
- raise RuntimeError("Database not configured")
384
425
 
385
- db = SessionLocal()
386
- try:
387
- session_repository = SessionRepository(db)
388
- message_repository = MessageRepository(db)
389
-
390
- # Create a simple data access object for transaction contexts
391
- # This provides the basic repository operations without business logic
392
- class SessionDataAccess:
393
- def __init__(self, session_repo, message_repo):
394
- self.session_repository = session_repo
395
- self.message_repository = message_repo
396
-
397
- def add_message_to_session(
398
- self,
399
- session_id,
400
- user_id,
401
- message,
402
- sender_type,
403
- sender_name,
404
- agent_id=None,
405
- ):
406
- # Simple data access - just save the message
407
- from uuid import uuid4
408
-
409
- from .shared.enums import MessageType
410
- from .shared import now_epoch_ms
411
-
412
- message_entity = Message(
413
- id=str(uuid4()),
414
- session_id=session_id,
415
- message=message,
416
- sender_type=sender_type,
417
- sender_name=sender_name,
418
- message_type=MessageType.TEXT,
419
- created_time=now_epoch_ms(),
420
- )
421
- return self.message_repository.save(message_entity)
422
-
423
- def get_session(self, session_id, user_id):
424
- # Use the session repository to find the session
425
- return self.session_repository.find_user_session(session_id, user_id)
426
-
427
- def create_session(
428
- self, user_id, name=None, agent_id=None, session_id=None
429
- ):
430
- # Create a new session using the session repository
431
- from uuid import uuid4
432
-
433
- from .repository.entities import Session
434
- from .shared import now_epoch_ms
435
-
436
- if not session_id:
437
- session_id = str(uuid4())
438
-
439
- # Leave name as None/empty - frontend will generate display name if needed
440
-
441
- now_ms = now_epoch_ms()
442
- session = Session(
443
- id=session_id,
444
- user_id=user_id,
445
- name=name,
446
- agent_id=agent_id,
447
- created_time=now_ms,
448
- updated_time=now_ms,
449
- )
450
-
451
- return self.session_repository.save(session)
452
-
453
- session_service = SessionDataAccess(session_repository, message_repository)
454
- yield session_service, db
455
- db.commit()
456
- except Exception:
457
- db.rollback()
458
- raise
459
- finally:
460
- db.close()
461
426
 
462
427
 
463
428
  def get_session_validator(
@@ -470,9 +435,13 @@ def get_session_validator(
470
435
 
471
436
  def validate_with_database(session_id: str, user_id: str) -> bool:
472
437
  try:
473
- with create_session_service_with_transaction() as (session_service, db):
474
- session_domain = session_service.get_session(session_id, user_id)
438
+ db = SessionLocal()
439
+ try:
440
+ session_repository = SessionRepository(db)
441
+ session_domain = session_repository.find_user_session(session_id, user_id)
475
442
  return session_domain is not None
443
+ finally:
444
+ db.close()
476
445
  except:
477
446
  return False
478
447
 
@@ -486,3 +455,30 @@ def get_session_validator(
486
455
  return bool(user_id)
487
456
 
488
457
  return validate_without_database
458
+
459
+
460
+ def get_db_optional() -> Generator[Session | None, None, None]:
461
+ """Optional database dependency that returns None if database is not configured."""
462
+ if SessionLocal is None:
463
+ log.debug("[Dependencies] Database not configured, returning None")
464
+ yield None
465
+ else:
466
+ db = SessionLocal()
467
+ try:
468
+ yield db
469
+ db.commit()
470
+ except Exception:
471
+ db.rollback()
472
+ raise
473
+ finally:
474
+ db.close()
475
+
476
+
477
+ def get_session_business_service_optional(
478
+ component: "WebUIBackendComponent" = Depends(get_sac_component),
479
+ ) -> SessionService | None:
480
+ """Optional session service dependency that returns None if database is not configured."""
481
+ if SessionLocal is None:
482
+ log.debug("[Dependencies] Database not configured, returning None for session service")
483
+ return None
484
+ return SessionService(component=component)
@@ -479,6 +479,12 @@ def _setup_routers() -> None:
479
479
  app.include_router(auth.router, prefix=api_prefix, tags=["Auth"])
480
480
  log.info("Legacy routers mounted for endpoints not yet migrated")
481
481
 
482
+ # Register shared exception handlers from community repo
483
+ from .shared.exception_handlers import register_exception_handlers
484
+ register_exception_handlers(app)
485
+ log.info("Registered shared exception handlers from community repo")
486
+
487
+ # Mount enterprise routers if available
482
488
  try:
483
489
  from solace_agent_mesh_enterprise.webui_backend.routers import get_enterprise_routers
484
490
 
@@ -494,9 +500,9 @@ def _setup_routers() -> None:
494
500
  except ImportError:
495
501
  log.debug("No enterprise package detected - skipping enterprise routers")
496
502
  except ModuleNotFoundError:
497
- log.debug("Enterprise router module not found - skipping enterprise routers")
503
+ log.debug("Enterprise module not found - skipping enterprise routers and exception handlers")
498
504
  except Exception as e:
499
- log.warning("Failed to load enterprise routers: %s", e)
505
+ log.warning("Failed to load enterprise routers and exception handlers: %s", e)
500
506
 
501
507
 
502
508
  def _setup_static_files() -> None:
@@ -2,7 +2,7 @@
2
2
  Message domain entity.
3
3
  """
4
4
 
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, ConfigDict
6
6
 
7
7
  from ...shared.enums import MessageType, SenderType
8
8
  from ...shared.types import MessageId, SessionId
@@ -11,6 +11,8 @@ from ...shared.types import MessageId, SessionId
11
11
  class Message(BaseModel):
12
12
  """Message domain entity with business logic."""
13
13
 
14
+ model_config = ConfigDict(from_attributes=True)
15
+
14
16
  id: MessageId
15
17
  session_id: SessionId
16
18
  message: str
@@ -2,7 +2,7 @@
2
2
  Session domain entity.
3
3
  """
4
4
 
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, ConfigDict
6
6
 
7
7
  from ...shared import now_epoch_ms
8
8
  from ...shared.types import AgentId, SessionId, UserId
@@ -11,6 +11,8 @@ from ...shared.types import AgentId, SessionId, UserId
11
11
  class Session(BaseModel):
12
12
  """Session domain entity with business logic."""
13
13
 
14
+ model_config = ConfigDict(from_attributes=True)
15
+
14
16
  id: SessionId
15
17
  user_id: UserId
16
18
  name: str | None = None
@@ -18,6 +18,11 @@ class ISessionRepository(ABC):
18
18
  """Find all sessions for a specific user."""
19
19
  pass
20
20
 
21
+ @abstractmethod
22
+ def count_by_user(self, user_id: UserId) -> int:
23
+ """Count total sessions for a specific user."""
24
+ pass
25
+
21
26
  @abstractmethod
22
27
  def find_user_session(
23
28
  self, session_id: SessionId, user_id: UserId
@@ -4,19 +4,26 @@ Message repository implementation using SQLAlchemy.
4
4
 
5
5
  from sqlalchemy.orm import Session as DBSession
6
6
 
7
+ from ..shared.base_repository import PaginatedRepository
7
8
  from ..shared.enums import MessageType, SenderType
8
9
  from ..shared.types import PaginationInfo, SessionId
9
10
  from .entities import Message
10
11
  from .interfaces import IMessageRepository
11
- from .models import MessageModel
12
+ from .models import MessageModel, CreateMessageModel, UpdateMessageModel
12
13
 
13
14
 
14
- class MessageRepository(IMessageRepository):
15
- """SQLAlchemy implementation of message repository."""
15
+ class MessageRepository(PaginatedRepository[MessageModel, Message], IMessageRepository):
16
+ """SQLAlchemy implementation of message repository using BaseRepository."""
16
17
 
17
18
  def __init__(self, db: DBSession):
19
+ super().__init__(MessageModel, Message)
18
20
  self.db = db
19
21
 
22
+ @property
23
+ def entity_name(self) -> str:
24
+ """Return the entity name for error messages."""
25
+ return "message"
26
+
20
27
  def find_by_session(
21
28
  self, session_id: SessionId, pagination: PaginationInfo | None = None
22
29
  ) -> list[Message]:
@@ -24,28 +31,28 @@ class MessageRepository(IMessageRepository):
24
31
  query = self.db.query(MessageModel).filter(
25
32
  MessageModel.session_id == session_id
26
33
  )
34
+ query = query.order_by(MessageModel.created_time.asc())
27
35
 
28
36
  if pagination:
29
37
  offset = (pagination.page - 1) * pagination.page_size
30
38
  query = query.offset(offset).limit(pagination.page_size)
31
39
 
32
- models = query.order_by(MessageModel.created_time.asc()).all()
33
- return [self._model_to_entity(model) for model in models]
40
+ models = query.all()
41
+ return [self._convert_model_to_entity(model) for model in models]
34
42
 
35
43
  def save(self, message: Message) -> Message:
36
44
  """Save or update a message."""
37
- model = (
38
- self.db.query(MessageModel).filter(MessageModel.id == message.id).first()
39
- )
45
+ existing_model = self.db.query(MessageModel).filter(MessageModel.id == message.id).first()
40
46
 
41
- if model:
42
- # Update existing
43
- model.message = message.message
44
- model.sender_type = message.sender_type.value
45
- model.sender_name = message.sender_name
47
+ if existing_model:
48
+ update_model = UpdateMessageModel(
49
+ message=message.message,
50
+ sender_type=message.sender_type.value,
51
+ sender_name=message.sender_name,
52
+ )
53
+ return self.update(self.db, message.id, update_model.model_dump())
46
54
  else:
47
- # Create new
48
- model = MessageModel(
55
+ create_model = CreateMessageModel(
49
56
  id=message.id,
50
57
  session_id=message.session_id,
51
58
  message=message.message,
@@ -53,11 +60,7 @@ class MessageRepository(IMessageRepository):
53
60
  sender_name=message.sender_name,
54
61
  created_time=message.created_time,
55
62
  )
56
- self.db.add(model)
57
-
58
- self.db.commit()
59
- self.db.refresh(model)
60
- return self._model_to_entity(model)
63
+ return self.create(self.db, create_model.model_dump())
61
64
 
62
65
  def delete_by_session(self, session_id: SessionId) -> bool:
63
66
  """Delete all messages in a session."""
@@ -66,11 +69,10 @@ class MessageRepository(IMessageRepository):
66
69
  .filter(MessageModel.session_id == session_id)
67
70
  .delete()
68
71
  )
69
- self.db.commit()
70
72
  return result > 0
71
73
 
72
- def _model_to_entity(self, model: MessageModel) -> Message:
73
- """Convert SQLAlchemy model to domain entity."""
74
+ def _convert_model_to_entity(self, model: MessageModel) -> Message:
75
+ """Convert SQLAlchemy model to domain entity with enum handling."""
74
76
  return Message(
75
77
  id=model.id,
76
78
  session_id=model.session_id,
@@ -1,9 +1,17 @@
1
1
  """
2
- SQLAlchemy models for database persistence.
2
+ SQLAlchemy models and Pydantic models for database persistence.
3
3
  """
4
4
 
5
5
  from .base import Base
6
- from .message_model import MessageModel
7
- from .session_model import SessionModel
6
+ from .message_model import MessageModel, CreateMessageModel, UpdateMessageModel
7
+ from .session_model import SessionModel, CreateSessionModel, UpdateSessionModel
8
8
 
9
- __all__ = ["Base", "MessageModel", "SessionModel"]
9
+ __all__ = [
10
+ "Base",
11
+ "MessageModel",
12
+ "SessionModel",
13
+ "CreateMessageModel",
14
+ "UpdateMessageModel",
15
+ "CreateSessionModel",
16
+ "UpdateSessionModel",
17
+ ]
@@ -1,7 +1,8 @@
1
1
  """
2
- Message SQLAlchemy model.
2
+ Message SQLAlchemy model and Pydantic models for strongly-typed operations.
3
3
  """
4
4
 
5
+ from pydantic import BaseModel
5
6
  from sqlalchemy import BigInteger, Column, ForeignKey, String, Text
6
7
  from sqlalchemy.orm import relationship
7
8
 
@@ -25,3 +26,20 @@ class MessageModel(Base):
25
26
 
26
27
  # Relationship to session
27
28
  session = relationship("SessionModel", back_populates="messages")
29
+
30
+
31
+ class CreateMessageModel(BaseModel):
32
+ """Pydantic model for creating a message."""
33
+ id: str
34
+ session_id: str
35
+ message: str
36
+ sender_type: str
37
+ sender_name: str
38
+ created_time: int
39
+
40
+
41
+ class UpdateMessageModel(BaseModel):
42
+ """Pydantic model for updating a message."""
43
+ message: str
44
+ sender_type: str
45
+ sender_name: str
@@ -1,7 +1,8 @@
1
1
  """
2
- Session SQLAlchemy model.
2
+ Session SQLAlchemy model and Pydantic models for strongly-typed operations.
3
3
  """
4
4
 
5
+ from pydantic import BaseModel
5
6
  from sqlalchemy import BigInteger, Column, String
6
7
  from sqlalchemy.orm import relationship
7
8
 
@@ -27,3 +28,20 @@ class SessionModel(Base):
27
28
  messages = relationship(
28
29
  "MessageModel", back_populates="session", cascade="all, delete-orphan"
29
30
  )
31
+
32
+
33
+ class CreateSessionModel(BaseModel):
34
+ """Pydantic model for creating a session."""
35
+ id: str
36
+ name: str | None
37
+ user_id: str
38
+ agent_id: str | None
39
+ created_time: int
40
+ updated_time: int
41
+
42
+
43
+ class UpdateSessionModel(BaseModel):
44
+ """Pydantic model for updating a session."""
45
+ name: str | None = None
46
+ agent_id: str | None = None
47
+ updated_time: int
@@ -4,30 +4,47 @@ Session repository implementation using SQLAlchemy.
4
4
 
5
5
  from sqlalchemy.orm import Session as DBSession
6
6
 
7
+ from ..shared.base_repository import PaginatedRepository
7
8
  from ..shared.types import PaginationInfo, SessionId, UserId
8
9
  from .entities import Message, Session
9
10
  from .interfaces import ISessionRepository
10
- from .models import MessageModel, SessionModel
11
+ from .models import (
12
+ MessageModel,
13
+ SessionModel,
14
+ CreateSessionModel,
15
+ UpdateSessionModel,
16
+ )
11
17
 
12
18
 
13
- class SessionRepository(ISessionRepository):
14
- """SQLAlchemy implementation of session repository."""
19
+ class SessionRepository(PaginatedRepository[SessionModel, Session], ISessionRepository):
20
+ """SQLAlchemy implementation of session repository using BaseRepository."""
15
21
 
16
22
  def __init__(self, db: DBSession):
23
+ super().__init__(SessionModel, Session)
17
24
  self.db = db
18
25
 
26
+ @property
27
+ def entity_name(self) -> str:
28
+ """Return the entity name for error messages."""
29
+ return "session"
30
+
19
31
  def find_by_user(
20
32
  self, user_id: UserId, pagination: PaginationInfo | None = None
21
33
  ) -> list[Session]:
22
34
  """Find all sessions for a specific user."""
23
35
  query = self.db.query(SessionModel).filter(SessionModel.user_id == user_id)
36
+ query = query.order_by(SessionModel.updated_time.desc())
24
37
 
25
38
  if pagination:
26
39
  offset = (pagination.page - 1) * pagination.page_size
27
40
  query = query.offset(offset).limit(pagination.page_size)
28
41
 
29
- models = query.order_by(SessionModel.updated_time.desc()).all()
30
- return [self._model_to_entity(model) for model in models]
42
+ models = query.all()
43
+ return [Session.model_validate(model) for model in models]
44
+
45
+ def count_by_user(self, user_id: UserId) -> int:
46
+ """Count total sessions for a specific user."""
47
+ return self.db.query(SessionModel).filter(SessionModel.user_id == user_id).count()
31
48
 
32
49
  def find_user_session(
33
50
  self, session_id: SessionId, user_id: UserId
@@ -41,22 +58,21 @@ class SessionRepository(ISessionRepository):
41
58
  )
42
59
  .first()
43
60
  )
44
- return self._model_to_entity(model) if model else None
61
+ return Session.model_validate(model) if model else None
45
62
 
46
63
  def save(self, session: Session) -> Session:
47
64
  """Save or update a session."""
48
- model = (
49
- self.db.query(SessionModel).filter(SessionModel.id == session.id).first()
50
- )
65
+ existing_model = self.db.query(SessionModel).filter(SessionModel.id == session.id).first()
51
66
 
52
- if model:
53
- # Update existing
54
- model.name = session.name
55
- model.agent_id = session.agent_id
56
- model.updated_time = session.updated_time
67
+ if existing_model:
68
+ update_model = UpdateSessionModel(
69
+ name=session.name,
70
+ agent_id=session.agent_id,
71
+ updated_time=session.updated_time,
72
+ )
73
+ return self.update(self.db, session.id, update_model.model_dump(exclude_none=True))
57
74
  else:
58
- # Create new
59
- model = SessionModel(
75
+ create_model = CreateSessionModel(
60
76
  id=session.id,
61
77
  name=session.name,
62
78
  user_id=session.user_id,
@@ -64,24 +80,22 @@ class SessionRepository(ISessionRepository):
64
80
  created_time=session.created_time,
65
81
  updated_time=session.updated_time,
66
82
  )
67
- self.db.add(model)
68
-
69
- self.db.commit()
70
- self.db.refresh(model)
71
- return self._model_to_entity(model)
83
+ return self.create(self.db, create_model.model_dump())
72
84
 
73
85
  def delete(self, session_id: SessionId, user_id: UserId) -> bool:
74
86
  """Delete a session belonging to a user."""
75
- result = (
76
- self.db.query(SessionModel)
77
- .filter(
78
- SessionModel.id == session_id,
79
- SessionModel.user_id == user_id,
80
- )
81
- .delete()
82
- )
83
- self.db.commit()
84
- return result > 0
87
+ # Check if session belongs to user first
88
+ session_model = self.db.query(SessionModel).filter(
89
+ SessionModel.id == session_id,
90
+ SessionModel.user_id == user_id,
91
+ ).first()
92
+
93
+ if not session_model:
94
+ return False
95
+
96
+ # Use BaseRepository delete method
97
+ super().delete(self.db, session_id)
98
+ return True
85
99
 
86
100
  def find_user_session_with_messages(
87
101
  self,
@@ -112,21 +126,11 @@ class SessionRepository(ISessionRepository):
112
126
 
113
127
  message_models = message_query.order_by(MessageModel.created_time.asc()).all()
114
128
 
115
- session = self._model_to_entity(session_model)
129
+ session = Session.model_validate(session_model)
116
130
  messages = [self._message_model_to_entity(model) for model in message_models]
117
131
 
118
132
  return session, messages
119
133
 
120
- def _model_to_entity(self, model: SessionModel) -> Session:
121
- """Convert SQLAlchemy model to domain entity."""
122
- return Session(
123
- id=model.id,
124
- user_id=model.user_id,
125
- name=model.name,
126
- agent_id=model.agent_id,
127
- created_time=model.created_time,
128
- updated_time=model.updated_time,
129
- )
130
134
 
131
135
  def _message_model_to_entity(self, model: MessageModel) -> Message:
132
136
  """Convert SQLAlchemy message model to domain entity."""