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,302 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import pytest
5
+ import time
6
+ import tempfile
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import threading
11
+ import requests
12
+ from unittest.mock import Mock, patch, MagicMock
13
+ from typing import Any
14
+
15
+ from mito_ai.streamlit_preview.manager import (
16
+ StreamlitPreviewManager,
17
+ PreviewProcess,
18
+ get_preview_manager
19
+ )
20
+
21
+
22
+ class TestStreamlitPreviewManager:
23
+ """Test cases for StreamlitPreviewManager."""
24
+
25
+ @pytest.fixture
26
+ def manager(self):
27
+ """Create a fresh manager instance for each test."""
28
+ return StreamlitPreviewManager()
29
+
30
+ @pytest.fixture
31
+ def sample_app_code(self):
32
+ """Sample streamlit app code for testing."""
33
+ return """
34
+ import streamlit as st
35
+
36
+ st.title("Test App")
37
+ st.write("Hello, World!")
38
+ """
39
+
40
+ def test_init(self, manager):
41
+ """Test manager initialization."""
42
+ assert manager._previews == {}
43
+ assert isinstance(manager._lock, type(threading.Lock()))
44
+ assert manager.log is not None
45
+
46
+ def test_get_free_port(self, manager):
47
+ """Test getting a free port."""
48
+ port = manager.get_free_port()
49
+ assert isinstance(port, int)
50
+ assert port > 0
51
+ assert port < 65536
52
+
53
+ # Test that we get different ports
54
+ port2 = manager.get_free_port()
55
+ assert port != port2
56
+
57
+ @pytest.mark.parametrize("app_code,preview_id,expected_success", [
58
+ ("import streamlit as st\nst.write('Hello')", "test_preview", True),
59
+ ("", "empty_preview", True),
60
+ ("import streamlit as st\n" * 1000 + "st.write('Large app')", "large_preview", True),
61
+ ])
62
+ def test_start_streamlit_preview_success_cases(self, manager, app_code, preview_id, expected_success):
63
+ """Test successful streamlit preview start with different app codes."""
64
+ with patch('subprocess.Popen') as mock_popen, \
65
+ patch('requests.get') as mock_requests_get, \
66
+ patch('tempfile.mkdtemp') as mock_mkdtemp:
67
+
68
+ # Setup mocks
69
+ app_directory = "/tmp/test_dir"
70
+ mock_mkdtemp.return_value = app_directory
71
+ mock_proc = Mock()
72
+ mock_proc.terminate.return_value = None
73
+ mock_proc.wait.return_value = None
74
+ mock_popen.return_value = mock_proc
75
+
76
+ mock_response = Mock()
77
+ mock_response.status_code = 200
78
+ mock_requests_get.return_value = mock_response
79
+
80
+ # Test
81
+ success, message, port = manager.start_streamlit_preview(app_directory, preview_id)
82
+
83
+ # Assertions
84
+ assert success == expected_success
85
+ if expected_success:
86
+ assert "successfully" in message.lower()
87
+ assert isinstance(port, int)
88
+ assert port > 0
89
+
90
+ # Verify subprocess was called correctly
91
+ mock_popen.assert_called_once()
92
+ call_args = mock_popen.call_args
93
+ assert "streamlit" in call_args[0][0]
94
+ assert "run" in call_args[0][0]
95
+ assert "--server.headless" in call_args[0][0]
96
+ assert "--server.address" in call_args[0][0]
97
+
98
+ # Cleanup
99
+ manager.stop_preview(preview_id)
100
+
101
+ @pytest.mark.parametrize("exception_type,expected_message", [
102
+ (Exception("Temp dir creation failed"), "failed to start preview"),
103
+ (OSError("Permission denied"), "failed to start preview"),
104
+ (ValueError("Invalid argument"), "failed to start preview"),
105
+ ])
106
+ def test_start_streamlit_preview_exceptions(self, manager, sample_app_code, exception_type, expected_message):
107
+ """Test streamlit preview start with different exceptions."""
108
+ with patch('tempfile.mkdtemp', side_effect=exception_type):
109
+ app_directory = "/tmp/test_dir"
110
+ success, message, port = manager.start_streamlit_preview(app_directory, "test_preview")
111
+
112
+ assert success is False
113
+ assert expected_message in message.lower()
114
+ assert port is None
115
+
116
+ @pytest.mark.parametrize("preview_id,expected_result", [
117
+ ("existing_preview", True),
118
+ ("non_existent", False),
119
+ ])
120
+ def test_stop_preview_scenarios(self, manager, sample_app_code, preview_id, expected_result):
121
+ """Test stopping previews with different scenarios."""
122
+ if expected_result:
123
+ # Start a preview first
124
+ with patch('subprocess.Popen') as mock_popen, \
125
+ patch('requests.get') as mock_requests_get, \
126
+ patch('tempfile.mkdtemp') as mock_mkdtemp, \
127
+ patch('builtins.open', create=True) as mock_open, \
128
+ patch('os.path.exists') as mock_exists:
129
+
130
+ app_directory = "/tmp/test_dir"
131
+ mock_mkdtemp.return_value = app_directory
132
+ mock_proc = Mock()
133
+ mock_proc.terminate.return_value = None
134
+ mock_proc.wait.return_value = None
135
+ mock_popen.return_value = mock_proc
136
+
137
+ mock_response = Mock()
138
+ mock_response.status_code = 200
139
+ mock_requests_get.return_value = mock_response
140
+
141
+ # Mock file operations
142
+ mock_file = Mock()
143
+ mock_open.return_value.__enter__.return_value = mock_file
144
+ mock_exists.return_value = True
145
+
146
+ manager.start_streamlit_preview(app_directory, preview_id)
147
+
148
+ @pytest.mark.parametrize("process_behavior,expected_kill_called", [
149
+ (subprocess.TimeoutExpired("cmd", 5), True),
150
+ (None, False), # Normal termination
151
+ ])
152
+ def test_stop_preview_process_behaviors(self, manager, sample_app_code, process_behavior, expected_kill_called):
153
+ """Test stopping preview with different process behaviors."""
154
+ with patch('subprocess.Popen') as mock_popen, \
155
+ patch('requests.get') as mock_requests_get, \
156
+ patch('tempfile.mkdtemp') as mock_mkdtemp, \
157
+ patch('builtins.open', create=True) as mock_open, \
158
+ patch('os.path.exists') as mock_exists:
159
+
160
+ # Setup mocks for start
161
+ app_directory = "/tmp/test_dir"
162
+ mock_mkdtemp.return_value = app_directory
163
+
164
+ mock_proc = Mock()
165
+ mock_proc.terminate.return_value = None
166
+ mock_proc.wait.return_value = None
167
+ mock_popen.return_value = mock_proc
168
+
169
+ mock_response = Mock()
170
+ mock_response.status_code = 200
171
+ mock_requests_get.return_value = mock_response
172
+
173
+ # Mock file operations
174
+ mock_file = Mock()
175
+ mock_open.return_value.__enter__.return_value = mock_file
176
+ mock_exists.return_value = True
177
+
178
+ # Start a preview
179
+ manager.start_streamlit_preview(app_directory, "test_preview")
180
+
181
+ # Setup process behavior for stop
182
+ if process_behavior:
183
+ # Configure the mock to raise the exception when called with timeout
184
+ def wait_with_timeout(*args, **kwargs):
185
+ if 'timeout' in kwargs:
186
+ raise process_behavior
187
+ return None
188
+ mock_proc.wait.side_effect = wait_with_timeout
189
+
190
+ @pytest.mark.parametrize("preview_id,expected_found", [
191
+ ("existing_preview", True),
192
+ ("non_existent", False),
193
+ ])
194
+ def test_get_preview_scenarios(self, manager, sample_app_code, preview_id, expected_found):
195
+ """Test getting previews with different scenarios."""
196
+ if expected_found:
197
+ # Start a preview first
198
+ with patch('subprocess.Popen') as mock_popen, \
199
+ patch('requests.get') as mock_requests_get, \
200
+ patch('tempfile.mkdtemp') as mock_mkdtemp, \
201
+ patch('builtins.open', create=True) as mock_open, \
202
+ patch('os.path.exists') as mock_exists:
203
+
204
+ mock_mkdtemp.return_value = "/tmp/test_dir"
205
+ mock_proc = Mock()
206
+ mock_proc.terminate.return_value = None
207
+ mock_proc.wait.return_value = None
208
+ mock_popen.return_value = mock_proc
209
+
210
+ mock_response = Mock()
211
+ mock_response.status_code = 200
212
+ mock_requests_get.return_value = mock_response
213
+
214
+ # Mock file operations
215
+ mock_file = Mock()
216
+ mock_open.return_value.__enter__.return_value = mock_file
217
+ mock_exists.return_value = True
218
+
219
+ manager.start_streamlit_preview("/tmp/test_dir", preview_id)
220
+
221
+ preview = manager.get_preview(preview_id)
222
+
223
+ if expected_found:
224
+ assert preview is not None
225
+ assert isinstance(preview, PreviewProcess)
226
+ assert preview.port > 0
227
+
228
+ # Cleanup
229
+ manager.stop_preview(preview_id)
230
+ else:
231
+ assert preview is None
232
+
233
+ def test_preview_process_dataclass(self):
234
+ """Test PreviewProcess dataclass."""
235
+ proc = Mock()
236
+ port = 8080
237
+
238
+ preview = PreviewProcess(
239
+ proc=proc,
240
+ port=port
241
+ )
242
+
243
+ assert preview.proc == proc
244
+ assert preview.port == port
245
+
246
+ def test_get_preview_manager_singleton(self):
247
+ """Test that get_preview_manager returns the same instance."""
248
+ manager1 = get_preview_manager()
249
+ manager2 = get_preview_manager()
250
+
251
+ assert manager1 is manager2
252
+ assert isinstance(manager1, StreamlitPreviewManager)
253
+
254
+ @pytest.mark.parametrize("num_previews", [1, 2, 3])
255
+ def test_concurrent_previews(self, manager, sample_app_code, num_previews):
256
+ """Test managing multiple concurrent previews."""
257
+ preview_ids = [f"preview_{i}" for i in range(num_previews)]
258
+ ports = []
259
+
260
+ with patch('subprocess.Popen') as mock_popen, \
261
+ patch('requests.get') as mock_requests_get, \
262
+ patch('tempfile.mkdtemp') as mock_mkdtemp, \
263
+ patch('builtins.open', create=True) as mock_open, \
264
+ patch('os.path.exists') as mock_exists:
265
+
266
+ # Setup mocks
267
+ mock_mkdtemp.return_value = "/tmp/test_dir"
268
+ mock_proc = Mock()
269
+ mock_proc.terminate.return_value = None
270
+ mock_proc.wait.return_value = None
271
+ mock_popen.return_value = mock_proc
272
+
273
+ mock_response = Mock()
274
+ mock_response.status_code = 200
275
+ mock_requests_get.return_value = mock_response
276
+
277
+ # Mock file operations
278
+ mock_file = Mock()
279
+ mock_open.return_value.__enter__.return_value = mock_file
280
+ mock_exists.return_value = True
281
+
282
+ # Start multiple previews
283
+ for preview_id in preview_ids:
284
+ success, _, port = manager.start_streamlit_preview("/tmp/test_dir", preview_id)
285
+ assert success is True
286
+ ports.append(port)
287
+
288
+ # Assertions
289
+ assert len(set(ports)) == num_previews # All ports should be different
290
+
291
+ # Check all previews exist
292
+ for preview_id in preview_ids:
293
+ assert manager.get_preview(preview_id) is not None
294
+
295
+ # Stop all previews
296
+ for preview_id in preview_ids:
297
+ assert manager.stop_preview(preview_id)
298
+
299
+ # Verify they're gone
300
+ for preview_id in preview_ids:
301
+ assert manager.get_preview(preview_id) is None
302
+
@@ -5,7 +5,7 @@ import pytest
5
5
  import anthropic
