janito 1.6.0__py3-none-any.whl → 1.8.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 (117) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config.py +3 -3
  3. janito/agent/config_defaults.py +3 -2
  4. janito/agent/conversation.py +73 -27
  5. janito/agent/conversation_api.py +104 -4
  6. janito/agent/conversation_exceptions.py +6 -0
  7. janito/agent/conversation_tool_calls.py +17 -3
  8. janito/agent/event.py +24 -0
  9. janito/agent/event_dispatcher.py +24 -0
  10. janito/agent/event_handler_protocol.py +5 -0
  11. janito/agent/event_system.py +15 -0
  12. janito/agent/message_handler.py +4 -1
  13. janito/agent/message_handler_protocol.py +5 -0
  14. janito/agent/openai_client.py +5 -6
  15. janito/agent/openai_schema_generator.py +23 -4
  16. janito/agent/platform_discovery.py +90 -0
  17. janito/agent/profile_manager.py +34 -110
  18. janito/agent/queued_message_handler.py +22 -3
  19. janito/agent/rich_message_handler.py +3 -1
  20. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +14 -0
  21. janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +13 -0
  22. janito/agent/test_handler_protocols.py +47 -0
  23. janito/agent/tests/__init__.py +1 -0
  24. janito/agent/tool_base.py +1 -1
  25. janito/agent/tool_executor.py +109 -0
  26. janito/agent/tool_registry.py +3 -75
  27. janito/agent/tool_use_tracker.py +46 -0
  28. janito/agent/tools/__init__.py +11 -8
  29. janito/agent/tools/ask_user.py +26 -12
  30. janito/agent/tools/create_directory.py +50 -18
  31. janito/agent/tools/create_file.py +60 -29
  32. janito/agent/tools/dir_walk_utils.py +16 -0
  33. janito/agent/tools/fetch_url.py +10 -11
  34. janito/agent/tools/find_files.py +49 -40
  35. janito/agent/tools/get_lines.py +60 -25
  36. janito/agent/tools/memory.py +48 -0
  37. janito/agent/tools/move_file.py +72 -23
  38. janito/agent/tools/outline_file/__init__.py +85 -0
  39. janito/agent/tools/outline_file/formatting.py +20 -0
  40. janito/agent/tools/outline_file/markdown_outline.py +14 -0
  41. janito/agent/tools/outline_file/python_outline.py +71 -0
  42. janito/agent/tools/present_choices.py +62 -0
  43. janito/agent/tools/present_choices_test.py +18 -0
  44. janito/agent/tools/remove_directory.py +31 -26
  45. janito/agent/tools/remove_file.py +31 -13
  46. janito/agent/tools/replace_text_in_file.py +135 -36
  47. janito/agent/tools/run_bash_command.py +113 -97
  48. janito/agent/tools/run_powershell_command.py +169 -0
  49. janito/agent/tools/run_python_command.py +53 -29
  50. janito/agent/tools/search_outline.py +17 -0
  51. janito/agent/tools/search_text.py +208 -0
  52. janito/agent/tools/tools_utils.py +47 -4
  53. janito/agent/tools/utils.py +14 -15
  54. janito/agent/tools/validate_file_syntax.py +163 -0
  55. janito/cli/_print_config.py +1 -1
  56. janito/cli/arg_parser.py +36 -4
  57. janito/cli/config_commands.py +1 -1
  58. janito/cli/logging_setup.py +7 -2
  59. janito/cli/main.py +97 -3
  60. janito/cli/runner/__init__.py +0 -2
  61. janito/cli/runner/_termweb_log_utils.py +17 -0
  62. janito/cli/runner/cli_main.py +121 -89
  63. janito/cli/runner/config.py +6 -4
  64. janito/cli/termweb_starter.py +73 -0
  65. janito/cli_chat_shell/chat_loop.py +52 -13
  66. janito/cli_chat_shell/chat_state.py +1 -1
  67. janito/cli_chat_shell/chat_ui.py +2 -3
  68. janito/cli_chat_shell/commands/__init__.py +17 -6
  69. janito/cli_chat_shell/commands/{history_reset.py → history_start.py} +13 -5
  70. janito/cli_chat_shell/commands/lang.py +16 -0
  71. janito/cli_chat_shell/commands/prompt.py +42 -0
  72. janito/cli_chat_shell/commands/session_control.py +36 -1
  73. janito/cli_chat_shell/commands/sum.py +49 -0
  74. janito/cli_chat_shell/commands/termweb_log.py +86 -0
  75. janito/cli_chat_shell/commands/utility.py +5 -2
  76. janito/cli_chat_shell/commands/verbose.py +29 -0
  77. janito/cli_chat_shell/load_prompt.py +47 -8
  78. janito/cli_chat_shell/session_manager.py +9 -1
  79. janito/cli_chat_shell/shell_command_completer.py +20 -0
  80. janito/cli_chat_shell/ui.py +110 -93
  81. janito/i18n/__init__.py +35 -0
  82. janito/i18n/messages.py +23 -0
  83. janito/i18n/pt.py +46 -0
  84. janito/rich_utils.py +43 -43
  85. janito/termweb/app.py +95 -0
  86. janito/termweb/static/editor.html +238 -0
  87. janito/termweb/static/editor.html.bak +238 -0
  88. janito/termweb/static/explorer.html.bak +59 -0
  89. janito/termweb/static/favicon.ico +0 -0
  90. janito/termweb/static/favicon.ico.bak +0 -0
  91. janito/termweb/static/index.html +55 -0
  92. janito/termweb/static/index.html.bak +55 -0
  93. janito/termweb/static/index.html.bak.bak +175 -0
  94. janito/termweb/static/landing.html.bak +36 -0
  95. janito/termweb/static/termicon.svg +1 -0
  96. janito/termweb/static/termweb.css +235 -0
  97. janito/termweb/static/termweb.css.bak +286 -0
  98. janito/termweb/static/termweb.js +187 -0
  99. janito/termweb/static/termweb.js.bak +187 -0
  100. janito/termweb/static/termweb.js.bak.bak +157 -0
  101. janito/termweb/static/termweb_quickopen.js +135 -0
  102. janito/termweb/static/termweb_quickopen.js.bak +125 -0
  103. janito/web/app.py +10 -13
  104. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/METADATA +73 -32
  105. janito-1.8.0.dist-info/RECORD +127 -0
  106. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/WHEEL +1 -1
  107. janito/agent/tool_registry_core.py +0 -2
  108. janito/agent/tools/get_file_outline.py +0 -117
  109. janito/agent/tools/py_compile_file.py +0 -40
  110. janito/agent/tools/replace_file.py +0 -51
  111. janito/agent/tools/search_files.py +0 -71
  112. janito/cli/runner/scan.py +0 -44
  113. janito/cli_chat_shell/commands/system.py +0 -73
  114. janito-1.6.0.dist-info/RECORD +0 -81
  115. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/entry_points.txt +0 -0
  116. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/licenses/LICENSE +0 -0
  117. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/top_level.txt +0 -0
