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/llm/tool/note.py ADDED
@@ -0,0 +1,70 @@
1
+ import os
2
+ from typing import Callable, List
3
+
4
+ from zrb.llm.note.manager import NoteManager
5
+
6
+
7
+ def create_note_tools(note_manager: NoteManager) -> List[Callable]:
8
+ async def read_long_term_note() -> str:
9
+ """
10
+ Retrieves your GLOBAL 🧠 Long-Term Memory.
11
+ This contains established preferences, personal facts, and context spanning multiple projects.
12
+ ALWAYS check this at the start of a session.
13
+ """
14
+ return note_manager.read("~")
15
+
16
+ read_long_term_note.__name__ = "read_long_term_note"
17
+
18
+ async def write_long_term_note(content: str) -> str:
19
+ """
20
+ Updates your GLOBAL 🧠 Long-Term Memory with CRITICAL information.
21
+ Use this to persist user preferences, personal facts, and cross-project rules.
22
+
23
+ **WARNING:** This COMPLETELY OVERWRITES the existing Long-Term Note.
24
+
25
+ **ARGS:**
26
+ - `content`: The full text to store in the global memory.
27
+ """
28
+ note_manager.write("~", content)
29
+ return "Global long-term note saved."
30
+
31
+ write_long_term_note.__name__ = "write_long_term_note"
32
+
33
+ async def read_contextual_note(path: str | None = None) -> str:
34
+ """
35
+ Retrieves LOCAL 📝 Contextual Notes for a specific project or directory.
36
+ Use this to recall architectural decisions or project-specific guidelines.
37
+
38
+ **ARGS:**
39
+ - `path`: Target file/dir path. Defaults to current working directory.
40
+ """
41
+ if path is None:
42
+ path = os.getcwd()
43
+ return note_manager.read(path)
44
+
45
+ read_contextual_note.__name__ = "read_contextual_note"
46
+
47
+ async def write_contextual_note(content: str, path: str | None = None) -> str:
48
+ """
49
+ Persists LOCAL 📝 Contextual Notes for a specific project or directory.
50
+ Use this to save architectural patterns or progress markers for the current task.
51
+
52
+ **WARNING:** This COMPLETELY OVERWRITES the contextual note for the specified path.
53
+
54
+ **ARGS:**
55
+ - `content`: The full text to store in the local memory.
56
+ - `path`: Target file/dir path. Defaults to current working directory.
57
+ """
58
+ if path is None:
59
+ path = os.getcwd()
60
+ note_manager.write(path, content)
61
+ return f"Contextual note saved for: {path}"
62
+
63
+ write_contextual_note.__name__ = "write_contextual_note"
64
+
65
+ return [
66
+ read_long_term_note,
67
+ write_long_term_note,
68
+ read_contextual_note,
69
+ write_contextual_note,
70
+ ]
@@ -199,11 +199,14 @@ def create_rag_from_directory(
199
199
  retrieve.__doc__ = dedent(
200
200
  f"""
201
201
  {tool_description}
202
- Args:
203
- query (str): The user query to search for in documents.
204
- Returns:
205
- dict[str, Any]: dictionary with search results:
206
- {{"ids": [...], "documents": [...], ...}}
202
+ This tool performs a semantic search across a curated knowledge base of documents.
203
+ It is highly effective for answering questions that require specific project knowledge not found in general training data.
204
+
205
+ **ARGS:**
206
+ - `query` (str): The semantic search query or question.
207
+
208
+ **RETURNS:**
209
+ - A dictionary containing matching document chunks ("documents") and their metadata.
207
210
  """
208
211
  ).strip()
209
212
  return retrieve
@@ -0,0 +1,53 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ from zrb.config.config import CFG
6
+
7
+
8
+ def search_internet(
9
+ query: str,
10
+ page: int = 1,
11
+ safe_search: str | None = None,
12
+ language: str | None = None,
13
+ ) -> dict[str, Any]:
14
+ """
15
+ Performs a live internet search using Brave Search to retrieve up-to-date information, news, or documentation.
16
+
17
+ **WHEN TO USE:**
18
+ - To find the latest information on rapidly changing topics (e.g., library updates, current events).
19
+ - To search for documentation or examples not present in the local codebase.
20
+ - To verify facts or find external resources.
21
+
22
+ **ARGS:**
23
+ - `query`: The search string or question.
24
+ - `page`: Result page number (default 1).
25
+ """
26
+ if safe_search is None:
27
+ safe_search = CFG.BRAVE_API_SAFE
28
+ if language is None:
29
+ language = CFG.BRAVE_API_LANG
30
+
31
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
32
+
33
+ response = requests.get(
34
+ "https://api.search.brave.com/res/v1/web/search",
35
+ headers={
36
+ "User-Agent": user_agent,
37
+ "Accept": "application/json",
38
+ "x-subscription-token": CFG.BRAVE_API_KEY,
39
+ },
40
+ params={
41
+ "q": query,
42
+ "count": "10",
43
+ "offset": (page - 1) * 10,
44
+ "safesearch": safe_search,
45
+ "search_lang": language,
46
+ "summary": "true",
47
+ },
48
+ )
49
+ if response.status_code != 200:
50
+ raise Exception(
51
+ f"Error: Unable to retrieve search results (status code: {response.status_code})"
52
+ )
53
+ return response.json()
@@ -0,0 +1,47 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ from zrb.config.config import CFG
6
+
7
+
8
+ def search_internet(
9
+ query: str,
10
+ page: int = 1,
11
+ safe_search: int | None = None,
12
+ language: str | None = None,
13
+ ) -> dict[str, Any]:
14
+ """
15
+ Performs a live internet search using SearXNG, an aggregator that combines results from multiple search engines.
16
+
17
+ **WHEN TO USE:**
18
+ - To gather diverse perspectives or information from across the web.
19
+ - To retrieve the latest data, documentation, or public resources.
20
+
21
+ **ARGS:**
22
+ - `query`: The search string or question.
23
+ - `page`: Result page number (default 1).
24
+ """
25
+ if safe_search is None:
26
+ safe_search = CFG.SEARXNG_SAFE
27
+ if language is None:
28
+ language = CFG.SEARXNG_LANG
29
+
30
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
31
+
32
+ response = requests.get(
33
+ url=f"{CFG.SEARXNG_BASE_URL}/search",
34
+ headers={"User-Agent": user_agent},
35
+ params={
36
+ "q": query,
37
+ "format": "json",
38
+ "pageno": page,
39
+ "safesearch": safe_search,
40
+ "language": language,
41
+ },
42
+ )
43
+ if response.status_code != 200:
44
+ raise Exception(
45
+ f"Error: Unable to retrieve search results (status code: {response.status_code})"
46
+ )
47
+ return response.json()
@@ -0,0 +1,47 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ from zrb.config.config import CFG
6
+
7
+
8
+ def search_internet(
9
+ query: str,
10
+ page: int = 1,
11
+ safe_search: str | None = None,
12
+ language: str | None = None,
13
+ ) -> dict[str, Any]:
14
+ """
15
+ Performs a live internet search using SerpApi (Google Search) to retrieve the most relevant and current information from the web.
16
+
17
+ **WHEN TO USE:**
18
+ - When you need precise, high-quality search results from Google.
19
+ - To find the latest official documentation, technical articles, or community discussions.
20
+
21
+ **ARGS:**
22
+ - `query`: The search string or question.
23
+ - `page`: Result page number (default 1).
24
+ """
25
+ if safe_search is None:
26
+ safe_search = CFG.SERPAPI_SAFE
27
+ if language is None:
28
+ language = CFG.SERPAPI_LANG
29
+
30
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
31
+
32
+ response = requests.get(
33
+ "https://serpapi.com/search",
34
+ headers={"User-Agent": user_agent},
35
+ params={
36
+ "q": query,
37
+ "start": (page - 1) * 10,
38
+ "hl": language,
39
+ "safe": safe_search,
40
+ "api_key": CFG.SERPAPI_KEY,
41
+ },
42
+ )
43
+ if response.status_code != 200:
44
+ raise Exception(
45
+ f"Error: Unable to retrieve search results (status code: {response.status_code})"
46
+ )
47
+ return response.json()
zrb/llm/tool/skill.py ADDED
@@ -0,0 +1,19 @@
1
+ from zrb.llm.skill.manager import SkillManager
2
+
3
+
4
+ def create_activate_skill_tool(skill_manager: SkillManager):
5
+ async def activate_skill_impl(name: str) -> str:
6
+ content = skill_manager.get_skill_content(name)
7
+ if content:
8
+ return f"<ACTIVATED_SKILL>\n{content}\n</ACTIVATED_SKILL>"
9
+ return f"Skill '{name}' not found."
10
+
11
+ activate_skill_impl.__name__ = "activate_skill"
12
+ activate_skill_impl.__doc__ = (
13
+ "Immediately activates a specialized expertise 'skill' to handle complex or domain-specific tasks. "
14
+ "Returns a set of authoritative instructions that YOU MUST follow to complete the task successfully. "
15
+ "Use this as soon as you identify a task that matches an available skill. "
16
+ "\n\n**ARGS:**"
17
+ "\n- `name`: The unique name of the skill to activate."
18
+ )
19
+ return activate_skill_impl
@@ -0,0 +1,70 @@
1
+ from typing import Any
2
+
3
+ from zrb.llm.agent.agent import create_agent, run_agent
4
+ from zrb.llm.config.config import LLMConfig
5
+ from zrb.llm.config.config import llm_config as default_config
6
+ from zrb.llm.config.limiter import LLMLimiter
7
+ from zrb.llm.config.limiter import llm_limiter as default_limiter
8
+ from zrb.llm.history_manager.any_history_manager import AnyHistoryManager
9
+ from zrb.llm.history_manager.file_history_manager import FileHistoryManager
10
+
11
+
12
+ def create_sub_agent_tool(
13
+ name: str,
14
+ description: str,
15
+ model: str | None = None,
16
+ system_prompt: str = "",
17
+ tools: list = [],
18
+ llm_config: LLMConfig | None = None,
19
+ llm_limitter: LLMLimiter | None = None,
20
+ history_manager: AnyHistoryManager | None = None,
21
+ conversation_name: str = "sub_agent_default",
22
+ ) -> Any:
23
+ """
24
+ Creates a Tool that invokes a sub-agent.
25
+ The sub-agent manages its own persistent history via HistoryManager
26
+ and handles approval via CLI fallback if needed.
27
+ """
28
+ config = llm_config or default_config
29
+ limiter = llm_limitter or default_limiter
30
+ manager = history_manager or FileHistoryManager(history_dir="~/.llm_chat/subagents")
31
+ final_model = model or config.model
32
+
33
+ agent_instance = None
34
+
35
+ async def run_sub_agent(prompt: str) -> str:
36
+ nonlocal agent_instance
37
+ if agent_instance is None:
38
+ agent_instance = create_agent(
39
+ model=final_model,
40
+ system_prompt=system_prompt,
41
+ tools=tools,
42
+ yolo=False,
43
+ )
44
+
45
+ # Load persistent history
46
+ history = manager.load(conversation_name)
47
+
48
+ # Execute agent with blocking confirmation loop for approvals
49
+ result, new_history = await run_agent(
50
+ agent=agent_instance,
51
+ message=prompt,
52
+ message_history=history,
53
+ limiter=limiter,
54
+ )
55
+
56
+ # Save updated history
57
+ manager.update(conversation_name, new_history)
58
+ manager.save(conversation_name)
59
+
60
+ return str(result)
61
+
62
+ run_sub_agent.__name__ = name
63
+ run_sub_agent.__doc__ = (
64
+ f"DELEGATION TOOL: {description}\n\n"
65
+ "Use this tool to delegate complex, multi-step sub-tasks to a specialized agent. "
66
+ "The sub-agent has its own memory and can perform its own tool calls."
67
+ "\n\n**ARGS:**"
68
+ "\n- `prompt`: The clear and detailed objective or instruction for the sub-agent."
69
+ )
70
+ return run_sub_agent
zrb/llm/tool/web.py ADDED
@@ -0,0 +1,97 @@
1
+ from zrb.config.config import CFG
2
+
3
+
4
+ async def open_web_page(url: str) -> dict:
5
+ """
6
+ Downloads and converts a web page into clean, readable Markdown.
7
+
8
+ **WHEN TO USE:**
9
+ - To read specific articles, documentation, or blog posts.
10
+ - To extract structured information from a known URL.
11
+
12
+ **ARGS:**
13
+ - `url`: The full web address to fetch.
14
+ """
15
+ html_content, links = await _fetch_page_content(url)
16
+ markdown_content = _convert_html_to_markdown(html_content)
17
+ return {"content": markdown_content, "links_on_page": links}
18
+
19
+
20
+ async def search_internet(
21
+ query: str,
22
+ page: int = 1,
23
+ ) -> dict:
24
+ """
25
+ Performs a broad internet search.
26
+
27
+ **WHEN TO USE:**
28
+ - When you need to find information but don't have a specific URL.
29
+
30
+ **ARGS:**
31
+ - `query`: The search string or question.
32
+ - `page`: Result page number (default 1).
33
+ """
34
+ if (
35
+ CFG.SEARCH_INTERNET_METHOD.strip().lower() == "serpapi"
36
+ and CFG.SERPAPI_KEY != ""
37
+ ):
38
+ from zrb.llm.tool.search.serpapi import search_internet as serpapi_search
39
+
40
+ return serpapi_search(query, page=page)
41
+ if (
42
+ CFG.SEARCH_INTERNET_METHOD.strip().lower() == "brave"
43
+ and CFG.BRAVE_API_KEY != ""
44
+ ):
45
+ from zrb.llm.tool.search.brave import search_internet as brave_search
46
+
47
+ return brave_search(query, page=page)
48
+ from zrb.llm.tool.search.searxng import search_internet as searxng_search
49
+
50
+ return searxng_search(query, page=page)
51
+
52
+
53
+ async def _fetch_page_content(url: str) -> tuple:
54
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
55
+ try:
56
+ from playwright.async_api import async_playwright
57
+
58
+ async with async_playwright() as p:
59
+ browser = await p.chromium.launch(headless=True)
60
+ page = await browser.new_page()
61
+ await page.set_extra_http_headers({"User-Agent": user_agent})
62
+ await page.goto(url, wait_until="networkidle", timeout=30000)
63
+ content = await page.content()
64
+ links = await page.eval_on_selector_all(
65
+ "a[href]",
66
+ "(elements, baseUrl) => elements.map(el => { const href = el.getAttribute('href'); if (!href || href.startsWith('#')) return null; try { return new URL(href, baseUrl).href; } catch (e) { return null; } }).filter(href => href !== null)",
67
+ url,
68
+ )
69
+ await browser.close()
70
+ return content, links
71
+ except Exception:
72
+ from urllib.parse import urljoin
73
+
74
+ import requests
75
+ from bs4 import BeautifulSoup
76
+
77
+ response = requests.get(url, headers={"User-Agent": user_agent})
78
+ response.raise_for_status()
79
+ soup = BeautifulSoup(response.text, "html.parser")
80
+ links = [
81
+ urljoin(url, a["href"])
82
+ for a in soup.find_all("a", href=True)
83
+ if not a["href"].startswith("#")
84
+ ]
85
+ return response.text, links
86
+
87
+
88
+ def _convert_html_to_markdown(html_text: str) -> str:
89
+ from bs4 import BeautifulSoup
90
+ from markdownify import markdownify as md
91
+
92
+ soup = BeautifulSoup(html_text, "html.parser")
93
+ for tag in soup(
94
+ ["script", "link", "meta", "style", "header", "footer", "nav", "aside"]
95
+ ):
96
+ tag.decompose()
97
+ return md(str(soup))
@@ -0,0 +1,66 @@
1
+ from zrb.config.config import CFG
2
+ from zrb.llm.tool.bash import run_shell_command
3
+ from zrb.runner.cli import cli
4
+
5
+
6
+ def create_list_zrb_task_tool():
7
+ def list_zrb_tasks_impl(group_name: str | None = None) -> str:
8
+ target_group = cli
9
+ if group_name:
10
+ parts = group_name.split()
11
+ for part in parts:
12
+ next_group = target_group.get_group_by_alias(part)
13
+ if not next_group:
14
+ return f"Error: Group '{part}' not found in '{group_name}'."
15
+ target_group = next_group
16
+ output = [f"Tasks in '{target_group.name}':"]
17
+ # Subgroups
18
+ if target_group.subgroups:
19
+ output.append("\n Groups:")
20
+ for alias, grp in target_group.subgroups.items():
21
+ output.append(f" - {alias}: {grp.description}")
22
+ # Tasks
23
+ if target_group.subtasks:
24
+ output.append("\n Tasks:")
25
+ for alias, task in target_group.subtasks.items():
26
+ output.append(f" - {alias}: {task.description}")
27
+ return "\n".join(output)
28
+
29
+ zrb_cmd = CFG.ROOT_GROUP_NAME
30
+ list_zrb_tasks_impl.__name__ = f"list_{zrb_cmd}_tasks"
31
+ list_zrb_tasks_impl.__doc__ = (
32
+ f"Discovery tool to browse all available {zrb_cmd} tasks and automation groups. "
33
+ "Use this to understand what predefined workflows exist in the current project."
34
+ "\n\n**ARGS:**"
35
+ "\n- `group_name`: Optional name of the group to browse (e.g., 'server')."
36
+ )
37
+ return list_zrb_tasks_impl
38
+
39
+
40
+ def create_run_zrb_task_tool():
41
+ async def run_zrb_task(
42
+ task_name: str, args: dict[str, str] = {}, timeout: int = 30
43
+ ) -> str:
44
+ """ """
45
+ # Construct command
46
+ cmd_parts = ["zrb"] + task_name.split()
47
+
48
+ for key, val in args.items():
49
+ cmd_parts.append(f"--{key}")
50
+ cmd_parts.append(str(val))
51
+
52
+ command = " ".join(cmd_parts)
53
+ return await run_shell_command(command, timeout=timeout)
54
+
55
+ zrb_cmd = CFG.ROOT_GROUP_NAME
56
+ run_zrb_task.__name__ = f"run_{zrb_cmd}_task"
57
+ run_zrb_task.__doc__ = (
58
+ f"Executes a predefined {zrb_cmd} automation task with specified arguments. "
59
+ "This is the preferred way to run project-specific workflows (e.g., deployments, scaffolding, specialized builds)."
60
+ "\n\n**IMPORTANT:** You must provide all required arguments."
61
+ "\n\n**ARGS:**"
62
+ "\n- `task_name`: The full alias path of the task (e.g., 'server start')."
63
+ "\n- `args`: Dictionary of arguments (e.g., {'port': '8080'})."
64
+ "\n- `timeout`: Maximum wait time in seconds (default 30)."
65
+ )
66
+ return run_zrb_task
@@ -0,0 +1,101 @@
1
+ from typing import TYPE_CHECKING, Any, Callable
2
+
3
+ from zrb.context.any_context import AnyContext
4
+
5
+ if TYPE_CHECKING:
6
+ from pydantic_ai.messages import UserContent
7
+
8
+
9
+ def normalize_attachments(
10
+ attachments: "list[UserContent]", print_fn: Callable[[str], Any] = print
11
+ ) -> "list[UserContent]":
12
+ import os
13
+ from pathlib import Path
14
+
15
+ from pydantic_ai import BinaryContent
16
+
17
+ from zrb.llm.util.attachment import get_media_type
18
+
19
+ final_attachments = []
20
+ for item in attachments:
21
+ if isinstance(item, str):
22
+ # Treat as path
23
+ path = os.path.abspath(os.path.expanduser(item))
24
+ if os.path.exists(path):
25
+ media_type = get_media_type(path)
26
+ if media_type:
27
+ try:
28
+ data = Path(path).read_bytes()
29
+ final_attachments.append(
30
+ BinaryContent(data=data, media_type=media_type)
31
+ )
32
+ except Exception as e:
33
+ print_fn(f"Failed to read attachment {path}: {e}")
34
+ else:
35
+ print_fn(f"Unknown media type for {path}")
36
+ else:
37
+ print_fn(f"Attachment file not found: {path}")
38
+ else:
39
+ # Assume it's already a suitable object (e.g. BinaryContent)
40
+ final_attachments.append(item)
41
+ return final_attachments
42
+
43
+
44
+ def get_attachments(
45
+ ctx: AnyContext,
46
+ attachment: "UserContent | list[UserContent] | Callable[[AnyContext], UserContent | list[UserContent]] | None" = None, # noqa
47
+ ) -> "list[UserContent]":
48
+ if attachment is None:
49
+ return []
50
+ if callable(attachment):
51
+ result = attachment(ctx)
52
+ if result is None:
53
+ return []
54
+ if isinstance(result, list):
55
+ return result
56
+ return [result]
57
+ if isinstance(attachment, list):
58
+ return attachment
59
+ return [attachment]
60
+
61
+
62
+ def get_media_type(filename: str) -> str | None:
63
+ """Guess media type string based on file extension."""
64
+ ext = filename.lower().rsplit(".", 1)[-1] if "." in filename else ""
65
+ mapping: dict[str, str] = {
66
+ # Audio
67
+ "wav": "audio/wav",
68
+ "mp3": "audio/mpeg",
69
+ "ogg": "audio/ogg",
70
+ "flac": "audio/flac",
71
+ "aiff": "audio/aiff",
72
+ "aac": "audio/aac",
73
+ # Image
74
+ "jpg": "image/jpeg",
75
+ "jpeg": "image/jpeg",
76
+ "png": "image/png",
77
+ "gif": "image/gif",
78
+ "webp": "image/webp",
79
+ # Document
80
+ "pdf": "application/pdf",
81
+ "txt": "text/plain",
82
+ "csv": "text/csv",
83
+ "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
84
+ "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
85
+ "html": "text/html",
86
+ "htm": "text/html",
87
+ "md": "text/markdown",
88
+ "doc": "application/msword",
89
+ "xls": "application/vnd.ms-excel",
90
+ # Video
91
+ "mkv": "video/x-matroska",
92
+ "mov": "video/quicktime",
93
+ "mp4": "video/mp4",
94
+ "webm": "video/webm",
95
+ "flv": "video/x-flv",
96
+ "mpeg": "video/mpeg",
97
+ "mpg": "video/mpeg",
98
+ "wmv": "video/x-ms-wmv",
99
+ "3gp": "video/3gpp",
100
+ }
101
+ return mapping.get(ext)