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,75 @@
|
|
1
|
+
from janito.agent.config import local_config, global_config, CONFIG_OPTIONS
|
2
|
+
from janito.agent.config_defaults import CONFIG_DEFAULTS
|
3
|
+
from janito.agent.runtime_config import unified_config, runtime_config
|
4
|
+
from janito.cli._print_config import print_config_items
|
5
|
+
from rich import print
|
6
|
+
import sys
|
7
|
+
|
8
|
+
def handle_config_shell(console, *args, **kwargs):
|
9
|
+
"""
|
10
|
+
/config show
|
11
|
+
/config set local key=value
|
12
|
+
/config set global key=value
|
13
|
+
/config reset local
|
14
|
+
/config reset global
|
15
|
+
"""
|
16
|
+
if not args or args[0] not in ("show", "set", "reset"):
|
17
|
+
console.print("[bold red]Usage:[/bold red] /config show | /config set local|global key=value | /config reset local|global")
|
18
|
+
return
|
19
|
+
|
20
|
+
if args[0] == "show":
|
21
|
+
# Show config, unified with CLI
|
22
|
+
from janito.cli._print_config import print_full_config
|
23
|
+
print_full_config(local_config, global_config, unified_config, CONFIG_DEFAULTS, console=console)
|
24
|
+
return
|
25
|
+
|
26
|
+
if args[0] == "reset":
|
27
|
+
if len(args) < 2 or args[1] not in ("local", "global"):
|
28
|
+
console.print("[bold red]Usage:[/bold red] /config reset local|global")
|
29
|
+
return
|
30
|
+
import os
|
31
|
+
from pathlib import Path
|
32
|
+
scope = args[1]
|
33
|
+
if scope == "local":
|
34
|
+
local_path = Path('.janito/config.json')
|
35
|
+
if local_path.exists():
|
36
|
+
os.remove(local_path)
|
37
|
+
console.print(f"[green]Removed local config file:[/green] {local_path}")
|
38
|
+
else:
|
39
|
+
console.print(f"[yellow]Local config file does not exist:[/yellow] {local_path}")
|
40
|
+
elif scope == "global":
|
41
|
+
global_path = Path.home() / '.janito/config.json'
|
42
|
+
if global_path.exists():
|
43
|
+
os.remove(global_path)
|
44
|
+
console.print(f"[green]Removed global config file:[/green] {global_path}")
|
45
|
+
else:
|
46
|
+
console.print(f"[yellow]Global config file does not exist:[/yellow] {global_path}")
|
47
|
+
console.print("[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]")
|
48
|
+
return
|
49
|
+
|
50
|
+
if args[0] == "set":
|
51
|
+
if len(args) < 3 or args[1] not in ("local", "global"):
|
52
|
+
console.print("[bold red]Usage:[/bold red] /config set local|global key=value")
|
53
|
+
return
|
54
|
+
scope = args[1]
|
55
|
+
try:
|
56
|
+
key, val = args[2].split("=", 1)
|
57
|
+
except ValueError:
|
58
|
+
console.print("[bold red]Invalid format, expected key=val[/bold red]")
|
59
|
+
return
|
60
|
+
key = key.strip()
|
61
|
+
if key not in CONFIG_OPTIONS:
|
62
|
+
console.print(f"[bold red]Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}")
|
63
|
+
return
|
64
|
+
val = val.strip()
|
65
|
+
if scope == "local":
|
66
|
+
local_config.set(key, val)
|
67
|
+
local_config.save()
|
68
|
+
runtime_config.set(key, val)
|
69
|
+
console.print(f"[green]Local config updated:[/green] {key} = {val}")
|
70
|
+
elif scope == "global":
|
71
|
+
global_config.set(key, val)
|
72
|
+
global_config.save()
|
73
|
+
runtime_config.set(key, val)
|
74
|
+
console.print(f"[green]Global config updated:[/green] {key} = {val}")
|
75
|
+
return
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
def load_prompt(filename=None):
|
4
|
+
"""
|
5
|
+
Load the system prompt from a file. If filename is None, use the default prompt file.
|
6
|
+
Returns the prompt string.
|
7
|
+
"""
|
8
|
+
if filename is None:
|
9
|
+
# Default prompt file path (can be customized)
|
10
|
+
filename = os.path.join(os.path.dirname(__file__), '../render_prompt/templates/system_instructions.j2')
|
11
|
+
filename = os.path.abspath(filename)
|
12
|
+
if not os.path.exists(filename):
|
13
|
+
raise FileNotFoundError(f"Prompt file not found: {filename}")
|
14
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
15
|
+
return f.read()
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
|
6
|
+
def load_last_summary(path='.janito/last_conversation.json'):
|
7
|
+
if not os.path.exists(path):
|
8
|
+
return None
|
9
|
+
try:
|
10
|
+
with open(path, 'r', encoding='utf-8') as f:
|
11
|
+
data = json.load(f)
|
12
|
+
return data
|
13
|
+
except Exception:
|
14
|
+
return None
|
15
|
+
|
16
|
+
|
17
|
+
def load_last_conversation(path='.janito/last_conversation.json'):
|
18
|
+
if not os.path.exists(path):
|
19
|
+
return [], [], None
|
20
|
+
try:
|
21
|
+
with open(path, 'r', encoding='utf-8') as f:
|
22
|
+
data = json.load(f)
|
23
|
+
messages = data.get('messages', [])
|
24
|
+
prompts = data.get('prompts', [])
|
25
|
+
usage = data.get('last_usage_info')
|
26
|
+
return messages, prompts, usage
|
27
|
+
except Exception:
|
28
|
+
return [], [], None
|
29
|
+
|
30
|
+
|
31
|
+
def save_conversation(messages, prompts, usage_info=None, path='.janito/last_conversation.json'):
|
32
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
33
|
+
data = {
|
34
|
+
'messages': messages,
|
35
|
+
'prompts': prompts,
|
36
|
+
'last_usage_info': usage_info
|
37
|
+
}
|
38
|
+
with open(path, 'w', encoding='utf-8') as f:
|
39
|
+
json.dump(data, f, indent=2)
|
40
|
+
|
41
|
+
|
42
|
+
def load_input_history():
|
43
|
+
history_dir = os.path.join('.janito', 'input_history')
|
44
|
+
os.makedirs(history_dir, exist_ok=True)
|
45
|
+
today_str = datetime.now().strftime('%y%m%d')
|
46
|
+
history_file = os.path.join(history_dir, f'{today_str}.json')
|
47
|
+
try:
|
48
|
+
with open(history_file, 'r', encoding='utf-8') as f:
|
49
|
+
return json.load(f)
|
50
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
51
|
+
return []
|
52
|
+
|
53
|
+
|
54
|
+
def save_input_history(history_list):
|
55
|
+
history_dir = os.path.join('.janito', 'input_history')
|
56
|
+
os.makedirs(history_dir, exist_ok=True)
|
57
|
+
today_str = datetime.now().strftime('%y%m%d')
|
58
|
+
history_file = os.path.join(history_dir, f'{today_str}.json')
|
59
|
+
with open(history_file, 'w', encoding='utf-8') as f:
|
60
|
+
json.dump(history_list, f, indent=2)
|
@@ -0,0 +1,136 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.markdown import Markdown
|
3
|
+
from prompt_toolkit import PromptSession
|
4
|
+
from prompt_toolkit.key_binding import KeyBindings
|
5
|
+
from prompt_toolkit.enums import EditingMode
|
6
|
+
from prompt_toolkit.formatted_text import HTML
|
7
|
+
from prompt_toolkit.styles import Style
|
8
|
+
from prompt_toolkit.history import InMemoryHistory
|
9
|
+
|
10
|
+
|
11
|
+
def print_summary(console, data, continue_session):
|
12
|
+
if not data:
|
13
|
+
return
|
14
|
+
msgs = data.get('messages', [])
|
15
|
+
last_user = next((m['content'] for m in reversed(msgs) if m.get('role') == 'user'), None)
|
16
|
+
last_assistant = next((m['content'] for m in reversed(msgs) if m.get('role') == 'assistant'), None)
|
17
|
+
usage = data.get('last_usage_info', {})
|
18
|
+
console.print('[bold cyan]Last saved conversation:[/bold cyan]')
|
19
|
+
console.print(f"Messages: {len(msgs)}")
|
20
|
+
if last_user:
|
21
|
+
console.print(f"Last user: [italic]{last_user[:100]}{'...' if len(last_user)>100 else ''}[/italic]")
|
22
|
+
if last_assistant:
|
23
|
+
console.print(f"Last assistant: [italic]{last_assistant[:100]}{'...' if len(last_assistant)>100 else ''}[/italic]")
|
24
|
+
if usage:
|
25
|
+
ptok = usage.get('prompt_tokens')
|
26
|
+
ctok = usage.get('completion_tokens')
|
27
|
+
tot = (ptok or 0) + (ctok or 0)
|
28
|
+
console.print(f"Tokens - Prompt: {ptok}, Completion: {ctok}, Total: {tot}")
|
29
|
+
if not continue_session:
|
30
|
+
console.print("[bold yellow]Type /continue to restore the last saved conversation.[/bold yellow]")
|
31
|
+
|
32
|
+
|
33
|
+
def print_welcome(console, version=None):
|
34
|
+
version_str = f" (v{version})" if version else ""
|
35
|
+
console.print(f"[bold green]Welcome to Janito{version_str}! Entering chat mode. Type /exit to exit.[/bold green]")
|
36
|
+
console.print("[yellow]To resume your previous conversation, type /continue at any time.[/yellow]")
|
37
|
+
|
38
|
+
|
39
|
+
def get_toolbar_func(messages_ref, last_usage_info_ref, last_elapsed_ref, model_name=None, role_ref=None):
|
40
|
+
def format_tokens(n):
|
41
|
+
if n is None:
|
42
|
+
return "?"
|
43
|
+
if n >= 1_000_000:
|
44
|
+
return f"{n/1_000_000:.1f}m"
|
45
|
+
if n >= 1_000:
|
46
|
+
return f"{n/1_000:.1f}k"
|
47
|
+
return str(n)
|
48
|
+
|
49
|
+
def get_toolbar():
|
50
|
+
left = f' Messages: <msg_count>{len(messages_ref())}</msg_count>'
|
51
|
+
usage = last_usage_info_ref()
|
52
|
+
last_elapsed = last_elapsed_ref()
|
53
|
+
if usage:
|
54
|
+
prompt_tokens = usage.get('prompt_tokens')
|
55
|
+
completion_tokens = usage.get('completion_tokens')
|
56
|
+
total_tokens = (prompt_tokens or 0) + (completion_tokens or 0)
|
57
|
+
speed = None
|
58
|
+
if last_elapsed and last_elapsed > 0:
|
59
|
+
speed = total_tokens / last_elapsed
|
60
|
+
left += (
|
61
|
+
f" | Tokens: In=<tokens_in>{format_tokens(prompt_tokens)}</tokens_in> / "
|
62
|
+
f"Out=<tokens_out>{format_tokens(completion_tokens)}</tokens_out> / "
|
63
|
+
f"Total=<tokens_total>{format_tokens(total_tokens)}</tokens_total>"
|
64
|
+
)
|
65
|
+
if speed is not None:
|
66
|
+
left += f", speed=<speed>{speed:.1f}</speed> tokens/sec"
|
67
|
+
|
68
|
+
from prompt_toolkit.application import get_app
|
69
|
+
|
70
|
+
# Compose first line with Model and Role
|
71
|
+
try:
|
72
|
+
width = get_app().output.get_size().columns
|
73
|
+
except Exception:
|
74
|
+
width = 80 # fallback default
|
75
|
+
|
76
|
+
model_part = f" Model: <model>{model_name}</model>" if model_name else ""
|
77
|
+
role_part = ""
|
78
|
+
if role_ref:
|
79
|
+
role = role_ref()
|
80
|
+
if role:
|
81
|
+
role_part = f"Role: <b>{role}</b>"
|
82
|
+
|
83
|
+
first_line_parts = []
|
84
|
+
if model_part:
|
85
|
+
first_line_parts.append(model_part)
|
86
|
+
if role_part:
|
87
|
+
first_line_parts.append(role_part)
|
88
|
+
first_line = " | ".join(first_line_parts)
|
89
|
+
|
90
|
+
help_part = "<b>/help</b> for help"
|
91
|
+
|
92
|
+
total_len = len(left) + len(help_part) + 3 # separators and spaces
|
93
|
+
if first_line:
|
94
|
+
total_len += len(first_line) + 3
|
95
|
+
|
96
|
+
if total_len < width:
|
97
|
+
padding = ' ' * (width - total_len)
|
98
|
+
second_line = f"{left}{padding} | {help_part}"
|
99
|
+
else:
|
100
|
+
second_line = f"{left} | {help_part}"
|
101
|
+
|
102
|
+
if first_line:
|
103
|
+
toolbar_text = first_line + "\n" + second_line
|
104
|
+
else:
|
105
|
+
toolbar_text = second_line
|
106
|
+
|
107
|
+
return HTML(toolbar_text)
|
108
|
+
|
109
|
+
return get_toolbar
|
110
|
+
|
111
|
+
|
112
|
+
def get_prompt_session(get_toolbar_func, mem_history):
|
113
|
+
style = Style.from_dict({
|
114
|
+
'bottom-toolbar': 'bg:#333333 #ffffff',
|
115
|
+
'b': 'bold',
|
116
|
+
'prompt': 'bold bg:#000080 #ffffff',
|
117
|
+
'model': 'bold bg:#005f5f #ffffff', # distinct background/foreground
|
118
|
+
'msg_count': 'bg:#333333 #ffff00 bold',
|
119
|
+
'tokens_in': 'ansicyan bold',
|
120
|
+
'tokens_out': 'ansigreen bold',
|
121
|
+
'tokens_total': 'ansiyellow bold',
|
122
|
+
'speed': 'ansimagenta bold',
|
123
|
+
'right': 'bg:#005f5f #ffffff',
|
124
|
+
'input': 'bg:#000080 #ffffff',
|
125
|
+
'': 'bg:#000080 #ffffff',
|
126
|
+
})
|
127
|
+
|
128
|
+
session = PromptSession(
|
129
|
+
multiline=False,
|
130
|
+
key_bindings=KeyBindings(),
|
131
|
+
editing_mode=EditingMode.EMACS,
|
132
|
+
bottom_toolbar=get_toolbar_func,
|
133
|
+
style=style,
|
134
|
+
history=mem_history
|
135
|
+
)
|
136
|
+
return session
|
janito/render_prompt.py
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
import jinja2
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
def render_system_prompt(role: str) -> str:
|
5
|
+
template_loader = jinja2.FileSystemLoader(searchpath=str(Path(__file__).parent / "templates"))
|
6
|
+
env = jinja2.Environment(loader=template_loader)
|
7
|
+
template = env.get_template("system_instructions.j2")
|
8
|
+
return template.render(role=role)
|
9
|
+
|
10
|
+
if __name__ == "__main__":
|
11
|
+
prompt = render_system_prompt("software engineer")
|
12
|
+
print(prompt)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
You are a helpful {{ role }}.
|
2
|
+
|
3
|
+
<context>
|
4
|
+
Always review `docs/structure.md` before conducting file-specific searches.
|
5
|
+
Unless specified otherwise, look for the files that match the questions context.
|
6
|
+
Explore files that might be relevant to the current task.
|
7
|
+
</context>
|
8
|
+
|
9
|
+
<analysis>
|
10
|
+
When analyzing issues, you might want to look into the git history for clues.
|
11
|
+
When using read_file specific a minimum of 100 lines.
|
12
|
+
</analysis>
|
13
|
+
|
14
|
+
<editing>
|
15
|
+
If in doubt during editing, use the `ask_user` function to get additional information; otherwise, proceed and inform the user of the decision made.
|
16
|
+
|
17
|
+
When you need to make changes to a file, consider the following:
|
18
|
+
|
19
|
+
- Use `file_str_replace` when you want to update or fix specific text fragments within a file without altering the rest of its content. It is preferred over full file replacement when:
|
20
|
+
- Only small, targeted changes are needed.
|
21
|
+
- You want to avoid the risk of accidentally overwriting unrelated content.
|
22
|
+
- The file is large, and rewriting the entire file would be inefficient.
|
23
|
+
- You want to preserve formatting, comments, or code structure outside the replaced text.
|
24
|
+
|
25
|
+
- When replacing files, review their current content before requesting the update.
|
26
|
+
|
27
|
+
- When reorganizing, moving files, or functions, search for references in other files that might need to be updated accordingly.
|
28
|
+
</editing>
|
29
|
+
|
30
|
+
<finishing>
|
31
|
+
After performing changes:
|
32
|
+
|
33
|
+
- Review the README content if there are user-exposed or public API changes.
|
34
|
+
- Use `git commit` to save the changes. Unless requested otherwise, review `git diff` when composing the commit message.
|
35
|
+
- Review `docs/structure.md` considering discovered, created, or modified files.
|
36
|
+
</finishing>
|
janito/web/__init__.py
ADDED
File without changes
|
janito/web/__main__.py
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
import sys
|
2
|
+
from . import app
|
3
|
+
|
4
|
+
|
5
|
+
def main():
|
6
|
+
port = 8000
|
7
|
+
if len(sys.argv) > 1:
|
8
|
+
try:
|
9
|
+
port = int(sys.argv[1])
|
10
|
+
except ValueError:
|
11
|
+
print(f"Invalid port number: {sys.argv[1]}")
|
12
|
+
sys.exit(1)
|
13
|
+
app.app.run(host='0.0.0.0', port=port, debug=True)
|
14
|
+
|
15
|
+
|
16
|
+
if __name__ == '__main__':
|
17
|
+
main()
|
janito/web/app.py
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
from flask import Flask, request, render_template, Response, send_from_directory, session, jsonify
|
2
|
+
from queue import Queue
|
3
|
+
import json
|
4
|
+
from janito.agent.queued_tool_handler import QueuedToolHandler
|
5
|
+
from janito.agent.agent import Agent
|
6
|
+
from janito.agent.config import get_api_key
|
7
|
+
from janito.render_prompt import render_system_prompt
|
8
|
+
import os
|
9
|
+
import threading
|
10
|
+
|
11
|
+
# Render system prompt once
|
12
|
+
system_prompt = render_system_prompt("software engineer")
|
13
|
+
|
14
|
+
app = Flask(
|
15
|
+
__name__,
|
16
|
+
static_url_path='/static',
|
17
|
+
static_folder=os.path.join(os.path.dirname(__file__), 'static')
|
18
|
+
)
|
19
|
+
|
20
|
+
# Secret key for session management
|
21
|
+
app.secret_key = 'replace_with_a_secure_random_secret_key'
|
22
|
+
|
23
|
+
# Path for persistent conversation storage
|
24
|
+
conversation_file = os.path.expanduser('~/.janito/last_conversation_web.json')
|
25
|
+
|
26
|
+
# Initially no conversation loaded
|
27
|
+
conversation = None
|
28
|
+
|
29
|
+
|
30
|
+
# Global event queue for streaming
|
31
|
+
stream_queue = Queue()
|
32
|
+
|
33
|
+
# Create a QueuedToolHandler with the queue
|
34
|
+
queued_handler = QueuedToolHandler(stream_queue)
|
35
|
+
|
36
|
+
# Instantiate the Agent with the custom tool handler
|
37
|
+
agent = Agent(
|
38
|
+
api_key=get_api_key(),
|
39
|
+
tool_handler=queued_handler
|
40
|
+
)
|
41
|
+
|
42
|
+
@app.route('/get_model_name')
|
43
|
+
def get_model_name():
|
44
|
+
return jsonify({"model": agent.model})
|
45
|
+
|
46
|
+
|
47
|
+
@app.route('/favicon.ico')
|
48
|
+
def favicon():
|
49
|
+
return send_from_directory(
|
50
|
+
os.path.join(app.root_path, 'static'),
|
51
|
+
'favicon.ico',
|
52
|
+
mimetype='image/vnd.microsoft.icon'
|
53
|
+
)
|
54
|
+
|
55
|
+
|
56
|
+
@app.route('/')
|
57
|
+
def index():
|
58
|
+
return render_template('index.html')
|
59
|
+
|
60
|
+
@app.route('/load_conversation')
|
61
|
+
def load_conversation():
|
62
|
+
global conversation
|
63
|
+
try:
|
64
|
+
with open(conversation_file, 'r') as f:
|
65
|
+
conversation = json.load(f)
|
66
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
67
|
+
conversation = []
|
68
|
+
return jsonify({'status': 'ok', 'conversation': conversation})
|
69
|
+
|
70
|
+
@app.route('/new_conversation', methods=['POST'])
|
71
|
+
def new_conversation():
|
72
|
+
global conversation
|
73
|
+
conversation = []
|
74
|
+
return jsonify({'status': 'ok'})
|
75
|
+
|
76
|
+
@app.route('/execute_stream', methods=['POST'])
|
77
|
+
def execute_stream():
|
78
|
+
data = request.get_json()
|
79
|
+
user_input = data.get('input', '')
|
80
|
+
|
81
|
+
global conversation
|
82
|
+
if conversation is None:
|
83
|
+
# If no conversation loaded, start a new one
|
84
|
+
conversation = []
|
85
|
+
|
86
|
+
# Always start with the system prompt as the first message
|
87
|
+
if not conversation or conversation[0]['role'] != 'system':
|
88
|
+
conversation.insert(0, {"role": "system", "content": system_prompt})
|
89
|
+
|
90
|
+
# Append the new user message
|
91
|
+
conversation.append({"role": "user", "content": user_input})
|
92
|
+
|
93
|
+
def run_agent():
|
94
|
+
response = agent.chat(
|
95
|
+
conversation,
|
96
|
+
on_content=lambda data: stream_queue.put({"type": "content", "content": data.get("content")})
|
97
|
+
)
|
98
|
+
if response and 'content' in response:
|
99
|
+
conversation.append({"role": "assistant", "content": response['content']})
|
100
|
+
try:
|
101
|
+
os.makedirs(os.path.dirname(conversation_file), exist_ok=True)
|
102
|
+
with open(conversation_file, 'w') as f:
|
103
|
+
json.dump(conversation, f, indent=2)
|
104
|
+
except Exception as e:
|
105
|
+
print(f"Error saving conversation: {e}")
|
106
|
+
stream_queue.put(None)
|
107
|
+
|
108
|
+
threading.Thread(target=run_agent, daemon=True).start()
|
109
|
+
|
110
|
+
def generate():
|
111
|
+
while True:
|
112
|
+
content = stream_queue.get()
|
113
|
+
if content is None:
|
114
|
+
break
|
115
|
+
if isinstance(content, tuple) and content[0] == 'tool_progress':
|
116
|
+
message = json.dumps({"type": "tool_progress", "data": content[1]})
|
117
|
+
else:
|
118
|
+
message = json.dumps(content)
|
119
|
+
yield f"data: {message}\n\n"
|
120
|
+
import sys
|
121
|
+
sys.stdout.flush()
|
122
|
+
|
123
|
+
return Response(
|
124
|
+
generate(),
|
125
|
+
mimetype='text/event-stream',
|
126
|
+
headers={
|
127
|
+
'Cache-Control': 'no-cache',
|
128
|
+
'X-Accel-Buffering': 'no',
|
129
|
+
'Connection': 'keep-alive',
|
130
|
+
'Transfer-Encoding': 'chunked'
|
131
|
+
}
|
132
|
+
)
|
@@ -0,0 +1,144 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: janito
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: An agent framework with built-in tools.
|
5
|
+
Author-email: João Pinto <joao.pinto@gmail.com>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: homepage, https://github.com/joaompinto/janito
|
8
|
+
Project-URL: repository, https://github.com/joaompinto/janito
|
9
|
+
Keywords: agent,framework,tools,automation
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Requires-Python: >=3.8
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
License-File: LICENSE
|
16
|
+
Requires-Dist: rich
|
17
|
+
Requires-Dist: openai
|
18
|
+
Requires-Dist: flask
|
19
|
+
Requires-Dist: pathspec
|
20
|
+
Dynamic: license-file
|
21
|
+
|
22
|
+
# 🚀 Janito: Natural Language Code Editing Agent
|
23
|
+
|
24
|
+
## ⚡ Quick Start
|
25
|
+
|
26
|
+
Run a one-off prompt:
|
27
|
+
```bash
|
28
|
+
python -m janito "Refactor the data processing module to improve readability."
|
29
|
+
```
|
30
|
+
|
31
|
+
Or start the interactive chat shell:
|
32
|
+
```bash
|
33
|
+
python -m janito
|
34
|
+
```
|
35
|
+
|
36
|
+
Launch the web UI:
|
37
|
+
```bash
|
38
|
+
python -m janito.web
|
39
|
+
```
|
40
|
+
|
41
|
+
---
|
42
|
+
|
43
|
+
Janito is a command-line and web-based AI agent designed to **edit code and manage files** using natural language instructions.
|
44
|
+
|
45
|
+
---
|
46
|
+
|
47
|
+
## ✨ Key Features
|
48
|
+
- 📝 **Code Editing via Natural Language:** Modify, create, or delete code files simply by describing the changes.
|
49
|
+
- 📁 **File & Directory Management:** Navigate, create, move, or remove files and folders.
|
50
|
+
- 🧠 **Context-Aware:** Understands your project structure for precise edits.
|
51
|
+
- 💬 **Interactive User Prompts:** Asks for clarification when needed.
|
52
|
+
- 🧩 **Extensible Tooling:** Built-in tools for file operations, shell commands, and more.
|
53
|
+
- 🌐 **Web Interface (In Development):** Upcoming simple web UI for streaming responses and tool progress.
|
54
|
+
|
55
|
+
---
|
56
|
+
|
57
|
+
## 📦 Installation
|
58
|
+
|
59
|
+
### Requirements
|
60
|
+
- Python 3.8+
|
61
|
+
|
62
|
+
### Install dependencies
|
63
|
+
```bash
|
64
|
+
pip install -e .
|
65
|
+
```
|
66
|
+
|
67
|
+
### Set your API key
|
68
|
+
Janito uses OpenAI-compatible APIs (default: `openrouter/optimus-alpha`). Set your API key using the CLI:
|
69
|
+
```bash
|
70
|
+
python -m janito --set-api-key your_api_key_here
|
71
|
+
```
|
72
|
+
|
73
|
+
### Obtain an API key from openrouter.io
|
74
|
+
1. Visit [https://openrouter.io/](https://openrouter.io/)
|
75
|
+
2. Sign in or create a free account.
|
76
|
+
3. Navigate to **API Keys** in your account dashboard.
|
77
|
+
4. Click **Create new key**, provide a name, and save the generated key.
|
78
|
+
5. Save it using the CLI:
|
79
|
+
```bash
|
80
|
+
python -m janito --set-api-key your_api_key_here
|
81
|
+
```
|
82
|
+
|
83
|
+
---
|
84
|
+
|
85
|
+
## ⚙️ Configuration
|
86
|
+
|
87
|
+
Janito supports multiple ways to configure API access, model, and behavior:
|
88
|
+
|
89
|
+
### API Key
|
90
|
+
|
91
|
+
- Set via CLI:
|
92
|
+
```bash
|
93
|
+
python -m janito --set-api-key your_api_key_here
|
94
|
+
```
|
95
|
+
|
96
|
+
### Configurable Options
|
97
|
+
|
98
|
+
| Key | Description | How to set | Default |
|
99
|
+
|-----------------|-----------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------|
|
100
|
+
| `api_key` | API key for OpenAI-compatible service | `--set-api-key`, config file | _None_ (required) |
|
101
|
+
| `model` | Model name to use | `--set-local-config model=...` or `--set-global-config` | `openrouter/optimus-alpha` |
|
102
|
+
| `base_url` | API base URL (OpenAI-compatible endpoint) | `--set-local-config base_url=...` or `--set-global-config` | `https://openrouter.ai/api/v1` |
|
103
|
+
| `role` | Role description for system prompt | CLI `--role` or config | "software engineer" |
|
104
|
+
| `system_prompt` | Override the entire system prompt | CLI `--system-prompt` or config | _Template-generated prompt_ |
|
105
|
+
| `temperature` | Sampling temperature (float, e.g., 0.0 - 2.0) | CLI `--temperature` or config | 0.2 |
|
106
|
+
| `max_tokens` | Maximum tokens for model response | CLI `--max-tokens` or config | 200000 |
|
107
|
+
|
108
|
+
### Config files
|
109
|
+
|
110
|
+
- **Local config:** `.janito/config.json` (project-specific)
|
111
|
+
- **Global config:** `~/.config/janito/config.json` (user-wide)
|
112
|
+
|
113
|
+
Set values via:
|
114
|
+
|
115
|
+
```bash
|
116
|
+
python -m janito --set-local-config key=value
|
117
|
+
python -m janito --set-global-config key=value
|
118
|
+
```
|
119
|
+
|
120
|
+
---
|
121
|
+
|
122
|
+
## 🚀 Build and Release
|
123
|
+
|
124
|
+
Janito provides scripts for automated build and release to PyPI:
|
125
|
+
|
126
|
+
### Bash (Linux/macOS)
|
127
|
+
|
128
|
+
```bash
|
129
|
+
./tools/release.sh
|
130
|
+
```
|
131
|
+
|
132
|
+
### PowerShell (Windows)
|
133
|
+
|
134
|
+
```powershell
|
135
|
+
./tools/release.ps1
|
136
|
+
```
|
137
|
+
|
138
|
+
These scripts will:
|
139
|
+
- Check for required tools (`hatch`, `twine`)
|
140
|
+
- Validate the version in `pyproject.toml` against PyPI and git tags
|
141
|
+
- Build the package
|
142
|
+
- Upload to PyPI
|
143
|
+
|
144
|
+
---
|