janito 2.3.1__py3-none-any.whl → 2.4.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 (96) hide show
  1. janito/__init__.py +1 -1
  2. janito/_version.py +57 -0
  3. janito/agent/setup_agent.py +92 -18
  4. janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +44 -0
  5. janito/cli/chat_mode/bindings.py +21 -2
  6. janito/cli/chat_mode/chat_entry.py +2 -3
  7. janito/cli/chat_mode/prompt_style.py +5 -0
  8. janito/cli/chat_mode/session.py +80 -94
  9. janito/cli/chat_mode/session_profile_select.py +80 -0
  10. janito/cli/chat_mode/shell/commands/__init__.py +13 -7
  11. janito/cli/chat_mode/shell/commands/_priv_check.py +5 -0
  12. janito/cli/chat_mode/shell/commands/conversation_restart.py +30 -0
  13. janito/cli/chat_mode/shell/commands/execute.py +42 -0
  14. janito/cli/chat_mode/shell/commands/help.py +6 -3
  15. janito/cli/chat_mode/shell/commands/model.py +28 -0
  16. janito/cli/chat_mode/shell/commands/read.py +37 -0
  17. janito/cli/chat_mode/shell/commands/tools.py +45 -18
  18. janito/cli/chat_mode/shell/commands/write.py +37 -0
  19. janito/cli/chat_mode/shell/commands.bak.zip +0 -0
  20. janito/cli/chat_mode/shell/session.bak.zip +0 -0
  21. janito/cli/chat_mode/toolbar.py +44 -27
  22. janito/cli/cli_commands/list_tools.py +44 -11
  23. janito/cli/cli_commands/model_utils.py +95 -95
  24. janito/cli/cli_commands/show_system_prompt.py +57 -14
  25. janito/cli/config.py +5 -6
  26. janito/cli/core/getters.py +33 -33
  27. janito/cli/core/runner.py +25 -18
  28. janito/cli/core/setters.py +10 -1
  29. janito/cli/main_cli.py +28 -5
  30. janito/cli/prompt_core.py +18 -2
  31. janito/cli/prompt_setup.py +56 -0
  32. janito/cli/single_shot_mode/handler.py +14 -73
  33. janito/cli/verbose_output.py +1 -1
  34. janito/config_manager.py +125 -112
  35. janito/drivers/dashscope.bak.zip +0 -0
  36. janito/drivers/openai/README.md +20 -0
  37. janito/drivers/openai_responses.bak.zip +0 -0
  38. janito/event_bus/event.py +2 -2
  39. janito/i18n/pt.py +0 -1
  40. janito/llm/README.md +23 -0
  41. janito/llm/agent.py +80 -16
  42. janito/llm/auth.py +63 -63
  43. janito/llm/driver.py +8 -0
  44. janito/provider_registry.py +178 -176
  45. janito/providers/azure_openai/model_info.py +16 -16
  46. janito/providers/dashscope.bak.zip +0 -0
  47. janito/providers/registry.py +26 -26
  48. janito/shell.bak.zip +0 -0
  49. janito/tools/DOCSTRING_STANDARD.txt +33 -0
  50. janito/tools/README.md +3 -0
  51. janito/tools/__init__.py +20 -6
  52. janito/tools/adapters/local/__init__.py +65 -62
  53. janito/tools/adapters/local/adapter.py +18 -35
  54. janito/tools/adapters/local/ask_user.py +3 -4
  55. janito/tools/adapters/local/copy_file.py +2 -2
  56. janito/tools/adapters/local/create_directory.py +2 -2
  57. janito/tools/adapters/local/create_file.py +2 -2
  58. janito/tools/adapters/local/delete_text_in_file.py +2 -2
  59. janito/tools/adapters/local/fetch_url.py +2 -2
  60. janito/tools/adapters/local/find_files.py +2 -1
  61. janito/tools/adapters/local/get_file_outline/core.py +2 -2
  62. janito/tools/adapters/local/get_file_outline/search_outline.py +2 -2
  63. janito/tools/adapters/local/move_file.py +2 -2
  64. janito/tools/adapters/local/open_html_in_browser.py +2 -1
  65. janito/tools/adapters/local/open_url.py +2 -2
  66. janito/tools/adapters/local/python_code_run.py +3 -3
  67. janito/tools/adapters/local/python_command_run.py +3 -3
  68. janito/tools/adapters/local/python_file_run.py +3 -3
  69. janito/tools/adapters/local/remove_directory.py +2 -2
  70. janito/tools/adapters/local/remove_file.py +2 -2
  71. janito/tools/adapters/local/replace_text_in_file.py +2 -2
  72. janito/tools/adapters/local/run_bash_command.py +3 -3
  73. janito/tools/adapters/local/run_powershell_command.py +3 -3
  74. janito/tools/adapters/local/search_text/core.py +2 -2
  75. janito/tools/adapters/local/validate_file_syntax/core.py +2 -2
  76. janito/tools/adapters/local/view_file.py +2 -1
  77. janito/tools/outline_file.bak.zip +0 -0
  78. janito/tools/permissions.py +45 -0
  79. janito/tools/permissions_parse.py +12 -0
  80. janito/tools/tool_base.py +14 -1
  81. janito/tools/tool_utils.py +4 -6
  82. janito/tools/tools_adapter.py +25 -20
  83. {janito-2.3.1.dist-info → janito-2.4.0.dist-info}/METADATA +51 -16
  84. {janito-2.3.1.dist-info → janito-2.4.0.dist-info}/RECORD +88 -74
  85. janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +0 -13
  86. janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +0 -37
  87. janito/cli/chat_mode/shell/commands/edit.py +0 -25
  88. janito/cli/chat_mode/shell/commands/exec.py +0 -27
  89. janito/cli/chat_mode/shell/commands/termweb_log.py +0 -92
  90. janito/cli/termweb_starter.py +0 -122
  91. janito/termweb/app.py +0 -95
  92. janito/version.py +0 -4
  93. {janito-2.3.1.dist-info → janito-2.4.0.dist-info}/WHEEL +0 -0
  94. {janito-2.3.1.dist-info → janito-2.4.0.dist-info}/entry_points.txt +0 -0
  95. {janito-2.3.1.dist-info → janito-2.4.0.dist-info}/licenses/LICENSE +0 -0
  96. {janito-2.3.1.dist-info → janito-2.4.0.dist-info}/top_level.txt +0 -0
