mojentic 0.9.0__py3-none-any.whl → 1.0.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.
Files changed (100) hide show
  1. _examples/async_dispatcher_example.py +12 -4
  2. _examples/async_llm_example.py +1 -2
  3. _examples/broker_as_tool.py +39 -14
  4. _examples/broker_examples.py +4 -6
  5. _examples/characterize_ollama.py +1 -1
  6. _examples/characterize_openai.py +1 -1
  7. _examples/chat_session.py +1 -1
  8. _examples/chat_session_with_tool.py +1 -1
  9. _examples/coding_file_tool.py +1 -3
  10. _examples/current_datetime_tool_example.py +1 -1
  11. _examples/embeddings.py +1 -1
  12. _examples/ephemeral_task_manager_example.py +13 -9
  13. _examples/fetch_openai_models.py +10 -3
  14. _examples/file_deduplication.py +6 -6
  15. _examples/image_analysis.py +2 -3
  16. _examples/image_broker.py +1 -1
  17. _examples/image_broker_splat.py +1 -1
  18. _examples/iterative_solver.py +2 -2
  19. _examples/model_characterization.py +2 -0
  20. _examples/openai_gateway_enhanced_demo.py +15 -5
  21. _examples/raw.py +1 -1
  22. _examples/react/agents/decisioning_agent.py +173 -15
  23. _examples/react/agents/summarization_agent.py +89 -0
  24. _examples/react/agents/thinking_agent.py +84 -14
  25. _examples/react/agents/tool_call_agent.py +83 -0
  26. _examples/react/formatters.py +38 -4
  27. _examples/react/models/base.py +60 -11
  28. _examples/react/models/events.py +76 -8
  29. _examples/react.py +71 -21
  30. _examples/recursive_agent.py +1 -1
  31. _examples/solver_chat_session.py +1 -7
  32. _examples/streaming.py +7 -5
  33. _examples/tell_user_example.py +3 -3
  34. _examples/tracer_demo.py +15 -17
  35. _examples/tracer_qt_viewer.py +49 -46
  36. mojentic/__init__.py +3 -3
  37. mojentic/agents/__init__.py +26 -8
  38. mojentic/agents/{agent_broker.py → agent_event_adapter.py} +3 -3
  39. mojentic/agents/async_aggregator_agent_spec.py +32 -33
  40. mojentic/agents/async_llm_agent.py +9 -5
  41. mojentic/agents/async_llm_agent_spec.py +21 -22
  42. mojentic/agents/base_async_agent.py +2 -2
  43. mojentic/agents/base_llm_agent.py +6 -2
  44. mojentic/agents/iterative_problem_solver.py +11 -5
  45. mojentic/agents/simple_recursive_agent.py +11 -10
  46. mojentic/agents/simple_recursive_agent_spec.py +423 -0
  47. mojentic/async_dispatcher.py +0 -1
  48. mojentic/async_dispatcher_spec.py +1 -1
  49. mojentic/context/__init__.py +0 -2
  50. mojentic/dispatcher.py +7 -8
  51. mojentic/llm/__init__.py +5 -5
  52. mojentic/llm/chat_session.py +24 -1
  53. mojentic/llm/chat_session_spec.py +40 -0
  54. mojentic/llm/gateways/__init__.py +19 -18
  55. mojentic/llm/gateways/anthropic.py +1 -0
  56. mojentic/llm/gateways/anthropic_messages_adapter.py +0 -1
  57. mojentic/llm/gateways/llm_gateway.py +1 -1
  58. mojentic/llm/gateways/ollama.py +2 -0
  59. mojentic/llm/gateways/openai.py +62 -58
  60. mojentic/llm/gateways/openai_message_adapter_spec.py +3 -3
  61. mojentic/llm/gateways/openai_model_registry.py +7 -6
  62. mojentic/llm/gateways/openai_model_registry_spec.py +1 -2
  63. mojentic/llm/gateways/openai_temperature_handling_spec.py +2 -2
  64. mojentic/llm/llm_broker.py +7 -5
  65. mojentic/llm/llm_broker_spec.py +7 -2
  66. mojentic/llm/message_composers.py +6 -3
  67. mojentic/llm/message_composers_spec.py +5 -1
  68. mojentic/llm/registry/__init__.py +0 -3
  69. mojentic/llm/tools/__init__.py +0 -9
  70. mojentic/llm/tools/ask_user_tool.py +11 -5
  71. mojentic/llm/tools/current_datetime.py +9 -6
  72. mojentic/llm/tools/date_resolver.py +10 -4
  73. mojentic/llm/tools/date_resolver_spec.py +0 -1
  74. mojentic/llm/tools/ephemeral_task_manager/append_task_tool.py +4 -1
  75. mojentic/llm/tools/ephemeral_task_manager/ephemeral_task_list.py +1 -1
  76. mojentic/llm/tools/ephemeral_task_manager/insert_task_after_tool.py +4 -1
  77. mojentic/llm/tools/ephemeral_task_manager/prepend_task_tool.py +5 -2
  78. mojentic/llm/tools/file_manager.py +131 -28
  79. mojentic/llm/tools/file_manager_spec.py +0 -3
  80. mojentic/llm/tools/llm_tool.py +1 -1
  81. mojentic/llm/tools/llm_tool_spec.py +0 -2
  82. mojentic/llm/tools/organic_web_search.py +4 -2
  83. mojentic/llm/tools/tell_user_tool.py +6 -2
  84. mojentic/llm/tools/tool_wrapper.py +2 -2
  85. mojentic/tracer/__init__.py +1 -10
  86. mojentic/tracer/event_store.py +7 -8
  87. mojentic/tracer/event_store_spec.py +1 -2
  88. mojentic/tracer/null_tracer.py +37 -43
  89. mojentic/tracer/tracer_events.py +8 -2
  90. mojentic/tracer/tracer_events_spec.py +6 -7
  91. mojentic/tracer/tracer_system.py +37 -36
  92. mojentic/tracer/tracer_system_spec.py +21 -6
  93. mojentic/utils/__init__.py +1 -1
  94. mojentic/utils/formatting.py +1 -0
  95. {mojentic-0.9.0.dist-info → mojentic-1.0.1.dist-info}/METADATA +47 -29
  96. mojentic-1.0.1.dist-info/RECORD +149 -0
  97. {mojentic-0.9.0.dist-info → mojentic-1.0.1.dist-info}/WHEEL +1 -1
  98. mojentic-0.9.0.dist-info/RECORD +0 -146
  99. {mojentic-0.9.0.dist-info → mojentic-1.0.1.dist-info}/licenses/LICENSE.md +0 -0
  100. {mojentic-0.9.0.dist-info → mojentic-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from collections import deque
4
- from typing import Optional, Type
5
4
  from uuid import uuid4
6
5
 
7
6
  import structlog
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import pytest
3
3
  import pytest_asyncio
4
- from unittest.mock import AsyncMock, MagicMock
4
+ from unittest.mock import MagicMock
5
5
 
6
6
  from mojentic.async_dispatcher import AsyncDispatcher
7
7
  from mojentic.event import Event, TerminateEvent
@@ -1,5 +1,3 @@
1
1
  """
2
2
  Mojentic context module for managing shared working memory and context.
3
3
  """
4
-
5
- from .shared_working_memory import SharedWorkingMemory
mojentic/dispatcher.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import logging
2
2
  import threading
3
3
  from time import sleep
4
- from typing import Optional, Type
5
4
  from uuid import uuid4
6
5
 
7
6
  import structlog
@@ -18,7 +17,7 @@ class Dispatcher:
18
17
  self.event_queue = []
19
18
  self._stop_event = threading.Event()
20
19
  self._thread = threading.Thread(target=self._dispatch_events)
21
-
20
+
22
21
  # Use null_tracer if no tracer is provided
23
22
  from mojentic.tracer import null_tracer
24
23
  self.tracer = tracer or null_tracer
@@ -49,16 +48,16 @@ class Dispatcher:
49
48
  events = []
50
49
  for agent in agents:
51
50
  logger.debug(f"Sending event to agent {agent}")
52
-
51
+
53
52
  # Record agent interaction in tracer system
54
53
  self.tracer.record_agent_interaction(
55
54
  from_agent=str(event.source),
56
55
  to_agent=str(type(agent)),
57
- event_type=str(type(event).__name__),
58
- event_id=event.correlation_id,
59
- source=type(self)
60
- )
61
-
56
+ event_type=str(type(event).__name__),
57
+ event_id=event.correlation_id,
58
+ source=type(self)
59
+ )
60
+
62
61
  # Process the event through the agent
