Skip to content

Commit 0a9e54b

Browse files
authored
Merge pull request #2078 from mito-ds/dev
Release December 5, 2025
2 parents 969d26c + b2ca344 commit 0a9e54b

File tree

19 files changed

+222
-66
lines changed

19 files changed

+222
-66
lines changed

mito-ai/mito_ai/completions/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class AgentResponse(BaseModel):
3535
get_cell_output_cell_id: Optional[str]
3636
next_steps: Optional[List[str]]
3737
analysis_assumptions: Optional[List[str]]
38-
edit_streamlit_app_prompt: Optional[str]
38+
streamlit_app_prompt: Optional[str]
3939

4040

4141
@dataclass(frozen=True)

mito-ai/mito_ai/completions/prompt_builders/agent_system_message.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,15 +231,17 @@ def create_agent_system_message_prompt(isChromeBrowser: bool) -> str:
231231
232232
{{
233233
type: 'create_streamlit_app',
234+
streamlit_app_prompt: str
234235
message: str
235236
}}
236237
237238
Important information:
238-
1. The message is a short summary of why you're creating the Streamlit app.
239-
2. Only use this tool when the user explicitly asks to create or preview a Streamlit app AND no Streamlit app is currently open.
240-
3. This tool creates a new app from scratch - use EDIT_STREAMLIT_APP tool if the user is asking you to edit, update, or modify an app that already exists.
241-
4. Using this tool will automatically open the app so the user can see a preview of the app.
242-
5. When you use this tool, assume that it successfully created the Streamlit app unless the user explicitly tells you otherwise. The app will remain open so that the user can view it until the user decides to close it. You do not need to continually use the create_streamlit_app tool to keep the app open.
239+
1. The streamlit_app_prompt is a short description of how the app should be structured. It should be a high level specification that includes things like what fields should be configurable, what tabs should exist, etc. It does not need to be overly detailed however.
240+
2. The message is a short summary of why you're creating the Streamlit app.
241+
3. Only use this tool when the user explicitly asks to create or preview a Streamlit app. If the streamlit app for this app already exists, then use an empty string '' as the streamlit_app_prompt.
242+
4. This tool creates a new app from scratch - use EDIT_STREAMLIT_APP tool if the user is asking you to edit, update, or modify an app that already exists.
243+
5. Using this tool will automatically open the app so the user can see a preview of the app. If the user is asking you to open an app that already exists, but not make any changes to the app, this is the correct tool.
244+
6. When you use this tool, assume that it successfully created the Streamlit app unless the user explicitly tells you otherwise. The app will remain open so that the user can view it until the user decides to close it. You do not need to continually use the create_streamlit_app tool to keep the app open.
243245
244246
<Example>
245247
@@ -248,6 +250,7 @@ def create_agent_system_message_prompt(isChromeBrowser: bool) -> str:
248250
Output:
249251
{{
250252
type: 'create_streamlit_app',
253+
streamlit_app_prompt: "The app should have a beginning date and end date input field at the top. It should then be followed by two tabs for the user to select between: current performance and projected performance.",
251254
message: "I'll convert your notebook into an app."
252255
}}
253256
@@ -264,12 +267,12 @@ def create_agent_system_message_prompt(isChromeBrowser: bool) -> str:
264267
{{
265268
type: 'edit_streamlit_app',
266269
message: str,
267-
edit_streamlit_app_prompt: str
270+
streamlit_app_prompt: str
268271
}}
269272
270273
Important information:
271274
1. The message is a short summary of why you're editing the Streamlit app.
272-
2. The edit_streamlit_app_prompt is REQUIRED and must contain specific instructions for the edit (e.g., "Make the title text larger", "Change the chart colors to blue", "Add a sidebar with filters").
275+
2. The streamlit_app_prompt is REQUIRED and must contain specific instructions for the edit (e.g., "Make the title text larger", "Change the chart colors to blue", "Add a sidebar with filters").
273276
3. Only use this tool when the user asks to edit, update, or modify a Streamlit app.
274277
4. The app does not need to already be open for you to use the tool. Using this tool will automatically open the streamlit app after applying the changes so the user can view it. You do not need to call the create_streamlit_app tool first.
275278
5. When you use this tool, assume that it successfully edited the Streamlit app unless the user explicitly tells you otherwise. The app will remain open so that the user can view it until the user decides to close it.

mito-ai/mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,24 @@
44
from typing import List
55
from mito_ai.streamlit_conversion.prompts.prompt_constants import MITO_TODO_PLACEHOLDER
66

