unique_toolkit 1.36.0__py3-none-any.whl → 1.38.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,17 +61,8 @@ def validate_and_init_language_model_info(
61
61
  LanguageModelInfo: The validated and initialized LanguageModelInfo object.
62
62
 
63
63
  """
64
- if isinstance(v, LanguageModelName):
64
+ if isinstance(v, LanguageModelName | str):
65
65
  return LanguageModelInfo.from_name(v)
66
- if isinstance(v, str):
67
- if v in [name.value for name in LanguageModelName]:
68
- return LanguageModelInfo.from_name(LanguageModelName(v))
69
-
70
- return LanguageModelInfo(
71
- name=v,
72
- version="custom",
73
- provider=LanguageModelProvider.CUSTOM,
74
- )
75
66
 
76
67
  return v
77
68
 
@@ -1,8 +1,11 @@
1
1
  from unique_toolkit.agentic.loop_runner.base import LoopIterationRunner
2
2
  from unique_toolkit.agentic.loop_runner.middleware import (
3
+ QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION,
3
4
  PlanningConfig,
4
5
  PlanningMiddleware,
5
6
  PlanningSchemaConfig,
7
+ QwenForcedToolCallMiddleware,
8
+ is_qwen_model,
6
9
  )
7
10
  from unique_toolkit.agentic.loop_runner.runners import (
8
11
  BasicLoopIterationRunner,
@@ -11,9 +14,12 @@ from unique_toolkit.agentic.loop_runner.runners import (
11
14
 
12
15
  __all__ = [
13
16
  "LoopIterationRunner",
17
+ "QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION",
14
18
  "PlanningConfig",
15
19
  "PlanningMiddleware",
16
20
  "PlanningSchemaConfig",
21
+ "QwenForcedToolCallMiddleware",
22
+ "is_qwen_model",
17
23
  "BasicLoopIterationRunnerConfig",
18
24
  "BasicLoopIterationRunner",
19
25
  ]
@@ -3,5 +3,17 @@ from unique_toolkit.agentic.loop_runner.middleware.planning import (
3
3
  PlanningMiddleware,
4
4
  PlanningSchemaConfig,
5
5
  )
6
+ from unique_toolkit.agentic.loop_runner.middleware.qwen_forced_tool_call import (
7
+ QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION,
8
+ QwenForcedToolCallMiddleware,
9
+ is_qwen_model,
10
+ )
6
11
 
7
- __all__ = ["PlanningConfig", "PlanningMiddleware", "PlanningSchemaConfig"]
12
+ __all__ = [
13
+ "PlanningConfig",
14
+ "PlanningMiddleware",
15
+ "PlanningSchemaConfig",
16
+ "QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION",
17
+ "QwenForcedToolCallMiddleware",
18
+ "is_qwen_model",
19
+ ]
@@ -0,0 +1,13 @@
1
+ from unique_toolkit.agentic.loop_runner.middleware.qwen_forced_tool_call.helpers import (
2
+ is_qwen_model,
3
+ )
4
+ from unique_toolkit.agentic.loop_runner.middleware.qwen_forced_tool_call.qwen_forced_tool_call import (
5
+ QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION,
6
+ QwenForcedToolCallMiddleware,
7
+ )
8
+
9
+ __all__ = [
10
+ "QwenForcedToolCallMiddleware",
11
+ "QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION",
12
+ "is_qwen_model",
13
+ ]
@@ -0,0 +1,33 @@
1
+ from unique_toolkit.language_model.infos import LanguageModelInfo
2
+ from unique_toolkit.language_model.schemas import (
3
+ LanguageModelMessageRole,
4
+ LanguageModelMessages,
5
+ )
6
+
7
+
8
+ def is_qwen_model(*, model: str | LanguageModelInfo | None) -> bool:
9
+ """Check if the model is a Qwen model."""
10
+ if isinstance(model, LanguageModelInfo):
11
+ name = model.name
12
+ # name is an Enum with a .value attribute
13
+ return "qwen" in str(getattr(name, "value", name)).lower()
14
+ elif isinstance(model, str):
15
+ return "qwen" in model.lower()
16
+ return False
17
+
18
+
19
+ def append_qwen_forced_tool_call_instruction(
20
+ *,
21
+ messages: LanguageModelMessages,
22
+ forced_tool_call_instruction: str,
23
+ ) -> LanguageModelMessages:
24
+ """Append tool call instruction to the last user message for Qwen models."""
25
+ messages_list = list(messages)
26
+ for i in range(len(messages_list) - 1, -1, -1):
27
+ msg = messages_list[i]
28
+ if msg.role == LanguageModelMessageRole.USER and isinstance(msg.content, str):
29
+ messages_list[i] = msg.model_copy(
30
+ update={"content": msg.content + "\n" + forced_tool_call_instruction}
31
+ )
32
+ break
33
+ return LanguageModelMessages(root=messages_list)
@@ -0,0 +1,50 @@
1
+ import logging
2
+ from typing import Unpack
3
+
4
+ from unique_toolkit.agentic.loop_runner.base import (
5
+ LoopIterationRunner,
6
+ _LoopIterationRunnerKwargs,
7
+ )
8
+ from unique_toolkit.agentic.loop_runner.middleware.qwen_forced_tool_call.helpers import (
9
+ append_qwen_forced_tool_call_instruction,
10
+ )
11
+ from unique_toolkit.chat.service import LanguageModelStreamResponse
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+ QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION = (
16
+ "Tool Call Instruction: \nYou always have to return a tool call. "
17
+ "You must start the response with <tool_call> and end with </tool_call>. "
18
+ "Do NOT provide natural language explanations, summaries, or any text outside the <tool_call> block."
19
+ )
20
+
21
+
22
+ class QwenForcedToolCallMiddleware(LoopIterationRunner):
23
+ def __init__(
24
+ self,
25
+ *,
26
+ loop_runner: LoopIterationRunner,
27
+ qwen_forced_tool_call_prompt_instruction: str,
28
+ ) -> None:
29
+ self._qwen_forced_tool_call_prompt_instruction = (
30
+ qwen_forced_tool_call_prompt_instruction
31
+ )
32
+ self._loop_runner = loop_runner
33
+
34
+ async def __call__(
35
+ self, **kwargs: Unpack[_LoopIterationRunnerKwargs]
36
+ ) -> LanguageModelStreamResponse:
37
+ tool_choices = kwargs.get("tool_choices") or []
38
+ iteration_index = kwargs["iteration_index"]
39
+
40
+ # For Qwen models, append tool call instruction to the last user message. These models ignore the parameter tool_choice.
41
+ if len(tool_choices) > 0 and iteration_index == 0 and kwargs.get("messages"):
42
+ _LOGGER.info(
43
+ "Appending tool call instruction to the last user message for Qwen models to force tool calls."
44
+ )
45
+ kwargs["messages"] = append_qwen_forced_tool_call_instruction(
46
+ messages=kwargs["messages"],
47
+ forced_tool_call_instruction=self._qwen_forced_tool_call_prompt_instruction,
48
+ )
49
+
50
+ return await self._loop_runner(**kwargs)
@@ -56,13 +56,15 @@ class BasicLoopIterationRunner(LoopIterationRunner):
56
56
 
57
57
  responses: list[LanguageModelStreamResponse] = []
58
58
 
59
+ available_tools = {t.name: t for t in kwargs.get("tools") or []}
60
+
59
61
  for opt in tool_choices:
60
- responses.append(
61
- await stream_response(
62
- loop_runner_kwargs=kwargs,
63
- tool_choice=opt,
64
- )
65
- )
62
+ func_name = opt.get("function", {}).get("name")
63
+ limited_tool = available_tools.get(func_name) if func_name else None
64
+ stream_kwargs = {"loop_runner_kwargs": kwargs, "tool_choice": opt}
65
+ if limited_tool:
66
+ stream_kwargs["tools"] = [limited_tool]
67
+ responses.append(await stream_response(**stream_kwargs))
66
68
 
67
69
  # Merge responses and refs:
68
70
  tool_calls = []
@@ -1,5 +1,9 @@
1
+ import json
2
+ import logging
3
+ import os
1
4
  from datetime import date
2
5
  from enum import StrEnum
6
+ from functools import lru_cache
3
7
  from typing import Annotated, Any, ClassVar, Optional, Self
4
8
 
5
9
  from pydantic import BaseModel, Field
@@ -9,6 +13,8 @@ from typing_extensions import deprecated
9
13
  from unique_toolkit._common.pydantic_helpers import get_configuration_dict
10
14
  from unique_toolkit.language_model.schemas import LanguageModelTokenLimits
11
15
 
16
+ _LOGGER = logging.getLogger(__name__)
17
+
12
18
 
13
19
  class LanguageModelName(StrEnum):
14
20
  AZURE_GPT_35_TURBO_0125 = "AZURE_GPT_35_TURBO_0125"
@@ -202,8 +208,84 @@ class LanguageModelInfo(BaseModel):
202
208
 
203
209
  default_options: dict[str, Any] = {}
204
210
 
211
+ _ENV_VAR: ClassVar[str] = "LANGUAGE_MODEL_INFOS"
212
+ """Environment variable name for custom language model infos."""
213
+
214
+ @classmethod
215
+ @lru_cache(maxsize=1)
216
+ def _load_from_env(cls) -> dict[str, dict]:
217
+ """
218
+ Load custom language model infos from environment variable.
219
+
220
+ The environment variable should contain a JSON string with a dict of
221
+ LanguageModelInfo-compatible dictionaries. The key is used for model lookup.
222
+
223
+ Example:
224
+ LANGUAGE_MODEL_INFOS='{"AZURE_GPT_4o_CUSTOM": {"name": "AZURE_GPT_4o_2024_1120",
225
+ "provider": "AZURE", "version": "custom", "capabilities": ["function_calling",
226
+ "streaming", "vision"], "token_limits": {"token_limit_input": 3000,
227
+ "token_limit_output": 150}}}'
228
+
229
+ Returns:
230
+ A dictionary mapping model keys to their info dictionaries.
231
+ """
232
+ env_value = os.getenv(cls._ENV_VAR)
233
+ if not env_value:
234
+ return {}
235
+
236
+ try:
237
+ model_infos_dict = json.loads(env_value)
238
+ if not isinstance(model_infos_dict, dict):
239
+ _LOGGER.error(
240
+ f"{cls._ENV_VAR} must be a JSON dict of model info objects. "
241
+ f"Got {type(model_infos_dict).__name__} instead."
242
+ )
243
+ return {}
244
+
245
+ # Validate each entry in the dictionary
246
+ valid_model_infos: dict[str, dict] = {}
247
+ for model_key, model_info in model_infos_dict.items():
248
+ if not isinstance(model_info, dict):
249
+ _LOGGER.warning(
250
+ f"Skipping invalid model info entry '{model_key}' in {cls._ENV_VAR}: "
251
+ f"expected dict, got {type(model_info).__name__}"
252
+ )
253
+ continue
254
+
255
+ valid_model_infos[model_key] = model_info
256
+
257
+ _LOGGER.debug(
258
+ f"Loaded {len(valid_model_infos)} custom language model infos from {cls._ENV_VAR}"
259
+ )
260
+ return valid_model_infos
261
+
262
+ except json.JSONDecodeError:
263
+ _LOGGER.error(
264
+ f"Failed to parse {cls._ENV_VAR} as JSON. "
265
+ "The environment variable should contain a valid JSON dict of model info objects.",
266
+ exc_info=True,
267
+ )
268
+ return {}
269
+
205
270
  @classmethod
206
- def from_name(cls, model_name: LanguageModelName) -> Self:
271
+ def from_name(cls, model_name: LanguageModelName | str) -> Self:
272
+ # Check environment variable first - env definitions take precedence
273
+ env_model_infos = cls._load_from_env()
274
+ model_name_str = (
275
+ model_name.value
276
+ if isinstance(model_name, LanguageModelName)
277
+ else model_name
278
+ )
279
+ if model_name_str in env_model_infos.keys():
280
+ try:
281
+ return cls.model_validate(env_model_infos[model_name_str])
282
+ except Exception:
283
+ _LOGGER.warning(
284
+ f"Failed to parse model info for '{model_name_str}' from "
285
+ f"{cls._ENV_VAR}. Falling back to default definition.",
286
+ exc_info=True,
287
+ )
288
+
207
289
  match model_name:
208
290
  case LanguageModelName.AZURE_GPT_35_TURBO_0125:
209
291
  return cls(
@@ -1723,14 +1805,7 @@ class LanguageModel:
1723
1805
 
1724
1806
  @classmethod
1725
1807
  def get_model_info(cls, model_name: LanguageModelName | str) -> LanguageModelInfo:
1726
- if isinstance(model_name, LanguageModelName):
1727
- return LanguageModelInfo.from_name(model_name)
1728
-
1729
- return LanguageModelInfo(
1730
- name=model_name,
1731
- version="custom",
1732
- provider=LanguageModelProvider.CUSTOM,
1733
- )
1808
+ return LanguageModelInfo.from_name(model_name)
1734
1809
 
1735
1810
  @classmethod
1736
1811
  def list_models(cls) -> list[LanguageModelInfo]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 1.36.0
3
+ Version: 1.38.0
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Cedric Klinkert
@@ -121,6 +121,12 @@ All notable changes to this project will be documented in this file.
121
121
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
122
122
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
123
123
 
124
+ ## [1.38.0] - 2025-12-15
125
+ - Including capability to load LanguageModelInfos in env variable
126
+
127
+ ## [1.37.0] - 2025-12-15
128
+ - Adding a prompt appendix to enforce forced tool calls when using Qwen models
129
+
124
130
  ## [1.36.0] - 2025-12-11
125
131
  - Add support for a sub agent tool system reminder when no references are present in the sub agent response.
126
132
 
@@ -39,7 +39,7 @@ unique_toolkit/_common/utils/structured_output/__init__.py,sha256=nm_orZrlCXL0FP
39
39
  unique_toolkit/_common/utils/structured_output/schema.py,sha256=Tp7kDYcmKtnUhcuRkH86TSYhylRff0ZZJYb2dLkISts,131
40
40
  unique_toolkit/_common/utils/write_configuration.py,sha256=fzvr4C-XBL3OSM3Od9TbqIxeeDS9_d9CLEyTq6DDknY,1409
41
41
  unique_toolkit/_common/validate_required_values.py,sha256=Y_M1ub9gIKP9qZ45F6Zq3ZHtuIqhmOjl8Z2Vd3avg8w,588
42
- unique_toolkit/_common/validators.py,sha256=LFZmAalNa886EXm1VYamFvfBuUZjYKwDdT_HOYU0BtE,2934
42
+ unique_toolkit/_common/validators.py,sha256=ElnkMsyEY24TfzfTVHvireyT39EnZgW5N40T0P4b6gE,2638
43
43
  unique_toolkit/agentic/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
44
44
  unique_toolkit/agentic/debug_info_manager/debug_info_manager.py,sha256=30ZZaw0vffjZjiu9AYdO1Sm8G9FN6XR2ehdOEUCKqh0,891
45
45
  unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py,sha256=_fIS6_DHA8A3AB64-LPgHgUGa1w0CFUWwtgV-ZbhkzA,10535
@@ -62,15 +62,18 @@ unique_toolkit/agentic/history_manager/history_construction_with_contents.py,sha
62
62
  unique_toolkit/agentic/history_manager/history_manager.py,sha256=7V7_173XkAjc8otBACF0G3dbqRs34FSlURbBPrE95Wk,9537
63
63
  unique_toolkit/agentic/history_manager/loop_token_reducer.py,sha256=3c-uonDovtanEJUpAO4zlA4-n9MS_Ws_V0Yb6G7hPM0,20172
64
64
  unique_toolkit/agentic/history_manager/utils.py,sha256=VIn_UmcR3jHtpux0qp5lQQzczgAm8XYSeQiPo87jC3A,3143
65
- unique_toolkit/agentic/loop_runner/__init__.py,sha256=QLCYmIyfcKQEbuv1Xm0VuR_xC8JyD2_aMIvt1TRFzvw,517
65
+ unique_toolkit/agentic/loop_runner/__init__.py,sha256=Kg-6Zgt--iIYk-biAkvnuwbButxPRPoVlrpppUblm0s,721
66
66
  unique_toolkit/agentic/loop_runner/_stream_handler_utils.py,sha256=FTGc5y8wkDnwnRVSYEdandgKz-FiySOsrTFFMadwP6E,1706
67
67
  unique_toolkit/agentic/loop_runner/base.py,sha256=3g4PalzV00o8kcRwHds2c2rtxW4idD7_7vS2Z7GkMvQ,1370
68
- unique_toolkit/agentic/loop_runner/middleware/__init__.py,sha256=_yeRH8xYigfJZyQ5-5lZUo2RXDJkGfftCQrKFm2rWb4,217
68
+ unique_toolkit/agentic/loop_runner/middleware/__init__.py,sha256=r9c_Ml2g7obglnEC7gDihSTUrZ6s1sNGCMuMXR0Yl90,520
69
69
  unique_toolkit/agentic/loop_runner/middleware/planning/__init__.py,sha256=Y9MlihNA8suNREixW98RF45bj0EMtD_tQuDrO2MEML4,304
70
70
  unique_toolkit/agentic/loop_runner/middleware/planning/planning.py,sha256=5d8kyipuFyI_1SQG49f165eOwSHeSG1qjbJQ7laeTsk,3218
71
71
  unique_toolkit/agentic/loop_runner/middleware/planning/schema.py,sha256=76C36CWCLfDAYYqtaQlhXsmkWM1fCqf8j-l5afQREKA,2869
72
+ unique_toolkit/agentic/loop_runner/middleware/qwen_forced_tool_call/__init__.py,sha256=lP8N8XLvV1irvGC6Q0FedAlBx-T2UPKotDRwkdx7neA,417
73
+ unique_toolkit/agentic/loop_runner/middleware/qwen_forced_tool_call/helpers.py,sha256=DGKW9i7mCXNTejO2fotCmqSzI2b5k89ybkJA0QQ75qU,1234
74
+ unique_toolkit/agentic/loop_runner/middleware/qwen_forced_tool_call/qwen_forced_tool_call.py,sha256=dp08YgL4UwVDTsJB-z7eiJn7zWEbqG8eiWfMOcvKZdI,1955
72
75
  unique_toolkit/agentic/loop_runner/runners/__init__.py,sha256=raaNpHcTfXkYURy0ysyacispSdQzYPDoG17PyR57uK4,205
73
- unique_toolkit/agentic/loop_runner/runners/basic.py,sha256=3swSPsefV1X-ltUC8iNAOrn9PL0abUUfWXJjhM4sShA,3116
76
+ unique_toolkit/agentic/loop_runner/runners/basic.py,sha256=SQzwkLEiraU8neXvPEc_uOBzC17PkCpOEsFrZ79YGCY,3379
74
77
  unique_toolkit/agentic/message_log_manager/__init__.py,sha256=3-KY_sGkPbNoSnrzwPY0FQIJNnsz4NHXvocXgGRUeuE,169
75
78
  unique_toolkit/agentic/message_log_manager/service.py,sha256=AiuIq2dKQg9Y8bEYgGcve1X8-WRRdqPZXaZXXLJxfFM,3057
76
79
  unique_toolkit/agentic/postprocessor/postprocessor_manager.py,sha256=s6HFhA61TE05aAay15NFTWI1JvdSlxmGpEVfpBbGFyM,7684
@@ -191,7 +194,7 @@ unique_toolkit/language_model/builder.py,sha256=4OKfwJfj3TrgO1ezc_ewIue6W7BCQ2ZY
191
194
  unique_toolkit/language_model/constants.py,sha256=B-topqW0r83dkC_25DeQfnPk3n53qzIHUCBS7YJ0-1U,119
192
195
  unique_toolkit/language_model/default_language_model.py,sha256=-_DBsJhLCsFdaU4ynAkyW0jYIl2lhrPybZm1K-GgVJs,125
193
196
  unique_toolkit/language_model/functions.py,sha256=PTBm2BBkuqISVHoyUqMIGHGXT-RMSAfz0F_Ylo2esQ8,18246
194
- unique_toolkit/language_model/infos.py,sha256=sZJOOij-dfReDxJWfd7ZwP3qx4KcN1LVqNchRafKmrY,79877
197
+ unique_toolkit/language_model/infos.py,sha256=Bh6-Fs_xbqqhEPRbEiudcrLPIHYPyfHuzfRZtBVs9I0,82806
195
198
  unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
196
199
  unique_toolkit/language_model/reference.py,sha256=nkX2VFz-IrUz8yqyc3G5jUMNwrNpxITBrMEKkbqqYoI,8583
197
200
  unique_toolkit/language_model/schemas.py,sha256=ATiHjhfGxoubS332XuhL9PKSoFewcWvPTUVBaNGWlJo,23994
@@ -209,7 +212,7 @@ unique_toolkit/short_term_memory/service.py,sha256=5PeVBu1ZCAfyDb2HLVvlmqSbyzBBu
209
212
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
210
213
  unique_toolkit/smart_rules/compile.py,sha256=Ozhh70qCn2yOzRWr9d8WmJeTo7AQurwd3tStgBMPFLA,1246
211
214
  unique_toolkit/test_utilities/events.py,sha256=_mwV2bs5iLjxS1ynDCjaIq-gjjKhXYCK-iy3dRfvO3g,6410
212
- unique_toolkit-1.36.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
213
- unique_toolkit-1.36.0.dist-info/METADATA,sha256=b-BMlh-JACksT29xq0D-VY8p3OZo-wXrrwJgn6Ronf0,46179
214
- unique_toolkit-1.36.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
215
- unique_toolkit-1.36.0.dist-info/RECORD,,
215
+ unique_toolkit-1.38.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
216
+ unique_toolkit-1.38.0.dist-info/METADATA,sha256=OJdgZ3Hlti9zeds9NTOC6OOibFeUsedeFjtZzVZJMTo,46376
217
+ unique_toolkit-1.38.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
218
+ unique_toolkit-1.38.0.dist-info/RECORD,,