zrb 1.15.3__py3-none-any.whl → 2.0.0a4__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.
Potentially problematic release.
This version of zrb might be problematic. Click here for more details.
- zrb/__init__.py +118 -133
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +55 -1
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/llm/chat.py +147 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
- zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
- zrb/builtin/searxng/config/settings.yml +5671 -0
- zrb/builtin/searxng/start.py +21 -0
- zrb/builtin/shell/autocomplete/bash.py +4 -3
- zrb/builtin/shell/autocomplete/zsh.py +4 -3
- zrb/callback/callback.py +8 -1
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +555 -169
- zrb/config/helper.py +84 -0
- zrb/config/web_auth_config.py +50 -35
- zrb/context/any_shared_context.py +20 -3
- zrb/context/context.py +39 -5
- zrb/context/print_fn.py +13 -0
- zrb/context/shared_context.py +17 -8
- zrb/group/any_group.py +3 -3
- zrb/group/group.py +3 -3
- zrb/input/any_input.py +5 -1
- zrb/input/base_input.py +18 -6
- zrb/input/option_input.py +41 -1
- zrb/input/text_input.py +7 -24
- zrb/llm/agent/__init__.py +9 -0
- zrb/llm/agent/agent.py +215 -0
- zrb/llm/agent/summarizer.py +20 -0
- zrb/llm/app/__init__.py +10 -0
- zrb/llm/app/completion.py +281 -0
- zrb/llm/app/confirmation/allow_tool.py +66 -0
- zrb/llm/app/confirmation/handler.py +178 -0
- zrb/llm/app/confirmation/replace_confirmation.py +77 -0
- zrb/llm/app/keybinding.py +34 -0
- zrb/llm/app/layout.py +117 -0
- zrb/llm/app/lexer.py +155 -0
- zrb/llm/app/redirection.py +28 -0
- zrb/llm/app/style.py +16 -0
- zrb/llm/app/ui.py +733 -0
- zrb/llm/config/__init__.py +4 -0
- zrb/llm/config/config.py +122 -0
- zrb/llm/config/limiter.py +247 -0
- zrb/llm/history_manager/__init__.py +4 -0
- zrb/llm/history_manager/any_history_manager.py +23 -0
- zrb/llm/history_manager/file_history_manager.py +91 -0
- zrb/llm/history_processor/summarizer.py +108 -0
- zrb/llm/note/__init__.py +3 -0
- zrb/llm/note/manager.py +122 -0
- zrb/llm/prompt/__init__.py +29 -0
- zrb/llm/prompt/claude_compatibility.py +92 -0
- zrb/llm/prompt/compose.py +55 -0
- zrb/llm/prompt/default.py +51 -0
- zrb/llm/prompt/markdown/file_extractor.md +112 -0
- zrb/llm/prompt/markdown/mandate.md +23 -0
- zrb/llm/prompt/markdown/persona.md +3 -0
- zrb/llm/prompt/markdown/repo_extractor.md +112 -0
- zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
- zrb/llm/prompt/markdown/summarizer.md +21 -0
- zrb/llm/prompt/note.py +41 -0
- zrb/llm/prompt/system_context.py +46 -0
- zrb/llm/prompt/zrb.py +41 -0
- zrb/llm/skill/__init__.py +3 -0
- zrb/llm/skill/manager.py +86 -0
- zrb/llm/task/__init__.py +4 -0
- zrb/llm/task/llm_chat_task.py +316 -0
- zrb/llm/task/llm_task.py +245 -0
- zrb/llm/tool/__init__.py +39 -0
- zrb/llm/tool/bash.py +75 -0
- zrb/llm/tool/code.py +266 -0
- zrb/llm/tool/file.py +419 -0
- zrb/llm/tool/note.py +70 -0
- zrb/{builtin/llm → llm}/tool/rag.py +33 -37
- zrb/llm/tool/search/brave.py +53 -0
- zrb/llm/tool/search/searxng.py +47 -0
- zrb/llm/tool/search/serpapi.py +47 -0
- zrb/llm/tool/skill.py +19 -0
- zrb/llm/tool/sub_agent.py +70 -0
- zrb/llm/tool/web.py +97 -0
- zrb/llm/tool/zrb_task.py +66 -0
- zrb/llm/util/attachment.py +101 -0
- zrb/llm/util/prompt.py +104 -0
- zrb/llm/util/stream_response.py +178 -0
- zrb/runner/cli.py +21 -20
- zrb/runner/common_util.py +24 -19
- zrb/runner/web_route/task_input_api_route.py +5 -5
- zrb/runner/web_util/user.py +7 -3
- zrb/session/any_session.py +12 -9
- zrb/session/session.py +38 -17
- zrb/task/any_task.py +24 -3
- zrb/task/base/context.py +42 -22
- zrb/task/base/execution.py +67 -55
- zrb/task/base/lifecycle.py +14 -7
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +113 -50
- zrb/task/base_trigger.py +16 -6
- zrb/task/cmd_task.py +6 -0
- zrb/task/http_check.py +11 -5
- zrb/task/make_task.py +5 -3
- zrb/task/rsync_task.py +30 -10
- zrb/task/scaffolder.py +7 -4
- zrb/task/scheduler.py +7 -4
- zrb/task/tcp_check.py +6 -4
- zrb/util/ascii_art/art/bee.txt +17 -0
- zrb/util/ascii_art/art/cat.txt +9 -0
- zrb/util/ascii_art/art/ghost.txt +16 -0
- zrb/util/ascii_art/art/panda.txt +17 -0
- zrb/util/ascii_art/art/rose.txt +14 -0
- zrb/util/ascii_art/art/unicorn.txt +15 -0
- zrb/util/ascii_art/banner.py +92 -0
- zrb/util/attr.py +54 -39
- zrb/util/cli/markdown.py +32 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/cmd/command.py +33 -10
- zrb/util/file.py +61 -33
- zrb/util/git.py +2 -2
- zrb/util/{llm/prompt.py → markdown.py} +2 -3
- zrb/util/match.py +78 -0
- zrb/util/run.py +3 -3
- zrb/util/string/conversion.py +1 -1
- zrb/util/truncate.py +23 -0
- zrb/util/yaml.py +204 -0
- zrb/xcom/xcom.py +10 -0
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
- zrb/attr/__init__.py +0 -0
- zrb/builtin/llm/chat_session.py +0 -311
- zrb/builtin/llm/history.py +0 -71
- zrb/builtin/llm/input.py +0 -27
- zrb/builtin/llm/llm_ask.py +0 -187
- zrb/builtin/llm/previous-session.js +0 -21
- zrb/builtin/llm/tool/__init__.py +0 -0
- zrb/builtin/llm/tool/api.py +0 -71
- zrb/builtin/llm/tool/cli.py +0 -38
- zrb/builtin/llm/tool/code.py +0 -254
- zrb/builtin/llm/tool/file.py +0 -626
- zrb/builtin/llm/tool/sub_agent.py +0 -137
- zrb/builtin/llm/tool/web.py +0 -195
- zrb/builtin/project/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
- zrb/builtin/project/create/__init__.py +0 -0
- zrb/builtin/shell/__init__.py +0 -0
- zrb/builtin/shell/autocomplete/__init__.py +0 -0
- zrb/callback/__init__.py +0 -0
- zrb/cmd/__init__.py +0 -0
- zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
- zrb/config/default_prompt/interactive_system_prompt.md +0 -35
- zrb/config/default_prompt/persona.md +0 -1
- zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
- zrb/config/default_prompt/summarization_prompt.md +0 -16
- zrb/config/default_prompt/system_prompt.md +0 -32
- zrb/config/llm_config.py +0 -243
- zrb/config/llm_context/config.py +0 -129
- zrb/config/llm_context/config_parser.py +0 -46
- zrb/config/llm_rate_limitter.py +0 -137
- zrb/content_transformer/__init__.py +0 -0
- zrb/context/__init__.py +0 -0
- zrb/dot_dict/__init__.py +0 -0
- zrb/env/__init__.py +0 -0
- zrb/group/__init__.py +0 -0
- zrb/input/__init__.py +0 -0
- zrb/runner/__init__.py +0 -0
- zrb/runner/web_route/__init__.py +0 -0
- zrb/runner/web_route/home_page/__init__.py +0 -0
- zrb/session/__init__.py +0 -0
- zrb/session_state_log/__init__.py +0 -0
- zrb/session_state_logger/__init__.py +0 -0
- zrb/task/__init__.py +0 -0
- zrb/task/base/__init__.py +0 -0
- zrb/task/llm/__init__.py +0 -0
- zrb/task/llm/agent.py +0 -243
- zrb/task/llm/config.py +0 -103
- zrb/task/llm/conversation_history.py +0 -128
- zrb/task/llm/conversation_history_model.py +0 -242
- zrb/task/llm/default_workflow/coding.md +0 -24
- zrb/task/llm/default_workflow/copywriting.md +0 -17
- zrb/task/llm/default_workflow/researching.md +0 -18
- zrb/task/llm/error.py +0 -95
- zrb/task/llm/history_summarization.py +0 -216
- zrb/task/llm/print_node.py +0 -101
- zrb/task/llm/prompt.py +0 -325
- zrb/task/llm/tool_wrapper.py +0 -220
- zrb/task/llm/typing.py +0 -3
- zrb/task/llm_task.py +0 -341
- zrb/task_status/__init__.py +0 -0
- zrb/util/__init__.py +0 -0
- zrb/util/cli/__init__.py +0 -0
- zrb/util/cmd/__init__.py +0 -0
- zrb/util/codemod/__init__.py +0 -0
- zrb/util/string/__init__.py +0 -0
- zrb/xcom/__init__.py +0 -0
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
from typing import Any, Awaitable, Callable, Protocol, TextIO
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from zrb.config.config import CFG
|
|
10
|
+
from zrb.util.yaml import yaml_dump
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UIProtocol(Protocol):
|
|
14
|
+
async def ask_user(self, prompt: str) -> str: ...
|
|
15
|
+
|
|
16
|
+
def append_to_output(
|
|
17
|
+
self,
|
|
18
|
+
*values: object,
|
|
19
|
+
sep: str = " ",
|
|
20
|
+
end: str = "\n",
|
|
21
|
+
file: TextIO | None = None,
|
|
22
|
+
flush: bool = False,
|
|
23
|
+
): ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ConfirmationMiddleware = Callable[
|
|
27
|
+
[UIProtocol, Any, str, Callable[[UIProtocol, Any, str], Awaitable[Any]]],
|
|
28
|
+
Awaitable[Any],
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConfirmationHandler:
|
|
33
|
+
def __init__(self, middlewares: list[ConfirmationMiddleware]):
|
|
34
|
+
self._middlewares = middlewares
|
|
35
|
+
|
|
36
|
+
def add_middleware(self, *middleware: ConfirmationMiddleware):
|
|
37
|
+
self.prepend_middleware(*middleware)
|
|
38
|
+
|
|
39
|
+
def prepend_middleware(self, *middleware: ConfirmationMiddleware):
|
|
40
|
+
self._middlewares = list(middleware) + self._middlewares
|
|
41
|
+
|
|
42
|
+
async def handle(self, ui: UIProtocol, call: Any) -> Any:
|
|
43
|
+
while True:
|
|
44
|
+
message = self._get_confirm_user_message(call)
|
|
45
|
+
ui.append_to_output(f"\n\n{message}", end="")
|
|
46
|
+
# Wait for user input
|
|
47
|
+
user_input = await ui.ask_user("")
|
|
48
|
+
user_response = user_input.strip()
|
|
49
|
+
|
|
50
|
+
# Build the chain
|
|
51
|
+
async def _next(
|
|
52
|
+
ui: UIProtocol, call: Any, response: str, index: int
|
|
53
|
+
) -> Any:
|
|
54
|
+
if index >= len(self._middlewares):
|
|
55
|
+
# Default if no middleware handles it
|
|
56
|
+
return None
|
|
57
|
+
middleware = self._middlewares[index]
|
|
58
|
+
return await middleware(
|
|
59
|
+
ui,
|
|
60
|
+
call,
|
|
61
|
+
response,
|
|
62
|
+
lambda u, c, r: _next(u, c, r, index + 1),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
result = await _next(ui, call, user_response, 0)
|
|
66
|
+
if result is None:
|
|
67
|
+
continue
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
def _get_confirm_user_message(self, call: Any) -> str:
|
|
71
|
+
args_section = ""
|
|
72
|
+
if f"{call.args}" != "{}":
|
|
73
|
+
args_str = self._format_args(call.args)
|
|
74
|
+
args_section = f" Arguments:\n{args_str}\n"
|
|
75
|
+
return (
|
|
76
|
+
f" 🎰 Executing tool '{call.tool_name}'\n"
|
|
77
|
+
f"{args_section}"
|
|
78
|
+
" ❓ Allow tool Execution? (✅ Y | 🛑 n | ✏️ e)? "
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _format_args(self, args: Any) -> str:
|
|
82
|
+
indent = " " * 7
|
|
83
|
+
try:
|
|
84
|
+
if isinstance(args, str):
|
|
85
|
+
try:
|
|
86
|
+
args = json.loads(args)
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
pass
|
|
89
|
+
args_str = yaml_dump(args)
|
|
90
|
+
# Indent nicely for display
|
|
91
|
+
return "\n".join([f"{indent}{line}" for line in args_str.splitlines()])
|
|
92
|
+
except Exception:
|
|
93
|
+
return f"{indent}{args}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def last_confirmation(
|
|
97
|
+
ui: UIProtocol,
|
|
98
|
+
call: Any,
|
|
99
|
+
user_response: str,
|
|
100
|
+
next_handler: Callable[[UIProtocol, Any, str], Awaitable[Any]],
|
|
101
|
+
) -> Any:
|
|
102
|
+
from pydantic_ai import ToolApproved, ToolDenied
|
|
103
|
+
|
|
104
|
+
if user_response.lower() in ("y", "yes", "ok", "okay", ""):
|
|
105
|
+
ui.append_to_output("\n✅ Execution approved.")
|
|
106
|
+
return ToolApproved()
|
|
107
|
+
elif user_response.lower() in ("n", "no"):
|
|
108
|
+
ui.append_to_output("\n🛑 Execution denied.")
|
|
109
|
+
return ToolDenied("User denied execution")
|
|
110
|
+
elif user_response.lower() in ("e", "edit"):
|
|
111
|
+
# Edit logic
|
|
112
|
+
try:
|
|
113
|
+
args = call.args
|
|
114
|
+
if isinstance(args, str):
|
|
115
|
+
try:
|
|
116
|
+
args = json.loads(args)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
# YAML for editing
|
|
121
|
+
is_yaml_edit = True
|
|
122
|
+
try:
|
|
123
|
+
content = yaml_dump(args)
|
|
124
|
+
extension = ".yaml"
|
|
125
|
+
except Exception:
|
|
126
|
+
# Fallback to JSON
|
|
127
|
+
content = json.dumps(args, indent=2)
|
|
128
|
+
extension = ".json"
|
|
129
|
+
is_yaml_edit = False
|
|
130
|
+
|
|
131
|
+
new_content = await wait_edit_content(
|
|
132
|
+
text_editor=CFG.DEFAULT_EDITOR,
|
|
133
|
+
content=content,
|
|
134
|
+
extension=extension,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Compare content
|
|
138
|
+
if new_content == content:
|
|
139
|
+
ui.append_to_output("\nℹ️ No changes made.")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
if is_yaml_edit:
|
|
144
|
+
new_args = yaml.safe_load(new_content)
|
|
145
|
+
else:
|
|
146
|
+
new_args = json.loads(new_content)
|
|
147
|
+
ui.append_to_output("\n✅ Execution approved (with modification).")
|
|
148
|
+
return ToolApproved(override_args=new_args)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
ui.append_to_output(f"\n❌ Invalid format: {e}. ", end="")
|
|
151
|
+
# Return None to signal loop retry
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
ui.append_to_output(f"\n❌ Error editing: {e}. ", end="")
|
|
156
|
+
return None
|
|
157
|
+
else:
|
|
158
|
+
ui.append_to_output("\n🛑 Execution denied.")
|
|
159
|
+
return ToolDenied(f"User denied execution with message: {user_response}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def wait_edit_content(
|
|
163
|
+
text_editor: str, content: str, extension: str = ".txt"
|
|
164
|
+
) -> str:
|
|
165
|
+
from prompt_toolkit.application import run_in_terminal
|
|
166
|
+
|
|
167
|
+
# Write temporary file
|
|
168
|
+
with tempfile.NamedTemporaryFile(suffix=extension, mode="w+", delete=False) as tf:
|
|
169
|
+
tf.write(content)
|
|
170
|
+
tf_path = tf.name
|
|
171
|
+
|
|
172
|
+
# Edit and wait
|
|
173
|
+
await run_in_terminal(lambda: subprocess.call([text_editor, tf_path]))
|
|
174
|
+
with open(tf_path, "r") as tf:
|
|
175
|
+
new_content = tf.read()
|
|
176
|
+
os.remove(tf_path)
|
|
177
|
+
|
|
178
|
+
return new_content
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
from typing import Any, Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.application import run_in_terminal
|
|
8
|
+
|
|
9
|
+
from zrb.config.config import CFG
|
|
10
|
+
from zrb.llm.app.confirmation.handler import UIProtocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def replace_confirmation(
|
|
14
|
+
ui: UIProtocol,
|
|
15
|
+
call: Any,
|
|
16
|
+
response: str,
|
|
17
|
+
next_handler: Callable[[UIProtocol, Any, str], Awaitable[Any]],
|
|
18
|
+
) -> Any:
|
|
19
|
+
from pydantic_ai import ToolApproved
|
|
20
|
+
|
|
21
|
+
if call.tool_name != "replace_in_file":
|
|
22
|
+
return await next_handler(ui, call, response)
|
|
23
|
+
|
|
24
|
+
if response.lower() not in ("e", "edit"):
|
|
25
|
+
return await next_handler(ui, call, response)
|
|
26
|
+
|
|
27
|
+
# It is replace_in_file and user wants to edit
|
|
28
|
+
args = call.args
|
|
29
|
+
if isinstance(args, str):
|
|
30
|
+
try:
|
|
31
|
+
args = json.loads(args)
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
old_text = args.get("old_text", "")
|
|
36
|
+
new_text = args.get("new_text", "")
|
|
37
|
+
|
|
38
|
+
# Create temporary files
|
|
39
|
+
with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".old") as tf_old:
|
|
40
|
+
tf_old.write(old_text)
|
|
41
|
+
old_path = tf_old.name
|
|
42
|
+
|
|
43
|
+
with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".new") as tf_new:
|
|
44
|
+
tf_new.write(new_text)
|
|
45
|
+
new_path = tf_new.name
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Prepare command
|
|
49
|
+
cmd_tpl = CFG.DEFAULT_DIFF_EDIT_COMMAND_TPL
|
|
50
|
+
cmd = cmd_tpl.format(old=old_path, new=new_path)
|
|
51
|
+
|
|
52
|
+
# Run command
|
|
53
|
+
await run_in_terminal(lambda: subprocess.call(cmd, shell=True))
|
|
54
|
+
|
|
55
|
+
# Read back new content
|
|
56
|
+
with open(new_path, "r") as f:
|
|
57
|
+
edited_new_text = f.read()
|
|
58
|
+
|
|
59
|
+
if edited_new_text != new_text:
|
|
60
|
+
# Update args
|
|
61
|
+
new_args = dict(args)
|
|
62
|
+
new_args["new_text"] = edited_new_text
|
|
63
|
+
ui.append_to_output("\n✅ Replacement modified.")
|
|
64
|
+
return ToolApproved(override_args=new_args)
|
|
65
|
+
else:
|
|
66
|
+
ui.append_to_output("\nℹ️ No changes made.")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
ui.append_to_output(f"\n❌ Error during diff edit: {e}")
|
|
71
|
+
return None
|
|
72
|
+
finally:
|
|
73
|
+
# Cleanup
|
|
74
|
+
if os.path.exists(old_path):
|
|
75
|
+
os.remove(old_path)
|
|
76
|
+
if os.path.exists(new_path):
|
|
77
|
+
os.remove(new_path)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import string
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.application import get_app
|
|
4
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
5
|
+
from prompt_toolkit.widgets import TextArea
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_output_keybindings(input_field: TextArea) -> KeyBindings:
|
|
9
|
+
kb = KeyBindings()
|
|
10
|
+
|
|
11
|
+
@kb.add("escape")
|
|
12
|
+
def _(event):
|
|
13
|
+
get_app().layout.focus(input_field)
|
|
14
|
+
|
|
15
|
+
@kb.add("c-c")
|
|
16
|
+
def _(event):
|
|
17
|
+
# Copy selection to clipboard
|
|
18
|
+
if event.current_buffer.selection_state:
|
|
19
|
+
data = event.current_buffer.copy_selection()
|
|
20
|
+
event.app.clipboard.set_data(data)
|
|
21
|
+
get_app().layout.focus(input_field)
|
|
22
|
+
|
|
23
|
+
def redirect_focus(event):
|
|
24
|
+
get_app().layout.focus(input_field)
|
|
25
|
+
input_field.buffer.insert_text(event.data)
|
|
26
|
+
|
|
27
|
+
for char in string.printable:
|
|
28
|
+
# Skip control characters (Tab, Newline, etc.)
|
|
29
|
+
# to preserve navigation/standard behavior
|
|
30
|
+
if char in "\t\n\r\x0b\x0c":
|
|
31
|
+
continue
|
|
32
|
+
kb.add(char)(redirect_focus)
|
|
33
|
+
|
|
34
|
+
return kb
|
zrb/llm/app/layout.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.formatted_text import HTML, AnyFormattedText
|
|
4
|
+
from prompt_toolkit.layout import HSplit, Layout, Window, WindowAlign
|
|
5
|
+
from prompt_toolkit.layout.containers import Float, FloatContainer
|
|
6
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
7
|
+
from prompt_toolkit.layout.menus import CompletionsMenu
|
|
8
|
+
from prompt_toolkit.lexers import Lexer
|
|
9
|
+
from prompt_toolkit.widgets import Frame, TextArea
|
|
10
|
+
|
|
11
|
+
from zrb.llm.app.completion import InputCompleter
|
|
12
|
+
from zrb.llm.history_manager.any_history_manager import AnyHistoryManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_input_field(
|
|
16
|
+
history_manager: AnyHistoryManager,
|
|
17
|
+
attach_commands: list[str],
|
|
18
|
+
exit_commands: list[str],
|
|
19
|
+
info_commands: list[str],
|
|
20
|
+
save_commands: list[str],
|
|
21
|
+
load_commands: list[str],
|
|
22
|
+
redirect_output_commands: list[str],
|
|
23
|
+
summarize_commands: list[str],
|
|
24
|
+
exec_commands: list[str],
|
|
25
|
+
) -> TextArea:
|
|
26
|
+
return TextArea(
|
|
27
|
+
height=4,
|
|
28
|
+
prompt=HTML('<style color="ansibrightblue"><b>>>> </b></style>'),
|
|
29
|
+
multiline=True,
|
|
30
|
+
wrap_lines=True,
|
|
31
|
+
completer=InputCompleter(
|
|
32
|
+
history_manager=history_manager,
|
|
33
|
+
attach_commands=attach_commands,
|
|
34
|
+
exit_commands=exit_commands,
|
|
35
|
+
info_commands=info_commands,
|
|
36
|
+
save_commands=save_commands,
|
|
37
|
+
load_commands=load_commands,
|
|
38
|
+
redirect_output_commands=redirect_output_commands,
|
|
39
|
+
summarize_commands=summarize_commands,
|
|
40
|
+
exec_commands=exec_commands,
|
|
41
|
+
),
|
|
42
|
+
complete_while_typing=True,
|
|
43
|
+
focus_on_click=True,
|
|
44
|
+
style="class:input_field",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_output_field(greeting: str, lexer: Lexer) -> TextArea:
|
|
49
|
+
return TextArea(
|
|
50
|
+
text=greeting.rstrip() + "\n\n",
|
|
51
|
+
read_only=True,
|
|
52
|
+
scrollbar=False,
|
|
53
|
+
wrap_lines=True,
|
|
54
|
+
lexer=lexer,
|
|
55
|
+
focus_on_click=True,
|
|
56
|
+
focusable=True,
|
|
57
|
+
style="class:output_field",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_layout(
|
|
62
|
+
title: str,
|
|
63
|
+
jargon: str,
|
|
64
|
+
input_field: TextArea,
|
|
65
|
+
output_field: TextArea,
|
|
66
|
+
info_bar_text: Callable[[], AnyFormattedText],
|
|
67
|
+
status_bar_text: Callable[[], AnyFormattedText],
|
|
68
|
+
) -> Layout:
|
|
69
|
+
title_bar_text = HTML(
|
|
70
|
+
f" <style bg='ansipurple' color='white'><b> {title} </b></style> "
|
|
71
|
+
f"<style color='#888888'>| {jargon}</style>"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return Layout(
|
|
75
|
+
FloatContainer(
|
|
76
|
+
content=HSplit(
|
|
77
|
+
[
|
|
78
|
+
# Title Bar
|
|
79
|
+
Window(
|
|
80
|
+
height=2,
|
|
81
|
+
content=FormattedTextControl(title_bar_text),
|
|
82
|
+
style="class:title-bar",
|
|
83
|
+
align=WindowAlign.CENTER,
|
|
84
|
+
),
|
|
85
|
+
# Info Bar
|
|
86
|
+
Window(
|
|
87
|
+
height=2,
|
|
88
|
+
content=FormattedTextControl(info_bar_text),
|
|
89
|
+
style="class:info-bar",
|
|
90
|
+
align=WindowAlign.CENTER,
|
|
91
|
+
),
|
|
92
|
+
# Chat History
|
|
93
|
+
Frame(output_field, title="Conversation", style="class:frame"),
|
|
94
|
+
# Input Area
|
|
95
|
+
Frame(
|
|
96
|
+
input_field,
|
|
97
|
+
title="(ENTER to send, CTRL+ENTER for newline, ESC to cancel)",
|
|
98
|
+
style="class:input-frame",
|
|
99
|
+
),
|
|
100
|
+
# Status Bar
|
|
101
|
+
Window(
|
|
102
|
+
height=1,
|
|
103
|
+
content=FormattedTextControl(status_bar_text),
|
|
104
|
+
style="class:bottom-toolbar",
|
|
105
|
+
),
|
|
106
|
+
]
|
|
107
|
+
),
|
|
108
|
+
floats=[
|
|
109
|
+
Float(
|
|
110
|
+
xcursor=True,
|
|
111
|
+
ycursor=True,
|
|
112
|
+
content=CompletionsMenu(max_height=16, scroll_offset=1),
|
|
113
|
+
),
|
|
114
|
+
],
|
|
115
|
+
),
|
|
116
|
+
focused_element=input_field,
|
|
117
|
+
)
|
zrb/llm/app/lexer.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.lexers import Lexer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CLIStyleLexer(Lexer):
|
|
7
|
+
def lex_document(self, document):
|
|
8
|
+
lines = document.lines
|
|
9
|
+
line_tokens = {} # Cache for tokens per line
|
|
10
|
+
|
|
11
|
+
# Global state for the document
|
|
12
|
+
current_attrs = set()
|
|
13
|
+
current_fg = ""
|
|
14
|
+
current_bg = ""
|
|
15
|
+
|
|
16
|
+
# Pre-process all lines to handle state across newlines
|
|
17
|
+
# Regex to find ANSI escape sequences (CSI)
|
|
18
|
+
ansi_escape = re.compile(r"\x1B\[([0-9;]*)m")
|
|
19
|
+
|
|
20
|
+
for lineno, line in enumerate(lines):
|
|
21
|
+
tokens = []
|
|
22
|
+
last_end = 0
|
|
23
|
+
|
|
24
|
+
def build_style():
|
|
25
|
+
parts = list(current_attrs)
|
|
26
|
+
if current_fg:
|
|
27
|
+
parts.append(current_fg)
|
|
28
|
+
if current_bg:
|
|
29
|
+
parts.append(current_bg)
|
|
30
|
+
return " ".join(parts)
|
|
31
|
+
|
|
32
|
+
for match in ansi_escape.finditer(line):
|
|
33
|
+
start, end = match.span()
|
|
34
|
+
|
|
35
|
+
# Add text before the escape sequence with current style
|
|
36
|
+
if start > last_end:
|
|
37
|
+
tokens.append((build_style(), line[last_end:start]))
|
|
38
|
+
|
|
39
|
+
# Parse codes
|
|
40
|
+
codes = match.group(1).split(";")
|
|
41
|
+
if not codes or codes == [""]:
|
|
42
|
+
codes = ["0"]
|
|
43
|
+
|
|
44
|
+
# Convert to integers
|
|
45
|
+
int_codes = []
|
|
46
|
+
for c in codes:
|
|
47
|
+
if c.isdigit():
|
|
48
|
+
int_codes.append(int(c))
|
|
49
|
+
|
|
50
|
+
i = 0
|
|
51
|
+
while i < len(int_codes):
|
|
52
|
+
c = int_codes[i]
|
|
53
|
+
i += 1
|
|
54
|
+
|
|
55
|
+
if c == 0:
|
|
56
|
+
current_attrs.clear()
|
|
57
|
+
current_fg = ""
|
|
58
|
+
current_bg = ""
|
|
59
|
+
elif c == 1:
|
|
60
|
+
current_attrs.add("bold")
|
|
61
|
+
elif c == 2:
|
|
62
|
+
current_attrs.add("class:faint")
|
|
63
|
+
elif c == 3:
|
|
64
|
+
current_attrs.add("italic")
|
|
65
|
+
elif c == 4:
|
|
66
|
+
current_attrs.add("underline")
|
|
67
|
+
elif c == 22:
|
|
68
|
+
current_attrs.discard("bold")
|
|
69
|
+
current_attrs.discard("class:faint")
|
|
70
|
+
elif c == 23:
|
|
71
|
+
current_attrs.discard("italic")
|
|
72
|
+
elif c == 24:
|
|
73
|
+
current_attrs.discard("underline")
|
|
74
|
+
elif 30 <= c <= 37:
|
|
75
|
+
colors = [
|
|
76
|
+
"#000000",
|
|
77
|
+
"#ff0000",
|
|
78
|
+
"#00ff00",
|
|
79
|
+
"#ffff00",
|
|
80
|
+
"#0000ff",
|
|
81
|
+
"#ff00ff",
|
|
82
|
+
"#00ffff",
|
|
83
|
+
"#ffffff",
|
|
84
|
+
]
|
|
85
|
+
current_fg = colors[c - 30]
|
|
86
|
+
elif c == 38:
|
|
87
|
+
if i < len(int_codes):
|
|
88
|
+
mode = int_codes[i]
|
|
89
|
+
i += 1
|
|
90
|
+
if mode == 5 and i < len(int_codes):
|
|
91
|
+
i += 1 # Skip 256 color
|
|
92
|
+
elif mode == 2 and i + 2 < len(int_codes):
|
|
93
|
+
r, g, b = (
|
|
94
|
+
int_codes[i],
|
|
95
|
+
int_codes[i + 1],
|
|
96
|
+
int_codes[i + 2],
|
|
97
|
+
)
|
|
98
|
+
i += 3
|
|
99
|
+
current_fg = f"#{r:02x}{g:02x}{b:02x}"
|
|
100
|
+
elif c == 39:
|
|
101
|
+
current_fg = ""
|
|
102
|
+
elif 40 <= c <= 47:
|
|
103
|
+
colors = [
|
|
104
|
+
"#000000",
|
|
105
|
+
"#ff0000",
|
|
106
|
+
"#00ff00",
|
|
107
|
+
"#ffff00",
|
|
108
|
+
"#0000ff",
|
|
109
|
+
"#ff00ff",
|
|
110
|
+
"#00ffff",
|
|
111
|
+
"#ffffff",
|
|
112
|
+
]
|
|
113
|
+
current_bg = f"bg:{colors[c - 40]}"
|
|
114
|
+
elif c == 48:
|
|
115
|
+
if i < len(int_codes):
|
|
116
|
+
mode = int_codes[i]
|
|
117
|
+
i += 1
|
|
118
|
+
if mode == 5 and i < len(int_codes):
|
|
119
|
+
i += 1
|
|
120
|
+
elif mode == 2 and i + 2 < len(int_codes):
|
|
121
|
+
r, g, b = (
|
|
122
|
+
int_codes[i],
|
|
123
|
+
int_codes[i + 1],
|
|
124
|
+
int_codes[i + 2],
|
|
125
|
+
)
|
|
126
|
+
i += 3
|
|
127
|
+
current_bg = f"bg:#{r:02x}{g:02x}{b:02x}"
|
|
128
|
+
elif c == 49:
|
|
129
|
+
current_bg = ""
|
|
130
|
+
elif 90 <= c <= 97:
|
|
131
|
+
colors = [
|
|
132
|
+
"#555555",
|
|
133
|
+
"#ff5555",
|
|
134
|
+
"#55ff55",
|
|
135
|
+
"#ffff55",
|
|
136
|
+
"#5555ff",
|
|
137
|
+
"#ff55ff",
|
|
138
|
+
"#55ffff",
|
|
139
|
+
"#ffffff",
|
|
140
|
+
]
|
|
141
|
+
current_fg = colors[c - 90]
|
|
142
|
+
|
|
143
|
+
last_end = end
|
|
144
|
+
|
|
145
|
+
# Add remaining text
|
|
146
|
+
if last_end < len(line):
|
|
147
|
+
tokens.append((build_style(), line[last_end:]))
|
|
148
|
+
|
|
149
|
+
# Store tokens for this line
|
|
150
|
+
line_tokens[lineno] = tokens
|
|
151
|
+
|
|
152
|
+
def get_line(lineno):
|
|
153
|
+
return line_tokens.get(lineno, [])
|
|
154
|
+
|
|
155
|
+
return get_line
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class StreamToUI(io.TextIOBase):
|
|
6
|
+
"""Redirect stdout to UI's append_to_output."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, ui_callback):
|
|
9
|
+
self.ui_callback = ui_callback
|
|
10
|
+
self.original_stdout = sys.stdout
|
|
11
|
+
self.original_stderr = sys.stderr
|
|
12
|
+
self._is_first_write = True
|
|
13
|
+
|
|
14
|
+
def write(self, text: str) -> int:
|
|
15
|
+
from prompt_toolkit.application import get_app
|
|
16
|
+
|
|
17
|
+
text = text.expandtabs(4)
|
|
18
|
+
if text:
|
|
19
|
+
if self._is_first_write:
|
|
20
|
+
self.ui_callback("\n", end="")
|
|
21
|
+
self._is_first_write = False
|
|
22
|
+
self.ui_callback(text, end="")
|
|
23
|
+
get_app().invalidate()
|
|
24
|
+
return len(text)
|
|
25
|
+
|
|
26
|
+
def flush(self):
|
|
27
|
+
self.original_stdout.flush()
|
|
28
|
+
self.original_stderr.flush()
|
zrb/llm/app/style.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from prompt_toolkit.styles import Style
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def create_style() -> Style:
|
|
5
|
+
return Style.from_dict(
|
|
6
|
+
{
|
|
7
|
+
"frame.label": "bg:#000000 #ffff00",
|
|
8
|
+
"thinking": "ansigreen italic",
|
|
9
|
+
"faint": "#888888",
|
|
10
|
+
"output_field": "bg:#000000 #eeeeee",
|
|
11
|
+
"input_field": "bg:#000000 #eeeeee",
|
|
12
|
+
"text": "#eeeeee",
|
|
13
|
+
"status": "reverse",
|
|
14
|
+
"bottom-toolbar": "bg:#333333 #aaaaaa",
|
|
15
|
+
}
|
|
16
|
+
)
|