zrb 1.15.3__py3-none-any.whl → 2.0.0a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of zrb might be problematic. Click here for more details.

Files changed (204) hide show
  1. zrb/__init__.py +118 -133
  2. zrb/attr/type.py +10 -7
  3. zrb/builtin/__init__.py +55 -1
  4. zrb/builtin/git.py +12 -1
  5. zrb/builtin/group.py +31 -15
  6. zrb/builtin/llm/chat.py +147 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  9. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  10. zrb/builtin/searxng/config/settings.yml +5671 -0
  11. zrb/builtin/searxng/start.py +21 -0
  12. zrb/builtin/shell/autocomplete/bash.py +4 -3
  13. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  14. zrb/callback/callback.py +8 -1
  15. zrb/cmd/cmd_result.py +2 -1
  16. zrb/config/config.py +555 -169
  17. zrb/config/helper.py +84 -0
  18. zrb/config/web_auth_config.py +50 -35
  19. zrb/context/any_shared_context.py +20 -3
  20. zrb/context/context.py +39 -5
  21. zrb/context/print_fn.py +13 -0
  22. zrb/context/shared_context.py +17 -8
  23. zrb/group/any_group.py +3 -3
  24. zrb/group/group.py +3 -3
  25. zrb/input/any_input.py +5 -1
  26. zrb/input/base_input.py +18 -6
  27. zrb/input/option_input.py +41 -1
  28. zrb/input/text_input.py +7 -24
  29. zrb/llm/agent/__init__.py +9 -0
  30. zrb/llm/agent/agent.py +215 -0
  31. zrb/llm/agent/summarizer.py +20 -0
  32. zrb/llm/app/__init__.py +10 -0
  33. zrb/llm/app/completion.py +281 -0
  34. zrb/llm/app/confirmation/allow_tool.py +66 -0
  35. zrb/llm/app/confirmation/handler.py +178 -0
  36. zrb/llm/app/confirmation/replace_confirmation.py +77 -0
  37. zrb/llm/app/keybinding.py +34 -0
  38. zrb/llm/app/layout.py +117 -0
  39. zrb/llm/app/lexer.py +155 -0
  40. zrb/llm/app/redirection.py +28 -0
  41. zrb/llm/app/style.py +16 -0
  42. zrb/llm/app/ui.py +733 -0
  43. zrb/llm/config/__init__.py +4 -0
  44. zrb/llm/config/config.py +122 -0
  45. zrb/llm/config/limiter.py +247 -0
  46. zrb/llm/history_manager/__init__.py +4 -0
  47. zrb/llm/history_manager/any_history_manager.py +23 -0
  48. zrb/llm/history_manager/file_history_manager.py +91 -0
  49. zrb/llm/history_processor/summarizer.py +108 -0
  50. zrb/llm/note/__init__.py +3 -0
  51. zrb/llm/note/manager.py +122 -0
  52. zrb/llm/prompt/__init__.py +29 -0
  53. zrb/llm/prompt/claude_compatibility.py +92 -0
  54. zrb/llm/prompt/compose.py +55 -0
  55. zrb/llm/prompt/default.py +51 -0
  56. zrb/llm/prompt/markdown/file_extractor.md +112 -0
  57. zrb/llm/prompt/markdown/mandate.md +23 -0
  58. zrb/llm/prompt/markdown/persona.md +3 -0
  59. zrb/llm/prompt/markdown/repo_extractor.md +112 -0
  60. zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
  61. zrb/llm/prompt/markdown/summarizer.md +21 -0
  62. zrb/llm/prompt/note.py +41 -0
  63. zrb/llm/prompt/system_context.py +46 -0
  64. zrb/llm/prompt/zrb.py +41 -0
  65. zrb/llm/skill/__init__.py +3 -0
  66. zrb/llm/skill/manager.py +86 -0
  67. zrb/llm/task/__init__.py +4 -0
  68. zrb/llm/task/llm_chat_task.py +316 -0
  69. zrb/llm/task/llm_task.py +245 -0
  70. zrb/llm/tool/__init__.py +39 -0
  71. zrb/llm/tool/bash.py +75 -0
  72. zrb/llm/tool/code.py +266 -0
  73. zrb/llm/tool/file.py +419 -0
  74. zrb/llm/tool/note.py +70 -0
  75. zrb/{builtin/llm → llm}/tool/rag.py +33 -37
  76. zrb/llm/tool/search/brave.py +53 -0
  77. zrb/llm/tool/search/searxng.py +47 -0
  78. zrb/llm/tool/search/serpapi.py +47 -0
  79. zrb/llm/tool/skill.py +19 -0
  80. zrb/llm/tool/sub_agent.py +70 -0
  81. zrb/llm/tool/web.py +97 -0
  82. zrb/llm/tool/zrb_task.py +66 -0
  83. zrb/llm/util/attachment.py +101 -0
  84. zrb/llm/util/prompt.py +104 -0
  85. zrb/llm/util/stream_response.py +178 -0
  86. zrb/runner/cli.py +21 -20
  87. zrb/runner/common_util.py +24 -19
  88. zrb/runner/web_route/task_input_api_route.py +5 -5
  89. zrb/runner/web_util/user.py +7 -3
  90. zrb/session/any_session.py +12 -9
  91. zrb/session/session.py +38 -17
  92. zrb/task/any_task.py +24 -3
  93. zrb/task/base/context.py +42 -22
  94. zrb/task/base/execution.py +67 -55
  95. zrb/task/base/lifecycle.py +14 -7
  96. zrb/task/base/monitoring.py +12 -7
  97. zrb/task/base_task.py +113 -50
  98. zrb/task/base_trigger.py +16 -6
  99. zrb/task/cmd_task.py +6 -0
  100. zrb/task/http_check.py +11 -5
  101. zrb/task/make_task.py +5 -3
  102. zrb/task/rsync_task.py +30 -10
  103. zrb/task/scaffolder.py +7 -4
  104. zrb/task/scheduler.py +7 -4
  105. zrb/task/tcp_check.py +6 -4
  106. zrb/util/ascii_art/art/bee.txt +17 -0
  107. zrb/util/ascii_art/art/cat.txt +9 -0
  108. zrb/util/ascii_art/art/ghost.txt +16 -0
  109. zrb/util/ascii_art/art/panda.txt +17 -0
  110. zrb/util/ascii_art/art/rose.txt +14 -0
  111. zrb/util/ascii_art/art/unicorn.txt +15 -0
  112. zrb/util/ascii_art/banner.py +92 -0
  113. zrb/util/attr.py +54 -39
  114. zrb/util/cli/markdown.py +32 -0
  115. zrb/util/cli/text.py +30 -0
  116. zrb/util/cmd/command.py +33 -10
  117. zrb/util/file.py +61 -33
  118. zrb/util/git.py +2 -2
  119. zrb/util/{llm/prompt.py → markdown.py} +2 -3
  120. zrb/util/match.py +78 -0
  121. zrb/util/run.py +3 -3
  122. zrb/util/string/conversion.py +1 -1
  123. zrb/util/truncate.py +23 -0
  124. zrb/util/yaml.py +204 -0
  125. zrb/xcom/xcom.py +10 -0
  126. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
  127. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
  128. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
  129. zrb/attr/__init__.py +0 -0
  130. zrb/builtin/llm/chat_session.py +0 -311
  131. zrb/builtin/llm/history.py +0 -71
  132. zrb/builtin/llm/input.py +0 -27
  133. zrb/builtin/llm/llm_ask.py +0 -187
  134. zrb/builtin/llm/previous-session.js +0 -21
  135. zrb/builtin/llm/tool/__init__.py +0 -0
  136. zrb/builtin/llm/tool/api.py +0 -71
  137. zrb/builtin/llm/tool/cli.py +0 -38
  138. zrb/builtin/llm/tool/code.py +0 -254
  139. zrb/builtin/llm/tool/file.py +0 -626
  140. zrb/builtin/llm/tool/sub_agent.py +0 -137
  141. zrb/builtin/llm/tool/web.py +0 -195
  142. zrb/builtin/project/__init__.py +0 -0
  143. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
  144. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
  145. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
  146. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
  147. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
  148. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
  149. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
  150. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
  151. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
  152. zrb/builtin/project/create/__init__.py +0 -0
  153. zrb/builtin/shell/__init__.py +0 -0
  154. zrb/builtin/shell/autocomplete/__init__.py +0 -0
  155. zrb/callback/__init__.py +0 -0
  156. zrb/cmd/__init__.py +0 -0
  157. zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
  158. zrb/config/default_prompt/interactive_system_prompt.md +0 -35
  159. zrb/config/default_prompt/persona.md +0 -1
  160. zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
  161. zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
  162. zrb/config/default_prompt/summarization_prompt.md +0 -16
  163. zrb/config/default_prompt/system_prompt.md +0 -32
  164. zrb/config/llm_config.py +0 -243
  165. zrb/config/llm_context/config.py +0 -129
  166. zrb/config/llm_context/config_parser.py +0 -46
  167. zrb/config/llm_rate_limitter.py +0 -137
  168. zrb/content_transformer/__init__.py +0 -0
  169. zrb/context/__init__.py +0 -0
  170. zrb/dot_dict/__init__.py +0 -0
  171. zrb/env/__init__.py +0 -0
  172. zrb/group/__init__.py +0 -0
  173. zrb/input/__init__.py +0 -0
  174. zrb/runner/__init__.py +0 -0
  175. zrb/runner/web_route/__init__.py +0 -0
  176. zrb/runner/web_route/home_page/__init__.py +0 -0
  177. zrb/session/__init__.py +0 -0
  178. zrb/session_state_log/__init__.py +0 -0
  179. zrb/session_state_logger/__init__.py +0 -0
  180. zrb/task/__init__.py +0 -0
  181. zrb/task/base/__init__.py +0 -0
  182. zrb/task/llm/__init__.py +0 -0
  183. zrb/task/llm/agent.py +0 -243
  184. zrb/task/llm/config.py +0 -103
  185. zrb/task/llm/conversation_history.py +0 -128
  186. zrb/task/llm/conversation_history_model.py +0 -242
  187. zrb/task/llm/default_workflow/coding.md +0 -24
  188. zrb/task/llm/default_workflow/copywriting.md +0 -17
  189. zrb/task/llm/default_workflow/researching.md +0 -18
  190. zrb/task/llm/error.py +0 -95
  191. zrb/task/llm/history_summarization.py +0 -216
  192. zrb/task/llm/print_node.py +0 -101
  193. zrb/task/llm/prompt.py +0 -325
  194. zrb/task/llm/tool_wrapper.py +0 -220
  195. zrb/task/llm/typing.py +0 -3
  196. zrb/task/llm_task.py +0 -341
  197. zrb/task_status/__init__.py +0 -0
  198. zrb/util/__init__.py +0 -0
  199. zrb/util/cli/__init__.py +0 -0
  200. zrb/util/cmd/__init__.py +0 -0
  201. zrb/util/codemod/__init__.py +0 -0
  202. zrb/util/string/__init__.py +0 -0
  203. zrb/xcom/__init__.py +0 -0
  204. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
