unique_toolkit 1.38.3__py3-none-any.whl → 1.39.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.
@@ -1,25 +1,35 @@
1
+ from unique_toolkit.agentic.loop_runner._iteration_handler_utils import (
2
+ handle_forced_tools_iteration,
3
+ handle_last_iteration,
4
+ handle_normal_iteration,
5
+ )
1
6
  from unique_toolkit.agentic.loop_runner.base import LoopIterationRunner
2
7
  from unique_toolkit.agentic.loop_runner.middleware import (
3
- QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION,
4
8
  PlanningConfig,
5
9
  PlanningMiddleware,
6
10
  PlanningSchemaConfig,
7
- QwenForcedToolCallMiddleware,
8
- is_qwen_model,
9
11
  )
10
12
  from unique_toolkit.agentic.loop_runner.runners import (
13
+ QWEN_FORCED_TOOL_CALL_INSTRUCTION,
14
+ QWEN_LAST_ITERATION_INSTRUCTION,
11
15
  BasicLoopIterationRunner,
12
16
  BasicLoopIterationRunnerConfig,
17
+ QwenLoopIterationRunner,
18
+ is_qwen_model,
13
19
  )
14
20
 
15
21
  __all__ = [
16
22
  "LoopIterationRunner",
17
- "QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION",
18
23
  "PlanningConfig",
19
24
  "PlanningMiddleware",
20
25
  "PlanningSchemaConfig",
21
- "QwenForcedToolCallMiddleware",
26
+ "QwenLoopIterationRunner",
22
27
  "is_qwen_model",
23
28
  "BasicLoopIterationRunnerConfig",
24
29
  "BasicLoopIterationRunner",
30
+ "handle_forced_tools_iteration",
31
+ "handle_last_iteration",
32
+ "handle_normal_iteration",
33
+ "QWEN_FORCED_TOOL_CALL_INSTRUCTION",
34
+ "QWEN_LAST_ITERATION_INSTRUCTION",
25
35
  ]
