mito-ai 0.1.45__py3-none-any.whl → 0.1.47__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 mito-ai might be problematic. Click here for more details.
- mito_ai/__init__.py +10 -1
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +90 -5
- mito_ai/app_deploy/handlers.py +97 -77
- mito_ai/app_deploy/models.py +16 -12
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/handlers.py +18 -20
- mito_ai/completions/models.py +4 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +6 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
- mito_ai/completions/prompt_builders/utils.py +14 -0
- mito_ai/constants.py +3 -0
- mito_ai/path_utils.py +56 -0
- mito_ai/streamlit_conversion/agent_utils.py +27 -106
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +166 -53
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
- mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +1 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +93 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +103 -119
- mito_ai/streamlit_conversion/streamlit_utils.py +18 -68
- mito_ai/streamlit_conversion/validate_streamlit_app.py +78 -96
- mito_ai/streamlit_preview/handlers.py +44 -85
- mito_ai/streamlit_preview/manager.py +6 -6
- mito_ai/streamlit_preview/utils.py +19 -18
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/message_history/test_message_history_utils.py +43 -19
- mito_ai/tests/providers/test_anthropic_client.py +178 -6
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +226 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +87 -114
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +42 -45
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +20 -14
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +13 -16
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +22 -26
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/anthropic_utils.py +8 -6
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/message_history_utils.py +7 -4
- mito_ai/utils/telemetry_utils.py +79 -11
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js +2126 -363
- mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +1 -0
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.684f82575fcc2e3b350c.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js +9 -9
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.684f82575fcc2e3b350c.js.map → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js.map +1 -1
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/METADATA +1 -1
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/RECORD +81 -69
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js.map +0 -1
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/licenses/LICENSE +0 -0
mito_ai/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import List, Dict
|
|
|
5
5
|
from jupyter_server.utils import url_path_join
|
|
6
6
|
from mito_ai.completions.handlers import CompletionHandler
|
|
7
7
|
from mito_ai.completions.providers import OpenAIProvider
|
|
8
|
+
from mito_ai.completions.message_history import GlobalMessageHistory
|
|
8
9
|
from mito_ai.app_deploy.handlers import AppDeployHandler
|
|
9
10
|
from mito_ai.streamlit_preview.handlers import StreamlitPreviewHandler
|
|
10
11
|
from mito_ai.log.urls import get_log_urls
|
|
@@ -16,6 +17,8 @@ from mito_ai.auth.urls import get_auth_urls
|
|
|
16
17
|
from mito_ai.streamlit_preview.urls import get_streamlit_preview_urls
|
|
17
18
|
from mito_ai.app_manager.handlers import AppManagerHandler
|
|
18
19
|
from mito_ai.file_uploads.urls import get_file_uploads_urls
|
|
20
|
+
from mito_ai.user.urls import get_user_urls
|
|
21
|
+
from mito_ai.chat_history.urls import get_chat_history_urls
|
|
19
22
|
|
|
20
23
|
# Force Matplotlib to use the Jupyter inline backend.
|
|
21
24
|
# Background: importing Streamlit sets os.environ["MPLBACKEND"] = "Agg" very early.
|
|
@@ -62,13 +65,17 @@ def _load_jupyter_server_extension(server_app) -> None: # type: ignore
|
|
|
62
65
|
base_url = web_app.settings["base_url"]
|
|
63
66
|
|
|
64
67
|
open_ai_provider = OpenAIProvider(config=server_app.config)
|
|
68
|
+
|
|
69
|
+
# Create a single GlobalMessageHistory instance for the entire server
|
|
70
|
+
# This ensures thread-safe access to the .mito/ai-chats directory
|
|
71
|
+
global_message_history = GlobalMessageHistory()
|
|
65
72
|
|
|
66
73
|
# WebSocket handlers
|
|
67
74
|
handlers = [
|
|
68
75
|
(
|
|
69
76
|
url_path_join(base_url, "mito-ai", "completions"),
|
|
70
77
|
CompletionHandler,
|
|
71
|
-
{"llm": open_ai_provider},
|
|
78
|
+
{"llm": open_ai_provider, "message_history": global_message_history},
|
|
72
79
|
),
|
|
73
80
|
(
|
|
74
81
|
url_path_join(base_url, "mito-ai", "app-deploy"),
|
|
@@ -100,6 +107,8 @@ def _load_jupyter_server_extension(server_app) -> None: # type: ignore
|
|
|
100
107
|
handlers.extend(get_auth_urls(base_url)) # type: ignore
|
|
101
108
|
handlers.extend(get_streamlit_preview_urls(base_url)) # type: ignore
|
|
102
109
|
handlers.extend(get_file_uploads_urls(base_url)) # type: ignore
|
|
110
|
+
handlers.extend(get_user_urls(base_url)) # type: ignore
|
|
111
|
+
handlers.extend(get_chat_history_urls(base_url, global_message_history)) # type: ignore
|
|
103
112
|
|
|
104
113
|
web_app.add_handlers(host_pattern, handlers)
|
|
105
114
|
server_app.log.info("Loaded the mito_ai server extension")
|
mito_ai/_version.py
CHANGED
mito_ai/anthropic_client.py
CHANGED
|
@@ -5,9 +5,9 @@ import json
|
|
|
5
5
|
import anthropic
|
|
6
6
|
from typing import Dict, Any, Optional, Tuple, Union, Callable, List, cast
|
|
7
7
|
|
|
8
|
-
from anthropic.types import Message, MessageParam
|
|
9
|
-
from mito_ai.completions.models import
|
|
10
|
-
from mito_ai.
|
|
8
|
+
from anthropic.types import Message, MessageParam, TextBlockParam
|
|
9
|
+
from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem, MessageType
|
|
10
|
+
from mito_ai.constants import MESSAGE_HISTORY_TRIM_THRESHOLD
|
|
11
11
|
from openai.types.chat import ChatCompletionMessageParam
|
|
12
12
|
from mito_ai.utils.anthropic_utils import get_anthropic_completion_from_mito_server, stream_anthropic_completion_from_mito_server, get_anthropic_completion_function_params
|
|
13
13
|
|
|
@@ -125,6 +125,90 @@ def get_anthropic_system_prompt_and_messages(messages: List[ChatCompletionMessag
|
|
|
125
125
|
return system_prompt, anthropic_messages
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def add_cache_control_to_message(message: MessageParam) -> MessageParam:
|
|
129
|
+
"""
|
|
130
|
+
Adds cache_control to a message's content.
|
|
131
|
+
Handles both string content and list of content blocks.
|
|
132
|
+
"""
|
|
133
|
+
content = message.get("content")
|
|
134
|
+
|
|
135
|
+
if isinstance(content, str):
|
|
136
|
+
# Simple string content - convert to list format with cache_control
|
|
137
|
+
return {
|
|
138
|
+
"role": message["role"],
|
|
139
|
+
"content": [
|
|
140
|
+
{
|
|
141
|
+
"type": "text",
|
|
142
|
+
"text": content,
|
|
143
|
+
"cache_control": {"type": "ephemeral"}
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
elif isinstance(content, list) and len(content) > 0:
|
|
149
|
+
# List of content blocks - add cache_control to last block
|
|
150
|
+
content_blocks = content.copy()
|
|
151
|
+
last_block = content_blocks[-1].copy()
|
|
152
|
+
last_block["cache_control"] = {"type": "ephemeral"}
|
|
153
|
+
content_blocks[-1] = last_block
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"role": message["role"],
|
|
157
|
+
"content": content_blocks
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
# Edge case: empty or malformed content
|
|
162
|
+
return message
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_anthropic_system_prompt_and_messages_with_caching(messages: List[ChatCompletionMessageParam]) -> Tuple[
|
|
166
|
+
Union[str, List[TextBlockParam], anthropic.Omit], List[MessageParam]]:
|
|
167
|
+
"""
|
|
168
|
+
Convert a list of OpenAI messages to a list of Anthropic messages with caching applied.
|
|
169
|
+
|
|
170
|
+
Caching Strategy:
|
|
171
|
+
1. System prompt (static) → Always cached
|
|
172
|
+
2. Stable conversation history → Cache at keep_recent boundary
|
|
173
|
+
3. Recent messages → Never cached (always fresh)
|
|
174
|
+
|
|
175
|
+
The keep_recent parameter determines which messages are stable and won't be trimmed.
|
|
176
|
+
We cache at the keep_recent boundary because those messages are guaranteed to be stable.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
# Get the base system prompt and messages
|
|
180
|
+
system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages(messages)
|
|
181
|
+
|
|
182
|
+
# 1. Cache the system prompt always
|
|
183
|
+
# If the system prompt is something like anthropic.Omit, we don't need to cache it
|
|
184
|
+
cached_system_prompt: Union[str, List[TextBlockParam], anthropic.Omit] = system_prompt
|
|
185
|
+
if isinstance(system_prompt, str):
|
|
186
|
+
cached_system_prompt = [{
|
|
187
|
+
"type": "text",
|
|
188
|
+
"text": system_prompt,
|
|
189
|
+
"cache_control": {"type": "ephemeral"}
|
|
190
|
+
}]
|
|
191
|
+
|
|
192
|
+
# 2. Cache conversation history at the boundary where the messages are stable.
|
|
193
|
+
# Messages are stable after they are more than MESSAGE_HISTORY_TRIM_THRESHOLD old.
|
|
194
|
+
# At this point, the messages are not edited anymore, so they will not invalidate the cache.
|
|
195
|
+
# If we included the messages before the boundary in the cache, then every time we send a new
|
|
196
|
+
# message, we would invalidate the cache and we would never get a cache hit except for the system prompt.
|
|
197
|
+
messages_with_cache = []
|
|
198
|
+
|
|
199
|
+
if len(anthropic_messages) > 0:
|
|
200
|
+
cache_boundary = len(anthropic_messages) - MESSAGE_HISTORY_TRIM_THRESHOLD - 1
|
|
201
|
+
|
|
202
|
+
# Add all messages, but only add cache_control to the message at the boundary
|
|
203
|
+
for i, msg in enumerate(anthropic_messages):
|
|
204
|
+
if i == cache_boundary:
|
|
205
|
+
messages_with_cache.append(add_cache_control_to_message(msg))
|
|
206
|
+
else:
|
|
207
|
+
messages_with_cache.append(msg)
|
|
208
|
+
|
|
209
|
+
return cached_system_prompt, messages_with_cache
|
|
210
|
+
|
|
211
|
+
|
|
128
212
|
class AnthropicClient:
|
|
129
213
|
"""
|
|
130
214
|
A client for interacting with the Anthropic API or the Mito server fallback.
|
|
@@ -149,7 +233,7 @@ class AnthropicClient:
|
|
|
149
233
|
"""
|
|
150
234
|
Get a response from Claude or the Mito server that adheres to the AgentResponse format.
|
|
151
235
|
"""
|
|
152
|
-
anthropic_system_prompt, anthropic_messages =
|
|
236
|
+
anthropic_system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
|
|
153
237
|
|
|
154
238
|
provider_data = get_anthropic_completion_function_params(
|
|
155
239
|
message_type=message_type,
|
|
@@ -166,6 +250,7 @@ class AnthropicClient:
|
|
|
166
250
|
# Unpack provider_data for direct API call
|
|
167
251
|
assert self.client is not None
|
|
168
252
|
response = self.client.messages.create(**provider_data)
|
|
253
|
+
|
|
169
254
|
if provider_data.get("tool_choice") is not None:
|
|
170
255
|
result = extract_and_parse_anthropic_json_response(response)
|
|
171
256
|
return json.dumps(result) if not isinstance(result, str) else result
|
|
@@ -192,7 +277,7 @@ class AnthropicClient:
|
|
|
192
277
|
async def stream_completions(self, messages: List[ChatCompletionMessageParam], model: str, message_id: str, message_type: MessageType,
|
|
193
278
|
reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None]) -> str:
|
|
194
279
|
try:
|
|
195
|
-
anthropic_system_prompt, anthropic_messages =
|
|
280
|
+
anthropic_system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
|
|
196
281
|
accumulated_response = ""
|
|
197
282
|
|
|
198
283
|
if self.api_key:
|
mito_ai/app_deploy/handlers.py
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
6
|
import logging
|
|
7
|
-
from typing import Any, Union, List
|
|
7
|
+
from typing import Any, Union, List, Optional
|
|
8
8
|
import tempfile
|
|
9
|
-
from mito_ai.
|
|
9
|
+
from mito_ai.path_utils import AbsoluteAppPath, does_app_path_exists, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
|
|
10
10
|
from mito_ai.utils.create import initialize_user
|
|
11
|
+
from mito_ai.utils.error_classes import StreamlitDeploymentError
|
|
11
12
|
from mito_ai.utils.version_utils import is_pro
|
|
12
13
|
from mito_ai.utils.websocket_base import BaseWebSocketHandler
|
|
13
14
|
from mito_ai.app_deploy.app_deploy_utils import add_files_to_zip
|
|
@@ -16,11 +17,13 @@ from mito_ai.app_deploy.models import (
|
|
|
16
17
|
AppDeployError,
|
|
17
18
|
DeployAppRequest,
|
|
18
19
|
ErrorMessage,
|
|
19
|
-
MessageType
|
|
20
20
|
)
|
|
21
|
+
from mito_ai.completions.models import MessageType
|
|
21
22
|
from mito_ai.logger import get_logger
|
|
22
23
|
from mito_ai.constants import ACTIVE_STREAMLIT_BASE_URL
|
|
24
|
+
from mito_ai.utils.telemetry_utils import log_streamlit_app_deployment_failure
|
|
23
25
|
import requests
|
|
26
|
+
import traceback
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
class AppDeployHandler(BaseWebSocketHandler):
|
|
@@ -67,41 +70,46 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
67
70
|
self.log.debug("App builder message received: %s", message)
|
|
68
71
|
|
|
69
72
|
try:
|
|
70
|
-
# Ensure message is a string before parsing
|
|
71
|
-
if not isinstance(message, str):
|
|
72
|
-
raise ValueError("Message must be a string")
|
|
73
|
-
|
|
74
73
|
parsed_message = self.parse_message(message)
|
|
75
74
|
message_type = parsed_message.get('type')
|
|
76
75
|
|
|
77
76
|
if message_type == MessageType.DEPLOY_APP.value:
|
|
78
77
|
# Handle build app request
|
|
79
78
|
deploy_app_request = DeployAppRequest(**parsed_message)
|
|
80
|
-
await self._handle_deploy_app(deploy_app_request)
|
|
79
|
+
response = await self._handle_deploy_app(deploy_app_request)
|
|
80
|
+
self.reply(response)
|
|
81
81
|
else:
|
|
82
82
|
self.log.error(f"Unknown message type: {message_type}")
|
|
83
83
|
error = AppDeployError(
|
|
84
84
|
error_type="InvalidRequest",
|
|
85
|
-
|
|
85
|
+
message=f"Unknown message type: {message_type}",
|
|
86
|
+
error_code=400
|
|
86
87
|
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
except
|
|
88
|
+
raise StreamlitDeploymentError(error)
|
|
89
|
+
|
|
90
|
+
except StreamlitDeploymentError as e:
|
|
90
91
|
self.log.error("Invalid app builder request", exc_info=e)
|
|
91
|
-
|
|
92
|
-
self.reply(
|
|
92
|
+
log_streamlit_app_deployment_failure('mito_server_key', MessageType.DEPLOY_APP, e.error.__dict__)
|
|
93
|
+
self.reply(
|
|
94
|
+
DeployAppReply(
|
|
95
|
+
parent_id=e.message_id,
|
|
96
|
+
url="",
|
|
97
|
+
error=ErrorMessage(**e.error.__dict__)
|
|
98
|
+
)
|
|
99
|
+
)
|
|
93
100
|
except Exception as e:
|
|
94
101
|
self.log.error("Error handling app builder message", exc_info=e)
|
|
95
102
|
error = AppDeployError.from_exception(
|
|
96
103
|
e,
|
|
97
104
|
hint="An error occurred while building the app. Please check the logs for details."
|
|
98
105
|
)
|
|
106
|
+
log_streamlit_app_deployment_failure('mito_server_key', MessageType.DEPLOY_APP, error.__dict__)
|
|
99
107
|
self.reply(ErrorMessage(**error.__dict__))
|
|
100
108
|
|
|
101
109
|
latency_ms = round((time.time() - start) * 1000)
|
|
102
110
|
self.log.info(f"App builder handler processed in {latency_ms} ms.")
|
|
103
111
|
|
|
104
|
-
async def _handle_deploy_app(self, message: DeployAppRequest) ->
|
|
112
|
+
async def _handle_deploy_app(self, message: DeployAppRequest) -> DeployAppReply:
|
|
105
113
|
"""Handle a build app request.
|
|
106
114
|
|
|
107
115
|
Args:
|
|
@@ -114,19 +122,22 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
114
122
|
|
|
115
123
|
if not message_id:
|
|
116
124
|
self.log.error("Missing message_id in request")
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
error = AppDeployError(
|
|
126
|
+
error_type="BadRequest",
|
|
127
|
+
message="Missing message_id in request",
|
|
128
|
+
error_code=400,
|
|
129
|
+
message_id=message_id
|
|
130
|
+
)
|
|
131
|
+
raise StreamlitDeploymentError(error)
|
|
132
|
+
|
|
119
133
|
if not notebook_path:
|
|
120
134
|
error = AppDeployError(
|
|
121
135
|
error_type="InvalidRequest",
|
|
122
|
-
|
|
136
|
+
message="Missing 'notebook_path' parameter",
|
|
137
|
+
error_code=400,
|
|
138
|
+
message_id=message_id
|
|
123
139
|
)
|
|
124
|
-
|
|
125
|
-
parent_id=message_id,
|
|
126
|
-
url="",
|
|
127
|
-
error=error
|
|
128
|
-
))
|
|
129
|
-
return
|
|
140
|
+
raise StreamlitDeploymentError(error)
|
|
130
141
|
|
|
131
142
|
# Validate JWT token if provided
|
|
132
143
|
token_preview = jwt_token[:20] if jwt_token else "No token provided"
|
|
@@ -136,55 +147,42 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
136
147
|
self.log.error("JWT token validation failed")
|
|
137
148
|
error = AppDeployError(
|
|
138
149
|
error_type="Unauthorized",
|
|
139
|
-
|
|
140
|
-
hint="Please sign in again to deploy your app."
|
|
150
|
+
message="Invalid authentication token",
|
|
151
|
+
hint="Please sign in again to deploy your app.",
|
|
152
|
+
error_code=401,
|
|
153
|
+
message_id=message_id
|
|
141
154
|
)
|
|
142
|
-
|
|
143
|
-
parent_id=message_id,
|
|
144
|
-
url="",
|
|
145
|
-
error=error
|
|
146
|
-
))
|
|
147
|
-
return
|
|
155
|
+
raise StreamlitDeploymentError(error)
|
|
148
156
|
else:
|
|
149
157
|
self.log.info("JWT token validation successful")
|
|
150
|
-
|
|
151
|
-
try:
|
|
152
|
-
notebook_path = str(notebook_path) if notebook_path else ""
|
|
153
158
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
159
|
+
notebook_path = str(notebook_path) if notebook_path else ""
|
|
160
|
+
absolute_notebook_path = get_absolute_notebook_path(notebook_path)
|
|
161
|
+
absolute_app_directory = get_absolute_notebook_dir_path(absolute_notebook_path)
|
|
162
|
+
app_path = get_absolute_app_path(absolute_app_directory)
|
|
163
|
+
|
|
164
|
+
# Check if the app.py file exists
|
|
165
|
+
app_path_exists = does_app_path_exists(app_path)
|
|
166
|
+
if not app_path_exists:
|
|
167
|
+
error = AppDeployError(
|
|
168
|
+
error_type="AppNotFound",
|
|
169
|
+
message="App not found",
|
|
170
|
+
hint="Please make sure the app.py file exists in the same directory as the notebook.",
|
|
171
|
+
error_code=400,
|
|
172
|
+
message_id=message_id
|
|
173
|
+
)
|
|
174
|
+
raise StreamlitDeploymentError(error)
|
|
175
|
+
|
|
176
|
+
# Finally, deploy the app
|
|
177
|
+
deploy_url = await self._deploy_app(app_path, files_to_upload, message_id, jwt_token)
|
|
178
|
+
|
|
179
|
+
# Send the response
|
|
180
|
+
return DeployAppReply(
|
|
181
|
+
parent_id=message_id,
|
|
182
|
+
url=deploy_url if deploy_url else ""
|
|
183
|
+
)
|
|
184
|
+
|
|
172
185
|
|
|
173
|
-
# Send the response
|
|
174
|
-
self.reply(DeployAppReply(
|
|
175
|
-
parent_id=message_id,
|
|
176
|
-
url=deploy_url
|
|
177
|
-
))
|
|
178
|
-
|
|
179
|
-
except Exception as e:
|
|
180
|
-
self.log.error(f"Error building app: {e}", exc_info=e)
|
|
181
|
-
error = AppDeployError.from_exception(e)
|
|
182
|
-
self.reply(DeployAppReply(
|
|
183
|
-
parent_id=message_id,
|
|
184
|
-
url="",
|
|
185
|
-
error=error
|
|
186
|
-
))
|
|
187
|
-
|
|
188
186
|
def _validate_jwt_token(self, token: str) -> bool:
|
|
189
187
|
"""Basic JWT token validation logic.
|
|
190
188
|
|
|
@@ -219,7 +217,7 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
219
217
|
return False
|
|
220
218
|
|
|
221
219
|
|
|
222
|
-
async def _deploy_app(self, app_path:
|
|
220
|
+
async def _deploy_app(self, app_path: AbsoluteAppPath, files_to_upload:List[str], message_id: str, jwt_token: str = '') -> Optional[str]:
|
|
223
221
|
"""Deploy the app using pre-signed URLs.
|
|
224
222
|
|
|
225
223
|
Args:
|
|
@@ -255,7 +253,7 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
255
253
|
expected_app_url = url_data['expected_app_url']
|
|
256
254
|
|
|
257
255
|
self.log.info(f"Received pre-signed URL. App will be available at: {expected_app_url}")
|
|
258
|
-
|
|
256
|
+
|
|
259
257
|
# Step 2: Create a zip file of the app.
|
|
260
258
|
temp_zip_path = None
|
|
261
259
|
try:
|
|
@@ -269,7 +267,14 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
269
267
|
upload_response = await self._upload_app_to_s3(temp_zip_path, presigned_url)
|
|
270
268
|
except Exception as e:
|
|
271
269
|
self.log.error(f"Error zipping app: {e}")
|
|
272
|
-
|
|
270
|
+
error = AppDeployError(
|
|
271
|
+
error_type="ZippingError",
|
|
272
|
+
message=f"Error zipping app: {e}",
|
|
273
|
+
traceback=traceback.format_exc(),
|
|
274
|
+
error_code=500,
|
|
275
|
+
message_id=message_id
|
|
276
|
+
)
|
|
277
|
+
raise StreamlitDeploymentError(error)
|
|
273
278
|
finally:
|
|
274
279
|
# Clean up
|
|
275
280
|
if temp_zip_path is not None:
|
|
@@ -278,20 +283,35 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
278
283
|
self.log.info(f"Upload successful! Status code: {upload_response.status_code}")
|
|
279
284
|
|
|
280
285
|
self.log.info(f"Deployment initiated. App will be available at: {expected_app_url}")
|
|
281
|
-
return expected_app_url
|
|
286
|
+
return str(expected_app_url)
|
|
282
287
|
|
|
283
288
|
except requests.exceptions.RequestException as e:
|
|
284
289
|
self.log.error(f"Error during API request: {e}")
|
|
285
290
|
if hasattr(e, 'response') and e.response is not None:
|
|
286
291
|
error_detail = e.response.json()
|
|
292
|
+
error_message = error_detail.get('error', "")
|
|
287
293
|
self.log.error(f"Server error details: {error_detail}")
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
294
|
+
else:
|
|
295
|
+
error_message = str(e)
|
|
296
|
+
|
|
297
|
+
error = AppDeployError(
|
|
298
|
+
error_type="APIException",
|
|
299
|
+
message=str(error_message),
|
|
300
|
+
traceback=traceback.format_exc(),
|
|
301
|
+
error_code=500,
|
|
302
|
+
message_id=message_id
|
|
303
|
+
)
|
|
304
|
+
raise StreamlitDeploymentError(error)
|
|
291
305
|
except Exception as e:
|
|
292
306
|
self.log.error(f"Error during deployment: {str(e)}")
|
|
293
|
-
|
|
294
|
-
|
|
307
|
+
error = AppDeployError(
|
|
308
|
+
error_type="DeploymentException",
|
|
309
|
+
message=str(e),
|
|
310
|
+
traceback=traceback.format_exc(),
|
|
311
|
+
error_code=500,
|
|
312
|
+
message_id=message_id
|
|
313
|
+
)
|
|
314
|
+
raise StreamlitDeploymentError(error)
|
|
295
315
|
|
|
296
316
|
async def _upload_app_to_s3(self, app_path: str, presigned_url: str) -> requests.Response:
|
|
297
317
|
"""Upload the app to S3 using the presigned URL."""
|
mito_ai/app_deploy/models.py
CHANGED
|
@@ -2,15 +2,10 @@
|
|
|
2
2
|
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
3
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
|
|
5
|
+
import traceback
|
|
6
6
|
from typing import Literal, Optional, List
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class MessageType(str, Enum):
|
|
10
|
-
"""Types of app deploy messages."""
|
|
11
|
-
DEPLOY_APP = "deploy-app"
|
|
12
|
-
|
|
13
|
-
|
|
14
9
|
@dataclass(frozen=True)
|
|
15
10
|
class AppDeployError:
|
|
16
11
|
"""Error information for app deploy operations."""
|
|
@@ -19,7 +14,13 @@ class AppDeployError:
|
|
|
19
14
|
error_type: str
|
|
20
15
|
|
|
21
16
|
# Error title.
|
|
22
|
-
|
|
17
|
+
message: str
|
|
18
|
+
|
|
19
|
+
#ID of parent to resolve response
|
|
20
|
+
message_id: Optional[str] = "InvalidMessageID"
|
|
21
|
+
|
|
22
|
+
# Error code
|
|
23
|
+
error_code: Optional[int] = 500
|
|
23
24
|
|
|
24
25
|
# Error traceback.
|
|
25
26
|
traceback: Optional[str] = None
|
|
@@ -28,21 +29,24 @@ class AppDeployError:
|
|
|
28
29
|
hint: Optional[str] = None
|
|
29
30
|
|
|
30
31
|
@classmethod
|
|
31
|
-
def from_exception(cls, e: Exception, hint: Optional[str] = None) -> "AppDeployError":
|
|
32
|
+
def from_exception(cls, e: Exception, hint: Optional[str] = None, error_code: Optional[int] = 500) -> "AppDeployError":
|
|
32
33
|
"""Create an error from an exception.
|
|
33
34
|
|
|
34
35
|
Args:
|
|
35
36
|
e: The exception.
|
|
36
37
|
hint: Optional hint to fix the error.
|
|
38
|
+
error_code: Optional error code which defaults to 500
|
|
37
39
|
|
|
38
40
|
Returns:
|
|
39
41
|
The app builder error.
|
|
40
42
|
"""
|
|
43
|
+
tb_str = "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
41
44
|
return cls(
|
|
42
45
|
error_type=type(e).__name__,
|
|
43
|
-
|
|
44
|
-
traceback=
|
|
46
|
+
message=str(e),
|
|
47
|
+
traceback=tb_str,
|
|
45
48
|
hint=hint,
|
|
49
|
+
error_code=error_code
|
|
46
50
|
)
|
|
47
51
|
|
|
48
52
|
|
|
@@ -59,7 +63,7 @@ class DeployAppRequest:
|
|
|
59
63
|
"""Request to deploy an app."""
|
|
60
64
|
|
|
61
65
|
# Request type.
|
|
62
|
-
type: Literal["
|
|
66
|
+
type: Literal["deploy_app"]
|
|
63
67
|
|
|
64
68
|
# Message ID.
|
|
65
69
|
message_id: str
|
|
@@ -88,4 +92,4 @@ class DeployAppReply:
|
|
|
88
92
|
error: Optional[AppDeployError] = None
|
|
89
93
|
|
|
90
94
|
# Type of reply.
|
|
91
|
-
type: Literal["
|
|
95
|
+
type: Literal["deploy_app"] = "deploy_app"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import tornado
|
|
5
|
+
from typing import List, Any
|
|
6
|
+
from jupyter_server.base.handlers import APIHandler
|
|
7
|
+
from mito_ai.completions.message_history import GlobalMessageHistory
|
|
8
|
+
from mito_ai.completions.models import ChatThreadMetadata
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChatHistoryHandler(APIHandler):
|
|
12
|
+
"""
|
|
13
|
+
Endpoints for working with chat history threads.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def initialize(self, message_history: GlobalMessageHistory) -> None:
|
|
17
|
+
"""Initialize the handler with the global message history instance."""
|
|
18
|
+
super().initialize()
|
|
19
|
+
self._message_history = message_history
|
|
20
|
+
|
|
21
|
+
@tornado.web.authenticated
|
|
22
|
+
def get(self, *args: Any, **kwargs: Any) -> None:
|
|
23
|
+
"""Get all chat history threads or a specific thread by ID."""
|
|
24
|
+
try:
|
|
25
|
+
# Check if a specific thread ID is provided in the URL
|
|
26
|
+
thread_id = kwargs.get("thread_id")
|
|
27
|
+
|
|
28
|
+
if thread_id:
|
|
29
|
+
# Get specific thread
|
|
30
|
+
if thread_id in self._message_history._chat_threads:
|
|
31
|
+
thread = self._message_history._chat_threads[thread_id]
|
|
32
|
+
thread_data = {
|
|
33
|
+
"thread_id": thread.thread_id,
|
|
34
|
+
"name": thread.name,
|
|
35
|
+
"creation_ts": thread.creation_ts,
|
|
36
|
+
"last_interaction_ts": thread.last_interaction_ts,
|
|
37
|
+
"display_history": thread.display_history,
|
|
38
|
+
"ai_optimized_history": thread.ai_optimized_history,
|
|
39
|
+
}
|
|
40
|
+
self.finish(thread_data)
|
|
41
|
+
else:
|
|
42
|
+
self.set_status(404)
|
|
43
|
+
self.finish({"error": f"Thread with ID {thread_id} not found"})
|
|
44
|
+
else:
|
|
45
|
+
# Get all threads
|
|
46
|
+
threads: List[ChatThreadMetadata] = self._message_history.get_threads()
|
|
47
|
+
|
|
48
|
+
# Convert to dict format for JSON serialization
|
|
49
|
+
threads_data = [
|
|
50
|
+
{
|
|
51
|
+
"thread_id": thread.thread_id,
|
|
52
|
+
"name": thread.name,
|
|
53
|
+
"creation_ts": thread.creation_ts,
|
|
54
|
+
"last_interaction_ts": thread.last_interaction_ts,
|
|
55
|
+
}
|
|
56
|
+
for thread in threads
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
self.finish({"threads": threads_data})
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.set_status(500)
|
|
63
|
+
self.finish({"error": str(e)})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from typing import List, Tuple, Any
|
|
5
|
+
from jupyter_server.utils import url_path_join
|
|
6
|
+
from mito_ai.chat_history.handlers import ChatHistoryHandler
|
|
7
|
+
from mito_ai.completions.message_history import GlobalMessageHistory
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_chat_history_urls(base_url: str, message_history: GlobalMessageHistory) -> List[Tuple[str, Any, dict]]:
|
|
11
|
+
"""Get all chat history related URL patterns.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
base_url: The base URL for the Jupyter server
|
|
15
|
+
message_history: The global message history instance
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
List of (url_pattern, handler_class, handler_kwargs) tuples
|
|
19
|
+
"""
|
|
20
|
+
BASE_URL = base_url + "/mito-ai/chat-history"
|
|
21
|
+
return [
|
|
22
|
+
(
|
|
23
|
+
url_path_join(BASE_URL, "threads"),
|
|
24
|
+
ChatHistoryHandler,
|
|
25
|
+
{"message_history": message_history},
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
url_path_join(BASE_URL, "threads", "(?P<thread_id>[^/]+)"),
|
|
29
|
+
ChatHistoryHandler,
|
|
30
|
+
{"message_history": message_history},
|
|
31
|
+
),
|
|
32
|
+
]
|