zrb 1.21.17__py3-none-any.whl → 1.21.33__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.
- zrb/attr/type.py +10 -7
- zrb/builtin/git.py +12 -1
- zrb/builtin/llm/chat_completion.py +287 -0
- zrb/builtin/llm/chat_session_cmd.py +90 -28
- zrb/builtin/llm/chat_trigger.py +6 -1
- zrb/builtin/llm/tool/cli.py +29 -13
- zrb/builtin/llm/tool/code.py +9 -1
- zrb/builtin/llm/tool/file.py +32 -6
- zrb/builtin/llm/tool/note.py +9 -9
- zrb/builtin/llm/tool/search/__init__.py +1 -0
- zrb/builtin/llm/tool/search/brave.py +66 -0
- zrb/builtin/llm/tool/search/searxng.py +61 -0
- zrb/builtin/llm/tool/search/serpapi.py +61 -0
- zrb/builtin/llm/tool/sub_agent.py +30 -10
- zrb/builtin/llm/tool/web.py +17 -72
- zrb/config/config.py +67 -26
- zrb/config/default_prompt/interactive_system_prompt.md +16 -13
- zrb/config/default_prompt/summarization_prompt.md +54 -8
- zrb/config/default_prompt/system_prompt.md +16 -18
- zrb/config/llm_rate_limitter.py +15 -6
- zrb/input/option_input.py +13 -1
- zrb/task/llm/agent.py +42 -143
- zrb/task/llm/agent_runner.py +152 -0
- zrb/task/llm/conversation_history.py +35 -24
- zrb/task/llm/conversation_history_model.py +4 -11
- zrb/task/llm/history_processor.py +206 -0
- zrb/task/llm/history_summarization.py +2 -179
- zrb/task/llm/print_node.py +14 -5
- zrb/task/llm/prompt.py +2 -17
- zrb/task/llm/subagent_conversation_history.py +41 -0
- zrb/task/llm/tool_confirmation_completer.py +41 -0
- zrb/task/llm/tool_wrapper.py +15 -11
- zrb/task/llm_task.py +41 -40
- zrb/util/attr.py +12 -7
- zrb/util/git.py +2 -2
- zrb/xcom/xcom.py +10 -0
- {zrb-1.21.17.dist-info → zrb-1.21.33.dist-info}/METADATA +3 -3
- {zrb-1.21.17.dist-info → zrb-1.21.33.dist-info}/RECORD +40 -32
- zrb/task/llm/history_summarization_tool.py +0 -24
- {zrb-1.21.17.dist-info → zrb-1.21.33.dist-info}/WHEEL +0 -0
- {zrb-1.21.17.dist-info → zrb-1.21.33.dist-info}/entry_points.txt +0 -0
zrb/builtin/llm/tool/file.py
CHANGED
|
@@ -89,6 +89,10 @@ def list_files(
|
|
|
89
89
|
"""
|
|
90
90
|
Lists files recursively up to a specified depth.
|
|
91
91
|
|
|
92
|
+
**EFFICIENCY TIP:**
|
|
93
|
+
Do NOT use this tool if you already know the file path (e.g., from the user's prompt).
|
|
94
|
+
Use `read_from_file` directly in that case. Only use this to explore directory structures.
|
|
95
|
+
|
|
92
96
|
Example:
|
|
93
97
|
list_files(path='src', include_hidden=False, depth=2)
|
|
94
98
|
|
|
@@ -128,7 +132,6 @@ def list_files(
|
|
|
128
132
|
if (include_hidden or not _is_hidden(d))
|
|
129
133
|
and not is_excluded(d, patterns_to_exclude)
|
|
130
134
|
]
|
|
131
|
-
|
|
132
135
|
for filename in files:
|
|
133
136
|
if (include_hidden or not _is_hidden(filename)) and not is_excluded(
|
|
134
137
|
filename, patterns_to_exclude
|
|
@@ -180,7 +183,14 @@ def read_from_file(
|
|
|
180
183
|
"""
|
|
181
184
|
Reads content from one or more files, optionally specifying line ranges.
|
|
182
185
|
|
|
186
|
+
**EFFICIENCY TIP:**
|
|
187
|
+
For source code or configuration files, prefer reading the **entire file** at once
|
|
188
|
+
to ensure you have full context (imports, class definitions, etc.).
|
|
189
|
+
Only use `start_line` and `end_line` for extremely large files (like logs) or
|
|
190
|
+
when you are certain only a specific section is needed.
|
|
191
|
+
|
|
183
192
|
Examples:
|
|
193
|
+
```
|
|
184
194
|
# Read entire content of a single file
|
|
185
195
|
read_from_file(file={'path': 'path/to/file.txt'})
|
|
186
196
|
|
|
@@ -193,6 +203,7 @@ def read_from_file(
|
|
|
193
203
|
{'path': 'path/to/file1.txt'},
|
|
194
204
|
{'path': 'path/to/file2.txt', 'start_line': 1, 'end_line': 5}
|
|
195
205
|
])
|
|
206
|
+
```
|
|
196
207
|
|
|
197
208
|
Args:
|
|
198
209
|
file (FileToRead | list[FileToRead]): A single file configuration or a list of them.
|
|
@@ -267,6 +278,7 @@ def write_to_file(
|
|
|
267
278
|
- Split larger content into multiple sequential calls (first 'w', then 'a').
|
|
268
279
|
|
|
269
280
|
Examples:
|
|
281
|
+
```
|
|
270
282
|
# Overwrite 'file.txt' with initial content
|
|
271
283
|
write_to_file(file={'path': 'path/to/file.txt', 'content': 'Initial content.'})
|
|
272
284
|
|
|
@@ -278,6 +290,7 @@ def write_to_file(
|
|
|
278
290
|
{'path': 'path/to/file1.txt', 'content': 'Content for file 1'},
|
|
279
291
|
{'path': 'path/to/file2.txt', 'content': 'Content for file 2', 'mode': 'w'}
|
|
280
292
|
])
|
|
293
|
+
```
|
|
281
294
|
|
|
282
295
|
Args:
|
|
283
296
|
file (FileToWrite | list[FileToWrite]): A single file configuration or a list of them.
|
|
@@ -434,6 +447,7 @@ def replace_in_file(
|
|
|
434
447
|
6. **DEFAULT:** Replaces **ALL** occurrences. Set `count=1` for first occurrence only.
|
|
435
448
|
|
|
436
449
|
Examples:
|
|
450
|
+
```
|
|
437
451
|
# Replace ALL occurrences
|
|
438
452
|
replace_in_file(file=[
|
|
439
453
|
{'path': 'file.txt', 'old_text': 'foo', 'new_text': 'bar'},
|
|
@@ -453,6 +467,7 @@ def replace_in_file(
|
|
|
453
467
|
'new_text': ' def new_fn():\n pass'
|
|
454
468
|
}
|
|
455
469
|
)
|
|
470
|
+
```
|
|
456
471
|
|
|
457
472
|
Args:
|
|
458
473
|
file: Single replacement config or list of them.
|
|
@@ -506,11 +521,17 @@ def replace_in_file(
|
|
|
506
521
|
|
|
507
522
|
|
|
508
523
|
async def analyze_file(
|
|
509
|
-
ctx: AnyContext, path: str, query: str,
|
|
524
|
+
ctx: AnyContext, path: str, query: str, token_threshold: int | None = None
|
|
510
525
|
) -> dict[str, Any]:
|
|
511
526
|
"""
|
|
512
527
|
Analyzes a file using a sub-agent for complex questions.
|
|
513
528
|
|
|
529
|
+
CRITICAL: The query must contain ALL necessary context, instructions, and information.
|
|
530
|
+
The sub-agent performing the analysis does NOT share your current conversation
|
|
531
|
+
history, memory, or global context.
|
|
532
|
+
The quality of analysis depends entirely on the query. Vague queries yield poor
|
|
533
|
+
results.
|
|
534
|
+
|
|
514
535
|
Example:
|
|
515
536
|
analyze_file(path='src/main.py', query='Summarize the main function.')
|
|
516
537
|
|
|
@@ -519,13 +540,13 @@ async def analyze_file(
|
|
|
519
540
|
path (str): The path to the file to analyze.
|
|
520
541
|
query (str): A specific analysis query with clear guidelines and
|
|
521
542
|
necessary information.
|
|
522
|
-
|
|
543
|
+
token_threshold (int | None): Max tokens.
|
|
523
544
|
|
|
524
545
|
Returns:
|
|
525
546
|
Analysis results.
|
|
526
547
|
"""
|
|
527
|
-
if
|
|
528
|
-
|
|
548
|
+
if token_threshold is None:
|
|
549
|
+
token_threshold = CFG.LLM_FILE_ANALYSIS_TOKEN_THRESHOLD
|
|
529
550
|
abs_path = os.path.abspath(os.path.expanduser(path))
|
|
530
551
|
if not os.path.exists(abs_path):
|
|
531
552
|
raise FileNotFoundError(f"File not found: {path}")
|
|
@@ -540,12 +561,17 @@ async def analyze_file(
|
|
|
540
561
|
),
|
|
541
562
|
system_prompt=CFG.LLM_FILE_EXTRACTOR_SYSTEM_PROMPT,
|
|
542
563
|
tools=[read_from_file, search_files],
|
|
564
|
+
auto_summarize=False,
|
|
565
|
+
remember_history=False,
|
|
566
|
+
yolo_mode=True,
|
|
543
567
|
)
|
|
544
568
|
payload = json.dumps(
|
|
545
569
|
{
|
|
546
570
|
"instruction": query,
|
|
547
571
|
"file_path": abs_path,
|
|
548
|
-
"file_content": llm_rate_limitter.clip_prompt(
|
|
572
|
+
"file_content": llm_rate_limitter.clip_prompt(
|
|
573
|
+
file_content, token_threshold
|
|
574
|
+
),
|
|
549
575
|
}
|
|
550
576
|
)
|
|
551
577
|
return await _analyze_file(ctx, payload)
|
zrb/builtin/llm/tool/note.py
CHANGED
|
@@ -5,9 +5,9 @@ from zrb.config.llm_context.config import llm_context_config
|
|
|
5
5
|
|
|
6
6
|
def read_long_term_note() -> str:
|
|
7
7
|
"""
|
|
8
|
-
Retrieves the GLOBAL
|
|
8
|
+
Retrieves the GLOBAL 🧠 Long Term Note shared across ALL sessions and projects.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Use this to recall user preferences, facts, and cross-project context.
|
|
11
11
|
|
|
12
12
|
Returns:
|
|
13
13
|
str: The current global note content.
|
|
@@ -18,16 +18,16 @@ def read_long_term_note() -> str:
|
|
|
18
18
|
|
|
19
19
|
def write_long_term_note(content: str) -> str:
|
|
20
20
|
"""
|
|
21
|
-
Persists CRITICAL facts to the GLOBAL
|
|
21
|
+
Persists CRITICAL facts to the GLOBAL 🧠 Long Term Note.
|
|
22
22
|
|
|
23
|
-
USE EAGERLY to save:
|
|
23
|
+
USE EAGERLY to save or update:
|
|
24
24
|
- User preferences (e.g., "I prefer Python", "No unit tests").
|
|
25
25
|
- User information (e.g., user name, user email address).
|
|
26
26
|
- Important facts (e.g., "My API key is in .env").
|
|
27
27
|
- Cross-project goals.
|
|
28
28
|
- Anything that will be useful for future interaction across projects.
|
|
29
29
|
|
|
30
|
-
WARNING: This OVERWRITES the entire
|
|
30
|
+
WARNING: This OVERWRITES the entire Long Term Note.
|
|
31
31
|
|
|
32
32
|
Args:
|
|
33
33
|
content (str): The text to strictly memorize.
|
|
@@ -41,7 +41,7 @@ def write_long_term_note(content: str) -> str:
|
|
|
41
41
|
|
|
42
42
|
def read_contextual_note(path: str | None = None) -> str:
|
|
43
43
|
"""
|
|
44
|
-
Retrieves LOCAL
|
|
44
|
+
Retrieves LOCAL 📝 Contextual Note specific to a directory path.
|
|
45
45
|
|
|
46
46
|
Use to recall project-specific architecture, code summaries, or past decisions
|
|
47
47
|
relevant to the current working location.
|
|
@@ -61,15 +61,15 @@ def read_contextual_note(path: str | None = None) -> str:
|
|
|
61
61
|
|
|
62
62
|
def write_contextual_note(content: str, path: str | None = None) -> str:
|
|
63
63
|
"""
|
|
64
|
-
Persists LOCAL facts specific to a
|
|
64
|
+
Persists LOCAL facts specific to a directory into 📝 Contextual Note.
|
|
65
65
|
|
|
66
|
-
USE EAGERLY to save:
|
|
66
|
+
USE EAGERLY to save or update:
|
|
67
67
|
- Architectural patterns for this project/directory.
|
|
68
68
|
- Summaries of large files or directories.
|
|
69
69
|
- Specific guidelines for this project.
|
|
70
70
|
- Anything related to this directory that will be useful for future interaction.
|
|
71
71
|
|
|
72
|
-
WARNING: This OVERWRITES the
|
|
72
|
+
WARNING: This OVERWRITES the entire Contextual Note for a directory.
|
|
73
73
|
|
|
74
74
|
Args:
|
|
75
75
|
content (str): The text to memorize for this location.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file makes the directory a Python package
|
|
@@ -0,0 +1,66 @@
|
|
|
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 an internet search using Brave Search.
|
|
16
|
+
|
|
17
|
+
Use this tool to find up-to-date information, answer questions about current events,
|
|
18
|
+
or research topics using a search engine.
|
|
19
|
+
|
|
20
|
+
**EFFICIENCY TIP:**
|
|
21
|
+
Make your `query` specific and keyword-rich to get the best results in a single call.
|
|
22
|
+
Avoid vague queries that require follow-up searches.
|
|
23
|
+
Bad: "new python features"
|
|
24
|
+
Good: "python 3.12 new features list release date"
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
query (str): The natural language search query (e.g., 'Soto Madura').
|
|
28
|
+
Do NOT include instructions, meta-talk, or internal reasoning.
|
|
29
|
+
Use concise terms as a human would in a search engine.
|
|
30
|
+
page (int): Search result page number. Defaults to 1.
|
|
31
|
+
safe_search (str | None): Safety setting. 'strict', 'moderate', or 'off'.
|
|
32
|
+
If None, uses the system default configuration.
|
|
33
|
+
language (str | None): Language code (e.g., 'en').
|
|
34
|
+
If None, uses the system default configuration.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: Summary of search results (titles, links, snippets).
|
|
38
|
+
"""
|
|
39
|
+
if safe_search is None:
|
|
40
|
+
safe_search = CFG.BRAVE_API_SAFE
|
|
41
|
+
if language is None:
|
|
42
|
+
language = CFG.BRAVE_API_LANG
|
|
43
|
+
|
|
44
|
+
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" # noqa
|
|
45
|
+
|
|
46
|
+
response = requests.get(
|
|
47
|
+
"https://api.search.brave.com/res/v1/web/search",
|
|
48
|
+
headers={
|
|
49
|
+
"User-Agent": user_agent,
|
|
50
|
+
"Accept": "application/json",
|
|
51
|
+
"x-subscription-token": CFG.BRAVE_API_KEY,
|
|
52
|
+
},
|
|
53
|
+
params={
|
|
54
|
+
"q": query,
|
|
55
|
+
"count": "10",
|
|
56
|
+
"offset": (page - 1) * 10,
|
|
57
|
+
"safesearch": safe_search,
|
|
58
|
+
"search_lang": language,
|
|
59
|
+
"summary": "true",
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
if response.status_code != 200:
|
|
63
|
+
raise Exception(
|
|
64
|
+
f"Error: Unable to retrieve search results (status code: {response.status_code})"
|
|
65
|
+
)
|
|
66
|
+
return response.json()
|
|
@@ -0,0 +1,61 @@
|
|
|
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 an internet search using SearXNG.
|
|
16
|
+
|
|
17
|
+
Use this tool to find up-to-date information, answer questions about current events,
|
|
18
|
+
or research topics using a search engine.
|
|
19
|
+
|
|
20
|
+
**EFFICIENCY TIP:**
|
|
21
|
+
Make your `query` specific and keyword-rich to get the best results in a single call.
|
|
22
|
+
Avoid vague queries that require follow-up searches.
|
|
23
|
+
Bad: "new python features"
|
|
24
|
+
Good: "python 3.12 new features list release date"
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
query (str): The natural language search query (e.g., 'Soto Madura').
|
|
28
|
+
Do NOT include instructions, meta-talk, or internal reasoning.
|
|
29
|
+
Use concise terms as a human would in a search engine.
|
|
30
|
+
page (int): Search result page number. Defaults to 1.
|
|
31
|
+
safe_search (int | None): Safety setting. 0 (None), 1 (Moderate), 2 (Strict).
|
|
32
|
+
If None, uses the system default configuration.
|
|
33
|
+
language (str | None): Language code (e.g., 'en').
|
|
34
|
+
If None, uses the system default configuration.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: Summary of search results (titles, links, snippets).
|
|
38
|
+
"""
|
|
39
|
+
if safe_search is None:
|
|
40
|
+
safe_search = CFG.SEARXNG_SAFE
|
|
41
|
+
if language is None:
|
|
42
|
+
language = CFG.SEARXNG_LANG
|
|
43
|
+
|
|
44
|
+
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" # noqa
|
|
45
|
+
|
|
46
|
+
response = requests.get(
|
|
47
|
+
url=f"{CFG.SEARXNG_BASE_URL}/search",
|
|
48
|
+
headers={"User-Agent": user_agent},
|
|
49
|
+
params={
|
|
50
|
+
"q": query,
|
|
51
|
+
"format": "json",
|
|
52
|
+
"pageno": page,
|
|
53
|
+
"safesearch": safe_search,
|
|
54
|
+
"language": language,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
if response.status_code != 200:
|
|
58
|
+
raise Exception(
|
|
59
|
+
f"Error: Unable to retrieve search results (status code: {response.status_code})"
|
|
60
|
+
)
|
|
61
|
+
return response.json()
|
|
@@ -0,0 +1,61 @@
|
|
|
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 an internet search using SerpApi (Google).
|
|
16
|
+
|
|
17
|
+
Use this tool to find up-to-date information, answer questions about current events,
|
|
18
|
+
or research topics using a search engine.
|
|
19
|
+
|
|
20
|
+
**EFFICIENCY TIP:**
|
|
21
|
+
Make your `query` specific and keyword-rich to get the best results in a single call.
|
|
22
|
+
Avoid vague queries that require follow-up searches.
|
|
23
|
+
Bad: "new python features"
|
|
24
|
+
Good: "python 3.12 new features list release date"
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
query (str): The natural language search query (e.g., 'Soto Madura').
|
|
28
|
+
Do NOT include instructions, meta-talk, or internal reasoning.
|
|
29
|
+
Use concise terms as a human would in a search engine.
|
|
30
|
+
page (int): Search result page number. Defaults to 1.
|
|
31
|
+
safe_search (str | None): Safety setting. 'active' or 'off'.
|
|
32
|
+
If None, uses the system default configuration.
|
|
33
|
+
language (str | None): Two-letter language code (e.g., 'en', 'id').
|
|
34
|
+
If None, uses the system default configuration.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: Summary of search results (titles, links, snippets).
|
|
38
|
+
"""
|
|
39
|
+
if safe_search is None:
|
|
40
|
+
safe_search = CFG.SERPAPI_SAFE
|
|
41
|
+
if language is None:
|
|
42
|
+
language = CFG.SERPAPI_LANG
|
|
43
|
+
|
|
44
|
+
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" # noqa
|
|
45
|
+
|
|
46
|
+
response = requests.get(
|
|
47
|
+
"https://serpapi.com/search",
|
|
48
|
+
headers={"User-Agent": user_agent},
|
|
49
|
+
params={
|
|
50
|
+
"q": query,
|
|
51
|
+
"start": (page - 1) * 10,
|
|
52
|
+
"hl": language,
|
|
53
|
+
"safe": safe_search,
|
|
54
|
+
"api_key": CFG.SERPAPI_KEY,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
if response.status_code != 200:
|
|
58
|
+
raise Exception(
|
|
59
|
+
f"Error: Unable to retrieve search results (status code: {response.status_code})"
|
|
60
|
+
)
|
|
61
|
+
return response.json()
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from collections.abc import Callable
|
|
2
3
|
from textwrap import dedent
|
|
3
4
|
from typing import TYPE_CHECKING, Any, Coroutine
|
|
4
5
|
|
|
5
6
|
from zrb.context.any_context import AnyContext
|
|
6
|
-
from zrb.task.llm.agent import create_agent_instance
|
|
7
|
+
from zrb.task.llm.agent import create_agent_instance
|
|
8
|
+
from zrb.task.llm.agent_runner import run_agent_iteration
|
|
7
9
|
from zrb.task.llm.config import get_model, get_model_settings
|
|
8
10
|
from zrb.task.llm.prompt import get_system_and_user_prompt
|
|
11
|
+
from zrb.task.llm.subagent_conversation_history import (
|
|
12
|
+
get_ctx_subagent_history,
|
|
13
|
+
set_ctx_subagent_history,
|
|
14
|
+
)
|
|
9
15
|
|
|
10
16
|
if TYPE_CHECKING:
|
|
11
17
|
from pydantic_ai import Tool
|
|
18
|
+
from pydantic_ai._agent_graph import HistoryProcessor
|
|
12
19
|
from pydantic_ai.models import Model
|
|
13
20
|
from pydantic_ai.settings import ModelSettings
|
|
14
21
|
from pydantic_ai.toolsets import AbstractToolset
|
|
@@ -25,8 +32,12 @@ def create_sub_agent_tool(
|
|
|
25
32
|
tools: "list[ToolOrCallable]" = [],
|
|
26
33
|
toolsets: list["AbstractToolset[None]"] = [],
|
|
27
34
|
yolo_mode: bool | list[str] | None = None,
|
|
35
|
+
history_processors: list["HistoryProcessor"] | None = None,
|
|
28
36
|
log_indent_level: int = 2,
|
|
29
|
-
|
|
37
|
+
agent_name: str | None = None,
|
|
38
|
+
auto_summarize: bool = True,
|
|
39
|
+
remember_history: bool = True,
|
|
40
|
+
) -> Callable[[AnyContext, str], Coroutine[Any, Any, Any]]:
|
|
30
41
|
"""
|
|
31
42
|
Create a tool that is another AI agent, capable of handling complex, multi-step sub-tasks.
|
|
32
43
|
|
|
@@ -52,8 +63,10 @@ def create_sub_agent_tool(
|
|
|
52
63
|
An asynchronous function that serves as the sub-agent tool. When called, it runs the
|
|
53
64
|
sub-agent with a given query and returns its final result.
|
|
54
65
|
"""
|
|
66
|
+
if agent_name is None:
|
|
67
|
+
agent_name = f"{tool_name}_agent"
|
|
55
68
|
|
|
56
|
-
async def run_sub_agent(ctx: AnyContext, query: str) ->
|
|
69
|
+
async def run_sub_agent(ctx: AnyContext, query: str) -> Any:
|
|
57
70
|
"""
|
|
58
71
|
Runs the sub-agent with the given query.
|
|
59
72
|
"""
|
|
@@ -72,7 +85,6 @@ def create_sub_agent_tool(
|
|
|
72
85
|
ctx=ctx,
|
|
73
86
|
model_settings_attr=model_settings,
|
|
74
87
|
)
|
|
75
|
-
|
|
76
88
|
if system_prompt is None:
|
|
77
89
|
resolved_system_prompt, query = get_system_and_user_prompt(
|
|
78
90
|
ctx=ctx,
|
|
@@ -92,24 +104,32 @@ def create_sub_agent_tool(
|
|
|
92
104
|
tools=tools,
|
|
93
105
|
toolsets=toolsets,
|
|
94
106
|
yolo_mode=yolo_mode,
|
|
107
|
+
history_processors=history_processors,
|
|
108
|
+
auto_summarize=auto_summarize,
|
|
95
109
|
)
|
|
96
|
-
|
|
97
110
|
sub_agent_run = None
|
|
98
111
|
# Run the sub-agent iteration
|
|
99
|
-
|
|
112
|
+
history_list = (
|
|
113
|
+
get_ctx_subagent_history(ctx, agent_name) if remember_history else []
|
|
114
|
+
)
|
|
100
115
|
sub_agent_run = await run_agent_iteration(
|
|
101
116
|
ctx=ctx,
|
|
102
117
|
agent=sub_agent_agent,
|
|
103
118
|
user_prompt=query,
|
|
104
119
|
attachments=[],
|
|
105
|
-
history_list=
|
|
120
|
+
history_list=history_list,
|
|
106
121
|
log_indent_level=log_indent_level,
|
|
107
122
|
)
|
|
108
|
-
|
|
109
123
|
# Return the sub-agent's final message content
|
|
110
124
|
if sub_agent_run and sub_agent_run.result:
|
|
111
125
|
# Return the final message content
|
|
112
|
-
|
|
126
|
+
if remember_history:
|
|
127
|
+
set_ctx_subagent_history(
|
|
128
|
+
ctx,
|
|
129
|
+
agent_name,
|
|
130
|
+
json.loads(sub_agent_run.result.all_messages_json()),
|
|
131
|
+
)
|
|
132
|
+
return sub_agent_run.result.output
|
|
113
133
|
ctx.log_warning("Sub-agent run did not produce a result.")
|
|
114
134
|
raise ValueError(f"{tool_name} not returning any result")
|
|
115
135
|
|
|
@@ -123,7 +143,7 @@ def create_sub_agent_tool(
|
|
|
123
143
|
query (str): The query or task for the sub-agent.
|
|
124
144
|
|
|
125
145
|
Returns:
|
|
126
|
-
|
|
146
|
+
Any: The final response or result from the sub-agent.
|
|
127
147
|
"""
|
|
128
148
|
).strip()
|
|
129
149
|
|
zrb/builtin/llm/tool/web.py
CHANGED
|
@@ -2,6 +2,7 @@ from collections.abc import Callable
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
from urllib.parse import urljoin
|
|
4
4
|
|
|
5
|
+
from zrb.builtin.llm.tool.search import brave, searxng, serpapi
|
|
5
6
|
from zrb.config.config import CFG
|
|
6
7
|
from zrb.config.llm_config import llm_config
|
|
7
8
|
|
|
@@ -13,6 +14,11 @@ async def open_web_page(url: str) -> dict[str, Any]:
|
|
|
13
14
|
Fetches, parses, and converts a web page to readable Markdown.
|
|
14
15
|
Preserves semantic structure, removes non-essentials, and extracts all absolute links.
|
|
15
16
|
|
|
17
|
+
**EFFICIENCY TIP:**
|
|
18
|
+
Use this tool to read the full content of a specific search result or article.
|
|
19
|
+
It returns clean Markdown and a list of links, which is perfect for deep-diving
|
|
20
|
+
into a topic without navigating a browser UI.
|
|
21
|
+
|
|
16
22
|
Example:
|
|
17
23
|
open_web_page(url='https://www.example.com/article')
|
|
18
24
|
|
|
@@ -30,78 +36,17 @@ async def open_web_page(url: str) -> dict[str, Any]:
|
|
|
30
36
|
def create_search_internet_tool() -> Callable:
|
|
31
37
|
if llm_config.default_search_internet_tool is not None:
|
|
32
38
|
return llm_config.default_search_internet_tool
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
page (int, optional): Search result page number. Defaults to 1.
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
dict: Summary of search results (titles, links, snippets).
|
|
48
|
-
"""
|
|
49
|
-
import requests
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
CFG.SEARCH_INTERNET_METHOD.strip().lower() == "serpapi"
|
|
53
|
-
and CFG.SERPAPI_KEY != ""
|
|
54
|
-
):
|
|
55
|
-
response = requests.get(
|
|
56
|
-
"https://serpapi.com/search",
|
|
57
|
-
headers={"User-Agent": _DEFAULT_USER_AGENT},
|
|
58
|
-
params={
|
|
59
|
-
"q": query,
|
|
60
|
-
"start": (page - 1) * 10,
|
|
61
|
-
"hl": CFG.SERPAPI_LANG,
|
|
62
|
-
"safe": CFG.SERPAPI_SAFE,
|
|
63
|
-
"api_key": CFG.SERPAPI_KEY,
|
|
64
|
-
},
|
|
65
|
-
)
|
|
66
|
-
elif (
|
|
67
|
-
CFG.SEARCH_INTERNET_METHOD.strip().lower() == "brave"
|
|
68
|
-
and CFG.BRAVE_API_KEY != ""
|
|
69
|
-
):
|
|
70
|
-
response = requests.get(
|
|
71
|
-
"https://api.search.brave.com/res/v1/web/search",
|
|
72
|
-
headers={
|
|
73
|
-
"User-Agent": _DEFAULT_USER_AGENT,
|
|
74
|
-
"Accept": "application/json",
|
|
75
|
-
"x-subscription-token": CFG.BRAVE_API_KEY,
|
|
76
|
-
},
|
|
77
|
-
params={
|
|
78
|
-
"q": query,
|
|
79
|
-
"count": "10",
|
|
80
|
-
"offset": (page - 1) * 10,
|
|
81
|
-
"safesearch": CFG.BRAVE_API_SAFE,
|
|
82
|
-
"search_lang": CFG.BRAVE_API_LANG,
|
|
83
|
-
"summary": "true",
|
|
84
|
-
},
|
|
85
|
-
)
|
|
86
|
-
else:
|
|
87
|
-
response = requests.get(
|
|
88
|
-
url=f"{CFG.SEARXNG_BASE_URL}/search",
|
|
89
|
-
headers={"User-Agent": _DEFAULT_USER_AGENT},
|
|
90
|
-
params={
|
|
91
|
-
"q": query,
|
|
92
|
-
"format": "json",
|
|
93
|
-
"pageno": page,
|
|
94
|
-
"safesearch": CFG.SEARXNG_SAFE,
|
|
95
|
-
"language": CFG.SEARXNG_LANG,
|
|
96
|
-
},
|
|
97
|
-
)
|
|
98
|
-
if response.status_code != 200:
|
|
99
|
-
raise Exception(
|
|
100
|
-
f"Error: Unable to retrieve search results (status code: {response.status_code})" # noqa
|
|
101
|
-
)
|
|
102
|
-
return response.json()
|
|
103
|
-
|
|
104
|
-
return search_internet
|
|
39
|
+
if (
|
|
40
|
+
CFG.SEARCH_INTERNET_METHOD.strip().lower() == "serpapi"
|
|
41
|
+
and CFG.SERPAPI_KEY != ""
|
|
42
|
+
):
|
|
43
|
+
return serpapi.search_internet
|
|
44
|
+
if (
|
|
45
|
+
CFG.SEARCH_INTERNET_METHOD.strip().lower() == "brave"
|
|
46
|
+
and CFG.BRAVE_API_KEY != ""
|
|
47
|
+
):
|
|
48
|
+
return brave.search_internet
|
|
49
|
+
return searxng.search_internet
|
|
105
50
|
|
|
106
51
|
|
|
107
52
|
async def _fetch_page_content(url: str) -> tuple[str, list[str]]:
|