unique_internal_search 2026.32.0.dev0__tar.gz → 2026.32.0.dev2__tar.gz

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.
Files changed (15) hide show
  1. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/PKG-INFO +2 -2
  2. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/pyproject.toml +2 -2
  3. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/config.py +3 -12
  4. unique_internal_search-2026.32.0.dev2/unique_internal_search/invocation_stats.py +33 -0
  5. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/service.py +46 -4
  6. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/README.md +0 -0
  7. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/__init__.py +0 -0
  8. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/prompts.py +0 -0
  9. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/services/message_log.py +0 -0
  10. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/uploaded_search/config.py +0 -0
  11. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/uploaded_search/prompts/__init__.py +0 -0
  12. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/uploaded_search/prompts/tool_description_for_system_prompt.j2 +0 -0
  13. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/uploaded_search/service.py +0 -0
  14. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/utils.py +0 -0
  15. {unique_internal_search-2026.32.0.dev0 → unique_internal_search-2026.32.0.dev2}/unique_internal_search/validators.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-internal-search
3
- Version: 2026.32.0.dev0
3
+ Version: 2026.32.0.dev2
4
4
  Summary:
5
5
  Author: Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri
6
6
  Author-email: Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>
7
7
  License: Proprietary
8
8
  Requires-Dist: pydantic>=2.8.2,<3
9
9
  Requires-Dist: typing-extensions>=4.9.0,<5
10
- Requires-Dist: unique-toolkit>=2026.32.0.dev0,<2026.32.0rc0
10
+ Requires-Dist: unique-toolkit>=2026.32.0.dev8,<2026.32.0rc0
11
11
  Requires-Python: >=3.12, <4
12
12
  Description-Content-Type: text/markdown
13
13
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_internal_search"
3
- version = "2026.32.0.dev0"
3
+ version = "2026.32.0.dev2"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "Proprietary" }
@@ -14,7 +14,7 @@ requires-python = ">=3.12,<4"
14
14
  dependencies = [
15
15
  "pydantic>=2.8.2,<3",
16
16
  "typing-extensions>=4.9.0,<5",
17
- "unique-toolkit>=2026.32.0.dev0,<2026.32.0rc0",
17
+ "unique-toolkit>=2026.32.0.dev8,<2026.32.0rc0",
18
18
  ]
19
19
 
20
20
  [build-system]
@@ -33,8 +33,7 @@ from unique_internal_search.prompts import (
33
33
  )
34
34
  from unique_internal_search.validators import get_string_field_with_pattern_validation
35
35
 
36
- DEFAULT_LIMIT_CHUNK_RELEVANCY_SORT_ENABLED = 200
37
- DEFAULT_LIMIT_CHUNK_RELEVANCY_SORT_DISABLED = 1000
36
+ DEFAULT_SEARCH_LIMIT = 200
38
37
 
39
38
 
40
39
  class ToolResponseSystemReminderConfig(BaseModel):
@@ -72,14 +71,6 @@ class ExperimentalFeatures(FeatureExtendedSourceSerialization):
72
71
  )
73
72
 
74
73
 
75
- def _search_limit_factory(data: dict[str, Any]) -> int:
76
- return (
77
- DEFAULT_LIMIT_CHUNK_RELEVANCY_SORT_ENABLED
78
- if data["chunk_relevancy_sort_config"].enabled
79
- else DEFAULT_LIMIT_CHUNK_RELEVANCY_SORT_DISABLED
80
- )
81
-
82
-
83
74
  _FIELD_ALIASES: dict[str, str] = {
84
75
  "ftsSearchLanguage": "searchLanguage",
85
76
  }
@@ -145,8 +136,8 @@ class InternalSearchConfig(BaseToolConfig):
145
136
  description="The chunk relevancy sort config to use for the search.",
146
137
  )
147
138
  limit: int = Field(
148
- default_factory=_search_limit_factory,
149
- description="The maximum limit of chunks returned by the search.",
139
+ default=DEFAULT_SEARCH_LIMIT,
140
+ description="Maximum number of chunks returned in total across all search strings, capped by the token budget. Also used as the per-search-string fetch ceiling.",
150
141
  )
