unique_toolkit 2026.30.0.dev7__py3-none-any.whl → 2026.30.0.dev8__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,12 +19,14 @@ on its own.
19
19
  from __future__ import annotations
20
20
 
21
21
  import asyncio
22
- import difflib
23
22
  import functools
24
23
  import json
24
+ import re
25
25
  from collections.abc import Callable
26
26
  from typing import TYPE_CHECKING, Any, Protocol, Self, overload
27
27
 
28
+ from rapidfuzz import fuzz
29
+
28
30
  from unique_toolkit._common.validate_required_values import validate_required_values
29
31
  from unique_toolkit.app.unique_settings import UniqueSettings
30
32
  from unique_toolkit.content.schemas import ContentInfo
@@ -44,6 +46,32 @@ if TYPE_CHECKING:
44
46
  from unique_toolkit.app.unique_settings import UniqueContext
45
47
 
46
48
 
49
+ _NON_ALNUM_RE = re.compile(r"[^a-zA-Z0-9]+")
50
+ _TRAILING_EXTENSION_RE = re.compile(r"\.[A-Za-z0-9]{1,8}$")
51
+
52
+
53
+ def _tokenize_for_fuzzy_scoring(
54
+ value: str, case_sensitive: bool, *, strip_extension: bool = False
55
+ ) -> str:
56
+ """Normalize ``value`` into whitespace-separated tokens for
57
+ :func:`rapidfuzz.fuzz.token_set_ratio`, which only tokenizes on
58
+ whitespace — filenames/paths use ``_``/``-``/``/`` (and other
59
+ punctuation) as their real word separators, so those need converting to
60
+ spaces first or every candidate is scored as a single opaque token (no
61
+ better than a plain char-ratio). Mirrors what
62
+ :func:`rapidfuzz.utils.default_process` does, minus the unconditional
63
+ lowercasing it bundles in — kept as a local regex rather than that
64
+ helper (via ``fuzz.token_set_ratio``'s ``processor=`` argument) so
65
+ ``case_sensitive=True`` can still skip lowercasing while keeping
66
+ separator normalization.
67
+ """
68
+ if strip_extension:
69
+ value = _TRAILING_EXTENSION_RE.sub("", value)
70
+ if not case_sensitive:
71
+ value = value.lower()
72
+ return _NON_ALNUM_RE.sub(" ", value).strip()
73
+
74
+
47
75
  class _CachedResolveTaskFactory(Protocol):
48
76
  """Structural type for :func:`functools.cache`-wrapped resolve-task factory.
49
77
 
@@ -340,16 +368,24 @@ class ContentTree:
340
368
  ) -> list[FuzzyMatch]:
341
369
  """Fuzzy-match ``query`` against visible file names and/or paths.
342
370
 
343
- Scoring uses :class:`difflib.SequenceMatcher` (stdlib), which returns a
344
- ratio in ``[0.0, 1.0]``. Matching is case-insensitive by default since
345
- file names in a knowledge base tend to be noisy.
371
+ Scoring uses :func:`rapidfuzz.fuzz.token_set_ratio` over each string's
372
+ tokens (split on whitespace, ``_``, ``-``, and ``/``, extension
373
+ stripped), scaled to ``[0.0, 1.0]``. Token-set scoring means a short
374
+ query that's a complete word inside a much longer filename scores
375
+ highly (ideally ``1.0`` for an exact token match) instead of being
376
+ penalized for the filename's unrelated length — a plain character-level
377
+ ratio (e.g. :class:`difflib.SequenceMatcher`, used here previously)
378
+ scores "alpensys" vs. "alpensys_budget_vs_actual_q3_2024.docx" at only
379
+ ~0.35, indistinguishable from genuinely unrelated files, because it
380
+ penalizes every unmatched character in the (much longer) candidate.
381
+ Matching is case-insensitive by default since file names in a
382
+ knowledge base tend to be noisy.
346
383
 
347
384
  Args:
348
385
  query: The search string (typically a fragment of a filename or path).
349
386
  limit: Maximum number of matches to return, after sorting by score
350
387
  descending.
351
- min_score: Drop matches scoring below this threshold. ``0.6`` is
352
- :mod:`difflib`'s own rule of thumb for "reasonably close".
388
+ min_score: Drop matches scoring below this threshold.
353
389
  match_on: Score against the basename (``"key"``), the joined folder
354
390
  path (``"path"``), or take the max of both (``"both"``).
355
391
  case_sensitive: If ``False`` (default) both sides are lowercased
@@ -369,25 +405,26 @@ class ContentTree:
369
405
  metadata_filter=metadata_filter,
370
406
  max_concurrent_scope_lookups=max_concurrent_scope_lookups,