63
62
  received_events = agent.receive_event(event)
64
63
  logger.debug(f"Agent {agent} returned {len(events)} events")
mojentic/llm/__init__.py CHANGED
@@ -3,13 +3,13 @@ Mojentic LLM module for interacting with Large Language Models.
3
3
  """
4
4
 
5
5
  # Main LLM components
6
- from .llm_broker import LLMBroker
7
- from .chat_session import ChatSession
8
- from .message_composers import MessageBuilder, FileTypeSensor
9
- from .registry.llm_registry import LLMRegistry
6
+ from .llm_broker import LLMBroker # noqa: F401
7
+ from .chat_session import ChatSession # noqa: F401
8
+ from .message_composers import MessageBuilder, FileTypeSensor # noqa: F401
9
+ from .registry.llm_registry import LLMRegistry # noqa: F401
10
10
 
11
11
  # Re-export gateway components at the LLM level
12
- from .gateways.models import (
12
+ from .gateways.models import ( # noqa: F401
13
13
  LLMMessage,
14
14
  LLMGatewayResponse,
15
15
  MessageRole
@@ -1,4 +1,4 @@
1
- from typing import List, Optional
1
+ from typing import Iterator, List, Optional
2
2
 
3
3
  from mojentic.llm import LLMBroker
4
4
  from mojentic.llm.gateways.models import LLMMessage, MessageRole
@@ -78,6 +78,29 @@ class ChatSession:
78
78
  self.insert_message(LLMMessage(role=MessageRole.Assistant, content=response))
79
79
  return response
80
80
 
81
+ def send_stream(self, query) -> Iterator[str]:
82
+ """
83
+ Send a query to the LLM and yield response chunks as they arrive. Records the query and
84
+ the full assembled response in the ongoing chat session after the stream is consumed.
85
+
86
+ Parameters
87
+ ----------
88
+ query : str
89
+ The query to send to the LLM.
90
+
91
+ Yields
92
+ ------
93
+ str
94
+ Content chunks from the LLM response as they arrive.
95
+ """
96
+ self.insert_message(LLMMessage(role=MessageRole.User, content=query))
97
+ accumulated = []
98
+ for chunk in self.llm.generate_stream(self.messages, tools=self.tools, temperature=self.temperature):
99
+ accumulated.append(chunk)
100
+ yield chunk
101
+ self._ensure_all_messages_are_sized()
102
+ self.insert_message(LLMMessage(role=MessageRole.Assistant, content="".join(accumulated)))
103
+
81
104
  def insert_message(self, message: LLMMessage):
82
105
  """
