mojentic 0.8.2__tar.gz → 0.8.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. {mojentic-0.8.2 → mojentic-0.8.4}/PKG-INFO +3 -3
  2. {mojentic-0.8.2 → mojentic-0.8.4}/README.md +2 -2
  3. {mojentic-0.8.2 → mojentic-0.8.4}/pyproject.toml +1 -1
  4. mojentic-0.8.4/src/_examples/tracer_qt_viewer.py +394 -0
  5. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/openai.py +12 -3
  6. mojentic-0.8.4/src/mojentic/llm/gateways/openai_spec.py +99 -0
  7. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/llm_broker.py +7 -1
  8. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/tracer/null_tracer.py +3 -0
  9. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/tracer/tracer_events.py +4 -0
  10. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/tracer/tracer_system.py +4 -0
  11. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic.egg-info/PKG-INFO +3 -3
  12. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic.egg-info/SOURCES.txt +2 -0
  13. {mojentic-0.8.2 → mojentic-0.8.4}/LICENSE.md +0 -0
  14. {mojentic-0.8.2 → mojentic-0.8.4}/setup.cfg +0 -0
  15. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/__init__.py +0 -0
  16. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/async_dispatcher_example.py +0 -0
  17. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/async_llm_example.py +0 -0
  18. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/broker_as_tool.py +0 -0
  19. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/broker_examples.py +0 -0
  20. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/broker_image_examples.py +0 -0
  21. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/characterize_ollama.py +0 -0
  22. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/characterize_openai.py +0 -0
  23. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/chat_session.py +0 -0
  24. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/chat_session_with_tool.py +0 -0
  25. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/coding_file_tool.py +0 -0
  26. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/current_datetime_tool_example.py +0 -0
  27. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/design_analysis.py +0 -0
  28. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/embeddings.py +0 -0
  29. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/ensures_files_exist.py +0 -0
  30. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/ephemeral_task_manager_example.py +0 -0
  31. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/fetch_openai_models.py +0 -0
  32. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/file_deduplication.py +0 -0
  33. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/file_tool.py +0 -0
  34. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/image_analysis.py +0 -0
  35. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/image_broker.py +0 -0
  36. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/image_broker_splat.py +0 -0
  37. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/iterative_solver.py +0 -0
  38. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/list_models.py +0 -0
  39. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/model_characterization.py +0 -0
  40. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/openai_gateway_enhanced_demo.py +0 -0
  41. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/oversized_embeddings.py +0 -0
  42. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/raw.py +0 -0
  43. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react/__init__.py +0 -0
  44. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react/agents/__init__.py +0 -0
  45. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react/agents/decisioning_agent.py +0 -0
  46. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react/agents/thinking_agent.py +0 -0
  47. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react/formatters.py +0 -0
  48. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react/models/__init__.py +0 -0
  49. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react/models/base.py +0 -0
  50. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react/models/events.py +0 -0
  51. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/react.py +0 -0
  52. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/recursive_agent.py +0 -0
  53. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/routed_send_response.py +0 -0
  54. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/simple_llm.py +0 -0
  55. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/simple_llm_repl.py +0 -0
  56. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/simple_structured.py +0 -0
  57. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/simple_tool.py +0 -0
  58. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/solver_chat_session.py +0 -0
  59. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/streaming.py +0 -0
  60. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/tell_user_example.py +0 -0
  61. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/tracer_demo.py +0 -0
  62. {mojentic-0.8.2 → mojentic-0.8.4}/src/_examples/working_memory.py +0 -0
  63. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/__init__.py +0 -0
  64. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/__init__.py +0 -0
  65. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/agent_broker.py +0 -0
  66. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/async_aggregator_agent.py +0 -0
  67. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/async_aggregator_agent_spec.py +0 -0
  68. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/async_llm_agent.py +0 -0
  69. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/async_llm_agent_spec.py +0 -0
  70. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/base_agent.py +0 -0
  71. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/base_async_agent.py +0 -0
  72. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/base_llm_agent.py +0 -0
  73. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/base_llm_agent_spec.py +0 -0
  74. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/correlation_aggregator_agent.py +0 -0
  75. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/iterative_problem_solver.py +0 -0
  76. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/output_agent.py +0 -0
  77. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/agents/simple_recursive_agent.py +0 -0
  78. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/async_dispatcher.py +0 -0
  79. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/async_dispatcher_spec.py +0 -0
  80. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/context/__init__.py +0 -0
  81. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/context/shared_working_memory.py +0 -0
  82. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/dispatcher.py +0 -0
  83. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/event.py +0 -0
  84. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/__init__.py +0 -0
  85. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/chat_session.py +0 -0
  86. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/chat_session_spec.py +0 -0
  87. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/__init__.py +0 -0
  88. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/anthropic.py +0 -0
  89. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/anthropic_messages_adapter.py +0 -0
  90. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/embeddings_gateway.py +0 -0
  91. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/file_gateway.py +0 -0
  92. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/llm_gateway.py +0 -0
  93. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/models.py +0 -0
  94. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/ollama.py +0 -0
  95. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/ollama_messages_adapter.py +0 -0
  96. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/ollama_messages_adapter_spec.py +0 -0
  97. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/openai_message_adapter_spec.py +0 -0
  98. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/openai_messages_adapter.py +0 -0
  99. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/openai_model_registry.py +0 -0
  100. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/openai_model_registry_spec.py +0 -0
  101. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/openai_temperature_handling_spec.py +0 -0
  102. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/gateways/tokenizer_gateway.py +0 -0
  103. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/llm_broker_spec.py +0 -0
  104. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/message_composers.py +0 -0
  105. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/message_composers_spec.py +0 -0
  106. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/registry/__init__.py +0 -0
  107. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/registry/llm_registry.py +0 -0
  108. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/registry/models.py +0 -0
  109. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/registry/populate_registry_from_ollama.py +0 -0
  110. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/__init__.py +0 -0
  111. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ask_user_tool.py +0 -0
  112. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/current_datetime.py +0 -0
  113. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/date_resolver.py +0 -0
  114. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/date_resolver_spec.py +0 -0
  115. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/__init__.py +0 -0
  116. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/append_task_tool.py +0 -0
  117. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/append_task_tool_spec.py +0 -0
  118. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/clear_tasks_tool.py +0 -0
  119. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/clear_tasks_tool_spec.py +0 -0
  120. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/complete_task_tool.py +0 -0
  121. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/complete_task_tool_spec.py +0 -0
  122. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list.py +0 -0
  123. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list_spec.py +0 -0
  124. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool.py +0 -0
  125. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool_spec.py +0 -0
  126. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/list_tasks_tool.py +0 -0
  127. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/list_tasks_tool_spec.py +0 -0
  128. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool.py +0 -0
  129. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool_spec.py +0 -0
  130. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/start_task_tool.py +0 -0
  131. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/ephemeral_task_manager/start_task_tool_spec.py +0 -0
  132. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/file_manager.py +0 -0
  133. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/file_manager_spec.py +0 -0
  134. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/llm_tool.py +0 -0
  135. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/llm_tool_spec.py +0 -0
  136. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/organic_web_search.py +0 -0
  137. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/tell_user_tool.py +0 -0
  138. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/tool_wrapper.py +0 -0
  139. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/llm/tools/tool_wrapper_spec.py +0 -0
  140. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/router.py +0 -0
  141. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/router_spec.py +0 -0
  142. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/tracer/__init__.py +0 -0
  143. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/tracer/event_store.py +0 -0
  144. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/tracer/event_store_spec.py +0 -0
  145. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/tracer/tracer_events_spec.py +0 -0
  146. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/tracer/tracer_system_spec.py +0 -0
  147. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/utils/__init__.py +0 -0
  148. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic/utils/formatting.py +0 -0
  149. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic.egg-info/dependency_links.txt +0 -0
  150. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic.egg-info/requires.txt +0 -0
  151. {mojentic-0.8.2 → mojentic-0.8.4}/src/mojentic.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mojentic
