janito 0.15.0__py3-none-any.whl → 1.0.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 (109) hide show
  1. janito/__init__.py +1 -5
  2. janito/__main__.py +3 -5
  3. janito/agent/__init__.py +1 -0
  4. janito/agent/agent.py +96 -0
  5. janito/agent/config.py +113 -0
  6. janito/agent/config_defaults.py +10 -0
  7. janito/agent/conversation.py +107 -0
  8. janito/agent/queued_tool_handler.py +16 -0
  9. janito/agent/runtime_config.py +30 -0
  10. janito/agent/tool_handler.py +124 -0
  11. janito/agent/tools/__init__.py +11 -0
  12. janito/agent/tools/ask_user.py +63 -0
  13. janito/agent/tools/bash_exec.py +58 -0
  14. janito/agent/tools/create_directory.py +19 -0
  15. janito/agent/tools/create_file.py +43 -0
  16. janito/agent/tools/fetch_url.py +48 -0
  17. janito/agent/tools/file_str_replace.py +48 -0
  18. janito/agent/tools/find_files.py +37 -0
  19. janito/agent/tools/gitignore_utils.py +40 -0
  20. janito/agent/tools/move_file.py +37 -0
  21. janito/agent/tools/remove_file.py +19 -0
  22. janito/agent/tools/rich_live.py +37 -0
  23. janito/agent/tools/rich_utils.py +31 -0
  24. janito/agent/tools/search_text.py +41 -0
  25. janito/agent/tools/view_file.py +34 -0
  26. janito/cli/__init__.py +0 -6
  27. janito/cli/_print_config.py +68 -0
  28. janito/cli/_utils.py +8 -0
  29. janito/cli/arg_parser.py +26 -0
  30. janito/cli/config_commands.py +131 -0
  31. janito/cli/logging_setup.py +27 -0
  32. janito/cli/main.py +39 -0
  33. janito/cli/runner.py +138 -0
  34. janito/cli_chat_shell/__init__.py +1 -0
  35. janito/cli_chat_shell/chat_loop.py +148 -0
  36. janito/cli_chat_shell/commands.py +202 -0
  37. janito/cli_chat_shell/config_shell.py +75 -0
  38. janito/cli_chat_shell/load_prompt.py +15 -0
  39. janito/cli_chat_shell/session_manager.py +60 -0
  40. janito/cli_chat_shell/ui.py +136 -0
  41. janito/render_prompt.py +12 -0
  42. janito/templates/system_instructions.j2 +38 -0
  43. janito/web/__init__.py +0 -0
  44. janito/web/__main__.py +17 -0
  45. janito/web/app.py +132 -0
  46. janito-1.0.1.dist-info/METADATA +144 -0
  47. janito-1.0.1.dist-info/RECORD +51 -0
  48. {janito-0.15.0.dist-info → janito-1.0.1.dist-info}/WHEEL +2 -1
  49. janito-1.0.1.dist-info/entry_points.txt +2 -0
  50. {janito-0.15.0.dist-info → janito-1.0.1.dist-info}/licenses/LICENSE +2 -2
  51. janito-1.0.1.dist-info/top_level.txt +1 -0
  52. janito/callbacks.py +0 -34
  53. janito/cli/agent/__init__.py +0 -7
  54. janito/cli/agent/conversation.py +0 -149
  55. janito/cli/agent/initialization.py +0 -168
  56. janito/cli/agent/query.py +0 -112
  57. janito/cli/agent.py +0 -12
  58. janito/cli/app.py +0 -178
  59. janito/cli/commands/__init__.py +0 -12
  60. janito/cli/commands/config.py +0 -30
  61. janito/cli/commands/history.py +0 -119
  62. janito/cli/commands/profile.py +0 -93
  63. janito/cli/commands/validation.py +0 -24
  64. janito/cli/commands/workspace.py +0 -31
  65. janito/cli/commands.py +0 -12
  66. janito/cli/output.py +0 -29
  67. janito/cli/utils.py +0 -22
  68. janito/config/README.md +0 -104
  69. janito/config/__init__.py +0 -16
  70. janito/config/cli/__init__.py +0 -28
  71. janito/config/cli/commands.py +0 -397
  72. janito/config/cli/validators.py +0 -77
  73. janito/config/core/__init__.py +0 -23
  74. janito/config/core/file_operations.py +0 -90
  75. janito/config/core/properties.py +0 -316
  76. janito/config/core/singleton.py +0 -282
  77. janito/config/profiles/__init__.py +0 -8
  78. janito/config/profiles/definitions.py +0 -38
  79. janito/config/profiles/manager.py +0 -80
  80. janito/data/instructions_template.txt +0 -34
  81. janito/token_report.py +0 -154
  82. janito/tools/__init__.py +0 -44
  83. janito/tools/bash/bash.py +0 -157
  84. janito/tools/bash/unix_persistent_bash.py +0 -215
  85. janito/tools/bash/win_persistent_bash.py +0 -341
  86. janito/tools/decorators.py +0 -90
  87. janito/tools/delete_file.py +0 -65
  88. janito/tools/fetch_webpage/__init__.py +0 -23
  89. janito/tools/fetch_webpage/core.py +0 -182
  90. janito/tools/find_files.py +0 -220
  91. janito/tools/move_file.py +0 -72
  92. janito/tools/prompt_user.py +0 -57
  93. janito/tools/replace_file.py +0 -63
  94. janito/tools/rich_console.py +0 -176
  95. janito/tools/search_text.py +0 -226
  96. janito/tools/str_replace_editor/__init__.py +0 -6
  97. janito/tools/str_replace_editor/editor.py +0 -55
  98. janito/tools/str_replace_editor/handlers/__init__.py +0 -16
  99. janito/tools/str_replace_editor/handlers/create.py +0 -60
  100. janito/tools/str_replace_editor/handlers/insert.py +0 -100
  101. janito/tools/str_replace_editor/handlers/str_replace.py +0 -94
  102. janito/tools/str_replace_editor/handlers/undo.py +0 -64
  103. janito/tools/str_replace_editor/handlers/view.py +0 -165
  104. janito/tools/str_replace_editor/utils.py +0 -33
  105. janito/tools/think.py +0 -37
  106. janito/tools/usage_tracker.py +0 -137
  107. janito-0.15.0.dist-info/METADATA +0 -481
  108. janito-0.15.0.dist-info/RECORD +0 -64
  109. janito-0.15.0.dist-info/entry_points.txt +0 -2