@@ -2,48 +2,32 @@
2
2
  PromptHandler: Handles prompt submission and response formatting for janito CLI (one-shot prompt execution).
3
3
  """
4
4
 
5
- import time
6
- from janito.version import __version__ as VERSION
7
- from janito.cli.prompt_core import PromptHandler as GenericPromptHandler
8
- from janito.cli.verbose_output import (
9
- print_verbose_header,
10
- print_performance,
11
- handle_exception,
12
- )
5
+ from __future__ import annotations
6
+
7
+ from janito.cli.prompt_setup import setup_agent_and_prompt_handler
13
8
  import janito.tools # Ensure all tools are registered
14
9
  from janito.cli.console import shared_console
15
10
 
16
11
 
17
12
  class PromptHandler:
18
- def __init__(self, args, provider_instance, llm_driver_config, role=None, exec_enabled=False):
13
+ def __init__(self, args, provider_instance, llm_driver_config, role=None, exec_enabled=False, allowed_permissions=None):
19
14
  self.args = args
20
15
  self.provider_instance = provider_instance
21
16
  self.llm_driver_config = llm_driver_config
22
17
  self.role = role
23
18
  self.exec_enabled = exec_enabled
24
- from janito.agent.setup_agent import create_configured_agent
25
-
26
- # DEBUG: Print exec_enabled propagation
27
- self.agent = create_configured_agent(
19
+ # Instantiate agent together with prompt handler using the shared helper
20
+ self.agent, self.generic_handler = setup_agent_and_prompt_handler(
21
+ args=args,
28
22
  provider_instance=provider_instance,
29
23
  llm_driver_config=llm_driver_config,
30
24
  role=role,
31
25
  verbose_tools=getattr(args, "verbose_tools", False),
32
26
  verbose_agent=getattr(args, "verbose_agent", False),
33
27
  exec_enabled=exec_enabled,
28
+ allowed_permissions=allowed_permissions,
29
+ profile=getattr(args, "profile", None),
34
30
  )
35
- # Setup conversation/history if needed
36
- # Dynamically enable/disable execution tools in the registry
37
- try:
38
- registry = __import__('janito.tools', fromlist=['get_local_tools_adapter']).get_local_tools_adapter()
39
- if hasattr(registry, 'set_execution_tools_enabled'):
40
- registry.set_execution_tools_enabled(exec_enabled)
41
- except Exception as e:
42
- shared_console.print(f"[yellow]Warning: Could not update execution tools dynamically in single-shot mode: {e}[/yellow]")
43
- self.generic_handler = GenericPromptHandler(
44
- args, [], provider_instance=provider_instance
45
- )
46
- self.generic_handler.agent = self.agent
47
31
 
48
32
  def handle(self) -> None:
49
33
  import traceback
@@ -75,56 +59,13 @@ class PromptHandler:
75
59
  self._post_prompt_actions()
76
60
 
77
61
  def _post_prompt_actions(self):
78
- final_event = getattr(self.agent, "last_event", None)
79
- if final_event is not None:
80
- self._print_exit_reason_and_parts(final_event)
81
- # --- BEGIN: Print token info in rich rule if --verbose is set ---
82
- if hasattr(self.args, "verbose") and self.args.verbose:
83
- from janito.perf_singleton import performance_collector
84
-
85
- token_info = performance_collector.get_last_request_usage()
86
- from rich.rule import Rule
87
- from rich import print as rich_print
88
- from janito.cli.utils import format_tokens
89
-
90
- if token_info:
91
- if isinstance(token_info, dict):
92
- token_str = " | ".join(
93
- f"{k}: {format_tokens(v) if isinstance(v, int) else v}"
94
- for k, v in token_info.items()
95
- )
96
- else:
97
- token_str = str(token_info)
98
- rich_print(Rule(f"[bold cyan]Token Usage[/bold cyan] {token_str}"))
99
- else:
100
- rich_print(Rule("[cyan]No token usage info available.[/cyan]"))
101
- else:
102
- shared_console.print("[yellow]No output produced by the model.[/yellow]")
62
+ # Align with chat mode: only print token usage summary
63
+ from janito.formatting_token import print_token_message_summary
64
+ from janito.perf_singleton import performance_collector
65
+ usage = performance_collector.get_last_request_usage()
66
+ print_token_message_summary(shared_console, msg_count=1, usage=usage)
103
67
  self._cleanup_driver_and_console()
104
68
 
105
- def _print_exit_reason_and_parts(self, final_event):
106
- exit_reason = (
107
- getattr(final_event, "metadata", {}).get("exit_reason")
108
- if hasattr(final_event, "metadata")
109
- else None
110
- )
111
- if exit_reason:
112
- print(f"[bold yellow]Exit reason: {exit_reason}[/bold yellow]")
113
- parts = getattr(final_event, "parts", None)
114
- if not exit_reason:
115
- if parts is None or len(parts) == 0:
116
- shared_console.print(
117
- "[yellow]No output produced by the model.[/yellow]"
118
- )
119
- else:
120
- if hasattr(self.args, "verbose_agent") and self.args.verbose_agent:
121
- print(
122
- "[yellow]No user-visible output. Model returned the following parts:"
123
- )
124
- for idx, part in enumerate(parts):
125
- print(
126
- f" [part {idx}] type: {type(part).__name__} | content: {getattr(part, 'content', repr(part))}"
127
- )
128
69
 
129
70
  def _cleanup_driver_and_console(self):
130
71
  if hasattr(self.agent, "join_driver"):
@@ -6,7 +6,7 @@ from rich import print as rich_print
6
6
  from rich.align import Align
7
7
  from rich.panel import Panel
8
8
  from rich.text import Text
9
- from janito.version import __version__ as VERSION
9
+ from janito import __version__ as VERSION
10
10
  from janito.cli.utils import format_tokens
11
11
 
12
12
 
janito/config_manager.py CHANGED
@@ -1,112 +1,125 @@
1
- import json
2
- from pathlib import Path
3
- from threading import Lock
4
-
5
-
6
- class ConfigManager:
7
- """
8
- Unified configuration manager supporting:
9
- - Defaults
10
- - File-based configuration
11
- - Runtime overrides (e.g., CLI args)
12
- """
13
-
14
- _instance = None
15
- _lock = Lock()
16
-
17
- def __new__(cls, *args, **kwargs):
18
- with cls._lock:
19
- if not cls._instance:
20
- cls._instance = super(ConfigManager, cls).__new__(cls)
21
- return cls._instance
22
-
23
- def __init__(self, config_path=None, defaults=None, runtime_overrides=None):
24
- # Lazy single-init
25
- if hasattr(self, "_initialized") and self._initialized:
26
- return
27
- self._initialized = True
28
-
29
- self.config_path = Path(config_path or Path.home() / ".janito" / "config.json")
30
- self.defaults = dict(defaults) if defaults else {}
31
- self.file_config = {}
32
- self.runtime_overrides = dict(runtime_overrides) if runtime_overrides else {}
33
- self._load_file_config()
34
-
35
- def _load_file_config(self):
36
- if self.config_path.exists():
37
- with open(self.config_path, "r", encoding="utf-8") as f:
38
- try:
39
- self.file_config = json.load(f)
40
- except Exception:
41
- self.file_config = {}
42
- else:
43
- self.file_config = {}
44
-
45
- def save(self):
46
- self.config_path.parent.mkdir(parents=True, exist_ok=True)
47
- with open(self.config_path, "w", encoding="utf-8") as f:
48
- json.dump(self.file_config, f, indent=2)
49
- f.write("\n")
50
-
51
- def get(self, key, default=None):
52
- # Precedence: runtime_overrides > file_config > defaults
53
- for layer in (self.runtime_overrides, self.file_config, self.defaults):
54
- if key in layer and layer[key] is not None:
55
- return layer[key]
56
- return default
57
-
58
- def runtime_set(self, key, value):
59
- self.runtime_overrides[key] = value
60
-
61
- def file_set(self, key, value):
62
- # Always reload, update, and persist
63
- self._load_file_config()
64
- self.file_config[key] = value
65
- with open(self.config_path, "w", encoding="utf-8") as f:
66
- json.dump(self.file_config, f, indent=2)
67
- f.write("\n")
68
-
69
- def all(self, layered=False):
70
- merged = dict(self.defaults)
71
- merged.update(self.file_config)
72
- merged.update(self.runtime_overrides)
73
- if layered:
74
- # Only file+runtime, i.e., what is saved to disk
75
- d = dict(self.file_config)
76
- d.update(self.runtime_overrides)
77
- return d
78
- return merged
79
-
80
- # Namespaced provider/model config
81
- def get_provider_config(self, provider, default=None):
82
- providers = self.file_config.get("providers") or {}
83
- return providers.get(provider) or (default or {})
84
-
85
- def set_provider_config(self, provider, key, value):
86
- if "providers" not in self.file_config:
87
- self.file_config["providers"] = {}
88
- if provider not in self.file_config["providers"]:
89
- self.file_config["providers"][provider] = {}
90
- self.file_config["providers"][provider][key] = value
91
-
92
- def get_provider_model_config(self, provider, model, default=None):
93
- return (
94
- self.file_config.get("providers")
95
- or {}.get(provider, {}).get("models", {}).get(model)
96
- or (default or {})
97
- )
98
-
99
- def set_provider_model_config(self, provider, model, key, value):
100
- if "providers" not in self.file_config:
101
- self.file_config["providers"] = {}
102
- if provider not in self.file_config["providers"]:
103
- self.file_config["providers"][provider] = {}
104
- if "models" not in self.file_config["providers"][provider]:
105
- self.file_config["providers"][provider]["models"] = {}
106
- if model not in self.file_config["providers"][provider]["models"]:
107
- self.file_config["providers"][provider]["models"][model] = {}
108
- self.file_config["providers"][provider]["models"][model][key] = value
109
-
110
- # Support loading runtime overrides after init (e.g. after parsing CLI args)
111
- def apply_runtime_overrides(self, overrides_dict):
112
- self.runtime_overrides.update(overrides_dict)
1
+ import json
2
+ from pathlib import Path
3
+ from threading import Lock
4
+
5
+
6
+ class ConfigManager:
7
+ """
8
+ Unified configuration manager supporting:
9
+ - Defaults
10
+ - File-based configuration
11
+ - Runtime overrides (e.g., CLI args)
12
+ """
13
+
14
+ _instance = None
15
+ _lock = Lock()
16
+
17
+ def __new__(cls, *args, **kwargs):
18
+ with cls._lock:
19
+ if not cls._instance:
20
+ cls._instance = super(ConfigManager, cls).__new__(cls)
21
+ return cls._instance
22
+
23
+ def __init__(self, config_path=None, defaults=None, runtime_overrides=None):
24
+ # Lazy single-init
25
+ if hasattr(self, "_initialized") and self._initialized:
26
+ return
27
+ self._initialized = True
28
+
29
+ self.config_path = Path(config_path or Path.home() / ".janito" / "config.json")
30
+ self.defaults = dict(defaults) if defaults else {}
31
+ self.file_config = {}
32
+ self.runtime_overrides = dict(runtime_overrides) if runtime_overrides else {}
33
+ self._load_file_config()
34
+ self._apply_tool_permissions_on_startup()
35
+
36
+ def _apply_tool_permissions_on_startup(self):
37
+ # On startup, read tool_permissions from config and set global permissions
38
+ perm_str = self.file_config.get("tool_permissions")
39
+ if perm_str:
40
+ try:
41
+ from janito.tools.permissions_parse import parse_permissions_string
42
+ from janito.tools.permissions import set_global_allowed_permissions
43
+ perms = parse_permissions_string(perm_str)
44
+ set_global_allowed_permissions(perms)
45
+ except Exception as e:
46
+ print(f"Warning: Failed to apply tool_permissions from config: {e}")
47
+
48
+ def _load_file_config(self):
49
+ if self.config_path.exists():
50
+ with open(self.config_path, "r", encoding="utf-8") as f:
51
+ try:
52
+ self.file_config = json.load(f)
53
+ except Exception:
54
+ self.file_config = {}
55
+ else:
56
+ self.file_config = {}
57
+
58
+ def save(self):
59
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
60
+ with open(self.config_path, "w", encoding="utf-8") as f:
61
+ json.dump(self.file_config, f, indent=2)
62
+ f.write("\n")
63
+
64
+ def get(self, key, default=None):
65
+ # Precedence: runtime_overrides > file_config > defaults
66
+ for layer in (self.runtime_overrides, self.file_config, self.defaults):
67
+ if key in layer and layer[key] is not None:
68
+ return layer[key]
69
+ return default
70
+
71
+ def runtime_set(self, key, value):
72
+ self.runtime_overrides[key] = value
73
+
74
+ def file_set(self, key, value):
75
+ # Always reload, update, and persist
76
+ self._load_file_config()
77
+ self.file_config[key] = value
78
+ with open(self.config_path, "w", encoding="utf-8") as f:
79
+ json.dump(self.file_config, f, indent=2)
80
+ f.write("\n")
81
+
82
+ def all(self, layered=False):
83
+ merged = dict(self.defaults)
84
+ merged.update(self.file_config)
85
+ merged.update(self.runtime_overrides)
86
+ if layered:
87
+ # Only file+runtime, i.e., what is saved to disk
88
+ d = dict(self.file_config)
89
+ d.update(self.runtime_overrides)
90
+ return d
91
+ return merged
92
+
93
+ # Namespaced provider/model config
94
+ def get_provider_config(self, provider, default=None):
95
+ providers = self.file_config.get("providers") or {}
96
+ return providers.get(provider) or (default or {})
97
+
98
+ def set_provider_config(self, provider, key, value):
99
+ if "providers" not in self.file_config:
100
+ self.file_config["providers"] = {}
101
+ if provider not in self.file_config["providers"]:
102
+ self.file_config["providers"][provider] = {}
103
+ self.file_config["providers"][provider][key] = value
104
+
105
+ def get_provider_model_config(self, provider, model, default=None):
106
+ return (
107
+ self.file_config.get("providers")
108
+ or {}.get(provider, {}).get("models", {}).get(model)
109
+ or (default or {})
110
+ )
111
+
112
+ def set_provider_model_config(self, provider, model, key, value):
113
+ if "providers" not in self.file_config:
114
+ self.file_config["providers"] = {}
115
+ if provider not in self.file_config["providers"]:
116
+ self.file_config["providers"][provider] = {}
117
+ if "models" not in self.file_config["providers"][provider]:
118
+ self.file_config["providers"][provider]["models"] = {}
119
+ if model not in self.file_config["providers"][provider]["models"]:
120
+ self.file_config["providers"][provider]["models"][model] = {}
121
+ self.file_config["providers"][provider]["models"][model][key] = value
122
+
123
+ # Support loading runtime overrides after init (e.g. after parsing CLI args)
124
+ def apply_runtime_overrides(self, overrides_dict):
125
+ self.runtime_overrides.update(overrides_dict)
Binary file
@@ -0,0 +1,20 @@
1
+ # OpenAI Driver Debugging
2
+
3
+ ## HTTP Debugging via Environment Variable
4
+
5
+ To debug HTTP requests and responses for the OpenAI driver, set the environment variable `OPENAI_DEBUG_HTTP=1` before running your application. This will print the full HTTP request and response bodies to the console for troubleshooting purposes.
6
+
7
+ **Example (PowerShell):**
8
+
9
+ ```
10
+ $env:OPENAI_DEBUG_HTTP=1
11
+ python your_app.py
12
+ ```
13
+
14
+ **Example (bash):**
15
+
16
+ ```
17
+ OPENAI_DEBUG_HTTP=1 python your_app.py
18
+ ```
19
+
20
+ This feature is implemented in `janito/drivers/openai/driver.py` and works by wrapping the OpenAI client HTTP transport with a debug logger when the environment variable is set.
Binary file
janito/event_bus/event.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import attr
2
2
  from typing import ClassVar
3
- from datetime import datetime
3
+ from datetime import datetime, timezone
4
4
 
5
5
 
6
6
  @attr.s(auto_attribs=True, kw_only=True)
@@ -12,4 +12,4 @@ class Event:
12
12
  """
