mojentic 0.6.0__py3-none-any.whl → 0.6.2__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 -2
- _examples/tracer_demo.py +78 -17
- mojentic/agents/simple_recursive_agent.py +20 -21
- mojentic/llm/gateways/openai.py +2 -2
- mojentic/llm/llm_broker.py +37 -27
- mojentic/llm/tools/llm_tool.py +7 -6
- mojentic/tracer/null_tracer.py +38 -26
- mojentic/tracer/tracer_events.py +27 -25
- mojentic/tracer/tracer_system.py +58 -42
- mojentic/tracer/tracer_system_spec.py +65 -54
- {mojentic-0.6.0.dist-info → mojentic-0.6.2.dist-info}/METADATA +1 -1
- {mojentic-0.6.0.dist-info → mojentic-0.6.2.dist-info}/RECORD +15 -15
- {mojentic-0.6.0.dist-info → mojentic-0.6.2.dist-info}/WHEEL +1 -1
- {mojentic-0.6.0.dist-info → mojentic-0.6.2.dist-info}/licenses/LICENSE.md +0 -0
- {mojentic-0.6.0.dist-info → mojentic-0.6.2.dist-info}/top_level.txt +0 -0
_examples/recursive_agent.py
CHANGED
|
@@ -42,7 +42,7 @@ async def demonstrate_async():
|
|
|
42
42
|
print(f"\nProblem (With Event Handling): {problem2}")
|
|
43
43
|
|
|
44
44
|
# Set up event handlers for monitoring the solution process
|
|
45
|
-
from mojentic.agents.simple_recursive_agent import
|
|
45
|
+
from mojentic.agents.simple_recursive_agent import GoalAchievedEvent, IterationCompletedEvent
|
|
46
46
|
|
|
47
47
|
# Define event handlers
|
|
48
48
|
def on_iteration_completed(event):
|
|
@@ -53,7 +53,7 @@ async def demonstrate_async():
|
|
|
53
53
|
|
|
54
54
|
# Subscribe to events
|
|
55
55
|
unsubscribe_iteration = agent.emitter.subscribe(IterationCompletedEvent, on_iteration_completed)
|
|
56
|
-
unsubscribe_solved = agent.emitter.subscribe(
|
|
56
|
+
unsubscribe_solved = agent.emitter.subscribe(GoalAchievedEvent, on_problem_solved)
|
|
57
57
|
|
|
58
58
|
# Solve the problem
|
|
59
59
|
solution2 = await agent.solve(problem2)
|
_examples/tracer_demo.py
CHANGED
|
@@ -4,12 +4,17 @@ Example script demonstrating the tracer system with ChatSession and tools.
|
|
|
4
4
|
This example shows how to use the tracer system to monitor an interactive
|
|
5
5
|
chat session with LLMBroker and tools. When the user exits the session,
|
|
6
6
|
the script displays a summary of all traced events.
|
|
7
|
+
|
|
8
|
+
It also demonstrates how correlation_id is used to trace related events
|
|
9
|
+
across the system, allowing you to track the flow of a request from start to finish.
|
|
7
10
|
"""
|
|
11
|
+
import uuid
|
|
8
12
|
from datetime import datetime
|
|
9
13
|
|
|
10
14
|
from mojentic.tracer import TracerSystem
|
|
11
15
|
from mojentic.tracer.tracer_events import LLMCallTracerEvent, LLMResponseTracerEvent, ToolCallTracerEvent
|
|
12
16
|
from mojentic.llm import ChatSession, LLMBroker
|
|
17
|
+
from mojentic.llm.gateways.models import LLMMessage, MessageRole
|
|
13
18
|
from mojentic.llm.tools.date_resolver import ResolveDateTool
|
|
14
19
|
|
|
15
20
|
|
|
@@ -18,7 +23,7 @@ def print_tracer_events(events):
|
|
|
18
23
|
print(f"\n{'-'*80}")
|
|
19
24
|
print("Tracer Events:")
|
|
20
25
|
print(f"{'-'*80}")
|
|
21
|
-
|
|
26
|
+
|
|
22
27
|
for i, event in enumerate(events, 1):
|
|
23
28
|
print(f"{i}. {event.printable_summary()}")
|
|
24
29
|
print()
|
|
@@ -28,70 +33,126 @@ def main():
|
|
|
28
33
|
"""Run a chat session with tracer system to monitor interactions."""
|
|
29
34
|
# Create a tracer system to monitor all interactions
|
|
30
35
|
tracer = TracerSystem()
|
|
31
|
-
|
|
36
|
+
|
|
32
37
|
# Create an LLM broker with the tracer
|
|
33
38
|
llm_broker = LLMBroker(model="llama3.3-70b-32k", tracer=tracer)
|
|
34
|
-
|
|
39
|
+
|
|
35
40
|
# Create a date resolver tool that will also use the tracer
|
|
36
41
|
date_tool = ResolveDateTool(llm_broker=llm_broker, tracer=tracer)
|
|
37
|
-
|
|
42
|
+
|
|
38
43
|
# Create a chat session with the broker and tool
|
|
39
44
|
chat_session = ChatSession(llm_broker, tools=[date_tool])
|
|
40
|
-
|
|
45
|
+
|
|
46
|
+
# Dictionary to store correlation_ids for each conversation turn
|
|
47
|
+
# This allows us to track related events across the system
|
|
48
|
+
conversation_correlation_ids = {}
|
|
49
|
+
|
|
41
50
|
print("Welcome to the chat session with tracer demonstration!")
|
|
42
51
|
print("Ask questions about dates (e.g., 'What day is next Friday?') or anything else.")
|
|
43
52
|
print("Behind the scenes, the tracer system is recording all interactions.")
|
|
53
|
+
print("Each interaction is assigned a unique correlation_id to trace related events.")
|
|
44
54
|
print("Press Enter with no input to exit and see the trace summary.")
|
|
45
55
|
print("-" * 80)
|
|
46
|
-
|
|
56
|
+
|
|
47
57
|
# Interactive chat session
|
|
58
|
+
turn_counter = 0
|
|
48
59
|
while True:
|
|
49
60
|
query = input("You: ")
|
|
50
61
|
if not query:
|
|
51
62
|
print("Exiting chat session...")
|
|
52
63
|
break
|
|
53
64
|
else:
|
|
65
|
+
# Generate a unique correlation_id for this conversation turn
|
|
66
|
+
# In a real system, this would be passed from the initiating event
|
|
67
|
+
# to all downstream events to maintain the causal chain
|
|
68
|
+
correlation_id = str(uuid.uuid4())
|
|
69
|
+
turn_counter += 1
|
|
70
|
+
conversation_correlation_ids[turn_counter] = correlation_id
|
|
71
|
+
|
|
72
|
+
print(f"[Turn {turn_counter}, correlation_id: {correlation_id[:8]}...]")
|
|
54
73
|
print("Assistant: ", end="")
|
|
74
|
+
|
|
75
|
+
# For demonstration purposes, we'll use the chat_session normally
|
|
76
|
+
# In a production system, you would modify ChatSession to accept and use correlation_id
|
|
55
77
|
response = chat_session.send(query)
|
|
78
|
+
|
|
79
|
+
# Alternatively, you could use the LLMBroker directly with correlation_id:
|
|
80
|
+
# messages = [LLMMessage(role=MessageRole.User, content=query)]
|
|
81
|
+
# response = llm_broker.generate(messages, tools=[date_tool], correlation_id=correlation_id)
|
|
82
|
+
|
|
56
83
|
print(response)
|
|
57
|
-
|
|
84
|
+
|
|
58
85
|
# After the user exits, display tracer event summary
|
|
59
86
|
print("\nTracer System Summary")
|
|
60
87
|
print("=" * 80)
|
|
61
88
|
print(f"You just had a conversation with an LLM, and the tracer recorded everything!")
|
|
62
|
-
|
|
89
|
+
|
|
63
90
|
# Get all events
|
|
64
91
|
all_events = tracer.get_events()
|
|
65
92
|
print(f"Total events recorded: {len(all_events)}")
|
|
66
93
|
print_tracer_events(all_events)
|
|
67
|
-
|
|
94
|
+
|
|
68
95
|
# Show how to filter events by type
|
|
69
96
|
print("\nYou can filter events by type:")
|
|
70
|
-
|
|
97
|
+
|
|
71
98
|
llm_calls = tracer.get_events(event_type=LLMCallTracerEvent)
|
|
72
99
|
print(f"LLM Call Events: {len(llm_calls)}")
|
|
73
100
|
if llm_calls:
|
|
74
101
|
print(f"Example: {llm_calls[0].printable_summary()}")
|
|
75
|
-
|
|
102
|
+
|
|
76
103
|
llm_responses = tracer.get_events(event_type=LLMResponseTracerEvent)
|
|
77
104
|
print(f"LLM Response Events: {len(llm_responses)}")
|
|
78
105
|
if llm_responses:
|
|
79
106
|
print(f"Example: {llm_responses[0].printable_summary()}")
|
|
80
|
-
|
|
107
|
+
|
|
81
108
|
tool_calls = tracer.get_events(event_type=ToolCallTracerEvent)
|
|
82
109
|
print(f"Tool Call Events: {len(tool_calls)}")
|
|
83
110
|
if tool_calls:
|
|
84
111
|
print(f"Example: {tool_calls[0].printable_summary()}")
|
|
85
|
-
|
|
112
|
+
|
|
86
113
|
# Show the last few events
|
|
87
114
|
print("\nThe last few events:")
|
|
88
115
|
last_events = tracer.get_last_n_tracer_events(3)
|
|
89
116
|
print_tracer_events(last_events)
|
|
90
|
-
|
|
117
|
+
|
|
91
118
|
# Show how to use time-based filtering
|
|
92
119
|
print("\nYou can also filter events by time range:")
|
|
93
120
|
print("Example: tracer.get_events(start_time=start_timestamp, end_time=end_timestamp)")
|
|
94
|
-
|
|
121
|
+
|
|
122
|
+
# Demonstrate filtering events by correlation_id
|
|
123
|
+
print("\nFiltering events by correlation_id:")
|
|
124
|
+
print("This is a powerful feature that allows you to trace all events related to a specific request")
|
|
125
|
+
|
|
126
|
+
# If we have any conversation turns, show events for the first turn
|
|
127
|
+
if conversation_correlation_ids:
|
|
128
|
+
# Get the correlation_id for the first turn
|
|
129
|
+
first_turn_id = 1
|
|
130
|
+
first_correlation_id = conversation_correlation_ids.get(first_turn_id)
|
|
131
|
+
|
|
132
|
+
if first_correlation_id:
|
|
133
|
+
print(f"\nEvents for conversation turn {first_turn_id} (correlation_id: {first_correlation_id[:8]}...):")
|
|
134
|
+
|
|
135
|
+
# Define a filter function that checks the correlation_id
|
|
136
|
+
def filter_by_correlation_id(event):
|
|
137
|
+
return event.correlation_id == first_correlation_id
|
|
138
|
+
|
|
139
|
+
# Get all events with this correlation_id
|
|
140
|
+
related_events = tracer.get_events(filter_func=filter_by_correlation_id)
|
|
141
|
+
|
|
142
|
+
if related_events:
|
|
143
|
+
print(f"Found {len(related_events)} related events")
|
|
144
|
+
print_tracer_events(related_events)
|
|
145
|
+
|
|
146
|
+
# Show how this helps trace the flow of a request
|
|
147
|
+
print("\nThe correlation_id allows you to trace the complete flow of a request:")
|
|
148
|
+
print("1. From the initial LLM call")
|
|
149
|
+
print("2. To the LLM response")
|
|
150
|
+
print("3. To any tool calls triggered by the LLM")
|
|
151
|
+
print("4. And any subsequent LLM calls with the tool results")
|
|
152
|
+
print("\nThis creates a complete audit trail for debugging and observability.")
|
|
153
|
+
else:
|
|
154
|
+
print("No events found with this correlation_id. This is unexpected and may indicate an issue.")
|
|
155
|
+
|
|
95
156
|
# Show how to extract specific information from events
|
|
96
157
|
if tool_calls:
|
|
97
158
|
print("\nDetailed analysis example - Tool usage stats:")
|
|
@@ -99,11 +160,11 @@ def main():
|
|
|
99
160
|
for event in tool_calls:
|
|
100
161
|
tool_name = event.tool_name
|
|
101
162
|
tool_names[tool_name] = tool_names.get(tool_name, 0) + 1
|
|
102
|
-
|
|
163
|
+
|
|
103
164
|
print("Tool usage frequency:")
|
|
104
165
|
for tool_name, count in tool_names.items():
|
|
105
166
|
print(f" - {tool_name}: {count} calls")
|
|
106
167
|
|
|
107
168
|
|
|
108
169
|
if __name__ == "__main__":
|
|
109
|
-
main()
|
|
170
|
+
main()
|
|
@@ -8,17 +8,16 @@ from typing import List, Optional
|
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
11
|
-
from mojentic.llm.
|
|
11
|
+
from mojentic.llm.chat_session import ChatSession
|
|
12
12
|
from mojentic.llm.llm_broker import LLMBroker
|
|
13
13
|
from mojentic.llm.tools.llm_tool import LLMTool
|
|
14
|
-
from mojentic.llm.chat_session import ChatSession
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
class
|
|
16
|
+
class GoalState(BaseModel):
|
|
18
17
|
"""
|
|
19
18
|
Represents the state of a problem-solving process.
|
|
20
19
|
"""
|
|
21
|
-
|
|
20
|
+
goal: str
|
|
22
21
|
iteration: int = 0
|
|
23
22
|
max_iterations: int = 5
|
|
24
23
|
solution: Optional[str] = None
|
|
@@ -29,10 +28,10 @@ class SolverEvent(BaseModel):
|
|
|
29
28
|
"""
|
|
30
29
|
Base class for solver events.
|
|
31
30
|
"""
|
|
32
|
-
state:
|
|
31
|
+
state: GoalState
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
class
|
|
34
|
+
class GoalSubmittedEvent(SolverEvent):
|
|
36
35
|
"""
|
|
37
36
|
Event triggered when a problem is submitted for solving.
|
|
38
37
|
"""
|
|
@@ -46,14 +45,14 @@ class IterationCompletedEvent(SolverEvent):
|
|
|
46
45
|
response: str
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
class
|
|
48
|
+
class GoalAchievedEvent(SolverEvent):
|
|
50
49
|
"""
|
|
51
50
|
Event triggered when a problem is solved.
|
|
52
51
|
"""
|
|
53
52
|
pass
|
|
54
53
|
|
|
55
54
|
|
|
56
|
-
class
|
|
55
|
+
class GoalFailedEvent(SolverEvent):
|
|
57
56
|
"""
|
|
58
57
|
Event triggered when a problem cannot be solved.
|
|
59
58
|
"""
|
|
@@ -168,7 +167,7 @@ class SimpleRecursiveAgent:
|
|
|
168
167
|
)
|
|
169
168
|
|
|
170
169
|
# Set up event handlers
|
|
171
|
-
self.emitter.subscribe(
|
|
170
|
+
self.emitter.subscribe(GoalSubmittedEvent, self._handle_problem_submitted)
|
|
172
171
|
self.emitter.subscribe(IterationCompletedEvent, self._handle_iteration_completed)
|
|
173
172
|
|
|
174
173
|
async def solve(self, problem: str) -> str:
|
|
@@ -189,7 +188,7 @@ class SimpleRecursiveAgent:
|
|
|
189
188
|
solution_future = asyncio.Future()
|
|
190
189
|
|
|
191
190
|
# Create the initial problem state
|
|
192
|
-
state =
|
|
191
|
+
state = GoalState(goal=problem, max_iterations=self.max_iterations)
|
|
193
192
|
|
|
194
193
|
# Define handlers for completion events
|
|
195
194
|
async def handle_solution_event(event):
|
|
@@ -197,12 +196,12 @@ class SimpleRecursiveAgent:
|
|
|
197
196
|
solution_future.set_result(event.state.solution)
|
|
198
197
|
|
|
199
198
|
# Subscribe to completion events
|
|
200
|
-
self.emitter.subscribe(
|
|
201
|
-
self.emitter.subscribe(
|
|
199
|
+
self.emitter.subscribe(GoalAchievedEvent, handle_solution_event)
|
|
200
|
+
self.emitter.subscribe(GoalFailedEvent, handle_solution_event)
|
|
202
201
|
self.emitter.subscribe(TimeoutEvent, handle_solution_event)
|
|
203
202
|
|
|
204
203
|
# Start the solving process
|
|
205
|
-
self.emitter.emit(
|
|
204
|
+
self.emitter.emit(GoalSubmittedEvent(state=state))
|
|
206
205
|
|
|
207
206
|
# Wait for the solution or timeout
|
|
208
207
|
try:
|
|
@@ -215,13 +214,13 @@ class SimpleRecursiveAgent:
|
|
|
215
214
|
self.emitter.emit(TimeoutEvent(state=state))
|
|
216
215
|
return timeout_message
|
|
217
216
|
|
|
218
|
-
async def _handle_problem_submitted(self, event:
|
|
217
|
+
async def _handle_problem_submitted(self, event: GoalSubmittedEvent):
|
|
219
218
|
"""
|
|
220
219
|
Handle a problem submitted event.
|
|
221
220
|
|
|
222
221
|
Parameters
|
|
223
222
|
----------
|
|
224
|
-
event :
|
|
223
|
+
event : GoalSubmittedEvent
|
|
225
224
|
The problem submitted event to handle
|
|
226
225
|
"""
|
|
227
226
|
# Start the first iteration
|
|
@@ -243,31 +242,31 @@ class SimpleRecursiveAgent:
|
|
|
243
242
|
if "FAIL".lower() in response.lower():
|
|
244
243
|
state.solution = f"Failed to solve after {state.iteration} iterations:\n{response}"
|
|
245
244
|
state.is_complete = True
|
|
246
|
-
self.emitter.emit(
|
|
245
|
+
self.emitter.emit(GoalFailedEvent(state=state))
|
|
247
246
|
return
|
|
248
247
|
elif "DONE".lower() in response.lower():
|
|
249
248
|
state.solution = response
|
|
250
249
|
state.is_complete = True
|
|
251
|
-
self.emitter.emit(
|
|
250
|
+
self.emitter.emit(GoalAchievedEvent(state=state))
|
|
252
251
|
return
|
|
253
252
|
|
|
254
253
|
# Check if we've reached the maximum number of iterations
|
|
255
254
|
if state.iteration >= state.max_iterations:
|
|
256
255
|
state.solution = f"Best solution after {state.max_iterations} iterations:\n{response}"
|
|
257
256
|
state.is_complete = True
|
|
258
|
-
self.emitter.emit(
|
|
257
|
+
self.emitter.emit(GoalAchievedEvent(state=state))
|
|
259
258
|
return
|
|
260
259
|
|
|
261
260
|
# If the problem is not solved and we haven't reached max_iterations, continue with next iteration
|
|
262
261
|
await self._process_iteration(state)
|
|
263
262
|
|
|
264
|
-
async def _process_iteration(self, state:
|
|
263
|
+
async def _process_iteration(self, state: GoalState):
|
|
265
264
|
"""
|
|
266
265
|
Process a single iteration of the problem-solving process.
|
|
267
266
|
|
|
268
267
|
Parameters
|
|
269
268
|
----------
|
|
270
|
-
state :
|
|
269
|
+
state : GoalState
|
|
271
270
|
The current state of the problem-solving process
|
|
272
271
|
"""
|
|
273
272
|
# Increment the iteration counter
|
|
@@ -276,7 +275,7 @@ class SimpleRecursiveAgent:
|
|
|
276
275
|
# Create a prompt for the LLM
|
|
277
276
|
prompt = f"""
|
|
278
277
|
Given the user request:
|
|
279
|
-
{state.
|
|
278
|
+
{state.goal}
|
|
280
279
|
|
|
281
280
|
Use the tools at your disposal to act on their request. You may wish to create a step-by-step plan for more complicated requests.
|
|
282
281
|
|
mojentic/llm/gateways/openai.py
CHANGED
|
@@ -24,8 +24,8 @@ class OpenAIGateway(LLMGateway):
|
|
|
24
24
|
The OpenAI API key to use.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
def __init__(self, api_key: str):
|
|
28
|
-
self.client = OpenAI(api_key=api_key)
|
|
27
|
+
def __init__(self, api_key: str, base_url: str = None):
|
|
28
|
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
|
29
29
|
|
|
30
30
|
def complete(self, **args) -> LLMGatewayResponse:
|
|
31
31
|
"""
|
mojentic/llm/llm_broker.py
CHANGED
|
@@ -44,11 +44,11 @@ class LLMBroker():
|
|
|
44
44
|
Optional tracer system to record LLM calls and responses.
|
|
45
45
|
"""
|
|
46
46
|
self.model = model
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
# Use null_tracer if no tracer is provided
|
|
49
49
|
from mojentic.tracer import null_tracer
|
|
50
50
|
self.tracer = tracer or null_tracer
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
if tokenizer is None:
|
|
53
53
|
self.tokenizer = TokenizerGateway()
|
|
54
54
|
else:
|
|
@@ -58,7 +58,8 @@ class LLMBroker():
|
|
|
58
58
|
else:
|
|
59
59
|
self.adapter = gateway
|
|
60
60
|
|
|
61
|
-
def generate(self, messages: List[LLMMessage], tools=None, temperature=1.0, num_ctx=32768, num_predict=-1
|
|
61
|
+
def generate(self, messages: List[LLMMessage], tools=None, temperature=1.0, num_ctx=32768, num_predict=-1,
|
|
62
|
+
correlation_id: str = None) -> str:
|
|
62
63
|
"""
|
|
63
64
|
Generate a text response from the LLM.
|
|
64
65
|
|
|
@@ -75,6 +76,8 @@ class LLMBroker():
|
|
|
75
76
|
The number of context tokens to use. Defaults to 32768.
|
|
76
77
|
num_predict : int
|
|
77
78
|
The number of tokens to predict. Defaults to no limit.
|
|
79
|
+
correlation_id : str
|
|
80
|
+
UUID string that is copied from cause-to-affect for tracing events.
|
|
78
81
|
|
|
79
82
|
Returns
|
|
80
83
|
-------
|
|
@@ -83,10 +86,10 @@ class LLMBroker():
|
|
|
83
86
|
"""
|
|
84
87
|
approximate_tokens = len(self.tokenizer.encode(self._content_to_count(messages)))
|
|
85
88
|
logger.info(f"Requesting llm response with approx {approximate_tokens} tokens")
|
|
86
|
-
|
|
89
|
+
|
|
87
90
|
# Convert messages to serializable dict for audit
|
|
88
91
|
messages_for_tracer = [m.dict() for m in messages]
|
|
89
|
-
|
|
92
|
+
|
|
90
93
|
# Record LLM call in tracer
|
|
91
94
|
tools_for_tracer = [{"name": t.name, "description": t.description} for t in tools] if tools else None
|
|
92
95
|
self.tracer.record_llm_call(
|
|
@@ -94,12 +97,13 @@ class LLMBroker():
|
|
|
94
97
|
messages_for_tracer,
|
|
95
98
|
temperature,
|
|
96
99
|
tools=tools_for_tracer,
|
|
97
|
-
source=type(self)
|
|
100
|
+
source=type(self),
|
|
101
|
+
correlation_id=correlation_id
|
|
98
102
|
)
|
|
99
|
-
|
|
103
|
+
|
|
100
104
|
# Measure call duration for audit
|
|
101
105
|
start_time = time.time()
|
|
102
|
-
|
|
106
|
+
|
|
103
107
|
result: LLMGatewayResponse = self.adapter.complete(
|
|
104
108
|
model=self.model,
|
|
105
109
|
messages=messages,
|
|
@@ -107,9 +111,9 @@ class LLMBroker():
|
|
|
107
111
|
temperature=temperature,
|
|
108
112
|
num_ctx=num_ctx,
|
|
109
113
|
num_predict=num_predict)
|
|
110
|
-
|
|
114
|
+
|
|
111
115
|
call_duration_ms = (time.time() - start_time) * 1000
|
|
112
|
-
|
|
116
|
+
|
|
113
117
|
# Record LLM response in tracer
|
|
114
118
|
tool_calls_for_tracer = [tc.dict() for tc in result.tool_calls] if result.tool_calls else None
|
|
115
119
|
self.tracer.record_llm_response(
|
|
@@ -117,7 +121,8 @@ class LLMBroker():
|
|
|
117
121
|
result.content,
|
|
118
122
|
tool_calls=tool_calls_for_tracer,
|
|
119
123
|
call_duration_ms=call_duration_ms,
|
|
120
|
-
source=type(self)
|
|
124
|
+
source=type(self),
|
|
125
|
+
correlation_id=correlation_id
|
|
121
126
|
)
|
|
122
127
|
|
|
123
128
|
if result.tool_calls and tools is not None:
|
|
@@ -128,28 +133,29 @@ class LLMBroker():
|
|
|
128
133
|
None):
|
|
129
134
|
logger.info('Calling function', function=tool_call.name)
|
|
130
135
|
logger.info('Arguments:', arguments=tool_call.arguments)
|
|
131
|
-
|
|
136
|
+
|
|
132
137
|
# Get the arguments before calling the tool
|
|
133
138
|
tool_arguments = tool_call.arguments
|
|
134
|
-
|
|
139
|
+
|
|
135
140
|
# Call the tool
|
|
136
141
|
output = tool.run(**tool_call.arguments)
|
|
137
|
-
|
|
142
|
+
|
|
138
143
|
# Record tool call in tracer
|
|
139
144
|
self.tracer.record_tool_call(
|
|
140
145
|
tool_call.name,
|
|
141
146
|
tool_arguments,
|
|
142
147
|
output,
|
|
143
148
|
caller="LLMBroker",
|
|
144
|
-
source=type(self)
|
|
149
|
+
source=type(self),
|
|
150
|
+
correlation_id=correlation_id
|
|
145
151
|
)
|
|
146
|
-
|
|
152
|
+
|
|
147
153
|
logger.info('Function output', output=output)
|
|
148
154
|
messages.append(LLMMessage(role=MessageRole.Assistant, tool_calls=[tool_call]))
|
|
149
155
|
messages.append(
|
|
150
156
|
LLMMessage(role=MessageRole.Tool, content=json.dumps(output), tool_calls=[tool_call]))
|
|
151
157
|
# {'role': 'tool', 'content': str(output), 'name': tool_call.name, 'tool_call_id': tool_call.id})
|
|
152
|
-
return self.generate(messages, tools, temperature, num_ctx, num_predict)
|
|
158
|
+
return self.generate(messages, tools, temperature, num_ctx, num_predict, correlation_id=correlation_id)
|
|
153
159
|
else:
|
|
154
160
|
logger.warn('Function not found', function=tool_call.name)
|
|
155
161
|
logger.info('Expected usage of missing function', expected_usage=tool_call)
|
|
@@ -165,7 +171,7 @@ class LLMBroker():
|
|
|
165
171
|
return content
|
|
166
172
|
|
|
167
173
|
def generate_object(self, messages: List[LLMMessage], object_model: Type[BaseModel], temperature=1.0, num_ctx=32768,
|
|
168
|
-
num_predict=-1) -> BaseModel:
|
|
174
|
+
num_predict=-1, correlation_id: str = None) -> BaseModel:
|
|
169
175
|
"""
|
|
170
176
|
Generate a structured response from the LLM and return it as an object.
|
|
171
177
|
|
|
@@ -181,6 +187,8 @@ class LLMBroker():
|
|
|
181
187
|
The number of context tokens to use. Defaults to 32768.
|
|
182
188
|
num_predict : int
|
|
183
189
|
The number of tokens to predict. Defaults to no limit.
|
|
190
|
+
correlation_id : str
|
|
191
|
+
UUID string that is copied from cause-to-affect for tracing events.
|
|
184
192
|
|
|
185
193
|
Returns
|
|
186
194
|
-------
|
|
@@ -189,27 +197,28 @@ class LLMBroker():
|
|
|
189
197
|
"""
|
|
190
198
|
approximate_tokens = len(self.tokenizer.encode(self._content_to_count(messages)))
|
|
191
199
|
logger.info(f"Requesting llm response with approx {approximate_tokens} tokens")
|
|
192
|
-
|
|
200
|
+
|
|
193
201
|
# Convert messages to serializable dict for audit
|
|
194
202
|
messages_for_tracer = [m.dict() for m in messages]
|
|
195
|
-
|
|
203
|
+
|
|
196
204
|
# Record LLM call in tracer
|
|
197
205
|
self.tracer.record_llm_call(
|
|
198
206
|
self.model,
|
|
199
207
|
messages_for_tracer,
|
|
200
208
|
temperature,
|
|
201
209
|
tools=None,
|
|
202
|
-
source=type(self)
|
|
210
|
+
source=type(self),
|
|
211
|
+
correlation_id=correlation_id
|
|
203
212
|
)
|
|
204
|
-
|
|
213
|
+
|
|
205
214
|
# Measure call duration for audit
|
|
206
215
|
start_time = time.time()
|
|
207
|
-
|
|
216
|
+
|
|
208
217
|
result = self.adapter.complete(model=self.model, messages=messages, object_model=object_model,
|
|
209
218
|
temperature=temperature, num_ctx=num_ctx, num_predict=num_predict)
|
|
210
|
-
|
|
219
|
+
|
|
211
220
|
call_duration_ms = (time.time() - start_time) * 1000
|
|
212
|
-
|
|
221
|
+
|
|
213
222
|
# Record LLM response in tracer with object representation
|
|
214
223
|
# Convert object to string for tracer
|
|
215
224
|
object_str = str(result.object.dict()) if hasattr(result.object, "dict") else str(result.object)
|
|
@@ -217,7 +226,8 @@ class LLMBroker():
|
|
|
217
226
|
self.model,
|
|
218
227
|
f"Structured response: {object_str}",
|
|
219
228
|
call_duration_ms=call_duration_ms,
|
|
220
|
-
source=type(self)
|
|
229
|
+
source=type(self),
|
|
230
|
+
correlation_id=correlation_id
|
|
221
231
|
)
|
|
222
|
-
|
|
232
|
+
|
|
223
233
|
return result.object
|
mojentic/llm/tools/llm_tool.py
CHANGED
|
@@ -9,7 +9,7 @@ class LLMTool:
|
|
|
9
9
|
def __init__(self, tracer: Optional[TracerSystem] = None):
|
|
10
10
|
"""
|
|
11
11
|
Initialize an LLM tool with optional tracer system.
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
Parameters
|
|
14
14
|
----------
|
|
15
15
|
tracer : TracerSystem, optional
|
|
@@ -18,22 +18,23 @@ class LLMTool:
|
|
|
18
18
|
# Use null_tracer if no tracer is provided
|
|
19
19
|
from mojentic.tracer import null_tracer
|
|
20
20
|
self.tracer = tracer or null_tracer
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
def run(self, **kwargs):
|
|
23
23
|
raise NotImplementedError
|
|
24
24
|
|
|
25
|
-
def call_tool(self, **kwargs):
|
|
25
|
+
def call_tool(self, correlation_id: str = None, **kwargs):
|
|
26
26
|
# Execute the tool and capture the result
|
|
27
27
|
result = self.run(**kwargs)
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
# Record the tool call in the tracer system (always safe to call with null_tracer)
|
|
30
30
|
self.tracer.record_tool_call(
|
|
31
31
|
tool_name=self.name,
|
|
32
32
|
arguments=kwargs,
|
|
33
33
|
result=result,
|
|
34
|
-
source=type(self)
|
|
34
|
+
source=type(self),
|
|
35
|
+
correlation_id=correlation_id
|
|
35
36
|
)
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
# Format the result
|
|
38
39
|
if isinstance(result, dict):
|
|
39
40
|
result = json.dumps(result)
|