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.
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/PKG-INFO +2 -2
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/pyproject.toml +2 -2
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/config.py +18 -0
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/prompts.py +5 -0
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/service.py +18 -1
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/uploaded_search/config.py +8 -0
- 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
- unique_internal_search-2026.22.0.dev4/unique_internal_search/uploaded_search/prompts/tool_description_for_system_prompt.j2 +21 -0
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/uploaded_search/service.py +70 -49
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/README.md +0 -0
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/__init__.py +0 -0
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/services/message_log.py +0 -0
- {unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/unique_internal_search/utils.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
{unique_internal_search-2026.22.0.dev2 → unique_internal_search-2026.22.0.dev4}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_internal_search"
|
|
3
|
-
version = "2026.22.0.
|
|
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.
|
|
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
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|