agentassert 0.1.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.
@@ -0,0 +1,153 @@
1
+ """Execution tree builder for visualizing agent traces."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from agentunit.trace.event import Event, EventType, LLMCallEvent, ToolCallEvent
8
+ from agentunit.trace.span import Span
9
+
10
+
11
+ class TreeNode(BaseModel):
12
+ """
13
+ A node in the execution tree.
14
+
15
+ Attributes:
16
+ label: Display label for the node.
17
+ event: The underlying event, if any.
18
+ children: Child nodes.
19
+ depth: Depth in the tree (0 = root).
20
+ """
21
+
22
+ label: str
23
+ event: Event | None = None
24
+ children: list["TreeNode"] = Field(default_factory=list)
25
+ depth: int = 0
26
+
27
+ model_config = {"arbitrary_types_allowed": True}
28
+
29
+ def add_child(self, node: "TreeNode") -> None:
30
+ """Add a child node."""
31
+ node.depth = self.depth + 1
32
+ self.children.append(node)
33
+
34
+
35
+ class ExecutionTree(BaseModel):
36
+ """
37
+ A tree representation of an agent's execution trace.
38
+
39
+ The ExecutionTree provides a hierarchical view of the agent's execution,
40
+ making it easy to visualize the sequence of LLM calls, tool invocations,
41
+ and decisions.
42
+
43
+ Attributes:
44
+ root: The root node of the tree.
45
+ spans: List of all spans in the execution.
46
+ """
47
+
48
+ root: TreeNode = Field(default_factory=lambda: TreeNode(label="Agent Execution"))
49
+ spans: list[Span] = Field(default_factory=list)
50
+
51
+ model_config = {"arbitrary_types_allowed": True}
52
+
53
+ @classmethod
54
+ def from_events(cls, events: list[Event], agent_input: str = "") -> "ExecutionTree":
55
+ """
56
+ Build an execution tree from a list of events.
57
+
58
+ Args:
59
+ events: List of events in execution order.
60
+ agent_input: The input that was given to the agent.
61
+
62
+ Returns:
63
+ An ExecutionTree representing the execution.
64
+ """
65
+ tree = cls()
66
+ tree.root.label = f"Agent Execution: {agent_input[:50]}..." if len(agent_input) > 50 else f"Agent Execution: {agent_input}"
67
+
68
+ for event in events:
69
+ node = cls._event_to_node(event)
70
+ tree.root.add_child(node)
71
+
72
+ return tree
73
+
74
+ @staticmethod
75
+ def _event_to_node(event: Event) -> TreeNode:
76
+ """Convert an event to a tree node."""
77
+ if event.event_type == EventType.LLM_CALL:
78
+ llm_event = event if isinstance(event, LLMCallEvent) else None
79
+ if llm_event:
80
+ content_preview = llm_event.response_content[:80]
81
+ if len(llm_event.response_content) > 80:
82
+ content_preview += "..."
83
+ label = f"[LLM] {llm_event.model}: {content_preview}"
84
+ else:
85
+ label = "[LLM] Call"
86
+ return TreeNode(label=label, event=event)
87
+
88
+ elif event.event_type == EventType.TOOL_CALL:
89
+ tool_event = event if isinstance(event, ToolCallEvent) else None
90
+ if tool_event:
91
+ status = "✓" if tool_event.success else "✗"
92
+ label = f"[TOOL] {tool_event.tool}({_format_input(tool_event.input)}) → {status}"
93
+ else:
94
+ label = "[TOOL] Call"
95
+ return TreeNode(label=label, event=event)
96
+
97
+ elif event.event_type == EventType.ERROR:
98
+ label = f"[ERROR] {event.metadata.get('message', 'Unknown error')}"
99
+ return TreeNode(label=label, event=event)
100
+
101
+ else:
102
+ label = f"[{event.event_type.value.upper()}]"
103
+ return TreeNode(label=label, event=event)
104
+
105
+ def render_text(self, indent: str = " ") -> str:
106
+ """
107
+ Render the tree as a text string.
108
+
109
+ Args:
110
+ indent: The indentation string to use for each level.
111
+
112
+ Returns:
113
+ A string representation of the tree.
114
+ """
115
+ lines: list[str] = []
116
+ self._render_node(self.root, lines, "", indent)
117
+ return "\n".join(lines)
118
+
119
+ def _render_node(
120
+ self,
121
+ node: TreeNode,
122
+ lines: list[str],
123
+ prefix: str,
124
+ indent: str,
125
+ ) -> None:
126
+ """Recursively render a node and its children."""
127
+ lines.append(f"{prefix}{node.label}")
128
+ for i, child in enumerate(node.children):
129
+ is_last = i == len(node.children) - 1
130
+ child_prefix = prefix + ("└─ " if is_last else "├─ ")
131
+ continuation = prefix + (" " if is_last else "│ ")
132
+ lines.append(f"{child_prefix}{child.label}")
133
+ # Render grandchildren with proper continuation
134
+ for j, grandchild in enumerate(child.children):
135
+ gc_is_last = j == len(child.children) - 1
136
+ gc_prefix = continuation + ("└─ " if gc_is_last else "├─ ")
137
+ lines.append(f"{gc_prefix}{grandchild.label}")
138
+
139
+
140
+ def _format_input(input_dict: dict[str, Any], max_len: int = 50) -> str:
141
+ """Format tool input for display."""
142
+ if not input_dict:
143
+ return ""
144
+ parts = []
145
+ for key, value in input_dict.items():
146
+ val_str = str(value)
147
+ if len(val_str) > 20:
148
+ val_str = val_str[:17] + "..."
149
+ parts.append(f"{key}={val_str}")
150
+ result = ", ".join(parts)
151
+ if len(result) > max_len:
152
+ result = result[: max_len - 3] + "..."
153
+ return result