solace-agent-mesh 1.5.1__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (180) hide show
  1. solace_agent_mesh/agent/adk/callbacks.py +0 -5
  2. solace_agent_mesh/agent/adk/models/lite_llm.py +123 -8
  3. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +245 -0
  4. solace_agent_mesh/agent/protocol/event_handlers.py +40 -1
  5. solace_agent_mesh/agent/proxies/__init__.py +0 -0
  6. solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
  7. solace_agent_mesh/agent/proxies/a2a/app.py +55 -0
  8. solace_agent_mesh/agent/proxies/a2a/component.py +1115 -0
  9. solace_agent_mesh/agent/proxies/a2a/config.py +140 -0
  10. solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
  11. solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
  12. solace_agent_mesh/agent/proxies/base/app.py +99 -0
  13. solace_agent_mesh/agent/proxies/base/component.py +619 -0
  14. solace_agent_mesh/agent/proxies/base/config.py +85 -0
  15. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +17 -0
  16. solace_agent_mesh/agent/sac/app.py +9 -3
  17. solace_agent_mesh/agent/sac/component.py +160 -8
  18. solace_agent_mesh/agent/tools/audio_tools.py +125 -8
  19. solace_agent_mesh/agent/tools/web_tools.py +10 -5
  20. solace_agent_mesh/agent/utils/artifact_helpers.py +141 -3
  21. solace_agent_mesh/assets/docs/404.html +3 -3
  22. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +1 -0
  23. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
  28. solace_agent_mesh/assets/docs/assets/js/{ad71b5ed.60668e9e.js → ad71b5ed.af3ecfd1.js} +1 -1
  29. solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
  30. solace_agent_mesh/assets/docs/assets/js/{da0b5bad.9d369087.js → da0b5bad.d08a9466.js} +1 -1
  31. solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
  32. solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +1 -0
  33. solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +1 -0
  34. solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +1 -0
  35. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +1 -0
  36. solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js → main.20feee82.js} +2 -2
  37. solace_agent_mesh/assets/docs/assets/js/runtime~main.0d198646.js +1 -0
  38. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +15 -4
  39. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
  40. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
  44. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
  45. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +262 -0
  50. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +3 -3
  51. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +31 -3
  52. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +3 -3
  53. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +3 -3
  54. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
  55. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +5 -5
  56. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
  57. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
  58. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
  59. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +6 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
  61. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +5 -5
  64. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
  65. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
  66. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
  67. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
  68. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
  69. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
  70. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
  71. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
  72. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
  73. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
  74. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
  75. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
  76. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  77. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
  78. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +6 -5
  79. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
  80. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
  81. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +100 -3
  82. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
  83. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  84. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
  85. solace_agent_mesh/assets/docs/lunr-index-1761165361160.json +1 -0
  86. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  87. solace_agent_mesh/assets/docs/search-doc-1761165361160.json +1 -0
  88. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  89. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  90. solace_agent_mesh/cli/__init__.py +1 -1
  91. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +2 -69
  92. solace_agent_mesh/cli/commands/eval_cmd.py +11 -49
  93. solace_agent_mesh/cli/commands/init_cmd/__init__.py +0 -5
  94. solace_agent_mesh/cli/commands/init_cmd/env_step.py +10 -12
  95. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +9 -61
  96. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +9 -49
  97. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +1 -2
  98. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DwrxZE0E.js → authCallback-BTf6dqwp.js} +1 -1
  99. solace_agent_mesh/client/webui/frontend/static/assets/{client-DarGQzyw.js → client-CaY59VuC.js} +1 -1
  100. solace_agent_mesh/client/webui/frontend/static/assets/main-BGTaW0uv.js +342 -0
  101. solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +1 -0
  102. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BKIeiHj_.js → vendor-BEmvJSYz.js} +1 -1
  103. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  104. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  105. solace_agent_mesh/common/a2a/__init__.py +24 -0
  106. solace_agent_mesh/common/a2a/artifact.py +39 -0
  107. solace_agent_mesh/common/a2a/events.py +29 -0
  108. solace_agent_mesh/common/a2a/message.py +68 -0
  109. solace_agent_mesh/common/a2a/protocol.py +73 -1
  110. solace_agent_mesh/common/agent_registry.py +83 -3
  111. solace_agent_mesh/common/constants.py +3 -1
  112. solace_agent_mesh/common/utils/pydantic_utils.py +12 -0
  113. solace_agent_mesh/config_portal/backend/common.py +1 -1
  114. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +98 -0
  115. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-44d62be6.js → manifest-61038fc6.js} +1 -1
  116. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  117. solace_agent_mesh/evaluation/evaluator.py +128 -104
  118. solace_agent_mesh/evaluation/message_organizer.py +116 -110
  119. solace_agent_mesh/evaluation/report_data_processor.py +84 -86
  120. solace_agent_mesh/evaluation/report_generator.py +73 -79
  121. solace_agent_mesh/evaluation/run.py +421 -235
  122. solace_agent_mesh/evaluation/shared/__init__.py +92 -0
  123. solace_agent_mesh/evaluation/shared/constants.py +47 -0
  124. solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
  125. solace_agent_mesh/evaluation/shared/helpers.py +35 -0
  126. solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
  127. solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
  128. solace_agent_mesh/evaluation/subscriber.py +111 -232
  129. solace_agent_mesh/evaluation/summary_builder.py +227 -117
  130. solace_agent_mesh/gateway/base/app.py +1 -1
  131. solace_agent_mesh/gateway/base/component.py +8 -1
  132. solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
  133. solace_agent_mesh/gateway/http_sse/component.py +98 -2
  134. solace_agent_mesh/gateway/http_sse/dependencies.py +4 -4
  135. solace_agent_mesh/gateway/http_sse/main.py +2 -1
  136. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +12 -13
  137. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +15 -18
  138. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +25 -18
  139. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +30 -26
  140. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +35 -44
  141. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +4 -3
  142. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +95 -203
  143. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +4 -3
  144. solace_agent_mesh/gateway/http_sse/routers/sessions.py +2 -2
  145. solace_agent_mesh/gateway/http_sse/routers/tasks.py +33 -41
  146. solace_agent_mesh/gateway/http_sse/routers/visualization.py +17 -11
  147. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +4 -4
  148. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +51 -43
  149. solace_agent_mesh/gateway/http_sse/services/session_service.py +20 -20
  150. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +8 -8
  151. solace_agent_mesh/gateway/http_sse/shared/base_repository.py +45 -71
  152. solace_agent_mesh/gateway/http_sse/shared/types.py +0 -18
  153. solace_agent_mesh/templates/gateway_config_template.yaml +0 -5
  154. solace_agent_mesh/templates/logging_config_template.ini +10 -6
  155. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +0 -3
  156. solace_agent_mesh/templates/shared_config.yaml +40 -0
  157. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/METADATA +47 -21
  158. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/RECORD +162 -141
  159. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.e49689dd.js +0 -1
  160. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.39d5851d.js +0 -1
  161. solace_agent_mesh/assets/docs/assets/js/71da7b71.804d6567.js +0 -1
  162. solace_agent_mesh/assets/docs/assets/js/77cf947d.64c9bd6c.js +0 -1
  163. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.dd810042.js +0 -1
  164. solace_agent_mesh/assets/docs/assets/js/db924877.cbc66f02.js +0 -1
  165. solace_agent_mesh/assets/docs/assets/js/de915948.139b4b9c.js +0 -1
  166. solace_agent_mesh/assets/docs/assets/js/e6f9706b.582a78ca.js +0 -1
  167. solace_agent_mesh/assets/docs/assets/js/f284c35a.5766a13d.js +0 -1
  168. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.9c0297a6.js +0 -1
  169. solace_agent_mesh/assets/docs/assets/js/runtime~main.18dc45dd.js +0 -1
  170. solace_agent_mesh/assets/docs/lunr-index-1760121512891.json +0 -1
  171. solace_agent_mesh/assets/docs/search-doc-1760121512891.json +0 -1
  172. solace_agent_mesh/client/webui/frontend/static/assets/main-2nd1gbaH.js +0 -339
  173. solace_agent_mesh/client/webui/frontend/static/assets/main-DoKXctCM.css +0 -1
  174. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-BNuqpWDc.js +0 -98
  175. solace_agent_mesh/evaluation/config_loader.py +0 -657
  176. solace_agent_mesh/evaluation/test_case_loader.py +0 -714
  177. /solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js.LICENSE.txt → main.20feee82.js.LICENSE.txt} +0 -0
  178. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/WHEEL +0 -0
  179. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/entry_points.txt +0 -0
  180. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -45,32 +45,32 @@ from ..dependencies import (
45
45
  get_sac_component,
46
46
  get_session_validator,
47
47
  get_shared_artifact_service,
48
- get_user_config,
49
48
  get_user_id,
50
49
  get_session_manager,
51
50
  get_session_business_service_optional,
52
51
  get_db_optional,
53
52
  )
