mojentic 0.5.6__py3-none-any.whl → 0.6.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.
- _examples/recursive_agent.py +2 -1
- _examples/tracer_demo.py +109 -0
- mojentic/dispatcher.py +17 -1
- mojentic/llm/gateways/openai_message_adapter_spec.py +7 -7
- mojentic/llm/gateways/openai_messages_adapter.py +58 -15
- mojentic/llm/llm_broker.py +85 -2
- mojentic/llm/message_composers_spec.py +43 -25
- mojentic/llm/tools/llm_tool.py +27 -1
- mojentic/llm/tools/llm_tool_spec.py +20 -11
- mojentic/tracer/__init__.py +16 -0
- mojentic/tracer/event_store.py +111 -0
- mojentic/tracer/event_store_spec.py +210 -0
- mojentic/tracer/null_tracer.py +191 -0
- mojentic/tracer/tracer_events.py +136 -0
- mojentic/tracer/tracer_events_spec.py +116 -0
- mojentic/tracer/tracer_system.py +285 -0
- mojentic/tracer/tracer_system_spec.py +255 -0
- {mojentic-0.5.6.dist-info → mojentic-0.6.0.dist-info}/METADATA +20 -10
- {mojentic-0.5.6.dist-info → mojentic-0.6.0.dist-info}/RECORD +22 -15
- {mojentic-0.5.6.dist-info → mojentic-0.6.0.dist-info}/WHEEL +1 -1
- mojentic/audit/event_store.py +0 -6
- mojentic/audit/event_store_spec.py +0 -26
- {mojentic-0.5.6.dist-info → mojentic-0.6.0.dist-info}/licenses/LICENSE.md +0 -0
- {mojentic-0.5.6.dist-info → mojentic-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
from mojentic.tracer.tracer_events import (
|
|
5
|
+
TracerEvent,
|
|
6
|
+
LLMCallTracerEvent,
|
|
7
|
+
LLMResponseTracerEvent,
|
|
8
|
+
ToolCallTracerEvent,
|
|
9
|
+
AgentInteractionTracerEvent
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DescribeTracerEvents:
|
|
14
|
+
"""
|
|
15
|
+
Test the tracer event classes to ensure they can be instantiated and have the required properties.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def should_create_base_tracer_event(self):
|
|
19
|
+
"""
|
|
20
|
+
Test creating a base tracer event.
|
|
21
|
+
"""
|
|
22
|
+
# Given / When
|
|
23
|
+
event = TracerEvent(
|
|
24
|
+
source=DescribeTracerEvents,
|
|
25
|
+
timestamp=time.time()
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Then
|
|
29
|
+
assert isinstance(event, TracerEvent)
|
|
30
|
+
assert isinstance(event.timestamp, float)
|
|
31
|
+
assert event.source == DescribeTracerEvents
|
|
32
|
+
|
|
33
|
+
def should_create_llm_call_tracer_event(self):
|
|
34
|
+
"""
|
|
35
|
+
Test creating an LLM call tracer event.
|
|
36
|
+
"""
|
|
37
|
+
# Given / When
|
|
38
|
+
messages = [{"role": "system", "content": "You are a helpful assistant"}]
|
|
39
|
+
event = LLMCallTracerEvent(
|
|
40
|
+
source=DescribeTracerEvents,
|
|
41
|
+
timestamp=time.time(),
|
|
42
|
+
model="test-model",
|
|
43
|
+
messages=messages,
|
|
44
|
+
temperature=0.7,
|
|
45
|
+
tools=None
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Then
|
|
49
|
+
assert isinstance(event, LLMCallTracerEvent)
|
|
50
|
+
assert event.model == "test-model"
|
|
51
|
+
assert event.messages == messages
|
|
52
|
+
assert event.temperature == 0.7
|
|
53
|
+
assert event.tools is None
|
|
54
|
+
|
|
55
|
+
def should_create_llm_response_tracer_event(self):
|
|
56
|
+
"""
|
|
57
|
+
Test creating an LLM response tracer event.
|
|
58
|
+
"""
|
|
59
|
+
# Given / When
|
|
60
|
+
event = LLMResponseTracerEvent(
|
|
61
|
+
source=DescribeTracerEvents,
|
|
62
|
+
timestamp=time.time(),
|
|
63
|
+
model="test-model",
|
|
64
|
+
content="This is a test response",
|
|
65
|
+
call_duration_ms=150.5
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Then
|
|
69
|
+
assert isinstance(event, LLMResponseTracerEvent)
|
|
70
|
+
assert event.model == "test-model"
|
|
71
|
+
assert event.content == "This is a test response"
|
|
72
|
+
assert event.call_duration_ms == 150.5
|
|
73
|
+
assert event.tool_calls is None
|
|
74
|
+
|
|
75
|
+
def should_create_tool_call_tracer_event(self):
|
|
76
|
+
"""
|
|
77
|
+
Test creating a tool call tracer event.
|
|
78
|
+
"""
|
|
79
|
+
# Given / When
|
|
80
|
+
arguments = {"query": "test query"}
|
|
81
|
+
event = ToolCallTracerEvent(
|
|
82
|
+
source=DescribeTracerEvents,
|
|
83
|
+
timestamp=time.time(),
|
|
84
|
+
tool_name="test-tool",
|
|
85
|
+
arguments=arguments,
|
|
86
|
+
result="test result",
|
|
87
|
+
caller="TestAgent"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Then
|
|
91
|
+
assert isinstance(event, ToolCallTracerEvent)
|
|
92
|
+
assert event.tool_name == "test-tool"
|
|
93
|
+
assert event.arguments == arguments
|
|
94
|
+
assert event.result == "test result"
|
|
95
|
+
assert event.caller == "TestAgent"
|
|
96
|
+
|
|
97
|
+
def should_create_agent_interaction_tracer_event(self):
|
|
98
|
+
"""
|
|
99
|
+
Test creating an agent interaction tracer event.
|
|
100
|
+
"""
|
|
101
|
+
# Given / When
|
|
102
|
+
event = AgentInteractionTracerEvent(
|
|
103
|
+
source=DescribeTracerEvents,
|
|
104
|
+
timestamp=time.time(),
|
|
105
|
+
from_agent="AgentA",
|
|
106
|
+
to_agent="AgentB",
|
|
107
|
+
event_type="RequestEvent",
|
|
108
|
+
event_id="12345"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Then
|
|
112
|
+
assert isinstance(event, AgentInteractionTracerEvent)
|
|
113
|
+
assert event.from_agent == "AgentA"
|
|
114
|
+
assert event.to_agent == "AgentB"
|
|
115
|
+
assert event.event_type == "RequestEvent"
|
|
116
|
+
assert event.event_id == "12345"
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TracerSystem module for coordinating tracer events.
|
|
3
|
+
|
|
4
|
+
This provides a central system for recording, filtering, and querying tracer events.
|
|
5
|
+
"""
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from mojentic.tracer.tracer_events import (
|
|
12
|
+
TracerEvent,
|
|
13
|
+
LLMCallTracerEvent,
|
|
14
|
+
LLMResponseTracerEvent,
|
|
15
|
+
ToolCallTracerEvent,
|
|
16
|
+
AgentInteractionTracerEvent
|
|
17
|
+
)
|
|
18
|
+
from mojentic.tracer.event_store import EventStore
|
|
19
|
+
from mojentic.event import Event
|
|
20
|
+
|
|
21
|
+
logger = structlog.get_logger()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TracerSystem:
|
|
25
|
+
"""
|
|
26
|
+
Central system for capturing and querying tracer events.
|
|
27
|
+
|
|
28
|
+
The TracerSystem is responsible for recording events related to LLM calls,
|
|
29
|
+
tool usage, and agent interactions, providing a way to trace through the
|
|
30
|
+
major events of the system.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, event_store: Optional[EventStore] = None, enabled: bool = True):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the tracer system.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
event_store : EventStore, optional
|
|
40
|
+
The event store to use for storing events. If None, a new EventStore will be created.
|
|
41
|
+
enabled : bool, default=True
|
|
42
|
+
Whether the tracer system is enabled. If False, no events will be recorded.
|
|
43
|
+
"""
|
|
44
|
+
self.event_store = event_store or EventStore()
|
|
45
|
+
self.enabled = enabled
|
|
46
|
+
|
|
47
|
+
def record_event(self, event: TracerEvent) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Record a tracer event in the event store.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
event : TracerEvent
|
|
54
|
+
The tracer event to record.
|
|
55
|
+
"""
|
|
56
|
+
if not self.enabled:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
self.event_store.store(event)
|
|
60
|
+
|
|
61
|
+
def record_llm_call(self,
|
|
62
|
+
model: str,
|
|
63
|
+
messages: List[Dict],
|
|
64
|
+
temperature: float = 1.0,
|
|
65
|
+
tools: Optional[List[Dict]] = None,
|
|
66
|
+
source: Any = None) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Record an LLM call event.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
model : str
|
|
73
|
+
The name of the LLM model being called.
|
|
74
|
+
messages : List[Dict]
|
|
75
|
+
The messages sent to the LLM.
|
|
76
|
+
temperature : float, default=1.0
|
|
77
|
+
The temperature setting for the LLM call.
|
|
78
|
+
tools : List[Dict], optional
|
|
79
|
+
The tools available to the LLM, if any.
|
|
80
|
+
source : Any, optional
|
|
81
|
+
The source of the event. If None, the TracerSystem class will be used.
|
|
82
|
+
"""
|
|
83
|
+
if not self.enabled:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
event = LLMCallTracerEvent(
|
|
87
|
+
source=source or type(self),
|
|
88
|
+
timestamp=time.time(),
|
|
89
|
+
model=model,
|
|
90
|
+
messages=messages,
|
|
91
|
+
temperature=temperature,
|
|
92
|
+
tools=tools
|
|
93
|
+
)
|
|
94
|
+
self.event_store.store(event)
|
|
95
|
+
|
|
96
|
+
def record_llm_response(self,
|
|
97
|
+
model: str,
|
|
98
|
+
content: str,
|
|
99
|
+
tool_calls: Optional[List[Dict]] = None,
|
|
100
|
+
call_duration_ms: Optional[float] = None,
|
|
101
|
+
source: Any = None) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Record an LLM response event.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
model : str
|
|
108
|
+
The name of the LLM model that responded.
|
|
109
|
+
content : str
|
|
110
|
+
The content of the LLM response.
|
|
111
|
+
tool_calls : List[Dict], optional
|
|
112
|
+
Any tool calls made by the LLM in its response.
|
|
113
|
+
call_duration_ms : float, optional
|
|
114
|
+
The duration of the LLM call in milliseconds.
|
|
115
|
+
source : Any, optional
|
|
116
|
+
The source of the event. If None, the TracerSystem class will be used.
|
|
117
|
+
"""
|
|
118
|
+
if not self.enabled:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
event = LLMResponseTracerEvent(
|
|
122
|
+
source=source or type(self),
|
|
123
|
+
timestamp=time.time(),
|
|
124
|
+
model=model,
|
|
125
|
+
content=content,
|
|
126
|
+
tool_calls=tool_calls,
|
|
127
|
+
call_duration_ms=call_duration_ms
|
|
128
|
+
)
|
|
129
|
+
self.event_store.store(event)
|
|
130
|
+
|
|
131
|
+
def record_tool_call(self,
|
|
132
|
+
tool_name: str,
|
|
133
|
+
arguments: Dict[str, Any],
|
|
134
|
+
result: Any,
|
|
135
|
+
caller: Optional[str] = None,
|
|
136
|
+
source: Any = None) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Record a tool call event.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
tool_name : str
|
|
143
|
+
The name of the tool being called.
|
|
144
|
+
arguments : Dict[str, Any]
|
|
145
|
+
The arguments provided to the tool.
|
|
146
|
+
result : Any
|
|
147
|
+
The result returned by the tool.
|
|
148
|
+
caller : str, optional
|
|
149
|
+
The name of the agent or component calling the tool.
|
|
150
|
+
source : Any, optional
|
|
151
|
+
The source of the event. If None, the TracerSystem class will be used.
|
|
152
|
+
"""
|
|
153
|
+
if not self.enabled:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
event = ToolCallTracerEvent(
|
|
157
|
+
source=source or type(self),
|
|
158
|
+
timestamp=time.time(),
|
|
159
|
+
tool_name=tool_name,
|
|
160
|
+
arguments=arguments,
|
|
161
|
+
result=result,
|
|
162
|
+
caller=caller
|
|
163
|
+
)
|
|
164
|
+
self.event_store.store(event)
|
|
165
|
+
|
|
166
|
+
def record_agent_interaction(self,
|
|
167
|
+
from_agent: str,
|
|
168
|
+
to_agent: str,
|
|
169
|
+
event_type: str,
|
|
170
|
+
event_id: Optional[str] = None,
|
|
171
|
+
source: Any = None) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Record an agent interaction event.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
from_agent : str
|
|
178
|
+
The name of the agent sending the event.
|
|
179
|
+
to_agent : str
|
|
180
|
+
The name of the agent receiving the event.
|
|
181
|
+
event_type : str
|
|
182
|
+
The type of event being processed.
|
|
183
|
+
event_id : str, optional
|
|
184
|
+
A unique identifier for the event.
|
|
185
|
+
source : Any, optional
|
|
186
|
+
The source of the event. If None, the TracerSystem class will be used.
|
|
187
|
+
"""
|
|
188
|
+
if not self.enabled:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
event = AgentInteractionTracerEvent(
|
|
192
|
+
source=source or type(self),
|
|
193
|
+
timestamp=time.time(),
|
|
194
|
+
from_agent=from_agent,
|
|
195
|
+
to_agent=to_agent,
|
|
196
|
+
event_type=event_type,
|
|
197
|
+
event_id=event_id
|
|
198
|
+
)
|
|
199
|
+
self.event_store.store(event)
|
|
200
|
+
|
|
201
|
+
def get_events(self,
|
|
202
|
+
event_type: Optional[Type[TracerEvent]] = None,
|
|
203
|
+
start_time: Optional[float] = None,
|
|
204
|
+
end_time: Optional[float] = None,
|
|
205
|
+
filter_func: Optional[Callable[[TracerEvent], bool]] = None) -> List[TracerEvent]:
|
|
206
|
+
"""
|
|
207
|
+
Get tracer events from the store, optionally filtered.
|
|
208
|
+
|
|
209
|
+
This is a convenience wrapper around the EventStore's get_events method,
|
|
210
|
+
specifically for tracer events.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
event_type : Type[TracerEvent], optional
|
|
215
|
+
Filter events by this specific tracer event type.
|
|
216
|
+
start_time : float, optional
|
|
217
|
+
Include events with timestamp >= start_time.
|
|
218
|
+
end_time : float, optional
|
|
219
|
+
Include events with timestamp <= end_time.
|
|
220
|
+
filter_func : Callable[[TracerEvent], bool], optional
|
|
221
|
+
Custom filter function to apply to events.
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
List[TracerEvent]
|
|
226
|
+
Events that match the filter criteria.
|
|
227
|
+
"""
|
|
228
|
+
# First filter to only TracerEvents
|
|
229
|
+
events = self.event_store.get_events(event_type=TracerEvent)
|
|
230
|
+
|
|
231
|
+
# Then apply additional filters
|
|
232
|
+
if event_type is not None:
|
|
233
|
+
events = [e for e in events if isinstance(e, event_type)]
|
|
234
|
+
|
|
235
|
+
if start_time is not None:
|
|
236
|
+
events = [e for e in events if e.timestamp >= start_time]
|
|
237
|
+
|
|
238
|
+
if end_time is not None:
|
|
239
|
+
events = [e for e in events if e.timestamp <= end_time]
|
|
240
|
+
|
|
241
|
+
if filter_func is not None:
|
|
242
|
+
events = [e for e in events if filter_func(e)]
|
|
243
|
+
|
|
244
|
+
return events
|
|
245
|
+
|
|
246
|
+
def get_last_n_tracer_events(self, n: int, event_type: Optional[Type[TracerEvent]] = None) -> List[TracerEvent]:
|
|
247
|
+
"""
|
|
248
|
+
Get the last N tracer events, optionally filtered by type.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
n : int
|
|
253
|
+
Number of events to return.
|
|
254
|
+
event_type : Type[TracerEvent], optional
|
|
255
|
+
Filter events by this specific tracer event type.
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
List[TracerEvent]
|
|
260
|
+
The last N tracer events that match the filter criteria.
|
|
261
|
+
"""
|
|
262
|
+
base_type = event_type or TracerEvent
|
|
263
|
+
return self.event_store.get_last_n_events(n, event_type=base_type)
|
|
264
|
+
|
|
265
|
+
def clear(self) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Clear all events from the event store.
|
|
268
|
+
"""
|
|
269
|
+
self.event_store.clear()
|
|
270
|
+
|
|
271
|
+
def enable(self) -> None:
|
|
272
|
+
"""
|
|
273
|
+
Enable the tracer system.
|
|
274
|
+
"""
|
|
275
|
+
self.enabled = True
|
|
276
|
+
|
|
277
|
+
def disable(self) -> None:
|
|
278
|
+
"""
|
|
279
|
+
Disable the tracer system.
|
|
280
|
+
"""
|
|
281
|
+
self.enabled = False
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Import and use the null tracer directly in client code via:
|
|
285
|
+
# from mojentic.tracer import null_tracer
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Dict, List, Optional, Type, Union
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from mojentic.tracer.tracer_events import (
|
|
7
|
+
TracerEvent,
|
|
8
|
+
LLMCallTracerEvent,
|
|
9
|
+
LLMResponseTracerEvent,
|
|
10
|
+
ToolCallTracerEvent,
|
|
11
|
+
AgentInteractionTracerEvent
|
|
12
|
+
)
|
|
13
|
+
from mojentic.tracer.tracer_system import TracerSystem
|
|
14
|
+
from mojentic.tracer.event_store import EventStore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DescribeTracerSystem:
|
|
18
|
+
"""
|
|
19
|
+
Tests for the TracerSystem class.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def should_initialize_with_default_event_store(self):
|
|
23
|
+
"""
|
|
24
|
+
Given no event store
|
|
25
|
+
When initializing a tracer system
|
|
26
|
+
Then it should create a default event store
|
|
27
|
+
"""
|
|
28
|
+
# Given / When
|
|
29
|
+
tracer_system = TracerSystem()
|
|
30
|
+
|
|
31
|
+
# Then
|
|
32
|
+
assert tracer_system.event_store is not None
|
|
33
|
+
assert isinstance(tracer_system.event_store, EventStore)
|
|
34
|
+
assert tracer_system.enabled is True
|
|
35
|
+
|
|
36
|
+
def should_initialize_with_provided_event_store(self):
|
|
37
|
+
"""
|
|
38
|
+
Given an event store
|
|
39
|
+
When initializing a tracer system with it
|
|
40
|
+
Then it should use the provided event store
|
|
41
|
+
"""
|
|
42
|
+
# Given
|
|
43
|
+
event_store = EventStore()
|
|
44
|
+
|
|
45
|
+
# When
|
|
46
|
+
tracer_system = TracerSystem(event_store=event_store)
|
|
47
|
+
|
|
48
|
+
# Then
|
|
49
|
+
assert tracer_system.event_store is event_store
|
|
50
|
+
|
|
51
|
+
def should_record_llm_call(self):
|
|
52
|
+
"""
|
|
53
|
+
Given an enabled tracer system
|
|
54
|
+
When recording an LLM call
|
|
55
|
+
Then it should store an LLMCallTracerEvent
|
|
56
|
+
"""
|
|
57
|
+
# Given
|
|
58
|
+
tracer_system = TracerSystem()
|
|
59
|
+
|
|
60
|
+
# When
|
|
61
|
+
messages = [{"role": "system", "content": "You are a helpful assistant."}]
|
|
62
|
+
tracer_system.record_llm_call("test-model", messages, 0.7)
|
|
63
|
+
|
|
64
|
+
# Then
|
|
65
|
+
events = tracer_system.get_events(event_type=LLMCallTracerEvent)
|
|
66
|
+
assert len(events) == 1
|
|
67
|
+
event = events[0]
|
|
68
|
+
assert event.model == "test-model"
|
|
69
|
+
assert event.messages == messages
|
|
70
|
+
assert event.temperature == 0.7
|
|
71
|
+
|
|
72
|
+
def should_record_llm_response(self):
|
|
73
|
+
"""
|
|
74
|
+
Given an enabled tracer system
|
|
75
|
+
When recording an LLM response
|
|
76
|
+
Then it should store an LLMResponseTracerEvent
|
|
77
|
+
"""
|
|
78
|
+
# Given
|
|
79
|
+
tracer_system = TracerSystem()
|
|
80
|
+
|
|
81
|
+
# When
|
|
82
|
+
tracer_system.record_llm_response(
|
|
83
|
+
"test-model",
|
|
84
|
+
"This is a test response",
|
|
85
|
+
call_duration_ms=150.5
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Then
|
|
89
|
+
events = tracer_system.get_events(event_type=LLMResponseTracerEvent)
|
|
90
|
+
assert len(events) == 1
|
|
91
|
+
event = events[0]
|
|
92
|
+
assert event.model == "test-model"
|
|
93
|
+
assert event.content == "This is a test response"
|
|
94
|
+
assert event.call_duration_ms == 150.5
|
|
95
|
+
|
|
96
|
+
def should_record_tool_call(self):
|
|
97
|
+
"""
|
|
98
|
+
Given an enabled tracer system
|
|
99
|
+
When recording a tool call
|
|
100
|
+
Then it should store a ToolCallTracerEvent
|
|
101
|
+
"""
|
|
102
|
+
# Given
|
|
103
|
+
tracer_system = TracerSystem()
|
|
104
|
+
|
|
105
|
+
# When
|
|
106
|
+
arguments = {"query": "test query"}
|
|
107
|
+
tracer_system.record_tool_call(
|
|
108
|
+
"test-tool",
|
|
109
|
+
arguments,
|
|
110
|
+
"test result",
|
|
111
|
+
"TestAgent"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Then
|
|
115
|
+
events = tracer_system.get_events(event_type=ToolCallTracerEvent)
|
|
116
|
+
assert len(events) == 1
|
|
117
|
+
event = events[0]
|
|
118
|
+
assert event.tool_name == "test-tool"
|
|
119
|
+
assert event.arguments == arguments
|
|
120
|
+
assert event.result == "test result"
|
|
121
|
+
assert event.caller == "TestAgent"
|
|
122
|
+
|
|
123
|
+
def should_record_agent_interaction(self):
|
|
124
|
+
"""
|
|
125
|
+
Given an enabled tracer system
|
|
126
|
+
When recording an agent interaction
|
|
127
|
+
Then it should store an AgentInteractionTracerEvent
|
|
128
|
+
"""
|
|
129
|
+
# Given
|
|
130
|
+
tracer_system = TracerSystem()
|
|
131
|
+
|
|
132
|
+
# When
|
|
133
|
+
tracer_system.record_agent_interaction(
|
|
134
|
+
"AgentA",
|
|
135
|
+
"AgentB",
|
|
136
|
+
"RequestEvent",
|
|
137
|
+
"12345"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Then
|
|
141
|
+
events = tracer_system.get_events(event_type=AgentInteractionTracerEvent)
|
|
142
|
+
assert len(events) == 1
|
|
143
|
+
event = events[0]
|
|
144
|
+
assert event.from_agent == "AgentA"
|
|
145
|
+
assert event.to_agent == "AgentB"
|
|
146
|
+
assert event.event_type == "RequestEvent"
|
|
147
|
+
assert event.event_id == "12345"
|
|
148
|
+
|
|
149
|
+
def should_not_record_when_disabled(self):
|
|
150
|
+
"""
|
|
151
|
+
Given a disabled tracer system
|
|
152
|
+
When recording events
|
|
153
|
+
Then no events should be stored
|
|
154
|
+
"""
|
|
155
|
+
# Given
|
|
156
|
+
tracer_system = TracerSystem(enabled=False)
|
|
157
|
+
|
|
158
|
+
# When
|
|
159
|
+
tracer_system.record_llm_call("test-model", [])
|
|
160
|
+
tracer_system.record_llm_response("test-model", "response")
|
|
161
|
+
tracer_system.record_tool_call("test-tool", {}, "result")
|
|
162
|
+
tracer_system.record_agent_interaction("AgentA", "AgentB", "Event")
|
|
163
|
+
|
|
164
|
+
# Then
|
|
165
|
+
assert len(tracer_system.get_events()) == 0
|
|
166
|
+
|
|
167
|
+
def should_filter_events_by_time_range(self):
|
|
168
|
+
"""
|
|
169
|
+
Given several tracer events with different timestamps
|
|
170
|
+
When filtered by time range
|
|
171
|
+
Then only events within that time range should be returned
|
|
172
|
+
"""
|
|
173
|
+
# Given
|
|
174
|
+
tracer_system = TracerSystem()
|
|
175
|
+
|
|
176
|
+
# Create events with specific timestamps
|
|
177
|
+
now = time.time()
|
|
178
|
+
tracer_system.event_store.store(
|
|
179
|
+
LLMCallTracerEvent(source=DescribeTracerSystem, timestamp=now - 100, model="model1", messages=[])
|
|
180
|
+
)
|
|
181
|
+
tracer_system.event_store.store(
|
|
182
|
+
LLMCallTracerEvent(source=DescribeTracerSystem, timestamp=now - 50, model="model2", messages=[])
|
|
183
|
+
)
|
|
184
|
+
tracer_system.event_store.store(
|
|
185
|
+
LLMCallTracerEvent(source=DescribeTracerSystem, timestamp=now, model="model3", messages=[])
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# When
|
|
189
|
+
result = tracer_system.get_events(start_time=now - 75, end_time=now - 25)
|
|
190
|
+
|
|
191
|
+
# Then
|
|
192
|
+
assert len(result) == 1
|
|
193
|
+
assert result[0].model == "model2"
|
|
194
|
+
|
|
195
|
+
def should_get_last_n_tracer_events(self):
|
|
196
|
+
"""
|
|
197
|
+
Given several tracer events of different types
|
|
198
|
+
When requesting the last N tracer events of a specific type
|
|
199
|
+
Then only the most recent N events of that type should be returned
|
|
200
|
+
"""
|
|
201
|
+
# Given
|
|
202
|
+
tracer_system = TracerSystem()
|
|
203
|
+
|
|
204
|
+
tracer_system.record_llm_call("model1", [])
|
|
205
|
+
tracer_system.record_tool_call("tool1", {}, "result1")
|
|
206
|
+
tracer_system.record_llm_call("model2", [])
|
|
207
|
+
tracer_system.record_llm_call("model3", [])
|
|
208
|
+
|
|
209
|
+
# When
|
|
210
|
+
result = tracer_system.get_last_n_tracer_events(2, event_type=LLMCallTracerEvent)
|
|
211
|
+
|
|
212
|
+
# Then
|
|
213
|
+
assert len(result) == 2
|
|
214
|
+
assert result[0].model == "model2"
|
|
215
|
+
assert result[1].model == "model3"
|
|
216
|
+
|
|
217
|
+
def should_enable_and_disable(self):
|
|
218
|
+
"""
|
|
219
|
+
Given a tracer system
|
|
220
|
+
When enabling and disabling it
|
|
221
|
+
Then its enabled state should change accordingly
|
|
222
|
+
"""
|
|
223
|
+
# Given
|
|
224
|
+
tracer_system = TracerSystem(enabled=False)
|
|
225
|
+
assert not tracer_system.enabled
|
|
226
|
+
|
|
227
|
+
# When
|
|
228
|
+
tracer_system.enable()
|
|
229
|
+
|
|
230
|
+
# Then
|
|
231
|
+
assert tracer_system.enabled
|
|
232
|
+
|
|
233
|
+
# When
|
|
234
|
+
tracer_system.disable()
|
|
235
|
+
|
|
236
|
+
# Then
|
|
237
|
+
assert not tracer_system.enabled
|
|
238
|
+
|
|
239
|
+
def should_clear_events(self):
|
|
240
|
+
"""
|
|
241
|
+
Given a tracer system with events
|
|
242
|
+
When clearing events
|
|
243
|
+
Then all events should be removed
|
|
244
|
+
"""
|
|
245
|
+
# Given
|
|
246
|
+
tracer_system = TracerSystem()
|
|
247
|
+
tracer_system.record_llm_call("model1", [])
|
|
248
|
+
tracer_system.record_tool_call("tool1", {}, "result1")
|
|
249
|
+
assert len(tracer_system.get_events()) == 2
|
|
250
|
+
|
|
251
|
+
# When
|
|
252
|
+
tracer_system.clear()
|
|
253
|
+
|
|
254
|
+
# Then
|
|
255
|
+
assert len(tracer_system.get_events()) == 0
|