solace-agent-mesh 1.4.6__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 (139) hide show
  1. solace_agent_mesh/agent/adk/runner.py +24 -8
  2. solace_agent_mesh/agent/sac/component.py +13 -1
  3. solace_agent_mesh/assets/docs/404.html +3 -3
  4. solace_agent_mesh/assets/docs/assets/js/{0e682baa.da822665.js → 0e682baa.d054e1d8.js} +1 -1
  5. solace_agent_mesh/assets/docs/assets/js/166ab619.e27886d9.js +1 -0
  6. solace_agent_mesh/assets/docs/assets/js/{1c6e87d2.43771adc.js → 1c6e87d2.e056b7e0.js} +1 -1
  7. solace_agent_mesh/assets/docs/assets/js/453a82a6.3c6bb61d.js +1 -0
  8. solace_agent_mesh/assets/docs/assets/js/483cef9a.4736f2d8.js +1 -0
  9. solace_agent_mesh/assets/docs/assets/js/{4c2787c2.fc6804f2.js → 4c2787c2.c1290a40.js} +1 -1
  10. solace_agent_mesh/assets/docs/assets/js/{5b4258a4.dff11eca.js → 5b4258a4.fdfd2325.js} +1 -1
  11. solace_agent_mesh/assets/docs/assets/js/{75384d09.abdf9cf9.js → 75384d09.c19e8b51.js} +1 -1
  12. solace_agent_mesh/assets/docs/assets/js/85387663.be2bc838.js +1 -0
  13. solace_agent_mesh/assets/docs/assets/js/945fb41e.16e00776.js +1 -0
  14. solace_agent_mesh/assets/docs/assets/js/a12a4955.25fbed32.js +1 -0
  15. solace_agent_mesh/assets/docs/assets/js/a3a92b25.af35e313.js +1 -0
  16. solace_agent_mesh/assets/docs/assets/js/ae0e903d.5fe5203f.js +1 -0
  17. solace_agent_mesh/assets/docs/assets/js/bac0be12.17de4316.js +1 -0
  18. solace_agent_mesh/assets/docs/assets/js/cee5d587.47904f5e.js +1 -0
  19. solace_agent_mesh/assets/docs/assets/js/d6a81ee7.829198f1.js +1 -0
  20. solace_agent_mesh/assets/docs/assets/js/f284c35a.ed8dd236.js +1 -0
  21. solace_agent_mesh/assets/docs/assets/js/f897a61a.126663fe.js +1 -0
  22. solace_agent_mesh/assets/docs/assets/js/{fbfa3e75.aca209c9.js → fbfa3e75.e144b16c.js} +1 -1
  23. solace_agent_mesh/assets/docs/assets/js/main.86924c42.js +2 -0
  24. solace_agent_mesh/assets/docs/assets/js/runtime~main.0d2ff2b6.js +1 -0
  25. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +4 -4
  26. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/rbac-setup-guilde/index.html +201 -0
  27. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +4 -4
  28. 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
  29. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +4 -4
  30. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +4 -4
  31. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +7 -7
  32. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +4 -4
  33. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +4 -4
  34. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +4 -4
  35. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +4 -4
  36. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +5 -5
  37. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +5 -5
  38. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +6 -6
  39. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +7 -7
  40. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +12 -13
  41. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/litellm_models/index.html +49 -0
  42. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +6 -6
  44. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +6 -6
  45. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +8 -8
  48. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +5 -5
  49. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +7 -7
  51. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +5 -5
  52. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +4 -4
  53. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
  54. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +4 -4
  55. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +4 -4
  56. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +4 -4
  57. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
  58. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +4 -4
  59. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +4 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
  61. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +4 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +6 -6
  64. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +7 -7
  65. solace_agent_mesh/assets/docs/lunr-index-1759246102819.json +1 -0
  66. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  67. solace_agent_mesh/assets/docs/search-doc-1759246102819.json +1 -0
  68. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  69. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  70. solace_agent_mesh/cli/__init__.py +1 -1
  71. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -3
  72. solace_agent_mesh/cli/commands/init_cmd/__init__.py +3 -3
  73. solace_agent_mesh/cli/commands/init_cmd/broker_step.py +1 -1
  74. solace_agent_mesh/cli/commands/init_cmd/env_step.py +1 -1
  75. solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +4 -4
  76. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +12 -1
  77. solace_agent_mesh/cli/commands/plugin_cmd/install_cmd.py +5 -5
  78. solace_agent_mesh/client/webui/frontend/static/assets/main-B0PHV3hm.js +339 -0
  79. solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
  80. solace_agent_mesh/config_portal/backend/common.py +1 -1
  81. solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-bFMKlzKf.js → _index-BNuqpWDc.js} +1 -1
  82. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-89db7c30.js → manifest-44d62be6.js} +1 -1
  83. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  84. solace_agent_mesh/gateway/http_sse/alembic/versions/20250916_f6e7d8c9b0a1_convert_timestamps_to_epoch_and_align_columns.py +112 -42
  85. solace_agent_mesh/gateway/http_sse/app.py +0 -28
  86. solace_agent_mesh/gateway/http_sse/component.py +21 -15
  87. solace_agent_mesh/gateway/http_sse/dependencies.py +84 -88
  88. solace_agent_mesh/gateway/http_sse/main.py +37 -15
  89. solace_agent_mesh/gateway/http_sse/repository/entities/message.py +3 -1
  90. solace_agent_mesh/gateway/http_sse/repository/entities/session.py +3 -1
  91. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +5 -0
  92. solace_agent_mesh/gateway/http_sse/repository/message_repository.py +25 -23
  93. solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +12 -4
  94. solace_agent_mesh/gateway/http_sse/repository/models/message_model.py +19 -1
  95. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +19 -1
  96. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +46 -42
  97. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +199 -59
  98. solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +1 -6
  99. solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +3 -17
  100. solace_agent_mesh/gateway/http_sse/routers/people.py +4 -37
  101. solace_agent_mesh/gateway/http_sse/routers/sessions.py +33 -68
  102. solace_agent_mesh/gateway/http_sse/routers/tasks.py +54 -28
  103. solace_agent_mesh/gateway/http_sse/services/session_service.py +60 -28
  104. solace_agent_mesh/gateway/http_sse/shared/__init__.py +122 -1
  105. solace_agent_mesh/gateway/http_sse/shared/base_repository.py +278 -0
  106. solace_agent_mesh/gateway/http_sse/shared/database_exceptions.py +274 -0
  107. solace_agent_mesh/gateway/http_sse/shared/database_helpers.py +43 -0
  108. solace_agent_mesh/gateway/http_sse/shared/error_dto.py +107 -0
  109. solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +192 -0
  110. solace_agent_mesh/gateway/http_sse/shared/exceptions.py +192 -0
  111. solace_agent_mesh/gateway/http_sse/shared/pagination.py +138 -0
  112. solace_agent_mesh/gateway/http_sse/shared/response_utils.py +134 -0
  113. solace_agent_mesh/gateway/http_sse/shared/utils.py +22 -0
  114. solace_agent_mesh/templates/plugin_agent_config_template.yaml +1 -1
  115. solace_agent_mesh/templates/plugin_custom_config_template.yaml +1 -1
  116. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +1 -1
  117. solace_agent_mesh/templates/shared_config.yaml +1 -1
  118. {solace_agent_mesh-1.4.6.dist-info → solace_agent_mesh-1.4.8.dist-info}/METADATA +34 -35
  119. {solace_agent_mesh-1.4.6.dist-info → solace_agent_mesh-1.4.8.dist-info}/RECORD +123 -110
  120. solace_agent_mesh/assets/docs/assets/js/166ab619.e8f3a7c7.js +0 -1
  121. solace_agent_mesh/assets/docs/assets/js/483cef9a.8d318c2f.js +0 -1
  122. solace_agent_mesh/assets/docs/assets/js/85387663.6bf41934.js +0 -1
  123. solace_agent_mesh/assets/docs/assets/js/945fb41e.abf2be91.js +0 -1
  124. solace_agent_mesh/assets/docs/assets/js/a3a92b25.1d029b81.js +0 -1
  125. solace_agent_mesh/assets/docs/assets/js/ae0e903d.c786e887.js +0 -1
  126. solace_agent_mesh/assets/docs/assets/js/bac0be12.27ee2c26.js +0 -1
  127. solace_agent_mesh/assets/docs/assets/js/beecea0d.8bbd852c.js +0 -1
  128. solace_agent_mesh/assets/docs/assets/js/cee5d587.f1e1ca86.js +0 -1
  129. solace_agent_mesh/assets/docs/assets/js/f284c35a.2b2f5048.js +0 -1
  130. solace_agent_mesh/assets/docs/assets/js/f897a61a.bc634a3e.js +0 -1
  131. solace_agent_mesh/assets/docs/assets/js/main.72d74e33.js +0 -2
  132. solace_agent_mesh/assets/docs/assets/js/runtime~main.3dcfaf51.js +0 -1
  133. solace_agent_mesh/assets/docs/lunr-index-1758893005563.json +0 -1
  134. solace_agent_mesh/assets/docs/search-doc-1758893005563.json +0 -1
  135. solace_agent_mesh/client/webui/frontend/static/assets/main-BKIoiLSu.js +0 -339
  136. /solace_agent_mesh/assets/docs/assets/js/{main.72d74e33.js.LICENSE.txt → main.86924c42.js.LICENSE.txt} +0 -0
  137. {solace_agent_mesh-1.4.6.dist-info → solace_agent_mesh-1.4.8.dist-info}/WHEEL +0 -0
  138. {solace_agent_mesh-1.4.6.dist-info → solace_agent_mesh-1.4.8.dist-info}/entry_points.txt +0 -0
  139. {solace_agent_mesh-1.4.6.dist-info → solace_agent_mesh-1.4.8.dist-info}/licenses/LICENSE +0 -0
