mojentic 0.8.4__py3-none-any.whl → 1.0.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/async_dispatcher_example.py +12 -4
- _examples/async_llm_example.py +1 -2
- _examples/broker_as_tool.py +42 -17
- _examples/broker_examples.py +5 -7
- _examples/broker_image_examples.py +1 -1
- _examples/characterize_ollama.py +3 -3
- _examples/characterize_openai.py +1 -1
- _examples/chat_session.py +2 -2
- _examples/chat_session_with_tool.py +2 -2
- _examples/coding_file_tool.py +16 -18
- _examples/current_datetime_tool_example.py +2 -2
- _examples/embeddings.py +1 -1
- _examples/ephemeral_task_manager_example.py +15 -11
- _examples/fetch_openai_models.py +10 -3
- _examples/file_deduplication.py +6 -6
- _examples/file_tool.py +5 -5
- _examples/image_analysis.py +2 -3
- _examples/image_broker.py +1 -1
- _examples/image_broker_splat.py +1 -1
- _examples/iterative_solver.py +3 -3
- _examples/model_characterization.py +2 -0
- _examples/openai_gateway_enhanced_demo.py +15 -5
- _examples/raw.py +1 -1
- _examples/react/agents/decisioning_agent.py +173 -15
- _examples/react/agents/summarization_agent.py +89 -0
- _examples/react/agents/thinking_agent.py +84 -14
- _examples/react/agents/tool_call_agent.py +83 -0
- _examples/react/formatters.py +38 -4
- _examples/react/models/base.py +60 -11
- _examples/react/models/events.py +76 -8
- _examples/react.py +71 -21
- _examples/recursive_agent.py +2 -2
- _examples/simple_llm.py +3 -3
- _examples/simple_llm_repl.py +1 -1
- _examples/simple_structured.py +1 -1
- _examples/simple_tool.py +2 -2
- _examples/solver_chat_session.py +5 -11
- _examples/streaming.py +36 -18
- _examples/tell_user_example.py +4 -4
- _examples/tracer_demo.py +18 -20
- _examples/tracer_qt_viewer.py +49 -46
- _examples/working_memory.py +1 -1
- mojentic/__init__.py +3 -3
- mojentic/agents/__init__.py +26 -8
- mojentic/agents/{agent_broker.py → agent_event_adapter.py} +3 -3
- mojentic/agents/async_aggregator_agent_spec.py +32 -33
- mojentic/agents/async_llm_agent.py +9 -5
- mojentic/agents/async_llm_agent_spec.py +21 -22
- mojentic/agents/base_async_agent.py +2 -2
- mojentic/agents/base_llm_agent.py +6 -2
- mojentic/agents/iterative_problem_solver.py +11 -5
- mojentic/agents/simple_recursive_agent.py +11 -10
- mojentic/agents/simple_recursive_agent_spec.py +423 -0
- mojentic/async_dispatcher.py +0 -1
- mojentic/async_dispatcher_spec.py +1 -1
- mojentic/context/__init__.py +0 -2
- mojentic/dispatcher.py +7 -8
- mojentic/llm/__init__.py +5 -5
- mojentic/llm/gateways/__init__.py +19 -18
- mojentic/llm/gateways/anthropic.py +1 -0
- mojentic/llm/gateways/anthropic_messages_adapter.py +0 -1
- mojentic/llm/gateways/llm_gateway.py +1 -1
- mojentic/llm/gateways/ollama.py +23 -18
- mojentic/llm/gateways/openai.py +243 -44
- mojentic/llm/gateways/openai_message_adapter_spec.py +3 -3
- mojentic/llm/gateways/openai_model_registry.py +7 -6
- mojentic/llm/gateways/openai_model_registry_spec.py +1 -2
- mojentic/llm/gateways/openai_temperature_handling_spec.py +2 -2
- mojentic/llm/llm_broker.py +162 -2
- mojentic/llm/llm_broker_spec.py +76 -2
- mojentic/llm/message_composers.py +6 -3
- mojentic/llm/message_composers_spec.py +5 -1
- mojentic/llm/registry/__init__.py +0 -3
- mojentic/llm/registry/populate_registry_from_ollama.py +2 -2
- mojentic/llm/tools/__init__.py +0 -9
- mojentic/llm/tools/ask_user_tool.py +11 -5
- mojentic/llm/tools/current_datetime.py +9 -6
- mojentic/llm/tools/date_resolver.py +10 -4
- mojentic/llm/tools/date_resolver_spec.py +0 -1
- mojentic/llm/tools/ephemeral_task_manager/append_task_tool.py +4 -1
- mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list.py +1 -1
- mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool.py +4 -1
- mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool.py +5 -2
- mojentic/llm/tools/file_manager.py +131 -28
- mojentic/llm/tools/file_manager_spec.py +0 -3
- mojentic/llm/tools/llm_tool.py +1 -1
- mojentic/llm/tools/llm_tool_spec.py +0 -2
- mojentic/llm/tools/organic_web_search.py +4 -2
- mojentic/llm/tools/tell_user_tool.py +6 -2
- mojentic/llm/tools/tool_wrapper.py +2 -2
- mojentic/tracer/__init__.py +1 -10
- mojentic/tracer/event_store.py +7 -8
- mojentic/tracer/event_store_spec.py +1 -2
- mojentic/tracer/null_tracer.py +37 -43
- mojentic/tracer/tracer_events.py +8 -2
- mojentic/tracer/tracer_events_spec.py +6 -7
- mojentic/tracer/tracer_system.py +37 -36
- mojentic/tracer/tracer_system_spec.py +21 -6
- mojentic/utils/__init__.py +1 -1
- mojentic/utils/formatting.py +1 -0
- {mojentic-0.8.4.dist-info → mojentic-1.0.0.dist-info}/METADATA +76 -27
- mojentic-1.0.0.dist-info/RECORD +149 -0
- mojentic-0.8.4.dist-info/RECORD +0 -146
- {mojentic-0.8.4.dist-info → mojentic-1.0.0.dist-info}/WHEEL +0 -0
- {mojentic-0.8.4.dist-info → mojentic-1.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {mojentic-0.8.4.dist-info → mojentic-1.0.0.dist-info}/top_level.txt +0 -0
_examples/tracer_qt_viewer.py
CHANGED
|
@@ -14,7 +14,6 @@ Usage:
|
|
|
14
14
|
import sys
|
|
15
15
|
import uuid
|
|
16
16
|
from datetime import datetime
|
|
17
|
-
from typing import Optional
|
|
18
17
|
|
|
19
18
|
try:
|
|
20
19
|
from PyQt6.QtWidgets import (
|
|
@@ -22,7 +21,7 @@ try:
|
|
|
22
21
|
QTableWidget, QTableWidgetItem, QTextEdit, QPushButton, QLabel,
|
|
23
22
|
QHeaderView, QSplitter
|
|
24
23
|
)
|
|
25
|
-
from PyQt6.QtCore import Qt,
|
|
24
|
+
from PyQt6.QtCore import Qt, pyqtSignal, QObject, QThread
|
|
26
25
|
from PyQt6.QtGui import QColor, QFont
|
|
27
26
|
except ImportError:
|
|
28
27
|
print("Error: PyQt6 is required for this example.")
|
|
@@ -86,7 +85,7 @@ class LLMWorker(QThread):
|
|
|
86
85
|
|
|
87
86
|
class TracerViewer(QMainWindow):
|
|
88
87
|
"""Qt window that displays tracer events in real-time."""
|
|
89
|
-
|
|
88
|
+
|
|
90
89
|
def __init__(self):
|
|
91
90
|
super().__init__()
|
|
92
91
|
self.events = []
|
|
@@ -99,13 +98,13 @@ class TracerViewer(QMainWindow):
|
|
|
99
98
|
|
|
100
99
|
self._setup_ui()
|
|
101
100
|
self._setup_tracer()
|
|
102
|
-
|
|
101
|
+
|
|
103
102
|
def _setup_ui(self):
|
|
104
103
|
"""Setup the user interface."""
|
|
105
104
|
central_widget = QWidget()
|
|
106
105
|
self.setCentralWidget(central_widget)
|
|
107
106
|
main_layout = QVBoxLayout(central_widget)
|
|
108
|
-
|
|
107
|
+
|
|
109
108
|
# Title and controls
|
|
110
109
|
header_layout = QHBoxLayout()
|
|
111
110
|
title_label = QLabel("Real-time Tracer Events")
|
|
@@ -114,26 +113,26 @@ class TracerViewer(QMainWindow):
|
|
|
114
113
|
title_font.setBold(True)
|
|
115
114
|
title_label.setFont(title_font)
|
|
116
115
|
header_layout.addWidget(title_label)
|
|
117
|
-
|
|
116
|
+
|
|
118
117
|
header_layout.addStretch()
|
|
119
|
-
|
|
118
|
+
|
|
120
119
|
self.clear_button = QPushButton("Clear Events")
|
|
121
120
|
self.clear_button.clicked.connect(self.clear_events)
|
|
122
121
|
header_layout.addWidget(self.clear_button)
|
|
123
|
-
|
|
122
|
+
|
|
124
123
|
self.test_button = QPushButton("Run Test Query")
|
|
125
124
|
self.test_button.clicked.connect(self.run_test_query)
|
|
126
125
|
header_layout.addWidget(self.test_button)
|
|
127
|
-
|
|
126
|
+
|
|
128
127
|
main_layout.addLayout(header_layout)
|
|
129
|
-
|
|
128
|
+
|
|
130
129
|
# Event counter
|
|
131
130
|
self.event_count_label = QLabel("Events: 0")
|
|
132
131
|
main_layout.addWidget(self.event_count_label)
|
|
133
|
-
|
|
132
|
+
|
|
134
133
|
# Splitter for table and details
|
|
135
134
|
splitter = QSplitter(Qt.Orientation.Vertical)
|
|
136
|
-
|
|
135
|
+
|
|
137
136
|
# Events table
|
|
138
137
|
self.events_table = QTableWidget()
|
|
139
138
|
self.events_table.setColumnCount(5)
|
|
@@ -151,53 +150,53 @@ class TracerViewer(QMainWindow):
|
|
|
151
150
|
)
|
|
152
151
|
self.events_table.itemSelectionChanged.connect(self.show_event_details)
|
|
153
152
|
splitter.addWidget(self.events_table)
|
|
154
|
-
|
|
153
|
+
|
|
155
154
|
# Event details panel
|
|
156
155
|
details_widget = QWidget()
|
|
157
156
|
details_layout = QVBoxLayout(details_widget)
|
|
158
157
|
details_label = QLabel("Event Details (click an event to see details)")
|
|
159
158
|
details_label.setFont(QFont("", 10, QFont.Weight.Bold))
|
|
160
159
|
details_layout.addWidget(details_label)
|
|
161
|
-
|
|
160
|
+
|
|
162
161
|
self.details_text = QTextEdit()
|
|
163
162
|
self.details_text.setReadOnly(True)
|
|
164
163
|
self.details_text.setFont(QFont("Courier", 9))
|
|
165
164
|
details_layout.addWidget(self.details_text)
|
|
166
|
-
|
|
165
|
+
|
|
167
166
|
splitter.addWidget(details_widget)
|
|
168
167
|
splitter.setSizes([400, 300])
|
|
169
|
-
|
|
168
|
+
|
|
170
169
|
main_layout.addWidget(splitter)
|
|
171
|
-
|
|
170
|
+
|
|
172
171
|
# Status bar
|
|
173
172
|
self.statusBar().showMessage("Ready. Waiting for tracer events...")
|
|
174
|
-
|
|
173
|
+
|
|
175
174
|
def _setup_tracer(self):
|
|
176
175
|
"""Setup the tracer system with callback."""
|
|
177
176
|
def on_event_stored(event):
|
|
178
177
|
"""Callback when an event is stored."""
|
|
179
178
|
if isinstance(event, TracerEvent):
|
|
180
179
|
self.event_signaler.event_occurred.emit(event)
|
|
181
|
-
|
|
180
|
+
|
|
182
181
|
event_store = EventStore(on_store_callback=on_event_stored)
|
|
183
182
|
self.tracer = TracerSystem(event_store=event_store)
|
|
184
|
-
|
|
183
|
+
|
|
185
184
|
def add_event_to_table(self, event: TracerEvent):
|
|
186
185
|
"""Add a new event to the table."""
|
|
187
186
|
self.events.append(event)
|
|
188
|
-
|
|
187
|
+
|
|
189
188
|
row = self.events_table.rowCount()
|
|
190
189
|
self.events_table.insertRow(row)
|
|
191
|
-
|
|
190
|
+
|
|
192
191
|
# Time
|
|
193
192
|
time_str = datetime.fromtimestamp(event.timestamp).strftime("%H:%M:%S.%f")[:-3]
|
|
194
193
|
time_item = QTableWidgetItem(time_str)
|
|
195
194
|
self.events_table.setItem(row, 0, time_item)
|
|
196
|
-
|
|
195
|
+
|
|
197
196
|
# Type
|
|
198
197
|
event_type = type(event).__name__.replace("TracerEvent", "")
|
|
199
198
|
type_item = QTableWidgetItem(event_type)
|
|
200
|
-
|
|
199
|
+
|
|
201
200
|
# Color code by type
|
|
202
201
|
if isinstance(event, LLMCallTracerEvent):
|
|
203
202
|
type_item.setBackground(QColor(107, 182, 96, 50)) # Mojility green
|
|
@@ -207,32 +206,32 @@ class TracerViewer(QMainWindow):
|
|
|
207
206
|
type_item.setBackground(QColor(102, 103, 103, 50)) # Mojility grey
|
|
208
207
|
elif isinstance(event, AgentInteractionTracerEvent):
|
|
209
208
|
type_item.setBackground(QColor(100, 149, 237, 50)) # Blue
|
|
210
|
-
|
|
209
|
+
|
|
211
210
|
self.events_table.setItem(row, 1, type_item)
|
|
212
|
-
|
|
211
|
+
|
|
213
212
|
# Correlation ID (shortened)
|
|
214
213
|
corr_id_short = event.correlation_id[:8] if event.correlation_id else "N/A"
|
|
215
214
|
corr_item = QTableWidgetItem(corr_id_short)
|
|
216
215
|
corr_item.setToolTip(event.correlation_id)
|
|
217
216
|
self.events_table.setItem(row, 2, corr_item)
|
|
218
|
-
|
|
217
|
+
|
|
219
218
|
# Summary
|
|
220
219
|
summary = self._get_event_summary(event)
|
|
221
220
|
summary_item = QTableWidgetItem(summary)
|
|
222
221
|
self.events_table.setItem(row, 3, summary_item)
|
|
223
|
-
|
|
222
|
+
|
|
224
223
|
# Duration
|
|
225
224
|
duration = ""
|
|
226
225
|
if isinstance(event, (LLMResponseTracerEvent, ToolCallTracerEvent)) and event.call_duration_ms:
|
|
227
226
|
duration = f"{event.call_duration_ms:.0f}"
|
|
228
227
|
duration_item = QTableWidgetItem(duration)
|
|
229
228
|
self.events_table.setItem(row, 4, duration_item)
|
|
230
|
-
|
|
229
|
+
|
|
231
230
|
# Scroll to bottom and update counter
|
|
232
231
|
self.events_table.scrollToBottom()
|
|
233
232
|
self.event_count_label.setText(f"Events: {len(self.events)}")
|
|
234
233
|
self.statusBar().showMessage(f"New event: {event_type}")
|
|
235
|
-
|
|
234
|
+
|
|
236
235
|
def _get_event_summary(self, event: TracerEvent) -> str:
|
|
237
236
|
"""Get a brief summary of the event."""
|
|
238
237
|
if isinstance(event, LLMCallTracerEvent):
|
|
@@ -245,21 +244,21 @@ class TracerViewer(QMainWindow):
|
|
|
245
244
|
elif isinstance(event, AgentInteractionTracerEvent):
|
|
246
245
|
return f"From: {event.from_agent} → To: {event.to_agent}"
|
|
247
246
|
return "Unknown event type"
|
|
248
|
-
|
|
247
|
+
|
|
249
248
|
def show_event_details(self):
|
|
250
249
|
"""Show detailed information about the selected event."""
|
|
251
250
|
selected_rows = self.events_table.selectedIndexes()
|
|
252
251
|
if not selected_rows:
|
|
253
252
|
return
|
|
254
|
-
|
|
253
|
+
|
|
255
254
|
row = selected_rows[0].row()
|
|
256
255
|
if row >= len(self.events):
|
|
257
256
|
return
|
|
258
|
-
|
|
257
|
+
|
|
259
258
|
event = self.events[row]
|
|
260
259
|
details = self._format_event_details(event)
|
|
261
260
|
self.details_text.setPlainText(details)
|
|
262
|
-
|
|
261
|
+
|
|
263
262
|
def _format_event_details(self, event: TracerEvent) -> str:
|
|
264
263
|
"""Format detailed event information."""
|
|
265
264
|
details = []
|
|
@@ -268,7 +267,7 @@ class TracerViewer(QMainWindow):
|
|
|
268
267
|
details.append(f"Correlation ID: {event.correlation_id}")
|
|
269
268
|
details.append(f"Source: {event.source}")
|
|
270
269
|
details.append("")
|
|
271
|
-
|
|
270
|
+
|
|
272
271
|
if isinstance(event, LLMCallTracerEvent):
|
|
273
272
|
details.append("=== LLM Call Details ===")
|
|
274
273
|
details.append(f"Model: {event.model}")
|
|
@@ -285,7 +284,7 @@ class TracerViewer(QMainWindow):
|
|
|
285
284
|
if event.tools:
|
|
286
285
|
details.append("")
|
|
287
286
|
details.append(f"Available Tools: {[t.get('name') for t in event.tools]}")
|
|
288
|
-
|
|
287
|
+
|
|
289
288
|
elif isinstance(event, LLMResponseTracerEvent):
|
|
290
289
|
details.append("=== LLM Response Details ===")
|
|
291
290
|
details.append(f"Model: {event.model}")
|
|
@@ -298,19 +297,23 @@ class TracerViewer(QMainWindow):
|
|
|
298
297
|
details.append(f"Tool Calls Made: {len(event.tool_calls)}")
|
|
299
298
|
for i, tc in enumerate(event.tool_calls, 1):
|
|
300
299
|
details.append(f" {i}. {tc}")
|
|
301
|
-
|
|
300
|
+
|
|
302
301
|
elif isinstance(event, ToolCallTracerEvent):
|
|
303
302
|
details.append("=== Tool Call Details ===")
|
|
304
303
|
details.append(f"Tool Name: {event.tool_name}")
|
|
305
304
|
details.append(f"Caller: {event.caller or 'N/A'}")
|
|
306
|
-
|
|
305
|
+
duration_text = (
|
|
306
|
+
f"{event.call_duration_ms:.2f} ms"
|
|
307
|
+
if event.call_duration_ms else "N/A"
|
|
308
|
+
)
|
|
309
|
+
details.append(f"Call Duration: {duration_text}")
|
|
307
310
|
details.append("")
|
|
308
311
|
details.append("Arguments:")
|
|
309
312
|
details.append(f" {event.arguments}")
|
|
310
313
|
details.append("")
|
|
311
314
|
details.append("Result:")
|
|
312
315
|
details.append(f" {event.result}")
|
|
313
|
-
|
|
316
|
+
|
|
314
317
|
elif isinstance(event, AgentInteractionTracerEvent):
|
|
315
318
|
details.append("=== Agent Interaction Details ===")
|
|
316
319
|
details.append(f"From Agent: {event.from_agent}")
|
|
@@ -318,9 +321,9 @@ class TracerViewer(QMainWindow):
|
|
|
318
321
|
details.append(f"Event Type: {event.event_type}")
|
|
319
322
|
if event.event_id:
|
|
320
323
|
details.append(f"Event ID: {event.event_id}")
|
|
321
|
-
|
|
324
|
+
|
|
322
325
|
return "\n".join(details)
|
|
323
|
-
|
|
326
|
+
|
|
324
327
|
def clear_events(self):
|
|
325
328
|
"""Clear all events from the display."""
|
|
326
329
|
self.events.clear()
|
|
@@ -329,7 +332,7 @@ class TracerViewer(QMainWindow):
|
|
|
329
332
|
self.event_count_label.setText("Events: 0")
|
|
330
333
|
self.tracer.clear()
|
|
331
334
|
self.statusBar().showMessage("Events cleared")
|
|
332
|
-
|
|
335
|
+
|
|
333
336
|
def run_test_query(self):
|
|
334
337
|
"""Run a test query to demonstrate the tracer."""
|
|
335
338
|
# Don't start a new query if one is already running
|
|
@@ -369,13 +372,13 @@ class TracerViewer(QMainWindow):
|
|
|
369
372
|
def main():
|
|
370
373
|
"""Main entry point for the tracer viewer application."""
|
|
371
374
|
app = QApplication(sys.argv)
|
|
372
|
-
|
|
375
|
+
|
|
373
376
|
# Set application style
|
|
374
377
|
app.setStyle('Fusion')
|
|
375
|
-
|
|
378
|
+
|
|
376
379
|
window = TracerViewer()
|
|
377
380
|
window.show()
|
|
378
|
-
|
|
381
|
+
|
|
379
382
|
print("\n" + "="*80)
|
|
380
383
|
print("Mojentic Tracer - Real-time Event Viewer")
|
|
381
384
|
print("="*80)
|
|
@@ -386,7 +389,7 @@ def main():
|
|
|
386
389
|
print("\nThe viewer will show events in real-time as they occur.")
|
|
387
390
|
print("Close the window to exit.")
|
|
388
391
|
print("="*80 + "\n")
|
|
389
|
-
|
|
392
|
+
|
|
390
393
|
sys.exit(app.exec())
|
|
391
394
|
|
|
392
395
|
|
_examples/working_memory.py
CHANGED
mojentic/__init__.py
CHANGED
|
@@ -8,9 +8,9 @@ import logging
|
|
|
8
8
|
import structlog
|
|
9
9
|
|
|
10
10
|
# Core components
|
|
11
|
-
from .dispatcher import Dispatcher
|
|
12
|
-
from .event import Event
|
|
13
|
-
from .router import Router
|
|
11
|
+
from .dispatcher import Dispatcher # noqa: F401
|
|
12
|
+
from .event import Event # noqa: F401
|
|
13
|
+
from .router import Router # noqa: F401
|
|
14
14
|
|
|
15
15
|
# Initialize logging
|
|
16
16
|
logging.basicConfig(level=logging.INFO)
|
mojentic/agents/__init__.py
CHANGED
|
@@ -3,14 +3,32 @@ Mojentic agents module for creating and working with various agent types.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
# Base agent types
|
|
6
|
-
from .base_agent import BaseAgent
|
|
7
|
-
from .base_llm_agent import BaseLLMAgent
|
|
6
|
+
from mojentic.agents.base_agent import BaseAgent
|
|
7
|
+
from mojentic.agents.base_llm_agent import BaseLLMAgent
|
|
8
|
+
from mojentic.agents.base_async_agent import BaseAsyncAgent
|
|
9
|
+
from mojentic.agents.async_llm_agent import BaseAsyncLLMAgent, BaseAsyncLLMAgentWithMemory
|
|
10
|
+
from mojentic.agents.async_aggregator_agent import AsyncAggregatorAgent
|
|
8
11
|
|
|
9
12
|
# Special purpose agents
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
13
|
-
from .simple_recursive_agent import SimpleRecursiveAgent
|
|
13
|
+
from mojentic.agents.iterative_problem_solver import IterativeProblemSolver
|
|
14
|
+
from mojentic.agents.simple_recursive_agent import SimpleRecursiveAgent
|
|
15
|
+
from mojentic.agents.output_agent import OutputAgent
|
|
14
16
|
|
|
15
|
-
#
|
|
16
|
-
from .
|
|
17
|
+
# Event adapters
|
|
18
|
+
from mojentic.agents.agent_event_adapter import AgentEventAdapter
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
# Base types
|
|
22
|
+
"BaseAgent",
|
|
23
|
+
"BaseLLMAgent",
|
|
24
|
+
"BaseAsyncAgent",
|
|
25
|
+
"BaseAsyncLLMAgent",
|
|
26
|
+
"BaseAsyncLLMAgentWithMemory",
|
|
27
|
+
"AsyncAggregatorAgent",
|
|
28
|
+
# Special purpose
|
|
29
|
+
"IterativeProblemSolver",
|
|
30
|
+
"SimpleRecursiveAgent",
|
|
31
|
+
"OutputAgent",
|
|
32
|
+
# Event adapters
|
|
33
|
+
"AgentEventAdapter",
|
|
34
|
+
]
|
|
@@ -4,7 +4,7 @@ from mojentic import Event
|
|
|
4
4
|
from mojentic.agents import BaseLLMAgent
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
class
|
|
7
|
+
class AgentEventAdapter:
|
|
8
8
|
"""
|
|
9
9
|
Wraps an agent to allow participation in an asynchronous event-driven system.
|
|
10
10
|
"""
|
|
@@ -14,8 +14,8 @@ class AgentBroker:
|
|
|
14
14
|
|
|
15
15
|
def receive_event(self, event: Event) -> List[Event]:
|
|
16
16
|
"""
|
|
17
|
-
receive_event is called by the event broker when an event is to be received by an agent. The
|
|
18
|
-
a list of events determined from the agent's output in response to the received event.
|
|
17
|
+
receive_event is called by the event broker when an event is to be received by an agent. The adapter will
|
|
18
|
+
return a list of events determined from the agent's output in response to the received event.
|
|
19
19
|
|
|
20
20
|
In this way, you can perform work based on the event, and generate whatever subsequent events may need to be
|
|
21
21
|
processed next.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import pytest
|
|
3
|
-
from unittest.mock import AsyncMock, MagicMock
|
|
4
3
|
|
|
5
4
|
from mojentic.agents.async_aggregator_agent import AsyncAggregatorAgent
|
|
6
5
|
from mojentic.event import Event
|
|
@@ -28,13 +27,13 @@ class TestResultEvent(Event):
|
|
|
28
27
|
|
|
29
28
|
class TestAsyncAggregator(AsyncAggregatorAgent):
|
|
30
29
|
"""A test implementation of AsyncAggregatorAgent."""
|
|
31
|
-
|
|
30
|
+
|
|
32
31
|
async def process_events(self, events):
|
|
33
32
|
"""Process the events and return a result event."""
|
|
34
33
|
# Extract events by type
|
|
35
34
|
event1 = next((e for e in events if isinstance(e, TestEvent1)), None)
|
|
36
35
|
event2 = next((e for e in events if isinstance(e, TestEvent2)), None)
|
|
37
|
-
|
|
36
|
+
|
|
38
37
|
if event1 and event2:
|
|
39
38
|
# Create a result combining the events
|
|
40
39
|
return [TestResultEvent(
|
|
@@ -61,7 +60,7 @@ def test_async_aggregator():
|
|
|
61
60
|
async def test_async_aggregator_init():
|
|
62
61
|
"""Test that the AsyncAggregatorAgent initializes correctly."""
|
|
63
62
|
agent = AsyncAggregatorAgent(event_types_needed=[TestEvent1, TestEvent2])
|
|
64
|
-
|
|
63
|
+
|
|
65
64
|
assert agent.event_types_needed == [TestEvent1, TestEvent2]
|
|
66
65
|
assert agent.results == {}
|
|
67
66
|
assert agent.futures == {}
|
|
@@ -71,9 +70,9 @@ async def test_async_aggregator_init():
|
|
|
71
70
|
async def test_async_aggregator_capture_results(async_aggregator):
|
|
72
71
|
"""Test that the AsyncAggregatorAgent captures results correctly."""
|
|
73
72
|
event = TestEvent1(source=str, correlation_id="test-id", message="Hello")
|
|
74
|
-
|
|
73
|
+
|
|
75
74
|
await async_aggregator._capture_results_if_needed(event)
|
|
76
|
-
|
|
75
|
+
|
|
77
76
|
assert "test-id" in async_aggregator.results
|
|
78
77
|
assert len(async_aggregator.results["test-id"]) == 1
|
|
79
78
|
assert async_aggregator.results["test-id"][0] == event
|
|
@@ -84,14 +83,14 @@ async def test_async_aggregator_has_all_needed(async_aggregator):
|
|
|
84
83
|
"""Test that the AsyncAggregatorAgent checks if all needed events are captured."""
|
|
85
84
|
event1 = TestEvent1(source=str, correlation_id="test-id", message="Hello")
|
|
86
85
|
event2 = TestEvent2(source=str, correlation_id="test-id", data="World")
|
|
87
|
-
|
|
86
|
+
|
|
88
87
|
# Initially, we don't have all needed events
|
|
89
88
|
assert not await async_aggregator._has_all_needed(event1)
|
|
90
|
-
|
|
89
|
+
|
|
91
90
|
# Capture the first event
|
|
92
91
|
await async_aggregator._capture_results_if_needed(event1)
|
|
93
92
|
assert not await async_aggregator._has_all_needed(event1)
|
|
94
|
-
|
|
93
|
+
|
|
95
94
|
# Capture the second event
|
|
96
95
|
await async_aggregator._capture_results_if_needed(event2)
|
|
97
96
|
assert await async_aggregator._has_all_needed(event2)
|
|
@@ -102,14 +101,14 @@ async def test_async_aggregator_get_and_reset_results(async_aggregator):
|
|
|
102
101
|
"""Test that the AsyncAggregatorAgent gets and resets results correctly."""
|
|
103
102
|
event1 = TestEvent1(source=str, correlation_id="test-id", message="Hello")
|
|
104
103
|
event2 = TestEvent2(source=str, correlation_id="test-id", data="World")
|
|
105
|
-
|
|
104
|
+
|
|
106
105
|
# Capture the events
|
|
107
106
|
await async_aggregator._capture_results_if_needed(event1)
|
|
108
107
|
await async_aggregator._capture_results_if_needed(event2)
|
|
109
|
-
|
|
108
|
+
|
|
110
109
|
# Get and reset the results
|
|
111
110
|
results = await async_aggregator._get_and_reset_results(event1)
|
|
112
|
-
|
|
111
|
+
|
|
113
112
|
assert len(results) == 2
|
|
114
113
|
assert results[0] == event1
|
|
115
114
|
assert results[1] == event2
|
|
@@ -121,17 +120,17 @@ async def test_async_aggregator_wait_for_events(async_aggregator):
|
|
|
121
120
|
"""Test that the AsyncAggregatorAgent waits for events correctly."""
|
|
122
121
|
event1 = TestEvent1(source=str, correlation_id="test-id", message="Hello")
|
|
123
122
|
event2 = TestEvent2(source=str, correlation_id="test-id", data="World")
|
|
124
|
-
|
|
123
|
+
|
|
125
124
|
# Start waiting for events in a separate task
|
|
126
125
|
wait_task = asyncio.create_task(async_aggregator.wait_for_events("test-id", timeout=1))
|
|
127
|
-
|
|
126
|
+
|
|
128
127
|
# Capture the events
|
|
129
128
|
await async_aggregator._capture_results_if_needed(event1)
|
|
130
129
|
await async_aggregator._capture_results_if_needed(event2)
|
|
131
|
-
|
|
130
|
+
|
|
132
131
|
# Wait for the task to complete
|
|
133
132
|
results = await wait_task
|
|
134
|
-
|
|
133
|
+
|
|
135
134
|
assert len(results) == 2
|
|
136
135
|
assert results[0] == event1
|
|
137
136
|
assert results[1] == event2
|
|
@@ -141,13 +140,13 @@ async def test_async_aggregator_wait_for_events(async_aggregator):
|
|
|
141
140
|
async def test_async_aggregator_wait_for_events_timeout(async_aggregator):
|
|
142
141
|
"""Test that the AsyncAggregatorAgent handles timeouts correctly."""
|
|
143
142
|
event1 = TestEvent1(source=str, correlation_id="test-id", message="Hello")
|
|
144
|
-
|
|
143
|
+
|
|
145
144
|
# Capture only one event
|
|
146
145
|
await async_aggregator._capture_results_if_needed(event1)
|
|
147
|
-
|
|
146
|
+
|
|
148
147
|
# Wait for events with a short timeout
|
|
149
148
|
results = await async_aggregator.wait_for_events("test-id", timeout=0.1)
|
|
150
|
-
|
|
149
|
+
|
|
151
150
|
# We should get the partial results
|
|
152
151
|
assert len(results) == 1
|
|
153
152
|
assert results[0] == event1
|
|
@@ -158,14 +157,14 @@ async def test_async_aggregator_receive_event_async(test_async_aggregator):
|
|
|
158
157
|
"""Test that the AsyncAggregatorAgent processes events correctly."""
|
|
159
158
|
event1 = TestEvent1(source=str, correlation_id="test-id", message="Hello")
|
|
160
159
|
event2 = TestEvent2(source=str, correlation_id="test-id", data="World")
|
|
161
|
-
|
|
160
|
+
|
|
162
161
|
# Receive the first event - should not process yet
|
|
163
162
|
result1 = await test_async_aggregator.receive_event_async(event1)
|
|
164
163
|
assert result1 == []
|
|
165
|
-
|
|
164
|
+
|
|
166
165
|
# Receive the second event - should process both events
|
|
167
166
|
result2 = await test_async_aggregator.receive_event_async(event2)
|
|
168
|
-
|
|
167
|
+
|
|
169
168
|
assert len(result2) == 1
|
|
170
169
|
assert isinstance(result2[0], TestResultEvent)
|
|
171
170
|
assert result2[0].result == "Hello - World"
|
|
@@ -176,14 +175,14 @@ async def test_async_aggregator_receive_event_async_wrong_order(test_async_aggre
|
|
|
176
175
|
"""Test that the AsyncAggregatorAgent processes events correctly regardless of order."""
|
|
177
176
|
event1 = TestEvent1(source=str, correlation_id="test-id", message="Hello")
|
|
178
177
|
event2 = TestEvent2(source=str, correlation_id="test-id", data="World")
|
|
179
|
-
|
|
178
|
+
|
|
180
179
|
# Receive the second event first - should not process yet
|
|
181
180
|
result1 = await test_async_aggregator.receive_event_async(event2)
|
|
182
181
|
assert result1 == []
|
|
183
|
-
|
|
182
|
+
|
|
184
183
|
# Receive the first event - should process both events
|
|
185
184
|
result2 = await test_async_aggregator.receive_event_async(event1)
|
|
186
|
-
|
|
185
|
+
|
|
187
186
|
assert len(result2) == 1
|
|
188
187
|
assert isinstance(result2[0], TestResultEvent)
|
|
189
188
|
assert result2[0].result == "Hello - World"
|
|
@@ -194,22 +193,22 @@ async def test_async_aggregator_receive_event_async_different_correlation_ids(te
|
|
|
194
193
|
"""Test that the AsyncAggregatorAgent handles different correlation_ids correctly."""
|
|
195
194
|
event1_id1 = TestEvent1(source=str, correlation_id="id1", message="Hello")
|
|
196
195
|
event2_id1 = TestEvent2(source=str, correlation_id="id1", data="World")
|
|
197
|
-
|
|
196
|
+
|
|
198
197
|
event1_id2 = TestEvent1(source=str, correlation_id="id2", message="Goodbye")
|
|
199
198
|
event2_id2 = TestEvent2(source=str, correlation_id="id2", data="Universe")
|
|
200
|
-
|
|
199
|
+
|
|
201
200
|
# Receive events for id1
|
|
202
201
|
await test_async_aggregator.receive_event_async(event1_id1)
|
|
203
202
|
result1 = await test_async_aggregator.receive_event_async(event2_id1)
|
|
204
|
-
|
|
203
|
+
|
|
205
204
|
# Receive events for id2
|
|
206
205
|
await test_async_aggregator.receive_event_async(event1_id2)
|
|
207
206
|
result2 = await test_async_aggregator.receive_event_async(event2_id2)
|
|
208
|
-
|
|
207
|
+
|
|
209
208
|
# Check results for id1
|
|
210
209
|
assert len(result1) == 1
|
|
211
210
|
assert result1[0].result == "Hello - World"
|
|
212
|
-
|
|
211
|
+
|
|
213
212
|
# Check results for id2
|
|
214
213
|
assert len(result2) == 1
|
|
215
214
|
assert result2[0].result == "Goodbye - Universe"
|
|
@@ -220,8 +219,8 @@ async def test_async_aggregator_process_events_base_implementation(async_aggrega
|
|
|
220
219
|
"""Test that the base process_events implementation returns an empty list."""
|
|
221
220
|
event1 = TestEvent1(source=str, correlation_id="test-id", message="Hello")
|
|
222
221
|
event2 = TestEvent2(source=str, correlation_id="test-id", data="World")
|
|
223
|
-
|
|
222
|
+
|
|
224
223
|
events = [event1, event2]
|
|
225
224
|
result = await async_aggregator.process_events(events)
|
|
226
|
-
|
|
227
|
-
assert result == []
|
|
225
|
+
|
|
226
|
+
assert result == []
|
|
@@ -152,8 +152,12 @@ class BaseAsyncLLMAgentWithMemory(BaseAsyncLLMAgent):
|
|
|
152
152
|
"""
|
|
153
153
|
messages = super()._create_initial_messages()
|
|
154
154
|
messages.extend([
|
|
155
|
-
LLMMessage(
|
|
156
|
-
|
|
155
|
+
LLMMessage(
|
|
156
|
+
content=(f"This is what you remember:\n"
|
|
157
|
+
f"{json.dumps(self.memory.get_working_memory(), indent=2)}"
|
|
158
|
+
f"\n\nRemember anything new you learn by storing it "
|
|
159
|
+
f"to your working memory in your response.")
|
|
160
|
+
),
|
|
157
161
|
LLMMessage(role=MessageRole.User, content=self.instructions),
|
|
158
162
|
])
|
|
159
163
|
return messages
|
|
@@ -180,7 +184,7 @@ class BaseAsyncLLMAgentWithMemory(BaseAsyncLLMAgent):
|
|
|
180
184
|
messages.extend([
|
|
181
185
|
LLMMessage(content=content),
|
|
182
186
|
])
|
|
183
|
-
|
|
187
|
+
|
|
184
188
|
# Use asyncio.to_thread to run the synchronous generate_object method in a separate thread
|
|
185
189
|
import asyncio
|
|
186
190
|
response = await asyncio.to_thread(
|
|
@@ -188,10 +192,10 @@ class BaseAsyncLLMAgentWithMemory(BaseAsyncLLMAgent):
|
|
|
188
192
|
messages=messages,
|
|
189
193
|
object_model=ResponseWithMemory
|
|
190
194
|
)
|
|
191
|
-
|
|
195
|
+
|
|
192
196
|
self.memory.merge_to_working_memory(response.memory)
|
|
193
197
|
|
|
194
198
|
d = response.model_dump()
|
|
195
199
|
del d["memory"]
|
|
196
200
|
|
|
197
|
-
return self.response_model.model_validate(d)
|
|
201
|
+
return self.response_model.model_validate(d)
|