mito-ai 0.1.33__py3-none-any.whl → 0.1.49__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.
Files changed (146) hide show
  1. mito_ai/__init__.py +49 -9
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +142 -67
  4. mito_ai/{app_builder → app_deploy}/__init__.py +1 -1
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/{app_builder → app_deploy}/models.py +35 -22
  8. mito_ai/app_manager/__init__.py +4 -0
  9. mito_ai/app_manager/handlers.py +167 -0
  10. mito_ai/app_manager/models.py +71 -0
  11. mito_ai/app_manager/utils.py +24 -0
  12. mito_ai/auth/README.md +18 -0
  13. mito_ai/auth/__init__.py +6 -0
  14. mito_ai/auth/handlers.py +96 -0
  15. mito_ai/auth/urls.py +13 -0
  16. mito_ai/chat_history/handlers.py +63 -0
  17. mito_ai/chat_history/urls.py +32 -0
  18. mito_ai/completions/completion_handlers/agent_execution_handler.py +1 -1
  19. mito_ai/completions/completion_handlers/chat_completion_handler.py +4 -4
  20. mito_ai/completions/completion_handlers/utils.py +99 -37
  21. mito_ai/completions/handlers.py +57 -20
  22. mito_ai/completions/message_history.py +9 -1
  23. mito_ai/completions/models.py +31 -7
  24. mito_ai/completions/prompt_builders/agent_execution_prompt.py +21 -2
  25. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
  26. mito_ai/completions/prompt_builders/agent_system_message.py +115 -42
  27. mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
  28. mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
  29. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  30. mito_ai/completions/prompt_builders/prompt_constants.py +23 -4
  31. mito_ai/completions/prompt_builders/utils.py +72 -10
  32. mito_ai/completions/providers.py +81 -47
  33. mito_ai/constants.py +25 -24
  34. mito_ai/file_uploads/__init__.py +3 -0
  35. mito_ai/file_uploads/handlers.py +248 -0
  36. mito_ai/file_uploads/urls.py +21 -0
  37. mito_ai/gemini_client.py +44 -48
  38. mito_ai/log/handlers.py +10 -3
  39. mito_ai/log/urls.py +3 -3
  40. mito_ai/openai_client.py +30 -44
  41. mito_ai/path_utils.py +70 -0
  42. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  43. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  44. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  45. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  46. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  47. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  48. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  49. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  50. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  51. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  52. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  53. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  54. mito_ai/streamlit_preview/__init__.py +6 -0
  55. mito_ai/streamlit_preview/handlers.py +111 -0
  56. mito_ai/streamlit_preview/manager.py +152 -0
  57. mito_ai/streamlit_preview/urls.py +22 -0
  58. mito_ai/streamlit_preview/utils.py +29 -0
  59. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  60. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  61. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  62. mito_ai/tests/file_uploads/__init__.py +2 -0
  63. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  64. mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
  65. mito_ai/tests/message_history/test_message_history_utils.py +103 -23
  66. mito_ai/tests/open_ai_utils_test.py +18 -22
  67. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  68. mito_ai/tests/providers/test_azure.py +2 -6
  69. mito_ai/tests/providers/test_capabilities.py +120 -0
  70. mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
  71. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  72. mito_ai/tests/providers/test_model_resolution.py +130 -0
  73. mito_ai/tests/providers/test_openai_client.py +57 -0
  74. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  75. mito_ai/tests/providers/test_provider_limits.py +42 -0
  76. mito_ai/tests/providers/test_providers.py +382 -0
  77. mito_ai/tests/providers/test_retry_logic.py +389 -0
  78. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  79. mito_ai/tests/providers/utils.py +85 -0
  80. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  81. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  82. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  83. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  84. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  85. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  86. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  87. mito_ai/tests/test_constants.py +31 -3
  88. mito_ai/tests/test_telemetry.py +12 -0
  89. mito_ai/tests/user/__init__.py +2 -0
  90. mito_ai/tests/user/test_user.py +120 -0
  91. mito_ai/tests/utils/test_anthropic_utils.py +6 -6
  92. mito_ai/user/handlers.py +45 -0
  93. mito_ai/user/urls.py +21 -0
  94. mito_ai/utils/anthropic_utils.py +55 -121
  95. mito_ai/utils/create.py +17 -1
  96. mito_ai/utils/error_classes.py +42 -0
  97. mito_ai/utils/gemini_utils.py +39 -94
  98. mito_ai/utils/message_history_utils.py +7 -4
  99. mito_ai/utils/mito_server_utils.py +242 -0
  100. mito_ai/utils/open_ai_utils.py +38 -155
  101. mito_ai/utils/provider_utils.py +49 -0
  102. mito_ai/utils/server_limits.py +1 -1
  103. mito_ai/utils/telemetry_utils.py +137 -5
  104. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
  105. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
  106. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -1
  107. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +2 -2
  108. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +15948 -8403
  109. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  110. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  111. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  112. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +58 -33
  113. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -0
  114. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +10 -2
  115. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  116. mito_ai-0.1.49.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
  117. mito_ai-0.1.49.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
  118. mito_ai-0.1.49.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
  119. mito_ai-0.1.49.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
  120. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
  121. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
  122. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
  123. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
  124. mito_ai-0.1.49.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
  125. mito_ai-0.1.49.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
  126. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2 -240
  127. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  128. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/METADATA +5 -2
  129. mito_ai-0.1.49.dist-info/RECORD +205 -0
  130. mito_ai/app_builder/handlers.py +0 -218
  131. mito_ai/tests/providers_test.py +0 -438
  132. mito_ai/tests/test_anthropic_client.py +0 -270
  133. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
  134. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
  135. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
  136. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
  137. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js.map +0 -1
  138. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
  139. mito_ai-0.1.33.dist-info/RECORD +0 -134
  140. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  141. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  142. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  143. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  144. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
  145. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
  146. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,50 @@
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 List
5
+ from mito_ai.streamlit_conversion.prompts.prompt_constants import search_replace_instructions
6
+ from mito_ai.streamlit_conversion.prompts.prompt_utils import add_line_numbers_to_code
7
+
8
+ def get_update_existing_app_prompt(notebook: List[dict], streamlit_app_code: str, edit_prompt: str) -> str:
9
+ """
10
+ This prompt is used to update an existing streamlit app.
11
+ """
12
+
13
+ existing_streamlit_app_code_with_line_numbers = add_line_numbers_to_code(streamlit_app_code)
14
+
15
+ return f"""
16
+
17
+ GOAL: You've previously created a first draft of the Streamlit app. Now the user reviewed it and provided feedback.Update the existing streamlit app according to the feedback provided by the user. Use the input notebook to help you understand what code needs to be added, changed, or modified to fulfill the user's edit request.
18
+
19
+ **CRITICAL COMPLETION REQUIREMENT:**
20
+ You have ONE and ONLY ONE opportunity to complete this edit request. If you do not finish the entire task completely, the application will be broken and unusable. This is your final chance to get it right.
21
+
22
+ **COMPLETION RULES:**
23
+ 1. **NEVER leave partial work** - If the edit request requires generating a list with 100 items, provide ALL 100 items.
24
+ 2. **NEVER use placeholders** - This is your only opportunity to fulfill this edit request, so do not leave yourself another TODOs.
25
+ 3. **NEVER assume "good enough"** - Complete the task to 100% satisfaction.
26
+ 4. **If the task seems large, that's exactly why it needs to be done now** - This is your only chance
27
+
28
+ **HOW TO DETERMINE IF TASK IS COMPLETE:**
29
+ - If building a list/dictionary: Include ALL items that should be in the final data structure.
30
+ - If creating functions: Implement ALL required functionality.
31
+ - If converting a visualization: Copy over ALL of the visualization code from the notebook, including all styling and formatting.
32
+
33
+ {search_replace_instructions}
34
+
35
+ ===============================================
36
+
37
+ INPUT NOTEBOOK:
38
+ {notebook}
39
+
40
+ ===============================================
41
+
42
+ EXISTING STREAMLIT APP:
43
+ {existing_streamlit_app_code_with_line_numbers}
44
+
45
+ ===============================================
46
+
47
+ USER EDIT REQUEST:
48
+ {edit_prompt}
49
+
50
+ """
@@ -0,0 +1,94 @@
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
+ from typing import List, Tuple
6
+
7
+ from mito_ai.utils.error_classes import StreamlitConversionError
8
+ from mito_ai.utils.telemetry_utils import log
9
+
10
+
11
+ def extract_search_replace_blocks(message_content: str) -> List[Tuple[str, str]]:
12
+ """
13
+ Extract all search_replace blocks from Claude's response.
14
+
15
+ Returns:
16
+ List of tuples (search_text, replace_text) for each search/replace block
17
+ """
18
+ if "```search_replace" not in message_content:
19
+ return []
20
+
21
+ pattern = r'```search_replace\n(.*?)```'
22
+ matches = re.findall(pattern, message_content, re.DOTALL)
23
+
24
+ search_replace_pairs = []
25
+ for match in matches:
26
+ # Split by the separator
27
+ if "=======" not in match:
28
+ continue
29
+
30
+ parts = match.split("=======", 1)
31
+ if len(parts) != 2:
32
+ continue
33
+
34
+ search_part = parts[0]
35
+ replace_part = parts[1]
36
+
37
+ # Extract search text (after SEARCH marker)
38
+ if ">>>>>>> SEARCH" in search_part:
39
+ search_text = search_part.split(">>>>>>> SEARCH", 1)[1].strip()
40
+ else:
41
+ continue
42
+
43
+ # Extract replace text (before REPLACE marker)
44
+ if "<<<<<<< REPLACE" in replace_part:
45
+ replace_text = replace_part.split("<<<<<<< REPLACE", 1)[0].strip()
46
+ else:
47
+ continue
48
+
49
+ search_replace_pairs.append((search_text, replace_text))
50
+
51
+ return search_replace_pairs
52
+
53
+
54
+ def apply_search_replace(text: str, search_replace_pairs: List[Tuple[str, str]]) -> str:
55
+ """
56
+ Apply search/replace operations to the given text.
57
+
58
+ Parameters
59
+ ----------
60
+ text : str
61
+ The original file contents.
62
+ search_replace_pairs : List[Tuple[str, str]]
63
+ List of (search_text, replace_text) tuples to apply.
64
+
65
+ Returns
66
+ -------
67
+ str
68
+ The updated contents after applying all search/replace operations.
69
+
70
+ Raises
71
+ ------
72
+ ValueError
73
+ If a search text is not found or found multiple times.
74
+ """
75
+ if not search_replace_pairs:
76
+ return text
77
+
78
+ result = text
79
+
80
+ for search_text, replace_text in search_replace_pairs:
81
+ # Count occurrences of search text
82
+ count = result.count(search_text)
83
+
84
+ if count == 0:
85
+ print("Search Text Not Found: ", repr(search_text))
86
+ raise StreamlitConversionError(f"Search text not found: {repr(search_text)}", error_code=500)
87
+ elif count > 1:
88
+ print("Search Text Found Multiple Times: ", repr(search_text))
89
+ log("mito_ai_search_text_found_multiple_times", params={"search_text": repr(search_text)}, key_type="mito_server_key")
90
+
91
+ # Perform the replacement
92
+ result = result.replace(search_text, replace_text, 1)
93
+
94
+ return result
@@ -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,6 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from .handlers import StreamlitPreviewHandler
5
+
6
+ __all__ = ['StreamlitPreviewHandler']
@@ -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)}"})