mito-ai 0.1.41__py3-none-any.whl → 0.1.43__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 (56) hide show
  1. mito_ai/__init__.py +7 -0
  2. mito_ai/_version.py +1 -1
  3. mito_ai/app_manager/__init__.py +4 -0
  4. mito_ai/app_manager/handlers.py +134 -0
  5. mito_ai/app_manager/models.py +57 -0
  6. mito_ai/app_manager/utils.py +24 -0
  7. mito_ai/completions/completion_handlers/agent_execution_handler.py +1 -1
  8. mito_ai/completions/completion_handlers/chat_completion_handler.py +2 -2
  9. mito_ai/completions/completion_handlers/utils.py +99 -37
  10. mito_ai/completions/prompt_builders/utils.py +7 -1
  11. mito_ai/file_uploads/handlers.py +49 -26
  12. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  13. mito_ai/tests/file_uploads/test_handlers.py +15 -0
  14. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  15. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  16. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  17. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.01a962c68c8fae380f30.js → mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.81703ac2bc645e5c2fc2.js +1324 -247
  18. mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.81703ac2bc645e5c2fc2.js.map +1 -0
  19. mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  20. mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  21. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9a70f033717ba8689564.js → mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.502aef26f0416fab7435.js +23 -23
  22. mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.502aef26f0416fab7435.js.map +1 -0
  23. mito_ai-0.1.43.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 +533 -0
  24. mito_ai-0.1.43.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 +1 -0
  25. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_mjs.16430abf3466c3153f59.js → mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +2977 -610
  26. mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
  27. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.61289bff0db44828605b.js → mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +1 -481
  28. mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
  29. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_core_dist_esm_singleton_apis_fetchAuthSession_mjs-node_modul-758875.dc495fd682071d97070c.js → mito_ai-0.1.43.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 +2 -60
  30. mito_ai-0.1.43.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 +1 -0
  31. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js → mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2 -240
  32. mito_ai-0.1.43.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  33. {mito_ai-0.1.41.dist-info → mito_ai-0.1.43.dist-info}/METADATA +1 -1
  34. {mito_ai-0.1.41.dist-info → mito_ai-0.1.43.dist-info}/RECORD +46 -41
  35. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.01a962c68c8fae380f30.js.map +0 -1
  36. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_aws-amplify_core_dist_esm_singleton_apis_fetchAuthSession_mjs.182232e7bc6311fe4528.js +0 -63
  37. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_aws-amplify_core_dist_esm_singleton_apis_fetchAuthSession_mjs.182232e7bc6311fe4528.js.map +0 -1
  38. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9a70f033717ba8689564.js.map +0 -1
  39. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_mjs.16430abf3466c3153f59.js.map +0 -1
  40. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_core_dist_esm_singleton_Amplify_mjs.3c0035b95fe369aede82.js +0 -2345
  41. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_core_dist_esm_singleton_Amplify_mjs.3c0035b95fe369aede82.js.map +0 -1
  42. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_core_dist_esm_singleton_apis_fetchAuthSession_mjs-node_modul-758875.dc495fd682071d97070c.js.map +0 -1
  43. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.61289bff0db44828605b.js.map +0 -1
  44. mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
  45. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  46. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  47. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  48. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  49. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  50. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  51. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
  52. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  53. {mito_ai-0.1.41.data → mito_ai-0.1.43.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  54. {mito_ai-0.1.41.dist-info → mito_ai-0.1.43.dist-info}/WHEEL +0 -0
  55. {mito_ai-0.1.41.dist-info → mito_ai-0.1.43.dist-info}/entry_points.txt +0 -0
  56. {mito_ai-0.1.41.dist-info → mito_ai-0.1.43.dist-info}/licenses/LICENSE +0 -0
mito_ai/__init__.py CHANGED
@@ -14,6 +14,7 @@ from mito_ai.settings.urls import get_settings_urls
14
14
  from mito_ai.rules.urls import get_rules_urls
15
15
  from mito_ai.auth.urls import get_auth_urls
16
16
  from mito_ai.streamlit_preview.urls import get_streamlit_preview_urls
17
+ from mito_ai.app_manager.handlers import AppManagerHandler
17
18
  from mito_ai.file_uploads.urls import get_file_uploads_urls
18
19
 
19
20
  # Force Matplotlib to use the Jupyter inline backend.
@@ -24,6 +25,7 @@ from mito_ai.file_uploads.urls import get_file_uploads_urls
24
25
  # We preempt this by selecting the canonical Jupyter inline backend BEFORE any
25
26
  # Matplotlib import, so figures render inline reliably. This must run very early.
26
27
  # See: https://github.com/streamlit/streamlit/issues/9640
28
+
27
29
  import os
28
30
  os.environ["MPLBACKEND"] = "module://matplotlib_inline.backend_inline"
29
31
 
@@ -82,6 +84,11 @@ def _load_jupyter_server_extension(server_app) -> None: # type: ignore
82
84
  url_path_join(base_url, "mito-ai", "version-check"),
83
85
  VersionCheckHandler,
84
86
  {},
87
+ ),
88
+ (
89
+ url_path_join(base_url, "mito-ai", "app-manager"),
90
+ AppManagerHandler,
91
+ {}
85
92
  )
