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.
Files changed (50) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/completions/prompt_builders/agent_system_message.py +7 -1
  3. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  4. mito_ai/completions/prompt_builders/prompt_constants.py +15 -0
  5. mito_ai/rules/handlers.py +46 -12
  6. mito_ai/rules/utils.py +170 -6
  7. mito_ai/tests/providers/test_providers.py +5 -5
  8. mito_ai/tests/rules/rules_test.py +100 -4
  9. mito_ai/utils/anthropic_utils.py +1 -2
  10. mito_ai/utils/model_utils.py +0 -2
  11. {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
  12. {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  13. {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
  14. 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
  15. mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.dccfa541c464ee0e5cd4.js.map +1 -0
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9735d9bfc8891147fee0.js.map +1 -0
  22. {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/METADATA +1 -1
  23. {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/RECORD +47 -45
  24. mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.44c109c7be36fb884d25.js.map +0 -1
  25. mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.f7decebaf69618541e0f.js.map +0 -1
  26. mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js.map +0 -1
  27. {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  28. {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
  29. {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
  30. {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
  31. {mito_ai-0.1.59.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  32. {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
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/WHEEL +0 -0
  49. {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/entry_points.txt +0 -0
  50. {mito_ai-0.1.59.dist-info → mito_ai-0.1.60.dist-info}/licenses/LICENSE +0 -0
mito_ai/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.1.59'
4
+ __version__ = VERSION = '0.1.60'
@@ -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 RULES_DIR_PATH, get_all_rules, get_rule, set_rules_file
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
- rules = get_all_rules()
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
- rule_content = get_rule(key)
26
- if rule_content is None:
27
- self.set_status(404)
28
- self.finish(json.dumps({"error": f"Rule with key '{key}' not found"}))
29
- else:
30
- self.finish(json.dumps({"key": key, "content": rule_content}))
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
- if rule_name.endswith('.md'):
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-sonnet-4-5-20250929",
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-sonnet-4-5-20250929",
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-sonnet-4-5-20250929"},
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-sonnet-4-5-20250929",
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-sonnet-4-5-20250929",
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["rules file "] == RULE_NAME
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
- assert response.json() == {"key": RULE_NAME, "content": RULE_CONTENT}
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
- assert f"{RULE_NAME}.md" in response_json
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
@@ -17,8 +17,7 @@ __user_id: Optional[str] = None
17
17
  ANTHROPIC_TIMEOUT = 60
18
18
  max_retries = 1
19
19
 
20
- FAST_ANTHROPIC_MODEL = "claude-haiku-4-5-20251001" # This should be in sync with ModelSelector.tsx
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:
@@ -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",
@@ -720,7 +720,7 @@
720
720
  "semver": {},
721
721
  "vscode-diff": {},
722
722
  "mito_ai": {
723
- "version": "0.1.59",
723
+ "version": "0.1.60",
724
724
  "singleton": true,
725
725
  "import": "/home/runner/work/mito/mito/mito-ai/lib/index.js"
726
726
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mito_ai",
3
- "version": "0.1.59",
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.f7decebaf69618541e0f.js",
144
+ "load": "static/remoteEntry.9735d9bfc8891147fee0.js",
145
145
  "extension": "./extension",
146
146
  "style": "./style"
147
147
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mito_ai",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "AI chat for JupyterLab",
5
5
  "keywords": [
6
6
  "jupyter",