unique_toolkit 2026.28.0.dev1__py3-none-any.whl → 2026.28.0.dev2__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,3 +1,7 @@
1
+ from unique_toolkit.agentic.tools.experimental.ask_user_tool import (
2
+ AskUserTool,
3
+ AskUserToolConfig,
4
+ )
1
5
  from unique_toolkit.agentic.tools.experimental.open_file_tool import (
2
6
  OpenFileTool,
3
7
  OpenFileToolRuntime,
@@ -19,6 +23,8 @@ from unique_toolkit.agentic.tools.experimental.todo import (
19
23
  )
20
24
 
21
25
  __all__ = [
26
+ "AskUserTool",
27
+ "AskUserToolConfig",
22
28
  "DisplayMode",
23
29
  "OpenFileTool",
24
30
  "OpenFileToolRuntime",
@@ -0,0 +1,14 @@
1
+ from unique_toolkit.agentic.tools.experimental.ask_user_tool.config import (
2
+ AskUserToolConfig,
3
+ )
4
+ from unique_toolkit.agentic.tools.experimental.ask_user_tool.tool import (
5
+ AskUserTool,
6
+ )
7
+ from unique_toolkit.agentic.tools.factory import ToolFactory
8
+
9
+ ToolFactory.register_tool(AskUserTool, AskUserToolConfig)
10
+
11
+ __all__ = [
12
+ "AskUserTool",
13
+ "AskUserToolConfig",
14
+ ]
@@ -0,0 +1,138 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import Field
4
+
5
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
6
+ from unique_toolkit.agentic.tools.schemas import BaseToolConfig
7
+ from unique_toolkit.elicitation import (
8
+ ElicitationCancelledException,
9
+ ElicitationDeclinedException,
10
+ ElicitationExpiredException,
11
+ )
12
+
13
+ _DEFAULT_TOOL_DESCRIPTION = """
14
+ Ask the user a question through a structured form and block until they answer. Use whenever you need information, a decision, or confirmation before continuing correctly: clarifying questions, confirmation before destructive or irreversible work, missing parameters, or choosing among plausible options. Do not ask the user in normal chat text while this tool is available—call AskUser, wait for the result, then continue. If unsure which interpretation, scope, or value applies, call AskUser instead of guessing. Destructive actions must include an explicit confirmation field. Form and chat anchoring are handled for you; the call blocks until the user responds, declines, cancels, or times out.
15
+ """.strip()
16
+
17
+ _DEFAULT_TOOL_DESCRIPTION_FOR_SYSTEM_PROMPT = """
18
+ - If your next message would ask the user for information, a decision, or confirmation, call AskUser instead of writing the question in chat.
19
+ - If more than one reasonable reading of the request exists, call AskUser before acting—do not silently pick a default.
20
+ - Destructive or irreversible actions require an explicit confirmation field (e.g. boolean `confirm`) in `response_schema`.
21
+ """.strip()
22
+
23
+ _DEFAULT_MESSAGE_DESCRIPTION = "The question or prompt shown to the user"
24
+
25
+ _DEFAULT_RESPONSE_SCHEMA_DESCRIPTION = """
26
+ JSON Schema object (root type "object") for the form shown to the user; you construct it. Single string property for free-text; boolean for a confirmation; string + enum for one choice (dropdown); array with items.enum for choose-many. Mark needed fields required and add short property descriptions (shown as help text). Prefer one small form over many calls.
27
+ Examples:
28
+ - free-text: {"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"]};
29
+ - confirm: {"type":"object","properties":{"confirm":{"type":"boolean"}},"required":["confirm"]};
30
+ - choice: {"type":"object","properties":{"region":{"type":"string","enum":["EU","US"]}},"required":["region"]};
31
+ - multi: {"type":"object","properties":{"topics":{"type":"array","items":{"type":"string","enum":["a","b"]}}}}.
32
+ """.strip()
33
+
34
+ _TIMEOUT_SECONDS_MIN = 1
35
+ _TIMOUT_SECONDS_MAX = 3600
36
+
37
+ _POLLING_SECONDS_MIN = 0.5
38
+ _POLLING_SECONDS_MAX = 60
39
+
40
+
41
+ class AskUserToolConfig(BaseToolConfig):
42
+ tool_description: Annotated[
43
+ str,
44
+ RJSFMetaTag.StringWidget.textarea(
45
+ rows=len(_DEFAULT_TOOL_DESCRIPTION.splitlines())
46
+ ),
47
+ ] = Field(
48
+ default=_DEFAULT_TOOL_DESCRIPTION,
49
+ description="Description shown to the model for this tool.",
50
+ )
51
+
52
+ tool_description_for_system_prompt: Annotated[
53
+ str,
54
+ RJSFMetaTag.StringWidget.textarea(
55
+ rows=len(_DEFAULT_TOOL_DESCRIPTION_FOR_SYSTEM_PROMPT.splitlines())
56
+ ),
57
+ ] = Field(
58
+ default=_DEFAULT_TOOL_DESCRIPTION_FOR_SYSTEM_PROMPT,
59
+ description="Extra system guidance about when and how to use AskUser.",
60
+ )
61
+
62
+ message_description: Annotated[
63
+ str,
64
+ RJSFMetaTag.StringWidget.textarea(
65
+ rows=len(_DEFAULT_MESSAGE_DESCRIPTION.splitlines())
66
+ ),
67
+ ] = Field(
68
+ default=_DEFAULT_MESSAGE_DESCRIPTION,
69
+ description="Description of the `message` parameter shown to the model.",
70
+ title="`message` tool input field description",
71
+ )
72
+
73
+ response_schema_description: Annotated[
74
+ str,
75
+ RJSFMetaTag.StringWidget.textarea(
76
+ rows=len(_DEFAULT_MESSAGE_DESCRIPTION.splitlines())
77
+ ),
78
+ ] = Field(
79
+ default=_DEFAULT_RESPONSE_SCHEMA_DESCRIPTION,
80
+ description="Description of the `response_schema` parameter shown to the model.",
81
+ title="`response_schema` tool input field description",
82
+ )
83
+
84
+ declined_message: Annotated[
85
+ str,
86
+ RJSFMetaTag.StringWidget.textarea(
87
+ rows=len(ElicitationDeclinedException.INSTRUCTION.splitlines())
88
+ ),
89
+ ] = Field(
90
+ default=ElicitationDeclinedException.INSTRUCTION,
91
+ description="Message returned to the model when the user declines the request.",
92
+ title="Declined response",
93
+ )
94
+
95
+ cancelled_message: Annotated[
96
+ str,
97
+ RJSFMetaTag.StringWidget.textarea(
98
+ rows=len(ElicitationCancelledException.INSTRUCTION.splitlines())
99
+ ),
100
+ ] = Field(
101
+ default=ElicitationCancelledException.INSTRUCTION,
102
+ description="Message returned to the model when the user cancels the request.",
103
+ title="Cancelled response",
104
+ )
105
+
106
+ expired_message: Annotated[
107
+ str,
108
+ RJSFMetaTag.StringWidget.textarea(
109
+ rows=len(ElicitationExpiredException.INSTRUCTION.splitlines())
110
+ ),
111
+ ] = Field(
112
+ default=ElicitationExpiredException.INSTRUCTION,
113
+ description="Message returned to the model when the request expires before the user responds.",
114
+ title="Expired response",
115
+ )
116
+
117
+ timeout_seconds: Annotated[
118
+ int,
119
+ RJSFMetaTag.NumberWidget.updown(
120
+ min=_TIMEOUT_SECONDS_MIN, max=_TIMOUT_SECONDS_MAX
121
+ ),
122
+ ] = Field(
123
+ default=300,
124
+ ge=_TIMEOUT_SECONDS_MIN,
125
+ le=_TIMOUT_SECONDS_MAX,
126
+ description="Maximum seconds to wait for the user to respond.",
127
+ )
128
+ poll_interval_seconds: Annotated[
129
+ float,
130
+ RJSFMetaTag.NumberWidget.updown(
131
+ min=_POLLING_SECONDS_MIN, max=_POLLING_SECONDS_MAX, step=0.5
132
+ ),
133
+ ] = Field(
134
+ default=1.0,
135
+ ge=_POLLING_SECONDS_MIN,
136
+ le=_POLLING_SECONDS_MAX,
137
+ description="Seconds to wait between attempts when checking whether user has responded.",
138
+ )
@@ -0,0 +1,149 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from typing import Annotated, override
5
+
6
+ from pydantic import BaseModel, Field, JsonValue, StringConstraints
7
+
8
+ from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
9
+ from unique_toolkit.agentic.tools.experimental.ask_user_tool.config import (
10
+ AskUserToolConfig,
11
+ )
12
+ from unique_toolkit.agentic.tools.schemas import ToolCallResponse
13
+ from unique_toolkit.agentic.tools.tool import Tool
14
+ from unique_toolkit.agentic.tools.tool_progress_reporter import ToolProgressReporter
15
+ from unique_toolkit.app.schemas import ChatEvent
16
+ from unique_toolkit.elicitation import (
17
+ ElicitationCancelledException,
18
+ ElicitationDeclinedException,
19
+ ElicitationExpiredException,
20
+ ElicitationMode,
21
+ )
22
+ from unique_toolkit.language_model.schemas import (
23
+ LanguageModelFunction,
24
+ LanguageModelToolDescription,
25
+ )
26
+
27
+ _LOGGER = logging.getLogger(__name__)
28
+
29
+ MessageType = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
30
+ ResponseSchemaType = dict[str, JsonValue]
31
+
32
+
33
+ class AskUserToolInput(BaseModel):
34
+ message: MessageType
35
+ response_schema: ResponseSchemaType
36
+
37
+
38
+ class AskUserTool(Tool[AskUserToolConfig]):
39
+ name = "AskUser"
40
+ DISPLAY_NAME = "Ask User"
41
+
42
+ def __init__(
43
+ self,
44
+ config: AskUserToolConfig,
45
+ event: ChatEvent,
46
+ tool_progress_reporter: ToolProgressReporter | None = None,
47
+ ) -> None:
48
+ super().__init__(
49
+ config=config, event=event, tool_progress_reporter=tool_progress_reporter
50
+ )
51
+ self._lock = asyncio.Lock()
52
+
53
+ @override
54
+ def display_name(self) -> str:
55
+ return self.DISPLAY_NAME
56
+
57
+ @override
58
+ def tool_description(self) -> LanguageModelToolDescription:
59
+ class AskUserToolInputWithDescriptions(AskUserToolInput):
60
+ message: MessageType = Field(description=self.config.message_description)
61
+ response_schema: ResponseSchemaType = Field(
62
+ description=self.config.response_schema_description
63
+ )
64
+
65
+ return LanguageModelToolDescription(
66
+ name=self.name,
67
+ description=self.config.tool_description,
68
+ parameters=AskUserToolInputWithDescriptions,
69
+ )
70
+
71
+ @override
72
+ def tool_description_for_system_prompt(self) -> str:
73
+ return self.config.tool_description_for_system_prompt
74
+
75
+ @override
76
+ def is_capability(self) -> bool:
77
+ return True
78
+
79
+ @override
80
+ def evaluation_check_list(self) -> list[EvaluationMetricName]:
81
+ return []
82
+
83
+ @override
84
+ def get_evaluation_checks_based_on_tool_response(
85
+ self,
86
+ tool_response: ToolCallResponse,
87
+ ) -> list[EvaluationMetricName]:
88
+ return []
89
+
90
+ @override
91
+ async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
92
+ # When multiple elicitation calls are created, the frontend will render them one by one
93
+ # We need to lock the execution of the tool so that the timeout starts when the user
94
+ # actually sees the elicitation call
95
+ async with self._lock:
96
+ return await self._run(tool_call)
97
+
98
+ async def _run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
99
+ params = AskUserToolInput.model_validate(tool_call.arguments)
100
+
101
+ service = self.chat_service.elicitation
102
+
103
+ _LOGGER.info("Creating elicitation request")
104
+
105
+ created = await service.create_async(
106
+ mode=ElicitationMode.FORM,
107
+ message=params.message,
108
+ tool_name=self.display_name(),
109
+ json_schema=params.response_schema,
110
+ expires_in_seconds=self.config.timeout_seconds,
111
+ )
112
+
113
+ _LOGGER.info("Elicitation request (id %s) successfully created", created.id)
114
+
115
+ try:
116
+ elicitation = await service.wait_for_response_async(
117
+ elicitation_id=created.id,
118
+ timeout_seconds=self.config.timeout_seconds,
119
+ poll_interval_seconds=self.config.poll_interval_seconds,
120
+ )
121
+ except ElicitationDeclinedException:
122
+ _LOGGER.info("Elicitation with id %s was declined", created.id)
123
+ return ToolCallResponse(
124
+ id=tool_call.id,
125
+ name=self.name,
126
+ content=self.config.declined_message,
127
+ )
128
+ except ElicitationCancelledException:
129
+ _LOGGER.info("Elicitation with id %s was cancelled", created.id)
130
+ return ToolCallResponse(
131
+ id=tool_call.id,
132
+ name=self.name,
133
+ content=self.config.cancelled_message,
134
+ )
135
+ except ElicitationExpiredException:
136
+ _LOGGER.info("Elicitation with id %s has expired", created.id)
137
+ return ToolCallResponse(
138
+ id=tool_call.id,
139
+ name=self.name,
140
+ content=self.config.expired_message,
141
+ )
142
+
143
+ _LOGGER.info("User responded to elicitation request")
144
+
145
+ return ToolCallResponse(
146
+ id=tool_call.id,
147
+ name=self.name,
148
+ content=json.dumps(elicitation.response_content, ensure_ascii=False),
149
+ )
@@ -1,7 +1,13 @@
1
+ import asyncio
1
2
  from typing import Any
2
3
 
3
4
  from unique_toolkit._common.validate_required_values import validate_required_values
4
5
  from unique_toolkit.app.schemas import ChatEvent, Correlation
6
+ from unique_toolkit.elicitation.exceptions import (
7
+ ElicitationCancelledException,
8
+ ElicitationDeclinedException,
9
+ ElicitationExpiredException,
10
+ )
5
11
  from unique_toolkit.elicitation.functions import (
6
12
  create_elicitation,
7
13
  create_elicitation_async,
@@ -18,6 +24,7 @@ from unique_toolkit.elicitation.schemas import (
18
24
  ElicitationList,
19
25
  ElicitationMode,
20
26
  ElicitationResponseResult,
27
+ ElicitationStatus,
21
28
  )
22
29
 
23
30
 
@@ -235,6 +242,45 @@ class ElicitationService:
235
242
  elicitation_id=elicitation_id,
236
243
  )
237
244
 
245
+ async def wait_for_response_async(
246
+ self,
247
+ *,
248
+ elicitation_id: str,
249
+ timeout_seconds: int,
250
+ poll_interval_seconds: float = 1.0,
251
+ ) -> Elicitation:
252
+ """
253
+ Poll an elicitation until the user accepts it.
254
+
255
+ Args:
256
+ elicitation_id (str): The elicitation ID to poll.
257
+ timeout_seconds (int): How long to wait before giving up.
258
+ poll_interval_seconds (float): Seconds between polls.
259
+
260
+ Returns:
261
+ Elicitation: The accepted elicitation.
262
+
263
+ Raises:
264
+ ElicitationDeclinedException: If the user declined.
265
+ ElicitationCancelledException: If the user cancelled.
266
+ ElicitationExpiredException: If the request expired or timed out.
267
+ """
268
+ num_trials = int(timeout_seconds / poll_interval_seconds) + 1
269
+ for _ in range(num_trials):
270
+ elicitation = await self.get_async(elicitation_id)
271
+ match elicitation.status:
272
+ case ElicitationStatus.ACCEPTED:
273
+ return elicitation
274
+ case ElicitationStatus.DECLINED:
275
+ raise ElicitationDeclinedException()
276
+ case ElicitationStatus.CANCELLED:
277
+ raise ElicitationCancelledException()
278
+ case ElicitationStatus.EXPIRED:
279
+ raise ElicitationExpiredException()
280
+ case _:
281
+ await asyncio.sleep(poll_interval_seconds)
282
+ raise ElicitationExpiredException()
283
+
238
284
  # List Pending Methods
239
285
  ############################################################################
240
286
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.28.0.dev1
3
+ Version: 2026.28.0.dev2
4
4
  Summary:
5
5
  Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
6
6
  Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
@@ -16,7 +16,7 @@ Requires-Dist: sseclient>=0.0.27,<0.0.28
16
16
  Requires-Dist: openai>=1.105.0,<3
17
17
  Requires-Dist: platformdirs>=4.0.0,<5
18
18
  Requires-Dist: pillow>=12.1.1
19
- Requires-Dist: unique-sdk>=2026.28.0.dev2,<2026.28.0rc0
19
+ Requires-Dist: unique-sdk>=2026.28.0.dev3,<2026.28.0rc0
20
20
  Requires-Dist: jambo>=0.1.2,<0.2
21
21
  Requires-Dist: jinja2>=3.1.6,<4
22
22
  Requires-Dist: docxtpl>=0.20.1,<0.21
@@ -175,7 +175,10 @@ unique_toolkit/agentic/tools/a2a/tool/service.py,sha256=mRrDYBEMYgk36xqLwtolRh9r
175
175
  unique_toolkit/agentic/tools/a2a/tool/test/test_service_utils.py,sha256=x4leRxnGgqD-XyXPkW2z1ekv4Ja7EStXd-24LGadPZA,24858
176
176
  unique_toolkit/agentic/tools/agent_chunks_hanlder.py,sha256=eO0g46wZOQlmWqYYVGiszRVe8K2LX-9qW30ZCdGg5eE,1943
177
177
  unique_toolkit/agentic/tools/config.py,sha256=LUKSENFvBuOPyllCGEmoO8Q_1HpAw4hZuTxfNmyFHWA,3905
178
- unique_toolkit/agentic/tools/experimental/__init__.py,sha256=SiH0_ELGwQUinX2qjZ4ziroJLqpmowYWLEzCxhqUm88,800
178
+ unique_toolkit/agentic/tools/experimental/__init__.py,sha256=ArEcXHGPtMRNsDnsyo9v1fev0EZWdJ6D7NWdPEFLQKM,956
179
+ unique_toolkit/agentic/tools/experimental/ask_user_tool/__init__.py,sha256=B8srRVw4vMLa4dZE5yoF1Bqpjlv8moh_m-dP2BVRjDc,375
180
+ unique_toolkit/agentic/tools/experimental/ask_user_tool/config.py,sha256=z95ZaxkMvsmjVaUYWWZrvpD21-Us1KNUrMgXzpF4uo4,5923
181
+ unique_toolkit/agentic/tools/experimental/ask_user_tool/tool.py,sha256=yTjONO1Ieh3YsHJzebNstd73YoK40V6wG9o_8-avk30,5111
179
182
  unique_toolkit/agentic/tools/experimental/open_file_tool/__init__.py,sha256=hPkz1tbiup8-QM62QMLWA1PpUmzX5VsNUnXsXw1_dMY,319
180
183
  unique_toolkit/agentic/tools/experimental/open_file_tool/config.py,sha256=FQylVpCkOw06PSnfODOBwWARRrAI7UoUELzApWNVb2w,4342
181
184
  unique_toolkit/agentic/tools/experimental/open_file_tool/runtime.py,sha256=SOIFV2N8USPGHNBnzId2nJPhZiJ3kGO5AwAiKJBNhdI,14383
@@ -269,7 +272,7 @@ unique_toolkit/elicitation/constants.py,sha256=ab24uagL6ynOKQrLMUChkFxI7dUyBYCoh
269
272
  unique_toolkit/elicitation/exceptions.py,sha256=hF17eW4GGrtHw0lo77YfZdyIy65SGaWhxoJPx46GK7c,4082
270
273
  unique_toolkit/elicitation/functions.py,sha256=RWIglWH9SpiGunqiU7hO2TXkqASkAfk4CfJ3m4uJewU,11417
271
274
  unique_toolkit/elicitation/schemas.py,sha256=ugss-r0r8O9XwGpYAkklnLDLvp1XVyzHS521HU8BOjY,5735
272
- unique_toolkit/elicitation/service.py,sha256=bzS6RC27z1fX8_eXBgH8WursZ1FJJt-yTMCNEh7Xj0g,10793
275
+ unique_toolkit/elicitation/service.py,sha256=cdJ8XVQhvhb_AkZQyd7IrkZ9ktLKs_TZlglNIpcUD-0,12507
273
276
  unique_toolkit/embedding/__init__.py,sha256=uUyzjonPvuDCYsvXCIt7ErQXopLggpzX-MEQd3_e2kE,250
274
277
  unique_toolkit/embedding/constants.py,sha256=Lj8-Lcy1FvuC31PM9Exq7vaFuxQV4pEI1huUMFX-J2M,52
275
278
  unique_toolkit/embedding/functions.py,sha256=OWzY-iPNBjwXgSNNXDmu89Lzu-PAMG3Q7igtUcFrcc8,1944
@@ -399,6 +402,6 @@ unique_toolkit/short_term_memory/service.py,sha256=Qo-c9yxzC1ZEEHJyIG_vnRs_ef0SK
399
402
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
400
403
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
401
404
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
402
- unique_toolkit-2026.28.0.dev1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
403
- unique_toolkit-2026.28.0.dev1.dist-info/METADATA,sha256=9_-Pt8xbGisiMP1DrYdlrlrn5yIYHbS0Bbo_5ZOWn-Q,7426
404
- unique_toolkit-2026.28.0.dev1.dist-info/RECORD,,
405
+ unique_toolkit-2026.28.0.dev2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
406
+ unique_toolkit-2026.28.0.dev2.dist-info/METADATA,sha256=-L08qC8wJBPOovWUmGDdBRkhEdhxxYe-8LGLw6dW_Rs,7426
407
+ unique_toolkit-2026.28.0.dev2.dist-info/RECORD,,