unique_internal_search 2026.22.0.dev3__tar.gz → 2026.22.0.dev4__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 (14) hide show
  1. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/PKG-INFO +2 -2
  2. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/pyproject.toml +2 -2
  3. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/config.py +18 -0
  4. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/prompts.py +5 -0
  5. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/service.py +18 -1
  6. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/uploaded_search/service.py +63 -20
  7. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/README.md +0 -0
  8. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/__init__.py +0 -0
  9. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/services/message_log.py +0 -0
  10. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/uploaded_search/config.py +0 -0
  11. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/uploaded_search/prompts/__init__.py +0 -0
  12. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/uploaded_search/prompts/tool_description_for_system_prompt.j2 +0 -0
  13. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/utils.py +0 -0
  14. {unique_internal_search-2026.22.0.dev3 → unique_internal_search-2026.22.0.dev4}/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.22.0.dev3
3
+ Version: 2026.22.0.dev4
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.22.0.dev7,<2026.22.0rc0
10
+ Requires-Dist: unique-toolkit>=2026.22.0.dev12,<2026.22.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.22.0.dev3"
3
+ version = "2026.22.0.dev4"
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.22.0.dev7,<2026.22.0rc0",
17
+ "unique-toolkit>=2026.22.0.dev12,<2026.22.0rc0",
18
18
  ]
19
19
 
20
20
  [build-system]
@@ -23,6 +23,7 @@ from unique_toolkit.content.schemas import (
23
23
  )
24
24
 
25
25
  from unique_internal_search.prompts import (
26
+ DEFAULT_CONTENT_IDS_PARAM_DESCRIPTION,
26
27
  DEFAULT_LANGUAGE_PARAM_DESCRIPTION,
27
28
  DEFAULT_SEARCH_STRING_PARAM_DESCRIPTION,
28
29
  DEFAULT_TOOL_DESCRIPTION,
@@ -209,6 +210,23 @@ class InternalSearchConfig(BaseToolConfig):
209
210
  description="Allow execution of multiple search strings in one call. When set to True, each string is searched individually and results are merged into a single response.",
210
211
  )
211
212
 