83
106
  Add a message onto the end of the chat session. If the total token count exceeds the max context, the oldest
@@ -94,6 +94,46 @@ class DescribeChatSession:
94
94
  assert chat_session.messages[1].content == "Query message 2"
95
95
  assert chat_session.messages[2].content == INTENDED_RESPONSE_MESSAGE
96
96
 
97
+ class DescribeStreamingSend:
98
+
99
+ def should_yield_content_chunks(self, chat_session):
100
+ chat_session.llm.generate_stream.return_value = iter(["Hello", " world"])
101
+
102
+ chunks = list(chat_session.send_stream("Query message"))
103
+
104
+ assert chunks == ["Hello", " world"]
105
+
106
+ def should_grow_message_history_after_stream_consumed(self, chat_session):
107
+ chat_session.llm.generate_stream.return_value = iter(["Response"])
108
+
109
+ list(chat_session.send_stream("Query message"))
110
+
111
+ assert len(chat_session.messages) == 3
112
+
113
+ def should_record_full_assembled_response_in_history(self, chat_session):
114
+ chat_session.llm.generate_stream.return_value = iter(["Hello", " world"])
115
+
116
+ list(chat_session.send_stream("Query message"))
117
+
118
+ assert chat_session.messages[2].content == "Hello world"
119
+
120
+ def should_record_user_message_in_history(self, chat_session):
121
+ chat_session.llm.generate_stream.return_value = iter(["Response"])
122
+
123
+ list(chat_session.send_stream("Query message"))
124
+
125
+ assert chat_session.messages[1].role == MessageRole.User
126
+ assert chat_session.messages[1].content == "Query message"
127
+
128
+ def should_respect_context_capacity(self, chat_session):
129
+ chat_session.llm.generate_stream.return_value = iter(["Response 1"])
130
+ list(chat_session.send_stream("Query 1"))
131
+
132
+ chat_session.llm.generate_stream.return_value = iter(["Response 2"])
133
+ list(chat_session.send_stream("Query 2"))
134
+
135
+ assert len(chat_session.messages) == 3
136
+
97
137
  class DescribeMessageRoles:
