zrb 1.5.5__py3-none-any.whl → 1.5.7__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.
@@ -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