86
93
  ]
87
94
 
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.41'
4
+ __version__ = VERSION = '0.1.43'
@@ -0,0 +1,4 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ """App manager module for Mito AI."""
@@ -0,0 +1,134 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ # app_manager/handlers.py
5
+ import os
6
+ import time
7
+ import logging
8
+ from typing import Union
9
+ from mito_ai.utils.websocket_base import BaseWebSocketHandler
10
+ from mito_ai.app_manager.models import (
11
+ App,
12
+ AppManagerError,
13
+ ManageAppRequest,
14
+ ManageAppReply,
15
+ ErrorMessage,
16
+ MessageType
17
+ )
18
+ from mito_ai.constants import ACTIVE_STREAMLIT_BASE_URL
19
+ from mito_ai.logger import get_logger
20
+ from mito_ai.app_manager.utils import convert_utc_to_local_time
21
+ import requests
22
+
23
+
24
+ class AppManagerHandler(BaseWebSocketHandler):
25
+ """Handler for app management requests."""
26
+
27
+ def initialize(self) -> None:
28
+ """Initialize the WebSocket handler."""
29
+ super().initialize()
30
+ self.log.debug("Initializing app manager websocket connection %s", self.request.path)
31
+
32
+ @property
33
+ def log(self) -> logging.Logger:
34
+ """Use Mito AI logger."""
35
+ return get_logger()
36
+
37
+ async def on_message(self, message: Union[str, bytes]) -> None:
38
+ """Handle incoming messages on the WebSocket."""
39
+ start = time.time()
40
+
41
+ # Convert bytes to string if needed
42
+ if isinstance(message, bytes):
43
+ message = message.decode('utf-8')
44
+
45
+ self.log.debug("App manager message received: %s", message)
46
+
47
+ try:
48
+ # Ensure message is a string before parsing
49
+ if not isinstance(message, str):
50
+ raise ValueError("Message must be a string")
51
+
52
+ parsed_message = self.parse_message(message)
53
+ message_type = parsed_message.get('type')
54
+ message_id = parsed_message.get('message_id')
55
+
56
+ if message_type == MessageType.MANAGE_APP.value:
57
+ # Handle manage app request
58
+ manage_app_request = ManageAppRequest(**parsed_message)
59
+ await self._handle_manage_app(manage_app_request)
60
+ else:
61
+ self.log.error(f"Unknown message type: {message_type}")
62
+ error_response = ErrorMessage(
63
+ error_type="InvalidRequest",
64
+ title=f"Unknown message type: {message_type}",
65
+ message_id=message_id
66
+ )
67
+ self.reply(error_response)
68
+
69
+ except ValueError as e:
70
+ self.log.error("Invalid app manager request", exc_info=e)
71
+ error_response = ErrorMessage(
72
+ error_type=type(e).__name__,
73
+ title=str(e),
74
+ message_id=parsed_message.get('message_id') if 'parsed_message' in locals() else None
75
+ )
76
+ self.reply(error_response)
77
+ except Exception as e:
78
+ self.log.error("Error handling app manager message", exc_info=e)
79
+ error_response = ErrorMessage(
80
+ error_type=type(e).__name__,
81
+ title=str(e),
82
+ message_id=parsed_message.get('message_id') if 'parsed_message' in locals() else None
83
+ )
84
+ self.reply(error_response)
85
+
86
+ latency_ms = round((time.time() - start) * 1000)
87
+ self.log.info(f"App manager handler processed in {latency_ms} ms.")
88
+
89
+ async def _handle_manage_app(self, request: ManageAppRequest) -> None:
90
+ """Handle a manage app request with hardcoded data."""
91
+ try:
92
+ jwt_token = request.jwt_token
93
+ headers = {}
94
+ if jwt_token and jwt_token != 'placeholder-jwt-token':
95
+ headers['Authorization'] = f'Bearer {jwt_token}'
96
+ else:
97
+ self.log.warning("No JWT token provided for API request")
98
+ return
99
+
100
+ manage_apps_response = requests.get(f"{ACTIVE_STREAMLIT_BASE_URL}/manage-apps",
101
+ headers=headers)
102
+ manage_apps_response.raise_for_status()
103
+
104
+ apps_data = manage_apps_response.json()
105
+
106
+ for app in apps_data:
107
+ if 'last_deployed_at' in app:
108
+ app['last_deployed_at'] = convert_utc_to_local_time(app['last_deployed_at'])
109
+
110
+ # Create successful response
111
+ reply = ManageAppReply(
112
+ apps=apps_data,
113
+ message_id=request.message_id
114
+ )
115
+ self.reply(reply)
116
+
117
+ except Exception as e:
118
+ self.log.error(f"Error handling manage app request: {e}", exc_info=e)
119
+
120
+ try:
121
+ error = AppManagerError.from_exception(e)
122
+ except Exception:
123
+ error = AppManagerError(
124
+ error_type=type(e).__name__,
125
+ title=str(e)
126
+ )
127
+
128
+ # Return error response
129
+ error_reply = ManageAppReply(
130
+ apps=[],
131
+ error=error,
132
+ message_id=request.message_id
133
+ )
134
+ self.reply(error_reply)
@@ -0,0 +1,57 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from typing import List, Optional
7
+
8
+ class MessageType(str, Enum):
9
+ """Types of app manager messages."""
10
+ MANAGE_APP = "manage-app"
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ManageAppRequest:
15
+ """Request to manage apps."""
16
+ type: str = "manage-app"
17
+ jwt_token: Optional[str] = None
18
+ message_id: Optional[str] = None
19
+
20
+ @dataclass(frozen=True)
21
+ class App:
22
+ """App information."""
23
+ app_name: str
24
+ url: str
25
+ status: str
26
+ last_deployed_at: str
27
+
28
+ @dataclass(frozen=True)
29
+ class AppManagerError:
30
+ """Error information for app manager operations."""
31
+ error_type: str
32
+ title: str
33
+ traceback: Optional[str] = None
34
+
35
+ @classmethod
36
+ def from_exception(cls, exc: Exception) -> 'AppManagerError':
37
+ return cls(
38
+ error_type=type(exc).__name__,
39
+ title=str(exc),
40
+ traceback=str(exc)
41
+ )
42
+
43
+ @dataclass(frozen=True)
44
+ class ManageAppReply:
45
+ """Reply to a manage app request."""
46
+ type: str = "manage-app"
47
+ apps: List[App] = field(default_factory=list)
48
+ error: Optional[AppManagerError] = None
49
+ message_id: Optional[str] = None
50
+
51
+ @dataclass(frozen=True)
52
+ class ErrorMessage:
53
+ """Error message."""
54
+ error_type: str
55
+ title: str
56
+ traceback: Optional[str] = None
57
+ message_id: Optional[str] = None
@@ -0,0 +1,24 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from datetime import datetime, timezone
5
+
6
+ def convert_utc_to_local_time(time_str: str) -> str:
7
+ """Convert UTC time to a user's local time"""
8
+ try:
9
+ # Remove the 'Z' suffix and parse the UTC datetime
10
+ utc_time_str = time_str.rstrip('Z')
11
+ utc_time = datetime.fromisoformat(utc_time_str)
12
+
13
+ # Set timezone to UTC
14
+ utc_time = utc_time.replace(tzinfo=timezone.utc)
15
+
16
+ # Convert to local timezone (system timezone)
17
+ local_time = utc_time.astimezone()
18
+
19
+ # Format as 'MMM DD HH:MM'
20
+ return local_time.strftime('%m-%d-%Y %H:%M')
21
+
22
+ except (ValueError, AttributeError) as e:
23
+ # Return original string if parsing fails
24
+ return time_str
@@ -38,7 +38,7 @@ class AgentExecutionHandler(CompletionHandler[AgentExecutionMetadata]):
38
38
  display_prompt = metadata.input