151
142
  chat_only: bool = Field(
152
143
  default=False,
@@ -0,0 +1,33 @@
1
+ """Run-scoped collection of language-model invocation usage."""
2
+
3
+ from collections.abc import Generator, Iterable
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar, Token
6
+
7
+ from unique_toolkit.language_model.invocation_stats import LanguageModelInvocationStats
8
+
9
+ _CURRENT_INVOCATION_STATS: ContextVar[list[LanguageModelInvocationStats] | None] = (
10
+ ContextVar("internal_search_invocation_stats", default=None)
11
+ )
12
+
13
+
14
+ @contextmanager
15
+ def invocation_stats_scope() -> Generator[list[LanguageModelInvocationStats]]:
16
+ """Collect stats for one internal-search run and isolate concurrent runs."""
17
+ invocation_stats: list[LanguageModelInvocationStats] = []
18
+ token: Token[list[LanguageModelInvocationStats] | None] = (
19
+ _CURRENT_INVOCATION_STATS.set(invocation_stats)
20
+ )
21
+ try:
22
+ yield invocation_stats
23
+ finally:
24
+ _CURRENT_INVOCATION_STATS.reset(token)
25
+
26
+
27
+ def record_invocation_stats(
28
+ invocation_stats: Iterable[LanguageModelInvocationStats],
29
+ ) -> None:
30
+ """Record usage when an internal-search run scope is active."""
31
+ current_invocation_stats = _CURRENT_INVOCATION_STATS.get()
32
+ if current_invocation_stats is not None:
33
+ current_invocation_stats.extend(invocation_stats)
@@ -37,6 +37,10 @@ from unique_toolkit.language_model.schemas import (
37
37
  )
38
38
 
39
39
  from unique_internal_search.config import InternalSearchConfig
40
+ from unique_internal_search.invocation_stats import (
41
+ invocation_stats_scope,
42
+ record_invocation_stats,
43
+ )
40
44
  from unique_internal_search.services.message_log import (
41
45
  InternalSearchMessageLogger,
42
46
  InternalSearchMessageLoggerNoop,
@@ -224,6 +228,13 @@ class InternalSearchService:
224
228
  for result in found_chunks_per_search_string
225
229
  for chunk in result.chunks
226
230
  ]
231
+ if len(found_chunks) > self.config.limit:
232
+ self.logger.info(
233
+ "Truncating %s chunks to the configured total limit of %s",
234
+ len(found_chunks),
235
+ self.config.limit,
236
+ )
237
+ found_chunks = found_chunks[: self.config.limit]
227
238
  selected_chunks = pick_content_chunks_for_token_window(
228
239
  found_chunks,
229
240
  self._get_max_tokens(),
@@ -255,13 +266,12 @@ class InternalSearchService:
255
266
  content_ids: list[str] | None = None,
256
267
  ) -> SearchStringResult:
257
268
  try:
258
- capped_limit = self._cap_limit_to_token_budget()
259
269
  found_chunks: list[
260
270
  ContentChunk
261
271
  ] = await self.content_service.search_content_chunks_async(
262
272
  search_string=search_string, # type: ignore
263
273
  search_type=self.config.search_type,
264
- limit=capped_limit,
274
+ limit=self._cap_limit_to_token_budget(),
265
275
  reranker_config=self.config.reranker_config,
266
276
  search_language=self.config.search_language,
267
277
  scope_ids=self.config.scope_ids,
@@ -311,6 +321,13 @@ class InternalSearchService:
311
321
  chunks=found_chunks,
312
322
  config=self.config.chunk_relevancy_sort_config,
313
323
  )
324
+ if isinstance(chunk_relevancy_sorter_result.relevancies, list):
325
+ record_invocation_stats(
326
+ invocation
327
+ for relevancy in chunk_relevancy_sorter_result.relevancies
328
+ if relevancy.relevancy is not None
329
+ for invocation in relevancy.relevancy.invocation_stats
330
+ )
314
331
  found_chunks = chunk_relevancy_sorter_result.content_chunks
315
332
  except ChunkRelevancySorterException as e:
316
333
  self.logger.warning(f"Error while sorting chunks: {e.error_message}")
@@ -348,10 +365,12 @@ class InternalSearchService:
348
365
  capped_limit = min(self.config.limit, token_based_limit)
349
366
  if capped_limit < self.config.limit:
350
367
  self.logger.info(
351
- f"Search limit capped from {self.config.limit} to {capped_limit} (token budget)"
368
+ "Search limit capped from %s to %s (token budget)",
369
+ self.config.limit,
370
+ capped_limit,
352
371
  )
353
372
  else:
354
- self.logger.info(f"Search limit: {capped_limit} (within token budget)")
373
+ self.logger.info("Search limit: %s (within token budget)", capped_limit)
355
374
  return capped_limit
356
375
 
357
376
 
@@ -474,6 +493,29 @@ class InternalSearchTool(Tool[InternalSearchConfig], InternalSearchService):
474
493
  # TODO: find a solution for tracking
475
494
  # @track(name="internal_search_tool_run")
476
495
  async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
496
+ with invocation_stats_scope() as invocation_stats:
497
+ try:
498
+ response = await self._run(tool_call)
499
+ except Exception as e:
500
+ # _run() has no top-level try/except of its own, so any error
501
+ # after self.search() (which may already have spent relevancy-
502
+ # sorting tokens) would otherwise escape straight to
503
+ # SafeTaskExecutor, which builds a fresh, stats-less error
504
+ # response one level up -- silently dropping tokens already
505
+ # spent before the failure.
506
+ # logger.exception() already attaches the active exception's
507
+ # type and traceback; don't stringify it into the message.
508
+ self.logger.exception("InternalSearch tool run failed")
509
+ return ToolCallResponse(
510
+ id=tool_call.id, # type: ignore
511
+ name=self.name,
512
+ error_message=str(e),
513
+ invocation_stats=invocation_stats,
514
+ )
515
+ response.invocation_stats.extend(invocation_stats)
516
+ return response
517
+
518
+ async def _run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
477
519
  """
478
520
  Perform a search in the Vector DB based on the user's message and generate a response.
479
521
  """