mito-ai 0.1.44__py3-none-any.whl → 0.1.46__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 (76) hide show
  1. mito_ai/__init__.py +10 -1
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +92 -8
  4. mito_ai/app_deploy/app_deploy_utils.py +25 -0
  5. mito_ai/app_deploy/handlers.py +9 -12
  6. mito_ai/app_deploy/models.py +4 -1
  7. mito_ai/chat_history/handlers.py +63 -0
  8. mito_ai/chat_history/urls.py +32 -0
  9. mito_ai/completions/handlers.py +44 -20
  10. mito_ai/completions/models.py +1 -0
  11. mito_ai/completions/prompt_builders/prompt_constants.py +22 -4
  12. mito_ai/constants.py +3 -0
  13. mito_ai/streamlit_conversion/agent_utils.py +148 -30
  14. mito_ai/streamlit_conversion/prompts/prompt_constants.py +147 -24
  15. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
  16. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +2 -2
  17. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
  18. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  19. mito_ai/streamlit_conversion/streamlit_agent_handler.py +101 -104
  20. mito_ai/streamlit_conversion/streamlit_system_prompt.py +1 -0
  21. mito_ai/streamlit_conversion/streamlit_utils.py +18 -17
  22. mito_ai/streamlit_conversion/validate_streamlit_app.py +66 -62
  23. mito_ai/streamlit_preview/handlers.py +5 -3
  24. mito_ai/streamlit_preview/utils.py +11 -7
  25. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  26. mito_ai/tests/deploy_app/test_app_deploy_utils.py +71 -0
  27. mito_ai/tests/message_history/test_message_history_utils.py +43 -19
  28. mito_ai/tests/providers/test_anthropic_client.py +180 -8
  29. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +368 -0
  30. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +533 -0
  31. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +71 -158
  32. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +16 -16
  33. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +16 -28
  34. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +2 -2
  35. mito_ai/tests/user/__init__.py +2 -0
  36. mito_ai/tests/user/test_user.py +120 -0
  37. mito_ai/tests/utils/test_anthropic_utils.py +4 -4
  38. mito_ai/user/handlers.py +33 -0
  39. mito_ai/user/urls.py +21 -0
  40. mito_ai/utils/anthropic_utils.py +15 -21
  41. mito_ai/utils/message_history_utils.py +4 -3
  42. mito_ai/utils/telemetry_utils.py +7 -4
  43. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  44. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  45. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  46. mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.cf2e3ad2797fbb53826b.js → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js +1520 -300
  47. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +1 -0
  48. mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5482493d1270f55b7283.js → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js +18 -18
  49. mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5482493d1270f55b7283.js.map → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map +1 -1
  50. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/METADATA +2 -2
  51. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/RECORD +75 -63
  52. mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.cf2e3ad2797fbb53826b.js.map +0 -1
  53. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  54. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  55. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  56. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  57. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  58. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  59. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  60. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  61. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  62. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  63. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  64. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  65. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
  66. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
  67. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
  68. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  69. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  70. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  71. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
  72. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  73. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  74. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/WHEEL +0 -0
  75. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/entry_points.txt +0 -0
  76. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/licenses/LICENSE +0 -0
@@ -1,135 +1,138 @@
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 apply_patch_to_text, extract_todo_placeholders, fix_diff_headers, 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, extract_unified_diff_blocks, get_app_code_from_file, parse_jupyter_notebook_to_extract_required_content
18
14
  from mito_ai.completions.models import MessageType
19
15
  from mito_ai.utils.telemetry_utils import log_streamlit_app_creation_error, log_streamlit_app_creation_retry, log_streamlit_app_creation_success
20
16
  from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
