zrb 1.5.4__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 +408 -405
- 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.4.dist-info → zrb-1.5.6.dist-info}/METADATA +1 -1
- {zrb-1.5.4.dist-info → zrb-1.5.6.dist-info}/RECORD +29 -18
- {zrb-1.5.4.dist-info → zrb-1.5.6.dist-info}/WHEEL +0 -0
- {zrb-1.5.4.dist-info → zrb-1.5.6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from pydantic import BaseModel
|
5
|
+
from pydantic_ai import Agent
|
6
|
+
from pydantic_ai.models import Model
|
7
|
+
from pydantic_ai.settings import ModelSettings
|
8
|
+
|
9
|
+
from zrb.context.any_context import AnyContext
|
10
|
+
from zrb.task.llm.agent_runner import run_agent_iteration
|
11
|
+
from zrb.task.llm.history import ListOfDict
|
12
|
+
|
13
|
+
|
14
|
+
# Configuration model for history summarization
|
15
|
+
class SummarizationConfig(BaseModel):
|
16
|
+
model_config = {"arbitrary_types_allowed": True}
|
17
|
+
model: Model | str | None = None
|
18
|
+
settings: ModelSettings | None = None
|
19
|
+
prompt: str
|
20
|
+
retries: int = 1
|
21
|
+
|
22
|
+
|
23
|
+
async def summarize_history(
|
24
|
+
ctx: AnyContext,
|
25
|
+
config: SummarizationConfig,
|
26
|
+
conversation_context: dict[str, Any],
|
27
|
+
history_list: ListOfDict,
|
28
|
+
) -> dict[str, Any]:
|
29
|
+
"""Runs an LLM call to summarize history and update the context."""
|
30
|
+
ctx.log_info("Attempting to summarize conversation history...")
|
31
|
+
|
32
|
+
summarization_agent = Agent(
|
33
|
+
model=config.model,
|
34
|
+
system_prompt=config.prompt,
|
35
|
+
tools=[], # No tools needed for summarization
|
36
|
+
mcp_servers=[],
|
37
|
+
model_settings=config.settings,
|
38
|
+
retries=config.retries,
|
39
|
+
)
|
40
|
+
|
41
|
+
# Prepare context and history for summarization prompt
|
42
|
+
try:
|
43
|
+
context_json = json.dumps(conversation_context)
|
44
|
+
history_to_summarize_json = json.dumps(history_list)
|
45
|
+
summarization_user_prompt = (
|
46
|
+
f"# Current Context\n{context_json}\n\n"
|
47
|
+
f"# Conversation History to Summarize\n{history_to_summarize_json}"
|
48
|
+
)
|
49
|
+
except Exception as e:
|
50
|
+
ctx.log_warning(f"Error formatting context/history for summarization: {e}")
|
51
|
+
return conversation_context # Return original context if formatting fails
|
52
|
+
|
53
|
+
try:
|
54
|
+
summary_run = await run_agent_iteration(
|
55
|
+
ctx=ctx,
|
56
|
+
agent=summarization_agent,
|
57
|
+
user_prompt=summarization_user_prompt,
|
58
|
+
history_list=[], # Summarization agent doesn't need prior history
|
59
|
+
)
|
60
|
+
if summary_run and summary_run.result.data:
|
61
|
+
summary_text = str(summary_run.result.data)
|
62
|
+
# Update context with the new summary
|
63
|
+
conversation_context["history_summary"] = summary_text
|
64
|
+
ctx.log_info("History summarized and added/updated in context.")
|
65
|
+
ctx.log_info(f"Conversaion summary: {summary_text}")
|
66
|
+
else:
|
67
|
+
ctx.log_warning("History summarization failed or returned no data.")
|
68
|
+
except Exception as e:
|
69
|
+
ctx.log_warning(f"Error during history summarization: {e}")
|
70
|
+
|
71
|
+
return conversation_context
|
@@ -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
|