39
39
 
40
40
  # Add the prompt to the message history
41
- new_ai_optimized_message = create_ai_optimized_message(prompt, metadata.base64EncodedActiveCellOutput)
41
+ new_ai_optimized_message = create_ai_optimized_message(prompt, metadata.base64EncodedActiveCellOutput, metadata.additionalContext)
42
42
  new_display_optimized_message: ChatCompletionMessageParam = {"role": "user", "content": display_prompt}
43
43
 
44
44
  await message_history.append_message(new_ai_optimized_message, new_display_optimized_message, model, provider, metadata.threadId)
@@ -47,7 +47,7 @@ class ChatCompletionHandler(CompletionHandler[ChatMessageMetadata]):
47
47
  display_prompt = f"```python{metadata.activeCellCode or ''}```{metadata.input}"
48
48
 
49
49
  # Add the prompt to the message history
50
- new_ai_optimized_message = create_ai_optimized_message(prompt, metadata.base64EncodedActiveCellOutput)
50
+ new_ai_optimized_message = create_ai_optimized_message(prompt, metadata.base64EncodedActiveCellOutput, metadata.additionalContext)
51
51
  new_display_optimized_message: ChatCompletionMessageParam = {"role": "user", "content": display_prompt}
52
52
  await message_history.append_message(new_ai_optimized_message, new_display_optimized_message, model, provider, metadata.threadId)