@@ -1,152 +1,68 @@
1
- from janito.agent.conversation import ConversationHandler
2
1
  from openai import OpenAI
3
- import jinja2
2
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
4
3
  from pathlib import Path
5
- import platform
6
- import os
4
+ from janito.agent.platform_discovery import get_platform_name, get_python_version
5
+ from janito.agent.platform_discovery import detect_shell
7
6
 
8
7
 
9
8
  class AgentProfileManager:
10
9
  REFERER = "www.janito.dev"
11
10
  TITLE = "Janito"
12
11
 
13
- def parse_style_string(self, style: str):
14
- if "-" in style:
15
- parts = style.split("-")
16
- return parts[0], parts[1:]
17
- return style, []
18
-
19
- def get_platform_name(self):
20
- sys_platform = platform.system().lower()
21
- if sys_platform.startswith("win"):
22
- return "windows"
23
- elif sys_platform.startswith("linux"):
24
- return "linux"
25
- elif sys_platform.startswith("darwin"):
26
- return "darwin"
27
- return sys_platform
28
-
29
- def get_python_version(self):
30
- return platform.python_version()
31
-
32
- def get_shell_info(self):
33
- shell = os.environ.get("SHELL")
34
- term = os.environ.get("TERM")
35
- term_program = os.environ.get("TERM_PROGRAM")
36
- if shell:
37
- info = shell
38
- elif os.environ.get("MSYSTEM"):
39
- info = f"Git Bash ({os.environ.get('MSYSTEM')})"
40
- elif os.environ.get("WSL_DISTRO_NAME"):
41
- info = f"WSL ({os.environ.get('WSL_DISTRO_NAME')})"
42
- else:
43
- comspec = os.environ.get("COMSPEC")
44
- if comspec:
45
- if "powershell" in comspec.lower():
46
- info = "PowerShell"
47
- elif "cmd" in comspec.lower():
48
- info = "cmd.exe"
49
- else:
50
- info = "Unknown shell"
51
- else:
52
- info = "Unknown shell"
53
- if term:
54
- info += f", TERM={term}"
55
- if term_program and term_program.lower() == "vscode":
56
- info += ", running in VSCode"
57
- home_dir = os.path.expanduser("~")
58
- if home_dir:
59
- info += f", HOME={home_dir}"
60
- return info
61
- return "unknown"
12
+ def set_role(self, new_role):
13
+ """Set the agent's role and force prompt re-rendering."""
14
+ self.role = new_role
15
+ self.refresh_prompt()
62
16
 
