zrb 1.10.2__py3-none-any.whl → 1.12.0__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/builtin/llm/chat_session.py +42 -14
- zrb/builtin/llm/llm_ask.py +11 -0
- zrb/builtin/llm/tool/file.py +2 -2
- zrb/config/config.py +31 -80
- zrb/config/default_prompt/file_extractor_system_prompt.md +12 -0
- zrb/config/default_prompt/interactive_system_prompt.md +31 -0
- zrb/config/default_prompt/persona.md +1 -0
- zrb/config/default_prompt/repo_extractor_system_prompt.md +112 -0
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +10 -0
- zrb/config/default_prompt/summarization_prompt.md +42 -0
- zrb/config/default_prompt/system_prompt.md +28 -0
- zrb/config/llm_config.py +89 -279
- zrb/config/llm_context/config.py +74 -0
- zrb/config/llm_context/config_handler.py +238 -0
- zrb/context/any_shared_context.py +10 -0
- zrb/context/context.py +8 -0
- zrb/context/shared_context.py +9 -0
- zrb/runner/web_route/task_session_api_route.py +1 -1
- zrb/task/llm/agent.py +2 -2
- zrb/task/llm/conversation_history_model.py +78 -226
- zrb/task/llm/default_workflow/coding.md +24 -0
- zrb/task/llm/default_workflow/copywriting.md +17 -0
- zrb/task/llm/default_workflow/researching.md +18 -0
- zrb/task/llm/history_summarization.py +6 -6
- zrb/task/llm/prompt.py +92 -41
- zrb/task/llm/tool_wrapper.py +20 -14
- zrb/task/llm_task.py +19 -23
- zrb/util/callable.py +23 -0
- zrb/util/llm/prompt.py +42 -6
- {zrb-1.10.2.dist-info → zrb-1.12.0.dist-info}/METADATA +2 -2
- {zrb-1.10.2.dist-info → zrb-1.12.0.dist-info}/RECORD +33 -20
- {zrb-1.10.2.dist-info → zrb-1.12.0.dist-info}/WHEEL +0 -0
- {zrb-1.10.2.dist-info → zrb-1.12.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,238 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
from typing import Callable, Generator, NamedTuple
|
4
|
+
|
5
|
+
|
6
|
+
class Section(NamedTuple):
|
7
|
+
name: str
|
8
|
+
key: str
|
9
|
+
content: str
|
10
|
+
config_file: str
|
11
|
+
|
12
|
+
|
13
|
+
def _parse_config_file(
|
14
|
+
config_file: str, lines: list[str]
|
15
|
+
) -> Generator[Section, None, None]:
|
16
|
+
"""
|
17
|
+
Parses a config file's lines, yielding sections.
|
18
|
+
It correctly handles markdown code fences.
|
19
|
+
"""
|
20
|
+
any_header_pattern = re.compile(r"^# (\w+):\s*(.*)")
|
21
|
+
fence_pattern = re.compile(r"^([`~]{3,})")
|
22
|
+
fence_stack = []
|
23
|
+
active_section_name = None
|
24
|
+
active_section_key = None
|
25
|
+
active_section_content = []
|
26
|
+
|
27
|
+
for line in lines:
|
28
|
+
stripped_line = line.strip()
|
29
|
+
fence_match = fence_pattern.match(stripped_line)
|
30
|
+
|
31
|
+
if fence_match:
|
32
|
+
current_fence = fence_match.group(1)
|
33
|
+
if (
|
34
|
+
fence_stack
|
35
|
+
and fence_stack[-1][0] == current_fence[0]
|
36
|
+
and len(current_fence) >= len(fence_stack[-1])
|
37
|
+
):
|
38
|
+
fence_stack.pop()
|
39
|
+
else:
|
40
|
+
fence_stack.append(current_fence)
|
41
|
+
|
42
|
+
if fence_stack:
|
43
|
+
if active_section_key is not None:
|
44
|
+
active_section_content.append(line)
|
45
|
+
continue
|
46
|
+
|
47
|
+
match = any_header_pattern.match(line)
|
48
|
+
if match:
|
49
|
+
if active_section_key is not None:
|
50
|
+
content = "".join(active_section_content).strip()
|
51
|
+
if content:
|
52
|
+
yield Section(
|
53
|
+
name=active_section_name,
|
54
|
+
key=active_section_key,
|
55
|
+
content=content,
|
56
|
+
config_file=config_file,
|
57
|
+
)
|
58
|
+
|
59
|
+
active_section_name = match.group(1)
|
60
|
+
active_section_key = match.group(2).strip()
|
61
|
+
active_section_content = []
|
62
|
+
elif active_section_key is not None:
|
63
|
+
active_section_content.append(line)
|
64
|
+
|
65
|
+
if active_section_key is not None:
|
66
|
+
content = "".join(active_section_content).strip()
|
67
|
+
if content:
|
68
|
+
yield Section(
|
69
|
+
name=active_section_name,
|
70
|
+
key=active_section_key,
|
71
|
+
content=content,
|
72
|
+
config_file=config_file,
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
def _get_config_file_hierarchy(path: str, config_file_name: str) -> list[str]:
|
77
|
+
"""Finds all config files from a given path up to the home directory."""
|
78
|
+
config_files = []
|
79
|
+
home_dir = os.path.expanduser("~")
|
80
|
+
current_path = os.path.abspath(path)
|
81
|
+
while True:
|
82
|
+
config_path = os.path.join(current_path, config_file_name)
|
83
|
+
if os.path.exists(config_path):
|
84
|
+
config_files.append(config_path)
|
85
|
+
if current_path == home_dir:
|
86
|
+
break
|
87
|
+
parent = os.path.dirname(current_path)
|
88
|
+
if parent == current_path: # Reached root
|
89
|
+
break
|
90
|
+
current_path = parent
|
91
|
+
return config_files
|
92
|
+
|
93
|
+
|
94
|
+
class LLMContextConfigHandler:
|
95
|
+
"""Handles the logic for a specific section of the config."""
|
96
|
+
|
97
|
+
def __init__(
|
98
|
+
self,
|
99
|
+
section_name: str,
|
100
|
+
config_file_name: str = "ZRB.md",
|
101
|
+
filter_section_func: Callable[[str, str], bool] | None = None,
|
102
|
+
resolve_section_path: bool = True,
|
103
|
+
):
|
104
|
+
self._section_name = section_name
|
105
|
+
self._config_file_name = config_file_name
|
106
|
+
self._filter_func = filter_section_func
|
107
|
+
self._resolve_section_path = resolve_section_path
|
108
|
+
|
109
|
+
def _include_section(self, section_path: str, base_path: str) -> bool:
|
110
|
+
if self._filter_func:
|
111
|
+
return self._filter_func(section_path, base_path)
|
112
|
+
return True
|
113
|
+
|
114
|
+
def get_section(self, cwd: str) -> dict[str, str]:
|
115
|
+
"""Gathers all relevant sections for a given path."""
|
116
|
+
abs_path = os.path.abspath(cwd)
|
117
|
+
all_sections = {}
|
118
|
+
config_files = _get_config_file_hierarchy(abs_path, self._config_file_name)
|
119
|
+
|
120
|
+
for config_file in reversed(config_files):
|
121
|
+
if not os.path.exists(config_file):
|
122
|
+
continue
|
123
|
+
with open(config_file, "r") as f:
|
124
|
+
lines = f.readlines()
|
125
|
+
|
126
|
+
for section in _parse_config_file(config_file, lines):
|
127
|
+
if section.name != self._section_name:
|
128
|
+
continue
|
129
|
+
|
130
|
+
config_dir = os.path.dirname(section.config_file)
|
131
|
+
key = (
|
132
|
+
os.path.abspath(os.path.join(config_dir, section.key))
|
133
|
+
if self._resolve_section_path
|
134
|
+
else section.key
|
135
|
+
)
|
136
|
+
|
137
|
+
if self._include_section(key, abs_path):
|
138
|
+
if key in all_sections:
|
139
|
+
all_sections[key] = f"{all_sections[key]}\n{section.content}"
|
140
|
+
else:
|
141
|
+
all_sections[key] = section.content
|
142
|
+
|
143
|
+
return all_sections
|
144
|
+
|
145
|
+
def add_to_section(self, content: str, key: str, cwd: str):
|
146
|
+
"""Adds content to a section block in the nearest configuration file."""
|
147
|
+
abs_search_path = os.path.abspath(cwd)
|
148
|
+
config_files = _get_config_file_hierarchy(
|
149
|
+
abs_search_path, self._config_file_name
|
150
|
+
)
|
151
|
+
closest_config_file = (
|
152
|
+
config_files[0]
|
153
|
+
if config_files
|
154
|
+
else os.path.join(os.path.expanduser("~"), self._config_file_name)
|
155
|
+
)
|
156
|
+
|
157
|
+
config_dir = os.path.dirname(closest_config_file)
|
158
|
+
header_key = key
|
159
|
+
if self._resolve_section_path and os.path.isabs(key):
|
160
|
+
if key == config_dir:
|
161
|
+
header_key = "."
|
162
|
+
elif key.startswith(config_dir):
|
163
|
+
header_key = f"./{os.path.relpath(key, config_dir)}"
|
164
|
+
header = f"# {self._section_name}: {header_key}"
|
165
|
+
new_content = content.strip()
|
166
|
+
lines = []
|
167
|
+
if os.path.exists(closest_config_file):
|
168
|
+
with open(closest_config_file, "r") as f:
|
169
|
+
lines = f.readlines()
|
170
|
+
header_index = next(
|
171
|
+
(i for i, line in enumerate(lines) if line.strip() == header), -1
|
172
|
+
)
|
173
|
+
if header_index != -1:
|
174
|
+
insert_index = len(lines)
|
175
|
+
for i in range(header_index + 1, len(lines)):
|
176
|
+
if re.match(r"^# \w+:", lines[i].strip()):
|
177
|
+
insert_index = i
|
178
|
+
break
|
179
|
+
if insert_index > 0 and lines[insert_index - 1].strip():
|
180
|
+
lines.insert(insert_index, f"\n{new_content}\n")
|
181
|
+
else:
|
182
|
+
lines.insert(insert_index, f"{new_content}\n")
|
183
|
+
else:
|
184
|
+
if lines and lines[-1].strip():
|
185
|
+
lines.append("\n\n")
|
186
|
+
lines.append(f"{header}\n")
|
187
|
+
lines.append(f"{new_content}\n")
|
188
|
+
with open(closest_config_file, "w") as f:
|
189
|
+
f.writelines(lines)
|
190
|
+
|
191
|
+
def remove_from_section(self, content: str, key: str, cwd: str) -> bool:
|
192
|
+
"""Removes content from a section block in all relevant config files."""
|
193
|
+
abs_search_path = os.path.abspath(cwd)
|
194
|
+
config_files = _get_config_file_hierarchy(
|
195
|
+
abs_search_path, self._config_file_name
|
196
|
+
)
|
197
|
+
content_to_remove = content.strip()
|
198
|
+
was_removed = False
|
199
|
+
for config_file_path in config_files:
|
200
|
+
if not os.path.exists(config_file_path):
|
201
|
+
continue
|
202
|
+
with open(config_file_path, "r") as f:
|
203
|
+
file_content = f.read()
|
204
|
+
config_dir = os.path.dirname(config_file_path)
|
205
|
+
header_key = key
|
206
|
+
if self._resolve_section_path and os.path.isabs(key):
|
207
|
+
if key == config_dir:
|
208
|
+
header_key = "."
|
209
|
+
elif key.startswith(config_dir):
|
210
|
+
header_key = f"./{os.path.relpath(key, config_dir)}"
|
211
|
+
header = f"# {self._section_name}: {header_key}"
|
212
|
+
# Use regex to find the section content
|
213
|
+
section_pattern = re.compile(
|
214
|
+
rf"^{re.escape(header)}\n(.*?)(?=\n# \w+:|\Z)",
|
215
|
+
re.DOTALL | re.MULTILINE,
|
216
|
+
)
|
217
|
+
match = section_pattern.search(file_content)
|
218
|
+
if not match:
|
219
|
+
continue
|
220
|
+
|
221
|
+
section_content = match.group(1)
|
222
|
+
# Remove the target content and handle surrounding newlines
|
223
|
+
new_section_content = section_content.replace(content_to_remove, "")
|
224
|
+
new_section_content = "\n".join(
|
225
|
+
line for line in new_section_content.splitlines() if line.strip()
|
226
|
+
)
|
227
|
+
|
228
|
+
if new_section_content != section_content.strip():
|
229
|
+
was_removed = True
|
230
|
+
# Reconstruct the file content
|
231
|
+
start = match.start(1)
|
232
|
+
end = match.end(1)
|
233
|
+
new_file_content = (
|
234
|
+
file_content[:start] + new_section_content + file_content[end:]
|
235
|
+
)
|
236
|
+
with open(config_file_path, "w") as f:
|
237
|
+
f.write(new_file_content)
|
238
|
+
return was_removed
|
@@ -18,6 +18,16 @@ class AnySharedContext(ABC):
|
|
18
18
|
rendering templates with additional data.
|
19
19
|
"""
|
20
20
|
|
21
|
+
@property
|
22
|
+
@abstractmethod
|
23
|
+
def is_web_mode(self) -> bool:
|
24
|
+
pass
|
25
|
+
|
26
|
+
@property
|
27
|
+
@abstractmethod
|
28
|
+
def is_tty(self) -> bool:
|
29
|
+
pass
|
30
|
+
|
21
31
|
@property
|
22
32
|
def input(self) -> DotDict:
|
23
33
|
pass
|
zrb/context/context.py
CHANGED
@@ -33,6 +33,14 @@ class Context(AnyContext):
|
|
33
33
|
class_name = self.__class__.__name__
|
34
34
|
return f"<{class_name} shared_ctx={self._shared_ctx}>"
|
35
35
|
|
36
|
+
@property
|
37
|
+
def is_web_mode(self) -> bool:
|
38
|
+
return self._shared_ctx.is_web_mode
|
39
|
+
|
40
|
+
@property
|
41
|
+
def is_tty(self) -> bool:
|
42
|
+
return self._shared_ctx.is_tty
|
43
|
+
|
36
44
|
@property
|
37
45
|
def input(self) -> DotDict:
|
38
46
|
return self._shared_ctx.input
|
zrb/context/shared_context.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import datetime
|
2
|
+
import sys
|
2
3
|
from typing import Any
|
3
4
|
|
4
5
|
from zrb.config.config import CFG
|
@@ -43,6 +44,14 @@ class SharedContext(AnySharedContext):
|
|
43
44
|
xcom = self._xcom
|
44
45
|
return f"<{class_name} input={input} args={args} xcom={xcom} env={env}>"
|
45
46
|
|
47
|
+
@property
|
48
|
+
def is_web_mode(self) -> bool:
|
49
|
+
return self.env.get("_ZRB_IS_WEB_MODE", "0") == "1"
|
50
|
+
|
51
|
+
@property
|
52
|
+
def is_tty(self) -> bool:
|
53
|
+
return sys.stdin.isatty()
|
54
|
+
|
46
55
|
@property
|
47
56
|
def input(self) -> DotDict:
|
48
57
|
return self._input
|
@@ -58,7 +58,7 @@ def serve_task_session_api(
|
|
58
58
|
session_name = residual_args[0] if residual_args else None
|
59
59
|
if not session_name:
|
60
60
|
shared_ctx = SharedContext(
|
61
|
-
env={**dict(os.environ), "
|
61
|
+
env={**dict(os.environ), "_ZRB_IS_WEB_MODE": "1"}
|
62
62
|
)
|
63
63
|
session = Session(shared_ctx=shared_ctx, root_group=root_group)
|
64
64
|
coro = asyncio.create_task(task.async_run(session, str_kwargs=inputs))
|
zrb/task/llm/agent.py
CHANGED
@@ -47,7 +47,7 @@ def create_agent_instance(
|
|
47
47
|
model=model,
|
48
48
|
system_prompt=system_prompt,
|
49
49
|
tools=tool_list,
|
50
|
-
|
50
|
+
toolsets=mcp_servers,
|
51
51
|
model_settings=model_settings,
|
52
52
|
retries=retries,
|
53
53
|
)
|
@@ -160,7 +160,7 @@ async def _run_single_agent_iteration(
|
|
160
160
|
else:
|
161
161
|
await llm_rate_limitter.throttle(agent_payload)
|
162
162
|
|
163
|
-
async with agent
|
163
|
+
async with agent:
|
164
164
|
async with agent.iter(
|
165
165
|
user_prompt=user_prompt,
|
166
166
|
message_history=ModelMessagesTypeAdapter.validate_python(history_list),
|