13
13
 
14
14
  category: ClassVar[str] = "generic"
15
- timestamp: datetime = attr.ib(factory=datetime.utcnow)
15
+ timestamp: datetime = attr.ib(factory=lambda: datetime.now(timezone.utc))
janito/i18n/pt.py CHANGED
@@ -3,7 +3,6 @@ translations = {
3
3
  "36107ed78ab25f6fb12ad8ce13018cd1ce6735d1": "Iniciando servidor web...",
4
4
  "70a0d194687568a47aa617fd85036ace1e69a982": "Deseja realmente sair? (s/n): ",
5
5
  "5c9ebcbbd7632ecb328bd52958b17158afaa32c6": "F12 = Ação Rápida (segue a ação recomendada)",
6
- "e4034394acca752c021b2ab50f60da8273e3c314": "TermWeb iniciado... Disponível em http://localhost:{selected_port}",
7
6
  "fe21121e2934234b68d19b2757532117d440c1e3": "Chave de API não encontrada. Por favor, configure 'api_key' no seu arquivo de configuração.",
8
7
  "c9e3759b1756eba35b381ce2b72cd659e132b01f": "Olá, {name}!",
9
8
  "ca1fee2f55baabdc2e4b0e9529c89ee024e62079": "Nenhum prompt fornecido nas mensagens",
janito/llm/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # janito.llm Package
2
+
3
+ This directory contains generic, provider-agnostic classes and methods for working with Large Language Models (LLMs) in the `janito` framework. Its purpose is to provide base abstractions that can be extended by provider-specific implementations.
4
+
5
+ ## Scope and Contents
6
+
7
+ - **driver.py**
8
+ - Contains `LLMDriver`, an abstract base class defining the core methods for LLM drivers. Subclasses should implement provider/model-specific logic, while benefiting from consistent streaming and event interfaces.
9
+ - **provider.py**
10
+ - Contains `LLMProvider`, an abstract base class for LLM API providers. This outlines the required interface for integrating new providers and retrieving model info or driver classes.
11
+
12
+ ## Usage
13
+ - Extend these base classes when adding new drivers or providers.
14
+ - Do not include provider-specific logic here; only generic mechanisms, patterns, or utilities applicable to any LLM integration belong in this package.
15
+
16
+ ## Example
17
+ ```python
18
+ from janito.llm.driver import LLMDriver
19
+ from janito.llm.provider import LLMProvider
20
+ ```
21
+
22
+ ---
23
+ This README clarifies the intention of the `llm` package as the generic/static contract for LLM drivers and providers.
janito/llm/agent.py CHANGED
@@ -94,6 +94,21 @@ class LLMAgent:
94
94
  autoescape=select_autoescape(),
95
95
  )