371
407
  )
372
- normalized_query = query if case_sensitive else query.lower()
408
+ normalized_query = _tokenize_for_fuzzy_scoring(query, case_sensitive)
373
409
 
374
410
  matches: list[FuzzyMatch] = []
375
411
  for content_info, segments in rows:
376
- key_candidate = content_info.key
377
- path_candidate = "/".join(segments)
378
- if not case_sensitive:
379
- key_candidate = key_candidate.lower()
380
- path_candidate = path_candidate.lower()
412
+ key_candidate = _tokenize_for_fuzzy_scoring(
413
+ content_info.key, case_sensitive, strip_extension=True
414
+ )
415
+ path_candidate = _tokenize_for_fuzzy_scoring(
416
+ "/".join(segments), case_sensitive
417
+ )
381
418
 
382
419
  score_key = match_on in ("key", "both")
383
420
  score_path = match_on in ("path", "both")
384
421
  key_score = (
385
- difflib.SequenceMatcher(None, normalized_query, key_candidate).ratio()
422
+ fuzz.token_set_ratio(normalized_query, key_candidate) / 100.0
386
423
  if score_key
387
424
  else 0.0
388
425
  )
389
426
  path_score = (
390
- difflib.SequenceMatcher(None, normalized_query, path_candidate).ratio()
427
+ fuzz.token_set_ratio(normalized_query, path_candidate) / 100.0
391
428
  if score_path
392
429
  else 0.0
393
430
  )