zrb/config/helper.py ADDED
@@ -0,0 +1,84 @@
1
+ import logging
2
+ import os
3
+ import platform
4
+
5
+
6
+ def get_env(env_name: str | list[str], default: str = "", prefix: str = "ZRB") -> str:
7
+ env_name_list = env_name if isinstance(env_name, list) else [env_name]
8
+ for name in env_name_list:
9
+ value = os.getenv(f"{prefix}_{name}", None)
10
+ if value is not None:
11
+ return value
12
+ return default
13
+
14
+
15
+ def get_current_shell() -> str:
16
+ if platform.system() == "Windows":
17
+ return "PowerShell"
18
+ current_shell = os.getenv("SHELL", "")
19
+ if current_shell.endswith("zsh"):
20
+ return "zsh"
21
+ return "bash"
22
+
23
+
24
+ def get_default_diff_edit_command(editor: str) -> str:
25
+ if editor in [
26
+ "code",
27
+ "vscode",
28
+ "vscodium",
29
+ "windsurf",
30
+ "cursor",
31
+ "zed",
32
+ "zeditor",
33
+ "agy",
34
+ ]:
35
+ return f"{editor} --wait --diff {{old}} {{new}}"
36
+ if editor == "emacs":
37
+ return 'emacs --eval \'(ediff-files "{old}" "{new}")\''
38
+ if editor in ["nvim", "vim"]:
39
+ return (
40
+ f"{editor} -d {{old}} {{new}} "
41
+ "-i NONE "
42
+ '-c "wincmd h | set readonly | wincmd l" '
43
+ '-c "highlight DiffAdd cterm=bold ctermbg=22 guibg=#005f00 | highlight DiffChange cterm=bold ctermbg=24 guibg=#005f87 | highlight DiffText ctermbg=21 guibg=#0000af | highlight DiffDelete ctermbg=52 guibg=#5f0000" ' # noqa
44
+ '-c "set showtabline=2 | set tabline=[Instructions]\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)" ' # noqa
45
+ '-c "wincmd h | setlocal statusline=OLD\\ FILE" '
46
+ '-c "wincmd l | setlocal statusline=%#StatusBold#NEW\\ FILE\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)" ' # noqa
47
+ '-c "autocmd BufWritePost * wqa"'
48
+ )
49
+ return 'vimdiff {old} {new} +"setlocal ro" +"wincmd l" +"autocmd BufWritePost <buffer> qa"' # noqa
50
+
51
+
52
+ def get_log_level(level: str) -> int:
53
+ level = level.upper()
54
+ log_levels = {
55
+ "CRITICAL": logging.CRITICAL, # 50
56
+ "FATAL": logging.CRITICAL, # 50
57
+ "ERROR": logging.ERROR, # 40
58
+ "WARN": logging.WARNING, # 30
59
+ "WARNING": logging.WARNING, # 30
60
+ "INFO": logging.INFO, # 20
61
+ "DEBUG": logging.DEBUG, # 10
62
+ "NOTSET": logging.NOTSET, # 0
63
+ }
64
+ if level in log_levels:
65
+ return log_levels[level]
66
+ return logging.WARNING
67
+
68
+
69
+ def get_max_token_threshold(
70
+ factor: float, max_tokens_per_minute: int, max_tokens_per_request: int
71
+ ) -> int:
72
+ return round(factor * min(max_tokens_per_minute, max_tokens_per_request))
73
+
74
+
75
+ def limit_token_threshold(
76
+ threshold: int,
77
+ factor: float,
78
+ max_tokens_per_minute: int,
79
+ max_tokens_per_request: int,
80
+ ) -> int:
81
+ return min(
82
+ threshold,
83
+ get_max_token_threshold(factor, max_tokens_per_minute, max_tokens_per_request),
84
+ )
@@ -41,58 +41,108 @@ class WebAuthConfig:
41
41
  return self._secret_key