53
53
 
@@ -110,7 +110,7 @@ class ChatCompletionHandler(CompletionHandler[ChatMessageMetadata]):
110
110
  display_prompt = f"```python{metadata.activeCellCode or ''}```{metadata.input}"
111
111
 
112
112
  # Add the prompt to the message history
113
- new_ai_optimized_message = create_ai_optimized_message(prompt, metadata.base64EncodedActiveCellOutput)
113
+ new_ai_optimized_message = create_ai_optimized_message(prompt, metadata.base64EncodedActiveCellOutput, metadata.additionalContext)
114
114
  new_display_optimized_message: ChatCompletionMessageParam = {"role": "user", "content": display_prompt}
115
115
  await message_history.append_message(new_ai_optimized_message, new_display_optimized_message, model, provider, metadata.threadId)
116
116
 
@@ -1,30 +1,39 @@
1
1
  # Copyright (c) Saga Inc.
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
+ import base64
4
5
  from typing import Optional, Union, List, Dict, Any, cast
5
6
  from mito_ai.completions.message_history import GlobalMessageHistory
6
7
  from mito_ai.completions.models import ThreadID
7
8
  from mito_ai.completions.providers import OpenAIProvider
8
9
  from openai.types.chat import ChatCompletionMessageParam
9
- from mito_ai.completions.prompt_builders.chat_system_message import create_chat_system_message_prompt
10
- from mito_ai.completions.prompt_builders.agent_system_message import create_agent_system_message_prompt
10
+ from mito_ai.completions.prompt_builders.chat_system_message import (
11
+ create_chat_system_message_prompt,
12
+ )
13
+ from mito_ai.completions.prompt_builders.agent_system_message import (
14
+ create_agent_system_message_prompt,
15
+ )
16
+
11
17
 
12
18
  async def append_chat_system_message(
13
- message_history: GlobalMessageHistory,
14
- model: str,
15
- provider: OpenAIProvider,
16
- thread_id: ThreadID
19
+ message_history: GlobalMessageHistory,
20
+ model: str,
21
+ provider: OpenAIProvider,
22
+ thread_id: ThreadID,
17
23
  ) -> None:
18
-
24
+
19
25
  # If the system message already exists, do nothing
20
- if any(msg["role"] == "system" for msg in message_history.get_ai_optimized_history(thread_id)):
26
+ if any(
27
+ msg["role"] == "system"
28
+ for msg in message_history.get_ai_optimized_history(thread_id)
29
+ ):
21
30
  return
22
-
31
+
23
32
  system_message_prompt = create_chat_system_message_prompt()
24
33
 
