janito 0.15.0__py3-none-any.whl → 1.0.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.
- 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 +135 -0
- janito/cli_chat_shell/__init__.py +1 -0
- janito/cli_chat_shell/chat_loop.py +147 -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 +36 -0
- janito/web/__init__.py +0 -0
- janito/web/__main__.py +17 -0
- janito/web/app.py +132 -0
- janito-1.0.0.dist-info/METADATA +144 -0
- janito-1.0.0.dist-info/RECORD +51 -0
- {janito-0.15.0.dist-info → janito-1.0.0.dist-info}/WHEEL +2 -1
- janito-1.0.0.dist-info/entry_points.txt +2 -0
- {janito-0.15.0.dist-info → janito-1.0.0.dist-info}/licenses/LICENSE +2 -2
- janito-1.0.0.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
@@ -0,0 +1,131 @@
|
|
1
|
+
import sys
|
2
|
+
from janito.agent.config import local_config, global_config
|
3
|
+
from janito.agent.runtime_config import unified_config, runtime_config
|
4
|
+
from rich import print
|
5
|
+
from ._utils import home_shorten
|
6
|
+
|
7
|
+
|
8
|
+
def handle_config_commands(args):
|
9
|
+
"""Handle --set-local-config, --set-global-config, --show-config. Exit if any are used."""
|
10
|
+
did_something = False
|
11
|
+
|
12
|
+
if args.set_local_config:
|
13
|
+
from janito.agent.config import CONFIG_OPTIONS
|
14
|
+
try:
|
15
|
+
key, val = args.set_local_config.split("=", 1)
|
16
|
+
except ValueError:
|
17
|
+
print("Invalid format for --set-local-config, expected key=val")
|
18
|
+
sys.exit(1)
|
19
|
+
key = key.strip()
|
20
|
+
if key not in CONFIG_OPTIONS:
|
21
|
+
print(f"Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}")
|
22
|
+
sys.exit(1)
|
23
|
+
local_config.set(key, val.strip())
|
24
|
+
local_config.save()
|
25
|
+
runtime_config.set(key, val.strip())
|
26
|
+
print(f"Local config updated: {key} = {val.strip()}")
|
27
|
+
did_something = True
|
28
|
+
|
29
|
+
if args.set_global_config:
|
30
|
+
from janito.agent.config import CONFIG_OPTIONS
|
31
|
+
try:
|
32
|
+
key, val = args.set_global_config.split("=", 1)
|
33
|
+
except ValueError:
|
34
|
+
print("Invalid format for --set-global-config, expected key=val")
|
35
|
+
sys.exit(1)
|
36
|
+
key = key.strip()
|
37
|
+
if key not in CONFIG_OPTIONS:
|
38
|
+
print(f"Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}")
|
39
|
+
sys.exit(1)
|
40
|
+
global_config.set(key, val.strip())
|
41
|
+
global_config.save()
|
42
|
+
runtime_config.set(key, val.strip())
|
43
|
+
print(f"Global config updated: {key} = {val.strip()}")
|
44
|
+
did_something = True
|
45
|
+
|
46
|
+
if args.set_api_key:
|
47
|
+
global_config.set("api_key", args.set_api_key.strip())
|
48
|
+
global_config.save()
|
49
|
+
runtime_config.set("api_key", args.set_api_key.strip())
|
50
|
+
print("Global API key saved.")
|
51
|
+
did_something = True
|
52
|
+
|
53
|
+
if args.show_config:
|
54
|
+
local_items = {}
|
55
|
+
global_items = {}
|
56
|
+
|
57
|
+
# Collect and group keys
|
58
|
+
from janito.agent.config_defaults import CONFIG_DEFAULTS
|
59
|
+
local_keys = set(local_config.all().keys())
|
60
|
+
global_keys = set(global_config.all().keys())
|
61
|
+
all_keys = set(CONFIG_DEFAULTS.keys()) | global_keys | local_keys
|
62
|
+
if not (local_keys or global_keys):
|
63
|
+
print("No configuration found.")
|
64
|
+
else:
|
65
|
+
from janito.agent.config import get_api_key
|
66
|
+
from janito.agent.runtime_config import unified_config
|
67
|
+
for key in sorted(local_keys):
|
68
|
+
if key == "api_key":
|
69
|
+
value = local_config.get("api_key")
|
70
|
+
value = value[:4] + '...' + value[-4:] if value and len(value) > 8 else ('***' if value else None)
|
71
|
+
else:
|
72
|
+
value = unified_config.get(key)
|
73
|
+
local_items[key] = value
|
74
|
+
for key in sorted(global_keys - local_keys):
|
75
|
+
if key == "api_key":
|
76
|
+
value = global_config.get("api_key")
|
77
|
+
value = value[:4] + '...' + value[-4:] if value and len(value) > 8 else ('***' if value else None)
|
78
|
+
else:
|
79
|
+
value = unified_config.get(key)
|
80
|
+
global_items[key] = value
|
81
|
+
|
82
|
+
# Mask API key
|
83
|
+
for cfg in (local_items, global_items):
|
84
|
+
if 'api_key' in cfg and cfg['api_key']:
|
85
|
+
val = cfg['api_key']
|
86
|
+
cfg['api_key'] = val[:4] + '...' + val[-4:] if len(val) > 8 else '***'
|
87
|
+
|
88
|
+
# Print local config
|
89
|
+
from ._print_config import print_config_items
|
90
|
+
print_config_items(local_items, color_label="[cyan]🏠 Local Configuration[/cyan]")
|
91
|
+
|
92
|
+
# Print global config
|
93
|
+
print_config_items(global_items, color_label="[yellow]🌐 Global Configuration[/yellow]")
|
94
|
+
|
95
|
+
|
96
|
+
# Show defaults for unset keys
|
97
|
+
shown_keys = set(local_items.keys()) | set(global_items.keys())
|
98
|
+
default_items = {k: v for k, v in CONFIG_DEFAULTS.items() if k not in shown_keys and k != 'api_key'}
|
99
|
+
if default_items:
|
100
|
+
print("[green]🟢 Defaults (not set in config files)[/green]")
|
101
|
+
for key, value in default_items.items():
|
102
|
+
# Special case for system_prompt: show template file if None
|
103
|
+
if key == "system_prompt" and value is None:
|
104
|
+
from pathlib import Path
|
105
|
+
template_path = Path(__file__).parent.parent / "templates" / "system_instructions.j2"
|
106
|
+
print(f"{key} = file: {home_shorten(str(template_path))}")
|
107
|
+
else:
|
108
|
+
print(f"{key} = {value}")
|
109
|
+
print()
|
110
|
+
did_something = True
|
111
|
+
|
112
|
+
import os
|
113
|
+
from pathlib import Path
|
114
|
+
if getattr(args, "config_reset_local", False):
|
115
|
+
local_path = Path('.janito/config.json')
|
116
|
+
if local_path.exists():
|
117
|
+
os.remove(local_path)
|
118
|
+
print(f"Removed local config file: {local_path}")
|
119
|
+
else:
|
120
|
+
print(f"Local config file does not exist: {local_path}")
|
121
|
+
sys.exit(0)
|
122
|
+
if getattr(args, "config_reset_global", False):
|
123
|
+
global_path = Path.home() / '.janito/config.json'
|
124
|
+
if global_path.exists():
|
125
|
+
os.remove(global_path)
|
126
|
+
print(f"Removed global config file: {global_path}")
|
127
|
+
else:
|
128
|
+
print(f"Global config file does not exist: {global_path}")
|
129
|
+
sys.exit(0)
|
130
|
+
if did_something:
|
131
|
+
sys.exit(0)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import os
|
2
|
+
import logging
|
3
|
+
|
4
|
+
|
5
|
+
def setup_verbose_logging(args):
|
6
|
+
if args.verbose_http or args.verbose_http_raw:
|
7
|
+
httpx_logger = logging.getLogger("httpx")
|
8
|
+
httpx_logger.setLevel(logging.DEBUG)
|
9
|
+
handler = logging.StreamHandler()
|
10
|
+
handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
11
|
+
httpx_logger.addHandler(handler)
|
12
|
+
|
13
|
+
if args.verbose_http_raw:
|
14
|
+
os.environ["HTTPX_LOG_LEVEL"] = "trace"
|
15
|
+
|
16
|
+
httpcore_logger = logging.getLogger("httpcore")
|
17
|
+
httpcore_logger.setLevel(logging.DEBUG)
|
18
|
+
handler_core = logging.StreamHandler()
|
19
|
+
handler_core.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
20
|
+
httpcore_logger.addHandler(handler_core)
|
21
|
+
|
22
|
+
# Re-add handler to httpx logger in case
|
23
|
+
httpx_logger = logging.getLogger("httpx")
|
24
|
+
httpx_logger.setLevel(logging.DEBUG)
|
25
|
+
handler = logging.StreamHandler()
|
26
|
+
handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
27
|
+
httpx_logger.addHandler(handler)
|
janito/cli/main.py
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
"""Main CLI entry point for Janito."""
|
2
|
+
|
3
|
+
from janito.cli.arg_parser import create_parser
|
4
|
+
from janito.cli.config_commands import handle_config_commands
|
5
|
+
from janito.cli.logging_setup import setup_verbose_logging
|
6
|
+
from janito.cli.runner import run_cli
|
7
|
+
|
8
|
+
|
9
|
+
def main():
|
10
|
+
"""Entry point for the Janito CLI.
|
11
|
+
|
12
|
+
Parses command-line arguments, handles config commands, sets up logging,
|
13
|
+
and launches either the CLI chat shell or the web server.
|
14
|
+
"""
|
15
|
+
# Ensure configs are loaded once at CLI startup
|
16
|
+
from janito.agent.config import local_config, global_config
|
17
|
+
local_config.load()
|
18
|
+
global_config.load()
|
19
|
+
|
20
|
+
parser = create_parser()
|
21
|
+
args = parser.parse_args()
|
22
|
+
|
23
|
+
from janito.agent.config import CONFIG_OPTIONS
|
24
|
+
from janito.agent.config_defaults import CONFIG_DEFAULTS
|
25
|
+
import sys
|
26
|
+
if getattr(args, "help_config", False):
|
27
|
+
print("Available configuration options:\n")
|
28
|
+
for key, desc in CONFIG_OPTIONS.items():
|
29
|
+
default = CONFIG_DEFAULTS.get(key, None)
|
30
|
+
print(f"{key:15} {desc} (default: {default})")
|
31
|
+
sys.exit(0)
|
32
|
+
|
33
|
+
handle_config_commands(args)
|
34
|
+
setup_verbose_logging(args)
|
35
|
+
if getattr(args, 'web', False):
|
36
|
+
import subprocess
|
37
|
+
subprocess.run(['python', '-m', 'janito.web'])
|
38
|
+
else:
|
39
|
+
run_cli(args)
|
janito/cli/runner.py
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
import sys
|
2
|
+
import os
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.markdown import Markdown
|
5
|
+
from janito.render_prompt import render_system_prompt
|
6
|
+
from janito.agent.agent import Agent
|
7
|
+
from janito.agent.conversation import MaxRoundsExceededError, EmptyResponseError, ProviderError
|
8
|
+
from janito.agent.runtime_config import unified_config, runtime_config
|
9
|
+
from janito.agent.config import get_api_key
|
10
|
+
from janito import __version__
|
11
|
+
from rich.rule import Rule
|
12
|
+
|
13
|
+
|
14
|
+
def format_tokens(n):
|
15
|
+
if n is None:
|
16
|
+
return "?"
|
17
|
+
try:
|
18
|
+
n = int(n)
|
19
|
+
except (TypeError, ValueError):
|
20
|
+
return str(n)
|
21
|
+
if n >= 1_000_000:
|
22
|
+
return f"{n/1_000_000:.1f}m"
|
23
|
+
if n >= 1_000:
|
24
|
+
return f"{n/1_000:.1f}k"
|
25
|
+
return str(n)
|
26
|
+
|
27
|
+
|
28
|
+
def run_cli(args):
|
29
|
+
if args.version:
|
30
|
+
print(f"janito version {__version__}")
|
31
|
+
sys.exit(0)
|
32
|
+
|
33
|
+
role = args.role or unified_config.get("role", "software engineer")
|
34
|
+
# if args.role:
|
35
|
+
# runtime_config.set('role', args.role)
|
36
|
+
system_prompt = args.system_prompt or unified_config.get("system_prompt")
|
37
|
+
if system_prompt is None:
|
38
|
+
system_prompt = render_system_prompt(role)
|
39
|
+
|
40
|
+
if args.show_system:
|
41
|
+
api_key = get_api_key()
|
42
|
+
model = unified_config.get('model')
|
43
|
+
agent = Agent(api_key=api_key, model=model)
|
44
|
+
print("Model:", agent.model)
|
45
|
+
print("Parameters: {}")
|
46
|
+
import json
|
47
|
+
print("System Prompt:", system_prompt or "(default system prompt not provided)")
|
48
|
+
sys.exit(0)
|
49
|
+
|
50
|
+
api_key = get_api_key()
|
51
|
+
|
52
|
+
model = unified_config.get('model')
|
53
|
+
base_url = unified_config.get('base_url', 'https://openrouter.ai/api/v1')
|
54
|
+
agent = Agent(api_key=api_key, model=model, system_prompt=system_prompt, verbose_tools=args.verbose_tools, base_url=base_url)
|
55
|
+
|
56
|
+
# Save runtime max_tokens override if provided
|
57
|
+
if args.max_tokens is not None:
|
58
|
+
runtime_config.set('max_tokens', args.max_tokens)
|
59
|
+
if not args.prompt:
|
60
|
+
console = Console()
|
61
|
+
|
62
|
+
if not getattr(args, 'continue_session', False):
|
63
|
+
save_path = os.path.join('.janito', 'last_conversation.json')
|
64
|
+
if os.path.exists(save_path):
|
65
|
+
try:
|
66
|
+
with open(save_path, 'r', encoding='utf-8') as f:
|
67
|
+
data = json.load(f)
|
68
|
+
messages = data.get('messages', [])
|
69
|
+
num_messages = len(messages)
|
70
|
+
console.print(f"[bold yellow]A previous conversation with {num_messages} messages was found.[/bold yellow]")
|
71
|
+
|
72
|
+
last_usage_info = data.get('last_usage_info')
|
73
|
+
if last_usage_info:
|
74
|
+
prompt_tokens = last_usage_info.get('prompt_tokens', 0)
|
75
|
+
completion_tokens = last_usage_info.get('completion_tokens', 0)
|
76
|
+
total_tokens = prompt_tokens + completion_tokens
|
77
|
+
console.print(Rule(f"Token usage - Prompt: {format_tokens(prompt_tokens)}, Completion: {format_tokens(completion_tokens)}, Total: {format_tokens(total_tokens)}"))
|
78
|
+
|
79
|
+
console.print("You can resume it anytime by typing [bold]/continue[/bold].")
|
80
|
+
except Exception:
|
81
|
+
pass # Fail silently if file is corrupt or unreadable
|
82
|
+
|
83
|
+
from janito.cli_chat_shell.chat_loop import start_chat_shell
|
84
|
+
start_chat_shell(agent, continue_session=getattr(args, 'continue_session', False))
|
85
|
+
sys.exit(0)
|
86
|
+
|
87
|
+
prompt = args.prompt
|
88
|
+
|
89
|
+
console = Console()
|
90
|
+
|
91
|
+
waiting_displayed = [True]
|
92
|
+
|
93
|
+
def on_content(data):
|
94
|
+
content = data.get("content", "")
|
95
|
+
if waiting_displayed[0]:
|
96
|
+
# Clear the waiting message
|
97
|
+
sys.stdout.flush()
|
98
|
+
waiting_displayed[0] = False
|
99
|
+
console.print(Markdown(content))
|
100
|
+
|
101
|
+
messages = []
|
102
|
+
if agent.system_prompt:
|
103
|
+
messages.append({"role": "system", "content": agent.system_prompt})
|
104
|
+
|
105
|
+
messages.append({"role": "user", "content": prompt})
|
106
|
+
|
107
|
+
try:
|
108
|
+
try:
|
109
|
+
response = agent.chat(
|
110
|
+
messages,
|
111
|
+
on_content=on_content,
|
112
|
+
spinner=True,
|
113
|
+
)
|
114
|
+
if args.verbose_response:
|
115
|
+
import json
|
116
|
+
console.print_json(json.dumps(response))
|
117
|
+
|
118
|
+
usage = response.get('usage')
|
119
|
+
if usage:
|
120
|
+
prompt_tokens = usage.get('prompt_tokens')
|
121
|
+
completion_tokens = usage.get('completion_tokens')
|
122
|
+
total_tokens = usage.get('total_tokens')
|
123
|
+
console.print(Rule(f"Token usage - Prompt: {format_tokens(prompt_tokens)}, Completion: {format_tokens(completion_tokens)}, Total: {format_tokens(total_tokens)}"))
|
124
|
+
except MaxRoundsExceededError:
|
125
|
+
print("[Error] Conversation exceeded maximum rounds.")
|
126
|
+
sys.exit(1)
|
127
|
+
except ProviderError as e:
|
128
|
+
print(f"[Error] Provider error: {e}")
|
129
|
+
sys.exit(1)
|
130
|
+
except EmptyResponseError as e:
|
131
|
+
print(f"[Error] {e}")
|
132
|
+
sys.exit(1)
|
133
|
+
except KeyboardInterrupt:
|
134
|
+
print("\n[Interrupted by user]")
|
135
|
+
sys.exit(1)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .chat_loop import start_chat_shell
|
@@ -0,0 +1,147 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.markdown import Markdown
|
3
|
+
from prompt_toolkit.history import InMemoryHistory
|
4
|
+
from .session_manager import load_last_conversation, load_input_history
|
5
|
+
from .ui import print_welcome, get_toolbar_func, get_prompt_session
|
6
|
+
from janito import __version__
|
7
|
+
from .commands import handle_command
|
8
|
+
from janito.agent.config import effective_config
|
9
|
+
from janito.agent.conversation import EmptyResponseError, ProviderError
|
10
|
+
|
11
|
+
|
12
|
+
def start_chat_shell(agent, continue_session=False):
|
13
|
+
console = Console()
|
14
|
+
|
15
|
+
# Load input history
|
16
|
+
history_list = load_input_history()
|
17
|
+
mem_history = InMemoryHistory()
|
18
|
+
for item in history_list:
|
19
|
+
mem_history.append_string(item)
|
20
|
+
|
21
|
+
# Initialize chat state variables
|
22
|
+
messages = []
|
23
|
+
last_usage_info = None
|
24
|
+
last_elapsed = None
|
25
|
+
|
26
|
+
state = {
|
27
|
+
'messages': messages,
|
28
|
+
'mem_history': mem_history,
|
29
|
+
'history_list': history_list,
|
30
|
+
'last_usage_info': last_usage_info,
|
31
|
+
'last_elapsed': last_elapsed,
|
32
|
+
}
|
33
|
+
|
34
|
+
# Restore conversation if requested
|
35
|
+
if continue_session:
|
36
|
+
msgs, prompts, usage = load_last_conversation()
|
37
|
+
messages = msgs
|
38
|
+
last_usage_info = usage
|
39
|
+
mem_history = InMemoryHistory()
|
40
|
+
for item in prompts:
|
41
|
+
mem_history.append_string(item)
|
42
|
+
# update state dict with restored data
|
43
|
+
|
44
|
+
state['messages'] = messages
|
45
|
+
state['last_usage_info'] = last_usage_info
|
46
|
+
state['mem_history'] = mem_history
|
47
|
+
console.print('[bold green]Restored last saved conversation.[/bold green]')
|
48
|
+
|
49
|
+
# Add system prompt if needed
|
50
|
+
if agent.system_prompt and not any(m.get('role') == 'system' for m in messages):
|
51
|
+
messages.insert(0, {"role": "system", "content": agent.system_prompt})
|
52
|
+
|
53
|
+
print_welcome(console, version=__version__)
|
54
|
+
|
55
|
+
# Toolbar references
|
56
|
+
def get_messages():
|
57
|
+
return messages
|
58
|
+
|
59
|
+
def get_usage():
|
60
|
+
return last_usage_info
|
61
|
+
|
62
|
+
def get_elapsed():
|
63
|
+
return last_elapsed
|
64
|
+
|
65
|
+
# Try to get model name from agent
|
66
|
+
model_name = getattr(agent, 'model', None)
|
67
|
+
|
68
|
+
session = get_prompt_session(
|
69
|
+
get_toolbar_func(
|
70
|
+
get_messages, get_usage, get_elapsed, model_name=model_name,
|
71
|
+
role_ref=lambda: effective_config.get('role')
|
72
|
+
),
|
73
|
+
mem_history
|
74
|
+
)
|
75
|
+
|
76
|
+
|
77
|
+
# Main chat loop
|
78
|
+
while True:
|
79
|
+
try:
|
80
|
+
if state.get('paste_mode'):
|
81
|
+
console.print('')
|
82
|
+
user_input = session.prompt('Multiline> ', multiline=True)
|
83
|
+
was_paste_mode = True
|
84
|
+
state['paste_mode'] = False
|
85
|
+
else:
|
86
|
+
from prompt_toolkit.formatted_text import HTML
|
87
|
+
user_input = session.prompt(HTML('<prompt>💬 </prompt>'), multiline=False)
|
88
|
+
was_paste_mode = False
|
89
|
+
except EOFError:
|
90
|
+
console.print("\n[bold red]Exiting...[/bold red]")
|
91
|
+
break
|
92
|
+
except KeyboardInterrupt:
|
93
|
+
console.print() # Move to next line
|
94
|
+
try:
|
95
|
+
confirm = input("Do you really want to exit? (y/n): ").strip().lower()
|
96
|
+
except KeyboardInterrupt:
|
97
|
+
console.print("\n[bold red]Exiting...[/bold red]")
|
98
|
+
break
|
99
|
+
if confirm == 'y':
|
100
|
+
console.print("[bold red]Exiting...[/bold red]")
|
101
|
+
break
|
102
|
+
else:
|
103
|
+
continue
|
104
|
+
|
105
|
+
if not was_paste_mode and user_input.strip().startswith('/'):
|
106
|
+
result = handle_command(user_input.strip(), console, agent=agent, messages=messages, mem_history=mem_history, state=state)
|
107
|
+
if result == 'exit':
|
108
|
+
break
|
109
|
+
continue
|
110
|
+
|
111
|
+
if not user_input.strip():
|
112
|
+
continue
|
113
|
+
|
114
|
+
mem_history.append_string(user_input)
|
115
|
+
messages.append({"role": "user", "content": user_input})
|
116
|
+
|
117
|
+
start_time = None
|
118
|
+
import time
|
119
|
+
start_time = time.time()
|
120
|
+
|
121
|
+
# Define streaming content handler
|
122
|
+
def on_content(chunk):
|
123
|
+
content_piece = chunk.get('content')
|
124
|
+
if content_piece:
|
125
|
+
console.print(Markdown(content_piece))
|
126
|
+
|
127
|
+
try:
|
128
|
+
response = agent.chat(messages, on_content=on_content, spinner=True)
|
129
|
+
except KeyboardInterrupt:
|
130
|
+
console.print("[bold yellow]Request interrupted. Returning to prompt.[/bold yellow]")
|
131
|
+
continue
|
132
|
+
except ProviderError as e:
|
133
|
+
console.print(f"[bold red]Provider error:[/bold red] {e}")
|
134
|
+
continue
|
135
|
+
except EmptyResponseError as e:
|
136
|
+
console.print(f"[bold red]Error:[/bold red] {e}")
|
137
|
+
continue
|
138
|
+
last_elapsed = time.time() - start_time
|
139
|
+
|
140
|
+
usage = response.get('usage')
|
141
|
+
last_usage_info = usage
|
142
|
+
|
143
|
+
# Save conversation and input history
|
144
|
+
from .session_manager import save_conversation, save_input_history
|
145
|
+
prompts = [h for h in mem_history.get_strings()]
|
146
|
+
save_conversation(messages, prompts, last_usage_info)
|
147
|
+
save_input_history(prompts)
|
@@ -0,0 +1,202 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import json
|
4
|
+
from prompt_toolkit.history import InMemoryHistory
|
5
|
+
from janito.render_prompt import render_system_prompt
|
6
|
+
from .load_prompt import load_prompt
|
7
|
+
from janito.agent.config import effective_config
|
8
|
+
|
9
|
+
|
10
|
+
def handle_exit(console, **kwargs):
|
11
|
+
console.print("[bold red]Exiting chat mode.[/bold red]")
|
12
|
+
sys.exit(0)
|
13
|
+
|
14
|
+
|
15
|
+
def handle_restart(console, **kwargs):
|
16
|
+
console.print("[bold yellow]Restarting CLI...[/bold yellow]")
|
17
|
+
os.execv(sys.executable, [sys.executable, "-m", "janito"] + sys.argv[1:])
|
18
|
+
|
19
|
+
|
20
|
+
def handle_continue(console, state, **kwargs):
|
21
|
+
save_path = os.path.join('.janito', 'last_conversation.json')
|
22
|
+
if not os.path.exists(save_path):
|
23
|
+
console.print('[bold red]No saved conversation found.[/bold red]')
|
24
|
+
return
|
25
|
+
try:
|
26
|
+
with open(save_path, 'r', encoding='utf-8') as f:
|
27
|
+
data = json.load(f)
|
28
|
+
state['messages'].clear()
|
29
|
+
state['messages'].extend(data.get('messages', []))
|
30
|
+
state['history_list'].clear()
|
31
|
+
state['history_list'].extend(data.get('prompts', []))
|
32
|
+
state['mem_history'] = InMemoryHistory()
|
33
|
+
for item in state['history_list']:
|
34
|
+
state['mem_history'].append_string(item)
|
35
|
+
state['last_usage_info'] = data.get('last_usage_info')
|
36
|
+
console.print('[bold green]Conversation restored from last session.[/bold green]')
|
37
|
+
except Exception as e:
|
38
|
+
console.print(f'[bold red]Failed to load conversation:[/bold red] {e}')
|
39
|
+
|
40
|
+
def handle_history(console, state, *args, **kwargs):
|
41
|
+
messages = state.get('messages', [])
|
42
|
+
try:
|
43
|
+
if not args:
|
44
|
+
# Default: last 5 messages
|
45
|
+
start = max(0, len(messages) - 5)
|
46
|
+
end = len(messages)
|
47
|
+
elif len(args) == 1:
|
48
|
+
count = int(args[0])
|
49
|
+
start = max(0, len(messages) - count)
|
50
|
+
end = len(messages)
|
51
|
+
elif len(args) >= 2:
|
52
|
+
start = int(args[0])
|
53
|
+
end = int(args[1]) + 1 # inclusive
|
54
|
+
else:
|
55
|
+
start = 0
|
56
|
+
end = len(messages)
|
57
|
+
|
58
|
+
console.print(f"[bold cyan]Showing messages {start} to {end - 1} (total {len(messages)}):[/bold cyan]")
|
59
|
+
for idx, msg in enumerate(messages[start:end], start=start):
|
60
|
+
role = msg.get('role', 'unknown')
|
61
|
+
content = msg.get('content', '')
|
62
|
+
console.print(f"[bold]{idx} [{role}]:[/bold] {content}")
|
63
|
+
except Exception as e:
|
64
|
+
console.print(f"[bold red]Error parsing arguments or displaying history:[/bold red] {e}")
|
65
|
+
|
66
|
+
def handle_help(console, **kwargs):
|
67
|
+
console.print("""
|
68
|
+
[bold green]Available commands:[/bold green]
|
69
|
+
/exit - Exit chat mode
|
70
|
+
/restart - Restart the CLI
|
71
|
+
/help - Show this help message
|
72
|
+
/continue - Restore last saved conversation
|
73
|
+
/reset - Reset conversation history
|
74
|
+
/system - Show the system prompt
|
75
|
+
/role - Change the system role
|
76
|
+
/clear - Clear the terminal screen
|
77
|
+
/multi - Provide multiline input as next message
|
78
|
+
/config - Show or set configuration (see: /config show, /config set local|global key=value)
|
79
|
+
""")
|
80
|
+
|
81
|
+
|
82
|
+
def handle_reload(console, *args, **kwargs):
|
83
|
+
"""
|
84
|
+
/reload [filename] - Reload the system prompt from the default or specified file
|
85
|
+
"""
|
86
|
+
agent = kwargs.get('agent')
|
87
|
+
state = kwargs.get('state')
|
88
|
+
filename = args[0] if args else None
|
89
|
+
try:
|
90
|
+
prompt_text = load_prompt(filename)
|
91
|
+
if hasattr(agent, 'system_prompt'):
|
92
|
+
agent.system_prompt = prompt_text
|
93
|
+
# Update the first system message in the conversation if present
|
94
|
+
messages = state.get('messages') if state else None
|
95
|
+
if messages:
|
96
|
+
for msg in messages:
|
97
|
+
if msg.get('role') == 'system':
|
98
|
+
msg['content'] = prompt_text
|
99
|
+
break
|
100
|
+
console.print(f"[bold green]System prompt reloaded from {'default file' if not filename else filename}![/bold green]")
|
101
|
+
except Exception as e:
|
102
|
+
console.print(f"[bold red]Failed to reload system prompt:[/bold red] {e}")
|
103
|
+
|
104
|
+
|
105
|
+
def handle_system(console, **kwargs):
|
106
|
+
prompt = getattr(kwargs.get('agent'), 'system_prompt', None)
|
107
|
+
if not prompt:
|
108
|
+
prompt = render_system_prompt("software engineer")
|
109
|
+
console.print(f"[bold magenta]System Prompt:[/bold magenta]\n{prompt}")
|
110
|
+
|
111
|
+
|
112
|
+
def handle_clear(console, **kwargs):
|
113
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
114
|
+
|
115
|
+
|
116
|
+
def handle_reset(console, state, **kwargs):
|
117
|
+
import os
|
118
|
+
save_path = os.path.join('.janito', 'last_conversation.json')
|
119
|
+
|
120
|
+
# Clear in-memory conversation and prompt history
|
121
|
+
state['messages'].clear()
|
122
|
+
state['history_list'].clear()
|
123
|
+
state['mem_history'] = InMemoryHistory()
|
124
|
+
state['last_usage_info'] = None
|
125
|
+
state['last_elapsed'] = None
|
126
|
+
|
127
|
+
# Delete saved conversation file if exists
|
128
|
+
if os.path.exists(save_path):
|
129
|
+
try:
|
130
|
+
os.remove(save_path)
|
131
|
+
console.print('[bold yellow]Deleted saved conversation history.[/bold yellow]')
|
132
|
+
except Exception as e:
|
133
|
+
console.print(f'[bold red]Failed to delete saved conversation:[/bold red] {e}')
|
134
|
+
else:
|
135
|
+
console.print('[bold yellow]No saved conversation to delete.[/bold yellow]')
|
136
|
+
|
137
|
+
console.print('[bold green]Conversation history has been reset.[/bold green]')
|
138
|
+
|
139
|
+
|
140
|
+
def handle_multi(console, state, **kwargs):
|
141
|
+
console.print("[bold yellow]Multiline mode activated. Provide or write your text and press Esc + Enter to submit.[/bold yellow]")
|
142
|
+
state['paste_mode'] = True
|
143
|
+
|
144
|
+
from janito.agent.runtime_config import runtime_config
|
145
|
+
from .config_shell import handle_config_shell
|
146
|
+
|
147
|
+
def handle_role(console, *args, **kwargs):
|
148
|
+
state = kwargs.get('state')
|
149
|
+
agent = kwargs.get('agent')
|
150
|
+
if not args:
|
151
|
+
console.print('[bold red]Usage: /role <new role description>[/bold red]')
|
152
|
+
return
|
153
|
+
new_role = ' '.join(args)
|
154
|
+
# Update system message in conversation
|
155
|
+
found = False
|
156
|
+
for msg in state['messages']:
|
157
|
+
if msg.get('role') == 'system':
|
158
|
+
msg['content'] = render_system_prompt(new_role)
|
159
|
+
found = True
|
160
|
+
break
|
161
|
+
if not found:
|
162
|
+
# Insert new system message at the beginning
|
163
|
+
state['messages'].insert(0, {'role': 'system', 'content': new_role})
|
164
|
+
# Update agent's system prompt attribute if exists
|
165
|
+
if hasattr(agent, 'system_prompt'):
|
166
|
+
agent.system_prompt = render_system_prompt(new_role)
|
167
|
+
# Also store the raw role string
|
168
|
+
if hasattr(agent, 'role_name'):
|
169
|
+
agent.role_name = new_role
|
170
|
+
else:
|
171
|
+
setattr(agent, 'role_name', new_role)
|
172
|
+
# Update runtime config so all lookups see the new role
|
173
|
+
runtime_config.set('role', new_role)
|
174
|
+
console.print(f"[bold green]System role updated to:[/bold green] {new_role}")
|
175
|
+
|
176
|
+
|
177
|
+
COMMAND_HANDLERS = {
|
178
|
+
"/history": handle_history,
|
179
|
+
"/continue": handle_continue,
|
180
|
+
"/exit": handle_exit,
|
181
|
+
"/restart": handle_restart,
|
182
|
+
|
183
|
+
"/help": handle_help,
|
184
|
+
"/multi": handle_multi,
|
185
|
+
"/system": handle_system,
|
186
|
+
"/role": handle_role,
|
187
|
+
"/clear": handle_clear,
|
188
|
+
"/reset": handle_reset,
|
189
|
+
"/config": handle_config_shell,
|
190
|
+
"/reload": handle_reload,
|
191
|
+
}
|
192
|
+
|
193
|
+
|
194
|
+
def handle_command(command, console, **kwargs):
|
195
|
+
parts = command.strip().split()
|
196
|
+
cmd = parts[0]
|
197
|
+
args = parts[1:]
|
198
|
+
handler = COMMAND_HANDLERS.get(cmd)
|
199
|
+
if handler:
|
200
|
+
return handler(console, *args, **kwargs)
|
201
|
+
console.print(f"[bold red]Invalid command: {cmd}. Type /help for a list of commands.[/bold red]")
|
202
|
+
return None
|