54
53
 
55
- if TYPE_CHECKING:
56
- from ....gateway.http_sse.component import WebUIBackendComponent
57
54
 
58
55
  from ..session_manager import SessionManager
59
56
  from ..services.session_service import SessionService
60
57
  from sqlalchemy.orm import Session
61
58
 
62
59
  from ....agent.utils.artifact_helpers import (
63
- DEFAULT_SCHEMA_MAX_KEYS,
64
- format_artifact_uri,
65
60
  get_artifact_info_list,
66
61
  load_artifact_content_or_metadata,
67
- save_artifact_with_metadata,
62
+ process_artifact_upload,
68
63
  )
69
64
 
65
+ if TYPE_CHECKING:
66
+ from ....gateway.http_sse.component import WebUIBackendComponent
67
+
70
68
  log = logging.getLogger(__name__)
71
69
 
70
+
72
71
  class ArtifactUploadResponse(BaseModel):
73
72
  """Response model for artifact upload with camelCase fields."""
73
+
74
74
  uri: str
75
75
  session_id: str = Field(..., alias="sessionId")
76
76
  filename: str
@@ -95,7 +95,11 @@ router = APIRouter()
95
95
  async def upload_artifact_with_session(
96
96
  request: FastAPIRequest,
97
97
  upload_file: UploadFile = File(..., description="The file content to upload"),
98
- sessionId: str | None = Form(None, description="Session ID (null/empty to create new session)", alias="sessionId"),
98
+ sessionId: str | None = Form(
99
+ None,
100
+ description="Session ID (null/empty to create new session)",
101
+ alias="sessionId",
102
+ ),
99
103
  filename: str = Form(..., description="The name of the artifact to create/update"),
100
104
  metadata_json: str | None = Form(
101
105
  None, description="JSON string of artifact metadata (e.g., description, source)"
@@ -106,7 +110,9 @@ async def upload_artifact_with_session(
106
110
  component: "WebUIBackendComponent" = Depends(get_sac_component),
107
111
  user_config: dict = Depends(ValidatedUserConfig(["tool:artifact:create"])),
108
112
  session_manager: SessionManager = Depends(get_session_manager),
109
- session_service: SessionService | None = Depends(get_session_business_service_optional),
113
+ session_service: SessionService | None = Depends(
114
+ get_session_business_service_optional
115
+ ),
110
116
  db: Session | None = Depends(get_db_optional),
111
117
  ):
112
118
  """
@@ -129,7 +135,11 @@ async def upload_artifact_with_session(
129
135
  else:
130
136
  # Create new session when no sessionId provided (like chat does for new conversations)
131
137
  effective_session_id = session_manager.create_new_session_id(request)
132
- log.info("%sCreated new session for file upload: %s", log_prefix, effective_session_id)
138
+ log.info(
139
+ "%sCreated new session for file upload: %s",
140
+ log_prefix,
141
+ effective_session_id,
142
+ )
133
143
 
134
144
  # Persist session in database if persistence is available (matching chat pattern)
135
145
  if session_service and db:
@@ -139,17 +149,27 @@ async def upload_artifact_with_session(
139
149
  user_id=user_id,
140
150
  session_id=effective_session_id,
141
151
  agent_id=None, # Will be determined when first message is sent
142
- name=None, # Will be set when first message is sent
152
+ name=None, # Will be set when first message is sent
143
153
  )
144
154
  db.commit()
145
- log.info("%sSession created and committed to database: %s", log_prefix, effective_session_id)
155
+ log.info(
156
+ "%sSession created and committed to database: %s",
157
+ log_prefix,
158
+ effective_session_id,
159
+ )
146
160
  except Exception as session_error:
147
161
  db.rollback()
148
- log.warning("%sSession persistence failed, continuing with in-memory session: %s",
149
- log_prefix, session_error)
162
+ log.warning(
163
+ "%sSession persistence failed, continuing with in-memory session: %s",
164
+ log_prefix,
165
+ session_error,
166
+ )
150
167
  else:
151
- log.debug("%sNo persistence available - using in-memory session: %s",
152
- log_prefix, effective_session_id)
168
+ log.debug(
169
+ "%sNo persistence available - using in-memory session: %s",
170
+ log_prefix,
171
+ effective_session_id,
172
+ )
153
173
 
154
174
  # Validate inputs
155
175
  if not filename or not filename.strip():
@@ -174,56 +194,83 @@ async def upload_artifact_with_session(
174
194
 
175
195
  # Validate session (now that we have an effective_session_id)
176
196
  if not validate_session(effective_session_id, user_id):
177
- log.warning("%sSession validation failed for session: %s", log_prefix, effective_session_id)
197
+ log.warning(
198
+ "%sSession validation failed for session: %s",
199
+ log_prefix,
200
+ effective_session_id,
201
+ )
178
202
  raise HTTPException(
179
203
  status_code=status.HTTP_403_FORBIDDEN,
180
204
  detail="Invalid session or insufficient permissions.",
181
205
  )
182
206
 
183
- log.info("%sUploading file '%s' to session '%s'", log_prefix, filename.strip(), effective_session_id)
207
+ log.info(
208
+ "%sUploading file '%s' to session '%s'",
209
+ log_prefix,
210
+ filename.strip(),
211
+ effective_session_id,
212
+ )
184
213
 
185
214
  try:
186
215
  # Read and validate file content
187
216
  content_bytes = await upload_file.read()
188
- if not content_bytes:
189
- raise HTTPException(
190
- status_code=status.HTTP_400_BAD_REQUEST,
191
- detail="File is empty.",
192
- )
193
217
 
194
218
  mime_type = upload_file.content_type or "application/octet-stream"
195
219
  filename_clean = filename.strip()
196
220
 
197
- log.debug("%sProcessing file: %s (%d bytes, %s)", log_prefix, filename_clean, len(content_bytes), mime_type)
198
-
199
- # Parse and validate metadata
200
- metadata = {}
201
- if metadata_json and metadata_json.strip():
202
- try:
203
- metadata = json.loads(metadata_json.strip())
204
- if not isinstance(metadata, dict):
205
- raise ValueError("Metadata must be a JSON object")
206
- except (json.JSONDecodeError, ValueError) as e:
207
- log.warning("%sInvalid metadata JSON: %s", log_prefix, e)
208
- raise HTTPException(
209
- status_code=status.HTTP_400_BAD_REQUEST,
210
- detail=f"Invalid JSON in metadata field: {str(e)}",
211
- )
212
-
213
- app_name = component.get_config("name", "A2A_WebUI_App")
221
+ log.debug(
222
+ "%sProcessing file: %s (%d bytes, %s)",
223
+ log_prefix,
224
+ filename_clean,
225
+ len(content_bytes),
226
+ mime_type,
227
+ )
214
228
 
215
- # Store the artifact using the service
216
- artifact_uri = await artifact_service.store(
217
- app_name=app_name,
229
+ # Use the common upload helper
230
+ upload_result = await process_artifact_upload(
231
+ artifact_service=artifact_service,
232
+ component=component,
218
233
  user_id=user_id,
219
234
  session_id=effective_session_id,
220
235
  filename=filename_clean,
221
236
  content_bytes=content_bytes,
222
237
  mime_type=mime_type,
223
- metadata=metadata,
238
+ metadata_json=metadata_json,
239
+ log_prefix=log_prefix,
240
+ )
241
+
242
+ if upload_result["status"] != "success":
243
+ error_msg = upload_result.get("message", "Failed to upload artifact")
244
+ error_type = upload_result.get("error", "unknown")
245
+
246
+ if error_type in ["invalid_filename", "empty_file"]:
247
+ status_code = status.HTTP_400_BAD_REQUEST
248
+ else:
249
+ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
250
+
251
+ log.error("%s%s", log_prefix, error_msg)
252
+ raise HTTPException(status_code=status_code, detail=error_msg)
253
+
254
+ artifact_uri = upload_result["artifact_uri"]
255
+ saved_version = upload_result["version"]
256
+
257
+ log.info(
258
+ "%sArtifact stored successfully: %s (%d bytes), version: %s",
259
+ log_prefix,
260
+ artifact_uri,
261
+ len(content_bytes),
262
+ saved_version,
224
263
  )
225
264
 
226
- log.info("%sArtifact stored successfully: %s (%d bytes)", log_prefix, artifact_uri, len(content_bytes))
265
+ # Get metadata from upload result (it was already parsed and validated)
266
+ metadata_dict = {}
267
+ if metadata_json and metadata_json.strip():
268
+ try:
269
+ metadata_dict = json.loads(metadata_json.strip())
270
+ if not isinstance(metadata_dict, dict):
271
+ metadata_dict = {}
272
+ except json.JSONDecodeError:
273
+ metadata_dict = {}
227
274
 
228
275
  # Return standardized response using Pydantic model (ensures camelCase conversion)
229
276
  return ArtifactUploadResponse(
@@ -232,8 +279,10 @@ async def upload_artifact_with_session(
232
279
  filename=filename_clean,
233
280
  size=len(content_bytes),
234
281
  mime_type=mime_type, # Will be returned as "mimeType" due to alias
235
- metadata=metadata,
236
- created_at=datetime.now(timezone.utc).isoformat(), # Will be returned as "createdAt" due to alias
282
+ metadata=metadata_dict,
283
+ created_at=datetime.now(
284
+ timezone.utc
285
+ ).isoformat(), # Will be returned as "createdAt" due to alias
237
286
  )
238
287
 
239
288
  except HTTPException:
@@ -785,7 +834,6 @@ async def get_artifact_by_uri(
785
834
  version,
786
835
  )
787
836
 
788
-
789
837
  log.info(
790
838
  "%s User '%s' authorized to access artifact URI.",
791
839
  log_id_prefix,
@@ -828,162 +876,6 @@ async def get_artifact_by_uri(
828
876
  )
829
877
 
830
878
 
831
- @router.post(
832
- "/{session_id}/{filename}",
833
- status_code=status.HTTP_201_CREATED,
834
- response_model=dict[str, Any],
835
- summary="Upload Artifact (Create/Update Version with Metadata)",
836
- description="Uploads file content and optional metadata to create or update an artifact version.",
837
- )
838
- async def upload_artifact(
839
- session_id: str = Path(
840
- ..., title="Session ID", description="The session ID to upload artifacts to"
841
- ),
842
- filename: str = Path(
843
- ..., title="Filename", description="The name of the artifact to create/update"
844
- ),
845
- upload_file: UploadFile = File(..., description="The file content to upload"),
846
- metadata_json: str | None = Form(
847
- None, description="JSON string of artifact metadata (e.g., description, source)"
848
- ),
849
- artifact_service: BaseArtifactService = Depends(get_shared_artifact_service),
850
- user_id: str = Depends(get_user_id),
851
- validate_session: Callable[[str, str], bool] = Depends(get_session_validator),
852
- component: "WebUIBackendComponent" = Depends(get_sac_component),
853
- user_config: dict = Depends(ValidatedUserConfig(["tool:artifact:create"])),
854
- ):
855
- """
856
- Uploads a file to create a new version of the specified artifact
857
- associated with the current user and session ID. Also saves associated metadata.
858
- """
859
- log_prefix = (
860
- f"[ArtifactRouter:Post:{filename}] User={user_id}, Session={session_id} -"
861
- )
862
- log.info(
863
- "%s Request received. Upload filename: '%s', content type: %s",
864
- log_prefix,
865
- upload_file.filename,
866
- upload_file.content_type,
867
- )
868
-
869
- # Validate session exists and belongs to user
870
- if not validate_session(session_id, user_id):
871
- log.warning("%s Session validation failed or access denied.", log_prefix)
872
- raise HTTPException(
873
- status_code=status.HTTP_404_NOT_FOUND,
874
- detail="Session not found or access denied.",
875
- )
876
-
877
- if artifact_service is None:
878
- log.error("%s Artifact service is not configured or available.", log_prefix)
879
- raise HTTPException(
880
- status_code=status.HTTP_501_NOT_IMPLEMENTED,
881
- detail="Artifact service is not configured.",
882
- )
883
-
884
- try:
885
- content_bytes = await upload_file.read()
886
- if not content_bytes:
887
- log.warning("%s Uploaded file is empty.", log_prefix)
888
- raise HTTPException(
889
- status_code=status.HTTP_400_BAD_REQUEST,
890
- detail="Uploaded file cannot be empty.",
891
- )
892
-
893
- mime_type = upload_file.content_type or "application/octet-stream"
894
-
895
- parsed_metadata = {}
896
- if metadata_json:
897
- try:
898
- parsed_metadata = json.loads(metadata_json)
899
- if not isinstance(parsed_metadata, dict):
900
- log.warning(
901
- "%s Metadata JSON did not parse to a dictionary. Ignoring.",
902
- log_prefix,
903
- )
904
- parsed_metadata = {}
905
- except json.JSONDecodeError as json_err:
906
- log.warning(
907
- "%s Failed to parse metadata_json: %s. Proceeding without it.",
908
- log_prefix,
909
- json_err,
910
- )
911
-
912
- app_name = component.get_config("name", "A2A_WebUI_App")
913
- current_timestamp = datetime.now(timezone.utc)
914
-
915
- save_result = await save_artifact_with_metadata(
916
- artifact_service=artifact_service,
917
- app_name=app_name,
918
- user_id=user_id,
919
- session_id=session_id,
920
- filename=filename,
921
- content_bytes=content_bytes,
922
- mime_type=mime_type,
923
- metadata_dict=parsed_metadata,
924
- timestamp=current_timestamp,
925
- schema_max_keys=component.get_config(
926
- "schema_max_keys", DEFAULT_SCHEMA_MAX_KEYS
927
- ),
928
- )
929
-
930
- if save_result["status"] == "success":
931
- log.info(
932
- "%s Artifact and metadata processing completed. Data version: %s, Metadata version: %s. Message: %s",
933
- log_prefix,
934
- save_result.get("data_version"),
935
- save_result.get("metadata_version"),
936
- save_result.get("message"),
937
- )
938
- saved_version = save_result.get("data_version")
939
- artifact_uri = format_artifact_uri(
940
- app_name=app_name,
941
- user_id=user_id,
942
- session_id=session_id,
943
- filename=filename,
944
- version=saved_version,
945
- )
946
- log.info(
947
- "%s Successfully saved artifact. Returning URI: %s",
948
- log_prefix,
949
- artifact_uri,
950
- )
951
- return {
952
- "filename": filename,
953
- "data_version": saved_version,
954
- "metadata_version": save_result.get("metadata_version"),
955
- "mime_type": mime_type,
956
- "size": len(content_bytes),
957
- "message": save_result.get("message"),
958
- "status": save_result["status"],
959
- "uri": artifact_uri,
960
- }
961
- else:
962
- log.error(
963
- "%s Failed to save artifact and metadata: %s",
964
- log_prefix,
965
- save_result.get("message"),
966
- )
967
- raise HTTPException(
968
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
969
- detail=save_result.get(
970
- "message", "Failed to save artifact with metadata."
971
- ),
972
- )
973
-
974
- except HTTPException:
975
- raise
976
- except Exception as e:
977
- log.exception("%s Error saving artifact: %s", log_prefix, e)
978
- raise HTTPException(
979
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
980
- detail=f"Failed to save artifact: {str(e)}",
981
- )
982
- finally:
983
- await upload_file.close()
984
- log.debug("%s Upload file closed.", log_prefix)
985
-
986
-
987
879
  @router.delete(
988
880
  "/{session_id}/{filename}",
989
881
  status_code=status.HTTP_204_NO_CONTENT,
@@ -4,7 +4,8 @@ Session-related response DTOs.
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field
6
6
 
7
- from ....shared.types import PaginationInfo, SessionId, UserId
7
+ from ....shared.pagination import PaginationMeta
8
+ from ....shared.types import SessionId, UserId
8
9
  from .base_responses import BaseTimestampResponse
9
10
 
10
11
 
@@ -20,10 +21,10 @@ class SessionResponse(BaseTimestampResponse):
20
21
 
21
22
 
22
23
  class SessionListResponse(BaseModel):
23
- """Response DTO for a list of sessions."""
24
+ """Response DTO for a list of sessions (legacy - use PaginatedResponse instead)."""
24
25
 
25
26
  model_config = ConfigDict(populate_by_name=True)
26
27
 
27
28
  sessions: list[SessionResponse]
28
- pagination: PaginationInfo | None = None
29
+ pagination: PaginationMeta | None = None
29
30
  total_count: int = Field(alias="totalCount")
@@ -149,8 +149,8 @@ async def save_task(
149
149
  # Check if task already exists to determine status code
150
150
  from ..repository.chat_task_repository import ChatTaskRepository
151
151
 
152
- task_repo = ChatTaskRepository(db)
153
- existing_task = task_repo.find_by_id(request.task_id, user_id)
152
+ task_repo = ChatTaskRepository()
153
+ existing_task = task_repo.find_by_id(db, request.task_id, user_id)
154
154
  is_update = existing_task is not None
155
155
 
156
156
  # Save the task - pass strings directly
@@ -3,50 +3,40 @@ API Router for submitting and managing tasks to agents.
3
3
  """
4
4
 
5
5
  import logging
6
- import yaml
7
6
  from datetime import datetime
8
- from fastapi import (
9
- APIRouter,
10
- Depends,
11
- HTTPException,
12
- Request as FastAPIRequest,
13
- Response,
14
- status,
15
- )
16
- from fastapi.exceptions import RequestValidationError
17
- from typing import List, Optional, Union
18
-
19
-
20
- from ....gateway.http_sse.session_manager import SessionManager
21
- from ....gateway.http_sse.services.task_service import TaskService
22
- from ....gateway.http_sse.services.session_service import SessionService
23
- from ....gateway.http_sse.repository.interfaces import ITaskRepository
24
- from ....gateway.http_sse.repository.entities import Task
25
- from ....gateway.http_sse.shared.types import PaginationParams, UserId
26
- from ..utils.stim_utils import create_stim_from_task_data
7
+ from typing import TYPE_CHECKING
27
8
 
9
+ import yaml
28
10
  from a2a.types import (
29
11
  CancelTaskRequest,
30
12
  SendMessageRequest,
31
- SendStreamingMessageRequest,
32
13
  SendMessageSuccessResponse,
14
+ SendStreamingMessageRequest,
33
15
  SendStreamingMessageSuccessResponse,
34
16
  )
35
- from ....common import a2a
17
+ from fastapi import APIRouter, Depends, HTTPException, Response, status
18
+ from fastapi import Request as FastAPIRequest
19
+ from sqlalchemy.orm import Session as DBSession
36
20
 
21
+ from ....common import a2a
37
22
  from ....gateway.http_sse.dependencies import (
38
- get_session_manager,
23
+ get_db,
39
24
  get_sac_component,
40
- get_task_service,
41
25
  get_session_business_service,
26
+ get_session_manager,
42
27
  get_task_repository,
43
- get_user_id,
28
+ get_task_service,
44
29
  get_user_config,
45
- get_session_business_service,
30
+ get_user_id,
46
31
  )
32
+ from ....gateway.http_sse.repository.entities import Task
33
+ from ....gateway.http_sse.repository.interfaces import ITaskRepository
47
34
  from ....gateway.http_sse.services.session_service import SessionService
48
-
49
- from typing import TYPE_CHECKING
35
+ from ....gateway.http_sse.services.task_service import TaskService
36
+ from ....gateway.http_sse.session_manager import SessionManager
37
+ from ....gateway.http_sse.shared.pagination import PaginationParams
38
+ from ....gateway.http_sse.shared.types import UserId
39
+ from ..utils.stim_utils import create_stim_from_task_data
50
40
 
51
41
  if TYPE_CHECKING:
52
42
  from ....gateway.http_sse.component import WebUIBackendComponent
@@ -55,9 +45,10 @@ router = APIRouter()
55
45
 
56
46
  log = logging.getLogger(__name__)
57
47
 
48
+
58
49
  async def _submit_task(
59
50
  request: FastAPIRequest,
60
- payload: Union[SendMessageRequest, SendStreamingMessageRequest],
51
+ payload: SendMessageRequest | SendStreamingMessageRequest,
61
52
  session_manager: SessionManager,
62
53
  component: "WebUIBackendComponent",
63
54
  is_streaming: bool,
@@ -203,25 +194,25 @@ async def _submit_task(
203
194
  )
204
195
 
205
196
 
206
- @router.get("/tasks", response_model=List[Task], tags=["Tasks"])
197
+ @router.get("/tasks", response_model=list[Task], tags=["Tasks"])
207
198
  async def search_tasks(
208
199
  request: FastAPIRequest,
209
- start_date: Optional[str] = None,
210
- end_date: Optional[str] = None,
211
- search: Optional[str] = None,
200
+ start_date: str | None = None,
201
+ end_date: str | None = None,
212
202
  page: int = 1,
213
203
  page_size: int = 20,
214
- query_user_id: Optional[str] = None,
204
+ query_user_id: str | None = None,
205
+ db: DBSession = Depends(get_db),
215
206
  user_id: UserId = Depends(get_user_id),
216
207
  user_config: dict = Depends(get_user_config),
217
208
  repo: ITaskRepository = Depends(get_task_repository),
218
209
  ):
219
210
  """
220
- Lists and searches for historical tasks.
221
- - Regular users can only search their own tasks.
222
- - Users with the 'tasks:read:all' scope can search for any user's tasks by providing `query_user_id`.
211
+ Lists and filters historical tasks by date.
212
+ - Regular users can only view their own tasks.
213
+ - Users with the 'tasks:read:all' scope can view any user's tasks by providing `query_user_id`.
223
214
  """
224
- log_prefix = f"[GET /api/v1/tasks] "
215
+ log_prefix = "[GET /api/v1/tasks] "
225
216
  log.info("%sRequest from user %s", log_prefix, user_id)
226
217
 
227
218
  target_user_id = user_id
@@ -265,14 +256,14 @@ async def search_tasks(
265
256
  detail="Invalid end_date format. Use ISO 8601 format.",
266
257
  )
267
258
 
268
- pagination = PaginationParams(page=page, page_size=page_size)
259
+ pagination = PaginationParams(page_number=page, page_size=page_size)
269
260
 
270
261
  try:
271
262
  tasks = repo.search(
263
+ db,
272
264
  user_id=target_user_id,
273
265
  start_date=start_time_ms,
274
266
  end_date=end_time_ms,
275
- search_query=search,
276
267
  pagination=pagination,
277
268
  )
278
269
  return tasks
@@ -288,6 +279,7 @@ async def search_tasks(
288
279
  async def get_task_as_stim_file(
289
280
  task_id: str,
290
281
  request: FastAPIRequest,
282
+ db: DBSession = Depends(get_db),
291
283
  user_id: UserId = Depends(get_user_id),
292
284
  user_config: dict = Depends(get_user_config),
293
285
  repo: ITaskRepository = Depends(get_task_repository),
@@ -299,7 +291,7 @@ async def get_task_as_stim_file(
299
291
  log.info("%sRequest from user %s", log_prefix, user_id)
300
292
 
301
293
  try:
302
- result = repo.find_by_id_with_events(task_id)
294
+ result = repo.find_by_id_with_events(db, task_id)
303
295
  if not result:
304
296
  raise HTTPException(
305
297
  status_code=status.HTTP_404_NOT_FOUND,
@@ -134,26 +134,32 @@ from sse_starlette.sse import EventSourceResponse
134
134
 
135
135
  def _generate_sse_url(fastapi_request: FastAPIRequest, stream_id: str) -> str:
136
136
  """
137
- Generate SSE endpoint URL with proper scheme detection for reverse proxy scenarios.
137
+ Generate SSE endpoint URL with proper scheme and host detection for reverse proxy scenarios.
138
138
 
139
139
  Args:
140
140
  fastapi_request: The FastAPI request object
141
141
  stream_id: The stream ID for the SSE endpoint
142
142
 
143
143
  Returns:
144
- Complete SSE URL with correct scheme (http/https)
144
+ Complete SSE URL with correct scheme (http/https) and host.
145
145
  """
146
+ base_url = fastapi_request.url_for(
147
+ "get_visualization_stream_events", stream_id=stream_id
148
+ )
149
+
146
150
  forwarded_proto = fastapi_request.headers.get("x-forwarded-proto")
147
- if forwarded_proto and forwarded_proto.lower() == "https":
148
- scheme = "https"
151
+ forwarded_host = fastapi_request.headers.get("x-forwarded-host")
152
+
153
+ if forwarded_proto and forwarded_host:
154
+ # In a reverse proxy environment like GitHub Codespaces, reconstruct the URL
155
+ # using the forwarded headers to ensure it's publicly accessible.
156
+ return str(base_url.replace(scheme=forwarded_proto, netloc=forwarded_host))
157
+ elif forwarded_proto:
158
+ # Handle cases with only a forwarded protocol (standard reverse proxy)
159
+ return str(base_url.replace(scheme=forwarded_proto))
149
160
  else:
150
- scheme = fastapi_request.url.scheme
151
-
152
- return str(
153
- fastapi_request.url_for(
154
- "get_visualization_stream_events", stream_id=stream_id
155
- ).replace(scheme=scheme)
156
- )
161
+ # Default behavior when not behind a reverse proxy
162
+ return str(base_url)
157
163
 
158
164
 
159
165
  def _translate_target_to_solace_topics(
@@ -188,8 +188,8 @@ class DataRetentionService:
188
188
 
189
189
  db = self.session_factory()
190
190
  try:
191
- repo = TaskRepository(db)
192
- total_deleted = repo.delete_tasks_older_than(cutoff_time_ms, batch_size)
191
+ repo = TaskRepository()
192
+ total_deleted = repo.delete_tasks_older_than(db, cutoff_time_ms, batch_size)
193
193
 
194
194
  if total_deleted == 0:
195
195
  log.info(
@@ -241,8 +241,8 @@ class DataRetentionService:
241
241
 
242
242
  db = self.session_factory()
243
243
  try:
244
- repo = FeedbackRepository(db)
245
- total_deleted = repo.delete_feedback_older_than(cutoff_time_ms, batch_size)
244
+ repo = FeedbackRepository()
245
+ total_deleted = repo.delete_feedback_older_than(db, cutoff_time_ms, batch_size)
246
246
 
247
247
  if total_deleted == 0:
248
248
  log.info(