janito 1.8.1__py3-none-any.whl → 1.10.0__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 (142) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -3
  5. janito/agent/config_utils.py +0 -9
  6. janito/agent/conversation.py +177 -114
  7. janito/agent/conversation_api.py +179 -159
  8. janito/agent/conversation_tool_calls.py +11 -8
  9. janito/agent/llm_conversation_history.py +70 -0
  10. janito/agent/openai_client.py +44 -21
  11. janito/agent/openai_schema_generator.py +164 -128
  12. janito/agent/platform_discovery.py +134 -77
  13. janito/agent/profile_manager.py +5 -5
  14. janito/agent/rich_message_handler.py +80 -31
  15. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
  16. janito/agent/test_openai_schema_generator.py +93 -0
  17. janito/agent/tool_base.py +7 -2
  18. janito/agent/tool_executor.py +63 -50
  19. janito/agent/tool_registry.py +5 -2
  20. janito/agent/tool_use_tracker.py +42 -5
  21. janito/agent/tools/__init__.py +13 -12
  22. janito/agent/tools/create_directory.py +9 -6
  23. janito/agent/tools/create_file.py +35 -54
  24. janito/agent/tools/delete_text_in_file.py +97 -0
  25. janito/agent/tools/fetch_url.py +50 -5
  26. janito/agent/tools/find_files.py +40 -26
  27. janito/agent/tools/get_file_outline/__init__.py +1 -0
  28. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
  29. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  30. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
  31. janito/agent/tools/get_lines.py +21 -12
  32. janito/agent/tools/move_file.py +13 -12
  33. janito/agent/tools/present_choices.py +3 -1
  34. janito/agent/tools/python_command_runner.py +150 -0
  35. janito/agent/tools/python_file_runner.py +148 -0
  36. janito/agent/tools/python_stdin_runner.py +154 -0
  37. janito/agent/tools/remove_directory.py +4 -2
  38. janito/agent/tools/remove_file.py +15 -13
  39. janito/agent/tools/replace_file.py +72 -0
  40. janito/agent/tools/replace_text_in_file.py +7 -5
  41. janito/agent/tools/run_bash_command.py +29 -72
  42. janito/agent/tools/run_powershell_command.py +142 -102
  43. janito/agent/tools/search_text.py +177 -131
  44. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  45. janito/agent/tools/validate_file_syntax/core.py +94 -0
  46. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  47. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  48. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  49. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  50. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  51. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  52. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  53. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  54. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  55. janito/agent/tools_utils/__init__.py +1 -0
  56. janito/agent/tools_utils/action_type.py +7 -0
  57. janito/agent/tools_utils/dir_walk_utils.py +24 -0
  58. janito/agent/tools_utils/formatting.py +49 -0
  59. janito/agent/tools_utils/gitignore_utils.py +69 -0
  60. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  61. janito/agent/tools_utils/utils.py +30 -0
  62. janito/cli/_livereload_log_utils.py +13 -0
  63. janito/cli/_print_config.py +63 -61
  64. janito/cli/arg_parser.py +57 -14
  65. janito/cli/cli_main.py +270 -0
  66. janito/cli/livereload_starter.py +60 -0
  67. janito/cli/main.py +166 -99
  68. janito/cli/one_shot.py +80 -0
  69. janito/cli/termweb_starter.py +2 -2
  70. janito/i18n/__init__.py +1 -1
  71. janito/livereload/app.py +25 -0
  72. janito/rich_utils.py +41 -25
  73. janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
  74. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  75. janito/shell/commands/conversation_restart.py +74 -0
  76. janito/shell/commands/edit.py +24 -0
  77. janito/shell/commands/history_view.py +18 -0
  78. janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
  79. janito/shell/commands/livelogs.py +42 -0
  80. janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
  81. janito/shell/commands/session.py +35 -0
  82. janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
  83. janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
  84. janito/shell/commands/tools.py +26 -0
  85. janito/shell/commands/track.py +36 -0
  86. janito/shell/commands/utility.py +28 -0
  87. janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
  88. janito/shell/commands.py +40 -0
  89. janito/shell/input_history.py +62 -0
  90. janito/shell/main.py +257 -0
  91. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  92. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  93. janito/shell/session/manager.py +101 -0
  94. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
  95. janito/termweb/app.py +3 -3
  96. janito/termweb/static/editor.css +142 -0
  97. janito/termweb/static/editor.css.bak +27 -0
  98. janito/termweb/static/editor.html +15 -213
  99. janito/termweb/static/editor.html.bak +16 -215
  100. janito/termweb/static/editor.js +209 -0
  101. janito/termweb/static/editor.js.bak +227 -0
  102. janito/termweb/static/index.html +2 -3
  103. janito/termweb/static/index.html.bak +2 -3
  104. janito/termweb/static/termweb.css.bak +33 -84
  105. janito/termweb/static/termweb.js +15 -34
  106. janito/termweb/static/termweb.js.bak +18 -36
  107. janito/tests/test_rich_utils.py +44 -0
  108. janito/web/app.py +0 -75
  109. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
  110. janito-1.10.0.dist-info/RECORD +158 -0
  111. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  112. janito/agent/tools/dir_walk_utils.py +0 -16
  113. janito/agent/tools/gitignore_utils.py +0 -46
  114. janito/agent/tools/memory.py +0 -48
  115. janito/agent/tools/outline_file/formatting.py +0 -20
  116. janito/agent/tools/outline_file/python_outline.py +0 -71
  117. janito/agent/tools/present_choices_test.py +0 -18
  118. janito/agent/tools/rich_live.py +0 -44
  119. janito/agent/tools/run_python_command.py +0 -163
  120. janito/agent/tools/tools_utils.py +0 -56
  121. janito/agent/tools/utils.py +0 -33
  122. janito/agent/tools/validate_file_syntax.py +0 -163
  123. janito/cli/runner/cli_main.py +0 -180
  124. janito/cli_chat_shell/chat_loop.py +0 -163
  125. janito/cli_chat_shell/chat_state.py +0 -38
  126. janito/cli_chat_shell/commands/history_start.py +0 -37
  127. janito/cli_chat_shell/commands/session.py +0 -48
  128. janito/cli_chat_shell/commands/sum.py +0 -49
  129. janito/cli_chat_shell/commands/utility.py +0 -32
  130. janito/cli_chat_shell/session_manager.py +0 -72
  131. janito-1.8.1.dist-info/RECORD +0 -127
  132. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  133. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  134. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  135. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  136. /janito/{cli/runner → shell}/__init__.py +0 -0
  137. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  138. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  139. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  140. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  141. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  142. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,16 @@ from janito.agent.message_handler_protocol import MessageHandlerProtocol
