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.
Files changed (106) hide show
  1. _examples/async_dispatcher_example.py +12 -4
  2. _examples/async_llm_example.py +1 -2
  3. _examples/broker_as_tool.py +42 -17
  4. _examples/broker_examples.py +5 -7
  5. _examples/broker_image_examples.py +1 -1
  6. _examples/characterize_ollama.py +3 -3
  7. _examples/characterize_openai.py +1 -1
  8. _examples/chat_session.py +2 -2
  9. _examples/chat_session_with_tool.py +2 -2
  10. _examples/coding_file_tool.py +16 -18
  11. _examples/current_datetime_tool_example.py +2 -2
  12. _examples/embeddings.py +1 -1
  13. _examples/ephemeral_task_manager_example.py +15 -11
  14. _examples/fetch_openai_models.py +10 -3
  15. _examples/file_deduplication.py +6 -6
  16. _examples/file_tool.py +5 -5
  17. _examples/image_analysis.py +2 -3
  18. _examples/image_broker.py +1 -1
  19. _examples/image_broker_splat.py +1 -1
  20. _examples/iterative_solver.py +3 -3
  21. _examples/model_characterization.py +2 -0
  22. _examples/openai_gateway_enhanced_demo.py +15 -5
  23. _examples/raw.py +1 -1
  24. _examples/react/agents/decisioning_agent.py +173 -15
  25. _examples/react/agents/summarization_agent.py +89 -0
  26. _examples/react/agents/thinking_agent.py +84 -14
  27. _examples/react/agents/tool_call_agent.py +83 -0
  28. _examples/react/formatters.py +38 -4
  29. _examples/react/models/base.py +60 -11
  30. _examples/react/models/events.py +76 -8
  31. _examples/react.py +71 -21
  32. _examples/recursive_agent.py +2 -2
  33. _examples/simple_llm.py +3 -3
  34. _examples/simple_llm_repl.py +1 -1
  35. _examples/simple_structured.py +1 -1
  36. _examples/simple_tool.py +2 -2
  37. _examples/solver_chat_session.py +5 -11
  38. _examples/streaming.py +36 -18
  39. _examples/tell_user_example.py +4 -4
  40. _examples/tracer_demo.py +18 -20
  41. _examples/tracer_qt_viewer.py +49 -46
  42. _examples/working_memory.py +1 -1
  43. mojentic/__init__.py +3 -3
  44. mojentic/agents/__init__.py +26 -8
  45. mojentic/agents/{agent_broker.py → agent_event_adapter.py} +3 -3
  46. mojentic/agents/async_aggregator_agent_spec.py +32 -33
  47. mojentic/agents/async_llm_agent.py +9 -5
  48. mojentic/agents/async_llm_agent_spec.py +21 -22
  49. mojentic/agents/base_async_agent.py +2 -2
  50. mojentic/agents/base_llm_agent.py +6 -2
  51. mojentic/agents/iterative_problem_solver.py +11 -5
  52. mojentic/agents/simple_recursive_agent.py +11 -10
  53. mojentic/agents/simple_recursive_agent_spec.py +423 -0
  54. mojentic/async_dispatcher.py +0 -1
  55. mojentic/async_dispatcher_spec.py +1 -1
  56. mojentic/context/__init__.py +0 -2
  57. mojentic/dispatcher.py +7 -8
  58. mojentic/llm/__init__.py +5 -5
  59. mojentic/llm/gateways/__init__.py +19 -18
  60. mojentic/llm/gateways/anthropic.py +1 -0
  61. mojentic/llm/gateways/anthropic_messages_adapter.py +0 -1
  62. mojentic/llm/gateways/llm_gateway.py +1 -1
  63. mojentic/llm/gateways/ollama.py +23 -18
  64. mojentic/llm/gateways/openai.py +243 -44
  65. mojentic/llm/gateways/openai_message_adapter_spec.py +3 -3
  66. mojentic/llm/gateways/openai_model_registry.py +7 -6
  67. mojentic/llm/gateways/openai_model_registry_spec.py +1 -2
  68. mojentic/llm/gateways/openai_temperature_handling_spec.py +2 -2
  69. mojentic/llm/llm_broker.py +162 -2
  70. mojentic/llm/llm_broker_spec.py +76 -2
  71. mojentic/llm/message_composers.py +6 -3
  72. mojentic/llm/message_composers_spec.py +5 -1
  73. mojentic/llm/registry/__init__.py +0 -3
  74. mojentic/llm/registry/populate_registry_from_ollama.py +2 -2
  75. mojentic/llm/tools/__init__.py +0 -9
  76. mojentic/llm/tools/ask_user_tool.py +11 -5
  77. mojentic/llm/tools/current_datetime.py +9 -6
  78. mojentic/llm/tools/date_resolver.py +10 -4
  79. mojentic/llm/tools/date_resolver_spec.py +0 -1
  80. mojentic/llm/tools/ephemeral_task_manager/append_task_tool.py +4 -1
  81. mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list.py +1 -1
  82. mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool.py +4 -1
  83. mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool.py +5 -2
  84. mojentic/llm/tools/file_manager.py +131 -28
  85. mojentic/llm/tools/file_manager_spec.py +0 -3
  86. mojentic/llm/tools/llm_tool.py +1 -1
  87. mojentic/llm/tools/llm_tool_spec.py +0 -2
  88. mojentic/llm/tools/organic_web_search.py +4 -2
  89. mojentic/llm/tools/tell_user_tool.py +6 -2
  90. mojentic/llm/tools/tool_wrapper.py +2 -2
  91. mojentic/tracer/__init__.py +1 -10
  92. mojentic/tracer/event_store.py +7 -8
  93. mojentic/tracer/event_store_spec.py +1 -2
  94. mojentic/tracer/null_tracer.py +37 -43
  95. mojentic/tracer/tracer_events.py +8 -2
  96. mojentic/tracer/tracer_events_spec.py +6 -7
  97. mojentic/tracer/tracer_system.py +37 -36
  98. mojentic/tracer/tracer_system_spec.py +21 -6
  99. mojentic/utils/__init__.py +1 -1
  100. mojentic/utils/formatting.py +1 -0
  101. {mojentic-0.8.4.dist-info → mojentic-1.0.0.dist-info}/METADATA +76 -27
  102. mojentic-1.0.0.dist-info/RECORD +149 -0
  103. mojentic-0.8.4.dist-info/RECORD +0 -146
  104. {mojentic-0.8.4.dist-info → mojentic-1.0.0.dist-info}/WHEEL +0 -0
  105. {mojentic-0.8.4.dist-info → mojentic-1.0.0.dist-info}/licenses/LICENSE.md +0 -0
  106. {mojentic-0.8.4.dist-info → mojentic-1.0.0.dist-info}/top_level.txt +0 -0
@@ -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, QTimer, pyqtSignal, QObject, QThread
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
- details.append(f"Call Duration: {event.call_duration_ms:.2f} ms" if event.call_duration_ms else "Duration: N/A")
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
 
@@ -51,7 +51,7 @@ memory = SharedWorkingMemory({
51
51
  })
52
52
 
53
53
  llm = LLMBroker("deepseek-r1:70b")
54
- # llm = LLMBroker("llama3.3-instruct-70b-32k")
54
+ # llm = LLMBroker("qwen3:32b-instruct-70b-32k")
55
55
  request_agent = RequestAgent(llm, memory)
56
56
  output_agent = OutputAgent()
57
57
 
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)
@@ -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, BaseLLMAgentWithMemory
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 .correlation_aggregator_agent import BaseAggregatingAgent
11
- from .output_agent import OutputAgent
12
- from .iterative_problem_solver import IterativeProblemSolver
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
- # Agent brokering
16
- from .agent_broker import AgentBroker
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 AgentBroker:
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 broker will return
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(content=f"This is what you remember:\n{json.dumps(self.memory.get_working_memory(), indent=2)}"
156
- f"\n\nRemember anything new you learn by storing it to your working memory in your response."),
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)