zrb 1.5.5__py3-none-any.whl → 1.5.6__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/__init__.py +2 -0
- zrb/__main__.py +28 -2
- zrb/builtin/llm/history.py +73 -0
- zrb/builtin/llm/input.py +27 -0
- zrb/builtin/llm/llm_chat.py +4 -61
- zrb/builtin/llm/tool/api.py +39 -17
- zrb/builtin/llm/tool/cli.py +19 -5
- zrb/builtin/llm/tool/file.py +270 -131
- zrb/builtin/llm/tool/rag.py +18 -1
- zrb/builtin/llm/tool/web.py +31 -14
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +6 -8
- zrb/config.py +1 -0
- zrb/llm_config.py +81 -15
- zrb/task/llm/__init__.py +0 -0
- zrb/task/llm/agent_runner.py +53 -0
- zrb/task/llm/context_enricher.py +86 -0
- zrb/task/llm/default_context.py +44 -0
- zrb/task/llm/error.py +77 -0
- zrb/task/llm/history.py +92 -0
- zrb/task/llm/history_summarizer.py +71 -0
- zrb/task/llm/print_node.py +98 -0
- zrb/task/llm/tool_wrapper.py +88 -0
- zrb/task/llm_task.py +279 -246
- zrb/util/file.py +8 -2
- zrb/util/load.py +2 -0
- {zrb-1.5.5.dist-info → zrb-1.5.6.dist-info}/METADATA +1 -1
- {zrb-1.5.5.dist-info → zrb-1.5.6.dist-info}/RECORD +29 -18
- {zrb-1.5.5.dist-info → zrb-1.5.6.dist-info}/WHEEL +0 -0
- {zrb-1.5.5.dist-info → zrb-1.5.6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from pydantic_ai import Agent
|
5
|
+
from pydantic_ai.messages import (
|
6
|
+
FinalResultEvent,
|
7
|
+
FunctionToolCallEvent,
|
8
|
+
FunctionToolResultEvent,
|
9
|
+
PartDeltaEvent,
|
10
|
+
PartStartEvent,
|
11
|
+
TextPartDelta,
|
12
|
+
ToolCallPartDelta,
|
13
|
+
)
|
14
|
+
|
15
|
+
from zrb.util.cli.style import stylize_faint
|
16
|
+
|
17
|
+
|
18
|
+
async def print_node(print_func: Callable, agent_run: Any, node: Any):
|
19
|
+
"""Prints the details of an agent execution node using a provided print function."""
|
20
|
+
if Agent.is_user_prompt_node(node):
|
21
|
+
# A user prompt node => The user has provided input
|
22
|
+
print_func(stylize_faint(f">> UserPromptNode: {node.user_prompt}"))
|
23
|
+
elif Agent.is_model_request_node(node):
|
24
|
+
# A model request node => We can stream tokens from the model's request
|
25
|
+
print_func(
|
26
|
+
stylize_faint(">> ModelRequestNode: streaming partial request tokens")
|
27
|
+
)
|
28
|
+
async with node.stream(agent_run.ctx) as request_stream:
|
29
|
+
is_streaming = False
|
30
|
+
async for event in request_stream:
|
31
|
+
if isinstance(event, PartStartEvent):
|
32
|
+
if is_streaming:
|
33
|
+
print_func("", plain=True)
|
34
|
+
print_func(
|
35
|
+
stylize_faint(
|
36
|
+
f"[Request] Starting part {event.index}: {event.part!r}"
|
37
|
+
),
|
38
|
+
)
|
39
|
+
is_streaming = False
|
40
|
+
elif isinstance(event, PartDeltaEvent):
|
41
|
+
if isinstance(event.delta, TextPartDelta):
|
42
|
+
print_func(
|
43
|
+
stylize_faint(f"{event.delta.content_delta}"),
|
44
|
+
end="",
|
45
|
+
plain=is_streaming,
|
46
|
+
)
|
47
|
+
elif isinstance(event.delta, ToolCallPartDelta):
|
48
|
+
print_func(
|
49
|
+
stylize_faint(f"{event.delta.args_delta}"),
|
50
|
+
end="",
|
51
|
+
plain=is_streaming,
|
52
|
+
)
|
53
|
+
is_streaming = True
|
54
|
+
elif isinstance(event, FinalResultEvent):
|
55
|
+
if is_streaming:
|
56
|
+
print_func("", plain=True)
|
57
|
+
print_func(
|
58
|
+
stylize_faint(f"[Result] tool_name={event.tool_name}"),
|
59
|
+
)
|
60
|
+
is_streaming = False
|
61
|
+
if is_streaming:
|
62
|
+
print_func("", plain=True)
|
63
|
+
elif Agent.is_call_tools_node(node):
|
64
|
+
# A handle-response node => The model returned some data, potentially calls a tool
|
65
|
+
print_func(
|
66
|
+
stylize_faint(">> CallToolsNode: streaming partial response & tool usage")
|
67
|
+
)
|
68
|
+
async with node.stream(agent_run.ctx) as handle_stream:
|
69
|
+
async for event in handle_stream:
|
70
|
+
if isinstance(event, FunctionToolCallEvent):
|
71
|
+
# Handle empty arguments across different providers
|
72
|
+
if event.part.args == "" or event.part.args is None:
|
73
|
+
event.part.args = {}
|
74
|
+
elif isinstance(
|
75
|
+
event.part.args, str
|
76
|
+
) and event.part.args.strip() in ["null", "{}"]:
|
77
|
+
# Some providers might send "null" or "{}" as a string
|
78
|
+
event.part.args = {}
|
79
|
+
# Handle dummy property if present (from our schema sanitization)
|
80
|
+
if (
|
81
|
+
isinstance(event.part.args, dict)
|
82
|
+
and "_dummy" in event.part.args
|
83
|
+
):
|
84
|
+
del event.part.args["_dummy"]
|
85
|
+
print_func(
|
86
|
+
stylize_faint(
|
87
|
+
f"[Tools] The LLM calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})" # noqa
|
88
|
+
)
|
89
|
+
)
|
90
|
+
elif isinstance(event, FunctionToolResultEvent):
|
91
|
+
print_func(
|
92
|
+
stylize_faint(
|
93
|
+
f"[Tools] Tool call {event.tool_call_id!r} returned => {event.result.content}" # noqa
|
94
|
+
)
|
95
|
+
)
|
96
|
+
elif Agent.is_end_node(node):
|
97
|
+
# Once an End node is reached, the agent run is complete
|
98
|
+
print_func(stylize_faint(f"{agent_run.result.data}"))
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import functools
|
2
|
+
import inspect
|
3
|
+
import traceback
|
4
|
+
from collections.abc import Callable
|
5
|
+
|
6
|
+
from zrb.task.llm.error import ToolExecutionError
|
7
|
+
from zrb.util.run import run_async
|
8
|
+
|
9
|
+
|
10
|
+
def wrap_tool(func: Callable) -> Callable:
|
11
|
+
"""Wraps a tool function to handle exceptions and context propagation.
|
12
|
+
|
13
|
+
- Catches exceptions during tool execution and returns a structured
|
14
|
+
JSON error message (`ToolExecutionError`).
|
15
|
+
- Inspects the original function signature for a 'ctx' parameter.
|
16
|
+
If found, the wrapper will accept 'ctx' and pass it to the function.
|
17
|
+
- If the original function has no parameters, injects a dummy '_dummy'
|
18
|
+
parameter into the wrapper's signature to ensure schema generation
|
19
|
+
for pydantic-ai.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
func: The tool function (sync or async) to wrap.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
An async wrapper function.
|
26
|
+
"""
|
27
|
+
original_sig = inspect.signature(func)
|
28
|
+
needs_ctx = "ctx" in original_sig.parameters
|
29
|
+
takes_no_args = len(original_sig.parameters) == 0
|
30
|
+
|
31
|
+
@functools.wraps(func)
|
32
|
+
async def wrapper(*args, **kwargs):
|
33
|
+
ctx_arg = None
|
34
|
+
if needs_ctx:
|
35
|
+
if args:
|
36
|
+
ctx_arg = args[0]
|
37
|
+
args = args[1:] # Remove ctx from args for the actual call if needed
|
38
|
+
elif "ctx" in kwargs:
|
39
|
+
ctx_arg = kwargs.pop("ctx")
|
40
|
+
|
41
|
+
try:
|
42
|
+
# Remove dummy argument if it was added for no-arg functions
|
43
|
+
if takes_no_args and "_dummy" in kwargs:
|
44
|
+
del kwargs["_dummy"]
|
45
|
+
|
46
|
+
if needs_ctx:
|
47
|
+
# Ensure ctx is passed correctly, even if original func had only ctx
|
48
|
+
if ctx_arg is None:
|
49
|
+
# This case should ideally not happen if takes_ctx is True in Tool
|
50
|
+
raise ValueError("Context (ctx) was expected but not provided.")
|
51
|
+
# Call with context
|
52
|
+
return await run_async(func(ctx_arg, *args, **kwargs))
|
53
|
+
else:
|
54
|
+
# Call without context
|
55
|
+
return await run_async(func(*args, **kwargs))
|
56
|
+
except Exception as e:
|
57
|
+
error_model = ToolExecutionError(
|
58
|
+
tool_name=func.__name__,
|
59
|
+
error_type=type(e).__name__,
|
60
|
+
message=str(e),
|
61
|
+
details=traceback.format_exc(),
|
62
|
+
)
|
63
|
+
return error_model.model_dump_json()
|
64
|
+
|
65
|
+
if takes_no_args:
|
66
|
+
# Inject dummy parameter for schema generation if original func took no args
|
67
|
+
new_sig = inspect.Signature(
|
68
|
+
parameters=[
|
69
|
+
inspect.Parameter(
|
70
|
+
"_dummy", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None
|
71
|
+
)
|
72
|
+
]
|
73
|
+
)
|
74
|
+
wrapper.__signature__ = new_sig
|
75
|
+
elif needs_ctx:
|
76
|
+
# Adjust signature if ctx was the *only* parameter originally
|
77
|
+
# This ensures the wrapper signature matches what pydantic-ai expects
|
78
|
+
params = list(original_sig.parameters.values())
|
79
|
+
if len(params) == 1 and params[0].name == "ctx":
|
80
|
+
new_sig = inspect.Signature(
|
81
|
+
parameters=[
|
82
|
+
inspect.Parameter("ctx", inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
83
|
+
]
|
84
|
+
)
|
85
|
+
wrapper.__signature__ = new_sig
|
86
|
+
# Otherwise, the original signature (including ctx) is fine for the wrapper
|
87
|
+
|
88
|
+
return wrapper
|