mito-ai 0.1.33__py3-none-any.whl → 0.1.49__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. mito_ai/__init__.py +49 -9
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +142 -67
  4. mito_ai/{app_builder → app_deploy}/__init__.py +1 -1
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/{app_builder → app_deploy}/models.py +35 -22
  8. mito_ai/app_manager/__init__.py +4 -0
  9. mito_ai/app_manager/handlers.py +167 -0
  10. mito_ai/app_manager/models.py +71 -0
  11. mito_ai/app_manager/utils.py +24 -0
  12. mito_ai/auth/README.md +18 -0
  13. mito_ai/auth/__init__.py +6 -0
  14. mito_ai/auth/handlers.py +96 -0
  15. mito_ai/auth/urls.py +13 -0
  16. mito_ai/chat_history/handlers.py +63 -0
  17. mito_ai/chat_history/urls.py +32 -0
  18. mito_ai/completions/completion_handlers/agent_execution_handler.py +1 -1
  19. mito_ai/completions/completion_handlers/chat_completion_handler.py +4 -4
  20. mito_ai/completions/completion_handlers/utils.py +99 -37
  21. mito_ai/completions/handlers.py +57 -20
  22. mito_ai/completions/message_history.py +9 -1
  23. mito_ai/completions/models.py +31 -7
  24. mito_ai/completions/prompt_builders/agent_execution_prompt.py +21 -2
  25. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
  26. mito_ai/completions/prompt_builders/agent_system_message.py +115 -42
  27. mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
  28. mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
  29. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  30. mito_ai/completions/prompt_builders/prompt_constants.py +23 -4
  31. mito_ai/completions/prompt_builders/utils.py +72 -10
  32. mito_ai/completions/providers.py +81 -47
  33. mito_ai/constants.py +25 -24
  34. mito_ai/file_uploads/__init__.py +3 -0
  35. mito_ai/file_uploads/handlers.py +248 -0
  36. mito_ai/file_uploads/urls.py +21 -0
  37. mito_ai/gemini_client.py +44 -48
  38. mito_ai/log/handlers.py +10 -3
  39. mito_ai/log/urls.py +3 -3
  40. mito_ai/openai_client.py +30 -44
  41. mito_ai/path_utils.py +70 -0
  42. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  43. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  44. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  45. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  46. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  47. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  48. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  49. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  50. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  51. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  52. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  53. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  54. mito_ai/streamlit_preview/__init__.py +6 -0
  55. mito_ai/streamlit_preview/handlers.py +111 -0
  56. mito_ai/streamlit_preview/manager.py +152 -0
  57. mito_ai/streamlit_preview/urls.py +22 -0
  58. mito_ai/streamlit_preview/utils.py +29 -0
  59. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  60. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  61. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  62. mito_ai/tests/file_uploads/__init__.py +2 -0
  63. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  64. mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
  65. mito_ai/tests/message_history/test_message_history_utils.py +103 -23
  66. mito_ai/tests/open_ai_utils_test.py +18 -22
  67. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  68. mito_ai/tests/providers/test_azure.py +2 -6
  69. mito_ai/tests/providers/test_capabilities.py +120 -0
  70. mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
  71. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  72. mito_ai/tests/providers/test_model_resolution.py +130 -0
  73. mito_ai/tests/providers/test_openai_client.py +57 -0
  74. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  75. mito_ai/tests/providers/test_provider_limits.py +42 -0
  76. mito_ai/tests/providers/test_providers.py +382 -0
  77. mito_ai/tests/providers/test_retry_logic.py +389 -0
  78. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  79. mito_ai/tests/providers/utils.py +85 -0
  80. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  81. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  82. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  83. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  84. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  85. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  86. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  87. mito_ai/tests/test_constants.py +31 -3
  88. mito_ai/tests/test_telemetry.py +12 -0
  89. mito_ai/tests/user/__init__.py +2 -0
  90. mito_ai/tests/user/test_user.py +120 -0
  91. mito_ai/tests/utils/test_anthropic_utils.py +6 -6
  92. mito_ai/user/handlers.py +45 -0
  93. mito_ai/user/urls.py +21 -0
  94. mito_ai/utils/anthropic_utils.py +55 -121
  95. mito_ai/utils/create.py +17 -1
  96. mito_ai/utils/error_classes.py +42 -0
  97. mito_ai/utils/gemini_utils.py +39 -94
  98. mito_ai/utils/message_history_utils.py +7 -4
  99. mito_ai/utils/mito_server_utils.py +242 -0
  100. mito_ai/utils/open_ai_utils.py +38 -155
  101. mito_ai/utils/provider_utils.py +49 -0
  102. mito_ai/utils/server_limits.py +1 -1
  103. mito_ai/utils/telemetry_utils.py +137 -5
  104. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
  105. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
  106. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -1
  107. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +2 -2
  108. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +15948 -8403
  109. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  110. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  111. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  112. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +58 -33
  113. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -0
  114. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +10 -2
  115. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  116. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +533 -0
  117. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +1 -0
  118. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
  119. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
  120. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
  121. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
  122. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
  123. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
  124. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +7440 -0
  125. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +1 -0
  126. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2 -240
  127. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  128. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/METADATA +5 -2
  129. mito_ai-0.1.49.dist-info/RECORD +205 -0
  130. mito_ai/app_builder/handlers.py +0 -218
  131. mito_ai/tests/providers_test.py +0 -438
  132. mito_ai/tests/test_anthropic_client.py +0 -270
  133. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
  134. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
  135. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
  136. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
  137. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js.map +0 -1
  138. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
  139. mito_ai-0.1.33.dist-info/RECORD +0 -134
  140. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  141. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  142. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  143. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  144. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
  145. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
  146. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,240 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from mito_ai.utils.error_classes import StreamlitConversionError
