zrb 1.8.10__py3-none-any.whl ā 1.21.29__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 +126 -113
- zrb/__main__.py +1 -1
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +2 -50
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/http.py +7 -8
- zrb/builtin/llm/attachment.py +40 -0
- zrb/builtin/llm/chat_completion.py +274 -0
- zrb/builtin/llm/chat_session.py +152 -85
- zrb/builtin/llm/chat_session_cmd.py +288 -0
- zrb/builtin/llm/chat_trigger.py +79 -0
- zrb/builtin/llm/history.py +7 -9
- zrb/builtin/llm/llm_ask.py +221 -98
- zrb/builtin/llm/tool/api.py +74 -52
- zrb/builtin/llm/tool/cli.py +46 -17
- zrb/builtin/llm/tool/code.py +71 -90
- zrb/builtin/llm/tool/file.py +301 -241
- zrb/builtin/llm/tool/note.py +84 -0
- zrb/builtin/llm/tool/rag.py +38 -8
- zrb/builtin/llm/tool/sub_agent.py +67 -50
- zrb/builtin/llm/tool/web.py +146 -122
- 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/setup/latex/ubuntu.py +1 -0
- zrb/builtin/setup/ubuntu.py +1 -1
- zrb/builtin/shell/autocomplete/bash.py +4 -3
- zrb/builtin/shell/autocomplete/zsh.py +4 -3
- zrb/builtin/todo.py +13 -2
- zrb/config/config.py +614 -0
- zrb/config/default_prompt/file_extractor_system_prompt.md +112 -0
- zrb/config/default_prompt/interactive_system_prompt.md +29 -0
- zrb/config/default_prompt/persona.md +1 -0
- zrb/config/default_prompt/repo_extractor_system_prompt.md +112 -0
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +29 -0
- zrb/config/default_prompt/summarization_prompt.md +57 -0
- zrb/config/default_prompt/system_prompt.md +38 -0
- zrb/config/llm_config.py +339 -0
- zrb/config/llm_context/config.py +166 -0
- zrb/config/llm_context/config_parser.py +40 -0
- zrb/config/llm_context/workflow.py +81 -0
- zrb/config/llm_rate_limitter.py +190 -0
- zrb/{runner ā config}/web_auth_config.py +17 -22
- zrb/context/any_shared_context.py +17 -1
- zrb/context/context.py +16 -2
- zrb/context/shared_context.py +18 -8
- zrb/group/any_group.py +12 -5
- zrb/group/group.py +67 -3
- zrb/input/any_input.py +5 -1
- zrb/input/base_input.py +18 -6
- zrb/input/option_input.py +13 -1
- zrb/input/text_input.py +8 -25
- zrb/runner/cli.py +25 -23
- zrb/runner/common_util.py +24 -19
- zrb/runner/web_app.py +3 -3
- zrb/runner/web_route/docs_route.py +1 -1
- zrb/runner/web_route/error_page/serve_default_404.py +1 -1
- zrb/runner/web_route/error_page/show_error_page.py +1 -1
- zrb/runner/web_route/home_page/home_page_route.py +2 -2
- zrb/runner/web_route/login_api_route.py +1 -1
- zrb/runner/web_route/login_page/login_page_route.py +2 -2
- zrb/runner/web_route/logout_api_route.py +1 -1
- zrb/runner/web_route/logout_page/logout_page_route.py +2 -2
- zrb/runner/web_route/node_page/group/show_group_page.py +1 -1
- zrb/runner/web_route/node_page/node_page_route.py +1 -1
- zrb/runner/web_route/node_page/task/show_task_page.py +1 -1
- zrb/runner/web_route/refresh_token_api_route.py +1 -1
- zrb/runner/web_route/static/static_route.py +1 -1
- zrb/runner/web_route/task_input_api_route.py +6 -6
- zrb/runner/web_route/task_session_api_route.py +20 -12
- zrb/runner/web_util/cookie.py +1 -1
- zrb/runner/web_util/token.py +1 -1
- zrb/runner/web_util/user.py +8 -4
- zrb/session/any_session.py +24 -17
- zrb/session/session.py +50 -25
- zrb/session_state_logger/any_session_state_logger.py +9 -4
- zrb/session_state_logger/file_session_state_logger.py +16 -6
- zrb/session_state_logger/session_state_logger_factory.py +1 -1
- zrb/task/any_task.py +30 -9
- zrb/task/base/context.py +17 -9
- zrb/task/base/execution.py +15 -8
- zrb/task/base/lifecycle.py +8 -4
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +69 -5
- zrb/task/base_trigger.py +12 -5
- zrb/task/cmd_task.py +1 -1
- zrb/task/llm/agent.py +154 -161
- zrb/task/llm/agent_runner.py +152 -0
- zrb/task/llm/config.py +47 -18
- zrb/task/llm/conversation_history.py +209 -0
- zrb/task/llm/conversation_history_model.py +67 -0
- zrb/task/llm/default_workflow/coding/workflow.md +41 -0
- zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
- zrb/task/llm/default_workflow/git/workflow.md +118 -0
- zrb/task/llm/default_workflow/golang/workflow.md +128 -0
- zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
- zrb/task/llm/default_workflow/java/workflow.md +146 -0
- zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
- zrb/task/llm/default_workflow/python/workflow.md +160 -0
- zrb/task/llm/default_workflow/researching/workflow.md +153 -0
- zrb/task/llm/default_workflow/rust/workflow.md +162 -0
- zrb/task/llm/default_workflow/shell/workflow.md +299 -0
- zrb/task/llm/error.py +24 -10
- zrb/task/llm/file_replacement.py +206 -0
- zrb/task/llm/file_tool_model.py +57 -0
- zrb/task/llm/history_processor.py +206 -0
- zrb/task/llm/history_summarization.py +11 -166
- zrb/task/llm/print_node.py +193 -69
- zrb/task/llm/prompt.py +242 -45
- zrb/task/llm/subagent_conversation_history.py +41 -0
- zrb/task/llm/tool_wrapper.py +260 -57
- zrb/task/llm/workflow.py +76 -0
- zrb/task/llm_task.py +182 -171
- zrb/task/make_task.py +2 -3
- zrb/task/rsync_task.py +26 -11
- zrb/task/scheduler.py +4 -4
- zrb/util/attr.py +54 -39
- zrb/util/callable.py +23 -0
- zrb/util/cli/markdown.py +12 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/file.py +29 -11
- zrb/util/git.py +8 -11
- zrb/util/git_diff_model.py +10 -0
- zrb/util/git_subtree.py +9 -14
- zrb/util/git_subtree_model.py +32 -0
- zrb/util/init_path.py +1 -1
- zrb/util/markdown.py +62 -0
- zrb/util/string/conversion.py +2 -2
- zrb/util/todo.py +17 -50
- zrb/util/todo_model.py +46 -0
- zrb/util/truncate.py +23 -0
- zrb/util/yaml.py +204 -0
- zrb/xcom/xcom.py +10 -0
- zrb-1.21.29.dist-info/METADATA +270 -0
- {zrb-1.8.10.dist-info ā zrb-1.21.29.dist-info}/RECORD +140 -98
- {zrb-1.8.10.dist-info ā zrb-1.21.29.dist-info}/WHEEL +1 -1
- zrb/config.py +0 -335
- zrb/llm_config.py +0 -411
- zrb/llm_rate_limitter.py +0 -125
- zrb/task/llm/context.py +0 -102
- zrb/task/llm/context_enrichment.py +0 -199
- zrb/task/llm/history.py +0 -211
- zrb-1.8.10.dist-info/METADATA +0 -264
- {zrb-1.8.10.dist-info ā zrb-1.21.29.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
4
|
+
from prompt_toolkit.document import Document
|
|
5
|
+
|
|
6
|
+
from zrb.builtin.llm.chat_session_cmd import (
|
|
7
|
+
ADD_SUB_CMD,
|
|
8
|
+
ATTACHMENT_ADD_SUB_CMD_DESC,
|
|
9
|
+
ATTACHMENT_CLEAR_SUB_CMD_DESC,
|
|
10
|
+
ATTACHMENT_CMD,
|
|
11
|
+
ATTACHMENT_CMD_DESC,
|
|
12
|
+
ATTACHMENT_SET_SUB_CMD_DESC,
|
|
13
|
+
CLEAR_SUB_CMD,
|
|
14
|
+
HELP_CMD,
|
|
15
|
+
HELP_CMD_DESC,
|
|
16
|
+
MULTILINE_END_CMD,
|
|
17
|
+
MULTILINE_END_CMD_DESC,
|
|
18
|
+
MULTILINE_START_CMD,
|
|
19
|
+
MULTILINE_START_CMD_DESC,
|
|
20
|
+
QUIT_CMD,
|
|
21
|
+
QUIT_CMD_DESC,
|
|
22
|
+
RUN_CLI_CMD,
|
|
23
|
+
RUN_CLI_CMD_DESC,
|
|
24
|
+
SAVE_CMD,
|
|
25
|
+
SAVE_CMD_DESC,
|
|
26
|
+
SET_SUB_CMD,
|
|
27
|
+
WORKFLOW_ADD_SUB_CMD_DESC,
|
|
28
|
+
WORKFLOW_CLEAR_SUB_CMD_DESC,
|
|
29
|
+
WORKFLOW_CMD,
|
|
30
|
+
WORKFLOW_CMD_DESC,
|
|
31
|
+
WORKFLOW_SET_SUB_CMD_DESC,
|
|
32
|
+
YOLO_CMD,
|
|
33
|
+
YOLO_CMD_DESC,
|
|
34
|
+
YOLO_SET_CMD_DESC,
|
|
35
|
+
YOLO_SET_FALSE_CMD_DESC,
|
|
36
|
+
YOLO_SET_TRUE_CMD_DESC,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ChatCompleter(Completer):
|
|
41
|
+
|
|
42
|
+
def get_completions(self, document: Document, complete_event: CompleteEvent):
|
|
43
|
+
# Slash command
|
|
44
|
+
for completion in self._complete_slash_command(document):
|
|
45
|
+
yield completion
|
|
46
|
+
for completion in self._complete_slash_file_command(document):
|
|
47
|
+
yield completion
|
|
48
|
+
# Appendix
|
|
49
|
+
for completion in self._complete_appendix(document):
|
|
50
|
+
yield completion
|
|
51
|
+
|
|
52
|
+
def _complete_slash_file_command(self, document: Document):
|
|
53
|
+
text = document.text_before_cursor
|
|
54
|
+
prefixes = []
|
|
55
|
+
for cmd in ATTACHMENT_CMD:
|
|
56
|
+
for subcmd in ADD_SUB_CMD:
|
|
57
|
+
prefixes.append(f"{cmd} {subcmd} ")
|
|
58
|
+
for prefix in prefixes:
|
|
59
|
+
if text.startswith(prefix):
|
|
60
|
+
pattern = text[len(prefix) :]
|
|
61
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=False)
|
|
62
|
+
for prefixed_option in [
|
|
63
|
+
f"{prefix}{option}" for option in potential_options
|
|
64
|
+
]:
|
|
65
|
+
yield Completion(
|
|
66
|
+
prefixed_option,
|
|
67
|
+
start_position=-len(text),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _complete_slash_command(self, document: Document):
|
|
71
|
+
text = document.text_before_cursor
|
|
72
|
+
if not text.startswith("/"):
|
|
73
|
+
return
|
|
74
|
+
for command, description in self._get_cmd_options().items():
|
|
75
|
+
if command.lower().startswith(text.lower()):
|
|
76
|
+
yield Completion(
|
|
77
|
+
command,
|
|
78
|
+
start_position=-len(text),
|
|
79
|
+
display_meta=description,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _complete_appendix(self, document: Document):
|
|
83
|
+
token = document.get_word_before_cursor(WORD=True)
|
|
84
|
+
prefix = "@"
|
|
85
|
+
if not token.startswith(prefix):
|
|
86
|
+
return
|
|
87
|
+
pattern = token[len(prefix) :]
|
|
88
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=False)
|
|
89
|
+
for prefixed_option in [f"{prefix}{option}" for option in potential_options]:
|
|
90
|
+
yield Completion(
|
|
91
|
+
prefixed_option,
|
|
92
|
+
start_position=-len(token),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _get_cmd_options(self):
|
|
96
|
+
cmd_options = {}
|
|
97
|
+
# Add all commands with their descriptions
|
|
98
|
+
for cmd in MULTILINE_START_CMD:
|
|
99
|
+
cmd_options[cmd] = MULTILINE_START_CMD_DESC
|
|
100
|
+
for cmd in MULTILINE_END_CMD:
|
|
101
|
+
cmd_options[cmd] = MULTILINE_END_CMD_DESC
|
|
102
|
+
for cmd in QUIT_CMD:
|
|
103
|
+
cmd_options[cmd] = QUIT_CMD_DESC
|
|
104
|
+
for cmd in WORKFLOW_CMD:
|
|
105
|
+
cmd_options[cmd] = WORKFLOW_CMD_DESC
|
|
106
|
+
for subcmd in ADD_SUB_CMD:
|
|
107
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_ADD_SUB_CMD_DESC
|
|
108
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
109
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_CLEAR_SUB_CMD_DESC
|
|
110
|
+
for subcmd in SET_SUB_CMD:
|
|
111
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_SET_SUB_CMD_DESC
|
|
112
|
+
for cmd in SAVE_CMD:
|
|
113
|
+
cmd_options[cmd] = SAVE_CMD_DESC
|
|
114
|
+
for cmd in ATTACHMENT_CMD:
|
|
115
|
+
cmd_options[cmd] = ATTACHMENT_CMD_DESC
|
|
116
|
+
for subcmd in ADD_SUB_CMD:
|
|
117
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_ADD_SUB_CMD_DESC
|
|
118
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
119
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_CLEAR_SUB_CMD_DESC
|
|
120
|
+
for subcmd in SET_SUB_CMD:
|
|
121
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_SET_SUB_CMD_DESC
|
|
122
|
+
for cmd in YOLO_CMD:
|
|
123
|
+
cmd_options[cmd] = YOLO_CMD_DESC
|
|
124
|
+
for subcmd in SET_SUB_CMD:
|
|
125
|
+
cmd_options[f"{cmd} {subcmd} true"] = YOLO_SET_TRUE_CMD_DESC
|
|
126
|
+
cmd_options[f"{cmd} {subcmd} false"] = YOLO_SET_FALSE_CMD_DESC
|
|
127
|
+
cmd_options[f"{cmd} {subcmd}"] = YOLO_SET_CMD_DESC
|
|
128
|
+
for cmd in HELP_CMD:
|
|
129
|
+
cmd_options[cmd] = HELP_CMD_DESC
|
|
130
|
+
for cmd in RUN_CLI_CMD:
|
|
131
|
+
cmd_options[cmd] = RUN_CLI_CMD_DESC
|
|
132
|
+
return dict(sorted(cmd_options.items()))
|
|
133
|
+
|
|
134
|
+
def _fuzzy_path_search(
|
|
135
|
+
self,
|
|
136
|
+
pattern: str,
|
|
137
|
+
root: str | None = None,
|
|
138
|
+
max_results: int = 20,
|
|
139
|
+
include_hidden: bool = False,
|
|
140
|
+
case_sensitive: bool = False,
|
|
141
|
+
dirs: bool = True,
|
|
142
|
+
files: bool = True,
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
"""
|
|
145
|
+
Return a list of filesystem paths under `root` that fuzzy-match `pattern`.
|
|
146
|
+
- pattern: e.g. "./some/x" or "proj util/io"
|
|
147
|
+
- include_hidden: if False skip files/dirs starting with '.'
|
|
148
|
+
- dirs/files booleans let you restrict results
|
|
149
|
+
- returns list of relative paths (from root), sorted best-first
|
|
150
|
+
"""
|
|
151
|
+
search_pattern = pattern
|
|
152
|
+
if root is None:
|
|
153
|
+
# Determine root and adjust pattern if necessary
|
|
154
|
+
expanded_pattern = os.path.expanduser(pattern)
|
|
155
|
+
if os.path.isabs(expanded_pattern) or pattern.startswith("~"):
|
|
156
|
+
# For absolute paths, find the deepest existing directory
|
|
157
|
+
if os.path.isdir(expanded_pattern):
|
|
158
|
+
root = expanded_pattern
|
|
159
|
+
search_pattern = ""
|
|
160
|
+
else:
|
|
161
|
+
root = os.path.dirname(expanded_pattern)
|
|
162
|
+
while root and not os.path.isdir(root) and len(root) > 1:
|
|
163
|
+
root = os.path.dirname(root)
|
|
164
|
+
if not os.path.isdir(root):
|
|
165
|
+
root = "." # Fallback
|
|
166
|
+
search_pattern = pattern
|
|
167
|
+
else:
|
|
168
|
+
try:
|
|
169
|
+
search_pattern = os.path.relpath(expanded_pattern, root)
|
|
170
|
+
if search_pattern == ".":
|
|
171
|
+
search_pattern = ""
|
|
172
|
+
except ValueError:
|
|
173
|
+
search_pattern = os.path.basename(pattern)
|
|
174
|
+
else:
|
|
175
|
+
root = "."
|
|
176
|
+
search_pattern = pattern
|
|
177
|
+
# Normalize pattern -> tokens split on path separators or whitespace
|
|
178
|
+
search_pattern = search_pattern.strip()
|
|
179
|
+
if search_pattern:
|
|
180
|
+
raw_tokens = [t for t in search_pattern.split(os.path.sep) if t]
|
|
181
|
+
else:
|
|
182
|
+
raw_tokens = []
|
|
183
|
+
# prepare tokens (case)
|
|
184
|
+
if not case_sensitive:
|
|
185
|
+
tokens = [t.lower() for t in raw_tokens]
|
|
186
|
+
else:
|
|
187
|
+
tokens = raw_tokens
|
|
188
|
+
# specific ignore list
|
|
189
|
+
try:
|
|
190
|
+
is_recursive = os.path.abspath(os.path.expanduser(root)).startswith(
|
|
191
|
+
os.path.abspath(os.getcwd())
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
is_recursive = False
|
|
195
|
+
# walk filesystem
|
|
196
|
+
candidates: list[tuple[float, str]] = []
|
|
197
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
198
|
+
# Filter directories
|
|
199
|
+
if not include_hidden:
|
|
200
|
+
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
|
201
|
+
rel_dir = os.path.relpath(dirpath, root)
|
|
202
|
+
# treat '.' as empty prefix
|
|
203
|
+
if rel_dir == ".":
|
|
204
|
+
rel_dir = ""
|
|
205
|
+
# build list of entries to test depending on files/dirs flags
|
|
206
|
+
entries = []
|
|
207
|
+
if dirs:
|
|
208
|
+
entries.extend([os.path.join(rel_dir, d) for d in dirnames])
|
|
209
|
+
if files:
|
|
210
|
+
entries.extend([os.path.join(rel_dir, f) for f in filenames])
|
|
211
|
+
if not is_recursive:
|
|
212
|
+
dirnames[:] = []
|
|
213
|
+
for ent in entries:
|
|
214
|
+
# Normalize presentation: use ./ prefix for relative paths
|
|
215
|
+
display_path = ent if ent else "."
|
|
216
|
+
# Skip hidden entries unless requested (double check for rel path segments)
|
|
217
|
+
if not include_hidden:
|
|
218
|
+
if any(
|
|
219
|
+
seg.startswith(".") for seg in display_path.split(os.sep) if seg
|
|
220
|
+
):
|
|
221
|
+
continue
|
|
222
|
+
cand = display_path.replace(os.sep, "/") # unify separator
|
|
223
|
+
cand_cmp = cand if case_sensitive else cand.lower()
|
|
224
|
+
last_pos = 0
|
|
225
|
+
score = 0.0
|
|
226
|
+
matched_all = True
|
|
227
|
+
for token in tokens:
|
|
228
|
+
# try contiguous substring search first
|
|
229
|
+
idx = cand_cmp.find(token, last_pos)
|
|
230
|
+
if idx != -1:
|
|
231
|
+
# good match: reward contiguous early matches
|
|
232
|
+
score += idx # smaller idx preferred
|
|
233
|
+
last_pos = idx + len(token)
|
|
234
|
+
else:
|
|
235
|
+
# fallback to subsequence matching
|
|
236
|
+
pos = self._find_subsequence_pos(cand_cmp, token, last_pos)
|
|
237
|
+
if pos is None:
|
|
238
|
+
matched_all = False
|
|
239
|
+
break
|
|
240
|
+
# subsequence match is less preferred than contiguous substring
|
|
241
|
+
score += pos + 0.5 * len(token)
|
|
242
|
+
last_pos = pos + len(token)
|
|
243
|
+
if matched_all:
|
|
244
|
+
# prefer shorter paths when score ties, so include length as tiebreaker
|
|
245
|
+
score += 0.01 * len(cand)
|
|
246
|
+
out = (
|
|
247
|
+
cand
|
|
248
|
+
if os.path.abspath(cand) == cand
|
|
249
|
+
else os.path.join(root, cand)
|
|
250
|
+
)
|
|
251
|
+
candidates.append((score, out))
|
|
252
|
+
# sort by score then lexicographically and return top results
|
|
253
|
+
candidates.sort(key=lambda x: (x[0], x[1]))
|
|
254
|
+
return [p for _, p in candidates[:max_results]]
|
|
255
|
+
|
|
256
|
+
def _find_subsequence_pos(
|
|
257
|
+
self, hay: str, needle: str, start: int = 0
|
|
258
|
+
) -> int | None:
|
|
259
|
+
"""
|
|
260
|
+
Try to locate needle in hay as a subsequence starting at `start`.
|
|
261
|
+
Returns the index of the first matched character of the subsequence or None if not match.
|
|
262
|
+
"""
|
|
263
|
+
if not needle:
|
|
264
|
+
return start
|
|
265
|
+
i = start
|
|
266
|
+
j = 0
|
|
267
|
+
first_pos = None
|
|
268
|
+
while i < len(hay) and j < len(needle):
|
|
269
|
+
if hay[i] == needle[j]:
|
|
270
|
+
if first_pos is None:
|
|
271
|
+
first_pos = i
|
|
272
|
+
j += 1
|
|
273
|
+
i += 1
|
|
274
|
+
return first_pos if j == len(needle) else None
|
zrb/builtin/llm/chat_session.py
CHANGED
|
@@ -1,84 +1,154 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module provides functions for managing interactive chat sessions with an LLM.
|
|
3
|
-
|
|
4
|
-
It handles reading user input, triggering the LLM task, and managing the
|
|
5
|
-
conversation flow via XCom.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
1
|
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
9
4
|
|
|
5
|
+
from zrb.builtin.llm.chat_session_cmd import (
|
|
6
|
+
ATTACHMENT_CMD,
|
|
7
|
+
HELP_CMD,
|
|
8
|
+
MULTILINE_END_CMD,
|
|
9
|
+
MULTILINE_START_CMD,
|
|
10
|
+
QUIT_CMD,
|
|
11
|
+
RUN_CLI_CMD,
|
|
12
|
+
SAVE_CMD,
|
|
13
|
+
WORKFLOW_CMD,
|
|
14
|
+
YOLO_CMD,
|
|
15
|
+
get_new_attachments,
|
|
16
|
+
get_new_workflows,
|
|
17
|
+
get_new_yolo_mode,
|
|
18
|
+
is_command_match,
|
|
19
|
+
print_commands,
|
|
20
|
+
print_current_attachments,
|
|
21
|
+
print_current_workflows,
|
|
22
|
+
print_current_yolo_mode,
|
|
23
|
+
run_cli_command,
|
|
24
|
+
save_final_result,
|
|
25
|
+
)
|
|
26
|
+
from zrb.builtin.llm.chat_trigger import llm_chat_trigger
|
|
27
|
+
from zrb.config.llm_config import llm_config
|
|
10
28
|
from zrb.context.any_context import AnyContext
|
|
11
|
-
from zrb.util.cli.
|
|
29
|
+
from zrb.util.cli.markdown import render_markdown
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from asyncio import StreamReader
|
|
33
|
+
|
|
34
|
+
from prompt_toolkit import PromptSession
|
|
12
35
|
|
|
13
36
|
|
|
14
37
|
async def read_user_prompt(ctx: AnyContext) -> str:
|
|
15
38
|
"""
|
|
16
39
|
Reads user input from the CLI for an interactive chat session.
|
|
17
|
-
|
|
18
|
-
Handles special commands like /bye, /multi, /end, and /help.
|
|
19
|
-
Triggers the LLM task and waits for the result.
|
|
20
|
-
|
|
21
|
-
Args:
|
|
22
|
-
ctx: The context object for the task.
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
The final result from the LLM session.
|
|
40
|
+
Orchestrates the session by calling helper functions.
|
|
26
41
|
"""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
result = await _trigger_ask_and_wait_for_result(
|
|
31
|
-
ctx,
|
|
32
|
-
user_prompt=ctx.input.message,
|
|
33
|
-
previous_session_name=ctx.input.previous_session,
|
|
34
|
-
start_new=ctx.input.start_new,
|
|
35
|
-
)
|
|
36
|
-
if result is not None:
|
|
37
|
-
final_result = result
|
|
38
|
-
if ctx.env.get("_ZRB_WEB_ENV", "0") != "0":
|
|
39
|
-
# On web environment this won't be interactive
|
|
40
|
-
return final_result
|
|
42
|
+
print_commands(ctx)
|
|
43
|
+
is_tty: bool = ctx.is_tty
|
|
44
|
+
reader: PromptSession[Any] | StreamReader = await _setup_input_reader(is_tty)
|
|
41
45
|
multiline_mode = False
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
is_first_time = True
|
|
47
|
+
current_workflows: str = ctx.input.workflows
|
|
48
|
+
current_yolo_mode: bool | str = ctx.input.yolo
|
|
49
|
+
current_attachments: str = ctx.input.attach
|
|
50
|
+
user_inputs: list[str] = []
|
|
51
|
+
final_result: str = ""
|
|
52
|
+
should_end = False
|
|
53
|
+
while not should_end:
|
|
44
54
|
await asyncio.sleep(0.01)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
previous_session_name: str | None = (
|
|
56
|
+
ctx.input.previous_session if is_first_time else ""
|
|
57
|
+
)
|
|
58
|
+
start_new: bool = ctx.input.start_new if is_first_time else False
|
|
59
|
+
if is_first_time and ctx.input.message.strip() != "":
|
|
60
|
+
user_input = ctx.input.message
|
|
61
|
+
else:
|
|
62
|
+
# Get user input based on mode
|
|
63
|
+
if not multiline_mode:
|
|
64
|
+
ctx.print("š¬ >>", plain=True)
|
|
65
|
+
user_input = await llm_chat_trigger.wait(reader, ctx)
|
|
66
|
+
if not multiline_mode:
|
|
67
|
+
ctx.print("", plain=True)
|
|
68
|
+
# At this point, is_first_time has to be False
|
|
69
|
+
if is_first_time:
|
|
70
|
+
is_first_time = False
|
|
71
|
+
# Handle user input (including slash commands)
|
|
72
|
+
if multiline_mode:
|
|
73
|
+
if is_command_match(user_input, MULTILINE_END_CMD):
|
|
74
|
+
ctx.print("", plain=True)
|
|
75
|
+
multiline_mode = False
|
|
76
|
+
else:
|
|
77
|
+
user_inputs.append(user_input)
|
|
78
|
+
continue
|
|
67
79
|
else:
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
if is_command_match(user_input, QUIT_CMD):
|
|
81
|
+
should_end = True
|
|
82
|
+
elif is_command_match(user_input, MULTILINE_START_CMD):
|
|
83
|
+
multiline_mode = True
|
|
84
|
+
ctx.print("", plain=True)
|
|
85
|
+
continue
|
|
86
|
+
elif is_command_match(user_input, WORKFLOW_CMD):
|
|
87
|
+
current_workflows = get_new_workflows(current_workflows, user_input)
|
|
88
|
+
print_current_workflows(ctx, current_workflows)
|
|
89
|
+
continue
|
|
90
|
+
elif is_command_match(user_input, SAVE_CMD):
|
|
91
|
+
save_final_result(ctx, user_input, final_result)
|
|
92
|
+
continue
|
|
93
|
+
elif is_command_match(user_input, ATTACHMENT_CMD):
|
|
94
|
+
current_attachments = get_new_attachments(
|
|
95
|
+
current_attachments, user_input
|
|
96
|
+
)
|
|
97
|
+
print_current_attachments(ctx, current_attachments)
|
|
70
98
|
continue
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
elif is_command_match(user_input, YOLO_CMD):
|
|
100
|
+
current_yolo_mode = get_new_yolo_mode(current_yolo_mode, user_input)
|
|
101
|
+
print_current_yolo_mode(ctx, current_yolo_mode)
|
|
102
|
+
continue
|
|
103
|
+
elif is_command_match(user_input, RUN_CLI_CMD):
|
|
104
|
+
run_cli_command(ctx, user_input)
|
|
105
|
+
continue
|
|
106
|
+
elif is_command_match(user_input, HELP_CMD):
|
|
107
|
+
print_commands(ctx)
|
|
108
|
+
continue
|
|
109
|
+
else:
|
|
110
|
+
user_inputs.append(user_input)
|
|
111
|
+
# Trigger LLM
|
|
112
|
+
user_prompt = "\n".join(user_inputs)
|
|
113
|
+
user_inputs = []
|
|
114
|
+
result = await _trigger_ask_and_wait_for_result(
|
|
115
|
+
ctx=ctx,
|
|
116
|
+
user_prompt=user_prompt,
|
|
117
|
+
attach=current_attachments,
|
|
118
|
+
workflows=current_workflows,
|
|
119
|
+
yolo_mode=current_yolo_mode,
|
|
120
|
+
previous_session_name=previous_session_name,
|
|
121
|
+
start_new=start_new,
|
|
122
|
+
)
|
|
123
|
+
current_attachments = ""
|
|
124
|
+
final_result = final_result if result is None else result
|
|
125
|
+
if ctx.is_web_mode or not is_tty:
|
|
126
|
+
return final_result
|
|
76
127
|
return final_result
|
|
77
128
|
|
|
78
129
|
|
|
130
|
+
async def _setup_input_reader(
|
|
131
|
+
is_interactive: bool,
|
|
132
|
+
) -> "PromptSession[Any] | StreamReader":
|
|
133
|
+
"""Sets up and returns the appropriate asynchronous input reader."""
|
|
134
|
+
if is_interactive:
|
|
135
|
+
from prompt_toolkit import PromptSession
|
|
136
|
+
|
|
137
|
+
return PromptSession()
|
|
138
|
+
|
|
139
|
+
loop = asyncio.get_event_loop()
|
|
140
|
+
reader = asyncio.StreamReader(loop=loop)
|
|
141
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
142
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
143
|
+
return reader
|
|
144
|
+
|
|
145
|
+
|
|
79
146
|
async def _trigger_ask_and_wait_for_result(
|
|
80
147
|
ctx: AnyContext,
|
|
81
148
|
user_prompt: str,
|
|
149
|
+
attach: str,
|
|
150
|
+
workflows: str,
|
|
151
|
+
yolo_mode: bool | str,
|
|
82
152
|
previous_session_name: str | None = None,
|
|
83
153
|
start_new: bool = False,
|
|
84
154
|
) -> str | None:
|
|
@@ -96,9 +166,14 @@ async def _trigger_ask_and_wait_for_result(
|
|
|
96
166
|
"""
|
|
97
167
|
if user_prompt.strip() == "":
|
|
98
168
|
return None
|
|
99
|
-
await _trigger_ask(
|
|
169
|
+
await _trigger_ask(
|
|
170
|
+
ctx, user_prompt, attach, workflows, yolo_mode, previous_session_name, start_new
|
|
171
|
+
)
|
|
100
172
|
result = await _wait_ask_result(ctx)
|
|
101
|
-
|
|
173
|
+
md_result = render_markdown(result) if result is not None else ""
|
|
174
|
+
ctx.print("\nš¤ >>", plain=True)
|
|
175
|
+
ctx.print(md_result, plain=True)
|
|
176
|
+
ctx.print("", plain=True)
|
|
102
177
|
return result
|
|
103
178
|
|
|
104
179
|
|
|
@@ -113,20 +188,29 @@ def get_llm_ask_input_mapping(callback_ctx: AnyContext):
|
|
|
113
188
|
A dictionary containing the input mapping for the LLM ask task.
|
|
114
189
|
"""
|
|
115
190
|
data = callback_ctx.xcom.ask_trigger.pop()
|
|
191
|
+
system_prompt = callback_ctx.input.system_prompt
|
|
192
|
+
if system_prompt is None or system_prompt.strip() == "":
|
|
193
|
+
system_prompt = llm_config.default_interactive_system_prompt
|
|
116
194
|
return {
|
|
117
195
|
"model": callback_ctx.input.model,
|
|
118
196
|
"base-url": callback_ctx.input.base_url,
|
|
119
197
|
"api-key": callback_ctx.input.api_key,
|
|
120
|
-
"system-prompt":
|
|
198
|
+
"system-prompt": system_prompt,
|
|
121
199
|
"start-new": data.get("start_new"),
|
|
122
200
|
"previous-session": data.get("previous_session_name"),
|
|
123
201
|
"message": data.get("message"),
|
|
202
|
+
"attach": data.get("attach"),
|
|
203
|
+
"workflows": data.get("workflows"),
|
|
204
|
+
"yolo": data.get("yolo"),
|
|
124
205
|
}
|
|
125
206
|
|
|
126
207
|
|
|
127
208
|
async def _trigger_ask(
|
|
128
209
|
ctx: AnyContext,
|
|
129
210
|
user_prompt: str,
|
|
211
|
+
attach: str,
|
|
212
|
+
workflows: str,
|
|
213
|
+
yolo_mode: bool | str,
|
|
130
214
|
previous_session_name: str | None = None,
|
|
131
215
|
start_new: bool = False,
|
|
132
216
|
):
|
|
@@ -146,6 +230,9 @@ async def _trigger_ask(
|
|
|
146
230
|
"previous_session_name": previous_session_name,
|
|
147
231
|
"start_new": start_new,
|
|
148
232
|
"message": user_prompt,
|
|
233
|
+
"attach": attach,
|
|
234
|
+
"workflows": workflows,
|
|
235
|
+
"yolo": yolo_mode,
|
|
149
236
|
}
|
|
150
237
|
)
|
|
151
238
|
|
|
@@ -181,23 +268,3 @@ async def _wait_ask_session_name(ctx: AnyContext) -> str:
|
|
|
181
268
|
while "ask_session_name" not in ctx.xcom or len(ctx.xcom.ask_session_name) == 0:
|
|
182
269
|
await asyncio.sleep(0.1)
|
|
183
270
|
return ctx.xcom.ask_session_name.pop()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _show_info(ctx: AnyContext):
|
|
187
|
-
"""
|
|
188
|
-
Displays the available chat session commands to the user.
|
|
189
|
-
|
|
190
|
-
Args:
|
|
191
|
-
ctx: The context object for the task.
|
|
192
|
-
"""
|
|
193
|
-
ctx.print(
|
|
194
|
-
"\n".join(
|
|
195
|
-
[
|
|
196
|
-
f"{stylize_bold_yellow('/bye')} {stylize_faint('Quit from chat session')}",
|
|
197
|
-
f"{stylize_bold_yellow('/multi')} {stylize_faint('Start multiline input')}",
|
|
198
|
-
f"{stylize_bold_yellow('/end')} {stylize_faint('End multiline input')}",
|
|
199
|
-
f"{stylize_bold_yellow('/help')} {stylize_faint('Show this message')}",
|
|
200
|
-
]
|
|
201
|
-
),
|
|
202
|
-
plain=True,
|
|
203
|
-
)
|