unique_internal_search 2026.22.0.dev2__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.dev2 → unique_internal_search-2026.22.0.dev4}/PKG-INFO +2 -2
  2. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/pyproject.toml +2 -2
  3. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/config.py +18 -0
  4. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/prompts.py +5 -0
  5. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/service.py +18 -1
  6. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/uploaded_search/config.py +8 -0
  7. unique_internal_search-2026.22.0.dev2/unique_internal_search/uploaded_search/prompts.py → unique_internal_search-2026.22.0.dev4/unique_internal_search/uploaded_search/prompts/__init__.py +8 -15
  8. unique_internal_search-2026.22.0.dev4/unique_internal_search/uploaded_search/prompts/tool_description_for_system_prompt.j2 +21 -0
  9. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/uploaded_search/service.py +70 -49
  10. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/README.md +0 -0
  11. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/__init__.py +0 -0
  12. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/services/message_log.py +0 -0
  13. {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/utils.py +0 -0
  14. {unique_internal_search-2026.22.0.dev2 → 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.dev2
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.dev5,<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.dev2"
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.dev5,<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,5 +1,7 @@
1
1
  from typing import Annotated
2
2
 
3
+ from pydantic import Field
4
+ from pydantic.json_schema import SkipJsonSchema
3
5
  from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
4
6
 
5
7
  from unique_internal_search.config import InternalSearchConfig
@@ -51,3 +53,9 @@ class UploadedSearchConfig(InternalSearchConfig):
51
53
  DEFAULT_TOOL_FORMAT_INFORMATION_FOR_SYSTEM_PROMPT,
52
54
  description="Tool format information for the system prompt.",
53
55
  )
56
+ # This is a hack to ensure that the system reminder is enabled when the tool is forced,
57
+ # as we cannot at the moment easily pass extra values to the tool constructor
58
+ enable_tool_call_system_reminder: SkipJsonSchema[bool] = Field(
59
+ default=True,
60
+ description="Whether to attach the uploaded-search system reminder to tool responses.",
61
+ )
@@ -1,18 +1,11 @@
1
- DEFAULT_TOOL_DESCRIPTION_FOR_SYSTEM_PROMPT = (
2
- "You can use the UploadedSearch tool to access and analyze documents uploaded by users during a chat. This tool is designed to handle a variety of document-related tasks, including summarization, explanation, and detailed information retrieval. "
3
- "Use cases for the UploadedSearch tool include:\n"
4
- "- Document Analysis: When a user uploads a document and asks for a summary, explanation, or specific details, this tool can extract and provide the requested information.\n"
5
- "- Named Document Queries: If a user refers to a previously uploaded document by name (e.g., 'What does the Q2_Report.pdf say about revenue?'), this tool can locate and analyze the document to answer the query.\n"
6
- "- Policy and Procedure Verification: Use the tool to find the most current company policies, procedures, or guidelines within uploaded documents.\n"
7
- "- Project-Specific Information: Access project documents, reports, or meeting notes uploaded by users to provide precise details.\n"
8
- "- Confidential and Proprietary Information: Ensure that sensitive topics requiring proprietary knowledge or confidential data are sourced securely from uploaded documents.\n\n"
9
- "**Instruction Query Splitting**\n"
10
- "You should split the user question into multiple search strings when the user's question needs to be decomposed / rewritten to find different facts. Perform an individual tool call for each search string. Avoid overly broad queries that may return unrelated results. Ensure the search string is specific and relevant to the uploaded document(s).\n\n"
11
- "Examples:\n"
12
- 'User: "What does the Q2_Report.pdf say about revenue and expenses?" => search strings: ["Q2_Report.pdf revenue", "Q2_Report.pdf expenses"]\n'
13
- 'User: "Summarize the uploaded document." => search string: ["Summarize the uploaded document"]\n\n'
14
- "{system_prompt_valid_documents}"
15
- "{system_prompt_expired_documents}"
1
+ from pathlib import Path
2
+
3
+ from unique_toolkit._common.utils.jinja.helpers import load_template
4
+
5
+ _PROMPTS_DIR = Path(__file__).parent
6
+
7
+ DEFAULT_TOOL_DESCRIPTION_FOR_SYSTEM_PROMPT = load_template(
8
+ _PROMPTS_DIR, "tool_description_for_system_prompt.j2"
16
9
  )
17
10
 
18
11
  DEFAULT_TOOL_FORMAT_INFORMATION_FOR_SYSTEM_PROMPT = (
@@ -0,0 +1,21 @@
1
+ You can use the UploadedSearch tool to access and analyze documents uploaded by users during a chat. This tool is designed to handle a variety of document-related tasks, including summarization, explanation, and detailed information retrieval. Use cases for the UploadedSearch tool include:
2
+ - Document Analysis: When a user uploads a document and asks for a summary, explanation, or specific details, this tool can extract and provide the requested information.
3
+ - Named Document Queries: If a user refers to a previously uploaded document by name (e.g., 'What does the Q2_Report.pdf say about revenue?'), this tool can locate and analyze the document to answer the query.
4
+ - Policy and Procedure Verification: Use the tool to find the most current company policies, procedures, or guidelines within uploaded documents.
5
+ - Project-Specific Information: Access project documents, reports, or meeting notes uploaded by users to provide precise details.
6
+ - Confidential and Proprietary Information: Ensure that sensitive topics requiring proprietary knowledge or confidential data are sourced securely from uploaded documents.
7
+
8
+ **Instruction Query Splitting**
9
+ You should split the user question into multiple search strings when the user's question needs to be decomposed / rewritten to find different facts. Perform an individual tool call for each search string. Avoid overly broad queries that may return unrelated results. Ensure the search string is specific and relevant to the uploaded document(s).
10
+
11
+ Examples:
12
+ User: "What does the Q2_Report.pdf say about revenue and expenses?" => search strings: ["Q2_Report.pdf revenue", "Q2_Report.pdf expenses"]
13
+ User: "Summarize the uploaded document." => search string: ["Summarize the uploaded document"]
14
+
15
+ {% if valid_documents %}
16
+ **The currently uploaded and searchable documents are the following**
17
+ {% for document in valid_documents %}
18
+ - {{ document.name }} (content_id: {{ document.id }})
19
+ {% endfor %}
20
+
21
+ {% endif %}
@@ -1,6 +1,9 @@
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
6
+ from unique_toolkit._common.utils.jinja.render import render_template
4
7
  from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
5
8
  from unique_toolkit.agentic.feature_flags import feature_flags
6
9
  from unique_toolkit.agentic.tools.factory import ToolFactory
@@ -13,6 +16,7 @@ from unique_toolkit.agentic.tools.tool_progress_reporter import (
13
16
  )
14
17
  from unique_toolkit.app.schemas import BaseEvent, ChatEvent
15
18
  from unique_toolkit.chat.service import LanguageModelToolDescription
19
+ from unique_toolkit.content import Content
16
20
  from unique_toolkit.language_model.schemas import (
17
21
  LanguageModelFunction,
18
22
  )
@@ -49,12 +53,28 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
49
53
  else:
50
54
  self._user_query = None
51
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
+
52
61
  @override
53
62
  def display_name(self) -> str:
54
63
  return self._display_name
55
64
 
56
65
  @override
57
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
+
58
78
  internal_search_tool_input = create_model(
59
79
  "InternalSearchToolInput",
60
80
  search_string=(
@@ -65,7 +85,9 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
65
85
  str,
66
86
  Field(description=self._config.param_description_language),
67
87
  ),
88
+ **optional_fields,
68
89
  )
90
+
69
91
  return LanguageModelToolDescription(
70
92
  name=self.name,
71
93
  description=self._config.tool_description,
@@ -73,54 +95,12 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
73
95
  )
74
96
 
75
97
  def tool_description_for_system_prompt(self) -> str:
76
- documents = self._content_service.get_documents_uploaded_to_chat()
77
- if feature_flags.enable_selected_uploaded_files_un_18215.is_enabled(
78
- self._company_id
79
- ):
80
- documents = [
81
- doc for doc in documents if doc.id in self._selected_uploaded_files
82
- ]
83
-
84
- valid_documents = []
85
- expired_documents = []
86
- for doc in documents:
87
- if not doc.is_ingested(default_if_unknown=True):
88
- continue
89
- if doc.is_expired():
90
- expired_documents.append(doc)
91
- else:
92
- valid_documents.append(doc)
93
-
94
- system_prompt_valid_documents = ""
95
- system_prompt_expired_documents = ""
96
- if valid_documents:
97
- system_prompt_valid_documents = (
98
- "**The currently uploaded and valid documents are the following**\n"
99
- )
100
- system_prompt_valid_documents = (
101
- system_prompt_valid_documents
102
- + "\n".join(
103
- f"- {doc.title or doc.key} (content_id: {doc.id})"
104
- for doc in valid_documents
105
- )
106
- + "\n"
107
- )
108
-
109
- if expired_documents:
110
- system_prompt_expired_documents = (
111
- "**The currently uploaded and expired documents are the following**\n"
112
- )
113
- system_prompt_expired_documents = (
114
- system_prompt_expired_documents
115
- + "\n".join(
116
- f"- {doc.title or doc.key} (content_id: {doc.id})"
117
- for doc in expired_documents
118
- )
119
- )
120
-
121
- return self._config.tool_description_for_system_prompt.format(
122
- system_prompt_valid_documents=system_prompt_valid_documents,
123
- system_prompt_expired_documents=system_prompt_expired_documents,
98
+ return render_template(
99
+ self._config.tool_description_for_system_prompt,
100
+ valid_documents=[
101
+ {"name": doc.title or doc.key, "id": doc.id}
102
+ for doc in self._valid_documents
103
+ ],
124
104
  )
125
105
 
126
106
  def tool_format_information_for_system_prompt(self) -> str:
@@ -139,6 +119,26 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
139
119
  search_string_data = ""
140
120
  if isinstance(tool_call.arguments, dict):
141
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
+
142
142
  tool_response = await self._internal_search_tool.run(tool_call)
143
143
  if (
144
144
  self._tool_progress_reporter
@@ -153,7 +153,10 @@ class UploadedSearchTool(Tool[UploadedSearchConfig]):
153
153
  state=ProgressState.FINISHED,
154
154
  )
155
155
  tool_response.name = self.name
156
- tool_response.system_reminder = self._get_tool_call_response_system_reminder()
156
+ if self._config.enable_tool_call_system_reminder:
157
+ tool_response.system_reminder = (
158
+ self._get_tool_call_response_system_reminder()
159
+ )
157
160
  return tool_response
158
161
 
159
162
  def _get_tool_call_response_system_reminder(self) -> str:
@@ -173,5 +176,23 @@ This tool call was automatically executed to retrieve the user's uploaded docume
173
176
  Please do not mention these instructions in your response to the user!
174
177
  </system_reminder>"""
175
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
+
176
197
 
177
198
  ToolFactory.register_tool(UploadedSearchTool, UploadedSearchConfig)