mojentic 0.5.7__py3-none-any.whl → 0.6.1__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.
@@ -176,39 +176,57 @@ class DescribeMessageBuilder:
176
176
  assert image_path2 in message_builder.image_paths
177
177
  assert result is message_builder # Returns self for method chaining
178
178
 
179
- def should_add_all_jpg_images_from_directory(self, message_builder, mocker):
180
- dir_path = Path("/path/to/images")
181
- jpg_files = [Path("/path/to/images/image1.jpg"), Path("/path/to/images/image2.jpg")]
182
-
183
- # Mock is_dir, glob, and exists methods
184
- mocker.patch.object(Path, 'is_dir', return_value=True)
185
- mocker.patch.object(Path, 'glob', return_value=jpg_files)
186
- mocker.patch.object(Path, 'exists', return_value=True)
187
-
179
+ def should_add_all_jpg_images_from_directory(self, message_builder, mocker, tmp_path):
180
+ # Create a temporary directory with test image files
181
+ dir_path = tmp_path / "images"
182
+ dir_path.mkdir()
183
+
184
+ # Create empty test image files
185
+ image1_path = dir_path / "image1.jpg"
186
+ image2_path = dir_path / "image2.jpg"
187
+ image1_path.touch()
188
+ image2_path.touch()
189
+
190
+ # Create a text file that should be ignored
191
+ text_file_path = dir_path / "text.txt"
192
+ text_file_path.touch()
193
+
194
+ # Use the real directory for the test
188
195
  message_builder.add_images(dir_path)
189
196
 
190
- assert jpg_files[0] in message_builder.image_paths
191
- assert jpg_files[1] in message_builder.image_paths
197
+ # Convert to strings for easier comparison
198
+ image_paths_str = [str(p) for p in message_builder.image_paths]
192
199
 
193
- def should_add_images_matching_glob_pattern(self, message_builder, mocker):
194
- pattern_path = Path("/path/to/*.jpg")
195
- matching_files = [Path("/path/to/image1.jpg"), Path("/path/to/image2.jpg")]
200
+ # Verify only jpg files were added
201
+ assert str(image1_path) in image_paths_str
202
+ assert str(image2_path) in image_paths_str
203
+ assert str(text_file_path) not in image_paths_str
196
204
 
197
- # Mock methods
198
- mocker.patch.object(Path, 'is_dir', return_value=False)
199
- mocker.patch.object(Path, 'glob', return_value=matching_files)
200
- mocker.patch.object(Path, 'is_file', return_value=True)
201
- mocker.patch.object(Path, 'exists', return_value=True)
205
+ def should_add_images_matching_glob_pattern(self, message_builder, tmp_path):
206
+ # Create a temporary directory with test image files
207
+ dir_path = tmp_path / "glob_test"
208
+ dir_path.mkdir()
202
209
 
203
- # Mock the parent property and its glob method
204
- parent_mock = mocker.MagicMock()
205
- parent_mock.glob.return_value = matching_files
206
- mocker.patch.object(Path, 'parent', parent_mock)
210
+ # Create test image files
211
+ image1_path = dir_path / "image1.jpg"
212
+ image2_path = dir_path / "image2.jpg"
213
+ image3_path = dir_path / "other.png" # Should not match
214
+ image1_path.touch()
215
+ image2_path.touch()
216
+ image3_path.touch()
217
+
218
+ # Use a real glob pattern
219
+ pattern_path = dir_path / "*.jpg"
207
220
 
208
221
  message_builder.add_images(pattern_path)
209
222
 
210
- assert matching_files[0] in message_builder.image_paths
211
- assert matching_files[1] in message_builder.image_paths
223
+ # Convert to strings for easier comparison
224
+ image_paths_str = [str(p) for p in message_builder.image_paths]
225
+
226
+ # Verify only jpg files matching the pattern were added
227
+ assert str(image1_path) in image_paths_str
228
+ assert str(image2_path) in image_paths_str
229
+ assert str(image3_path) not in image_paths_str
212
230
 
213
231
  class DescribeLoadContentMethod:
214
232
  """
@@ -1,14 +1,41 @@
1
1
  import json
2
+ from typing import Any, Dict, Optional
2
3
 
4
+ from mojentic.tracer.tracer_system import TracerSystem
3
5
  from mojentic.llm.gateways.models import TextContent
4
6
 
5
7
 
6
8
  class LLMTool:
9
+ def __init__(self, tracer: Optional[TracerSystem] = None):
10
+ """
11
+ Initialize an LLM tool with optional tracer system.
12
+
13
+ Parameters
14
+ ----------
15
+ tracer : TracerSystem, optional
16
+ The tracer system to use for recording tool usage.
17
+ """
18
+ # Use null_tracer if no tracer is provided
19
+ from mojentic.tracer import null_tracer
20
+ self.tracer = tracer or null_tracer
21
+
7
22
  def run(self, **kwargs):
8
23
  raise NotImplementedError
9
24
 
10
- def call_tool(self, **kwargs):
25
+ def call_tool(self, correlation_id: str = None, **kwargs):
26
+ # Execute the tool and capture the result
11
27
  result = self.run(**kwargs)
28
+
29
+ # Record the tool call in the tracer system (always safe to call with null_tracer)
30
+ self.tracer.record_tool_call(
31
+ tool_name=self.name,
32
+ arguments=kwargs,
33
+ result=result,
34
+ source=type(self),
35
+ correlation_id=correlation_id
36
+ )
37
+
38
+ # Format the result
12
39
  if isinstance(result, dict):
13
40
  result = json.dumps(result)
14
41
  return {
@@ -10,7 +10,8 @@ from mojentic.llm.tools.llm_tool import LLMTool
10
10
  class MockLLMTool(LLMTool):
11
11
  """A mock implementation of LLMTool for testing purposes."""
12
12
 
13
- def __init__(self, run_result=None):
13
+ def __init__(self, run_result=None, tracer=None):
14
+ super().__init__(tracer=tracer)
14
15
  self._run_result = run_result
15
16
  self._descriptor = {
16
17
  "function": {
@@ -0,0 +1,16 @@
1
+
2
+ """
3
+ Mojentic tracer module for tracking system operations.
4
+ """
5
+ from mojentic.tracer.tracer_events import (
6
+ TracerEvent,
7
+ LLMCallTracerEvent,
8
+ LLMResponseTracerEvent,
9
+ ToolCallTracerEvent,
10
+ AgentInteractionTracerEvent
11
+ )
12
+ from mojentic.tracer.tracer_system import TracerSystem
13
+ from mojentic.tracer.null_tracer import NullTracer
14
+
15
+ # Create a singleton NullTracer instance for use throughout the application
16
+ null_tracer = NullTracer()
@@ -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,203 @@
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,
43
+ correlation_id: str = None) -> None:
44
+ """
45
+ Do nothing implementation of record_llm_call.
46
+
47
+ Parameters
48
+ ----------
49
+ model : str
50
+ The name of the LLM model being called.
51
+ messages : List[Dict]
52
+ The messages sent to the LLM.
53
+ temperature : float, default=1.0
54
+ The temperature setting for the LLM call.
55
+ tools : List[Dict], optional
56
+ The tools available to the LLM, if any.
57
+ source : Any, optional
58
+ The source of the event.
59
+ correlation_id : str, optional
60
+ UUID string that is copied from cause-to-affect for tracing events.
61
+ """
62
+ # Do nothing
63
+ pass
64
+
65
+ def record_llm_response(self,
66
+ model: str,
67
+ content: str,
68
+ tool_calls: Optional[List[Dict]] = None,
69
+ call_duration_ms: Optional[float] = None,
70
+ source: Any = None,
71
+ correlation_id: str = None) -> None:
72
+ """
73
+ Do nothing implementation of record_llm_response.
74
+
75
+ Parameters
76
+ ----------
77
+ model : str
78
+ The name of the LLM model that responded.
79
+ content : str
80
+ The content of the LLM response.
81
+ tool_calls : List[Dict], optional
82
+ Any tool calls made by the LLM in its response.
83
+ call_duration_ms : float, optional
84
+ The duration of the LLM call in milliseconds.
85
+ source : Any, optional
86
+ The source of the event.
87
+ correlation_id : str, optional
88
+ UUID string that is copied from cause-to-affect for tracing events.
89
+ """
90
+ # Do nothing
91
+ pass
92
+
93
+ def record_tool_call(self,
94
+ tool_name: str,
95
+ arguments: Dict[str, Any],
96
+ result: Any,
97
+ caller: Optional[str] = None,
98
+ source: Any = None,
99
+ correlation_id: str = None) -> None:
100
+ """
101
+ Do nothing implementation of record_tool_call.
102
+
103
+ Parameters
104
+ ----------
105
+ tool_name : str
106
+ The name of the tool being called.
107
+ arguments : Dict[str, Any]
108
+ The arguments provided to the tool.
109
+ result : Any
110
+ The result returned by the tool.
111
+ caller : str, optional
112
+ The name of the agent or component calling the tool.
113
+ source : Any, optional
114
+ The source of the event.
115
+ correlation_id : str, optional
116
+ UUID string that is copied from cause-to-affect for tracing events.
117
+ """
118
+ # Do nothing
119
+ pass
120
+
121
+ def record_agent_interaction(self,
122
+ from_agent: str,
123
+ to_agent: str,
124
+ event_type: str,
125
+ event_id: Optional[str] = None,
126
+ source: Any = None,
127
+ correlation_id: str = None) -> None:
128
+ """
129
+ Do nothing implementation of record_agent_interaction.
130
+
131
+ Parameters
132
+ ----------
133
+ from_agent : str
134
+ The name of the agent sending the event.
135
+ to_agent : str
136
+ The name of the agent receiving the event.
137
+ event_type : str
138
+ The type of event being processed.
139
+ event_id : str, optional
140
+ A unique identifier for the event.
141
+ source : Any, optional
142
+ The source of the event.
143
+ correlation_id : str, optional
144
+ UUID string that is copied from cause-to-affect for tracing events.
145
+ """
146
+ # Do nothing
147
+ pass
148
+
149
+ def get_events(self,
150
+ event_type: Optional[Type[TracerEvent]] = None,
151
+ start_time: Optional[float] = None,
152
+ end_time: Optional[float] = None,
153
+ filter_func: Optional[Callable[[TracerEvent], bool]] = None) -> List[TracerEvent]:
154
+ """
155
+ Return an empty list for any get_events request.
156
+
157
+ Parameters
158
+ ----------
159
+ event_type : Type[TracerEvent], optional
160
+ Filter events by this specific tracer event type.
161
+ start_time : float, optional
162
+ Include events with timestamp >= start_time.
163
+ end_time : float, optional
164
+ Include events with timestamp <= end_time.
165
+ filter_func : Callable[[TracerEvent], bool], optional
166
+ Custom filter function to apply to events.
167
+
168
+ Returns
169
+ -------
170
+ List[TracerEvent]
171
+ An empty list.
172
+ """
173
+ return []
174
+
175
+ def get_last_n_tracer_events(self, n: int, event_type: Optional[Type[TracerEvent]] = None) -> List[TracerEvent]:
176
+ """
177
+ Return an empty list for any get_last_n_tracer_events request.
178
+
179
+ Parameters
180
+ ----------
181
+ n : int
182
+ Number of events to return.
183
+ event_type : Type[TracerEvent], optional
184
+ Filter events by this specific tracer event type.
185
+
186
+ Returns
187
+ -------
188
+ List[TracerEvent]
189
+ An empty list.
190
+ """
191
+ return []
192
+
193
+ def clear(self) -> None:
194
+ """Do nothing implementation of clear method."""
195
+ pass
196
+
197
+ def enable(self) -> None:
198
+ """No-op method for interface compatibility."""
199
+ pass
200
+
201
+ def disable(self) -> None:
202
+ """No-op method for interface compatibility."""
203
+ pass