25
34
  system_message: ChatCompletionMessageParam = {
26
35
  "role": "system",
27
- "content": system_message_prompt
36
+ "content": system_message_prompt,
28
37
  }
29
38
 
30
39
  await message_history.append_message(
@@ -32,54 +41,107 @@ async def append_chat_system_message(
32
41
  display_message=system_message,
33
42
  model=model,
34
43
  llm_provider=provider,
35
- thread_id=thread_id
44
+ thread_id=thread_id,
36
45
  )
37
46
 
47
+
38
48
  async def append_agent_system_message(
39
- message_history: GlobalMessageHistory,
40
- model: str,
41
- provider: OpenAIProvider,
42
- thread_id: ThreadID,
43
- isChromeBrowser: bool
49
+ message_history: GlobalMessageHistory,
50
+ model: str,
51
+ provider: OpenAIProvider,
52
+ thread_id: ThreadID,
53
+ isChromeBrowser: bool,
44
54
  ) -> None:
45
-
55
+
46
56
  # If the system message already exists, do nothing
47
- if any(msg["role"] == "system" for msg in message_history.get_ai_optimized_history(thread_id)):
57
+ if any(
58
+ msg["role"] == "system"
59
+ for msg in message_history.get_ai_optimized_history(thread_id)
60
+ ):
48
61
  return
49
-
62
+
50
63
  system_message_prompt = create_agent_system_message_prompt(isChromeBrowser)
51
-
64
+
52
65
  system_message: ChatCompletionMessageParam = {
53
66
  "role": "system",
54
- "content": system_message_prompt
67
+ "content": system_message_prompt,
55
68
  }
56
-
69
+
57
70
  await message_history.append_message(
58
71
  ai_optimized_message=system_message,
59
72
  display_message=system_message,
60
73
  model=model,
61
74
  llm_provider=provider,
62
- thread_id=thread_id
75
+ thread_id=thread_id,
63
76
  )
64
-
65
- def create_ai_optimized_message(text: str, base64EncodedActiveCellOutput: Optional[str] = None) -> ChatCompletionMessageParam:
77
+
78
+
79
+ def extract_and_encode_images_from_additional_context(
80
+ additional_context: Optional[List[Dict[str, str]]],
81
+ ) -> List[str]:
82
+ encoded_images = []
83
+
84
+ for context in additional_context or []:
85
+ if context["type"].startswith("image/"):
86
+ try:
87
+ with open(context["value"], "rb") as image_file:
88
+ image_data = image_file.read()
89
+ base64_encoded = base64.b64encode(image_data).decode("utf-8")
90
+ encoded_images.append(f"data:{context['type']};base64,{base64_encoded}")
91
+ except (FileNotFoundError, IOError) as e:
92
+ print(f"Error reading image file {context['value']}: {e}")
93
+ continue
94
+
95
+ return encoded_images
96
+
97
+
98
+ def create_ai_optimized_message(
99
+ text: str,
100
+ base64EncodedActiveCellOutput: Optional[str] = None,
101
+ additional_context: Optional[List[Dict[str, str]]] = None,
102
+ ) -> ChatCompletionMessageParam:
66
103
 
67
104
  message_content: Union[str, List[Dict[str, Any]]]
68
- if base64EncodedActiveCellOutput is not None and base64EncodedActiveCellOutput != '':
69
- message_content = [
105
+ encoded_images = extract_and_encode_images_from_additional_context(
106
+ additional_context
107
+ )
108
+
109
+ has_uploaded_image = len(encoded_images) > 0
110
+ has_active_cell_output = (
111
+ base64EncodedActiveCellOutput is not None
112
+ and base64EncodedActiveCellOutput != ""
113
+ )
114
+
115
+ if has_uploaded_image or has_active_cell_output:
116
+ message_content = [
70
117
  {
71
118
  "type": "text",
72
119
  "text": text,
73
- },
74
- {
75
- "type": "image_url",
76
- "image_url": {"url": f"data:image/png;base64,{base64EncodedActiveCellOutput}"},
77
120
  }
78
- ]
121
+ ]
122
+
123
+ for img in encoded_images:
124
+ message_content.append(
125
+ {
126
+ "type": "image_url",
127
+ "image_url": {
128
+ "url": img
129
+ },
130
+ }
131
+ )
132
+
133
+ if has_active_cell_output:
134
+ message_content.append(
135
+ {
136
+ "type": "image_url",
137
+ "image_url": {
138
+ "url": f"data:image/png;base64,{base64EncodedActiveCellOutput}"
139
+ },
140
+ }
141
+ )
79
142
  else:
80
143
  message_content = text
81
-
82
- return cast(ChatCompletionMessageParam, {
83
- "role": "user",
84
- "content": message_content
85
- })
144
+
145
+ return cast(
146
+ ChatCompletionMessageParam, {"role": "user", "content": message_content}
147
+ )
@@ -38,6 +38,7 @@ def get_selected_context_str(additional_context: Optional[List[Dict[str, str]]])
38
38
  selected_variables = [context["value"] for context in additional_context if context.get("type") == "variable"]
39
39
  selected_files = [context["value"] for context in additional_context if context.get("type") == "file"]
40
40
  selected_db_connections = [context["value"] for context in additional_context if context.get("type") == "db"]
41
+ selected_images = [context["value"] for context in additional_context if context.get("type", "").startswith("image/")]
41
42
 
42
43
  # STEP 2: Create a list of strings (instructions) for each context type
43
44
  context_parts = []
@@ -60,6 +61,11 @@ def get_selected_context_str(additional_context: Optional[List[Dict[str, str]]])
60
61
  + "\n".join(selected_db_connections)
61
62
  )
