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.

Files changed (82) hide show
  1. mito_ai/__init__.py +10 -1
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +90 -5
  4. mito_ai/app_deploy/handlers.py +97 -77
  5. mito_ai/app_deploy/models.py +16 -12
  6. mito_ai/chat_history/handlers.py +63 -0
  7. mito_ai/chat_history/urls.py +32 -0
  8. mito_ai/completions/handlers.py +18 -20
  9. mito_ai/completions/models.py +4 -1
  10. mito_ai/completions/prompt_builders/agent_execution_prompt.py +6 -1
  11. mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
  12. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  13. mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
  14. mito_ai/completions/prompt_builders/utils.py +14 -0
  15. mito_ai/constants.py +3 -0
  16. mito_ai/path_utils.py +56 -0
  17. mito_ai/streamlit_conversion/agent_utils.py +27 -106
  18. mito_ai/streamlit_conversion/prompts/prompt_constants.py +166 -53
  19. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
  20. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
  21. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
  22. mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +1 -0
  23. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  24. mito_ai/streamlit_conversion/search_replace_utils.py +93 -0
  25. mito_ai/streamlit_conversion/streamlit_agent_handler.py +103 -119
  26. mito_ai/streamlit_conversion/streamlit_utils.py +18 -68
  27. mito_ai/streamlit_conversion/validate_streamlit_app.py +78 -96
  28. mito_ai/streamlit_preview/handlers.py +44 -85
  29. mito_ai/streamlit_preview/manager.py +6 -6
  30. mito_ai/streamlit_preview/utils.py +19 -18
  31. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  32. mito_ai/tests/message_history/test_message_history_utils.py +43 -19
  33. mito_ai/tests/providers/test_anthropic_client.py +178 -6
  34. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +226 -0
  35. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +87 -114
  36. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +42 -45
  37. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +20 -14
  38. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +13 -16
  39. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +22 -26
  40. mito_ai/tests/user/__init__.py +2 -0
  41. mito_ai/tests/user/test_user.py +120 -0
  42. mito_ai/user/handlers.py +45 -0
  43. mito_ai/user/urls.py +21 -0
  44. mito_ai/utils/anthropic_utils.py +8 -6
  45. mito_ai/utils/create.py +17 -1
  46. mito_ai/utils/error_classes.py +42 -0
  47. mito_ai/utils/message_history_utils.py +7 -4
  48. mito_ai/utils/telemetry_utils.py +79 -11
  49. {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
  50. {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  51. {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
  52. 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
  53. mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +1 -0
  54. 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
  55. 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
  56. {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/METADATA +1 -1
  57. {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/RECORD +81 -69
  58. mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js.map +0 -1
  59. {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  60. {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
  61. {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
  62. {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
  63. {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {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
  77. {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
  78. {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
  79. {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
  80. {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/WHEEL +0 -0
  81. {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/entry_points.txt +0 -0
  82. {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
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.1.45'
4
+ __version__ = VERSION = '0.1.47'
@@ -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 CompletionError, ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem, MessageType
10
- from mito_ai.utils.mito_server_utils import ProviderCompletionException
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 = get_anthropic_system_prompt_and_messages(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 = get_anthropic_system_prompt_and_messages(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:
@@ -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.streamlit_conversion.streamlit_utils import get_app_path
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
- title=f"Unknown message type: {message_type}"
85
+ message=f"Unknown message type: {message_type}",
86
+ error_code=400
86
87
  )
87
- self.reply(ErrorMessage(**error.__dict__))
88
-
89
- except ValueError as e:
88
+ raise StreamlitDeploymentError(error)
89
+
90
+ except StreamlitDeploymentError as e:
90
91
  self.log.error("Invalid app builder request", exc_info=e)
91
- error = AppDeployError.from_exception(e)
92
- self.reply(ErrorMessage(**error.__dict__))
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) -> None:
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
- return
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
- title="Missing 'notebook_path' parameter"
136
+ message="Missing 'notebook_path' parameter",
137
+ error_code=400,
138
+ message_id=message_id
123
139
  )
124
- self.reply(DeployAppReply(
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
- title="Invalid authentication token",
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
- self.reply(DeployAppReply(
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
- app_directory = os.path.dirname(notebook_path)
155
-
156
- # Check if the app.py file exists
157
- app_path = get_app_path(app_directory)
158
- if app_path is None:
159
- error = AppDeployError(
160
- error_type="AppNotFound",
161
- title="App not found",
162
- hint="Please make sure the app.py file exists in the same directory as the notebook."
163
- )
164
- self.reply(DeployAppReply(
165
- parent_id=message_id,
166
- url="",
167
- error=error
168
- ))
169
-
170
- # Finally, deploy the app
171
- deploy_url = await self._deploy_app(app_directory, files_to_upload, jwt_token)
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: str, files_to_upload:List[str], jwt_token: str = '') -> str:
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
- raise
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 # type: ignore
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
- if 'error' in error_detail:
289
- raise Exception(error_detail['error'])
290
- raise
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
- raise
294
- raise RuntimeError("Unexpected error in _deploy_app")
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."""
@@ -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
- from enum import Enum
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
- title: str
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
- title=str(e),
44
- traceback=getattr(e, "__traceback__", None) and str(e.__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["deploy-app"]
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["deploy-app"] = "deploy-app"
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
+ ]