mito-ai 0.1.45__py3-none-any.whl → 0.1.47__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mito-ai might be problematic. Click here for more details.

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