mito-ai 0.1.52__py3-none-any.whl → 0.1.54__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.
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +4 -3
- mito_ai/completions/models.py +1 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +10 -7
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +18 -2
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +12 -12
- mito_ai/streamlit_preview/handlers.py +13 -6
- mito_ai/streamlit_preview/manager.py +4 -1
- mito_ai/streamlit_preview/utils.py +4 -4
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +7 -7
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +4 -3
- mito_ai/utils/anthropic_utils.py +28 -3
- mito_ai/utils/tokens.py +29 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -4
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -3
- mito_ai-0.1.52.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5ec1e525d244fc8588cf.js → mito_ai-0.1.54.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.31462f8f6a76b1cefbeb.js +575 -104
- mito_ai-0.1.54.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.31462f8f6a76b1cefbeb.js.map +1 -0
- mito_ai-0.1.52.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.89927e1d3b5962d57ae3.js → mito_ai-0.1.54.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.3f3c98eaba66bf084c66.js +3 -3
- mito_ai-0.1.52.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.89927e1d3b5962d57ae3.js.map → mito_ai-0.1.54.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.3f3c98eaba66bf084c66.js.map +1 -1
- {mito_ai-0.1.52.dist-info → mito_ai-0.1.54.dist-info}/METADATA +1 -1
- {mito_ai-0.1.52.dist-info → mito_ai-0.1.54.dist-info}/RECORD +46 -45
- {mito_ai-0.1.52.dist-info → mito_ai-0.1.54.dist-info}/WHEEL +1 -1
- mito_ai-0.1.52.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5ec1e525d244fc8588cf.js.map +0 -1
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.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
- {mito_ai-0.1.52.data → mito_ai-0.1.54.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
- {mito_ai-0.1.52.data → mito_ai-0.1.54.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
- {mito_ai-0.1.52.data → mito_ai-0.1.54.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
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.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
- {mito_ai-0.1.52.data → mito_ai-0.1.54.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
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.52.dist-info → mito_ai-0.1.54.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.52.dist-info → mito_ai-0.1.54.dist-info}/licenses/LICENSE +0 -0
mito_ai/_version.py
CHANGED
mito_ai/anthropic_client.py
CHANGED
|
@@ -9,12 +9,11 @@ from anthropic.types import Message, MessageParam, TextBlockParam
|
|
|
9
9
|
from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem, MessageType
|
|
10
10
|
from mito_ai.constants import MESSAGE_HISTORY_TRIM_THRESHOLD
|
|
11
11
|
from openai.types.chat import ChatCompletionMessageParam
|
|
12
|
-
from mito_ai.utils.anthropic_utils import get_anthropic_completion_from_mito_server, stream_anthropic_completion_from_mito_server, get_anthropic_completion_function_params
|
|
12
|
+
from mito_ai.utils.anthropic_utils import get_anthropic_completion_from_mito_server, select_correct_model, stream_anthropic_completion_from_mito_server, get_anthropic_completion_function_params
|
|
13
13
|
|
|
14
14
|
# Max tokens is a required parameter for the Anthropic API.
|
|
15
15
|
# We set it to a high number so that we can edit large code cells
|
|
16
|
-
|
|
17
|
-
MAX_TOKENS = 8_000
|
|
16
|
+
MAX_TOKENS = 64_000
|
|
18
17
|
|
|
19
18
|
def extract_and_parse_anthropic_json_response(response: Message) -> Union[object, Any]:
|
|
20
19
|
"""
|
|
@@ -278,6 +277,8 @@ class AnthropicClient:
|
|
|
278
277
|
reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None]) -> str:
|
|
279
278
|
try:
|
|
280
279
|
anthropic_system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
|
|
280
|
+
model = select_correct_model(model, message_type, anthropic_system_prompt, anthropic_messages)
|
|
281
|
+
|
|
281
282
|
accumulated_response = ""
|
|
282
283
|
|
|
283
284
|
if self.api_key:
|
mito_ai/completions/models.py
CHANGED
|
@@ -35,7 +35,7 @@ class AgentResponse(BaseModel):
|
|
|
35
35
|
get_cell_output_cell_id: Optional[str]
|
|
36
36
|
next_steps: Optional[List[str]]
|
|
37
37
|
analysis_assumptions: Optional[List[str]]
|
|
38
|
-
|
|
38
|
+
streamlit_app_prompt: Optional[str]
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@dataclass(frozen=True)
|
|
@@ -231,15 +231,17 @@ When you want to create a new Streamlit app from the current notebook, respond w
|
|
|
231
231
|
|
|
232
232
|
{{
|
|
233
233
|
type: 'create_streamlit_app',
|
|
234
|
+
streamlit_app_prompt: str
|
|
234
235
|
message: str
|
|
235
236
|
}}
|
|
236
237
|
|
|
237
238
|
Important information:
|
|
238
|
-
1. The
|
|
239
|
-
2.
|
|
240
|
-
3.
|
|
241
|
-
4.
|
|
242
|
-
5.
|
|
239
|
+
1. The streamlit_app_prompt is a short description of how the app should be structured. It should be a high level specification that includes things like what fields should be configurable, what tabs should exist, etc. It does not need to be overly detailed however.
|
|
240
|
+
2. The message is a short summary of why you're creating the Streamlit app.
|
|
241
|
+
3. Only use this tool when the user explicitly asks to create or preview a Streamlit app. If the streamlit app for this app already exists, then use an empty string '' as the streamlit_app_prompt.
|
|
242
|
+
4. This tool creates a new app from scratch - use EDIT_STREAMLIT_APP tool if the user is asking you to edit, update, or modify an app that already exists.
|
|
243
|
+
5. Using this tool will automatically open the app so the user can see a preview of the app. If the user is asking you to open an app that already exists, but not make any changes to the app, this is the correct tool.
|
|
244
|
+
6. When you use this tool, assume that it successfully created the Streamlit app unless the user explicitly tells you otherwise. The app will remain open so that the user can view it until the user decides to close it. You do not need to continually use the create_streamlit_app tool to keep the app open.
|
|
243
245
|
|
|
244
246
|
<Example>
|
|
245
247
|
|
|
@@ -248,6 +250,7 @@ Your task: Show me my notebook as an app.
|
|
|
248
250
|
Output:
|
|
249
251
|
{{
|
|
250
252
|
type: 'create_streamlit_app',
|
|
253
|
+
streamlit_app_prompt: "The app should have a beginning date and end date input field at the top. It should then be followed by two tabs for the user to select between: current performance and projected performance.",
|
|
251
254
|
message: "I'll convert your notebook into an app."
|
|
252
255
|
}}
|
|
253
256
|
|
|
@@ -264,12 +267,12 @@ When you want to edit an existing Streamlit app, respond with this format:
|
|
|
264
267
|
{{
|
|
265
268
|
type: 'edit_streamlit_app',
|
|
266
269
|
message: str,
|
|
267
|
-
|
|
270
|
+
streamlit_app_prompt: str
|
|
268
271
|
}}
|
|
269
272
|
|
|
270
273
|
Important information:
|
|
271
274
|
1. The message is a short summary of why you're editing the Streamlit app.
|
|
272
|
-
2. The
|
|
275
|
+
2. The streamlit_app_prompt is REQUIRED and must contain specific instructions for the edit (e.g., "Make the title text larger", "Change the chart colors to blue", "Add a sidebar with filters").
|
|
273
276
|
3. Only use this tool when the user asks to edit, update, or modify a Streamlit app.
|
|
274
277
|
4. The app does not need to already be open for you to use the tool. Using this tool will automatically open the streamlit app after applying the changes so the user can view it. You do not need to call the create_streamlit_app tool first.
|
|
275
278
|
5. When you use this tool, assume that it successfully edited the Streamlit app unless the user explicitly tells you otherwise. The app will remain open so that the user can view it until the user decides to close it.
|
|
@@ -4,10 +4,24 @@
|
|
|
4
4
|
from typing import List
|
|
5
5
|
from mito_ai.streamlit_conversion.prompts.prompt_constants import MITO_TODO_PLACEHOLDER
|
|
6
6
|
|
|
7
|
-
def
|
|
7
|
+
def get_streamlit_app_spec_section(streamlit_app_prompt: str) -> str:
|
|
8
|
+
if streamlit_app_prompt == '':
|
|
9
|
+
return ''
|
|
10
|
+
|
|
11
|
+
return f"""
|
|
12
|
+
Here is a high level outline of the streamlit app. Use your best judgement to implement this structure.
|
|
13
|
+
|
|
14
|
+
{streamlit_app_prompt}
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_streamlit_app_creation_prompt(notebook: List[dict], streamlit_app_prompt: str) -> str:
|
|
8
20
|
"""
|
|
9
21
|
This prompt is used to create a streamlit app from a notebook.
|
|
10
22
|
"""
|
|
23
|
+
streamlit_app_spec_section = get_streamlit_app_spec_section(streamlit_app_prompt)
|
|
24
|
+
|
|
11
25
|
return f"""Convert the following Jupyter notebook into a Streamlit application.
|
|
12
26
|
|
|
13
27
|
GOAL: Create a complete, runnable Streamlit app that accurately represents the notebook. It must completely convert the notebook.
|
|
@@ -40,7 +54,9 @@ data = [
|
|
|
40
54
|
]
|
|
41
55
|
</Example>
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
{streamlit_app_spec_section}
|
|
58
|
+
|
|
59
|
+
NOTEBOOK TO CONVERT:
|
|
44
60
|
|
|
45
61
|
{notebook}
|
|
46
62
|
"""
|
|
@@ -16,10 +16,10 @@ from mito_ai.utils.error_classes import StreamlitConversionError
|
|
|
16
16
|
from mito_ai.utils.telemetry_utils import log_streamlit_app_validation_retry, log_streamlit_app_conversion_success
|
|
17
17
|
from mito_ai.path_utils import AbsoluteNotebookPath, AppFileName, get_absolute_notebook_dir_path, get_absolute_app_path, get_app_file_name
|
|
18
18
|
|
|
19
|
-
async def generate_new_streamlit_code(notebook: List[dict]) -> str:
|
|
19
|
+
async def generate_new_streamlit_code(notebook: List[dict], streamlit_app_prompt: str) -> str:
|
|
20
20
|
"""Send a query to the agent, get its response and parse the code"""
|
|
21
21
|
|
|
22
|
-
prompt_text = get_streamlit_app_creation_prompt(notebook)
|
|
22
|
+
prompt_text = get_streamlit_app_creation_prompt(notebook, streamlit_app_prompt)
|
|
23
23
|
|
|
24
24
|
messages: List[MessageParam] = [
|
|
25
25
|
cast(MessageParam, {
|
|
@@ -100,7 +100,7 @@ async def correct_error_in_generation(error: str, streamlit_app_code: str) -> st
|
|
|
100
100
|
|
|
101
101
|
return streamlit_app_code
|
|
102
102
|
|
|
103
|
-
async def streamlit_handler(notebook_path: AbsoluteNotebookPath, app_file_name: AppFileName,
|
|
103
|
+
async def streamlit_handler(create_new_app: bool, notebook_path: AbsoluteNotebookPath, app_file_name: AppFileName, streamlit_app_prompt: str = "") -> None:
|
|
104
104
|
"""Handler function for streamlit code generation and validation"""
|
|
105
105
|
|
|
106
106
|
# Convert to absolute path for consistent handling
|
|
@@ -108,22 +108,22 @@ async def streamlit_handler(notebook_path: AbsoluteNotebookPath, app_file_name:
|
|
|
108
108
|
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
109
109
|
app_path = get_absolute_app_path(app_directory, app_file_name)
|
|
110
110
|
|
|
111
|
-
if
|
|
111
|
+
if create_new_app:
|
|
112
|
+
# Otherwise generate a new streamlit app
|
|
113
|
+
streamlit_code = await generate_new_streamlit_code(notebook_code, streamlit_app_prompt)
|
|
114
|
+
else:
|
|
112
115
|
# If the user is editing an existing streamlit app, use the update function
|
|
113
|
-
|
|
116
|
+
existing_streamlit_code = get_app_code_from_file(app_path)
|
|
114
117
|
|
|
115
|
-
if
|
|
118
|
+
if existing_streamlit_code is None:
|
|
116
119
|
raise StreamlitConversionError("Error updating existing streamlit app because app.py file was not found.", 404)
|
|
117
120
|
|
|
118
|
-
streamlit_code = await update_existing_streamlit_code(notebook_code,
|
|
119
|
-
else:
|
|
120
|
-
# Otherwise generate a new streamlit app
|
|
121
|
-
streamlit_code = await generate_new_streamlit_code(notebook_code)
|
|
121
|
+
streamlit_code = await update_existing_streamlit_code(notebook_code, existing_streamlit_code, streamlit_app_prompt)
|
|
122
122
|
|
|
123
123
|
# Then, after creating/updating the app, validate that the new code runs
|
|
124
124
|
errors = validate_app(streamlit_code, notebook_path)
|
|
125
125
|
tries = 0
|
|
126
|
-
while len(errors)>0 and tries < 5:
|
|
126
|
+
while len(errors) > 0 and tries < 5:
|
|
127
127
|
for error in errors:
|
|
128
128
|
streamlit_code = await correct_error_in_generation(error, streamlit_code)
|
|
129
129
|
|
|
@@ -141,4 +141,4 @@ async def streamlit_handler(notebook_path: AbsoluteNotebookPath, app_file_name:
|
|
|
141
141
|
|
|
142
142
|
# Finally, update the app.py file with the new code
|
|
143
143
|
create_app_file(app_path, streamlit_code)
|
|
144
|
-
log_streamlit_app_conversion_success('mito_server_key', MessageType.STREAMLIT_CONVERSION,
|
|
144
|
+
log_streamlit_app_conversion_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, streamlit_app_prompt)
|
|
@@ -22,12 +22,14 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
22
22
|
self.preview_manager = StreamlitPreviewManager()
|
|
23
23
|
|
|
24
24
|
@tornado.web.authenticated
|
|
25
|
+
|
|
25
26
|
async def post(self) -> None:
|
|
26
27
|
"""Start a new streamlit preview."""
|
|
27
28
|
try:
|
|
29
|
+
|
|
28
30
|
# Parse and validate request
|
|
29
31
|
body = self.get_json_body()
|
|
30
|
-
notebook_path, notebook_id, force_recreate,
|
|
32
|
+
notebook_path, notebook_id, force_recreate, streamlit_app_prompt = validate_request_body(body)
|
|
31
33
|
|
|
32
34
|
# Ensure app exists
|
|
33
35
|
absolute_notebook_path = get_absolute_notebook_path(notebook_path)
|
|
@@ -35,14 +37,19 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
35
37
|
app_file_name = get_app_file_name(notebook_id)
|
|
36
38
|
absolute_app_path = get_absolute_app_path(absolute_notebook_dir_path, app_file_name)
|
|
37
39
|
app_path_exists = does_app_path_exist(absolute_app_path)
|
|
38
|
-
|
|
40
|
+
|
|
39
41
|
if not app_path_exists or force_recreate:
|
|
40
42
|
if not app_path_exists:
|
|
41
43
|
print("[Mito AI] App path not found, generating streamlit code")
|
|
42
44
|
else:
|
|
43
45
|
print("[Mito AI] Force recreating streamlit app")
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
# Create a new app
|
|
48
|
+
await streamlit_handler(True, absolute_notebook_path, app_file_name, streamlit_app_prompt)
|
|
49
|
+
elif streamlit_app_prompt != '':
|
|
50
|
+
# Update an existing app if there is a prompt provided. Otherwise, the user is just
|
|
51
|
+
# starting an existing app so we can skip the streamlit_handler all together
|
|
52
|
+
await streamlit_handler(False, absolute_notebook_path, app_file_name, streamlit_app_prompt)
|
|
46
53
|
|
|
47
54
|
# Start preview
|
|
48
55
|
# TODO: There's a bug here where when the user rebuilds and already running app. Instead of
|
|
@@ -58,7 +65,7 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
58
65
|
"port": port,
|
|
59
66
|
"url": f"http://localhost:{port}"
|
|
60
67
|
})
|
|
61
|
-
log_streamlit_app_preview_success('mito_server_key', MessageType.STREAMLIT_CONVERSION,
|
|
68
|
+
log_streamlit_app_preview_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, streamlit_app_prompt)
|
|
62
69
|
|
|
63
70
|
except StreamlitConversionError as e:
|
|
64
71
|
print(e)
|
|
@@ -71,7 +78,7 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
71
78
|
MessageType.STREAMLIT_CONVERSION,
|
|
72
79
|
error_message,
|
|
73
80
|
formatted_traceback,
|
|
74
|
-
|
|
81
|
+
streamlit_app_prompt,
|
|
75
82
|
)
|
|
76
83
|
except StreamlitPreviewError as e:
|
|
77
84
|
print(e)
|
|
@@ -79,7 +86,7 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
79
86
|
formatted_traceback = traceback.format_exc()
|
|
80
87
|
self.set_status(e.error_code)
|
|
81
88
|
self.finish({"error": error_message})
|
|
82
|
-
log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback,
|
|
89
|
+
log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, streamlit_app_prompt)
|
|
83
90
|
except Exception as e:
|
|
84
91
|
print(f"Exception in streamlit preview handler: {e}")
|
|
85
92
|
self.set_status(500)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import socket
|
|
5
5
|
import subprocess
|
|
6
|
+
import sys
|
|
6
7
|
import time
|
|
7
8
|
import threading
|
|
8
9
|
import requests
|
|
@@ -54,8 +55,10 @@ class StreamlitPreviewManager:
|
|
|
54
55
|
port = self.get_free_port()
|
|
55
56
|
|
|
56
57
|
# Start streamlit process
|
|
58
|
+
# Use sys.executable -m streamlit to ensure it works on Windows
|
|
59
|
+
# where streamlit may not be directly executable in PATH
|
|
57
60
|
cmd = [
|
|
58
|
-
"streamlit", "run", app_file_name,
|
|
61
|
+
sys.executable, "-m", "streamlit", "run", app_file_name,
|
|
59
62
|
"--server.port", str(port),
|
|
60
63
|
"--server.headless", "true",
|
|
61
64
|
"--server.address", "localhost",
|
|
@@ -22,8 +22,8 @@ def validate_request_body(body: Optional[dict]) -> Tuple[str, str, bool, str]:
|
|
|
22
22
|
if not isinstance(force_recreate, bool):
|
|
23
23
|
raise StreamlitPreviewError("force_recreate must be a boolean", 400)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
if not isinstance(
|
|
27
|
-
raise StreamlitPreviewError("
|
|
25
|
+
streamlit_app_prompt = body.get("streamlit_app_prompt", "")
|
|
26
|
+
if not isinstance(streamlit_app_prompt, str):
|
|
27
|
+
raise StreamlitPreviewError("streamlit_app_prompt must be a string", 400)
|
|
28
28
|
|
|
29
|
-
return notebook_path, notebook_id, force_recreate,
|
|
29
|
+
return notebook_path, notebook_id, force_recreate, streamlit_app_prompt
|
|
@@ -89,7 +89,7 @@ class TestGenerateStreamlitCode:
|
|
|
89
89
|
mock_stream.return_value = mock_async_gen()
|
|
90
90
|
|
|
91
91
|
notebook_data: List[dict] = [{"cells": []}]
|
|
92
|
-
result = await generate_new_streamlit_code(notebook_data)
|
|
92
|
+
result = await generate_new_streamlit_code(notebook_data, '')
|
|
93
93
|
|
|
94
94
|
expected_code = "import streamlit\nst.title('Hello')\n"
|
|
95
95
|
assert result == expected_code
|
|
@@ -158,11 +158,11 @@ class TestStreamlitHandler:
|
|
|
158
158
|
# Construct the expected app path using the same method as the production code
|
|
159
159
|
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
160
160
|
expected_app_path = get_absolute_app_path(app_directory, app_file_name)
|
|
161
|
-
await streamlit_handler(notebook_path, app_file_name)
|
|
161
|
+
await streamlit_handler(True, notebook_path, app_file_name)
|
|
162
162
|
|
|
163
163
|
# Verify calls
|
|
164
164
|
mock_parse.assert_called_once_with(notebook_path)
|
|
165
|
-
mock_generate_code.assert_called_once_with(mock_notebook_data)
|
|
165
|
+
mock_generate_code.assert_called_once_with(mock_notebook_data, '')
|
|
166
166
|
mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
|
|
167
167
|
mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
|
|
168
168
|
|
|
@@ -187,7 +187,7 @@ class TestStreamlitHandler:
|
|
|
187
187
|
|
|
188
188
|
# Now it should raise an exception instead of returning a tuple
|
|
189
189
|
with pytest.raises(Exception):
|
|
190
|
-
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
190
|
+
await streamlit_handler(True, AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'), '')
|
|
191
191
|
|
|
192
192
|
# Verify that error correction was called 5 times (once per error, 5 retries)
|
|
193
193
|
# Each retry processes 1 error, so 5 retries = 5 calls
|
|
@@ -215,7 +215,7 @@ class TestStreamlitHandler:
|
|
|
215
215
|
|
|
216
216
|
# Now it should raise an exception instead of returning a tuple
|
|
217
217
|
with pytest.raises(Exception):
|
|
218
|
-
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
218
|
+
await streamlit_handler(True, AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'), '')
|
|
219
219
|
|
|
220
220
|
@pytest.mark.asyncio
|
|
221
221
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
@@ -225,7 +225,7 @@ class TestStreamlitHandler:
|
|
|
225
225
|
mock_parse.side_effect = FileNotFoundError("Notebook not found")
|
|
226
226
|
|
|
227
227
|
with pytest.raises(FileNotFoundError, match="Notebook not found"):
|
|
228
|
-
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
228
|
+
await streamlit_handler(True, AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'), '')
|
|
229
229
|
|
|
230
230
|
@pytest.mark.asyncio
|
|
231
231
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
@@ -240,7 +240,7 @@ class TestStreamlitHandler:
|
|
|
240
240
|
mock_generate_code.side_effect = Exception("Generation failed")
|
|
241
241
|
|
|
242
242
|
with pytest.raises(Exception, match="Generation failed"):
|
|
243
|
-
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
243
|
+
await streamlit_handler(True, AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'), '')
|
|
244
244
|
|
|
245
245
|
|
|
246
246
|
|
|
@@ -99,9 +99,10 @@ class TestStreamlitPreviewHandler:
|
|
|
99
99
|
assert mock_streamlit_handler.called
|
|
100
100
|
# Verify it was called with the correct arguments
|
|
101
101
|
call_args = mock_streamlit_handler.call_args
|
|
102
|
-
assert call_args[0][0] ==
|
|
103
|
-
assert call_args[0][1] ==
|
|
104
|
-
assert call_args[0][2] ==
|
|
102
|
+
assert call_args[0][0] == True
|
|
103
|
+
assert call_args[0][1] == os.path.abspath(notebook_path) # First argument should be the absolute notebook path
|
|
104
|
+
assert call_args[0][2] == app_file_name # Second argument should be the app file name
|
|
105
|
+
assert call_args[0][3] == "" # Third argument should be the edit_prompt
|
|
105
106
|
else:
|
|
106
107
|
mock_streamlit_handler.assert_not_called()
|
|
107
108
|
|
mito_ai/utils/anthropic_utils.py
CHANGED
|
@@ -10,6 +10,7 @@ from mito_ai.completions.models import AgentResponse, MessageType, ResponseForma
|
|
|
10
10
|
from mito_ai.utils.schema import UJ_STATIC_USER_ID, UJ_USER_EMAIL
|
|
11
11
|
from mito_ai.utils.db import get_user_field
|
|
12
12
|
from mito_ai.constants import MITO_ANTHROPIC_URL
|
|
13
|
+
from mito_ai.utils.tokens import get_rough_token_estimatation_anthropic
|
|
13
14
|
|
|
14
15
|
__user_email: Optional[str] = None
|
|
15
16
|
__user_id: Optional[str] = None
|
|
@@ -17,7 +18,29 @@ __user_id: Optional[str] = None
|
|
|
17
18
|
ANTHROPIC_TIMEOUT = 60
|
|
18
19
|
max_retries = 1
|
|
19
20
|
|
|
20
|
-
FAST_ANTHROPIC_MODEL = "claude-
|
|
21
|
+
FAST_ANTHROPIC_MODEL = "claude-haiku-4-5-20251001" # This should be in sync with ModelSelector.tsx
|
|
22
|
+
LARGE_CONTEXT_MODEL = "claude-sonnet-4-5-20250929" # This should be in sync with ModelSelector.tsx
|
|
23
|
+
|
|
24
|
+
def does_message_exceed_max_tokens(system: Union[str, List[TextBlockParam], anthropic.Omit], messages: List[MessageParam]) -> bool:
|
|
25
|
+
token_estimation = get_rough_token_estimatation_anthropic(system, messages)
|
|
26
|
+
|
|
27
|
+
if token_estimation is not None and token_estimation > 200_000:
|
|
28
|
+
return True
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def select_correct_model(default_model: str, message_type: MessageType, system: Union[str, List[TextBlockParam], anthropic.Omit], messages: List[MessageParam]) -> str:
|
|
32
|
+
|
|
33
|
+
message_exceeds_fast_model_context_limit = does_message_exceed_max_tokens(system, messages)
|
|
34
|
+
if message_exceeds_fast_model_context_limit:
|
|
35
|
+
# Anthropic lets us use beta mode to extend context window for sonnet class models
|
|
36
|
+
# but not haiku models
|
|
37
|
+
return LARGE_CONTEXT_MODEL
|
|
38
|
+
|
|
39
|
+
message_requires_fast_model = does_message_require_fast_model(message_type)
|
|
40
|
+
if message_requires_fast_model:
|
|
41
|
+
return FAST_ANTHROPIC_MODEL
|
|
42
|
+
|
|
43
|
+
return default_model
|
|
21
44
|
|
|
22
45
|
def _prepare_anthropic_request_data_and_headers(
|
|
23
46
|
model: Union[str, None],
|
|
@@ -36,6 +59,7 @@ def _prepare_anthropic_request_data_and_headers(
|
|
|
36
59
|
__user_email = get_user_field(UJ_USER_EMAIL)
|
|
37
60
|
if __user_id is None:
|
|
38
61
|
__user_id = get_user_field(UJ_STATIC_USER_ID)
|
|
62
|
+
|
|
39
63
|
# Build the inner data dict (excluding timeout, max_retries, email, user_id)
|
|
40
64
|
inner_data: Dict[str, Any] = {
|
|
41
65
|
"model": model,
|
|
@@ -44,6 +68,7 @@ def _prepare_anthropic_request_data_and_headers(
|
|
|
44
68
|
"messages": messages,
|
|
45
69
|
"betas": ["context-1m-2025-08-07"]
|
|
46
70
|
}
|
|
71
|
+
|
|
47
72
|
# Add system to inner_data only if it is not anthropic.Omit
|
|
48
73
|
if not isinstance(system, anthropic.Omit):
|
|
49
74
|
inner_data["system"] = system
|
|
@@ -139,8 +164,7 @@ def get_anthropic_completion_function_params(
|
|
|
139
164
|
Only includes fields needed for the Anthropic API.
|
|
140
165
|
"""
|
|
141
166
|
|
|
142
|
-
|
|
143
|
-
model = FAST_ANTHROPIC_MODEL if message_requires_fast_model else model
|
|
167
|
+
model = select_correct_model(model, message_type, system, messages)
|
|
144
168
|
|
|
145
169
|
provider_data = {
|
|
146
170
|
"model": model,
|
|
@@ -166,3 +190,4 @@ def get_anthropic_completion_function_params(
|
|
|
166
190
|
provider_data["stream"] = stream
|
|
167
191
|
# Optionally handle response_format_info if Anthropic supports it in the future
|
|
168
192
|
return provider_data
|
|
193
|
+
|
mito_ai/utils/tokens.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from typing import List, Union, Optional
|
|
5
|
+
import anthropic
|
|
6
|
+
from anthropic.types import MessageParam, TextBlockParam, ToolUnionParam
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_rough_token_estimatation_anthropic(system_message: Union[str, List[TextBlockParam], anthropic.Omit], messages: List[MessageParam]) -> Optional[float]:
|
|
10
|
+
"""
|
|
11
|
+
Get a very rough estimation of the number of tokens in a conversation.
|
|
12
|
+
We bias towards overestimating to make sure we don't accidentally
|
|
13
|
+
think a conversation is safe to send to an AI without having applied an
|
|
14
|
+
optimization strategy.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
stringified_system_message = str(system_message)
|
|
19
|
+
stringified_messages = str(messages)
|
|
20
|
+
total_stringified_context = stringified_system_message + stringified_messages
|
|
21
|
+
|
|
22
|
+
# The general rule of thumb is: 1 token is about 4 characters.
|
|
23
|
+
# To be safe we use: 1 token is about 3 characters
|
|
24
|
+
# This helps make sure we always overestimate
|
|
25
|
+
return len(total_stringified_context) / 3
|
|
26
|
+
|
|
27
|
+
except:
|
|
28
|
+
return None
|
|
29
|
+
|
{mito_ai-0.1.52.data → mito_ai-0.1.54.data}/data/share/jupyter/labextensions/mito_ai/package.json
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mito_ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
4
4
|
"description": "AI chat for JupyterLab",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
35
35
|
"build:labextension": "jupyter labextension build .",
|
|
36
36
|
"build:labextension:dev": "jupyter labextension build --development True .",
|
|
37
|
-
"build:lib": "
|
|
38
|
-
"build:lib:prod": "
|
|
37
|
+
"build:lib": "rimraf buildcache && npx tsc --sourceMap",
|
|
38
|
+
"build:lib:prod": "rimraf buildcache && npx tsc",
|
|
39
39
|
"clean": "jlpm clean:lib",
|
|
40
40
|
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
41
41
|
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
"outputDir": "mito_ai/labextension",
|
|
141
141
|
"schemaDir": "schema",
|
|
142
142
|
"_build": {
|
|
143
|
-
"load": "static/remoteEntry.
|
|
143
|
+
"load": "static/remoteEntry.3f3c98eaba66bf084c66.js",
|
|
144
144
|
"extension": "./extension",
|
|
145
145
|
"style": "./style"
|
|
146
146
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mito_ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
4
4
|
"description": "AI chat for JupyterLab",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
35
35
|
"build:labextension": "jupyter labextension build .",
|
|
36
36
|
"build:labextension:dev": "jupyter labextension build --development True .",
|
|
37
|
-
"build:lib": "
|
|
38
|
-
"build:lib:prod": "
|
|
37
|
+
"build:lib": "rimraf buildcache && npx tsc --sourceMap",
|
|
38
|
+
"build:lib:prod": "rimraf buildcache && npx tsc",
|
|
39
39
|
"clean": "jlpm clean:lib",
|
|
40
40
|
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
41
41
|
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|