5
5
  console = Console()
6
6
 
7
7
 
8
+ def _print_metadata_if_verbose(msg):
9
+ if unified_config.get("verbose_messages", False):
10
+ # Exclude 'message' field from metadata
11
+ meta = {k: v for k, v in msg.items() if k != "message"}
12
+ if meta:
13
+ console.print("[bold][cyan]Message metadata:[/cyan][/bold]")
14
+ for k, v in meta.items():
15
+ console.print(f" [green]{k}[/green]: [magenta]{v}[/magenta]")
16
+
17
+
8
18
  class RichMessageHandler(MessageHandlerProtocol):
9
19
  """
10
20
  Unified message handler for all output (tool, agent, system) using Rich for styled output.
@@ -18,13 +28,10 @@ class RichMessageHandler(MessageHandlerProtocol):
18
28
  Handles a dict with 'type' and 'message'.
19
29
  All messages must be dicts. Raises if not.
20
30
  """
21
- # Check trust config: suppress all output except 'content' if enabled
22
31
  trust = runtime_config.get("trust")
23
32
  if trust is None:
24
33
  trust = unified_config.get("trust", False)
25
34
 
26
- from rich.markdown import Markdown
27
-
28
35
  if not isinstance(msg, dict):
29
36
  raise TypeError(
30
37
  f"RichMessageHandler.handle_message expects a dict with 'type' and 'message', got {type(msg)}: {msg!r}"
@@ -35,32 +42,74 @@ class RichMessageHandler(MessageHandlerProtocol):
35
42
 
36
43
  if trust and msg_type != "content":
37
44
  return # Suppress all except content
38
- if msg_type == "content":
39
- self.console.print(Markdown(message))
40
- elif msg_type == "info":
41
- self.console.print(message, style="cyan", end="")
42
- elif msg_type == "success":
43
- self.console.print(message, style="bold green", end="\n")
44
- elif msg_type == "error":
45
- self.console.print(message, style="bold red", end="\n")
46
- elif msg_type == "progress":
47
- self._handle_progress(message)
48
- elif msg_type == "warning":
49
- self.console.print(message, style="bold yellow", end="\n")
50
- elif msg_type == "stdout":
51
- from rich.text import Text
52
-
53
- self.console.print(
54
- Text(message, style="on #003300", no_wrap=True, overflow=None),
55
- end="",
56
- )
57
- elif msg_type == "stderr":
58
- from rich.text import Text
59
45
 
60
- self.console.print(
61
- Text(message, style="on #330000", no_wrap=True, overflow=None),
62
- end="",
63
- )
64
- else:
65
- # Ignore unsupported message types silently
66
- return
46
+ handler_map = {
47
+ "content": self._handle_content,
48
+ "info": self._handle_info,
49
+ "success": self._handle_success,
50
+ "error": self._handle_error,
51
+ "progress": self._handle_progress,
52
+ "warning": self._handle_warning,
53
+ "stdout": self._handle_stdout,
54
+ "stderr": self._handle_stderr,
55
+ }
56
+
57
+ handler = handler_map.get(msg_type)
58
+ if handler:
59
+ handler(msg, message)
60
+ # Ignore unsupported message types silently
61
+
62
+ def _handle_content(self, msg, message):
63
+ from rich.markdown import Markdown
64
+
65
+ _print_metadata_if_verbose(msg)
66
+ self.console.print(Markdown(message))
67
+
68
+ def _handle_info(self, msg, message):
69
+ action_type = msg.get("action_type", None)
70
+ style = "cyan" # default
71
+ action_type_name = action_type.name if action_type else None
72
+ if action_type_name == "READ":
73
+ style = "cyan"
74
+ elif action_type_name == "WRITE":
75
+ style = "bright_magenta"
76
+ elif action_type_name == "EXECUTE":
77
+ style = "yellow"
78
+ _print_metadata_if_verbose(msg)
79
+ self.console.print(f" {message}", style=style, end="")
80
+
81
+ def _handle_success(self, msg, message):
82
+ _print_metadata_if_verbose(msg)
83
+ self.console.print(message, style="bold green", end="\n")
84
+
85
+ def _handle_error(self, msg, message):
86
+ _print_metadata_if_verbose(msg)
87
+ self.console.print(message, style="bold red", end="\n")
88
+
89
+ def _handle_progress(self, msg, message=None):
90
+ _print_metadata_if_verbose(msg)
91
+ # Existing logic for progress messages (if any)
92
+ # Placeholder: implement as needed
93
+ pass
94
+
95
+ def _handle_warning(self, msg, message):
96
+ _print_metadata_if_verbose(msg)
97
+ self.console.print(message, style="bold yellow", end="\n")
98
+
99
+ def _handle_stdout(self, msg, message):
100
+ from rich.text import Text
101
+
102
+ _print_metadata_if_verbose(msg)
103
+ self.console.print(
104
+ Text(message, style="on #003300", no_wrap=True, overflow=None),
105
+ end="",
106
+ )
107
+
108
+ def _handle_stderr(self, msg, message):
109
+ from rich.text import Text
110
+
111
+ _print_metadata_if_verbose(msg)
112
+ self.console.print(
113
+ Text(message, style="on #330000", no_wrap=True, overflow=None),
114
+ end="",
115
+ )
@@ -1,14 +1,15 @@
1
- You are acting as: {{ role }}
1
+ You are: {{ role }}
2
2
 
3
- You have access for development purposes to the following environment:
3
+ You will be developing and testing in the following environment:
4
4
  Platform: {{ platform }}
5
5
  Python version: {{ python_version }}
6
6
  Shell/Environment: {{ shell_info }}
7
7
 
8
- Answer according to the following guidelines:
9
- - Start by searching for text/files in the project.
8
+ Respond according to the following guidelines:
9
+ - Before answering check for files that might be related to the question
10
10
  - Before using your namespace functions, provide a concise explanation.
11
- - Prefer making localized edits using string replacements. If the required change is extensive, replace the entire file instead.
12
- - When locating code lines to change, consider the previous lines content for indentation precise replacement.
13
- - After edting files, validate them.
14
-
11
+ - When planning to get the entire file content, set the range line numbers to None
12
+ - Do not provide the code in the messages, use the namespace functions to provide the code changes.
13
+ - Prefer making localized edits using string replacements. If the required change is extensive, replace the entire file instead, provide full content without placeholders.
14
+ - Before creating files search the code for the location related to the file purpose
15
+ - Once development or updates are finished, ensure that all new or updated packages, modules, functions, and methods
@@ -0,0 +1,93 @@
1
+ """
2
+ Tests for OpenAISchemaGenerator class-based API.
3
+ """
4
+
5
+ import pytest
6
+ from janito.agent.openai_schema_generator import OpenAISchemaGenerator
7
+
8
+
9
+ class DummyTool:
10
+ """
11
+ Dummy tool for testing.
12
+
13
+ Args:
14
+ foo (str): Foo parameter.
15
+ bar (int): Bar parameter.
16
+ Returns:
17
+ The result as a string.
18
+ """
19
+
20
+ name = "dummy_tool"
21
+
22
+ def run(self, foo: str, bar: int) -> str:
23
+ """Run the dummy tool."""
24
+ return f"{foo}-{bar}"
25
+
26
+
27
+ # Simulate decorator metadata for tests
28
+ DummyTool._tool_run_method = DummyTool().run
29
+ DummyTool._tool_name = DummyTool.name
30
+
31
+
32
+ def test_generate_schema_success():
33
+ generator = OpenAISchemaGenerator()
34
+ tool = DummyTool
35
+ schema = generator.generate_schema(tool)
36
+ assert schema["name"] == tool.name
37
+ assert "foo" in schema["parameters"]["properties"]
38
+ assert "bar" in schema["parameters"]["properties"]
39
+ assert schema["parameters"]["properties"]["foo"]["type"] == "string"
40
+ assert schema["parameters"]["properties"]["bar"]["type"] == "integer"
41
+ assert schema["description"].startswith("Dummy tool for testing.")
42
+
43
+
44
+ def test_generate_schema_missing_type():
45
+ class BadTool:
46
+ """
47
+ Args:
48
+ foo (str): Foo parameter.
49
+ Returns:
50
+ String result.
51
+ """
52
+
53
+ name = "bad_tool"
54
+
55
+ def run(self, foo):
56
+ return str(foo)
57
+
58
+ BadTool._tool_run_method = BadTool().run
59
+ BadTool._tool_name = BadTool.name
60
+ generator = OpenAISchemaGenerator()
61
+ with pytest.raises(ValueError):
62
+ generator.generate_schema(BadTool)
63
+
64
+
65
+ def test_generate_schema_missing_doc():
66
+ class BadTool2:
67
+ """
68
+ Args:
69
+ foo (str): Foo parameter.
70
+ Returns:
71
+ String result.
72
+ """
73
+
74
+ name = "bad_tool2"
75
+
76
+ def run(self, foo: str, bar: int) -> str:
77
+ return str(foo)
78
+
79
+ BadTool2._tool_run_method = BadTool2().run
80
+ BadTool2._tool_name = BadTool2.name
81
+ generator = OpenAISchemaGenerator()
82
+ with pytest.raises(ValueError):
83
+ generator.generate_schema(BadTool2)
84
+
85
+
86
+ def test_generate_schema_requires_metadata():
87
+ class NotRegisteredTool:
88
+ def run(self, foo: str) -> str:
89
+ return foo
90
+
91
+ generator = OpenAISchemaGenerator()
92
+ with pytest.raises(ValueError):
93
+ generator.generate_schema(NotRegisteredTool)
janito/agent/tool_base.py CHANGED
@@ -37,9 +37,14 @@ class ToolBase(ABC):
37
37
  if hasattr(self, "_progress_callback") and self._progress_callback:
38
38
  self._progress_callback(progress)
39
39
 
40
- def report_info(self, message: str):
40
+ def report_info(self, action_type, message: str):
41
41
  self.update_progress(
42
- {"type": "info", "tool": self.__class__.__name__, "message": message}
42
+ {
43
+ "type": "info",
44
+ "tool": self.__class__.__name__,
45
+ "action_type": action_type,
46
+ "message": message,
47
+ }
43
48
  )
44
49
 
45
50
  def report_success(self, message: str):
@@ -3,7 +3,6 @@
3
3
  ToolExecutor: Responsible for executing tools, validating arguments, handling errors, and reporting progress.
4
4
  """
5
5
 
6
- import json
7
6
  from janito.i18n import tr
8
7
  import inspect
9
8
  from janito.agent.tool_base import ToolBase
@@ -14,24 +13,35 @@ class ToolExecutor:
14
13
  def __init__(self, message_handler=None):
15
14
  self.message_handler = message_handler
16
15
 
17
- def execute(self, tool_entry, tool_call):
16
+ def execute(self, tool_entry, tool_call, arguments):
18
17
  import uuid
19
18
 
20
- call_id = getattr(tool_call, "id", None) or str(uuid.uuid4())
19
+ call_id = getattr(tool_call, "id", None)
20
+ if call_id is None:
21
+ raise ValueError("Tool call is missing required 'id' from server.")
21
22
  func = tool_entry["function"]
22
- args = json.loads(tool_call.function.arguments)
23
- tool_call_reason = args.pop(
24
- "tool_call_reason", None
25
- ) # Extract and remove 'tool_call_reason' if present
26
- # Record tool usage
27
- try:
28
- from janito.agent.tool_use_tracker import ToolUseTracker
23
+ args = arguments
24
+ if runtime_config.get("no_tools_tracking", False):
25
+ tool_call_reason = None
26
+ else:
27
+ tool_call_reason = args.pop(
28
+ "tool_call_reason", None
29
+ ) # Extract and remove 'tool_call_reason' if present
29
30
 
30
- ToolUseTracker().record(tool_call.function.name, dict(args))
31
+ self._maybe_log_tool_call(tool_call, args, tool_call_reason)
32
+ instance = self._maybe_set_progress_callback(func)
33
+ self._emit_tool_call_event(tool_call, call_id, args, tool_call_reason)
34
+ self._validate_arguments(func, args, tool_call, call_id, tool_call_reason)
35
+ try:
36
+ result = func(**args)
37
+ self._emit_tool_result_event(tool_call, call_id, result, tool_call_reason)
38
+ self._record_tool_usage(tool_call, args, result)
39
+ return result
31
40
  except Exception as e:
32
- if runtime_config.get("verbose", False):
33
- print(f"[ToolExecutor] ToolUseTracker record failed: {e}")
41
+ self._emit_tool_error_event(tool_call, call_id, str(e), tool_call_reason)
42
+ raise
34
43
 
44
+ def _maybe_log_tool_call(self, tool_call, args, tool_call_reason):
35
45
  verbose = runtime_config.get("verbose", False)
36
46
  if verbose:
37
47
  print(
@@ -48,12 +58,16 @@ class ToolExecutor:
48
58
  tool_call_reason=tool_call_reason,
49
59
  )
50
60
  )
61
+
62
+ def _maybe_set_progress_callback(self, func):
51
63
  instance = None
52
64
  if hasattr(func, "__self__") and isinstance(func.__self__, ToolBase):
53
65
  instance = func.__self__
54
66
  if self.message_handler:
55
67
  instance._progress_callback = self.message_handler.handle_message
56
- # Emit tool_call event before calling the tool
68
+ return instance
69
+
70
+ def _emit_tool_call_event(self, tool_call, call_id, args, tool_call_reason):
57
71
  if self.message_handler:
58
72
  event = {
59
73
  "type": "tool_call",
@@ -61,49 +75,48 @@ class ToolExecutor:
61
75
  "call_id": call_id,
62
76
  "arguments": args,
63
77
  }
64
- if tool_call_reason:
78
+ if tool_call_reason and not runtime_config.get("no_tools_tracking", False):
65
79
  event["tool_call_reason"] = tool_call_reason
66
80
  self.message_handler.handle_message(event)
67
- # Argument validation
81
+
82
+ def _validate_arguments(self, func, args, tool_call, call_id, tool_call_reason):
68
83
  sig = inspect.signature(func)
69
84
  try:
70
85
  sig.bind(**args)
71
86
  except TypeError as e:
72
87
  error_msg = f"Argument validation error for tool '{tool_call.function.name}': {str(e)}"
73
- if self.message_handler:
74
- error_event = {
75
- "type": "tool_error",
76
- "tool": tool_call.function.name,
77
- "call_id": call_id,
78
- "error": error_msg,
79
- }
80
- if tool_call_reason:
81
- error_event["tool_call_reason"] = tool_call_reason
82
- self.message_handler.handle_message(error_event)
88
+ self._emit_tool_error_event(tool_call, call_id, error_msg, tool_call_reason)
83
89
  raise TypeError(error_msg)
84
- # Execute tool
90
+
91
+ def _emit_tool_result_event(self, tool_call, call_id, result, tool_call_reason):
92
+ if self.message_handler:
93
+ result_event = {
94
+ "type": "tool_result",
95
+ "tool": tool_call.function.name,
96
+ "call_id": call_id,
97
+ "result": result,
98
+ }
99
+ if tool_call_reason and not runtime_config.get("no_tools_tracking", False):
100
+ result_event["tool_call_reason"] = tool_call_reason
101
+ self.message_handler.handle_message(result_event)
102
+
103
+ def _emit_tool_error_event(self, tool_call, call_id, error, tool_call_reason):
104
+ if self.message_handler:
105
+ error_event = {
106
+ "type": "tool_error",
107
+ "tool": tool_call.function.name,
108
+ "call_id": call_id,
109
+ "error": error,
110
+ }
111
+ if tool_call_reason and not runtime_config.get("no_tools_tracking", False):
112
+ error_event["tool_call_reason"] = tool_call_reason
113
+ self.message_handler.handle_message(error_event)
114
+
115
+ def _record_tool_usage(self, tool_call, args, result):
85
116
  try:
86
- result = func(**args)
87
- if self.message_handler:
88
- result_event = {
89
- "type": "tool_result",
90
- "tool": tool_call.function.name,
91
- "call_id": call_id,
92
- "result": result,
93
- }
94
- if tool_call_reason:
95
- result_event["tool_call_reason"] = tool_call_reason
96
- self.message_handler.handle_message(result_event)
97
- return result
117
+ from janito.agent.tool_use_tracker import ToolUseTracker
118
+
119
+ ToolUseTracker().record(tool_call.function.name, dict(args), result)
98
120
  except Exception as e:
99
- if self.message_handler:
100
- error_event = {
101
- "type": "tool_error",
102
- "tool": tool_call.function.name,
103
- "call_id": call_id,
104
- "error": str(e),
105
- }
106
- if tool_call_reason:
107
- error_event["tool_call_reason"] = tool_call_reason
108
- self.message_handler.handle_message(error_event)
109
- raise
121
+ if runtime_config.get("verbose", False):
122
+ print(f"[ToolExecutor] ToolUseTracker record failed: {e}")
@@ -1,6 +1,6 @@
1
1
  # janito/agent/tool_registry.py
2
2
  from janito.agent.tool_base import ToolBase
3
- from janito.agent.openai_schema_generator import generate_openai_function_schema
3
+ from janito.agent.openai_schema_generator import OpenAISchemaGenerator
4
4
 
5
5
  _tool_registry = {}
6
6
 
@@ -17,9 +17,12 @@ def register_tool(tool=None, *, name: str = None):
17
17
  f"Tool '{tool.__name__}' must implement a callable 'call' method."
18
18
  )
19
19
  tool_name = override_name or instance.name
20
+ # Add metadata for schema generation
21
+ tool._tool_run_method = instance.run
22
+ tool._tool_name = tool_name
20
23
  if tool_name in _tool_registry:
21
24
  raise ValueError(f"Tool '{tool_name}' is already registered.")
22
- schema = generate_openai_function_schema(instance.run, tool_name, tool_class=tool)
25
+ schema = OpenAISchemaGenerator().generate_schema(tool)
23
26
  _tool_registry[tool_name] = {
24
27
  "function": instance.run,
25
28
  "description": schema["description"],
@@ -1,7 +1,14 @@
1
1
  import threading
2
+ import os
2
3
  from typing import Any, Dict, List
3
4
 
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
+
5
12
  class ToolUseTracker:
6
13
  _instance = None
7
14
  _lock = threading.Lock()
@@ -14,25 +21,39 @@ class ToolUseTracker:
14
21
  cls._instance._history = []
15
22
  return cls._instance
16
23
 
17
- def record(self, tool_name: str, params: Dict[str, Any]):
18
- self._history.append({"tool": tool_name, "params": params})
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
+ )
19
32
 
20
33
  def get_history(self) -> List[Dict[str, Any]]:
21
34
  return list(self._history)
22
35
 
23
36
  def get_operations_on_file(self, file_path: str) -> List[Dict[str, Any]]:
37
+ norm_file_path = normalize_path(file_path)
24
38
  ops = []
25
39
  for entry in self._history:
26
40
  params = entry["params"]
27
- if any(isinstance(v, str) and file_path in v for v in params.values()):
28
- ops.append(entry)
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
29
46
  return ops
30
47
 
31
48
  def file_fully_read(self, file_path: str) -> bool:
49
+ norm_file_path = normalize_path(file_path)
32
50
  for entry in self._history:
33
51
  if entry["tool"] == "get_lines":
34
52
  params = entry["params"]
35
- if params.get("file_path") == file_path:
53
+ if (
54
+ "file_path" in params
55
+ and normalize_path(params["file_path"]) == norm_file_path
56
+ ):
36
57
  # If both from_line and to_line are None, full file was read
37
58
  if (
38
59
  params.get("from_line") is None
@@ -41,6 +62,22 @@ class ToolUseTracker:
41
62
  return True
42
63
  return False
43
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"] == "replace_file":
71
+ return True
72
+ if last["tool"] == "get_lines":
73
+ params = last["params"]
74
+ if params.get("from_line") is None and params.get("to_line") is None:
75
+ return True
76
+ return False
77
+
78
+ def clear_history(self):
79
+ self._history.clear()
80
+
44
81
  @classmethod
45
82
  def instance(cls):
46
83
  return cls()
@@ -1,22 +1,24 @@
1
1
  from . import ask_user
2
2
  from . import create_directory
3
3
  from . import create_file
4
+ from . import replace_file
4
5
  from . import fetch_url
5
6
  from . import find_files
6
7
  from . import get_lines
7
- from . import outline_file
8
- from . import gitignore_utils
8
+ from .get_file_outline import core # noqa: F401,F811
9
9
  from . import move_file
10
- from . import validate_file_syntax
10
+ from .validate_file_syntax import core # noqa: F401,F811
11
11
  from . import remove_directory
12
12
  from . import remove_file
13
13
  from . import replace_text_in_file
14
- from . import rich_live
14
+ from . import delete_text_in_file
15
15
  from . import run_bash_command
16
16
  from . import run_powershell_command
17
- from . import run_python_command
18
17
  from . import present_choices
19
18
  from . import search_text
19
+ from . import python_command_runner
20
+ from . import python_file_runner
21
+ from . import python_stdin_runner
20
22
 
21
23
  __all__ = [
22
24
  "ask_user",
@@ -24,21 +26,20 @@ __all__ = [
24
26
  "create_file",
25
27
  "fetch_url",
26
28
  "find_files",
27
- "outline_file",
29
+ "GetFileOutlineTool",
28
30
  "get_lines",
29
- "gitignore_utils",
30
31
  "move_file",
31
32
  "validate_file_syntax",
32
33
  "remove_directory",
33
34
  "remove_file",
35
+ "replace_file",
34
36
  "replace_text_in_file",
35
- "rich_live",
37
+ "delete_text_in_file",
36
38
  "run_bash_command",
37
39
  "run_powershell_command",
38
- "run_python_command",
39
- "search_files",
40
- "tools_utils",
41
- "memory",
42
40
  "present_choices",
43
41
  "search_text",
42
+ "python_command_runner",
43
+ "python_file_runner",
44
+ "python_stdin_runner",
44
45
  ]
@@ -1,6 +1,9 @@
1
1
  from janito.agent.tool_registry import register_tool
2
- from janito.agent.tools.utils import expand_path, display_path
2
+
3
+ # from janito.agent.tools_utils.expand_path import expand_path
4
+ from janito.agent.tools_utils.utils import display_path
3
5
  from janito.agent.tool_base import ToolBase
6
+ from janito.agent.tools_utils.action_type import ActionType
4
7
  from janito.i18n import tr
5
8
  import os
6
9
 
@@ -18,10 +21,12 @@ class CreateDirectoryTool(ToolBase):
18
21
  """
19
22
 
20
23
  def run(self, file_path: str) -> str:
21
- file_path = expand_path(file_path)
24
+ # file_path = expand_path(file_path)
25
+ # Using file_path as is
22
26
  disp_path = display_path(file_path)
23
27
  self.report_info(
24
- tr("📁 Creating directory: '{disp_path}' ...", disp_path=disp_path)
28
+ ActionType.WRITE,
29
+ tr("📁 Creating directory '{disp_path}' ...", disp_path=disp_path),
25
30
  )
26
31
  try:
27
32
  if os.path.exists(file_path):
@@ -47,9 +52,7 @@ class CreateDirectoryTool(ToolBase):
47
52
  disp_path=disp_path,
48
53
  )
49
54
  os.makedirs(file_path, exist_ok=True)
50
- self.report_success(
51
- tr("✅ Directory created at '{disp_path}'", disp_path=disp_path)
52
- )
55
+ self.report_success(tr("✅ Directory created"))
53
56
  return tr(
54
57
  "✅ Successfully created the directory at '{disp_path}'.",
55
58
  disp_path=disp_path,