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
@@ -0,0 +1,245 @@
1
+ from collections.abc import Callable
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ from zrb.attr.type import BoolAttr, StrAttr, fstring
5
+ from zrb.config.config import CFG
6
+ from zrb.context.any_context import AnyContext
7
+ from zrb.context.print_fn import PrintFn
8
+ from zrb.env.any_env import AnyEnv
9
+ from zrb.input.any_input import AnyInput
10
+ from zrb.llm.agent.agent import create_agent, run_agent
11
+ from zrb.llm.config.config import LLMConfig
12
+ from zrb.llm.config.config import llm_config as default_llm_config
13
+ from zrb.llm.config.limiter import LLMLimiter
14
+ from zrb.llm.config.limiter import llm_limiter as default_llm_limitter
15
+ from zrb.llm.history_manager.any_history_manager import AnyHistoryManager
16
+ from zrb.llm.history_manager.file_history_manager import FileHistoryManager
17
+ from zrb.llm.history_processor.summarizer import (
18
+ summarize_history,
19
+ )
20
+ from zrb.llm.prompt.compose import PromptManager
21
+ from zrb.llm.util.attachment import get_attachments
22
+ from zrb.llm.util.stream_response import (
23
+ create_event_handler,
24
+ create_faint_printer,
25
+ )
26
+ from zrb.task.any_task import AnyTask
27
+ from zrb.task.base_task import BaseTask
28
+ from zrb.util.attr import get_attr, get_bool_attr
29
+ from zrb.util.string.name import get_random_name
30
+
31
+ if TYPE_CHECKING:
32
+ from pydantic_ai import Tool, UserContent
33
+ from pydantic_ai._agent_graph import HistoryProcessor
34
+ from pydantic_ai.models import Model
35
+ from pydantic_ai.settings import ModelSettings
36
+ from pydantic_ai.tools import ToolFuncEither
37
+ from pydantic_ai.toolsets import AbstractToolset
38
+
39
+
40
+ class LLMTask(BaseTask):
41
+
42
+ def __init__(
43
+ self,
44
+ name: str,
45
+ color: int | None = None,
46
+ icon: str | None = None,
47
+ description: str | None = None,
48
+ cli_only: bool = False,
49
+ input: list[AnyInput | None] | AnyInput | None = None,
50
+ env: list[AnyEnv | None] | AnyEnv | None = None,
51
+ system_prompt: (
52
+ "Callable[[AnyContext], str | fstring | None] | str | None"
53
+ ) = None,
54
+ render_system_prompt: bool = False,
55
+ prompt_manager: PromptManager | None = None,
56
+ tools: list["Tool | ToolFuncEither"] = [],
57
+ toolsets: list["AbstractToolset[None]"] = [],
58
+ message: StrAttr | None = None,
59
+ render_message: bool = True,
60
+ attachment: "UserContent | list[UserContent] | Callable[[AnyContext], UserContent | list[UserContent]] | None" = None, # noqa
61
+ history_processors: list["HistoryProcessor"] = [],
62
+ llm_config: LLMConfig | None = None,
63
+ llm_limitter: LLMLimiter | None = None,
64
+ model: (
65
+ "Callable[[AnyContext], Model | str | fstring | None] | Model | None"
66
+ ) = None,
67
+ render_model: bool = True,
68
+ model_settings: (
69
+ "ModelSettings | Callable[[AnyContext], ModelSettings] | None"
70
+ ) = None,
71
+ conversation_name: StrAttr | None = None,
72
+ render_conversation_name: bool = True,
73
+ history_manager: AnyHistoryManager | None = None,
74
+ tool_confirmation: Callable[[Any], Any] | None = None,
75
+ yolo: BoolAttr = False,
76
+ summarize_command: list[str] = [],
77
+ execute_condition: bool | str | Callable[[AnyContext], bool] = True,
78
+ retries: int = 2,
79
+ retry_period: float = 0,
80
+ readiness_check: list[AnyTask] | AnyTask | None = None,
81
+ readiness_check_delay: float = 0.5,
82
+ readiness_check_period: float = 5,
83
+ readiness_failure_threshold: int = 1,
84
+ readiness_timeout: int = 60,
85
+ monitor_readiness: bool = False,
86
+ upstream: list[AnyTask] | AnyTask | None = None,
87
+ fallback: list[AnyTask] | AnyTask | None = None,
88
+ successor: list[AnyTask] | AnyTask | None = None,
89
+ print_fn: PrintFn | None = None,
90
+ ):
91
+ super().__init__(
92
+ name=name,
93
+ color=color,
94
+ icon=icon,
95
+ description=description,
96
+ cli_only=cli_only,
97
+ input=input,
98
+ env=env,
99
+ execute_condition=execute_condition,
100
+ retries=retries,
101
+ retry_period=retry_period,
102
+ readiness_check=readiness_check,
103
+ readiness_check_delay=readiness_check_delay,
104
+ readiness_check_period=readiness_check_period,
105
+ readiness_failure_threshold=readiness_failure_threshold,
106
+ readiness_timeout=readiness_timeout,
107
+ monitor_readiness=monitor_readiness,
108
+ upstream=upstream,
109
+ fallback=fallback,
110
+ successor=successor,
111
+ print_fn=print_fn,
112
+ )
113
+ self._llm_config = default_llm_config if llm_config is None else llm_config
114
+ self._llm_limitter = (
115
+ default_llm_limitter if llm_limitter is None else llm_limitter
116
+ )
117
+ self._system_prompt = system_prompt
118
+ self._render_system_prompt = render_system_prompt
119
+ self._prompt_manager = prompt_manager
120
+ self._tools = tools
121
+ self._toolsets = toolsets
122
+ self._message = message
123
+ self._render_message = render_message
124
+ self._attachment = attachment
125
+ self._history_processors = history_processors
126
+ self._model = model
127
+ self._render_model = render_model
128
+ self._model_settings = model_settings
129
+ self._conversation_name = conversation_name
130
+ self._render_conversation_name = render_conversation_name
131
+ self._history_manager = (
132
+ FileHistoryManager(history_dir=CFG.LLM_HISTORY_DIR)
133
+ if history_manager is None
134
+ else history_manager
135
+ )
136
+ self._tool_confirmation = tool_confirmation
137
+ self._yolo = yolo
138
+ self._summarize_command = summarize_command
139
+
140
+ @property
141
+ def prompt_manager(self) -> PromptManager:
142
+ if self._prompt_manager is None:
143
+ raise ValueError(f"Task {self.name} doesn't have prompt_manager")
144
+ return self._prompt_manager
145
+
146
+ def add_toolset(self, *toolset: "AbstractToolset"):
147
+ self.append_toolset(*toolset)
148
+
149
+ def append_toolset(self, *toolset: "AbstractToolset"):
150
+ self._toolsets += list(toolset)
151
+
152
+ def add_tool(self, *tool: "Tool | ToolFuncEither"):
153
+ self.append_tool(*tool)
154
+
155
+ def append_tool(self, *tool: "Tool | ToolFuncEither"):
156
+ self._tools += list(tool)
157
+
158
+ def add_history_processor(self, *processor: "HistoryProcessor"):
159
+ self.append_history_processor(*processor)
160
+
161
+ def append_history_processor(self, *processor: "HistoryProcessor"):
162
+ self._history_processors += list(processor)
163
+
164
+ async def _exec_action(self, ctx: AnyContext) -> Any:
165
+ conversation_name = self._get_conversation_name(ctx)
166
+ message_history = self._history_manager.load(conversation_name)
167
+ user_message = get_attr(ctx, self._message, "", self._render_message)
168
+ user_attachments = get_attachments(ctx, self._attachment)
169
+
170
+ if (
171
+ isinstance(user_message, str)
172
+ and user_message.strip() in self._summarize_command
173
+ ):
174
+ ctx.print("Compressing conversation history...", plain=True)
175
+ new_history = await summarize_history(message_history)
176
+ self._history_manager.update(conversation_name, new_history)
177
+ self._history_manager.save(conversation_name)
178
+ return "Conversation history compressed."
179
+
180
+ yolo = get_bool_attr(ctx, self._yolo, False)
181
+ system_prompt = self._get_system_prompt(ctx)
182
+ ctx.log_debug(f"SYSTEM PROMPT: {system_prompt}")
183
+ agent = create_agent(
184
+ model=self._get_model(ctx),
185
+ system_prompt=self._get_system_prompt(ctx),
186
+ tools=self._tools,
187
+ toolsets=self._toolsets,
188
+ model_settings=self._get_model_settings(ctx),
189
+ history_processors=self._history_processors,
190
+ yolo=yolo,
191
+ )
192
+
193
+ print_event = create_faint_printer(ctx)
194
+ handle_event = create_event_handler(
195
+ print_event,
196
+ show_tool_call_detail=CFG.LLM_SHOW_TOOL_CALL_PREPARATION,
197
+ show_tool_result=CFG.LLM_SHOW_TOOL_CALL_RESULT,
198
+ )
199
+
200
+ output, new_history = await run_agent(
201
+ agent=agent,
202
+ message=user_message,
203
+ message_history=message_history,
204
+ limiter=self._llm_limitter,
205
+ attachments=user_attachments,
206
+ print_fn=ctx.print,
207
+ event_handler=handle_event,
208
+ tool_confirmation=self._tool_confirmation,
209
+ )
210
+
211
+ self._history_manager.update(conversation_name, new_history)
212
+ self._history_manager.save(conversation_name)
213
+ ctx.log_debug(f"All messages: {new_history}")
214
+
215
+ return output
216
+
217
+ def _get_system_prompt(self, ctx: AnyContext) -> str:
218
+ if self._prompt_manager is None:
219
+ return str(
220
+ get_attr(ctx, self._system_prompt, "", self._render_system_prompt)
221
+ )
222
+ compose_prompt = self._prompt_manager.compose_prompt()
223
+ return compose_prompt(ctx)
224
+
225
+ def _get_conversation_name(self, ctx: AnyContext) -> str:
226
+ conversation_name = str(
227
+ get_attr(ctx, self._conversation_name, "", self._render_conversation_name)
228
+ )
229
+ if conversation_name.strip() == "":
230
+ conversation_name = get_random_name()
231
+ return conversation_name
232
+
233
+ def _get_model_settings(self, ctx: AnyContext) -> "ModelSettings | None":
234
+ model_settings = self._model_settings
235
+ rendered_model_settings = get_attr(ctx, model_settings, None)
236
+ if rendered_model_settings is not None:
237
+ return rendered_model_settings
238
+ return self._llm_config.model_settings
239
+
240
+ def _get_model(self, ctx: AnyContext) -> "str | Model":
241
+ model = self._model
242
+ rendered_model = get_attr(ctx, model, None, auto_render=self._render_model)
243
+ if rendered_model is not None:
244
+ return rendered_model
245
+ return self._llm_config.model
@@ -0,0 +1,39 @@
1
+ from zrb.llm.tool.bash import run_shell_command
2
+ from zrb.llm.tool.code import analyze_code
3
+ from zrb.llm.tool.file import (
4
+ analyze_file,
5
+ list_files,
6
+ read_file,
7
+ read_files,
8
+ replace_in_file,
9
+ search_files,
10
+ write_file,
11
+ write_files,
12
+ )
13
+ from zrb.llm.tool.note import create_note_tools
14
+ from zrb.llm.tool.rag import create_rag_from_directory
15
+ from zrb.llm.tool.skill import create_activate_skill_tool
16
+ from zrb.llm.tool.sub_agent import create_sub_agent_tool
17
+ from zrb.llm.tool.web import open_web_page, search_internet
18
+ from zrb.llm.tool.zrb_task import create_list_zrb_task_tool, create_run_zrb_task_tool
19
+
20
+ __all__ = [
21
+ "run_shell_command",
22
+ "analyze_code",
23
+ "list_files",
24
+ "read_file",
25
+ "read_files",
26
+ "write_file",
27
+ "write_files",
28
+ "replace_in_file",
29
+ "search_files",
30
+ "analyze_file",
31
+ "create_note_tools",
32
+ "create_rag_from_directory",
33
+ "create_activate_skill_tool",
34
+ "create_sub_agent_tool",
35
+ "open_web_page",
36
+ "search_internet",
37
+ "create_list_zrb_task_tool",
38
+ "create_run_zrb_task_tool",
39
+ ]
zrb/llm/tool/bash.py ADDED
@@ -0,0 +1,75 @@
1
+ import asyncio
2
+ import re
3
+
4
+ from zrb.util.cli.style import stylize_faint
5
+
6
+
7
+ async def run_shell_command(command: str, timeout: int = 30) -> str:
8
+ """
9
+ Executes a shell command on the host system and returns its combined stdout and stderr.
10
+ This is a powerful tool for running builds, tests, or system utilities.
11
+
12
+ **CRITICAL SAFETY:**
13
+ - DO NOT run destructive commands (e.g., `rm -rf /`) without absolute certainty.
14
+ - Prefer specialized tools (like `read_file` or `write_file`) for file operations.
15
+
16
+ **USAGE GUIDELINES:**
17
+ - Use non-interactive commands.
18
+ - If a command is expected to produce massive output, use `timeout` or pipe to a file.
19
+ - The output is streamed to the console in real-time.
20
+
21
+ Args:
22
+ command (str): The full shell command to execute.
23
+ timeout (int): Maximum wait time in seconds before terminating the process. Defaults to 30.
24
+ """
25
+ ANSI_ESCAPE = re.compile(
26
+ r"(?:\x1B\[[0-?]*[ -/]*[@-~])|" # CSI (Control Sequence Introducer)
27
+ r"(?:\x1B\][^\a\x1b]*[\a\x1b])|" # OSC (Operating System Command)
28
+ r"(?:\x1B[0-9=>])" # Simple 2-byte (DECSC, DECRC, etc.)
29
+ )
30
+ try:
31
+ process = await asyncio.create_subprocess_shell(
32
+ command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
33
+ )
34
+
35
+ stdout_lines = []
36
+ stderr_lines = []
37
+
38
+ async def read_stream(stream, lines_list, prefix=""):
39
+ while True:
40
+ line = await stream.readline()
41
+ if not line:
42
+ break
43
+ decoded = line.decode()
44
+ if decoded:
45
+ shown = ANSI_ESCAPE.sub("", decoded)
46
+ shown = stylize_faint(shown)
47
+ print(f"{prefix} {shown}", end="") # Stream to console
48
+ lines_list.append(decoded)
49
+
50
+ # Wait for the process to complete or timeout
51
+ try:
52
+ await asyncio.wait_for(
53
+ asyncio.gather(
54
+ read_stream(process.stdout, stdout_lines, ""),
55
+ read_stream(process.stderr, stderr_lines, "[stderr] "),
56
+ process.wait(),
57
+ ),
58
+ timeout=timeout,
59
+ )
60
+ except asyncio.TimeoutError:
61
+ if process.returncode is None:
62
+ try:
63
+ process.terminate()
64
+ await process.wait()
65
+ except ProcessLookupError:
66
+ pass
67
+ return f"Error: Command timed out after {timeout} seconds."
68
+
69
+ output = "\n".join(stdout_lines)
70
+ error = "\n".join(stderr_lines)
71
+
72
+ return f"Exit Code: {process.returncode}\nStdout:\n{output}\nStderr:\n{error}"
73
+
74
+ except Exception as e:
75
+ return f"Error executing command: {e}"
zrb/llm/tool/code.py ADDED
@@ -0,0 +1,266 @@
1
+ import fnmatch
2
+ import json
3
+ import os
4
+
5
+ from zrb.config.config import CFG
6
+ from zrb.llm.agent.agent import create_agent, run_agent
7
+ from zrb.llm.config.config import llm_config
8
+ from zrb.llm.config.limiter import llm_limiter
9
+ from zrb.llm.prompt.default import (
10
+ get_repo_extractor_system_prompt,
11
+ get_repo_summarizer_system_prompt,
12
+ )
13
+ from zrb.llm.tool.file import DEFAULT_EXCLUDED_PATTERNS
14
+
15
+ _DEFAULT_EXTENSIONS = [
16
+ "py",
17
+ "go",
18
+ "java",
19
+ "ts",
20
+ "js",
21
+ "rs",
22
+ "rb",
23
+ "php",
24
+ "sh",
25
+ "bash",
26
+ "c",
27
+ "cpp",
28
+ "h",
29
+ "hpp",
30
+ "cs",
31
+ "swift",
32
+ "kt",
33
+ "scala",
34
+ "m",
35
+ "pl",
36
+ "lua",
37
+ "sql",
38
+ "html",
39
+ "css",
40
+ "scss",
41
+ "less",
42
+ "json",
43
+ "yaml",
44
+ "yml",
45
+ "toml",
46
+ "ini",
47
+ "xml",
48
+ "md",
49
+ "rst",
50
+ "txt",
51
+ ]
52
+
53
+
54
+ async def analyze_code(
55
+ path: str,
56
+ query: str,
57
+ extensions: list[str] | None = None,
58
+ exclude_patterns: list[str] | None = None,
59
+ ) -> str:
60
+ """
61
+ Performs a deep, semantic analysis of an entire codebase or directory to answer complex architectural or logic-based questions.
62
+ This tool uses a 'map-reduce' strategy to handle large repositories that exceed single-prompt limits.
63
+
64
+ **WHEN TO USE:**
65
+ - To understand system-wide flows, architectural patterns, or cross-file dependencies.
66
+ - When you need a summary of how a feature is implemented across multiple files.
67
+ - To identify potential refactoring opportunities or technical debt.
68
+
69
+ **LIMITATIONS:**
70
+ - It extracts and summarizes information; it does not read every single byte if the repo is massive.
71
+ - For precise line-by-line reading of a known file, use `read_file` instead.
72
+
73
+ Args:
74
+ path (str): Path to the directory or repository.
75
+ query (str): A clear, specific question or analysis goal (e.g., "How is authentication handled?").
76
+ extensions (list[str], optional): File extensions to include (e.g., ["py", "ts"]).
77
+ exclude_patterns (list[str], optional): Glob patterns to ignore (e.g., ["tests/*"]).
78
+
79
+ Returns:
80
+ str: A comprehensive analytical report.
81
+ """
82
+ if extensions is None:
83
+ extensions = _DEFAULT_EXTENSIONS
84
+ if exclude_patterns is None:
85
+ exclude_patterns = DEFAULT_EXCLUDED_PATTERNS
86
+
87
+ abs_path = os.path.abspath(os.path.expanduser(path))
88
+ if not os.path.exists(abs_path):
89
+ return f"Error: Path not found: {path}"
90
+
91
+ # 1. Gather files
92
+ file_metadatas = _get_file_metadatas(abs_path, extensions, exclude_patterns)
93
+ if not file_metadatas:
94
+ return "No files found matching the criteria."
95
+
96
+ print(f" 📝 Extraction ({len(file_metadatas)} files)")
97
+
98
+ # 2. Extract Info
99
+ extraction_token_threshold = CFG.LLM_REPO_ANALYSIS_EXTRACTION_TOKEN_THRESHOLD
100
+ extracted_infos = await _extract_info(
101
+ file_metadatas=file_metadatas,
102
+ query=query,
103
+ token_limit=extraction_token_threshold,
104
+ )
105
+
106
+ if not extracted_infos:
107
+ return "No information could be extracted from the files."
108
+
109
+ if len(extracted_infos) == 1:
110
+ return extracted_infos[0]
111
+
112
+ # 3. Summarize Info (Reduce)
113
+ summarization_token_threshold = CFG.LLM_REPO_ANALYSIS_SUMMARIZATION_TOKEN_THRESHOLD
114
+ summarized_infos = extracted_infos
115
+
116
+ while len(summarized_infos) > 1:
117
+ print(f" 📝 Summarization ({len(summarized_infos)} chunks)")
118
+ summarized_infos = await _summarize_info(
119
+ extracted_infos=summarized_infos,
120
+ query=query,
121
+ token_limit=summarization_token_threshold,
122
+ )
123
+
124
+ return summarized_infos[0]
125
+
126
+
127
+ def _get_file_metadatas(
128
+ dir_path: str,
129
+ extensions: list[str],
130
+ exclude_patterns: list[str],
131
+ ) -> list[dict[str, str]]:
132
+ metadata_list = []
133
+ for root, _, files in os.walk(dir_path):
134
+ files.sort()
135
+ for file in files:
136
+ if not any(file.endswith(f".{ext}") for ext in extensions):
137
+ continue
138
+ file_path = os.path.join(root, file)
139
+ try:
140
+ rel_path = os.path.relpath(file_path, dir_path)
141
+ if _is_excluded(rel_path, exclude_patterns):
142
+ continue
143
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
144
+ metadata_list.append({"path": rel_path, "content": f.read()})
145
+ except Exception as e:
146
+ print(f"Error reading file {file_path}: {e}")
147
+ metadata_list.sort(key=lambda m: m["path"])
148
+ return metadata_list
149
+
150
+
151
+ def _is_excluded(name: str, patterns: list[str]) -> bool:
152
+ for pattern in patterns:
153
+ if fnmatch.fnmatch(name, pattern):
154
+ return True
155
+ parts = name.split(os.path.sep)
156
+ for part in parts:
157
+ if fnmatch.fnmatch(part, pattern):
158
+ return True
159
+ return False
160
+
161
+
162
+ async def _extract_info(
163
+ file_metadatas: list[dict[str, str]],
164
+ query: str,
165
+ token_limit: int,
166
+ ) -> list[str]:
167
+ agent = create_agent(
168
+ model=llm_config.model,
169
+ system_prompt=get_repo_extractor_system_prompt(),
170
+ )
171
+
172
+ extracted_infos = []
173
+ content_buffer = []
174
+ current_token_count = 0
175
+
176
+ # We estimate token count of the prompt template overhead
177
+ base_overhead = 100
178
+
179
+ for metadata in file_metadatas:
180
+ path = metadata.get("path", "")
181
+ content = metadata.get("content", "")
182
+ file_obj = {"path": path, "content": content}
183
+ file_str = json.dumps(file_obj)
184
+ file_tokens = llm_limiter.count_tokens(file_str)
185
+
186
+ if current_token_count + file_tokens + base_overhead > token_limit:
187
+ if content_buffer:
188
+ await _run_extraction(agent, query, content_buffer, extracted_infos)
189
+
190
+ content_buffer = [file_obj]
191
+ current_token_count = file_tokens
192
+ else:
193
+ content_buffer.append(file_obj)
194
+ current_token_count += file_tokens
195
+
196
+ # Process remaining buffer
197
+ if content_buffer:
198
+ await _run_extraction(agent, query, content_buffer, extracted_infos)
199
+
200
+ return extracted_infos
201
+
202
+
203
+ async def _run_extraction(agent, query, content_buffer, extracted_infos):
204
+ prompt_data = {
205
+ "main_assistant_query": query,
206
+ "files": content_buffer,
207
+ }
208
+ # We serialize to JSON for the prompt
209
+ message = json.dumps(prompt_data)
210
+
211
+ result, _ = await run_agent(
212
+ agent=agent,
213
+ message=message,
214
+ message_history=[], # Stateless
215
+ limiter=llm_limiter,
216
+ )
217
+ extracted_infos.append(str(result))
218
+
219
+
220
+ async def _summarize_info(
221
+ extracted_infos: list[str],
222
+ query: str,
223
+ token_limit: int,
224
+ ) -> list[str]:
225
+ agent = create_agent(
226
+ model=llm_config.model,
227
+ system_prompt=get_repo_summarizer_system_prompt(),
228
+ )
229
+
230
+ summarized_infos = []
231
+ content_buffer = ""
232
+ # Overhead for prompt structure
233
+ base_overhead = 100
234
+
235
+ for info in extracted_infos:
236
+ # Check if adding this info exceeds limit
237
+ if (
238
+ llm_limiter.count_tokens(content_buffer + info) + base_overhead
239
+ > token_limit
240
+ ):
241
+ if content_buffer:
242
+ await _run_summarization(agent, query, content_buffer, summarized_infos)
243
+ content_buffer = info
244
+ else:
245
+ content_buffer += info + "\n"
246
+
247
+ if content_buffer:
248
+ await _run_summarization(agent, query, content_buffer, summarized_infos)
249
+
250
+ return summarized_infos
251
+
252
+
253
+ async def _run_summarization(agent, query, content_buffer, summarized_infos):
254
+ prompt_data = {
255
+ "main_assistant_query": query,
256
+ "extracted_info": content_buffer,
257
+ }
258
+ message = json.dumps(prompt_data)
259
+
260
+ result, _ = await run_agent(
261
+ agent=agent,
262
+ message=message,
263
+ message_history=[], # Stateless
264
+ limiter=llm_limiter,
265
+ )
266
+ summarized_infos.append(str(result))