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
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
@@ -87,3 +87,14 @@ class AnySharedContext(ABC):
87
87
  str: The rendered template as a string.
88
88
  """
89
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
@@ -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,11 +122,14 @@ 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
@@ -137,7 +152,20 @@ class Context(AnyContext):
137
152
  prefix = f"{formatted_time}{attempt_status} {padded_styled_task_name} ⬤ "
138
153
  self.append_to_shared_log(remove_style(f"{prefix} {message}{end}"))
139
154
  stylized_prefix = stylize(prefix, color=color)
140
- 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)
141
169
 
142
170
  def log_debug(
143
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,6 +39,7 @@ 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__
@@ -108,3 +111,13 @@ class SharedContext(AnySharedContext):
108
111
  "double_quote": double_quote,
109
112
  },
110
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/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):
@@ -58,10 +64,32 @@ class OptionInput(BaseInput):
58
64
  self, shared_ctx: AnySharedContext, prompt_message: str, options: list[str]
59
65
  ) -> str:
60
66
  from prompt_toolkit import PromptSession
61
- from prompt_toolkit.completion import WordCompleter
62
67
 
63
68
  if shared_ctx.is_tty:
64
69
  reader = PromptSession()
65
- option_completer = WordCompleter(options, ignore_case=True)
70
+ option_completer = self._get_option_completer(options)
66
71
  return reader.prompt(f"{prompt_message}: ", completer=option_completer)
67
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)
@@ -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
+ ]
zrb/llm/agent/agent.py ADDED
@@ -0,0 +1,215 @@
1
+ from contextvars import ContextVar
2
+ from typing import TYPE_CHECKING, Any, Callable
3
+
4
+ from zrb.llm.config.config import llm_config as default_llm_config
5
+ from zrb.llm.config.limiter import LLMLimiter
6
+ from zrb.llm.util.attachment import normalize_attachments
7
+ from zrb.llm.util.prompt import expand_prompt
8
+
9
+ # Context variable to propagate tool confirmation callback to sub-agents
10
+ tool_confirmation_var: ContextVar[Callable[[Any], Any] | None] = ContextVar(
11
+ "tool_confirmation", default=None
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from pydantic_ai import Agent, DeferredToolRequests, DeferredToolResults, Tool
16
+ from pydantic_ai._agent_graph import HistoryProcessor
17
+ from pydantic_ai.messages import UserPromptPart
18
+ from pydantic_ai.models import Model
19
+ from pydantic_ai.output import OutputDataT, OutputSpec
20
+ from pydantic_ai.settings import ModelSettings
21
+ from pydantic_ai.tools import ToolFuncEither
22
+ from pydantic_ai.toolsets import AbstractToolset
23
+
24
+
25
+ def create_agent(
26
+ model: "Model | str | None" = None,
27
+ system_prompt: str = "",
28
+ tools: list["Tool | ToolFuncEither"] = [],
29
+ toolsets: list["AbstractToolset[None]"] = [],
30
+ model_settings: "ModelSettings | None" = None,
31
+ history_processors: list["HistoryProcessor"] | None = None,
32
+ output_type: "OutputSpec[OutputDataT]" = str,
33
+ retries: int = 1,
34
+ yolo: bool = False,
35
+ ) -> "Agent[None, Any]":
36
+ from pydantic_ai import Agent, DeferredToolRequests
37
+ from pydantic_ai.toolsets import FunctionToolset
38
+
39
+ # Expand system prompt with references
40
+ effective_system_prompt = expand_prompt(system_prompt)
41
+
42
+ final_output_type = output_type
43
+ effective_toolsets = list(toolsets)
44
+ if tools:
45
+ effective_toolsets.append(FunctionToolset(tools=tools))
46
+
47
+ if not yolo:
48
+ final_output_type = output_type | DeferredToolRequests
49
+ effective_toolsets = [ts.approval_required() for ts in effective_toolsets]
50
+
51
+ if model is None:
52
+ model = default_llm_config.model
53
+
54
+ return Agent(
55
+ model=model,
56
+ output_type=final_output_type,
57
+ instructions=effective_system_prompt,
58
+ toolsets=effective_toolsets,
59
+ model_settings=model_settings,
60
+ history_processors=history_processors,
61
+ retries=retries,
62
+ )
63
+
64
+
65
+ async def run_agent(
66
+ agent: "Agent[None, Any]",
67
+ message: str | None,
68
+ message_history: list[Any],
69
+ limiter: LLMLimiter,
70
+ attachments: list[Any] | None = None,
71
+ print_fn: Callable[[str], Any] = print,
72
+ event_handler: Callable[[Any], Any] | None = None,
73
+ tool_confirmation: Callable[[Any], Any] | None = None,
74
+ ) -> tuple[Any, list[Any]]:
75
+ """
76
+ Runs the agent with rate limiting, history management, and optional CLI confirmation loop.
77
+ Returns (result_output, new_message_history).
78
+ """
79
+ import asyncio
80
+
81
+ from pydantic_ai import AgentRunResultEvent, DeferredToolRequests
82
+
83
+ # Resolve tool confirmation callback (Arg > Context > None)
84
+ effective_tool_confirmation = tool_confirmation
85
+ if effective_tool_confirmation is None:
86
+ effective_tool_confirmation = tool_confirmation_var.get()
87
+
88
+ # Set context var for sub-agents
89
+ token = tool_confirmation_var.set(effective_tool_confirmation)
90
+
91
+ try:
92
+ # Expand user message with references
93
+ effective_message = expand_prompt(message) if message else message
94
+
95
+ # Prepare Prompt Content
96
+ prompt_content = _get_prompt_content(effective_message, attachments, print_fn)
97
+
98
+ # 1. Prune & Throttle
99
+ current_history = await _acquire_rate_limit(
100
+ limiter, prompt_content, message_history, print_fn
101
+ )
102
+ current_message = prompt_content
103
+ current_results = None
104
+
105
+ # 2. Execution Loop
106
+ while True:
107
+ result_output = None
108
+ run_history = []
109
+
110
+ async for event in agent.run_stream_events(
111
+ current_message,
112
+ message_history=current_history,
113
+ deferred_tool_results=current_results,
114
+ ):
115
+ await asyncio.sleep(0)
116
+ if isinstance(event, AgentRunResultEvent):
117
+ result = event.result
118
+ result_output = result.output
119
+ run_history = result.all_messages()
120
+ if event_handler:
121
+ await event_handler(event)
122
+
123
+ # Handle Deferred Calls
124
+ if isinstance(result_output, DeferredToolRequests):
125
+ current_results = await _process_deferred_requests(
126
+ result_output, effective_tool_confirmation
127
+ )
128
+ if current_results is None:
129
+ return result_output, run_history
130
+ # Prepare next iteration
131
+ current_message = None
132
+ current_history = run_history
133
+ continue
134
+ return result_output, run_history
135
+ finally:
136
+ tool_confirmation_var.reset(token)
137
+
138
+
139
+ def _get_prompt_content(
140
+ message: str | None, attachments: list[Any] | None, print_fn: Callable[[str], Any]
141
+ ) -> "list[UserPromptPart] | str | None":
142
+ from pydantic_ai.messages import UserPromptPart
143
+
144
+ prompt_content = message
145
+ if attachments:
146
+ attachments = normalize_attachments(attachments, print_fn)
147
+ parts: list[UserPromptPart] = []
148
+ if message:
149
+ parts.append(UserPromptPart(content=message))
150
+ parts.extend(attachments)
151
+ prompt_content = parts
152
+ return prompt_content
153
+
154
+
155
+ async def _acquire_rate_limit(
156
+ limiter: LLMLimiter,
157
+ message: str | None,
158
+ message_history: list[Any],
159
+ print_fn: Callable[[str], Any],
160
+ ) -> list[Any]:
161
+ """Prunes history and waits if rate limits are exceeded."""
162
+ if not message:
163
+ return message_history
164
+
165
+ # Prune
166
+ pruned_history = limiter.fit_context_window(message_history, message)
167
+
168
+ # Throttle
169
+ est_tokens = limiter.count_tokens(pruned_history) + limiter.count_tokens(message)
170
+ await limiter.acquire(
171
+ est_tokens, notifier=lambda msg: print_fn(msg) if msg else None
172
+ )
173
+
174
+ return pruned_history
175
+
176
+
177
+ async def _process_deferred_requests(
178
+ result_output: "DeferredToolRequests",
179
+ effective_tool_confirmation: Callable[[Any], Any] | None,
180
+ ) -> "DeferredToolResults | None":
181
+ """Handles tool approvals/denials via callback or CLI fallback."""
182
+ import asyncio
183
+ import inspect
184
+
185
+ from pydantic_ai import DeferredToolResults, ToolApproved, ToolDenied
186
+
187
+ all_requests = (result_output.calls or []) + (result_output.approvals or [])
188
+ if not all_requests:
189
+ return None
190
+
191
+ current_results = DeferredToolResults()
192
+
193
+ for call in all_requests:
194
+ if effective_tool_confirmation:
195
+ res = effective_tool_confirmation(call)
196
+ if inspect.isawaitable(res):
197
+ result = await res
198
+ else:
199
+ result = res
200
+ current_results.approvals[call.tool_call_id] = result
201
+ else:
202
+ # CLI Fallback
203
+ prompt_text = f"Execute tool '{call.tool_name}' with args {call.args}?"
204
+ prompt_cli = f"\n[?] {prompt_text} (y/N) "
205
+
206
+ # We use asyncio.to_thread(input, ...) to avoid blocking the loop
207
+ user_input = await asyncio.to_thread(input, prompt_cli)
208
+ answer = user_input.strip().lower() in ("y", "yes")
209
+
210
+ if answer:
211
+ current_results.approvals[call.tool_call_id] = ToolApproved()
212
+ else:
213
+ current_results.approvals[call.tool_call_id] = ToolDenied("User denied")
214
+
215
+ return current_results
@@ -0,0 +1,20 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from zrb.llm.agent.agent import create_agent
4
+ from zrb.llm.prompt.default import get_summarizer_system_prompt
5
+
6
+ if TYPE_CHECKING:
7
+ from pydantic_ai import Agent
8
+ from pydantic_ai.models import Model
9
+
10
+
11
+ def create_summarizer_agent(
12
+ model: "str | None | Model" = None,
13
+ system_prompt: str | None = None,
14
+ ) -> "Agent[None, str]":
15
+ effective_system_prompt = system_prompt or get_summarizer_system_prompt()
16
+
17
+ return create_agent(
18
+ model=model,
19
+ system_prompt=effective_system_prompt,
20
+ )
@@ -0,0 +1,10 @@
1
+ from zrb.llm.app.confirmation.allow_tool import allow_tool_usage
2
+ from zrb.llm.app.confirmation.handler import ConfirmationMiddleware, last_confirmation
3
+ from zrb.llm.app.confirmation.replace_confirmation import replace_confirmation
4
+
5
+ __all__ = [
6
+ "allow_tool_usage",
7
+ "ConfirmationMiddleware",
8
+ "last_confirmation",
9
+ "replace_confirmation",
10
+ ]