21
17
 
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
51
-
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
- messages: List[MessageParam] = [
56
- cast(MessageParam, {
57
- "role": "user",
58
- "content": [{
59
- "type": "text",
60
- "text": get_streamlit_app_creation_prompt(notebook)
61
- }]
62
- })
63
- ]
64
-
65
- agent_response = await self.get_response_from_agent(messages)
66
-
67
- converted_code = extract_code_blocks(agent_response)
68
-
69
- # Extract the TODOs from the agent's response
70
- todo_placeholders = extract_todo_placeholders(agent_response)
71
-
72
- for todo_placeholder in todo_placeholders:
73
- print(f"Processing AI TODO: {todo_placeholder}")
74
- todo_prompt = get_finish_todo_prompt(notebook, converted_code, todo_placeholder)
75
- todo_messages: List[MessageParam] = [
76
- cast(MessageParam, {
77
- "role": "user",
78
- "content": [{
79
- "type": "text",
80
- "text": todo_prompt
81
- }]
82
- })
83
- ]
84
- todo_response = await self.get_response_from_agent(todo_messages)
85
-
86
- # Apply the diff to the streamlit app
87
- exctracted_diff = extract_unified_diff_blocks(todo_response)
88
- fixed_diff = fix_diff_headers(exctracted_diff)
89
- converted_code = apply_patch_to_text(converted_code, fixed_diff)
90
-
91
- return converted_code
92
-
18
+ def get_app_directory(notebook_path: str) -> str:
19
+ # Make sure the path is absolute if it is not already
20
+ absolute_notebook_path = os.path.abspath(notebook_path)
21
+
22
+ # Get the directory of the notebook
23
+ app_directory = os.path.dirname(absolute_notebook_path)
24
+ return app_directory
93
25
 