42
42
  return CFG.WEB_SECRET_KEY
43
43
 
44
+ @secret_key.setter
45
+ def secret_key(self, secret_key: str):
46
+ self._secret_key = secret_key
47
+
44
48
  @property
45
49
  def access_token_expire_minutes(self) -> int:
46
50
  if self._access_token_expire_minutes is not None:
47
51
  return self._access_token_expire_minutes
48
52
  return CFG.WEB_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES
49
53
 
54
+ @access_token_expire_minutes.setter
55
+ def access_token_expire_minutes(self, minutes: int):
56
+ self._access_token_expire_minutes = minutes
57
+
50
58
  @property
51
59
  def refresh_token_expire_minutes(self) -> int:
52
60
  if self._refresh_token_expire_minutes is not None:
53
61
  return self._refresh_token_expire_minutes
54
62
  return CFG.WEB_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES
55
63
 
64
+ @refresh_token_expire_minutes.setter
65
+ def refresh_token_expire_minutes(self, minutes: int):
66
+ self._refresh_token_expire_minutes = minutes
67
+
56
68
  @property
57
69
  def access_token_cookie_name(self) -> str:
58
70
  if self._access_token_cookie_name is not None:
59
71
  return self._access_token_cookie_name
60
72
  return CFG.WEB_ACCESS_TOKEN_COOKIE_NAME
