atlas-chat 0.1.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.
Files changed (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,546 @@
1
+ """
2
+ Notification utilities - pure functions for handling chat event notifications.
3
+
4
+ This module provides stateless utility functions for sending various types
5
+ of notifications during chat operations without maintaining any state.
6
+ Also includes minimal sanitization to avoid leaking sensitive tokens/paths
7
+ in filenames returned from tools.
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import re
13
+ from typing import Any, Awaitable, Callable, Dict, List, Optional
14
+ from urllib.parse import urlparse
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Type hint for update callback
19
+ UpdateCallback = Callable[[Dict[str, Any]], Awaitable[None]]
20
+
21
+
22
+ _S3_KEY_PREFIX_PATTERN = re.compile(r"^(?:\d{9,})_[0-9a-fA-F]{6,}_(.+)$")
23
+
24
+
25
+ def _sanitize_filename_value(value: Any) -> Any:
26
+ """Return a user-safe filename string with no token or internal prefixes.
27
+
28
+ - If not a string, return as-is
29
+ - Strip query string (e.g., ?token=...)
30
+ - If URL, keep basename of the path
31
+ - Else if path-like, keep basename
32
+ - If basename matches ts_hash_original.ext, return original.ext
33
+ """
34
+ if not isinstance(value, str) or not value:
35
+ return value
36
+
37
+ # Drop query
38
+ without_query = value.split("?", 1)[0]
39
+
40
+ # Extract path from URL if any
41
+ path = without_query
42
+ if without_query.startswith("http://") or without_query.startswith("https://"):
43
+ try:
44
+ parsed = urlparse(without_query)
45
+ path = parsed.path or without_query
46
+ except Exception:
47
+ path = without_query
48
+
49
+ # Basename only
50
+ basename = path.rsplit("/", 1)[-1]
51
+
52
+ # Strip known storage prefix pattern 1755396436_d71d38d7_original.csv
53
+ m = _S3_KEY_PREFIX_PATTERN.match(basename)
54
+ if m:
55
+ return m.group(1)
56
+ return basename
57
+
58
+
59
+ def _sanitize_result_for_ui(obj: Any) -> Any:
60
+ """Recursively sanitize tool result content for UI display.
61
+
62
+ Rules:
63
+ - Any key literally named 'filename' is reduced to a clean basename.
64
+ - For common structures like {'file': {'filename': ...}}, sanitize nested filename too.
65
+ - Lists and nested dicts are traversed.
66
+ """
67
+ try:
68
+ if isinstance(obj, dict):
69
+ sanitized: Dict[str, Any] = {}
70
+ for k, v in obj.items():
71
+ if k == "filename":
72
+ sanitized[k] = _sanitize_filename_value(v)
73
+ elif k == "file" and isinstance(v, dict):
74
+ # Typical shape in artifacts-like objects
75
+ inner = dict(v)
76
+ if "filename" in inner:
77
+ inner["filename"] = _sanitize_filename_value(inner.get("filename"))
78
+ sanitized[k] = _sanitize_result_for_ui(inner)
79
+ else:
80
+ sanitized[k] = _sanitize_result_for_ui(v)
81
+ return sanitized
82
+ if isinstance(obj, list):
83
+ return [_sanitize_result_for_ui(x) for x in obj]
84
+ return obj
85
+ except Exception:
86
+ # Fail open on sanitization to avoid breaking UI updates
87
+ return obj
88
+
89
+
90
+ async def safe_notify(callback: UpdateCallback, message: Dict[str, Any]) -> None:
91
+ """
92
+ Invoke callback safely, logging but suppressing exceptions.
93
+
94
+ Pure function that handles notification errors gracefully.
95
+ """
96
+ try:
97
+ await callback(message)
98
+ except Exception as e:
99
+ logger.warning(f"Update callback failed: {e}")
100
+
101
+
102
+ async def notify_tool_start(
103
+ tool_call,
104
+ parsed_args: Dict[str, Any],
105
+ update_callback: Optional[UpdateCallback]
106
+ ) -> None:
107
+ """
108
+ Send tool start notification.
109
+
110
+ Pure function that creates and sends tool start notification.
111
+ """
112
+ if not update_callback:
113
+ return
114
+
115
+ # Derive server name for display context
116
+ parts = tool_call.function.name.split("_")
117
+ server_name = "_".join(parts[:-1]) if len(parts) > 1 else "unknown"
118
+
119
+ payload = {
120
+ "type": "tool_start",
121
+ "tool_call_id": tool_call.id,
122
+ "tool_name": tool_call.function.name,
123
+ "server_name": server_name,
124
+ "arguments": parsed_args
125
+ }
126
+ await safe_notify(update_callback, payload)
127
+
128
+
129
+ async def notify_tool_complete(
130
+ tool_call,
131
+ result,
132
+ parsed_args: Dict[str, Any],
133
+ update_callback: Optional[UpdateCallback]
134
+ ) -> None:
135
+ """
136
+ Send tool completion notification with canvas handling.
137
+
138
+ Pure function that handles tool completion notifications.
139
+ """
140
+ if not update_callback:
141
+ return
142
+
143
+ # Standard completion notification (with sanitized result for UI)
144
+ result_content = getattr(result, "content", None)
145
+ # If content is JSON string, parse first so we can sanitize nested filename fields
146
+ if isinstance(result_content, str):
147
+ try:
148
+ parsed = json.loads(result_content)
149
+ sanitized_content = _sanitize_result_for_ui(parsed)
150
+ except Exception:
151
+ sanitized_content = _sanitize_result_for_ui(result_content)
152
+ else:
153
+ sanitized_content = _sanitize_result_for_ui(result_content)
154
+ complete_payload = {
155
+ "type": "tool_complete",
156
+ "tool_call_id": tool_call.id,
157
+ "tool_name": tool_call.function.name,
158
+ "success": result.success,
159
+ "result": sanitized_content
160
+ }
161
+
162
+ # Canvas tool special handling
163
+ if tool_call.function.name == "canvas_canvas":
164
+ await notify_canvas_content(parsed_args, update_callback)
165
+
166
+ # Send artifacts to frontend if available
167
+ try:
168
+ arts = getattr(result, "artifacts", None)
169
+ disp = getattr(result, "display_config", None)
170
+ if arts and isinstance(arts, list):
171
+ logger.debug(
172
+ "Tool result has artifacts/display: artifacts=%d, has_display=%s",
173
+ len(arts),
174
+ bool(disp),
175
+ )
176
+ # Send artifacts as progress_artifacts so they display in canvas
177
+ await safe_notify(update_callback, {
178
+ "type": "intermediate_update",
179
+ "update_type": "progress_artifacts",
180
+ "data": {
181
+ "artifacts": arts,
182
+ "display": disp or {},
183
+ "tool_call_id": tool_call.id,
184
+ "tool_name": tool_call.function.name
185
+ }
186
+ })
187
+ logger.info(f"Sent {len(arts)} artifact(s) from tool {tool_call.function.name} to frontend")
188
+ except Exception:
189
+ # Fail open on artifact/display logging to avoid breaking tool completion
190
+ logger.warning("Error sending artifacts to frontend", exc_info=True)
191
+
192
+ await safe_notify(update_callback, complete_payload)
193
+
194
+
195
+ async def notify_tool_progress(
196
+ tool_call_id: str,
197
+ tool_name: str,
198
+ progress: float,
199
+ total: Optional[float],
200
+ message: Optional[str],
201
+ update_callback: Optional[UpdateCallback]
202
+ ) -> None:
203
+ """
204
+ Send tool progress notification.
205
+
206
+ Emits an event shaped for the UI to render progress bars/messages.
207
+
208
+ Enhanced to support structured progress updates:
209
+ - If message starts with "MCP_UPDATE:", parse as JSON for special updates
210
+ - Supports canvas updates, system messages, and file artifacts during execution
211
+ """
212
+ if not update_callback:
213
+ return
214
+
215
+ try:
216
+ # Check for structured progress updates
217
+ if message and message.startswith("MCP_UPDATE:"):
218
+ try:
219
+ structured_data = json.loads(message[11:]) # Remove "MCP_UPDATE:" prefix
220
+ await _handle_structured_progress_update(
221
+ tool_call_id=tool_call_id,
222
+ tool_name=tool_name,
223
+ progress=progress,
224
+ total=total,
225
+ structured_data=structured_data,
226
+ update_callback=update_callback
227
+ )
228
+ return
229
+ except json.JSONDecodeError as e:
230
+ logger.warning(f"Failed to parse structured progress update: {e}")
231
+ # Fall through to regular progress handling
232
+
233
+ # Regular progress notification
234
+ pct: Optional[float] = None
235
+ if total is not None and total != 0:
236
+ try:
237
+ pct = (float(progress) / float(total)) * 100.0
238
+ except Exception:
239
+ pct = None
240
+ payload = {
241
+ "type": "tool_progress",
242
+ "tool_call_id": tool_call_id,
243
+ "tool_name": tool_name,
244
+ "progress": progress,
245
+ "total": total,
246
+ "percentage": pct,
247
+ "message": message or "",
248
+ }
249
+ await safe_notify(update_callback, payload)
250
+ except Exception as e:
251
+ logger.warning(f"Failed to emit tool_progress: {e}")
252
+
253
+
254
+ async def _handle_structured_progress_update(
255
+ tool_call_id: str,
256
+ tool_name: str,
257
+ progress: float,
258
+ total: Optional[float],
259
+ structured_data: Dict[str, Any],
260
+ update_callback: UpdateCallback
261
+ ) -> None:
262
+ """
263
+ Handle structured progress updates from MCP servers.
264
+
265
+ Supports:
266
+ - canvas_update: Display content in canvas during tool execution
267
+ - system_message: Add rich system messages to chat history
268
+ - artifacts: Send file artifacts during execution
269
+ """
270
+ update_type = structured_data.get("type")
271
+
272
+ if update_type == "canvas_update":
273
+ # Display content in canvas
274
+ content = structured_data.get("content")
275
+ if content:
276
+ await safe_notify(update_callback, {
277
+ "type": "canvas_content",
278
+ "content": content
279
+ })
280
+ logger.info(f"Tool {tool_name} sent canvas update during execution")
281
+
282
+ elif update_type == "system_message":
283
+ # Send rich system message to chat
284
+ msg_content = structured_data.get("message", "")
285
+ msg_subtype = structured_data.get("subtype", "info")
286
+ await safe_notify(update_callback, {
287
+ "type": "intermediate_update",
288
+ "update_type": "system_message",
289
+ "data": {
290
+ "message": msg_content,
291
+ "subtype": msg_subtype,
292
+ "tool_call_id": tool_call_id,
293
+ "tool_name": tool_name
294
+ }
295
+ })
296
+ logger.info(f"Tool {tool_name} sent system message during execution")
297
+
298
+ elif update_type == "artifacts":
299
+ # Send file artifacts during execution
300
+ artifacts = structured_data.get("artifacts", [])
301
+ display_config = structured_data.get("display")
302
+ if artifacts:
303
+ await safe_notify(update_callback, {
304
+ "type": "intermediate_update",
305
+ "update_type": "progress_artifacts",
306
+ "data": {
307
+ "artifacts": artifacts,
308
+ "display": display_config,
309
+ "tool_call_id": tool_call_id,
310
+ "tool_name": tool_name
311
+ }
312
+ })
313
+ logger.info(f"Tool {tool_name} sent {len(artifacts)} artifact(s) during execution")
314
+
315
+ # Still send progress info along with the structured update
316
+ pct: Optional[float] = None
317
+ if total is not None and total != 0:
318
+ try:
319
+ pct = (float(progress) / float(total)) * 100.0
320
+ except Exception:
321
+ pct = None
322
+
323
+ await safe_notify(update_callback, {
324
+ "type": "tool_progress",
325
+ "tool_call_id": tool_call_id,
326
+ "tool_name": tool_name,
327
+ "progress": progress,
328
+ "total": total,
329
+ "percentage": pct,
330
+ "message": structured_data.get("progress_message", "Processing..."),
331
+ })
332
+
333
+
334
+ async def notify_canvas_content(
335
+ parsed_args: Dict[str, Any],
336
+ update_callback: UpdateCallback
337
+ ) -> None:
338
+ """
339
+ Send canvas content notification.
340
+
341
+ Pure function that extracts and sends canvas content.
342
+ """
343
+ try:
344
+ content_arg = parsed_args.get("content") if isinstance(parsed_args, dict) else None
345
+ if content_arg:
346
+ logger.info("Emitting canvas_content event (length=%s)", len(content_arg) if isinstance(content_arg, str) else "obj")
347
+ await safe_notify(update_callback, {
348
+ "type": "canvas_content",
349
+ "content": content_arg
350
+ })
351
+ else:
352
+ logger.info("Canvas tool called without 'content' arg; skipping canvas_content event")
353
+ except Exception as e:
354
+ logger.warning("Failed to emit canvas_content event: %s", e)
355
+
356
+
357
+ async def notify_tool_error(
358
+ tool_call,
359
+ error: str,
360
+ update_callback: Optional[UpdateCallback]
361
+ ) -> None:
362
+ """
363
+ Send tool error notification.
364
+
365
+ Pure function that creates and sends error notification.
366
+ """
367
+ if not update_callback:
368
+ return
369
+
370
+ await safe_notify(update_callback, {
371
+ "type": "tool_error",
372
+ "tool_call_id": tool_call.id,
373
+ "tool_name": tool_call.function.name,
374
+ "error": error
375
+ })
376
+
377
+
378
+ async def notify_chat_response(
379
+ message: str,
380
+ has_pending_tools: bool = False,
381
+ update_callback: Optional[UpdateCallback] = None
382
+ ) -> None:
383
+ """
384
+ Send chat response notification.
385
+
386
+ Pure function that notifies about chat responses.
387
+ """
388
+ if not update_callback:
389
+ return
390
+
391
+ await safe_notify(update_callback, {
392
+ "type": "chat_response",
393
+ "message": message,
394
+ "has_pending_tools": has_pending_tools
395
+ })
396
+
397
+
398
+ async def notify_response_complete(update_callback: Optional[UpdateCallback]) -> None:
399
+ """
400
+ Send response completion notification.
401
+
402
+ Pure function that signals completion.
403
+ """
404
+ if not update_callback:
405
+ return
406
+
407
+ await safe_notify(update_callback, {"type": "response_complete"})
408
+
409
+
410
+ async def notify_tool_synthesis(
411
+ message: str,
412
+ update_callback: Optional[UpdateCallback]
413
+ ) -> None:
414
+ """
415
+ Send tool synthesis notification.
416
+
417
+ Pure function that notifies about synthesis results.
418
+ """
419
+ if not update_callback:
420
+ return
421
+
422
+ if message and message.strip():
423
+ await safe_notify(update_callback, {
424
+ "type": "tool_synthesis",
425
+ "message": message
426
+ })
427
+
428
+
429
+ async def notify_agent_update(
430
+ update_type: str,
431
+ connection,
432
+ **kwargs
433
+ ) -> None:
434
+ """
435
+ Send agent mode update notification.
436
+
437
+ Pure function that handles agent-specific notifications.
438
+ """
439
+ if not connection:
440
+ return
441
+
442
+ try:
443
+ payload = {
444
+ "type": "agent_update",
445
+ "update_type": update_type,
446
+ **kwargs
447
+ }
448
+ await connection.send_json(payload)
449
+ except Exception as e:
450
+ logger.warning(f"Agent update notification failed: {e}")
451
+
452
+
453
+ async def notify_files_update(
454
+ organized_files: Dict[str, Any],
455
+ update_callback: Optional[UpdateCallback]
456
+ ) -> None:
457
+ """
458
+ Send files update notification.
459
+
460
+ Pure function that notifies about file changes.
461
+ """
462
+ if not update_callback:
463
+ return
464
+
465
+ await safe_notify(update_callback, {
466
+ "type": "intermediate_update",
467
+ "update_type": "files_update",
468
+ "data": organized_files
469
+ })
470
+
471
+
472
+ async def notify_canvas_files(
473
+ canvas_files: List[Dict[str, Any]],
474
+ update_callback: Optional[UpdateCallback]
475
+ ) -> None:
476
+ """
477
+ Send canvas files notification.
478
+
479
+ Pure function that notifies about canvas-displayable files.
480
+ """
481
+ if not update_callback or not canvas_files:
482
+ return
483
+
484
+ await safe_notify(update_callback, {
485
+ "type": "intermediate_update",
486
+ "update_type": "canvas_files",
487
+ "data": {"files": canvas_files}
488
+ })
489
+
490
+
491
+ async def notify_tool_log(
492
+ server_name: str,
493
+ tool_name: Optional[str],
494
+ tool_call_id: Optional[str],
495
+ level: str,
496
+ message: str,
497
+ extra: Dict[str, Any],
498
+ update_callback: UpdateCallback
499
+ ) -> None:
500
+ """Send a log message from an MCP tool to the UI.
501
+
502
+ Args:
503
+ server_name: Name of the MCP server
504
+ tool_name: Name of the tool (if during tool execution)
505
+ tool_call_id: ID of the tool call (if during tool execution)
506
+ level: Log level (debug, info, warning, error, etc.)
507
+ message: Log message
508
+ extra: Extra metadata from the log
509
+ update_callback: Callback to send updates
510
+ """
511
+ await safe_notify(update_callback, {
512
+ "type": "intermediate_update",
513
+ "update_type": "tool_log",
514
+ "data": {
515
+ "server_name": server_name,
516
+ "tool_name": tool_name,
517
+ "tool_call_id": tool_call_id,
518
+ "level": level,
519
+ "message": message,
520
+ "extra": extra,
521
+ }
522
+ })
523
+
524
+
525
+ def create_error_response(error_message: str, message_type: str = "error") -> Dict[str, str]:
526
+ """
527
+ Create standardized error response.
528
+
529
+ Pure function that creates consistent error responses.
530
+ """
531
+ return {
532
+ "type": message_type,
533
+ "message": error_message
534
+ }
535
+
536
+
537
+ def create_chat_response(message: str, message_type: str = "chat_response") -> Dict[str, str]:
538
+ """
539
+ Create standardized chat response.
540
+
541
+ Pure function that creates consistent chat responses.
542
+ """
543
+ return {
544
+ "type": message_type,
545
+ "message": message
546
+ }