@@ -104,22 +104,38 @@ def _upgrade_sqlite(current_time_ms: int) -> None:
104
104
  def _upgrade_standard_sql(current_time_ms: int) -> None:
105
105
  """Handle PostgreSQL/MySQL upgrade using ALTER COLUMN (standard SQL approach)."""
106
106
 
107
+ bind = op.get_bind()
108
+
107
109
  # For sessions table
108
110
  op.add_column("sessions", sa.Column("created_time", sa.BigInteger(), nullable=True))
109
111
  op.add_column("sessions", sa.Column("updated_time", sa.BigInteger(), nullable=True))
110
112
 
111
- # Convert timestamps using EXTRACT function
112
- op.execute("""
113
- UPDATE sessions
114
- SET created_time = CAST(EXTRACT(EPOCH FROM created_at) * 1000 AS BIGINT)
115
- WHERE created_at IS NOT NULL
116
- """)
117
-
118
- op.execute("""
119
- UPDATE sessions
120
- SET updated_time = CAST(EXTRACT(EPOCH FROM updated_at) * 1000 AS BIGINT)
121
- WHERE updated_at IS NOT NULL
122
- """)
113
+ # Convert timestamps using database-appropriate functions
114
+ if bind.dialect.name == 'postgresql':
115
+ op.execute("""
116
+ UPDATE sessions
117
+ SET created_time = CAST(EXTRACT(EPOCH FROM created_at) * 1000 AS BIGINT)
118
+ WHERE created_at IS NOT NULL
119
+ """)
120
+
121
+ op.execute("""
122
+ UPDATE sessions
123
+ SET updated_time = CAST(EXTRACT(EPOCH FROM updated_at) * 1000 AS BIGINT)
124
+ WHERE updated_at IS NOT NULL
125
+ """)
126
+ else:
127
+ # MySQL and other databases use UNIX_TIMESTAMP
128
+ op.execute("""
129
+ UPDATE sessions
130
+ SET created_time = CAST(UNIX_TIMESTAMP(created_at) * 1000 AS UNSIGNED)
131
+ WHERE created_at IS NOT NULL
132
+ """)
133
+
134
+ op.execute("""
135
+ UPDATE sessions
136
+ SET updated_time = CAST(UNIX_TIMESTAMP(updated_at) * 1000 AS UNSIGNED)
137
+ WHERE updated_at IS NOT NULL
138
+ """)
123
139
 