62
63
 
63
- # STEP 3: Combine into a single string
64
+ if len(selected_images) > 0:
65
+ context_parts.append(
66
+ "The following images have been selected by the user to be used in the task:\n"
67
+ + "\n".join(selected_images)
68
+ )
64
69
 
70
+ # STEP 3: Combine into a single string
65
71
  return "\n\n".join(context_parts)
@@ -6,7 +6,38 @@ import tempfile
6
6
  import tornado
7
7
  from typing import Dict, Any
8
8
  from jupyter_server.base.handlers import APIHandler
9
- from mito_ai.utils.telemetry_utils import log_file_upload_attempt, log_file_upload_failure
9
+ from mito_ai.utils.telemetry_utils import (
10
+ log_file_upload_attempt,
11
+ log_file_upload_failure,
12
+ )
13
+
14
+ MAX_IMAGE_SIZE_MB = 3
15
+
16
+
17
+ def _is_image_file(filename: str) -> bool:
18
+ image_extensions = {
19
+ ".jpg",
20
+ ".jpeg",
21
+ ".png",
22
+ ".gif",
23
+ ".bmp",
24
+ ".tiff",
25
+ ".tif",
26
+ ".webp",
27
+ ".svg",
28
+ }
29
+ file_extension = os.path.splitext(filename)[1].lower()
30
+ return file_extension in image_extensions
31
+
32
+
33
+ def _check_image_size_limit(file_data: bytes, filename: str) -> None:
34
+ if not _is_image_file(filename):
35
+ return
36
+
37
+ file_size_mb = len(file_data) / (1024 * 1024) # Convert bytes to MB
38
+
39
+ if file_size_mb > MAX_IMAGE_SIZE_MB:
40
+ raise ValueError(f"Image exceeded {MAX_IMAGE_SIZE_MB}MB limit.")
10
41
 
11
42
 
12
43
  class FileUploadHandler(APIHandler):
@@ -50,7 +81,7 @@ class FileUploadHandler(APIHandler):
50
81
  self.finish()
51
82
 
52
83
  except Exception as e:
53
- self._handle_error(f"Failed to save file: {str(e)}")
84
+ self._handle_error(str(e))
54
85
 
55
86
  def _validate_file_upload(self) -> bool:
56
87
  """Validate that a file was uploaded in the request."""
@@ -90,6 +121,9 @@ class FileUploadHandler(APIHandler):
90
121
  self, filename: str, file_data: bytes, notebook_dir: str
91
122
  ) -> None:
92
123
  """Handle regular (non-chunked) file upload."""
124
+ # Check image file size limit before saving
125
+ _check_image_size_limit(file_data, filename)
126
+
93
127
  file_path = os.path.join(notebook_dir, filename)
94
128
  with open(file_path, "wb") as f:
95
129
  f.write(file_data)
