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.
- janito/__init__.py +1 -5
- janito/__main__.py +3 -5
- janito/agent/__init__.py +1 -0
- janito/agent/agent.py +96 -0
- janito/agent/config.py +113 -0
- janito/agent/config_defaults.py +10 -0
- janito/agent/conversation.py +107 -0
- janito/agent/queued_tool_handler.py +16 -0
- janito/agent/runtime_config.py +30 -0
- janito/agent/tool_handler.py +124 -0
- janito/agent/tools/__init__.py +11 -0
- janito/agent/tools/ask_user.py +63 -0
- janito/agent/tools/bash_exec.py +58 -0
- janito/agent/tools/create_directory.py +19 -0
- janito/agent/tools/create_file.py +43 -0
- janito/agent/tools/fetch_url.py +48 -0
- janito/agent/tools/file_str_replace.py +48 -0
- janito/agent/tools/find_files.py +37 -0
- janito/agent/tools/gitignore_utils.py +40 -0
- janito/agent/tools/move_file.py +37 -0
- janito/agent/tools/remove_file.py +19 -0
- janito/agent/tools/rich_live.py +37 -0
- janito/agent/tools/rich_utils.py +31 -0
- janito/agent/tools/search_text.py +41 -0
- janito/agent/tools/view_file.py +34 -0
- janito/cli/__init__.py +0 -6
- janito/cli/_print_config.py +68 -0
- janito/cli/_utils.py +8 -0
- janito/cli/arg_parser.py +26 -0
- janito/cli/config_commands.py +131 -0
- janito/cli/logging_setup.py +27 -0
- janito/cli/main.py +39 -0
- janito/cli/runner.py +138 -0
- janito/cli_chat_shell/__init__.py +1 -0
- janito/cli_chat_shell/chat_loop.py +148 -0
- janito/cli_chat_shell/commands.py +202 -0
- janito/cli_chat_shell/config_shell.py +75 -0
- janito/cli_chat_shell/load_prompt.py +15 -0
- janito/cli_chat_shell/session_manager.py +60 -0
- janito/cli_chat_shell/ui.py +136 -0
- janito/render_prompt.py +12 -0
- janito/templates/system_instructions.j2 +38 -0
- janito/web/__init__.py +0 -0
- janito/web/__main__.py +17 -0
- janito/web/app.py +132 -0
- janito-1.0.1.dist-info/METADATA +144 -0
- janito-1.0.1.dist-info/RECORD +51 -0
- {janito-0.15.0.dist-info → janito-1.0.1.dist-info}/WHEEL +2 -1
- janito-1.0.1.dist-info/entry_points.txt +2 -0
- {janito-0.15.0.dist-info → janito-1.0.1.dist-info}/licenses/LICENSE +2 -2
- janito-1.0.1.dist-info/top_level.txt +1 -0
- janito/callbacks.py +0 -34
- janito/cli/agent/__init__.py +0 -7
- janito/cli/agent/conversation.py +0 -149
- janito/cli/agent/initialization.py +0 -168
- janito/cli/agent/query.py +0 -112
- janito/cli/agent.py +0 -12
- janito/cli/app.py +0 -178
- janito/cli/commands/__init__.py +0 -12
- janito/cli/commands/config.py +0 -30
- janito/cli/commands/history.py +0 -119
- janito/cli/commands/profile.py +0 -93
- janito/cli/commands/validation.py +0 -24
- janito/cli/commands/workspace.py +0 -31
- janito/cli/commands.py +0 -12
- janito/cli/output.py +0 -29
- janito/cli/utils.py +0 -22
- janito/config/README.md +0 -104
- janito/config/__init__.py +0 -16
- janito/config/cli/__init__.py +0 -28
- janito/config/cli/commands.py +0 -397
- janito/config/cli/validators.py +0 -77
- janito/config/core/__init__.py +0 -23
- janito/config/core/file_operations.py +0 -90
- janito/config/core/properties.py +0 -316
- janito/config/core/singleton.py +0 -282
- janito/config/profiles/__init__.py +0 -8
- janito/config/profiles/definitions.py +0 -38
- janito/config/profiles/manager.py +0 -80
- janito/data/instructions_template.txt +0 -34
- janito/token_report.py +0 -154
- janito/tools/__init__.py +0 -44
- janito/tools/bash/bash.py +0 -157
- janito/tools/bash/unix_persistent_bash.py +0 -215
- janito/tools/bash/win_persistent_bash.py +0 -341
- janito/tools/decorators.py +0 -90
- janito/tools/delete_file.py +0 -65
- janito/tools/fetch_webpage/__init__.py +0 -23
- janito/tools/fetch_webpage/core.py +0 -182
- janito/tools/find_files.py +0 -220
- janito/tools/move_file.py +0 -72
- janito/tools/prompt_user.py +0 -57
- janito/tools/replace_file.py +0 -63
- janito/tools/rich_console.py +0 -176
- janito/tools/search_text.py +0 -226
- janito/tools/str_replace_editor/__init__.py +0 -6
- janito/tools/str_replace_editor/editor.py +0 -55
- janito/tools/str_replace_editor/handlers/__init__.py +0 -16
- janito/tools/str_replace_editor/handlers/create.py +0 -60
- janito/tools/str_replace_editor/handlers/insert.py +0 -100
- janito/tools/str_replace_editor/handlers/str_replace.py +0 -94
- janito/tools/str_replace_editor/handlers/undo.py +0 -64
- janito/tools/str_replace_editor/handlers/view.py +0 -165
- janito/tools/str_replace_editor/utils.py +0 -33
- janito/tools/think.py +0 -37
- janito/tools/usage_tracker.py +0 -137
- janito-0.15.0.dist-info/METADATA +0 -481
- janito-0.15.0.dist-info/RECORD +0 -64
- janito-0.15.0.dist-info/entry_points.txt +0 -2
janito/__init__.py
CHANGED
janito/__main__.py
CHANGED
janito/agent/__init__.py
ADDED
@@ -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}"
|