3
- Version: 0.8.2
3
+ Version: 0.8.4
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
@@ -170,9 +170,9 @@ pip install -e ".[dev]"
170
170
  pytest
171
171
  ```
172
172
 
173
- ## ⚠️ Project Status
173
+ ## Project Status
174
174
 
175
- While the Layer 1 API (LLMBroker, LLMGateway, tool use) has stabilized, the Layer 2 agentic capabilities are under heavy development and will likely change significantly.
175
+ The agentic aspects of this framework are in the highest state of flux. The first layer has stabilized, as have the simpler parts of the second layer, and we're working on the stability of the asynchronous pubsub architecture. We expect Python 3.14 will be the real enabler for the async aspects of the second layer.
176
176
 
177
177
  ## 📄 License
178
178
 
@@ -131,9 +131,9 @@ pip install -e ".[dev]"
131
131
  pytest
132
132
  ```
133
133
 
134
- ## ⚠️ Project Status
134
+ ## Project Status
135
135
 
136
- While the Layer 1 API (LLMBroker, LLMGateway, tool use) has stabilized, the Layer 2 agentic capabilities are under heavy development and will likely change significantly.
136
+ The agentic aspects of this framework are in the highest state of flux. The first layer has stabilized, as have the simpler parts of the second layer, and we're working on the stability of the asynchronous pubsub architecture. We expect Python 3.14 will be the real enabler for the async aspects of the second layer.
137
137
 
