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,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)
zrb/llm/util/prompt.py ADDED
@@ -0,0 +1,104 @@
1
+ import os
2
+ import re
3
+
4
+ from zrb.util.file import list_files, read_file
5
+
6
+
7
+ def expand_prompt(prompt: str) -> str:
8
+ """
9
+ Expands @reference patterns in the prompt into a Reference + Appendix style.
10
+ Example: "Check @main.py" -> "Check main.py (see Appendix)\n...[Appendix with content]..."
11
+ """
12
+ if not prompt:
13
+ return prompt
14
+
15
+ # Regex to capture @path.
16
+ # Matches @ followed by typical path chars.
17
+ # We'll allow alphanumeric, _, -, ., /, \, and ~ (home dir).
18
+ pattern = re.compile(r"@(?P<path>[\w~\-\./\\]+)")
19
+
20
+ matches = list(pattern.finditer(prompt))
21
+ if not matches:
22
+ return prompt
23
+
24
+ appendix_entries: list[str] = []
25
+ # We construct the new string by slicing.
26
+ last_idx = 0
27
+ parts = []
28
+
29
+ for match in matches:
30
+ # Add text before match
31
+ parts.append(prompt[last_idx : match.start()])
32
+
33
+ path_ref = match.group("path")
34
+ original_token = match.group(0)
35
+
36
+ # Check existence
37
+ expanded_path = os.path.expanduser(path_ref)
38
+ abs_path = os.path.abspath(expanded_path)
39
+
40
+ content = ""
41
+ header = ""
42
+ is_valid_ref = False
43
+
44
+ if os.path.isfile(abs_path):
45
+ try:
46
+ content = read_file(abs_path)
47
+ header = f"File Content: `{path_ref}`"
48
+ is_valid_ref = True
49
+ except Exception:
50
+ pass
51
+ elif os.path.isdir(abs_path):
52
+ try:
53
+ # Use list_files for directory structure
54
+ file_list = list_files(abs_path, depth=2)
55
+ content = "\n".join(file_list)
56
+ if not content:
57
+ content = "(Empty directory)"
58
+ header = f"Directory Listing: `{path_ref}`"
59
+ is_valid_ref = True
60
+ except Exception:
61
+ pass
62
+
63
+ if not is_valid_ref:
64
+ # Fallback: leave original token if unreadable or not found
65
+ parts.append(original_token)
66
+ last_idx = match.end()
67
+ continue
68
+
69
+ # If we successfully got content
70
+ parts.append(f"`{path_ref}` (see Appendix)")
71
+
72
+ # Add to appendix with strict instructions
73
+ entry_lines = [
74
+ f"### {header}",
75
+ f"> **SYSTEM NOTE:** The content of `{path_ref}` is provided below.",
76
+ "> **DO NOT** use tools like `read_file` or `list_files` to read this path again.",
77
+ "> Use the content provided here directly.\n",
78
+ "```",
79
+ f"{content}",
80
+ "```",
81
+ ]
82
+ appendix_entries.append("\n".join(entry_lines))
83
+
84
+ last_idx = match.end()
85
+
86
+ # Add remaining text
87
+ parts.append(prompt[last_idx:])
88
+
89
+ new_prompt = "".join(parts)
90
+
91
+ if appendix_entries:
92
+ sep = "=" * 20
93
+ header_lines = [
94
+ f"\n\n{sep}APPENDIX: PRE-LOADED CONTEXT {sep}",
95
+ "⚠️ **SYSTEM INSTRUCTION**: The user has attached the following content.",
96
+ "You MUST use this provided content for your analysis.",
97
+ "**DO NOT** consume resources by calling `read_file` or `list_files`",
98
+ "or `run_shell_command` to read these specific paths again.\n",
99
+ ]
100
+ appendix_section = "\n".join(header_lines)
101
+ appendix_section += "\n\n".join(appendix_entries)
102
+ new_prompt += appendix_section
103
+
104
+ return new_prompt