63
17
  def render_prompt(self):
64
- main_style, features = self.parse_style_string(self.interaction_style)
65
18
  base_dir = Path(__file__).parent / "templates"
66
19
  profiles_dir = base_dir / "profiles"
67
- features_dir = base_dir / "features"
68
- loader = jinja2.ChoiceLoader(
69
- [
70
- jinja2.FileSystemLoader(str(profiles_dir)),
71
- jinja2.FileSystemLoader(str(features_dir)),
72
- ]
73
- )
74
- env = jinja2.Environment(loader=loader)
75
- if main_style == "technical":
76
- main_template = "system_prompt_template_technical.j2"
20
+ if getattr(self, "lang", "en") == "pt":
21
+ main_template_name = "system_prompt_template_base_pt.txt.j2"
77
22
  else:
78
- main_template = "system_prompt_template.j2"
79
- platform_name = self.get_platform_name()
80
- python_version = self.get_python_version()
81
- shell_info = self.get_shell_info()
82
- if not features:
83
- # Inject tech.txt existence and content
84
- tech_txt_path = Path(".janito") / "tech.txt"
85
- tech_txt_exists = tech_txt_path.exists()
86
- tech_txt_content = ""
87
- if tech_txt_exists:
88
- try:
89
- tech_txt_content = tech_txt_path.read_text(encoding="utf-8")
90
- except Exception:
91
- tech_txt_content = "⚠️ Error reading janito/tech.txt."
92
- template = env.get_template(main_template)
93
- return template.render(
94
- role=self.role,
95
- interaction_mode=self.interaction_mode,
96
- platform=platform_name,
97
- python_version=python_version,
98
- shell_info=shell_info,
99
- tech_txt_exists=tech_txt_exists,
100
- tech_txt_content=tech_txt_content,
101
- )
102
- parent_template = main_template
103
- # Inject tech.txt existence and content for feature templates as well
104
- tech_txt_path = Path(".janito") / "tech.txt"
105
- tech_txt_exists = tech_txt_path.exists()
106
- tech_txt_content = ""
107
- if tech_txt_exists:
108
- try:
109
- tech_txt_content = tech_txt_path.read_text(encoding="utf-8")
110
- except Exception:
111
- tech_txt_content = "⚠️ Error reading janito/tech.txt."
23
+ main_template_name = "system_prompt_template_base.txt.j2"
24
+ platform_name = get_platform_name()
25
+ python_version = get_python_version()
26
+ shell_info = detect_shell()
27
+
112
28
  context = {
113
29
  "role": self.role,
114
30
  "interaction_mode": self.interaction_mode,
115
31
  "platform": platform_name,
116
32
  "python_version": python_version,
117
33
  "shell_info": shell_info,
118
- "tech_txt_exists": tech_txt_exists,
119
- "tech_txt_content": tech_txt_content,
120
34
  }
121
- for feature in features:
122
- feature_template = f"system_prompt_template_{feature}.j2"
123
- template = env.get_template(feature_template)
124
- context["parent_template"] = parent_template
125
- rendered = template.render(**context)
126
- parent_template = feature_template
127
- return rendered
35
+ env = Environment(
36
+ loader=FileSystemLoader(str(profiles_dir)),
37
+ autoescape=select_autoescape(["txt", "j2"]),
38
+ )
39
+ template = env.get_template(main_template_name)
40
+ prompt = template.render(**context)
41
+ return prompt
128
42
 