7-
def get_streamlit_app_creation_prompt(notebook: List[dict]) -> str:
7+
def get_streamlit_app_spec_section(streamlit_app_prompt: str) -> str:
8+
if streamlit_app_prompt == '':
9+
return ''
10+
11+
return f"""
12+
Here is a high level outline of the streamlit app. Use your best judgement to implement this structure.
13+
14+
{streamlit_app_prompt}
15+
16+
"""
17+
18+
19+
def get_streamlit_app_creation_prompt(notebook: List[dict], streamlit_app_prompt: str) -> str:
820
"""
921
This prompt is used to create a streamlit app from a notebook.
1022
"""
23+
streamlit_app_spec_section = get_streamlit_app_spec_section(streamlit_app_prompt)
24+
1125
return f"""Convert the following Jupyter notebook into a Streamlit application.
1226
1327
GOAL: Create a complete, runnable Streamlit app that accurately represents the notebook. It must completely convert the notebook.
@@ -40,7 +54,9 @@ def get_streamlit_app_creation_prompt(notebook: List[dict]) -> str:
4054
]
4155
</Example>
4256
43-
Notebook to convert:
57+
{streamlit_app_spec_section}
58+
59+
NOTEBOOK TO CONVERT:
4460
4561
{notebook}
4662
"""

mito-ai/mito_ai/streamlit_conversion/streamlit_agent_handler.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
from mito_ai.utils.telemetry_utils import log_streamlit_app_validation_retry, log_streamlit_app_conversion_success
1717
from mito_ai.path_utils import AbsoluteNotebookPath, AppFileName, get_absolute_notebook_dir_path, get_absolute_app_path, get_app_file_name
1818

19-
async def generate_new_streamlit_code(notebook: List[dict]) -> str:
19+
async def generate_new_streamlit_code(notebook: List[dict], streamlit_app_prompt: str) -> str:
2020
"""Send a query to the agent, get its response and parse the code"""
2121

22-
prompt_text = get_streamlit_app_creation_prompt(notebook)
22+
prompt_text = get_streamlit_app_creation_prompt(notebook, streamlit_app_prompt)
2323