138
138
  ## 📄 License
139
139
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mojentic"
3
- version = "0.8.2"
3
+ version = "0.8.4"
4
4
  authors = [
5
5
  { name = "Stacey Vetzal", email = "stacey@vetzal.com" },
6
6
  ]
@@ -0,0 +1,394 @@
1
+ """
2
+ Real-time Tracer Event Viewer with Qt GUI.
3
+
4
+ This example demonstrates how to use the tracer system with a Qt GUI that displays
5
+ tracer events in real-time as they occur. Users can click on events to see detailed
6
+ information.
7
+
8
+ Requirements:
9
+ pip install PyQt6
10
+
11
+ Usage:
12
+ python tracer_qt_viewer.py
13
+ """
14
+ import sys
15
+ import uuid
16
+ from datetime import datetime
17
+ from typing import Optional
18
+
19
+ try:
20
+ from PyQt6.QtWidgets import (
21
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
22
+ QTableWidget, QTableWidgetItem, QTextEdit, QPushButton, QLabel,
23
+ QHeaderView, QSplitter
24
+ )
25
+ from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QObject, QThread
26
+ from PyQt6.QtGui import QColor, QFont
27
+ except ImportError:
28
+ print("Error: PyQt6 is required for this example.")
29
+ print("Install it with: pip install PyQt6")
30
+ sys.exit(1)
31
+
32
+ from mojentic.tracer import TracerSystem, EventStore
33
+ from mojentic.tracer.tracer_events import (
34
+ TracerEvent, LLMCallTracerEvent, LLMResponseTracerEvent,
35
+ ToolCallTracerEvent, AgentInteractionTracerEvent
36
+ )
37
+ from mojentic.llm import LLMBroker
38
+ from mojentic.llm.tools.date_resolver import ResolveDateTool
39
+ from mojentic.llm.gateways.models import LLMMessage, MessageRole
40
+
41
+
42
+ class EventSignaler(QObject):
43
+ """Qt signal emitter for tracer events (needed for thread safety)."""
44
+ event_occurred = pyqtSignal(object)
45
+
46
+
47
+ class LLMWorker(QThread):
48
+ """Worker thread for running LLM queries without blocking the UI."""
49
+
50
+ finished = pyqtSignal(str) # Emits response when complete
51
+ error = pyqtSignal(str) # Emits error message if something goes wrong
52
+
53
+ def __init__(self, tracer: TracerSystem):
54
+ super().__init__()
55
+ self.tracer = tracer
56
+
57
+ def run(self):
58
+ """Execute the LLM query in a background thread."""
59
+ try:
60
+ # Create LLM broker with our tracer
61
+ llm_broker = LLMBroker(model="qwen3:32b", tracer=self.tracer)
62
+
63
+ # Create a date resolver tool
64
+ date_tool = ResolveDateTool(tracer=self.tracer)
65
+
66
+ # Generate a correlation ID for this request
67
+ correlation_id = str(uuid.uuid4())
68
+
69
+ # Create a test query
70
+ messages = [
71
+ LLMMessage(role=MessageRole.User, content="What is the date next Friday?")
72
+ ]
73
+
74
+ # Execute the query (this will generate tracer events)
75
+ response = llm_broker.generate(
76
+ messages,
77
+ tools=[date_tool],
78
+ correlation_id=correlation_id
79
+ )
80
+
81
+ self.finished.emit(response)
82
+
83
+ except Exception as e:
84
+ self.error.emit(str(e))
85
+
86
+
87
+ class TracerViewer(QMainWindow):
88
+ """Qt window that displays tracer events in real-time."""
89
+
90
+ def __init__(self):
91
+ super().__init__()
92
+ self.events = []
93
+ self.event_signaler = EventSignaler()
94
+ self.event_signaler.event_occurred.connect(self.add_event_to_table)
95
+ self.worker = None # Track the worker thread
96
+
97
+ self.setWindowTitle("Mojentic Tracer - Real-time Event Viewer")
98
+ self.setGeometry(100, 100, 1200, 700)
99
+
100
+ self._setup_ui()
101
+ self._setup_tracer()
102
+
103
+ def _setup_ui(self):
104
+ """Setup the user interface."""
105
+ central_widget = QWidget()
106
+ self.setCentralWidget(central_widget)
107
+ main_layout = QVBoxLayout(central_widget)
108
+
109
+ # Title and controls
110
+ header_layout = QHBoxLayout()
111
+ title_label = QLabel("Real-time Tracer Events")
112
+ title_font = QFont()
113
+ title_font.setPointSize(14)
114
+ title_font.setBold(True)
115
+ title_label.setFont(title_font)
116
+ header_layout.addWidget(title_label)
117
+
118
+ header_layout.addStretch()
119
+
120
+ self.clear_button = QPushButton("Clear Events")
121
+ self.clear_button.clicked.connect(self.clear_events)
122
+ header_layout.addWidget(self.clear_button)
123
+
124
+ self.test_button = QPushButton("Run Test Query")
125
+ self.test_button.clicked.connect(self.run_test_query)
126
+ header_layout.addWidget(self.test_button)
127
+
128
+ main_layout.addLayout(header_layout)
129
+
130
+ # Event counter
131
+ self.event_count_label = QLabel("Events: 0")
132
+ main_layout.addWidget(self.event_count_label)
133
+
134
+ # Splitter for table and details
135
+ splitter = QSplitter(Qt.Orientation.Vertical)
136
+
137
+ # Events table
138
+ self.events_table = QTableWidget()
139
+ self.events_table.setColumnCount(5)
140
+ self.events_table.setHorizontalHeaderLabels([
141
+ "Time", "Type", "Correlation ID", "Summary", "Duration (ms)"
142
+ ])
143
+ self.events_table.horizontalHeader().setSectionResizeMode(
144
+ 3, QHeaderView.ResizeMode.Stretch
145
+ )
146
+ self.events_table.setSelectionBehavior(
147
+ QTableWidget.SelectionBehavior.SelectRows
148
+ )
149
+ self.events_table.setEditTriggers(
150
+ QTableWidget.EditTrigger.NoEditTriggers
151
+ )
152
+ self.events_table.itemSelectionChanged.connect(self.show_event_details)
153
+ splitter.addWidget(self.events_table)
154
+
155
+ # Event details panel
156
+ details_widget = QWidget()
157
+ details_layout = QVBoxLayout(details_widget)
158
+ details_label = QLabel("Event Details (click an event to see details)")
159
+ details_label.setFont(QFont("", 10, QFont.Weight.Bold))
160
+ details_layout.addWidget(details_label)
161
+
162
+ self.details_text = QTextEdit()
163
+ self.details_text.setReadOnly(True)
164
+ self.details_text.setFont(QFont("Courier", 9))
165
+ details_layout.addWidget(self.details_text)
166
+
167
+ splitter.addWidget(details_widget)
168
+ splitter.setSizes([400, 300])
169
+
170
+ main_layout.addWidget(splitter)
171
+
172
+ # Status bar
173
+ self.statusBar().showMessage("Ready. Waiting for tracer events...")
174
+
175
+ def _setup_tracer(self):
176
+ """Setup the tracer system with callback."""
177
+ def on_event_stored(event):
178
+ """Callback when an event is stored."""
179
+ if isinstance(event, TracerEvent):
180
+ self.event_signaler.event_occurred.emit(event)
181
+
182
+ event_store = EventStore(on_store_callback=on_event_stored)
183
+ self.tracer = TracerSystem(event_store=event_store)
184
+
185
+ def add_event_to_table(self, event: TracerEvent):
186
+ """Add a new event to the table."""
187
+ self.events.append(event)
188
+
189
+ row = self.events_table.rowCount()
190
+ self.events_table.insertRow(row)
191
+
192
+ # Time
193
+ time_str = datetime.fromtimestamp(event.timestamp).strftime("%H:%M:%S.%f")[:-3]
194
+ time_item = QTableWidgetItem(time_str)
195
+ self.events_table.setItem(row, 0, time_item)
196
+
197
+ # Type
198
+ event_type = type(event).__name__.replace("TracerEvent", "")
199
+ type_item = QTableWidgetItem(event_type)
200
+
201
+ # Color code by type
202
+ if isinstance(event, LLMCallTracerEvent):
203
+ type_item.setBackground(QColor(107, 182, 96, 50)) # Mojility green
204
+ elif isinstance(event, LLMResponseTracerEvent):
205
+ type_item.setBackground(QColor(107, 182, 96, 100))
206
+ elif isinstance(event, ToolCallTracerEvent):
207
+ type_item.setBackground(QColor(102, 103, 103, 50)) # Mojility grey
208
+ elif isinstance(event, AgentInteractionTracerEvent):
209
+ type_item.setBackground(QColor(100, 149, 237, 50)) # Blue
210
+
211
+ self.events_table.setItem(row, 1, type_item)
212
+
213
+ # Correlation ID (shortened)
214
+ corr_id_short = event.correlation_id[:8] if event.correlation_id else "N/A"
215
+ corr_item = QTableWidgetItem(corr_id_short)
216
+ corr_item.setToolTip(event.correlation_id)
217
+ self.events_table.setItem(row, 2, corr_item)
218
+
219
+ # Summary
220
+ summary = self._get_event_summary(event)
221
+ summary_item = QTableWidgetItem(summary)
222
+ self.events_table.setItem(row, 3, summary_item)
223
+
224
+ # Duration
225
+ duration = ""
226
+ if isinstance(event, (LLMResponseTracerEvent, ToolCallTracerEvent)) and event.call_duration_ms:
227
+ duration = f"{event.call_duration_ms:.0f}"
228
+ duration_item = QTableWidgetItem(duration)
229
+ self.events_table.setItem(row, 4, duration_item)
230
+
231
+ # Scroll to bottom and update counter
232
+ self.events_table.scrollToBottom()
233
+ self.event_count_label.setText(f"Events: {len(self.events)}")
234
+ self.statusBar().showMessage(f"New event: {event_type}")
235
+
236
+ def _get_event_summary(self, event: TracerEvent) -> str:
237
+ """Get a brief summary of the event."""
238
+ if isinstance(event, LLMCallTracerEvent):
239
+ return f"Model: {event.model}, Messages: {len(event.messages)}"
240
+ elif isinstance(event, LLMResponseTracerEvent):
241
+ content_preview = event.content[:50] + "..." if len(event.content) > 50 else event.content
242
+ return f"Model: {event.model}, Response: {content_preview}"
243
+ elif isinstance(event, ToolCallTracerEvent):
244
+ return f"Tool: {event.tool_name}, Caller: {event.caller or 'N/A'}"
245
+ elif isinstance(event, AgentInteractionTracerEvent):
246
+ return f"From: {event.from_agent} → To: {event.to_agent}"
247
+ return "Unknown event type"
248
+
249
+ def show_event_details(self):
250
+ """Show detailed information about the selected event."""
251
+ selected_rows = self.events_table.selectedIndexes()
252
+ if not selected_rows:
253
+ return
254
+
255
+ row = selected_rows[0].row()
256
+ if row >= len(self.events):
257
+ return
258
+
259
+ event = self.events[row]
260
+ details = self._format_event_details(event)
261
+ self.details_text.setPlainText(details)
262
+
263
+ def _format_event_details(self, event: TracerEvent) -> str:
264
+ """Format detailed event information."""
265
+ details = []
266
+ details.append(f"Event Type: {type(event).__name__}")
267
+ details.append(f"Timestamp: {datetime.fromtimestamp(event.timestamp)}")
268
+ details.append(f"Correlation ID: {event.correlation_id}")
269
+ details.append(f"Source: {event.source}")
270
+ details.append("")
271
+
272
+ if isinstance(event, LLMCallTracerEvent):
273
+ details.append("=== LLM Call Details ===")
274
+ details.append(f"Model: {event.model}")
275
+ details.append(f"Temperature: {event.temperature}")
276
+ details.append(f"Number of Messages: {len(event.messages)}")
277
+ details.append("")
278
+ details.append("Messages:")
279
+ for i, msg in enumerate(event.messages, 1):
280
+ details.append(f" {i}. Role: {msg.get('role', 'N/A')}")
281
+ content = msg.get('content', 'N/A')
282
+ if len(str(content)) > 200:
283
+ content = str(content)[:200] + "..."
284
+ details.append(f" Content: {content}")
285
+ if event.tools:
286
+ details.append("")
287
+ details.append(f"Available Tools: {[t.get('name') for t in event.tools]}")
288
+
289
+ elif isinstance(event, LLMResponseTracerEvent):
290
+ details.append("=== LLM Response Details ===")
291
+ details.append(f"Model: {event.model}")
292
+ details.append(f"Call Duration: {event.call_duration_ms:.2f} ms" if event.call_duration_ms else "N/A")
293
+ details.append("")
294
+ details.append("Response Content:")
295
+ details.append(event.content)
296
+ if event.tool_calls:
297
+ details.append("")
298
+ details.append(f"Tool Calls Made: {len(event.tool_calls)}")
299
+ for i, tc in enumerate(event.tool_calls, 1):
300
+ details.append(f" {i}. {tc}")
301
+
302
+ elif isinstance(event, ToolCallTracerEvent):
303
+ details.append("=== Tool Call Details ===")
304
+ details.append(f"Tool Name: {event.tool_name}")
305
+ details.append(f"Caller: {event.caller or 'N/A'}")
306
+ details.append(f"Call Duration: {event.call_duration_ms:.2f} ms" if event.call_duration_ms else "Duration: N/A")
307
+ details.append("")
308
+ details.append("Arguments:")
309
+ details.append(f" {event.arguments}")
310
+ details.append("")
311
+ details.append("Result:")
312
+ details.append(f" {event.result}")
313
+
314
+ elif isinstance(event, AgentInteractionTracerEvent):
315
+ details.append("=== Agent Interaction Details ===")
316
+ details.append(f"From Agent: {event.from_agent}")
317
+ details.append(f"To Agent: {event.to_agent}")
318
+ details.append(f"Event Type: {event.event_type}")
319
+ if event.event_id:
320
+ details.append(f"Event ID: {event.event_id}")
321
+
322
+ return "\n".join(details)
323
+
324
+ def clear_events(self):
325
+ """Clear all events from the display."""
326
+ self.events.clear()
327
+ self.events_table.setRowCount(0)
328
+ self.details_text.clear()
329
+ self.event_count_label.setText("Events: 0")
330
+ self.tracer.clear()
331
+ self.statusBar().showMessage("Events cleared")
332
+
333
+ def run_test_query(self):
334
+ """Run a test query to demonstrate the tracer."""
335
+ # Don't start a new query if one is already running
336
+ if self.worker and self.worker.isRunning():
337
+ self.statusBar().showMessage("Query already running, please wait...")
338
+ return
339
+
340
+ self.statusBar().showMessage("Running test query in background...")
341
+ self.test_button.setEnabled(False)
342
+
343
+ # Create and configure worker thread
344
+ self.worker = LLMWorker(self.tracer)
345
+ self.worker.finished.connect(self._on_query_finished)
346
+ self.worker.error.connect(self._on_query_error)
347
+
348
+ # Start the worker thread
349
+ self.worker.start()
350
+
351
+ def _on_query_finished(self, response: str):
352
+ """Handle successful query completion."""
353
+ self.statusBar().showMessage(f"Test query completed. Response: {response[:50]}...")
354
+ self.test_button.setEnabled(True)
355
+
356
+ def _on_query_error(self, error_msg: str):
357
+ """Handle query error."""
358
+ self.statusBar().showMessage(f"Error during test query: {error_msg}")
359
+ self.test_button.setEnabled(True)
360
+
361
+ def closeEvent(self, event):
362
+ """Clean up worker thread when window closes."""
363
+ if self.worker and self.worker.isRunning():
364
+ self.worker.quit()
365
+ self.worker.wait()
366
+ event.accept()
367
+
368
+
369
+ def main():
370
+ """Main entry point for the tracer viewer application."""
371
+ app = QApplication(sys.argv)
372
+
373
+ # Set application style
374
+ app.setStyle('Fusion')
375
+
376
+ window = TracerViewer()
377
+ window.show()
378
+
379
+ print("\n" + "="*80)
380
+ print("Mojentic Tracer - Real-time Event Viewer")
381
+ print("="*80)
382
+ print("\nThe Qt window is now open. You can:")
383
+ print(" 1. Click 'Run Test Query' to generate sample tracer events")
384
+ print(" 2. Click on any event row to see detailed information")
385
+ print(" 3. Click 'Clear Events' to reset the display")
386
+ print("\nThe viewer will show events in real-time as they occur.")
387
+ print("Close the window to exit.")
388
+ print("="*80 + "\n")
389
+
390
+ sys.exit(app.exec())
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import os
2
3
  from itertools import islice