129
43
  def __init__(
130
44
  self,
131
45
  api_key,
132
46
  model,
133
47
  role,
134
- interaction_style,
48
+ profile_name,
135
49
  interaction_mode,
136
50
  verbose_tools,
137
51
  base_url,
138
52
  azure_openai_api_version,
139
53
  use_azure_openai,
54
+ lang="en",
140
55
  ):
141
56
  self.api_key = api_key
142
57
  self.model = model
143
58
  self.role = role
144
- self.interaction_style = interaction_style
59
+ self.profile_name = "base"
145
60
  self.interaction_mode = interaction_mode
146
61
  self.verbose_tools = verbose_tools
147
62
  self.base_url = base_url
148
63
  self.azure_openai_api_version = azure_openai_api_version
149
64
  self.use_azure_openai = use_azure_openai
65
+ self.lang = lang
150
66
  if use_azure_openai:
151
67
  from openai import AzureOpenAI
152
68
 
@@ -161,7 +77,15 @@ class AgentProfileManager:
161
77
  api_key=api_key,
162
78
  default_headers={"HTTP-Referer": self.REFERER, "X-Title": self.TITLE},
163
79
  )
164
- self.agent = ConversationHandler(self.client, model)
80
+ from janito.agent.openai_client import Agent
81
+
82
+ self.agent = Agent(
83
+ api_key=api_key,
84
+ model=model,
85
+ base_url=base_url,
86
+ use_azure_openai=use_azure_openai,
87
+ azure_openai_api_version=azure_openai_api_version,
88
+ )
165
89
  self.system_prompt_template = None
166
90
 
167
91
  def refresh_prompt(self):
@@ -1,3 +1,6 @@
1
+ from janito.i18n import tr
2
+
3
+
1
4
  class QueuedMessageHandler:
2
5
  def __init__(self, queue, *args, **kwargs):
3
6
  self._queue = queue
@@ -6,17 +9,33 @@ class QueuedMessageHandler:
6
9
  # Unified: send content (agent/LLM) messages to the frontend via queue
7
10
  if not isinstance(msg, dict):
8
11
  raise TypeError(
9
- f"QueuedMessageHandler.handle_message expects a dict with 'type' and 'message', got {type(msg)}: {msg!r}"
12
+ tr(
13
+ "QueuedMessageHandler.handle_message expects a dict with 'type' and 'message', got {msg_type}: {msg!r}",
14
+ msg_type=type(msg),
15
+ msg=msg,
16
+ )
10
17
  )
11
18
  msg_type = msg.get("type", "info")
12
19
  # For tool_call and tool_result, print and forward the full dict
13
20
  if msg_type in ("tool_call", "tool_result"):
14
- print(f"[QueuedMessageHandler] {msg_type}: {msg}")
21
+ print(
22
+ tr(
23
+ "[QueuedMessageHandler] {msg_type}: {msg}",
24
+ msg_type=msg_type,
25
+ msg=msg,
26
+ )
27
+ )
15
28
  self._queue.put(msg)
16
29
  return
17
30
  message = msg.get("message", "")
18
31
  # For normal agent/user/info messages, emit type 'content' for frontend compatibility
19
- print(f"[QueuedMessageHandler] {msg_type}: {message}")
32
+ print(
33
+ tr(
34
+ "[QueuedMessageHandler] {msg_type}: {message}",
35
+ msg_type=msg_type,
36
+ message=message,
37
+ )
38
+ )
20
39
  if msg_type == "content":
21
40
  self._queue.put({"type": "content", "content": message})
22
41
  elif msg_type == "info":
@@ -1,10 +1,11 @@
1
1
  from rich.console import Console
2
2
  from janito.agent.runtime_config import runtime_config, unified_config
3
+ from janito.agent.message_handler_protocol import MessageHandlerProtocol
3
4
 
4
5
  console = Console()
5
6
 
6
7
 
7
- class RichMessageHandler:
8
+ class RichMessageHandler(MessageHandlerProtocol):
8
9
  """
9
10
  Unified message handler for all output (tool, agent, system) using Rich for styled output.
10
11
  """
@@ -31,6 +32,7 @@ class RichMessageHandler:
31
32
 
32
33
  msg_type = msg.get("type", "info")
33
34
  message = msg.get("message", "")
35
+
34
36
  if trust and msg_type != "content":
35
37
  return # Suppress all except content
36
38
  if msg_type == "content":