124
140
  # Set current epoch ms for null values
125
141
  op.execute(f"""
@@ -145,11 +161,19 @@ def _upgrade_standard_sql(current_time_ms: int) -> None:
145
161
  # For chat_messages table
146
162
  op.add_column("chat_messages", sa.Column("created_time", sa.BigInteger(), nullable=True))
147
163
 
148
- op.execute("""
149
- UPDATE chat_messages
150
- SET created_time = CAST(EXTRACT(EPOCH FROM created_at) * 1000 AS BIGINT)
151
- WHERE created_at IS NOT NULL
152
- """)
164
+ if bind.dialect.name == 'postgresql':
165
+ op.execute("""
166
+ UPDATE chat_messages
167
+ SET created_time = CAST(EXTRACT(EPOCH FROM created_at) * 1000 AS BIGINT)
168
+ WHERE created_at IS NOT NULL
169
+ """)
170
+ else:
171
+ # MySQL and other databases use UNIX_TIMESTAMP
172
+ op.execute("""
173
+ UPDATE chat_messages
174
+ SET created_time = CAST(UNIX_TIMESTAMP(created_at) * 1000 AS UNSIGNED)
175
+ WHERE created_at IS NOT NULL
176
+ """)
153
177
 
154
178
  op.execute(f"""
155
179
  UPDATE chat_messages
@@ -199,18 +223,41 @@ def _create_updated_indexes() -> None:
199
223
 
200
224
  def _create_indexes_safe(index_name: str, table_name: str, columns: list) -> None:
201
225
  """Create index only if it doesn't exist."""
202
- try:
203
- op.create_index(index_name, table_name, columns)
204
- except Exception:
205
- pass # Index might already exist
226
+ bind = op.get_bind()
227
+ inspector = sa.inspect(bind)
228
+
229
+ if bind.dialect.name == 'postgresql':
230
+ columns_str = ', '.join(columns)
231
+ bind.execute(sa.text(f"CREATE INDEX IF NOT EXISTS {index_name} ON {table_name} ({columns_str})"))
232
+ elif bind.dialect.name == 'sqlite':
233
+ try:
234
+ op.create_index(index_name, table_name, columns)
235
+ except Exception:
236
+ pass
237
+ else:
238
+ existing_indexes = inspector.get_indexes(table_name)
239
+ index_exists = any(idx['name'] == index_name for idx in existing_indexes)
240
+ if not index_exists:
241
+ op.create_index(index_name, table_name, columns)
206
242
 
207
243
 
208
244
  def _drop_index_safe(index_name: str, table_name: str) -> None:
209
245
  """Drop index only if it exists."""
210
- try:
211
- op.drop_index(index_name, table_name=table_name)
212
- except Exception:
213
- pass # Index might not exist
246
+ bind = op.get_bind()
247
+ inspector = sa.inspect(bind)
248
+
249
+ if bind.dialect.name == 'postgresql':
250
+ bind.execute(sa.text(f"DROP INDEX IF EXISTS {index_name}"))
251
+ elif bind.dialect.name == 'sqlite':
252
+ try:
253
+ op.drop_index(index_name, table_name=table_name)
254
+ except Exception:
255
+ pass
256
+ else:
257
+ existing_indexes = inspector.get_indexes(table_name)
258
+ index_exists = any(idx['name'] == index_name for idx in existing_indexes)
259
+ if index_exists:
260
+ op.drop_index(index_name, table_name=table_name)
214
261
 
215
262
 
216
263
  def downgrade() -> None:
@@ -297,6 +344,7 @@ def _downgrade_sqlite() -> None:
297
344
 
298
345
  def _downgrade_standard_sql() -> None:
299
346
  """Handle PostgreSQL/MySQL downgrade using ALTER COLUMN."""
347
+ bind = op.get_bind()
300
348
 
301
349
  # Drop indexes on new columns
302
350
  _drop_index_safe("ix_chat_messages_session_id_created_time", "chat_messages")
@@ -308,18 +356,32 @@ def _downgrade_standard_sql() -> None:
308
356
  op.add_column("sessions", sa.Column("created_at", sa.DateTime(), nullable=True))
309
357
  op.add_column("sessions", sa.Column("updated_at", sa.DateTime(), nullable=True))
310
358
 
311
- # Convert epoch milliseconds back to datetime
312
- op.execute("""
313
- UPDATE sessions
314
- SET created_at = to_timestamp(created_time / 1000.0)
315
- WHERE created_time IS NOT NULL
316
- """)
317
-
318
- op.execute("""
319
- UPDATE sessions
320
- SET updated_at = to_timestamp(updated_time / 1000.0)
321
- WHERE updated_time IS NOT NULL
322
- """)
359
+ # Convert epoch milliseconds back to datetime based on database type
360
+ if bind.dialect.name == 'postgresql':
361
+ op.execute("""
362
+ UPDATE sessions
363
+ SET created_at = to_timestamp(created_time / 1000.0)
364
+ WHERE created_time IS NOT NULL
365
+ """)
366
+
367
+ op.execute("""
368
+ UPDATE sessions
369
+ SET updated_at = to_timestamp(updated_time / 1000.0)
370
+ WHERE updated_time IS NOT NULL
371
+ """)
372
+ else:
373
+ # MySQL and other databases
374
+ op.execute("""
375
+ UPDATE sessions
376
+ SET created_at = FROM_UNIXTIME(created_time / 1000.0)
377
+ WHERE created_time IS NOT NULL
378
+ """)
379
+
380
+ op.execute("""
381
+ UPDATE sessions
382
+ SET updated_at = FROM_UNIXTIME(updated_time / 1000.0)
383
+ WHERE updated_time IS NOT NULL
384
+ """)
323
385
 
324
386
  op.drop_column("sessions", "created_time")
325
387
  op.drop_column("sessions", "updated_time")
@@ -327,11 +389,19 @@ def _downgrade_standard_sql() -> None:
327
389
  # For chat_messages table: convert back to datetime column
328
390
  op.add_column("chat_messages", sa.Column("created_at", sa.DateTime(), nullable=True))
329
391
 
330
- op.execute("""
331
- UPDATE chat_messages
332
- SET created_at = to_timestamp(created_time / 1000.0)
333
- WHERE created_time IS NOT NULL
334
- """)
392
+ if bind.dialect.name == 'postgresql':
393
+ op.execute("""
394
+ UPDATE chat_messages
395
+ SET created_at = to_timestamp(created_time / 1000.0)
396
+ WHERE created_time IS NOT NULL
397
+ """)
398
+ else:
399
+ # MySQL and other databases
400
+ op.execute("""
401
+ UPDATE chat_messages
402
+ SET created_at = FROM_UNIXTIME(created_time / 1000.0)
403
+ WHERE created_time IS NOT NULL
404
+ """)
335
405
 
336
406
  op.drop_column("chat_messages", "created_time")
337
407
 
@@ -4,9 +4,6 @@ Defines configuration schema and programmatically creates the WebUIBackendCompon
4
4
  """
5
5
 
6
6
  from typing import Any, Dict, List
7
- import os
8
- from alembic import command
9
- from alembic.config import Config
10
7
  from solace_ai_connector.common.log import log
11
8
 
12
9
  from ...gateway.http_sse.component import WebUIBackendComponent
@@ -188,31 +185,6 @@ class WebUIBackendApp(BaseGatewayApp):
188
185
  )
189
186
  super().__init__(app_info, **kwargs)
190
187
 
191
- try:
192
-
193
- alembic_ini_path = os.path.join(os.path.dirname(__file__), "alembic.ini")
194
- if os.path.exists(alembic_ini_path):
195
- log.debug("Loading Alembic configuration from alembic.ini.")
196
- alembic_cfg = Config(alembic_ini_path)
197
- else:
198
- log.warning(
199
- "alembic.ini not found. Falling back to programmatic configuration."
200
- )
201
- alembic_cfg = Config()
202
- alembic_cfg.set_main_option(
203
- "script_location",
204
- os.path.join(os.path.dirname(__file__), "alembic"),
205
- )
206
-
207
- session_service_config = self.get_config("session_service", {})
208
- db_url = session_service_config.get("database_url")
209
- if db_url:
210
- alembic_cfg.set_main_option("sqlalchemy.url", db_url)
211
- command.upgrade(alembic_cfg, "head")
212
- else:
213
- log.warning("Database URL not configured. Skipping migrations.")
214
- except Exception as e:
215
- log.warning(f"Alembic migration failed: {e}")
216
188
 
217
189
  log.debug("%s WebUIBackendApp initialization complete.", self.name)
218
190
 
@@ -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)
@@ -310,38 +310,48 @@ def _setup_alembic_config(database_url: str) -> Config:
310
310
 
311
311
 
312
312
  def _run_community_migrations(database_url: str) -> None:
313
+ """
314
+ Run Alembic migrations for the community database schema.
315
+ This includes sessions, chat_messages tables and their indexes.
316
+ """
313
317
  try:
314
318
  from sqlalchemy import create_engine
315
319
 
320
+ log.info("Starting community migrations...")
316
321
  engine = create_engine(database_url)
317
322
  inspector = sa.inspect(engine)
318
323
  existing_tables = inspector.get_table_names()
319
324
 
320
325
  if not existing_tables or "sessions" not in existing_tables:
321
- log.info("Running community database migrations...")
326
+ log.info("Running initial community database setup")
322
327
  alembic_cfg = _setup_alembic_config(database_url)
323
328
  command.upgrade(alembic_cfg, "head")
324
- log.info("Community database migrations complete.")
329
+ log.info("Community database migrations completed")
325
330
  else:
326
- log.info(
327
- "Community database tables already exist, skipping community migrations."
328
- )
331
+ log.info("Checking for community schema updates")
332
+ alembic_cfg = _setup_alembic_config(database_url)
333
+ command.upgrade(alembic_cfg, "head")
334
+ log.info("Community database schema is current")
329
335
  except Exception as e:
330
336
  log.warning(
331
- "Community migration check failed, attempting to run migrations anyway: %s",
337
+ "Community migration check failed: %s - attempting to run migrations",
332
338
  e,
333
339
  )
334
340
  try:
335
341
  alembic_cfg = _setup_alembic_config(database_url)
336
342
  command.upgrade(alembic_cfg, "head")
337
- log.info("Community database migrations complete.")
343
+ log.info("Community database migrations completed")
338
344
  except Exception as migration_error:
339
- log.warning(
340
- "Community migration failed but continuing: %s", migration_error
341
- )
345
+ log.error("Community migration failed: %s", migration_error)
346
+ log.error("Check database connectivity and permissions")
347
+ raise RuntimeError(f"Community database migration failed: {migration_error}") from migration_error
342
348
 
343
349
 
344
350
  def _run_enterprise_migrations(component: "WebUIBackendComponent", database_url: str) -> None:
351
+ """
352
+ Run migrations for enterprise features like advanced analytics, audit logs, etc.
353
+ This is optional and only runs if the enterprise package is available.
354
+ """
345
355
  try:
346
356
  from solace_agent_mesh_enterprise.webui_backend.migration_runner import (
347
357
  run_migrations,
@@ -349,19 +359,25 @@ def _run_enterprise_migrations(component: "WebUIBackendComponent", database_url:
349
359
 
350
360
  webui_app = component.get_app()
351
361
  app_config = getattr(webui_app, "app_config", {}) if webui_app else {}
352
- log.info("Running enterprise migrations...")
362
+ log.info("Starting enterprise migrations...")
353
363
  run_migrations(database_url, app_config)
354
364
  log.info("Enterprise migrations completed")
355
365
  except (ImportError, ModuleNotFoundError):
356
366
  log.debug("Enterprise module not found - skipping enterprise migrations")
357
367
  except Exception as e:
358
- log.warning("Enterprise migration failed but continuing: %s", e)
368
+ log.error("Enterprise migration failed: %s", e)
369
+ log.error("Advanced features may be unavailable")
370
+ raise RuntimeError(f"Enterprise database migration failed: {e}") from e
359
371
 
360
372
 
361
373
  def _setup_database(component: "WebUIBackendComponent", database_url: str) -> None:
374
+ """
375
+ Initialize database connection and run all required migrations.
376
+ This sets up both community and enterprise database schemas.
377
+ """
362
378
  dependencies.init_database(database_url)
363
379
  log.info("Persistence enabled - sessions will be stored in database")
364
- log.info("Checking database migrations...")
380
+ log.info("Running database migrations...")
365
381
 
366
382
  _run_community_migrations(database_url)
367
383
  _run_enterprise_migrations(component, database_url)
@@ -463,6 +479,12 @@ def _setup_routers() -> None:
463
479
  app.include_router(auth.router, prefix=api_prefix, tags=["Auth"])
464
480
  log.info("Legacy routers mounted for endpoints not yet migrated")
465
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
466
488
  try:
467
489
  from solace_agent_mesh_enterprise.webui_backend.routers import get_enterprise_routers
468
490
 
@@ -478,9 +500,9 @@ def _setup_routers() -> None:
478
500
  except ImportError:
479
501
  log.debug("No enterprise package detected - skipping enterprise routers")
480
502
  except ModuleNotFoundError:
481
- log.debug("Enterprise router module not found - skipping enterprise routers")
503
+ log.debug("Enterprise module not found - skipping enterprise routers and exception handlers")
482
504
  except Exception as e:
483
- log.warning("Failed to load enterprise routers: %s", e)
505
+ log.warning("Failed to load enterprise routers and exception handlers: %s", e)
484
506
 
485
507
 
486
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