mojentic 0.5.7__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _examples/recursive_agent.py +2 -1
- _examples/tracer_demo.py +109 -0
- mojentic/dispatcher.py +17 -1
- mojentic/llm/gateways/openai_message_adapter_spec.py +7 -7
- mojentic/llm/gateways/openai_messages_adapter.py +58 -15
- mojentic/llm/llm_broker.py +85 -2
- mojentic/llm/message_composers_spec.py +43 -25
- mojentic/llm/tools/llm_tool.py +26 -0
- mojentic/llm/tools/llm_tool_spec.py +2 -1
- mojentic/tracer/__init__.py +16 -0
- mojentic/tracer/event_store.py +111 -0
- mojentic/tracer/event_store_spec.py +210 -0
- mojentic/tracer/null_tracer.py +191 -0
- mojentic/tracer/tracer_events.py +136 -0
- mojentic/tracer/tracer_events_spec.py +116 -0
- mojentic/tracer/tracer_system.py +285 -0
- mojentic/tracer/tracer_system_spec.py +255 -0
- {mojentic-0.5.7.dist-info → mojentic-0.6.0.dist-info}/METADATA +20 -10
- {mojentic-0.5.7.dist-info → mojentic-0.6.0.dist-info}/RECORD +22 -15
- {mojentic-0.5.7.dist-info → mojentic-0.6.0.dist-info}/WHEEL +1 -1
- mojentic/audit/event_store.py +0 -6
- mojentic/audit/event_store_spec.py +0 -26
- {mojentic-0.5.7.dist-info → mojentic-0.6.0.dist-info}/licenses/LICENSE.md +0 -0
- {mojentic-0.5.7.dist-info → mojentic-0.6.0.dist-info}/top_level.txt +0 -0
_examples/recursive_agent.py
CHANGED
|
@@ -21,7 +21,8 @@ async def demonstrate_async():
|
|
|
21
21
|
3. Running multiple problem-solving tasks concurrently
|
|
22
22
|
"""
|
|
23
23
|
# Initialize the LLM broker with your preferred model
|
|
24
|
-
llm = LLMBroker(model="llama3.3-70b-32k")
|
|
24
|
+
# llm = LLMBroker(model="llama3.3-70b-32k")
|
|
25
|
+
llm = LLMBroker(model="qwen3:30b-a3b-q4_K_M")
|
|
25
26
|
|
|
26
27
|
# Create the agent with a maximum of 3 iterations
|
|
27
28
|
agent = SimpleRecursiveAgent(llm=llm, max_iterations=3)
|
_examples/tracer_demo.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example script demonstrating the tracer system with ChatSession and tools.
|
|
3
|
+
|
|
4
|
+
This example shows how to use the tracer system to monitor an interactive
|
|
5
|
+
chat session with LLMBroker and tools. When the user exits the session,
|
|
6
|
+
the script displays a summary of all traced events.
|
|
7
|
+
"""
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from mojentic.tracer import TracerSystem
|
|
11
|
+
from mojentic.tracer.tracer_events import LLMCallTracerEvent, LLMResponseTracerEvent, ToolCallTracerEvent
|
|
12
|
+
from mojentic.llm import ChatSession, LLMBroker
|
|
13
|
+
from mojentic.llm.tools.date_resolver import ResolveDateTool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def print_tracer_events(events):
|
|
17
|
+
"""Print tracer events using their printable_summary method."""
|
|
18
|
+
print(f"\n{'-'*80}")
|
|
19
|
+
print("Tracer Events:")
|
|
20
|
+
print(f"{'-'*80}")
|
|
21
|
+
|
|
22
|
+
for i, event in enumerate(events, 1):
|
|
23
|
+
print(f"{i}. {event.printable_summary()}")
|
|
24
|
+
print()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
"""Run a chat session with tracer system to monitor interactions."""
|
|
29
|
+
# Create a tracer system to monitor all interactions
|
|
30
|
+
tracer = TracerSystem()
|
|
31
|
+
|
|
32
|
+
# Create an LLM broker with the tracer
|
|
33
|
+
llm_broker = LLMBroker(model="llama3.3-70b-32k", tracer=tracer)
|
|
34
|
+
|
|
35
|
+
# Create a date resolver tool that will also use the tracer
|
|
36
|
+
date_tool = ResolveDateTool(llm_broker=llm_broker, tracer=tracer)
|
|
37
|
+
|
|
38
|
+
# Create a chat session with the broker and tool
|
|
39
|
+
chat_session = ChatSession(llm_broker, tools=[date_tool])
|
|
40
|
+
|
|
41
|
+
print("Welcome to the chat session with tracer demonstration!")
|
|
42
|
+
print("Ask questions about dates (e.g., 'What day is next Friday?') or anything else.")
|
|
43
|
+
print("Behind the scenes, the tracer system is recording all interactions.")
|
|
44
|
+
print("Press Enter with no input to exit and see the trace summary.")
|
|
45
|
+
print("-" * 80)
|
|
46
|
+
|
|
47
|
+
# Interactive chat session
|
|
48
|
+
while True:
|
|
49
|
+
query = input("You: ")
|
|
50
|
+
if not query:
|
|
51
|
+
print("Exiting chat session...")
|
|
52
|
+
break
|
|
53
|
+
else:
|
|
54
|
+
print("Assistant: ", end="")
|
|
55
|
+
response = chat_session.send(query)
|
|
56
|
+
print(response)
|
|
57
|
+
|
|
58
|
+
# After the user exits, display tracer event summary
|
|
59
|
+
print("\nTracer System Summary")
|
|
60
|
+
print("=" * 80)
|
|
61
|
+
print(f"You just had a conversation with an LLM, and the tracer recorded everything!")
|
|
62
|
+
|
|
63
|
+
# Get all events
|
|
64
|
+
all_events = tracer.get_events()
|
|
65
|
+
print(f"Total events recorded: {len(all_events)}")
|
|
66
|
+
print_tracer_events(all_events)
|
|
67
|
+
|
|
68
|
+
# Show how to filter events by type
|
|
69
|
+
print("\nYou can filter events by type:")
|
|
70
|
+
|
|
71
|
+
llm_calls = tracer.get_events(event_type=LLMCallTracerEvent)
|
|
72
|
+
print(f"LLM Call Events: {len(llm_calls)}")
|
|
73
|
+
if llm_calls:
|
|
74
|
+
print(f"Example: {llm_calls[0].printable_summary()}")
|
|
75
|
+
|
|
76
|
+
llm_responses = tracer.get_events(event_type=LLMResponseTracerEvent)
|
|
77
|
+
print(f"LLM Response Events: {len(llm_responses)}")
|
|
78
|
+
if llm_responses:
|
|
79
|
+
print(f"Example: {llm_responses[0].printable_summary()}")
|
|
80
|
+
|
|
81
|
+
tool_calls = tracer.get_events(event_type=ToolCallTracerEvent)
|
|
82
|
+
print(f"Tool Call Events: {len(tool_calls)}")
|
|
83
|
+
if tool_calls:
|
|
84
|
+
print(f"Example: {tool_calls[0].printable_summary()}")
|
|
85
|
+
|
|
86
|
+
# Show the last few events
|
|
87
|
+
print("\nThe last few events:")
|
|
88
|
+
last_events = tracer.get_last_n_tracer_events(3)
|
|
89
|
+
print_tracer_events(last_events)
|
|
90
|
+
|
|
91
|
+
# Show how to use time-based filtering
|
|
92
|
+
print("\nYou can also filter events by time range:")
|
|
93
|
+
print("Example: tracer.get_events(start_time=start_timestamp, end_time=end_timestamp)")
|
|
94
|
+
|
|
95
|
+
# Show how to extract specific information from events
|
|
96
|
+
if tool_calls:
|
|
97
|
+
print("\nDetailed analysis example - Tool usage stats:")
|
|
98
|
+
tool_names = {}
|
|
99
|
+
for event in tool_calls:
|
|
100
|
+
tool_name = event.tool_name
|
|
101
|
+
tool_names[tool_name] = tool_names.get(tool_name, 0) + 1
|
|
102
|
+
|
|
103
|
+
print("Tool usage frequency:")
|
|
104
|
+
for tool_name, count in tool_names.items():
|
|
105
|
+
print(f" - {tool_name}: {count} calls")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
mojentic/dispatcher.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import threading
|
|
3
3
|
from time import sleep
|
|
4
|
+
from typing import Optional, Type
|
|
4
5
|
from uuid import uuid4
|
|
5
6
|
|
|
6
7
|
import structlog
|
|
@@ -11,12 +12,16 @@ logger = structlog.get_logger()
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class Dispatcher:
|
|
14
|
-
def __init__(self, router, shared_working_memory=None, batch_size=5):
|
|
15
|
+
def __init__(self, router, shared_working_memory=None, batch_size=5, tracer=None):
|
|
15
16
|
self.router = router
|
|
16
17
|
self.batch_size = batch_size
|
|
17
18
|
self.event_queue = []
|
|
18
19
|
self._stop_event = threading.Event()
|
|
19
20
|
self._thread = threading.Thread(target=self._dispatch_events)
|
|
21
|
+
|
|
22
|
+
# Use null_tracer if no tracer is provided
|
|
23
|
+
from mojentic.tracer import null_tracer
|
|
24
|
+
self.tracer = tracer or null_tracer
|
|
20
25
|
|
|
21
26
|
logger.debug("Starting event dispatch thread")
|
|
22
27
|
self._thread.start()
|
|
@@ -44,6 +49,17 @@ class Dispatcher:
|
|
|
44
49
|
events = []
|
|
45
50
|
for agent in agents:
|
|
46
51
|
logger.debug(f"Sending event to agent {agent}")
|
|
52
|
+
|
|
53
|
+
# Record agent interaction in tracer system
|
|
54
|
+
self.tracer.record_agent_interaction(
|
|
55
|
+
from_agent=str(event.source),
|
|
56
|
+
to_agent=str(type(agent)),
|
|
57
|
+
event_type=str(type(event).__name__),
|
|
58
|
+
event_id=event.correlation_id,
|
|
59
|
+
source=type(self)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Process the event through the agent
|
|
47
63
|
received_events = agent.receive_event(event)
|
|
48
64
|
logger.debug(f"Agent {agent} returned {len(events)} events")
|
|
49
65
|
events.extend(received_events)
|
|
@@ -92,13 +92,13 @@ class DescribeOpenAIMessagesAdapter:
|
|
|
92
92
|
When adapting to OpenAI format
|
|
93
93
|
Then it should convert to the correct format with structured content array
|
|
94
94
|
"""
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
# Patch our own methods that encapsulate external library calls
|
|
96
|
+
mocker.patch('mojentic.llm.gateways.openai_messages_adapter.read_file_as_binary',
|
|
97
|
+
return_value=b'fake_image_data')
|
|
98
|
+
mocker.patch('mojentic.llm.gateways.openai_messages_adapter.encode_base64',
|
|
99
|
+
return_value='ZmFrZV9pbWFnZV9kYXRhX2VuY29kZWQ=')
|
|
100
|
+
mocker.patch('mojentic.llm.gateways.openai_messages_adapter.get_image_type',
|
|
101
|
+
side_effect=lambda path: 'jpg' if path.endswith('.jpg') else 'png')
|
|
102
102
|
|
|
103
103
|
image_paths = ["/path/to/image1.jpg", "/path/to/image2.png"]
|
|
104
104
|
messages = [LLMMessage(role=MessageRole.User, content="What's in these images?", image_paths=image_paths)]
|
|
@@ -10,6 +10,53 @@ from mojentic.llm.gateways.models import LLMMessage, MessageRole
|
|
|
10
10
|
logger = structlog.get_logger()
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def read_file_as_binary(file_path: str) -> bytes:
|
|
14
|
+
"""Read a file as binary data.
|
|
15
|
+
|
|
16
|
+
This function encapsulates the external library call to open() so it can be mocked in tests.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
file_path: Path to the file to read
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Binary content of the file
|
|
23
|
+
"""
|
|
24
|
+
with open(file_path, "rb") as file:
|
|
25
|
+
return file.read()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def encode_base64(data: bytes) -> str:
|
|
29
|
+
"""Encode binary data as base64 string.
|
|
30
|
+
|
|
31
|
+
This function encapsulates the external library call to base64.b64encode() so it can be mocked in tests.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
data: Binary data to encode
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Base64-encoded string
|
|
38
|
+
"""
|
|
39
|
+
return base64.b64encode(data).decode('utf-8')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_image_type(file_path: str) -> str:
|
|
43
|
+
"""Determine image type from file extension.
|
|
44
|
+
|
|
45
|
+
This function encapsulates the external library call to os.path.splitext() so it can be mocked in tests.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
file_path: Path to the image file
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Image type (e.g., 'jpg', 'png')
|
|
52
|
+
"""
|
|
53
|
+
_, ext = os.path.splitext(file_path)
|
|
54
|
+
image_type = ext.lstrip('.').lower()
|
|
55
|
+
if image_type not in ['jpeg', 'jpg', 'png', 'gif', 'webp']:
|
|
56
|
+
image_type = 'jpeg' # Default to jpeg if unknown extension
|
|
57
|
+
return image_type
|
|
58
|
+
|
|
59
|
+
|
|
13
60
|
def adapt_messages_to_openai(messages: List[LLMMessage]):
|
|
14
61
|
new_messages: List[dict[str, Any]] = []
|
|
15
62
|
for m in messages:
|
|
@@ -25,21 +72,17 @@ def adapt_messages_to_openai(messages: List[LLMMessage]):
|
|
|
25
72
|
# Add each image as a base64-encoded URL
|
|
26
73
|
for image_path in m.image_paths:
|
|
27
74
|
try:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"image_url": {
|
|
40
|
-
"url": f"data:image/{image_type};base64,{base64_image}"
|
|
41
|
-
}
|
|
42
|
-
})
|
|
75
|
+
# Use our encapsulated methods instead of direct library calls
|
|
76
|
+
binary_data = read_file_as_binary(image_path)
|
|
77
|
+
base64_image = encode_base64(binary_data)
|
|
78
|
+
image_type = get_image_type(image_path)
|
|
79
|
+
|
|
80
|
+
content.append({
|
|
81
|
+
"type": "image_url",
|
|
82
|
+
"image_url": {
|
|
83
|
+
"url": f"data:image/{image_type};base64,{base64_image}"
|
|
84
|
+
}
|
|
85
|
+
})
|
|
43
86
|
except Exception as e:
|
|
44
87
|
logger.error("Failed to encode image", error=str(e), image_path=image_path)
|
|
45
88
|
|
mojentic/llm/llm_broker.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import time
|
|
2
3
|
from typing import List, Optional, Type
|
|
3
4
|
|
|
4
5
|
import structlog
|
|
5
6
|
from pydantic import BaseModel
|
|
6
7
|
|
|
8
|
+
from mojentic.tracer.tracer_system import TracerSystem
|
|
7
9
|
from mojentic.llm.gateways.llm_gateway import LLMGateway
|
|
8
10
|
from mojentic.llm.gateways.models import MessageRole, LLMMessage, LLMGatewayResponse
|
|
9
11
|
from mojentic.llm.gateways.ollama import OllamaGateway
|
|
@@ -21,8 +23,10 @@ class LLMBroker():
|
|
|
21
23
|
adapter: LLMGateway
|
|
22
24
|
tokenizer: TokenizerGateway
|
|
23
25
|
model: str
|
|
26
|
+
tracer: Optional[TracerSystem]
|
|
24
27
|
|
|
25
|
-
def __init__(self, model: str, gateway: Optional[LLMGateway] = None, tokenizer: Optional[TokenizerGateway] = None
|
|
28
|
+
def __init__(self, model: str, gateway: Optional[LLMGateway] = None, tokenizer: Optional[TokenizerGateway] = None,
|
|
29
|
+
tracer: Optional[TracerSystem] = None):
|
|
26
30
|
"""
|
|
27
31
|
Create an instance of the LLMBroker.
|
|
28
32
|
|
|
@@ -36,8 +40,15 @@ class LLMBroker():
|
|
|
36
40
|
tokenizer
|
|
37
41
|
The gateway to use for tokenization. This is used to log approximate token counts for the LLM calls. If
|
|
38
42
|
None, `mxbai-embed-large` is used on a local Ollama server.
|
|
43
|
+
tracer
|
|
44
|
+
Optional tracer system to record LLM calls and responses.
|
|
39
45
|
"""
|
|
40
46
|
self.model = model
|
|
47
|
+
|
|
48
|
+
# Use null_tracer if no tracer is provided
|
|
49
|
+
from mojentic.tracer import null_tracer
|
|
50
|
+
self.tracer = tracer or null_tracer
|
|
51
|
+
|
|
41
52
|
if tokenizer is None:
|
|
42
53
|
self.tokenizer = TokenizerGateway()
|
|
43
54
|
else:
|
|
@@ -72,7 +83,23 @@ class LLMBroker():
|
|
|
72
83
|
"""
|
|
73
84
|
approximate_tokens = len(self.tokenizer.encode(self._content_to_count(messages)))
|
|
74
85
|
logger.info(f"Requesting llm response with approx {approximate_tokens} tokens")
|
|
75
|
-
|
|
86
|
+
|
|
87
|
+
# Convert messages to serializable dict for audit
|
|
88
|
+
messages_for_tracer = [m.dict() for m in messages]
|
|
89
|
+
|
|
90
|
+
# Record LLM call in tracer
|
|
91
|
+
tools_for_tracer = [{"name": t.name, "description": t.description} for t in tools] if tools else None
|
|
92
|
+
self.tracer.record_llm_call(
|
|
93
|
+
self.model,
|
|
94
|
+
messages_for_tracer,
|
|
95
|
+
temperature,
|
|
96
|
+
tools=tools_for_tracer,
|
|
97
|
+
source=type(self)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Measure call duration for audit
|
|
101
|
+
start_time = time.time()
|
|
102
|
+
|
|
76
103
|
result: LLMGatewayResponse = self.adapter.complete(
|
|
77
104
|
model=self.model,
|
|
78
105
|
messages=messages,
|
|
@@ -80,6 +107,18 @@ class LLMBroker():
|
|
|
80
107
|
temperature=temperature,
|
|
81
108
|
num_ctx=num_ctx,
|
|
82
109
|
num_predict=num_predict)
|
|
110
|
+
|
|
111
|
+
call_duration_ms = (time.time() - start_time) * 1000
|
|
112
|
+
|
|
113
|
+
# Record LLM response in tracer
|
|
114
|
+
tool_calls_for_tracer = [tc.dict() for tc in result.tool_calls] if result.tool_calls else None
|
|
115
|
+
self.tracer.record_llm_response(
|
|
116
|
+
self.model,
|
|
117
|
+
result.content,
|
|
118
|
+
tool_calls=tool_calls_for_tracer,
|
|
119
|
+
call_duration_ms=call_duration_ms,
|
|
120
|
+
source=type(self)
|
|
121
|
+
)
|
|
83
122
|
|
|
84
123
|
if result.tool_calls and tools is not None:
|
|
85
124
|
logger.info("Tool call requested")
|
|
@@ -89,7 +128,22 @@ class LLMBroker():
|
|
|
89
128
|
None):
|
|
90
129
|
logger.info('Calling function', function=tool_call.name)
|
|
91
130
|
logger.info('Arguments:', arguments=tool_call.arguments)
|
|
131
|
+
|
|
132
|
+
# Get the arguments before calling the tool
|
|
133
|
+
tool_arguments = tool_call.arguments
|
|
134
|
+
|
|
135
|
+
# Call the tool
|
|
92
136
|
output = tool.run(**tool_call.arguments)
|
|
137
|
+
|
|
138
|
+
# Record tool call in tracer
|
|
139
|
+
self.tracer.record_tool_call(
|
|
140
|
+
tool_call.name,
|
|
141
|
+
tool_arguments,
|
|
142
|
+
output,
|
|
143
|
+
caller="LLMBroker",
|
|
144
|
+
source=type(self)
|
|
145
|
+
)
|
|
146
|
+
|
|
93
147
|
logger.info('Function output', output=output)
|
|
94
148
|
messages.append(LLMMessage(role=MessageRole.Assistant, tool_calls=[tool_call]))
|
|
95
149
|
messages.append(
|
|
@@ -135,6 +189,35 @@ class LLMBroker():
|
|
|
135
189
|
"""
|
|
136
190
|
approximate_tokens = len(self.tokenizer.encode(self._content_to_count(messages)))
|
|
137
191
|
logger.info(f"Requesting llm response with approx {approximate_tokens} tokens")
|
|
192
|
+
|
|
193
|
+
# Convert messages to serializable dict for audit
|
|
194
|
+
messages_for_tracer = [m.dict() for m in messages]
|
|
195
|
+
|
|
196
|
+
# Record LLM call in tracer
|
|
197
|
+
self.tracer.record_llm_call(
|
|
198
|
+
self.model,
|
|
199
|
+
messages_for_tracer,
|
|
200
|
+
temperature,
|
|
201
|
+
tools=None,
|
|
202
|
+
source=type(self)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Measure call duration for audit
|
|
206
|
+
start_time = time.time()
|
|
207
|
+
|
|
138
208
|
result = self.adapter.complete(model=self.model, messages=messages, object_model=object_model,
|
|
139
209
|
temperature=temperature, num_ctx=num_ctx, num_predict=num_predict)
|
|
210
|
+
|
|
211
|
+
call_duration_ms = (time.time() - start_time) * 1000
|
|
212
|
+
|
|
213
|
+
# Record LLM response in tracer with object representation
|
|
214
|
+
# Convert object to string for tracer
|
|
215
|
+
object_str = str(result.object.dict()) if hasattr(result.object, "dict") else str(result.object)
|
|
216
|
+
self.tracer.record_llm_response(
|
|
217
|
+
self.model,
|
|
218
|
+
f"Structured response: {object_str}",
|
|
219
|
+
call_duration_ms=call_duration_ms,
|
|
220
|
+
source=type(self)
|
|
221
|
+
)
|
|
222
|
+
|
|
140
223
|
return result.object
|
|
@@ -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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
|
|
197
|
+
# Convert to strings for easier comparison
|
|
198
|
+
image_paths_str = [str(p) for p in message_builder.image_paths]
|
|
192
199
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
"""
|
mojentic/llm/tools/llm_tool.py
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
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
25
|
def call_tool(self, **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
|
+
)
|
|
36
|
+
|
|
37
|
+
# Format the result
|
|
12
38
|
if isinstance(result, dict):
|
|
13
39
|
result = json.dumps(result)
|
|
14
40
|
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()
|