6
6
  from typing import List, Dict, Any, Tuple, Union, cast
7
7
  from anthropic.types import MessageParam, ToolUnionParam, ToolParam
8
- from mito_ai.utils.anthropic_utils import _prepare_anthropic_request_data_and_headers
8
+ from mito_ai.utils.anthropic_utils import ANTHROPIC_TIMEOUT, _prepare_anthropic_request_data_and_headers
9
9
  from mito_ai.completions.models import MessageType
10
10
  from mito_ai.utils.schema import UJ_STATIC_USER_ID, UJ_USER_EMAIL
11
11
  from mito_ai.utils.db import get_user_field
@@ -52,7 +52,7 @@ def test_basic_request_preparation():
52
52
  )
53
53
 
54
54
  assert headers == {"Content-Type": "application/json"}
55
- assert data["timeout"] == 30
55
+ assert data["timeout"] == ANTHROPIC_TIMEOUT
56
56
  assert data["max_retries"] == 1
57
57
  assert data["email"] == "test@example.com"
58
58
  assert data["user_id"] == "test_user_id"
@@ -23,7 +23,7 @@ from mito_ai.constants import MITO_ANTHROPIC_URL
23
23
  __user_email: Optional[str] = None
24
24
  __user_id: Optional[str] = None
25
25
 
