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,58 +1,58 @@
1
- import attr
2
- from typing import Any, ClassVar
3
- from janito.event_bus.event import Event
4
-
5
-
6
- @attr.s(auto_attribs=True, kw_only=True)
7
- class ToolEvent(Event):
8
- """
9
- Base class for events related to tool calls (external or internal tools).
10
- Includes tool name and request ID for correlation.
11
- """
12
-
13
- category: ClassVar[str] = "tool"
14
- tool_name: str
15
- request_id: str
16
-
17
-
18
- @attr.s(auto_attribs=True, kw_only=True)
19
- class ToolCallStarted(ToolEvent):
20
- """
21
- Event indicating that a tool call has started.
22
- Contains the arguments passed to the tool.
23
- """
24
-
25
- arguments: Any
26
-
27
-
28
- @attr.s(auto_attribs=True, kw_only=True)
29
- class ToolCallFinished(ToolEvent):
30
- """
31
- Event indicating that a tool call has finished.
32
- Contains the result returned by the tool.
33
- """
34
-
35
- result: Any
36
-
37
-
38
- @attr.s(auto_attribs=True, kw_only=True)
39
- class ToolRunError(ToolEvent):
40
- """
41
- Event indicating that an error occurred during tool execution (for event bus, not exception handling).
42
- """
43
-
44
- error: str
45
- exception: Exception = None
46
- arguments: Any = None
47
-
48
-
49
- @attr.s(auto_attribs=True, kw_only=True)
50
- class ToolCallError(ToolEvent):
51
- """
52
- Event indicating that the tool could not be called (e.g., tool not found, invalid arguments, or invocation failure).
53
- This is distinct from ToolRunError, which is for errors during execution after the tool has started running.
54
- """
55
-
56
- error: str
57
- exception: Exception = None
58
- arguments: Any = None
1
+ import attr
2
+ from typing import Any, ClassVar
3
+ from janito.event_bus.event import Event
4
+
5
+
6
+ @attr.s(auto_attribs=True, kw_only=True)
7
+ class ToolEvent(Event):
8
+ """
9
+ Base class for events related to tool calls (external or internal tools).
10
+ Includes tool name and request ID for correlation.
11
+ """
12
+
13
+ category: ClassVar[str] = "tool"
14
+ tool_name: str
15
+ request_id: str
16
+
17
+
18
+ @attr.s(auto_attribs=True, kw_only=True)
19
+ class ToolCallStarted(ToolEvent):
20
+ """
21
+ Event indicating that a tool call has started.
22
+ Contains the arguments passed to the tool.
23
+ """
24
+
25
+ arguments: Any
26
+
27
+
28
+ @attr.s(auto_attribs=True, kw_only=True)
29
+ class ToolCallFinished(ToolEvent):
30
+ """
31
+ Event indicating that a tool call has finished.
32
+ Contains the result returned by the tool.
33
+ """
34
+
35
+ result: Any
36
+
37
+
38
+ @attr.s(auto_attribs=True, kw_only=True)
39
+ class ToolRunError(ToolEvent):
40
+ """
41
+ Event indicating that an error occurred during tool execution (for event bus, not exception handling).
42
+ """
43
+
44
+ error: str
45
+ exception: Exception = None
46
+ arguments: Any = None
47
+
48
+
49
+ @attr.s(auto_attribs=True, kw_only=True)
50
+ class ToolCallError(ToolEvent):
51
+ """
52
+ Event indicating that the tool could not be called (e.g., tool not found, invalid arguments, or invocation failure).
53
+ This is distinct from ToolRunError, which is for errors during execution after the tool has started running.
54
+ """
55
+
56
+ error: str
57
+ exception: Exception = None
58
+ arguments: Any = None
@@ -1,12 +1,12 @@
1
- class ToolRunException(Exception):
2
- """
3
- Exception raised when a tool runs but fails due to an internal error or runtime exception.
4
- This is distinct from ToolRunError event, which is for event bus notification.
5
- """
6
-
7
- def __init__(self, tool_name, error, arguments=None, exception=None):
8
- self.tool_name = tool_name
9
- self.error = error
10
- self.arguments = arguments
11
- self.original_exception = exception
12
- super().__init__(f"ToolRunException: {tool_name}: {error}")
1
+ class ToolRunException(Exception):
2
+ """
3
+ Exception raised when a tool runs but fails due to an internal error or runtime exception.
4
+ This is distinct from ToolRunError event, which is for event bus notification.
5
+ """
6
+
7
+ def __init__(self, tool_name, error, arguments=None, exception=None):
8
+ self.tool_name = tool_name
9
+ self.error = error
10
+ self.arguments = arguments
11
+ self.original_exception = exception
12
+ super().__init__(f"ToolRunException: {tool_name}: {error}")
@@ -1,81 +1,81 @@
1
- import threading
2
- import os
3
- from typing import Any, Dict, List
4
-
5
-
6
- def normalize_path(path: str) -> str:
7
- if not isinstance(path, str):
8
- return path
9
- return os.path.normcase(os.path.abspath(path))
10
-
11
-
12
- class ToolUseTracker:
13
- _instance = None
14
- _lock = threading.Lock()
15
-
16
- def __new__(cls):
17
- if not cls._instance:
18
- with cls._lock:
19
- if not cls._instance:
20
- cls._instance = super().__new__(cls)
21
- cls._instance._history = []
22
- return cls._instance
23
-
24
- def record(self, tool_name: str, params: Dict[str, Any], result: Any = None):
25
- # Normalize file_path in params if present
26
- norm_params = params.copy()
27
- if "file_path" in norm_params:
28
- norm_params["file_path"] = normalize_path(norm_params["file_path"])
29
- self._history.append(
30
- {"tool": tool_name, "params": norm_params, "result": result}
31
- )
32
-
33
- def get_history(self) -> List[Dict[str, Any]]:
34
- return list(self._history)
35
-
36
- def get_operations_on_file(self, file_path: str) -> List[Dict[str, Any]]:
37
- norm_file_path = normalize_path(file_path)
38
- ops = []
39
- for entry in self._history:
40
- params = entry["params"]
41
- # Normalize any string param values for comparison
42
- for v in params.values():
43
- if isinstance(v, str) and normalize_path(v) == norm_file_path:
44
- ops.append(entry)
45
- break
46
- return ops
47
-
48
- def file_fully_read(self, file_path: str) -> bool:
49
- norm_file_path = normalize_path(file_path)
50
- for entry in self._history:
51
- if entry["tool"] == "view_file":
52
- params = entry["params"]
53
- if (
54
- "file_path" in params
55
- and normalize_path(params["file_path"]) == norm_file_path
56
- ):
57
- # If both from_line and to_line are None, full file was read
58
- if (
59
- params.get("from_line") is None
60
- and params.get("to_line") is None
61
- ):
62
- return True
63
- return False
64
-
65
- def last_operation_is_full_read_or_replace(self, file_path: str) -> bool:
66
- ops = self.get_operations_on_file(file_path)
67
- if not ops:
68
- return False
69
- last = ops[-1]
70
- if last["tool"] == "view_file":
71
- params = last["params"]
72
- if params.get("from_line") is None and params.get("to_line") is None:
73
- return True
74
- return False
75
-
76
- def clear_history(self):
77
- self._history.clear()
78
-
79
- @classmethod
80
- def instance(cls):
81
- return cls()
1
+ import threading
2
+ import os
3
+ from typing import Any, Dict, List
4
+
5
+
6
+ def normalize_path(path: str) -> str:
7
+ if not isinstance(path, str):
8
+ return path
9
+ return os.path.normcase(os.path.abspath(path))
10
+
11
+
12
+ class ToolUseTracker:
13
+ _instance = None
14
+ _lock = threading.Lock()
15
+
16
+ def __new__(cls):
17
+ if not cls._instance:
18
+ with cls._lock:
19
+ if not cls._instance:
20
+ cls._instance = super().__new__(cls)
21
+ cls._instance._history = []
22
+ return cls._instance
23
+
24
+ def record(self, tool_name: str, params: Dict[str, Any], result: Any = None):
25
+ # Normalize file_path in params if present
26
+ norm_params = params.copy()
27
+ if "file_path" in norm_params:
28
+ norm_params["file_path"] = normalize_path(norm_params["file_path"])
29
+ self._history.append(
30
+ {"tool": tool_name, "params": norm_params, "result": result}
31
+ )
32
+
33
+ def get_history(self) -> List[Dict[str, Any]]:
34
+ return list(self._history)
35
+
36
+ def get_operations_on_file(self, file_path: str) -> List[Dict[str, Any]]:
37
+ norm_file_path = normalize_path(file_path)
38
+ ops = []
39
+ for entry in self._history:
40
+ params = entry["params"]
41
+ # Normalize any string param values for comparison
42
+ for v in params.values():
43
+ if isinstance(v, str) and normalize_path(v) == norm_file_path:
44
+ ops.append(entry)
45
+ break
46
+ return ops
47
+
48
+ def file_fully_read(self, file_path: str) -> bool:
49
+ norm_file_path = normalize_path(file_path)
50
+ for entry in self._history:
51
+ if entry["tool"] == "view_file":
52
+ params = entry["params"]
53
+ if (
54
+ "file_path" in params
55
+ and normalize_path(params["file_path"]) == norm_file_path
56
+ ):
57
+ # If both from_line and to_line are None, full file was read
58
+ if (
59
+ params.get("from_line") is None
60
+ and params.get("to_line") is None
61
+ ):
62
+ return True
63
+ return False
64
+
65
+ def last_operation_is_full_read_or_replace(self, file_path: str) -> bool:
66
+ ops = self.get_operations_on_file(file_path)
67
+ if not ops:
68
+ return False
69
+ last = ops[-1]
70
+ if last["tool"] == "view_file":
71
+ params = last["params"]
72
+ if params.get("from_line") is None and params.get("to_line") is None:
73
+ return True
74
+ return False
75
+
76
+ def clear_history(self):
77
+ self._history.clear()
78
+
79
+ @classmethod
80
+ def instance(cls):
81
+ return cls()
@@ -1,45 +1,45 @@
1
- """
2
- Utility functions for the janito project.
3
- Add your shared helper functions here.
4
- """
5
-
6
- import os
7
- import urllib.parse
8
-
9
-
10
- def example_utility_function(x):
11
- """A simple example utility function."""
12
- return f"Processed: {x}"
13
-
14
-
15
- def display_path(path):
16
- """
17
- Returns a display-friendly path. Injects an ANSI hyperlink to a local web file viewer using a hardcoded port.
18
- Args:
19
- path (str): Path to display.
20
- Returns:
21
- str: Display path, as an ANSI hyperlink.
22
- """
23
- from janito.cli.config import get_termweb_port
24
-
25
- port = get_termweb_port()
26
- if os.path.isabs(path):
27
- cwd = os.path.abspath(os.getcwd())
28
- abs_path = os.path.abspath(path)
29
- # Check if the absolute path is within the current working directory
30
- if abs_path.startswith(cwd + os.sep):
31
- disp = os.path.relpath(abs_path, cwd)
32
- else:
33
- disp = path
34
- else:
35
- disp = os.path.relpath(path)
36
- url = f"http://localhost:{port}/?path={urllib.parse.quote(path)}"
37
- # Use Rich markup for hyperlinks
38
- return f"[link={url}]{disp}[/link]"
39
-
40
-
41
- def pluralize(word: str, count: int) -> str:
42
- """Return the pluralized form of word if count != 1, unless word already ends with 's'."""
43
- if count == 1 or word.endswith("s"):
44
- return word
45
- return word + "s"
1
+ """
2
+ Utility functions for the janito project.
3
+ Add your shared helper functions here.
4
+ """
5
+
6
+ import os
7
+ import urllib.parse
8
+
9
+
10
+ def example_utility_function(x):
11
+ """A simple example utility function."""
12
+ return f"Processed: {x}"
13
+
14
+
15
+ def display_path(path):
16
+ """
17
+ Returns a display-friendly path. Injects an ANSI hyperlink to a local web file viewer using a hardcoded port.
18
+ Args:
19
+ path (str): Path to display.
20
+ Returns:
21
+ str: Display path, as an ANSI hyperlink.
22
+ """
23
+ from janito.cli.config import get_termweb_port
24
+
25
+ port = get_termweb_port()
26
+ if os.path.isabs(path):
27
+ cwd = os.path.abspath(os.getcwd())
28
+ abs_path = os.path.abspath(path)
29
+ # Check if the absolute path is within the current working directory
30
+ if abs_path.startswith(cwd + os.sep):
31
+ disp = os.path.relpath(abs_path, cwd)
32
+ else:
33
+ disp = path
34
+ else:
35
+ disp = os.path.relpath(path)
36
+ url = f"http://localhost:{port}/?path={urllib.parse.quote(path)}"
37
+ # Use Rich markup for hyperlinks
38
+ return f"[link={url}]{disp}[/link]"
39
+
40
+
41
+ def pluralize(word: str, count: int) -> str:
42
+ """Return the pluralized form of word if count != 1, unless word already ends with 's'."""
43
+ if count == 1 or word.endswith("s"):
44
+ return word
45
+ return word + "s"
@@ -1,104 +1,104 @@
1
- import inspect
2
- import typing
3
- import re
4
-
5
-
6
- class ToolSchemaBase:
7
- def parse_param_section(self, lines, param_section_headers):
8
- param_descs = {}
9
- in_params = False
10
- for line in lines:
11
- stripped_line = line.strip()
12
- if any(
13
- stripped_line.lower().startswith(h + ":") or stripped_line.lower() == h
14
- for h in param_section_headers
15
- ):
16
- in_params = True
17
- continue
18
- if in_params:
19
- m = re.match(
20
- r"([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\(([^)]+)\))?\s*[:\-]?\s*(.+)",
21
- stripped_line,
22
- )
23
- if m:
24
- param, _, desc = m.groups()
25
- param_descs[param] = desc.strip()
26
- elif stripped_line and stripped_line[0] != "-":
27
- if param_descs:
28
- last = list(param_descs)[-1]
29
- param_descs[last] += " " + stripped_line
30
- if (
31
- stripped_line.lower().startswith("returns:")
32
- or stripped_line.lower() == "returns"
33
- ):
34
- break
35
- return param_descs
36
-
37
- def parse_return_section(self, lines):
38
- in_returns = False
39
- return_desc = ""
40
- for line in lines:
41
- stripped_line = line.strip()
42
- if (
43
- stripped_line.lower().startswith("returns:")
44
- or stripped_line.lower() == "returns"
45
- ):
46
- in_returns = True
47
- continue
48
- if in_returns:
49
- if stripped_line:
50
- return_desc += (" " if return_desc else "") + stripped_line
51
- return return_desc
52
-
53
- def parse_docstring(self, docstring: str):
54
- if not docstring:
55
- return "", {}, ""
56
- lines = docstring.strip().split("\n")
57
- summary = lines[0].strip()
58
- param_section_headers = ("args", "arguments", "params", "parameters")
59
- param_descs = self.parse_param_section(lines[1:], param_section_headers)
60
- return_desc = self.parse_return_section(lines[1:])
61
- return summary, param_descs, return_desc
62
-
63
- def validate_tool_class(self, tool_class):
64
- if not hasattr(tool_class, "tool_name") or not isinstance(
65
- tool_class.tool_name, str
66
- ):
67
- raise ValueError(
68
- "Tool class must have a class-level 'tool_name' attribute (str) for registry and schema generation."
69
- )
70
- if not hasattr(tool_class, "run") or not callable(getattr(tool_class, "run")):
71
- raise ValueError("Tool class must have a callable 'run' method.")
72
- func = tool_class.run
73
- tool_name = tool_class.tool_name
74
- sig = inspect.signature(func)
75
- if sig.return_annotation is inspect._empty or sig.return_annotation is not str:
76
- raise ValueError(
77
- f"Tool '{tool_name}' must have an explicit return type of 'str'. Found: {sig.return_annotation}"
78
- )
79
- missing_type_hints = [
80
- name
81
- for name, param in sig.parameters.items()
82
- if name != "self" and param.annotation is inspect._empty
83
- ]
84
- if missing_type_hints:
85
- raise ValueError(
86
- f"Tool '{tool_name}' is missing type hints for parameter(s): {', '.join(missing_type_hints)}.\nAll parameters must have explicit type hints for schema generation."
87
- )
88
- class_doc = (
89
- tool_class.__doc__.strip() if tool_class and tool_class.__doc__ else ""
90
- )
91
- summary, param_descs, return_desc = self.parse_docstring(class_doc)
92
- description = summary
93
- if return_desc:
94
- description += f"\n\nReturns: {return_desc}"
95
- undocumented = [
96
- name
97
- for name, param in sig.parameters.items()
98
- if name != "self" and name not in param_descs
99
- ]
100
- if undocumented:
101
- raise ValueError(
102
- f"Tool '{tool_name}' is missing docstring documentation for parameter(s): {', '.join(undocumented)}.\nParameter documentation must be provided in the Tool class docstring, not the method docstring."
103
- )
104
- return func, tool_name, sig, summary, param_descs, return_desc, description
1
+ import inspect
2
+ import typing
3
+ import re
4
+
5
+
6
+ class ToolSchemaBase:
7
+ def parse_param_section(self, lines, param_section_headers):
8
+ param_descs = {}
9
+ in_params = False
10
+ for line in lines:
11
+ stripped_line = line.strip()
12
+ if any(
13
+ stripped_line.lower().startswith(h + ":") or stripped_line.lower() == h
14
+ for h in param_section_headers
15
+ ):
16
+ in_params = True
17
+ continue
18
+ if in_params:
19
+ m = re.match(
20
+ r"([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\(([^)]+)\))?\s*[:\-]?\s*(.+)",
21
+ stripped_line,
22
+ )
23
+ if m:
24
+ param, _, desc = m.groups()
25
+ param_descs[param] = desc.strip()
26
+ elif stripped_line and stripped_line[0] != "-":
27
+ if param_descs:
28
+ last = list(param_descs)[-1]
29
+ param_descs[last] += " " + stripped_line
30
+ if (
31
+ stripped_line.lower().startswith("returns:")
32
+ or stripped_line.lower() == "returns"
33
+ ):
34
+ break
35
+ return param_descs
36
+
37
+ def parse_return_section(self, lines):
38
+ in_returns = False
39
+ return_desc = ""
40
+ for line in lines:
41
+ stripped_line = line.strip()
42
+ if (
43
+ stripped_line.lower().startswith("returns:")
44
+ or stripped_line.lower() == "returns"
45
+ ):
46
+ in_returns = True
47
+ continue
48
+ if in_returns:
49
+ if stripped_line:
50
+ return_desc += (" " if return_desc else "") + stripped_line
51
+ return return_desc
52
+
53
+ def parse_docstring(self, docstring: str):
54
+ if not docstring:
55
+ return "", {}, ""
56
+ lines = docstring.strip().split("\n")
57
+ summary = lines[0].strip()
58
+ param_section_headers = ("args", "arguments", "params", "parameters")
59
+ param_descs = self.parse_param_section(lines[1:], param_section_headers)
60
+ return_desc = self.parse_return_section(lines[1:])
61
+ return summary, param_descs, return_desc
62
+
63
+ def validate_tool_class(self, tool_class):
64
+ if not hasattr(tool_class, "tool_name") or not isinstance(
65
+ tool_class.tool_name, str
66
+ ):
67
+ raise ValueError(
68
+ "Tool class must have a class-level 'tool_name' attribute (str) for registry and schema generation."
69
+ )
70
+ if not hasattr(tool_class, "run") or not callable(getattr(tool_class, "run")):
71
+ raise ValueError("Tool class must have a callable 'run' method.")
72
+ func = tool_class.run
73
+ tool_name = tool_class.tool_name
74
+ sig = inspect.signature(func)
75
+ if sig.return_annotation is inspect._empty or sig.return_annotation is not str:
76
+ raise ValueError(
77
+ f"Tool '{tool_name}' must have an explicit return type of 'str'. Found: {sig.return_annotation}"
78
+ )
79
+ missing_type_hints = [
80
+ name
81
+ for name, param in sig.parameters.items()
82
+ if name != "self" and param.annotation is inspect._empty
83
+ ]
84
+ if missing_type_hints:
85
+ raise ValueError(
86
+ f"Tool '{tool_name}' is missing type hints for parameter(s): {', '.join(missing_type_hints)}.\nAll parameters must have explicit type hints for schema generation."
87
+ )
88
+ class_doc = (
89
+ tool_class.__doc__.strip() if tool_class and tool_class.__doc__ else ""
90
+ )
91
+ summary, param_descs, return_desc = self.parse_docstring(class_doc)
92
+ description = summary
93
+ if return_desc:
94
+ description += f"\n\nReturns: {return_desc}"
95
+ undocumented = [
96
+ name
97
+ for name, param in sig.parameters.items()
98
+ if name != "self" and name not in param_descs
99
+ ]
100
+ if undocumented:
101
+ raise ValueError(
102
+ f"Tool '{tool_name}' is missing docstring documentation for parameter(s): {', '.join(undocumented)}.\nParameter documentation must be provided in the Tool class docstring, not the method docstring."
103
+ )
104
+ return func, tool_name, sig, summary, param_descs, return_desc, description
janito/version.py CHANGED
@@ -1,4 +1,4 @@
1
- # janito/version.py
2
- """Single source of truth for the janito package version."""
3
-
4
- __version__ = "2.3.0"
1
+ # janito/version.py
2
+ """Single source of truth for the janito package version."""
3
+
4
+ __version__ = "2.3.1"