@@ -0,0 +1,14 @@
1
+ You are acting as: {{ role }}
2
+
3
+ You have access for development purposes to the following environment:
4
+ Platform: {{ platform }}
5
+ Python version: {{ python_version }}
6
+ Shell/Environment: {{ shell_info }}
7
+
8
+ Answer according to the following guidelines:
9
+ - Start by searching for text/files in the project.
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
+
@@ -0,0 +1,13 @@
1
+ Você está atuando como: {{ role }}
2
+
3
+ Você tem acesso, para fins de desenvolvimento, ao seguinte ambiente:
4
+ Plataforma: {{ platform }}
5
+ Versão do Python: {{ python_version }}
6
+ Shell/Ambiente: {{ shell_info }}
7
+
8
+ Responda seguindo estas diretrizes:
9
+ - Antes de usar suas funções de namespace, forneça uma explicação concisa.
10
+ - Na primeira mensagem faça uma pesquisa no projeto.
11
+ - Prefira fazer edições localizadas usando substituições de string. Se a alteração necessária for extensa, substitua o arquivo inteiro.
12
+ - Ao localizar linhas de código para alterar, consider o conteúdo das linhas anteriores para uma substituição precisa de indentação.
13
+ - Após editar arquivos, valide-os.
@@ -0,0 +1,47 @@
1
+ from janito.agent.message_handler_protocol import MessageHandlerProtocol
2
+ from janito.agent.event_handler_protocol import EventHandlerProtocol
3
+ from janito.agent.event_dispatcher import EventDispatcher
4
+
5
+
6
+ class DummyMessageHandler(MessageHandlerProtocol):
7
+ def __init__(self):
8
+ self.last_message = None
9
+
10
+ def handle_message(self, msg, msg_type=None):
11
+ self.last_message = (msg, msg_type)
12
+
13
+
14
+ class DummyEvent:
15
+ def __init__(self, type_, payload):
16
+ self.type = type_
17
+ self.payload = payload
18
+
19
+
20
+ class DummyEventHandler(EventHandlerProtocol):
21
+ def __init__(self):
22
+ self.last_event = None
23
+
24
+ def handle_event(self, event):
25
+ self.last_event = event
26
+
27
+
28
+ def test_message_handler():
29
+ handler = DummyMessageHandler()
30
+ handler.handle_message({"type": "info", "message": "hello"}, "info")
31
+ assert handler.last_message[0]["message"] == "hello"
32
+ print("MessageHandlerProtocol test passed")
33
+
34
+
35
+ def test_event_dispatcher():
36
+ dispatcher = EventDispatcher()
37
+ handler = DummyEventHandler()
38
+ dispatcher.register("test", handler)
39
+ event = DummyEvent("test", {"foo": "bar"})
40
+ dispatcher.dispatch(event)
41
+ assert handler.last_event.payload["foo"] == "bar"
42
+ print("EventHandlerProtocol/EventDispatcher test passed")
43
+
44
+
45
+ if __name__ == "__main__":
46
+ test_message_handler()
47
+ test_event_dispatcher()
@@ -0,0 +1 @@
1
+ # Makes this directory a package for test discovery
janito/agent/tool_base.py CHANGED
@@ -17,7 +17,7 @@ class ToolBase(ABC):
17
17
  self.update_progress({"type": "stderr", "message": message})
18
18
 
19
19
  @abstractmethod