61
73
 
74
+ @access_token_cookie_name.setter
75
+ def access_token_cookie_name(self, name: str):
76
+ self._access_token_cookie_name = name
77
+
62
78
  @property
63
79
  def refresh_token_cookie_name(self) -> str:
64
80
  if self._refresh_token_cookie_name is not None:
65
81
  return self._refresh_token_cookie_name
66
82
  return CFG.WEB_REFRESH_TOKEN_COOKIE_NAME
67
83
 
84
+ @refresh_token_cookie_name.setter
85
+ def refresh_token_cookie_name(self, name: str):
86
+ self._refresh_token_cookie_name = name
87
+
68
88
  @property
69
89
  def enable_auth(self) -> bool:
70
90
  if self._enable_auth is not None:
71
91
  return self._enable_auth
72
92
  return CFG.WEB_ENABLE_AUTH
73
93
 
94
+ @enable_auth.setter
95
+ def enable_auth(self, enable: bool):
96
+ self._enable_auth = enable
97
+
74
98
  @property
75
99
  def super_admin_username(self) -> str:
76
100
  if self._super_admin_username is not None:
77
101
  return self._super_admin_username
78
102
  return CFG.WEB_SUPER_ADMIN_USERNAME
79
103
 
104
+ @super_admin_username.setter
105
+ def super_admin_username(self, username: str):
106
+ self._super_admin_username = username
107
+
80
108
  @property
81
109
  def super_admin_password(self) -> str:
82
110
  if self._super_admin_password is not None:
83
111
  return self._super_admin_password
84
112
  return CFG.WEB_SUPER_ADMIN_PASSWORD
85
113
 
114
+ @super_admin_password.setter
115
+ def super_admin_password(self, password: str):
116
+ self._super_admin_password = password
117
+
86
118
  @property
87
119
  def guest_username(self) -> str:
88
120
  if self._guest_username is not None:
89
121
  return self._guest_username
90
122
  return CFG.WEB_GUEST_USERNAME
91
123
 
124
+ @guest_username.setter
125
+ def guest_username(self, username: str):
126
+ self._guest_username = username
127
+
92
128
  @property
93
129
  def guest_accessible_tasks(self) -> list[AnyTask | str]:
94
130
  return self._guest_accessible_tasks
95
131
 
132
+ @guest_accessible_tasks.setter
133
+ def guest_accessible_tasks(self, tasks: list[AnyTask | str]):
134
+ self._guest_accessible_tasks = tasks
135
+
136
+ @property
137
+ def find_user_by_username_callback(self) -> Callable[[str], "User | None"] | None:
138
+ return self._find_user_by_username
139
+
140
+ @find_user_by_username_callback.setter
141
+ def find_user_by_username_callback(
142
+ self, find_user_by_username: Callable[[str], "User | None"]
143
+ ):
144
+ self._find_user_by_username = find_user_by_username
145
+
96
146
  @property