98
138
  """
99
139
  Specifications for message role handling
@@ -3,23 +3,24 @@ Mojentic LLM gateways module for connecting to various LLM providers.
3
3
  """
4
4
 
5
5
  # Gateway implementations
6
- from .llm_gateway import LLMGateway
7
- from .ollama import OllamaGateway
8
- from .openai import OpenAIGateway
9
- from .anthropic import AnthropicGateway
10
- from .file_gateway import FileGateway
11
- from .embeddings_gateway import EmbeddingsGateway
12
- from .tokenizer_gateway import TokenizerGateway
13
-
14
- # Message adapters
15
- from .anthropic_messages_adapter import adapt_messages_to_anthropic
16
- from .ollama_messages_adapter import adapt_messages_to_ollama
17
- from .openai_messages_adapter import adapt_messages_to_openai
6
+ from mojentic.llm.gateways.llm_gateway import LLMGateway
7
+ from mojentic.llm.gateways.ollama import OllamaGateway
8
+ from mojentic.llm.gateways.openai import OpenAIGateway
9
+ from mojentic.llm.gateways.anthropic import AnthropicGateway
10
+ from mojentic.llm.gateways.tokenizer_gateway import TokenizerGateway
11
+ from mojentic.llm.gateways.embeddings_gateway import EmbeddingsGateway
18
12
 
19
13
  # Common models
20
- from .models import (
21
- LLMMessage,
22
- MessageRole,
23
- LLMGatewayResponse,
24
- LLMToolCall
25
- )
14
+ from mojentic.llm.gateways.models import LLMMessage, LLMToolCall, LLMGatewayResponse
15
+
16
+ __all__ = [
17
+ "LLMGateway",
18
+ "OllamaGateway",
19
+ "OpenAIGateway",
20
+ "AnthropicGateway",
21
+ "TokenizerGateway",
22
+ "EmbeddingsGateway",
23
+ "LLMMessage",
24
+ "LLMToolCall",
25
+ "LLMGatewayResponse",
26
+ ]
@@ -9,6 +9,7 @@ from mojentic.llm.gateways.anthropic_messages_adapter import adapt_messages_to_a
9
9
 
10
10
  logger = structlog.get_logger()
11
11
 
12
+
12
13
  class AnthropicGateway(LLMGateway):
13
14
  def __init__(self, api_key: str):
14
15
  self.client = Anthropic(api_key=api_key)
@@ -1,5 +1,4 @@
1
1
  import base64
2
- import json
3
2
  import os
4
3
  from typing import List, Any
5
4
 
@@ -1,4 +1,4 @@
1
- from typing import List, Optional, Type, Any
1
+ from typing import List, Optional, Type
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
@@ -9,6 +9,7 @@ from mojentic.llm.gateways.ollama_messages_adapter import adapt_messages_to_olla
9
9
 
10
10
  logger = structlog.get_logger()
11
11
 
12
+
12
13
  class StreamingResponse(BaseModel):
13
14
  """
14
15
  Wrapper for streaming response chunks.
@@ -23,6 +24,7 @@ class StreamingResponse(BaseModel):
23
24
  content: Optional[str] = None
24
25
  tool_calls: Optional[List] = None
25
26
 
27
+
26
28
  class OllamaGateway(LLMGateway):
27
29
  """
28
30
  This class is a gateway to the Ollama LLM service.
@@ -1,19 +1,17 @@
1
1
  import json
2
2
  import os
3
3
  from itertools import islice
4
- from typing import Type, List, Iterable, Optional, Iterator, Dict
4
+ from typing import List, Iterable, Optional, Iterator, Dict
5
5
 
6
6
  import numpy as np
7
7
  import structlog
8
8
  from openai import OpenAI, BadRequestError
9
- from pydantic import BaseModel
10
9
 
11
10
  from mojentic.llm.gateways.llm_gateway import LLMGateway
12
- from mojentic.llm.gateways.models import LLMToolCall, LLMGatewayResponse, LLMMessage
11
+ from mojentic.llm.gateways.models import LLMToolCall, LLMGatewayResponse
13
12
  from mojentic.llm.gateways.openai_messages_adapter import adapt_messages_to_openai
14
13
  from mojentic.llm.gateways.openai_model_registry import get_model_registry, ModelType
15
14
  from mojentic.llm.gateways.tokenizer_gateway import TokenizerGateway