@@ -100,8 +134,6 @@ class FileUploadHandler(APIHandler):
100
134
  self, filename: str, file_data: bytes, chunk_number: int, total_chunks: int
101
135
  ) -> None:
102
136
  """Save a chunk to a temporary file."""
103
- print(f"DEBUG: Saving chunk {chunk_number}/{total_chunks} for file {filename}")
104
-
105
137
  # Initialize temporary directory for this file if it doesn't exist
106
138
  if filename not in self._temp_dirs:
107
139
  temp_dir = tempfile.mkdtemp(prefix=f"mito_upload_{filename}_")
@@ -110,7 +142,6 @@ class FileUploadHandler(APIHandler):
110
142
  "total_chunks": total_chunks,
111
143
  "received_chunks": set(),
112
144
  }
113
- print(f"DEBUG: Created temp dir {temp_dir} for file {filename}")
114
145
 
115
146
  # Save the chunk to the temporary directory
116
147
  chunk_filename = os.path.join(
@@ -121,28 +152,20 @@ class FileUploadHandler(APIHandler):
121
152
 
122
153
  # Mark this chunk as received
123
154
  self._temp_dirs[filename]["received_chunks"].add(chunk_number)
124
- print(
125
- f"DEBUG: Saved chunk {chunk_number}, total received: {len(self._temp_dirs[filename]['received_chunks'])}/{total_chunks}"
126
- )
127
155
 
128
156
  def _are_all_chunks_received(self, filename: str, total_chunks: int) -> bool:
129
157
  """Check if all chunks for a file have been received."""
130
158
  if filename not in self._temp_dirs:
131
- print(f"DEBUG: No temp dir found for {filename}")
132
159
  return False
133
160
 
134
161
  received_chunks = self._temp_dirs[filename]["received_chunks"]
135
162
  is_complete = len(received_chunks) == total_chunks
136
- print(
137
- f"DEBUG: Checking completion for {filename}: {len(received_chunks)}/{total_chunks} chunks received, complete: {is_complete}"
138
- )
139
163
  return is_complete
140
164
 
141
165
  def _reconstruct_file(
142
166
  self, filename: str, total_chunks: int, notebook_dir: str
143
167
  ) -> None:
144
168
  """Reconstruct the final file from all chunks and clean up temporary directory."""
145
- print(f"DEBUG: Starting reconstruction for {filename}")
146
169
 
147
170
  if filename not in self._temp_dirs:
148
171
  raise ValueError(f"No temporary directory found for file: {filename}")
@@ -150,23 +173,23 @@ class FileUploadHandler(APIHandler):
150
173
  temp_dir = self._temp_dirs[filename]["temp_dir"]
151
174
  file_path = os.path.join(notebook_dir, filename)
152
175
 
153
- print(f"DEBUG: Reconstructing from {temp_dir} to {file_path}")
154
-
155
176
  try:
156
- # Reconstruct the file from chunks
177
+ # First, read all chunks to check total file size for images
178
+ all_file_data = b""
179
+ for i in range(1, total_chunks + 1):
180
+ chunk_filename = os.path.join(temp_dir, f"chunk_{i}")
181
+ with open(chunk_filename, "rb") as chunk_file:
182
+ chunk_data = chunk_file.read()
183
+ all_file_data += chunk_data
184
+
185
+ # Check image file size limit before saving
186
+ _check_image_size_limit(all_file_data, filename)
187
+
188
+ # Write the complete file
157
189
  with open(file_path, "wb") as final_file:
158
- for i in range(1, total_chunks + 1):
159
- chunk_filename = os.path.join(temp_dir, f"chunk_{i}")
160
- print(f"DEBUG: Reading chunk {i} from {chunk_filename}")
161
- with open(chunk_filename, "rb") as chunk_file:
162
- chunk_data = chunk_file.read()
163
- final_file.write(chunk_data)
164
- print(f"DEBUG: Wrote {len(chunk_data)} bytes from chunk {i}")
165
-
166
- print(f"DEBUG: Successfully reconstructed {filename}")
190
+ final_file.write(all_file_data)
167
191
  finally:
168
192
  # Clean up the temporary directory
169
- print(f"DEBUG: Cleaning up temp dir for {filename}")
170
193
  self._cleanup_temp_dir(filename)
171
194
 
172
195
  def _cleanup_temp_dir(self, filename: str) -> None: