solace-agent-mesh 1.4.11__py3-none-any.whl → 1.5.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.
- solace_agent_mesh/agent/adk/adk_llm.txt +3 -4
- solace_agent_mesh/agent/adk/adk_llm_detail.txt +566 -0
- solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +1 -1
- solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +1 -0
- solace_agent_mesh/agent/adk/callbacks.py +51 -2
- solace_agent_mesh/agent/adk/models/lite_llm.py +1 -0
- solace_agent_mesh/agent/adk/models/models_llm.txt +1 -2
- solace_agent_mesh/agent/adk/services.py +30 -15
- solace_agent_mesh/agent/adk/setup.py +4 -0
- solace_agent_mesh/agent/agent_llm.txt +1 -1
- solace_agent_mesh/agent/agent_llm_detail.txt +1702 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +2 -13
- solace_agent_mesh/agent/protocol/protocol_llm.txt +15 -2
- solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +92 -0
- solace_agent_mesh/agent/sac/component.py +51 -21
- solace_agent_mesh/agent/sac/sac_llm.txt +15 -1
- solace_agent_mesh/agent/sac/sac_llm_detail.txt +200 -0
- solace_agent_mesh/agent/sac/task_execution_context.py +73 -0
- solace_agent_mesh/agent/testing/testing_llm_detail.txt +68 -0
- solace_agent_mesh/agent/tools/tools_llm.txt +148 -154
- solace_agent_mesh/agent/tools/tools_llm_detail.txt +274 -0
- solace_agent_mesh/agent/utils/utils_llm.txt +1 -1
- solace_agent_mesh/agent/utils/utils_llm_detail.txt +149 -0
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/0e682baa.d54b8668.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/483cef9a.bf9398af.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{main.dc155742.js → main.0c149855.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/{runtime~main.0d2ff2b6.js → runtime~main.c66557e4.js} +1 -1
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/rbac-setup-guilde/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +8 -4
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/litellm_models/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +7 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1760032255022.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1760032255022.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-j1LW-wlq.js → authCallback-DwrxZE0E.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{client-B9p_nFNA.js → client-DarGQzyw.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-CZbpmwfA.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-C__uuUkB.js +339 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{vendor-CS5YMf8a.js → vendor-BKIeiHj_.js} +80 -70
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
- solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
- solace_agent_mesh/common/a2a/a2a_llm.txt +1 -1
- solace_agent_mesh/common/a2a/a2a_llm_detail.txt +193 -0
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +1 -1
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +736 -0
- solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +23 -0
- solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +93 -15
- solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +23 -0
- solace_agent_mesh/common/common_llm.txt +24 -39
- solace_agent_mesh/common/common_llm_detail.txt +2562 -0
- solace_agent_mesh/common/data_parts.py +9 -1
- solace_agent_mesh/common/middleware/middleware_llm_detail.txt +185 -0
- solace_agent_mesh/common/sac/sac_llm.txt +1 -1
- solace_agent_mesh/common/sac/sac_llm_detail.txt +82 -0
- solace_agent_mesh/common/sam_events/sam_events_llm.txt +104 -0
- solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +115 -0
- solace_agent_mesh/common/services/identity_service.py +7 -4
- solace_agent_mesh/common/services/providers/local_file_identity_service.py +4 -2
- solace_agent_mesh/common/services/services_llm.txt +57 -6
- solace_agent_mesh/common/services/services_llm_detail.txt +459 -0
- solace_agent_mesh/common/utils/embeds/embeds_llm.txt +1 -1
- solace_agent_mesh/common/utils/utils_llm.txt +75 -87
- solace_agent_mesh/common/utils/utils_llm_detail.txt +572 -0
- solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +101 -0
- solace_agent_mesh/gateway/base/app.py +1 -1
- solace_agent_mesh/gateway/base/base_llm.txt +1 -1
- solace_agent_mesh/gateway/base/base_llm_detail.txt +235 -0
- solace_agent_mesh/gateway/base/component.py +1 -1
- solace_agent_mesh/gateway/gateway_llm.txt +242 -235
- solace_agent_mesh/gateway/gateway_llm_detail.txt +3885 -0
- solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +295 -0
- solace_agent_mesh/gateway/http_sse/alembic/env.py +10 -1
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251006_98882922fa59_add_tasks_events_feedback_chat_tasks.py +190 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +155 -0
- solace_agent_mesh/gateway/http_sse/alembic.ini +1 -1
- solace_agent_mesh/gateway/http_sse/app.py +148 -2
- solace_agent_mesh/gateway/http_sse/component.py +368 -60
- solace_agent_mesh/gateway/http_sse/components/components_llm.txt +46 -6
- solace_agent_mesh/gateway/http_sse/components/task_logger_forwarder.py +108 -0
- solace_agent_mesh/gateway/http_sse/components/visualization_forwarder_component.py +1 -1
- solace_agent_mesh/gateway/http_sse/dependencies.py +116 -26
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +172 -172
- solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +3278 -0
- solace_agent_mesh/gateway/http_sse/main.py +146 -41
- solace_agent_mesh/gateway/http_sse/repository/__init__.py +3 -12
- solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +103 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/__init__.py +5 -3
- solace_agent_mesh/gateway/http_sse/repository/entities/chat_task.py +75 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +263 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/feedback.py +20 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session_history.py +0 -16
- solace_agent_mesh/gateway/http_sse/repository/entities/task.py +25 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/task_event.py +21 -0
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +81 -0
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +73 -18
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +9 -5
- solace_agent_mesh/gateway/http_sse/repository/models/chat_task_model.py +31 -0
- solace_agent_mesh/gateway/http_sse/repository/models/feedback_model.py +21 -0
- solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +266 -0
- solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +3 -3
- solace_agent_mesh/gateway/http_sse/repository/models/task_event_model.py +25 -0
- solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +32 -0
- solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +340 -0
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +4 -53
- solace_agent_mesh/gateway/http_sse/repository/task_repository.py +173 -0
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/config.py +26 -4
- solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +346 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +3 -3
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +83 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +2 -10
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/task_requests.py +58 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/__init__.py +5 -3
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +107 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +1 -15
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/task_responses.py +30 -0
- solace_agent_mesh/gateway/http_sse/routers/feedback.py +37 -0
- solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +255 -204
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +220 -40
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +168 -42
- solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +272 -0
- solace_agent_mesh/gateway/http_sse/services/feedback_service.py +241 -0
- solace_agent_mesh/gateway/http_sse/services/people_service.py +0 -80
- solace_agent_mesh/gateway/http_sse/services/services_llm.txt +177 -13
- solace_agent_mesh/gateway/http_sse/services/session_service.py +151 -84
- solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +317 -0
- solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +25 -14
- solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +285 -0
- solace_agent_mesh/gateway/http_sse/shared/types.py +7 -0
- solace_agent_mesh/gateway/http_sse/utils/__init__.py +1 -0
- solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +32 -0
- solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +47 -0
- solace_agent_mesh/solace_agent_mesh_llm.txt +1 -1
- solace_agent_mesh/solace_agent_mesh_llm_detail.txt +8599 -0
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/METADATA +1 -1
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/RECORD +179 -131
- solace_agent_mesh/agent/adk/invocation_monitor.py +0 -295
- solace_agent_mesh/assets/docs/assets/js/0e682baa.d054e1d8.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/483cef9a.4736f2d8.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1759514789087.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1759514789087.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-ChRwcV89.css +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-DnnE01OM.js +0 -339
- solace_agent_mesh/gateway/http_sse/repository/entities/message.py +0 -41
- solace_agent_mesh/gateway/http_sse/repository/message_repository.py +0 -84
- solace_agent_mesh/gateway/http_sse/repository/models/message_model.py +0 -45
- /solace_agent_mesh/assets/docs/assets/js/{main.dc155742.js.LICENSE.txt → main.0c149855.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -29,6 +29,7 @@ from ...gateway.http_sse.routers import (
|
|
|
29
29
|
sse,
|
|
30
30
|
tasks,
|
|
31
31
|
visualization,
|
|
32
|
+
feedback,
|
|
32
33
|
)
|
|
33
34
|
from .routers.sessions import router as session_router
|
|
34
35
|
from .routers.tasks import router as task_router
|
|
@@ -43,6 +44,9 @@ app = FastAPI(
|
|
|
43
44
|
description="Backend API and SSE server for the A2A Web UI, hosted by Solace AI Connector.",
|
|
44
45
|
)
|
|
45
46
|
|
|
47
|
+
# Global flag to track if dependencies have been initialized
|
|
48
|
+
_dependencies_initialized = False
|
|
49
|
+
|
|
46
50
|
|
|
47
51
|
def _extract_access_token(request: FastAPIRequest) -> str:
|
|
48
52
|
auth_header = request.headers.get("Authorization")
|
|
@@ -62,7 +66,9 @@ def _extract_access_token(request: FastAPIRequest) -> str:
|
|
|
62
66
|
return None
|
|
63
67
|
|
|
64
68
|
|
|
65
|
-
async def _validate_token(
|
|
69
|
+
async def _validate_token(
|
|
70
|
+
auth_service_url: str, auth_provider: str, access_token: str
|
|
71
|
+
) -> bool:
|
|
66
72
|
async with httpx.AsyncClient() as client:
|
|
67
73
|
validation_response = await client.post(
|
|
68
74
|
f"{auth_service_url}/is_token_valid",
|
|
@@ -72,7 +78,9 @@ async def _validate_token(auth_service_url: str, auth_provider: str, access_toke
|
|
|
72
78
|
return validation_response.status_code == 200
|
|
73
79
|
|
|
74
80
|
|
|
75
|
-
async def _get_user_info(
|
|
81
|
+
async def _get_user_info(
|
|
82
|
+
auth_service_url: str, auth_provider: str, access_token: str
|
|
83
|
+
) -> dict:
|
|
76
84
|
async with httpx.AsyncClient() as client:
|
|
77
85
|
userinfo_response = await client.get(
|
|
78
86
|
f"{auth_service_url}/user_info?provider={auth_provider}",
|
|
@@ -100,7 +108,9 @@ def _extract_user_identifier(user_info: dict) -> str:
|
|
|
100
108
|
)
|
|
101
109
|
|
|
102
110
|
if user_identifier and user_identifier.lower() == "unknown":
|
|
103
|
-
log.warning(
|
|
111
|
+
log.warning(
|
|
112
|
+
"AuthMiddleware: IDP returned 'Unknown' as user identifier. Using fallback."
|
|
113
|
+
)
|
|
104
114
|
return "sam_dev_user"
|
|
105
115
|
|
|
106
116
|
return user_identifier
|
|
@@ -124,7 +134,9 @@ def _extract_user_details(user_info: dict, user_identifier: str) -> tuple:
|
|
|
124
134
|
return email_from_auth, display_name
|
|
125
135
|
|
|
126
136
|
|
|
127
|
-
async def _create_user_state_without_identity_service(
|
|
137
|
+
async def _create_user_state_without_identity_service(
|
|
138
|
+
user_identifier: str, email_from_auth: str, display_name: str
|
|
139
|
+
) -> dict:
|
|
128
140
|
final_user_id = user_identifier or email_from_auth or "sam_dev_user"
|
|
129
141
|
if not final_user_id or final_user_id.lower() in ["unknown", "null", "none", ""]:
|
|
130
142
|
final_user_id = "sam_dev_user"
|
|
@@ -146,7 +158,13 @@ async def _create_user_state_without_identity_service(user_identifier: str, emai
|
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
|
|
149
|
-
async def _create_user_state_with_identity_service(
|
|
161
|
+
async def _create_user_state_with_identity_service(
|
|
162
|
+
identity_service,
|
|
163
|
+
user_identifier: str,
|
|
164
|
+
email_from_auth: str,
|
|
165
|
+
display_name: str,
|
|
166
|
+
user_info: dict,
|
|
167
|
+
) -> dict:
|
|
150
168
|
lookup_value = email_from_auth if "@" in email_from_auth else user_identifier
|
|
151
169
|
user_profile = await identity_service.get_user_profile(
|
|
152
170
|
{identity_service.lookup_key: lookup_value, "user_info": user_info}
|
|
@@ -186,24 +204,35 @@ def _create_auth_middleware(component):
|
|
|
186
204
|
return
|
|
187
205
|
|
|
188
206
|
skip_paths = [
|
|
189
|
-
"/api/v1/config",
|
|
190
|
-
"/api/v1/auth/
|
|
207
|
+
"/api/v1/config",
|
|
208
|
+
"/api/v1/auth/callback",
|
|
209
|
+
"/api/v1/auth/login",
|
|
210
|
+
"/api/v1/auth/refresh",
|
|
211
|
+
"/api/v1/csrf-token",
|
|
212
|
+
"/health",
|
|
191
213
|
]
|
|
192
214
|
|
|
193
215
|
if any(request.url.path.startswith(path) for path in skip_paths):
|
|
194
216
|
await self.app(scope, receive, send)
|
|
195
217
|
return
|
|
196
218
|
|
|
197
|
-
use_auth = dependencies.api_config and dependencies.api_config.get(
|
|
219
|
+
use_auth = dependencies.api_config and dependencies.api_config.get(
|
|
220
|
+
"frontend_use_authorization"
|
|
221
|
+
)
|
|
198
222
|
|
|
199
223
|
if use_auth:
|
|
200
224
|
await self._handle_authenticated_request(request, scope, receive, send)
|
|
201
225
|
else:
|
|
202
226
|
request.state.user = {
|
|
203
|
-
"id": "sam_dev_user",
|
|
204
|
-
"
|
|
227
|
+
"id": "sam_dev_user",
|
|
228
|
+
"name": "Sam Dev User",
|
|
229
|
+
"email": "sam@dev.local",
|
|
230
|
+
"authenticated": True,
|
|
231
|
+
"auth_method": "development",
|
|
205
232
|
}
|
|
206
|
-
log.debug(
|
|
233
|
+
log.debug(
|
|
234
|
+
"AuthMiddleware: Set development user state with id: sam_dev_user"
|
|
235
|
+
)
|
|
207
236
|
|
|
208
237
|
await self.app(scope, receive, send)
|
|
209
238
|
|
|
@@ -214,13 +243,18 @@ def _create_auth_middleware(component):
|
|
|
214
243
|
log.warning("AuthMiddleware: No access token found. Returning 401.")
|
|
215
244
|
response = JSONResponse(
|
|
216
245
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
217
|
-
content={
|
|
246
|
+
content={
|
|
247
|
+
"detail": "Not authenticated",
|
|
248
|
+
"error_type": "authentication_required",
|
|
249
|
+
},
|
|
218
250
|
)
|
|
219
251
|
await response(scope, receive, send)
|
|
220
252
|
return
|
|
221
253
|
|
|
222
254
|
try:
|
|
223
|
-
auth_service_url = dependencies.api_config.get(
|
|
255
|
+
auth_service_url = dependencies.api_config.get(
|
|
256
|
+
"external_auth_service_url"
|
|
257
|
+
)
|
|
224
258
|
auth_provider = dependencies.api_config.get("external_auth_provider")
|
|
225
259
|
|
|
226
260
|
if not auth_service_url:
|
|
@@ -232,47 +266,85 @@ def _create_auth_middleware(component):
|
|
|
232
266
|
await response(scope, receive, send)
|
|
233
267
|
return
|
|
234
268
|
|
|
235
|
-
if not await _validate_token(
|
|
269
|
+
if not await _validate_token(
|
|
270
|
+
auth_service_url, auth_provider, access_token
|
|
271
|
+
):
|
|
236
272
|
log.warning("AuthMiddleware: Token validation failed")
|
|
237
273
|
response = JSONResponse(
|
|
238
274
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
239
|
-
content={
|
|
275
|
+
content={
|
|
276
|
+
"detail": "Invalid token",
|
|
277
|
+
"error_type": "invalid_token",
|
|
278
|
+
},
|
|
240
279
|
)
|
|
241
280
|
await response(scope, receive, send)
|
|
242
281
|
return
|
|
243
282
|
|
|
244
|
-
user_info = await _get_user_info(
|
|
283
|
+
user_info = await _get_user_info(
|
|
284
|
+
auth_service_url, auth_provider, access_token
|
|
285
|
+
)
|
|
245
286
|
if not user_info:
|
|
246
|
-
log.warning(
|
|
287
|
+
log.warning(
|
|
288
|
+
"AuthMiddleware: Failed to get user info from external auth service"
|
|
289
|
+
)
|
|
247
290
|
response = JSONResponse(
|
|
248
291
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
249
|
-
content={
|
|
292
|
+
content={
|
|
293
|
+
"detail": "Could not retrieve user info from auth provider",
|
|
294
|
+
"error_type": "user_info_failed",
|
|
295
|
+
},
|
|
250
296
|
)
|
|
251
297
|
await response(scope, receive, send)
|
|
252
298
|
return
|
|
253
299
|
|
|
254
300
|
user_identifier = _extract_user_identifier(user_info)
|
|
255
|
-
if not user_identifier or user_identifier.lower() in [
|
|
256
|
-
|
|
301
|
+
if not user_identifier or user_identifier.lower() in [
|
|
302
|
+
"null",
|
|
303
|
+
"none",
|
|
304
|
+
"",
|
|
305
|
+
]:
|
|
306
|
+
log.error(
|
|
307
|
+
"AuthMiddleware: No valid user identifier from OAuth provider"
|
|
308
|
+
)
|
|
257
309
|
response = JSONResponse(
|
|
258
310
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
259
|
-
content={
|
|
311
|
+
content={
|
|
312
|
+
"detail": "OAuth provider returned no valid user identifier",
|
|
313
|
+
"error_type": "invalid_user_identifier_from_provider",
|
|
314
|
+
},
|
|
260
315
|
)
|
|
261
316
|
await response(scope, receive, send)
|
|
262
317
|
return
|
|
263
318
|
|
|
264
|
-
email_from_auth, display_name = _extract_user_details(
|
|
319
|
+
email_from_auth, display_name = _extract_user_details(
|
|
320
|
+
user_info, user_identifier
|
|
321
|
+
)
|
|
265
322
|
|
|
266
323
|
identity_service = self.component.identity_service
|
|
267
324
|
if not identity_service:
|
|
268
|
-
request.state.user =
|
|
325
|
+
request.state.user = (
|
|
326
|
+
await _create_user_state_without_identity_service(
|
|
327
|
+
user_identifier, email_from_auth, display_name
|
|
328
|
+
)
|
|
329
|
+
)
|
|
269
330
|
else:
|
|
270
|
-
user_state = await _create_user_state_with_identity_service(
|
|
331
|
+
user_state = await _create_user_state_with_identity_service(
|
|
332
|
+
identity_service,
|
|
333
|
+
user_identifier,
|
|
334
|
+
email_from_auth,
|
|
335
|
+
display_name,
|
|
336
|
+
user_info,
|
|
337
|
+
)
|
|
271
338
|
if not user_state:
|
|
272
|
-
log.error(
|
|
339
|
+
log.error(
|
|
340
|
+
"AuthMiddleware: User authenticated but not found in internal IdentityService"
|
|
341
|
+
)
|
|
273
342
|
response = JSONResponse(
|
|
274
343
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
275
|
-
content={
|
|
344
|
+
content={
|
|
345
|
+
"detail": "User not authorized for this application",
|
|
346
|
+
"error_type": "not_authorized",
|
|
347
|
+
},
|
|
276
348
|
)
|
|
277
349
|
await response(scope, receive, send)
|
|
278
350
|
return
|
|
@@ -287,10 +359,14 @@ def _create_auth_middleware(component):
|
|
|
287
359
|
await response(scope, receive, send)
|
|
288
360
|
return
|
|
289
361
|
except Exception as exc:
|
|
290
|
-
log.error(
|
|
362
|
+
log.error(
|
|
363
|
+
"An unexpected error occurred during token validation: %s", exc
|
|
364
|
+
)
|
|
291
365
|
response = JSONResponse(
|
|
292
366
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
293
|
-
content={
|
|
367
|
+
content={
|
|
368
|
+
"detail": "An internal error occurred during authentication"
|
|
369
|
+
},
|
|
294
370
|
)
|
|
295
371
|
await response(scope, receive, send)
|
|
296
372
|
return
|
|
@@ -343,10 +419,14 @@ def _run_community_migrations(database_url: str) -> None:
|
|
|
343
419
|
except Exception as migration_error:
|
|
344
420
|
log.error("Community migration failed: %s", migration_error)
|
|
345
421
|
log.error("Check database connectivity and permissions")
|
|
346
|
-
raise RuntimeError(
|
|
422
|
+
raise RuntimeError(
|
|
423
|
+
f"Community database migration failed: {migration_error}"
|
|
424
|
+
) from migration_error
|
|
347
425
|
|
|
348
426
|
|
|
349
|
-
def _run_enterprise_migrations(
|
|
427
|
+
def _run_enterprise_migrations(
|
|
428
|
+
component: "WebUIBackendComponent", database_url: str
|
|
429
|
+
) -> None:
|
|
350
430
|
"""
|
|
351
431
|
Run migrations for enterprise features like advanced analytics, audit logs, etc.
|
|
352
432
|
This is optional and only runs if the enterprise package is available.
|
|
@@ -420,7 +500,15 @@ def setup_dependencies(component: "WebUIBackendComponent", database_url: str = N
|
|
|
420
500
|
backward compatibility with existing API contracts.
|
|
421
501
|
|
|
422
502
|
If database_url is None, runs in compatibility mode with in-memory sessions.
|
|
503
|
+
|
|
504
|
+
This function is idempotent and safe to call multiple times.
|
|
423
505
|
"""
|
|
506
|
+
global _dependencies_initialized
|
|
507
|
+
|
|
508
|
+
if _dependencies_initialized:
|
|
509
|
+
log.debug("[setup_dependencies] Dependencies already initialized, skipping")
|
|
510
|
+
return
|
|
511
|
+
|
|
424
512
|
dependencies.set_component_instance(component)
|
|
425
513
|
|
|
426
514
|
if database_url:
|
|
@@ -441,6 +529,9 @@ def setup_dependencies(component: "WebUIBackendComponent", database_url: str = N
|
|
|
441
529
|
_setup_routers()
|
|
442
530
|
_setup_static_files()
|
|
443
531
|
|
|
532
|
+
_dependencies_initialized = True
|
|
533
|
+
log.info("[setup_dependencies] Dependencies initialization complete")
|
|
534
|
+
|
|
444
535
|
|
|
445
536
|
def _setup_middleware(component: "WebUIBackendComponent") -> None:
|
|
446
537
|
allowed_origins = component.get_cors_origins()
|
|
@@ -467,39 +558,50 @@ def _setup_routers() -> None:
|
|
|
467
558
|
|
|
468
559
|
app.include_router(session_router, prefix=api_prefix, tags=["Sessions"])
|
|
469
560
|
app.include_router(user_router, prefix=f"{api_prefix}/users", tags=["Users"])
|
|
470
|
-
app.include_router(task_router, prefix=f"{api_prefix}/tasks", tags=["Tasks"])
|
|
471
561
|
app.include_router(config.router, prefix=api_prefix, tags=["Config"])
|
|
472
562
|
app.include_router(agent_cards.router, prefix=api_prefix, tags=["Agent Cards"])
|
|
473
|
-
app.include_router(tasks.router, prefix=api_prefix, tags=["
|
|
563
|
+
app.include_router(tasks.router, prefix=api_prefix, tags=["Tasks"])
|
|
474
564
|
app.include_router(sse.router, prefix=f"{api_prefix}/sse", tags=["SSE"])
|
|
475
|
-
app.include_router(
|
|
476
|
-
|
|
565
|
+
app.include_router(
|
|
566
|
+
artifacts.router, prefix=f"{api_prefix}/artifacts", tags=["Artifacts"]
|
|
567
|
+
)
|
|
568
|
+
app.include_router(
|
|
569
|
+
visualization.router,
|
|
570
|
+
prefix=f"{api_prefix}/visualization",
|
|
571
|
+
tags=["Visualization"],
|
|
572
|
+
)
|
|
477
573
|
app.include_router(people.router, prefix=api_prefix, tags=["People"])
|
|
478
574
|
app.include_router(auth.router, prefix=api_prefix, tags=["Auth"])
|
|
575
|
+
app.include_router(feedback.router, prefix=api_prefix, tags=["Feedback"])
|
|
479
576
|
log.info("Legacy routers mounted for endpoints not yet migrated")
|
|
480
577
|
|
|
481
578
|
# Register shared exception handlers from community repo
|
|
482
579
|
from .shared.exception_handlers import register_exception_handlers
|
|
580
|
+
|
|
483
581
|
register_exception_handlers(app)
|
|
484
582
|
log.info("Registered shared exception handlers from community repo")
|
|
485
583
|
|
|
486
584
|
# Mount enterprise routers if available
|
|
487
585
|
try:
|
|
488
|
-
from solace_agent_mesh_enterprise.webui_backend.routers import
|
|
586
|
+
from solace_agent_mesh_enterprise.webui_backend.routers import (
|
|
587
|
+
get_enterprise_routers,
|
|
588
|
+
)
|
|
489
589
|
|
|
490
590
|
enterprise_routers = get_enterprise_routers()
|
|
491
591
|
for router_config in enterprise_routers:
|
|
492
592
|
app.include_router(
|
|
493
593
|
router_config["router"],
|
|
494
594
|
prefix=router_config["prefix"],
|
|
495
|
-
tags=router_config["tags"]
|
|
595
|
+
tags=router_config["tags"],
|
|
496
596
|
)
|
|
497
597
|
log.info("Mounted %d enterprise routers", len(enterprise_routers))
|
|
498
598
|
|
|
499
599
|
except ImportError:
|
|
500
600
|
log.debug("No enterprise package detected - skipping enterprise routers")
|
|
501
601
|
except ModuleNotFoundError:
|
|
502
|
-
log.debug(
|
|
602
|
+
log.debug(
|
|
603
|
+
"Enterprise module not found - skipping enterprise routers and exception handlers"
|
|
604
|
+
)
|
|
503
605
|
except Exception as e:
|
|
504
606
|
log.warning("Failed to load enterprise routers and exception handlers: %s", e)
|
|
505
607
|
|
|
@@ -535,7 +637,7 @@ async def http_exception_handler(request: FastAPIRequest, exc: HTTPException):
|
|
|
535
637
|
Returns JSON-RPC format for tasks/SSE endpoints, REST format for others.
|
|
536
638
|
"""
|
|
537
639
|
log.warning(
|
|
538
|
-
"HTTP Exception: Status=%s, Detail=%s, Request: %s %s",
|
|
640
|
+
"HTTP Exception Handler triggered: Status=%s, Detail=%s, Request: %s %s",
|
|
539
641
|
exc.status_code,
|
|
540
642
|
exc.detail,
|
|
541
643
|
request.method,
|
|
@@ -594,7 +696,7 @@ async def validation_exception_handler(
|
|
|
594
696
|
Handles Pydantic validation errors with format detection.
|
|
595
697
|
"""
|
|
596
698
|
log.warning(
|
|
597
|
-
"
|
|
699
|
+
"Validation Exception Handler triggered: %s, Request: %s %s",
|
|
598
700
|
exc.errors(),
|
|
599
701
|
request.method,
|
|
600
702
|
request.url,
|
|
@@ -603,7 +705,7 @@ async def validation_exception_handler(
|
|
|
603
705
|
message="Invalid request parameters", data=exc.errors(), request_id=None
|
|
604
706
|
)
|
|
605
707
|
return JSONResponse(
|
|
606
|
-
status_code=status.
|
|
708
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
607
709
|
content=response.model_dump(exclude_none=True),
|
|
608
710
|
)
|
|
609
711
|
|
|
@@ -614,7 +716,10 @@ async def generic_exception_handler(request: FastAPIRequest, exc: Exception):
|
|
|
614
716
|
Handles any other unexpected exceptions with format detection.
|
|
615
717
|
"""
|
|
616
718
|
log.exception(
|
|
617
|
-
"
|
|
719
|
+
"Generic Exception Handler triggered: %s, Request: %s %s",
|
|
720
|
+
exc,
|
|
721
|
+
request.method,
|
|
722
|
+
request.url,
|
|
618
723
|
)
|
|
619
724
|
error_obj = a2a.create_internal_error(
|
|
620
725
|
message="An unexpected server error occurred: %s" % type(exc).__name__
|
|
@@ -3,35 +3,26 @@ Repository layer containing all data access logic organized by entity type.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
# Interfaces
|
|
6
|
-
from .interfaces import
|
|
6
|
+
from .interfaces import ISessionRepository
|
|
7
7
|
|
|
8
8
|
# Implementations
|
|
9
|
-
from .message_repository import MessageRepository
|
|
10
9
|
from .session_repository import SessionRepository
|
|
11
10
|
|
|
12
11
|
# Entities (re-exported for convenience)
|
|
13
12
|
from .entities.session import Session
|
|
14
|
-
from .entities.message import Message
|
|
15
|
-
from .entities.session_history import SessionHistory
|
|
16
13
|
|
|
17
14
|
# Models (re-exported for convenience)
|
|
18
15
|
from .models.base import Base
|
|
19
16
|
from .models.session_model import SessionModel
|
|
20
|
-
from .models.message_model import MessageModel
|
|
21
17
|
|
|
22
18
|
__all__ = [
|
|
23
19
|
# Interfaces
|
|
24
|
-
"IMessageRepository",
|
|
25
20
|
"ISessionRepository",
|
|
26
21
|
# Implementations
|
|
27
|
-
"MessageRepository",
|
|
28
22
|
"SessionRepository",
|
|
29
23
|
# Entities
|
|
30
|
-
"
|
|
31
|
-
"Session",
|
|
32
|
-
"SessionHistory",
|
|
24
|
+
"Session",
|
|
33
25
|
# Models
|
|
34
26
|
"Base",
|
|
35
|
-
"MessageModel",
|
|
36
27
|
"SessionModel",
|
|
37
|
-
]
|
|
28
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatTask repository implementation using SQLAlchemy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.orm import Session as DBSession
|
|
8
|
+
|
|
9
|
+
from ..shared import now_epoch_ms
|
|
10
|
+
from ..shared.types import SessionId, UserId
|
|
11
|
+
from .entities import ChatTask
|
|
12
|
+
from .interfaces import IChatTaskRepository
|
|
13
|
+
from .models import ChatTaskModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatTaskRepository(IChatTaskRepository):
|
|
17
|
+
"""SQLAlchemy implementation of chat task repository."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, db: DBSession):
|
|
20
|
+
self.db = db
|
|
21
|
+
|
|
22
|
+
def save(self, task: ChatTask) -> ChatTask:
|
|
23
|
+
"""Save or update a chat task (upsert)."""
|
|
24
|
+
existing = self.db.query(ChatTaskModel).filter(
|
|
25
|
+
ChatTaskModel.id == task.id
|
|
26
|
+
).first()
|
|
27
|
+
|
|
28
|
+
if existing:
|
|
29
|
+
# Update existing task - store strings directly
|
|
30
|
+
existing.user_message = task.user_message
|
|
31
|
+
existing.message_bubbles = task.message_bubbles # Already a string
|
|
32
|
+
existing.task_metadata = task.task_metadata # Already a string
|
|
33
|
+
existing.updated_time = now_epoch_ms()
|
|
34
|
+
else:
|
|
35
|
+
# Create new task - store strings directly
|
|
36
|
+
model = ChatTaskModel(
|
|
37
|
+
id=task.id,
|
|
38
|
+
session_id=task.session_id,
|
|
39
|
+
user_id=task.user_id,
|
|
40
|
+
user_message=task.user_message,
|
|
41
|
+
message_bubbles=task.message_bubbles, # Already a string
|
|
42
|
+
task_metadata=task.task_metadata, # Already a string
|
|
43
|
+
created_time=task.created_time,
|
|
44
|
+
updated_time=task.updated_time
|
|
45
|
+
)
|
|
46
|
+
self.db.add(model)
|
|
47
|
+
|
|
48
|
+
self.db.commit()
|
|
49
|
+
|
|
50
|
+
# Reload to get updated values
|
|
51
|
+
model = self.db.query(ChatTaskModel).filter(
|
|
52
|
+
ChatTaskModel.id == task.id
|
|
53
|
+
).first()
|
|
54
|
+
|
|
55
|
+
return self._model_to_entity(model)
|
|
56
|
+
|
|
57
|
+
def find_by_session(
|
|
58
|
+
self,
|
|
59
|
+
session_id: SessionId,
|
|
60
|
+
user_id: UserId
|
|
61
|
+
) -> List[ChatTask]:
|
|
62
|
+
"""Find all tasks for a session."""
|
|
63
|
+
models = self.db.query(ChatTaskModel).filter(
|
|
64
|
+
ChatTaskModel.session_id == session_id,
|
|
65
|
+
ChatTaskModel.user_id == user_id
|
|
66
|
+
).order_by(ChatTaskModel.created_time.asc()).all()
|
|
67
|
+
|
|
68
|
+
return [self._model_to_entity(m) for m in models]
|
|
69
|
+
|
|
70
|
+
def find_by_id(
|
|
71
|
+
self,
|
|
72
|
+
task_id: str,
|
|
73
|
+
user_id: UserId
|
|
74
|
+
) -> Optional[ChatTask]:
|
|
75
|
+
"""Find a specific task."""
|
|
76
|
+
model = self.db.query(ChatTaskModel).filter(
|
|
77
|
+
ChatTaskModel.id == task_id,
|
|
78
|
+
ChatTaskModel.user_id == user_id
|
|
79
|
+
).first()
|
|
80
|
+
|
|
81
|
+
return self._model_to_entity(model) if model else None
|
|
82
|
+
|
|
83
|
+
def delete_by_session(self, session_id: SessionId) -> bool:
|
|
84
|
+
"""Delete all tasks for a session."""
|
|
85
|
+
result = self.db.query(ChatTaskModel).filter(
|
|
86
|
+
ChatTaskModel.session_id == session_id
|
|
87
|
+
).delete()
|
|
88
|
+
self.db.commit()
|
|
89
|
+
return result > 0
|
|
90
|
+
|
|
91
|
+
def _model_to_entity(self, model: ChatTaskModel) -> ChatTask:
|
|
92
|
+
"""Convert SQLAlchemy model to domain entity."""
|
|
93
|
+
# No deserialization - just pass strings through
|
|
94
|
+
return ChatTask(
|
|
95
|
+
id=model.id,
|
|
96
|
+
session_id=model.session_id,
|
|
97
|
+
user_id=model.user_id,
|
|
98
|
+
user_message=model.user_message,
|
|
99
|
+
message_bubbles=model.message_bubbles, # String (opaque)
|
|
100
|
+
task_metadata=model.task_metadata, # String (opaque)
|
|
101
|
+
created_time=model.created_time,
|
|
102
|
+
updated_time=model.updated_time
|
|
103
|
+
)
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
Domain entities for the repository layer.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from .
|
|
5
|
+
from .chat_task import ChatTask
|
|
6
|
+
from .feedback import Feedback
|
|
6
7
|
from .session import Session
|
|
7
|
-
from .
|
|
8
|
+
from .task import Task
|
|
9
|
+
from .task_event import TaskEvent
|
|
8
10
|
|
|
9
|
-
__all__ = ["
|
|
11
|
+
__all__ = ["ChatTask", "Feedback", "Session", "Task", "TaskEvent"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatTask domain entity.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChatTask(BaseModel):
|
|
12
|
+
"""ChatTask domain entity with business logic."""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(from_attributes=True)
|
|
15
|
+
|
|
16
|
+
id: str
|
|
17
|
+
session_id: str
|
|
18
|
+
user_id: str
|
|
19
|
+
user_message: Optional[str] = None
|
|
20
|
+
message_bubbles: str # JSON string (opaque to backend)
|
|
21
|
+
task_metadata: Optional[str] = None # JSON string (opaque to backend)
|
|
22
|
+
created_time: int
|
|
23
|
+
updated_time: Optional[int] = None
|
|
24
|
+
|
|
25
|
+
@field_validator("message_bubbles")
|
|
26
|
+
@classmethod
|
|
27
|
+
def validate_message_bubbles(cls, v: str) -> str:
|
|
28
|
+
"""Validate that message_bubbles is a non-empty JSON string."""
|
|
29
|
+
if not v or not v.strip():
|
|
30
|
+
raise ValueError("message_bubbles cannot be empty")
|
|
31
|
+
|
|
32
|
+
# Validate it's valid JSON (but don't validate structure)
|
|
33
|
+
try:
|
|
34
|
+
parsed = json.loads(v)
|
|
35
|
+
if not isinstance(parsed, list) or len(parsed) == 0:
|
|
36
|
+
raise ValueError("message_bubbles must be a non-empty JSON array")
|
|
37
|
+
except json.JSONDecodeError as e:
|
|
38
|
+
raise ValueError(f"message_bubbles must be valid JSON: {e}")
|
|
39
|
+
|
|
40
|
+
return v
|
|
41
|
+
|
|
42
|
+
@field_validator("task_metadata")
|
|
43
|
+
@classmethod
|
|
44
|
+
def validate_task_metadata(cls, v: Optional[str]) -> Optional[str]:
|
|
45
|
+
"""Validate that task_metadata is valid JSON if provided."""
|
|
46
|
+
if v is None or not v.strip():
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
# Validate it's valid JSON (but don't validate structure)
|
|
50
|
+
try:
|
|
51
|
+
json.loads(v)
|
|
52
|
+
except json.JSONDecodeError as e:
|
|
53
|
+
raise ValueError(f"task_metadata must be valid JSON: {e}")
|
|
54
|
+
|
|
55
|
+
return v
|
|
56
|
+
|
|
57
|
+
def add_feedback(self, feedback_type: str, feedback_text: Optional[str] = None) -> None:
|
|
58
|
+
"""Add or update feedback for this task."""
|
|
59
|
+
# Parse metadata, update, re-serialize
|
|
60
|
+
metadata = json.loads(self.task_metadata) if self.task_metadata else {}
|
|
61
|
+
|
|
62
|
+
metadata["feedback"] = {
|
|
63
|
+
"type": feedback_type,
|
|
64
|
+
"text": feedback_text,
|
|
65
|
+
"submitted": True
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
self.task_metadata = json.dumps(metadata)
|
|
69
|
+
|
|
70
|
+
def get_feedback(self) -> Optional[Dict[str, Any]]:
|
|
71
|
+
"""Get feedback for this task."""
|
|
72
|
+
if self.task_metadata:
|
|
73
|
+
metadata = json.loads(self.task_metadata)
|
|
74
|
+
return metadata.get("feedback")
|
|
75
|
+
return None
|