mito-ai 0.1.50__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 +114 -0
- mito_ai/_version.py +4 -0
- mito_ai/anthropic_client.py +334 -0
- mito_ai/app_deploy/__init__.py +6 -0
- mito_ai/app_deploy/app_deploy_utils.py +44 -0
- mito_ai/app_deploy/handlers.py +345 -0
- mito_ai/app_deploy/models.py +98 -0
- mito_ai/app_manager/__init__.py +4 -0
- mito_ai/app_manager/handlers.py +167 -0
- mito_ai/app_manager/models.py +71 -0
- mito_ai/app_manager/utils.py +24 -0
- mito_ai/auth/README.md +18 -0
- mito_ai/auth/__init__.py +6 -0
- mito_ai/auth/handlers.py +96 -0
- mito_ai/auth/urls.py +13 -0
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/completion_handlers/__init__.py +3 -0
- mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +59 -0
- mito_ai/completions/completion_handlers/agent_execution_handler.py +66 -0
- mito_ai/completions/completion_handlers/chat_completion_handler.py +141 -0
- mito_ai/completions/completion_handlers/code_explain_handler.py +113 -0
- mito_ai/completions/completion_handlers/completion_handler.py +42 -0
- mito_ai/completions/completion_handlers/inline_completer_handler.py +48 -0
- mito_ai/completions/completion_handlers/smart_debug_handler.py +160 -0
- mito_ai/completions/completion_handlers/utils.py +147 -0
- mito_ai/completions/handlers.py +415 -0
- mito_ai/completions/message_history.py +401 -0
- mito_ai/completions/models.py +404 -0
- mito_ai/completions/prompt_builders/__init__.py +3 -0
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +57 -0
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +160 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +472 -0
- mito_ai/completions/prompt_builders/chat_name_prompt.py +15 -0
- mito_ai/completions/prompt_builders/chat_prompt.py +116 -0
- mito_ai/completions/prompt_builders/chat_system_message.py +92 -0
- mito_ai/completions/prompt_builders/explain_code_prompt.py +32 -0
- mito_ai/completions/prompt_builders/inline_completer_prompt.py +197 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +170 -0
- mito_ai/completions/prompt_builders/smart_debug_prompt.py +199 -0
- mito_ai/completions/prompt_builders/utils.py +84 -0
- mito_ai/completions/providers.py +284 -0
- mito_ai/constants.py +63 -0
- mito_ai/db/__init__.py +3 -0
- mito_ai/db/crawlers/__init__.py +6 -0
- mito_ai/db/crawlers/base_crawler.py +61 -0
- mito_ai/db/crawlers/constants.py +43 -0
- mito_ai/db/crawlers/snowflake.py +71 -0
- mito_ai/db/handlers.py +168 -0
- mito_ai/db/models.py +31 -0
- mito_ai/db/urls.py +34 -0
- mito_ai/db/utils.py +185 -0
- mito_ai/docker/mssql/compose.yml +37 -0
- mito_ai/docker/mssql/init/setup.sql +21 -0
- mito_ai/docker/mysql/compose.yml +18 -0
- mito_ai/docker/mysql/init/setup.sql +13 -0
- mito_ai/docker/oracle/compose.yml +17 -0
- mito_ai/docker/oracle/init/setup.sql +20 -0
- mito_ai/docker/postgres/compose.yml +17 -0
- mito_ai/docker/postgres/init/setup.sql +13 -0
- mito_ai/enterprise/__init__.py +3 -0
- mito_ai/enterprise/utils.py +15 -0
- mito_ai/file_uploads/__init__.py +3 -0
- mito_ai/file_uploads/handlers.py +248 -0
- mito_ai/file_uploads/urls.py +21 -0
- mito_ai/gemini_client.py +232 -0
- mito_ai/log/handlers.py +38 -0
- mito_ai/log/urls.py +21 -0
- mito_ai/logger.py +37 -0
- mito_ai/openai_client.py +382 -0
- mito_ai/path_utils.py +70 -0
- mito_ai/rules/handlers.py +44 -0
- mito_ai/rules/urls.py +22 -0
- mito_ai/rules/utils.py +56 -0
- mito_ai/settings/handlers.py +41 -0
- mito_ai/settings/urls.py +20 -0
- mito_ai/settings/utils.py +42 -0
- mito_ai/streamlit_conversion/agent_utils.py +37 -0
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
- mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
- mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
- mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
- mito_ai/streamlit_preview/__init__.py +6 -0
- mito_ai/streamlit_preview/handlers.py +111 -0
- mito_ai/streamlit_preview/manager.py +152 -0
- mito_ai/streamlit_preview/urls.py +22 -0
- mito_ai/streamlit_preview/utils.py +29 -0
- mito_ai/tests/__init__.py +3 -0
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
- mito_ai/tests/conftest.py +53 -0
- mito_ai/tests/create_agent_system_message_prompt_test.py +22 -0
- mito_ai/tests/data/prompt_lg.py +69 -0
- mito_ai/tests/data/prompt_sm.py +6 -0
- mito_ai/tests/data/prompt_xl.py +13 -0
- mito_ai/tests/data/stock_data.sqlite3 +0 -0
- mito_ai/tests/db/conftest.py +39 -0
- mito_ai/tests/db/connections_test.py +102 -0
- mito_ai/tests/db/mssql_test.py +29 -0
- mito_ai/tests/db/mysql_test.py +29 -0
- mito_ai/tests/db/oracle_test.py +29 -0
- mito_ai/tests/db/postgres_test.py +29 -0
- mito_ai/tests/db/schema_test.py +93 -0
- mito_ai/tests/db/sqlite_test.py +31 -0
- mito_ai/tests/db/test_db_constants.py +61 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
- mito_ai/tests/file_uploads/__init__.py +2 -0
- mito_ai/tests/file_uploads/test_handlers.py +282 -0
- mito_ai/tests/message_history/test_generate_short_chat_name.py +120 -0
- mito_ai/tests/message_history/test_message_history_utils.py +469 -0
- mito_ai/tests/open_ai_utils_test.py +152 -0
- mito_ai/tests/performance_test.py +329 -0
- mito_ai/tests/providers/test_anthropic_client.py +447 -0
- mito_ai/tests/providers/test_azure.py +631 -0
- mito_ai/tests/providers/test_capabilities.py +120 -0
- mito_ai/tests/providers/test_gemini_client.py +195 -0
- mito_ai/tests/providers/test_mito_server_utils.py +448 -0
- mito_ai/tests/providers/test_model_resolution.py +130 -0
- mito_ai/tests/providers/test_openai_client.py +57 -0
- mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
- mito_ai/tests/providers/test_provider_limits.py +42 -0
- mito_ai/tests/providers/test_providers.py +382 -0
- mito_ai/tests/providers/test_retry_logic.py +389 -0
- mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
- mito_ai/tests/providers/utils.py +85 -0
- mito_ai/tests/rules/conftest.py +26 -0
- mito_ai/tests/rules/rules_test.py +117 -0
- mito_ai/tests/server_limits_test.py +406 -0
- mito_ai/tests/settings/conftest.py +26 -0
- mito_ai/tests/settings/settings_test.py +70 -0
- mito_ai/tests/settings/test_settings_constants.py +9 -0
- mito_ai/tests/streamlit_conversion/__init__.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
- mito_ai/tests/test_constants.py +47 -0
- mito_ai/tests/test_telemetry.py +12 -0
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/__init__.py +3 -0
- mito_ai/tests/utils/test_anthropic_utils.py +162 -0
- mito_ai/tests/utils/test_gemini_utils.py +98 -0
- mito_ai/tests/version_check_test.py +169 -0
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/__init__.py +3 -0
- mito_ai/utils/anthropic_utils.py +168 -0
- mito_ai/utils/create.py +94 -0
- mito_ai/utils/db.py +74 -0
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/gemini_utils.py +133 -0
- mito_ai/utils/message_history_utils.py +87 -0
- mito_ai/utils/mito_server_utils.py +242 -0
- mito_ai/utils/open_ai_utils.py +200 -0
- mito_ai/utils/provider_utils.py +49 -0
- mito_ai/utils/schema.py +86 -0
- mito_ai/utils/server_limits.py +152 -0
- mito_ai/utils/telemetry_utils.py +480 -0
- mito_ai/utils/utils.py +89 -0
- mito_ai/utils/version_utils.py +94 -0
- mito_ai/utils/websocket_base.py +88 -0
- mito_ai/version_check.py +60 -0
- mito_ai-0.1.50.data/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +7 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/build_log.json +728 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/package.json +243 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +238 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +37 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +21602 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js +619 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style.js +4 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +712 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
- mito_ai-0.1.50.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 +533 -0
- mito_ai-0.1.50.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 +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
- mito_ai-0.1.50.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 +7440 -0
- mito_ai-0.1.50.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 +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2792 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +4859 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +1 -0
- mito_ai-0.1.50.dist-info/METADATA +221 -0
- mito_ai-0.1.50.dist-info/RECORD +205 -0
- mito_ai-0.1.50.dist-info/WHEEL +4 -0
- mito_ai-0.1.50.dist-info/entry_points.txt +2 -0
- mito_ai-0.1.50.dist-info/licenses/LICENSE +3 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from anthropic.types import MessageParam
|
|
5
|
+
from typing import List, cast
|
|
6
|
+
from mito_ai.streamlit_conversion.agent_utils import extract_todo_placeholders, get_response_from_agent
|
|
7
|
+
from mito_ai.streamlit_conversion.prompts.streamlit_app_creation_prompt import get_streamlit_app_creation_prompt
|
|
8
|
+
from mito_ai.streamlit_conversion.prompts.streamlit_error_correction_prompt import get_streamlit_error_correction_prompt
|
|
9
|
+
from mito_ai.streamlit_conversion.prompts.streamlit_finish_todo_prompt import get_finish_todo_prompt
|
|
10
|
+
from mito_ai.streamlit_conversion.prompts.update_existing_app_prompt import get_update_existing_app_prompt
|
|
11
|
+
from mito_ai.streamlit_conversion.validate_streamlit_app import validate_app
|
|
12
|
+
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
|
|
13
|
+
from mito_ai.streamlit_conversion.search_replace_utils import extract_search_replace_blocks, apply_search_replace
|
|
14
|
+
from mito_ai.completions.models import MessageType
|
|
15
|
+
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
16
|
+
from mito_ai.utils.telemetry_utils import log_streamlit_app_validation_retry, log_streamlit_app_conversion_success
|
|
17
|
+
from mito_ai.path_utils import AbsoluteNotebookPath, AppFileName, get_absolute_notebook_dir_path, get_absolute_app_path, get_app_file_name
|
|
18
|
+
|
|
19
|
+
async def generate_new_streamlit_code(notebook: List[dict]) -> str:
|
|
20
|
+
"""Send a query to the agent, get its response and parse the code"""
|
|
21
|
+
|
|
22
|
+
prompt_text = get_streamlit_app_creation_prompt(notebook)
|
|
23
|
+
|
|
24
|
+
messages: List[MessageParam] = [
|
|
25
|
+
cast(MessageParam, {
|
|
26
|
+
"role": "user",
|
|
27
|
+
"content": [{
|
|
28
|
+
"type": "text",
|
|
29
|
+
"text": prompt_text
|
|
30
|
+
}]
|
|
31
|
+
})
|
|
32
|
+
]
|
|
33
|
+
agent_response = await get_response_from_agent(messages)
|
|
34
|
+
converted_code = extract_code_blocks(agent_response)
|
|
35
|
+
|
|
36
|
+
# Extract the TODOs from the agent's response
|
|
37
|
+
todo_placeholders = extract_todo_placeholders(agent_response)
|
|
38
|
+
|
|
39
|
+
for todo_placeholder in todo_placeholders:
|
|
40
|
+
print(f"Processing AI TODO: {todo_placeholder}")
|
|
41
|
+
todo_prompt = get_finish_todo_prompt(notebook, converted_code, todo_placeholder)
|
|
42
|
+
todo_messages: List[MessageParam] = [
|
|
43
|
+
cast(MessageParam, {
|
|
44
|
+
"role": "user",
|
|
45
|
+
"content": [{
|
|
46
|
+
"type": "text",
|
|
47
|
+
"text": todo_prompt
|
|
48
|
+
}]
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
todo_response = await get_response_from_agent(todo_messages)
|
|
52
|
+
|
|
53
|
+
# Apply the search/replace to the streamlit app
|
|
54
|
+
search_replace_pairs = extract_search_replace_blocks(todo_response)
|
|
55
|
+
converted_code = apply_search_replace(converted_code, search_replace_pairs)
|
|
56
|
+
|
|
57
|
+
return converted_code
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def update_existing_streamlit_code(notebook: List[dict], streamlit_app_code: str, edit_prompt: str) -> str:
|
|
61
|
+
"""Send a query to the agent, get its response and parse the code"""
|
|
62
|
+
prompt_text = get_update_existing_app_prompt(notebook, streamlit_app_code, edit_prompt)
|
|
63
|
+
|
|
64
|
+
messages: List[MessageParam] = [
|
|
65
|
+
cast(MessageParam, {
|
|
66
|
+
"role": "user",
|
|
67
|
+
"content": [{
|
|
68
|
+
"type": "text",
|
|
69
|
+
"text": prompt_text
|
|
70
|
+
}]
|
|
71
|
+
})
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
agent_response = await get_response_from_agent(messages)
|
|
75
|
+
print(f"[Mito AI Search/Replace Tool]:\n {agent_response}")
|
|
76
|
+
|
|
77
|
+
# Apply the search/replace to the streamlit app
|
|
78
|
+
search_replace_pairs = extract_search_replace_blocks(agent_response)
|
|
79
|
+
converted_code = apply_search_replace(streamlit_app_code, search_replace_pairs)
|
|
80
|
+
print(f"[Mito AI Search/Replace Tool]\nConverted code\n: {converted_code}")
|
|
81
|
+
return converted_code
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def correct_error_in_generation(error: str, streamlit_app_code: str) -> str:
|
|
85
|
+
"""If errors are present, send it back to the agent to get corrections in code"""
|
|
86
|
+
messages: List[MessageParam] = [
|
|
87
|
+
cast(MessageParam, {
|
|
88
|
+
"role": "user",
|
|
89
|
+
"content": [{
|
|
90
|
+
"type": "text",
|
|
91
|
+
"text": get_streamlit_error_correction_prompt(error, streamlit_app_code)
|
|
92
|
+
}]
|
|
93
|
+
})
|
|
94
|
+
]
|
|
95
|
+
agent_response = await get_response_from_agent(messages)
|
|
96
|
+
|
|
97
|
+
# Apply the search/replace to the streamlit app
|
|
98
|
+
search_replace_pairs = extract_search_replace_blocks(agent_response)
|
|
99
|
+
streamlit_app_code = apply_search_replace(streamlit_app_code, search_replace_pairs)
|
|
100
|
+
|
|
101
|
+
return streamlit_app_code
|
|
102
|
+
|
|
103
|
+
async def streamlit_handler(notebook_path: AbsoluteNotebookPath, app_file_name: AppFileName, edit_prompt: str = "") -> None:
|
|
104
|
+
"""Handler function for streamlit code generation and validation"""
|
|
105
|
+
|
|
106
|
+
# Convert to absolute path for consistent handling
|
|
107
|
+
notebook_code = parse_jupyter_notebook_to_extract_required_content(notebook_path)
|
|
108
|
+
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
109
|
+
app_path = get_absolute_app_path(app_directory, app_file_name)
|
|
110
|
+
|
|
111
|
+
if edit_prompt != "":
|
|
112
|
+
# If the user is editing an existing streamlit app, use the update function
|
|
113
|
+
streamlit_code = get_app_code_from_file(app_path)
|
|
114
|
+
|
|
115
|
+
if streamlit_code is None:
|
|
116
|
+
raise StreamlitConversionError("Error updating existing streamlit app because app.py file was not found.", 404)
|
|
117
|
+
|
|
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)
|
|
122
|
+
|
|
123
|
+
# Then, after creating/updating the app, validate that the new code runs
|
|
124
|
+
errors = validate_app(streamlit_code, notebook_path)
|
|
125
|
+
tries = 0
|
|
126
|
+
while len(errors)>0 and tries < 5:
|
|
127
|
+
for error in errors:
|
|
128
|
+
streamlit_code = await correct_error_in_generation(error, streamlit_code)
|
|
129
|
+
|
|
130
|
+
errors = validate_app(streamlit_code, notebook_path)
|
|
131
|
+
|
|
132
|
+
if len(errors)>0:
|
|
133
|
+
# TODO: We can't easily get the key type here, so for the beta release
|
|
134
|
+
# we are just defaulting to the mito server key since that is by far the most common.
|
|
135
|
+
log_streamlit_app_validation_retry('mito_server_key', MessageType.STREAMLIT_CONVERSION, errors)
|
|
136
|
+
tries+=1
|
|
137
|
+
|
|
138
|
+
if len(errors)>0:
|
|
139
|
+
final_errors = ', '.join(errors)
|
|
140
|
+
raise StreamlitConversionError(f"Streamlit agent failed generating code after max retries. Errors: {final_errors}", 500)
|
|
141
|
+
|
|
142
|
+
# Finally, update the app.py file with the new code
|
|
143
|
+
create_app_file(app_path, streamlit_code)
|
|
144
|
+
log_streamlit_app_conversion_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import json
|
|
6
|
+
from typing import Dict, List, Optional, Any
|
|
7
|
+
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath
|
|
8
|
+
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract_code_blocks(message_content: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Extract all code blocks from Claude's response.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
message_content (str): The actual content from the agent's response
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: Removes the ```python``` part to be able to parse the code
|
|
20
|
+
"""
|
|
21
|
+
if "```python" not in message_content:
|
|
22
|
+
return message_content
|
|
23
|
+
|
|
24
|
+
# Use regex to find all Python code blocks
|
|
25
|
+
pattern = r'```python\n(.*?)```'
|
|
26
|
+
matches = re.findall(pattern, message_content, re.DOTALL)
|
|
27
|
+
|
|
28
|
+
# Concatenate with single newlines
|
|
29
|
+
result = '\n'.join(matches)
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
def create_app_file(app_path: AbsoluteAppPath, code: str) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Create .py file and write code to it with error handling
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
with open(app_path, 'w', encoding='utf-8') as f:
|
|
38
|
+
f.write(code)
|
|
39
|
+
except IOError as e:
|
|
40
|
+
raise StreamlitConversionError(f"Error creating app file: {str(e)}", 500)
|
|
41
|
+
|
|
42
|
+
def get_app_code_from_file(app_path: AbsoluteAppPath) -> Optional[str]:
|
|
43
|
+
with open(app_path, 'r', encoding='utf-8') as f:
|
|
44
|
+
return f.read()
|
|
45
|
+
|
|
46
|
+
def parse_jupyter_notebook_to_extract_required_content(notebook_path: AbsoluteNotebookPath) -> List[Dict[str, Any]]:
|
|
47
|
+
"""
|
|
48
|
+
Read a Jupyter notebook and filter cells to keep only cell_type and source fields.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
notebook_path: Absolute path to the .ipynb file
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
dict: Filtered notebook dictionary with only cell_type and source in cells
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
FileNotFoundError: If the notebook file doesn't exist
|
|
58
|
+
json.JSONDecodeError: If the file is not valid JSON
|
|
59
|
+
KeyError: If the notebook doesn't have the expected structure
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Read the notebook file
|
|
64
|
+
with open(notebook_path, 'r', encoding='utf-8') as f:
|
|
65
|
+
notebook_data: Dict[str, Any] = json.load(f)
|
|
66
|
+
|
|
67
|
+
# Check if 'cells' key exists
|
|
68
|
+
if 'cells' not in notebook_data:
|
|
69
|
+
raise StreamlitConversionError("Notebook does not contain 'cells' key", 400)
|
|
70
|
+
|
|
71
|
+
# Filter each cell to keep only cell_type and source
|
|
72
|
+
filtered_cells: List[Dict[str, Any]] = []
|
|
73
|
+
for cell in notebook_data['cells']:
|
|
74
|
+
filtered_cell: Dict[str, Any] = {
|
|
75
|
+
'cell_type': cell.get('cell_type', ''),
|
|
76
|
+
'source': cell.get('source', [])
|
|
77
|
+
}
|
|
78
|
+
filtered_cells.append(filtered_cell)
|
|
79
|
+
|
|
80
|
+
return filtered_cells
|
|
81
|
+
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
raise StreamlitConversionError(f"Notebook file not found: {notebook_path}", 404)
|
|
84
|
+
except json.JSONDecodeError as e:
|
|
85
|
+
raise StreamlitConversionError(f"Invalid JSON in notebook file: {str(e)}", 400)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import traceback
|
|
7
|
+
import ast
|
|
8
|
+
import warnings
|
|
9
|
+
from typing import List, Tuple, Optional, Dict, Any, Generator
|
|
10
|
+
from streamlit.testing.v1 import AppTest
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from mito_ai.path_utils import AbsoluteNotebookPath, get_absolute_notebook_dir_path
|
|
13
|
+
|
|
14
|
+
warnings.filterwarnings("ignore", message=".*bare mode.*")
|
|
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
|
|
24
|
+
|
|
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
|
|
38
|
+
|
|
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.
|
|
51
|
+
|
|
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
|
|
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
|
|
77
|
+
try:
|
|
78
|
+
os.unlink(temp_path)
|
|
79
|
+
except OSError:
|
|
80
|
+
pass # File might already be deleted
|
|
81
|
+
|
|
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]] = []
|
|
85
|
+
|
|
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})
|
|
91
|
+
|
|
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)})
|
|
98
|
+
|
|
99
|
+
return errors
|
|
100
|
+
|
|
101
|
+
def validate_app(app_code: str, notebook_path: AbsoluteNotebookPath) -> List[str]:
|
|
102
|
+
"""Convenience function to validate Streamlit code"""
|
|
103
|
+
errors = check_for_errors(app_code, notebook_path)
|
|
104
|
+
stringified_errors = [str(error) for error in errors]
|
|
105
|
+
return stringified_errors
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import uuid
|
|
5
|
+
from mito_ai.streamlit_preview.utils import validate_request_body
|
|
6
|
+
import tornado
|
|
7
|
+
from jupyter_server.base.handlers import APIHandler
|
|
8
|
+
from mito_ai.streamlit_preview.manager import StreamlitPreviewManager
|
|
9
|
+
from mito_ai.path_utils import get_absolute_notebook_dir_path, get_absolute_notebook_path, get_absolute_app_path, does_app_path_exist, get_app_file_name
|
|
10
|
+
from mito_ai.utils.telemetry_utils import log_streamlit_app_conversion_error, log_streamlit_app_preview_failure, log_streamlit_app_preview_success
|
|
11
|
+
from mito_ai.completions.models import MessageType
|
|
12
|
+
from mito_ai.utils.error_classes import StreamlitConversionError, StreamlitPreviewError
|
|
13
|
+
from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StreamlitPreviewHandler(APIHandler):
|
|
18
|
+
"""REST handler for streamlit preview operations."""
|
|
19
|
+
|
|
20
|
+
def initialize(self) -> None:
|
|
21
|
+
"""Initialize the handler."""
|
|
22
|
+
self.preview_manager = StreamlitPreviewManager()
|
|
23
|
+
|
|
24
|
+
@tornado.web.authenticated
|
|
25
|
+
async def post(self) -> None:
|
|
26
|
+
"""Start a new streamlit preview."""
|
|
27
|
+
try:
|
|
28
|
+
# Parse and validate request
|
|
29
|
+
body = self.get_json_body()
|
|
30
|
+
notebook_path, notebook_id, force_recreate, edit_prompt = validate_request_body(body)
|
|
31
|
+
|
|
32
|
+
# Ensure app exists
|
|
33
|
+
absolute_notebook_path = get_absolute_notebook_path(notebook_path)
|
|
34
|
+
absolute_notebook_dir_path = get_absolute_notebook_dir_path(absolute_notebook_path)
|
|
35
|
+
app_file_name = get_app_file_name(notebook_id)
|
|
36
|
+
absolute_app_path = get_absolute_app_path(absolute_notebook_dir_path, app_file_name)
|
|
37
|
+
app_path_exists = does_app_path_exist(absolute_app_path)
|
|
38
|
+
|
|
39
|
+
if not app_path_exists or force_recreate:
|
|
40
|
+
if not app_path_exists:
|
|
41
|
+
print("[Mito AI] App path not found, generating streamlit code")
|
|
42
|
+
else:
|
|
43
|
+
print("[Mito AI] Force recreating streamlit app")
|
|
44
|
+
|
|
45
|
+
await streamlit_handler(absolute_notebook_path, app_file_name, edit_prompt)
|
|
46
|
+
|
|
47
|
+
# Start preview
|
|
48
|
+
# TODO: There's a bug here where when the user rebuilds and already running app. Instead of
|
|
49
|
+
# creating a new process, we should update the existing process. The app displayed to the user
|
|
50
|
+
# does update, but that's just because of hot reloading when we overwrite the app.py file.
|
|
51
|
+
preview_id = str(uuid.uuid4())
|
|
52
|
+
port = self.preview_manager.start_streamlit_preview(absolute_notebook_dir_path, app_file_name, preview_id)
|
|
53
|
+
|
|
54
|
+
# Return success response
|
|
55
|
+
self.finish({
|
|
56
|
+
"type": 'success',
|
|
57
|
+
"id": preview_id,
|
|
58
|
+
"port": port,
|
|
59
|
+
"url": f"http://localhost:{port}"
|
|
60
|
+
})
|
|
61
|
+
log_streamlit_app_preview_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
|
|
62
|
+
|
|
63
|
+
except StreamlitConversionError as e:
|
|
64
|
+
print(e)
|
|
65
|
+
self.set_status(e.error_code)
|
|
66
|
+
error_message = str(e)
|
|
67
|
+
formatted_traceback = traceback.format_exc()
|
|
68
|
+
self.finish({"error": error_message})
|
|
69
|
+
log_streamlit_app_conversion_error(
|
|
70
|
+
'mito_server_key',
|
|
71
|
+
MessageType.STREAMLIT_CONVERSION,
|
|
72
|
+
error_message,
|
|
73
|
+
formatted_traceback,
|
|
74
|
+
edit_prompt,
|
|
75
|
+
)
|
|
76
|
+
except StreamlitPreviewError as e:
|
|
77
|
+
print(e)
|
|
78
|
+
error_message = str(e)
|
|
79
|
+
formatted_traceback = traceback.format_exc()
|
|
80
|
+
self.set_status(e.error_code)
|
|
81
|
+
self.finish({"error": error_message})
|
|
82
|
+
log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, edit_prompt)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"Exception in streamlit preview handler: {e}")
|
|
85
|
+
self.set_status(500)
|
|
86
|
+
error_message = str(e)
|
|
87
|
+
formatted_traceback = traceback.format_exc()
|
|
88
|
+
self.finish({"error": error_message})
|
|
89
|
+
log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, "")
|
|
90
|
+
|
|
91
|
+
@tornado.web.authenticated
|
|
92
|
+
def delete(self, preview_id: str) -> None:
|
|
93
|
+
"""Stop a streamlit preview."""
|
|
94
|
+
try:
|
|
95
|
+
if not preview_id:
|
|
96
|
+
self.set_status(400)
|
|
97
|
+
self.finish({"error": "Missing preview_id parameter"})
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Stop the preview
|
|
101
|
+
stopped = self.preview_manager.stop_preview(preview_id)
|
|
102
|
+
|
|
103
|
+
if stopped:
|
|
104
|
+
self.set_status(204) # No content
|
|
105
|
+
else:
|
|
106
|
+
self.set_status(404)
|
|
107
|
+
self.finish({"error": f"Preview {preview_id} not found"})
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.set_status(500)
|
|
111
|
+
self.finish({"error": f"Internal server error: {str(e)}"})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
import threading
|
|
8
|
+
import requests
|
|
9
|
+
from typing import Dict, Optional, Tuple
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from mito_ai.logger import get_logger
|
|
12
|
+
from mito_ai.path_utils import AbsoluteNotebookDirPath, AppFileName
|
|
13
|
+
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PreviewProcess:
|
|
18
|
+
"""Data class to track a streamlit preview process."""
|
|
19
|
+
proc: subprocess.Popen
|
|
20
|
+
port: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StreamlitPreviewManager:
|
|
24
|
+
"""Manages streamlit preview processes and their lifecycle."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self._previews: Dict[str, PreviewProcess] = {}
|
|
28
|
+
self._lock = threading.Lock()
|
|
29
|
+
self.log = get_logger()
|
|
30
|
+
|
|
31
|
+
def get_free_port(self) -> int:
|
|
32
|
+
"""Get a free port for streamlit to use."""
|
|
33
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
34
|
+
s.bind(('', 0))
|
|
35
|
+
s.listen(1)
|
|
36
|
+
port = int(s.getsockname()[1])
|
|
37
|
+
|
|
38
|
+
return port
|
|
39
|
+
|
|
40
|
+
def start_streamlit_preview(self, app_directory: AbsoluteNotebookDirPath, app_file_name: AppFileName, preview_id: str) -> int:
|
|
41
|
+
"""Start a streamlit preview process.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
app_code: The streamlit app code to run
|
|
45
|
+
preview_id: Unique identifier for this preview
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tuple of (success, message, port)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
|
|
53
|
+
# Get free port
|
|
54
|
+
port = self.get_free_port()
|
|
55
|
+
|
|
56
|
+
# Start streamlit process
|
|
57
|
+
cmd = [
|
|
58
|
+
"streamlit", "run", app_file_name,
|
|
59
|
+
"--server.port", str(port),
|
|
60
|
+
"--server.headless", "true",
|
|
61
|
+
"--server.address", "localhost",
|
|
62
|
+
"--server.enableXsrfProtection", "false",
|
|
63
|
+
"--server.runOnSave", "true", # auto-reload when app is saved
|
|
64
|
+
"--logger.level", "error"
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# TODO: Security considerations for production:
|
|
68
|
+
# - Consider enabling XSRF protection if needed, but we might already get this with the APIHandler?
|
|
69
|
+
# - Add authentication headers to streamlit
|
|
70
|
+
|
|
71
|
+
proc = subprocess.Popen(
|
|
72
|
+
cmd,
|
|
73
|
+
stdout=subprocess.PIPE,
|
|
74
|
+
stderr=subprocess.PIPE,
|
|
75
|
+
text=True,
|
|
76
|
+
cwd=app_directory
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Wait for app to be ready
|
|
80
|
+
ready = self._wait_for_app_ready(port)
|
|
81
|
+
if not ready:
|
|
82
|
+
proc.terminate()
|
|
83
|
+
proc.wait()
|
|
84
|
+
raise StreamlitPreviewError("Streamlit app failed to start as app is not ready", 500)
|
|
85
|
+
|
|
86
|
+
# Register the process
|
|
87
|
+
with self._lock:
|
|
88
|
+
self._previews[preview_id] = PreviewProcess(
|
|
89
|
+
proc=proc,
|
|
90
|
+
port=port,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.log.info(f"Started streamlit preview {preview_id} on port {port}")
|
|
94
|
+
return port
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.log.error(f"Error starting streamlit preview: {e}")
|
|
98
|
+
raise StreamlitPreviewError(f"Failed to start preview: {str(e)}", 500)
|
|
99
|
+
|
|
100
|
+
def _wait_for_app_ready(self, port: int, timeout: int = 30) -> bool:
|
|
101
|
+
"""Wait for streamlit app to be ready on the given port."""
|
|
102
|
+
start_time = time.time()
|
|
103
|
+
|
|
104
|
+
while time.time() - start_time < timeout:
|
|
105
|
+
try:
|
|
106
|
+
response = requests.get(f"http://localhost:{port}", timeout=5)
|
|
107
|
+
if response.status_code == 200:
|
|
108
|
+
return True
|
|
109
|
+
except requests.exceptions.RequestException as e:
|
|
110
|
+
self.log.info(f"Waiting for app to be ready...")
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
time.sleep(1)
|
|
114
|
+
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def stop_preview(self, preview_id: str) -> bool:
|
|
118
|
+
"""Stop a streamlit preview process.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
preview_id: The preview ID to stop
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if stopped successfully, False if not found
|
|
125
|
+
"""
|
|
126
|
+
self.log.info(f"Stopping preview {preview_id}")
|
|
127
|
+
with self._lock:
|
|
128
|
+
if preview_id not in self._previews:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
preview = self._previews[preview_id]
|
|
132
|
+
|
|
133
|
+
# Terminate process
|
|
134
|
+
try:
|
|
135
|
+
preview.proc.terminate()
|
|
136
|
+
preview.proc.wait(timeout=5)
|
|
137
|
+
except subprocess.TimeoutExpired:
|
|
138
|
+
preview.proc.kill()
|
|
139
|
+
preview.proc.wait()
|
|
140
|
+
except Exception as e:
|
|
141
|
+
self.log.error(f"Error terminating process {preview_id}: {e}")
|
|
142
|
+
|
|
143
|
+
# Remove from registry
|
|
144
|
+
del self._previews[preview_id]
|
|
145
|
+
|
|
146
|
+
self.log.info(f"Stopped streamlit preview {preview_id}")
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
def get_preview(self, preview_id: str) -> Optional[PreviewProcess]:
|
|
150
|
+
"""Get a preview process by ID."""
|
|
151
|
+
with self._lock:
|
|
152
|
+
return self._previews.get(preview_id)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from typing import Any, List, Tuple
|
|
5
|
+
from jupyter_server.utils import url_path_join
|
|
6
|
+
from mito_ai.streamlit_preview.handlers import StreamlitPreviewHandler
|
|
7
|
+
|
|
8
|
+
def get_streamlit_preview_urls(base_url: str) -> List[Tuple[str, Any, dict]]:
|
|
9
|
+
"""Get all streamlit preview related URL patterns.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
base_url: The base URL for the Jupyter server
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
List of (url_pattern, handler_class, handler_kwargs) tuples
|
|
16
|
+
"""
|
|
17
|
+
BASE_URL = base_url + "/mito-ai"
|
|
18
|
+
|
|
19
|
+
return [
|
|
20
|
+
(url_path_join(BASE_URL, "streamlit-preview"), StreamlitPreviewHandler, {}),
|
|
21
|
+
(url_path_join(BASE_URL, "streamlit-preview/(.+)"), StreamlitPreviewHandler, {}),
|
|
22
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from typing import Tuple, Optional
|
|
5
|
+
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_request_body(body: Optional[dict]) -> Tuple[str, str, bool, str]:
|
|
9
|
+
"""Validate the request body and extract notebook_path and force_recreate."""
|
|
10
|
+
if body is None:
|
|
11
|
+
raise StreamlitPreviewError("Invalid or missing JSON body", 400)
|
|
12
|
+
|
|
13
|
+
notebook_path = body.get("notebook_path")
|
|
14
|
+
if not notebook_path:
|
|
15
|
+
raise StreamlitPreviewError("Missing notebook_path parameter", 400)
|
|
16
|
+
|
|
17
|
+
notebook_id = body.get("notebook_id")
|
|
18
|
+
if not notebook_id:
|
|
19
|
+
raise StreamlitPreviewError("Missing notebook_id parameter", 400)
|
|
20
|
+
|
|
21
|
+
force_recreate = body.get("force_recreate", False)
|
|
22
|
+
if not isinstance(force_recreate, bool):
|
|
23
|
+
raise StreamlitPreviewError("force_recreate must be a boolean", 400)
|
|
24
|
+
|
|
25
|
+
edit_prompt = body.get("edit_prompt", "")
|
|
26
|
+
if not isinstance(edit_prompt, str):
|
|
27
|
+
raise StreamlitPreviewError("edit_prompt must be a string", 400)
|
|
28
|
+
|
|
29
|
+
return notebook_path, notebook_id, force_recreate, edit_prompt
|