94
- async def correct_error_in_generation(self, error: str, streamlit_app_code: str) -> str:
95
- """If errors are present, send it back to the agent to get corrections in code"""
96
- messages: List[MessageParam] = [
26
+ async def generate_new_streamlit_code(notebook: List[dict]) -> str:
27
+ """Send a query to the agent, get its response and parse the code"""
28
+
29
+ prompt_text = get_streamlit_app_creation_prompt(notebook)
30
+
31
+ messages: List[MessageParam] = [
32
+ cast(MessageParam, {
33
+ "role": "user",
34
+ "content": [{
35
+ "type": "text",
36
+ "text": prompt_text
37
+ }]
38
+ })
39
+ ]
40
+ agent_response = await get_response_from_agent(messages)
41
+ converted_code = extract_code_blocks(agent_response)
42
+
43
+ # Extract the TODOs from the agent's response
44
+ todo_placeholders = extract_todo_placeholders(agent_response)
45
+
46
+ for todo_placeholder in todo_placeholders:
47
+ print(f"Processing AI TODO: {todo_placeholder}")
48
+ todo_prompt = get_finish_todo_prompt(notebook, converted_code, todo_placeholder)
49
+ todo_messages: List[MessageParam] = [
97
50
  cast(MessageParam, {
98
51
  "role": "user",
99
52
  "content": [{
100
53
  "type": "text",
101
- "text": get_streamlit_error_correction_prompt(error, streamlit_app_code)
54
+ "text": todo_prompt
102
55
  }]
103
56
  })
104
57
  ]
105
- agent_response = await self.get_response_from_agent(messages)
58
+ todo_response = await get_response_from_agent(todo_messages)
106
59
 
107
60
  # Apply the diff to the streamlit app
108
- exctracted_diff = extract_unified_diff_blocks(agent_response)
109
-
110
- print(f"\n\nExtracted diff: {exctracted_diff}")
61
+ exctracted_diff = extract_unified_diff_blocks(todo_response)
111
62
  fixed_diff = fix_diff_headers(exctracted_diff)
112
- streamlit_app_code = apply_patch_to_text(streamlit_app_code, fixed_diff)
113
-
114
- print("\n\nUpdated app code: ", streamlit_app_code)
63
+ converted_code = apply_patch_to_text(converted_code, fixed_diff)
64
+
65
+ return converted_code
115
66
 
116
- return streamlit_app_code
117
67
 
68
+ async def update_existing_streamlit_code(notebook: List[dict], streamlit_app_code: str, edit_prompt: str) -> str:
69
+ """Send a query to the agent, get its response and parse the code"""
70
+ prompt_text = get_update_existing_app_prompt(notebook, streamlit_app_code, edit_prompt)
71
+
72
+ messages: List[MessageParam] = [
73
+ cast(MessageParam, {
74
+ "role": "user",
75
+ "content": [{
76
+ "type": "text",
77
+ "text": prompt_text
78
+ }]
79
+ })
80
+ ]
81
+
82
+ agent_response = await get_response_from_agent(messages)
83
+ exctracted_diff = extract_unified_diff_blocks(agent_response)
84
+ fixed_diff = fix_diff_headers(exctracted_diff)
85
+ print(fixed_diff)
86
+ converted_code = apply_patch_to_text(streamlit_app_code, fixed_diff)
87
+ return converted_code
88
+
89
+
90
+ async def correct_error_in_generation(error: str, streamlit_app_code: str) -> str:
91
+ """If errors are present, send it back to the agent to get corrections in code"""
92
+ messages: List[MessageParam] = [
93
+ cast(MessageParam, {
94
+ "role": "user",
95
+ "content": [{
96
+ "type": "text",
97
+ "text": get_streamlit_error_correction_prompt(error, streamlit_app_code)
98
+ }]
99
+ })
100
+ ]
101
+ agent_response = await get_response_from_agent(messages)
102
+
103
+ # Apply the diff to the streamlit app
104
+ exctracted_diff = extract_unified_diff_blocks(agent_response)
105
+ fixed_diff = fix_diff_headers(exctracted_diff)
106
+ streamlit_app_code = apply_patch_to_text(streamlit_app_code, fixed_diff)
118
107
 
119
- async def streamlit_handler(notebook_path: str) -> Tuple[bool, Optional[str], str]:
108
+ return streamlit_app_code
109
+
110
+ async def streamlit_handler(notebook_path: str, edit_prompt: str = "") -> Tuple[bool, Optional[str], str]:
120
111
  """Handler function for streamlit code generation and validation"""
121
112
 
122
113
  clean_directory_check(notebook_path)
123
114
 
124
115
  notebook_code = parse_jupyter_notebook_to_extract_required_content(notebook_path)
125
- streamlit_code_generator = StreamlitCodeGeneration()
126
- streamlit_code = await streamlit_code_generator.generate_streamlit_code(notebook_code)
116
+ app_directory = get_app_directory(notebook_path)
127
117
 
118
+ if edit_prompt != "":
119
+ # If the user is editing an existing streamlit app, use the update function
120
+ streamlit_code = get_app_code_from_file(app_directory)
121
+
122
+ if streamlit_code is None:
123
+ return False, '', "Error updating existing streamlit app because app.py file was not found."
124
+
125
+ streamlit_code = await update_existing_streamlit_code(notebook_code, streamlit_code, edit_prompt)
126
+ else:
127
+ # Otherwise generate a new streamlit app
128
+ streamlit_code = await generate_new_streamlit_code(notebook_code)
129
+
130
+ # Then, after creating/updating the app, validate that the new code runs
128
131
  has_validation_error, errors = validate_app(streamlit_code, notebook_path)
129
132
  tries = 0
130
133
  while has_validation_error and tries < 5:
131
134
  for error in errors:
132
- streamlit_code = await streamlit_code_generator.correct_error_in_generation(error, streamlit_code)
135
+ streamlit_code = await correct_error_in_generation(error, streamlit_code)
133
136
 
134
137
  has_validation_error, errors = validate_app(streamlit_code, notebook_path)
135
138
 
@@ -140,19 +143,13 @@ async def streamlit_handler(notebook_path: str) -> Tuple[bool, Optional[str], st
140
143
  tries+=1
141
144
 
142
145
  if has_validation_error:
143
- log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, error)
146
+ log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, error, edit_prompt)
144
147
  return False, '', "Error generating streamlit code by agent"
145
148
 
146
- # Convert to absolute path for directory calculation
147
- absolute_notebook_path = notebook_path
148
- if not (notebook_path.startswith('/') or (len(notebook_path) > 1 and notebook_path[1] == ':')):
149
- absolute_notebook_path = os.path.join(os.getcwd(), notebook_path)
150
-
151
- app_directory = os.path.dirname(absolute_notebook_path)
149
+ # Finally, update the app.py file with the new code
152
150
  success_flag, app_path, message = create_app_file(app_directory, streamlit_code)
153
-
154
151
  if not success_flag:
155
- log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, message)
152
+ log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, message, edit_prompt)
156
153
 
157
- log_streamlit_app_creation_success('mito_server_key', MessageType.STREAMLIT_CONVERSION)
154
+ log_streamlit_app_creation_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
158
155
  return success_flag, app_path, message
