mito-ai 0.1.37__py3-none-any.whl → 0.1.39__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 +17 -1
  2. mito_ai/_version.py +1 -1
  3. mito_ai/app_builder/handlers.py +43 -38
  4. mito_ai/app_builder/models.py +1 -1
  5. mito_ai/completions/handlers.py +1 -1
  6. mito_ai/completions/prompt_builders/agent_system_message.py +18 -45
  7. mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
  8. mito_ai/log/handlers.py +10 -3
  9. mito_ai/log/urls.py +3 -3
  10. mito_ai/openai_client.py +1 -1
  11. mito_ai/streamlit_conversion/agent_utils.py +116 -0
  12. mito_ai/streamlit_conversion/prompts/prompt_constants.py +59 -0
  13. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  14. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +45 -0
  15. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  16. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +44 -0
  17. mito_ai/streamlit_conversion/streamlit_agent_handler.py +90 -44
  18. mito_ai/streamlit_conversion/streamlit_system_prompt.py +30 -17
  19. mito_ai/streamlit_conversion/streamlit_utils.py +48 -8
  20. mito_ai/streamlit_conversion/validate_streamlit_app.py +116 -0
  21. mito_ai/streamlit_preview/__init__.py +7 -0
  22. mito_ai/streamlit_preview/handlers.py +164 -0
  23. mito_ai/streamlit_preview/manager.py +159 -0
  24. mito_ai/streamlit_preview/urls.py +22 -0
  25. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +166 -78
  26. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +4 -5
  27. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +119 -0
  28. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +302 -0
  29. mito_ai/tests/utils/test_anthropic_utils.py +2 -2
  30. mito_ai/utils/anthropic_utils.py +4 -4
  31. mito_ai/utils/open_ai_utils.py +0 -4
  32. mito_ai/utils/telemetry_utils.py +28 -1
  33. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
  34. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  35. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  36. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +6 -1
  37. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js +799 -116
  38. mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js.map +1 -0
  39. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.606207904e6aaa42b1bf.js +5 -5
  40. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js.map → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.606207904e6aaa42b1bf.js.map +1 -1
  41. {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/METADATA +4 -1
  42. {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/RECORD +53 -42
  43. mito_ai/streamlit_conversion/validate_and_run_streamlit_code.py +0 -207
  44. mito_ai/tests/streamlit_conversion/test_validate_and_run_streamlit_code.py +0 -418
  45. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js.map +0 -1
  46. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  47. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  48. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  49. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  50. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js +0 -0
  51. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -0
  52. {mito_ai-0.1.37.data → mito_ai-0.1.39.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.37.data → mito_ai-0.1.39.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.37.dist-info → mito_ai-0.1.39.dist-info}/WHEEL +0 -0
  55. {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/entry_points.txt +0 -0
  56. {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,164 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import os
5
+ import tempfile
6
+ import uuid
7
+ import tornado
8
+ from jupyter_server.base.handlers import APIHandler
9
+ from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
10
+ from mito_ai.streamlit_preview.manager import get_preview_manager
11
+ from mito_ai.utils.create import initialize_user
12
+
13
+
14
+ class StreamlitPreviewHandler(APIHandler):
15
+ """REST handler for streamlit preview operations."""
16
+
17
+ def initialize(self) -> None:
18
+ """Initialize the handler."""
19
+ self.preview_manager = get_preview_manager()
20
+
21
+ def _resolve_notebook_path(self, notebook_path: str) -> str:
22
+ """
23
+ Resolve the notebook path to an absolute path that can be found by the backend.
24
+
25
+ This method handles path resolution issues that can occur in different environments:
26
+
27
+ 1. **Test Environment**: Playwright tests create temporary directories with complex
28
+ paths like 'mitoai_ui_tests-app_builde-ab3a5-n-Test-Preview-as-Streamlit-chromium/'
29
+ that the backend can't directly access.
30
+
31
+ 2. **JupyterHub/Cloud Deployments**: In cloud environments, users may have notebooks
32
+ in subdirectories that aren't immediately accessible from the server root.
33
+
34
+ 3. **Docker Containers**: When running in containers, the working directory and
35
+ file paths may not align with what the frontend reports.
36
+
37
+ 4. **Multi-user Environments**: In enterprise deployments, users may have notebooks
38
+ in user-specific directories that require path resolution.
39
+
40
+ The method tries multiple strategies:
41
+ 1. If the path is already absolute, return it as-is
42
+ 2. Try to resolve relative to the Jupyter server's root directory
43
+ 3. Search recursively through subdirectories for a file with the same name
44
+ 4. Return the original path if not found (will cause a clear error message)
45
+
46
+ Args:
47
+ notebook_path (str): The notebook path from the frontend (may be relative or absolute)
48
+
49
+ Returns:
50
+ str: The resolved absolute path to the notebook file
51
+ """
52
+ # If the path is already absolute, return it
53
+ if os.path.isabs(notebook_path):
54
+ return notebook_path
55
+
56
+ # Get the Jupyter server's root directory
57
+ server_root = self.settings.get('server_root_dir', os.getcwd())
58
+
59
+ # Try to find the notebook file in the server root
60
+ resolved_path = os.path.join(server_root, notebook_path)
61
+ if os.path.exists(resolved_path):
62
+ return resolved_path
63
+
64
+ # If not found, try to find it in subdirectories
65
+ # This handles cases where the notebook is in a subdirectory that the frontend
66
+ # doesn't know about, or where the path structure differs between frontend and backend
67
+ for root, dirs, files in os.walk(server_root):
68
+ if os.path.basename(notebook_path) in files:
69
+ return os.path.join(root, os.path.basename(notebook_path))
70
+
71
+ # If still not found, return the original path (will cause a clear error)
72
+ # This ensures we get a meaningful error message rather than a generic "file not found"
73
+ return os.path.join(os.getcwd(), notebook_path)
74
+
75
+ @tornado.web.authenticated
76
+ async def post(self) -> None:
77
+ """Start a new streamlit preview.
78
+
79
+ Expected JSON body:
80
+ {
81
+ "notebook_path": "path/to/notebook.ipynb"
82
+ }
83
+
84
+ Returns:
85
+ {
86
+ "id": "preview_id",
87
+ "port": 8501,
88
+ "url": "http://localhost:8501"
89
+ }
90
+ """
91
+ try:
92
+ # Parse request body
93
+ body = self.get_json_body()
94
+ if body is None:
95
+ self.set_status(400)
96
+ self.finish({"error": 'Invalid or missing JSON body'})
97
+ return
98
+
99
+ notebook_path = body.get('notebook_path')
100
+
101
+ if not notebook_path:
102
+ self.set_status(400)
103
+ self.finish({"error": 'Missing notebook_path parameter'})
104
+ return
105
+
106
+ # Resolve the notebook path to find the actual file
107
+ resolved_notebook_path = self._resolve_notebook_path(notebook_path)
108
+
109
+ # Generate preview ID
110
+ preview_id = str(uuid.uuid4())
111
+
112
+ # Generate streamlit code using existing handler
113
+ print('notebook_path', notebook_path)
114
+ success, app_path, message = await streamlit_handler(resolved_notebook_path)
115
+
116
+ if not success or app_path is None:
117
+ self.set_status(500)
118
+ self.finish({"error": f'Failed to generate streamlit code: {message}'})
119
+ return
120
+
121
+ # Start streamlit preview
122
+ resolved_app_directory = os.path.dirname(resolved_notebook_path)
123
+ success, message, port = self.preview_manager.start_streamlit_preview(resolved_app_directory, preview_id)
124
+
125
+ if not success:
126
+ self.set_status(500)
127
+ self.finish({"error": f'Failed to start preview: {message}'})
128
+ return
129
+
130
+ # Return success response - APIHandler automatically handles JSON serialization
131
+ self.finish({
132
+ 'id': preview_id,
133
+ 'port': port,
134
+ 'url': f'http://localhost:{port}'
135
+ })
136
+
137
+ except Exception as e:
138
+ print(f"Error in streamlit preview handler: {e}")
139
+ self.set_status(500)
140
+
141
+ # Respond with the error
142
+ self.finish({"error": str(e)})
143
+
144
+ @tornado.web.authenticated
145
+ def delete(self, preview_id: str) -> None:
146
+ """Stop a streamlit preview."""
147
+ try:
148
+ if not preview_id:
149
+ self.set_status(400)
150
+ self.finish({"error": 'Missing preview_id parameter'})
151
+ return
152
+
153
+ # Stop the preview
154
+ stopped = self.preview_manager.stop_preview(preview_id)
155
+
156
+ if stopped:
157
+ self.set_status(204) # No content
158
+ else:
159
+ self.set_status(404)
160
+ self.finish({"error": f'Preview {preview_id} not found'})
161
+
162
+ except Exception as e:
163
+ self.set_status(500)
164
+ self.finish({"error": f'Internal server error: {str(e)}'})
@@ -0,0 +1,159 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import os
5
+ import socket
6
+ import subprocess
7
+ import tempfile
8
+ import time
9
+ import threading
10
+ import requests
11
+ from typing import Dict, Optional, Tuple
12
+ from dataclasses import dataclass
13
+ from mito_ai.logger import get_logger
14
+
15
+
16
+ @dataclass
17
+ class PreviewProcess:
18
+ """Data class to track a streamlit preview process."""
19
+ proc: subprocess.Popen
20
+ port: int
21
+
22
+
23
+ class StreamlitPreviewManager:
24
+ """Manages streamlit preview processes and their lifecycle."""
25
+
26
+ def __init__(self) -> None:
27
+ self._previews: Dict[str, PreviewProcess] = {}
28
+ self._lock = threading.Lock()
29
+ self.log = get_logger()
30
+
31
+ def get_free_port(self) -> int:
32
+ """Get a free port for streamlit to use."""
33
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
34
+ s.bind(('', 0))
35
+ s.listen(1)
36
+ port = int(s.getsockname()[1])
37
+
38
+ return port
39
+
40
+ def start_streamlit_preview(self, app_directory: str, preview_id: str) -> Tuple[bool, str, Optional[int]]:
41
+ """Start a streamlit preview process.
42
+
43
+ Args:
44
+ app_code: The streamlit app code to run
45
+ preview_id: Unique identifier for this preview
46
+
47
+ Returns:
48
+ Tuple of (success, message, port)
49
+ """
50
+ try:
51
+
52
+ # Get free port
53
+ port = self.get_free_port()
54
+
55
+ # Start streamlit process
56
+ cmd = [
57
+ "streamlit", "run", 'app.py', # Since we run this command from the app_directory, we always just run app.py
58
+ "--server.port", str(port),
59
+ "--server.headless", "true",
60
+ "--server.address", "localhost",
61
+ "--server.enableXsrfProtection", "false",
62
+ "--server.runOnSave", "true", # auto-reload when app.py is saved
63
+ "--logger.level", "error"
64
+ ]
65
+
66
+ # TODO: Security considerations for production:
67
+ # - Consider enabling XSRF protection if needed, but we might already get this with the APIHandler?
68
+ # - Add authentication headers to streamlit
69
+
70
+ proc = subprocess.Popen(
71
+ cmd,
72
+ stdout=subprocess.PIPE,
73
+ stderr=subprocess.PIPE,
74
+ text=True,
75
+ cwd=app_directory
76
+ )
77
+
78
+ # Wait for app to be ready
79
+ ready = self._wait_for_app_ready(port)
80
+ if not ready:
81
+ proc.terminate()
82
+ proc.wait()
83
+ return False, "Streamlit app failed to start", None
84
+
85
+ # Register the process
86
+ with self._lock:
87
+ self._previews[preview_id] = PreviewProcess(
88
+ proc=proc,
89
+ port=port,
90
+ )
91
+
92
+ self.log.info(f"Started streamlit preview {preview_id} on port {port}")
93
+ return True, "Preview started successfully", port
94
+
95
+ except Exception as e:
96
+ self.log.error(f"Error starting streamlit preview: {e}")
97
+ return False, f"Failed to start preview: {str(e)}", None
98
+
99
+ def _wait_for_app_ready(self, port: int, timeout: int = 30) -> bool:
100
+ """Wait for streamlit app to be ready on the given port."""
101
+ start_time = time.time()
102
+
103
+ while time.time() - start_time < timeout:
104
+ try:
105
+ response = requests.get(f"http://localhost:{port}", timeout=5)
106
+ if response.status_code == 200:
107
+ return True
108
+ except requests.exceptions.RequestException as e:
109
+ print(f"Error waiting for app to be ready: {e}")
110
+ pass
111
+
112
+ time.sleep(1)
113
+
114
+ return False
115
+
116
+ def stop_preview(self, preview_id: str) -> bool:
117
+ """Stop a streamlit preview process.
118
+
119
+ Args:
120
+ preview_id: The preview ID to stop
121
+
122
+ Returns:
123
+ True if stopped successfully, False if not found
124
+ """
125
+ print(f"Stopping preview {preview_id}")
126
+ with self._lock:
127
+ if preview_id not in self._previews:
128
+ return False
129
+
130
+ preview = self._previews[preview_id]
131
+
132
+ # Terminate process
133
+ try:
134
+ preview.proc.terminate()
135
+ preview.proc.wait(timeout=5)
136
+ except subprocess.TimeoutExpired:
137
+ preview.proc.kill()
138
+ preview.proc.wait()
139
+ except Exception as e:
140
+ self.log.error(f"Error terminating process {preview_id}: {e}")
141
+
142
+ # Remove from registry
143
+ del self._previews[preview_id]
144
+
145
+ self.log.info(f"Stopped streamlit preview {preview_id}")
146
+ return True
147
+
148
+ def get_preview(self, preview_id: str) -> Optional[PreviewProcess]:
149
+ """Get a preview process by ID."""
150
+ with self._lock:
151
+ return self._previews.get(preview_id)
152
+
153
+ # Global instance
154
+ _preview_manager = StreamlitPreviewManager()
155
+
156
+
157
+ def get_preview_manager() -> StreamlitPreviewManager:
158
+ """Get the global preview manager instance."""
159
+ return _preview_manager
@@ -0,0 +1,22 @@
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 Any, List, Tuple
5
+ from jupyter_server.utils import url_path_join
6
+ from mito_ai.streamlit_preview.handlers import StreamlitPreviewHandler
7
+
8
+ def get_streamlit_preview_urls(base_url: str) -> List[Tuple[str, Any, dict]]:
9
+ """Get all streamlit preview related URL patterns.
10
+
11
+ Args:
12
+ base_url: The base URL for the Jupyter server
13
+
14
+ Returns:
15
+ List of (url_pattern, handler_class, handler_kwargs) tuples
16
+ """
17
+ BASE_URL = base_url + "/mito-ai"
18
+
19
+ return [
20
+ (url_path_join(BASE_URL, "streamlit-preview"), StreamlitPreviewHandler, {}),
21
+ (url_path_join(BASE_URL, "streamlit-preview/(.+)"), StreamlitPreviewHandler, {}),
22
+ ]