janito 2.3.0__py3-none-any.whl → 2.3.1__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 (93) hide show
  1. janito/__init__.py +6 -6
  2. janito/cli/chat_mode/shell/autocomplete.py +21 -21
  3. janito/cli/chat_mode/shell/commands/clear.py +12 -12
  4. janito/cli/chat_mode/shell/commands/multi.py +51 -51
  5. janito/cli/chat_mode/shell/input_history.py +62 -62
  6. janito/cli/cli_commands/list_models.py +35 -35
  7. janito/cli/cli_commands/list_providers.py +9 -9
  8. janito/cli/cli_commands/list_tools.py +53 -53
  9. janito/cli/cli_commands/model_selection.py +50 -50
  10. janito/cli/cli_commands/model_utils.py +95 -95
  11. janito/cli/cli_commands/set_api_key.py +19 -19
  12. janito/cli/cli_commands/show_config.py +51 -51
  13. janito/cli/cli_commands/show_system_prompt.py +62 -62
  14. janito/cli/core/__init__.py +4 -4
  15. janito/cli/core/event_logger.py +59 -59
  16. janito/cli/core/getters.py +33 -33
  17. janito/cli/core/unsetters.py +54 -54
  18. janito/cli/single_shot_mode/__init__.py +6 -6
  19. janito/config.py +5 -5
  20. janito/config_manager.py +112 -112
  21. janito/drivers/anthropic/driver.py +113 -113
  22. janito/formatting_token.py +54 -54
  23. janito/i18n/__init__.py +35 -35
  24. janito/i18n/messages.py +23 -23
  25. janito/i18n/pt.py +47 -47
  26. janito/llm/__init__.py +5 -5
  27. janito/llm/agent.py +443 -443
  28. janito/llm/auth.py +63 -63
  29. janito/llm/driver_config_builder.py +34 -34
  30. janito/llm/driver_input.py +12 -12
  31. janito/llm/message_parts.py +60 -60
  32. janito/llm/model.py +38 -38
  33. janito/llm/provider.py +196 -196
  34. janito/provider_registry.py +176 -176
  35. janito/providers/anthropic/model_info.py +22 -22
  36. janito/providers/anthropic/provider.py +2 -0
  37. janito/providers/azure_openai/model_info.py +16 -16
  38. janito/providers/azure_openai/provider.py +3 -0
  39. janito/providers/deepseek/__init__.py +1 -1
  40. janito/providers/deepseek/model_info.py +16 -16
  41. janito/providers/deepseek/provider.py +94 -91
  42. janito/providers/google/provider.py +3 -0
  43. janito/providers/mistralai/provider.py +3 -0
  44. janito/providers/openai/provider.py +4 -0
  45. janito/tools/adapters/__init__.py +1 -1
  46. janito/tools/adapters/local/ask_user.py +102 -102
  47. janito/tools/adapters/local/copy_file.py +84 -84
  48. janito/tools/adapters/local/create_directory.py +69 -69
  49. janito/tools/adapters/local/create_file.py +82 -82
  50. janito/tools/adapters/local/fetch_url.py +97 -97
  51. janito/tools/adapters/local/find_files.py +138 -138
  52. janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
  53. janito/tools/adapters/local/get_file_outline/core.py +117 -117
  54. janito/tools/adapters/local/get_file_outline/java_outline.py +40 -40
  55. janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
  56. janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
  57. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
  58. janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
  59. janito/tools/adapters/local/python_code_run.py +166 -166
  60. janito/tools/adapters/local/python_command_run.py +164 -164
  61. janito/tools/adapters/local/python_file_run.py +163 -163
  62. janito/tools/adapters/local/run_bash_command.py +176 -176
  63. janito/tools/adapters/local/run_powershell_command.py +219 -219
  64. janito/tools/adapters/local/search_text/__init__.py +1 -1
  65. janito/tools/adapters/local/search_text/core.py +201 -201
  66. janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
  67. janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
  68. janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
  69. janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
  70. janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
  71. janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
  72. janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
  73. janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
  74. janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
  75. janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
  76. janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
  77. janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
  78. janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
  79. janito/tools/adapters/local/view_file.py +167 -167
  80. janito/tools/inspect_registry.py +17 -17
  81. janito/tools/tool_base.py +105 -105
  82. janito/tools/tool_events.py +58 -58
  83. janito/tools/tool_run_exception.py +12 -12
  84. janito/tools/tool_use_tracker.py +81 -81
  85. janito/tools/tool_utils.py +45 -45
  86. janito/tools/tools_schema.py +104 -104
  87. janito/version.py +4 -4
  88. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/METADATA +390 -388
  89. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/RECORD +93 -93
  90. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/WHEEL +0 -0
  91. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/entry_points.txt +0 -0
  92. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/licenses/LICENSE +0 -0
  93. {janito-2.3.0.dist-info → janito-2.3.1.dist-info}/top_level.txt +0 -0