@@ -22,6 +22,7 @@ STREAMLIT IMPLEMENTATION GUIDELINES:
22
22
  - Include all text explanations and insights from markdown cells
23
23
  - Add interactive elements where beneficial (filters, selectors, etc.)
24
24
  - Ensure professional styling and layout suitable for executives
25
+ - Just create the streamlit app code, do not include a _main_ function block. The file will be run directly using `streamlit run app.py`.
25
26
 
26
27
  CRITICAL REQUIREMENTS:
27
28
  1. **PRESERVE ALL CODE EXACTLY**: Every line of code, every data structure, every import must be included in full
@@ -4,7 +4,7 @@
4
4
  import re
5
5
  import json
6
6
  import os
7
- from typing import Dict, Optional, Tuple, Any
7
+ from typing import Dict, List, Optional, Tuple, Any
8
8
  from pathlib import Path
9
9
 
10
10
  def extract_code_blocks(message_content: str) -> str:
@@ -25,7 +25,8 @@ def extract_code_blocks(message_content: str) -> str:
25
25
  matches = re.findall(pattern, message_content, re.DOTALL)
26
26
 
27
27
  # Concatenate with single newlines
28
- return '\n'.join(matches)
28
+ result = '\n'.join(matches)
29
+ return result
29
30
 
30
31
  def extract_unified_diff_blocks(message_content: str) -> str:
31
32
  """
@@ -53,14 +54,23 @@ def create_app_file(app_directory: str, code: str) -> Tuple[bool, str, str]:
53
54
  """
54
55
  try:
55
56
  app_path = os.path.join(app_directory, "app.py")
56
- with open(app_path, 'w') as f:
57
+
58
+ with open(app_path, 'w', encoding='utf-8') as f:
57
59
  f.write(code)
60
+
58
61
  return True, app_path, f"Successfully created {app_directory}"
59
62
  except IOError as e:
60
63
  return False, '', f"Error creating file: {str(e)}"
61
64
  except Exception as e:
62
65
  return False, '', f"Unexpected error: {str(e)}"
63
66
 
67
+ def get_app_code_from_file(app_directory: str) -> Optional[str]:
68
+ app_path = get_app_path(app_directory)
69
+ if app_path is None:
70
+ return None
71
+ with open(app_path, 'r', encoding='utf-8') as f:
72
+ return f.read()
73
+
64
74
 
65
75
  def get_app_path(app_directory: str) -> Optional[str]:
66
76
  """
@@ -70,9 +80,8 @@ def get_app_path(app_directory: str) -> Optional[str]:
70
80
  if not os.path.exists(app_path):
71
81
  return None
72
82
  return app_path
73
-
74
83
 
75
- def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Dict[str, Any]:
84
+ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> List[Dict[str, Any]]:
76
85
  """
77
86
  Read a Jupyter notebook and filter cells to keep only cell_type and source fields.
78
87
 
@@ -102,18 +111,15 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Di
102
111
  raise KeyError("Notebook does not contain 'cells' key")
103
112
 
104
113
  # Filter each cell to keep only cell_type and source
105
- filtered_cells = []
114
+ filtered_cells: List[Dict[str, Any]] = []
106
115
  for cell in notebook_data['cells']:
107
- filtered_cell = {
116
+ filtered_cell: Dict[str, Any] = {
108
117
  'cell_type': cell.get('cell_type', ''),
109
118
  'source': cell.get('source', [])
110
119
  }
111
120
  filtered_cells.append(filtered_cell)
112
-
113
- # Update the notebook data with filtered cells
114
- notebook_data['cells'] = filtered_cells
115
-
116
- return notebook_data
121
+
122
+ return filtered_cells
117
123
 
118
124
  except FileNotFoundError:
119
125
  raise FileNotFoundError(f"Notebook file not found: {notebook_path}")
@@ -139,8 +145,3 @@ def clean_directory_check(notebook_path: str) -> None:
139
145
 
140
146
  if not dir_path.exists():
141
147
  raise ValueError(f"Directory does not exist: {dir_path}")
142
-
143
- file_count = len([f for f in dir_path.iterdir() if f.is_file()])
144
- if file_count > 10:
145
- raise ValueError(
146
- f"Too many files in directory: 10 allowed but {file_count} present. Create a new directory and retry")
@@ -23,42 +23,52 @@ from mito_ai.streamlit_conversion.streamlit_utils import resolve_notebook_path
23
23
  warnings.filterwarnings("ignore", message=".*bare mode.*")
24
24
 
25
25
 
26
- class StreamlitValidator:
27
- def __init__(self, port: int = 8501) -> None:
28
- self.temp_dir: Optional[str] = None
26
+ def get_syntax_error(app_code: str) -> Optional[str]:
27
+ """Check if the Python code has valid syntax"""
28
+ try:
29
+ ast.parse(app_code)
30
+ return None
31
+ except SyntaxError as e:
32
+ error_msg = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
33
+ return error_msg
29
34
 
30
- def get_syntax_error(self, app_code: str) -> Optional[str]:
31
- """Check if the Python code has valid syntax"""
35
+ def get_runtime_errors(app_code: str, app_path: str) -> Optional[List[Dict[str, Any]]]:
36
+ """Start the Streamlit app in a subprocess"""
37
+
38
+ directory = os.path.dirname(app_path)
39
+
40
+ @contextmanager
41
+ def change_working_directory(path: str) -> Generator[None, Any, None]:
42
+ """
43
+ Context manager to temporarily change working directory
44
+ so that relative paths are still valid when we run the app
45
+ """
46
+ if path == '':
47
+ yield
48
+
49
+ original_cwd = os.getcwd()
32
50
  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
51
+ os.chdir(path)
52
+ yield
53
+ finally:
54
+ os.chdir(original_cwd)
55
+
56
+ with change_working_directory(directory):
57
+ # Create a temporary file that uses UTF-8 encoding so
58
+ # we don't run into issues with non-ASCII characters on Windows.
59
+ # We use utf-8 encoding when writing the app.py file so this validation
60
+ # code mirrors the actual file.
38
61
 
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)
59
-
60
- with change_working_directory(directory):
61
- app_test = AppTest.from_string(app_code, default_timeout=30)
62
+ # Note: Since the AppTest.from_file tries to open the file, we need to first close the file
63
+ # by exiting the context manager and using the delete=False flag so that the file still exists.
64
+ # Windows can't open the same file twice at the same time. We cleanup at the end.
65
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as f:
66
+ f.write(app_code)
67
+ temp_path = f.name
68
+
69
+ try:
70
+ # Run Streamlit test from file with UTF-8 encoding
71
+ app_test = AppTest.from_file(temp_path, default_timeout=30)
62
72
  app_test.run()
63
73
 
64
74
  # Check for exceptions
@@ -72,44 +82,38 @@ class StreamlitValidator:
72
82
  return errors
73
83
 
74
84
  return None
85
+ finally:
86
+ # Clean up the temporary file
87
+ try:
88
+ os.unlink(temp_path)
89
+ except OSError:
90
+ pass # File might already be deleted
75
91
 
76
- def cleanup(self) -> None:
77
- """Clean up the temporary files"""
78
- if self.temp_dir and os.path.exists(self.temp_dir):
79
- shutil.rmtree(self.temp_dir)
80
- self.temp_dir = None
81
-
82
- def _validate_app(self, app_code: str, app_path: str) -> 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 = self.get_syntax_error(app_code)
89
- if syntax_error:
90
- errors.append({'type': 'syntax', 'details': syntax_error})
92
+ def check_for_errors(app_code: str, app_path: str) -> List[Dict[str, Any]]:
93
+ """Complete validation pipeline"""
94
+ errors: List[Dict[str, Any]] = []
91
95
 
92
- runtime_errors = self.get_runtime_errors(app_code, app_path)
93
-
94
- print('Found Runtime Errors', runtime_errors)
95
-
96
- if runtime_errors:
97
- errors.extend(runtime_errors)
98
-
99
- except Exception as e:
100
- errors.append({'type': 'validation', 'details': str(e)})
96
+ try:
97
+ # Step 1: Check syntax
98
+ syntax_error = get_syntax_error(app_code)
99
+ if syntax_error:
100
+ errors.append({'type': 'syntax', 'details': syntax_error})
101
101
 
102
- finally:
103
- self.cleanup()
102
+ runtime_errors = get_runtime_errors(app_code, app_path)
103
+
104
+ if runtime_errors:
105
+ errors.extend(runtime_errors)
106
+
107
+ except Exception as e:
108
+ errors.append({'type': 'validation', 'details': str(e)})
104
109
 