2424
messages: List[MessageParam] = [
2525
cast(MessageParam, {
@@ -100,30 +100,30 @@ async def correct_error_in_generation(error: str, streamlit_app_code: str) -> st
100100

101101
return streamlit_app_code
102102

103-
async def streamlit_handler(notebook_path: AbsoluteNotebookPath, app_file_name: AppFileName, edit_prompt: str = "") -> None:
103+
async def streamlit_handler(create_new_app: bool, notebook_path: AbsoluteNotebookPath, app_file_name: AppFileName, streamlit_app_prompt: str = "") -> None:
104104
"""Handler function for streamlit code generation and validation"""
105105

106106
# Convert to absolute path for consistent handling
107107
notebook_code = parse_jupyter_notebook_to_extract_required_content(notebook_path)
108108
app_directory = get_absolute_notebook_dir_path(notebook_path)
109109
app_path = get_absolute_app_path(app_directory, app_file_name)
110110

111-
if edit_prompt != "":
111+
if create_new_app:
112+
# Otherwise generate a new streamlit app
113+
streamlit_code = await generate_new_streamlit_code(notebook_code, streamlit_app_prompt)
114+
else:
112115
# If the user is editing an existing streamlit app, use the update function
113-
streamlit_code = get_app_code_from_file(app_path)
116+
existing_streamlit_code = get_app_code_from_file(app_path)
114117

115-
if streamlit_code is None:
118+
if existing_streamlit_code is None:
116119
raise StreamlitConversionError("Error updating existing streamlit app because app.py file was not found.", 404)
117120

118-
streamlit_code = await update_existing_streamlit_code(notebook_code, streamlit_code, edit_prompt)
119-
else:
120-
# Otherwise generate a new streamlit app
121-
streamlit_code = await generate_new_streamlit_code(notebook_code)
121+
streamlit_code = await update_existing_streamlit_code(notebook_code, existing_streamlit_code, streamlit_app_prompt)
122122

123123
# Then, after creating/updating the app, validate that the new code runs
124124
errors = validate_app(streamlit_code, notebook_path)
125125
tries = 0
126-
while len(errors)>0 and tries < 5:
126+
while len(errors) > 0 and tries < 5:
127127
for error in errors:
128128
streamlit_code = await correct_error_in_generation(error, streamlit_code)
129129

@@ -141,4 +141,4 @@ async def streamlit_handler(notebook_path: AbsoluteNotebookPath, app_file_name:
141141

142142
# Finally, update the app.py file with the new code
143143
create_app_file(app_path, streamlit_code)
144-
log_streamlit_app_conversion_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
144+
log_streamlit_app_conversion_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, streamlit_app_prompt)

mito-ai/mito_ai/streamlit_preview/handlers.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,34 @@ def initialize(self) -> None:
2222
self.preview_manager = StreamlitPreviewManager()
2323

2424
@tornado.web.authenticated
25+
2526
async def post(self) -> None:
2627
"""Start a new streamlit preview."""
2728
try:
29+
2830
# Parse and validate request
2931
body = self.get_json_body()
30-
notebook_path, notebook_id, force_recreate, edit_prompt = validate_request_body(body)
32+
notebook_path, notebook_id, force_recreate, streamlit_app_prompt = validate_request_body(body)
3133

3234
# Ensure app exists
3335
absolute_notebook_path = get_absolute_notebook_path(notebook_path)
3436
absolute_notebook_dir_path = get_absolute_notebook_dir_path(absolute_notebook_path)
3537
app_file_name = get_app_file_name(notebook_id)
3638
absolute_app_path = get_absolute_app_path(absolute_notebook_dir_path, app_file_name)
3739
app_path_exists = does_app_path_exist(absolute_app_path)
38-
40+
3941
if not app_path_exists or force_recreate:
4042
if not app_path_exists:
4143
print("[Mito AI] App path not found, generating streamlit code")
4244
else:
4345
print("[Mito AI] Force recreating streamlit app")
4446

45-
await streamlit_handler(absolute_notebook_path, app_file_name, edit_prompt)
47+
# Create a new app
48+
await streamlit_handler(True, absolute_notebook_path, app_file_name, streamlit_app_prompt)
49+
elif streamlit_app_prompt != '':
50+
# Update an existing app if there is a prompt provided. Otherwise, the user is just
51+
# starting an existing app so we can skip the streamlit_handler all together
52+
await streamlit_handler(False, absolute_notebook_path, app_file_name, streamlit_app_prompt)
4653

4754
# Start preview
4855
# TODO: There's a bug here where when the user rebuilds and already running app. Instead of
@@ -58,7 +65,7 @@ async def post(self) -> None:
5865
"port": port,
5966
"url": f"http://localhost:{port}"
6067
})
61-
log_streamlit_app_preview_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
68+
log_streamlit_app_preview_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, streamlit_app_prompt)
6269

6370
except StreamlitConversionError as e:
6471
print(e)
@@ -71,15 +78,15 @@ async def post(self) -> None:
7178
MessageType.STREAMLIT_CONVERSION,
7279
error_message,
7380
formatted_traceback,
74-
edit_prompt,
81+
streamlit_app_prompt,
7582
)
7683
except StreamlitPreviewError as e:
7784
print(e)
7885
error_message = str(e)
7986
formatted_traceback = traceback.format_exc()
8087
self.set_status(e.error_code)
8188
self.finish({"error": error_message})
82-
log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, edit_prompt)
89+
log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, streamlit_app_prompt)
8390
except Exception as e:
8491
print(f"Exception in streamlit preview handler: {e}")
8592
self.set_status(500)

