mojentic 0.5.7__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.
@@ -0,0 +1,111 @@
1
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
2
+ import time
3
+ from datetime import datetime
4
+
5
+ from mojentic.event import Event
6
+ from mojentic.tracer.tracer_events import TracerEvent
7
+
8
+
9
+ class EventStore:
10
+ """
11
+ Store for capturing and querying events, particularly useful for tracer events.
12
+ """
13
+ def __init__(self, on_store_callback: Optional[Callable[[Event], None]] = None):
14
+ """
15
+ Initialize an EventStore.
16
+
17
+ Parameters
18
+ ----------
19
+ on_store_callback : Callable[[Event], None], optional
20
+ A callback function that will be called whenever an event is stored.
21
+ The callback receives the stored event as its argument.
22
+ """
23
+ self.events = []
24
+ self.on_store_callback = on_store_callback
25
+
26
+ def store(self, event: Event) -> None:
27
+ """
28
+ Store an event in the event store.
29
+
30
+ Parameters
31
+ ----------
32
+ event : Event
33
+ The event to store.
34
+ """
35
+ self.events.append(event)
36
+
37
+ # Call the callback if it exists
38
+ if self.on_store_callback is not None:
39
+ self.on_store_callback(event)
40
+
41
+ def get_events(self,
42
+ event_type: Optional[Type[Event]] = None,
43
+ start_time: Optional[float] = None,
44
+ end_time: Optional[float] = None,
45
+ filter_func: Optional[Callable[[Event], bool]] = None) -> List[Event]:
46
+ """
47
+ Get events from the store, optionally filtered by type, time range, and custom filter function.
48
+
49
+ Parameters
50
+ ----------
51
+ event_type : Type[Event], optional
52
+ Filter events by this specific event type.
53
+ start_time : float, optional
54
+ Include events with timestamp >= start_time (only applies to TracerEvent types).
55
+ end_time : float, optional
56
+ Include events with timestamp <= end_time (only applies to TracerEvent types).
57
+ filter_func : Callable[[Event], bool], optional
58
+ Custom filter function to apply to events.
59
+
60
+ Returns
61
+ -------
62
+ List[Event]
63
+ Events that match the filter criteria.
64
+ """
65
+ result = self.events
66
+
67
+ # Filter by event type if specified
68
+ if event_type is not None:
69
+ result = [e for e in result if isinstance(e, event_type)]
70
+
71
+ # Filter by time range if dealing with TracerEvents
72
+ if start_time is not None:
73
+ result = [e for e in result if isinstance(e, TracerEvent) and e.timestamp >= start_time]
74
+
75
+ if end_time is not None:
76
+ result = [e for e in result if isinstance(e, TracerEvent) and e.timestamp <= end_time]
77
+
78
+ # Apply custom filter function if provided
79
+ if filter_func is not None:
80
+ result = [e for e in result if filter_func(e)]
81
+
82
+ return result
83
+
84
+ def clear(self) -> None:
85
+ """
86
+ Clear all events from the store.
87
+ """
88
+ self.events = []
89
+
90
+ def get_last_n_events(self, n: int, event_type: Optional[Type[Event]] = None) -> List[Event]:
91
+ """
92
+ Get the last N events, optionally filtered by type.
93
+
94
+ Parameters
95
+ ----------
96
+ n : int
97
+ Number of events to return.
98
+ event_type : Type[Event], optional
99
+ Filter events by this specific event type.
100
+
101
+ Returns
102
+ -------
103
+ List[Event]
104
+ The last N events that match the filter criteria.
105
+ """
106
+ if event_type is not None:
107
+ filtered = [e for e in self.events if isinstance(e, event_type)]
108
+ else:
109
+ filtered = self.events
110
+
111
+ return filtered[-n:] if n < len(filtered) else filtered
@@ -0,0 +1,210 @@
1
+ import time
2
+ from typing import List
3
+
4
+ from mojentic import Event
5
+ from mojentic.tracer.tracer_events import TracerEvent, LLMCallTracerEvent, AgentInteractionTracerEvent
6
+ from mojentic.tracer.event_store import EventStore
7
+
8
+
9
+ class TestEvent(Event):
10
+ """A simple event for testing."""
11
+ value: int
12
+
13
+
14
+ class TestTracerEvent(TracerEvent):
15
+ """A simple tracer event for testing."""
16
+ value: int
17
+
18
+
19
+ class DescribeEventStore:
20
+ """
21
+ The immutable event store is key to the traceability of agent interactions.
22
+ """
23
+
24
+ def should_store_an_event(self):
25
+ """
26
+ Given an event
27
+ When asked to store the event
28
+ Then the event should be stored
29
+ """
30
+ # Given
31
+ event = Event(
32
+ source=DescribeEventStore,
33
+ )
34
+ event_store = EventStore()
35
+
36
+ # When
37
+ event_store.store(event)
38
+
39
+ # Then
40
+ assert event in event_store.events
41
+
42
+ def should_filter_events_by_type(self):
43
+ """
44
+ Given several events of different types
45
+ When filtered by a specific type
46
+ Then only events of that type should be returned
47
+ """
48
+ # Given
49
+ event_store = EventStore()
50
+ event1 = Event(source=DescribeEventStore)
51
+ event2 = TestEvent(source=DescribeEventStore, value=42)
52
+ event3 = TestEvent(source=DescribeEventStore, value=43)
53
+ event_store.store(event1)
54
+ event_store.store(event2)
55
+ event_store.store(event3)
56
+
57
+ # When
58
+ result = event_store.get_events(event_type=TestEvent)
59
+
60
+ # Then
61
+ assert len(result) == 2
62
+ assert event1 not in result
63
+ assert event2 in result
64
+ assert event3 in result
65
+
66
+ def should_filter_tracer_events_by_time_range(self):
67
+ """
68
+ Given several tracer events with different timestamps
69
+ When filtered by time range
70
+ Then only events within that time range should be returned
71
+ """
72
+ # Given
73
+ event_store = EventStore()
74
+ now = time.time()
75
+ event1 = TestTracerEvent(source=DescribeEventStore, timestamp=now - 100, value=1)
76
+ event2 = TestTracerEvent(source=DescribeEventStore, timestamp=now - 50, value=2)
77
+ event3 = TestTracerEvent(source=DescribeEventStore, timestamp=now, value=3)
78
+ event_store.store(event1)
79
+ event_store.store(event2)
80
+ event_store.store(event3)
81
+
82
+ # When
83
+ result = event_store.get_events(start_time=now - 75, end_time=now - 25)
84
+
85
+ # Then
86
+ assert len(result) == 1
87
+ assert event1 not in result
88
+ assert event2 in result
89
+ assert event3 not in result
90
+
91
+ def should_apply_custom_filter_function(self):
92
+ """
93
+ Given several events
94
+ When filtered with a custom filter function
95
+ Then only events matching the filter should be returned
96
+ """
97
+ # Given
98
+ event_store = EventStore()
99
+ event1 = TestEvent(source=DescribeEventStore, value=10)
100
+ event2 = TestEvent(source=DescribeEventStore, value=20)
101
+ event3 = TestEvent(source=DescribeEventStore, value=30)
102
+ event_store.store(event1)
103
+ event_store.store(event2)
104
+ event_store.store(event3)
105
+
106
+ # When
107
+ result = event_store.get_events(filter_func=lambda e: isinstance(e, TestEvent) and e.value > 15)
108
+
109
+ # Then
110
+ assert len(result) == 2
111
+ assert event1 not in result
112
+ assert event2 in result
113
+ assert event3 in result
114
+
115
+ def should_clear_events(self):
116
+ """
117
+ Given several stored events
118
+ When the event store is cleared
119
+ Then all events should be removed
120
+ """
121
+ # Given
122
+ event_store = EventStore()
123
+ event_store.store(Event(source=DescribeEventStore))
124
+ event_store.store(Event(source=DescribeEventStore))
125
+ assert len(event_store.events) == 2
126
+
127
+ # When
128
+ event_store.clear()
129
+
130
+ # Then
131
+ assert len(event_store.events) == 0
132
+
133
+ def should_get_last_n_events(self):
134
+ """
135
+ Given several events
136
+ When requesting the last N events
137
+ Then only the most recent N events should be returned
138
+ """
139
+ # Given
140
+ event_store = EventStore()
141
+ event1 = TestEvent(source=DescribeEventStore, value=1)
142
+ event2 = TestEvent(source=DescribeEventStore, value=2)
143
+ event3 = TestEvent(source=DescribeEventStore, value=3)
144
+ event4 = Event(source=DescribeEventStore)
145
+ event_store.store(event1)
146
+ event_store.store(event2)
147
+ event_store.store(event3)
148
+ event_store.store(event4)
149
+
150
+ # When
151
+ result = event_store.get_last_n_events(2)
152
+
153
+ # Then
154
+ assert len(result) == 2
155
+ assert event1 not in result
156
+ assert event2 not in result
157
+ assert event3 in result
158
+ assert event4 in result
159
+
160
+ def should_get_last_n_events_by_type(self):
161
+ """
162
+ Given several events of different types
163
+ When requesting the last N events of a specific type
164
+ Then only the most recent N events of that type should be returned
165
+ """
166
+ # Given
167
+ event_store = EventStore()
168
+ event1 = TestEvent(source=DescribeEventStore, value=1)
169
+ event2 = Event(source=DescribeEventStore)
170
+ event3 = TestEvent(source=DescribeEventStore, value=2)
171
+ event4 = TestEvent(source=DescribeEventStore, value=3)
172
+ event_store.store(event1)
173
+ event_store.store(event2)
174
+ event_store.store(event3)
175
+ event_store.store(event4)
176
+
177
+ # When
178
+ result = event_store.get_last_n_events(2, event_type=TestEvent)
179
+
180
+ # Then
181
+ assert len(result) == 2
182
+ assert event1 not in result
183
+ assert event2 not in result
184
+ assert event3 in result
185
+ assert event4 in result
186
+
187
+ def should_call_callback_when_storing_event(self):
188
+ """
189
+ Given an event store with a callback
190
+ When an event is stored
191
+ Then the callback should be called with the event
192
+ """
193
+ # Given
194
+ called_events = []
195
+
196
+ def callback(event):
197
+ called_events.append(event)
198
+
199
+ event_store = EventStore(on_store_callback=callback)
200
+ event1 = TestEvent(source=DescribeEventStore, value=1)
201
+ event2 = TestEvent(source=DescribeEventStore, value=2)
202
+
203
+ # When
204
+ event_store.store(event1)
205
+ event_store.store(event2)
206
+
207
+ # Then
208
+ assert len(called_events) == 2
209
+ assert called_events[0] == event1
210
+ assert called_events[1] == event2
@@ -0,0 +1,191 @@
1
+ """
2
+ NullTracer implementation to eliminate conditional checks in the code.
3
+
4
+ This module provides a NullTracer that implements the same interface as TracerSystem
5
+ but performs no operations, following the Null Object Pattern.
6
+ """
7
+ from typing import Any, Callable, Dict, List, Optional, Type
8
+
9
+ from mojentic.tracer.tracer_events import TracerEvent
10
+
11
+
12
+ class NullTracer:
13
+ """
14
+ A no-op implementation of TracerSystem that silently discards all tracing operations.
15
+
16
+ This class follows the Null Object Pattern to eliminate conditional checks in client code.
17
+ All record methods are overridden to do nothing, and all query methods return empty results.
18
+ """
19
+
20
+ def __init__(self):
21
+ """Initialize the NullTracer with disabled state."""
22
+ self.enabled = False
23
+ self.event_store = None
24
+
25
+ def record_event(self, event: TracerEvent) -> None:
26
+ """
27
+ Do nothing implementation of record_event.
28
+
29
+ Parameters
30
+ ----------
31
+ event : TracerEvent
32
+ The tracer event to record (will be ignored).
33
+ """
34
+ # Do nothing
35
+ pass
36
+
37
+ def record_llm_call(self,
38
+ model: str,
39
+ messages: List[Dict],
40
+ temperature: float = 1.0,
41
+ tools: Optional[List[Dict]] = None,
42
+ source: Any = None) -> None:
43
+ """
44
+ Do nothing implementation of record_llm_call.
45
+
46
+ Parameters
47
+ ----------
48
+ model : str
49
+ The name of the LLM model being called.
50
+ messages : List[Dict]
51
+ The messages sent to the LLM.
52
+ temperature : float, default=1.0
53
+ The temperature setting for the LLM call.
54
+ tools : List[Dict], optional
55
+ The tools available to the LLM, if any.
56
+ source : Any, optional
57
+ The source of the event.
58
+ """
59
+ # Do nothing
60
+ pass
61
+
62
+ def record_llm_response(self,
63
+ model: str,
64
+ content: str,
65
+ tool_calls: Optional[List[Dict]] = None,
66
+ call_duration_ms: Optional[float] = None,
67
+ source: Any = None) -> None:
68
+ """
69
+ Do nothing implementation of record_llm_response.
70
+
71
+ Parameters
72
+ ----------
73
+ model : str
74
+ The name of the LLM model that responded.
75
+ content : str
76
+ The content of the LLM response.
77
+ tool_calls : List[Dict], optional
78
+ Any tool calls made by the LLM in its response.
79
+ call_duration_ms : float, optional
80
+ The duration of the LLM call in milliseconds.
81
+ source : Any, optional
82
+ The source of the event.
83
+ """
84
+ # Do nothing
85
+ pass
86
+
87
+ def record_tool_call(self,
88
+ tool_name: str,
89
+ arguments: Dict[str, Any],
90
+ result: Any,
91
+ caller: Optional[str] = None,
92
+ source: Any = None) -> None:
93
+ """
94
+ Do nothing implementation of record_tool_call.
95
+
96
+ Parameters
97
+ ----------
98
+ tool_name : str
99
+ The name of the tool being called.
100
+ arguments : Dict[str, Any]
101
+ The arguments provided to the tool.
102
+ result : Any
103
+ The result returned by the tool.
104
+ caller : str, optional
105
+ The name of the agent or component calling the tool.
106
+ source : Any, optional
107
+ The source of the event.
108
+ """
109
+ # Do nothing
110
+ pass
111
+
112
+ def record_agent_interaction(self,
113
+ from_agent: str,
114
+ to_agent: str,
115
+ event_type: str,
116
+ event_id: Optional[str] = None,
117
+ source: Any = None) -> None:
118
+ """
119
+ Do nothing implementation of record_agent_interaction.
120
+
121
+ Parameters
122
+ ----------
123
+ from_agent : str
124
+ The name of the agent sending the event.
125
+ to_agent : str
126
+ The name of the agent receiving the event.
127
+ event_type : str
128
+ The type of event being processed.
129
+ event_id : str, optional
130
+ A unique identifier for the event.
131
+ source : Any, optional
132
+ The source of the event.
133
+ """
134
+ # Do nothing
135
+ pass
136
+
137
+ def get_events(self,
138
+ event_type: Optional[Type[TracerEvent]] = None,
139
+ start_time: Optional[float] = None,
140
+ end_time: Optional[float] = None,
141
+ filter_func: Optional[Callable[[TracerEvent], bool]] = None) -> List[TracerEvent]:
142
+ """
143
+ Return an empty list for any get_events request.
144
+
145
+ Parameters
146
+ ----------
147
+ event_type : Type[TracerEvent], optional
148
+ Filter events by this specific tracer event type.
149
+ start_time : float, optional
150
+ Include events with timestamp >= start_time.
151
+ end_time : float, optional
152
+ Include events with timestamp <= end_time.
153
+ filter_func : Callable[[TracerEvent], bool], optional
154
+ Custom filter function to apply to events.
155
+
156
+ Returns
157
+ -------
158
+ List[TracerEvent]
159
+ An empty list.
160
+ """
161
+ return []
162
+
163
+ def get_last_n_tracer_events(self, n: int, event_type: Optional[Type[TracerEvent]] = None) -> List[TracerEvent]:
164
+ """
165
+ Return an empty list for any get_last_n_tracer_events request.
166
+
167
+ Parameters
168
+ ----------
169
+ n : int
170
+ Number of events to return.
171
+ event_type : Type[TracerEvent], optional
172
+ Filter events by this specific tracer event type.
173
+
174
+ Returns
175
+ -------
176
+ List[TracerEvent]
177
+ An empty list.
178
+ """
179
+ return []
180
+
181
+ def clear(self) -> None:
182
+ """Do nothing implementation of clear method."""
183
+ pass
184
+
185
+ def enable(self) -> None:
186
+ """No-op method for interface compatibility."""
187
+ pass
188
+
189
+ def disable(self) -> None:
190
+ """No-op method for interface compatibility."""
191
+ pass
@@ -0,0 +1,136 @@
1
+ """
2
+ Defines tracer event types for tracking system interactions.
3
+ """
4
+ from typing import Any, Dict, List, Optional, Type
5
+ from datetime import datetime
6
+
7
+ from pydantic import Field
8
+
9
+ from mojentic.event import Event
10
+
11
+
12
+ class TracerEvent(Event):
13
+ """
14
+ Base class for all tracer-specific events.
15
+
16
+ Tracer events are used to track system interactions for observability purposes.
17
+ They are distinct from regular events which are used for agent communication.
18
+ """
19
+ timestamp: float = Field(..., description="Timestamp when the event occurred")
20
+
21
+ def printable_summary(self) -> str:
22
+ """
23
+ Return a formatted string summary of the event.
24
+
25
+ Returns
26
+ -------
27
+ str
28
+ A formatted string with the event information.
29
+ """
30
+ event_time = datetime.fromtimestamp(self.timestamp).strftime("%H:%M:%S.%f")[:-3]
31
+ return f"[{event_time}] {type(self).__name__}"
32
+
33
+
34
+ class LLMCallTracerEvent(TracerEvent):
35
+ """
36
+ Records when an LLM is called with specific messages.
37
+ """
38
+ model: str = Field(..., description="The LLM model that was used")
39
+ messages: List[dict] = Field(..., description="The messages sent to the LLM")
40
+ temperature: float = Field(1.0, description="The temperature setting used for the call")
41
+ tools: Optional[List[Dict]] = Field(None, description="The tools available to the LLM, if any")
42
+
43
+ def printable_summary(self) -> str:
44
+ """Return a formatted summary of the LLM call event."""
45
+ base_summary = super().printable_summary()
46
+ summary = f"{base_summary}\n Model: {self.model}"
47
+
48
+ if self.messages:
49
+ msg_count = len(self.messages)
50
+ summary += f"\n Messages: {msg_count} message{'s' if msg_count != 1 else ''}"
51
+
52
+ if self.temperature != 1.0:
53
+ summary += f"\n Temperature: {self.temperature}"
54
+
55
+ if self.tools:
56
+ tool_names = [tool.get('name', 'unknown') for tool in self.tools]
57
+ summary += f"\n Available Tools: {', '.join(tool_names)}"
58
+
59
+ return summary
60
+
61
+
62
+ class LLMResponseTracerEvent(TracerEvent):
63
+ """
64
+ Records when an LLM responds to a call.
65
+ """
66
+ model: str = Field(..., description="The LLM model that was used")
67
+ content: str = Field(..., description="The content of the LLM response")
68
+ tool_calls: Optional[List[Dict]] = Field(None, description="Any tool calls made by the LLM")
69
+ call_duration_ms: Optional[float] = Field(None, description="Duration of the LLM call in milliseconds")
70
+
71
+ def printable_summary(self) -> str:
72
+ """Return a formatted summary of the LLM response event."""
73
+ base_summary = super().printable_summary()
74
+ summary = f"{base_summary}\n Model: {self.model}"
75
+
76
+ if self.content:
77
+ content_preview = self.content[:100] + "..." if len(self.content) > 100 else self.content
78
+ summary += f"\n Content: {content_preview}"
79
+
80
+ if self.tool_calls:
81
+ tool_count = len(self.tool_calls)
82
+ summary += f"\n Tool Calls: {tool_count} call{'s' if tool_count != 1 else ''}"
83
+
84
+ if self.call_duration_ms is not None:
85
+ summary += f"\n Duration: {self.call_duration_ms:.2f}ms"
86
+
87
+ return summary
88
+
89
+
90
+ class ToolCallTracerEvent(TracerEvent):
91
+ """
92
+ Records when a tool is called during agent execution.
93
+ """
94
+ tool_name: str = Field(..., description="Name of the tool that was called")
95
+ arguments: Dict[str, Any] = Field(..., description="Arguments provided to the tool")
96
+ result: Any = Field(..., description="Result returned by the tool")
97
+ caller: Optional[str] = Field(None, description="Name of the agent or component that called the tool")
98
+
99
+ def printable_summary(self) -> str:
100
+ """Return a formatted summary of the tool call event."""
101
+ base_summary = super().printable_summary()
102
+ summary = f"{base_summary}\n Tool: {self.tool_name}"
103
+
104
+ if self.arguments:
105
+ summary += f"\n Arguments: {self.arguments}"
106
+
107
+ if self.result is not None:
108
+ result_str = str(self.result)
109
+ result_preview = result_str[:100] + "..." if len(result_str) > 100 else result_str
110
+ summary += f"\n Result: {result_preview}"
111
+
112
+ if self.caller:
113
+ summary += f"\n Caller: {self.caller}"
114
+
115
+ return summary
116
+
117
+
118
+ class AgentInteractionTracerEvent(TracerEvent):
119
+ """
120
+ Records interactions between agents.
121
+ """
122
+ from_agent: str = Field(..., description="Name of the agent sending the event")
123
+ to_agent: str = Field(..., description="Name of the agent receiving the event")
124
+ event_type: str = Field(..., description="Type of event being processed")
125
+ event_id: Optional[str] = Field(None, description="Unique identifier for the event")
126
+
127
+ def printable_summary(self) -> str:
128
+ """Return a formatted summary of the agent interaction event."""
129
+ base_summary = super().printable_summary()
130
+ summary = f"{base_summary}\n From: {self.from_agent} → To: {self.to_agent}"
131
+ summary += f"\n Event Type: {self.event_type}"
132
+
133
+ if self.event_id:
134
+ summary += f"\n Event ID: {self.event_id}"
135
+
136
+ return summary