16
- from mojentic.llm.tools.llm_tool import LLMTool
17
15
  from mojentic.llm.gateways.ollama import StreamingResponse
18
16
 
19
17
  logger = structlog.get_logger()
@@ -77,10 +75,10 @@ class OpenAIGateway(LLMGateway):
77
75
  capabilities = self.model_registry.get_model_capabilities(model)
78
76
 
79
77
  logger.debug("Adapting parameters for model",
80
- model=model,
81
- model_type=capabilities.model_type.value,
82
- supports_tools=capabilities.supports_tools,
83
- supports_streaming=capabilities.supports_streaming)
78
+ model=model,
79
+ model_type=capabilities.model_type.value,
80
+ supports_tools=capabilities.supports_tools,
81
+ supports_streaming=capabilities.supports_streaming)
84
82
 
85
83
  # Handle token limit parameter conversion
86
84
  if 'max_tokens' in adapted_args:
@@ -89,16 +87,16 @@ class OpenAIGateway(LLMGateway):
89
87
  # Convert max_tokens to max_completion_tokens for reasoning models
90
88
  adapted_args[token_param] = adapted_args.pop('max_tokens')
91
89
  logger.info("Converted token limit parameter for model",
92
- model=model,
93
- from_param='max_tokens',
94
- to_param=token_param,
95
- value=adapted_args[token_param])
90
+ model=model,
91
+ from_param='max_tokens',
92
+ to_param=token_param,
93
+ value=adapted_args[token_param])
96
94
 
97
95
  # Validate tool usage for models that don't support tools
98
96
  if 'tools' in adapted_args and adapted_args['tools'] and not capabilities.supports_tools:
99
97
  logger.warning("Model does not support tools, removing tool configuration",
100
- model=model,
101
- num_tools=len(adapted_args['tools']))
98
+ model=model,
99
+ num_tools=len(adapted_args['tools']))
102
100
  adapted_args['tools'] = None # Set to None instead of removing the key
103
101
 
104
102
  # Handle temperature restrictions for specific models
@@ -108,18 +106,19 @@ class OpenAIGateway(LLMGateway):
108
106
  # Check if model supports temperature parameter at all
109
107
  if capabilities.supported_temperatures == []:
110
108
  # Model doesn't support temperature parameter at all - remove it
111
- logger.warning("Model does not support temperature parameter, removing it",
112
- model=model,
113
- requested_temperature=temperature)
109
+ logger.warning("Model does not support temperature parameter at all",
110
+ model=model,
111
+ requested_temperature=temperature)
114
112
  adapted_args.pop('temperature', None)
115
113
  elif not capabilities.supports_temperature(temperature):
116
114
  # Model supports temperature but not this specific value - use default
117
115
  default_temp = 1.0
118
- logger.warning("Model does not support requested temperature, using default",
119
- model=model,
120
- requested_temperature=temperature,
121
- default_temperature=default_temp,
122
- supported_temperatures=capabilities.supported_temperatures)
116
+ logger.warning(
117
+ "Model does not support requested temperature, using default",
118
+ model=model,
119
+ requested_temperature=temperature,
120
+ default_temperature=default_temp,
121
+ supported_temperatures=capabilities.supported_temperatures)
123
122
  adapted_args['temperature'] = default_temp
124
123
 
125
124
  return adapted_args
@@ -139,13 +138,12 @@ class OpenAIGateway(LLMGateway):
139
138
 
140
139
  # Warning for tools on reasoning models that don't support them
141
140
  if (capabilities.model_type == ModelType.REASONING and
142
- not capabilities.supports_tools and
143
- 'tools' in args and args['tools']):
141
+ not capabilities.supports_tools and
142
+ 'tools' in args and args['tools']):
144
143
  logger.warning(
145
144
  "Reasoning model may not support tools",
146
145
  model=model,
147
- num_tools=len(args['tools'])
148
- )
146
+ num_tools=len(args['tools']))
149
147
 
150
148
  # Validate token limits (check both possible parameter names)
151
149
  token_value = args.get('max_tokens') or args.get('max_completion_tokens')
@@ -155,8 +153,7 @@ class OpenAIGateway(LLMGateway):
155
153
  "Requested token limit exceeds model maximum",
156
154
  model=model,
157
155
  requested=token_value,
158
- max_allowed=capabilities.max_output_tokens
159
- )
156
+ max_allowed=capabilities.max_output_tokens)
160
157
 