3
4
  from typing import Type, List, Iterable, Optional
4
5
 
@@ -23,11 +24,19 @@ class OpenAIGateway(LLMGateway):
23
24
 
24
25
  Parameters
25
26
  ----------
26
- api_key : str
27
- The OpenAI API key to use.
27
+ api_key : str, optional
28
+ The OpenAI API key to use. If not provided, defaults to the value of the
29
+ OPENAI_API_KEY environment variable.
30
+ base_url : str, optional
31
+ The base URL for the OpenAI API. If not provided, defaults to the value of the
32
+ OPENAI_API_ENDPOINT environment variable, or None if not set.
28
33
  """
29
34
 
30
- def __init__(self, api_key: str, base_url: Optional[str] = None):
35
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
36
+ if api_key is None:
37
+ api_key = os.getenv("OPENAI_API_KEY")
38
+ if base_url is None:
39
+ base_url = os.getenv("OPENAI_API_ENDPOINT")
31
40
  self.client = OpenAI(api_key=api_key, base_url=base_url)
32
41
  self.model_registry = get_model_registry()
33
42
 
@@ -0,0 +1,99 @@
1
+ import os
2
+ from unittest.mock import patch
3
+
4
+ from mojentic.llm.gateways.openai import OpenAIGateway
5
+
6
+
7
+ class DescribeOpenAIGateway:
8
+ """
9
+ Unit tests for the OpenAI gateway
10
+ """
11
+
12
+ class DescribeInitialization:
13
+ """
14
+ Tests for OpenAI gateway initialization
15
+ """
16
+
17
+ def should_initialize_with_api_key(self, mocker):
18
+ api_key = "test-api-key"
19
+ mock_openai = mocker.patch('mojentic.llm.gateways.openai.OpenAI')
20
+
21
+ gateway = OpenAIGateway(api_key=api_key)
22
+
23
+ mock_openai.assert_called_once_with(api_key=api_key, base_url=None)
24
+ assert gateway.client is not None
25
+
26
+ def should_initialize_with_api_key_and_base_url(self, mocker):
27
+ api_key = "test-api-key"
28
+ base_url = "https://custom.openai.com"
29
+ mock_openai = mocker.patch('mojentic.llm.gateways.openai.OpenAI')
30
+
31
+ gateway = OpenAIGateway(api_key=api_key, base_url=base_url)
32
+
33
+ mock_openai.assert_called_once_with(api_key=api_key, base_url=base_url)
34
+ assert gateway.client is not None
35
+
36
+ def should_read_api_key_from_environment_variable(self, mocker):
37
+ api_key = "test-api-key-from-env"
38
+ mock_openai = mocker.patch('mojentic.llm.gateways.openai.OpenAI')
39
+
40
+ with patch.dict(os.environ, {'OPENAI_API_KEY': api_key}):
41
+ gateway = OpenAIGateway()
42
+
43
+ mock_openai.assert_called_once_with(api_key=api_key, base_url=None)
44
+ assert gateway.client is not None
45
+
46
+ def should_read_base_url_from_environment_variable(self, mocker):
47
+ api_key = "test-api-key"
48
+ endpoint = "https://corporate.openai.com"
49
+ mock_openai = mocker.patch('mojentic.llm.gateways.openai.OpenAI')
50
+
51
+ with patch.dict(os.environ, {'OPENAI_API_ENDPOINT': endpoint}):
52
+ gateway = OpenAIGateway(api_key=api_key)
53
+
54
+ mock_openai.assert_called_once_with(api_key=api_key, base_url=endpoint)
55
+ assert gateway.client is not None
56
+
57
+ def should_read_both_from_environment_variables(self, mocker):
58
+ api_key = "test-api-key-from-env"
59
+ endpoint = "https://corporate.openai.com"
60
+ mock_openai = mocker.patch('mojentic.llm.gateways.openai.OpenAI')
61
+
62
+ with patch.dict(os.environ, {'OPENAI_API_KEY': api_key, 'OPENAI_API_ENDPOINT': endpoint}):
63
+ gateway = OpenAIGateway()
64
+
65
+ mock_openai.assert_called_once_with(api_key=api_key, base_url=endpoint)
66
+ assert gateway.client is not None
67
+
68
+ def should_prefer_explicit_api_key_over_environment_variable(self, mocker):
69
+ api_key_env = "test-api-key-from-env"
70
+ api_key_explicit = "test-api-key-explicit"
71
+ mock_openai = mocker.patch('mojentic.llm.gateways.openai.OpenAI')
72
+
73
+ with patch.dict(os.environ, {'OPENAI_API_KEY': api_key_env}):
74
+ gateway = OpenAIGateway(api_key=api_key_explicit)
75
+
76
+ mock_openai.assert_called_once_with(api_key=api_key_explicit, base_url=None)
77
+ assert gateway.client is not None
78
+
79
+ def should_prefer_explicit_base_url_over_environment_variable(self, mocker):
80
+ api_key = "test-api-key"
81
+ endpoint_env = "https://corporate.openai.com"
82
+ endpoint_explicit = "https://explicit.openai.com"
83
+ mock_openai = mocker.patch('mojentic.llm.gateways.openai.OpenAI')
84
+
85
+ with patch.dict(os.environ, {'OPENAI_API_ENDPOINT': endpoint_env}):
86
+ gateway = OpenAIGateway(api_key=api_key, base_url=endpoint_explicit)
87
+
88
+ mock_openai.assert_called_once_with(api_key=api_key, base_url=endpoint_explicit)
89
+ assert gateway.client is not None
90
+
91
+ def should_use_none_when_no_endpoint_specified(self, mocker):
92
+ api_key = "test-api-key"
93
+ mock_openai = mocker.patch('mojentic.llm.gateways.openai.OpenAI')
94
+
95
+ with patch.dict(os.environ, {}, clear=True):
96
+ gateway = OpenAIGateway(api_key=api_key)
97
+
98
+ mock_openai.assert_called_once_with(api_key=api_key, base_url=None)
99
+ assert gateway.client is not None
@@ -43,7 +43,7 @@ class LLMBroker():
43
43
  tokenizer
44
44
  The gateway to use for tokenization. This is used to log approximate token counts for
45
45
  the LLM calls. If
46
- None, `mxbai-embed-large` is used on a local Ollama server.
46
+ None, tiktoken's `cl100k_base` tokenizer is used.
47
47
  tracer
48
48
  Optional tracer system to record LLM calls and responses.
49
49
  """
