mito-ai 0.1.45__py3-none-any.whl → 0.1.47__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.
- mito_ai/__init__.py +10 -1
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +90 -5
- mito_ai/app_deploy/handlers.py +97 -77
- mito_ai/app_deploy/models.py +16 -12
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/handlers.py +18 -20
- mito_ai/completions/models.py +4 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +6 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
- mito_ai/completions/prompt_builders/utils.py +14 -0
- mito_ai/constants.py +3 -0
- mito_ai/path_utils.py +56 -0
- mito_ai/streamlit_conversion/agent_utils.py +27 -106
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +166 -53
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
- mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +1 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +93 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +103 -119
- mito_ai/streamlit_conversion/streamlit_utils.py +18 -68
- mito_ai/streamlit_conversion/validate_streamlit_app.py +78 -96
- mito_ai/streamlit_preview/handlers.py +44 -85
- mito_ai/streamlit_preview/manager.py +6 -6
- mito_ai/streamlit_preview/utils.py +19 -18
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/message_history/test_message_history_utils.py +43 -19
- mito_ai/tests/providers/test_anthropic_client.py +178 -6
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +226 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +87 -114
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +42 -45
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +20 -14
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +13 -16
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +22 -26
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/anthropic_utils.py +8 -6
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/message_history_utils.py +7 -4
- mito_ai/utils/telemetry_utils.py +79 -11
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js +2126 -363
- mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +1 -0
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.684f82575fcc2e3b350c.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js +9 -9
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.684f82575fcc2e3b350c.js.map → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js.map +1 -1
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/METADATA +1 -1
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/RECORD +81 -69
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js.map +0 -1
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.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.45.dist-info → mito_ai-0.1.47.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,161 +1,145 @@
|
|
|
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 extract_todo_placeholders, 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,
|
|
17
|
-
from mito_ai.
|
|
13
|
+
from mito_ai.streamlit_conversion.streamlit_utils import extract_code_blocks, create_app_file, get_app_code_from_file, parse_jupyter_notebook_to_extract_required_content
|
|
14
|
+
from mito_ai.streamlit_conversion.search_replace_utils import extract_search_replace_blocks, apply_search_replace
|
|
18
15
|
from mito_ai.completions.models import MessageType
|
|
19
|
-
from mito_ai.utils.
|
|
20
|
-
from mito_ai.
|
|
21
|
-
|
|
22
|
-
STREAMLIT_AI_MODEL = "claude-3-5-haiku-latest"
|
|
23
|
-
|
|
24
|
-
class StreamlitCodeGeneration:
|
|
25
|
-
@property
|
|
26
|
-
def log(self) -> logging.Logger:
|
|
27
|
-
"""Use Mito AI logger."""
|
|
28
|
-
return get_logger()
|
|
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
|
|
16
|
+
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
17
|
+
from mito_ai.utils.telemetry_utils import log_streamlit_app_validation_retry, log_streamlit_app_conversion_success
|
|
18
|
+
from mito_ai.path_utils import AbsoluteNotebookPath, get_absolute_notebook_dir_path, get_absolute_app_path
|
|
51
19
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
20
|
+
async def generate_new_streamlit_code(notebook: List[dict]) -> str:
|
|
21
|
+
"""Send a query to the agent, get its response and parse the code"""
|
|
22
|
+
|
|
23
|
+
prompt_text = get_streamlit_app_creation_prompt(notebook)
|
|
24
|
+
|
|
25
|
+
messages: List[MessageParam] = [
|
|
26
|
+
cast(MessageParam, {
|
|
27
|
+
"role": "user",
|
|
28
|
+
"content": [{
|
|
29
|
+
"type": "text",
|
|
30
|
+
"text": prompt_text
|
|
31
|
+
}]
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
agent_response = await get_response_from_agent(messages)
|
|
35
|
+
converted_code = extract_code_blocks(agent_response)
|
|
36
|
+
|
|
37
|
+
# Extract the TODOs from the agent's response
|
|
38
|
+
todo_placeholders = extract_todo_placeholders(agent_response)
|
|
39
|
+
|
|
40
|
+
for todo_placeholder in todo_placeholders:
|
|
41
|
+
print(f"Processing AI TODO: {todo_placeholder}")
|
|
42
|
+
todo_prompt = get_finish_todo_prompt(notebook, converted_code, todo_placeholder)
|
|
43
|
+
todo_messages: List[MessageParam] = [
|
|
58
44
|
cast(MessageParam, {
|
|
59
45
|
"role": "user",
|
|
60
46
|
"content": [{
|
|
61
47
|
"type": "text",
|
|
62
|
-
"text":
|
|
48
|
+
"text": todo_prompt
|
|
63
49
|
}]
|
|
64
50
|
})
|
|
65
51
|
]
|
|
52
|
+
todo_response = await get_response_from_agent(todo_messages)
|
|
66
53
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
for todo_placeholder in todo_placeholders:
|
|
74
|
-
print(f"Processing AI TODO: {todo_placeholder}")
|
|
75
|
-
todo_prompt = get_finish_todo_prompt(notebook, converted_code, todo_placeholder)
|
|
76
|
-
todo_messages: List[MessageParam] = [
|
|
77
|
-
cast(MessageParam, {
|
|
78
|
-
"role": "user",
|
|
79
|
-
"content": [{
|
|
80
|
-
"type": "text",
|
|
81
|
-
"text": todo_prompt
|
|
82
|
-
}]
|
|
83
|
-
})
|
|
84
|
-
]
|
|
85
|
-
todo_response = await self.get_response_from_agent(todo_messages)
|
|
86
|
-
|
|
87
|
-
# Apply the diff to the streamlit app
|
|
88
|
-
exctracted_diff = extract_unified_diff_blocks(todo_response)
|
|
89
|
-
fixed_diff = fix_diff_headers(exctracted_diff)
|
|
90
|
-
converted_code = apply_patch_to_text(converted_code, fixed_diff)
|
|
91
|
-
|
|
92
|
-
return converted_code
|
|
54
|
+
# Apply the search/replace to the streamlit app
|
|
55
|
+
search_replace_pairs = extract_search_replace_blocks(todo_response)
|
|
56
|
+
converted_code = apply_search_replace(converted_code, search_replace_pairs)
|
|
57
|
+
|
|
58
|
+
return converted_code
|
|
93
59
|
|
|
94
60
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
61
|
+
async def update_existing_streamlit_code(notebook: List[dict], streamlit_app_code: str, edit_prompt: str) -> str:
|
|
62
|
+
"""Send a query to the agent, get its response and parse the code"""
|
|
63
|
+
prompt_text = get_update_existing_app_prompt(notebook, streamlit_app_code, edit_prompt)
|
|
64
|
+
|
|
65
|
+
messages: List[MessageParam] = [
|
|
66
|
+
cast(MessageParam, {
|
|
67
|
+
"role": "user",
|
|
68
|
+
"content": [{
|
|
69
|
+
"type": "text",
|
|
70
|
+
"text": prompt_text
|
|
71
|
+
}]
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
agent_response = await get_response_from_agent(messages)
|
|
76
|
+
print(f"[Mito AI Search/Replace Tool]:\n {agent_response}")
|
|
77
|
+
|
|
78
|
+
# Apply the search/replace to the streamlit app
|
|
79
|
+
search_replace_pairs = extract_search_replace_blocks(agent_response)
|
|
80
|
+
converted_code = apply_search_replace(streamlit_app_code, search_replace_pairs)
|
|
81
|
+
print(f"[Mito AI Search/Replace Tool]\nConverted code\n: {converted_code}")
|
|
82
|
+
return converted_code
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def correct_error_in_generation(error: str, streamlit_app_code: str) -> str:
|
|
86
|
+
"""If errors are present, send it back to the agent to get corrections in code"""
|
|
87
|
+
messages: List[MessageParam] = [
|
|
88
|
+
cast(MessageParam, {
|
|
89
|
+
"role": "user",
|
|
90
|
+
"content": [{
|
|
91
|
+
"type": "text",
|
|
92
|
+
"text": get_streamlit_error_correction_prompt(error, streamlit_app_code)
|
|
93
|
+
}]
|
|
94
|
+
})
|
|
95
|
+
]
|
|
96
|
+
agent_response = await get_response_from_agent(messages)
|
|
97
|
+
|
|
98
|
+
# Apply the search/replace to the streamlit app
|
|
99
|
+
search_replace_pairs = extract_search_replace_blocks(agent_response)
|
|
100
|
+
streamlit_app_code = apply_search_replace(streamlit_app_code, search_replace_pairs)
|
|
118
101
|
|
|
102
|
+
return streamlit_app_code
|
|
119
103
|
|
|
120
|
-
async def streamlit_handler(notebook_path: str) ->
|
|
104
|
+
async def streamlit_handler(notebook_path: AbsoluteNotebookPath, edit_prompt: str = "") -> None:
|
|
121
105
|
"""Handler function for streamlit code generation and validation"""
|
|
122
106
|
|
|
123
|
-
|
|
124
|
-
|
|
107
|
+
# Convert to absolute path for consistent handling
|
|
125
108
|
notebook_code = parse_jupyter_notebook_to_extract_required_content(notebook_path)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
streamlit_code = await streamlit_code_generator.generate_streamlit_code(notebook_code)
|
|
109
|
+
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
110
|
+
app_path = get_absolute_app_path(app_directory)
|
|
129
111
|
|
|
112
|
+
if edit_prompt != "":
|
|
113
|
+
# If the user is editing an existing streamlit app, use the update function
|
|
114
|
+
streamlit_code = get_app_code_from_file(app_path)
|
|
115
|
+
|
|
116
|
+
if streamlit_code is None:
|
|
117
|
+
raise StreamlitConversionError("Error updating existing streamlit app because app.py file was not found.", 404)
|
|
118
|
+
|
|
119
|
+
streamlit_code = await update_existing_streamlit_code(notebook_code, streamlit_code, edit_prompt)
|
|
120
|
+
else:
|
|
121
|
+
# Otherwise generate a new streamlit app
|
|
122
|
+
streamlit_code = await generate_new_streamlit_code(notebook_code)
|
|
123
|
+
|
|
124
|
+
# Then, after creating/updating the app, validate that the new code runs
|
|
130
125
|
has_validation_error, errors = validate_app(streamlit_code, notebook_path)
|
|
131
126
|
tries = 0
|
|
132
127
|
while has_validation_error and tries < 5:
|
|
133
128
|
for error in errors:
|
|
134
|
-
streamlit_code = await
|
|
129
|
+
streamlit_code = await correct_error_in_generation(error, streamlit_code)
|
|
135
130
|
|
|
136
131
|
has_validation_error, errors = validate_app(streamlit_code, notebook_path)
|
|
137
132
|
|
|
138
133
|
if has_validation_error:
|
|
139
134
|
# TODO: We can't easily get the key type here, so for the beta release
|
|
140
135
|
# we are just defaulting to the mito server key since that is by far the most common.
|
|
141
|
-
|
|
136
|
+
log_streamlit_app_validation_retry('mito_server_key', MessageType.STREAMLIT_CONVERSION, errors)
|
|
142
137
|
tries+=1
|
|
143
138
|
|
|
144
139
|
if has_validation_error:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# Convert to absolute path for directory calculation
|
|
149
|
-
absolute_notebook_path = notebook_path
|
|
150
|
-
if not (notebook_path.startswith('/') or (len(notebook_path) > 1 and notebook_path[1] == ':')):
|
|
151
|
-
absolute_notebook_path = os.path.join(os.getcwd(), notebook_path)
|
|
152
|
-
|
|
153
|
-
app_directory = os.path.dirname(absolute_notebook_path)
|
|
154
|
-
|
|
155
|
-
success_flag, app_path, message = create_app_file(app_directory, streamlit_code)
|
|
156
|
-
|
|
157
|
-
if not success_flag:
|
|
158
|
-
log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, message)
|
|
140
|
+
final_errors = ', '.join(errors)
|
|
141
|
+
raise StreamlitConversionError(f"Streamlit agent failed generating code after max retries. Errors: {final_errors}", 500)
|
|
159
142
|
|
|
160
|
-
|
|
161
|
-
|
|
143
|
+
# Finally, update the app.py file with the new code
|
|
144
|
+
create_app_file(app_path, streamlit_code)
|
|
145
|
+
log_streamlit_app_conversion_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import re
|
|
5
5
|
import json
|
|
6
|
-
import
|
|
7
|
-
from
|
|
6
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
7
|
+
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
def extract_code_blocks(message_content: str) -> str:
|
|
11
13
|
"""
|
|
@@ -28,19 +30,7 @@ def extract_code_blocks(message_content: str) -> str:
|
|
|
28
30
|
result = '\n'.join(matches)
|
|
29
31
|
return result
|
|
30
32
|
|
|
31
|
-
def
|
|
32
|
-
"""
|
|
33
|
-
Extract all unified_diff blocks from Claude's response.
|
|
34
|
-
"""
|
|
35
|
-
if "```unified_diff" not in message_content:
|
|
36
|
-
return message_content
|
|
37
|
-
|
|
38
|
-
pattern = r'```unified_diff\n(.*?)```'
|
|
39
|
-
matches = re.findall(pattern, message_content, re.DOTALL)
|
|
40
|
-
return '\n'.join(matches)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def create_app_file(app_directory: str, code: str) -> Tuple[bool, str, str]:
|
|
33
|
+
def create_app_file(app_path: AbsoluteAppPath, code: str) -> None:
|
|
44
34
|
"""
|
|
45
35
|
Create app.py file and write code to it with error handling
|
|
46
36
|
|
|
@@ -53,34 +43,21 @@ def create_app_file(app_directory: str, code: str) -> Tuple[bool, str, str]:
|
|
|
53
43
|
|
|
54
44
|
"""
|
|
55
45
|
try:
|
|
56
|
-
app_path = os.path.join(app_directory, "app.py")
|
|
57
|
-
|
|
58
46
|
with open(app_path, 'w', encoding='utf-8') as f:
|
|
59
47
|
f.write(code)
|
|
60
|
-
|
|
61
|
-
return True, app_path, f"Successfully created {app_directory}"
|
|
62
48
|
except IOError as e:
|
|
63
|
-
|
|
64
|
-
except Exception as e:
|
|
65
|
-
return False, '', f"Unexpected error: {str(e)}"
|
|
49
|
+
raise StreamlitConversionError(f"Error creating app file: {str(e)}", 500)
|
|
66
50
|
|
|
51
|
+
def get_app_code_from_file(app_path: AbsoluteAppPath) -> Optional[str]:
|
|
52
|
+
with open(app_path, 'r', encoding='utf-8') as f:
|
|
53
|
+
return f.read()
|
|
67
54
|
|
|
68
|
-
def
|
|
69
|
-
"""
|
|
70
|
-
Check if the app.py file exists in the given directory.
|
|
71
|
-
"""
|
|
72
|
-
app_path = os.path.join(app_directory, "app.py")
|
|
73
|
-
if not os.path.exists(app_path):
|
|
74
|
-
return None
|
|
75
|
-
return app_path
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Dict[str, Any]:
|
|
55
|
+
def parse_jupyter_notebook_to_extract_required_content(notebook_path: AbsoluteNotebookPath) -> List[Dict[str, Any]]:
|
|
79
56
|
"""
|
|
80
57
|
Read a Jupyter notebook and filter cells to keep only cell_type and source fields.
|
|
81
58
|
|
|
82
59
|
Args:
|
|
83
|
-
notebook_path
|
|
60
|
+
notebook_path: Absolute path to the .ipynb file
|
|
84
61
|
|
|
85
62
|
Returns:
|
|
86
63
|
dict: Filtered notebook dictionary with only cell_type and source in cells
|
|
@@ -90,10 +67,6 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Di
|
|
|
90
67
|
json.JSONDecodeError: If the file is not valid JSON
|
|
91
68
|
KeyError: If the notebook doesn't have the expected structure
|
|
92
69
|
"""
|
|
93
|
-
# Convert to absolute path if it's not already absolute
|
|
94
|
-
# Handle both Unix-style absolute paths (starting with /) and Windows-style absolute paths
|
|
95
|
-
if not (notebook_path.startswith('/') or (len(notebook_path) > 1 and notebook_path[1] == ':')):
|
|
96
|
-
notebook_path = os.path.join(os.getcwd(), notebook_path)
|
|
97
70
|
|
|
98
71
|
try:
|
|
99
72
|
# Read the notebook file
|
|
@@ -102,43 +75,20 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Di
|
|
|
102
75
|
|
|
103
76
|
# Check if 'cells' key exists
|
|
104
77
|
if 'cells' not in notebook_data:
|
|
105
|
-
raise
|
|
78
|
+
raise StreamlitConversionError("Notebook does not contain 'cells' key", 400)
|
|
106
79
|
|
|
107
80
|
# Filter each cell to keep only cell_type and source
|
|
108
|
-
filtered_cells = []
|
|
81
|
+
filtered_cells: List[Dict[str, Any]] = []
|
|
109
82
|
for cell in notebook_data['cells']:
|
|
110
|
-
filtered_cell = {
|
|
83
|
+
filtered_cell: Dict[str, Any] = {
|
|
111
84
|
'cell_type': cell.get('cell_type', ''),
|
|
112
85
|
'source': cell.get('source', [])
|
|
113
86
|
}
|
|
114
87
|
filtered_cells.append(filtered_cell)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
notebook_data['cells'] = filtered_cells
|
|
118
|
-
|
|
119
|
-
return notebook_data
|
|
88
|
+
|
|
89
|
+
return filtered_cells
|
|
120
90
|
|
|
121
91
|
except FileNotFoundError:
|
|
122
|
-
raise
|
|
92
|
+
raise StreamlitConversionError(f"Notebook file not found: {notebook_path}", 404)
|
|
123
93
|
except json.JSONDecodeError as e:
|
|
124
|
-
|
|
125
|
-
raise json.JSONDecodeError(f"Invalid JSON in notebook file: {str(e)}", e.doc if hasattr(e, 'doc') else '', e.pos if hasattr(e, 'pos') else 0)
|
|
126
|
-
except Exception as e:
|
|
127
|
-
raise Exception(f"Error processing notebook: {str(e)}")
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def resolve_notebook_path(notebook_path:str) -> str:
|
|
131
|
-
# Convert to absolute path if it's not already absolute
|
|
132
|
-
# Handle both Unix-style absolute paths (starting with /) and Windows-style absolute paths
|
|
133
|
-
if not (notebook_path.startswith('/') or (len(notebook_path) > 1 and notebook_path[1] == ':')):
|
|
134
|
-
notebook_path = os.path.join(os.getcwd(), notebook_path)
|
|
135
|
-
return notebook_path
|
|
136
|
-
|
|
137
|
-
def clean_directory_check(notebook_path: str) -> None:
|
|
138
|
-
notebook_path = resolve_notebook_path(notebook_path)
|
|
139
|
-
# pathlib handles the cross OS path conversion automatically
|
|
140
|
-
path = Path(notebook_path).resolve()
|
|
141
|
-
dir_path = path.parent
|
|
142
|
-
|
|
143
|
-
if not dir_path.exists():
|
|
144
|
-
raise ValueError(f"Directory does not exist: {dir_path}")
|
|
94
|
+
raise StreamlitConversionError(f"Invalid JSON in notebook file: {str(e)}", 400)
|
|
@@ -1,124 +1,106 @@
|
|
|
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 sys
|
|
5
4
|
import os
|
|
6
|
-
import time
|
|
7
|
-
import requests
|
|
8
5
|
import tempfile
|
|
9
|
-
import shutil
|
|
10
6
|
import traceback
|
|
11
7
|
import ast
|
|
12
|
-
import importlib.util
|
|
13
8
|
import warnings
|
|
14
9
|
from typing import List, Tuple, Optional, Dict, Any, Generator
|
|
15
10
|
from streamlit.testing.v1 import AppTest
|
|
16
11
|
from contextlib import contextmanager
|
|
17
|
-
from mito_ai.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# warnings.filterwarnings("ignore", message=r".*missing ScriptRunContext.*")
|
|
21
|
-
# warnings.filterwarnings("ignore", category=UserWarning)
|
|
12
|
+
from mito_ai.path_utils import AbsoluteNotebookPath, get_absolute_notebook_dir_path
|
|
22
13
|
|
|
23
14
|
warnings.filterwarnings("ignore", message=".*bare mode.*")
|
|
24
15
|
|
|
16
|
+
def get_syntax_error(app_code: str) -> Optional[str]:
|
|
17
|
+
"""Check if the Python code has valid syntax"""
|
|
18
|
+
try:
|
|
19
|
+
ast.parse(app_code)
|
|
20
|
+
return None
|
|
21
|
+
except SyntaxError as e:
|
|
22
|
+
error_msg = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
23
|
+
return error_msg
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_runtime_errors(self, app_code: str, app_path: str) -> Optional[List[Dict[str, Any]]]:
|
|
40
|
-
"""Start the Streamlit app in a subprocess"""
|
|
41
|
-
|
|
42
|
-
directory = os.path.dirname(app_path)
|
|
43
|
-
|
|
44
|
-
@contextmanager
|
|
45
|
-
def change_working_directory(path: str) -> Generator[None, Any, None]:
|
|
46
|
-
"""
|
|
47
|
-
Context manager to temporarily change working directory
|
|
48
|
-
so that relative paths are still valid when we run the app
|
|
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)
|
|
25
|
+
def get_runtime_errors(app_code: str, app_path: AbsoluteNotebookPath) -> Optional[List[Dict[str, Any]]]:
|
|
26
|
+
"""Start the Streamlit app in a subprocess"""
|
|
27
|
+
|
|
28
|
+
directory = get_absolute_notebook_dir_path(app_path)
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def change_working_directory(path: str) -> Generator[None, Any, None]:
|
|
32
|
+
"""
|
|
33
|
+
Context manager to temporarily change working directory
|
|
34
|
+
so that relative paths are still valid when we run the app
|
|
35
|
+
"""
|
|
36
|
+
if path == '':
|
|
37
|
+
yield
|
|
59
38
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
39
|
+
original_cwd = os.getcwd()
|
|
40
|
+
try:
|
|
41
|
+
os.chdir(path)
|
|
42
|
+
yield
|
|
43
|
+
finally:
|
|
44
|
+
os.chdir(original_cwd)
|
|
45
|
+
|
|
46
|
+
with change_working_directory(directory):
|
|
47
|
+
# Create a temporary file that uses UTF-8 encoding so
|
|
48
|
+
# we don't run into issues with non-ASCII characters on Windows.
|
|
49
|
+
# We use utf-8 encoding when writing the app.py file so this validation
|
|
50
|
+
# code mirrors the actual file.
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
52
|
+
# Note: Since the AppTest.from_file tries to open the file, we need to first close the file
|
|
53
|
+
# by exiting the context manager and using the delete=False flag so that the file still exists.
|
|
54
|
+
# Windows can't open the same file twice at the same time. We cleanup at the end.
|
|
55
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as f:
|
|
56
|
+
f.write(app_code)
|
|
57
|
+
temp_path = f.name
|
|
72
58
|
|
|
59
|
+
try:
|
|
60
|
+
# Run Streamlit test from file with UTF-8 encoding
|
|
61
|
+
app_test = AppTest.from_file(temp_path, default_timeout=30)
|
|
62
|
+
app_test.run()
|
|
63
|
+
|
|
64
|
+
# Check for exceptions
|
|
65
|
+
if app_test.exception:
|
|
66
|
+
errors = [{'type': 'exception', 'details': exc.value, 'message': exc.message, 'stack_trace': exc.stack_trace} for exc in app_test.exception]
|
|
67
|
+
return errors
|
|
68
|
+
|
|
69
|
+
# Check for error messages
|
|
70
|
+
if app_test.error:
|
|
71
|
+
errors = [{'type': 'error', 'details': err.value} for err in app_test.error]
|
|
72
|
+
return errors
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
finally:
|
|
76
|
+
# Clean up the temporary file
|
|
73
77
|
try:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# Check for exceptions
|
|
79
|
-
if app_test.exception:
|
|
80
|
-
errors = [{'type': 'exception', 'details': exc.value, 'message': exc.message, 'stack_trace': exc.stack_trace} for exc in app_test.exception]
|
|
81
|
-
return errors
|
|
82
|
-
|
|
83
|
-
# Check for error messages
|
|
84
|
-
if app_test.error:
|
|
85
|
-
errors = [{'type': 'error', 'details': err.value} for err in app_test.error]
|
|
86
|
-
return errors
|
|
87
|
-
|
|
88
|
-
return None
|
|
89
|
-
finally:
|
|
90
|
-
# Clean up the temporary file
|
|
91
|
-
try:
|
|
92
|
-
os.unlink(temp_path)
|
|
93
|
-
except OSError:
|
|
94
|
-
pass # File might already be deleted
|
|
78
|
+
os.unlink(temp_path)
|
|
79
|
+
except OSError:
|
|
80
|
+
pass # File might already be deleted
|
|
95
81
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
def check_for_errors(app_code: str, app_path: AbsoluteNotebookPath) -> List[Dict[str, Any]]:
|
|
83
|
+
"""Complete validation pipeline"""
|
|
84
|
+
errors: List[Dict[str, Any]] = []
|
|
99
85
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
try:
|
|
87
|
+
# Step 1: Check syntax
|
|
88
|
+
syntax_error = get_syntax_error(app_code)
|
|
89
|
+
if syntax_error:
|
|
90
|
+
errors.append({'type': 'syntax', 'details': syntax_error})
|
|
105
91
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
errors.append({'type': 'validation', 'details': str(e)})
|
|
92
|
+
runtime_errors = get_runtime_errors(app_code, app_path)
|
|
93
|
+
if runtime_errors:
|
|
94
|
+
errors.extend(runtime_errors)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
errors.append({'type': 'validation', 'details': str(e)})
|
|
113
98
|
|
|
114
|
-
|
|
99
|
+
return errors
|
|
115
100
|
|
|
116
|
-
def validate_app(app_code: str, notebook_path:
|
|
101
|
+
def validate_app(app_code: str, notebook_path: AbsoluteNotebookPath) -> Tuple[bool, List[str]]:
|
|
117
102
|
"""Convenience function to validate Streamlit code"""
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
validator = StreamlitValidator()
|
|
121
|
-
errors = validator._validate_app(app_code, notebook_path)
|
|
103
|
+
errors = check_for_errors(app_code, notebook_path)
|
|
122
104
|
|
|
123
105
|
has_validation_error = len(errors) > 0
|
|
124
106
|
stringified_errors = [str(error) for error in errors]
|