20
- def call(self, **kwargs):
20
+ def run(self, **kwargs):
21
21
  """
22
22
  Abstract call method for tool execution. Should be overridden by subclasses.
23
23
 
@@ -0,0 +1,109 @@
1
+ # janito/agent/tool_executor.py
2
+ """
3
+ ToolExecutor: Responsible for executing tools, validating arguments, handling errors, and reporting progress.
4
+ """
5
+
6
+ import json
7
+ from janito.i18n import tr
8
+ import inspect
9
+ from janito.agent.tool_base import ToolBase
10
+ from janito.agent.runtime_config import runtime_config
11
+
12
+
13
+ class ToolExecutor:
14
+ def __init__(self, message_handler=None):
15
+ self.message_handler = message_handler
16
+
17
+ def execute(self, tool_entry, tool_call):
18
+ import uuid
19
+
20
+ call_id = getattr(tool_call, "id", None) or str(uuid.uuid4())
21
+ 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
29
+
30
+ ToolUseTracker().record(tool_call.function.name, dict(args))
31
+ except Exception as e:
32
+ if runtime_config.get("verbose", False):
33
+ print(f"[ToolExecutor] ToolUseTracker record failed: {e}")
34
+
35
+ verbose = runtime_config.get("verbose", False)
36
+ if verbose:
37
+ print(
38
+ tr(
39
+ "[ToolExecutor] {tool_name} called with arguments: {args}",
40
+ tool_name=tool_call.function.name,
41
+ args=args,
42
+ )
43
+ )
44
+ if runtime_config.get("verbose_reason", False) and tool_call_reason:
45
+ print(
46
+ tr(
47
+ "[ToolExecutor] Reason for call: {tool_call_reason}",
48
+ tool_call_reason=tool_call_reason,
49
+ )
50
+ )
51
+ instance = None
52
+ if hasattr(func, "__self__") and isinstance(func.__self__, ToolBase):
53
+ instance = func.__self__
54
+ if self.message_handler:
55
+ instance._progress_callback = self.message_handler.handle_message
56
+ # Emit tool_call event before calling the tool
57
+ if self.message_handler:
58
+ event = {
59
+ "type": "tool_call",
60
+ "tool": tool_call.function.name,
61
+ "call_id": call_id,
62
+ "arguments": args,
63
+ }
64
+ if tool_call_reason:
65
+ event["tool_call_reason"] = tool_call_reason
66
+ self.message_handler.handle_message(event)
67
+ # Argument validation
68
+ sig = inspect.signature(func)
69
+ try:
70
+ sig.bind(**args)
71
+ except TypeError as e:
72
+ 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)
83
+ raise TypeError(error_msg)
84
+ # Execute tool
85
+ 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
98
+ 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
@@ -1,8 +1,6 @@
1
1
  # janito/agent/tool_registry.py
2
- import json
3
2
  from janito.agent.tool_base import ToolBase
4
3
  from janito.agent.openai_schema_generator import generate_openai_function_schema
5
- import inspect
6
4
 
7
5
  _tool_registry = {}
8
6
 
@@ -14,16 +12,16 @@ def register_tool(tool=None, *, name: str = None):
14
12
  if not (isinstance(tool, type) and issubclass(tool, ToolBase)):
15
13
  raise TypeError("Tool must be a class derived from ToolBase.")
16
14
  instance = tool()
17
- if not hasattr(instance, "call") or not callable(instance.call):
15
+ if not hasattr(instance, "run") or not callable(instance.run):
18
16
  raise TypeError(
19
17
  f"Tool '{tool.__name__}' must implement a callable 'call' method."
20
18
  )
21
19
  tool_name = override_name or instance.name
22
20
  if tool_name in _tool_registry:
23
21
  raise ValueError(f"Tool '{tool_name}' is already registered.")
24
- schema = generate_openai_function_schema(instance.call, tool_name, tool_class=tool)
22
+ schema = generate_openai_function_schema(instance.run, tool_name, tool_class=tool)
25
23
  _tool_registry[tool_name] = {
26
- "function": instance.call,
24
+ "function": instance.run,
27
25
  "description": schema["description"],
28
26
  "parameters": schema["parameters"],
29
27
  "class": tool,
@@ -46,73 +44,3 @@ def get_tool_schemas():
46
44
  }
47
45
  )
48
46
  return schemas
49
-
50
-
51
- def handle_tool_call(tool_call, message_handler=None, verbose=False):
52
- import uuid
53
-
54
- call_id = getattr(tool_call, "id", None) or str(uuid.uuid4())
55
- tool_entry = _tool_registry.get(tool_call.function.name)
56
- if not tool_entry:
57
- return f"Unknown tool: {tool_call.function.name}"
58
- func = tool_entry["function"]
59
- args = json.loads(tool_call.function.arguments)
60
- if verbose:
61
- print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
62
- instance = None
63
- if hasattr(func, "__self__") and isinstance(func.__self__, ToolBase):
64
- instance = func.__self__
65
- if message_handler:
66
- instance._progress_callback = message_handler.handle_message
67
- # Emit tool_call event before calling the tool
68
- if message_handler:
69
- message_handler.handle_message(
70
- {
71
- "type": "tool_call",
72
- "tool": tool_call.function.name,
73
- "call_id": call_id,
74
- "arguments": args,
75
- }
76
- )
77
- # --- Argument validation start ---
78
- sig = inspect.signature(func)
79
- try:
80
- sig.bind(**args)
81
- except TypeError as e:
82
- error_msg = (
83
- f"Argument validation error for tool '{tool_call.function.name}': {str(e)}"
84
- )
85
- if message_handler:
86
- message_handler.handle_message(
87
- {
88
- "type": "tool_error",
89
- "tool": tool_call.function.name,
90
- "call_id": call_id,
91
- "error": error_msg,
92
- }
93
- )
94
- raise TypeError(error_msg)
95
- # --- Argument validation end ---
96
- try:
97
- result = func(**args)
98
- if message_handler:
99
- message_handler.handle_message(
100
- {
101
- "type": "tool_result",
102
- "tool": tool_call.function.name,
103
- "call_id": call_id,
104
- "result": result,
105
- }
106
- )
107
- return result
108
- except Exception as e:
109
- if message_handler:
110
- message_handler.handle_message(
111
- {
112
- "type": "tool_error",
113
- "tool": tool_call.function.name,
114
- "call_id": call_id,
115
- "error": str(e),
116
- }
117
- )
118
- raise
@@ -0,0 +1,46 @@
1
+ import threading
2
+ from typing import Any, Dict, List
3
+
4
+
5
+ class ToolUseTracker:
6
+ _instance = None
7
+ _lock = threading.Lock()
8
+
9
+ def __new__(cls):
10
+ if not cls._instance:
11
+ with cls._lock:
12
+ if not cls._instance:
13
+ cls._instance = super().__new__(cls)
14
+ cls._instance._history = []
15
+ return cls._instance
16
+
17
+ def record(self, tool_name: str, params: Dict[str, Any]):
18
+ self._history.append({"tool": tool_name, "params": params})
19
+
20
+ def get_history(self) -> List[Dict[str, Any]]:
21
+ return list(self._history)
22
+
23
+ def get_operations_on_file(self, file_path: str) -> List[Dict[str, Any]]:
24
+ ops = []
25
+ for entry in self._history:
26
+ params = entry["params"]
27
+ if any(isinstance(v, str) and file_path in v for v in params.values()):
28
+ ops.append(entry)
29
+ return ops
30
+
31
+ def file_fully_read(self, file_path: str) -> bool:
32
+ for entry in self._history:
33
+ if entry["tool"] == "get_lines":
34
+ params = entry["params"]
35
+ if params.get("file_path") == file_path:
36
+ # If both from_line and to_line are None, full file was read
37
+ if (
38
+ params.get("from_line") is None
39
+ and params.get("to_line") is None
40
+ ):
41
+ return True
42
+ return False
43
+
44
+ @classmethod
45
+ def instance(cls):
46
+ return cls()
@@ -3,20 +3,20 @@ from . import create_directory
3
3
  from . import create_file
4
4
  from . import fetch_url
5
5
  from . import find_files
6
- from . import get_file_outline
7
6
  from . import get_lines
7
+ from . import outline_file
8
8
  from . import gitignore_utils
9
9
  from . import move_file
10
- from . import py_compile_file
10
+ from . import validate_file_syntax
11
11
  from . import remove_directory
12
12
  from . import remove_file
13
13
  from . import replace_text_in_file
14
14
  from . import rich_live
15
15
  from . import run_bash_command
16
+ from . import run_powershell_command
16
17
  from . import run_python_command
17
- from . import search_files
18
- from . import tools_utils
19
- from . import replace_file
18
+ from . import present_choices
19
+ from . import search_text
20
20
 
21
21
  __all__ = [
22
22
  "ask_user",
@@ -24,18 +24,21 @@ __all__ = [
24
24
  "create_file",
25
25
  "fetch_url",
26
26
  "find_files",
27
- "get_file_outline",
27
+ "outline_file",
28
28
  "get_lines",
29
29
  "gitignore_utils",
30
30
  "move_file",
31
- "py_compile_file",
31
+ "validate_file_syntax",
32
32
  "remove_directory",
33
33
  "remove_file",
34
34
  "replace_text_in_file",
35
35
  "rich_live",
36
36
  "run_bash_command",
37
+ "run_powershell_command",
37
38
  "run_python_command",
38
39
  "search_files",
39
40
  "tools_utils",
40
- "replace_file",
41
+ "memory",
42
+ "present_choices",
43
+ "search_text",
41
44
  ]