97
147
  def default_user(self) -> "User":
98
148
  from zrb.runner.web_schema.user import User
@@ -127,41 +177,6 @@ class WebAuthConfig:
127
177
  return [self.default_user]
128
178
  return self._user_list + [self.super_admin, self.default_user]
129
179
 
130
- def set_secret_key(self, secret_key: str):
131
- self._secret_key = secret_key
132
-
133
- def set_access_token_expire_minutes(self, minutes: int):
134
- self._access_token_expire_minutes = minutes
135
-
136
- def set_refresh_token_expire_minutes(self, minutes: int):
137
- self._refresh_token_expire_minutes = minutes
138
-
139
- def set_access_token_cookie_name(self, name: str):
140
- self._access_token_cookie_name = name
141
-
142
- def set_refresh_token_cookie_name(self, name: str):
143
- self._refresh_token_cookie_name = name
144
-
145
- def set_enable_auth(self, enable: bool):
146
- self._enable_auth = enable
147
-
148
- def set_super_admin_username(self, username: str):
149
- self._super_admin_username = username
150
-
151
- def set_super_admin_password(self, password: str):
152
- self._super_admin_password = password
153
-
154
- def set_guest_username(self, username: str):
155
- self._guest_username = username
156
-
157
- def set_guest_accessible_tasks(self, tasks: list[AnyTask | str]):
158
- self._guest_accessible_tasks = tasks
159
-
160
- def set_find_user_by_username(
161
- self, find_user_by_username: Callable[[str], "User | None"]
162
- ):
163
- self._find_user_by_username = find_user_by_username
164
-
165
180
  def append_user(self, user: "User"):