5
+ import pytest
6
+ from mito_ai.streamlit_conversion.search_replace_utils import apply_search_replace
7
+
8
+
9
+ @pytest.mark.parametrize("original_text,search_replace_pairs,expected_result", [
10
+ # Test case 1: Simple title change
11
+ (
12
+ """import streamlit as st
13
+
14
+ st.markdown(\"\"\"
15
+ <style>
16
+ #MainMenu {visibility: hidden;}
17
+ .stAppDeployButton {display:none;}
18
+ footer {visibility: hidden;}
19
+ .stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
20
+ </style>
21
+ \"\"\", unsafe_allow_html=True)
22
+
23
+ st.title("Simple Calculation")
24
+
25
+ x = 5
26
+ y = 10
27
+ result = x + y
28
+
29
+ st.write(f"x = {x}")
30
+ st.write(f"y = {y}")
31
+ st.write(f"x + y = {result}")""",
32
+ [("st.title(\"Simple Calculation\")", "st.title(\"Math Examples\")")],
33
+ """import streamlit as st
34
+
35
+ st.markdown(\"\"\"
36
+ <style>
37
+ #MainMenu {visibility: hidden;}
38
+ .stAppDeployButton {display:none;}
39
+ footer {visibility: hidden;}
40
+ .stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
41
+ </style>
42
+ \"\"\", unsafe_allow_html=True)
43
+
44
+ st.title("Math Examples")
45
+
46
+ x = 5
47
+ y = 10
48
+ result = x + y
49
+
50
+ st.write(f"x = {x}")
51
+ st.write(f"y = {y}")
52
+ st.write(f"x + y = {result}")"""
53
+ ),
54
+
55
+ # Test case 2: Add new content
56
+ (
57
+ """import streamlit as st
58
+
59
+ st.title("My App")""",
60
+ [("st.title(\"My App\")", """st.title("My App")
61
+ st.header("Welcome")
62
+ st.write("This is a test app")""")],
63
+ """import streamlit as st
64
+
65
+ st.title("My App")
66
+ st.header("Welcome")
67
+ st.write("This is a test app")"""
68
+ ),
69
+
70
+ # Test case 3: Remove lines
71
+ (
72
+ """import streamlit as st
73
+
74
+ st.header("Welcome")
75
+ st.title("My App")
76
+ st.write("This is a test app")""",
77
+ [("""st.header("Welcome")
78
+ st.title("My App")
79
+ st.write("This is a test app")""", "st.title(\"My App\")")],
80
+ """import streamlit as st
81
+
82
+ st.title("My App")"""
83
+ ),
84
+
85
+ # Test case 4: Multiple replacements
86
+ (
87
+ """import streamlit as st
88
+
89
+ st.title("Old Title")
90
+ x = 5
91
+ y = 10
92
+ st.write("Old message")""",
93
+ [
94
+ ("st.title(\"Old Title\")", "st.title(\"New Title\")"),
95
+ ("st.write(\"Old message\")", "st.write(\"New message\")")
96
+ ],
97
+ """import streamlit as st
98
+
99
+ st.title("New Title")
100
+ x = 5
101
+ y = 10
102
+ st.write("New message")"""
103
+ ),
104
+
105
+ # Test case 5: Empty search/replace pairs
106
+ (
107
+ """import streamlit as st
108
+
109
+ st.title("My App")""",
110
+ [],
111
+ """import streamlit as st
112
+
113
+ st.title("My App")"""
114
+ ),
115
+
116
+ # Test case 6: Complex replacement with context
117
+ (
118
+ """import streamlit as st
119
+
120
+ # This is a comment
121
+ st.title("Old Title")
122
+ # Another comment
123
+ x = 5
124
+ y = 10
125
+ # Final comment""",
126
+ [("""# This is a comment
127
+ st.title("Old Title")
128
+ # Another comment""", """# This is a comment
129
+ st.title("New Title")
130
+ # Another comment""")],
131
+ """import streamlit as st
132
+
133
+ # This is a comment
134
+ st.title("New Title")
135
+ # Another comment
136
+ x = 5
137
+ y = 10
138
+ # Final comment"""
139
+ ),
140
+
141
+ # Test case 7: Replace multiple consecutive lines
142
+ (
143
+ """import streamlit as st
144
+
145
+ st.title("My App")
146
+ st.write("Line 1")
147
+ st.write("Line 2")
148
+ st.write("Line 3")
149
+
150
+ x = 5""",
151
+ [("""st.write("Line 1")
152
+ st.write("Line 2")
153
+ st.write("Line 3")""", "st.write(\"New content\")")],
154
+ """import streamlit as st
155
+
156
+ st.title("My App")
157
+ st.write("New content")
158
+
159
+ x = 5"""
160
+ ),
161
+
162
+ # Test case 8: Add lines at the beginning
163
+ (
164
+ """import streamlit as st
165
+
166
+ st.title("My App")""",
167
+ [("import streamlit as st", """import pandas as pd
168
+ import streamlit as st""")],
169
+ """import pandas as pd
170
+ import streamlit as st
171
+
172
+ st.title("My App")"""
173
+ ),
174
+
175
+ # Test case 9: Add lines at the end
176
+ (
177
+ """import streamlit as st
178
+
179
+ st.title("My App")""",
180
+ [("st.title(\"My App\")", """st.title("My App")
181
+
182
+ st.write("Footer content")
183
+ st.write("More footer")""")],
184
+ """import streamlit as st
185
+
186
+ st.title("My App")
187
+
188
+ st.write("Footer content")
189
+ st.write("More footer")"""
190
+ ),
191
+
192
+ # Test case 10: Add emoji to streamlit app title
193
+ (
194
+ """import streamlit as st
195
+
196
+ st.title("My App")
197
+ st.write("Welcome to my application")""",
198
+ [("st.title(\"My App\")", "st.title(\"🚀 My App\")")],
199
+ """import streamlit as st
200
+
201
+ st.title("🚀 My App")
202
+ st.write("Welcome to my application")"""
203
+ ),
204
+
205
+ # Test case 11: Only replace first occurrence when search text exists multiple times
206
+ (
207
+ """import streamlit as st
208
+
209
+ st.write("Hello World")
210
+ st.title("My App")
211
+ st.write("Hello World")
212
+ st.write("Another message")""",
213
+ [("st.write(\"Hello World\")", "st.write(\"Hi There\")")],
214
+ """import streamlit as st
215
+
216
+ st.write("Hi There")
217
+ st.title("My App")
218
+ st.write("Hello World")
219
+ st.write("Another message")"""
220
+ )
221
+ ])
222
+ def test_apply_search_replace(original_text, search_replace_pairs, expected_result):
223
+ """Test the apply_search_replace function with various search/replace scenarios."""
224
+ result = apply_search_replace(original_text, search_replace_pairs)
225
+
226
+ print(f"Original text: {repr(original_text)}")
227
+ print(f"Search/replace pairs: {search_replace_pairs}")
228
+ print(f"Expected result: {repr(expected_result)}")
229
+ print(f"Result: {repr(result)}")
230
+
231
+ assert result == expected_result
232
+
233
+
234
+ def test_apply_search_replace_search_not_found():
235
+ """Test that ValueError is raised when search text is not found."""
236
+ with pytest.raises(StreamlitConversionError, match="Search text not found"):
237
+ apply_search_replace("st.title(\"My App\")", [("st.title(\"Not Found\")", "st.title(\"New Title\")")])
238
+
239
+
240
+
@@ -0,0 +1,246 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from typing import List
5
+ from anthropic.types import MessageParam
6
+ import pytest
7
+ import os
8
+ from unittest.mock import patch, AsyncMock, MagicMock
9
+ from mito_ai.streamlit_conversion.streamlit_agent_handler import (
10
+ get_response_from_agent,
11
+ generate_new_streamlit_code,
12
+ correct_error_in_generation,
13
+ streamlit_handler
14
+ )
15
+ from mito_ai.path_utils import AbsoluteNotebookPath, AppFileName, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
16
+
17
+ # Add this line to enable async support
18
+ pytest_plugins = ('pytest_asyncio',)
19
+
20
+
21
+ class TestGetResponseFromAgent:
22
+ """Test cases for get_response_from_agent function"""
23
+
24
+ @pytest.mark.asyncio
25
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
26
+ async def test_get_response_from_agent_success(self, mock_stream):
27
+ """Test get_response_from_agent with successful response"""
28
+ # Mock the async generator
29
+ async def mock_async_gen():
30
+ yield "Here's your code:\n```python\nimport streamlit\nst.title('Test')\n```"
31
+
32
+ mock_stream.return_value = mock_async_gen()
33
+
34
+ messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
35
+ response = await get_response_from_agent(messages)
36
+
37
+ assert response is not None
38
+ assert len(response) > 0
39
+ assert "import streamlit" in response
40
+
41
+ @pytest.mark.asyncio
42
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
43
+ @pytest.mark.parametrize("mock_items,expected_result", [
44
+ (["Hello", " World", "!"], "Hello World!"),
45
+ ([], ""),
46
+ (["Here's your code: import streamlit"], "Here's your code: import streamlit")
47
+ ])
48
+ async def test_get_response_from_agent_parametrized(self, mock_stream, mock_items, expected_result):
49
+ """Test response from agent with different scenarios"""
50
+ # Mock the async generator
51
+ async def mock_async_gen():
52
+ for item in mock_items:
53
+ yield item
54
+
55
+ mock_stream.return_value = mock_async_gen()
56
+
57
+ messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
58
+ result = await get_response_from_agent(messages)
59
+
60
+ assert result == expected_result
61
+ mock_stream.assert_called_once()
62
+
63
+
64
+ @pytest.mark.asyncio
65
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
66
+ async def test_get_response_from_agent_exception(self, mock_stream):
67
+ """Test exception handling in get_response_from_agent"""
68
+ mock_stream.side_effect = Exception("API Error")
69
+
70
+ messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
71
+
72
+ with pytest.raises(Exception, match="API Error"):
73
+ await get_response_from_agent(messages)
74
+
75
+
76
+ class TestGenerateStreamlitCode:
77
+ """Test cases for generate_new_streamlit_code function"""
78
+
79
+ @pytest.mark.asyncio
80
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
81
+ async def test_generate_new_streamlit_code_success(self, mock_stream):
82
+ """Test successful streamlit code generation"""
83
+ mock_response = "Here's your code:\n```python\nimport streamlit\nst.title('Hello')\n```"
84
+
85
+ async def mock_async_gen():
86
+ for item in [mock_response]:
87
+ yield item
88
+
89
+ mock_stream.return_value = mock_async_gen()
90
+
91
+ notebook_data: List[dict] = [{"cells": []}]
92
+ result = await generate_new_streamlit_code(notebook_data)
93
+
94
+ expected_code = "import streamlit\nst.title('Hello')\n"
95
+ assert result == expected_code
96
+
97
+
98
+ class TestCorrectErrorInGeneration:
99
+ """Test cases for correct_error_in_generation function"""
100
+
101
+ @pytest.mark.asyncio
102
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
103
+ async def test_correct_error_in_generation_success(self, mock_stream):
104
+ """Test successful error correction"""
105
+ mock_response = """```search_replace
106
+ >>>>>>> SEARCH
107
+ st.title('Test')
108
+ =======
109
+ st.title('Fixed')
110
+ <<<<<<< REPLACE
111
+ ```"""
112
+ async def mock_async_gen():
113
+ for item in [mock_response]:
114
+ yield item
115
+
116
+ mock_stream.return_value = mock_async_gen()
117
+
118
+ result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')\n")
119
+
120
+ expected_code = "import streamlit\nst.title('Fixed')\n"
121
+ assert result == expected_code
122
+
123
+ @pytest.mark.asyncio
124
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
125
+ async def test_correct_error_in_generation_exception(self, mock_stream):
126
+ """Test exception handling in error correction"""
127
+ mock_stream.side_effect = Exception("API Error")
128
+
129
+ with pytest.raises(Exception, match="API Error"):
130
+ await correct_error_in_generation("Some error", "import streamlit\nst.title('Test')")
131
+
132
+
133
+ class TestStreamlitHandler:
134
+ """Test cases for streamlit_handler function"""
135
+
136
+ @pytest.mark.asyncio
137
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
138
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
139
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
140
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
141
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.log_streamlit_app_conversion_success')
142
+ async def test_streamlit_handler_success(self, mock_log_success, mock_create_file, mock_validator, mock_generate_code, mock_parse):
143
+ """Test successful streamlit handler execution"""
144
+ # Mock notebook parsing
145
+ mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
146
+ mock_parse.return_value = mock_notebook_data
147
+
148
+ # Mock code generation
149
+ mock_generate_code.return_value = "import streamlit\nst.title('Test')"
150
+
151
+ # Mock validation (no errors)
152
+ mock_validator.return_value = []
153
+
154
+ # Use a relative path that will work cross-platform
155
+ notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
156
+ app_file_name = AppFileName('test-app-file-name.py')
157
+
158
+ # Construct the expected app path using the same method as the production code
159
+ app_directory = get_absolute_notebook_dir_path(notebook_path)
160
+ expected_app_path = get_absolute_app_path(app_directory, app_file_name)
161
+ await streamlit_handler(notebook_path, app_file_name)
162
+
163
+ # Verify calls
164
+ mock_parse.assert_called_once_with(notebook_path)
165
+ mock_generate_code.assert_called_once_with(mock_notebook_data)
166
+ mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
167
+ mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
168
+
169
+ @pytest.mark.asyncio
170
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
171
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
172
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.correct_error_in_generation')
173
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
174
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.log_streamlit_app_validation_retry')
175
+ async def test_streamlit_handler_max_retries_exceeded(self, mock_log_retry, mock_validator, mock_correct_error, mock_generate_code, mock_parse):
176
+ """Test streamlit handler when max retries are exceeded"""
177
+ # Mock notebook parsing
178
+ mock_notebook_data: List[dict] = [{"cells": []}]
179
+ mock_parse.return_value = mock_notebook_data
180
+
181
+ # Mock code generation
182
+ mock_generate_code.return_value = "import streamlit\nst.title('Test')"
183
+ mock_correct_error.return_value = "import streamlit\nst.title('Fixed')"
184
+
185
+ # Mock validation (always errors) - validate_app returns List[str]
186
+ mock_validator.return_value = ["Persistent error"]
187
+
188
+ # Now it should raise an exception instead of returning a tuple
189
+ with pytest.raises(Exception):
190
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
191
+
192
+ # Verify that error correction was called 5 times (once per error, 5 retries)
193
+ # Each retry processes 1 error, so 5 retries = 5 calls
194
+ assert mock_correct_error.call_count == 5
195
+
196
+ @pytest.mark.asyncio
197
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
198
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
199
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
200
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
201
+ async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
202
+ """Test streamlit handler when file creation fails"""
203
+ # Mock notebook parsing
204
+ mock_notebook_data: List[dict] = [{"cells": []}]
205
+ mock_parse.return_value = mock_notebook_data
206
+
207
+ # Mock code generation
208
+ mock_generate_code.return_value = "import streamlit\nst.title('Test')"
209
+
210
+ # Mock validation (no errors) - validate_app returns List[str]
211
+ mock_validator.return_value = []
212
+
213
+ # Mock file creation failure - now it should raise an exception
214
+ mock_create_file.side_effect = Exception("Permission denied")
215
+
216
+ # Now it should raise an exception instead of returning a tuple
217
+ with pytest.raises(Exception):
218
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
219
+
220
+ @pytest.mark.asyncio
221
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
222
+ async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
223
+ """Test streamlit handler when notebook parsing fails"""
224
+
225
+ mock_parse.side_effect = FileNotFoundError("Notebook not found")
226
+
227
+ with pytest.raises(FileNotFoundError, match="Notebook not found"):
228
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
229
+
230
+ @pytest.mark.asyncio
231
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
232
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
233
+ async def test_streamlit_handler_generation_exception(self, mock_generate_code, mock_parse):
234
+ """Test streamlit handler when code generation fails"""
235
+ # Mock notebook parsing
236
+ mock_notebook_data: List[dict] = [{"cells": []}]
237
+ mock_parse.return_value = mock_notebook_data
238
+
239
+ # Mock code generation failure
240
+ mock_generate_code.side_effect = Exception("Generation failed")
241
+
242
+ with pytest.raises(Exception, match="Generation failed"):
243
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
244
+
245
+
246
+
@@ -0,0 +1,193 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import pytest
5
+ import json
6
+ import tempfile
7
+ import os
8
+ from unittest.mock import patch, mock_open
9
+ from mito_ai.streamlit_conversion.streamlit_utils import (
10
+ extract_code_blocks,
11
+ create_app_file,
12
+ parse_jupyter_notebook_to_extract_required_content
13
+ )
14
+ from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookDirPath, AbsoluteNotebookPath, get_absolute_notebook_path
15
+ from typing import Dict, Any
16
+
17
+ class TestExtractCodeBlocks:
18
+ """Test cases for extract_code_blocks function"""
19
+
20
+ def test_extract_code_blocks_with_python_blocks(self):
21
+ """Test extracting code from message with python code blocks"""
22
+ message = "Here's some code:\n```python\nimport streamlit\nst.title('Hello')\n```\nThat's it!"
23
+ result = extract_code_blocks(message)
24
+ expected = "import streamlit\nst.title('Hello')\n"
25
+ assert result == expected
26
+
27
+ def test_extract_code_blocks_without_python_blocks(self):
28
+ """Test when message doesn't contain python code blocks"""
29
+ message = "This is just regular text without code blocks"
30
+ result = extract_code_blocks(message)
31
+ assert result == message
32
+
33
+ def test_extract_code_blocks_empty_message(self):
34
+ """Test with empty message"""
35
+ message = ""
36
+ result = extract_code_blocks(message)
37
+ assert result == message
38
+
39
+ def test_extract_code_blocks_multiple_blocks(self):
40
+ """Test extracting from first python block when multiple exist"""
41
+ message = "```python\ncode1\n```\n```python\ncode2\n```"
42
+ result = extract_code_blocks(message)
43
+ expected = "code1\n\ncode2\n"
44
+ assert result == expected
45
+
46
+
47
+ class TestCreateAppFile:
48
+ """Test cases for create_app_file function"""
49
+
50
+ def test_create_app_file_success(self, tmp_path):
51
+ """Test successful file creation"""
52
+ app_path = os.path.join(str(tmp_path), "app.py")
53
+ code = "import streamlit\nst.title('Test')"
54
+
55
+ create_app_file(AbsoluteAppPath(app_path), code)
56
+
57
+ assert app_path is not None
58
+ assert os.path.exists(app_path)
59
+
60
+ # Verify file was created with correct content
61
+ with open(app_path, 'r') as f:
62
+ content = f.read()
63
+ assert content == code
64
+
65
+ def test_create_app_file_io_error(self):
66
+ """Test file creation with IO error"""
67
+ file_path = AbsoluteAppPath("/nonexistent/path/that/should/fail")
68
+ code = "import streamlit"
69
+
70
+ with pytest.raises(Exception):
71
+ create_app_file(file_path, code)
72
+
73
+ @patch('builtins.open', side_effect=Exception("Unexpected error"))
74
+ def test_create_app_file_unexpected_error(self, mock_open):
75
+ """Test file creation with unexpected error"""
76
+ app_path = AbsoluteAppPath("/tmp/test")
77
+ code = "import streamlit"
78
+
79
+ with pytest.raises(Exception, match="Unexpected error"):
80
+ create_app_file(app_path, code)
81
+
82
+ def test_create_app_file_empty_code(self, tmp_path):
83
+ """Test creating file with empty code"""
84
+ app_path = AbsoluteAppPath(os.path.join(str(tmp_path), "app.py"))
85
+ code = ""
86
+
87
+ create_app_file(app_path, code)
88
+
89
+ assert app_path is not None
90
+ assert os.path.exists(app_path)
91
+
92
+ with open(app_path, 'r') as f:
93
+ content = f.read()
94
+ assert content == ""
95
+
96
+
97
+ class TestParseJupyterNotebookToExtractRequiredContent:
98
+ """Test cases for parse_jupyter_notebook_to_extract_required_content function"""
99
+
100
+ def test_parse_valid_notebook(self, tmp_path):
101
+ """Test parsing a valid notebook with cells"""
102
+ notebook_data: Dict[str, Any] = {
103
+ "cells": [
104
+ {
105
+ "cell_type": "code",
106
+ "source": ["import pandas as pd\n", "df = pd.DataFrame()\n"],
107
+ "metadata": {"some": "metadata"},
108
+ "execution_count": 1
109
+ },
110
+ {
111
+ "cell_type": "markdown",
112
+ "source": ["# Title\n", "Some text\n"],
113
+ "metadata": {"another": "metadata"}
114
+ }
115
+ ],
116
+ "metadata": {"notebook_metadata": "value"},
117
+ "nbformat": 4
118
+ }
119
+
120
+ notebook_path = tmp_path / "test.ipynb"
121
+ with open(notebook_path, 'w') as f:
122
+ json.dump(notebook_data, f)
123
+
124
+ absolute_path = get_absolute_notebook_path(str(notebook_path))
125
+ result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
126
+
127
+ # Check that only cell_type and source are preserved
128
+ assert len(result) == 2
129
+ assert result[0]['cell_type'] == 'code'
130
+ assert result[0]['source'] == ["import pandas as pd\n", "df = pd.DataFrame()\n"]
131
+ assert 'metadata' not in result[0]
132
+ assert 'execution_count' not in result[0]
133
+
134
+ assert result[1]['cell_type'] == 'markdown'
135
+ assert result[1]['source'] == ["# Title\n", "Some text\n"]
136
+ assert 'metadata' not in result[1]
137
+
138
+ def test_parse_notebook_file_not_found(self):
139
+ """Test parsing non-existent notebook file"""
140
+ from mito_ai.utils.error_classes import StreamlitConversionError
141
+ with pytest.raises(StreamlitConversionError, match="Notebook file not found"):
142
+ parse_jupyter_notebook_to_extract_required_content(AbsoluteNotebookPath("/nonexistent/path/notebook.ipynb"))
143
+
144
+ def test_parse_notebook_with_missing_cell_fields(self, tmp_path):
145
+ """Test parsing notebook where cells are missing cell_type or source"""
146
+ notebook_data: Dict[str, Any] = {
147
+ "cells": [
148
+ {
149
+ "cell_type": "code"
150
+ # Missing source field
151
+ },
152
+ {
153
+ "source": ["some text"]
154
+ # Missing cell_type field
155
+ },
156
+ {
157
+ "cell_type": "markdown",
158
+ "source": ["# Title"]
159
+ }
160
+ ]
161
+ }
162
+
163
+ notebook_path = tmp_path / "test.ipynb"
164
+ with open(notebook_path, 'w') as f:
165
+ json.dump(notebook_data, f)
166
+
167
+ absolute_path = get_absolute_notebook_path(str(notebook_path))
168
+ result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
169
+
170
+ assert len(result) == 3
171
+ assert result[0]['cell_type'] == 'code'
172
+ assert result[0]['source'] == [] # Default empty list
173
+
174
+ assert result[1]['cell_type'] == '' # Default empty string
175
+ assert result[1]['source'] == ["some text"]
176
+
177
+ assert result[2]['cell_type'] == 'markdown'
178
+ assert result[2]['source'] == ["# Title"]
179
+
180
+ def test_parse_empty_notebook(self, tmp_path):
181
+ """Test parsing notebook with empty cells list"""
182
+ notebook_data: Dict[str, Any] = {
183
+ "cells": []
184
+ }
185
+
186
+ notebook_path = tmp_path / "test.ipynb"
187
+ with open(notebook_path, 'w') as f:
188
+ json.dump(notebook_data, f)
189
+
190
+ absolute_path = get_absolute_notebook_path(str(notebook_path))
191
+ result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
192
+
193
+ assert result == []