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.
- zrb/__init__.py +118 -133
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +55 -1
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/llm/chat.py +147 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
- zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
- zrb/builtin/searxng/config/settings.yml +5671 -0
- zrb/builtin/searxng/start.py +21 -0
- zrb/builtin/shell/autocomplete/bash.py +4 -3
- zrb/builtin/shell/autocomplete/zsh.py +4 -3
- zrb/callback/callback.py +8 -1
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +555 -169
- zrb/config/helper.py +84 -0
- zrb/config/web_auth_config.py +50 -35
- zrb/context/any_shared_context.py +20 -3
- zrb/context/context.py +39 -5
- zrb/context/print_fn.py +13 -0
- zrb/context/shared_context.py +17 -8
- zrb/group/any_group.py +3 -3
- zrb/group/group.py +3 -3
- zrb/input/any_input.py +5 -1
- zrb/input/base_input.py +18 -6
- zrb/input/option_input.py +41 -1
- zrb/input/text_input.py +7 -24
- zrb/llm/agent/__init__.py +9 -0
- zrb/llm/agent/agent.py +215 -0
- zrb/llm/agent/summarizer.py +20 -0
- zrb/llm/app/__init__.py +10 -0
- zrb/llm/app/completion.py +281 -0
- zrb/llm/app/confirmation/allow_tool.py +66 -0
- zrb/llm/app/confirmation/handler.py +178 -0
- zrb/llm/app/confirmation/replace_confirmation.py +77 -0
- zrb/llm/app/keybinding.py +34 -0
- zrb/llm/app/layout.py +117 -0
- zrb/llm/app/lexer.py +155 -0
- zrb/llm/app/redirection.py +28 -0
- zrb/llm/app/style.py +16 -0
- zrb/llm/app/ui.py +733 -0
- zrb/llm/config/__init__.py +4 -0
- zrb/llm/config/config.py +122 -0
- zrb/llm/config/limiter.py +247 -0
- zrb/llm/history_manager/__init__.py +4 -0
- zrb/llm/history_manager/any_history_manager.py +23 -0
- zrb/llm/history_manager/file_history_manager.py +91 -0
- zrb/llm/history_processor/summarizer.py +108 -0
- zrb/llm/note/__init__.py +3 -0
- zrb/llm/note/manager.py +122 -0
- zrb/llm/prompt/__init__.py +29 -0
- zrb/llm/prompt/claude_compatibility.py +92 -0
- zrb/llm/prompt/compose.py +55 -0
- zrb/llm/prompt/default.py +51 -0
- zrb/llm/prompt/markdown/file_extractor.md +112 -0
- zrb/llm/prompt/markdown/mandate.md +23 -0
- zrb/llm/prompt/markdown/persona.md +3 -0
- zrb/llm/prompt/markdown/repo_extractor.md +112 -0
- zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
- zrb/llm/prompt/markdown/summarizer.md +21 -0
- zrb/llm/prompt/note.py +41 -0
- zrb/llm/prompt/system_context.py +46 -0
- zrb/llm/prompt/zrb.py +41 -0
- zrb/llm/skill/__init__.py +3 -0
- zrb/llm/skill/manager.py +86 -0
- zrb/llm/task/__init__.py +4 -0
- zrb/llm/task/llm_chat_task.py +316 -0
- zrb/llm/task/llm_task.py +245 -0
- zrb/llm/tool/__init__.py +39 -0
- zrb/llm/tool/bash.py +75 -0
- zrb/llm/tool/code.py +266 -0
- zrb/llm/tool/file.py +419 -0
- zrb/llm/tool/note.py +70 -0
- zrb/{builtin/llm → llm}/tool/rag.py +33 -37
- zrb/llm/tool/search/brave.py +53 -0
- zrb/llm/tool/search/searxng.py +47 -0
- zrb/llm/tool/search/serpapi.py +47 -0
- zrb/llm/tool/skill.py +19 -0
- zrb/llm/tool/sub_agent.py +70 -0
- zrb/llm/tool/web.py +97 -0
- zrb/llm/tool/zrb_task.py +66 -0
- zrb/llm/util/attachment.py +101 -0
- zrb/llm/util/prompt.py +104 -0
- zrb/llm/util/stream_response.py +178 -0
- zrb/runner/cli.py +21 -20
- zrb/runner/common_util.py +24 -19
- zrb/runner/web_route/task_input_api_route.py +5 -5
- zrb/runner/web_util/user.py +7 -3
- zrb/session/any_session.py +12 -9
- zrb/session/session.py +38 -17
- zrb/task/any_task.py +24 -3
- zrb/task/base/context.py +42 -22
- zrb/task/base/execution.py +67 -55
- zrb/task/base/lifecycle.py +14 -7
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +113 -50
- zrb/task/base_trigger.py +16 -6
- zrb/task/cmd_task.py +6 -0
- zrb/task/http_check.py +11 -5
- zrb/task/make_task.py +5 -3
- zrb/task/rsync_task.py +30 -10
- zrb/task/scaffolder.py +7 -4
- zrb/task/scheduler.py +7 -4
- zrb/task/tcp_check.py +6 -4
- zrb/util/ascii_art/art/bee.txt +17 -0
- zrb/util/ascii_art/art/cat.txt +9 -0
- zrb/util/ascii_art/art/ghost.txt +16 -0
- zrb/util/ascii_art/art/panda.txt +17 -0
- zrb/util/ascii_art/art/rose.txt +14 -0
- zrb/util/ascii_art/art/unicorn.txt +15 -0
- zrb/util/ascii_art/banner.py +92 -0
- zrb/util/attr.py +54 -39
- zrb/util/cli/markdown.py +32 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/cmd/command.py +33 -10
- zrb/util/file.py +61 -33
- zrb/util/git.py +2 -2
- zrb/util/{llm/prompt.py → markdown.py} +2 -3
- zrb/util/match.py +78 -0
- zrb/util/run.py +3 -3
- zrb/util/string/conversion.py +1 -1
- zrb/util/truncate.py +23 -0
- zrb/util/yaml.py +204 -0
- zrb/xcom/xcom.py +10 -0
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
- zrb/attr/__init__.py +0 -0
- zrb/builtin/llm/chat_session.py +0 -311
- zrb/builtin/llm/history.py +0 -71
- zrb/builtin/llm/input.py +0 -27
- zrb/builtin/llm/llm_ask.py +0 -187
- zrb/builtin/llm/previous-session.js +0 -21
- zrb/builtin/llm/tool/__init__.py +0 -0
- zrb/builtin/llm/tool/api.py +0 -71
- zrb/builtin/llm/tool/cli.py +0 -38
- zrb/builtin/llm/tool/code.py +0 -254
- zrb/builtin/llm/tool/file.py +0 -626
- zrb/builtin/llm/tool/sub_agent.py +0 -137
- zrb/builtin/llm/tool/web.py +0 -195
- zrb/builtin/project/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
- zrb/builtin/project/create/__init__.py +0 -0
- zrb/builtin/shell/__init__.py +0 -0
- zrb/builtin/shell/autocomplete/__init__.py +0 -0
- zrb/callback/__init__.py +0 -0
- zrb/cmd/__init__.py +0 -0
- zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
- zrb/config/default_prompt/interactive_system_prompt.md +0 -35
- zrb/config/default_prompt/persona.md +0 -1
- zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
- zrb/config/default_prompt/summarization_prompt.md +0 -16
- zrb/config/default_prompt/system_prompt.md +0 -32
- zrb/config/llm_config.py +0 -243
- zrb/config/llm_context/config.py +0 -129
- zrb/config/llm_context/config_parser.py +0 -46
- zrb/config/llm_rate_limitter.py +0 -137
- zrb/content_transformer/__init__.py +0 -0
- zrb/context/__init__.py +0 -0
- zrb/dot_dict/__init__.py +0 -0
- zrb/env/__init__.py +0 -0
- zrb/group/__init__.py +0 -0
- zrb/input/__init__.py +0 -0
- zrb/runner/__init__.py +0 -0
- zrb/runner/web_route/__init__.py +0 -0
- zrb/runner/web_route/home_page/__init__.py +0 -0
- zrb/session/__init__.py +0 -0
- zrb/session_state_log/__init__.py +0 -0
- zrb/session_state_logger/__init__.py +0 -0
- zrb/task/__init__.py +0 -0
- zrb/task/base/__init__.py +0 -0
- zrb/task/llm/__init__.py +0 -0
- zrb/task/llm/agent.py +0 -243
- zrb/task/llm/config.py +0 -103
- zrb/task/llm/conversation_history.py +0 -128
- zrb/task/llm/conversation_history_model.py +0 -242
- zrb/task/llm/default_workflow/coding.md +0 -24
- zrb/task/llm/default_workflow/copywriting.md +0 -17
- zrb/task/llm/default_workflow/researching.md +0 -18
- zrb/task/llm/error.py +0 -95
- zrb/task/llm/history_summarization.py +0 -216
- zrb/task/llm/print_node.py +0 -101
- zrb/task/llm/prompt.py +0 -325
- zrb/task/llm/tool_wrapper.py +0 -220
- zrb/task/llm/typing.py +0 -3
- zrb/task/llm_task.py +0 -341
- zrb/task_status/__init__.py +0 -0
- zrb/util/__init__.py +0 -0
- zrb/util/cli/__init__.py +0 -0
- zrb/util/cmd/__init__.py +0 -0
- zrb/util/codemod/__init__.py +0 -0
- zrb/util/string/__init__.py +0 -0
- zrb/xcom/__init__.py +0 -0
- {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))
|
zrb/llm/tool/zrb_task.py
ADDED
|
@@ -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
|