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.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- 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
|
+
}
|