96
96
  template = env.get_template(Path(self.system_prompt_template).name)
97
+ # Refresh allowed_permissions in context before rendering
98
+ from janito.tools.permissions import get_global_allowed_permissions
99
+ from janito.tools.tool_base import ToolPermissions
100
+ perms = get_global_allowed_permissions()
101
+ if isinstance(perms, ToolPermissions):
102
+ perm_str = ""
103
+ if perms.read:
104
+ perm_str += "r"
105
+ if perms.write:
106
+ perm_str += "w"
107
+ if perms.execute:
108
+ perm_str += "x"
109
+ self._template_vars["allowed_permissions"] = perm_str or None
110
+ else:
111
+ self._template_vars["allowed_permissions"] = perms
97
112
  self.system_prompt = template.render(**self._template_vars)
98
113
 
99
114
  def get_system_prompt(self) -> str:
@@ -165,13 +180,10 @@ class LLMAgent:
165
180
  if getattr(self, "verbose_agent", False):
166
181
  print("[agent] [DEBUG] Entered _process_next_response")
167
182
  elapsed = 0.0
168
- try:
169
- if getattr(self, "verbose_agent", False):
170
- print("[agent] [DEBUG] Waiting for event from output_queue...")
171
- return self._poll_for_event(poll_timeout, max_wait_time)
172
- except KeyboardInterrupt:
173
- self._handle_keyboard_interrupt()
174
- return None, False
183
+ if getattr(self, "verbose_agent", False):
184
+ print("[agent] [DEBUG] Waiting for event from output_queue...")
185
+ # Let KeyboardInterrupt propagate to caller
186
+ return self._poll_for_event(poll_timeout, max_wait_time)
175
187
 