161
158
  def complete(self, **kwargs) -> LLMGatewayResponse:
162
159
  """
@@ -219,8 +216,8 @@ class OpenAIGateway(LLMGateway):
219
216
  adapted_args = self._adapt_parameters_for_model(model, args)
220
217
  except Exception as e:
221
218
  logger.error("Failed to adapt parameters for model",
222
- model=model,
223
- error=str(e))
219
+ model=model,
220
+ error=str(e))
224
221
  raise
225
222
 
226
223
  # Validate parameters after adaptation
@@ -251,25 +248,26 @@ class OpenAIGateway(LLMGateway):
251
248
  openai_args['max_completion_tokens'] = adapted_args['max_completion_tokens']
252
249
 
253
250
  logger.debug("Making OpenAI API call",
254
- model=openai_args['model'],
255
- has_tools='tools' in openai_args,
256
- has_object_model='response_format' in openai_args,
257
- token_param='max_completion_tokens' if 'max_completion_tokens' in openai_args else 'max_tokens')
251
+ model=openai_args['model'],
252
+ has_tools='tools' in openai_args,
253
+ has_object_model='response_format' in openai_args,
254
+ token_param='max_completion_tokens' if 'max_completion_tokens' in openai_args else 'max_tokens')
258
255
 
259
256
  try:
260
257
  response = completion(**openai_args)
261
258
  except BadRequestError as e:
262
259
  # Enhanced error handling for parameter issues
263
260
  if "max_tokens" in str(e) and "max_completion_tokens" in str(e):
264
- logger.error("Parameter error detected - model may require different token parameter",
265
- model=model,
266
- error=str(e),
267
- suggestion="This model may be a reasoning model requiring max_completion_tokens")
261
+ logger.error(
262
+ "Parameter error detected - model may require different token parameter",
263
+ model=model,
264
+ error=str(e),
265
+ suggestion="This model may be a reasoning model requiring max_completion_tokens")
268
266
  raise e
269
267
  except Exception as e:
270
268
  logger.error("OpenAI API call failed",
271
- model=model,
272
- error=str(e))
269
+ model=model,
270
+ error=str(e))
273
271
  raise e
274
272
 
275
273
  object = None
@@ -281,11 +279,16 @@ class OpenAIGateway(LLMGateway):
281
279
  if response_content is not None:
282
280
  object = adapted_args['object_model'].model_validate_json(response_content)
283
281
  else:
284
- logger.error("No response content available for object validation", object_model=adapted_args['object_model'])
282
+ logger.error(
283
+ "No response content available for object validation",
284
+ object_model=adapted_args['object_model'])
285
285
  except Exception as e:
286
- response_content = response.choices[0].message.content if response.choices else "No response content"
287
- logger.error("Failed to validate model", error=str(e), response=response_content,
288
- object_model=adapted_args['object_model'])
286
+ response_content = (response.choices[0].message.content
287
+ if response.choices else "No response content")
288
+ logger.error("Failed to validate model",
289
+ error=str(e),
290
+ response=response_content,
291
+ object_model=adapted_args['object_model'])
289
292
 
290
293
  if response.choices[0].message.tool_calls is not None:
291
294
  for t in response.choices[0].message.tool_calls:
@@ -363,8 +366,8 @@ class OpenAIGateway(LLMGateway):
363
366
  adapted_args = self._adapt_parameters_for_model(model, args)
364
367
  except Exception as e:
365
368
  logger.error("Failed to adapt parameters for model",
366
- model=model,
367
- error=str(e))
369
+ model=model,
370
+ error=str(e))
368
371
  raise
369
372
 
370
373
  # Validate parameters after adaptation
@@ -399,23 +402,24 @@ class OpenAIGateway(LLMGateway):
399
402
  openai_args['max_completion_tokens'] = adapted_args['max_completion_tokens']
400
403
 
401
404
  logger.debug("Making OpenAI streaming API call",
402
- model=openai_args['model'],
403
- has_tools='tools' in openai_args,
404
- token_param='max_completion_tokens' if 'max_completion_tokens' in openai_args else 'max_tokens')
405
+ model=openai_args['model'],
406
+ has_tools='tools' in openai_args,
407
+ token_param='max_completion_tokens' if 'max_completion_tokens' in openai_args else 'max_tokens')
405
408
 
406
409
  try:
407
410
  stream = self.client.chat.completions.create(**openai_args)
408
411
  except BadRequestError as e:
409
412
  if "max_tokens" in str(e) and "max_completion_tokens" in str(e):
410
- logger.error("Parameter error detected - model may require different token parameter",
411
- model=model,
412
- error=str(e),
413
- suggestion="This model may be a reasoning model requiring max_completion_tokens")
413
+ logger.error(
414
+ "Parameter error detected - model may require different token parameter",
415
+ model=model,
416
+ error=str(e),
417
+ suggestion="This model may be a reasoning model requiring max_completion_tokens")
414
418
  raise e
415
419
  except Exception as e:
416
420
  logger.error("OpenAI streaming API call failed",
417
- model=model,
418
- error=str(e))
421
+ model=model,
422
+ error=str(e))
419
423
  raise e
420
424
 
421
425
  # Accumulate tool calls as they stream in
@@ -477,9 +481,9 @@ class OpenAIGateway(LLMGateway):
477
481
  complete_tool_calls.append(tool_call)
478
482
  except json.JSONDecodeError as e:
479
483
  logger.error("Failed to parse tool call arguments",
480
- tool_name=tc['name'],
481
- arguments=tc['arguments'],
482
- error=str(e))
484
+ tool_name=tc['name'],
485
+ arguments=tc['arguments'],
486
+ error=str(e))
483
487
 
484
488
  if complete_tool_calls:
485
489
  # Convert to the format expected by ollama's tool calls for compatibility
@@ -93,11 +93,11 @@ class DescribeOpenAIMessagesAdapter:
93
93
  Then it should convert to the correct format with structured content array
94
94
  """
