zrb 1.21.31__py3-none-any.whl → 1.21.43__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/builtin/llm/chat_completion.py +94 -84
- zrb/builtin/llm/chat_session.py +90 -30
- zrb/builtin/llm/chat_session_cmd.py +115 -22
- zrb/builtin/llm/chat_trigger.py +92 -5
- zrb/builtin/llm/history.py +14 -7
- zrb/builtin/llm/llm_ask.py +16 -7
- zrb/builtin/llm/tool/cli.py +34 -15
- zrb/builtin/llm/tool/file.py +14 -2
- zrb/builtin/llm/tool/search/brave.py +8 -2
- zrb/builtin/llm/tool/search/searxng.py +8 -2
- zrb/builtin/llm/tool/search/serpapi.py +8 -2
- zrb/builtin/llm/tool/sub_agent.py +4 -1
- zrb/builtin/llm/tool/web.py +5 -0
- zrb/builtin/llm/xcom_names.py +3 -0
- zrb/callback/callback.py +8 -1
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +6 -2
- zrb/config/default_prompt/interactive_system_prompt.md +15 -12
- zrb/config/default_prompt/system_prompt.md +16 -18
- zrb/config/llm_rate_limitter.py +36 -13
- zrb/context/context.py +11 -0
- zrb/input/option_input.py +30 -2
- zrb/task/base/context.py +25 -13
- zrb/task/base/execution.py +52 -47
- zrb/task/base/lifecycle.py +1 -1
- zrb/task/base_task.py +31 -45
- zrb/task/base_trigger.py +0 -1
- zrb/task/cmd_task.py +3 -0
- zrb/task/llm/agent.py +39 -31
- zrb/task/llm/agent_runner.py +65 -3
- zrb/task/llm/default_workflow/researching/workflow.md +2 -0
- zrb/task/llm/history_list.py +13 -0
- zrb/task/llm/history_processor.py +4 -13
- zrb/task/llm/print_node.py +79 -25
- zrb/task/llm/prompt.py +70 -40
- zrb/task/llm/tool_wrapper.py +4 -1
- zrb/task/llm/workflow.py +54 -15
- zrb/task/llm_task.py +87 -33
- zrb/task/rsync_task.py +2 -0
- zrb/util/cmd/command.py +33 -10
- zrb/util/match.py +71 -0
- zrb/util/run.py +3 -3
- {zrb-1.21.31.dist-info → zrb-1.21.43.dist-info}/METADATA +1 -1
- {zrb-1.21.31.dist-info → zrb-1.21.43.dist-info}/RECORD +46 -43
- {zrb-1.21.31.dist-info → zrb-1.21.43.dist-info}/WHEEL +0 -0
- {zrb-1.21.31.dist-info → zrb-1.21.43.dist-info}/entry_points.txt +0 -0
zrb/task/base_task.py
CHANGED
|
@@ -3,7 +3,7 @@ import inspect
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from zrb.attr.type import
|
|
6
|
+
from zrb.attr.type import fstring
|
|
7
7
|
from zrb.context.any_context import AnyContext
|
|
8
8
|
from zrb.env.any_env import AnyEnv
|
|
9
9
|
from zrb.input.any_input import AnyInput
|
|
@@ -55,7 +55,7 @@ class BaseTask(AnyTask):
|
|
|
55
55
|
input: list[AnyInput | None] | AnyInput | None = None,
|
|
56
56
|
env: list[AnyEnv | None] | AnyEnv | None = None,
|
|
57
57
|
action: fstring | Callable[[AnyContext], Any] | None = None,
|
|
58
|
-
execute_condition:
|
|
58
|
+
execute_condition: bool | str | Callable[[AnyContext], bool] = True,
|
|
59
59
|
retries: int = 2,
|
|
60
60
|
retry_period: float = 0,
|
|
61
61
|
readiness_check: list[AnyTask] | AnyTask | None = None,
|
|
@@ -68,9 +68,18 @@ class BaseTask(AnyTask):
|
|
|
68
68
|
fallback: list[AnyTask] | AnyTask | None = None,
|
|
69
69
|
successor: list[AnyTask] | AnyTask | None = None,
|
|
70
70
|
):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
# Optimized stack retrieval
|
|
72
|
+
frame = inspect.currentframe()
|
|
73
|
+
if frame is not None:
|
|
74
|
+
caller_frame = frame.f_back
|
|
75
|
+
self.__decl_file = (
|
|
76
|
+
caller_frame.f_code.co_filename if caller_frame else "unknown"
|
|
77
|
+
)
|
|
78
|
+
self.__decl_line = caller_frame.f_lineno if caller_frame else 0
|
|
79
|
+
else:
|
|
80
|
+
self.__decl_file = "unknown"
|
|
81
|
+
self.__decl_line = 0
|
|
82
|
+
|
|
74
83
|
self._name = name
|
|
75
84
|
self._color = color
|
|
76
85
|
self._icon = icon
|
|
@@ -80,10 +89,10 @@ class BaseTask(AnyTask):
|
|
|
80
89
|
self._envs = env
|
|
81
90
|
self._retries = retries
|
|
82
91
|
self._retry_period = retry_period
|
|
83
|
-
self._upstreams = upstream
|
|
84
|
-
self._fallbacks = fallback
|
|
85
|
-
self._successors = successor
|
|
86
|
-
self._readiness_checks = readiness_check
|
|
92
|
+
self._upstreams = self._ensure_task_list(upstream)
|
|
93
|
+
self._fallbacks = self._ensure_task_list(fallback)
|
|
94
|
+
self._successors = self._ensure_task_list(successor)
|
|
95
|
+
self._readiness_checks = self._ensure_task_list(readiness_check)
|
|
87
96
|
self._readiness_check_delay = readiness_check_delay
|
|
88
97
|
self._readiness_check_period = readiness_check_period
|
|
89
98
|
self._readiness_failure_threshold = readiness_failure_threshold
|
|
@@ -92,6 +101,13 @@ class BaseTask(AnyTask):
|
|
|
92
101
|
self._execute_condition = execute_condition
|
|
93
102
|
self._action = action
|
|
94
103
|
|
|
104
|
+
def _ensure_task_list(self, tasks: AnyTask | list[AnyTask] | None) -> list[AnyTask]:
|
|
105
|
+
if tasks is None:
|
|
106
|
+
return []
|
|
107
|
+
if isinstance(tasks, list):
|
|
108
|
+
return tasks
|
|
109
|
+
return [tasks]
|
|
110
|
+
|
|
95
111
|
def __repr__(self):
|
|
96
112
|
return f"<{self.__class__.__name__} name={self.name}>"
|
|
97
113
|
|
|
@@ -132,18 +148,10 @@ class BaseTask(AnyTask):
|
|
|
132
148
|
@property
|
|
133
149
|
def fallbacks(self) -> list[AnyTask]:
|
|
134
150
|
"""Returns the list of fallback tasks."""
|
|
135
|
-
|
|
136
|
-
return []
|
|
137
|
-
elif isinstance(self._fallbacks, list):
|
|
138
|
-
return self._fallbacks
|
|
139
|
-
return [self._fallbacks] # Assume single task
|
|
151
|
+
return self._fallbacks
|
|
140
152
|
|
|
141
153
|
def append_fallback(self, fallbacks: AnyTask | list[AnyTask]):
|
|
142
154
|
"""Appends fallback tasks, ensuring no duplicates."""
|
|
143
|
-
if self._fallbacks is None:
|
|
144
|
-
self._fallbacks = []
|
|
145
|
-
elif not isinstance(self._fallbacks, list):
|
|
146
|
-
self._fallbacks = [self._fallbacks]
|
|
147
155
|
to_add = fallbacks if isinstance(fallbacks, list) else [fallbacks]
|
|
148
156
|
for fb in to_add:
|
|
149
157
|
if fb not in self._fallbacks:
|
|
@@ -152,18 +160,10 @@ class BaseTask(AnyTask):
|
|
|
152
160
|
@property
|
|
153
161
|
def successors(self) -> list[AnyTask]:
|
|
154
162
|
"""Returns the list of successor tasks."""
|
|
155
|
-
|
|
156
|
-
return []
|
|
157
|
-
elif isinstance(self._successors, list):
|
|
158
|
-
return self._successors
|
|
159
|
-
return [self._successors] # Assume single task
|
|
163
|
+
return self._successors
|
|
160
164
|
|
|
161
165
|
def append_successor(self, successors: AnyTask | list[AnyTask]):
|
|
162
166
|
"""Appends successor tasks, ensuring no duplicates."""
|
|
163
|
-
if self._successors is None:
|
|
164
|
-
self._successors = []
|
|
165
|
-
elif not isinstance(self._successors, list):
|
|
166
|
-
self._successors = [self._successors]
|
|
167
167
|
to_add = successors if isinstance(successors, list) else [successors]
|
|
168
168
|
for succ in to_add:
|
|
169
169
|
if succ not in self._successors:
|
|
@@ -172,18 +172,10 @@ class BaseTask(AnyTask):
|
|
|
172
172
|
@property
|
|
173
173
|
def readiness_checks(self) -> list[AnyTask]:
|
|
174
174
|
"""Returns the list of readiness check tasks."""
|
|
175
|
-
|
|
176
|
-
return []
|
|
177
|
-
elif isinstance(self._readiness_checks, list):
|
|
178
|
-
return self._readiness_checks
|
|
179
|
-
return [self._readiness_checks] # Assume single task
|
|
175
|
+
return self._readiness_checks
|
|
180
176
|
|
|
181
177
|
def append_readiness_check(self, readiness_checks: AnyTask | list[AnyTask]):
|
|
182
178
|
"""Appends readiness check tasks, ensuring no duplicates."""
|
|
183
|
-
if self._readiness_checks is None:
|
|
184
|
-
self._readiness_checks = []
|
|
185
|
-
elif not isinstance(self._readiness_checks, list):
|
|
186
|
-
self._readiness_checks = [self._readiness_checks]
|
|
187
179
|
to_add = (
|
|
188
180
|
readiness_checks
|
|
189
181
|
if isinstance(readiness_checks, list)
|
|
@@ -196,18 +188,10 @@ class BaseTask(AnyTask):
|
|
|
196
188
|
@property
|
|
197
189
|
def upstreams(self) -> list[AnyTask]:
|
|
198
190
|
"""Returns the list of upstream tasks."""
|
|
199
|
-
|
|
200
|
-
return []
|
|
201
|
-
elif isinstance(self._upstreams, list):
|
|
202
|
-
return self._upstreams
|
|
203
|
-
return [self._upstreams] # Assume single task
|
|
191
|
+
return self._upstreams
|
|
204
192
|
|
|
205
193
|
def append_upstream(self, upstreams: AnyTask | list[AnyTask]):
|
|
206
194
|
"""Appends upstream tasks, ensuring no duplicates."""
|
|
207
|
-
if self._upstreams is None:
|
|
208
|
-
self._upstreams = []
|
|
209
|
-
elif not isinstance(self._upstreams, list):
|
|
210
|
-
self._upstreams = [self._upstreams]
|
|
211
195
|
to_add = upstreams if isinstance(upstreams, list) else [upstreams]
|
|
212
196
|
for up in to_add:
|
|
213
197
|
if up not in self._upstreams:
|
|
@@ -277,6 +261,8 @@ class BaseTask(AnyTask):
|
|
|
277
261
|
try:
|
|
278
262
|
# Delegate to the helper function for the default behavior
|
|
279
263
|
return await run_default_action(self, ctx)
|
|
264
|
+
except (KeyboardInterrupt, GeneratorExit):
|
|
265
|
+
raise
|
|
280
266
|
except BaseException as e:
|
|
281
267
|
additional_error_note = (
|
|
282
268
|
f"Task: {self.name} ({self.__decl_file}:{self.__decl_line})"
|
zrb/task/base_trigger.py
CHANGED
|
@@ -5,7 +5,6 @@ from typing import Any
|
|
|
5
5
|
from zrb.attr.type import fstring
|
|
6
6
|
from zrb.callback.any_callback import AnyCallback
|
|
7
7
|
from zrb.context.any_context import AnyContext
|
|
8
|
-
from zrb.context.any_shared_context import AnySharedContext
|
|
9
8
|
from zrb.context.shared_context import SharedContext
|
|
10
9
|
from zrb.dot_dict.dot_dict import DotDict
|
|
11
10
|
from zrb.env.any_env import AnyEnv
|
zrb/task/cmd_task.py
CHANGED
|
@@ -48,6 +48,7 @@ class CmdTask(BaseTask):
|
|
|
48
48
|
warn_unrecommended_command: bool | None = None,
|
|
49
49
|
max_output_line: int = 1000,
|
|
50
50
|
max_error_line: int = 1000,
|
|
51
|
+
execution_timeout: int = 3600,
|
|
51
52
|
is_interactive: bool = False,
|
|
52
53
|
execute_condition: BoolAttr = True,
|
|
53
54
|
retries: int = 2,
|
|
@@ -103,6 +104,7 @@ class CmdTask(BaseTask):
|
|
|
103
104
|
self._render_cwd = render_cwd
|
|
104
105
|
self._max_output_line = max_output_line
|
|
105
106
|
self._max_error_line = max_error_line
|
|
107
|
+
self._execution_timeout = execution_timeout
|
|
106
108
|
self._should_plain_print = plain_print
|
|
107
109
|
self._should_warn_unrecommended_command = warn_unrecommended_command
|
|
108
110
|
self._is_interactive = is_interactive
|
|
@@ -142,6 +144,7 @@ class CmdTask(BaseTask):
|
|
|
142
144
|
register_pid_method=lambda pid: ctx.xcom.get(xcom_pid_key).push(pid),
|
|
143
145
|
max_output_line=self._max_output_line,
|
|
144
146
|
max_error_line=self._max_error_line,
|
|
147
|
+
timeout=self._execution_timeout,
|
|
145
148
|
is_interactive=self._is_interactive,
|
|
146
149
|
)
|
|
147
150
|
# Check for errors
|
zrb/task/llm/agent.py
CHANGED
|
@@ -39,39 +39,10 @@ def create_agent_instance(
|
|
|
39
39
|
auto_summarize: bool = True,
|
|
40
40
|
) -> "Agent[None, Any]":
|
|
41
41
|
"""Creates a new Agent instance with configured tools and servers."""
|
|
42
|
-
from pydantic_ai import Agent,
|
|
42
|
+
from pydantic_ai import Agent, Tool
|
|
43
43
|
from pydantic_ai.tools import GenerateToolJsonSchema
|
|
44
|
-
from pydantic_ai.toolsets import ToolsetTool, WrapperToolset
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
class ConfirmationWrapperToolset(WrapperToolset):
|
|
48
|
-
ctx: AnyContext
|
|
49
|
-
yolo_mode: bool | list[str]
|
|
50
|
-
|
|
51
|
-
async def call_tool(
|
|
52
|
-
self, name: str, tool_args: dict, ctx: RunContext, tool: ToolsetTool[None]
|
|
53
|
-
) -> Any:
|
|
54
|
-
# The `tool` object is passed in. Use it for inspection.
|
|
55
|
-
# Define a temporary function that performs the actual tool call.
|
|
56
|
-
async def execute_delegated_tool_call(**params):
|
|
57
|
-
# Pass all arguments down the chain.
|
|
58
|
-
return await self.wrapped.call_tool(name, tool_args, ctx, tool)
|
|
59
|
-
|
|
60
|
-
# For the confirmation UI, make our temporary function look like the real one.
|
|
61
|
-
try:
|
|
62
|
-
execute_delegated_tool_call.__name__ = name
|
|
63
|
-
execute_delegated_tool_call.__doc__ = tool.function.__doc__
|
|
64
|
-
execute_delegated_tool_call.__signature__ = inspect.signature(
|
|
65
|
-
tool.function
|
|
66
|
-
)
|
|
67
|
-
except (AttributeError, TypeError):
|
|
68
|
-
pass # Ignore if we can't inspect the original function
|
|
69
|
-
# Use the existing wrap_func to get the confirmation logic
|
|
70
|
-
wrapped_executor = wrap_func(
|
|
71
|
-
execute_delegated_tool_call, self.ctx, self.yolo_mode
|
|
72
|
-
)
|
|
73
|
-
# Call the wrapped executor. This will trigger the confirmation prompt.
|
|
74
|
-
return await wrapped_executor(**tool_args)
|
|
45
|
+
ConfirmationWrapperToolset = _get_confirmation_wrapper_toolset_class()
|
|
75
46
|
|
|
76
47
|
if yolo_mode is None:
|
|
77
48
|
yolo_mode = False
|
|
@@ -132,6 +103,43 @@ def create_agent_instance(
|
|
|
132
103
|
)
|
|
133
104
|
|
|
134
105
|
|
|
106
|
+
def _get_confirmation_wrapper_toolset_class():
|
|
107
|
+
from pydantic_ai import RunContext
|
|
108
|
+
from pydantic_ai.toolsets import ToolsetTool, WrapperToolset
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class ConfirmationWrapperToolset(WrapperToolset):
|
|
112
|
+
ctx: AnyContext
|
|
113
|
+
yolo_mode: bool | list[str]
|
|
114
|
+
|
|
115
|
+
async def call_tool(
|
|
116
|
+
self, name: str, tool_args: dict, ctx: RunContext, tool: ToolsetTool[None]
|
|
117
|
+
) -> Any:
|
|
118
|
+
# The `tool` object is passed in. Use it for inspection.
|
|
119
|
+
# Define a temporary function that performs the actual tool call.
|
|
120
|
+
async def execute_delegated_tool_call(**params):
|
|
121
|
+
# Pass all arguments down the chain.
|
|
122
|
+
return await self.wrapped.call_tool(name, tool_args, ctx, tool)
|
|
123
|
+
|
|
124
|
+
# For the confirmation UI, make our temporary function look like the real one.
|
|
125
|
+
try:
|
|
126
|
+
execute_delegated_tool_call.__name__ = name
|
|
127
|
+
execute_delegated_tool_call.__doc__ = tool.function.__doc__
|
|
128
|
+
execute_delegated_tool_call.__signature__ = inspect.signature(
|
|
129
|
+
tool.function
|
|
130
|
+
)
|
|
131
|
+
except (AttributeError, TypeError):
|
|
132
|
+
pass # Ignore if we can't inspect the original function
|
|
133
|
+
# Use the existing wrap_func to get the confirmation logic
|
|
134
|
+
wrapped_executor = wrap_func(
|
|
135
|
+
execute_delegated_tool_call, self.ctx, self.yolo_mode
|
|
136
|
+
)
|
|
137
|
+
# Call the wrapped executor. This will trigger the confirmation prompt.
|
|
138
|
+
return await wrapped_executor(**tool_args)
|
|
139
|
+
|
|
140
|
+
return ConfirmationWrapperToolset
|
|
141
|
+
|
|
142
|
+
|
|
135
143
|
def get_agent(
|
|
136
144
|
ctx: AnyContext,
|
|
137
145
|
model: "str | Model",
|
zrb/task/llm/agent_runner.py
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import termios
|
|
6
|
+
import tty
|
|
2
7
|
from collections.abc import Callable
|
|
3
8
|
from typing import TYPE_CHECKING, Any
|
|
4
9
|
|
|
@@ -93,11 +98,17 @@ async def _run_single_agent_iteration(
|
|
|
93
98
|
message_history=ModelMessagesTypeAdapter.validate_python(history_list),
|
|
94
99
|
usage_limits=UsageLimits(request_limit=None), # We don't want limit
|
|
95
100
|
) as agent_run:
|
|
101
|
+
escape_task = asyncio.create_task(_wait_for_escape(ctx))
|
|
96
102
|
async for node in agent_run:
|
|
97
103
|
# Each node represents a step in the agent's execution
|
|
98
104
|
try:
|
|
99
105
|
await print_node(
|
|
100
|
-
_get_plain_printer(ctx),
|
|
106
|
+
_get_plain_printer(ctx),
|
|
107
|
+
agent_run,
|
|
108
|
+
node,
|
|
109
|
+
ctx.is_tty,
|
|
110
|
+
log_indent_level,
|
|
111
|
+
lambda: escape_task.done(),
|
|
101
112
|
)
|
|
102
113
|
except APIError as e:
|
|
103
114
|
# Extract detailed error information from the response
|
|
@@ -108,12 +119,63 @@ async def _run_single_agent_iteration(
|
|
|
108
119
|
ctx.log_error(f"Error processing node: {str(e)}")
|
|
109
120
|
ctx.log_error(f"Error type: {type(e).__name__}")
|
|
110
121
|
raise
|
|
122
|
+
if escape_task.done():
|
|
123
|
+
break
|
|
124
|
+
# Clean escape_task
|
|
125
|
+
if not escape_task.done():
|
|
126
|
+
try:
|
|
127
|
+
escape_task.cancel()
|
|
128
|
+
await escape_task
|
|
129
|
+
except asyncio.CancelledError:
|
|
130
|
+
pass
|
|
111
131
|
return agent_run
|
|
112
132
|
|
|
113
133
|
|
|
134
|
+
async def _wait_for_escape(ctx: AnyContext) -> None:
|
|
135
|
+
if not ctx.is_tty:
|
|
136
|
+
# Wait forever
|
|
137
|
+
await asyncio.Future()
|
|
138
|
+
return
|
|
139
|
+
loop = asyncio.get_event_loop()
|
|
140
|
+
future = loop.create_future()
|
|
141
|
+
fd = sys.stdin.fileno()
|
|
142
|
+
old_settings = termios.tcgetattr(fd)
|
|
143
|
+
try:
|
|
144
|
+
tty.setcbreak(fd)
|
|
145
|
+
loop.add_reader(fd, _create_escape_detector(ctx, future, fd))
|
|
146
|
+
await future
|
|
147
|
+
except asyncio.CancelledError:
|
|
148
|
+
raise
|
|
149
|
+
finally:
|
|
150
|
+
loop.remove_reader(fd)
|
|
151
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _create_escape_detector(
|
|
155
|
+
ctx: AnyContext, future: asyncio.Future[Any], fd: int | Any
|
|
156
|
+
) -> Callable[[], None]:
|
|
157
|
+
def on_stdin():
|
|
158
|
+
try:
|
|
159
|
+
# Read just one byte
|
|
160
|
+
ch = os.read(fd, 1)
|
|
161
|
+
if ch == b"\x1b":
|
|
162
|
+
ctx.print("\n🚫 Interrupted by user.", plain=True)
|
|
163
|
+
if not future.done():
|
|
164
|
+
future.set_result(None)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
if not future.done():
|
|
167
|
+
future.set_exception(e)
|
|
168
|
+
|
|
169
|
+
return on_stdin
|
|
170
|
+
|
|
171
|
+
|
|
114
172
|
def _create_print_throttle_notif(ctx: AnyContext) -> Callable[[str], None]:
|
|
115
|
-
def _print_throttle_notif(
|
|
116
|
-
|
|
173
|
+
def _print_throttle_notif(text: str, *args: Any, **kwargs: Any):
|
|
174
|
+
new_line = kwargs.get("new_line", True)
|
|
175
|
+
prefix = "\r" if not new_line else "\n"
|
|
176
|
+
if text != "":
|
|
177
|
+
prefix = f"{prefix} 🐢 Request Throttled: "
|
|
178
|
+
ctx.print(stylize_faint(f"{prefix}{text}"), plain=True, end="")
|
|
117
179
|
|
|
118
180
|
return _print_throttle_notif
|
|
119
181
|
|
|
@@ -9,6 +9,7 @@ Follow this workflow to deliver accurate, well-sourced, and synthesized research
|
|
|
9
9
|
- **Source Hierarchy:** Prioritize authoritative sources over secondary ones
|
|
10
10
|
- **Synthesis Excellence:** Connect information into coherent narratives
|
|
11
11
|
- **Comprehensive Attribution:** Cite all significant claims and data points
|
|
12
|
+
- **Mandatory Citations:** ALWAYS include a "Sources" section at the end of the response with a list of all URLs used.
|
|
12
13
|
|
|
13
14
|
# Tool Usage Guideline
|
|
14
15
|
- Use `search_internet` for web research and information gathering
|
|
@@ -114,6 +115,7 @@ Follow this workflow to deliver accurate, well-sourced, and synthesized research
|
|
|
114
115
|
- **Detailed Analysis:** Provide comprehensive information for deep dives
|
|
115
116
|
- **Actionable Insights:** Highlight implications and recommended actions
|
|
116
117
|
- **Further Reading:** Suggest additional resources for interested readers
|
|
118
|
+
- **Sources:** ALWAYS end the response with a "Sources" section listing all URLs used.
|
|
117
119
|
|
|
118
120
|
# Risk Assessment Guidelines
|
|
119
121
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def remove_system_prompt_and_instruction(
|
|
5
|
+
history_list: list[dict[str, Any]],
|
|
6
|
+
) -> list[dict[str, Any]]:
|
|
7
|
+
for msg in history_list:
|
|
8
|
+
if msg.get("instructions"):
|
|
9
|
+
msg["instructions"] = ""
|
|
10
|
+
msg["parts"] = [
|
|
11
|
+
p for p in msg.get("parts", []) if p.get("part_kind") != "system-prompt"
|
|
12
|
+
]
|
|
13
|
+
return history_list
|
|
@@ -88,30 +88,21 @@ def create_summarize_history_processor(
|
|
|
88
88
|
messages: list[ModelMessage],
|
|
89
89
|
) -> list[ModelMessage]:
|
|
90
90
|
history_list = json.loads(ModelMessagesTypeAdapter.dump_json(messages))
|
|
91
|
-
|
|
91
|
+
history_list_str = json.dumps(history_list)
|
|
92
92
|
# Estimate token usage
|
|
93
93
|
# Note: Pydantic ai has run context parameter
|
|
94
94
|
# (https://ai.pydantic.dev/message-history/#runcontext-parameter)
|
|
95
95
|
# But we cannot use run_ctx.usage.total_tokens because total token keep increasing
|
|
96
96
|
# even after summariztion.
|
|
97
|
-
estimated_token_usage = rate_limitter.count_token(
|
|
97
|
+
estimated_token_usage = rate_limitter.count_token(history_list_str)
|
|
98
98
|
_print_request_info(
|
|
99
99
|
ctx, estimated_token_usage, summarization_token_threshold, messages
|
|
100
100
|
)
|
|
101
101
|
if estimated_token_usage < summarization_token_threshold or len(messages) == 1:
|
|
102
102
|
return messages
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
key: obj[key]
|
|
106
|
-
for key in obj
|
|
107
|
-
if index == len(history_list) - 1 or key != "instructions"
|
|
108
|
-
}
|
|
109
|
-
for index, obj in enumerate(history_list)
|
|
110
|
-
]
|
|
111
|
-
history_json_str_without_instruction = json.dumps(
|
|
112
|
-
history_list_without_instruction
|
|
103
|
+
summarization_message = (
|
|
104
|
+
f"Summarize the following conversation: {history_list_str}"
|
|
113
105
|
)
|
|
114
|
-
summarization_message = f"Summarize the following conversation: {history_json_str_without_instruction}"
|
|
115
106
|
summarization_agent = Agent[None, ConversationSummary](
|
|
116
107
|
model=summarization_model,
|
|
117
108
|
output_type=save_conversation_summary,
|
zrb/task/llm/print_node.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import json
|
|
2
3
|
from collections.abc import Callable
|
|
3
4
|
from typing import Any
|
|
@@ -7,7 +8,12 @@ from zrb.util.cli.style import stylize_faint
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
async def print_node(
|
|
10
|
-
print_func: Callable,
|
|
11
|
+
print_func: Callable,
|
|
12
|
+
agent_run: Any,
|
|
13
|
+
node: Any,
|
|
14
|
+
is_tty: bool,
|
|
15
|
+
log_indent_level: int = 0,
|
|
16
|
+
stop_check: Callable[[], bool] | None = None,
|
|
11
17
|
):
|
|
12
18
|
"""Prints the details of an agent execution node using a provided print function."""
|
|
13
19
|
from pydantic_ai import Agent
|
|
@@ -24,45 +30,83 @@ async def print_node(
|
|
|
24
30
|
)
|
|
25
31
|
|
|
26
32
|
meta = getattr(node, "id", None) or getattr(node, "request_id", None)
|
|
33
|
+
progress_char_list = ["|", "/", "-", "\\"]
|
|
34
|
+
progress_index = 0
|
|
27
35
|
if Agent.is_user_prompt_node(node):
|
|
28
36
|
print_func(_format_header("🔠 Receiving input...", log_indent_level))
|
|
29
|
-
|
|
37
|
+
return
|
|
38
|
+
if Agent.is_model_request_node(node):
|
|
30
39
|
# A model request node => We can stream tokens from the model's request
|
|
31
|
-
|
|
40
|
+
esc_notif = " (Press esc to cancel)" if is_tty else ""
|
|
41
|
+
print_func(_format_header(f"🧠 Processing{esc_notif}...", log_indent_level))
|
|
32
42
|
# Reference: https://ai.pydantic.dev/agents/#streaming-all-events-and-output
|
|
33
43
|
try:
|
|
34
44
|
async with node.stream(agent_run.ctx) as request_stream:
|
|
35
45
|
is_streaming = False
|
|
46
|
+
is_tool_processing = False
|
|
36
47
|
async for event in request_stream:
|
|
48
|
+
if stop_check and stop_check():
|
|
49
|
+
return
|
|
50
|
+
await asyncio.sleep(0)
|
|
37
51
|
if isinstance(event, PartStartEvent) and event.part:
|
|
38
52
|
if is_streaming:
|
|
39
53
|
print_func("")
|
|
40
54
|
content = _get_event_part_content(event)
|
|
41
55
|
print_func(_format_content(content, log_indent_level), end="")
|
|
42
56
|
is_streaming = True
|
|
43
|
-
|
|
57
|
+
is_tool_processing = False
|
|
58
|
+
continue
|
|
59
|
+
if isinstance(event, PartDeltaEvent):
|
|
44
60
|
if isinstance(event.delta, TextPartDelta):
|
|
45
61
|
content_delta = event.delta.content_delta
|
|
46
62
|
print_func(
|
|
47
63
|
_format_stream_content(content_delta, log_indent_level),
|
|
48
64
|
end="",
|
|
49
65
|
)
|
|
50
|
-
|
|
66
|
+
is_tool_processing = False
|
|
67
|
+
is_streaming = True
|
|
68
|
+
continue
|
|
69
|
+
if isinstance(event.delta, ThinkingPartDelta):
|
|
51
70
|
content_delta = event.delta.content_delta
|
|
52
71
|
print_func(
|
|
53
72
|
_format_stream_content(content_delta, log_indent_level),
|
|
54
73
|
end="",
|
|
55
74
|
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
is_tool_processing = False
|
|
76
|
+
is_streaming = True
|
|
77
|
+
continue
|
|
78
|
+
if isinstance(event.delta, ToolCallPartDelta):
|
|
79
|
+
if CFG.LLM_SHOW_TOOL_CALL_PREPARATION:
|
|
80
|
+
args_delta = event.delta.args_delta
|
|
81
|
+
if isinstance(args_delta, dict):
|
|
82
|
+
args_delta = json.dumps(args_delta)
|
|
83
|
+
print_func(
|
|
84
|
+
_format_stream_content(
|
|
85
|
+
args_delta, log_indent_level
|
|
86
|
+
),
|
|
87
|
+
end="",
|
|
88
|
+
)
|
|
89
|
+
is_streaming = True
|
|
90
|
+
is_tool_processing = True
|
|
91
|
+
continue
|
|
92
|
+
prefix = "\n" if not is_tool_processing else ""
|
|
93
|
+
progress_char = progress_char_list[progress_index]
|
|
60
94
|
print_func(
|
|
61
|
-
|
|
95
|
+
_format_content(
|
|
96
|
+
f"Preparing Tool Parameters... {progress_char}",
|
|
97
|
+
log_indent_level,
|
|
98
|
+
prefix=f"\r{prefix}",
|
|
99
|
+
),
|
|
62
100
|
end="",
|
|
63
101
|
)
|
|
102
|
+
progress_index += 1
|
|
103
|
+
if progress_index >= len(progress_char_list):
|
|
104
|
+
progress_index = 0
|
|
105
|
+
is_tool_processing = True
|
|
106
|
+
is_streaming = True
|
|
107
|
+
continue
|
|
64
108
|
is_streaming = True
|
|
65
|
-
|
|
109
|
+
if isinstance(event, FinalResultEvent) and event.tool_name:
|
|
66
110
|
if is_streaming:
|
|
67
111
|
print_func("")
|
|
68
112
|
tool_name = event.tool_name
|
|
@@ -72,6 +116,7 @@ async def print_node(
|
|
|
72
116
|
)
|
|
73
117
|
)
|
|
74
118
|
is_streaming = False
|
|
119
|
+
is_tool_processing = False
|
|
75
120
|
if is_streaming:
|
|
76
121
|
print_func("")
|
|
77
122
|
except UnexpectedModelBehavior as e:
|
|
@@ -85,12 +130,16 @@ async def print_node(
|
|
|
85
130
|
log_indent_level,
|
|
86
131
|
)
|
|
87
132
|
)
|
|
88
|
-
|
|
133
|
+
return
|
|
134
|
+
if Agent.is_call_tools_node(node):
|
|
89
135
|
# A handle-response node => The model returned some data, potentially calls a tool
|
|
90
136
|
print_func(_format_header("🧰 Calling Tool...", log_indent_level))
|
|
91
137
|
try:
|
|
92
138
|
async with node.stream(agent_run.ctx) as handle_stream:
|
|
93
139
|
async for event in handle_stream:
|
|
140
|
+
if stop_check and stop_check():
|
|
141
|
+
return
|
|
142
|
+
await asyncio.sleep(0)
|
|
94
143
|
if isinstance(event, FunctionToolCallEvent):
|
|
95
144
|
args = _get_event_part_args(event)
|
|
96
145
|
call_id = event.part.tool_call_id
|
|
@@ -100,7 +149,8 @@ async def print_node(
|
|
|
100
149
|
f"{call_id} | Call {tool_name} {args}", log_indent_level
|
|
101
150
|
)
|
|
102
151
|
)
|
|
103
|
-
|
|
152
|
+
continue
|
|
153
|
+
if (
|
|
104
154
|
isinstance(event, FunctionToolResultEvent)
|
|
105
155
|
and event.tool_call_id
|
|
106
156
|
):
|
|
@@ -113,12 +163,10 @@ async def print_node(
|
|
|
113
163
|
log_indent_level,
|
|
114
164
|
)
|
|
115
165
|
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
)
|
|
166
|
+
continue
|
|
167
|
+
print_func(
|
|
168
|
+
_format_content(f"{call_id} | Executed", log_indent_level)
|
|
169
|
+
)
|
|
122
170
|
except UnexpectedModelBehavior as e:
|
|
123
171
|
print_func("") # ensure newline consistency
|
|
124
172
|
print_func(
|
|
@@ -130,9 +178,11 @@ async def print_node(
|
|
|
130
178
|
log_indent_level,
|
|
131
179
|
)
|
|
132
180
|
)
|
|
133
|
-
|
|
181
|
+
return
|
|
182
|
+
if Agent.is_end_node(node):
|
|
134
183
|
# Once an End node is reached, the agent run is complete
|
|
135
184
|
print_func(_format_header("✅ Completed...", log_indent_level))
|
|
185
|
+
return
|
|
136
186
|
|
|
137
187
|
|
|
138
188
|
def _format_header(text: str | None, log_indent_level: int = 0) -> str:
|
|
@@ -145,8 +195,10 @@ def _format_header(text: str | None, log_indent_level: int = 0) -> str:
|
|
|
145
195
|
)
|
|
146
196
|
|
|
147
197
|
|
|
148
|
-
def _format_content(
|
|
149
|
-
|
|
198
|
+
def _format_content(
|
|
199
|
+
text: str | None, log_indent_level: int = 0, prefix: str = ""
|
|
200
|
+
) -> str:
|
|
201
|
+
return prefix + _format(
|
|
150
202
|
text,
|
|
151
203
|
base_indent=2,
|
|
152
204
|
first_indent=3,
|
|
@@ -155,8 +207,10 @@ def _format_content(text: str | None, log_indent_level: int = 0) -> str:
|
|
|
155
207
|
)
|
|
156
208
|
|
|
157
209
|
|
|
158
|
-
def _format_stream_content(
|
|
159
|
-
|
|
210
|
+
def _format_stream_content(
|
|
211
|
+
text: str | None, log_indent_level: int = 0, prefix: str = ""
|
|
212
|
+
) -> str:
|
|
213
|
+
return prefix + _format(
|
|
160
214
|
text,
|
|
161
215
|
base_indent=2,
|
|
162
216
|
indent=3,
|
|
@@ -207,7 +261,7 @@ def _truncate_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
|
207
261
|
return {key: _truncate_arg(val) for key, val in kwargs.items()}
|
|
208
262
|
|
|
209
263
|
|
|
210
|
-
def _truncate_arg(arg: str, length: int =
|
|
264
|
+
def _truncate_arg(arg: str, length: int = 30) -> str:
|
|
211
265
|
if isinstance(arg, str) and len(arg) > length:
|
|
212
266
|
return f"{arg[:length-4]} ..."
|
|
213
267
|
return arg
|