solace-agent-mesh 1.4.1__py3-none-any.whl → 1.4.3__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 (84) hide show
  1. solace_agent_mesh/agent/adk/services.py +10 -3
  2. solace_agent_mesh/agent/sac/app.py +4 -1
  3. solace_agent_mesh/assets/docs/404.html +3 -3
  4. solace_agent_mesh/assets/docs/assets/js/beecea0d.8bbd852c.js +1 -0
  5. solace_agent_mesh/assets/docs/assets/js/{main.9bc1a102.js → main.66eec320.js} +2 -2
  6. solace_agent_mesh/assets/docs/assets/js/{runtime~main.f2b4ea70.js → runtime~main.355446b2.js} +1 -1
  7. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +3 -3
  8. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +3 -3
  9. 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
  10. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
  11. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
  12. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
  13. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +3 -3
  14. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
  15. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
  16. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
  17. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
  18. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
  19. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
  20. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
  21. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +6 -3
  22. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
  23. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  24. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
  25. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
  26. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
  27. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
  28. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +3 -3
  29. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
  30. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
  31. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
  32. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
  33. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
  34. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
  35. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
  36. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
  37. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
  38. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
  39. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
  40. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
  41. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
  42. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
  43. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
  44. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
  45. solace_agent_mesh/assets/docs/lunr-index-1758138634356.json +1 -0
  46. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  47. solace_agent_mesh/assets/docs/search-doc-1758138634356.json +1 -0
  48. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  49. solace_agent_mesh/cli/__init__.py +1 -1
  50. solace_agent_mesh/client/webui/frontend/static/assets/main-8xbvgfVK.css +1 -0
  51. solace_agent_mesh/client/webui/frontend/static/assets/main-Cv2k8j3R.js +339 -0
  52. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +13 -13
  53. solace_agent_mesh/client/webui/frontend/static/index.html +13 -13
  54. solace_agent_mesh/gateway/base/app.py +121 -70
  55. solace_agent_mesh/gateway/http_sse/alembic/versions/20250916_f6e7d8c9b0a1_convert_timestamps_to_epoch_and_align_columns.py +342 -0
  56. solace_agent_mesh/gateway/http_sse/alembic.ini +4 -6
  57. solace_agent_mesh/gateway/http_sse/app.py +151 -100
  58. solace_agent_mesh/gateway/http_sse/dependencies.py +30 -24
  59. solace_agent_mesh/gateway/http_sse/repository/entities/message.py +3 -5
  60. solace_agent_mesh/gateway/http_sse/repository/entities/session.py +7 -11
  61. solace_agent_mesh/gateway/http_sse/repository/message_repository.py +11 -7
  62. solace_agent_mesh/gateway/http_sse/repository/models/message_model.py +7 -7
  63. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +10 -8
  64. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +18 -14
  65. solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +42 -0
  66. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +25 -23
  67. solace_agent_mesh/gateway/http_sse/routers/sessions.py +10 -19
  68. solace_agent_mesh/gateway/http_sse/services/session_service.py +5 -6
  69. solace_agent_mesh/gateway/http_sse/shared/__init__.py +17 -1
  70. solace_agent_mesh/gateway/http_sse/shared/timestamp_utils.py +97 -0
  71. solace_agent_mesh/gateway/http_sse/shared/types.py +23 -7
  72. {solace_agent_mesh-1.4.1.dist-info → solace_agent_mesh-1.4.3.dist-info}/METADATA +1 -1
  73. {solace_agent_mesh-1.4.1.dist-info → solace_agent_mesh-1.4.3.dist-info}/RECORD +79 -76
  74. solace_agent_mesh/assets/docs/assets/js/beecea0d.ae31f6a7.js +0 -1
  75. solace_agent_mesh/assets/docs/lunr-index-1758036158289.json +0 -1
  76. solace_agent_mesh/assets/docs/search-doc-1758036158289.json +0 -1
  77. solace_agent_mesh/client/webui/frontend/static/assets/main-B6BpuH9K.js +0 -339
  78. solace_agent_mesh/client/webui/frontend/static/assets/main-B9s_V9tJ.css +0 -1
  79. /solace_agent_mesh/assets/docs/assets/js/{main.9bc1a102.js.LICENSE.txt → main.66eec320.js.LICENSE.txt} +0 -0
  80. /solace_agent_mesh/gateway/http_sse/alembic/versions/{d5b3f8f2e9a0_create_initial_database.py → 20250910_d5b3f8f2e9a0_create_initial_database.py} +0 -0
  81. /solace_agent_mesh/gateway/http_sse/alembic/versions/{b1c2d3e4f5g6_add_database_indexes.py → 20250911_b1c2d3e4f5g6_add_database_indexes.py} +0 -0
  82. {solace_agent_mesh-1.4.1.dist-info → solace_agent_mesh-1.4.3.dist-info}/WHEEL +0 -0
  83. {solace_agent_mesh-1.4.1.dist-info → solace_agent_mesh-1.4.3.dist-info}/entry_points.txt +0 -0
  84. {solace_agent_mesh-1.4.1.dist-info → solace_agent_mesh-1.4.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,15 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/assets/favicon-BLgzUch9.ico" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Solace Agent Mesh</title>
8
- <script type="module" crossorigin src="/assets/authCallback-j1LW-wlq.js"></script>
9
- <link rel="modulepreload" crossorigin href="/assets/vendor-CS5YMf8a.js">
10
- <link rel="modulepreload" crossorigin href="/assets/client-B9p_nFNA.js">
11
- </head>
12
- <body>
13
- <div id="root"></div>
14
- </body>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/assets/favicon-BLgzUch9.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Solace Agent Mesh</title>
8
+ <script type="module" crossorigin src="/assets/authCallback-j1LW-wlq.js"></script>
9
+ <link rel="modulepreload" crossorigin href="/assets/vendor-CS5YMf8a.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/client-B9p_nFNA.js">
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ </body>
15
15
  </html>
@@ -1,16 +1,16 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/assets/favicon-BLgzUch9.ico" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Solace Agent Mesh</title>
8
- <script type="module" crossorigin src="/assets/main-B6BpuH9K.js"></script>
9
- <link rel="modulepreload" crossorigin href="/assets/vendor-CS5YMf8a.js">
10
- <link rel="modulepreload" crossorigin href="/assets/client-B9p_nFNA.js">
11
- <link rel="stylesheet" crossorigin href="/assets/main-B9s_V9tJ.css">
12
- </head>
13
- <body>
14
- <div id="root"></div>
15
- </body>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/assets/favicon-BLgzUch9.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Solace Agent Mesh</title>
8
+ <script type="module" crossorigin src="/assets/main-Cv2k8j3R.js"></script>
9
+ <link rel="modulepreload" crossorigin href="/assets/vendor-CS5YMf8a.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/client-B9p_nFNA.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/main-8xbvgfVK.css">
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ </body>
16
16
  </html>
@@ -4,15 +4,13 @@ Base App class for Gateway implementations in the Solace AI Connector.
4
4
 
5
5
  import uuid
6
6
  from abc import abstractmethod
7
- from typing import Any, Dict, List, Type, Optional, Literal
7
+ from typing import Any, Dict, List, Type
8
8
 
9
- from pydantic import Field, ValidationError
10
9
  from solace_ai_connector.common.log import log
11
10
  from solace_ai_connector.common.utils import deep_merge
12
11
  from solace_ai_connector.flow.app import App
13
12
  from solace_ai_connector.components.component_base import ComponentBase
14
13
 
15
- from ...common.utils.pydantic_utils import SamConfigBase
16
14
  from ...common.a2a import (
17
15
  get_discovery_topic,
18
16
  get_gateway_response_subscription_topic,
@@ -24,58 +22,91 @@ class BaseGatewayComponent(ComponentBase):
24
22
  pass
25
23
 
26
24
 
27
- class BaseGatewayAppConfig(SamConfigBase):
28
- """Base Pydantic model for gateway application configuration."""
29
-
30
- namespace: str = Field(
31
- ...,
32
- description="Absolute topic prefix for A2A communication (e.g., 'myorg/dev').",
33
- )
34
- gateway_id: Optional[str] = Field(
35
- default=None,
36
- description="Unique ID for this gateway instance. Auto-generated if omitted.",
37
- )
38
- artifact_service: Dict[str, Any] = Field(
39
- ...,
40
- description="Configuration for the SHARED ADK Artifact Service.",
41
- )
42
- enable_embed_resolution: bool = Field(
43
- default=True,
44
- description="Enable late-stage 'artifact_content' embed resolution in the gateway.",
45
- )
46
- gateway_max_artifact_resolve_size_bytes: int = Field(
47
- default=104857600, # 100MB
48
- description="Maximum size of an individual artifact's raw content for 'artifact_content' embeds and max total accumulated size for a parent artifact after internal recursive resolution.",
49
- )
50
- gateway_recursive_embed_depth: int = Field(
51
- default=12,
52
- description="Maximum depth for recursively resolving 'artifact_content' embeds within files.",
53
- )
54
- artifact_handling_mode: Literal["reference", "embed", "passthrough"] = Field(
55
- default="reference",
56
- description=(
57
- "How the gateway handles file parts from clients. "
58
- "'reference': Save inline file bytes to the artifact store and replace with a URI. "
59
- "'embed': Resolve file URIs and embed content as bytes. "
60
- "'passthrough': Send file parts to the agent as-is."
61
- ),
62
- )
63
- gateway_max_message_size_bytes: int = Field(
64
- default=10_000_000, # 10MB
65
- description="Maximum allowed message size in bytes for messages published by the gateway.",
66
- )
67
- default_user_identity: Optional[str] = Field(
68
- default=None,
69
- description="Default user identity to use when no user authentication is provided. WARNING: Only use in development environments with trusted access!",
70
- )
71
- force_user_identity: Optional[str] = Field(
72
- default=None,
73
- description="Override any provided user identity with this value. WARNING: Development only! This completely replaces authentication.",
74
- )
75
- identity_service: Optional[Dict[str, Any]] = Field(
76
- default=None,
77
- description="Configuration for the pluggable Identity Service provider.",
78
- )
25
+ BASE_GATEWAY_APP_SCHEMA: Dict[str, List[Dict[str, Any]]] = {
26
+ "config_parameters": [
27
+ {
28
+ "name": "namespace",
29
+ "required": True,
30
+ "type": "string",
31
+ "description": "Absolute topic prefix for A2A communication (e.g., 'myorg/dev').",
32
+ },
33
+ {
34
+ "name": "gateway_id",
35
+ "required": False,
36
+ "type": "string",
37
+ "default": None,
38
+ "description": "Unique ID for this gateway instance. Auto-generated if omitted.",
39
+ },
40
+ {
41
+ "name": "artifact_service",
42
+ "required": True,
43
+ "type": "object",
44
+ "description": "Configuration for the SHARED ADK Artifact Service.",
45
+ },
46
+ {
47
+ "name": "enable_embed_resolution",
48
+ "required": False,
49
+ "type": "boolean",
50
+ "default": True,
51
+ "description": "Enable late-stage 'artifact_content' embed resolution in the gateway.",
52
+ },
53
+ {
54
+ "name": "gateway_max_artifact_resolve_size_bytes",
55
+ "required": False,
56
+ "type": "integer",
57
+ "default": 104857600, # 100MB
58
+ "description": "Maximum size of an individual artifact's raw content for 'artifact_content' embeds and max total accumulated size for a parent artifact after internal recursive resolution.",
59
+ },
60
+ {
61
+ "name": "gateway_recursive_embed_depth",
62
+ "required": False,
63
+ "type": "integer",
64
+ "default": 12,
65
+ "description": "Maximum depth for recursively resolving 'artifact_content' embeds within files.",
66
+ },
67
+ {
68
+ "name": "artifact_handling_mode",
69
+ "required": False,
70
+ "type": "string",
71
+ "default": "reference",
72
+ "description": (
73
+ "How the gateway handles file parts from clients. "
74
+ "'reference': Save inline file bytes to the artifact store and replace with a URI. "
75
+ "'embed': Resolve file URIs and embed content as bytes. "
76
+ "'passthrough': Send file parts to the agent as-is."
77
+ ),
78
+ "enum": ["reference", "embed", "passthrough"],
79
+ },
80
+ {
81
+ "name": "gateway_max_message_size_bytes",
82
+ "required": False,
83
+ "type": "integer",
84
+ "default": 10_000_000, # 10MB
85
+ "description": "Maximum allowed message size in bytes for messages published by the gateway.",
86
+ },
87
+ # --- Default User Identity Configuration ---
88
+ {
89
+ "name": "default_user_identity",
90
+ "required": False,
91
+ "type": "string",
92
+ "description": "Default user identity to use when no user authentication is provided. WARNING: Only use in development environments with trusted access!",
93
+ },
94
+ {
95
+ "name": "force_user_identity",
96
+ "required": False,
97
+ "type": "string",
98
+ "description": "Override any provided user identity with this value. WARNING: Development only! This completely replaces authentication.",
99
+ },
100
+ # --- Identity Service Configuration ---
101
+ {
102
+ "name": "identity_service",
103
+ "required": False,
104
+ "type": "object",
105
+ "default": None,
106
+ "description": "Configuration for the pluggable Identity Service provider.",
107
+ },
108
+ ]
109
+ }
79
110
 
80
111
 
81
112
  class BaseGatewayApp(App):
@@ -83,13 +114,44 @@ class BaseGatewayApp(App):
83
114
  Base class for Gateway applications.
84
115
 
85
116
  Handles common configuration, Solace broker setup, and instantiation
86
- of the gateway-specific component.
117
+ of the gateway-specific component. It also automatically merges its
118
+ base schema with specific schema parameters defined by subclasses.
87
119
  """
88
120
 
89
- # This is now a placeholder. Validation is handled by Pydantic models in subclasses.
90
- app_schema: Dict[str, List[Dict[str, Any]]] = {"config_parameters": []}
121
+ app_schema: Dict[str, List[Dict[str, Any]]] = BASE_GATEWAY_APP_SCHEMA
91
122
  SPECIFIC_APP_SCHEMA_PARAMS_ATTRIBUTE_NAME = "SPECIFIC_APP_SCHEMA_PARAMS"
92
123
 
124
+ def __init_subclass__(cls, **kwargs):
125
+ """
126
+ Automatically merges the base gateway schema with specific schema
127
+ parameters defined in any subclass.
128
+ """
129
+ super().__init_subclass__(**kwargs)
130
+
131
+ specific_params = getattr(
132
+ cls, cls.SPECIFIC_APP_SCHEMA_PARAMS_ATTRIBUTE_NAME, []
133
+ )
134
+
135
+ if not isinstance(specific_params, list):
136
+ log.warning(
137
+ "Class attribute '%s' in %s is not a list. Schema merging might be incorrect.",
138
+ cls.SPECIFIC_APP_SCHEMA_PARAMS_ATTRIBUTE_NAME,
139
+ cls.__name__,
140
+ )
141
+ specific_params = []
142
+
143
+ base_params = BaseGatewayApp.app_schema.get("config_parameters", [])
144
+
145
+ merged_config_parameters = list(base_params)
146
+ merged_config_parameters.extend(specific_params)
147
+
148
+ cls.app_schema = {"config_parameters": merged_config_parameters}
149
+ log.debug(
150
+ "BaseGatewayApp.__init_subclass__ created merged app_schema for %s with %d params.",
151
+ cls.__name__,
152
+ len(merged_config_parameters),
153
+ )
154
+
93
155
  def __init__(self, app_info: Dict[str, Any], **kwargs):
94
156
  """
95
157
  Initializes the BaseGatewayApp.
@@ -111,17 +173,6 @@ class BaseGatewayApp(App):
111
173
  code_config_app_block, yaml_app_config_block
112
174
  )
113
175
 
114
- try:
115
- # Validate the configuration against the base model
116
- validated_config = BaseGatewayAppConfig.model_validate_and_clean(
117
- resolved_app_config_block
118
- )
119
- # Use the validated model's dict representation
120
- resolved_app_config_block = validated_config
121
- except ValidationError as e:
122
- log.error("Base Gateway configuration validation failed:\n%s", e)
123
- raise ValueError(f"Invalid Base Gateway configuration: {e}") from e
124
-
125
176
  self.namespace: str = resolved_app_config_block.get("namespace")
126
177
  if not self.namespace:
127
178
  raise ValueError(
@@ -238,4 +289,4 @@ class BaseGatewayApp(App):
238
289
  Returns:
239
290
  The specific gateway component class (e.g., WebUIBackendComponent).
240
291
  """
241
- pass
292
+ pass
@@ -0,0 +1,342 @@
1
+ """Convert timestamps to epoch milliseconds and align column names
2
+
3
+ Revision ID: f6e7d8c9b0a1
4
+ Revises: b1c2d3e4f5g6
5
+ Create Date: 2025-09-16 16:30:00.000000
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "f6e7d8c9b0a1"
16
+ down_revision: str | None = "b1c2d3e4f5g6"
17
+ branch_labels: str | Sequence[str] | None = None
18
+ depends_on: str | Sequence[str] | None = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Convert datetime columns to epoch milliseconds and rename columns."""
23
+ from sqlalchemy import inspect
24
+ import time
25
+
26
+ bind = op.get_bind()
27
+ inspector = inspect(bind)
28
+ current_time_ms = int(time.time() * 1000)
29
+
30
+ if bind.dialect.name == 'sqlite':
31
+ # SQLite doesn't support ALTER COLUMN, so we need to recreate tables
32
+ _upgrade_sqlite(current_time_ms)
33
+ else:
34
+ # PostgreSQL, MySQL, and other databases support ALTER COLUMN
35
+ _upgrade_standard_sql(current_time_ms)
36
+
37
+
38
+ def _upgrade_sqlite(current_time_ms: int) -> None:
39
+ """Handle SQLite upgrade by recreating tables (SQLite doesn't support dropping columns)."""
40
+
41
+ # 1. Create new sessions table with epoch timestamp columns
42
+ op.create_table(
43
+ 'sessions_new',
44
+ sa.Column('id', sa.String(), nullable=False),
45
+ sa.Column('name', sa.String(), nullable=True),
46
+ sa.Column('user_id', sa.String(), nullable=False),
47
+ sa.Column('agent_id', sa.String(), nullable=True),
48
+ sa.Column('created_time', sa.BigInteger(), nullable=False),
49
+ sa.Column('updated_time', sa.BigInteger(), nullable=False),
50
+ sa.PrimaryKeyConstraint('id')
51
+ )
52
+
53
+ # 2. Copy data from old table with timestamp conversion
54
+ op.execute(f"""
55
+ INSERT INTO sessions_new (id, name, user_id, agent_id, created_time, updated_time)
56
+ SELECT
57
+ id,
58
+ name,
59
+ user_id,
60
+ agent_id,
61
+ COALESCE(CAST(strftime('%s', created_at) * 1000 AS INTEGER), {current_time_ms}) as created_time,
62
+ COALESCE(CAST(strftime('%s', updated_at) * 1000 AS INTEGER), {current_time_ms}) as updated_time
63
+ FROM sessions
64
+ """)
65
+
66
+ # 3. Drop old table and rename new table
67
+ op.drop_table('sessions')
68
+ op.rename_table('sessions_new', 'sessions')
69
+
70
+ # 4. Create new chat_messages table with epoch timestamp column
71
+ op.create_table(
72
+ 'chat_messages_new',
73
+ sa.Column('id', sa.String(), nullable=False),
74
+ sa.Column('session_id', sa.String(), nullable=False),
75
+ sa.Column('message', sa.Text(), nullable=False),
76
+ sa.Column('sender_type', sa.String(), nullable=True),
77
+ sa.Column('sender_name', sa.String(), nullable=True),
78
+ sa.Column('created_time', sa.BigInteger(), nullable=False),
79
+ sa.PrimaryKeyConstraint('id'),
80
+ sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE')
81
+ )
82
+
83
+ # 5. Copy data from old chat_messages table with timestamp conversion
84
+ op.execute(f"""
85
+ INSERT INTO chat_messages_new (id, session_id, message, sender_type, sender_name, created_time)
86
+ SELECT
87
+ id,
88
+ session_id,
89
+ message,
90
+ sender_type,
91
+ sender_name,
92
+ COALESCE(CAST(strftime('%s', created_at) * 1000 AS INTEGER), {current_time_ms}) as created_time
93
+ FROM chat_messages
94
+ """)
95
+
96
+ # 6. Drop old table and rename new table
97
+ op.drop_table('chat_messages')
98
+ op.rename_table('chat_messages_new', 'chat_messages')
99
+
100
+ # 7. Create indexes
101
+ _create_updated_indexes()
102
+
103
+
104
+ def _upgrade_standard_sql(current_time_ms: int) -> None:
105
+ """Handle PostgreSQL/MySQL upgrade using ALTER COLUMN (standard SQL approach)."""
106
+
107
+ # For sessions table
108
+ op.add_column("sessions", sa.Column("created_time", sa.BigInteger(), nullable=True))
109
+ op.add_column("sessions", sa.Column("updated_time", sa.BigInteger(), nullable=True))
110
+
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
+ """)
123
+
124
+ # Set current epoch ms for null values
125
+ op.execute(f"""
126
+ UPDATE sessions
127
+ SET created_time = {current_time_ms}
128
+ WHERE created_time IS NULL
129
+ """)
130
+
131
+ op.execute(f"""
132
+ UPDATE sessions
133
+ SET updated_time = {current_time_ms}
134
+ WHERE updated_time IS NULL
135
+ """)
136
+
137
+ # Make new columns NOT NULL
138
+ op.alter_column("sessions", "created_time", nullable=False)
139
+ op.alter_column("sessions", "updated_time", nullable=False)
140
+
141
+ # Drop old columns
142
+ op.drop_column("sessions", "created_at")
143
+ op.drop_column("sessions", "updated_at")
144
+
145
+ # For chat_messages table
146
+ op.add_column("chat_messages", sa.Column("created_time", sa.BigInteger(), nullable=True))
147
+
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
+ """)
153
+
154
+ op.execute(f"""
155
+ UPDATE chat_messages
156
+ SET created_time = {current_time_ms}
157
+ WHERE created_time IS NULL
158
+ """)
159
+
160
+ op.alter_column("chat_messages", "created_time", nullable=False)
161
+ op.drop_column("chat_messages", "created_at")
162
+
163
+ # Add indexes - this will be called after either upgrade path
164
+ _create_updated_indexes()
165
+
166
+
167
+ def _create_updated_indexes() -> None:
168
+ """Create indexes on new timestamp columns."""
169
+
170
+ # For SQLite, indexes are recreated when tables are recreated
171
+ # For other databases, we need to manage index transitions
172
+
173
+ bind = op.get_bind()
174
+
175
+ if bind.dialect.name == 'sqlite':
176
+ # SQLite: Create all indexes fresh (old ones were dropped with table recreation)
177
+ _create_indexes_safe("ix_sessions_user_id", "sessions", ["user_id"])
178
+ _create_indexes_safe("ix_sessions_agent_id", "sessions", ["agent_id"])
179
+ _create_indexes_safe("ix_sessions_updated_time", "sessions", ["updated_time"])
180
+ _create_indexes_safe("ix_sessions_user_id_updated_time", "sessions", ["user_id", "updated_time"])
181
+
182
+ _create_indexes_safe("ix_chat_messages_session_id", "chat_messages", ["session_id"])
183
+ _create_indexes_safe("ix_chat_messages_created_time", "chat_messages", ["created_time"])
184
+ _create_indexes_safe("ix_chat_messages_session_id_created_time", "chat_messages", ["session_id", "created_time"])
185
+
186
+ else:
187
+ # PostgreSQL/MySQL: Drop old indexes and create new ones
188
+ _drop_index_safe("ix_sessions_updated_at", "sessions")
189
+ _drop_index_safe("ix_sessions_user_id_updated_at", "sessions")
190
+ _drop_index_safe("ix_chat_messages_created_at", "chat_messages")
191
+ _drop_index_safe("ix_chat_messages_session_id_created_at", "chat_messages")
192
+
193
+ # Create new indexes (user_id and agent_id indexes already exist)
194
+ _create_indexes_safe("ix_sessions_updated_time", "sessions", ["updated_time"])
195
+ _create_indexes_safe("ix_sessions_user_id_updated_time", "sessions", ["user_id", "updated_time"])
196
+ _create_indexes_safe("ix_chat_messages_created_time", "chat_messages", ["created_time"])
197
+ _create_indexes_safe("ix_chat_messages_session_id_created_time", "chat_messages", ["session_id", "created_time"])
198
+
199
+
200
+ def _create_indexes_safe(index_name: str, table_name: str, columns: list) -> None:
201
+ """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
206
+
207
+
208
+ def _drop_index_safe(index_name: str, table_name: str) -> None:
209
+ """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
214
+
215
+
216
+ def downgrade() -> None:
217
+ """Convert back to datetime columns with original names."""
218
+ bind = op.get_bind()
219
+
220
+ if bind.dialect.name == 'sqlite':
221
+ _downgrade_sqlite()
222
+ else:
223
+ _downgrade_standard_sql()
224
+
225
+
226
+ def _downgrade_sqlite() -> None:
227
+ """Handle SQLite downgrade by recreating tables with original datetime columns."""
228
+
229
+ # 1. Create sessions table with original datetime columns
230
+ op.create_table(
231
+ 'sessions_old',
232
+ sa.Column('id', sa.String(), nullable=False),
233
+ sa.Column('name', sa.String(), nullable=True),
234
+ sa.Column('user_id', sa.String(), nullable=False),
235
+ sa.Column('agent_id', sa.String(), nullable=True),
236
+ sa.Column('created_at', sa.DateTime(), nullable=True),
237
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
238
+ sa.PrimaryKeyConstraint('id')
239
+ )
240
+
241
+ # 2. Copy data back with timestamp conversion
242
+ op.execute("""
243
+ INSERT INTO sessions_old (id, name, user_id, agent_id, created_at, updated_at)
244
+ SELECT
245
+ id,
246
+ name,
247
+ user_id,
248
+ agent_id,
249
+ datetime(created_time / 1000.0, 'unixepoch') as created_at,
250
+ datetime(updated_time / 1000.0, 'unixepoch') as updated_at
251
+ FROM sessions
252
+ """)
253
+
254
+ # 3. Drop new table and rename old table
255
+ op.drop_table('sessions')
256
+ op.rename_table('sessions_old', 'sessions')
257
+
258
+ # 4. Create chat_messages table with original datetime column
259
+ op.create_table(
260
+ 'chat_messages_old',
261
+ sa.Column('id', sa.String(), nullable=False),
262
+ sa.Column('session_id', sa.String(), nullable=False),
263
+ sa.Column('message', sa.Text(), nullable=False),
264
+ sa.Column('sender_type', sa.String(), nullable=True),
265
+ sa.Column('sender_name', sa.String(), nullable=True),
266
+ sa.Column('created_at', sa.DateTime(), nullable=True),
267
+ sa.PrimaryKeyConstraint('id'),
268
+ sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], ondelete='CASCADE')
269
+ )
270
+
271
+ # 5. Copy data back with timestamp conversion
272
+ op.execute("""
273
+ INSERT INTO chat_messages_old (id, session_id, message, sender_type, sender_name, created_at)
274
+ SELECT
275
+ id,
276
+ session_id,
277
+ message,
278
+ sender_type,
279
+ sender_name,
280
+ datetime(created_time / 1000.0, 'unixepoch') as created_at
281
+ FROM chat_messages
282
+ """)
283
+
284
+ # 6. Drop new table and rename old table
285
+ op.drop_table('chat_messages')
286
+ op.rename_table('chat_messages_old', 'chat_messages')
287
+
288
+ # 7. Recreate original indexes
289
+ _create_indexes_safe("ix_sessions_user_id", "sessions", ["user_id"])
290
+ _create_indexes_safe("ix_sessions_agent_id", "sessions", ["agent_id"])
291
+ _create_indexes_safe("ix_sessions_updated_at", "sessions", ["updated_at"])
292
+ _create_indexes_safe("ix_sessions_user_id_updated_at", "sessions", ["user_id", "updated_at"])
293
+ _create_indexes_safe("ix_chat_messages_session_id", "chat_messages", ["session_id"])
294
+ _create_indexes_safe("ix_chat_messages_created_at", "chat_messages", ["created_at"])
295
+ _create_indexes_safe("ix_chat_messages_session_id_created_at", "chat_messages", ["session_id", "created_at"])
296
+
297
+
298
+ def _downgrade_standard_sql() -> None:
299
+ """Handle PostgreSQL/MySQL downgrade using ALTER COLUMN."""
300
+
301
+ # Drop indexes on new columns
302
+ _drop_index_safe("ix_chat_messages_session_id_created_time", "chat_messages")
303
+ _drop_index_safe("ix_chat_messages_created_time", "chat_messages")
304
+ _drop_index_safe("ix_sessions_user_id_updated_time", "sessions")
305
+ _drop_index_safe("ix_sessions_updated_time", "sessions")
306
+
307
+ # For sessions table: convert back to datetime columns
308
+ op.add_column("sessions", sa.Column("created_at", sa.DateTime(), nullable=True))
309
+ op.add_column("sessions", sa.Column("updated_at", sa.DateTime(), nullable=True))
310
+
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
+ """)
323
+
324
+ op.drop_column("sessions", "created_time")
325
+ op.drop_column("sessions", "updated_time")
326
+
327
+ # For chat_messages table: convert back to datetime column
328
+ op.add_column("chat_messages", sa.Column("created_at", sa.DateTime(), nullable=True))
329
+
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
+ """)
335
+
336
+ op.drop_column("chat_messages", "created_time")
337
+
338
+ # Recreate the old indexes
339
+ _create_indexes_safe("ix_sessions_updated_at", "sessions", ["updated_at"])
340
+ _create_indexes_safe("ix_sessions_user_id_updated_at", "sessions", ["user_id", "updated_at"])
341
+ _create_indexes_safe("ix_chat_messages_created_at", "chat_messages", ["created_at"])
342
+ _create_indexes_safe("ix_chat_messages_session_id_created_at", "chat_messages", ["session_id", "created_at"])
@@ -5,13 +5,11 @@
5
5
  # this is typically a path given in POSIX (e.g. forward slashes)
6
6
  # format, relative to the token %(here)s which refers to the location of this
7
7
  # ini file
8
- script_location = alembic
8
+ script_location = %(here)s/alembic
9
9
 
10
- # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
11
- # Uncomment the line below if you want the files to be prepended with date and time
12
- # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
13
- # for all available tokens
14
- # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
10
+ # Template used to generate migration file names
11
+ # Format: YYYYMMDD_revision_description
12
+ file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s_%%(slug)s
15
13
 
16
14
  # sys.path path, will be prepended to sys.path if present.
17
15
  # defaults to the current working directory. for multiple paths, the path separator