26
- timeout = 30
26
+ ANTHROPIC_TIMEOUT = 60
27
27
  max_retries = 1
28
28
 
29
29
  FAST_ANTHROPIC_MODEL = "claude-3-5-haiku-latest"
@@ -63,7 +63,7 @@ def _prepare_anthropic_request_data_and_headers(
63
63
  inner_data["stream"] = stream
64
64
  # Compose the outer data dict
65
65
  data = {
66
- "timeout": timeout,
66
+ "timeout": ANTHROPIC_TIMEOUT,
67
67
  "max_retries": max_retries,
68
68
  "email": __user_email,
69
69
  "user_id": __user_id,
@@ -90,7 +90,7 @@ async def get_anthropic_completion_from_mito_server(
90
90
  MITO_ANTHROPIC_URL,
91
91
  headers,
92
92
  data,
93
- timeout,
93
+ ANTHROPIC_TIMEOUT,
94
94
  max_retries,
95
95
  message_type,
96
96
  provider_name="Claude"
@@ -118,7 +118,7 @@ async def stream_anthropic_completion_from_mito_server(
118
118
  url=MITO_ANTHROPIC_URL,
119
119
  headers=headers,
120
120
  data=data,
121
- timeout=timeout,
121
+ timeout=ANTHROPIC_TIMEOUT,
122
122
  max_retries=max_retries,
123
123
  message_type=message_type,
124
124
  reply_fn=actual_reply_fn,
@@ -196,9 +196,5 @@ def get_open_ai_completion_function_params(
196
196
  "strict": True
197
197
  }
198
198
  }
199
-
200
- # o3-mini will error if we try setting the temperature
201
- if not model.startswith("o3"):
202
- completion_function_params["temperature"] = 0.0
203
199
 
204
200
  return completion_function_params
@@ -174,7 +174,6 @@ def log(
174
174
  If telemetry is not turned off and we are not running tests,
175
175
  we log the ai event
176
176
  """
177
-
178
177
  final_params: Dict[str, Any] = params or {}
179
178
 
180
179
  # Then, make sure to add the user email
@@ -355,4 +354,32 @@ def log_db_connection_error(connection_type: str, error_message: str) -> None:
355
354
  "error_message": error_message,
356
355
  }
357
356
  )
357
+
358
+ #################################
359
+ # Streamlit Conversion
360
+ #################################
358
361
 
362
+ def log_streamlit_app_creation_success(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType) -> None:
363
+ log(
364
+ "mito_ai_streamlit_app_creation_success",
365
+ key_type=key_type
366
+ )
367
+
368
+ def log_streamlit_app_creation_retry(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType, error: str) -> None:
369
+ log(
370
+ "mito_ai_streamlit_app_creation_retry",
371
+ params={
372
+ "error_message": error,
373
+ },
374
+ key_type=key_type
375
+ )
376
+
377
+ def log_streamlit_app_creation_error(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType, error: str) -> None:
378
+ log(
379
+ "mito_ai_streamlit_app_creation_error",
380
+ params={
381
+ "error_message": error,
382
+ },
383
+ key_type=key_type
384
+ )
385
+
@@ -710,7 +710,7 @@
710
710
  "semver": {},
711
711
  "vscode-diff": {},
712
712
  "mito_ai": {
713
- "version": "0.1.37",
713
+ "version": "0.1.39",
714
714
  "singleton": true,
715
715
  "import": "/home/runner/work/mito/mito/mito-ai/lib/index.js"
716
716
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mito_ai",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "AI chat for JupyterLab",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -138,7 +138,7 @@
138
138
  "outputDir": "mito_ai/labextension",
139
139
  "schemaDir": "schema",
140
140
  "_build": {
141
- "load": "static/remoteEntry.93ecc9bc0edba61535cc.js",
141
+ "load": "static/remoteEntry.606207904e6aaa42b1bf.js",
142
142
  "extension": "./extension",
143
143
  "style": "./style"
144
144
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mito_ai",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "AI chat for JupyterLab",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -27,10 +27,15 @@
27
27
  }
28
28
  ],
29
29
  "Notebook": [
30
+ {
31
+ "name": "preview-app",
32
+ "command": "toolbar-button:preview-as-streamlit",
33
+ "rank": 1000
34
+ },
30
35
  {
31
36
  "name": "convert-to-streamlit",
32
37
  "command": "toolbar-button:convert-to-streamlit",
33
- "rank": 1000
38
+ "rank": 1001
34
39
  }
35
40
  ]
36
41
  }