176
188
  def _poll_for_event(self, poll_timeout, max_wait_time):
177
189
  elapsed = 0.0
@@ -201,15 +213,6 @@ class LLMAgent:
201
213
  ]:
202
214
  return (event, False)
203
215
 
204
- def _handle_keyboard_interrupt(self):
205
- if hasattr(self, "input_queue") and self.input_queue is not None:
206
- from janito.driver_events import RequestFinished
207
-
208
- cancel_event = RequestFinished(
209
- status=RequestStatus.CANCELLED,
210
- reason="User interrupted (KeyboardInterrupt)",
211
- )
212
- self.input_queue.put(cancel_event)
213
216
 
214
217
  def _get_event_from_output_queue(self, poll_timeout):
215
218
  try:
@@ -302,6 +305,13 @@ class LLMAgent:
302
305
  and hasattr(self.driver, "clear_output_queue")
303
306
  ):
304
307
  self.driver.clear_output_queue()
308
+ # Drain input queue before sending new messages
309
+ if (
310
+ hasattr(self, "driver")
311
+ and self.driver
312
+ and hasattr(self.driver, "clear_input_queue")
313
+ ):
314
+ self.driver.clear_input_queue()
305
315
  """
306
316
  Main agent conversation loop supporting function/tool calls and conversation history extension, now as a blocking event-driven loop with event publishing.
307
317
 
@@ -329,6 +339,7 @@ class LLMAgent:
329
339
  try:
330
340
  result, added_tool_results = self._process_next_response()
331
341
  except KeyboardInterrupt:
342
+ # Propagate the interrupt to the caller, but signal the driver to cancel first
332
343
  cancel_event.set()
333
344
  raise
334
345
  if getattr(self, "verbose_agent", False):
@@ -420,6 +431,59 @@ class LLMAgent:
420
431
  return self.llm_provider.model_name
421
432
  return "?"
422
433
 
434
+ def reset_driver_config_to_model_defaults(self, model_name: str):
435
+ """
436
+ Reset all driver config fields to the model's defaults for the current provider (overwriting any user customizations).
437
+ """
438
+ provider = self.llm_provider
439
+ # Find model spec
440
+ model_spec = None
441
+ if hasattr(provider, "MODEL_SPECS"):
442
+ model_spec = provider.MODEL_SPECS.get(model_name)
443
+ if not model_spec:
444
+ raise ValueError(f"Model '{model_name}' not found in provider MODEL_SPECS.")
445
+ # Overwrite all config fields with model defaults
446
+ config = getattr(provider, "driver_config", None)
447
+ if config is None:
448
+ return
449
+ config.model = model_name
450
+ # Standard fields, with safe conversion for int fields
451
+ def safe_int(val):
452
+ try:
453
+ if val is None or val == "N/A":
454
+ return None
455
+ return int(val)
456
+ except Exception:
457
+ return None
458
+ def safe_float(val):
459
+ try:
460
+ if val is None or val == "N/A":
461
+ return None
462
+ return float(val)
463
+ except Exception:
464
+ return None
465
+ config.temperature = safe_float(getattr(model_spec, "default_temp", None))
466
+ config.max_tokens = safe_int(getattr(model_spec, "max_response", None))
467
+ config.max_completion_tokens = safe_int(getattr(model_spec, "max_cot", None))
468
+ # Optionally reset other fields to None/defaults
469
+ config.top_p = None
470
+ config.presence_penalty = None
471
+ config.frequency_penalty = None
472
+ config.stop = None
473
+ config.reasoning_effort = None
474
+ # Update driver if present
475
+ if self.driver is not None:
476
+ if hasattr(self.driver, "model_name"):
477
+ self.driver.model_name = model_name
478
+ if hasattr(self.driver, "config"):
479
+ self.driver.config = config
480
+
481
+ def change_model(self, model_name: str):
482
+ """
483
+ Change the model for the agent's provider and driver config, and update the driver if present.
484
+ """
485
+ self.reset_driver_config_to_model_defaults(model_name)
486
+
423
487
  def join_driver(self, timeout=None):
424
488
  """
425
489
  Wait for the driver's background thread to finish. Call this before exiting to avoid daemon thread shutdown errors.