mojentic 0.6.2__py3-none-any.whl → 0.7.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.
- _examples/async_dispatcher_example.py +241 -0
- _examples/async_llm_example.py +236 -0
- mojentic/agents/async_aggregator_agent.py +162 -0
- mojentic/agents/async_aggregator_agent_spec.py +227 -0
- mojentic/agents/async_llm_agent.py +197 -0
- mojentic/agents/async_llm_agent_spec.py +166 -0
- mojentic/agents/base_async_agent.py +27 -0
- mojentic/async_dispatcher.py +134 -0
- mojentic/async_dispatcher_spec.py +244 -0
- mojentic/llm/gateways/models.py +3 -3
- mojentic/llm/gateways/ollama.py +4 -4
- mojentic/llm/gateways/openai.py +3 -3
- mojentic/llm/gateways/openai_messages_adapter.py +8 -4
- mojentic/llm/llm_broker.py +4 -4
- {mojentic-0.6.2.dist-info → mojentic-0.7.1.dist-info}/METADATA +2 -1
- {mojentic-0.6.2.dist-info → mojentic-0.7.1.dist-info}/RECORD +19 -10
- {mojentic-0.6.2.dist-info → mojentic-0.7.1.dist-info}/WHEEL +0 -0
- {mojentic-0.6.2.dist-info → mojentic-0.7.1.dist-info}/licenses/LICENSE.md +0 -0
- {mojentic-0.6.2.dist-info → mojentic-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from collections import deque
|
|
4
|
+
from typing import Optional, Type
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
from mojentic.event import TerminateEvent
|
|
10
|
+
|
|
11
|
+
logger = structlog.get_logger()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncDispatcher:
|
|
15
|
+
"""
|
|
16
|
+
AsyncDispatcher class is an asynchronous version of the Dispatcher class.
|
|
17
|
+
It uses asyncio and deque for event processing.
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self, router, shared_working_memory=None, batch_size=5, tracer=None):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the AsyncDispatcher.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
router : Router
|
|
26
|
+
The router to use for routing events to agents
|
|
27
|
+
shared_working_memory : SharedWorkingMemory, optional
|
|
28
|
+
The shared working memory to use
|
|
29
|
+
batch_size : int, optional
|
|
30
|
+
The number of events to process in each batch
|
|
31
|
+
tracer : Tracer, optional
|
|
32
|
+
The tracer to use for tracing events
|
|
33
|
+
"""
|
|
34
|
+
self.router = router
|
|
35
|
+
self.batch_size = batch_size
|
|
36
|
+
self.event_queue = deque()
|
|
37
|
+
self._stop_event = asyncio.Event()
|
|
38
|
+
self._task = None
|
|
39
|
+
|
|
40
|
+
# Use null_tracer if no tracer is provided
|
|
41
|
+
from mojentic.tracer import null_tracer
|
|
42
|
+
self.tracer = tracer or null_tracer
|
|
43
|
+
|
|
44
|
+
async def start(self):
|
|
45
|
+
"""
|
|
46
|
+
Start the event dispatch task.
|
|
47
|
+
"""
|
|
48
|
+
logger.debug("Starting event dispatch task")
|
|
49
|
+
self._task = asyncio.create_task(self._dispatch_events())
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
async def stop(self):
|
|
53
|
+
"""
|
|
54
|
+
Stop the event dispatch task.
|
|
55
|
+
"""
|
|
56
|
+
self._stop_event.set()
|
|
57
|
+
if self._task:
|
|
58
|
+
await self._task
|
|
59
|
+
|
|
60
|
+
async def wait_for_empty_queue(self, timeout=None):
|
|
61
|
+
"""
|
|
62
|
+
Wait for the event queue to be empty.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
timeout : float, optional
|
|
67
|
+
The timeout in seconds
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
bool
|
|
72
|
+
True if the queue is empty, False if the timeout was reached
|
|
73
|
+
"""
|
|
74
|
+
start_time = asyncio.get_event_loop().time()
|
|
75
|
+
while len(self.event_queue) > 0:
|
|
76
|
+
if timeout is not None and asyncio.get_event_loop().time() - start_time > timeout:
|
|
77
|
+
return False
|
|
78
|
+
await asyncio.sleep(0.1)
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def dispatch(self, event):
|
|
82
|
+
"""
|
|
83
|
+
Dispatch an event to the event queue.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
event : Event
|
|
88
|
+
The event to dispatch
|
|
89
|
+
"""
|
|
90
|
+
logger.log(logging.DEBUG, f"Dispatching event: {event}")
|
|
91
|
+
if event.correlation_id is None:
|
|
92
|
+
event.correlation_id = str(uuid4())
|
|
93
|
+
self.event_queue.append(event)
|
|
94
|
+
|
|
95
|
+
async def _dispatch_events(self):
|
|
96
|
+
"""
|
|
97
|
+
Dispatch events from the event queue to agents.
|
|
98
|
+
"""
|
|
99
|
+
while not self._stop_event.is_set():
|
|
100
|
+
for _ in range(self.batch_size):
|
|
101
|
+
logger.debug("Checking for events")
|
|
102
|
+
if len(self.event_queue) > 0:
|
|
103
|
+
logger.debug(f"{len(self.event_queue)} events in queue")
|
|
104
|
+
event = self.event_queue.popleft()
|
|
105
|
+
logger.debug(f"Processing event: {event}")
|
|
106
|
+
agents = self.router.get_agents(event)
|
|
107
|
+
logger.debug(f"Found {len(agents)} agents for event type {type(event)}")
|
|
108
|
+
events = []
|
|
109
|
+
for agent in agents:
|
|
110
|
+
logger.debug(f"Sending event to agent {agent}")
|
|
111
|
+
|
|
112
|
+
# Record agent interaction in tracer system
|
|
113
|
+
self.tracer.record_agent_interaction(
|
|
114
|
+
from_agent=str(event.source),
|
|
115
|
+
to_agent=str(type(agent)),
|
|
116
|
+
event_type=str(type(event).__name__),
|
|
117
|
+
event_id=event.correlation_id,
|
|
118
|
+
source=type(self)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Process the event through the agent
|
|
122
|
+
# If the agent is an async agent, await its receive_event method
|
|
123
|
+
if hasattr(agent, 'receive_event_async'):
|
|
124
|
+
received_events = await agent.receive_event_async(event)
|
|
125
|
+
else:
|
|
126
|
+
received_events = agent.receive_event(event)
|
|
127
|
+
|
|
128
|
+
logger.debug(f"Agent {agent} returned {len(received_events)} events")
|
|
129
|
+
events.extend(received_events)
|
|
130
|
+
for fe in events:
|
|
131
|
+
if type(fe) is TerminateEvent:
|
|
132
|
+
self._stop_event.set()
|
|
133
|
+
self.dispatch(fe)
|
|
134
|
+
await asyncio.sleep(0.1) # Use asyncio.sleep instead of time.sleep
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import pytest
|
|
3
|
+
import pytest_asyncio
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
5
|
+
|
|
6
|
+
from mojentic.async_dispatcher import AsyncDispatcher
|
|
7
|
+
from mojentic.event import Event, TerminateEvent
|
|
8
|
+
from mojentic.agents.base_async_agent import BaseAsyncAgent
|
|
9
|
+
from mojentic.router import Router
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SampleEvent(Event):
|
|
13
|
+
"""A simple event for testing."""
|
|
14
|
+
message: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SampleResponseEvent(Event):
|
|
18
|
+
"""A response event for testing."""
|
|
19
|
+
response: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsyncTestAgent(BaseAsyncAgent):
|
|
23
|
+
"""A test async agent that returns a SampleResponseEvent."""
|
|
24
|
+
|
|
25
|
+
async def receive_event_async(self, event):
|
|
26
|
+
if isinstance(event, SampleEvent):
|
|
27
|
+
return [SampleResponseEvent(
|
|
28
|
+
source=type(self),
|
|
29
|
+
correlation_id=event.correlation_id,
|
|
30
|
+
response=f"Processed: {event.message}"
|
|
31
|
+
)]
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SyncTestAgent:
|
|
36
|
+
"""A test sync agent that returns a SampleResponseEvent."""
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
# Create a mock for tracking calls
|
|
40
|
+
self._mock = MagicMock()
|
|
41
|
+
|
|
42
|
+
def receive_event(self, event):
|
|
43
|
+
# Track the call
|
|
44
|
+
self._mock(event)
|
|
45
|
+
|
|
46
|
+
if isinstance(event, SampleEvent):
|
|
47
|
+
return [SampleResponseEvent(
|
|
48
|
+
source=type(self),
|
|
49
|
+
correlation_id=event.correlation_id,
|
|
50
|
+
response=f"Processed sync: {event.message}"
|
|
51
|
+
)]
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
def assert_called_once(self):
|
|
55
|
+
# Delegate to the mock
|
|
56
|
+
self._mock.assert_called_once()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def router():
|
|
61
|
+
"""Create a router for testing."""
|
|
62
|
+
return Router()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def async_agent():
|
|
67
|
+
"""Create an async agent for testing."""
|
|
68
|
+
return AsyncTestAgent()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
def sync_agent():
|
|
73
|
+
"""Create a sync agent for testing."""
|
|
74
|
+
return SyncTestAgent()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest_asyncio.fixture
|
|
78
|
+
async def dispatcher(router):
|
|
79
|
+
"""Create and start an AsyncDispatcher for testing."""
|
|
80
|
+
dispatcher = AsyncDispatcher(router)
|
|
81
|
+
await dispatcher.start()
|
|
82
|
+
yield dispatcher
|
|
83
|
+
await dispatcher.stop()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.asyncio
|
|
87
|
+
async def test_async_dispatcher_init(router):
|
|
88
|
+
"""Test that the AsyncDispatcher initializes correctly."""
|
|
89
|
+
dispatcher = AsyncDispatcher(router)
|
|
90
|
+
|
|
91
|
+
assert dispatcher.router == router
|
|
92
|
+
assert dispatcher.batch_size == 5
|
|
93
|
+
assert len(dispatcher.event_queue) == 0
|
|
94
|
+
assert dispatcher._task is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
async def test_async_dispatcher_start_stop(router):
|
|
99
|
+
"""Test that the AsyncDispatcher starts and stops correctly."""
|
|
100
|
+
dispatcher = AsyncDispatcher(router)
|
|
101
|
+
|
|
102
|
+
# Start the dispatcher
|
|
103
|
+
await dispatcher.start()
|
|
104
|
+
assert dispatcher._task is not None
|
|
105
|
+
assert not dispatcher._stop_event.is_set()
|
|
106
|
+
|
|
107
|
+
# Stop the dispatcher
|
|
108
|
+
await dispatcher.stop()
|
|
109
|
+
assert dispatcher._stop_event.is_set()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.asyncio
|
|
113
|
+
async def test_async_dispatcher_dispatch(dispatcher, router, async_agent):
|
|
114
|
+
"""Test that the AsyncDispatcher dispatches events correctly."""
|
|
115
|
+
# Register the agent
|
|
116
|
+
router.add_route(SampleEvent, async_agent)
|
|
117
|
+
|
|
118
|
+
# Create and dispatch an event
|
|
119
|
+
event = SampleEvent(source=str, message="Hello")
|
|
120
|
+
dispatcher.dispatch(event)
|
|
121
|
+
|
|
122
|
+
# Wait for the event to be processed
|
|
123
|
+
await dispatcher.wait_for_empty_queue(timeout=1)
|
|
124
|
+
|
|
125
|
+
# The event should have been processed and removed from the queue
|
|
126
|
+
assert len(dispatcher.event_queue) == 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_async_dispatcher_with_async_agent(dispatcher, router, async_agent):
|
|
131
|
+
"""Test that the AsyncDispatcher works with async agents."""
|
|
132
|
+
# Register the agent
|
|
133
|
+
router.add_route(SampleEvent, async_agent)
|
|
134
|
+
|
|
135
|
+
# Create and dispatch an event
|
|
136
|
+
event = SampleEvent(source=str, message="Hello")
|
|
137
|
+
dispatcher.dispatch(event)
|
|
138
|
+
|
|
139
|
+
# Wait for the event to be processed
|
|
140
|
+
await dispatcher.wait_for_empty_queue(timeout=1)
|
|
141
|
+
|
|
142
|
+
# The event should have been processed and removed from the queue
|
|
143
|
+
assert len(dispatcher.event_queue) == 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.asyncio
|
|
147
|
+
async def test_async_dispatcher_with_sync_agent(dispatcher, router, sync_agent):
|
|
148
|
+
"""Test that the AsyncDispatcher works with sync agents."""
|
|
149
|
+
# Register the agent
|
|
150
|
+
router.add_route(SampleEvent, sync_agent)
|
|
151
|
+
|
|
152
|
+
# Create and dispatch an event
|
|
153
|
+
event = SampleEvent(source=str, message="Hello")
|
|
154
|
+
dispatcher.dispatch(event)
|
|
155
|
+
|
|
156
|
+
# Wait for the event to be processed
|
|
157
|
+
await dispatcher.wait_for_empty_queue(timeout=1)
|
|
158
|
+
|
|
159
|
+
# The event should have been processed and removed from the queue
|
|
160
|
+
assert len(dispatcher.event_queue) == 0
|
|
161
|
+
|
|
162
|
+
# Verify that the sync agent's receive_event method was called
|
|
163
|
+
sync_agent.assert_called_once()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@pytest.mark.asyncio
|
|
167
|
+
async def test_async_dispatcher_terminate_event(dispatcher, router):
|
|
168
|
+
"""Test that the AsyncDispatcher handles TerminateEvent correctly."""
|
|
169
|
+
# Create a mock agent that returns a TerminateEvent
|
|
170
|
+
class TerminateAgent(BaseAsyncAgent):
|
|
171
|
+
async def receive_event_async(self, event):
|
|
172
|
+
return [TerminateEvent(source=type(self))]
|
|
173
|
+
|
|
174
|
+
terminate_agent = TerminateAgent()
|
|
175
|
+
|
|
176
|
+
# Register the agent
|
|
177
|
+
router.add_route(SampleEvent, terminate_agent)
|
|
178
|
+
|
|
179
|
+
# Create and dispatch an event
|
|
180
|
+
event = SampleEvent(source=str, message="Hello")
|
|
181
|
+
dispatcher.dispatch(event)
|
|
182
|
+
|
|
183
|
+
# Wait a moment for the event to be processed
|
|
184
|
+
await asyncio.sleep(0.5)
|
|
185
|
+
|
|
186
|
+
# The dispatcher should have stopped
|
|
187
|
+
assert dispatcher._stop_event.is_set()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@pytest.mark.asyncio
|
|
191
|
+
async def test_async_dispatcher_wait_for_empty_queue(dispatcher):
|
|
192
|
+
"""Test that wait_for_empty_queue works correctly."""
|
|
193
|
+
# Queue is initially empty
|
|
194
|
+
assert await dispatcher.wait_for_empty_queue() is True
|
|
195
|
+
|
|
196
|
+
# Add multiple events to the queue to test batch processing
|
|
197
|
+
for i in range(10):
|
|
198
|
+
event = SampleEvent(source=str, message=f"Event {i}")
|
|
199
|
+
dispatcher.dispatch(event)
|
|
200
|
+
|
|
201
|
+
# Queue is not empty
|
|
202
|
+
assert len(dispatcher.event_queue) > 0
|
|
203
|
+
|
|
204
|
+
# Wait for the queue to be empty with a sufficient timeout
|
|
205
|
+
# The dispatcher should process all events
|
|
206
|
+
assert await dispatcher.wait_for_empty_queue(timeout=1) is True
|
|
207
|
+
|
|
208
|
+
# Verify the queue is empty
|
|
209
|
+
assert len(dispatcher.event_queue) == 0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@pytest.mark.asyncio
|
|
213
|
+
async def test_async_dispatcher_batch_processing(dispatcher, router, async_agent):
|
|
214
|
+
"""Test that the AsyncDispatcher processes events in batches."""
|
|
215
|
+
# Register the agent
|
|
216
|
+
router.add_route(SampleEvent, async_agent)
|
|
217
|
+
|
|
218
|
+
# Set a small batch size
|
|
219
|
+
dispatcher.batch_size = 2
|
|
220
|
+
|
|
221
|
+
# Create and dispatch multiple events
|
|
222
|
+
for i in range(5):
|
|
223
|
+
event = SampleEvent(source=str, message=f"Event {i}")
|
|
224
|
+
dispatcher.dispatch(event)
|
|
225
|
+
|
|
226
|
+
# Wait for all events to be processed
|
|
227
|
+
await dispatcher.wait_for_empty_queue(timeout=2)
|
|
228
|
+
|
|
229
|
+
# All events should have been processed
|
|
230
|
+
assert len(dispatcher.event_queue) == 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@pytest.mark.asyncio
|
|
234
|
+
async def test_async_dispatcher_correlation_id(dispatcher):
|
|
235
|
+
"""Test that the AsyncDispatcher assigns correlation_id if not provided."""
|
|
236
|
+
# Create an event without a correlation_id
|
|
237
|
+
event = SampleEvent(source=str, message="Hello")
|
|
238
|
+
assert event.correlation_id is None
|
|
239
|
+
|
|
240
|
+
# Dispatch the event
|
|
241
|
+
dispatcher.dispatch(event)
|
|
242
|
+
|
|
243
|
+
# The event should now have a correlation_id
|
|
244
|
+
assert event.correlation_id is not None
|
mojentic/llm/gateways/models.py
CHANGED
|
@@ -46,7 +46,7 @@ class LLMToolCall(BaseModel):
|
|
|
46
46
|
"""
|
|
47
47
|
A tool call to be made available to the LLM.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Attributes
|
|
50
50
|
----------
|
|
51
51
|
id : Optional[str]
|
|
52
52
|
The identifier of the tool call.
|
|
@@ -64,7 +64,7 @@ class LLMMessage(BaseModel):
|
|
|
64
64
|
"""
|
|
65
65
|
A message to be sent to the LLM. These would accumulate during a chat session with an LLM.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Attributes
|
|
68
68
|
----------
|
|
69
69
|
role : MessageRole
|
|
70
70
|
The role of the message in the conversation.
|
|
@@ -89,7 +89,7 @@ class LLMGatewayResponse(BaseModel):
|
|
|
89
89
|
"""
|
|
90
90
|
The response from the LLM gateway, abstracting you from the quirks of a specific LLM.
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
Attributes
|
|
93
93
|
----------
|
|
94
94
|
content : Optional[Union[str, dict[str, str]]]
|
|
95
95
|
The content of the response.
|
mojentic/llm/gateways/ollama.py
CHANGED
|
@@ -41,8 +41,8 @@ class OllamaGateway(LLMGateway):
|
|
|
41
41
|
"""
|
|
42
42
|
Complete the LLM request by delegating to the Ollama service.
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
Keyword Arguments
|
|
45
|
+
----------------
|
|
46
46
|
model : str
|
|
47
47
|
The name of the model to use, as appears in `ollama list`.
|
|
48
48
|
messages : List[LLMMessage]
|
|
@@ -107,8 +107,8 @@ class OllamaGateway(LLMGateway):
|
|
|
107
107
|
"""
|
|
108
108
|
Stream the LLM response from Ollama service.
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
Keyword Arguments
|
|
111
|
+
----------------
|
|
112
112
|
model : str
|
|
113
113
|
The name of the model to use, as appears in `ollama list`.
|
|
114
114
|
messages : List[LLMMessage]
|
mojentic/llm/gateways/openai.py
CHANGED
|
@@ -31,8 +31,8 @@ class OpenAIGateway(LLMGateway):
|
|
|
31
31
|
"""
|
|
32
32
|
Complete the LLM request by delegating to the OpenAI service.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
Keyword Arguments
|
|
35
|
+
----------------
|
|
36
36
|
model : str
|
|
37
37
|
The name of the model to use, as appears in `ollama list`.
|
|
38
38
|
messages : List[LLMMessage]
|
|
@@ -148,4 +148,4 @@ class OpenAIGateway(LLMGateway):
|
|
|
148
148
|
tokenizer = TokenizerGateway()
|
|
149
149
|
tokens = tokenizer.encode(text)
|
|
150
150
|
chunks_iterator = self._batched(tokens, chunk_length)
|
|
151
|
-
yield from chunks_iterator
|
|
151
|
+
yield from chunks_iterator
|
|
@@ -48,13 +48,17 @@ def get_image_type(file_path: str) -> str:
|
|
|
48
48
|
file_path: Path to the image file
|
|
49
49
|
|
|
50
50
|
Returns:
|
|
51
|
-
Image type (e.g., '
|
|
51
|
+
Image type (e.g., 'jpeg', 'png')
|
|
52
52
|
"""
|
|
53
53
|
_, ext = os.path.splitext(file_path)
|
|
54
54
|
image_type = ext.lstrip('.').lower()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
|
|
56
|
+
# Convert 'jpg' to 'jpeg'
|
|
57
|
+
if image_type == 'jpg':
|
|
58
|
+
return 'jpeg'
|
|
59
|
+
|
|
60
|
+
# Use 'jpeg' for unknown extensions, otherwise use the detected type
|
|
61
|
+
return image_type if image_type in ['jpeg', 'png', 'gif', 'webp'] else 'jpeg'
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
def adapt_messages_to_openai(messages: List[LLMMessage]):
|
mojentic/llm/llm_broker.py
CHANGED
|
@@ -88,7 +88,7 @@ class LLMBroker():
|
|
|
88
88
|
logger.info(f"Requesting llm response with approx {approximate_tokens} tokens")
|
|
89
89
|
|
|
90
90
|
# Convert messages to serializable dict for audit
|
|
91
|
-
messages_for_tracer = [m.
|
|
91
|
+
messages_for_tracer = [m.model_dump() for m in messages]
|
|
92
92
|
|
|
93
93
|
# Record LLM call in tracer
|
|
94
94
|
tools_for_tracer = [{"name": t.name, "description": t.description} for t in tools] if tools else None
|
|
@@ -115,7 +115,7 @@ class LLMBroker():
|
|
|
115
115
|
call_duration_ms = (time.time() - start_time) * 1000
|
|
116
116
|
|
|
117
117
|
# Record LLM response in tracer
|
|
118
|
-
tool_calls_for_tracer = [tc.
|
|
118
|
+
tool_calls_for_tracer = [tc.model_dump() for tc in result.tool_calls] if result.tool_calls else None
|
|
119
119
|
self.tracer.record_llm_response(
|
|
120
120
|
self.model,
|
|
121
121
|
result.content,
|
|
@@ -199,7 +199,7 @@ class LLMBroker():
|
|
|
199
199
|
logger.info(f"Requesting llm response with approx {approximate_tokens} tokens")
|
|
200
200
|
|
|
201
201
|
# Convert messages to serializable dict for audit
|
|
202
|
-
messages_for_tracer = [m.
|
|
202
|
+
messages_for_tracer = [m.model_dump() for m in messages]
|
|
203
203
|
|
|
204
204
|
# Record LLM call in tracer
|
|
205
205
|
self.tracer.record_llm_call(
|
|
@@ -221,7 +221,7 @@ class LLMBroker():
|
|
|
221
221
|
|
|
222
222
|
# Record LLM response in tracer with object representation
|
|
223
223
|
# Convert object to string for tracer
|
|
224
|
-
object_str = str(result.object.
|
|
224
|
+
object_str = str(result.object.model_dump()) if hasattr(result.object, "model_dump") else str(result.object)
|
|
225
225
|
self.tracer.record_llm_response(
|
|
226
226
|
self.model,
|
|
227
227
|
f"Structured response: {object_str}",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mojentic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Mojentic is an agentic framework that aims to provide a simple and flexible way to assemble teams of agents to solve complex problems.
|
|
5
5
|
Author-email: Stacey Vetzal <stacey@vetzal.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/svetzal/mojentic
|
|
@@ -24,6 +24,7 @@ Requires-Dist: serpapi
|
|
|
24
24
|
Requires-Dist: colorama
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
27
28
|
Requires-Dist: pytest-spec; extra == "dev"
|
|
28
29
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
29
30
|
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
_examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
_examples/async_dispatcher_example.py,sha256=rlaShGpXUsTN7RwVxOo8sbxQjYYJ13gKRB9PWQ3sVTE,8454
|
|
3
|
+
_examples/async_llm_example.py,sha256=IgYBMawHhaKWbA6zkurH-raqBez9nODfjHEAHI7PRek,8708
|
|
2
4
|
_examples/broker_as_tool.py,sha256=axLrX9lzQAAxSTCmm1njvOk63EWfVRG3NU6pLiMyYH8,2962
|
|
3
5
|
_examples/broker_examples.py,sha256=7tES2KyKc3uigBuxkwOTf5UGhp7nVeVqLhd8fEHSWEc,1901
|
|
4
6
|
_examples/broker_image_examples.py,sha256=8dpZ2RFmRYhJqoeWklzlPxmvj9m4M393452SO_D2bJY,1133
|
|
@@ -42,13 +44,20 @@ _examples/react/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
42
44
|
_examples/react/models/base.py,sha256=F3hR023XQofs3Op37sruuONxAU7eRjGnLnf4515BeYU,1239
|
|
43
45
|
_examples/react/models/events.py,sha256=0lRY0D5VrSkIGEKnSEQQBRyxxMQl71yxPkdMBuVBKHw,1062
|
|
44
46
|
mojentic/__init__.py,sha256=Jrllv5OKZKjkfVNLRJV6yT3NZA8ZELSrqj3798H4cDs,1172
|
|
47
|
+
mojentic/async_dispatcher.py,sha256=uDw4thjK0O7OAuV-v35s6k7ZIm6BLcZ8qhRZU_UKdn0,4735
|
|
48
|
+
mojentic/async_dispatcher_spec.py,sha256=wBlEzdw6wvMf5g982824nFxzP6EGtMM5yO-sd-fYx2w,7279
|
|
45
49
|
mojentic/dispatcher.py,sha256=Jx73v1799pYqzdLBDnq1tbPzXVEs3xArwtMhZvByyH8,2760
|
|
46
50
|
mojentic/event.py,sha256=P0kgI0Si41ifCTLNtpfkks0OktlK4wuAj3oMCDtdUyo,330
|
|
47
51
|
mojentic/router.py,sha256=7z2xqgpsuHmA32HfH3rAgDk3zLhUBxz8DHPt2vDlVOU,377
|
|
48
52
|
mojentic/router_spec.py,sha256=CRYeKEa7dz_yN8ABAi5DgY5C0mJW7she3Kw0NzxoHx4,876
|
|
49
53
|
mojentic/agents/__init__.py,sha256=HQe0ygpRMamz9x7dCcri_aAMwr8XDTCMls2g2g0LBL0,345
|
|
50
54
|
mojentic/agents/agent_broker.py,sha256=ulLuCzqDtPPxLSB8dre8Js1fwR3Uls7gGtJrmhcelwo,927
|
|
55
|
+
mojentic/agents/async_aggregator_agent.py,sha256=mv4iFND6krvZQjnBmR9Z9Quu6a_b1zl_opSquKIjqFk,5355
|
|
56
|
+
mojentic/agents/async_aggregator_agent_spec.py,sha256=TsetYelNy2biD_mYN3qqKbF7Zg6-9Xi_5pYXGAM4WAU,8340
|
|
57
|
+
mojentic/agents/async_llm_agent.py,sha256=KSTpQ40zVpbPYwwg73la3WY1LKBeBL2HsfJoUCp_rTI,6464
|
|
58
|
+
mojentic/agents/async_llm_agent_spec.py,sha256=myNKJlgK0mqHzBWQfeB_TpcT0Mwv9CvyqNcqurvVGIc,5371
|
|
51
59
|
mojentic/agents/base_agent.py,sha256=m1SRyzY9ou4tesFDszonC4Y8moQMg9eofvJeuhoXmM0,722
|
|
60
|
+
mojentic/agents/base_async_agent.py,sha256=oUYDhMOqTyDmlmCt1q3S0gLrTK-w-FnlCfTOpYZ7ln8,888
|
|
52
61
|
mojentic/agents/base_llm_agent.py,sha256=26pe0nS7UD1x1-NZjGMQMguxxQBjcRDyXMzcSmJOd2c,3094
|
|
53
62
|
mojentic/agents/base_llm_agent_spec.py,sha256=1kzayCtAZY7oWloaFMh7-NkTtiKihDCLD3i9HwH3_Uc,2571
|
|
54
63
|
mojentic/agents/correlation_aggregator_agent.py,sha256=okdq92rMNwIXTgnuwwn3zp4JzSxTh05vGWzTGocOgbU,1134
|
|
@@ -60,7 +69,7 @@ mojentic/context/shared_working_memory.py,sha256=Zt9MNGErEkDIUAaHvyhEOiTaEobI9l0
|
|
|
60
69
|
mojentic/llm/__init__.py,sha256=mwdPpTRofw7_KSlW6ZQmcM-GSpyISWyI2bxphKpV7A0,180
|
|
61
70
|
mojentic/llm/chat_session.py,sha256=H2gY0mZYVym8jC69VHsmKaRZ9T87Suyw0-TW5r850nA,3992
|
|
62
71
|
mojentic/llm/chat_session_spec.py,sha256=8-jj-EHV2WwWuvo3t8I75kSEAYiG1nR-OEwkkLTi_z0,3872
|
|
63
|
-
mojentic/llm/llm_broker.py,sha256=
|
|
72
|
+
mojentic/llm/llm_broker.py,sha256=i9jhSulXV4lQX9G6YOQTiMZt4UMipqUfvl-P-gmXQXo,9200
|
|
64
73
|
mojentic/llm/llm_broker_spec.py,sha256=40lzmYm_6Zje6z5MQ7_o3gSBThLsNW_l_1mZTUVll6A,5342
|
|
65
74
|
mojentic/llm/message_composers.py,sha256=Fo9o7UGZOOIYoGI_DyOfP_oMiEiCMQz-zdWdTKtozVk,12108
|
|
66
75
|
mojentic/llm/message_composers_spec.py,sha256=RNW14Zb-kIBWT5Wy9cZQyxHPrcRIaBFjBYCR9N-m0kE,12109
|
|
@@ -70,13 +79,13 @@ mojentic/llm/gateways/anthropic_messages_adapter.py,sha256=K6kEZeVt7E1endbGMLsh5
|
|
|
70
79
|
mojentic/llm/gateways/embeddings_gateway.py,sha256=kcOhiyHzOyQgKgwPDQJD5oVvfwk71GsBgMYJOIDv5NU,1347
|
|
71
80
|
mojentic/llm/gateways/file_gateway.py,sha256=3bZpalSyl_R4016WzCmmjUBDtAgPsmx19eVGv6p1Ufk,1418
|
|
72
81
|
mojentic/llm/gateways/llm_gateway.py,sha256=5BayP6VuMgMHdAzCFaXLRjRCWh-IOYnaq_s4LZ0_3x4,2559
|
|
73
|
-
mojentic/llm/gateways/models.py,sha256=
|
|
74
|
-
mojentic/llm/gateways/ollama.py,sha256=
|
|
82
|
+
mojentic/llm/gateways/models.py,sha256=OyIaMHKrrx6dHo5FbC8qOFct7PRql9wqbe_BJlgDSDE,3015
|
|
83
|
+
mojentic/llm/gateways/ollama.py,sha256=fBnmG4B0Trq8BJa8eZgrYUEVgb3kiTPytW1e6aTIjj8,7607
|
|
75
84
|
mojentic/llm/gateways/ollama_messages_adapter.py,sha256=kUN_p2FyN88_trXMcL-Xsn9xPBU7pGKlJwTUEUCf6G4,1404
|
|
76
85
|
mojentic/llm/gateways/ollama_messages_adapter_spec.py,sha256=gVRbWDrHOa1EiZ0CkEWe0pGn-GKRqdGb-x56HBQeYSE,4981
|
|
77
|
-
mojentic/llm/gateways/openai.py,sha256=
|
|
86
|
+
mojentic/llm/gateways/openai.py,sha256=P40KKfNCmY_EKDP6VUKu7sM2GaqX_iUPx6_2_BYb80Q,5489
|
|
78
87
|
mojentic/llm/gateways/openai_message_adapter_spec.py,sha256=ITBSV5njldV_x0NPgjmg8Okf9KzevQJ8dTXM-t6ubcg,6612
|
|
79
|
-
mojentic/llm/gateways/openai_messages_adapter.py,sha256=
|
|
88
|
+
mojentic/llm/gateways/openai_messages_adapter.py,sha256=Scal68JKKdBHB35ok1c5DeWYdD6Wra5oXSsPxJyyXSQ,3947
|
|
80
89
|
mojentic/llm/gateways/tokenizer_gateway.py,sha256=ztuqfunlJ6xmyUPPHcC_69-kegiNJD6jdSEde7hDh2w,485
|
|
81
90
|
mojentic/llm/registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
82
91
|
mojentic/llm/registry/llm_registry.py,sha256=beyrgGrkXx5ZckUJzC1nQ461vra0fF6s_qRaEdi5bsg,2508
|
|
@@ -121,8 +130,8 @@ mojentic/tracer/tracer_system.py,sha256=7CPy_2tlsHtXQ4DcO5oo52N9a9WS0GH-mjeINzu6
|
|
|
121
130
|
mojentic/tracer/tracer_system_spec.py,sha256=TNm0f9LV__coBx0JGEKyzzNN9mFjCSG_SSrRISO8Xeg,8632
|
|
122
131
|
mojentic/utils/__init__.py,sha256=lqECkkoFvHFttDnafRE1vvh0Dmna_lwupMToP5VvX5k,115
|
|
123
132
|
mojentic/utils/formatting.py,sha256=bPrwwdluXdQ8TsFxfWtHNOeMWKNvAfABSoUnnA1g7c8,947
|
|
124
|
-
mojentic-0.
|
|
125
|
-
mojentic-0.
|
|
126
|
-
mojentic-0.
|
|
127
|
-
mojentic-0.
|
|
128
|
-
mojentic-0.
|
|
133
|
+
mojentic-0.7.1.dist-info/licenses/LICENSE.md,sha256=txSgV8n5zY1W3NiF5HHsCwlaW0e8We1cSC6TuJUqxXA,1060
|
|
134
|
+
mojentic-0.7.1.dist-info/METADATA,sha256=tH9I2vnk7ypAmVkNzkZhdn3i_KuWQWQkEIKhtJZdQkM,5429
|
|
135
|
+
mojentic-0.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
136
|
+
mojentic-0.7.1.dist-info/top_level.txt,sha256=Q-BvPQ8Eu1jnEqK8Xkr6A9C8Xa1z38oPZRHuA5MCTqg,19
|
|
137
|
+
mojentic-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|