@@ -0,0 +1,95 @@
1
+ import logging
2
+ from typing import Protocol, Unpack, cast
3
+
4
+ from unique_toolkit.agentic.loop_runner._stream_handler_utils import stream_response
5
+ from unique_toolkit.agentic.loop_runner.base import (
6
+ _LoopIterationRunnerKwargs,
7
+ )
8
+ from unique_toolkit.chat.functions import LanguageModelStreamResponse
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ class PrepareForcedToolIterationKwargs(Protocol):
14
+ def __call__(
15
+ self,
16
+ func_name: str | None,
17
+ per_choice_kwargs: _LoopIterationRunnerKwargs,
18
+ ) -> _LoopIterationRunnerKwargs: ...
19
+
20
+
21
+ async def handle_last_iteration(
22
+ **kwargs: Unpack[_LoopIterationRunnerKwargs],
23
+ ) -> LanguageModelStreamResponse:
24
+ _LOGGER.info("Reached last iteration, removing tools and producing final response")
25
+
26
+ return await stream_response(
27
+ loop_runner_kwargs=kwargs,
28
+ tools=None,
29
+ )
30
+
31
+
32
+ async def handle_normal_iteration(
33
+ **kwargs: Unpack[_LoopIterationRunnerKwargs],
34
+ ) -> LanguageModelStreamResponse:
35
+ _LOGGER.info("Running loop iteration %d", kwargs["iteration_index"])
36
+
37
+ return await stream_response(loop_runner_kwargs=kwargs)
38
+
39
+
40
+ async def handle_forced_tools_iteration(
41
+ **kwargs: Unpack[_LoopIterationRunnerKwargs],
42
+ ) -> LanguageModelStreamResponse:
43
+ return await run_forced_tools_iteration(loop_runner_kwargs=kwargs)
44
+
45
+
46
+ async def run_forced_tools_iteration(
47
+ *,
48
+ loop_runner_kwargs: _LoopIterationRunnerKwargs,
49
+ prepare_loop_runner_kwargs: PrepareForcedToolIterationKwargs | None = None,
50
+ ) -> LanguageModelStreamResponse:
51
+ """
52
+ Execute a "forced tools" iteration by running one stream_response per tool choice,
53
+ then merging tool calls and references into a single response.
54
+
55
+ Some models (e.g. Qwen) need per-tool-choice message rewriting; this can be done
56
+ via prepare_loop_runner_kwargs (called once per tool choice, on a copy of kwargs).
57
+ """
58
+ assert "tool_choices" in loop_runner_kwargs
59
+
60
+ tool_choices = loop_runner_kwargs["tool_choices"]
61
+ assert len(tool_choices) > 0, (
62
+ "run_forced_tools_iteration requires at least one tool choice"
63
+ )
64
+ _LOGGER.info("Forcing tools calls: %s", tool_choices)
65
+
66
+ responses: list[LanguageModelStreamResponse] = []
67
+
68
+ available_tools = {t.name: t for t in loop_runner_kwargs.get("tools") or []}
69
+
70
+ for opt in tool_choices:
71
+ func_name = opt.get("function", {}).get("name")
72
+
73
+ per_choice_kwargs = cast(_LoopIterationRunnerKwargs, dict(loop_runner_kwargs))
74
+ if prepare_loop_runner_kwargs:
75
+ per_choice_kwargs = prepare_loop_runner_kwargs(func_name, per_choice_kwargs)
76
+
77
+ limited_tool = available_tools.get(func_name) if func_name else None
78
+ stream_kwargs = {"loop_runner_kwargs": per_choice_kwargs, "tool_choice": opt}
79
+ if limited_tool:
80
+ stream_kwargs["tools"] = [limited_tool]
81
+ responses.append(await stream_response(**stream_kwargs))
82
+
83
+ # Merge responses and refs:
84
+ tool_calls = []
85
+ references = []
86
+ for r in responses:
87
+ if r.tool_calls:
88
+ tool_calls.extend(r.tool_calls)
89
+ references.extend(r.message.references)
90
+
91
+ response = responses[0]
92
+ response.tool_calls = tool_calls if len(tool_calls) > 0 else None
93
+ response.message.references = references
94
+
95
+ return response
@@ -3,17 +3,9 @@ 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
- )
11
6
 
12
7
  __all__ = [
13
8
  "PlanningConfig",
14
9
  "PlanningMiddleware",
15
10
  "PlanningSchemaConfig",
16
- "QWEN_FORCED_TOOL_CALL_PROMPT_INSTRUCTION",
17
- "QwenForcedToolCallMiddleware",
18
- "is_qwen_model",
19
11
  ]
@@ -2,5 +2,18 @@ from unique_toolkit.agentic.loop_runner.runners.basic import (
2
2
  BasicLoopIterationRunner,
3
3
  BasicLoopIterationRunnerConfig,
4
4
  )
5
+ from unique_toolkit.agentic.loop_runner.runners.qwen import (
6
+ QWEN_FORCED_TOOL_CALL_INSTRUCTION,
7
+ QWEN_LAST_ITERATION_INSTRUCTION,
8
+ QwenLoopIterationRunner,
9
+ is_qwen_model,
10
+ )
5
11
 
6
- __all__ = ["BasicLoopIterationRunnerConfig", "BasicLoopIterationRunner"]
12
+ __all__ = [
13
+ "BasicLoopIterationRunnerConfig",
14
+ "BasicLoopIterationRunner",
15
+ "QwenLoopIterationRunner",
16
+ "QWEN_FORCED_TOOL_CALL_INSTRUCTION",
17
+ "QWEN_LAST_ITERATION_INSTRUCTION",
18
+ "is_qwen_model",
19
+ ]
@@ -4,7 +4,11 @@ from typing import Unpack, override
4
4
  from pydantic import BaseModel
5
5
 
6
6
  from unique_toolkit._common.pydantic_helpers import get_configuration_dict