janito/__init__.py CHANGED
@@ -1,5 +1 @@
1
- """
2
- Janito package.
3
- """
4
-
5
- __version__ = "0.15.0"
1
+ __version__ = "1.0.1"
janito/__main__.py CHANGED
@@ -1,7 +1,5 @@
1
- """
2
- Main entry point for Janito.
3
- """
4
- from janito.cli import app
1
+ from janito.cli.main import main
2
+
5
3
 
6
4
  if __name__ == "__main__":
7
- app()
5
+ main()
@@ -0,0 +1 @@
1
+ from . import tools
janito/agent/agent.py ADDED
@@ -0,0 +1,96 @@
1
+ """Agent module: defines the core LLM agent with tool and conversation handling."""
2
+
3
+ import os
4
+ import json
5
+ from openai import OpenAI
6
+ from janito.agent.conversation import ConversationHandler
7
+ from janito.agent.tool_handler import ToolHandler
8
+
9
+ class Agent:
10
+ """LLM Agent capable of handling conversations and tool calls."""
11
+
12
+ REFERER = "www.janito.dev"
13
+ TITLE = "Janito"
14
+
15
+ def __init__(
16
+ self,
17
+ api_key: str,
18
+ model: str = None,
19
+ system_prompt: str | None = None,
20
+ verbose_tools: bool = False,
21
+ tool_handler = None,
22
+ base_url: str = "https://openrouter.ai/api/v1"
23
+ ):
24
+ """
25
+ Initialize the Agent.
26
+
27
+ Args:
28
+ api_key: API key for OpenAI-compatible service.
29
+ model: Model name to use.
30
+ system_prompt: Optional system prompt override.
31
+ verbose_tools: Enable verbose tool call logging.
32
+ tool_handler: Optional custom ToolHandler instance.
33
+ base_url: API base URL.
34
+ """
35
+ self.api_key = api_key
36
+ self.model = model
37
+ self.system_prompt = system_prompt
38
+ if os.environ.get("USE_AZURE_OPENAI"):
39
+ from openai import AzureOpenAI
40
+ self.client = AzureOpenAI(
41
+ api_key=api_key,
42
+ azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
43
+ api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2023-05-15"),
44
+ )
45
+ else:
46
+ self.client = OpenAI(
47
+ base_url=base_url,
48
+ api_key=api_key,
49
+ default_headers={
50
+ "HTTP-Referer": self.REFERER,
51
+ "X-Title": self.TITLE
52
+ }
53
+ )
54
+ if tool_handler is not None:
55
+ self.tool_handler = tool_handler
56
+ else:
57
+ self.tool_handler = ToolHandler(verbose=verbose_tools)
58
+
59
+ self.conversation_handler = ConversationHandler(
60
+ self.client, self.model, self.tool_handler
61
+ )
62
+
63
+ def chat(self, messages, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None):
64
+ import time
65
+ from janito.agent.conversation import ProviderError
66
+
67
+ max_retries = 5
68
+ for attempt in range(1, max_retries + 1):
69
+ try:
70
+ return self.conversation_handler.handle_conversation(
71
+ messages,
72
+ max_tokens=max_tokens,
73
+ on_content=on_content,
74
+ on_tool_progress=on_tool_progress,
75
+ verbose_response=verbose_response,
76
+ spinner=spinner
77
+ )
78
+ except ProviderError as e:
79
+ error_data = getattr(e, 'error_data', {}) or {}
80
+ code = error_data.get('code', '')
81
+ # Retry only on 5xx errors
82
+ if isinstance(code, int) and 500 <= code < 600:
83
+ pass
84
+ elif isinstance(code, str) and code.isdigit() and 500 <= int(code) < 600:
85
+ code = int(code)
86
+ else:
87
+ raise
88
+
89
+ if attempt < max_retries:
90
+ print(f"ProviderError with 5xx code encountered (attempt {attempt}/{max_retries}). Retrying in 5 seconds...")
91
+ time.sleep(5)
92
+ else:
93
+ print("Max retries reached. Raising error.")
94
+ raise
95
+ except Exception:
96
+ raise
janito/agent/config.py ADDED
@@ -0,0 +1,113 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from threading import Lock
5
+
6
+
7
+ class SingletonMeta(type):
8
+ _instances = {}
9
+ _lock: Lock = Lock()
10
+
11
+ def __call__(cls, *args, **kwargs):
12
+ with cls._lock:
13
+ if cls not in cls._instances:
14
+ instance = super().__call__(*args, **kwargs)
15
+ cls._instances[cls] = instance
16
+ return cls._instances[cls]
17
+
18
+
19
+ class BaseConfig:
20
+ def __init__(self):
21
+ self._data = {}
22
+
23
+ def get(self, key, default=None):
24
+ return self._data.get(key, default)
25
+
26
+ def set(self, key, value):
27
+ self._data[key] = value
28
+
29
+ def all(self):
30
+ return self._data
31
+
32
+
33
+
34
+
35
+ class FileConfig(BaseConfig):
36
+ def __init__(self, path):
37
+ super().__init__()
38
+ self.path = Path(path).expanduser()
39
+ self.load()
40
+
41
+ def load(self):
42
+ if self.path.exists():
43
+ try:
44
+ with open(self.path, 'r') as f:
45
+ self._data = json.load(f)
46
+ # Remove keys with value None (null in JSON)
47
+ self._data = {k: v for k, v in self._data.items() if v is not None}
48
+ except Exception as e:
49
+ print(f"Warning: Failed to load config file {self.path}: {e}")
50
+ self._data = {}
51
+ else:
52
+ self._data = {}
53
+
54
+ def save(self):
55
+ self.path.parent.mkdir(parents=True, exist_ok=True)
56
+ with open(self.path, 'w') as f:
57
+ json.dump(self._data, f, indent=2)
58
+
59
+
60
+ CONFIG_OPTIONS = {
61
+ "api_key": "API key for OpenAI-compatible service (required)",
62
+ "model": "Model name to use (e.g., 'openrouter/optimus-alpha')",
63
+ "base_url": "API base URL (OpenAI-compatible endpoint)",
64
+ "role": "Role description for the system prompt (e.g., 'software engineer')",
65
+ "system_prompt": "Override the entire system prompt text",
66
+ "temperature": "Sampling temperature (float, e.g., 0.0 - 2.0)",
67
+ "max_tokens": "Maximum tokens for model response (int)"
68
+ }
69
+
70
+ # Import defaults for reference
71
+ from .config_defaults import CONFIG_DEFAULTS
72
+
73
+ class EffectiveConfig:
74
+ """Read-only merged view of local and global configs"""
75
+ def __init__(self, local_cfg, global_cfg):
76
+ self.local_cfg = local_cfg
77
+ self.global_cfg = global_cfg
78
+
79
+ def get(self, key, default=None):
80
+ from .config_defaults import CONFIG_DEFAULTS
81
+ for cfg in (self.local_cfg, self.global_cfg):
82
+ val = cfg.get(key)
83
+ if val is not None:
84
+ # Treat explicit None/null as not set
85
+ if val is None:
86
+ continue
87
+ return val
88
+ # Use centralized defaults if no config found
89
+ if default is None and key in CONFIG_DEFAULTS:
90
+ return CONFIG_DEFAULTS[key]
91
+ return default
92
+
93
+ def all(self):
94
+ merged = {}
95
+ # Start with global, override with local
96
+ for cfg in (self.global_cfg, self.local_cfg):
97
+ merged.update(cfg.all())
98
+ return merged
99
+
100
+
101
+ # Singleton instances
102
+
103
+ local_config = FileConfig(Path('.janito/config.json'))
104
+ global_config = FileConfig(Path.home() / '.janito/config.json')
105
+
106
+ effective_config = EffectiveConfig(local_config, global_config)
107
+
108
+ def get_api_key():
109
+ """Retrieve API key from config files (local, then global)."""
110
+ api_key = effective_config.get("api_key")
111
+ if api_key:
112
+ return api_key
113
+ raise ValueError("API key not found. Please configure 'api_key' in your config.")
@@ -0,0 +1,10 @@
1
+ # Centralized config defaults for Janito
2
+ CONFIG_DEFAULTS = {
3
+ "api_key": None, # Must be set by user
4
+ "model": "openrouter/optimus-alpha", # Default model
5
+ "base_url": "https://openrouter.ai/api/v1",
6
+ "role": "software engineer",
7
+ "system_prompt": None, # None means auto-generate from role
8
+ "temperature": 0.2,
9
+ "max_tokens": 200000,
10
+ }
@@ -0,0 +1,107 @@
1
+ import json
2
+
3
+ class MaxRoundsExceededError(Exception):
4
+ pass
5
+
6
+ class EmptyResponseError(Exception):
7
+ pass
8
+
9
+ class ProviderError(Exception):
10
+ def __init__(self, message, error_data):
11
+ self.error_data = error_data
12
+ super().__init__(message)
13
+
14
+ class ConversationHandler:
15
+ def __init__(self, client, model, tool_handler):
16
+ self.client = client
17
+ self.model = model
18
+ self.tool_handler = tool_handler
19
+
20
+ def handle_conversation(self, messages, max_rounds=50, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None):
21
+ if not messages:
22
+ raise ValueError("No prompt provided in messages")
23
+
24
+ from rich.console import Console
25
+ console = Console()
26
+
27
+ from janito.agent.runtime_config import unified_config
28
+
29
+ # Resolve max_tokens priority: runtime param > config > default
30
+ resolved_max_tokens = max_tokens
31
+ if resolved_max_tokens is None:
32
+ resolved_max_tokens = unified_config.get('max_tokens', 200000)
33
+
34
+ for _ in range(max_rounds):
35
+ if spinner:
36
+ # Calculate word count for all messages
37
+ word_count = sum(len(str(m.get('content', '')).split()) for m in messages if 'content' in m)
38
+ spinner_msg = f"[bold green]Waiting for AI response... ({word_count} words in conversation)"
39
+ with console.status(spinner_msg, spinner="dots") as status:
40
+ response = self.client.chat.completions.create(
41
+ model=self.model,
42
+ messages=messages,
43
+ tools=self.tool_handler.get_tool_schemas(),
44
+ tool_choice="auto",
45
+ temperature=0.2,
46
+ max_tokens=resolved_max_tokens
47
+ )
48
+ status.stop()
49
+ # console.print("\r\033[2K", end="") # Clear the spinner line removed
50
+ else:
51
+ response = self.client.chat.completions.create(
52
+ model=self.model,
53
+ messages=messages,
54
+ tools=self.tool_handler.get_tool_schemas(),
55
+ tool_choice="auto",
56
+ temperature=0.2,
57
+ max_tokens=resolved_max_tokens
58
+ )
59
+
60
+ if verbose_response:
61
+ import pprint
62
+ pprint.pprint(response)
63
+
64
+ # Check for provider errors
65
+ if hasattr(response, 'error') and response.error:
66
+ error_msg = response.error.get('message', 'Unknown provider error')
67
+ error_code = response.error.get('code', 'unknown')
68
+ raise ProviderError(f"Provider error: {error_msg} (Code: {error_code})", response.error)
69
+
70
+ if not response.choices:
71
+ raise EmptyResponseError("The LLM API returned no choices in the response.")
72
+
73
+ choice = response.choices[0]
74
+
75
+ # Extract token usage info if available
76
+ usage = getattr(response, 'usage', None)
77
+ if usage:
78
+ usage_info = {
79
+ 'prompt_tokens': getattr(usage, 'prompt_tokens', None),
80
+ 'completion_tokens': getattr(usage, 'completion_tokens', None),
81
+ 'total_tokens': getattr(usage, 'total_tokens', None)
82
+ }
83
+ else:
84
+ usage_info = None
85
+
86
+ # Call the on_content callback if provided and content is not None
87
+ if on_content is not None and choice.message.content is not None:
88
+ on_content({"content": choice.message.content})
89
+
90
+ # If no tool calls, return the assistant's message and usage info
91
+ if not choice.message.tool_calls:
92
+ return {
93
+ "content": choice.message.content,
94
+ "usage": usage_info
95
+ }
96
+
97
+ tool_responses = []
98
+ for tool_call in choice.message.tool_calls:
99
+ result = self.tool_handler.handle_tool_call(tool_call, on_progress=on_tool_progress)
100
+ tool_responses.append({"tool_call_id": tool_call.id, "content": result})
101
+
102
+ messages.append({"role": "assistant", "content": choice.message.content, "tool_calls": [tc.to_dict() for tc in choice.message.tool_calls]})
103
+
104
+ for tr in tool_responses:
105
+ messages.append({"role": "tool", "tool_call_id": tr["tool_call_id"], "content": tr["content"]})
106
+
107
+ raise MaxRoundsExceededError("Max conversation rounds exceeded")
@@ -0,0 +1,16 @@
1
+ from janito.agent.tool_handler import ToolHandler
2
+
3
+ class QueuedToolHandler(ToolHandler):
4
+ def __init__(self, queue, *args, **kwargs):
5
+ super().__init__(*args, **kwargs)
6
+ self._queue = queue
7
+
8
+ def handle_tool_call(self, tool_call, on_progress=None):
9
+ def enqueue_progress(data):
10
+
11
+ self._queue.put(('tool_progress', data))
12
+
13
+ if on_progress is None:
14
+ on_progress = enqueue_progress
15
+
16
+ return super().handle_tool_call(tool_call, on_progress=on_progress)
@@ -0,0 +1,30 @@
1
+ from .config import BaseConfig, EffectiveConfig, effective_config
2
+
3
+ class RuntimeConfig(BaseConfig):
4
+ """In-memory only config, reset on restart"""
5
+ pass
6
+
7
+ runtime_config = RuntimeConfig()
8
+
9
+ class UnifiedConfig:
10
+ """
11
+ Config lookup order:
12
+ 1. runtime_config (in-memory, highest priority)
13
+ 2. effective_config (local/global, read-only)
14
+ """
15
+ def __init__(self, runtime_cfg, effective_cfg):
16
+ self.runtime_cfg = runtime_cfg
17
+ self.effective_cfg = effective_cfg
18
+
19
+ def get(self, key, default=None):
20
+ val = self.runtime_cfg.get(key)
21
+ if val is not None:
22
+ return val
23
+ return self.effective_cfg.get(key, default)
24
+
25
+ def all(self):
26
+ merged = dict(self.effective_cfg.all())
27
+ merged.update(self.runtime_cfg.all())
28
+ return merged
29
+
30
+ unified_config = UnifiedConfig(runtime_config, effective_config)
@@ -0,0 +1,124 @@
1
+ import os
2
+ import json
3
+ import traceback
4
+
5
+ class ToolHandler:
6
+ _tool_registry = {}
7
+
8
+ @classmethod
9
+ def register_tool(cls, func):
10
+ import inspect
11
+
12
+ name = func.__name__
13
+ description = func.__doc__ or ""
14
+
15
+ sig = inspect.signature(func)
16
+ params_schema = {
17
+ "type": "object",
18
+ "properties": {},
19
+ "required": []
20
+ }
21
+
22
+ for param_name, param in sig.parameters.items():
23
+ if param.annotation is param.empty:
24
+ raise TypeError(f"Parameter '{param_name}' in tool '{name}' is missing a type hint.")
25
+ param_type = param.annotation
26
+ json_type = "string"
27
+ if param_type == int:
28
+ json_type = "integer"
29
+ elif param_type == float:
30
+ json_type = "number"
31
+ elif param_type == bool:
32
+ json_type = "boolean"
33
+ elif param_type == dict:
34
+ json_type = "object"
35
+ elif param_type == list:
36
+ json_type = "array"
37
+ params_schema["properties"][param_name] = {"type": json_type}
38
+ if param.default is param.empty:
39
+ params_schema["required"].append(param_name)
40
+
41
+ cls._tool_registry[name] = {
42
+ "function": func,
43
+ "description": description,
44
+ "parameters": params_schema
45
+ }
46
+ return func
47
+
48
+ def __init__(self, verbose=False):
49
+ self.verbose = verbose
50
+ self.tools = []
51
+
52
+ def register(self, func):
53
+ self.tools.append(func)
54
+ return func
55
+
56
+ def get_tools(self):
57
+ return self.tools
58
+
59
+ def get_tool_schemas(self):
60
+ schemas = []
61
+ for name, entry in self._tool_registry.items():
62
+ schemas.append({
63
+ "type": "function",
64
+ "function": {
65
+ "name": name,
66
+ "description": entry["description"],
67
+ "parameters": entry["parameters"]
68
+ }
69
+ })
70
+ return schemas
71
+
72
+ def handle_tool_call(self, tool_call, on_progress=None):
73
+ import uuid
74
+ call_id = getattr(tool_call, 'id', None) or str(uuid.uuid4())
75
+ tool_entry = self._tool_registry.get(tool_call.function.name)
76
+ if not tool_entry:
77
+ return f"Unknown tool: {tool_call.function.name}"
78
+ func = tool_entry["function"]
79
+ args = json.loads(tool_call.function.arguments)
80
+ if self.verbose:
81
+ print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
82
+ try:
83
+ import inspect
84
+ sig = inspect.signature(func)
85
+ if on_progress:
86
+ on_progress({
87
+ 'event': 'start',
88
+ 'call_id': call_id,
89
+ 'tool': tool_call.function.name,
90
+ 'args': args
91
+ })
92
+ if 'on_progress' in sig.parameters and on_progress is not None:
93
+ args['on_progress'] = on_progress
94
+ result = func(**args)
95
+ if self.verbose:
96
+ preview = result
97
+ if isinstance(result, str):
98
+ lines = result.splitlines()
99
+ if len(lines) > 10:
100
+ preview = "\n".join(lines[:10]) + "\n... (truncated)"
101
+ elif len(result) > 500:
102
+ preview = result[:500] + "... (truncated)"
103
+ print(f"[Tool Result] {tool_call.function.name} returned:\n{preview}")
104
+ if on_progress:
105
+ on_progress({
106
+ 'event': 'finish',
107
+ 'call_id': call_id,
108
+ 'tool': tool_call.function.name,
109
+ 'args': args,
110
+ 'result': result
111
+ })
112
+ return result
113
+ except Exception as e:
114
+ tb = traceback.format_exc()
115
+ if on_progress:
116
+ on_progress({
117
+ 'event': 'finish',
118
+ 'call_id': call_id,
119
+ 'tool': tool_call.function.name,
120
+ 'args': args,
121
+ 'error': str(e),
122
+ 'traceback': tb
123
+ })
124
+ return f"Error running tool {tool_call.function.name}: {e}"
@@ -0,0 +1,11 @@
1
+ from .ask_user import ask_user
2
+ from .create_directory import create_directory
3
+ from .create_file import create_file
4
+ from .remove_file import remove_file
5
+ from .view_file import view_file
6
+ from .find_files import find_files
7
+ from .search_text import search_text
8
+ from .bash_exec import bash_exec
9
+ from .fetch_url import fetch_url
10
+ from .move_file import move_file
11
+ from .file_str_replace import file_str_replace
@@ -0,0 +1,63 @@
1
+ from janito.agent.tool_handler import ToolHandler
2
+ from prompt_toolkit import PromptSession
3
+ from prompt_toolkit.key_binding import KeyBindings
4
+ from prompt_toolkit.enums import EditingMode
5
+ from prompt_toolkit.formatted_text import HTML
6
+ from prompt_toolkit.styles import Style
7
+
8
+
9
+ @ToolHandler.register_tool
10
+ def ask_user(question: str) -> str:
11
+ """
12
+ Ask the user a question and return their response.
13
+
14
+ question: The question to ask the user
15
+ """
16
+ from rich import print as rich_print
17
+ from rich.panel import Panel
18
+
19
+ rich_print(Panel.fit(question, title="Question", style="cyan"))
20
+
21
+ bindings = KeyBindings()
22
+
23
+ mode = {'multiline': False}
24
+
25
+ @bindings.add('c-r')
26
+ def _(event):
27
+ # Disable reverse search
28
+ pass
29
+
30
+ style = Style.from_dict({
31
+ 'bottom-toolbar': 'bg:#333333 #ffffff',
32
+ 'b': 'bold',
33
+ 'prompt': 'bold bg:#000080 #ffffff',
34
+ })
35
+
36
+ def get_toolbar():
37
+ if mode['multiline']:
38
+ return HTML('<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>')
39
+ else:
40
+ return HTML('<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>')
41
+
42
+ session = PromptSession(
43
+ multiline=False,
44
+ key_bindings=bindings,
45
+ editing_mode=EditingMode.EMACS,
46
+ bottom_toolbar=get_toolbar,
47
+ style=style
48
+ )
49
+
50
+ prompt_icon = HTML('<prompt>💬 </prompt>')
51
+
52
+ while True:
53
+ response = session.prompt(prompt_icon)
54
+ if not mode['multiline'] and response.strip() == '/multi':
55
+ mode['multiline'] = True
56
+ session.multiline = True
57
+ continue
58
+ elif mode['multiline'] and response.strip() == '/single':
59
+ mode['multiline'] = False
60
+ session.multiline = False
61
+ continue
62
+ else:
63
+ return response
@@ -0,0 +1,58 @@
1
+ from janito.agent.tool_handler import ToolHandler
2
+ from janito.agent.tools.rich_utils import print_info, print_bash_stdout, print_bash_stderr
3
+ import subprocess
4
+ import threading
5
+ from typing import Callable, Optional
6
+
7
+
8
+ @ToolHandler.register_tool
9
+ def bash_exec(command: str, on_progress: Optional[Callable[[dict], None]] = None) -> str:
10
+ """
11
+ command: The Bash command to execute.
12
+ on_progress: Optional callback function for streaming progress updates.
13
+
14
+ Execute a non interactive bash command and print output live.
15
+
16
+ Returns:
17
+ str: A formatted message string containing stdout, stderr, and return code.
18
+ """
19
+ print_info(f"[bash_exec] Executing command: {command}")
20
+ result = {'stdout': '', 'stderr': '', 'returncode': None}
21
+
22
+ def run_command():
23
+ try:
24
+ process = subprocess.Popen(
25
+ command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace'
26
+ )
27
+ stdout_lines = []
28
+ stderr_lines = []
29
+
30
+ def read_stream(stream, collector, print_func, stream_name):
31
+ for line in iter(stream.readline, ''):
32
+ collector.append(line)
33
+ print_func(line.rstrip())
34
+ if callable(on_progress):
35
+ on_progress({'stream': stream_name, 'line': line.rstrip()})
36
+ stream.close()
37
+
38
+ stdout_thread = threading.Thread(target=read_stream, args=(process.stdout, stdout_lines, print_bash_stdout, 'stdout'))
39
+ stderr_thread = threading.Thread(target=read_stream, args=(process.stderr, stderr_lines, print_bash_stderr, 'stderr'))
40
+ stdout_thread.start()
41
+ stderr_thread.start()
42
+ stdout_thread.join()
43
+ stderr_thread.join()
44
+ result['returncode'] = process.wait()
45
+ result['stdout'] = ''.join(stdout_lines)
46
+ result['stderr'] = ''.join(stderr_lines)
47
+ except Exception as e:
48
+ result['stderr'] = str(e)
49
+ result['returncode'] = -1
50
+
51
+ thread = threading.Thread(target=run_command)
52
+ thread.start()
53
+ thread.join() # Wait for the thread to finish
54
+
55
+ print_info(f"[bash_exec] Command execution completed.")
56
+ print_info(f"[bash_exec] Return code: {result['returncode']}")
57
+
58
+ return f"stdout:\n{result['stdout']}\nstderr:\n{result['stderr']}\nreturncode: {result['returncode']}"
@@ -0,0 +1,19 @@
1
+ import os
2
+ from janito.agent.tool_handler import ToolHandler
3
+ from janito.agent.tools.rich_utils import print_info, print_success, print_error, format_path
4
+
5
+ @ToolHandler.register_tool
6
+ def create_directory(path: str) -> str:
7
+ """
8
+ Create a directory at the specified path.
9
+
10
+ path: The path of the directory to create
11
+ """
12
+ print_info(f"📁 Creating directory: '{format_path(path)}' ... ")
13
+ try:
14
+ os.makedirs(path, exist_ok=True)
15
+ print_success("✅ Success")
16
+ return f"✅ Directory '{path}' created successfully."
17
+ except Exception as e:
18
+ print_error(f"❌ Error: {e}")
19
+ return f"❌ Error creating directory '{path}': {e}"