@@ -230,7 +230,7 @@ class KnowledgeBaseService:
230
230
  searches = search_content_chunks(
231
231
  user_id=self._user_id,
232
232
  company_id=self._company_id,
233
- chat_id="",
233
+ chat_id=None,
234
234
  search_string=search_string,
235
235
  search_type=search_type,
236
236
  limit=limit,
@@ -327,7 +327,7 @@ class KnowledgeBaseService:
327
327
  searches = await search_content_chunks_async(
328
328
  user_id=self._user_id,
329
329
  company_id=self._company_id,
330
- chat_id="",
330
+ chat_id=None,
331
331
  search_string=search_string,
332
332
  search_type=search_type,
333
333
  limit=limit,
@@ -363,7 +363,7 @@ class KnowledgeBaseService:
363
363
  return search_contents(
364
364
  user_id=self._user_id,
365
365
  company_id=self._company_id,
366
- chat_id="",
366
+ chat_id=None,
367
367
  where=where,
368
368
  include_failed_content=include_failed_content,
369
369
  )
@@ -387,7 +387,7 @@ class KnowledgeBaseService:
387
387
  return await search_contents_async(
388
388
  user_id=self._user_id,
389
389
  company_id=self._company_id,
390
- chat_id="",
390
+ chat_id=None,
391
391
  where=where,
392
392
  include_failed_content=include_failed_content,
393
393
  )
@@ -429,7 +429,7 @@ class KnowledgeBaseService:
429
429
  content_name=content_name,
430
430
  mime_type=mime_type,
431
431
  scope_id=scope_id,
432
- chat_id="",
432
+ chat_id=None,
433
433
  skip_ingestion=skip_ingestion,
434
434
  ingestion_config=ingestion_config,
435
435
  metadata=metadata,
@@ -470,7 +470,7 @@ class KnowledgeBaseService:
470
470
  content_name=content_name,
471
471
  mime_type=mime_type,
472
472
  scope_id=scope_id,
473
- chat_id="",
473
+ chat_id=None,
474
474
  skip_ingestion=skip_ingestion,
475
475
  ingestion_config=ingestion_config,
476
476
  metadata=metadata,
@@ -511,7 +511,7 @@ class KnowledgeBaseService:
511
511
  content_name=content_name,
512
512
  mime_type=mime_type,
513
513
  scope_id=scope_id,
514
- chat_id="",
514
+ chat_id=None,
515
515
  skip_ingestion=skip_ingestion,
516
516
  skip_excel_ingestion=skip_excel_ingestion,
517
517
  ingestion_config=ingestion_config,
@@ -544,7 +544,7 @@ class KnowledgeBaseService:
544
544
  user_id=self._user_id,
545
545
  company_id=self._company_id,
546
546
  content_id=content_id,
547
- chat_id="",
547
+ chat_id=None,
548
548
  filename=output_filename,
549
549
  tmp_dir_path=output_dir_path,
550
550
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.30.0.dev7
3
+ Version: 2026.30.0.dev8
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>
@@ -32,6 +32,7 @@ Requires-Dist: pypandoc>=1.16.2,<2
32
32
  Requires-Dist: tenacity>=9.1.4
33
33
  Requires-Dist: pydantic-core>=2.41.5
34
34
  Requires-Dist: cachetools>=5.0,<6
35
+ Requires-Dist: rapidfuzz>=3.0,<4
35
36
  Requires-Dist: fastapi>=0.121.0,<0.122 ; extra == 'fastapi'
36
37
  Requires-Dist: starlette>=0.49.1 ; extra == 'fastapi'
37
38
  Requires-Dist: uvicorn>=0.37.0,<0.38 ; extra == 'fastapi'
@@ -294,7 +294,7 @@ unique_toolkit/experimental/components/_base.py,sha256=XZHKffTIy331ZQnsywl0m1XkJ
294
294
  unique_toolkit/experimental/components/content_tree/__init__.py,sha256=30eyVB7CzDC9WZTXj-FYQNY20o0m987SV3dn6WdFI-U,2103
295
295
  unique_toolkit/experimental/components/content_tree/functions.py,sha256=xLBHNHBsoySIESU-jvmU-6_-5LrbiXkv7-btxbCf_38,8690
296
296
  unique_toolkit/experimental/components/content_tree/schemas.py,sha256=qRF8UUxhB616lzFSi5FV6vIdLXDHry3fWWb3eSTMJAo,3942
297
- unique_toolkit/experimental/components/content_tree/service.py,sha256=WO1ZujJ504UHi_i_Wx5L-k7ZmNiHh_1xc6cHDxY0qCM,17160
297
+ unique_toolkit/experimental/components/content_tree/service.py,sha256=U04U5Gq1jElUlVbSjefHAIefBi8KRZhKUwmotADx2GU,18907
298
298
  unique_toolkit/experimental/components/internal_search/__init__.py,sha256=Jj-ujG_SS5-uqCrrNidRn_H0sQAeSvyWtt9Tw0pkbzI,3091
299
299
  unique_toolkit/experimental/components/internal_search/base/__init__.py,sha256=GB7vNBvbF6V1oLjKEf2KVvukr-BwgYpAkD5jKtGG9wU,929
300
300
  unique_toolkit/experimental/components/internal_search/base/config.py,sha256=Bf77n0YSEFXRZKAqe4efAOITnyD3hH5Gp-gEVbMw1qE,2482
@@ -397,7 +397,7 @@ unique_toolkit/protocols/support.py,sha256=x5edzbBmKAUuvm3oLFh52gkgQsqR5LU7M0t8R
397
397
  unique_toolkit/services/__init__.py,sha256=b9Wl7Wvz1JmZDbDexIZzBoXZ0wscXuczGAH8LoL-Qak,572
398
398
  unique_toolkit/services/chat_service.py,sha256=MAueuSfATFJH8aSeNxbnBI_Ne1UgYUIk3i5F1f2ALFQ,97692
399
399
  unique_toolkit/services/factory.py,sha256=7gNeanyHkgX23Iz7RMCQTCkE9gI5asCI11nXxM0jjx4,9832
400
- unique_toolkit/services/knowledge_base.py,sha256=a9itNRxIrGDIuXkLiTLBsA-m2ncy8F8DwCLCXtWm_GY,43446
400
+ unique_toolkit/services/knowledge_base.py,sha256=3pkYoHVtJL7ZtEq_H-p6_v4IpJ_KXkVLlapF0xkl1-U,43462
401
401
  unique_toolkit/short_term_memory/__init__.py,sha256=2mI3AUrffgH7Yt-xS57EGqnHf7jnn6xquoKEhJqk3Wg,185
402
402
  unique_toolkit/short_term_memory/constants.py,sha256=698CL6-wjup2MvU19RxSmQk3gX7aqW_OOpZB7sbz_Xg,34
403
403
  unique_toolkit/short_term_memory/functions.py,sha256=mbEeWkJ5Jr0cvg1_m6l9aS1L6nWE5ZhviNzpKlCs-a8,7907
@@ -406,6 +406,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
406
406
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
407
407
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
408
408
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
409
- unique_toolkit-2026.30.0.dev7.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
410
- unique_toolkit-2026.30.0.dev7.dist-info/METADATA,sha256=tf1qkimmD41c7yoIbUFNmKEgYkiJx-zSyOA1trTxzgQ,7426
411
- unique_toolkit-2026.30.0.dev7.dist-info/RECORD,,
409
+ unique_toolkit-2026.30.0.dev8.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
410
+ unique_toolkit-2026.30.0.dev8.dist-info/METADATA,sha256=N_hbI82hHIaIXf7Yuw1mFfQmVfoIkrGGQGMv4IdEBnQ,7459
411
+ unique_toolkit-2026.30.0.dev8.dist-info/RECORD,,