janito 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/agent.py +27 -10
  3. janito/agent/config.py +37 -9
  4. janito/agent/config_utils.py +9 -0
  5. janito/agent/conversation.py +19 -3
  6. janito/agent/runtime_config.py +1 -0
  7. janito/agent/tool_handler.py +154 -52
  8. janito/agent/tools/__init__.py +9 -8
  9. janito/agent/tools/ask_user.py +2 -1
  10. janito/agent/tools/fetch_url.py +27 -35
  11. janito/agent/tools/file_ops.py +72 -0
  12. janito/agent/tools/find_files.py +47 -26
  13. janito/agent/tools/get_lines.py +58 -0
  14. janito/agent/tools/py_compile.py +26 -0
  15. janito/agent/tools/python_exec.py +47 -0
  16. janito/agent/tools/remove_directory.py +38 -0
  17. janito/agent/tools/replace_text_in_file.py +67 -0
  18. janito/agent/tools/run_bash_command.py +134 -0
  19. janito/agent/tools/search_files.py +52 -0
  20. janito/cli/_print_config.py +12 -12
  21. janito/cli/arg_parser.py +6 -1
  22. janito/cli/config_commands.py +56 -8
  23. janito/cli/runner.py +21 -9
  24. janito/cli_chat_shell/chat_loop.py +5 -3
  25. janito/cli_chat_shell/commands.py +34 -37
  26. janito/cli_chat_shell/config_shell.py +1 -1
  27. janito/cli_chat_shell/load_prompt.py +1 -1
  28. janito/cli_chat_shell/session_manager.py +11 -15
  29. janito/cli_chat_shell/ui.py +17 -8
  30. janito/render_prompt.py +3 -1
  31. janito/web/app.py +76 -19
  32. janito-1.3.0.dist-info/METADATA +142 -0
  33. janito-1.3.0.dist-info/RECORD +51 -0
  34. janito/agent/tools/bash_exec.py +0 -58
  35. janito/agent/tools/create_directory.py +0 -19
  36. janito/agent/tools/create_file.py +0 -43
  37. janito/agent/tools/file_str_replace.py +0 -48
  38. janito/agent/tools/move_file.py +0 -37
  39. janito/agent/tools/remove_file.py +0 -19
  40. janito/agent/tools/search_text.py +0 -41
  41. janito/agent/tools/view_file.py +0 -34
  42. janito/templates/system_instructions.j2 +0 -38
  43. janito-1.2.0.dist-info/METADATA +0 -85
  44. janito-1.2.0.dist-info/RECORD +0 -51
  45. {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/WHEEL +0 -0
  46. {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/entry_points.txt +0 -0
  47. {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/licenses/LICENSE +0 -0
  48. {janito-1.2.0.dist-info → janito-1.3.0.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,18 @@ def handle_config_commands(args):
9
9
  """Handle --set-local-config, --set-global-config, --show-config. Exit if any are used."""
10
10
  did_something = False
11
11
 
12
+ if args.run_config:
13
+ for run_item in args.run_config:
14
+ try:
15
+ key, val = run_item.split("=", 1)
16
+ except ValueError:
17
+ print("Invalid format for --run-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
+ runtime_config.set(key, val.strip())
12
24
  if args.set_local_config:
13
25
  from janito.agent.config import CONFIG_OPTIONS
14
26
  try:
@@ -34,17 +46,33 @@ def handle_config_commands(args):
34
46
  print("Invalid format for --set-global-config, expected key=val")
35
47
  sys.exit(1)
36
48
  key = key.strip()
37
- if key not in CONFIG_OPTIONS:
49
+ if key not in CONFIG_OPTIONS and not key.startswith("template."):
38
50
  print(f"Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}")
39
51
  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
52
+ if key.startswith('template.'):
53
+ subkey = key[len('template.'):]
54
+ template_dict = global_config.get('template', {})
55
+ template_dict[subkey] = val.strip()
56
+ global_config.set('template', template_dict)
57
+ global_config.save()
58
+ # Remove legacy flat key if present
59
+ if key in global_config._data:
60
+ del global_config._data[key]
61
+ runtime_config.set('template', template_dict)
62
+ print(f"Global config updated: template.{subkey} = {val.strip()}")
63
+ did_something = True
64
+ else:
65
+ global_config.set(key, val.strip())
66
+ global_config.save()
67
+ runtime_config.set(key, val.strip())
68
+ print(f"Global config updated: {key} = {val.strip()}")
69
+ did_something = True
45
70
 
46
71
  if args.set_api_key:
47
- global_config.set("api_key", args.set_api_key.strip())
72
+ # Merge: load full config, update api_key, save all
73
+ existing = dict(global_config.all())
74
+ existing["api_key"] = args.set_api_key.strip()
75
+ global_config._data = existing
48
76
  global_config.save()
49
77
  runtime_config.set("api_key", args.set_api_key.strip())
50
78
  print("Global API key saved.")
@@ -64,7 +92,18 @@ def handle_config_commands(args):
64
92
  else:
65
93
  from janito.agent.config import get_api_key
66
94
  from janito.agent.runtime_config import unified_config
95
+ # Handle template as nested dict
67
96
  for key in sorted(local_keys):
97
+ if key == "template":
98
+ template_dict = local_config.get("template", {})
99
+ if template_dict:
100
+ local_items["template"] = f"({len(template_dict)} keys set)"
101
+ for tkey, tval in template_dict.items():
102
+ local_items[f" template.{tkey}"] = tval
103
+ continue
104
+ if key.startswith("template."):
105
+ # Skip legacy flat keys
106
+ continue
68
107
  if key == "api_key":
69
108
  value = local_config.get("api_key")
70
109
  value = value[:4] + '...' + value[-4:] if value and len(value) > 8 else ('***' if value else None)
@@ -72,6 +111,15 @@ def handle_config_commands(args):
72
111
  value = unified_config.get(key)
73
112
  local_items[key] = value
74
113
  for key in sorted(global_keys - local_keys):
114
+ if key == "template":
115
+ template_dict = global_config.get("template", {})
116
+ if template_dict:
117
+ global_items["template"] = f"({len(template_dict)} keys set)"
118
+ for tkey, tval in template_dict.items():
119
+ global_items[f" template.{tkey}"] = tval
120
+ continue
121
+ if key.startswith("template."):
122
+ continue
75
123
  if key == "api_key":
76
124
  value = global_config.get("api_key")
77
125
  value = value[:4] + '...' + value[-4:] if value and len(value) > 8 else ('***' if value else None)
@@ -102,7 +150,7 @@ def handle_config_commands(args):
102
150
  # Special case for system_prompt: show template file if None
103
151
  if key == "system_prompt" and value is None:
104
152
  from pathlib import Path
105
- template_path = Path(__file__).parent.parent / "templates" / "system_instructions.j2"
153
+ template_path = Path(__file__).parent / "agent" / "templates" / "system_instructions.j2"
106
154
  print(f"{key} = (default template path: {home_shorten(str(template_path))})")
107
155
  else:
108
156
  print(f"{key} = {value}")
janito/cli/runner.py CHANGED
@@ -31,6 +31,7 @@ def run_cli(args):
31
31
  sys.exit(0)
32
32
 
33
33
  role = args.role or unified_config.get("role", "software engineer")
34
+
34
35
  # Ensure runtime_config is updated so chat shell sees the role
35
36
  if args.role:
36
37
  runtime_config.set('role', args.role)
@@ -39,21 +40,28 @@ def run_cli(args):
39
40
  if getattr(args, 'model', None):
40
41
  runtime_config.set('model', args.model)
41
42
 
43
+ # Set runtime_config['max_tools'] if --max-tools is provided
44
+ if getattr(args, 'max_tools', None) is not None:
45
+ runtime_config.set('max_tools', args.max_tools)
46
+
47
+ # Set trust mode if enabled
48
+ if getattr(args, 'trust', False):
49
+ runtime_config.set('trust', True)
50
+
42
51
  # New logic for --system-file
43
52
  system_prompt = None
44
53
  if getattr(args, 'system_file', None):
45
- try:
46
- with open(args.system_file, 'r', encoding='utf-8') as f:
47
- system_prompt = f.read()
48
- runtime_config.set('system_prompt_file', args.system_file)
49
- except Exception as e:
50
- print(f"[red]Failed to read system prompt file:[/red] {e}")
51
- sys.exit(1)
54
+ with open(args.system_file, 'r', encoding='utf-8') as f:
55
+ system_prompt = f.read()
56
+ runtime_config.set('system_prompt_file', args.system_file)
57
+
52
58
  else:
53
59
  system_prompt = args.system_prompt or unified_config.get("system_prompt")
54
60
  if args.system_prompt:
55
61
  runtime_config.set('system_prompt', system_prompt)
56
62
  if system_prompt is None:
63
+ # Pass full merged config (runtime overrides effective)
64
+
57
65
  system_prompt = render_system_prompt(role)
58
66
 
59
67
  if args.show_system:
@@ -72,10 +80,12 @@ def run_cli(args):
72
80
  # Always get model from unified_config (which checks runtime_config first)
73
81
  model = unified_config.get('model')
74
82
  base_url = unified_config.get('base_url', 'https://openrouter.ai/api/v1')
83
+ azure_openai_api_version = unified_config.get('azure_openai_api_version', '2023-05-15')
75
84
  # Handle --enable-tools flag
76
85
  from janito.agent.tool_handler import ToolHandler
77
- tool_handler = ToolHandler(verbose=args.verbose_tools, enable_tools=not getattr(args, 'disable_tools', False))
78
- agent = Agent(api_key=api_key, model=model, system_prompt=system_prompt, verbose_tools=args.verbose_tools, base_url=base_url, tool_handler=tool_handler)
86
+ tool_handler = ToolHandler(verbose=args.verbose_tools, enable_tools=not getattr(args, 'no_tools', False))
87
+ use_azure_openai = unified_config.get('use_azure_openai', False)
88
+ agent = Agent(api_key=api_key, model=model, system_prompt=system_prompt, verbose_tools=args.verbose_tools, base_url=base_url, tool_handler=tool_handler, azure_openai_api_version=azure_openai_api_version, use_azure_openai=use_azure_openai)
79
89
 
80
90
  # Save runtime max_tokens override if provided
81
91
  if args.max_tokens is not None:
@@ -103,10 +113,12 @@ def run_cli(args):
103
113
 
104
114
  try:
105
115
  try:
116
+ max_rounds = runtime_config.get('max_rounds', 50)
106
117
  response = agent.chat(
107
118
  messages,
108
119
  on_content=on_content,
109
120
  spinner=True,
121
+ max_rounds=max_rounds,
110
122
  )
111
123
  if args.verbose_response:
112
124
  import json
@@ -10,7 +10,7 @@ from janito.agent.runtime_config import runtime_config
10
10
  from janito.agent.conversation import EmptyResponseError, ProviderError
11
11
 
12
12
 
13
- def start_chat_shell(agent, continue_session=False):
13
+ def start_chat_shell(agent, continue_session=False, max_rounds=50):
14
14
  console = Console()
15
15
 
16
16
  # Load input history
@@ -51,7 +51,7 @@ def start_chat_shell(agent, continue_session=False):
51
51
  if agent.system_prompt and not any(m.get('role') == 'system' for m in messages):
52
52
  messages.insert(0, {"role": "system", "content": agent.system_prompt})
53
53
 
54
- print_welcome(console, version=__version__)
54
+ print_welcome(console, version=__version__, continued=continue_session)
55
55
 
56
56
  # Toolbar references
57
57
  def get_messages():
@@ -77,6 +77,8 @@ def start_chat_shell(agent, continue_session=False):
77
77
 
78
78
  # Main chat loop
79
79
  while True:
80
+ # max_rounds is now available for use in the chat loop
81
+
80
82
  try:
81
83
  if state.get('paste_mode'):
82
84
  console.print('')
@@ -126,7 +128,7 @@ def start_chat_shell(agent, continue_session=False):
126
128
  console.print(Markdown(content_piece))
127
129
 
128
130
  try:
129
- response = agent.chat(messages, on_content=on_content, spinner=True)
131
+ response = agent.chat(messages, on_content=on_content, spinner=True, max_rounds=max_rounds)
130
132
  except KeyboardInterrupt:
131
133
  console.print("[bold yellow]Request interrupted. Returning to prompt.[/bold yellow]")
132
134
  continue
@@ -22,46 +22,43 @@ def handle_continue(console, state, **kwargs):
22
22
  if not os.path.exists(save_path):
23
23
  console.print('[bold red]No saved conversation found.[/bold red]')
24
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}')
25
+
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
+
39
38
 
40
39
  def handle_history(console, state, *args, **kwargs):
41
40
  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}")
41
+ if not args:
42
+ # Default: last 5 messages
43
+ start = max(0, len(messages) - 5)
44
+ end = len(messages)
45
+ elif len(args) == 1:
46
+ count = int(args[0])
47
+ start = max(0, len(messages) - count)
48
+ end = len(messages)
49
+ elif len(args) >= 2:
50
+ start = int(args[0])
51
+ end = int(args[1]) + 1 # inclusive
52
+ else:
53
+ start = 0
54
+ end = len(messages)
55
+
56
+ console.print(f"[bold cyan]Showing messages {start} to {end - 1} (total {len(messages)}):[/bold cyan]")
57
+ for idx, msg in enumerate(messages[start:end], start=start):
58
+ role = msg.get('role', 'unknown')
59
+ content = msg.get('content', '')
60
+ console.print(f"[bold]{idx} [{role}]:[/bold] {content}")
61
+
65
62
 
66
63
  def handle_help(console, **kwargs):
67
64
  console.print("""
@@ -58,7 +58,7 @@ def handle_config_shell(console, *args, **kwargs):
58
58
  console.print("[bold red]Invalid format, expected key=val[/bold red]")
59
59
  return
60
60
  key = key.strip()
61
- if key not in CONFIG_OPTIONS:
61
+ if key not in CONFIG_OPTIONS and not key.startswith("template."):
62
62
  console.print(f"[bold red]Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}")
63
63
  return
64
64
  val = val.strip()
@@ -7,7 +7,7 @@ def load_prompt(filename=None):
7
7
  """
8
8
  if filename is None:
9
9
  # Default prompt file path (can be customized)
10
- filename = os.path.join(os.path.dirname(__file__), '../render_prompt/templates/system_instructions.j2')
10
+ filename = os.path.join(os.path.dirname(__file__), '../agent/templates/system_instructions.j2')
11
11
  filename = os.path.abspath(filename)
12
12
  if not os.path.exists(filename):
13
13
  raise FileNotFoundError(f"Prompt file not found: {filename}")
@@ -6,26 +6,22 @@ from datetime import datetime
6
6
  def load_last_summary(path='.janito/last_conversation.json'):
7
7
  if not os.path.exists(path):
8
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
9
+ with open(path, 'r', encoding='utf-8') as f:
10
+ data = json.load(f)
11
+ return data
12
+
15
13
 
16
14
 
17
15
  def load_last_conversation(path='.janito/last_conversation.json'):
18
16
  if not os.path.exists(path):
19
17
  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
18
+ with open(path, 'r', encoding='utf-8') as f:
19
+ data = json.load(f)
20
+ messages = data.get('messages', [])
21
+ prompts = data.get('prompts', [])
22
+ usage = data.get('last_usage_info')
23
+ return messages, prompts, usage
24
+
29
25
 
30
26
 
31
27
  def save_conversation(messages, prompts, usage_info=None, path='.janito/last_conversation.json'):
@@ -30,10 +30,11 @@ def print_summary(console, data, continue_session):
30
30
  console.print("[bold yellow]Type /continue to restore the last saved conversation.[/bold yellow]")
31
31
 
32
32
 
33
- def print_welcome(console, version=None):
33
+ def print_welcome(console, version=None, continued=False):
34
34
  version_str = f" (v{version})" if version else ""
35
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]")
36
+ if not continued:
37
+ console.print("[yellow]To resume your previous conversation, type /continue at any time.[/yellow]")
37
38
 
38
39
 
39
40
  def get_toolbar_func(messages_ref, last_usage_info_ref, last_elapsed_ref, model_name=None, role_ref=None):
@@ -68,10 +69,8 @@ def get_toolbar_func(messages_ref, last_usage_info_ref, last_elapsed_ref, model_
68
69
  from prompt_toolkit.application import get_app
69
70
 
70
71
  # 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
72
+ width = get_app().output.get_size().columns
73
+
75
74
 
76
75
  model_part = f" Model: <model>{model_name}</model>" if model_name else ""
77
76
  role_part = ""
@@ -87,7 +86,7 @@ def get_toolbar_func(messages_ref, last_usage_info_ref, last_elapsed_ref, model_
87
86
  first_line_parts.append(role_part)
88
87
  first_line = " | ".join(first_line_parts)
89
88
 
90
- help_part = "<b>/help</b> for help"
89
+ help_part = "<b>/help</b> for help | <b>F12</b>: just do it"
91
90
 
92
91
  total_len = len(left) + len(help_part) + 3 # separators and spaces
93
92
  if first_line:
@@ -110,6 +109,7 @@ def get_toolbar_func(messages_ref, last_usage_info_ref, last_elapsed_ref, model_
110
109
 
111
110
 
112
111
  def get_prompt_session(get_toolbar_func, mem_history):
112
+ from prompt_toolkit.key_binding import KeyBindings
113
113
  style = Style.from_dict({
114
114
  'bottom-toolbar': 'bg:#333333 #ffffff',
115
115
  'b': 'bold',
@@ -125,9 +125,18 @@ def get_prompt_session(get_toolbar_func, mem_history):
125
125
  '': 'bg:#000080 #ffffff',
126
126
  })
127
127
 
128
+ kb = KeyBindings()
129
+
130
+ @kb.add('f12')
131
+ def _(event):
132
+ """When F12 is pressed, send 'just do it' as input immediately."""
133
+ buf = event.app.current_buffer
134
+ buf.text = 'just do it'
135
+ buf.validate_and_handle()
136
+
128
137
  session = PromptSession(
129
138
  multiline=False,
130
- key_bindings=KeyBindings(),
139
+ key_bindings=kb,
131
140
  editing_mode=EditingMode.EMACS,
132
141
  bottom_toolbar=get_toolbar_func,
133
142
  style=style,
janito/render_prompt.py CHANGED
@@ -1,8 +1,10 @@
1
1
  import jinja2
2
2
  from pathlib import Path
3
3
 
4
+ from janito.agent.runtime_config import unified_config
5
+
4
6
  def render_system_prompt(role: str) -> str:
5
- template_loader = jinja2.FileSystemLoader(searchpath=str(Path(__file__).parent / "templates"))
7
+ template_loader = jinja2.FileSystemLoader(searchpath=str(Path(__file__).parent / "agent" / "templates"))
6
8
  env = jinja2.Environment(loader=template_loader)
7
9
  template = env.get_template("system_instructions.j2")
8
10
  return template.render(role=role)
janito/web/app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, request, render_template, Response, send_from_directory, session, jsonify
1
+ from flask import Flask, request, Response, send_from_directory, session, jsonify
2
2
  from queue import Queue
3
3
  import json
4
4
  from janito.agent.queued_tool_handler import QueuedToolHandler
@@ -8,8 +8,15 @@ from janito.render_prompt import render_system_prompt
8
8
  import os
9
9
  import threading
10
10
 
11
- # Render system prompt once
12
- system_prompt = render_system_prompt("software engineer")
11
+ from janito.agent.runtime_config import unified_config
12
+
13
+ # Render system prompt from config
14
+ role = unified_config.get("role", "software engineer")
15
+ system_prompt_override = unified_config.get("system_prompt")
16
+ if system_prompt_override:
17
+ system_prompt = system_prompt_override
18
+ else:
19
+ system_prompt = render_system_prompt(role)
13
20
 
14
21
  app = Flask(
15
22
  __name__,
@@ -33,15 +40,59 @@ stream_queue = Queue()
33
40
  # Create a QueuedToolHandler with the queue
34
41
  queued_handler = QueuedToolHandler(stream_queue)
35
42
 
36
- # Instantiate the Agent with the custom tool handler
43
+ # Instantiate the Agent with config-driven parameters
37
44
  agent = Agent(
38
- api_key=get_api_key(),
45
+ api_key=unified_config.get("api_key"),
46
+ model=unified_config.get("model"),
47
+ base_url=unified_config.get("base_url"),
39
48
  tool_handler=queued_handler
40
49
  )
41
50
 
42
- @app.route('/get_model_name')
43
- def get_model_name():
44
- return jsonify({"model": agent.model})
51
+ @app.route('/get_config')
52
+ def get_config():
53
+ # Expose full config for the web app: defaults, effective, runtime (mask api_key)
54
+ from janito.agent.runtime_config import unified_config
55
+ from janito.agent.config_defaults import CONFIG_DEFAULTS
56
+ # Start with defaults
57
+ config = dict(CONFIG_DEFAULTS)
58
+ # Overlay effective config
59
+ config.update(unified_config.effective_cfg.all())
60
+ # Overlay runtime config (highest priority)
61
+ config.update(unified_config.runtime_cfg.all())
62
+ api_key = config.get("api_key")
63
+ if api_key:
64
+ config["api_key"] = api_key[:4] + '...' + api_key[-4:] if len(api_key) > 8 else '***'
65
+ return jsonify(config)
66
+
67
+ @app.route('/set_config', methods=['POST'])
68
+ def set_config():
69
+ from janito.agent.runtime_config import runtime_config
70
+ from janito.agent.config import CONFIG_OPTIONS
71
+ from janito.agent.config_defaults import CONFIG_DEFAULTS
72
+ data = request.get_json()
73
+ key = data.get('key')
74
+ value = data.get('value')
75
+ if key not in CONFIG_OPTIONS:
76
+ return jsonify({'status': 'error', 'message': f'Invalid config key: {key}'}), 400
77
+ # Type coercion based on defaults
78
+ default = CONFIG_DEFAULTS.get(key)
79
+ if default is not None and value is not None:
80
+ try:
81
+ if isinstance(default, bool):
82
+ value = bool(value)
83
+ elif isinstance(default, int):
84
+ value = int(value)
85
+ elif isinstance(default, float):
86
+ value = float(value)
87
+ # else: leave as string or None
88
+ except Exception as e:
89
+ return jsonify({'status': 'error', 'message': f'Invalid value type for {key}: {e}'}), 400
90
+ runtime_config.set(key, value)
91
+ # Mask api_key in response
92
+ resp_value = value
93
+ if key == 'api_key' and value:
94
+ resp_value = value[:4] + '...' + value[-4:] if len(value) > 8 else '***'
95
+ return jsonify({'status': 'ok', 'key': key, 'value': resp_value})
45
96
 
46
97
 
47
98
  @app.route('/favicon.ico')
@@ -91,19 +142,25 @@ def execute_stream():
91
142
  conversation.append({"role": "user", "content": user_input})
92
143
 
93
144
  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
145
  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)
146
+ response = agent.chat(
147
+ conversation,
148
+ on_content=lambda data: stream_queue.put({"type": "content", "content": data.get("content")})
149
+ )
150
+ if response and 'content' in response:
151
+ conversation.append({"role": "assistant", "content": response['content']})
152
+ try:
153
+ os.makedirs(os.path.dirname(conversation_file), exist_ok=True)
154
+ with open(conversation_file, 'w') as f:
155
+ json.dump(conversation, f, indent=2)
156
+ except Exception as e:
157
+ print(f"Error saving conversation: {e}")
104
158
  except Exception as e:
105
- print(f"Error saving conversation: {e}")
106
- stream_queue.put(None)
159
+ import traceback
160
+ tb = traceback.format_exc()
161
+ stream_queue.put({"type": "error", "error": str(e), "traceback": tb})
162
+ finally:
163
+ stream_queue.put(None)
107
164
 
108
165
  threading.Thread(target=run_agent, daemon=True).start()
109
166