95
95
  # Patch our own methods that encapsulate external library calls
96
- mocker.patch('mojentic.llm.gateways.openai_messages_adapter.read_file_as_binary',
96
+ mocker.patch('mojentic.llm.gateways.openai_messages_adapter.read_file_as_binary',
97
97
  return_value=b'fake_image_data')
98
- mocker.patch('mojentic.llm.gateways.openai_messages_adapter.encode_base64',
98
+ mocker.patch('mojentic.llm.gateways.openai_messages_adapter.encode_base64',
99
99
  return_value='ZmFrZV9pbWFnZV9kYXRhX2VuY29kZWQ=')
100
- mocker.patch('mojentic.llm.gateways.openai_messages_adapter.get_image_type',
100
+ mocker.patch('mojentic.llm.gateways.openai_messages_adapter.get_image_type',
101
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"]
@@ -6,13 +6,13 @@ their specific parameter requirements and capabilities.
6
6
  """
7
7
 
8
8
  from enum import Enum
9
- from typing import Dict, Set, Optional, List, TYPE_CHECKING
9
+ from typing import Dict, Optional, List, TYPE_CHECKING
10
10
  from dataclasses import dataclass
11
11
 
12
12
  import structlog
13
13
 
14
14
  if TYPE_CHECKING:
15
- from mojentic.llm.gateways.openai import OpenAIGateway
15
+ pass
16
16
 
17
17
  logger = structlog.get_logger()
18
18
 
@@ -20,9 +20,9 @@ logger = structlog.get_logger()
20
20
  class ModelType(Enum):
21
21
  """Classification of OpenAI model types based on their capabilities and parameters."""
22
22
  REASONING = "reasoning" # Models like o1, o3 that use max_completion_tokens
23
- CHAT = "chat" # Standard chat models that use max_tokens
24
- EMBEDDING = "embedding" # Text embedding models
25
- MODERATION = "moderation" # Content moderation models
23
+ CHAT = "chat" # Standard chat models that use max_tokens
24
+ EMBEDDING = "embedding" # Text embedding models
25
+ MODERATION = "moderation" # Content moderation models
26
26
 
27
27
 
28
28
  @dataclass
@@ -346,6 +346,7 @@ class OpenAIModelRegistry:
346
346
  # Global registry instance
347
347
  _registry = OpenAIModelRegistry()
348
348
 
349
+
349
350
  def get_model_registry() -> OpenAIModelRegistry:
350
351
  """Get the global OpenAI model registry instance."""
351
- return _registry
352
+ return _registry
@@ -2,7 +2,6 @@
2
2
  Tests for the OpenAI Model Registry system.
3
3
  """
4
4
 
5
- import pytest
6
5
  from mojentic.llm.gateways.openai_model_registry import (
7
6
  OpenAIModelRegistry,
8
7
  ModelType,
@@ -178,4 +177,4 @@ class DescribeOpenAIModelRegistry:
178
177
  assert ModelType.REASONING.value == "reasoning"
179
178
  assert ModelType.CHAT.value == "chat"
180
179
  assert ModelType.EMBEDDING.value == "embedding"
181
- assert ModelType.MODERATION.value == "moderation"
180
+ assert ModelType.MODERATION.value == "moderation"
@@ -3,7 +3,7 @@ from unittest.mock import MagicMock
3
3
 
4
4
  from mojentic.llm.gateways.openai import OpenAIGateway
5
5
  from mojentic.llm.gateways.openai_model_registry import get_model_registry
6
- from mojentic.llm.gateways.models import LLMMessage, MessageRole, LLMGatewayResponse
6
+ from mojentic.llm.gateways.models import LLMMessage, MessageRole
7
7
 
8
8
 
9
9
  @pytest.fixture
@@ -242,4 +242,4 @@ class DescribeModelCapabilitiesTemperatureRestrictions:
242
242
  capabilities = registry.get_model_capabilities(model)
243
243
  assert capabilities.supports_temperature(1.0) is True
244
244
  assert capabilities.supports_temperature(0.1) is False
245
- assert capabilities.supported_temperatures == [1.0]
245
+ assert capabilities.supported_temperatures == [1.0]
@@ -7,7 +7,7 @@ from pydantic import BaseModel
7
7
 
8
8
  from mojentic.llm.gateways.llm_gateway import LLMGateway
9
9
  from mojentic.llm.gateways.models import MessageRole, LLMMessage, LLMGatewayResponse, LLMToolCall
10
- from mojentic.llm.gateways.ollama import OllamaGateway, StreamingResponse
10
+ from mojentic.llm.gateways.ollama import OllamaGateway
11
11
  from mojentic.llm.gateways.tokenizer_gateway import TokenizerGateway
12
12
  from mojentic.tracer.tracer_system import TracerSystem
13
13
 
@@ -183,8 +183,8 @@ class LLMBroker():
183
183
  return result.content
184
184
 
185
185
  def generate_stream(self, messages: List[LLMMessage], tools=None, temperature=1.0, num_ctx=32768,
186
- num_predict=-1, max_tokens=16384,
187
- correlation_id: str = None) -> Iterator[str]:
186
+ num_predict=-1, max_tokens=16384,
187
+ correlation_id: str = None) -> Iterator[str]:
188
188
  """
189
189
  Generate a streaming text response from the LLM.
190
190
 
@@ -334,8 +334,10 @@ class LLMBroker():
334
334
  tool_calls=[tool_call]))
335
335
 
336
336
  # Recursively stream the response after tool execution
337
- yield from self.generate_stream(messages, tools, temperature, num_ctx, num_predict,
338
- max_tokens, correlation_id=correlation_id)
337
+ yield from self.generate_stream(
338
+ messages, tools, temperature, num_ctx, num_predict,
339
+ max_tokens, correlation_id=correlation_id
340
+ )
339
341
  return # Exit after recursive call
340
342
  else:
341
343
  logger.warn('Function not found', function=tool_name)
@@ -1,4 +1,3 @@
1
- from unittest.mock import MagicMock
2
1
 
3
2
  import pytest
4
3
  from pydantic import BaseModel
@@ -11,10 +10,12 @@ class SimpleModel(BaseModel):
11
10
  text: str
12
11
  number: int
13
12
 
13
+
14
14
  class NestedModel(BaseModel):
15
15
  title: str
16
16
  details: SimpleModel
17
17
 
18
+
18
19
  class ComplexModel(BaseModel):
19
20
  name: str
20
21
  items: list[SimpleModel]
@@ -120,7 +121,11 @@ class DescribeLLMBroker:
120
121
  metadata={"key1": "value1", "key2": "value2"}
121
122
  )
122
123
  mock_gateway.complete.return_value = LLMGatewayResponse(
123
- content='{"name": "test", "items": [{"text": "item1", "number": 1}, {"text": "item2", "number": 2}], "metadata": {"key1": "value1", "key2": "value2"}}',
124
+ content=(
125
+ '{"name": "test", "items": [{"text": "item1", "number": 1}, '
126
+ '{"text": "item2", "number": 2}], '
127
+ '"metadata": {"key1": "value1", "key2": "value2"}}'
128
+ ),
124
129
  object=mock_object,
125
130
  tool_calls=[]
126
131
  )