mojentic 0.6.0__py3-none-any.whl → 0.6.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.
@@ -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 ProblemSolvedEvent, IterationCompletedEvent
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(ProblemSolvedEvent, on_problem_solved)
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.gateways.models import LLMMessage
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 ProblemState(BaseModel):
16
+ class GoalState(BaseModel):
18
17
  """
19
18
  Represents the state of a problem-solving process.
20
19
  """
21
- problem: str
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: ProblemState
31
+ state: GoalState
33
32
 
34
33
 
35
- class ProblemSubmittedEvent(SolverEvent):
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 ProblemSolvedEvent(SolverEvent):
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 ProblemFailedEvent(SolverEvent):
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(ProblemSubmittedEvent, self._handle_problem_submitted)
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 = ProblemState(problem=problem, max_iterations=self.max_iterations)
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(ProblemSolvedEvent, handle_solution_event)
201
- self.emitter.subscribe(ProblemFailedEvent, handle_solution_event)
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(ProblemSubmittedEvent(state=state))
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: ProblemSubmittedEvent):
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 : ProblemSubmittedEvent
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(ProblemFailedEvent(state=state))
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(ProblemSolvedEvent(state=state))
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(ProblemSolvedEvent(state=state))
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: ProblemState):
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 : ProblemState
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.problem}
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
 
@@ -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) -> str:
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
@@ -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)