mito-ai/mito_ai/streamlit_preview/manager.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import socket
55
import subprocess
6+
import sys
67
import time
78
import threading
89
import requests
@@ -54,8 +55,10 @@ def start_streamlit_preview(self, app_directory: AbsoluteNotebookDirPath, app_fi
5455
port = self.get_free_port()
5556

5657
# Start streamlit process
58+
# Use sys.executable -m streamlit to ensure it works on Windows
59+
# where streamlit may not be directly executable in PATH
5760
cmd = [
58-
"streamlit", "run", app_file_name,
61+
sys.executable, "-m", "streamlit", "run", app_file_name,
5962
"--server.port", str(port),
6063
"--server.headless", "true",
6164
"--server.address", "localhost",

mito-ai/mito_ai/streamlit_preview/utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ def validate_request_body(body: Optional[dict]) -> Tuple[str, str, bool, str]:
2222
if not isinstance(force_recreate, bool):
2323
raise StreamlitPreviewError("force_recreate must be a boolean", 400)
2424

25-
edit_prompt = body.get("edit_prompt", "")
26-
if not isinstance(edit_prompt, str):
27-
raise StreamlitPreviewError("edit_prompt must be a string", 400)
25+
streamlit_app_prompt = body.get("streamlit_app_prompt", "")
26+
if not isinstance(streamlit_app_prompt, str):
27+
raise StreamlitPreviewError("streamlit_app_prompt must be a string", 400)
2828

29-
return notebook_path, notebook_id, force_recreate, edit_prompt
29+
return notebook_path, notebook_id, force_recreate, streamlit_app_prompt

mito-ai/mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async def mock_async_gen():
8989
mock_stream.return_value = mock_async_gen()
9090

9191
notebook_data: List[dict] = [{"cells": []}]
92-
result = await generate_new_streamlit_code(notebook_data)
92+
result = await generate_new_streamlit_code(notebook_data, '')
9393

9494
expected_code = "import streamlit\nst.title('Hello')\n"
9595
assert result == expected_code
@@ -158,11 +158,11 @@ async def test_streamlit_handler_success(self, mock_log_success, mock_create_fil
158158
# Construct the expected app path using the same method as the production code
159159
app_directory = get_absolute_notebook_dir_path(notebook_path)
160160
expected_app_path = get_absolute_app_path(app_directory, app_file_name)
161-
await streamlit_handler(notebook_path, app_file_name)
161+
await streamlit_handler(True, notebook_path, app_file_name)
162162

163163
# Verify calls
164164
mock_parse.assert_called_once_with(notebook_path)
165-
mock_generate_code.assert_called_once_with(mock_notebook_data)
165+
mock_generate_code.assert_called_once_with(mock_notebook_data, '')
166166
mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
167167
mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
168168

@@ -187,7 +187,7 @@ async def test_streamlit_handler_max_retries_exceeded(self, mock_log_retry, mock
187187

188188
# Now it should raise an exception instead of returning a tuple
189189
with pytest.raises(Exception):
190-
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
190+
await streamlit_handler(True, AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'), '')
191191

192192
# Verify that error correction was called 5 times (once per error, 5 retries)
193193
# Each retry processes 1 error, so 5 retries = 5 calls
@@ -215,7 +215,7 @@ async def test_streamlit_handler_file_creation_failure(self, mock_create_file, m
215215

216216
# Now it should raise an exception instead of returning a tuple
217217
with pytest.raises(Exception):
218-
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
218+
await streamlit_handler(True, AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'), '')
219219

220220
@pytest.mark.asyncio
221221
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
@@ -225,7 +225,7 @@ async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
225225
mock_parse.side_effect = FileNotFoundError("Notebook not found")
226226

227227
with pytest.raises(FileNotFoundError, match="Notebook not found"):
228-
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
228+
await streamlit_handler(True, AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'), '')
229229

230230
@pytest.mark.asyncio
231231
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
@@ -240,7 +240,7 @@ async def test_streamlit_handler_generation_exception(self, mock_generate_code,
240240
mock_generate_code.side_effect = Exception("Generation failed")
241241

242242
with pytest.raises(Exception, match="Generation failed"):
243-
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
243+
await streamlit_handler(True, AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'), '')
244244

245245

246246

mito-ai/mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,10 @@ def mock_finish_func(response):
9999
assert mock_streamlit_handler.called
100100
# Verify it was called with the correct arguments
101101
call_args = mock_streamlit_handler.call_args
102-
assert call_args[0][0] == os.path.abspath(notebook_path) # First argument should be the absolute notebook path
103-
assert call_args[0][1] == app_file_name # Second argument should be the app file name
104-
assert call_args[0][2] == "" # Third argument should be the edit_prompt
102+
assert call_args[0][0] == True
103+
assert call_args[0][1] == os.path.abspath(notebook_path) # First argument should be the absolute notebook path
104+
assert call_args[0][2] == app_file_name # Second argument should be the app file name
105+
assert call_args[0][3] == "" # Third argument should be the edit_prompt
105106
else:
106107
mock_streamlit_handler.assert_not_called()
107108

mito-ai/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
3535
"build:labextension": "jupyter labextension build .",
3636
"build:labextension:dev": "jupyter labextension build --development True .",
37-
"build:lib": "rm -rf buildcache && npx tsc --sourceMap",
38-
"build:lib:prod": "rm -rf buildcache && npx tsc",
37+
"build:lib": "rimraf buildcache && npx tsc --sourceMap",
38+
"build:lib:prod": "rimraf buildcache && npx tsc",
3939
"clean": "jlpm clean:lib",
4040
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
4141
"clean:lintcache": "rimraf .eslintcache .stylelintcache",

0 commit comments

Comments
 (0)