janito 2.3.1__py3-none-any.whl → 2.5.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 -1
- janito/_version.py +57 -0
- janito/agent/setup_agent.py +95 -21
- janito/agent/templates/profiles/system_prompt_template_assistant.txt.j2 +1 -0
- janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +44 -0
- janito/cli/chat_mode/bindings.py +21 -2
- janito/cli/chat_mode/chat_entry.py +2 -3
- janito/cli/chat_mode/prompt_style.py +5 -0
- janito/cli/chat_mode/script_runner.py +153 -0
- janito/cli/chat_mode/session.py +128 -122
- janito/cli/chat_mode/session_profile_select.py +80 -0
- janito/cli/chat_mode/shell/commands/__init__.py +19 -9
- janito/cli/chat_mode/shell/commands/_priv_check.py +5 -0
- janito/cli/chat_mode/shell/commands/bang.py +36 -0
- janito/cli/chat_mode/shell/commands/conversation_restart.py +31 -24
- janito/cli/chat_mode/shell/commands/execute.py +42 -0
- janito/cli/chat_mode/shell/commands/help.py +7 -4
- janito/cli/chat_mode/shell/commands/model.py +28 -0
- janito/cli/chat_mode/shell/commands/prompt.py +0 -8
- janito/cli/chat_mode/shell/commands/read.py +37 -0
- janito/cli/chat_mode/shell/commands/tools.py +45 -18
- janito/cli/chat_mode/shell/commands/write.py +37 -0
- janito/cli/chat_mode/shell/commands.bak.zip +0 -0
- janito/cli/chat_mode/shell/input_history.py +1 -1
- janito/cli/chat_mode/shell/session/manager.py +0 -68
- janito/cli/chat_mode/shell/session.bak.zip +0 -0
- janito/cli/chat_mode/toolbar.py +44 -27
- janito/cli/cli_commands/list_tools.py +44 -11
- janito/cli/cli_commands/model_utils.py +95 -95
- janito/cli/cli_commands/show_system_prompt.py +57 -14
- janito/cli/config.py +5 -6
- janito/cli/core/getters.py +33 -33
- janito/cli/core/runner.py +27 -20
- janito/cli/core/setters.py +10 -1
- janito/cli/main_cli.py +40 -10
- janito/cli/prompt_core.py +18 -2
- janito/cli/prompt_setup.py +56 -0
- janito/cli/rich_terminal_reporter.py +21 -6
- janito/cli/single_shot_mode/handler.py +24 -77
- janito/cli/verbose_output.py +1 -1
- janito/config_manager.py +125 -112
- janito/drivers/dashscope.bak.zip +0 -0
- janito/drivers/driver_registry.py +0 -2
- janito/drivers/openai/README.md +20 -0
- janito/drivers/openai_responses.bak.zip +0 -0
- janito/event_bus/event.py +2 -2
- janito/formatting_token.py +7 -6
- janito/i18n/pt.py +0 -1
- janito/llm/README.md +23 -0
- janito/llm/agent.py +80 -16
- janito/llm/auth.py +63 -63
- janito/llm/driver.py +8 -0
- janito/provider_registry.py +178 -176
- janito/providers/__init__.py +0 -2
- janito/providers/azure_openai/model_info.py +16 -16
- janito/providers/dashscope.bak.zip +0 -0
- janito/providers/provider_static_info.py +0 -3
- janito/providers/registry.py +26 -26
- janito/shell.bak.zip +0 -0
- janito/tools/DOCSTRING_STANDARD.txt +33 -0
- janito/tools/README.md +3 -0
- janito/tools/__init__.py +20 -6
- janito/tools/adapters/local/__init__.py +65 -62
- janito/tools/adapters/local/adapter.py +18 -35
- janito/tools/adapters/local/ask_user.py +3 -4
- janito/tools/adapters/local/copy_file.py +2 -2
- janito/tools/adapters/local/create_directory.py +2 -2
- janito/tools/adapters/local/create_file.py +2 -2
- janito/tools/adapters/local/delete_text_in_file.py +2 -2
- janito/tools/adapters/local/fetch_url.py +2 -2
- janito/tools/adapters/local/find_files.py +2 -1
- janito/tools/adapters/local/get_file_outline/core.py +2 -2
- janito/tools/adapters/local/get_file_outline/search_outline.py +2 -2
- janito/tools/adapters/local/move_file.py +2 -2
- janito/tools/adapters/local/open_html_in_browser.py +2 -1
- janito/tools/adapters/local/open_url.py +2 -2
- janito/tools/adapters/local/python_code_run.py +3 -3
- janito/tools/adapters/local/python_command_run.py +3 -3
- janito/tools/adapters/local/python_file_run.py +3 -3
- janito/tools/adapters/local/remove_directory.py +2 -2
- janito/tools/adapters/local/remove_file.py +2 -2
- janito/tools/adapters/local/replace_text_in_file.py +2 -2
- janito/tools/adapters/local/run_bash_command.py +3 -3
- janito/tools/adapters/local/run_powershell_command.py +3 -3
- janito/tools/adapters/local/search_text/core.py +2 -2
- janito/tools/adapters/local/validate_file_syntax/core.py +3 -3
- janito/tools/adapters/local/view_file.py +2 -1
- janito/tools/outline_file.bak.zip +0 -0
- janito/tools/permissions.py +45 -0
- janito/tools/permissions_parse.py +12 -0
- janito/tools/tool_base.py +14 -11
- janito/tools/tool_utils.py +4 -6
- janito/tools/tools_adapter.py +25 -20
- {janito-2.3.1.dist-info → janito-2.5.0.dist-info}/METADATA +46 -24
- {janito-2.3.1.dist-info → janito-2.5.0.dist-info}/RECORD +99 -82
- janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +0 -13
- janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +0 -37
- janito/cli/chat_mode/shell/commands/edit.py +0 -25
- janito/cli/chat_mode/shell/commands/exec.py +0 -27
- janito/cli/chat_mode/shell/commands/termweb_log.py +0 -92
- janito/cli/termweb_starter.py +0 -122
- janito/termweb/app.py +0 -95
- janito/version.py +0 -4
- {janito-2.3.1.dist-info → janito-2.5.0.dist-info}/WHEEL +0 -0
- {janito-2.3.1.dist-info → janito-2.5.0.dist-info}/entry_points.txt +0 -0
- {janito-2.3.1.dist-info → janito-2.5.0.dist-info}/licenses/LICENSE +0 -0
- {janito-2.3.1.dist-info → janito-2.5.0.dist-info}/top_level.txt +0 -0
janito/__init__.py
CHANGED
janito/_version.py
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
"""Version handling for Janito.
|
2
|
+
Attempts to obtain the package version in the following order:
|
3
|
+
1. If a janito.version module exists (generated when the package is built with
|
4
|
+
setuptools-scm), use the version attribute from that module.
|
5
|
+
2. Ask importlib.metadata for the installed distribution version – works for
|
6
|
+
both regular and editable installs handled by pip.
|
7
|
+
3. Fall back to calling setuptools_scm.get_version() directly, using the git
|
8
|
+
repository when running from source without an installed distribution.
|
9
|
+
4. If everything else fails, return the literal string ``"unknown"`` so that
|
10
|
+
the application continues to work even when the version cannot be
|
11
|
+
determined.
|
12
|
+
|
13
|
+
This layered approach guarantees that a meaningful version string is returned
|
14
|
+
in most development and production scenarios while keeping Janito free from
|
15
|
+
hard-coded version numbers.
|
16
|
+
"""
|
17
|
+
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
import pathlib
|
21
|
+
from importlib import metadata as importlib_metadata
|
22
|
+
|
23
|
+
__all__ = ["__version__"]
|
24
|
+
|
25
|
+
|
26
|
+
# 1. "janito.version" (generated at build time by setuptools-scm)
|
27
|
+
try:
|
28
|
+
from . import version as _generated_version # type: ignore
|
29
|
+
|
30
|
+
__version__: str = _generated_version.version # pytype: disable=module-attr
|
31
|
+
except ImportError: # pragma: no cover – not available in editable installs
|
32
|
+
|
33
|
+
def _resolve_version() -> str:
|
34
|
+
"""Resolve the version string using several fallbacks."""
|
35
|
+
|
36
|
+
# 2. importlib.metadata – works for both regular and `pip install -e`.
|
37
|
+
try:
|
38
|
+
return importlib_metadata.version("janito")
|
39
|
+
except importlib_metadata.PackageNotFoundError:
|
40
|
+
pass # Not installed – probably running from a source checkout.
|
41
|
+
|
42
|
+
# 3. setuptools_scm – query the VCS metadata directly.
|
43
|
+
try:
|
44
|
+
from setuptools_scm import get_version # Imported lazily.
|
45
|
+
|
46
|
+
package_root = pathlib.Path(__file__).resolve().parent.parent
|
47
|
+
return get_version(root=str(package_root), relative_to=__file__)
|
48
|
+
except Exception: # pragma: no cover – any failure here falls through
|
49
|
+
# Either setuptools_scm is not available or this is not a git repo.
|
50
|
+
pass
|
51
|
+
|
52
|
+
# 4. Ultimate fallback – return a placeholder.
|
53
|
+
return "unknown"
|
54
|
+
|
55
|
+
__version__ = _resolve_version()
|
56
|
+
|
57
|
+
|
janito/agent/setup_agent.py
CHANGED
@@ -20,62 +20,123 @@ def setup_agent(
|
|
20
20
|
output_queue=None,
|
21
21
|
verbose_tools=False,
|
22
22
|
verbose_agent=False,
|
23
|
-
|
23
|
+
|
24
|
+
allowed_permissions=None,
|
25
|
+
profile=None,
|
26
|
+
profile_system_prompt=None,
|
24
27
|
):
|
25
28
|
"""
|
26
|
-
Creates an agent
|
29
|
+
Creates an agent. A system prompt is rendered from a template only when a profile is specified.
|
27
30
|
"""
|
28
31
|
tools_provider = get_local_tools_adapter()
|
29
32
|
tools_provider.set_verbose_tools(verbose_tools)
|
30
33
|
|
31
|
-
|
34
|
+
# If zero_mode is enabled or no profile is given we skip the system prompt.
|
35
|
+
if zero_mode or (profile is None and profile_system_prompt is None):
|
32
36
|
# Pass provider to agent, let agent create driver
|
33
37
|
agent = LLMAgent(
|
34
38
|
provider_instance,
|
35
39
|
tools_provider,
|
36
|
-
agent_name=role or "
|
40
|
+
agent_name=role or "developer",
|
37
41
|
system_prompt=None,
|
42
|
+
input_queue=input_queue,
|
43
|
+
output_queue=output_queue,
|
44
|
+
verbose_agent=verbose_agent,
|
45
|
+
)
|
46
|
+
if role:
|
47
|
+
agent.template_vars["role"] = role
|
48
|
+
return agent
|
49
|
+
# If profile_system_prompt is set, use it directly
|
50
|
+
if profile_system_prompt is not None:
|
51
|
+
agent = LLMAgent(
|
52
|
+
provider_instance,
|
53
|
+
tools_provider,
|
54
|
+
agent_name=role or "developer",
|
55
|
+
system_prompt=profile_system_prompt,
|
56
|
+
input_queue=input_queue,
|
57
|
+
output_queue=output_queue,
|
38
58
|
verbose_agent=verbose_agent,
|
39
59
|
)
|
60
|
+
agent.template_vars["role"] = role or "developer"
|
61
|
+
agent.template_vars["profile"] = None
|
62
|
+
agent.template_vars["profile_system_prompt"] = profile_system_prompt
|
40
63
|
return agent
|
41
|
-
# Normal flow
|
64
|
+
# Normal flow (profile-specific system prompt)
|
42
65
|
if templates_dir is None:
|
43
66
|
# Set default template directory
|
44
67
|
templates_dir = Path(__file__).parent / "templates" / "profiles"
|
45
|
-
|
68
|
+
template_filename = f"system_prompt_template_{profile}.txt.j2"
|
69
|
+
template_path = templates_dir / template_filename
|
46
70
|
|
47
71
|
template_content = None
|
48
72
|
if template_path.exists():
|
49
73
|
with open(template_path, "r", encoding="utf-8") as file:
|
50
74
|
template_content = file.read()
|
51
75
|
else:
|
52
|
-
# Try package import fallback: janito.agent.templates.profiles.
|
76
|
+
# Try package import fallback: janito.agent.templates.profiles.system_prompt_template_<profile>.txt.j2
|
53
77
|
try:
|
54
78
|
with importlib.resources.files("janito.agent.templates.profiles").joinpath(
|
55
|
-
|
79
|
+
template_filename
|
56
80
|
).open("r", encoding="utf-8") as file:
|
57
81
|
template_content = file.read()
|
58
82
|
except (FileNotFoundError, ModuleNotFoundError, AttributeError):
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
83
|
+
if profile:
|
84
|
+
raise FileNotFoundError(
|
85
|
+
f"[janito] Could not find profile-specific template '{template_filename}' in {template_path} nor in janito.agent.templates.profiles package."
|
86
|
+
)
|
87
|
+
else:
|
88
|
+
warnings.warn(
|
89
|
+
f"[janito] Could not find {template_filename} in {template_path} nor in janito.agent.templates.profiles package."
|
90
|
+
)
|
91
|
+
raise FileNotFoundError(
|
92
|
+
f"Template file not found in either {template_path} or package resource."
|
93
|
+
)
|
65
94
|
|
66
95
|
import time
|
67
96
|
template = Template(template_content)
|
68
97
|
# Prepare context for Jinja2 rendering from llm_driver_config
|
69
98
|
# Compose context for Jinja2 rendering without using to_dict or temperature
|
70
99
|
context = {}
|
71
|
-
context["role"] = role or "
|
72
|
-
|
73
|
-
|
74
|
-
|
100
|
+
context["role"] = role or "developer"
|
101
|
+
context["profile"] = profile
|
102
|
+
# Normalize and inject allowed tool permissions
|
103
|
+
from janito.tools.tool_base import ToolPermissions
|
104
|
+
from janito.tools.permissions import get_global_allowed_permissions
|
105
|
+
|
106
|
+
if allowed_permissions is None:
|
107
|
+
# Fallback to globally configured permissions if not explicitly provided
|
108
|
+
allowed_permissions = get_global_allowed_permissions()
|
109
|
+
|
110
|
+
# Convert ToolPermissions -> string like "rwx" so the Jinja template can use
|
111
|
+
# membership checks such as `'r' in allowed_permissions`.
|
112
|
+
if isinstance(allowed_permissions, ToolPermissions):
|
113
|
+
perm_str = ""
|
114
|
+
if allowed_permissions.read:
|
115
|
+
perm_str += "r"
|
116
|
+
if allowed_permissions.write:
|
117
|
+
perm_str += "w"
|
118
|
+
if allowed_permissions.execute:
|
119
|
+
perm_str += "x"
|
120
|
+
allowed_permissions = perm_str or None # None if empty
|
121
|
+
|
122
|
+
context["allowed_permissions"] = allowed_permissions
|
123
|
+
|
124
|
+
# Inject platform information only when execute permission is granted
|
125
|
+
if allowed_permissions and 'x' in allowed_permissions:
|
75
126
|
pd = PlatformDiscovery()
|
76
127
|
context["platform"] = pd.get_platform_name()
|
77
128
|
context["python_version"] = pd.get_python_version()
|
78
129
|
context["shell_info"] = pd.detect_shell()
|
130
|
+
# DEBUG: Show permissions passed to template
|
131
|
+
from rich import print as rich_print
|
132
|
+
debug_flag = False
|
133
|
+
try:
|
134
|
+
debug_flag = (hasattr(sys, 'argv') and ('--debug' in sys.argv or '--verbose' in sys.argv or '-v' in sys.argv))
|
135
|
+
except Exception:
|
136
|
+
pass
|
137
|
+
if debug_flag:
|
138
|
+
rich_print(f"[bold magenta][DEBUG][/bold magenta] Rendering system prompt template '[cyan]{template_filename}[/cyan]' with allowed_permissions: [yellow]{allowed_permissions}[/yellow]")
|
139
|
+
rich_print(f"[bold magenta][DEBUG][/bold magenta] Template context: [green]{context}[/green]")
|
79
140
|
start_render = time.time()
|
80
141
|
rendered_prompt = template.render(**context)
|
81
142
|
end_render = time.time()
|
@@ -85,13 +146,18 @@ def setup_agent(
|
|
85
146
|
agent = LLMAgent(
|
86
147
|
provider_instance,
|
87
148
|
tools_provider,
|
88
|
-
agent_name=role or "
|
149
|
+
agent_name=role or "developer",
|
89
150
|
system_prompt=rendered_prompt,
|
90
151
|
input_queue=input_queue,
|
91
152
|
output_queue=output_queue,
|
92
153
|
verbose_agent=verbose_agent,
|
93
154
|
)
|
94
155
|
agent.template_vars["role"] = context["role"]
|
156
|
+
agent.template_vars["profile"] = profile
|
157
|
+
# Store template path and context for dynamic prompt refresh
|
158
|
+
agent.system_prompt_template = str(template_path)
|
159
|
+
agent._template_vars = context.copy()
|
160
|
+
agent._original_template_vars = context.copy()
|
95
161
|
return agent
|
96
162
|
|
97
163
|
|
@@ -104,7 +170,10 @@ def create_configured_agent(
|
|
104
170
|
verbose_agent=False,
|
105
171
|
templates_dir=None,
|
106
172
|
zero_mode=False,
|
107
|
-
|
173
|
+
|
174
|
+
allowed_permissions=None,
|
175
|
+
profile=None,
|
176
|
+
profile_system_prompt=None,
|
108
177
|
):
|
109
178
|
"""
|
110
179
|
Normalizes agent setup for all CLI modes.
|
@@ -131,6 +200,8 @@ def create_configured_agent(
|
|
131
200
|
input_queue = getattr(driver, "input_queue", None)
|
132
201
|
output_queue = getattr(driver, "output_queue", None)
|
133
202
|
|
203
|
+
# Automatically enable system prompt when a profile is specified
|
204
|
+
|
134
205
|
agent = setup_agent(
|
135
206
|
provider_instance=provider_instance,
|
136
207
|
llm_driver_config=llm_driver_config,
|
@@ -141,7 +212,10 @@ def create_configured_agent(
|
|
141
212
|
output_queue=output_queue,
|
142
213
|
verbose_tools=verbose_tools,
|
143
214
|
verbose_agent=verbose_agent,
|
144
|
-
|
215
|
+
|
216
|
+
allowed_permissions=allowed_permissions,
|
217
|
+
profile=profile,
|
218
|
+
profile_system_prompt=profile_system_prompt,
|
145
219
|
)
|
146
220
|
if driver is not None:
|
147
221
|
agent.driver = driver # Attach driver to agent for thread management
|
@@ -0,0 +1 @@
|
|
1
|
+
You are a helpful assistant.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
{# General role setup
|
2
|
+
ex. "Search in code" -> Python Developer -> find(*.py) | Java Developer -> find(*.java)
|
3
|
+
#}
|
4
|
+
You are: {{ role }}
|
5
|
+
|
6
|
+
{# Improves tool selection and platform specific constrains, eg, path format, C:\ vs /path #}
|
7
|
+
{% if allowed_permissions and 'x' in allowed_permissions %}
|
8
|
+
You will be using the following environment:
|
9
|
+
Platform: {{ platform }}
|
10
|
+
Python version: {{ python_version }}
|
11
|
+
Shell/Environment: {{ shell_info }}
|
12
|
+
{% endif %}
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
{% if allowed_permissions and 'r' in allowed_permissions %}
|
17
|
+
Before answering map the questions to artifacts found in the current directory - the current project.
|
18
|
+
{% endif %}
|
19
|
+
|
20
|
+
Respond according to the following guidelines:
|
21
|
+
{% if allowed_permissions %}
|
22
|
+
- Before using the namespace tools provide a short reason
|
23
|
+
{% endif %}
|
24
|
+
{% if allowed_permissions and 'r' in allowed_permissions %}
|
25
|
+
{# Exploratory hint #}
|
26
|
+
- Before answering to the user, explore the content related to the question
|
27
|
+
{# Reduces chunking roundtip #}
|
28
|
+
- When exploring full files content, provide empty range to read the entire files instead of chunked reads
|
29
|
+
{% endif %}
|
30
|
+
{% if allowed_permissions and 'w' in allowed_permissions %}
|
31
|
+
{# Reduce unrequest code verbosity overhead #}
|
32
|
+
- Use the namespace functions to deliver the code changes instead of showing the code.
|
33
|
+
{# Drive edit mode, place holders critical as shown to be crucial to avoid corruption with code placeholders #}
|
34
|
+
- Prefer making localized edits using string replacements. If the required change is extensive, replace the entire file instead, provide full content without placeholders.
|
35
|
+
{# Without this, the LLM choses to create files from a literal interpretation of the purpose and intention #}
|
36
|
+
- Before creating files search the code for the location related to the file purpose
|
37
|
+
{# This will trigger a search for the old names/locations to be updates #}
|
38
|
+
- After moving, removing or renaming functions or classes to different modules, update all imports, references, tests, and documentation to reflect the new locations, then verify functionality.
|
39
|
+
{# Keeping docstrings update is key to have semanatic match between prompts and code #}
|
40
|
+
- Once development or updates are finished, ensure that new or updated packages, modules, functions are properly documented.
|
41
|
+
{# Trying to prevent surrogates generation, found this frequently in gpt4.1/windows #}
|
42
|
+
- While writing code, if you need an emoji or special Unicode character in a string, then insert the actual character (e.g., 📖) directly instead of using surrogate pairs or escape sequences.
|
43
|
+
{% endif %}
|
44
|
+
|
janito/cli/chat_mode/bindings.py
CHANGED
@@ -3,7 +3,7 @@ Key bindings for Janito Chat CLI.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from prompt_toolkit.key_binding import KeyBindings
|
6
|
-
|
6
|
+
from janito.tools.permissions import get_global_allowed_permissions
|
7
7
|
|
8
8
|
class KeyBindingsFactory:
|
9
9
|
@staticmethod
|
@@ -31,7 +31,26 @@ class KeyBindingsFactory:
|
|
31
31
|
@bindings.add("f2")
|
32
32
|
def _(event):
|
33
33
|
buf = event.app.current_buffer
|
34
|
-
|
34
|
+
# Toggle read permission based on current state
|
35
|
+
current = get_global_allowed_permissions()
|
36
|
+
next_state = "off" if getattr(current, "read", False) else "on"
|
37
|
+
buf.text = f"/read {next_state}"
|
38
|
+
buf.validate_and_handle()
|
39
|
+
|
40
|
+
@bindings.add("f3")
|
41
|
+
def _(event):
|
42
|
+
buf = event.app.current_buffer
|
43
|
+
current = get_global_allowed_permissions()
|
44
|
+
next_state = "off" if getattr(current, "write", False) else "on"
|
45
|
+
buf.text = f"/write {next_state}"
|
46
|
+
buf.validate_and_handle()
|
47
|
+
|
48
|
+
@bindings.add("f4")
|
49
|
+
def _(event):
|
50
|
+
buf = event.app.current_buffer
|
51
|
+
current = get_global_allowed_permissions()
|
52
|
+
next_state = "off" if getattr(current, "execute", False) else "on"
|
53
|
+
buf.text = f"/execute {next_state}"
|
35
54
|
buf.validate_and_handle()
|
36
55
|
|
37
56
|
@bindings.add("f12")
|
@@ -10,11 +10,10 @@ from janito.cli.chat_mode.session import ChatSession
|
|
10
10
|
|
11
11
|
def main(args=None):
|
12
12
|
console = Console()
|
13
|
+
console.clear()
|
13
14
|
from janito.version import __version__
|
14
15
|
|
15
|
-
|
16
|
-
f"[bold green]Welcome to the Janito Chat Mode (v{__version__})! Type /exit or press Ctrl+C to quit.[/bold green]"
|
17
|
-
)
|
16
|
+
|
18
17
|
session = ChatSession(console, args=args)
|
19
18
|
session.run()
|
20
19
|
|
@@ -15,5 +15,10 @@ chat_shell_style = Style.from_dict(
|
|
15
15
|
"tokens_in": "fg:#00af5f",
|
16
16
|
"tokens_out": "fg:#01814a",
|
17
17
|
"max-tokens": "fg:#888888",
|
18
|
+
|
19
|
+
|
20
|
+
"key-toggle-on": "bg:#ffd700 fg:#232323 bold",
|
21
|
+
"key-toggle-off": "bg:#444444 fg:#ffffff bold",
|
22
|
+
"cmd-label": "bg:#ff9500 fg:#232323 bold",
|
18
23
|
}
|
19
24
|
)
|
@@ -0,0 +1,153 @@
|
|
1
|
+
"""
|
2
|
+
Scripted runner for Janito chat mode.
|
3
|
+
|
4
|
+
This utility allows you to execute the interactive ``ChatSession`` logic with
|
5
|
+
an *in-memory* list of user inputs, making it much easier to write automated
|
6
|
+
unit or integration tests for the chat CLI without resorting to fragile
|
7
|
+
pseudo-terminal tricks.
|
8
|
+
|
9
|
+
The runner monkey-patches the private ``_handle_input`` method so that the
|
10
|
+
chat loop thinks it is receiving interactive input, while in reality the
|
11
|
+
values come from the provided list. All output is captured through a
|
12
|
+
``rich.console.Console`` instance configured with ``record=True`` so the test
|
13
|
+
can later inspect the rendered text.
|
14
|
+
|
15
|
+
Typical usage
|
16
|
+
-------------
|
17
|
+
>>> from janito.cli.chat_mode.script_runner import ChatScriptRunner
|
18
|
+
>>> inputs = ["Hello!", "/exit"]
|
19
|
+
>>> runner = ChatScriptRunner(inputs)
|
20
|
+
>>> transcript = runner.run()
|
21
|
+
>>> assert "Hello!" in transcript
|
22
|
+
|
23
|
+
The ``ChatScriptRunner`` purposefully replaces the internal call to the agent
|
24
|
+
with a real agent call by default. If you want to use a stub, you must modify the runner implementation.
|
25
|
+
"""
|
26
|
+
from __future__ import annotations
|
27
|
+
|
28
|
+
from types import MethodType
|
29
|
+
from typing import List, Optional
|
30
|
+
|
31
|
+
from rich.console import Console
|
32
|
+
|
33
|
+
from janito.cli.chat_mode.session import ChatSession
|
34
|
+
from janito.provider_registry import ProviderRegistry
|
35
|
+
from janito.llm.driver_config import LLMDriverConfig
|
36
|
+
|
37
|
+
__all__ = ["ChatScriptRunner"]
|
38
|
+
|
39
|
+
|
40
|
+
auth_warning = (
|
41
|
+
"[yellow]ChatScriptRunner is executing in stubbed-agent mode; no calls to an "
|
42
|
+
"external LLM provider will be made.[/yellow]"
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
class ChatScriptRunner:
|
48
|
+
"""Run a **ChatSession** non-interactively using a predefined set of inputs."""
|
49
|
+
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
inputs: List[str],
|
53
|
+
*,
|
54
|
+
console: Optional[Console] = None,
|
55
|
+
provider: str = "openai",
|
56
|
+
model: str = "gpt-4.1",
|
57
|
+
use_real_agent: bool = True,
|
58
|
+
**chat_session_kwargs,
|
59
|
+
) -> None:
|
60
|
+
"""Create the runner.
|
61
|
+
|
62
|
+
Parameters
|
63
|
+
----------
|
64
|
+
inputs:
|
65
|
+
Ordered list of strings that will be fed to the chat loop.
|
66
|
+
console:
|
67
|
+
Optional *rich* console. If *None*, a new one is created with
|
68
|
+
*record=True* so that output can later be retrieved through
|
69
|
+
:py:meth:`rich.console.Console.export_text`.
|
70
|
+
use_real_agent:
|
71
|
+
chat_session_kwargs:
|
72
|
+
Extra keyword arguments forwarded to :class:`janito.cli.chat_mode.session.ChatSession`.
|
73
|
+
"""
|
74
|
+
self._input_queue = list(inputs)
|
75
|
+
self.console = console or Console(record=True)
|
76
|
+
self.provider = provider
|
77
|
+
self.model = model
|
78
|
+
self.use_real_agent = use_real_agent
|
79
|
+
# Ensure we always pass a non-interactive *args* namespace so that the
|
80
|
+
# normal ChatSession logic skips the Questionary profile prompt which
|
81
|
+
# is incompatible with headless test runs.
|
82
|
+
if "args" not in chat_session_kwargs or chat_session_kwargs["args"] is None:
|
83
|
+
from types import SimpleNamespace
|
84
|
+
chat_session_kwargs["args"] = SimpleNamespace(
|
85
|
+
profile="developer",
|
86
|
+
provider=self.provider,
|
87
|
+
model=self.model,
|
88
|
+
)
|
89
|
+
|
90
|
+
# Create the ChatSession instance **after** we monkey-patch methods that rely on
|
91
|
+
# prompt-toolkit so that no attempt is made to instantiate terminal UIs in
|
92
|
+
# a headless environment like CI.
|
93
|
+
|
94
|
+
# 1) Patch *ChatSession._create_prompt_session* to do nothing – the
|
95
|
+
# interactive session object is irrelevant for scripted runs.
|
96
|
+
from types import MethodType as _MT
|
97
|
+
if "_original_create_prompt_session" not in ChatSession.__dict__:
|
98
|
+
ChatSession._original_create_prompt_session = ChatSession._create_prompt_session # type: ignore[attr-defined]
|
99
|
+
ChatSession._create_prompt_session = _MT(lambda _self: None, ChatSession) # type: ignore[method-assign]
|
100
|
+
|
101
|
+
# Resolve provider instance now so that ChatSession uses a ready agent
|
102
|
+
provider_instance = ProviderRegistry().get_instance(self.provider)
|
103
|
+
if provider_instance is None:
|
104
|
+
raise RuntimeError(f"Provider '{self.provider}' is not available on this system.")
|
105
|
+
driver_config = LLMDriverConfig(model=self.model)
|
106
|
+
chat_session_kwargs.setdefault("provider_instance", provider_instance)
|
107
|
+
chat_session_kwargs.setdefault("llm_driver_config", driver_config)
|
108
|
+
|
109
|
+
self.chat_session = ChatSession(console=self.console, **chat_session_kwargs)
|
110
|
+
|
111
|
+
|
112
|
+
# Monkey-patch the *ChatSession._handle_input* method so that it pops
|
113
|
+
# from our in-memory queue instead of reading from stdin.
|
114
|
+
def _script_handle_input(this: ChatSession, _prompt_session_unused): # noqa: D401
|
115
|
+
if not self._input_queue:
|
116
|
+
# Signal normal shutdown
|
117
|
+
this._handle_exit()
|
118
|
+
return None
|
119
|
+
return self._input_queue.pop(0)
|
120
|
+
|
121
|
+
# Bind the method to the *chat_session* instance.
|
122
|
+
self.chat_session._handle_input = MethodType( # type: ignore[assignment]
|
123
|
+
_script_handle_input, self.chat_session
|
124
|
+
)
|
125
|
+
|
126
|
+
# ---------------------------------------------------------------------
|
127
|
+
# Public helpers
|
128
|
+
# ---------------------------------------------------------------------
|
129
|
+
def run(self) -> str:
|
130
|
+
"""Execute the chat session and return the captured transcript."""
|
131
|
+
self.chat_session.run()
|
132
|
+
return self.console.export_text()
|
133
|
+
|
134
|
+
# ---------------------------------------------------------------------
|
135
|
+
# Helpers to introspect results
|
136
|
+
# ---------------------------------------------------------------------
|
137
|
+
def get_history(self):
|
138
|
+
"""Return the structured conversation history produced by the LLM."""
|
139
|
+
try:
|
140
|
+
return self.chat_session.shell_state.conversation_history.get_history()
|
141
|
+
except Exception:
|
142
|
+
return []
|
143
|
+
|
144
|
+
def get_last_response(self) -> str | None:
|
145
|
+
"""Return the *assistant* content of the last message, if any."""
|
146
|
+
history = self.get_history()
|
147
|
+
for message in reversed(history):
|
148
|
+
if message.get("role") == "assistant":
|
149
|
+
return message.get("content")
|
150
|
+
return None
|
151
|
+
|
152
|
+
# Convenience alias so tests can simply call *runner()*
|
153
|
+
__call__ = run
|