mito-ai 0.1.59__py3-none-any.whl → 0.1.60__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/completions/prompt_builders/agent_system_message.py +7 -1
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +15 -0
- mito_ai/rules/handlers.py +46 -12
- mito_ai/rules/utils.py +170 -6
- mito_ai/tests/providers/test_providers.py +5 -5
- mito_ai/tests/rules/rules_test.py +100 -4
- mito_ai/utils/anthropic_utils.py +1 -2
- mito_ai/utils/model_utils.py +0 -2
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.44c109c7be36fb884d25.js → mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.dccfa541c464ee0e5cd4.js +676 -106
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.dccfa541c464ee0e5cd4.js.map +1 -0
- mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js → mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_css-loader_dist_cjs_js_style_base_css.3594c54c9d209e1ed56e.js +2 -460
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_css-loader_dist_cjs_js_style_base_css.3594c54c9d209e1ed56e.js.map +1 -0
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_css-loader_dist_runtime_api_js-node_modules_css-loader_dist_runtime_sourceMaps_j-49e54d.3972dd8e7542bba478ad.js +463 -0
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_css-loader_dist_runtime_api_js-node_modules_css-loader_dist_runtime_sourceMaps_j-49e54d.3972dd8e7542bba478ad.js.map +1 -0
- mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.f7decebaf69618541e0f.js → mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9735d9bfc8891147fee0.js +6 -6
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9735d9bfc8891147fee0.js.map +1 -0
- {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/METADATA +1 -1
- {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/RECORD +47 -45
- mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.44c109c7be36fb884d25.js.map +0 -1
- mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.f7decebaf69618541e0f.js.map +0 -1
- mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js.map +0 -1
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.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.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.css +0 -0
- {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.js +0 -0
- {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/licenses/LICENSE +0 -0
mito_ai/_version.py
CHANGED
|
@@ -10,6 +10,7 @@ from mito_ai.completions.prompt_builders.prompt_constants import (
|
|
|
10
10
|
get_database_rules
|
|
11
11
|
)
|
|
12
12
|
from mito_ai.completions.prompt_builders.prompt_section_registry.base import PromptSection
|
|
13
|
+
from mito_ai.rules.utils import get_default_rules_content
|
|
13
14
|
|
|
14
15
|
def create_agent_system_message_prompt(isChromeBrowser: bool) -> str:
|
|
15
16
|
|
|
@@ -474,7 +475,12 @@ Important information:
|
|
|
474
475
|
|
|
475
476
|
# Database rules
|
|
476
477
|
sections.append(SG.Generic("Database Rules", get_database_rules()))
|
|
477
|
-
|
|
478
|
+
|
|
479
|
+
# Default rules
|
|
480
|
+
default_rules = get_default_rules_content()
|
|
481
|
+
if default_rules:
|
|
482
|
+
sections.append(SG.Generic("Default (User Defined) Rules", default_rules))
|
|
483
|
+
|
|
478
484
|
# RULES OF YOUR WORKING PROCESS
|
|
479
485
|
sections.append(SG.Generic("Rules Of Working Process", f"""The user is going to ask you to guide them as through the process of completing a task. You will help them complete a task over the course of an entire conversation with them. The user will first share with you what they want to accomplish. You will then use a tool to execute the first step of the task, they will execute the tool and return to you the updated notebook state with you, and then you will give them the next step of the task. You will continue to give them the next step of the task until they have completed the task.
|
|
480
486
|
|
|
@@ -11,6 +11,7 @@ from mito_ai.completions.prompt_builders.prompt_constants import (
|
|
|
11
11
|
get_database_rules
|
|
12
12
|
)
|
|
13
13
|
from mito_ai.completions.prompt_builders.prompt_section_registry.base import PromptSection
|
|
14
|
+
from mito_ai.rules.utils import get_default_rules_content
|
|
14
15
|
|
|
15
16
|
def create_chat_system_message_prompt() -> str:
|
|
16
17
|
sections: List[PromptSection] = []
|
|
@@ -34,6 +35,9 @@ Other useful information:
|
|
|
34
35
|
|
|
35
36
|
sections.append(SG.Generic("Chart Config Rules", CHART_CONFIG_RULES))
|
|
36
37
|
sections.append(SG.Generic("DatabaseRules", get_database_rules()))
|
|
38
|
+
default_rules = get_default_rules_content()
|
|
39
|
+
if default_rules:
|
|
40
|
+
sections.append(SG.Generic("Default (User Defined) Rules", default_rules))
|
|
37
41
|
sections.append(SG.Generic("Citation Rules", CITATION_RULES))
|
|
38
42
|
sections.append(SG.Generic("Cell Reference Rules", CELL_REFERENCE_RULES))
|
|
39
43
|
|
|
@@ -26,6 +26,17 @@ Rules:
|
|
|
26
26
|
- NEVER include comments on the same line as a variable assignment. Each variable assignment must be on its own line with no trailing comments.
|
|
27
27
|
- For string values, use either single or double quotes (e.g., TITLE = "Sales by Product" or TITLE = 'Sales by Product'). Do not use nested quotes (e.g., do NOT use '"value"').
|
|
28
28
|
|
|
29
|
+
Fixed acceptable ranges (matplotlib constraints):
|
|
30
|
+
- For numeric variables that have a fixed acceptable range, add a line immediately BEFORE the variable assignment: # RANGE VARIABLE_NAME MIN MAX
|
|
31
|
+
- This allows the Chart Wizard to clamp inputs and prevent invalid values. Use the following ranges when you use these variables:
|
|
32
|
+
- ALPHA (opacity): 0 1
|
|
33
|
+
- FIGURE_SIZE (tuple width, height in inches): 1 24 (each element)
|
|
34
|
+
- LINE_WIDTH, LINEWIDTH, LWD: 0 20
|
|
35
|
+
- FONT_SIZE, FONTSIZE, FONT_SIZE_TITLE, FONT_SIZE_LABEL: 0.1 72
|
|
36
|
+
- MARKER_SIZE, MARKERSIZE, S: 0 1000
|
|
37
|
+
- DPI: 1 600
|
|
38
|
+
- Any other numeric or tuple variable that you know has matplotlib constraints: add # RANGE VARIABLE_NAME MIN MAX with the appropriate min and max.
|
|
39
|
+
|
|
29
40
|
Common Mistakes to Avoid:
|
|
30
41
|
- WRONG: COLOR = '"#1877F2" # Meta Blue' (nested quotes and inline comment)
|
|
31
42
|
- WRONG: COLOR = "#1877F2" # Meta Blue (inline comment)
|
|
@@ -38,6 +49,10 @@ TITLE = "Sales by Product"
|
|
|
38
49
|
X_LABEL = "Product"
|
|
39
50
|
Y_LABEL = "Sales"
|
|
40
51
|
BAR_COLOR = "#000000"
|
|
52
|
+
# RANGE ALPHA 0 1
|
|
53
|
+
ALPHA = 0.8
|
|
54
|
+
# RANGE FIGURE_SIZE 1 24
|
|
55
|
+
FIGURE_SIZE = (12, 6)
|
|
41
56
|
# === END CONFIG ===
|
|
42
57
|
"""
|
|
43
58
|
|
mito_ai/rules/handlers.py
CHANGED
|
@@ -7,7 +7,16 @@ from typing import Any, Final, Union
|
|
|
7
7
|
import tornado
|
|
8
8
|
import os
|
|
9
9
|
from jupyter_server.base.handlers import APIHandler
|
|
10
|
-
from mito_ai.rules.utils import
|
|
10
|
+
from mito_ai.rules.utils import (
|
|
11
|
+
RULES_DIR_PATH,
|
|
12
|
+
cleanup_rules_metadata,
|
|
13
|
+
delete_rule,
|
|
14
|
+
get_all_rules,
|
|
15
|
+
get_rule,
|
|
16
|
+
get_rule_default,
|
|
17
|
+
set_rule_default,
|
|
18
|
+
set_rules_file,
|
|
19
|
+
)
|
|
11
20
|
|
|
12
21
|
|
|
13
22
|
class RulesHandler(APIHandler):
|
|
@@ -17,17 +26,26 @@ class RulesHandler(APIHandler):
|
|
|
17
26
|
def get(self, key: Union[str, None] = None) -> None:
|
|
18
27
|
"""Get a specific rule by key or all rules if no key provided"""
|
|
19
28
|
if key is None or key == '':
|
|
20
|
-
# No key provided, return all rules
|
|
21
|
-
|
|
29
|
+
# No key provided, return all rules with is_default flag
|
|
30
|
+
rule_files = get_all_rules()
|
|
31
|
+
rules = [
|
|
32
|
+
{"name": name, "is_default": get_rule_default(name)}
|
|
33
|
+
for name in rule_files
|
|
34
|
+
]
|
|
22
35
|
self.finish(json.dumps(rules))
|
|
23
36
|
else:
|
|
24
37
|
# Key provided, return specific rule
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
try:
|
|
39
|
+
rule_content = get_rule(key)
|
|
40
|
+
if rule_content is None:
|
|
41
|
+
self.set_status(404)
|
|
42
|
+
self.finish(json.dumps({"error": f"Rule with key '{key}' not found"}))
|
|
43
|
+
else:
|
|
44
|
+
is_default = get_rule_default(key)
|
|
45
|
+
self.finish(json.dumps({"key": key, "content": rule_content, "is_default": is_default}))
|
|
46
|
+
except ValueError as e:
|
|
47
|
+
self.set_status(400)
|
|
48
|
+
self.finish(json.dumps({"error": str(e)}))
|
|
31
49
|
|
|
32
50
|
@tornado.web.authenticated
|
|
33
51
|
def put(self, key: str) -> None:
|
|
@@ -37,8 +55,24 @@ class RulesHandler(APIHandler):
|
|
|
37
55
|
self.set_status(400)
|
|
38
56
|
self.finish(json.dumps({"error": "Content is required"}))
|
|
39
57
|
return
|
|
40
|
-
|
|
41
|
-
set_rules_file(key, data['content'])
|
|
42
|
-
self.finish(json.dumps({"status": "updated", "rules file ": key}))
|
|
43
58
|
|
|
59
|
+
try:
|
|
60
|
+
set_rules_file(key, data['content'])
|
|
61
|
+
if 'is_default' in data:
|
|
62
|
+
set_rule_default(key, bool(data['is_default']))
|
|
63
|
+
cleanup_rules_metadata()
|
|
64
|
+
self.finish(json.dumps({"status": "updated", "rules_file": key}))
|
|
65
|
+
except ValueError as e:
|
|
66
|
+
self.set_status(400)
|
|
67
|
+
self.finish(json.dumps({"error": str(e)}))
|
|
44
68
|
|
|
69
|
+
@tornado.web.authenticated
|
|
70
|
+
def delete(self, key: str) -> None:
|
|
71
|
+
"""Delete a rule by key (rule name)."""
|
|
72
|
+
try:
|
|
73
|
+
delete_rule(key)
|
|
74
|
+
cleanup_rules_metadata()
|
|
75
|
+
self.finish(json.dumps({"status": "deleted", "key": key}))
|
|
76
|
+
except ValueError as e:
|
|
77
|
+
self.set_status(400)
|
|
78
|
+
self.finish(json.dumps({"error": str(e)}))
|
mito_ai/rules/utils.py
CHANGED
|
@@ -1,37 +1,166 @@
|
|
|
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
|
-
from typing import Any, Final, List, Optional
|
|
4
|
+
from typing import Any, Final, List, Optional, cast
|
|
5
5
|
import os
|
|
6
|
+
import json
|
|
6
7
|
from mito_ai.utils.schema import MITO_FOLDER
|
|
7
8
|
|
|
8
9
|
RULES_DIR_PATH: Final[str] = os.path.join(MITO_FOLDER, 'rules')
|
|
10
|
+
RULES_METADATA_FILENAME: Final[str] = '_metadata.json'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _sanitize_rule_name(rule_name: str) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Sanitizes a rule name to prevent path traversal attacks.
|
|
16
|
+
Raises ValueError if the rule name contains unsafe characters.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
rule_name: The rule name to sanitize
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The sanitized rule name (with .md extension stripped if present)
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If the rule name contains path traversal sequences or other unsafe characters
|
|
26
|
+
"""
|
|
27
|
+
if not rule_name:
|
|
28
|
+
raise ValueError("Rule name cannot be empty")
|
|
29
|
+
|
|
30
|
+
# Strip .md extension if present
|
|
31
|
+
if rule_name.endswith('.md'):
|
|
32
|
+
rule_name = rule_name[:-3]
|
|
33
|
+
|
|
34
|
+
# Check for path traversal sequences
|
|
35
|
+
if '..' in rule_name or '/' in rule_name or '\\' in rule_name:
|
|
36
|
+
raise ValueError(f"Rule name contains invalid characters: {rule_name}")
|
|
37
|
+
|
|
38
|
+
# Check for absolute paths
|
|
39
|
+
if os.path.isabs(rule_name):
|
|
40
|
+
raise ValueError(f"Rule name cannot be an absolute path: {rule_name}")
|
|
41
|
+
|
|
42
|
+
# Check for null bytes or other control characters
|
|
43
|
+
if '\x00' in rule_name:
|
|
44
|
+
raise ValueError("Rule name cannot contain null bytes")
|
|
45
|
+
|
|
46
|
+
# Ensure it's a valid filename (no reserved characters on Windows)
|
|
47
|
+
# Windows reserved: < > : " | ? *
|
|
48
|
+
invalid_chars = set('<>:|?*"')
|
|
49
|
+
if any(c in rule_name for c in invalid_chars):
|
|
50
|
+
raise ValueError(f"Rule name contains invalid filename characters: {rule_name}")
|
|
51
|
+
|
|
52
|
+
return rule_name
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _validate_rule_path(file_path: str, rule_name: str) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Validates that a rule file path is within the rules directory.
|
|
58
|
+
This provides defense-in-depth protection against path traversal attacks.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
file_path: The file path to validate
|
|
62
|
+
rule_name: The rule name (for error messages)
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the resolved path is outside RULES_DIR_PATH
|
|
66
|
+
"""
|
|
67
|
+
resolved_path = os.path.abspath(file_path)
|
|
68
|
+
rules_dir_abs = os.path.abspath(RULES_DIR_PATH)
|
|
69
|
+
if not resolved_path.startswith(rules_dir_abs):
|
|
70
|
+
raise ValueError(f"Invalid rule name: {rule_name}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_metadata_path() -> str:
|
|
74
|
+
return os.path.join(RULES_DIR_PATH, RULES_METADATA_FILENAME)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load_metadata() -> dict[Any, Any]:
|
|
78
|
+
path = _get_metadata_path()
|
|
79
|
+
if not os.path.exists(path):
|
|
80
|
+
return {}
|
|
81
|
+
try:
|
|
82
|
+
with open(path, 'r') as f:
|
|
83
|
+
return cast(dict[Any, Any], json.load(f))
|
|
84
|
+
except (json.JSONDecodeError, OSError):
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _save_metadata(metadata: dict) -> None:
|
|
89
|
+
if not os.path.exists(RULES_DIR_PATH):
|
|
90
|
+
os.makedirs(RULES_DIR_PATH)
|
|
91
|
+
path = _get_metadata_path()
|
|
92
|
+
with open(path, 'w') as f:
|
|
93
|
+
json.dump(metadata, f, indent=2)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_rule_default(rule_name: str) -> bool:
|
|
97
|
+
"""Returns whether the rule is marked as a default (auto-applied) rule."""
|
|
98
|
+
if rule_name.endswith('.md'):
|
|
99
|
+
rule_name = rule_name[:-3]
|
|
100
|
+
metadata = _load_metadata()
|
|
101
|
+
entry = metadata.get(rule_name, {})
|
|
102
|
+
return bool(entry.get('is_default', False))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def set_rule_default(rule_name: str, is_default: bool) -> None:
|
|
106
|
+
"""Sets whether the rule is a default (auto-applied) rule."""
|
|
107
|
+
if rule_name.endswith('.md'):
|
|
108
|
+
rule_name = rule_name[:-3]
|
|
109
|
+
metadata = _load_metadata()
|
|
110
|
+
metadata[rule_name] = {**metadata.get(rule_name, {}), 'is_default': is_default}
|
|
111
|
+
_save_metadata(metadata)
|
|
112
|
+
|
|
9
113
|
|
|
10
114
|
def set_rules_file(rule_name: str, value: Any) -> None:
|
|
11
115
|
"""
|
|
12
116
|
Updates the value of a specific rule file in the rules directory
|
|
13
117
|
"""
|
|
118
|
+
# Sanitize rule name to prevent path traversal
|
|
119
|
+
rule_name = _sanitize_rule_name(rule_name)
|
|
120
|
+
|
|
14
121
|
# Ensure the directory exists
|
|
15
122
|
if not os.path.exists(RULES_DIR_PATH):
|
|
16
123
|
os.makedirs(RULES_DIR_PATH)
|
|
17
|
-
|
|
124
|
+
|
|
18
125
|
# Create the file path to the rule name as a .md file
|
|
19
126
|
file_path = os.path.join(RULES_DIR_PATH, f"{rule_name}.md")
|
|
20
127
|
|
|
128
|
+
# Additional safety check: ensure the resolved path is still within RULES_DIR_PATH
|
|
129
|
+
_validate_rule_path(file_path, rule_name)
|
|
130
|
+
|
|
21
131
|
with open(file_path, 'w+') as f:
|
|
22
132
|
f.write(value)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def delete_rule(rule_name: str) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Deletes a rule file from the rules directory. Normalizes rule_name (strips .md).
|
|
138
|
+
Metadata for this rule is removed by cleanup_rules_metadata().
|
|
139
|
+
"""
|
|
140
|
+
# Sanitize rule name to prevent path traversal
|
|
141
|
+
rule_name = _sanitize_rule_name(rule_name)
|
|
142
|
+
|
|
143
|
+
file_path = os.path.join(RULES_DIR_PATH, f"{rule_name}.md")
|
|
144
|
+
|
|
145
|
+
# Additional safety check: ensure the resolved path is still within RULES_DIR_PATH
|
|
146
|
+
_validate_rule_path(file_path, rule_name)
|
|
23
147
|
|
|
148
|
+
if os.path.exists(file_path):
|
|
149
|
+
os.remove(file_path)
|
|
150
|
+
|
|
24
151
|
|
|
25
152
|
def get_rule(rule_name: str) -> Optional[str]:
|
|
26
153
|
"""
|
|
27
154
|
Retrieves the value of a specific rule file from the rules directory
|
|
28
155
|
"""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
rule_name = rule_name[:-3]
|
|
156
|
+
# Sanitize rule name to prevent path traversal
|
|
157
|
+
rule_name = _sanitize_rule_name(rule_name)
|
|
32
158
|
|
|
33
159
|
file_path = os.path.join(RULES_DIR_PATH, f"{rule_name}.md")
|
|
34
160
|
|
|
161
|
+
# Additional safety check: ensure the resolved path is still within RULES_DIR_PATH
|
|
162
|
+
_validate_rule_path(file_path, rule_name)
|
|
163
|
+
|
|
35
164
|
if not os.path.exists(file_path):
|
|
36
165
|
return None
|
|
37
166
|
|
|
@@ -47,10 +176,45 @@ def get_all_rules() -> List[str]:
|
|
|
47
176
|
if not os.path.exists(RULES_DIR_PATH):
|
|
48
177
|
os.makedirs(RULES_DIR_PATH)
|
|
49
178
|
return [] # Return empty list if directory didn't exist
|
|
50
|
-
|
|
179
|
+
|
|
51
180
|
try:
|
|
52
181
|
return [f for f in os.listdir(RULES_DIR_PATH) if f.endswith('.md')]
|
|
53
182
|
except OSError as e:
|
|
54
183
|
# Log the error if needed and return empty list
|
|
55
184
|
print(f"Error reading rules directory: {e}")
|
|
56
185
|
return []
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def cleanup_rules_metadata() -> None:
|
|
189
|
+
"""
|
|
190
|
+
Removes metadata entries for rules that no longer exist on disk (deleted or renamed).
|
|
191
|
+
Call after rule create/update so metadata stays in sync with actual rule files.
|
|
192
|
+
"""
|
|
193
|
+
current_files = get_all_rules()
|
|
194
|
+
current_rule_names = {f[:-3] if f.endswith('.md') else f for f in current_files}
|
|
195
|
+
metadata = _load_metadata()
|
|
196
|
+
if not metadata:
|
|
197
|
+
return
|
|
198
|
+
keys_to_remove = [k for k in metadata if k not in current_rule_names]
|
|
199
|
+
if not keys_to_remove:
|
|
200
|
+
return
|
|
201
|
+
for k in keys_to_remove:
|
|
202
|
+
del metadata[k]
|
|
203
|
+
_save_metadata(metadata)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_default_rules_content() -> str:
|
|
207
|
+
"""
|
|
208
|
+
Returns the concatenated content of all rules marked as default (auto-applied).
|
|
209
|
+
Each rule is included as "Rule name:\n\n{content}". Returns empty string if no default rules.
|
|
210
|
+
"""
|
|
211
|
+
rule_files = get_all_rules()
|
|
212
|
+
parts: List[str] = []
|
|
213
|
+
for f in rule_files:
|
|
214
|
+
rule_name = f[:-3] if f.endswith('.md') else f
|
|
215
|
+
if not get_rule_default(rule_name):
|
|
216
|
+
continue
|
|
217
|
+
content = get_rule(rule_name)
|
|
218
|
+
if content and content.strip():
|
|
219
|
+
parts.append(f"{rule_name}:\n\n{content}")
|
|
220
|
+
return '\n\n'.join(parts) if parts else ""
|
|
@@ -59,7 +59,7 @@ def reset_env_vars(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
59
59
|
"name": "claude",
|
|
60
60
|
"env_vars": {"ANTHROPIC_API_KEY": "claude-key"},
|
|
61
61
|
"constants": {"ANTHROPIC_API_KEY": "claude-key", "OPENAI_API_KEY": None},
|
|
62
|
-
"model": "claude-
|
|
62
|
+
"model": "claude-haiku-4-5-20251001",
|
|
63
63
|
"mock_patch": "mito_ai.provider_manager.AnthropicClient",
|
|
64
64
|
"mock_method": "request_completions",
|
|
65
65
|
"provider_name": "Claude",
|
|
@@ -143,7 +143,7 @@ async def test_completion_request(
|
|
|
143
143
|
"name": "claude",
|
|
144
144
|
"env_vars": {"ANTHROPIC_API_KEY": "claude-key"},
|
|
145
145
|
"constants": {"ANTHROPIC_API_KEY": "claude-key", "OPENAI_API_KEY": None},
|
|
146
|
-
"model": "claude-
|
|
146
|
+
"model": "claude-haiku-4-5-20251001",
|
|
147
147
|
"mock_patch": "mito_ai.provider_manager.AnthropicClient",
|
|
148
148
|
"mock_method": "stream_completions",
|
|
149
149
|
"provider_name": "Claude",
|
|
@@ -235,7 +235,7 @@ def test_claude_error_handling(monkeypatch: pytest.MonkeyPatch, provider_config:
|
|
|
235
235
|
|
|
236
236
|
mock_client = MagicMock()
|
|
237
237
|
mock_client.capabilities = AICapabilities(
|
|
238
|
-
configuration={"model": "claude-
|
|
238
|
+
configuration={"model": "claude-haiku-4-5-20251001"},
|
|
239
239
|
provider="Claude",
|
|
240
240
|
type="ai_capabilities"
|
|
241
241
|
)
|
|
@@ -258,7 +258,7 @@ def test_claude_error_handling(monkeypatch: pytest.MonkeyPatch, provider_config:
|
|
|
258
258
|
},
|
|
259
259
|
{
|
|
260
260
|
"name": "claude_fallback",
|
|
261
|
-
"model": "claude-
|
|
261
|
+
"model": "claude-haiku-4-5-20251001",
|
|
262
262
|
"mock_function": "mito_ai.anthropic_client.get_anthropic_completion_from_mito_server",
|
|
263
263
|
"provider_name": "Claude",
|
|
264
264
|
"key_type": "claude"
|
|
@@ -315,7 +315,7 @@ async def test_mito_server_fallback_completion_request(
|
|
|
315
315
|
},
|
|
316
316
|
{
|
|
317
317
|
"name": "claude_fallback",
|
|
318
|
-
"model": "claude-
|
|
318
|
+
"model": "claude-haiku-4-5-20251001",
|
|
319
319
|
"mock_function": "mito_ai.anthropic_client.stream_anthropic_completion_from_mito_server",
|
|
320
320
|
"provider_name": "Claude",
|
|
321
321
|
"key_type": "claude"
|
|
@@ -21,7 +21,7 @@ def test_put_rule_with_auth(jp_base_url):
|
|
|
21
21
|
|
|
22
22
|
response_json = response.json()
|
|
23
23
|
assert response_json["status"] == "updated"
|
|
24
|
-
assert response_json["
|
|
24
|
+
assert response_json["rules_file"] == RULE_NAME
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def test_put_rule_with_no_auth(jp_base_url):
|
|
@@ -55,12 +55,22 @@ def test_put_rule_missing_content(jp_base_url):
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def test_get_rule_with_auth(jp_base_url):
|
|
58
|
+
# First create the rule
|
|
59
|
+
requests.put(
|
|
60
|
+
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
61
|
+
headers={"Authorization": f"token {TOKEN}"},
|
|
62
|
+
json={"content": RULE_CONTENT},
|
|
63
|
+
)
|
|
64
|
+
# Then get it
|
|
58
65
|
response = requests.get(
|
|
59
66
|
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
60
67
|
headers={"Authorization": f"token {TOKEN}"},
|
|
61
68
|
)
|
|
62
69
|
assert response.status_code == 200
|
|
63
|
-
|
|
70
|
+
response_json = response.json()
|
|
71
|
+
assert response_json["key"] == RULE_NAME
|
|
72
|
+
assert response_json["content"] == RULE_CONTENT
|
|
73
|
+
assert "is_default" in response_json # is_default field should be present
|
|
64
74
|
|
|
65
75
|
|
|
66
76
|
def test_get_rule_with_no_auth(jp_base_url):
|
|
@@ -90,6 +100,13 @@ def test_get_nonexistent_rule_with_auth(jp_base_url):
|
|
|
90
100
|
|
|
91
101
|
|
|
92
102
|
def test_get_all_rules_with_auth(jp_base_url):
|
|
103
|
+
# First create the rule
|
|
104
|
+
requests.put(
|
|
105
|
+
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
106
|
+
headers={"Authorization": f"token {TOKEN}"},
|
|
107
|
+
json={"content": RULE_CONTENT},
|
|
108
|
+
)
|
|
109
|
+
# Then get all rules
|
|
93
110
|
response = requests.get(
|
|
94
111
|
jp_base_url + f"/mito-ai/rules",
|
|
95
112
|
headers={"Authorization": f"token {TOKEN}"},
|
|
@@ -98,8 +115,13 @@ def test_get_all_rules_with_auth(jp_base_url):
|
|
|
98
115
|
|
|
99
116
|
response_json = response.json()
|
|
100
117
|
assert isinstance(response_json, list)
|
|
101
|
-
# Should contain our test rule (with .md extension)
|
|
102
|
-
|
|
118
|
+
# Should contain our test rule (with .md extension) as an object with name and is_default
|
|
119
|
+
rule_names = [rule["name"] for rule in response_json]
|
|
120
|
+
assert f"{RULE_NAME}.md" in rule_names
|
|
121
|
+
# Verify structure of rule objects
|
|
122
|
+
test_rule = next((r for r in response_json if r["name"] == f"{RULE_NAME}.md"), None)
|
|
123
|
+
assert test_rule is not None
|
|
124
|
+
assert "is_default" in test_rule
|
|
103
125
|
|
|
104
126
|
|
|
105
127
|
def test_get_all_rules_with_no_auth(jp_base_url):
|
|
@@ -115,3 +137,77 @@ def test_get_all_rules_with_incorrect_auth(jp_base_url):
|
|
|
115
137
|
headers={"Authorization": f"token incorrect-token"},
|
|
116
138
|
)
|
|
117
139
|
assert response.status_code == 403 # Forbidden
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# --- DELETE RULES ---
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_delete_rule_with_auth(jp_base_url):
|
|
146
|
+
# First create the rule
|
|
147
|
+
requests.put(
|
|
148
|
+
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
149
|
+
headers={"Authorization": f"token {TOKEN}"},
|
|
150
|
+
json={"content": RULE_CONTENT},
|
|
151
|
+
)
|
|
152
|
+
# Verify it exists
|
|
153
|
+
get_response = requests.get(
|
|
154
|
+
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
155
|
+
headers={"Authorization": f"token {TOKEN}"},
|
|
156
|
+
)
|
|
157
|
+
assert get_response.status_code == 200
|
|
158
|
+
|
|
159
|
+
# Delete it
|
|
160
|
+
delete_response = requests.delete(
|
|
161
|
+
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
162
|
+
headers={"Authorization": f"token {TOKEN}"},
|
|
163
|
+
)
|
|
164
|
+
assert delete_response.status_code == 200
|
|
165
|
+
delete_json = delete_response.json()
|
|
166
|
+
assert delete_json["status"] == "deleted"
|
|
167
|
+
assert delete_json["key"] == RULE_NAME
|
|
168
|
+
|
|
169
|
+
# Verify it no longer exists
|
|
170
|
+
get_response_after_delete = requests.get(
|
|
171
|
+
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
172
|
+
headers={"Authorization": f"token {TOKEN}"},
|
|
173
|
+
)
|
|
174
|
+
assert get_response_after_delete.status_code == 404
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_delete_rule_with_no_auth(jp_base_url):
|
|
178
|
+
response = requests.delete(
|
|
179
|
+
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
180
|
+
)
|
|
181
|
+
assert response.status_code == 403 # Forbidden
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_delete_rule_with_incorrect_auth(jp_base_url):
|
|
185
|
+
response = requests.delete(
|
|
186
|
+
jp_base_url + f"/mito-ai/rules/{RULE_NAME}",
|
|
187
|
+
headers={"Authorization": f"token incorrect-token"}, # <- wrong token
|
|
188
|
+
)
|
|
189
|
+
assert response.status_code == 403 # Forbidden
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_delete_nonexistent_rule_with_auth(jp_base_url):
|
|
193
|
+
# Delete a rule that doesn't exist (should succeed - idempotent operation)
|
|
194
|
+
response = requests.delete(
|
|
195
|
+
jp_base_url + f"/mito-ai/rules/nonexistent_rule",
|
|
196
|
+
headers={"Authorization": f"token {TOKEN}"},
|
|
197
|
+
)
|
|
198
|
+
assert response.status_code == 200
|
|
199
|
+
response_json = response.json()
|
|
200
|
+
assert response_json["status"] == "deleted"
|
|
201
|
+
assert response_json["key"] == "nonexistent_rule"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_delete_rule_invalid_name(jp_base_url):
|
|
205
|
+
# Try to delete with invalid rule name (contains Windows reserved character ':')
|
|
206
|
+
# This will reach the handler but fail validation in _sanitize_rule_name
|
|
207
|
+
response = requests.delete(
|
|
208
|
+
jp_base_url + f"/mito-ai/rules/rule:with:colons",
|
|
209
|
+
headers={"Authorization": f"token {TOKEN}"},
|
|
210
|
+
)
|
|
211
|
+
assert response.status_code == 400 # Bad Request
|
|
212
|
+
response_json = response.json()
|
|
213
|
+
assert "error" in response_json
|
mito_ai/utils/anthropic_utils.py
CHANGED
|
@@ -17,8 +17,7 @@ __user_id: Optional[str] = None
|
|
|
17
17
|
ANTHROPIC_TIMEOUT = 60
|
|
18
18
|
max_retries = 1
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
LARGE_CONTEXT_MODEL = "claude-sonnet-4-5-20250929" # This should be in sync with ModelSelector.tsx
|
|
20
|
+
LARGE_CONTEXT_MODEL = "claude-sonnet-4-5-20250929"
|
|
22
21
|
EXTENDED_CONTEXT_BETA = "context-1m-2025-08-07" # Beta feature for extended context window support
|
|
23
22
|
|
|
24
23
|
def does_message_exceed_max_tokens(system: Union[str, List[TextBlockParam], anthropic.Omit], messages: List[MessageParam]) -> bool:
|
mito_ai/utils/model_utils.py
CHANGED
|
@@ -9,7 +9,6 @@ from mito_ai.enterprise.utils import is_abacus_configured
|
|
|
9
9
|
# Model ordering: [fastest, ..., slowest] for each provider
|
|
10
10
|
ANTHROPIC_MODEL_ORDER = [
|
|
11
11
|
"claude-haiku-4-5-20251001", # Fastest
|
|
12
|
-
"claude-sonnet-4-5-20250929", # Slower
|
|
13
12
|
]
|
|
14
13
|
|
|
15
14
|
OPENAI_MODEL_ORDER = [
|
|
@@ -27,7 +26,6 @@ GEMINI_MODEL_ORDER = [
|
|
|
27
26
|
STANDARD_MODELS = [
|
|
28
27
|
"gpt-4.1",
|
|
29
28
|
"gpt-5.2",
|
|
30
|
-
"claude-sonnet-4-5-20250929",
|
|
31
29
|
"claude-haiku-4-5-20251001",
|
|
32
30
|
"gemini-3-flash-preview",
|
|
33
31
|
"gemini-3-pro-preview",
|
{mito_ai-0.1.59.data → mito_ai-0.1.60.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.60",
|
|
4
4
|
"description": "AI chat for JupyterLab",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"schemaDir": "schema",
|
|
142
142
|
"themePath": "style/theme/theme.css",
|
|
143
143
|
"_build": {
|
|
144
|
-
"load": "static/remoteEntry.
|
|
144
|
+
"load": "static/remoteEntry.9735d9bfc8891147fee0.js",
|
|
145
145
|
"extension": "./extension",
|
|
146
146
|
"style": "./style"
|
|
147
147
|
}
|