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.

Files changed (147) hide show
  1. zrb/__init__.py +126 -113
  2. zrb/__main__.py +1 -1
  3. zrb/attr/type.py +10 -7
  4. zrb/builtin/__init__.py +2 -50
  5. zrb/builtin/git.py +12 -1
  6. zrb/builtin/group.py +31 -15
  7. zrb/builtin/http.py +7 -8
  8. zrb/builtin/llm/attachment.py +40 -0
  9. zrb/builtin/llm/chat_completion.py +274 -0
  10. zrb/builtin/llm/chat_session.py +152 -85
  11. zrb/builtin/llm/chat_session_cmd.py +288 -0
  12. zrb/builtin/llm/chat_trigger.py +79 -0
  13. zrb/builtin/llm/history.py +7 -9
  14. zrb/builtin/llm/llm_ask.py +221 -98
  15. zrb/builtin/llm/tool/api.py +74 -52
  16. zrb/builtin/llm/tool/cli.py +46 -17
  17. zrb/builtin/llm/tool/code.py +71 -90
  18. zrb/builtin/llm/tool/file.py +301 -241
  19. zrb/builtin/llm/tool/note.py +84 -0
  20. zrb/builtin/llm/tool/rag.py +38 -8
  21. zrb/builtin/llm/tool/sub_agent.py +67 -50
  22. zrb/builtin/llm/tool/web.py +146 -122
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  25. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  26. zrb/builtin/searxng/config/settings.yml +5671 -0
  27. zrb/builtin/searxng/start.py +21 -0
  28. zrb/builtin/setup/latex/ubuntu.py +1 -0
  29. zrb/builtin/setup/ubuntu.py +1 -1
  30. zrb/builtin/shell/autocomplete/bash.py +4 -3
  31. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  32. zrb/builtin/todo.py +13 -2
  33. zrb/config/config.py +614 -0
  34. zrb/config/default_prompt/file_extractor_system_prompt.md +112 -0
  35. zrb/config/default_prompt/interactive_system_prompt.md +29 -0
  36. zrb/config/default_prompt/persona.md +1 -0
  37. zrb/config/default_prompt/repo_extractor_system_prompt.md +112 -0
  38. zrb/config/default_prompt/repo_summarizer_system_prompt.md +29 -0
  39. zrb/config/default_prompt/summarization_prompt.md +57 -0
  40. zrb/config/default_prompt/system_prompt.md +38 -0
  41. zrb/config/llm_config.py +339 -0
  42. zrb/config/llm_context/config.py +166 -0
  43. zrb/config/llm_context/config_parser.py +40 -0
  44. zrb/config/llm_context/workflow.py +81 -0
  45. zrb/config/llm_rate_limitter.py +190 -0
  46. zrb/{runner → config}/web_auth_config.py +17 -22
  47. zrb/context/any_shared_context.py +17 -1
  48. zrb/context/context.py +16 -2
  49. zrb/context/shared_context.py +18 -8
  50. zrb/group/any_group.py +12 -5
  51. zrb/group/group.py +67 -3
  52. zrb/input/any_input.py +5 -1
  53. zrb/input/base_input.py +18 -6
  54. zrb/input/option_input.py +13 -1
  55. zrb/input/text_input.py +8 -25
  56. zrb/runner/cli.py +25 -23
  57. zrb/runner/common_util.py +24 -19
  58. zrb/runner/web_app.py +3 -3
  59. zrb/runner/web_route/docs_route.py +1 -1
  60. zrb/runner/web_route/error_page/serve_default_404.py +1 -1
  61. zrb/runner/web_route/error_page/show_error_page.py +1 -1
  62. zrb/runner/web_route/home_page/home_page_route.py +2 -2
  63. zrb/runner/web_route/login_api_route.py +1 -1
  64. zrb/runner/web_route/login_page/login_page_route.py +2 -2
  65. zrb/runner/web_route/logout_api_route.py +1 -1
  66. zrb/runner/web_route/logout_page/logout_page_route.py +2 -2
  67. zrb/runner/web_route/node_page/group/show_group_page.py +1 -1
  68. zrb/runner/web_route/node_page/node_page_route.py +1 -1
  69. zrb/runner/web_route/node_page/task/show_task_page.py +1 -1
  70. zrb/runner/web_route/refresh_token_api_route.py +1 -1
  71. zrb/runner/web_route/static/static_route.py +1 -1
  72. zrb/runner/web_route/task_input_api_route.py +6 -6
  73. zrb/runner/web_route/task_session_api_route.py +20 -12
  74. zrb/runner/web_util/cookie.py +1 -1
  75. zrb/runner/web_util/token.py +1 -1
  76. zrb/runner/web_util/user.py +8 -4
  77. zrb/session/any_session.py +24 -17
  78. zrb/session/session.py +50 -25
  79. zrb/session_state_logger/any_session_state_logger.py +9 -4
  80. zrb/session_state_logger/file_session_state_logger.py +16 -6
  81. zrb/session_state_logger/session_state_logger_factory.py +1 -1
  82. zrb/task/any_task.py +30 -9
  83. zrb/task/base/context.py +17 -9
  84. zrb/task/base/execution.py +15 -8
  85. zrb/task/base/lifecycle.py +8 -4
  86. zrb/task/base/monitoring.py +12 -7
  87. zrb/task/base_task.py +69 -5
  88. zrb/task/base_trigger.py +12 -5
  89. zrb/task/cmd_task.py +1 -1
  90. zrb/task/llm/agent.py +154 -161
  91. zrb/task/llm/agent_runner.py +152 -0
  92. zrb/task/llm/config.py +47 -18
  93. zrb/task/llm/conversation_history.py +209 -0
  94. zrb/task/llm/conversation_history_model.py +67 -0
  95. zrb/task/llm/default_workflow/coding/workflow.md +41 -0
  96. zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
  97. zrb/task/llm/default_workflow/git/workflow.md +118 -0
  98. zrb/task/llm/default_workflow/golang/workflow.md +128 -0
  99. zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
  100. zrb/task/llm/default_workflow/java/workflow.md +146 -0
  101. zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
  102. zrb/task/llm/default_workflow/python/workflow.md +160 -0
  103. zrb/task/llm/default_workflow/researching/workflow.md +153 -0
  104. zrb/task/llm/default_workflow/rust/workflow.md +162 -0
  105. zrb/task/llm/default_workflow/shell/workflow.md +299 -0
  106. zrb/task/llm/error.py +24 -10
  107. zrb/task/llm/file_replacement.py +206 -0
  108. zrb/task/llm/file_tool_model.py +57 -0
  109. zrb/task/llm/history_processor.py +206 -0
  110. zrb/task/llm/history_summarization.py +11 -166
  111. zrb/task/llm/print_node.py +193 -69
  112. zrb/task/llm/prompt.py +242 -45
  113. zrb/task/llm/subagent_conversation_history.py +41 -0
  114. zrb/task/llm/tool_wrapper.py +260 -57
  115. zrb/task/llm/workflow.py +76 -0
  116. zrb/task/llm_task.py +182 -171
  117. zrb/task/make_task.py +2 -3
  118. zrb/task/rsync_task.py +26 -11
  119. zrb/task/scheduler.py +4 -4
  120. zrb/util/attr.py +54 -39
  121. zrb/util/callable.py +23 -0
  122. zrb/util/cli/markdown.py +12 -0
  123. zrb/util/cli/text.py +30 -0
  124. zrb/util/file.py +29 -11
  125. zrb/util/git.py +8 -11
  126. zrb/util/git_diff_model.py +10 -0
  127. zrb/util/git_subtree.py +9 -14
  128. zrb/util/git_subtree_model.py +32 -0
  129. zrb/util/init_path.py +1 -1
  130. zrb/util/markdown.py +62 -0
  131. zrb/util/string/conversion.py +2 -2
  132. zrb/util/todo.py +17 -50
  133. zrb/util/todo_model.py +46 -0
  134. zrb/util/truncate.py +23 -0
  135. zrb/util/yaml.py +204 -0
  136. zrb/xcom/xcom.py +10 -0
  137. zrb-1.21.29.dist-info/METADATA +270 -0
  138. {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/RECORD +140 -98
  139. {zrb-1.8.10.dist-info → zrb-1.21.29.dist-info}/WHEEL +1 -1
  140. zrb/config.py +0 -335
  141. zrb/llm_config.py +0 -411
  142. zrb/llm_rate_limitter.py +0 -125
  143. zrb/task/llm/context.py +0 -102
  144. zrb/task/llm/context_enrichment.py +0 -199
  145. zrb/task/llm/history.py +0 -211
  146. zrb-1.8.10.dist-info/METADATA +0 -264
  147. {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
@@ -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.style import stylize_bold_yellow, stylize_faint
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
- _show_info(ctx)
28
- final_result = ""
29
- ctx.print(stylize_faint("šŸ§‘ >> ") + f"{ctx.input.message}", plain=True)
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
- user_inputs = []
43
- while True:
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
- ctx.print(stylize_faint("šŸ§‘ >> "), end="", plain=True)
46
- user_input = input()
47
- # Handle special input
48
- if user_input.strip().lower() in ("/bye", "/quit"):
49
- user_prompt = "\n".join(user_inputs)
50
- user_inputs = []
51
- result = await _trigger_ask_and_wait_for_result(ctx, user_prompt)
52
- if result is not None:
53
- final_result = result
54
- break
55
- elif user_input.strip().lower() in ("/multi"):
56
- multiline_mode = True
57
- elif user_input.strip().lower() in ("/end"):
58
- multiline_mode = False
59
- user_prompt = "\n".join(user_inputs)
60
- user_inputs = []
61
- result = await _trigger_ask_and_wait_for_result(ctx, user_prompt)
62
- if result is not None:
63
- final_result = result
64
- elif user_input.strip().lower() in ("/help", "/info"):
65
- _show_info(ctx)
66
- continue
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
- user_inputs.append(user_input)
69
- if multiline_mode:
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
- user_prompt = "\n".join(user_inputs)
72
- user_inputs = []
73
- result = await _trigger_ask_and_wait_for_result(ctx, user_prompt)
74
- if result is not None:
75
- final_result = result
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(ctx, user_prompt, previous_session_name, start_new)
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
- ctx.print(f"{stylize_faint('šŸ¤– >>')} {result}", plain=True)
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": callback_ctx.input.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
- )