7
- from unique_toolkit.agentic.loop_runner._stream_handler_utils import stream_response
7
+ from unique_toolkit.agentic.loop_runner._iteration_handler_utils import (
8
+ handle_forced_tools_iteration,
9
+ handle_last_iteration,
10
+ handle_normal_iteration,
11
+ )
8
12
  from unique_toolkit.agentic.loop_runner.base import (
9
13
  LoopIterationRunner,
10
14
  _LoopIterationRunnerKwargs,
@@ -26,60 +30,6 @@ class BasicLoopIterationRunner(LoopIterationRunner):
26
30
  def __init__(self, config: BasicLoopIterationRunnerConfig) -> None:
27
31
  self._config = config
28
32
 
29
- async def _handle_last_iteration(
30
- self, **kwargs: Unpack[_LoopIterationRunnerKwargs]
31
- ) -> LanguageModelStreamResponse:
32
- _LOGGER.info(
33
- "Reached last iteration, removing tools and producing final response"
34
- )
35
-
36
- return await stream_response(
37
- loop_runner_kwargs=kwargs,
38
- tools=None,
39
- )
40
-
41
- async def _handle_normal_iteration(
42
- self, **kwargs: Unpack[_LoopIterationRunnerKwargs]
43
- ) -> LanguageModelStreamResponse:
44
- _LOGGER.info("Running loop iteration %d", kwargs["iteration_index"])
45
-
46
- return await stream_response(loop_runner_kwargs=kwargs)
47
-
48
- async def _handle_forced_tools_iteration(
49
- self,
50
- **kwargs: Unpack[_LoopIterationRunnerKwargs],
51
- ) -> LanguageModelStreamResponse:
52
- assert "tool_choices" in kwargs
53
-
54
- tool_choices = kwargs["tool_choices"]
55
- _LOGGER.info("Forcing tools calls: %s", tool_choices)
56
-
57
- responses: list[LanguageModelStreamResponse] = []
58
-
59
- available_tools = {t.name: t for t in kwargs.get("tools") or []}
60
-
61
- for opt in tool_choices:
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))
68
-
69
- # Merge responses and refs:
70
- tool_calls = []
71
- references = []
72
- for r in responses:
73
- if r.tool_calls:
74
- tool_calls.extend(r.tool_calls)
75
- references.extend(r.message.references)
76
-
77
- response = responses[0]
78
- response.tool_calls = tool_calls if len(tool_calls) > 0 else None
79
- response.message.references = references
80
-
81
- return response
82
-
83
33
  @override
