unique_toolkit 1.16.0__py3-none-any.whl → 1.16.2__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.
@@ -280,9 +280,9 @@ if __name__ == "__main__":
280
280
  class CombinedParams(GetUserPathParams, GetUserRequestBody):
281
281
  pass
282
282
 
283
- UserEndpoint = build_api_operation(
283
+ UserApiOperation = build_api_operation(
284
284
  method=EndpointMethods.GET,
285
- url_template=Template("https://api.example.com/users/{user_id}"),
285
+ path_template=Template("/users/{user_id}"),
286
286
  path_params_constructor=GetUserPathParams,
287
287
  payload_constructor=GetUserRequestBody,
288
288
  response_model_type=UserResponse,
@@ -290,7 +290,7 @@ if __name__ == "__main__":
290
290
 
291
291
  human_verification_manager = HumanVerificationManagerForApiCalling(
292
292
  logger=logging.getLogger(__name__),
293
- operation=UserEndpoint,
293
+ operation=UserApiOperation,
294
294
  requestor_type=RequestorType.FAKE,
295
295
  return_value={"id": 100, "name": "John Doe"},
296
296
  )
@@ -4,7 +4,7 @@ the endpoints without having to know the details of the endpoints.
4
4
  """
5
5
 
6
6
  from enum import StrEnum
7
- from string import Formatter, Template
7
+ from string import Template
8
8
  from typing import (
9
9
  Any,
10
10
  Callable,
@@ -16,7 +16,6 @@ from typing import (
16
16
  )
17
17
 
18
18
  from pydantic import BaseModel
19
- from typing_extensions import deprecated
20
19
 
21
20
  # Paramspecs
22
21
  PayloadParamSpec = ParamSpec("PayloadParamSpec")
@@ -67,13 +66,12 @@ class ApiOperationProtocol(
67
66
  def response_model() -> type[ResponseType]: ...
68
67
 
69
68
  @staticmethod
70
- def create_url(
69
+ def create_path(
71
70
  *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
72
71
  ) -> str: ...
73
72
 
74
73
  @staticmethod
75
- def create_url_from_model(path_params: PathParamsType) -> str: ...
76
-
74
+ def create_path_from_model(path_params: PathParamsType) -> str: ...
77
75
  @staticmethod
78
76
  def create_payload(
79
77
  *args: PayloadParamSpec.args, **kwargs: PayloadParamSpec.kwargs
@@ -98,7 +96,7 @@ class ApiOperationProtocol(
98
96
  def build_api_operation(
99
97
  *,
100
98
  method: HttpMethods,
101
- url_template: Template,
99
+ path_template: Template,
102
100
  path_params_constructor: Callable[PathParamsSpec, PathParamsType],
103
101
  payload_constructor: Callable[PayloadParamSpec, PayloadType],
104
102
  response_model_type: type[ResponseType],
@@ -143,33 +141,19 @@ def build_api_operation(
143
141
  return response_model_type
144
142
 
145
143
  @staticmethod
146
- def create_url(
147
- *args: PathParamsSpec.args,
148
- **kwargs: PathParamsSpec.kwargs,
149
- ) -> str:
150
- """Create URL from path parameters."""
151
- path_model = Operation.path_params_model()(*args, **kwargs)
152
- path_dict = path_model.model_dump(**dump_options)
153
-
154
- # Extract expected path parameters from template
155
- template_params = [
156
- fname
157
- for _, fname, _, _ in Formatter().parse(url_template.template)
158
- if fname is not None
159
- ]
160
-
161
- # Verify all required path parameters are present
162
- missing_params = [
163
- param for param in template_params if param not in path_dict
164
- ]
165
- if missing_params:
166
- raise ValueError(f"Missing path parameters: {missing_params}")
167
-
168
- return url_template.substitute(**path_dict)
144
+ def path_template() -> Template:
145
+ return path_template
169
146
 
170
147
  @staticmethod
171
- def create_url_from_model(path_params: PathParamsType) -> str:
172
- return url_template.substitute(**path_params.model_dump(**dump_options))
148
+ def create_path_from_model(path_params: PathParamsType) -> str:
149
+ return path_template.substitute(**path_params.model_dump(**dump_options))
150
+
151
+ @staticmethod
152
+ def create_path(
153
+ *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
154
+ ) -> str:
155
+ model = Operation.path_params_model()(*args, **kwargs)
156
+ return Operation.create_path_from_model(model)
173
157
 
174
158
  @staticmethod
175
159
  def create_payload(
@@ -204,81 +188,3 @@ def build_api_operation(
204
188
  return path_params, payload
205
189
 
206
190
  return Operation
207
-
208
-
209
- @deprecated("Use ApiOperationProtocol instead")
210
- class EndpointClassProtocol(ApiOperationProtocol):
211
- pass
212
-
213
-
214
- @deprecated("Use build_api_operation instead")
215
- def build_endpoint_class(
216
- *,
217
- method: HttpMethods,
218
- url_template: Template,
219
- path_params_constructor: Callable[PathParamsSpec, PathParamsType],
220
- payload_constructor: Callable[PayloadParamSpec, PayloadType],
221
- response_model_type: type[ResponseType],
222
- dump_options: dict | None = None,
223
- ) -> type[
224
- ApiOperationProtocol[
225
- PathParamsSpec,
226
- PathParamsType,
227
- PayloadParamSpec,
228
- PayloadType,
229
- ResponseType,
230
- ]
231
- ]:
232
- return build_api_operation(
233
- method=method,
234
- url_template=url_template,
235
- path_params_constructor=path_params_constructor,
236
- payload_constructor=payload_constructor,
237
- response_model_type=response_model_type,
238
- dump_options=dump_options,
239
- )
240
-
241
-
242
- if __name__ == "__main__":
243
- # Example models
244
- class GetUserPathParams(BaseModel):
245
- """Path parameters for the user endpoint."""
246
-
247
- user_id: int
248
-
249
- class GetUserRequestBody(BaseModel):
250
- """Request body/query parameters for the user endpoint."""
251
-
252
- include_profile: bool = False
253
-
254
- class UserResponse(BaseModel):
255
- """Response model for user data."""
256
-
257
- id: int
258
- name: str
259
-
260
- # Example usage of make_endpoint_class
261
- UserOperation = build_endpoint_class(
262
- url_template=Template("/users/${user_id}"),
263
- path_params_constructor=GetUserPathParams,
264
- payload_constructor=GetUserRequestBody,
265
- response_model_type=UserResponse,
266
- method=EndpointMethods.GET,
267
- )
268
-
269
- # Create URL from path parameters
270
- url = UserOperation.create_url(user_id=123)
271
- print(f"URL: {url}")
272
-
273
- # Create payload from request body parameters
274
- payload = UserOperation.create_payload(include_profile=True)
275
- print(f"Payload: {payload}")
276
-
277
- # Create response from endpoint
278
- response = UserOperation.handle_response(
279
- {
280
- "id": 123,
281
- "name": "John Doe",
282
- }
283
- )
284
- print(f"Response: {response}")
@@ -2,7 +2,7 @@ from enum import StrEnum
2
2
  from typing import Any, Callable, Generic, Protocol, TypeVar
3
3
  from urllib.parse import urljoin, urlparse
4
4
 
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, Field
6
6
  from typing_extensions import ParamSpec
7
7
 
8
8
  from unique_toolkit._common.endpoint_builder import (
@@ -25,25 +25,15 @@ CombinedParamsType = TypeVar("CombinedParamsType", bound=BaseModel)
25
25
  ResponseT_co = TypeVar("ResponseT_co", bound=BaseModel, covariant=True)
26
26
 
27
27
 
28
- def _construct_full_url(base_url: str, url: str) -> str:
29
- """
30
- Construct full URL from base_url and url.
31
- If base_url is provided and url is absolute, strip the scheme/netloc from url.
32
- """
33
- if not base_url:
34
- return url
35
-
36
- parsed = urlparse(url)
37
- if parsed.scheme:
38
- # URL is absolute, extract only path + query + fragment
39
- url = parsed._replace(scheme="", netloc="").geturl()
28
+ class RequestContext(BaseModel):
29
+ base_url: str
30
+ headers: dict[str, str] = Field(default_factory=dict)
40
31
 
41
- return urljoin(base_url, url)
42
32
 
43
-
44
- class RequestContext(BaseModel):
45
- base_url: str = ""
46
- headers: dict[str, str] | None = None
33
+ def _verify_url(url: str) -> None:
34
+ parse_result = urlparse(url)
35
+ if not (parse_result.netloc and parse_result.scheme):
36
+ raise ValueError("Scheme and netloc are required for url")
47
37
 
48
38
 
49
39
  class EndpointRequestorProtocol(Protocol, Generic[CombinedParamsSpec, ResponseT_co]):
@@ -142,12 +132,15 @@ def build_request_requestor(
142
132
  combined=kwargs
143
133
  )
144
134
 
145
- url = cls._operation.create_url_from_model(path_params)
135
+ path = cls._operation.create_path_from_model(path_params)
136
+ url = urljoin(context.base_url, path)
137
+ _verify_url(url)
138
+
146
139
  payload = cls._operation.create_payload_from_model(payload_model)
147
140
 
148
141
  response = requests.request(
149
142
  method=cls._operation.request_method(),
150
- url=_construct_full_url(context.base_url, url),
143
+ url=url,
151
144
  headers=context.headers,
152
145
  json=payload,
153
146
  )
@@ -198,13 +191,13 @@ def build_httpx_requestor(
198
191
  combined=kwargs
199
192
  )
200
193
 
194
+ path = cls._operation.create_path_from_model(path_params)
195
+ url = urljoin(context.base_url, path)
196
+ _verify_url(url)
201
197
  with httpx.Client() as client:
202
198
  response = client.request(
203
199
  method=cls._operation.request_method(),
204
- url=_construct_full_url(
205
- base_url=context.base_url,
206
- url=cls._operation.create_url_from_model(path_params),
207
- ),
200
+ url=url,
208
201
  headers=headers,
209
202
  json=cls._operation.create_payload_from_model(payload_model),
210
203
  )
@@ -223,13 +216,13 @@ def build_httpx_requestor(
223
216
  combined=kwargs
224
217
  )
225
218
 
219
+ path = cls._operation.create_path_from_model(path_params)
220
+ url = urljoin(context.base_url, path)
221
+ _verify_url(url)
226
222
  async with httpx.AsyncClient() as client:
227
223
  response = await client.request(
228
224
  method=cls._operation.request_method(),
229
- url=_construct_full_url(
230
- base_url=context.base_url,
231
- url=cls._operation.create_url_from_model(path_params),
232
- ),
225
+ url=url,
233
226
  headers=headers,
234
227
  json=cls._operation.create_payload_from_model(payload_model),
235
228
  )
@@ -279,14 +272,14 @@ def build_aiohttp_requestor(
279
272
  path_params, payload_model = cls._operation.models_from_combined(
280
273
  combined=kwargs
281
274
  )
275
+ path = cls._operation.create_path_from_model(path_params)
276
+ url = urljoin(context.base_url, path)
277
+ _verify_url(url)
282
278
 
283
279
  async with aiohttp.ClientSession() as session:
284
280
  response = await session.request(
285
281
  method=cls._operation.request_method(),
286
- url=_construct_full_url(
287
- base_url=context.base_url,
288
- url=cls._operation.create_url_from_model(path_params),
289
- ),
282
+ url=url,
290
283
  headers=headers,
291
284
  json=cls._operation.create_payload_from_model(payload_model),
292
285
  )
@@ -360,7 +353,7 @@ if __name__ == "__main__":
360
353
 
361
354
  UserEndpoint = build_api_operation(
362
355
  method=HttpMethods.GET,
363
- url_template=Template("https://api.example.com/users/{user_id}"),
356
+ path_template=Template("/users/{user_id}"),
364
357
  path_params_constructor=GetUserPathParams,
365
358
  payload_constructor=GetUserRequestBody,
366
359
  response_model_type=UserResponse,
@@ -1,3 +1,5 @@
1
+ from typing import Any
2
+
1
3
  from unique_toolkit.agentic.tools.schemas import ToolCallResponse
2
4
 
3
5
 
@@ -5,14 +7,22 @@ class DebugInfoManager:
5
7
  def __init__(self):
6
8
  self.debug_info = {"tools": []}
7
9
 
8
- def extract_tool_debug_info(self, tool_call_responses: list[ToolCallResponse]):
10
+ def extract_tool_debug_info(
11
+ self,
12
+ tool_call_responses: list[ToolCallResponse],
13
+ loop_iteration_index: int | None = None,
14
+ ):
9
15
  for tool_call_response in tool_call_responses:
10
- self.debug_info["tools"].append(
11
- {"name": tool_call_response.name, "data": tool_call_response.debug_info}
12
- )
16
+ tool_info = {
17
+ "name": tool_call_response.name,
18
+ "info": tool_call_response.debug_info,
19
+ }
20
+ if loop_iteration_index is not None:
21
+ tool_info["info"]["loop_iteration"] = loop_iteration_index
22
+ self.debug_info["tools"].append(tool_info)
13
23
 
14
- def add(self, key, value):
24
+ def add(self, key: str, value: Any) -> None:
15
25
  self.debug_info = self.debug_info | {key: value}
16
26
 
17
- def get(self):
27
+ def get(self) -> dict[str, Any]:
18
28
  return self.debug_info
@@ -0,0 +1,278 @@
1
+ """
2
+ Test suite for DebugInfoManager class.
3
+
4
+ This test suite validates the DebugInfoManager's ability to:
5
+ 1. Initialize with empty debug info
6
+ 2. Extract tool debug info from ToolCallResponse objects
7
+ 3. Handle loop iteration indices
8
+ 4. Add arbitrary key-value pairs to debug info
9
+ 5. Retrieve the complete debug info dictionary
10
+ """
11
+
12
+ from unique_toolkit.agentic.debug_info_manager.debug_info_manager import (
13
+ DebugInfoManager,
14
+ )
15
+ from unique_toolkit.agentic.tools.schemas import ToolCallResponse
16
+
17
+
18
+ class TestDebugInfoManager:
19
+ """Test suite for DebugInfoManager functionality."""
20
+
21
+ def test_init__initializes_empty_debug_info__on_creation(self):
22
+ """Test that DebugInfoManager initializes with empty tools list."""
23
+ manager = DebugInfoManager()
24
+
25
+ assert manager.debug_info == {"tools": []}
26
+ assert manager.get() == {"tools": []}
27
+
28
+ def test_extract_tool_debug_info__adds_single_tool__with_valid_response(self):
29
+ """Test extracting debug info from a single ToolCallResponse."""
30
+ manager = DebugInfoManager()
31
+ tool_call_response = ToolCallResponse(
32
+ id="tool_1",
33
+ name="TestTool",
34
+ debug_info={"execution_time": "100ms", "status": "success"},
35
+ )
36
+
37
+ manager.extract_tool_debug_info([tool_call_response])
38
+
39
+ debug_info = manager.get()
40
+ assert len(debug_info["tools"]) == 1
41
+ assert debug_info["tools"][0]["name"] == "TestTool"
42
+ assert debug_info["tools"][0]["info"]["execution_time"] == "100ms"
43
+ assert debug_info["tools"][0]["info"]["status"] == "success"
44
+
45
+ def test_extract_tool_debug_info__adds_multiple_tools__with_multiple_responses(
46
+ self,
47
+ ):
48
+ """Test extracting debug info from multiple ToolCallResponse objects."""
49
+ manager = DebugInfoManager()
50
+ tool_call_responses = [
51
+ ToolCallResponse(
52
+ id="tool_1",
53
+ name="SearchTool",
54
+ debug_info={"query": "test query", "results": 5},
55
+ ),
56
+ ToolCallResponse(
57
+ id="tool_2",
58
+ name="CalculatorTool",
59
+ debug_info={"operation": "add", "result": 42},
60
+ ),
61
+ ToolCallResponse(
62
+ id="tool_3",
63
+ name="WeatherTool",
64
+ debug_info={"location": "New York", "temperature": "72F"},
65
+ ),
66
+ ]
67
+
68
+ manager.extract_tool_debug_info(tool_call_responses)
69
+
70
+ debug_info = manager.get()
71
+ assert len(debug_info["tools"]) == 3
72
+ assert debug_info["tools"][0]["name"] == "SearchTool"
73
+ assert debug_info["tools"][1]["name"] == "CalculatorTool"
74
+ assert debug_info["tools"][2]["name"] == "WeatherTool"
75
+
76
+ def test_extract_tool_debug_info__preserves_order__with_sequential_calls(self):
77
+ """Test that multiple calls to extract_tool_debug_info preserve order."""
78
+ manager = DebugInfoManager()
79
+
80
+ # First call
81
+ manager.extract_tool_debug_info(
82
+ [ToolCallResponse(id="tool_1", name="Tool1", debug_info={"step": 1})]
83
+ )
84
+
85
+ # Second call
86
+ manager.extract_tool_debug_info(
87
+ [ToolCallResponse(id="tool_2", name="Tool2", debug_info={"step": 2})]
88
+ )
89
+
90
+ # Third call
91
+ manager.extract_tool_debug_info(
92
+ [ToolCallResponse(id="tool_3", name="Tool3", debug_info={"step": 3})]
93
+ )
94
+
95
+ debug_info = manager.get()
96
+ assert len(debug_info["tools"]) == 3
97
+ assert debug_info["tools"][0]["info"]["step"] == 1
98
+ assert debug_info["tools"][1]["info"]["step"] == 2
99
+ assert debug_info["tools"][2]["info"]["step"] == 3
100
+
101
+ def test_extract_tool_debug_info__adds_loop_iteration__when_index_provided(self):
102
+ """Test that loop_iteration_index is added to debug info when provided."""
103
+ manager = DebugInfoManager()
104
+ tool_call_response = ToolCallResponse(
105
+ id="tool_1", name="IterativeTool", debug_info={"status": "processing"}
106
+ )
107
+
108
+ manager.extract_tool_debug_info([tool_call_response], loop_iteration_index=3)
109
+
110
+ debug_info = manager.get()
111
+ assert debug_info["tools"][0]["info"]["loop_iteration"] == 3
112
+ assert debug_info["tools"][0]["info"]["status"] == "processing"
113
+
114
+ def test_extract_tool_debug_info__omits_loop_iteration__when_index_is_none(self):
115
+ """Test that loop_iteration is not added when index is None."""
116
+ manager = DebugInfoManager()
117
+ tool_call_response = ToolCallResponse(
118
+ id="tool_1", name="SingleRunTool", debug_info={"status": "complete"}
119
+ )
120
+
121
+ manager.extract_tool_debug_info([tool_call_response], loop_iteration_index=None)
122
+
123
+ debug_info = manager.get()
124
+ assert "loop_iteration" not in debug_info["tools"][0]["info"]
125
+ assert debug_info["tools"][0]["info"]["status"] == "complete"
126
+
127
+ def test_extract_tool_debug_info__handles_empty_debug_info__gracefully(self):
128
+ """Test extracting from ToolCallResponse with empty debug_info dict."""
129
+ manager = DebugInfoManager()
130
+ tool_call_response = ToolCallResponse(
131
+ id="tool_1", name="MinimalTool", debug_info={}
132
+ )
133
+
134
+ manager.extract_tool_debug_info([tool_call_response])
135
+
136
+ debug_info = manager.get()
137
+ assert len(debug_info["tools"]) == 1
138
+ assert debug_info["tools"][0]["name"] == "MinimalTool"
139
+ assert debug_info["tools"][0]["info"] == {}
140
+
141
+ def test_extract_tool_debug_info__handles_empty_list__without_error(self):
142
+ """Test that passing an empty list doesn't cause errors."""
143
+ manager = DebugInfoManager()
144
+
145
+ manager.extract_tool_debug_info([])
146
+
147
+ debug_info = manager.get()
148
+ assert debug_info["tools"] == []
149
+
150
+ def test_add__adds_new_key_value_pair__to_debug_info(self):
151
+ """Test adding a new key-value pair to debug_info."""
152
+ manager = DebugInfoManager()
153
+
154
+ manager.add("execution_summary", {"total_time": "500ms", "total_calls": 5})
155
+
156
+ debug_info = manager.get()
157
+ assert "execution_summary" in debug_info
158
+ assert debug_info["execution_summary"]["total_time"] == "500ms"
159
+ assert debug_info["execution_summary"]["total_calls"] == 5
160
+
161
+ def test_add__preserves_tools_list__when_adding_new_keys(self):
162
+ """Test that add() preserves the tools list."""
163
+ manager = DebugInfoManager()
164
+ manager.extract_tool_debug_info(
165
+ [
166
+ ToolCallResponse(
167
+ id="tool_1", name="TestTool", debug_info={"test": "data"}
168
+ )
169
+ ]
170
+ )
171
+
172
+ manager.add("metadata", {"version": "1.0"})
173
+
174
+ debug_info = manager.get()
175
+ assert len(debug_info["tools"]) == 1
176
+ assert debug_info["tools"][0]["name"] == "TestTool"
177
+ assert debug_info["metadata"]["version"] == "1.0"
178
+
179
+ def test_add__overwrites_existing_key__when_key_exists(self):
180
+ """Test that add() overwrites an existing key."""
181
+ manager = DebugInfoManager()
182
+ manager.add("status", "in_progress")
183
+ manager.add("status", "completed")
184
+
185
+ debug_info = manager.get()
186
+ assert debug_info["status"] == "completed"
187
+
188
+ def test_add__adds_multiple_keys__with_sequential_calls(self):
189
+ """Test adding multiple key-value pairs with sequential calls."""
190
+ manager = DebugInfoManager()
191
+
192
+ manager.add("key1", "value1")
193
+ manager.add("key2", {"nested": "value2"})
194
+ manager.add("key3", [1, 2, 3])
195
+
196
+ debug_info = manager.get()
197
+ assert debug_info["key1"] == "value1"
198
+ assert debug_info["key2"]["nested"] == "value2"
199
+ assert debug_info["key3"] == [1, 2, 3]
200
+
201
+ def test_get__returns_complete_debug_info__with_mixed_data(self):
202
+ """Test get() returns complete debug info with tools and custom keys."""
203
+ manager = DebugInfoManager()
204
+
205
+ # Add tool debug info
206
+ manager.extract_tool_debug_info(
207
+ [ToolCallResponse(id="tool_1", name="Tool1", debug_info={"data": "test"})],
208
+ loop_iteration_index=0,
209
+ )
210
+
211
+ # Add custom keys
212
+ manager.add("start_time", "2025-10-16T10:00:00")
213
+ manager.add("end_time", "2025-10-16T10:01:00")
214
+
215
+ debug_info = manager.get()
216
+
217
+ assert "tools" in debug_info
218
+ assert "start_time" in debug_info
219
+ assert "end_time" in debug_info
220
+ assert len(debug_info["tools"]) == 1
221
+ assert debug_info["start_time"] == "2025-10-16T10:00:00"
222
+
223
+ def test_integration__complete_workflow__with_all_operations(self):
224
+ """Integration test: complete workflow using all DebugInfoManager methods."""
225
+ manager = DebugInfoManager()
226
+
227
+ # Initial state
228
+ assert manager.get() == {"tools": []}
229
+
230
+ # Add some metadata
231
+ manager.add("session_id", "abc-123")
232
+ manager.add("user_id", "user-456")
233
+
234
+ # First tool call (loop iteration 0)
235
+ manager.extract_tool_debug_info(
236
+ [
237
+ ToolCallResponse(
238
+ id="tool_1",
239
+ name="SearchTool",
240
+ debug_info={"query": "AI research", "hits": 100},
241
+ )
242
+ ],
243
+ loop_iteration_index=0,
244
+ )
245
+
246
+ # Second tool call (loop iteration 1)
247
+ manager.extract_tool_debug_info(
248
+ [
249
+ ToolCallResponse(
250
+ id="tool_2",
251
+ name="AnalysisTool",
252
+ debug_info={"processed": 50, "relevant": 10},
253
+ ),
254
+ ToolCallResponse(
255
+ id="tool_3",
256
+ name="SummaryTool",
257
+ debug_info={"paragraphs": 3, "words": 250},
258
+ ),
259
+ ],
260
+ loop_iteration_index=1,
261
+ )
262
+
263
+ # Add final summary
264
+ manager.add("summary", {"total_tools": 3, "total_iterations": 2})
265
+
266
+ # Verify complete debug info
267
+ debug_info = manager.get()
268
+
269
+ assert debug_info["session_id"] == "abc-123"
270
+ assert debug_info["user_id"] == "user-456"
271
+ assert len(debug_info["tools"]) == 3
272
+ assert debug_info["tools"][0]["name"] == "SearchTool"
273
+ assert debug_info["tools"][0]["info"]["loop_iteration"] == 0
274
+ assert debug_info["tools"][1]["name"] == "AnalysisTool"
275
+ assert debug_info["tools"][1]["info"]["loop_iteration"] == 1
276
+ assert debug_info["tools"][2]["name"] == "SummaryTool"
277
+ assert debug_info["tools"][2]["info"]["loop_iteration"] == 1
278
+ assert debug_info["summary"]["total_tools"] == 3
@@ -62,6 +62,14 @@ class BaseToolManager(ABC):
62
62
  def get_tool_by_name(self, name: str) -> Tool | None:
63
63
  raise NotImplementedError()
64
64
 
65
+ @abstractmethod
66
+ def get_tool_choices(self) -> list[str]:
67
+ raise NotImplementedError()
68
+
69
+ @abstractmethod
70
+ def get_exclusive_tools(self) -> list[str]:
71
+ raise NotImplementedError()
72
+
65
73
  def does_a_tool_take_control(self, tool_calls: list[LanguageModelFunction]) -> bool:
66
74
  for tool_call in tool_calls:
67
75
  tool_instance = self.get_tool_by_name(tool_call.name)
@@ -121,6 +129,14 @@ class BaseToolManager(ABC):
121
129
  unpacked_tool_call_result = self._create_tool_call_response(
122
130
  result, tool_calls[i]
123
131
  )
132
+ if unpacked_tool_call_result.debug_info is None:
133
+ unpacked_tool_call_result.debug_info = {}
134
+ unpacked_tool_call_result.debug_info["is_exclusive"] = (
135
+ tool_calls[i].name in self.get_exclusive_tools()
136
+ )
137
+ unpacked_tool_call_result.debug_info["is_forced"] = (
138
+ tool_calls[i].name in self.get_tool_choices()
139
+ )
124
140
  tool_call_results_unpacked.append(unpacked_tool_call_result)
125
141
 
126
142
  return tool_call_results_unpacked
@@ -230,6 +246,9 @@ class ToolManager(BaseToolManager):
230
246
  self._tools = []
231
247
  self._tool_choices = event.payload.tool_choices
232
248
  self._disabled_tools = event.payload.disabled_tools
249
+ self._exclusive_tools = [
250
+ tool.name for tool in self._config.tools if tool.is_exclusive
251
+ ]
233
252
  # this needs to be a set of strings to avoid duplicates
234
253
  self._tool_evaluation_check_list: set[EvaluationMetricName] = set()
235
254
  self._mcp_manager = mcp_manager
@@ -299,6 +318,14 @@ class ToolManager(BaseToolManager):
299
318
  return tool
300
319
  return None
301
320
 
321
+ @override
322
+ def get_tool_choices(self) -> list[str]:
323
+ return self._tool_choices
324
+
325
+ @override
326
+ def get_exclusive_tools(self) -> list[str]:
327
+ return self._exclusive_tools
328
+
302
329
  def get_tools(self) -> list[Tool]:
303
330
  return self._tools # type: ignore
304
331
 
@@ -393,6 +420,14 @@ class ResponsesApiToolManager(BaseToolManager):
393
420
  def get_tool_by_name(self, name: str) -> Tool | None:
394
421
  return self._tool_manager.get_tool_by_name(name)
395
422
 
423
+ @override
424
+ def get_tool_choices(self) -> list[str]:
425
+ return self._tool_manager._tool_choices
426
+
427
+ @override
428
+ def get_exclusive_tools(self) -> list[str]:
429
+ return self._tool_manager._exclusive_tools
430
+
396
431
  @property
397
432
  def sub_agents(self) -> list[SubAgentTool]:
398
433
  return self._tool_manager.sub_agents
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 1.16.0
3
+ Version: 1.16.2
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Cedric Klinkert
@@ -117,6 +117,11 @@ All notable changes to this project will be documented in this file.
117
117
 
118
118
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
119
119
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
120
+ ## [1.16.2] - 2025-10-16
121
+ - Reduce operation dependency on path instead of full url
122
+
123
+ ## [1.16.1] - 2025-10-16
124
+ - Update debug info for better tool call tracking
120
125
 
121
126
  ## [1.16.0] - 2025-10-16
122
127
  - Add responses api support.
@@ -1,7 +1,7 @@
1
1
  unique_toolkit/__init__.py,sha256=qrQ0kgAZnmGR6-UpWOpAL4yd-2ic5Jjwh6s8et-7ZTc,1372
2
2
  unique_toolkit/_common/_base_service.py,sha256=S8H0rAebx7GsOldA7xInLp3aQJt9yEPDQdsGSFRJsGg,276
3
3
  unique_toolkit/_common/_time_utils.py,sha256=ztmTovTvr-3w71Ns2VwXC65OKUUh-sQlzbHdKTQWm-w,135
4
- unique_toolkit/_common/api_calling/human_verification_manager.py,sha256=ZgWThDHdTNFmgNdE4dwR7L7qPV73nSe5wOLpcB1QbWM,11919
4
+ unique_toolkit/_common/api_calling/human_verification_manager.py,sha256=AeGzaGYlo8RHwEp7jhWM7gdx32dRyLUIioZRfrVbgUI,11905
5
5
  unique_toolkit/_common/base_model_type_attribute.py,sha256=7rzVqjXa0deYEixeo_pJSJcQ7nKXpWK_UGpOiEH3yZY,10382
6
6
  unique_toolkit/_common/chunk_relevancy_sorter/config.py,sha256=tHETuMIC4CA_TPwU0oaHbckaKhvMFMYdO_d4lNRKnRc,1806
7
7
  unique_toolkit/_common/chunk_relevancy_sorter/exception.py,sha256=1mY4zjbvnXsd5oIxwiVsma09bS2XRnHrxW8KJBGtgCM,126
@@ -9,8 +9,8 @@ unique_toolkit/_common/chunk_relevancy_sorter/schemas.py,sha256=YAyvXzVk8h5q6FEs
9
9
  unique_toolkit/_common/chunk_relevancy_sorter/service.py,sha256=ZX1pxcy53zh3Ha0_pN6yYIbMX1acRxcvqKTPTKpGKwA,13938
10
10
  unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py,sha256=giD9b5W8A0xP18dZCcrLUruoGi38BeBvPnO1phY7Sp0,8892
11
11
  unique_toolkit/_common/default_language_model.py,sha256=XCZu6n270QkxEeTpj5NZJda6Ok_IR-GcS8w30DU21aI,343
12
- unique_toolkit/_common/endpoint_builder.py,sha256=WzJrJ7azUQhvQRd-vsFFoyj6omJpHiVYrh1UFxNQvVg,8242
13
- unique_toolkit/_common/endpoint_requestor.py,sha256=7rDpeEvmQbLtn3iNW8NftyvAqDkLFGHoYN1h5AFDxho,12319
12
+ unique_toolkit/_common/endpoint_builder.py,sha256=09Y8rC83oNsmxBLAf7WiHqK_OjZmeKb82TPzF0SLSVM,5499
13
+ unique_toolkit/_common/endpoint_requestor.py,sha256=3B7ekfiTmgCEc2h1_m94DsfPwigE2yfiFUmbAAdjuVc,12039
14
14
  unique_toolkit/_common/exception.py,sha256=hwh60UUawHDyPFNs-Wom-Gc6Yb09gPelftAuW1tXE6o,779
15
15
  unique_toolkit/_common/feature_flags/schema.py,sha256=F1NdVJFNU8PKlS7bYzrIPeDu2LxRqHSM9pyw622a1Kk,547
16
16
  unique_toolkit/_common/pydantic/rjsf_tags.py,sha256=T3AZIF8wny3fFov66s258nEl1GqfKevFouTtG6k9PqU,31219
@@ -26,7 +26,8 @@ unique_toolkit/_common/utils/write_configuration.py,sha256=fzvr4C-XBL3OSM3Od9Tbq
26
26
  unique_toolkit/_common/validate_required_values.py,sha256=Y_M1ub9gIKP9qZ45F6Zq3ZHtuIqhmOjl8Z2Vd3avg8w,588
27
27
  unique_toolkit/_common/validators.py,sha256=LFZmAalNa886EXm1VYamFvfBuUZjYKwDdT_HOYU0BtE,2934
28
28
  unique_toolkit/agentic/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
29
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py,sha256=8u3_oxcln7y2zOsfiGh5YOm1zYAlV5QxZ5YAsbEJG0c,584
29
+ unique_toolkit/agentic/debug_info_manager/debug_info_manager.py,sha256=30ZZaw0vffjZjiu9AYdO1Sm8G9FN6XR2ehdOEUCKqh0,891
30
+ unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py,sha256=_fIS6_DHA8A3AB64-LPgHgUGa1w0CFUWwtgV-ZbhkzA,10535
30
31
  unique_toolkit/agentic/evaluation/config.py,sha256=zcW7m63Yt5G39hN2If8slBl6Eu3jTRoRPjYaUMn54Uk,987
31
32
  unique_toolkit/agentic/evaluation/context_relevancy/prompts.py,sha256=EdHFUOB581yVxcOL8482KUv_LzaRjuiem71EF8udYMc,1331
32
33
  unique_toolkit/agentic/evaluation/context_relevancy/schema.py,sha256=lZd0TPzH43ifgWWGg3WO6b1AQX8aK2R9y51yH0d1DHM,2919
@@ -94,7 +95,7 @@ unique_toolkit/agentic/tools/schemas.py,sha256=0ZR8xCdGj1sEdPE0lfTIG2uSR5zqWoprU
94
95
  unique_toolkit/agentic/tools/test/test_mcp_manager.py,sha256=PVRvkK3M21rzONpy5VE_i3vEbAGIz1haW_VPVwiPDI0,15724
95
96
  unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py,sha256=dod5QPqgGUInVAGXAbsAKNTEypIi6pUEWhDbJr9YfUU,6307
96
97
  unique_toolkit/agentic/tools/tool.py,sha256=m56VLxiHuKU2_J5foZp00xhm5lTxWEW7zRLGbIE9ssU,6744
97
- unique_toolkit/agentic/tools/tool_manager.py,sha256=escdnEHzhaKFsyATJyv1JODWBHq7MiEJFTRd5mDnPbI,15133
98
+ unique_toolkit/agentic/tools/tool_manager.py,sha256=DtxJobe_7QKFe6CjnMhCP-mnKO6MjnZeDXsO3jBoC9w,16283
98
99
  unique_toolkit/agentic/tools/tool_progress_reporter.py,sha256=ixud9VoHey1vlU1t86cW0-WTvyTwMxNSWBon8I11SUk,7955
99
100
  unique_toolkit/agentic/tools/utils/__init__.py,sha256=iD1YYzf9LcJFv95Z8BqCAFSewNBabybZRZyvPKGfvro,27
100
101
  unique_toolkit/agentic/tools/utils/execution/__init__.py,sha256=OHiKpqBnfhBiEQagKVWJsZlHv8smPp5OI4dFIexzibw,37
@@ -164,7 +165,7 @@ unique_toolkit/short_term_memory/service.py,sha256=5PeVBu1ZCAfyDb2HLVvlmqSbyzBBu
164
165
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
165
166
  unique_toolkit/smart_rules/compile.py,sha256=Ozhh70qCn2yOzRWr9d8WmJeTo7AQurwd3tStgBMPFLA,1246
166
167
  unique_toolkit/test_utilities/events.py,sha256=_mwV2bs5iLjxS1ynDCjaIq-gjjKhXYCK-iy3dRfvO3g,6410
167
- unique_toolkit-1.16.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
168
- unique_toolkit-1.16.0.dist-info/METADATA,sha256=W19F9u_XVuzsfFZPaDsNZ64NHvNcrxrEojIVFjfPOkM,37441
169
- unique_toolkit-1.16.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
170
- unique_toolkit-1.16.0.dist-info/RECORD,,
168
+ unique_toolkit-1.16.2.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
169
+ unique_toolkit-1.16.2.dist-info/METADATA,sha256=x7WrizUo_LYUUzuW5XwaOATe89GBQpT0v2_KQUlAXAo,37600
170
+ unique_toolkit-1.16.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
171
+ unique_toolkit-1.16.2.dist-info/RECORD,,