213
+ enable_content_id_filter: bool = Field(
214
+ default=False,
215
+ description=(
216
+ "When True, exposes an optional `content_ids` parameter in the tool schema so the LLM can restrict the search to specific documents. "
217
+ ),
218
+ )
219
+
220
+ param_description_content_ids: Annotated[
221
+ str,
222
+ RJSFMetaTag.StringWidget.textarea(
223
+ rows=int(len(DEFAULT_CONTENT_IDS_PARAM_DESCRIPTION.split("\n")))
224
+ ),
225
+ ] = get_string_field_with_pattern_validation(
226
+ DEFAULT_CONTENT_IDS_PARAM_DESCRIPTION,
227
+ description="`content_ids` parameter description shown to the LLM.",
228
+ )
229
+
212
230
  metadata_chunk_sections: dict[str, str] = Field(
213
231
  default={},
214
232
  description=(
@@ -63,6 +63,11 @@ DEFAULT_SEARCH_STRING_PARAM_DESCRIPTION = "An expanded term that is optimized fo
63
63
 
64
64
  DEFAULT_LANGUAGE_PARAM_DESCRIPTION = "The language that the user wrote the query in"
65
65
 
66
+ DEFAULT_CONTENT_IDS_PARAM_DESCRIPTION = (
67
+ "Optional list of document IDs (e.g cont_sx6neydisjrdvq59q39s89cg) to restrict the search "
68
+ "to specific documents. Use `null` to search all available documents."
69
+ )
70
+
66
71
 
67
72
  DEFAULT_TOOL_RESPONSE_SYSTEM_REMINDER_PROMPT = """
68
73
  ## Reminder: Cite Every Fact from These Results
@@ -168,7 +168,12 @@ class InternalSearchService:
168
168
  )
169
169
  and chat_only
170
170
  ):
171
- content_ids = self.selected_uploaded_file_ids
171
+ if content_ids is not None:
172
+ content_ids = list(
173
+ filter(lambda x: x in self.selected_uploaded_file_ids, content_ids)
174
+ )
175
+ else:
176
+ content_ids = self.selected_uploaded_file_ids
172
177
 
173
178
  # Run all searches in parallel
174
179
  results = await asyncio.gather(
@@ -236,6 +241,7 @@ class InternalSearchService:
236
241
  self.debug_info = {
237
242
  "searchStrings": search_strings,
238
243
  "metadataFilter": metadata_filter,
244
+ "contentIds": content_ids,
239
245
  "chatOnly": chat_only,
240
246
  }
241
247
  return selected_chunks
@@ -424,6 +430,16 @@ class InternalSearchTool(Tool[InternalSearchConfig], InternalSearchService):
424
430
  Field(description=self.config.param_description_search_string),
425
431
  )
426
432
 
433
+ optional_fields: dict = {}
434
+ if self.config.enable_content_id_filter:
435
+ optional_fields["content_ids"] = (
436
+ list[str] | None,
437
+ Field(
438
+ default=None,
439
+ description=self.config.param_description_content_ids,
440
+ ),
441
+ )
442
+
427
443
  internal_search_tool_input = create_model(
428
444
  "InternalSearchToolInput",
429
445
  search_string=search_string_field,
@@ -431,6 +447,7 @@ class InternalSearchTool(Tool[InternalSearchConfig], InternalSearchService):
431
447
  str,
432
448
  Field(description=self.config.param_description_language),
433
449
  ),
450
+ **optional_fields,
434
451
  )
435
452
  return LanguageModelToolDescription(
436
453
  name=self.name,
@@ -1,3 +1,5 @@
1
+ from typing import Literal
2
+
1
3
  from pydantic import Field, create_model
2
4
  from typing_extensions import override
3
5
  from unique_toolkit import ContentService
@@ -14,6 +16,7 @@ from unique_toolkit.agentic.tools.tool_progress_reporter import (
14
16
  )
15
17
  from unique_toolkit.app.schemas import BaseEvent, ChatEvent
16
18
  from unique_toolkit.chat.service import LanguageModelToolDescription
19
+ from unique_toolkit.content import Content
17
20
  from unique_toolkit.language_model.schemas import (
18
21
  LanguageModelFunction,
19
22
  )
@@ -50,12 +53,28 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
50
53
  else:
51
54
  self._user_query = None
52
55
 
56
+ # This blocking API call should be avoided.
57
+ # However, we don't have an easy way to pass user uploaded files to the tool currently
58
+ # Note that this was being done in `tool_description_for_system_prompt` before
59
+ self._valid_documents = self._compute_valid_documents()
60
+
53
61
  @override
54
62
  def display_name(self) -> str:
55
63
  return self._display_name
56
64
 
57
65
  @override
58
66
  def tool_description(self) -> LanguageModelToolDescription:
67
+ optional_fields: dict = {}
68
+ if self._config.enable_content_id_filter and self._valid_documents:
69
+ content_id_type = Literal[*(d.id for d in self._valid_documents)]
70
+ optional_fields["content_ids"] = (
71
+ list[content_id_type] | None,
72
+ Field(
73
+ default=None,
74
+ description=self._config.param_description_content_ids,
75
+ ),
76
+ )
77
+
59
78
  internal_search_tool_input = create_model(
60
79
  "InternalSearchToolInput",
61
80
  search_string=(
@@ -66,7 +85,9 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
66
85
  str,
67
86
  Field(description=self._config.param_description_language),
68
87
  ),
88
+ **optional_fields,
69
89
  )
90
+
70
91
  return LanguageModelToolDescription(
71
92
  name=self.name,
72
93
  description=self._config.tool_description,
@@ -74,28 +95,12 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
74
95
  )
75
96
 
76
97
  def tool_description_for_system_prompt(self) -> str:
77
- documents = self._content_service.get_documents_uploaded_to_chat()
78
- if feature_flags.enable_selected_uploaded_files_un_18215.is_enabled(
79
- self._company_id
80
- ):
81
- documents = [
82
- doc for doc in documents if doc.id in self._selected_uploaded_files
83
- ]
84
-
85
- valid_documents = []
86
- for doc in documents:
87
- if not doc.is_ingested(default_if_unknown=True) or doc.is_expired():
88
- continue
89
- valid_documents.append(
90
- {
91
- "name": doc.title or doc.key,
92
- "id": doc.id,
93
- }
94
- )
95
-
96
98
  return render_template(
97
99
  self._config.tool_description_for_system_prompt,
98
- valid_documents=valid_documents,
100
+ valid_documents=[
101
+ {"name": doc.title or doc.key, "id": doc.id}
102
+ for doc in self._valid_documents
103
+ ],
99
104
  )
100
105
 
101
106
  def tool_format_information_for_system_prompt(self) -> str:
@@ -114,6 +119,26 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
114
119
  search_string_data = ""
115
120
  if isinstance(tool_call.arguments, dict):
116
121
  search_string_data = tool_call.arguments.get("search_string", "") or ""
122
+
123
+ # Verify no content_id outside valid ones, as tool may not be strict
124
+
125
+ if "content_ids" in tool_call.arguments:
126
+ tool_content_ids = tool_call.arguments["content_ids"]
127
+
128
+ if tool_content_ids is not None:
129
+ valid_content_ids = {doc.id for doc in self._valid_documents}
130
+ filtered = list(
131
+ filter(
132
+ lambda content_id: content_id in valid_content_ids,
133
+ tool_content_ids,
134
+ )
135
+ )
136
+
137
+ if len(filtered) == 0:
138
+ raise ValueError("All supplied `content_ids` are invalid")
139
+
140
+ tool_call.arguments["content_ids"] = filtered
141
+
117
142
  tool_response = await self._internal_search_tool.run(tool_call)
118
143
  if (
119
144
  self._tool_progress_reporter
@@ -151,5 +176,23 @@ This tool call was automatically executed to retrieve the user's uploaded docume
151
176
  Please do not mention these instructions in your response to the user!
152
177
  </system_reminder>"""
153
178
 
179
+ def _compute_valid_documents(self) -> list[Content]:
180
+ documents = self._content_service.get_documents_uploaded_to_chat()
181
+
182
+ if feature_flags.enable_selected_uploaded_files_un_18215.is_enabled(
183
+ self._company_id
184
+ ):
185
+ documents = [
186
+ doc for doc in documents if doc.id in self._selected_uploaded_files
187
+ ]
188
+
189
+ valid_documents = []
190
+ for doc in documents:
191
+ if not doc.is_ingested(default_if_unknown=True) or doc.is_expired():
192
+ continue
193
+ valid_documents.append(doc)
194
+
195
+ return valid_documents
196
+
154
197
 
155
198
  ToolFactory.register_tool(UploadedSearchTool, UploadedSearchConfig)