84
34
  async def __call__(
85
35
  self,
@@ -89,8 +39,8 @@ class BasicLoopIterationRunner(LoopIterationRunner):
89
39
  iteration_index = kwargs["iteration_index"]
90
40
 
91
41
  if len(tool_choices) > 0 and iteration_index == 0:
92
- return await self._handle_forced_tools_iteration(**kwargs)
42
+ return await handle_forced_tools_iteration(**kwargs)
93
43
  elif iteration_index == self._config.max_loop_iterations - 1:
94
- return await self._handle_last_iteration(**kwargs)
44
+ return await handle_last_iteration(**kwargs)
95
45
  else:
96
- return await self._handle_normal_iteration(**kwargs)
46
+ return await handle_normal_iteration(**kwargs)
@@ -0,0 +1,15 @@
1
+ from unique_toolkit.agentic.loop_runner.runners.qwen.helpers import (
2
+ is_qwen_model,
3
+ )
4
+ from unique_toolkit.agentic.loop_runner.runners.qwen.qwen_runner import (
5
+ QWEN_FORCED_TOOL_CALL_INSTRUCTION,
6
+ QWEN_LAST_ITERATION_INSTRUCTION,
7
+ QwenLoopIterationRunner,
8
+ )
9
+
10
+ __all__ = [
11
+ "QwenLoopIterationRunner",
12
+ "is_qwen_model",
13
+ "QWEN_FORCED_TOOL_CALL_INSTRUCTION",
14
+ "QWEN_LAST_ITERATION_INSTRUCTION",
15
+ ]
@@ -1,5 +1,6 @@
1
1
  from unique_toolkit.language_model.infos import LanguageModelInfo
2
2
  from unique_toolkit.language_model.schemas import (
3
+ LanguageModelAssistantMessage,
3
4
  LanguageModelMessageRole,
4
5
  LanguageModelMessages,
5
6
  )
@@ -31,3 +32,17 @@ def append_qwen_forced_tool_call_instruction(
31
32
  )
32
33
  break
33
34
  return LanguageModelMessages(root=messages_list)
35
+
36
+
37
+ def append_qwen_last_iteration_assistant_message(
38
+ *,
39
+ messages: LanguageModelMessages,
40
+ last_iteration_instruction: str,
41
+ ) -> LanguageModelMessages:
42
+ """Append an assistant message at the end to indicate no further tool calls are allowed."""
43
+ messages_list = list(messages)
44
+ assistant_message = LanguageModelAssistantMessage(
45
+ content=last_iteration_instruction,
46
+ )
47
+ messages_list.append(assistant_message)
48
+ return LanguageModelMessages(root=messages_list)
@@ -0,0 +1,118 @@
1
+ import logging
2
+ from typing import Unpack
3
+
4
+ from unique_toolkit.agentic.loop_runner._iteration_handler_utils import (
5
+ handle_last_iteration,
6
+ handle_normal_iteration,
7
+ run_forced_tools_iteration,
8
+ )
9
+ from unique_toolkit.agentic.loop_runner.base import (
10
+ LoopIterationRunner,
11
+ _LoopIterationRunnerKwargs,
12
+ )
13
+ from unique_toolkit.agentic.loop_runner.runners.qwen.helpers import (
14
+ append_qwen_forced_tool_call_instruction,
15
+ append_qwen_last_iteration_assistant_message,
16
+ )
17
+ from unique_toolkit.chat.service import ChatService, LanguageModelStreamResponse
18
+
19
+ _LOGGER = logging.getLogger(__name__)
20
+
21
+ QWEN_FORCED_TOOL_CALL_INSTRUCTION = (
22
+ "**Tool Call Instruction:** \nYou MUST call the tool {TOOL_NAME}. "
23
+ "You must start the response with <tool_call>. "
24
+ "Do NOT provide natural language explanations, summaries, or any text outside the <tool_call> block."
25
+ )
26
+
27
+ QWEN_LAST_ITERATION_INSTRUCTION = "The maximum number of loop iteration have been reached. Not further tool calls are allowed. Based on the found information, an answer should be generated"
28
+
29
+
30
+ class QwenLoopIterationRunner(LoopIterationRunner):
31
+ def __init__(
32
+ self,
33
+ *,
34
+ qwen_forced_tool_call_instruction: str,
35
+ qwen_last_iteration_instruction: str,
36
+ max_loop_iterations: int,
37
+ chat_service: ChatService,
38
+ ) -> None:
39
+ self._qwen_forced_tool_call_instruction = qwen_forced_tool_call_instruction
40
+ self._qwen_last_iteration_instruction = qwen_last_iteration_instruction
41
+ self._max_loop_iterations = max_loop_iterations
42
+ self._chat_service = chat_service
43
+
44
+ async def __call__(
45
+ self, **kwargs: Unpack[_LoopIterationRunnerKwargs]
46
+ ) -> LanguageModelStreamResponse:
47
+ tool_choices = kwargs.get("tool_choices") or []
48
+ iteration_index = kwargs["iteration_index"]
49
+
50
+ if len(tool_choices) > 0 and iteration_index == 0:
51
+ return await self._qwen_handle_forced_tools_iteration(**kwargs)
52
+ elif iteration_index == self._max_loop_iterations - 1:
53
+ return await self._qwen_handle_last_iteration(**kwargs)
54
+ else:
55
+ return await self._qwen_handle_normal_iteration(**kwargs)
56
+
57
+ async def _qwen_handle_forced_tools_iteration(
58
+ self, **kwargs: Unpack[_LoopIterationRunnerKwargs]
59
+ ) -> LanguageModelStreamResponse:
60
+ # For Qwen models, append tool call instruction to the last user message. These models ignore the parameter tool_choice.
61
+ # As the message has to be modified for each tool call instruction, the function from the basic runner cant be used.
62
+ original_messages = kwargs["messages"].model_copy(deep=True)
63
+
64
+ def _prepare(
65
+ func_name: str | None,
66
+ per_choice_kwargs: _LoopIterationRunnerKwargs,
67
+ ) -> _LoopIterationRunnerKwargs:
68
+ prompt_instruction = self._qwen_forced_tool_call_instruction.format(
69
+ TOOL_NAME=func_name or ""
70
+ )
71
+ per_choice_kwargs["messages"] = append_qwen_forced_tool_call_instruction(
72
+ messages=original_messages,
73
+ forced_tool_call_instruction=prompt_instruction,
74
+ )
75
+ return per_choice_kwargs
76
+
77
+ response = await run_forced_tools_iteration(
78
+ loop_runner_kwargs=kwargs,
79
+ prepare_loop_runner_kwargs=_prepare,
80
+ )
81
+ return self._process_response(response)
82
+
83
+ async def _qwen_handle_last_iteration(
84
+ self, **kwargs: Unpack[_LoopIterationRunnerKwargs]
85
+ ) -> LanguageModelStreamResponse:
86
+ # For Qwen models, append an assistant message with instructions to not call any tool in this iteration.
87
+ _LOGGER.info(
88
+ "Reached last iteration, removing tools. Appending assistant message with instructions to not call any tool in this iteration."
89
+ )
90
+ kwargs["messages"] = append_qwen_last_iteration_assistant_message(
91
+ messages=kwargs["messages"],
92
+ last_iteration_instruction=self._qwen_last_iteration_instruction,
93
+ )
94
+
95
+ response = await handle_last_iteration(
96
+ **kwargs,
97
+ )
98
+
99
+ return self._process_response(response)
100
+
101
+ async def _qwen_handle_normal_iteration(
102
+ self, **kwargs: Unpack[_LoopIterationRunnerKwargs]
103
+ ) -> LanguageModelStreamResponse:
104
+ response = await handle_normal_iteration(**kwargs)
105
+ return self._process_response(response)
106
+
107
+ def _process_response(
108
+ self, response: LanguageModelStreamResponse
109
+ ) -> LanguageModelStreamResponse:
110
+ # Check if content of response is </tool_call>
111
+ if "</tool_call>" == response.message.text.strip():
112
+ _LOGGER.warning(
113
+ "Response contains only <tool_call>. This is not allowed. Returning empty response."
114
+ )
115
+ self._chat_service.modify_assistant_message(content="")
116
+ response.message.text = ""
117
+
118
+ return response
@@ -120,11 +120,15 @@ class McpServer(BaseModel):
120
120
  class ChatEventUserMessage(BaseModel):
121
121
  model_config = model_config
122
122
 
123
- id: str
124
- text: str
125
- original_text: str
126
- created_at: str
127
- language: str
123
+ id: str = Field(
124
+ description="The id of the user message. On an event this corresponds to the user message that created the event."
125
+ )
126
+ text: str = Field(description="The text of the user message.")
127
+ original_text: str = Field(description="The original text of the user message.")
128
+ created_at: str = Field(
129
+ description="The creation date and time of the user message."
130
+ )
131
+ language: str = Field(description="The language of the user message.")
128
132
 
129
133
 
130
134
  @deprecated(
@@ -140,8 +144,12 @@ class EventUserMessage(ChatEventUserMessage):
140
144
  class ChatEventAssistantMessage(BaseModel):
141
145
  model_config = model_config
142
146
 
143
- id: str
144
- created_at: str
147
+ id: str = Field(
148
+ description="The id of the assistant message. On an event this corresponds to the assistant message that will be returned by the process handling the event."
149
+ )
150
+ created_at: str = Field(
151
+ description="The creation date and time of the assistant message."
152
+ )
145
153
 
146
154
 
147
155
  @deprecated(