zrb 1.21.31__py3-none-any.whl → 1.21.43__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/builtin/llm/chat_completion.py +94 -84
- zrb/builtin/llm/chat_session.py +90 -30
- zrb/builtin/llm/chat_session_cmd.py +115 -22
- zrb/builtin/llm/chat_trigger.py +92 -5
- zrb/builtin/llm/history.py +14 -7
- zrb/builtin/llm/llm_ask.py +16 -7
- zrb/builtin/llm/tool/cli.py +34 -15
- zrb/builtin/llm/tool/file.py +14 -2
- zrb/builtin/llm/tool/search/brave.py +8 -2
- zrb/builtin/llm/tool/search/searxng.py +8 -2
- zrb/builtin/llm/tool/search/serpapi.py +8 -2
- zrb/builtin/llm/tool/sub_agent.py +4 -1
- zrb/builtin/llm/tool/web.py +5 -0
- zrb/builtin/llm/xcom_names.py +3 -0
- zrb/callback/callback.py +8 -1
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +6 -2
- zrb/config/default_prompt/interactive_system_prompt.md +15 -12
- zrb/config/default_prompt/system_prompt.md +16 -18
- zrb/config/llm_rate_limitter.py +36 -13
- zrb/context/context.py +11 -0
- zrb/input/option_input.py +30 -2
- zrb/task/base/context.py +25 -13
- zrb/task/base/execution.py +52 -47
- zrb/task/base/lifecycle.py +1 -1
- zrb/task/base_task.py +31 -45
- zrb/task/base_trigger.py +0 -1
- zrb/task/cmd_task.py +3 -0
- zrb/task/llm/agent.py +39 -31
- zrb/task/llm/agent_runner.py +65 -3
- zrb/task/llm/default_workflow/researching/workflow.md +2 -0
- zrb/task/llm/history_list.py +13 -0
- zrb/task/llm/history_processor.py +4 -13
- zrb/task/llm/print_node.py +79 -25
- zrb/task/llm/prompt.py +70 -40
- zrb/task/llm/tool_wrapper.py +4 -1
- zrb/task/llm/workflow.py +54 -15
- zrb/task/llm_task.py +87 -33
- zrb/task/rsync_task.py +2 -0
- zrb/util/cmd/command.py +33 -10
- zrb/util/match.py +71 -0
- zrb/util/run.py +3 -3
- {zrb-1.21.31.dist-info → zrb-1.21.43.dist-info}/METADATA +1 -1
- {zrb-1.21.31.dist-info → zrb-1.21.43.dist-info}/RECORD +46 -43
- {zrb-1.21.31.dist-info → zrb-1.21.43.dist-info}/WHEEL +0 -0
- {zrb-1.21.31.dist-info → zrb-1.21.43.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import
|
|
2
|
+
from typing import Callable
|
|
3
3
|
|
|
4
|
+
from zrb.config.config import CFG
|
|
4
5
|
from zrb.context.any_context import AnyContext
|
|
5
6
|
from zrb.task.llm.workflow import get_available_workflows
|
|
6
7
|
from zrb.util.cli.markdown import render_markdown
|
|
@@ -10,7 +11,8 @@ from zrb.util.cli.style import (
|
|
|
10
11
|
stylize_error,
|
|
11
12
|
stylize_faint,
|
|
12
13
|
)
|
|
13
|
-
from zrb.util.
|
|
14
|
+
from zrb.util.cmd.command import run_command
|
|
15
|
+
from zrb.util.file import read_file, write_file
|
|
14
16
|
from zrb.util.markdown import make_markdown_section
|
|
15
17
|
from zrb.util.string.conversion import FALSE_STRS, TRUE_STRS, to_boolean
|
|
16
18
|
|
|
@@ -18,14 +20,20 @@ MULTILINE_START_CMD = ["/multi", "/multiline"]
|
|
|
18
20
|
MULTILINE_END_CMD = ["/end"]
|
|
19
21
|
QUIT_CMD = ["/bye", "/quit", "/q", "/exit"]
|
|
20
22
|
WORKFLOW_CMD = ["/workflow", "/workflows", "/skill", "/skills", "/w"]
|
|
23
|
+
RESPONSE_CMD = ["/response", "/res"]
|
|
21
24
|
SAVE_CMD = ["/save", "/s"]
|
|
25
|
+
LOAD_CMD = ["/load", "/l"]
|
|
22
26
|
ATTACHMENT_CMD = ["/attachment", "/attachments", "/attach"]
|
|
23
27
|
YOLO_CMD = ["/yolo"]
|
|
24
28
|
HELP_CMD = ["/help", "/info"]
|
|
29
|
+
RUN_CLI_CMD = ["/run", "/exec", "/execute", "/cmd", "/cli", "!"]
|
|
30
|
+
SESSION_CMD = ["/session", "/conversation", "/convo"]
|
|
31
|
+
|
|
25
32
|
ADD_SUB_CMD = ["add"]
|
|
26
33
|
SET_SUB_CMD = ["set"]
|
|
27
34
|
CLEAR_SUB_CMD = ["clear"]
|
|
28
|
-
|
|
35
|
+
SAVE_SUB_CMD = ["save"]
|
|
36
|
+
LOAD_SUB_CMD = ["load"]
|
|
29
37
|
|
|
30
38
|
# Command display constants
|
|
31
39
|
MULTILINE_START_CMD_DESC = "Start multiline input"
|
|
@@ -33,14 +41,20 @@ MULTILINE_END_CMD_DESC = "End multiline input"
|
|
|
33
41
|
QUIT_CMD_DESC = "Quit from chat session"
|
|
34
42
|
WORKFLOW_CMD_DESC = "Show active workflows"
|
|
35
43
|
WORKFLOW_ADD_SUB_CMD_DESC = (
|
|
36
|
-
"Add active workflow "
|
|
37
|
-
f"(e.g., `{WORKFLOW_CMD[0]} {ADD_SUB_CMD[0]} coding,researching`)"
|
|
44
|
+
"Add active workflow " f"(e.g., `{WORKFLOW_CMD[0]} {ADD_SUB_CMD[0]} coding`)"
|
|
38
45
|
)
|
|
39
46
|
WORKFLOW_SET_SUB_CMD_DESC = (
|
|
40
|
-
"Set active workflows "
|
|
47
|
+
"Set active workflows "
|
|
48
|
+
f"(e.g., `{WORKFLOW_CMD[0]} {SET_SUB_CMD[0]} coding,researching`)"
|
|
41
49
|
)
|
|
42
50
|
WORKFLOW_CLEAR_SUB_CMD_DESC = "Deactivate all workflows"
|
|
43
|
-
|
|
51
|
+
RESPONSE_CMD_DESC = "Manage response"
|
|
52
|
+
RESPONSE_SAVE_SUB_CMD_DESC = (
|
|
53
|
+
"Save last response to a file "
|
|
54
|
+
f"(e.g., `{RESPONSE_CMD[0]} {SAVE_SUB_CMD[0]} conclusion.md`)"
|
|
55
|
+
)
|
|
56
|
+
SAVE_CMD_DESC = "Save current session " f"(e.g., `{SAVE_CMD[0]} save-point`)"
|
|
57
|
+
LOAD_CMD_DESC = "Load session " f"(e.g., `{LOAD_CMD[0]} save-point`)"
|
|
44
58
|
ATTACHMENT_CMD_DESC = "Show current attachment"
|
|
45
59
|
ATTACHMENT_ADD_SUB_CMD_DESC = (
|
|
46
60
|
"Attach a file " f"(e.g., `{ATTACHMENT_CMD[0]} {ADD_SUB_CMD[0]} ./logo.png`)"
|
|
@@ -59,6 +73,7 @@ YOLO_SET_TRUE_CMD_DESC = "Activate YOLO mode for all tools"
|
|
|
59
73
|
YOLO_SET_FALSE_CMD_DESC = "Deactivate YOLO mode for all tools"
|
|
60
74
|
RUN_CLI_CMD_DESC = "Run a non-interactive CLI command"
|
|
61
75
|
HELP_CMD_DESC = "Show info/help"
|
|
76
|
+
SESSION_CMD_DESC = "Show current session"
|
|
62
77
|
|
|
63
78
|
|
|
64
79
|
def print_current_yolo_mode(
|
|
@@ -73,7 +88,9 @@ def print_current_yolo_mode(
|
|
|
73
88
|
|
|
74
89
|
def print_current_attachments(ctx: AnyContext, current_attachments_value: str) -> None:
|
|
75
90
|
attachments_str = (
|
|
76
|
-
|
|
91
|
+
", ".join(current_attachments_value.split(","))
|
|
92
|
+
if current_attachments_value != ""
|
|
93
|
+
else "*Not Set*"
|
|
77
94
|
)
|
|
78
95
|
ctx.print(render_markdown(f"📎 Current attachments: {attachments_str}"), plain=True)
|
|
79
96
|
ctx.print("", plain=True)
|
|
@@ -87,7 +104,7 @@ def print_current_workflows(ctx: AnyContext, current_workflows_value: str) -> No
|
|
|
87
104
|
else "*No Available Workflow*"
|
|
88
105
|
)
|
|
89
106
|
current_workflows_str = (
|
|
90
|
-
current_workflows_value
|
|
107
|
+
", ".join(current_workflows_value.split(","))
|
|
91
108
|
if current_workflows_value != ""
|
|
92
109
|
else "*No Active Workflow*"
|
|
93
110
|
)
|
|
@@ -105,8 +122,10 @@ def print_current_workflows(ctx: AnyContext, current_workflows_value: str) -> No
|
|
|
105
122
|
ctx.print("", plain=True)
|
|
106
123
|
|
|
107
124
|
|
|
108
|
-
def
|
|
109
|
-
|
|
125
|
+
def handle_response_cmd(ctx: AnyContext, user_input: str, final_result: str) -> None:
|
|
126
|
+
if not is_command_match(user_input, RESPONSE_CMD, SAVE_SUB_CMD):
|
|
127
|
+
return
|
|
128
|
+
save_path = get_command_param(user_input, RESPONSE_CMD, SAVE_SUB_CMD)
|
|
110
129
|
save_path = os.path.expanduser(save_path)
|
|
111
130
|
if os.path.exists(save_path):
|
|
112
131
|
ctx.print(
|
|
@@ -118,13 +137,12 @@ def save_final_result(ctx: AnyContext, user_input: str, final_result: str) -> No
|
|
|
118
137
|
ctx.print(f"Response saved to {save_path}", plain=True)
|
|
119
138
|
|
|
120
139
|
|
|
121
|
-
def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
140
|
+
async def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
122
141
|
command = get_command_param(user_input, RUN_CLI_CMD)
|
|
123
|
-
|
|
124
|
-
command,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
text=True,
|
|
142
|
+
cmd_result, return_code = await run_command(
|
|
143
|
+
[CFG.DEFAULT_SHELL, "-c", command],
|
|
144
|
+
print_method=_create_faint_print(ctx),
|
|
145
|
+
timeout=3600,
|
|
128
146
|
)
|
|
129
147
|
ctx.print(
|
|
130
148
|
render_markdown(
|
|
@@ -132,10 +150,14 @@ def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
|
132
150
|
f"`{command}`",
|
|
133
151
|
"\n".join(
|
|
134
152
|
[
|
|
135
|
-
make_markdown_section("📤 Stdout", result.stdout, as_code=True),
|
|
136
|
-
make_markdown_section("🚫 Stderr", result.stderr, as_code=True),
|
|
137
153
|
make_markdown_section(
|
|
138
|
-
"
|
|
154
|
+
"📤 Stdout", cmd_result.output, as_code=True
|
|
155
|
+
),
|
|
156
|
+
make_markdown_section(
|
|
157
|
+
"🚫 Stderr", cmd_result.error, as_code=True
|
|
158
|
+
),
|
|
159
|
+
make_markdown_section(
|
|
160
|
+
"🎯 Return code", f"Return Code: {return_code}"
|
|
139
161
|
),
|
|
140
162
|
]
|
|
141
163
|
),
|
|
@@ -146,8 +168,20 @@ def run_cli_command(ctx: AnyContext, user_input: str) -> None:
|
|
|
146
168
|
ctx.print("", plain=True)
|
|
147
169
|
|
|
148
170
|
|
|
171
|
+
def _create_faint_print(ctx: AnyContext) -> Callable[..., None]:
|
|
172
|
+
def print_faint(text: str):
|
|
173
|
+
ctx.print(stylize_faint(f" {text}"), plain=True)
|
|
174
|
+
|
|
175
|
+
return print_faint
|
|
176
|
+
|
|
177
|
+
|
|
149
178
|
def get_new_yolo_mode(old_yolo_mode: str | bool, user_input: str) -> str | bool:
|
|
150
|
-
|
|
179
|
+
if not is_command_match(user_input, YOLO_CMD):
|
|
180
|
+
return old_yolo_mode
|
|
181
|
+
if is_command_match(user_input, YOLO_CMD, SET_SUB_CMD):
|
|
182
|
+
new_yolo_mode = get_command_param(user_input, YOLO_CMD, SET_SUB_CMD)
|
|
183
|
+
else:
|
|
184
|
+
new_yolo_mode = get_command_param(user_input, YOLO_CMD)
|
|
151
185
|
if new_yolo_mode != "":
|
|
152
186
|
if new_yolo_mode in TRUE_STRS or new_yolo_mode in FALSE_STRS:
|
|
153
187
|
return to_boolean(new_yolo_mode)
|
|
@@ -189,6 +223,59 @@ def get_new_workflows(old_workflow: str, user_input: str) -> str:
|
|
|
189
223
|
return _normalize_comma_separated_str(old_workflow)
|
|
190
224
|
|
|
191
225
|
|
|
226
|
+
def handle_session(
|
|
227
|
+
ctx: AnyContext,
|
|
228
|
+
current_session_name: str | None,
|
|
229
|
+
current_start_new: bool,
|
|
230
|
+
user_input: str,
|
|
231
|
+
) -> tuple[str | None, bool]:
|
|
232
|
+
if is_command_match(user_input, SAVE_CMD):
|
|
233
|
+
save_point = get_command_param(user_input, SAVE_CMD)
|
|
234
|
+
if not save_point:
|
|
235
|
+
ctx.print(render_markdown("️⚠️ Save point name is required."), plain=True)
|
|
236
|
+
return current_session_name, current_start_new
|
|
237
|
+
if not current_session_name:
|
|
238
|
+
ctx.print(
|
|
239
|
+
render_markdown(
|
|
240
|
+
"⚠️ No active session to save. Please start a conversation first."
|
|
241
|
+
),
|
|
242
|
+
plain=True,
|
|
243
|
+
)
|
|
244
|
+
return current_session_name, current_start_new
|
|
245
|
+
save_point_path = os.path.join(CFG.LLM_HISTORY_DIR, "save-point", save_point)
|
|
246
|
+
write_file(save_point_path, current_session_name)
|
|
247
|
+
ctx.print(
|
|
248
|
+
render_markdown(
|
|
249
|
+
f"Session saved to save-point: {save_point} ({current_session_name})"
|
|
250
|
+
),
|
|
251
|
+
plain=True,
|
|
252
|
+
)
|
|
253
|
+
return current_session_name, current_start_new
|
|
254
|
+
if is_command_match(user_input, LOAD_CMD):
|
|
255
|
+
save_point = get_command_param(user_input, LOAD_CMD)
|
|
256
|
+
if not save_point:
|
|
257
|
+
ctx.print(render_markdown("⚠️ Save point name is required."), plain=True)
|
|
258
|
+
return current_session_name, current_start_new
|
|
259
|
+
save_point_path = os.path.join(CFG.LLM_HISTORY_DIR, "save-point", save_point)
|
|
260
|
+
if not os.path.exists(save_point_path):
|
|
261
|
+
ctx.print(
|
|
262
|
+
render_markdown(f"⚠️ Save point '{save_point}' not found."), plain=True
|
|
263
|
+
)
|
|
264
|
+
return current_session_name, current_start_new
|
|
265
|
+
current_session_name = read_file(save_point_path).strip()
|
|
266
|
+
ctx.print(
|
|
267
|
+
render_markdown(f"Loaded session: {current_session_name}"), plain=True
|
|
268
|
+
)
|
|
269
|
+
# When loading a session, we shouldn't start a new one
|
|
270
|
+
return current_session_name, False
|
|
271
|
+
if is_command_match(user_input, SESSION_CMD):
|
|
272
|
+
ctx.print(
|
|
273
|
+
render_markdown(f"Current session: {current_session_name}"), plain=True
|
|
274
|
+
)
|
|
275
|
+
return current_session_name, current_start_new
|
|
276
|
+
return current_session_name, current_start_new
|
|
277
|
+
|
|
278
|
+
|
|
192
279
|
def _normalize_comma_separated_str(comma_separated_str: str) -> str:
|
|
193
280
|
return ",".join(
|
|
194
281
|
[
|
|
@@ -252,7 +339,13 @@ def print_commands(ctx: AnyContext):
|
|
|
252
339
|
WORKFLOW_SET_SUB_CMD_DESC,
|
|
253
340
|
),
|
|
254
341
|
_show_subcommand(CLEAR_SUB_CMD[0], "", WORKFLOW_CLEAR_SUB_CMD_DESC),
|
|
255
|
-
_show_command(
|
|
342
|
+
_show_command(SESSION_CMD[0], SESSION_CMD_DESC),
|
|
343
|
+
_show_command(SAVE_CMD[0], SAVE_CMD_DESC),
|
|
344
|
+
_show_command(LOAD_CMD[0], LOAD_CMD_DESC),
|
|
345
|
+
_show_command(RESPONSE_CMD[0], RESPONSE_CMD_DESC),
|
|
346
|
+
_show_subcommand(
|
|
347
|
+
SAVE_SUB_CMD[0], "<file-path>", RESPONSE_SAVE_SUB_CMD_DESC
|
|
348
|
+
),
|
|
256
349
|
_show_command(YOLO_CMD[0], YOLO_CMD_DESC),
|
|
257
350
|
_show_subcommand(SET_SUB_CMD[0], "true", YOLO_SET_TRUE_CMD_DESC),
|
|
258
351
|
_show_subcommand(SET_SUB_CMD[0], "false", YOLO_SET_FALSE_CMD_DESC),
|
zrb/builtin/llm/chat_trigger.py
CHANGED
|
@@ -4,7 +4,9 @@ from asyncio import StreamReader
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Callable, Coroutine
|
|
5
5
|
|
|
6
6
|
from zrb.builtin.llm.chat_completion import get_chat_completer
|
|
7
|
+
from zrb.config.llm_config import llm_config
|
|
7
8
|
from zrb.context.any_context import AnyContext
|
|
9
|
+
from zrb.util.git import get_current_branch
|
|
8
10
|
from zrb.util.run import run_async
|
|
9
11
|
|
|
10
12
|
if TYPE_CHECKING:
|
|
@@ -27,10 +29,20 @@ class LLMChatTrigger:
|
|
|
27
29
|
self._triggers.append(single_trigger)
|
|
28
30
|
|
|
29
31
|
async def wait(
|
|
30
|
-
self,
|
|
32
|
+
self,
|
|
33
|
+
ctx: AnyContext,
|
|
34
|
+
reader: "PromptSession[Any] | StreamReader",
|
|
35
|
+
current_session_name: str | None,
|
|
36
|
+
is_first_time: bool,
|
|
31
37
|
) -> str:
|
|
32
38
|
trigger_tasks = [
|
|
33
|
-
asyncio.create_task(
|
|
39
|
+
asyncio.create_task(
|
|
40
|
+
run_async(
|
|
41
|
+
self._read_next_line(
|
|
42
|
+
ctx, reader, current_session_name, is_first_time
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
)
|
|
34
46
|
] + [asyncio.create_task(run_async(trigger(ctx))) for trigger in self._triggers]
|
|
35
47
|
final_result: str = ""
|
|
36
48
|
try:
|
|
@@ -47,22 +59,33 @@ class LLMChatTrigger:
|
|
|
47
59
|
except asyncio.CancelledError:
|
|
48
60
|
ctx.print("Task cancelled.", plain=True)
|
|
49
61
|
final_result = "/bye"
|
|
62
|
+
for task in trigger_tasks:
|
|
63
|
+
task.cancel()
|
|
50
64
|
except KeyboardInterrupt:
|
|
51
65
|
ctx.print("KeyboardInterrupt detected. Exiting...", plain=True)
|
|
52
66
|
final_result = "/bye"
|
|
67
|
+
for task in trigger_tasks:
|
|
68
|
+
task.cancel()
|
|
53
69
|
return final_result
|
|
54
70
|
|
|
55
71
|
async def _read_next_line(
|
|
56
|
-
self,
|
|
72
|
+
self,
|
|
73
|
+
ctx: AnyContext,
|
|
74
|
+
reader: "PromptSession[Any] | StreamReader",
|
|
75
|
+
current_session_name: str | None,
|
|
76
|
+
is_first_time: bool,
|
|
57
77
|
) -> str:
|
|
58
78
|
"""Reads one line of input using the provided reader."""
|
|
59
79
|
from prompt_toolkit import PromptSession
|
|
60
80
|
|
|
61
81
|
try:
|
|
62
82
|
if isinstance(reader, PromptSession):
|
|
63
|
-
bottom_toolbar =
|
|
83
|
+
bottom_toolbar = await self._get_bottom_toolbar(
|
|
84
|
+
ctx, current_session_name, is_first_time
|
|
85
|
+
)
|
|
64
86
|
return await reader.prompt_async(
|
|
65
|
-
completer=get_chat_completer(),
|
|
87
|
+
completer=get_chat_completer(),
|
|
88
|
+
bottom_toolbar=bottom_toolbar,
|
|
66
89
|
)
|
|
67
90
|
line_bytes = await reader.readline()
|
|
68
91
|
if not line_bytes:
|
|
@@ -74,5 +97,69 @@ class LLMChatTrigger:
|
|
|
74
97
|
ctx.print("KeyboardInterrupt detected. Exiting...", plain=True)
|
|
75
98
|
return "/bye"
|
|
76
99
|
|
|
100
|
+
async def _get_bottom_toolbar(
|
|
101
|
+
self, ctx: AnyContext, current_session_name: str | None, is_first_time: bool
|
|
102
|
+
) -> str:
|
|
103
|
+
import shutil
|
|
104
|
+
|
|
105
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
106
|
+
previous_session_name = self._get_previous_session_name(
|
|
107
|
+
ctx, current_session_name, is_first_time
|
|
108
|
+
)
|
|
109
|
+
current_branch = await self._get_current_branch()
|
|
110
|
+
current_model = self._get_current_model(ctx)
|
|
111
|
+
left_text = f"📌 {os.getcwd()} ({current_branch}) | 🧠 {current_model}"
|
|
112
|
+
right_text = f"📚 Previous Session: {previous_session_name}"
|
|
113
|
+
padding = (
|
|
114
|
+
terminal_width
|
|
115
|
+
- self._get_display_width(left_text)
|
|
116
|
+
- self._get_display_width(right_text)
|
|
117
|
+
- 1
|
|
118
|
+
)
|
|
119
|
+
if padding > 0:
|
|
120
|
+
return f"{left_text}{' ' * padding}{right_text}"
|
|
121
|
+
return f"{left_text} {right_text}"
|
|
122
|
+
|
|
123
|
+
def _get_display_width(self, text: str) -> int:
|
|
124
|
+
import unicodedata
|
|
125
|
+
|
|
126
|
+
width = 0
|
|
127
|
+
for char in text:
|
|
128
|
+
eaw = unicodedata.east_asian_width(char)
|
|
129
|
+
if eaw in ("F", "W"): # Fullwidth or Wide
|
|
130
|
+
width += 2
|
|
131
|
+
elif eaw == "A": # Ambiguous
|
|
132
|
+
width += 1 # Usually 1 in non-East Asian contexts
|
|
133
|
+
else: # Narrow, Halfwidth, Neutral
|
|
134
|
+
width += 1
|
|
135
|
+
return width
|
|
136
|
+
|
|
137
|
+
def _get_current_model(self, ctx: AnyContext) -> str:
|
|
138
|
+
if "model" in ctx.input and ctx.input.model:
|
|
139
|
+
return ctx.input.model
|
|
140
|
+
return str(llm_config.default_model_name)
|
|
141
|
+
|
|
142
|
+
def _get_previous_session_name(
|
|
143
|
+
self, ctx: AnyContext, current_session_name: str | None, is_first_time: bool
|
|
144
|
+
) -> str:
|
|
145
|
+
if is_first_time:
|
|
146
|
+
start_new: bool = ctx.input.start_new
|
|
147
|
+
if (
|
|
148
|
+
not start_new
|
|
149
|
+
and "previous_session" in ctx.input
|
|
150
|
+
and ctx.input.previous_session is not None
|
|
151
|
+
):
|
|
152
|
+
return ctx.input.previous_session
|
|
153
|
+
return "<No Session>"
|
|
154
|
+
if not current_session_name:
|
|
155
|
+
return "<No Session>"
|
|
156
|
+
return current_session_name
|
|
157
|
+
|
|
158
|
+
async def _get_current_branch(self) -> str:
|
|
159
|
+
try:
|
|
160
|
+
return await get_current_branch(os.getcwd(), print_method=lambda x: x)
|
|
161
|
+
except Exception:
|
|
162
|
+
return "<Not a git repo>"
|
|
163
|
+
|
|
77
164
|
|
|
78
165
|
llm_chat_trigger = LLMChatTrigger()
|
zrb/builtin/llm/history.py
CHANGED
|
@@ -17,13 +17,10 @@ def read_chat_conversation(ctx: AnyContext) -> dict[str, Any] | list | None:
|
|
|
17
17
|
return None # Indicate no history to load
|
|
18
18
|
previous_session_name = ctx.input.previous_session
|
|
19
19
|
if not previous_session_name: # Check for empty string or None
|
|
20
|
-
|
|
21
|
-
if
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return None
|
|
25
|
-
else:
|
|
26
|
-
return None # No previous session specified and no last session found
|
|
20
|
+
last_session_name = get_last_session_name()
|
|
21
|
+
if last_session_name is None:
|
|
22
|
+
return None
|
|
23
|
+
previous_session_name = last_session_name
|
|
27
24
|
conversation_file_path = os.path.join(
|
|
28
25
|
CFG.LLM_HISTORY_DIR, f"{previous_session_name}.json"
|
|
29
26
|
)
|
|
@@ -51,6 +48,16 @@ def read_chat_conversation(ctx: AnyContext) -> dict[str, Any] | list | None:
|
|
|
51
48
|
return None
|
|
52
49
|
|
|
53
50
|
|
|
51
|
+
def get_last_session_name() -> str | None:
|
|
52
|
+
last_session_file_path = os.path.join(CFG.LLM_HISTORY_DIR, "last-session")
|
|
53
|
+
if not os.path.isfile(last_session_file_path):
|
|
54
|
+
return None
|
|
55
|
+
last_session_name = read_file(last_session_file_path).strip()
|
|
56
|
+
if not last_session_name: # Handle empty last-session file
|
|
57
|
+
return None
|
|
58
|
+
return last_session_name
|
|
59
|
+
|
|
60
|
+
|
|
54
61
|
def write_chat_conversation(ctx: AnyContext, history_data: ConversationHistory):
|
|
55
62
|
"""Writes the conversation history data (including context) to a session file."""
|
|
56
63
|
os.makedirs(CFG.LLM_HISTORY_DIR, exist_ok=True)
|
zrb/builtin/llm/llm_ask.py
CHANGED
|
@@ -31,6 +31,11 @@ from zrb.builtin.llm.tool.web import (
|
|
|
31
31
|
create_search_internet_tool,
|
|
32
32
|
open_web_page,
|
|
33
33
|
)
|
|
34
|
+
from zrb.builtin.llm.xcom_names import (
|
|
35
|
+
LLM_ASK_ERROR_XCOM_NAME,
|
|
36
|
+
LLM_ASK_RESULT_XCOM_NAME,
|
|
37
|
+
LLM_ASK_SESSION_XCOM_NAME,
|
|
38
|
+
)
|
|
34
39
|
from zrb.callback.callback import Callback
|
|
35
40
|
from zrb.config.config import CFG
|
|
36
41
|
from zrb.config.llm_config import llm_config
|
|
@@ -40,6 +45,7 @@ from zrb.input.bool_input import BoolInput
|
|
|
40
45
|
from zrb.input.str_input import StrInput
|
|
41
46
|
from zrb.input.text_input import TextInput
|
|
42
47
|
from zrb.task.base_trigger import BaseTrigger
|
|
48
|
+
from zrb.task.llm.workflow import LLM_LOADED_WORKFLOW_XCOM_NAME
|
|
43
49
|
from zrb.task.llm_task import LLMTask
|
|
44
50
|
from zrb.util.string.conversion import to_boolean
|
|
45
51
|
|
|
@@ -99,6 +105,8 @@ def _get_default_yolo_mode(ctx: AnyContext) -> str:
|
|
|
99
105
|
|
|
100
106
|
|
|
101
107
|
def _render_yolo_mode_input(ctx: AnyContext) -> list[str] | bool:
|
|
108
|
+
if isinstance(ctx.input.yolo, bool):
|
|
109
|
+
return ctx.input.yolo
|
|
102
110
|
if ctx.input.yolo.strip() == "":
|
|
103
111
|
return []
|
|
104
112
|
elements = [element.strip() for element in ctx.input.yolo.split(",")]
|
|
@@ -172,9 +180,9 @@ def _get_inputs(require_message: bool = True) -> list[AnyInput | None]:
|
|
|
172
180
|
always_prompt=False,
|
|
173
181
|
),
|
|
174
182
|
TextInput(
|
|
175
|
-
"
|
|
176
|
-
description="Workflows",
|
|
177
|
-
prompt="Workflows",
|
|
183
|
+
"workflow",
|
|
184
|
+
description="Workflows (comma separated)",
|
|
185
|
+
prompt="Workflows (comma separated)",
|
|
178
186
|
default=lambda ctx: ",".join(llm_config.default_workflows),
|
|
179
187
|
allow_positional_parsing=False,
|
|
180
188
|
always_prompt=False,
|
|
@@ -237,7 +245,7 @@ llm_ask = LLMTask(
|
|
|
237
245
|
None if ctx.input.system_prompt.strip() == "" else ctx.input.system_prompt
|
|
238
246
|
),
|
|
239
247
|
workflows=lambda ctx: (
|
|
240
|
-
None if ctx.input.
|
|
248
|
+
None if ctx.input.workflow.strip() == "" else ctx.input.workflow.split(",")
|
|
241
249
|
),
|
|
242
250
|
attachment=_render_attach_input,
|
|
243
251
|
message="{ctx.input.message}",
|
|
@@ -258,9 +266,10 @@ llm_group.add_task(
|
|
|
258
266
|
callback=Callback(
|
|
259
267
|
task=llm_ask,
|
|
260
268
|
input_mapping=get_llm_ask_input_mapping,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
xcom_mapping={LLM_LOADED_WORKFLOW_XCOM_NAME: LLM_LOADED_WORKFLOW_XCOM_NAME},
|
|
270
|
+
result_queue=LLM_ASK_RESULT_XCOM_NAME,
|
|
271
|
+
error_queue=LLM_ASK_ERROR_XCOM_NAME,
|
|
272
|
+
session_name_queue=LLM_ASK_SESSION_XCOM_NAME,
|
|
264
273
|
),
|
|
265
274
|
retries=0,
|
|
266
275
|
cli_only=True,
|
zrb/builtin/llm/tool/cli.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import asyncio
|
|
2
2
|
import sys
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from zrb.config.config import CFG
|
|
6
|
+
from zrb.context.any_context import AnyContext
|
|
7
|
+
from zrb.util.cli.style import stylize_faint
|
|
8
|
+
from zrb.util.cmd.command import run_command
|
|
3
9
|
|
|
4
10
|
if sys.version_info >= (3, 12):
|
|
5
11
|
from typing import TypedDict
|
|
@@ -15,17 +21,25 @@ class ShellCommandResult(TypedDict):
|
|
|
15
21
|
return_code: The return code, 0 indicating no error
|
|
16
22
|
stdout: Standard output
|
|
17
23
|
stderr: Standard error
|
|
24
|
+
display: Combination of standard output and standard error, interlaced
|
|
18
25
|
"""
|
|
19
26
|
|
|
20
27
|
return_code: int
|
|
21
28
|
stdout: str
|
|
22
29
|
stderr: str
|
|
30
|
+
display: str
|
|
23
31
|
|
|
24
32
|
|
|
25
|
-
def run_shell_command(
|
|
33
|
+
async def run_shell_command(
|
|
34
|
+
ctx: AnyContext, command: str, timeout: int = 30
|
|
35
|
+
) -> ShellCommandResult:
|
|
26
36
|
"""
|
|
27
37
|
Executes a non-interactive shell command on the user's machine.
|
|
28
38
|
|
|
39
|
+
**EFFICIENCY TIP:**
|
|
40
|
+
Combine multiple shell commands into a single call using `&&` or `;` to save steps.
|
|
41
|
+
Example: `mkdir new_dir && cd new_dir && touch file.txt`
|
|
42
|
+
|
|
29
43
|
CRITICAL: This tool runs with user-level permissions. Explain commands that modify
|
|
30
44
|
the system (e.g., `git`, `pip`) and ask for confirmation.
|
|
31
45
|
IMPORTANT: Long-running processes should be run in the background (e.g., `command &`).
|
|
@@ -42,23 +56,28 @@ def run_shell_command(command: str, timeout: int = 30) -> ShellCommandResult:
|
|
|
42
56
|
dict: return_code, stdout, and stderr.
|
|
43
57
|
"""
|
|
44
58
|
try:
|
|
45
|
-
|
|
46
|
-
command,
|
|
47
|
-
|
|
48
|
-
capture_output=True,
|
|
49
|
-
text=True,
|
|
59
|
+
cmd_result, return_code = await run_command(
|
|
60
|
+
[CFG.DEFAULT_SHELL, "-c", command],
|
|
61
|
+
print_method=_create_faint_print(ctx),
|
|
50
62
|
timeout=timeout,
|
|
51
63
|
)
|
|
52
64
|
return {
|
|
53
|
-
"return_code":
|
|
54
|
-
"stdout":
|
|
55
|
-
"stderr":
|
|
65
|
+
"return_code": return_code,
|
|
66
|
+
"stdout": cmd_result.output,
|
|
67
|
+
"stderr": cmd_result.error,
|
|
68
|
+
"display": cmd_result.display,
|
|
56
69
|
}
|
|
57
|
-
except
|
|
58
|
-
stdout = e.stdout.decode() if isinstance(e.stdout, bytes) else (e.stdout or "")
|
|
59
|
-
stderr = e.stderr.decode() if isinstance(e.stderr, bytes) else (e.stderr or "")
|
|
70
|
+
except asyncio.TimeoutError:
|
|
60
71
|
return {
|
|
61
72
|
"return_code": 124,
|
|
62
|
-
"stdout":
|
|
63
|
-
"stderr": f"
|
|
73
|
+
"stdout": "",
|
|
74
|
+
"stderr": f"Command timeout after {timeout} seconds",
|
|
75
|
+
"display": f"Command timeout after {timeout} seconds",
|
|
64
76
|
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _create_faint_print(ctx: AnyContext) -> Callable[..., None]:
|
|
80
|
+
def print_faint(text: str):
|
|
81
|
+
ctx.print(stylize_faint(f" {text}"), plain=True)
|
|
82
|
+
|
|
83
|
+
return print_faint
|
zrb/builtin/llm/tool/file.py
CHANGED
|
@@ -89,6 +89,10 @@ def list_files(
|
|
|
89
89
|
"""
|
|
90
90
|
Lists files recursively up to a specified depth.
|
|
91
91
|
|
|
92
|
+
**EFFICIENCY TIP:**
|
|
93
|
+
Do NOT use this tool if you already know the file path (e.g., from the user's prompt).
|
|
94
|
+
Use `read_from_file` directly in that case. Only use this to explore directory structures.
|
|
95
|
+
|
|
92
96
|
Example:
|
|
93
97
|
list_files(path='src', include_hidden=False, depth=2)
|
|
94
98
|
|
|
@@ -179,6 +183,12 @@ def read_from_file(
|
|
|
179
183
|
"""
|
|
180
184
|
Reads content from one or more files, optionally specifying line ranges.
|
|
181
185
|
|
|
186
|
+
**EFFICIENCY TIP:**
|
|
187
|
+
For source code or configuration files, prefer reading the **entire file** at once
|
|
188
|
+
to ensure you have full context (imports, class definitions, etc.).
|
|
189
|
+
Only use `start_line` and `end_line` for extremely large files (like logs) or
|
|
190
|
+
when you are certain only a specific section is needed.
|
|
191
|
+
|
|
182
192
|
Examples:
|
|
183
193
|
```
|
|
184
194
|
# Read entire content of a single file
|
|
@@ -264,8 +274,9 @@ def write_to_file(
|
|
|
264
274
|
- CORRECT: "content": "He said \"Hello\""
|
|
265
275
|
- WRONG: "content": "He said \\"Hello\\"" <-- This breaks JSON parsing!
|
|
266
276
|
2. **SIZE LIMIT:** Content MUST NOT exceed 4000 characters.
|
|
267
|
-
-
|
|
268
|
-
-
|
|
277
|
+
- **STRICT PROHIBITION:** You are FORBIDDEN from writing more than 4000 characters in a single call.
|
|
278
|
+
- This is due to LLM output token limits, which will cause truncation and failure.
|
|
279
|
+
- To write larger files, you MUST split the content into multiple sequential calls (e.g., first 'w', then 'a').
|
|
269
280
|
|
|
270
281
|
Examples:
|
|
271
282
|
```
|
|
@@ -553,6 +564,7 @@ async def analyze_file(
|
|
|
553
564
|
tools=[read_from_file, search_files],
|
|
554
565
|
auto_summarize=False,
|
|
555
566
|
remember_history=False,
|
|
567
|
+
yolo_mode=True,
|
|
556
568
|
)
|
|
557
569
|
payload = json.dumps(
|
|
558
570
|
{
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
import requests
|
|
4
|
-
|
|
5
3
|
from zrb.config.config import CFG
|
|
6
4
|
|
|
7
5
|
|
|
@@ -17,6 +15,12 @@ def search_internet(
|
|
|
17
15
|
Use this tool to find up-to-date information, answer questions about current events,
|
|
18
16
|
or research topics using a search engine.
|
|
19
17
|
|
|
18
|
+
**EFFICIENCY TIP:**
|
|
19
|
+
Make your `query` specific and keyword-rich to get the best results in a single call.
|
|
20
|
+
Avoid vague queries that require follow-up searches.
|
|
21
|
+
Bad: "new python features"
|
|
22
|
+
Good: "python 3.12 new features list release date"
|
|
23
|
+
|
|
20
24
|
Args:
|
|
21
25
|
query (str): The natural language search query (e.g., 'Soto Madura').
|
|
22
26
|
Do NOT include instructions, meta-talk, or internal reasoning.
|
|
@@ -30,6 +34,8 @@ def search_internet(
|
|
|
30
34
|
Returns:
|
|
31
35
|
dict: Summary of search results (titles, links, snippets).
|
|
32
36
|
"""
|
|
37
|
+
import requests
|
|
38
|
+
|
|
33
39
|
if safe_search is None:
|
|
34
40
|
safe_search = CFG.BRAVE_API_SAFE
|
|
35
41
|
if language is None:
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
import requests
|
|
4
|
-
|
|
5
3
|
from zrb.config.config import CFG
|
|
6
4
|
|
|
7
5
|
|
|
@@ -17,6 +15,12 @@ def search_internet(
|
|
|
17
15
|
Use this tool to find up-to-date information, answer questions about current events,
|
|
18
16
|
or research topics using a search engine.
|
|
19
17
|
|
|
18
|
+
**EFFICIENCY TIP:**
|
|
19
|
+
Make your `query` specific and keyword-rich to get the best results in a single call.
|
|
20
|
+
Avoid vague queries that require follow-up searches.
|
|
21
|
+
Bad: "new python features"
|
|
22
|
+
Good: "python 3.12 new features list release date"
|
|
23
|
+
|
|
20
24
|
Args:
|
|
21
25
|
query (str): The natural language search query (e.g., 'Soto Madura').
|
|
22
26
|
Do NOT include instructions, meta-talk, or internal reasoning.
|
|
@@ -30,6 +34,8 @@ def search_internet(
|
|
|
30
34
|
Returns:
|
|
31
35
|
dict: Summary of search results (titles, links, snippets).
|
|
32
36
|
"""
|
|
37
|
+
import requests
|
|
38
|
+
|
|
33
39
|
if safe_search is None:
|
|
34
40
|
safe_search = CFG.SEARXNG_SAFE
|
|
35
41
|
if language is None:
|