zrb 1.21.29__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.

Files changed (192) hide show
  1. zrb/__init__.py +118 -129
  2. zrb/builtin/__init__.py +54 -2
  3. zrb/builtin/llm/chat.py +147 -0
  4. zrb/callback/callback.py +8 -1
  5. zrb/cmd/cmd_result.py +2 -1
  6. zrb/config/config.py +491 -280
  7. zrb/config/helper.py +84 -0
  8. zrb/config/web_auth_config.py +50 -35
  9. zrb/context/any_shared_context.py +13 -2
  10. zrb/context/context.py +31 -3
  11. zrb/context/print_fn.py +13 -0
  12. zrb/context/shared_context.py +14 -1
  13. zrb/input/option_input.py +30 -2
  14. zrb/llm/agent/__init__.py +9 -0
  15. zrb/llm/agent/agent.py +215 -0
  16. zrb/llm/agent/summarizer.py +20 -0
  17. zrb/llm/app/__init__.py +10 -0
  18. zrb/llm/app/completion.py +281 -0
  19. zrb/llm/app/confirmation/allow_tool.py +66 -0
  20. zrb/llm/app/confirmation/handler.py +178 -0
  21. zrb/llm/app/confirmation/replace_confirmation.py +77 -0
  22. zrb/llm/app/keybinding.py +34 -0
  23. zrb/llm/app/layout.py +117 -0
  24. zrb/llm/app/lexer.py +155 -0
  25. zrb/llm/app/redirection.py +28 -0
  26. zrb/llm/app/style.py +16 -0
  27. zrb/llm/app/ui.py +733 -0
  28. zrb/llm/config/__init__.py +4 -0
  29. zrb/llm/config/config.py +122 -0
  30. zrb/llm/config/limiter.py +247 -0
  31. zrb/llm/history_manager/__init__.py +4 -0
  32. zrb/llm/history_manager/any_history_manager.py +23 -0
  33. zrb/llm/history_manager/file_history_manager.py +91 -0
  34. zrb/llm/history_processor/summarizer.py +108 -0
  35. zrb/llm/note/__init__.py +3 -0
  36. zrb/llm/note/manager.py +122 -0
  37. zrb/llm/prompt/__init__.py +29 -0
  38. zrb/llm/prompt/claude_compatibility.py +92 -0
  39. zrb/llm/prompt/compose.py +55 -0
  40. zrb/llm/prompt/default.py +51 -0
  41. zrb/llm/prompt/markdown/mandate.md +23 -0
  42. zrb/llm/prompt/markdown/persona.md +3 -0
  43. zrb/llm/prompt/markdown/summarizer.md +21 -0
  44. zrb/llm/prompt/note.py +41 -0
  45. zrb/llm/prompt/system_context.py +46 -0
  46. zrb/llm/prompt/zrb.py +41 -0
  47. zrb/llm/skill/__init__.py +3 -0
  48. zrb/llm/skill/manager.py +86 -0
  49. zrb/llm/task/__init__.py +4 -0
  50. zrb/llm/task/llm_chat_task.py +316 -0
  51. zrb/llm/task/llm_task.py +245 -0
  52. zrb/llm/tool/__init__.py +39 -0
  53. zrb/llm/tool/bash.py +75 -0
  54. zrb/llm/tool/code.py +266 -0
  55. zrb/llm/tool/file.py +419 -0
  56. zrb/llm/tool/note.py +70 -0
  57. zrb/{builtin/llm → llm}/tool/rag.py +8 -5
  58. zrb/llm/tool/search/brave.py +53 -0
  59. zrb/llm/tool/search/searxng.py +47 -0
  60. zrb/llm/tool/search/serpapi.py +47 -0
  61. zrb/llm/tool/skill.py +19 -0
  62. zrb/llm/tool/sub_agent.py +70 -0
  63. zrb/llm/tool/web.py +97 -0
  64. zrb/llm/tool/zrb_task.py +66 -0
  65. zrb/llm/util/attachment.py +101 -0
  66. zrb/llm/util/prompt.py +104 -0
  67. zrb/llm/util/stream_response.py +178 -0
  68. zrb/session/any_session.py +0 -3
  69. zrb/session/session.py +1 -1
  70. zrb/task/base/context.py +25 -13
  71. zrb/task/base/execution.py +52 -47
  72. zrb/task/base/lifecycle.py +7 -4
  73. zrb/task/base_task.py +48 -49
  74. zrb/task/base_trigger.py +4 -1
  75. zrb/task/cmd_task.py +6 -0
  76. zrb/task/http_check.py +11 -5
  77. zrb/task/make_task.py +3 -0
  78. zrb/task/rsync_task.py +5 -0
  79. zrb/task/scaffolder.py +7 -4
  80. zrb/task/scheduler.py +3 -0
  81. zrb/task/tcp_check.py +6 -4
  82. zrb/util/ascii_art/art/bee.txt +17 -0
  83. zrb/util/ascii_art/art/cat.txt +9 -0
  84. zrb/util/ascii_art/art/ghost.txt +16 -0
  85. zrb/util/ascii_art/art/panda.txt +17 -0
  86. zrb/util/ascii_art/art/rose.txt +14 -0
  87. zrb/util/ascii_art/art/unicorn.txt +15 -0
  88. zrb/util/ascii_art/banner.py +92 -0
  89. zrb/util/cli/markdown.py +22 -2
  90. zrb/util/cmd/command.py +33 -10
  91. zrb/util/file.py +51 -32
  92. zrb/util/match.py +78 -0
  93. zrb/util/run.py +3 -3
  94. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/METADATA +9 -15
  95. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/RECORD +100 -128
  96. zrb/attr/__init__.py +0 -0
  97. zrb/builtin/llm/attachment.py +0 -40
  98. zrb/builtin/llm/chat_completion.py +0 -274
  99. zrb/builtin/llm/chat_session.py +0 -270
  100. zrb/builtin/llm/chat_session_cmd.py +0 -288
  101. zrb/builtin/llm/chat_trigger.py +0 -79
  102. zrb/builtin/llm/history.py +0 -71
  103. zrb/builtin/llm/input.py +0 -27
  104. zrb/builtin/llm/llm_ask.py +0 -269
  105. zrb/builtin/llm/previous-session.js +0 -21
  106. zrb/builtin/llm/tool/__init__.py +0 -0
  107. zrb/builtin/llm/tool/api.py +0 -75
  108. zrb/builtin/llm/tool/cli.py +0 -52
  109. zrb/builtin/llm/tool/code.py +0 -236
  110. zrb/builtin/llm/tool/file.py +0 -560
  111. zrb/builtin/llm/tool/note.py +0 -84
  112. zrb/builtin/llm/tool/sub_agent.py +0 -150
  113. zrb/builtin/llm/tool/web.py +0 -171
  114. zrb/builtin/project/__init__.py +0 -0
  115. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
  116. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
  117. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
  118. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
  119. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
  120. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
  121. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
  122. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
  123. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
  124. zrb/builtin/project/create/__init__.py +0 -0
  125. zrb/builtin/shell/__init__.py +0 -0
  126. zrb/builtin/shell/autocomplete/__init__.py +0 -0
  127. zrb/callback/__init__.py +0 -0
  128. zrb/cmd/__init__.py +0 -0
  129. zrb/config/default_prompt/interactive_system_prompt.md +0 -29
  130. zrb/config/default_prompt/persona.md +0 -1
  131. zrb/config/default_prompt/summarization_prompt.md +0 -57
  132. zrb/config/default_prompt/system_prompt.md +0 -38
  133. zrb/config/llm_config.py +0 -339
  134. zrb/config/llm_context/config.py +0 -166
  135. zrb/config/llm_context/config_parser.py +0 -40
  136. zrb/config/llm_context/workflow.py +0 -81
  137. zrb/config/llm_rate_limitter.py +0 -190
  138. zrb/content_transformer/__init__.py +0 -0
  139. zrb/context/__init__.py +0 -0
  140. zrb/dot_dict/__init__.py +0 -0
  141. zrb/env/__init__.py +0 -0
  142. zrb/group/__init__.py +0 -0
  143. zrb/input/__init__.py +0 -0
  144. zrb/runner/__init__.py +0 -0
  145. zrb/runner/web_route/__init__.py +0 -0
  146. zrb/runner/web_route/home_page/__init__.py +0 -0
  147. zrb/session/__init__.py +0 -0
  148. zrb/session_state_log/__init__.py +0 -0
  149. zrb/session_state_logger/__init__.py +0 -0
  150. zrb/task/__init__.py +0 -0
  151. zrb/task/base/__init__.py +0 -0
  152. zrb/task/llm/__init__.py +0 -0
  153. zrb/task/llm/agent.py +0 -204
  154. zrb/task/llm/agent_runner.py +0 -152
  155. zrb/task/llm/config.py +0 -122
  156. zrb/task/llm/conversation_history.py +0 -209
  157. zrb/task/llm/conversation_history_model.py +0 -67
  158. zrb/task/llm/default_workflow/coding/workflow.md +0 -41
  159. zrb/task/llm/default_workflow/copywriting/workflow.md +0 -68
  160. zrb/task/llm/default_workflow/git/workflow.md +0 -118
  161. zrb/task/llm/default_workflow/golang/workflow.md +0 -128
  162. zrb/task/llm/default_workflow/html-css/workflow.md +0 -135
  163. zrb/task/llm/default_workflow/java/workflow.md +0 -146
  164. zrb/task/llm/default_workflow/javascript/workflow.md +0 -158
  165. zrb/task/llm/default_workflow/python/workflow.md +0 -160
  166. zrb/task/llm/default_workflow/researching/workflow.md +0 -153
  167. zrb/task/llm/default_workflow/rust/workflow.md +0 -162
  168. zrb/task/llm/default_workflow/shell/workflow.md +0 -299
  169. zrb/task/llm/error.py +0 -95
  170. zrb/task/llm/file_replacement.py +0 -206
  171. zrb/task/llm/file_tool_model.py +0 -57
  172. zrb/task/llm/history_processor.py +0 -206
  173. zrb/task/llm/history_summarization.py +0 -25
  174. zrb/task/llm/print_node.py +0 -221
  175. zrb/task/llm/prompt.py +0 -321
  176. zrb/task/llm/subagent_conversation_history.py +0 -41
  177. zrb/task/llm/tool_wrapper.py +0 -361
  178. zrb/task/llm/typing.py +0 -3
  179. zrb/task/llm/workflow.py +0 -76
  180. zrb/task/llm_task.py +0 -379
  181. zrb/task_status/__init__.py +0 -0
  182. zrb/util/__init__.py +0 -0
  183. zrb/util/cli/__init__.py +0 -0
  184. zrb/util/cmd/__init__.py +0 -0
  185. zrb/util/codemod/__init__.py +0 -0
  186. zrb/util/string/__init__.py +0 -0
  187. zrb/xcom/__init__.py +0 -0
  188. /zrb/{config/default_prompt/file_extractor_system_prompt.md → llm/prompt/markdown/file_extractor.md} +0 -0
  189. /zrb/{config/default_prompt/repo_extractor_system_prompt.md → llm/prompt/markdown/repo_extractor.md} +0 -0
  190. /zrb/{config/default_prompt/repo_summarizer_system_prompt.md → llm/prompt/markdown/repo_summarizer.md} +0 -0
  191. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +0 -0
  192. {zrb-1.21.29.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
@@ -1,274 +0,0 @@
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
@@ -1,270 +0,0 @@
1
- import asyncio
2
- import sys
3
- from typing import TYPE_CHECKING, Any
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
28
- from zrb.context.any_context import AnyContext
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
35
-
36
-
37
- async def read_user_prompt(ctx: AnyContext) -> str:
38
- """
39
- Reads user input from the CLI for an interactive chat session.
40
- Orchestrates the session by calling helper functions.
41
- """
42
- print_commands(ctx)
43
- is_tty: bool = ctx.is_tty
44
- reader: PromptSession[Any] | StreamReader = await _setup_input_reader(is_tty)
45
- multiline_mode = False
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:
54
- await asyncio.sleep(0.01)
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
79
- else:
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)
98
- continue
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
127
- return final_result
128
-
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
-
146
- async def _trigger_ask_and_wait_for_result(
147
- ctx: AnyContext,
148
- user_prompt: str,
149
- attach: str,
150
- workflows: str,
151
- yolo_mode: bool | str,
152
- previous_session_name: str | None = None,
153
- start_new: bool = False,
154
- ) -> str | None:
155
- """
156
- Triggers the LLM ask task and waits for the result via XCom.
157
-
158
- Args:
159
- ctx: The context object for the task.
160
- user_prompt: The user's message to send to the LLM.
161
- previous_session_name: The name of the previous chat session (optional).
162
- start_new: Whether to start a new conversation (optional).
163
-
164
- Returns:
165
- The result from the LLM task, or None if the user prompt is empty.
166
- """
167
- if user_prompt.strip() == "":
168
- return None
169
- await _trigger_ask(
170
- ctx, user_prompt, attach, workflows, yolo_mode, previous_session_name, start_new
171
- )
172
- result = await _wait_ask_result(ctx)
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)
177
- return result
178
-
179
-
180
- def get_llm_ask_input_mapping(callback_ctx: AnyContext):
181
- """
182
- Generates the input mapping for the LLM ask task from the callback context.
183
-
184
- Args:
185
- callback_ctx: The context object for the callback.
186
-
187
- Returns:
188
- A dictionary containing the input mapping for the LLM ask task.
189
- """
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
194
- return {
195
- "model": callback_ctx.input.model,
196
- "base-url": callback_ctx.input.base_url,
197
- "api-key": callback_ctx.input.api_key,
198
- "system-prompt": system_prompt,
199
- "start-new": data.get("start_new"),
200
- "previous-session": data.get("previous_session_name"),
201
- "message": data.get("message"),
202
- "attach": data.get("attach"),
203
- "workflows": data.get("workflows"),
204
- "yolo": data.get("yolo"),
205
- }
206
-
207
-
208
- async def _trigger_ask(
209
- ctx: AnyContext,
210
- user_prompt: str,
211
- attach: str,
212
- workflows: str,
213
- yolo_mode: bool | str,
214
- previous_session_name: str | None = None,
215
- start_new: bool = False,
216
- ):
217
- """
218
- Triggers the LLM ask task by pushing data to the 'ask_trigger' XCom queue.
219
-
220
- Args:
221
- ctx: The context object for the task.
222
- user_prompt: The user's message to send to the LLM.
223
- previous_session_name: The name of the previous chat session (optional).
224
- start_new: Whether to start a new conversation (optional).
225
- """
226
- if previous_session_name is None:
227
- previous_session_name = await _wait_ask_session_name(ctx)
228
- ctx.xcom["ask_trigger"].push(
229
- {
230
- "previous_session_name": previous_session_name,
231
- "start_new": start_new,
232
- "message": user_prompt,
233
- "attach": attach,
234
- "workflows": workflows,
235
- "yolo": yolo_mode,
236
- }
237
- )
238
-
239
-
240
- async def _wait_ask_result(ctx: AnyContext) -> str | None:
241
- """
242
- Waits for and retrieves the LLM task result from the 'ask_result' XCom queue.
243
-
244
- Args:
245
- ctx: The context object for the task.
246
-
247
- Returns:
248
- The result string from the LLM task.
249
- """
250
- while "ask_result" not in ctx.xcom or len(ctx.xcom.ask_result) == 0:
251
- await asyncio.sleep(0.1)
252
- if "ask_error" in ctx.xcom and len(ctx.xcom.ask_error) > 0:
253
- ctx.xcom.ask_error.pop()
254
- return None
255
- return ctx.xcom.ask_result.pop()
256
-
257
-
258
- async def _wait_ask_session_name(ctx: AnyContext) -> str:
259
- """
260
- Waits for and retrieves the LLM chat session name from the 'ask_session_name' XCom queue.
261
-
262
- Args:
263
- ctx: The context object for the task.
264
-
265
- Returns:
266
- The session name string.
267
- """
268
- while "ask_session_name" not in ctx.xcom or len(ctx.xcom.ask_session_name) == 0:
269
- await asyncio.sleep(0.1)
270
- return ctx.xcom.ask_session_name.pop()