@@ -146,15 +146,21 @@ class LLMBroker():
146
146
  # Get the arguments before calling the tool
147
147
  tool_arguments = tool_call.arguments
148
148
 
149
+ # Measure tool execution time
150
+ tool_start_time = time.time()
151
+
149
152
  # Call the tool
150
153
  output = tool.run(**tool_call.arguments)
151
154
 
155
+ tool_duration_ms = (time.time() - tool_start_time) * 1000
156
+
152
157
  # Record tool call in tracer
153
158
  self.tracer.record_tool_call(
154
159
  tool_call.name,
155
160
  tool_arguments,
156
161
  output,
157
162
  caller="LLMBroker",
163
+ call_duration_ms=tool_duration_ms,
158
164
  source=type(self),
159
165
  correlation_id=correlation_id
160
166
  )
@@ -95,6 +95,7 @@ class NullTracer:
95
95
  arguments: Dict[str, Any],
96
96
  result: Any,
97
97
  caller: Optional[str] = None,
98
+ call_duration_ms: Optional[float] = None,
98
99
  source: Any = None,
99
100
  correlation_id: str = None) -> None:
100
101
  """
@@ -110,6 +111,8 @@ class NullTracer:
110
111
  The result returned by the tool.
111
112
  caller : str, optional
112
113
  The name of the agent or component calling the tool.
114
+ call_duration_ms : float, optional
115
+ The duration of the tool call in milliseconds.
113
116
  source : Any, optional
114
117
  The source of the event.
115
118
  correlation_id : str, optional
@@ -97,6 +97,7 @@ class ToolCallTracerEvent(TracerEvent):
97
97
  arguments: Dict[str, Any] = Field(..., description="Arguments provided to the tool")
98
98
  result: Any = Field(..., description="Result returned by the tool")
99
99
  caller: Optional[str] = Field(None, description="Name of the agent or component that called the tool")
100
+ call_duration_ms: Optional[float] = Field(None, description="Duration of the tool call in milliseconds")
100
101
 
101
102
  def printable_summary(self) -> str:
102
103
  """Return a formatted summary of the tool call event."""
@@ -114,6 +115,9 @@ class ToolCallTracerEvent(TracerEvent):
114
115
  if self.caller:
115
116
  summary += f"\n Caller: {self.caller}"
116
117
 
118
+ if self.call_duration_ms is not None:
119
+ summary += f"\n Duration: {self.call_duration_ms:.2f}ms"
120
+
117
121
  return summary
118
122
 
119
123
 
@@ -141,6 +141,7 @@ class TracerSystem:
141
141
  arguments: Dict[str, Any],
142
142
  result: Any,
143
143
  caller: Optional[str] = None,
144
+ call_duration_ms: Optional[float] = None,
144
145
  source: Any = None,
145
146
  correlation_id: str = None) -> None:
146
147
  """