105
- return errors
110
+ return errors
106
111
 
107
112
  def validate_app(app_code: str, notebook_path: str) -> Tuple[bool, List[str]]:
108
113
  """Convenience function to validate Streamlit code"""
109
114
  notebook_path = resolve_notebook_path(notebook_path)
110
115
 
111
- validator = StreamlitValidator()
112
- errors = validator._validate_app(app_code, notebook_path)
116
+ errors = check_for_errors(app_code, notebook_path)
113
117
 
114
118
  has_validation_error = len(errors) > 0
115
119
  stringified_errors = [str(error) for error in errors]
@@ -82,15 +82,17 @@ class StreamlitPreviewHandler(APIHandler):
82
82
  try:
83
83
  # Parse and validate request
84
84
  body = self.get_json_body()
85
- is_valid, error_msg, notebook_path, force_recreate = validate_request_body(body)
86
- if not is_valid or notebook_path is None:
85
+ is_valid, error_msg, notebook_path, force_recreate, edit_prompt = validate_request_body(body)
86
+ if not is_valid or not notebook_path:
87
87
  self.set_status(400)
88
88
  self.finish({"error": error_msg})
89
89
  return
90
90
 
91
91
  # Ensure app exists
92
92
  resolved_notebook_path = self._resolve_notebook_path(notebook_path)
93
- success, error_msg = await ensure_app_exists(resolved_notebook_path, force_recreate)
93
+
94
+ success, error_msg = await ensure_app_exists(resolved_notebook_path, force_recreate, edit_prompt)
95
+
94
96
  if not success:
95
97
  self.set_status(500)
96
98
  self.finish({"error": error_msg})
@@ -7,22 +7,26 @@ from mito_ai.streamlit_conversion.streamlit_utils import get_app_path
7
7
  from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
8
8
 
9
9
 
10
- def validate_request_body(body: Optional[dict]) -> Tuple[bool, str, Optional[str], bool]:
10
+ def validate_request_body(body: Optional[dict]) -> Tuple[bool, str, Optional[str], bool, str]:
11
11
  """Validate the request body and extract notebook_path and force_recreate."""
12
12
  if body is None:
13
- return False, "Invalid or missing JSON body", None, False
13
+ return False, "Invalid or missing JSON body", None, False, ""
14
14
 
15
15
  notebook_path = body.get("notebook_path")
16
16
  if not notebook_path:
17
- return False, "Missing notebook_path parameter", None, False
17
+ return False, "Missing notebook_path parameter", None, False, ""
18
18
 
19
19
  force_recreate = body.get("force_recreate", False)
20
20
  if not isinstance(force_recreate, bool):
21
- return False, "force_recreate must be a boolean", None, False
21
+ return False, "force_recreate must be a boolean", None, False, ""
22
+
23
+ edit_prompt = body.get("edit_prompt", "")
24
+ if not isinstance(edit_prompt, str):
25
+ return False, "edit_prompt must be a string", None, False, ""
22
26
 
23
- return True, "", notebook_path, force_recreate
27
+ return True, "", notebook_path, force_recreate, edit_prompt
24
28
 
25
- async def ensure_app_exists(resolved_notebook_path: str, force_recreate: bool = False) -> Tuple[bool, str]:
29
+ async def ensure_app_exists(resolved_notebook_path: str, force_recreate: bool = False, edit_prompt: str = "") -> Tuple[bool, str]:
26
30
  """Ensure app.py exists, generating it if necessary or if force_recreate is True."""
27
31
  # Check if the app already exists
28
32
  app_path = get_app_path(os.path.dirname(resolved_notebook_path))
@@ -33,7 +37,7 @@ async def ensure_app_exists(resolved_notebook_path: str, force_recreate: bool =
33
37
  else:
34
38
  print("[Mito AI] Force recreating streamlit app")
35
39
 
36
- success, app_path, message = await streamlit_handler(resolved_notebook_path)
40
+ success, app_path, message = await streamlit_handler(resolved_notebook_path, edit_prompt)
37
41
 
38
42
  if not success or app_path is None:
39
43
  return False, f"Failed to generate streamlit code: {message}"