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.
Files changed (205) hide show
  1. mito_ai/__init__.py +114 -0
  2. mito_ai/_version.py +4 -0
  3. mito_ai/anthropic_client.py +334 -0
  4. mito_ai/app_deploy/__init__.py +6 -0
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/app_deploy/models.py +98 -0
  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/__init__.py +3 -0
  19. mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +59 -0
  20. mito_ai/completions/completion_handlers/agent_execution_handler.py +66 -0
  21. mito_ai/completions/completion_handlers/chat_completion_handler.py +141 -0
  22. mito_ai/completions/completion_handlers/code_explain_handler.py +113 -0
  23. mito_ai/completions/completion_handlers/completion_handler.py +42 -0
  24. mito_ai/completions/completion_handlers/inline_completer_handler.py +48 -0
  25. mito_ai/completions/completion_handlers/smart_debug_handler.py +160 -0
  26. mito_ai/completions/completion_handlers/utils.py +147 -0
  27. mito_ai/completions/handlers.py +415 -0
  28. mito_ai/completions/message_history.py +401 -0
  29. mito_ai/completions/models.py +404 -0
  30. mito_ai/completions/prompt_builders/__init__.py +3 -0
  31. mito_ai/completions/prompt_builders/agent_execution_prompt.py +57 -0
  32. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +160 -0
  33. mito_ai/completions/prompt_builders/agent_system_message.py +472 -0
  34. mito_ai/completions/prompt_builders/chat_name_prompt.py +15 -0
  35. mito_ai/completions/prompt_builders/chat_prompt.py +116 -0
  36. mito_ai/completions/prompt_builders/chat_system_message.py +92 -0
  37. mito_ai/completions/prompt_builders/explain_code_prompt.py +32 -0
  38. mito_ai/completions/prompt_builders/inline_completer_prompt.py +197 -0
  39. mito_ai/completions/prompt_builders/prompt_constants.py +170 -0
  40. mito_ai/completions/prompt_builders/smart_debug_prompt.py +199 -0
  41. mito_ai/completions/prompt_builders/utils.py +84 -0
  42. mito_ai/completions/providers.py +284 -0
  43. mito_ai/constants.py +63 -0
  44. mito_ai/db/__init__.py +3 -0
  45. mito_ai/db/crawlers/__init__.py +6 -0
  46. mito_ai/db/crawlers/base_crawler.py +61 -0
  47. mito_ai/db/crawlers/constants.py +43 -0
  48. mito_ai/db/crawlers/snowflake.py +71 -0
  49. mito_ai/db/handlers.py +168 -0
  50. mito_ai/db/models.py +31 -0
  51. mito_ai/db/urls.py +34 -0
  52. mito_ai/db/utils.py +185 -0
  53. mito_ai/docker/mssql/compose.yml +37 -0
  54. mito_ai/docker/mssql/init/setup.sql +21 -0
  55. mito_ai/docker/mysql/compose.yml +18 -0
  56. mito_ai/docker/mysql/init/setup.sql +13 -0
  57. mito_ai/docker/oracle/compose.yml +17 -0
  58. mito_ai/docker/oracle/init/setup.sql +20 -0
  59. mito_ai/docker/postgres/compose.yml +17 -0
  60. mito_ai/docker/postgres/init/setup.sql +13 -0
  61. mito_ai/enterprise/__init__.py +3 -0
  62. mito_ai/enterprise/utils.py +15 -0
  63. mito_ai/file_uploads/__init__.py +3 -0
  64. mito_ai/file_uploads/handlers.py +248 -0
  65. mito_ai/file_uploads/urls.py +21 -0
  66. mito_ai/gemini_client.py +232 -0
  67. mito_ai/log/handlers.py +38 -0
  68. mito_ai/log/urls.py +21 -0
  69. mito_ai/logger.py +37 -0
  70. mito_ai/openai_client.py +382 -0
  71. mito_ai/path_utils.py +70 -0
  72. mito_ai/rules/handlers.py +44 -0
  73. mito_ai/rules/urls.py +22 -0
  74. mito_ai/rules/utils.py +56 -0
  75. mito_ai/settings/handlers.py +41 -0
  76. mito_ai/settings/urls.py +20 -0
  77. mito_ai/settings/utils.py +42 -0
  78. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  79. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  80. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  81. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  82. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  83. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  84. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  85. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  86. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  87. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  88. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  89. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  90. mito_ai/streamlit_preview/__init__.py +6 -0
  91. mito_ai/streamlit_preview/handlers.py +111 -0
  92. mito_ai/streamlit_preview/manager.py +152 -0
  93. mito_ai/streamlit_preview/urls.py +22 -0
  94. mito_ai/streamlit_preview/utils.py +29 -0
  95. mito_ai/tests/__init__.py +3 -0
  96. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  97. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  98. mito_ai/tests/conftest.py +53 -0
  99. mito_ai/tests/create_agent_system_message_prompt_test.py +22 -0
  100. mito_ai/tests/data/prompt_lg.py +69 -0
  101. mito_ai/tests/data/prompt_sm.py +6 -0
  102. mito_ai/tests/data/prompt_xl.py +13 -0
  103. mito_ai/tests/data/stock_data.sqlite3 +0 -0
  104. mito_ai/tests/db/conftest.py +39 -0
  105. mito_ai/tests/db/connections_test.py +102 -0
  106. mito_ai/tests/db/mssql_test.py +29 -0
  107. mito_ai/tests/db/mysql_test.py +29 -0
  108. mito_ai/tests/db/oracle_test.py +29 -0
  109. mito_ai/tests/db/postgres_test.py +29 -0
  110. mito_ai/tests/db/schema_test.py +93 -0
  111. mito_ai/tests/db/sqlite_test.py +31 -0
  112. mito_ai/tests/db/test_db_constants.py +61 -0
  113. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  114. mito_ai/tests/file_uploads/__init__.py +2 -0
  115. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  116. mito_ai/tests/message_history/test_generate_short_chat_name.py +120 -0
  117. mito_ai/tests/message_history/test_message_history_utils.py +469 -0
  118. mito_ai/tests/open_ai_utils_test.py +152 -0
  119. mito_ai/tests/performance_test.py +329 -0
  120. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  121. mito_ai/tests/providers/test_azure.py +631 -0
  122. mito_ai/tests/providers/test_capabilities.py +120 -0
  123. mito_ai/tests/providers/test_gemini_client.py +195 -0
  124. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  125. mito_ai/tests/providers/test_model_resolution.py +130 -0
  126. mito_ai/tests/providers/test_openai_client.py +57 -0
  127. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  128. mito_ai/tests/providers/test_provider_limits.py +42 -0
  129. mito_ai/tests/providers/test_providers.py +382 -0
  130. mito_ai/tests/providers/test_retry_logic.py +389 -0
  131. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  132. mito_ai/tests/providers/utils.py +85 -0
  133. mito_ai/tests/rules/conftest.py +26 -0
  134. mito_ai/tests/rules/rules_test.py +117 -0
  135. mito_ai/tests/server_limits_test.py +406 -0
  136. mito_ai/tests/settings/conftest.py +26 -0
  137. mito_ai/tests/settings/settings_test.py +70 -0
  138. mito_ai/tests/settings/test_settings_constants.py +9 -0
  139. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  140. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  141. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  142. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  143. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  144. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  145. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  146. mito_ai/tests/test_constants.py +47 -0
  147. mito_ai/tests/test_telemetry.py +12 -0
  148. mito_ai/tests/user/__init__.py +2 -0
  149. mito_ai/tests/user/test_user.py +120 -0
  150. mito_ai/tests/utils/__init__.py +3 -0
  151. mito_ai/tests/utils/test_anthropic_utils.py +162 -0
  152. mito_ai/tests/utils/test_gemini_utils.py +98 -0
  153. mito_ai/tests/version_check_test.py +169 -0
  154. mito_ai/user/handlers.py +45 -0
  155. mito_ai/user/urls.py +21 -0
  156. mito_ai/utils/__init__.py +3 -0
  157. mito_ai/utils/anthropic_utils.py +168 -0
  158. mito_ai/utils/create.py +94 -0
  159. mito_ai/utils/db.py +74 -0
  160. mito_ai/utils/error_classes.py +42 -0
  161. mito_ai/utils/gemini_utils.py +133 -0
  162. mito_ai/utils/message_history_utils.py +87 -0
  163. mito_ai/utils/mito_server_utils.py +242 -0
  164. mito_ai/utils/open_ai_utils.py +200 -0
  165. mito_ai/utils/provider_utils.py +49 -0
  166. mito_ai/utils/schema.py +86 -0
  167. mito_ai/utils/server_limits.py +152 -0
  168. mito_ai/utils/telemetry_utils.py +480 -0
  169. mito_ai/utils/utils.py +89 -0
  170. mito_ai/utils/version_utils.py +94 -0
  171. mito_ai/utils/websocket_base.py +88 -0
  172. mito_ai/version_check.py +60 -0
  173. mito_ai-0.1.50.data/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +7 -0
  174. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/build_log.json +728 -0
  175. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/package.json +243 -0
  176. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +238 -0
  177. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +37 -0
  178. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +21602 -0
  179. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  180. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  181. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  182. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js +619 -0
  183. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js.map +1 -0
  184. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style.js +4 -0
  185. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +712 -0
  186. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  187. 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
  188. 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
  189. 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
  190. 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
  191. 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
  192. 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
  193. 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
  194. 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
  195. 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
  196. 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
  197. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2792 -0
  198. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  199. 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
  200. 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
  201. mito_ai-0.1.50.dist-info/METADATA +221 -0
  202. mito_ai-0.1.50.dist-info/RECORD +205 -0
  203. mito_ai-0.1.50.dist-info/WHEEL +4 -0
  204. mito_ai-0.1.50.dist-info/entry_points.txt +2 -0
  205. 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,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)}"})
@@ -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
@@ -0,0 +1,3 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+