@@ -156,6 +157,8 @@ class TracerSystem:
156
157
  The result returned by the tool.
157
158
  caller : str, optional
158
159
  The name of the agent or component calling the tool.
160
+ call_duration_ms : float, optional
161
+ The duration of the tool call in milliseconds.
159
162
  source : Any, optional
160
163
  The source of the event. If None, the TracerSystem class will be used.
161
164
  correlation_id : str, required
@@ -171,6 +174,7 @@ class TracerSystem:
171
174
  arguments=arguments,
172
175
  result=result,
173
176
  caller=caller,
177
+ call_duration_ms=call_duration_ms,
174
178
  correlation_id=correlation_id
175
179
  )
176
180
  self.event_store.store(event)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mojentic
3
- Version: 0.8.2
3
+ Version: 0.8.4
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
@@ -170,9 +170,9 @@ pip install -e ".[dev]"
170
170
  pytest
171
171
  ```
172
172
 
173
- ## ⚠️ Project Status
173
+ ## Project Status
174
174
 
175
- While the Layer 1 API (LLMBroker, LLMGateway, tool use) has stabilized, the Layer 2 agentic capabilities are under heavy development and will likely change significantly.
175
+ The agentic aspects of this framework are in the highest state of flux. The first layer has stabilized, as have the simpler parts of the second layer, and we're working on the stability of the asynchronous pubsub architecture. We expect Python 3.14 will be the real enabler for the async aspects of the second layer.
176
176
 
177
177
  ## 📄 License
178
178