mito-ai 0.1.44__py3-none-any.whl → 0.1.46__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.
- mito_ai/__init__.py +10 -1
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +92 -8
- mito_ai/app_deploy/app_deploy_utils.py +25 -0
- mito_ai/app_deploy/handlers.py +9 -12
- mito_ai/app_deploy/models.py +4 -1
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/handlers.py +44 -20
- mito_ai/completions/models.py +1 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +22 -4
- mito_ai/constants.py +3 -0
- mito_ai/streamlit_conversion/agent_utils.py +148 -30
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +147 -24
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +2 -2
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +101 -104
- mito_ai/streamlit_conversion/streamlit_system_prompt.py +1 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +18 -17
- mito_ai/streamlit_conversion/validate_streamlit_app.py +66 -62
- mito_ai/streamlit_preview/handlers.py +5 -3
- mito_ai/streamlit_preview/utils.py +11 -7
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +71 -0
- mito_ai/tests/message_history/test_message_history_utils.py +43 -19
- mito_ai/tests/providers/test_anthropic_client.py +180 -8
- mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +368 -0
- mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +533 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +71 -158
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +16 -16
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +16 -28
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +2 -2
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/test_anthropic_utils.py +4 -4
- mito_ai/user/handlers.py +33 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/anthropic_utils.py +15 -21
- mito_ai/utils/message_history_utils.py +4 -3
- mito_ai/utils/telemetry_utils.py +7 -4
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.cf2e3ad2797fbb53826b.js → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js +1520 -300
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +1 -0
- mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5482493d1270f55b7283.js → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js +18 -18
- mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5482493d1270f55b7283.js.map → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map +1 -1
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/METADATA +2 -2
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/RECORD +75 -63
- mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.cf2e3ad2797fbb53826b.js.map +0 -1
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.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
- {mito_ai-0.1.44.data → mito_ai-0.1.46.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
- {mito_ai-0.1.44.data → mito_ai-0.1.46.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
- {mito_ai-0.1.44.data → mito_ai-0.1.46.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
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.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
- {mito_ai-0.1.44.data → mito_ai-0.1.46.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
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,135 +1,138 @@
|
|
|
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 logging
|
|
5
4
|
import os
|
|
6
5
|
from anthropic.types import MessageParam
|
|
7
6
|
from typing import List, Optional, Tuple, cast
|
|
8
|
-
|
|
9
|
-
from mito_ai.logger import get_logger
|
|
10
|
-
from mito_ai.streamlit_conversion.agent_utils import apply_patch_to_text, extract_todo_placeholders, fix_diff_headers
|
|
7
|
+
from mito_ai.streamlit_conversion.agent_utils import apply_patch_to_text, extract_todo_placeholders, fix_diff_headers, get_response_from_agent
|
|
11
8
|
from mito_ai.streamlit_conversion.prompts.streamlit_app_creation_prompt import get_streamlit_app_creation_prompt
|
|
12
9
|
from mito_ai.streamlit_conversion.prompts.streamlit_error_correction_prompt import get_streamlit_error_correction_prompt
|
|
13
10
|
from mito_ai.streamlit_conversion.prompts.streamlit_finish_todo_prompt import get_finish_todo_prompt
|
|
14
|
-
from mito_ai.streamlit_conversion.
|
|
11
|
+
from mito_ai.streamlit_conversion.prompts.update_existing_app_prompt import get_update_existing_app_prompt
|
|
15
12
|
from mito_ai.streamlit_conversion.validate_streamlit_app import validate_app
|
|
16
|
-
from mito_ai.streamlit_conversion.streamlit_utils import extract_code_blocks, create_app_file, extract_unified_diff_blocks, parse_jupyter_notebook_to_extract_required_content
|
|
17
|
-
from mito_ai.utils.anthropic_utils import stream_anthropic_completion_from_mito_server
|
|
13
|
+
from mito_ai.streamlit_conversion.streamlit_utils import extract_code_blocks, create_app_file, extract_unified_diff_blocks, get_app_code_from_file, parse_jupyter_notebook_to_extract_required_content
|
|
18
14
|
from mito_ai.completions.models import MessageType
|
|
19
15
|
from mito_ai.utils.telemetry_utils import log_streamlit_app_creation_error, log_streamlit_app_creation_retry, log_streamlit_app_creation_success
|
|
20
16
|
from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
async def get_response_from_agent(self, message_to_agent: List[MessageParam]) -> str:
|
|
31
|
-
"""Gets the streaming response from the agent using the mito server"""
|
|
32
|
-
model = STREAMLIT_AI_MODEL
|
|
33
|
-
max_tokens = 8192 # 64_000
|
|
34
|
-
temperature = 0.2
|
|
35
|
-
|
|
36
|
-
self.log.info("Getting response from agent...")
|
|
37
|
-
accumulated_response = ""
|
|
38
|
-
async for stream_chunk in stream_anthropic_completion_from_mito_server(
|
|
39
|
-
model = model,
|
|
40
|
-
max_tokens = max_tokens,
|
|
41
|
-
temperature = temperature,
|
|
42
|
-
system = streamlit_system_prompt,
|
|
43
|
-
messages = message_to_agent,
|
|
44
|
-
stream=True,
|
|
45
|
-
message_type=MessageType.STREAMLIT_CONVERSION,
|
|
46
|
-
reply_fn=None,
|
|
47
|
-
message_id=""
|
|
48
|
-
):
|
|
49
|
-
accumulated_response += stream_chunk
|
|
50
|
-
return accumulated_response
|
|
51
|
-
|
|
52
|
-
async def generate_streamlit_code(self, notebook: dict) -> str:
|
|
53
|
-
"""Send a query to the agent, get its response and parse the code"""
|
|
54
|
-
|
|
55
|
-
messages: List[MessageParam] = [
|
|
56
|
-
cast(MessageParam, {
|
|
57
|
-
"role": "user",
|
|
58
|
-
"content": [{
|
|
59
|
-
"type": "text",
|
|
60
|
-
"text": get_streamlit_app_creation_prompt(notebook)
|
|
61
|
-
}]
|
|
62
|
-
})
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
agent_response = await self.get_response_from_agent(messages)
|
|
66
|
-
|
|
67
|
-
converted_code = extract_code_blocks(agent_response)
|
|
68
|
-
|
|
69
|
-
# Extract the TODOs from the agent's response
|
|
70
|
-
todo_placeholders = extract_todo_placeholders(agent_response)
|
|
71
|
-
|
|
72
|
-
for todo_placeholder in todo_placeholders:
|
|
73
|
-
print(f"Processing AI TODO: {todo_placeholder}")
|
|
74
|
-
todo_prompt = get_finish_todo_prompt(notebook, converted_code, todo_placeholder)
|
|
75
|
-
todo_messages: List[MessageParam] = [
|
|
76
|
-
cast(MessageParam, {
|
|
77
|
-
"role": "user",
|
|
78
|
-
"content": [{
|
|
79
|
-
"type": "text",
|
|
80
|
-
"text": todo_prompt
|
|
81
|
-
}]
|
|
82
|
-
})
|
|
83
|
-
]
|
|
84
|
-
todo_response = await self.get_response_from_agent(todo_messages)
|
|
85
|
-
|
|
86
|
-
# Apply the diff to the streamlit app
|
|
87
|
-
exctracted_diff = extract_unified_diff_blocks(todo_response)
|
|
88
|
-
fixed_diff = fix_diff_headers(exctracted_diff)
|
|
89
|
-
converted_code = apply_patch_to_text(converted_code, fixed_diff)
|
|
90
|
-
|
|
91
|
-
return converted_code
|
|
92
|
-
|
|
18
|
+
def get_app_directory(notebook_path: str) -> str:
|
|
19
|
+
# Make sure the path is absolute if it is not already
|
|
20
|
+
absolute_notebook_path = os.path.abspath(notebook_path)
|
|
21
|
+
|
|
22
|
+
# Get the directory of the notebook
|
|
23
|
+
app_directory = os.path.dirname(absolute_notebook_path)
|
|
24
|
+
return app_directory
|
|
93
25
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
26
|
+
async def generate_new_streamlit_code(notebook: List[dict]) -> str:
|
|
27
|
+
"""Send a query to the agent, get its response and parse the code"""
|
|
28
|
+
|
|
29
|
+
prompt_text = get_streamlit_app_creation_prompt(notebook)
|
|
30
|
+
|
|
31
|
+
messages: List[MessageParam] = [
|
|
32
|
+
cast(MessageParam, {
|
|
33
|
+
"role": "user",
|
|
34
|
+
"content": [{
|
|
35
|
+
"type": "text",
|
|
36
|
+
"text": prompt_text
|
|
37
|
+
}]
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
agent_response = await get_response_from_agent(messages)
|
|
41
|
+
converted_code = extract_code_blocks(agent_response)
|
|
42
|
+
|
|
43
|
+
# Extract the TODOs from the agent's response
|
|
44
|
+
todo_placeholders = extract_todo_placeholders(agent_response)
|
|
45
|
+
|
|
46
|
+
for todo_placeholder in todo_placeholders:
|
|
47
|
+
print(f"Processing AI TODO: {todo_placeholder}")
|
|
48
|
+
todo_prompt = get_finish_todo_prompt(notebook, converted_code, todo_placeholder)
|
|
49
|
+
todo_messages: List[MessageParam] = [
|
|
97
50
|
cast(MessageParam, {
|
|
98
51
|
"role": "user",
|
|
99
52
|
"content": [{
|
|
100
53
|
"type": "text",
|
|
101
|
-
"text":
|
|
54
|
+
"text": todo_prompt
|
|
102
55
|
}]
|
|
103
56
|
})
|
|
104
57
|
]
|
|
105
|
-
|
|
58
|
+
todo_response = await get_response_from_agent(todo_messages)
|
|
106
59
|
|
|
107
60
|
# Apply the diff to the streamlit app
|
|
108
|
-
exctracted_diff = extract_unified_diff_blocks(
|
|
109
|
-
|
|
110
|
-
print(f"\n\nExtracted diff: {exctracted_diff}")
|
|
61
|
+
exctracted_diff = extract_unified_diff_blocks(todo_response)
|
|
111
62
|
fixed_diff = fix_diff_headers(exctracted_diff)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
63
|
+
converted_code = apply_patch_to_text(converted_code, fixed_diff)
|
|
64
|
+
|
|
65
|
+
return converted_code
|
|
115
66
|
|
|
116
|
-
return streamlit_app_code
|
|
117
67
|
|
|
68
|
+
async def update_existing_streamlit_code(notebook: List[dict], streamlit_app_code: str, edit_prompt: str) -> str:
|
|
69
|
+
"""Send a query to the agent, get its response and parse the code"""
|
|
70
|
+
prompt_text = get_update_existing_app_prompt(notebook, streamlit_app_code, edit_prompt)
|
|
71
|
+
|
|
72
|
+
messages: List[MessageParam] = [
|
|
73
|
+
cast(MessageParam, {
|
|
74
|
+
"role": "user",
|
|
75
|
+
"content": [{
|
|
76
|
+
"type": "text",
|
|
77
|
+
"text": prompt_text
|
|
78
|
+
}]
|
|
79
|
+
})
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
agent_response = await get_response_from_agent(messages)
|
|
83
|
+
exctracted_diff = extract_unified_diff_blocks(agent_response)
|
|
84
|
+
fixed_diff = fix_diff_headers(exctracted_diff)
|
|
85
|
+
print(fixed_diff)
|
|
86
|
+
converted_code = apply_patch_to_text(streamlit_app_code, fixed_diff)
|
|
87
|
+
return converted_code
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def correct_error_in_generation(error: str, streamlit_app_code: str) -> str:
|
|
91
|
+
"""If errors are present, send it back to the agent to get corrections in code"""
|
|
92
|
+
messages: List[MessageParam] = [
|
|
93
|
+
cast(MessageParam, {
|
|
94
|
+
"role": "user",
|
|
95
|
+
"content": [{
|
|
96
|
+
"type": "text",
|
|
97
|
+
"text": get_streamlit_error_correction_prompt(error, streamlit_app_code)
|
|
98
|
+
}]
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
agent_response = await get_response_from_agent(messages)
|
|
102
|
+
|
|
103
|
+
# Apply the diff to the streamlit app
|
|
104
|
+
exctracted_diff = extract_unified_diff_blocks(agent_response)
|
|
105
|
+
fixed_diff = fix_diff_headers(exctracted_diff)
|
|
106
|
+
streamlit_app_code = apply_patch_to_text(streamlit_app_code, fixed_diff)
|
|
118
107
|
|
|
119
|
-
|
|
108
|
+
return streamlit_app_code
|
|
109
|
+
|
|
110
|
+
async def streamlit_handler(notebook_path: str, edit_prompt: str = "") -> Tuple[bool, Optional[str], str]:
|
|
120
111
|
"""Handler function for streamlit code generation and validation"""
|
|
121
112
|
|
|
122
113
|
clean_directory_check(notebook_path)
|
|
123
114
|
|
|
124
115
|
notebook_code = parse_jupyter_notebook_to_extract_required_content(notebook_path)
|
|
125
|
-
|
|
126
|
-
streamlit_code = await streamlit_code_generator.generate_streamlit_code(notebook_code)
|
|
116
|
+
app_directory = get_app_directory(notebook_path)
|
|
127
117
|
|
|
118
|
+
if edit_prompt != "":
|
|
119
|
+
# If the user is editing an existing streamlit app, use the update function
|
|
120
|
+
streamlit_code = get_app_code_from_file(app_directory)
|
|
121
|
+
|
|
122
|
+
if streamlit_code is None:
|
|
123
|
+
return False, '', "Error updating existing streamlit app because app.py file was not found."
|
|
124
|
+
|
|
125
|
+
streamlit_code = await update_existing_streamlit_code(notebook_code, streamlit_code, edit_prompt)
|
|
126
|
+
else:
|
|
127
|
+
# Otherwise generate a new streamlit app
|
|
128
|
+
streamlit_code = await generate_new_streamlit_code(notebook_code)
|
|
129
|
+
|
|
130
|
+
# Then, after creating/updating the app, validate that the new code runs
|
|
128
131
|
has_validation_error, errors = validate_app(streamlit_code, notebook_path)
|
|
129
132
|
tries = 0
|
|
130
133
|
while has_validation_error and tries < 5:
|
|
131
134
|
for error in errors:
|
|
132
|
-
streamlit_code = await
|
|
135
|
+
streamlit_code = await correct_error_in_generation(error, streamlit_code)
|
|
133
136
|
|
|
134
137
|
has_validation_error, errors = validate_app(streamlit_code, notebook_path)
|
|
135
138
|
|
|
@@ -140,19 +143,13 @@ async def streamlit_handler(notebook_path: str) -> Tuple[bool, Optional[str], st
|
|
|
140
143
|
tries+=1
|
|
141
144
|
|
|
142
145
|
if has_validation_error:
|
|
143
|
-
log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, error)
|
|
146
|
+
log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, error, edit_prompt)
|
|
144
147
|
return False, '', "Error generating streamlit code by agent"
|
|
145
148
|
|
|
146
|
-
#
|
|
147
|
-
absolute_notebook_path = notebook_path
|
|
148
|
-
if not (notebook_path.startswith('/') or (len(notebook_path) > 1 and notebook_path[1] == ':')):
|
|
149
|
-
absolute_notebook_path = os.path.join(os.getcwd(), notebook_path)
|
|
150
|
-
|
|
151
|
-
app_directory = os.path.dirname(absolute_notebook_path)
|
|
149
|
+
# Finally, update the app.py file with the new code
|
|
152
150
|
success_flag, app_path, message = create_app_file(app_directory, streamlit_code)
|
|
153
|
-
|
|
154
151
|
if not success_flag:
|
|
155
|
-
log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, message)
|
|
152
|
+
log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, message, edit_prompt)
|
|
156
153
|
|
|
157
|
-
log_streamlit_app_creation_success('mito_server_key', MessageType.STREAMLIT_CONVERSION)
|
|
154
|
+
log_streamlit_app_creation_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
|
|
158
155
|
return success_flag, app_path, message
|
|
@@ -22,6 +22,7 @@ STREAMLIT IMPLEMENTATION GUIDELINES:
|
|
|
22
22
|
- Include all text explanations and insights from markdown cells
|
|
23
23
|
- Add interactive elements where beneficial (filters, selectors, etc.)
|
|
24
24
|
- Ensure professional styling and layout suitable for executives
|
|
25
|
+
- Just create the streamlit app code, do not include a _main_ function block. The file will be run directly using `streamlit run app.py`.
|
|
25
26
|
|
|
26
27
|
CRITICAL REQUIREMENTS:
|
|
27
28
|
1. **PRESERVE ALL CODE EXACTLY**: Every line of code, every data structure, every import must be included in full
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import re
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
-
from typing import Dict, Optional, Tuple, Any
|
|
7
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
def extract_code_blocks(message_content: str) -> str:
|
|
@@ -25,7 +25,8 @@ def extract_code_blocks(message_content: str) -> str:
|
|
|
25
25
|
matches = re.findall(pattern, message_content, re.DOTALL)
|
|
26
26
|
|
|
27
27
|
# Concatenate with single newlines
|
|
28
|
-
|
|
28
|
+
result = '\n'.join(matches)
|
|
29
|
+
return result
|
|
29
30
|
|
|
30
31
|
def extract_unified_diff_blocks(message_content: str) -> str:
|
|
31
32
|
"""
|
|
@@ -53,14 +54,23 @@ def create_app_file(app_directory: str, code: str) -> Tuple[bool, str, str]:
|
|
|
53
54
|
"""
|
|
54
55
|
try:
|
|
55
56
|
app_path = os.path.join(app_directory, "app.py")
|
|
56
|
-
|
|
57
|
+
|
|
58
|
+
with open(app_path, 'w', encoding='utf-8') as f:
|
|
57
59
|
f.write(code)
|
|
60
|
+
|
|
58
61
|
return True, app_path, f"Successfully created {app_directory}"
|
|
59
62
|
except IOError as e:
|
|
60
63
|
return False, '', f"Error creating file: {str(e)}"
|
|
61
64
|
except Exception as e:
|
|
62
65
|
return False, '', f"Unexpected error: {str(e)}"
|
|
63
66
|
|
|
67
|
+
def get_app_code_from_file(app_directory: str) -> Optional[str]:
|
|
68
|
+
app_path = get_app_path(app_directory)
|
|
69
|
+
if app_path is None:
|
|
70
|
+
return None
|
|
71
|
+
with open(app_path, 'r', encoding='utf-8') as f:
|
|
72
|
+
return f.read()
|
|
73
|
+
|
|
64
74
|
|
|
65
75
|
def get_app_path(app_directory: str) -> Optional[str]:
|
|
66
76
|
"""
|
|
@@ -70,9 +80,8 @@ def get_app_path(app_directory: str) -> Optional[str]:
|
|
|
70
80
|
if not os.path.exists(app_path):
|
|
71
81
|
return None
|
|
72
82
|
return app_path
|
|
73
|
-
|
|
74
83
|
|
|
75
|
-
def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Dict[str, Any]:
|
|
84
|
+
def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> List[Dict[str, Any]]:
|
|
76
85
|
"""
|
|
77
86
|
Read a Jupyter notebook and filter cells to keep only cell_type and source fields.
|
|
78
87
|
|
|
@@ -102,18 +111,15 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Di
|
|
|
102
111
|
raise KeyError("Notebook does not contain 'cells' key")
|
|
103
112
|
|
|
104
113
|
# Filter each cell to keep only cell_type and source
|
|
105
|
-
filtered_cells = []
|
|
114
|
+
filtered_cells: List[Dict[str, Any]] = []
|
|
106
115
|
for cell in notebook_data['cells']:
|
|
107
|
-
filtered_cell = {
|
|
116
|
+
filtered_cell: Dict[str, Any] = {
|
|
108
117
|
'cell_type': cell.get('cell_type', ''),
|
|
109
118
|
'source': cell.get('source', [])
|
|
110
119
|
}
|
|
111
120
|
filtered_cells.append(filtered_cell)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
notebook_data['cells'] = filtered_cells
|
|
115
|
-
|
|
116
|
-
return notebook_data
|
|
121
|
+
|
|
122
|
+
return filtered_cells
|
|
117
123
|
|
|
118
124
|
except FileNotFoundError:
|
|
119
125
|
raise FileNotFoundError(f"Notebook file not found: {notebook_path}")
|
|
@@ -139,8 +145,3 @@ def clean_directory_check(notebook_path: str) -> None:
|
|
|
139
145
|
|
|
140
146
|
if not dir_path.exists():
|
|
141
147
|
raise ValueError(f"Directory does not exist: {dir_path}")
|
|
142
|
-
|
|
143
|
-
file_count = len([f for f in dir_path.iterdir() if f.is_file()])
|
|
144
|
-
if file_count > 10:
|
|
145
|
-
raise ValueError(
|
|
146
|
-
f"Too many files in directory: 10 allowed but {file_count} present. Create a new directory and retry")
|
|
@@ -23,42 +23,52 @@ from mito_ai.streamlit_conversion.streamlit_utils import resolve_notebook_path
|
|
|
23
23
|
warnings.filterwarnings("ignore", message=".*bare mode.*")
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
def get_syntax_error(app_code: str) -> Optional[str]:
|
|
27
|
+
"""Check if the Python code has valid syntax"""
|
|
28
|
+
try:
|
|
29
|
+
ast.parse(app_code)
|
|
30
|
+
return None
|
|
31
|
+
except SyntaxError as e:
|
|
32
|
+
error_msg = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
33
|
+
return error_msg
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
def get_runtime_errors(app_code: str, app_path: str) -> Optional[List[Dict[str, Any]]]:
|
|
36
|
+
"""Start the Streamlit app in a subprocess"""
|
|
37
|
+
|
|
38
|
+
directory = os.path.dirname(app_path)
|
|
39
|
+
|
|
40
|
+
@contextmanager
|
|
41
|
+
def change_working_directory(path: str) -> Generator[None, Any, None]:
|
|
42
|
+
"""
|
|
43
|
+
Context manager to temporarily change working directory
|
|
44
|
+
so that relative paths are still valid when we run the app
|
|
45
|
+
"""
|
|
46
|
+
if path == '':
|
|
47
|
+
yield
|
|
48
|
+
|
|
49
|
+
original_cwd = os.getcwd()
|
|
32
50
|
try:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
os.chdir(path)
|
|
52
|
+
yield
|
|
53
|
+
finally:
|
|
54
|
+
os.chdir(original_cwd)
|
|
55
|
+
|
|
56
|
+
with change_working_directory(directory):
|
|
57
|
+
# Create a temporary file that uses UTF-8 encoding so
|
|
58
|
+
# we don't run into issues with non-ASCII characters on Windows.
|
|
59
|
+
# We use utf-8 encoding when writing the app.py file so this validation
|
|
60
|
+
# code mirrors the actual file.
|
|
38
61
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"""
|
|
50
|
-
if path == '':
|
|
51
|
-
yield
|
|
52
|
-
|
|
53
|
-
original_cwd = os.getcwd()
|
|
54
|
-
try:
|
|
55
|
-
os.chdir(path)
|
|
56
|
-
yield
|
|
57
|
-
finally:
|
|
58
|
-
os.chdir(original_cwd)
|
|
59
|
-
|
|
60
|
-
with change_working_directory(directory):
|
|
61
|
-
app_test = AppTest.from_string(app_code, default_timeout=30)
|
|
62
|
+
# Note: Since the AppTest.from_file tries to open the file, we need to first close the file
|
|
63
|
+
# by exiting the context manager and using the delete=False flag so that the file still exists.
|
|
64
|
+
# Windows can't open the same file twice at the same time. We cleanup at the end.
|
|
65
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as f:
|
|
66
|
+
f.write(app_code)
|
|
67
|
+
temp_path = f.name
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Run Streamlit test from file with UTF-8 encoding
|
|
71
|
+
app_test = AppTest.from_file(temp_path, default_timeout=30)
|
|
62
72
|
app_test.run()
|
|
63
73
|
|
|
64
74
|
# Check for exceptions
|
|
@@ -72,44 +82,38 @@ class StreamlitValidator:
|
|
|
72
82
|
return errors
|
|
73
83
|
|
|
74
84
|
return None
|
|
85
|
+
finally:
|
|
86
|
+
# Clean up the temporary file
|
|
87
|
+
try:
|
|
88
|
+
os.unlink(temp_path)
|
|
89
|
+
except OSError:
|
|
90
|
+
pass # File might already be deleted
|
|
75
91
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
shutil.rmtree(self.temp_dir)
|
|
80
|
-
self.temp_dir = None
|
|
81
|
-
|
|
82
|
-
def _validate_app(self, app_code: str, app_path: str) -> List[Dict[str, Any]]:
|
|
83
|
-
"""Complete validation pipeline"""
|
|
84
|
-
errors: List[Dict[str, Any]] = []
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
# Step 1: Check syntax
|
|
88
|
-
syntax_error = self.get_syntax_error(app_code)
|
|
89
|
-
if syntax_error:
|
|
90
|
-
errors.append({'type': 'syntax', 'details': syntax_error})
|
|
92
|
+
def check_for_errors(app_code: str, app_path: str) -> List[Dict[str, Any]]:
|
|
93
|
+
"""Complete validation pipeline"""
|
|
94
|
+
errors: List[Dict[str, Any]] = []
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
errors.extend(runtime_errors)
|
|
98
|
-
|
|
99
|
-
except Exception as e:
|
|
100
|
-
errors.append({'type': 'validation', 'details': str(e)})
|
|
96
|
+
try:
|
|
97
|
+
# Step 1: Check syntax
|
|
98
|
+
syntax_error = get_syntax_error(app_code)
|
|
99
|
+
if syntax_error:
|
|
100
|
+
errors.append({'type': 'syntax', 'details': syntax_error})
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
runtime_errors = get_runtime_errors(app_code, app_path)
|
|
103
|
+
|
|
104
|
+
if runtime_errors:
|
|
105
|
+
errors.extend(runtime_errors)
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
errors.append({'type': 'validation', 'details': str(e)})
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
return errors
|
|
106
111
|
|
|
107
112
|
def validate_app(app_code: str, notebook_path: str) -> Tuple[bool, List[str]]:
|
|
108
113
|
"""Convenience function to validate Streamlit code"""
|
|
109
114
|
notebook_path = resolve_notebook_path(notebook_path)
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
errors = validator._validate_app(app_code, notebook_path)
|
|
116
|
+
errors = check_for_errors(app_code, notebook_path)
|
|
113
117
|
|
|
114
118
|
has_validation_error = len(errors) > 0
|
|
115
119
|
stringified_errors = [str(error) for error in errors]
|
|
@@ -82,15 +82,17 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
82
82
|
try:
|
|
83
83
|
# Parse and validate request
|
|
84
84
|
body = self.get_json_body()
|
|
85
|
-
is_valid, error_msg, notebook_path, force_recreate = validate_request_body(body)
|
|
86
|
-
if not is_valid or notebook_path
|
|
85
|
+
is_valid, error_msg, notebook_path, force_recreate, edit_prompt = validate_request_body(body)
|
|
86
|
+
if not is_valid or not notebook_path:
|
|
87
87
|
self.set_status(400)
|
|
88
88
|
self.finish({"error": error_msg})
|
|
89
89
|
return
|
|
90
90
|
|
|
91
91
|
# Ensure app exists
|
|
92
92
|
resolved_notebook_path = self._resolve_notebook_path(notebook_path)
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
success, error_msg = await ensure_app_exists(resolved_notebook_path, force_recreate, edit_prompt)
|
|
95
|
+
|
|
94
96
|
if not success:
|
|
95
97
|
self.set_status(500)
|
|
96
98
|
self.finish({"error": error_msg})
|
|
@@ -7,22 +7,26 @@ from mito_ai.streamlit_conversion.streamlit_utils import get_app_path
|
|
|
7
7
|
from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def validate_request_body(body: Optional[dict]) -> Tuple[bool, str, Optional[str], bool]:
|
|
10
|
+
def validate_request_body(body: Optional[dict]) -> Tuple[bool, str, Optional[str], bool, str]:
|
|
11
11
|
"""Validate the request body and extract notebook_path and force_recreate."""
|
|
12
12
|
if body is None:
|
|
13
|
-
return False, "Invalid or missing JSON body", None, False
|
|
13
|
+
return False, "Invalid or missing JSON body", None, False, ""
|
|
14
14
|
|
|
15
15
|
notebook_path = body.get("notebook_path")
|
|
16
16
|
if not notebook_path:
|
|
17
|
-
return False, "Missing notebook_path parameter", None, False
|
|
17
|
+
return False, "Missing notebook_path parameter", None, False, ""
|
|
18
18
|
|
|
19
19
|
force_recreate = body.get("force_recreate", False)
|
|
20
20
|
if not isinstance(force_recreate, bool):
|
|
21
|
-
return False, "force_recreate must be a boolean", None, False
|
|
21
|
+
return False, "force_recreate must be a boolean", None, False, ""
|
|
22
|
+
|
|
23
|
+
edit_prompt = body.get("edit_prompt", "")
|
|
24
|
+
if not isinstance(edit_prompt, str):
|
|
25
|
+
return False, "edit_prompt must be a string", None, False, ""
|
|
22
26
|
|
|
23
|
-
return True, "", notebook_path, force_recreate
|
|
27
|
+
return True, "", notebook_path, force_recreate, edit_prompt
|
|
24
28
|
|
|
25
|
-
async def ensure_app_exists(resolved_notebook_path: str, force_recreate: bool = False) -> Tuple[bool, str]:
|
|
29
|
+
async def ensure_app_exists(resolved_notebook_path: str, force_recreate: bool = False, edit_prompt: str = "") -> Tuple[bool, str]:
|
|
26
30
|
"""Ensure app.py exists, generating it if necessary or if force_recreate is True."""
|
|
27
31
|
# Check if the app already exists
|
|
28
32
|
app_path = get_app_path(os.path.dirname(resolved_notebook_path))
|
|
@@ -33,7 +37,7 @@ async def ensure_app_exists(resolved_notebook_path: str, force_recreate: bool =
|
|
|
33
37
|
else:
|
|
34
38
|
print("[Mito AI] Force recreating streamlit app")
|
|
35
39
|
|
|
36
|
-
success, app_path, message = await streamlit_handler(resolved_notebook_path)
|
|
40
|
+
success, app_path, message = await streamlit_handler(resolved_notebook_path, edit_prompt)
|
|
37
41
|
|
|
38
42
|
if not success or app_path is None:
|
|
39
43
|
return False, f"Failed to generate streamlit code: {message}"
|