166
181
  duplicates = [
167
182
  existing_user
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations # Enables forward references
2
2
 
3
+ import sys
3
4
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, TextIO
5
6
 
6
7
  from zrb.dot_dict.dot_dict import DotDict
7
- from zrb.xcom.xcom import Xcom
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from zrb.session import any_session
@@ -29,26 +29,32 @@ class AnySharedContext(ABC):
29
29
  pass
30
30
 
31
31
  @property
32
+ @abstractmethod
32
33
  def input(self) -> DotDict:
33
34
  pass
34
35
 
35
36
  @property
37
+ @abstractmethod
36
38
  def env(self) -> DotDict:
37
39
  pass
38
40
 
39
41
  @property
42
+ @abstractmethod
40
43
  def args(self) -> list[Any]:
41
44
  pass
42
45
 
43
46
  @property
44
- def xcom(self) -> DotDict[str, Xcom]:
47
+ @abstractmethod
48
+ def xcom(self) -> DotDict:
45
49
  pass
46
50
 
47
51
  @property
52
+ @abstractmethod
48
53
  def shared_log(self) -> list[str]:
49
54
  pass
50
55
 
51
56
  @property
57
+ @abstractmethod
52
58
  def session(self) -> any_session.AnySession | None:
53
59
  pass
54
60
 
@@ -81,3 +87,14 @@ class AnySharedContext(ABC):
81
87
  str: The rendered template as a string.
82
88
  """
83
89
  pass
90
+
91
+ @abstractmethod
92
+ def shared_print(
93
+ self,
94
+ *values: object,
95
+ sep: str = " ",
96
+ end: str = "\n",
97
+ file: TextIO | None = sys.stderr,
98
+ flush: bool = True,
99
+ ):
100
+ pass
zrb/context/context.py CHANGED
@@ -63,7 +63,7 @@ class Context(AnyContext):
63
63
 
64
64
  @property
65
65
  def session(self) -> AnySession | None:
66
- return self._shared_ctx._session
66
+ return self._shared_ctx.session
67
67
 
68
68
  def update_task_env(self, task_env: dict[str, str]):
69
69
  self._env.update(task_env)
@@ -101,6 +101,18 @@ class Context(AnyContext):
101
101
  return template
102
102
  return float(self.render(template))
103
103
 
104
+ def shared_print(
105
+ self,
106
+ *values: object,
107
+ sep: str = " ",
108
+ end: str = "\n",
109
+ file: TextIO | None = sys.stderr,
110
+ flush: bool = True,
111
+ ):
112
+ return self._shared_ctx.shared_print(
113
+ *values, sep=sep, end=end, file=file, flush=flush
114
+ )
115
+
104
116
  def print(
105
117
  self,
106
118
  *values: object,
@@ -110,16 +122,25 @@ class Context(AnyContext):
110
122
  flush: bool = True,
111
123
  plain: bool = False,
112
124
  ):
113
- sep = " " if sep is None else sep
125
+ if sep is None:
126
+ sep = " "
127
+ if end is None:
128
+ end = "\n"
114
129
  message = sep.join([f"{value}" for value in values])
115
130
  if plain:
116
131
  # self.append_to_shared_log(remove_style(message))
117
- print(message, sep=sep, end=end, file=file, flush=flush)
132
+ self.shared_print(message, sep=sep, end=end, file=file, flush=flush)
118
133
  self.append_to_shared_log(remove_style(f"{message}{end}"))
119
134
  return
120
135
  color = self._color
121
136
  icon = self._icon
122
- max_name_length = max(len(name) + len(icon) for name in self.session.task_names)
137
+ # Handle case where session is None (e.g., in tests)
138
+ if self.session is None:
139
+ max_name_length = len(self._task_name) + len(icon)
140
+ else:
141
+ max_name_length = max(
142
+ len(name) + len(icon) for name in self.session.task_names
143
+ )
123
144
  styled_task_name = f"{icon} {self._task_name}"
124
145
  padded_styled_task_name = styled_task_name.rjust(max_name_length + 1)
125
146
  if self._attempt == 0:
@@ -131,7 +152,20 @@ class Context(AnyContext):
131
152
  prefix = f"{formatted_time}{attempt_status} {padded_styled_task_name} ⬤ "
132
153
  self.append_to_shared_log(remove_style(f"{prefix} {message}{end}"))
133
154
  stylized_prefix = stylize(prefix, color=color)
134
- print(f"{stylized_prefix} {message}", sep=sep, end=end, file=file, flush=flush)
155
+ self.shared_print(
156
+ f"{stylized_prefix} {message}", sep=sep, end=end, file=file, flush=flush
157
+ )
158
+
159
+ def print_err(
160
+ self,
161
+ *values: object,
162
+ sep: str | None = " ",
163
+ end: str | None = "\n",
164
+ file: TextIO | None = sys.stderr,
165
+ flush: bool = True,
166
+ plain: bool = False,
167
+ ):
168
+ self.print(*values, sep=sep, end=end, file=file, flush=flush, plain=plain)
135
169
 
136
170
  def log_debug(
137
171
  self,
@@ -0,0 +1,13 @@
1
+ from typing import Protocol, TextIO
2
+
3
+
4
+ class PrintFn(Protocol):
5
+ def __call__(
6
+ self,
7
+ *values: object,
8
+ sep: str = " ",
9
+ end: str = "\n",
10
+ file: TextIO | None = None,
11
+ flush: bool = False,
12
+ ) -> None:
13
+ pass
@@ -1,9 +1,10 @@
1
1
  import datetime
2
2
  import sys
3
- from typing import Any
3
+ from typing import Any, TextIO
4
4
 
5
5
  from zrb.config.config import CFG
6
6
  from zrb.context.any_shared_context import AnySharedContext
7
+ from zrb.context.print_fn import PrintFn
7
8
  from zrb.dot_dict.dot_dict import DotDict
8
9
  from zrb.session.any_session import AnySession
9
10
  from zrb.util.string.conversion import (
@@ -28,6 +29,7 @@ class SharedContext(AnySharedContext):
28
29
  xcom: dict[str, Xcom] = {},
29
30
  logging_level: int | None = None,
30
31
  is_web_mode: bool = False,
32
+ print_fn: PrintFn | None = None,
31
33
  ):
32
34
  self.__logging_level = logging_level
33
35
  self._input = DotDict(input)
@@ -37,14 +39,11 @@ class SharedContext(AnySharedContext):
37
39
  self._session: AnySession | None = None
38
40
  self._log = []
39
41
  self._is_web_mode = is_web_mode
42
+ self._print_fn = print_fn if print_fn is not None else print
40
43
 
41
44
  def __repr__(self):
42
45
  class_name = self.__class__.__name__
43
- input = self._input
44
- args = self._args
45
- env = self._env
46
- xcom = self._xcom
47
- return f"<{class_name} input={input} args={args} xcom={xcom} env={env}>"
46
+ return f"<{class_name}>"
48
47
 
49
48
  @property
50
49
  def is_web_mode(self) -> bool:
@@ -70,7 +69,7 @@ class SharedContext(AnySharedContext):
70
69
  return self._args
71
70
 
72
71
  @property
73
- def xcom(self) -> DotDict[str, Xcom]:
72
+ def xcom(self) -> DotDict:
74
73
  return self._xcom
75
74
 
76
75
  @property
@@ -85,7 +84,7 @@ class SharedContext(AnySharedContext):
85
84
  self._log.append(message)
86
85
  session = self.session
87
86
  if session is not None:
88
- session_parent: AnySession = session.parent
87
+ session_parent: AnySession | None = session.parent
89
88
  if session_parent is not None:
90
89
  session_parent.shared_ctx.append_to_shared_log(message)
91
90
 
@@ -112,3 +111,13 @@ class SharedContext(AnySharedContext):
112
111
  "double_quote": double_quote,
113
112
  },
114
113
  )
114
+
115
+ def shared_print(
116
+ self,
117
+ *values: object,
118
+ sep: str = " ",
119
+ end: str = "\n",
120
+ file: TextIO | None = sys.stderr,
121
+ flush: bool = True,
122
+ ):
123
+ return self._print_fn(*values, sep=sep, end=end, file=file, flush=flush)
zrb/group/any_group.py CHANGED
@@ -35,11 +35,11 @@ class AnyGroup(ABC):
35
35
  pass
36
36
 
37
37
  @abstractmethod
38
- def add_group(self, group: "AnyGroup | str") -> "AnyGroup":
38
+ def add_group(self, group: "AnyGroup", alias: str | None = None) -> "AnyGroup":
39
39
  pass
40
40
 
41
41
  @abstractmethod
42
- def add_task(self, task: AnyTask, alias: str | None = None) -> AnyTask:
42
+ def add_task(self, task: "AnyTask", alias: str | None = None) -> "AnyTask":
43
43
  pass
44
44
 
45
45
  @abstractmethod
@@ -55,5 +55,5 @@ class AnyGroup(ABC):
55
55
  pass
56
56
 
57
57
  @abstractmethod
58
- def get_group_by_alias(self, name: str) -> "AnyGroup | None":
58
+ def get_group_by_alias(self, alias: str) -> "AnyGroup | None":
59
59
  pass
zrb/group/group.py CHANGED
@@ -33,15 +33,15 @@ class Group(AnyGroup):
33
33
  def subgroups(self) -> dict[str, AnyGroup]:
34
34
  names = list(self._groups.keys())
35
35
  names.sort()
36
- return {name: self._groups.get(name) for name in names}
36
+ return {name: self._groups[name] for name in names}
37
37
 
38
38
  @property
39
39
  def subtasks(self) -> dict[str, AnyTask]:
40
40
  alias = list(self._tasks.keys())
41
41
  alias.sort()
42
- return {name: self._tasks.get(name) for name in alias}
42
+ return {name: self._tasks[name] for name in alias}
43
43
 
44
- def add_group(self, group: AnyGroup | str, alias: str | None = None) -> AnyGroup:
44
+ def add_group(self, group: AnyGroup, alias: str | None = None) -> AnyGroup:
45
45
  real_group = Group(group) if isinstance(group, str) else group
46
46
  alias = alias if alias is not None else real_group.name
47
47
  self._groups[alias] = real_group
zrb/input/any_input.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
+ from typing import Any
2
3
 
3
4
  from zrb.context.any_shared_context import AnySharedContext
4
5
 
@@ -35,7 +36,10 @@ class AnyInput(ABC):
35
36
 
36
37
  @abstractmethod
37
38
  def update_shared_context(
38
- self, shared_ctx: AnySharedContext, str_value: str | None = None
39
+ self,
40
+ shared_ctx: AnySharedContext,
41
+ str_value: str | None = None,
42
+ value: Any = None,
39
43
  ):
40
44
  pass
41
45
 
zrb/input/base_input.py CHANGED
@@ -58,11 +58,15 @@ class BaseInput(AnyInput):
58
58
  return f'<input name="{name}" placeholder="{description}" value="{default}" />'
59
59
 
60
60
  def update_shared_context(
61
- self, shared_ctx: AnySharedContext, str_value: str | None = None
61
+ self,
62
+ shared_ctx: AnySharedContext,
63
+ str_value: str | None = None,
64
+ value: Any = None,
62
65
  ):
63
- if str_value is None:
64
- str_value = self.get_default_str(shared_ctx)
65
- value = self._parse_str_value(str_value)
66
+ if value is None:
67
+ if str_value is None:
68
+ str_value = self.get_default_str(shared_ctx)
69
+ value = self._parse_str_value(str_value)
66
70
  if self.name in shared_ctx.input:
67
71
  raise ValueError(f"Input already defined in the context: {self.name}")
68
72
  shared_ctx.input[self.name] = value
@@ -91,12 +95,20 @@ class BaseInput(AnyInput):
91
95
  default_str = self.get_default_str(shared_ctx)
92
96
  if default_str != "":
93
97
  prompt_message = f"{prompt_message} [{default_str}]"
94
- print(f"{prompt_message}: ", end="")
95
- value = input()
98
+ value = self._read_line(shared_ctx, prompt_message)
96
99
  if value.strip() == "":
97
100
  value = default_str
98
101
  return value
99
102
 
103
+ def _read_line(self, shared_ctx: AnySharedContext, prompt_message: str) -> str:
104
+ if not shared_ctx.is_tty:
105
+ print(f"{prompt_message}: ", end="")
106
+ return input()
107
+ from prompt_toolkit import PromptSession
108
+
109
+ reader = PromptSession()
110
+ return reader.prompt(f"{prompt_message}: ")
111
+
100
112
  def get_default_str(self, shared_ctx: AnySharedContext) -> str:
101
113
  """Get default value as str"""
102
114
  default_value = get_attr(
zrb/input/option_input.py CHANGED
@@ -1,7 +1,13 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  from zrb.attr.type import StrAttr, StrListAttr
2
4
  from zrb.context.any_shared_context import AnySharedContext
3
5
  from zrb.input.base_input import BaseInput
4
6
  from zrb.util.attr import get_str_list_attr
7
+ from zrb.util.match import fuzzy_match
8
+
9
+ if TYPE_CHECKING:
10
+ from prompt_toolkit.completion import Completer
5
11
 
6
12
 
7
13
  class OptionInput(BaseInput):
@@ -47,9 +53,43 @@ class OptionInput(BaseInput):
47
53
  option_str = ", ".join(options)
48
54
  if default_value != "":
49
55
  prompt_message = f"{prompt_message} ({option_str}) [{default_value}]"
50
- value = input(f"{prompt_message}: ")
56
+ value = self._get_value_from_user_input(shared_ctx, prompt_message, options)
51
57
  if value.strip() != "" and value.strip() not in options:
52
58
  value = self._prompt_cli_str(shared_ctx)
53
59
  if value.strip() == "":
54
60
  value = default_value
55
61
  return value
62
+
63
+ def _get_value_from_user_input(
64
+ self, shared_ctx: AnySharedContext, prompt_message: str, options: list[str]
65
+ ) -> str:
66
+ from prompt_toolkit import PromptSession
67
+
68
+ if shared_ctx.is_tty:
69
+ reader = PromptSession()
70
+ option_completer = self._get_option_completer(options)
71
+ return reader.prompt(f"{prompt_message}: ", completer=option_completer)
72
+ return input(f"{prompt_message}: ")
73
+
74
+ def _get_option_completer(self, options: list[str]) -> "Completer":
75
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
76
+ from prompt_toolkit.document import Document
77
+
78
+ class OptionCompleter(Completer):
79
+ def __init__(self, options: list[str]):
80
+ self._options = options
81
+
82
+ def get_completions(
83
+ self, document: Document, complete_event: CompleteEvent
84
+ ):
85
+ search_pattern = document.get_word_before_cursor(WORD=True)
86
+ candidates = []
87
+ for option in self._options:
88
+ matched, score = fuzzy_match(option, search_pattern)
89
+ if matched:
90
+ candidates.append((score, option))
91
+ candidates.sort(key=lambda x: (x[0], x[1]))
92
+ for _, option in candidates:
93
+ yield Completion(option, start_position=-len(search_pattern))
94
+
95
+ return OptionCompleter(options)
zrb/input/text_input.py CHANGED
@@ -1,12 +1,9 @@
1
- import os
2
- import subprocess
3
- import tempfile
4
1
  from collections.abc import Callable
5
2
 
6
3
  from zrb.config.config import CFG
7
4
  from zrb.context.any_shared_context import AnySharedContext
8
5
  from zrb.input.base_input import BaseInput
9
- from zrb.util.file import read_file
6
+ from zrb.util.cli.text import edit_text
10
7
 
11
8
 
12
9
  class TextInput(BaseInput):
@@ -85,24 +82,10 @@ class TextInput(BaseInput):
85
82
  comment_prompt_message = (
86
83
  f"{self.comment_start}{prompt_message}{self.comment_end}"
87
84
  )
88
- comment_prompt_message_eol = f"{comment_prompt_message}\n"
89
85
  default_value = self.get_default_str(shared_ctx)
90
- with tempfile.NamedTemporaryFile(
91
- delete=False, suffix=self._extension
92
- ) as temp_file:
93
- temp_file_name = temp_file.name
94
- temp_file.write(comment_prompt_message_eol.encode())
95
- # Pre-fill with default content
96
- if default_value:
97
- temp_file.write(default_value.encode())
98
- temp_file.flush()
99
- subprocess.call([self.editor_cmd, temp_file_name])
100
- # Read the edited content
101
- edited_content = read_file(temp_file_name)
102
- parts = [
103
- text.strip() for text in edited_content.split(comment_prompt_message, 1)
104
- ]
105
- edited_content = "\n".join(parts).lstrip()
106
- os.remove(temp_file_name)
107
- print(f"{prompt_message}: {edited_content}")
108
- return edited_content
86
+ return edit_text(
87
+ prompt_message=comment_prompt_message,
88
+ value=default_value,
89
+ editor=self.editor_cmd,
90
+ extension=self._extension,
91
+ )
@@ -0,0 +1,9 @@
1
+ from zrb.llm.agent.agent import create_agent, run_agent, tool_confirmation_var
2
+ from zrb.llm.agent.summarizer import create_summarizer_agent
3
+
4
+ __all__ = [
5
+ "create_agent",
6
+ "run_agent",
7
+ "tool_confirmation_var",
8
+ "create_summarizer_agent",
9
+ ]