unique_toolkit 2026.38.0.dev2__py3-none-any.whl → 2026.38.0.dev4__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.
@@ -19,14 +19,21 @@ from __future__ import annotations
19
19
  import asyncio
20
20
  import json
21
21
  import logging
22
- from collections.abc import Awaitable, Callable
22
+ from collections.abc import Awaitable, Callable, Mapping
23
23
  from pathlib import PurePosixPath
24
- from typing import Any
24
+ from typing import Any, cast
25
25
 
26
26
  import unique_sdk
27
27
  from typing_extensions import deprecated
28
28
 
29
29
  from unique_toolkit.content.schemas import BaseFolderInfo, ContentInfo
30
+ from unique_toolkit.content.smart_rules import (
31
+ AndStatement,
32
+ Operator,
33
+ OrStatement,
34
+ UniqueQL,
35
+ parse_uniqueql,
36
+ )
30
37
  from unique_toolkit.experimental.components.content_tree.schemas import (
31
38
  FolderWalkSnapshot,
32
39
  PathTrieNode,
@@ -69,6 +76,137 @@ def _parse_content_infos_payload(payload: Any) -> tuple[list[ContentInfo], int]:
69
76
  return files, total
70
77
 
71
78
 
79
+ _NULLISH_OPERATORS = {
80
+ Operator.IS_NULL.value,
81
+ Operator.IS_NOT_NULL.value,
82
+ Operator.IS_EMPTY.value,
83
+ Operator.IS_NOT_EMPTY.value,
84
+ }
85
+
86
+
87
+ def _content_uniqueql_record(info: ContentInfo) -> dict[str, Any]:
88
+ dumped = info.model_dump(by_alias=True, mode="json")
89
+ metadata = dumped.get("metadata") or {}
90
+ if not isinstance(metadata, dict):
91
+ metadata = {}
92
+ record = {
93
+ key: value
94
+ for key, value in dumped.items()
95
+ if key != "metadata" and value is not None
96
+ }
97
+ record["metadata"] = metadata
98
+ for key, value in metadata.items():
99
+ record.setdefault(key, value)
100
+ return record
101
+
102
+
103
+ def _uniqueql_fill_nullish_values(node: Any) -> Any:
104
+ """Statement.value cannot be None; documented isNull/isEmpty UniqueQL uses null."""
105
+ if not isinstance(node, dict):
106
+ return node
107
+ if "and" in node:
108
+ return {
109
+ **node,
110
+ "and": [_uniqueql_fill_nullish_values(item) for item in node["and"]],
111
+ }
112
+ if "or" in node:
113
+ return {
114
+ **node,
115
+ "or": [_uniqueql_fill_nullish_values(item) for item in node["or"]],
116
+ }
117
+ filled = dict(node)
118
+ if filled.get("operator") in _NULLISH_OPERATORS and filled.get("value") is None:
119
+ filled["value"] = True
120
+ if isinstance(filled.get("value"), dict):
121
+ filled["value"] = _uniqueql_fill_nullish_values(filled["value"])
122
+ return filled
123
+
124
+
125
+ def _uniqueql_matches(record: Mapping[str, Any], query: UniqueQL) -> bool:
126
+ if isinstance(query, AndStatement):
127
+ return bool(query.and_list) and all(
128
+ _uniqueql_matches(record, child) for child in query.and_list
129
+ )
130
+ if isinstance(query, OrStatement):
131
+ return bool(query.or_list) and any(
132
+ _uniqueql_matches(record, child) for child in query.or_list
133
+ )
134
+ actual: Any = record
135
+ for key in query.path:
136
+ if not isinstance(actual, Mapping):
137
+ actual = None
138
+ break
139
+ actual = actual.get(key)
140
+ expected = query.value
141
+ operator = query.operator
142
+ is_empty = actual is None or actual in ("", [], {})
143
+ match operator:
144
+ case Operator.EQUALS:
145
+ return actual == expected
146
+ case Operator.NOT_EQUALS:
147
+ return actual != expected
148
+ case Operator.CONTAINS:
149
+ try:
150
+ return expected in cast(Any, actual)
151
+ except TypeError:
152
+ return False
153
+ case Operator.NOT_CONTAINS:
154
+ try:
155
+ return expected not in cast(Any, actual)
156
+ except TypeError:
157
+ return True
158
+ case Operator.IN:
159
+ return isinstance(expected, list) and actual in expected
160
+ case Operator.NOT_IN:
161
+ return isinstance(expected, list) and actual not in expected
162
+ case Operator.OVERLAPS:
163
+ return (
164
+ isinstance(actual, list)
165
+ and isinstance(expected, list)
166
+ and any(item in actual for item in expected)
167
+ )
168
+ case Operator.NOT_OVERLAPS:
169
+ return not (
170
+ isinstance(actual, list)
171
+ and isinstance(expected, list)
172
+ and any(item in actual for item in expected)
173
+ )
174
+ case Operator.GREATER_THAN:
175
+ try:
176
+ return cast(Any, actual) > expected
177
+ except TypeError:
178
+ return False
179
+ case Operator.GREATER_THAN_OR_EQUAL:
180
+ try:
181
+ return cast(Any, actual) >= expected
182
+ except TypeError:
183
+ return False
184
+ case Operator.LESS_THAN:
185
+ try:
186
+ return cast(Any, actual) < expected
187
+ except TypeError:
188
+ return False
189
+ case Operator.LESS_THAN_OR_EQUAL:
190
+ try:
191
+ return cast(Any, actual) <= expected
192
+ except TypeError:
193
+ return False
194
+ case Operator.IS_NULL:
195
+ return actual is None
196
+ case Operator.IS_NOT_NULL:
197
+ return actual is not None
198
+ case Operator.IS_EMPTY:
199
+ return is_empty
200
+ case Operator.IS_NOT_EMPTY:
201
+ return not is_empty
202
+ case Operator.NESTED:
203
+ return (
204
+ isinstance(expected, (AndStatement, OrStatement))
205
+ and isinstance(actual, Mapping)
206
+ and _uniqueql_matches(actual, expected)
207
+ )
208
+
209
+
72
210
  def _propagate_wait_interrupt(result: object) -> None:
73
211
  """Re-raise timeout/cancellation so callers can return a partial snapshot."""
74
212
  if isinstance(result, (asyncio.CancelledError, TimeoutError)):
@@ -139,7 +277,8 @@ async def _list_direct_children_async(
139
277
 
140
278
  ``scope_id=None`` is the knowledge-base root. ``Content.get_infos`` without
141
279
  ``parentId`` lists the **entire** catalog, so root only fetches folders.
142
- Files are listed per folder with ``parentId``.
280
+ Files are listed per folder with ``parentId``. Unique rejects ``parentId``
281
+ with ``metadataFilter``, so UniqueQL is applied to the returned files.
143
282
  """
144
283
  parent_params: dict[str, Any] = {}
145
284
  if scope_id:
@@ -164,23 +303,38 @@ async def _list_direct_children_async(
164
303
  if not scope_id:
165
304
  return await folders_task
166
305
 
306
+ parsed_filter = (
307
+ parse_uniqueql(_uniqueql_fill_nullish_values(metadata_filter))
308
+ if metadata_filter
309
+ else None
310
+ )
311
+
167
312
  async def _content_page(skip: int) -> Any:
168
- params = dict(parent_params)
169
- if metadata_filter:
170
- params["metadataFilter"] = metadata_filter
171
313
  return await unique_sdk.Content.get_infos_async(
172
314
  user_id=user_id,
173
315
  company_id=company_id,
174
316
  skip=skip,
175
317
  take=step_size,
176
- **params,
318
+ **parent_params,
177
319
  )
178
320
 
321
+ def _parse_content_page(payload: Any) -> tuple[list[ContentInfo], int]:
322
+ files, total = _parse_content_infos_payload(payload)
323
+ # Keep parentId listing + local UniqueQL until UniqueQL/folderId metadata
324
+ # listing is equivalent (soft-delete/versioning); then drop this record filter.
325
+ if parsed_filter is not None:
326
+ files = [
327
+ info
328
+ for info in files
329
+ if _uniqueql_matches(_content_uniqueql_record(info), parsed_filter)
330
+ ]
331
+ return files, total
332
+
179
333
  folder_result, file_result = await asyncio.gather(
180
334
  folders_task,
181
335
  _paginate_parent_listing(
182
336
  _content_page,
183
- _parse_content_infos_payload,
337
+ _parse_content_page,
184
338
  step_size=step_size,
185
339
  max_concurrent_requests=max_concurrent_page_fetches,
186
340
  on_page=on_files,
@@ -75,12 +75,26 @@ class InternalSearchBaseService( # pyright: ignore[reportImplicitAbstractClass]
75
75
  )
76
76
  )
77
77
 
78
- results = await asyncio.gather(
79
- *[self._search_single_query(query=q) for q in search_queries],
80
- return_exceptions=True,
78
+ # TaskGroup awaits cancelled children so httpcore can release the connection.
79
+ results: list[SearchStringResult | BaseException | None] = [None] * len(
80
+ search_queries
81
81
  )
82
82
 
83
- found = self._collect_results(results, search_queries)
83
+ async def _run(i: int, query: str) -> None:
84
+ try:
85
+ results[i] = await self._search_single_query(query=query)
86
+ except Exception as exc:
87
+ # not BaseException: CancelledError must reach the group
88
+ results[i] = exc
89
+
90
+ async with asyncio.TaskGroup() as tg:
91
+ for i, query in enumerate(search_queries):
92
+ tg.create_task(_run(i, query))
93
+
94
+ found = self._collect_results(
95
+ [r for r in results if r is not None],
96
+ search_queries,
97
+ )
84
98
  return await self._finalize_run(
85
99
  search_queries,
86
100
  found,
@@ -110,6 +110,7 @@ class LanguageModelName(StrEnum):
110
110
  GROK_4_1_FAST_NON_REASONING = "litellm:grok-4-1-fast-non-reasoning"
111
111
  GROK_4_1_FAST_REASONING = "litellm:grok-4-1-fast-reasoning"
112
112
  GROK_4_5 = "litellm:grok-4-5"
113
+ GROK_4_6 = "litellm:grok-4-6"
113
114
  LITELLM_OPENAI_GPT_5 = "litellm:openai-gpt-5"
114
115
  LITELLM_OPENAI_GPT_5_MINI = "litellm:openai-gpt-5-mini"
115
116
  LITELLM_OPENAI_GPT_5_NANO = "litellm:openai-gpt-5-nano"
@@ -2195,6 +2196,33 @@ class LanguageModelInfo(BaseModel):
2195
2196
  published_at=date(2026, 7, 16),
2196
2197
  supported_reasoning_efforts=["low", "medium", "high"],
2197
2198
  )
2199
+ case LanguageModelName.GROK_4_6:
2200
+ # Specs from https://docs.x.ai/developers/grok-4-6
2201
+ # Announcement date from https://x.ai/news/grok-4-6
2202
+ return cls(
2203
+ name=model_name,
2204
+ capabilities=[
2205
+ ModelCapabilities.FUNCTION_CALLING,
2206
+ ModelCapabilities.REASONING,
2207
+ ModelCapabilities.STREAMING,
2208
+ ModelCapabilities.STRUCTURED_OUTPUT,
2209
+ ModelCapabilities.VISION,
2210
+ ],
2211
+ provider=LanguageModelProvider.LITELLM,
2212
+ family=ModelFamily.XAI,
2213
+ version="grok-4-6",
2214
+ encoder_name=EncoderName.O200K_BASE, # TODO: Update encoder with grok tokenizer
2215
+ token_limits=LanguageModelTokenLimits(
2216
+ # Context window is 500_000, we leave 20_000 tokens as buffer due to tokenizer mismatch
2217
+ # Assign 90% for input and 10% for output
2218
+ token_limit_input=int(480_000 * 0.9),
2219
+ token_limit_output=int(480_000 * 0.1),
2220
+ ),
2221
+ info_cutoff_at=date(2026, 1, day=1),
2222
+ published_at=date(2026, 8, 12),
2223
+ supported_reasoning_efforts=["low", "medium", "high", "xhigh"],
2224
+ default_options={"reasoning_effort": "high"},
2225
+ )
2198
2226
  case LanguageModelName.LITELLM_OPENAI_GPT_5:
2199
2227
  return cls(
2200
2228
  name=model_name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.38.0.dev2
3
+ Version: 2026.38.0.dev4
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>
@@ -304,14 +304,14 @@ unique_toolkit/experimental/_internal/streaming/pattern_replacer.py,sha256=RLLQK
304
304
  unique_toolkit/experimental/components/__init__.py,sha256=rl-s8AnHZoLEOB8ei9jQgiRIndfMWrzWJm8gEMzNcS4,2029
305
305
  unique_toolkit/experimental/components/_base.py,sha256=XZHKffTIy331ZQnsywl0m1XkJ4DCjHV2on5EBkaG-ik,4153
306
306
  unique_toolkit/experimental/components/content_tree/__init__.py,sha256=zAMzctv__M5e4OppVD_neO471V4zKBeHcbVrixZciaM,1946
307
- unique_toolkit/experimental/components/content_tree/functions.py,sha256=CzRnEKST7JUtoaqALqT37J2tdpHmeLPtnsiw-Xu8Zys,14231
307
+ unique_toolkit/experimental/components/content_tree/functions.py,sha256=SwWxgwjXEhZcUh7bZJOFpJ_CwldEzLnMzMi69EEZeAQ,19465
308
308
  unique_toolkit/experimental/components/content_tree/schemas.py,sha256=8MphCYk8VCnNk5v274voeKd9HLvsOHjpk7G8D4YPuWU,9719
309
309
  unique_toolkit/experimental/components/content_tree/service.py,sha256=OKkPz-prJAr_8VzK0PLYTDZutV8LqlOaXnev57YxID0,25415
310
310
  unique_toolkit/experimental/components/internal_search/__init__.py,sha256=Jj-ujG_SS5-uqCrrNidRn_H0sQAeSvyWtt9Tw0pkbzI,3091
311
311
  unique_toolkit/experimental/components/internal_search/base/__init__.py,sha256=GB7vNBvbF6V1oLjKEf2KVvukr-BwgYpAkD5jKtGG9wU,929
312
312
  unique_toolkit/experimental/components/internal_search/base/config.py,sha256=Bf77n0YSEFXRZKAqe4efAOITnyD3hH5Gp-gEVbMw1qE,2482
313
313
  unique_toolkit/experimental/components/internal_search/base/schemas.py,sha256=N9UkSNDuKidNpec8Wimfow2-EhIWqK5G7j7c_Mn28Jw,1431
314
- unique_toolkit/experimental/components/internal_search/base/service.py,sha256=XHXvkyy2dL0GwM2AFZdAte2upMmcKBVyA_fllEJYxjY,6934
314
+ unique_toolkit/experimental/components/internal_search/base/service.py,sha256=uM6PFixs0yEkpnjVfQOwXowR-aAy1bV0dGsRGcC03Vs,7478
315
315
  unique_toolkit/experimental/components/internal_search/base/utils.py,sha256=sjePAtSs5-GXIbWh0sDwC8wXbX_XWiaTx21AZ7i7sto,6431
316
316
  unique_toolkit/experimental/components/internal_search/chat/__init__.py,sha256=mnlw9kKlpHWwiYhQeECXOd0-54muym2TZj0QhAsFSa0,246
317
317
  unique_toolkit/experimental/components/internal_search/chat/config.py,sha256=2DQhY5TIMIXAqpezORDeRf3AV5_2jV1LnTKBqzrSLbk,879
@@ -394,7 +394,7 @@ unique_toolkit/language_model/builder.py,sha256=KwBzsD6-fvQnB4p2I7M-5Tk6j4tmQ973
394
394
  unique_toolkit/language_model/constants.py,sha256=B-topqW0r83dkC_25DeQfnPk3n53qzIHUCBS7YJ0-1U,119
395
395
  unique_toolkit/language_model/default_language_model.py,sha256=mcyuFs_V6tp52C8xJSHiEHO4kFGj6xed1gvozvCRD7k,1181
396
396
  unique_toolkit/language_model/functions.py,sha256=W6JC4LUh-LZ9waDgph--xVmKeQ7Ls6NRAqDx9DwQdeY,26691
397
- unique_toolkit/language_model/infos.py,sha256=TY4HtNbOxwokq3YW4QNiAyA8eq2ZvzzaRhGAfDkKG3M,149525
397
+ unique_toolkit/language_model/infos.py,sha256=NWVMUxcL4lOHmgNmEXZyj7qTq-mt_gfYYWGMweCRtlE,151046
398
398
  unique_toolkit/language_model/invocation_stats.py,sha256=CQbpUz394IsVd8eNTB1gPSMt5f0UXmkN1amNsqtAW6U,5857
399
399
  unique_toolkit/language_model/model_costs.py,sha256=SMYhikdN4h_qMUKKrpHYyYO5o4MW_QlZGGEr643Hw0M,4354
400
400
  unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
@@ -422,6 +422,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
422
422
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
423
423
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
424
424
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
425
- unique_toolkit-2026.38.0.dev2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
426
- unique_toolkit-2026.38.0.dev2.dist-info/METADATA,sha256=GxmLZU9cThG9AWrCXho8un_5tr73a85ZeZXJPu4-NXo,7821
427
- unique_toolkit-2026.38.0.dev2.dist-info/RECORD,,
425
+ unique_toolkit-2026.38.0.dev4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
426
+ unique_toolkit-2026.38.0.dev4.dist-info/METADATA,sha256=bLPgw9um9D6gC7lAdRZksRhdY2iSUB7paSAIS8f2ccI,7821
427
+ unique_toolkit-2026.38.0.dev4.dist-info/RECORD,,