@@ -1,69 +1,69 @@
1
- from janito.tools.adapters.local.adapter import register_local_tool
2
-
3
- from janito.tools.tool_utils import display_path
4
- from janito.tools.tool_base import ToolBase
5
- from janito.report_events import ReportAction
6
- from janito.i18n import tr
7
- import os
8
-
9
-
10
- @register_local_tool
11
- class CreateDirectoryTool(ToolBase):
12
- """
13
- Create a new directory at the specified file_path.
14
- Args:
15
- file_path (str): Path for the new directory.
16
- Returns:
17
- str: Status message indicating the result. Example:
18
- - "5c5 Successfully created the directory at ..."
19
- - "5d7 Cannot create directory: ..."
20
- """
21
-
22
- tool_name = "create_directory"
23
-
24
- def run(self, file_path: str) -> str:
25
- # file_path = expand_path(file_path)
26
- # Using file_path as is
27
- disp_path = display_path(file_path)
28
- self.report_action(
29
- tr("📁 Create directory '{disp_path}' ...", disp_path=disp_path),
30
- ReportAction.CREATE,
31
- )
32
- try:
33
- if os.path.exists(file_path):
34
- if not os.path.isdir(file_path):
35
- self.report_error(
36
- tr(
37
- "❌ Path '{disp_path}' exists and is not a directory.",
38
- disp_path=disp_path,
39
- )
40
- )
41
- return tr(
42
- "❌ Path '{disp_path}' exists and is not a directory.",
43
- disp_path=disp_path,
44
- )
45
- self.report_error(
46
- tr(
47
- "❗ Directory '{disp_path}' already exists.",
48
- disp_path=disp_path,
49
- )
50
- )
51
- return tr(
52
- "❗ Cannot create directory: '{disp_path}' already exists.",
53
- disp_path=disp_path,
54
- )
55
- os.makedirs(file_path, exist_ok=True)
56
- self.report_success(tr("✅ Directory created"))
57
- return tr(
58
- "✅ Successfully created the directory at '{disp_path}'.",
59
- disp_path=disp_path,
60
- )
61
- except Exception as e:
62
- self.report_error(
63
- tr(
64
- "❌ Error creating directory '{disp_path}': {error}",
65
- disp_path=disp_path,
66
- error=e,
67
- )
68
- )
69
- return tr("❌ Cannot create directory: {error}", error=e)
1
+ from janito.tools.adapters.local.adapter import register_local_tool
2
+
3
+ from janito.tools.tool_utils import display_path
4
+ from janito.tools.tool_base import ToolBase
5
+ from janito.report_events import ReportAction
6
+ from janito.i18n import tr
7
+ import os
8
+
9
+
10
+ @register_local_tool
11
+ class CreateDirectoryTool(ToolBase):
12
+ """
13
+ Create a new directory at the specified file_path.
14
+ Args:
15
+ file_path (str): Path for the new directory.
16
+ Returns:
17
+ str: Status message indicating the result. Example:
18
+ - "5c5 Successfully created the directory at ..."
19
+ - "5d7 Cannot create directory: ..."
20
+ """
21
+
22
+ tool_name = "create_directory"
23
+
24
+ def run(self, file_path: str) -> str:
25
+ # file_path = expand_path(file_path)
26
+ # Using file_path as is
27
+ disp_path = display_path(file_path)
28
+ self.report_action(
29
+ tr("📁 Create directory '{disp_path}' ...", disp_path=disp_path),
30
+ ReportAction.CREATE,
31
+ )
32
+ try:
33
+ if os.path.exists(file_path):
34
+ if not os.path.isdir(file_path):
35
+ self.report_error(
36
+ tr(
37
+ "❌ Path '{disp_path}' exists and is not a directory.",
38
+ disp_path=disp_path,
39
+ )
40
+ )
41
+ return tr(
42
+ "❌ Path '{disp_path}' exists and is not a directory.",
43
+ disp_path=disp_path,
44
+ )
45
+ self.report_error(
46
+ tr(
47
+ "❗ Directory '{disp_path}' already exists.",
48
+ disp_path=disp_path,
49
+ )
50
+ )
51
+ return tr(
52
+ "❗ Cannot create directory: '{disp_path}' already exists.",
53
+ disp_path=disp_path,
54
+ )
55
+ os.makedirs(file_path, exist_ok=True)
56
+ self.report_success(tr("✅ Directory created"))
57
+ return tr(
58
+ "✅ Successfully created the directory at '{disp_path}'.",
59
+ disp_path=disp_path,
60
+ )
61
+ except Exception as e:
62
+ self.report_error(
63
+ tr(
64
+ "❌ Error creating directory '{disp_path}': {error}",
65
+ disp_path=disp_path,
66
+ error=e,
67
+ )
68
+ )
69
+ return tr("❌ Cannot create directory: {error}", error=e)
@@ -1,82 +1,82 @@
1
- import os
2
- from janito.tools.adapters.local.adapter import register_local_tool
3
-
4
- from janito.tools.tool_utils import display_path
5
- from janito.tools.tool_base import ToolBase
6
- from janito.report_events import ReportAction
7
- from janito.i18n import tr
8
-
9
-
10
- from janito.tools.adapters.local.validate_file_syntax.core import validate_file_syntax
11
-
12
-
13
- @register_local_tool
14
- class CreateFileTool(ToolBase):
15
- """
16
- Create a new file with the given content.
17
-
18
- Args:
19
- file_path (str): Path to the file to create.
20
- content (str): Content to write to the file.
21
- overwrite (bool, optional): Overwrite existing file if True. Default: False. Recommended only after reading the file to be overwritten.
22
- Returns:
23
- str: Status message indicating the result. Example:
24
- - "✅ Successfully created the file at ..."
25
-
26
- Note: Syntax validation is automatically performed after this operation.
27
- """
28
-
29
- tool_name = "create_file"
30
-
31
- def run(self, file_path: str, content: str, overwrite: bool = False) -> str:
32
- expanded_file_path = file_path # Using file_path as is
33
- disp_path = display_path(expanded_file_path)
34
- file_path = expanded_file_path
35
- if os.path.exists(file_path) and not overwrite:
36
- try:
37
- with open(file_path, "r", encoding="utf-8", errors="replace") as f:
38
- existing_content = f.read()
39
- except Exception as e:
40
- existing_content = f"[Error reading file: {e}]"
41
- return tr(
42
- "❗ Cannot create file: file already exists at '{disp_path}'.\n--- Current file content ---\n{existing_content}",
43
- disp_path=disp_path,
44
- existing_content=existing_content,
45
- )
46
- # Determine if we are overwriting an existing file
47
- is_overwrite = os.path.exists(file_path) and overwrite
48
- if is_overwrite:
49
- # Overwrite branch: log only overwrite warning (no create message)
50
- self.report_action(
51
- tr("⚠️ Overwriting file '{disp_path}'", disp_path=disp_path),
52
- ReportAction.CREATE,
53
- )
54
- dir_name = os.path.dirname(file_path)
55
- if dir_name:
56
- os.makedirs(dir_name, exist_ok=True)
57
- if not is_overwrite:
58
- # Create branch: log file creation message
59
- self.report_action(
60
- tr("📝 Create file '{disp_path}' ...", disp_path=disp_path),
61
- ReportAction.CREATE,
62
- )
63
- with open(file_path, "w", encoding="utf-8", errors="replace") as f:
64
- f.write(content)
65
- new_lines = content.count("\n") + 1 if content else 0
66
- self.report_success(
67
- tr("✅ {new_lines} lines", new_lines=new_lines), ReportAction.CREATE
68
- )
69
- # Perform syntax validation and append result
70
- validation_result = validate_file_syntax(file_path)
71
- if is_overwrite:
72
- # Overwrite branch: return minimal overwrite info to user
73
- return (
74
- tr("✅ {new_lines} lines", new_lines=new_lines)
75
- + f"\n{validation_result}"
76
- )
77
- else:
78
- # Create branch: return detailed create success to user
79
- return (
80
- tr("✅ Created file {new_lines} lines.", new_lines=new_lines)
81
- + f"\n{validation_result}"
82
- )
1
+ import os
2
+ from janito.tools.adapters.local.adapter import register_local_tool
3
+
4
+ from janito.tools.tool_utils import display_path
5
+ from janito.tools.tool_base import ToolBase
6
+ from janito.report_events import ReportAction
7
+ from janito.i18n import tr
8
+
9
+
10
+ from janito.tools.adapters.local.validate_file_syntax.core import validate_file_syntax
11
+
12
+
13
+ @register_local_tool
14
+ class CreateFileTool(ToolBase):
15
+ """
16
+ Create a new file with the given content.
17
+
18
+ Args:
19
+ file_path (str): Path to the file to create.
20
+ content (str): Content to write to the file.
21
+ overwrite (bool, optional): Overwrite existing file if True. Default: False. Recommended only after reading the file to be overwritten.
22
+ Returns:
23
+ str: Status message indicating the result. Example:
24
+ - "✅ Successfully created the file at ..."
25
+
26
+ Note: Syntax validation is automatically performed after this operation.
27
+ """
28
+
29
+ tool_name = "create_file"
30
+
31
+ def run(self, file_path: str, content: str, overwrite: bool = False) -> str:
32
+ expanded_file_path = file_path # Using file_path as is
33
+ disp_path = display_path(expanded_file_path)
34
+ file_path = expanded_file_path
35
+ if os.path.exists(file_path) and not overwrite:
36
+ try:
37
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
38
+ existing_content = f.read()
39
+ except Exception as e:
40
+ existing_content = f"[Error reading file: {e}]"
41
+ return tr(
42
+ "❗ Cannot create file: file already exists at '{disp_path}'.\n--- Current file content ---\n{existing_content}",
43
+ disp_path=disp_path,
44
+ existing_content=existing_content,
45
+ )
46
+ # Determine if we are overwriting an existing file
47
+ is_overwrite = os.path.exists(file_path) and overwrite
48
+ if is_overwrite:
49
+ # Overwrite branch: log only overwrite warning (no create message)
50
+ self.report_action(
51
+ tr("⚠️ Overwriting file '{disp_path}'", disp_path=disp_path),
52
+ ReportAction.CREATE,
53
+ )
54
+ dir_name = os.path.dirname(file_path)
55
+ if dir_name:
56
+ os.makedirs(dir_name, exist_ok=True)
57
+ if not is_overwrite:
58
+ # Create branch: log file creation message
59
+ self.report_action(
60
+ tr("📝 Create file '{disp_path}' ...", disp_path=disp_path),
61
+ ReportAction.CREATE,
62
+ )
63
+ with open(file_path, "w", encoding="utf-8", errors="replace") as f:
64
+ f.write(content)
65
+ new_lines = content.count("\n") + 1 if content else 0
66
+ self.report_success(
67
+ tr("✅ {new_lines} lines", new_lines=new_lines), ReportAction.CREATE
68
+ )
69
+ # Perform syntax validation and append result
70
+ validation_result = validate_file_syntax(file_path)
71
+ if is_overwrite:
72
+ # Overwrite branch: return minimal overwrite info to user
73
+ return (
74
+ tr("✅ {new_lines} lines", new_lines=new_lines)
75
+ + f"\n{validation_result}"
76
+ )
77
+ else:
78
+ # Create branch: return detailed create success to user
79
+ return (
80
+ tr("✅ Created file {new_lines} lines.", new_lines=new_lines)
81
+ + f"\n{validation_result}"
82
+ )
@@ -1,97 +1,97 @@
1
- import requests
2
- from bs4 import BeautifulSoup
3
- from janito.tools.adapters.local.adapter import register_local_tool
4
- from janito.tools.tool_base import ToolBase
5
- from janito.report_events import ReportAction
6
- from janito.i18n import tr
7
- from janito.tools.tool_utils import pluralize
8
-
9
-
10
- @register_local_tool
11
- class FetchUrlTool(ToolBase):
12
- """
13
- Fetch the content of a web page and extract its text.
14
-
15
- Args:
16
- url (str): The URL of the web page to fetch.
17
- search_strings (list[str], optional): Strings to search for in the page content.
18
- Returns:
19
- str: Extracted text content from the web page, or a warning message. Example:
20
- - "<main text content...>"
21
- - "No lines found for the provided search strings."
22
- - "Warning: Empty URL provided. Operation skipped."
23
- """
24
-
25
- tool_name = "fetch_url"
26
-
27
- def run(self, url: str, search_strings: list[str] = None) -> str:
28
- if not url.strip():
29
- self.report_warning(tr("ℹ️ Empty URL provided."), ReportAction.READ)
30
- return tr("Warning: Empty URL provided. Operation skipped.")
31
- self.report_action(tr("🌐 Fetch URL '{url}' ...", url=url), ReportAction.READ)
32
- try:
33
- response = requests.get(url, timeout=10)
34
- response.raise_for_status()
35
- except requests.exceptions.HTTPError as http_err:
36
- status_code = http_err.response.status_code if http_err.response else None
37
- if status_code and 400 <= status_code < 500:
38
- self.report_error(
39
- tr(
40
- "❗ HTTP {status_code} error for URL: {url}",
41
- status_code=status_code,
42
- url=url,
43
- ),
44
- ReportAction.READ,
45
- )
46
- return tr(
47
- "Warning: HTTP {status_code} error for URL: {url}",
48
- status_code=status_code,
49
- url=url,
50
- )
51
- else:
52
- self.report_error(
53
- tr(
54
- "❗ HTTP error for URL: {url}: {err}",
55
- url=url,
56
- err=str(http_err),
57
- ),
58
- ReportAction.READ,
59
- )
60
- return tr(
61
- "Warning: HTTP error for URL: {url}: {err}",
62
- url=url,
63
- err=str(http_err),
64
- )
65
- except Exception as err:
66
- self.report_error(
67
- tr("❗ Error fetching URL: {url}: {err}", url=url, err=str(err)),
68
- ReportAction.READ,
69
- )
70
- return tr(
71
- "Warning: Error fetching URL: {url}: {err}", url=url, err=str(err)
72
- )
73
- soup = BeautifulSoup(response.text, "html.parser")
74
- text = soup.get_text(separator="\n")
75
- if search_strings:
76
- filtered = []
77
- for s in search_strings:
78
- idx = text.find(s)
79
- if idx != -1:
80
- start = max(0, idx - 200)
81
- end = min(len(text), idx + len(s) + 200)
82
- snippet = text[start:end]
83
- filtered.append(snippet)
84
- if filtered:
85
- text = "\n...\n".join(filtered)
86
- else:
87
- text = tr("No lines found for the provided search strings.")
88
- num_lines = len(text.splitlines())
89
- self.report_success(
90
- tr(
91
- "✅ {num_lines} {line_word}",
92
- num_lines=num_lines,
93
- line_word=pluralize("line", num_lines),
94
- ),
95
- ReportAction.READ,
96
- )
97
- return text
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+ from janito.tools.adapters.local.adapter import register_local_tool
4
+ from janito.tools.tool_base import ToolBase
5
+ from janito.report_events import ReportAction
6
+ from janito.i18n import tr
7
+ from janito.tools.tool_utils import pluralize
8
+
9
+
10
+ @register_local_tool
11
+ class FetchUrlTool(ToolBase):
12
+ """
13
+ Fetch the content of a web page and extract its text.
14
+
15
+ Args:
16
+ url (str): The URL of the web page to fetch.
17
+ search_strings (list[str], optional): Strings to search for in the page content.
18
+ Returns:
19
+ str: Extracted text content from the web page, or a warning message. Example:
20
+ - "<main text content...>"
21
+ - "No lines found for the provided search strings."
22
+ - "Warning: Empty URL provided. Operation skipped."
23
+ """
24
+
25
+ tool_name = "fetch_url"
26
+
27
+ def run(self, url: str, search_strings: list[str] = None) -> str:
28
+ if not url.strip():
29
+ self.report_warning(tr("ℹ️ Empty URL provided."), ReportAction.READ)
30
+ return tr("Warning: Empty URL provided. Operation skipped.")
31
+ self.report_action(tr("🌐 Fetch URL '{url}' ...", url=url), ReportAction.READ)
32
+ try:
33
+ response = requests.get(url, timeout=10)
34
+ response.raise_for_status()
35
+ except requests.exceptions.HTTPError as http_err:
36
+ status_code = http_err.response.status_code if http_err.response else None
37
+ if status_code and 400 <= status_code < 500:
38
+ self.report_error(
39
+ tr(
40
+ "❗ HTTP {status_code} error for URL: {url}",
41
+ status_code=status_code,
42
+ url=url,
43
+ ),
44
+ ReportAction.READ,
45
+ )
46
+ return tr(
47
+ "Warning: HTTP {status_code} error for URL: {url}",
48
+ status_code=status_code,
49
+ url=url,
50
+ )
51
+ else:
52
+ self.report_error(
53
+ tr(
54
+ "❗ HTTP error for URL: {url}: {err}",
55
+ url=url,
56
+ err=str(http_err),
57
+ ),
58
+ ReportAction.READ,
59
+ )
60
+ return tr(
61
+ "Warning: HTTP error for URL: {url}: {err}",
62
+ url=url,
63
+ err=str(http_err),
64
+ )
65
+ except Exception as err:
66
+ self.report_error(
67
+ tr("❗ Error fetching URL: {url}: {err}", url=url, err=str(err)),
68
+ ReportAction.READ,
69
+ )
70
+ return tr(
71
+ "Warning: Error fetching URL: {url}: {err}", url=url, err=str(err)
72
+ )
73
+ soup = BeautifulSoup(response.text, "html.parser")
74
+ text = soup.get_text(separator="\n")
75
+ if search_strings:
76
+ filtered = []
77
+ for s in search_strings:
78
+ idx = text.find(s)
79
+ if idx != -1:
80
+ start = max(0, idx - 200)
81
+ end = min(len(text), idx + len(s) + 200)
82
+ snippet = text[start:end]
83
+ filtered.append(snippet)
84
+ if filtered:
85
+ text = "\n...\n".join(filtered)
86
+ else:
87
+ text = tr("No lines found for the provided search strings.")
88
+ num_lines = len(text.splitlines())
89
+ self.report_success(
90
+ tr(
91
+ "✅ {num_lines} {line_word}",
92
+ num_lines=num_lines,
93
+ line_word=pluralize("line", num_lines),
94
+ ),
95
+ ReportAction.READ,
96
+ )
97
+ return text