unique_toolkit 0.7.7__py3-none-any.whl → 1.23.0__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.
Potentially problematic release.
This version of unique_toolkit might be problematic. Click here for more details.
- unique_toolkit/__init__.py +28 -1
- unique_toolkit/_common/api_calling/human_verification_manager.py +343 -0
- unique_toolkit/_common/base_model_type_attribute.py +303 -0
- unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
- unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
- unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
- unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
- unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
- unique_toolkit/_common/default_language_model.py +12 -0
- unique_toolkit/_common/docx_generator/__init__.py +7 -0
- unique_toolkit/_common/docx_generator/config.py +12 -0
- unique_toolkit/_common/docx_generator/schemas.py +80 -0
- unique_toolkit/_common/docx_generator/service.py +252 -0
- unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
- unique_toolkit/_common/endpoint_builder.py +305 -0
- unique_toolkit/_common/endpoint_requestor.py +430 -0
- unique_toolkit/_common/exception.py +24 -0
- unique_toolkit/_common/feature_flags/schema.py +9 -0
- unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
- unique_toolkit/_common/pydantic_helpers.py +154 -0
- unique_toolkit/_common/referencing.py +53 -0
- unique_toolkit/_common/string_utilities.py +140 -0
- unique_toolkit/_common/tests/test_referencing.py +521 -0
- unique_toolkit/_common/tests/test_string_utilities.py +506 -0
- unique_toolkit/_common/token/image_token_counting.py +67 -0
- unique_toolkit/_common/token/token_counting.py +204 -0
- unique_toolkit/_common/utils/__init__.py +1 -0
- unique_toolkit/_common/utils/files.py +43 -0
- unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
- unique_toolkit/_common/utils/structured_output/schema.py +5 -0
- unique_toolkit/_common/utils/write_configuration.py +51 -0
- unique_toolkit/_common/validators.py +101 -4
- unique_toolkit/agentic/__init__.py +1 -0
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
- unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
- unique_toolkit/agentic/evaluation/config.py +36 -0
- unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
- unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
- unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
- unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
- unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
- unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
- unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
- unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
- unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
- unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
- unique_toolkit/agentic/history_manager/history_manager.py +242 -0
- unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
- unique_toolkit/agentic/history_manager/utils.py +96 -0
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
- unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
- unique_toolkit/agentic/responses_api/__init__.py +19 -0
- unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
- unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
- unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
- unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
- unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
- unique_toolkit/agentic/tools/__init__.py +1 -0
- unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
- unique_toolkit/agentic/tools/a2a/config.py +17 -0
- unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
- unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
- unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
- unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
- unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
- unique_toolkit/agentic/tools/a2a/manager.py +55 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
- unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
- unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
- unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
- unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
- unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
- unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
- unique_toolkit/agentic/tools/config.py +167 -0
- unique_toolkit/agentic/tools/factory.py +44 -0
- unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
- unique_toolkit/agentic/tools/mcp/manager.py +71 -0
- unique_toolkit/agentic/tools/mcp/models.py +28 -0
- unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
- unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
- unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
- unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
- unique_toolkit/agentic/tools/schemas.py +141 -0
- unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
- unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
- unique_toolkit/agentic/tools/tool.py +183 -0
- unique_toolkit/agentic/tools/tool_manager.py +523 -0
- unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
- unique_toolkit/agentic/tools/utils/__init__.py +19 -0
- unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
- unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
- unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
- unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
- unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
- unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
- unique_toolkit/app/__init__.py +6 -0
- unique_toolkit/app/dev_util.py +180 -0
- unique_toolkit/app/init_sdk.py +32 -1
- unique_toolkit/app/schemas.py +198 -31
- unique_toolkit/app/unique_settings.py +367 -0
- unique_toolkit/chat/__init__.py +8 -1
- unique_toolkit/chat/deprecated/service.py +232 -0
- unique_toolkit/chat/functions.py +642 -77
- unique_toolkit/chat/rendering.py +34 -0
- unique_toolkit/chat/responses_api.py +461 -0
- unique_toolkit/chat/schemas.py +133 -2
- unique_toolkit/chat/service.py +115 -767
- unique_toolkit/content/functions.py +153 -4
- unique_toolkit/content/schemas.py +122 -15
- unique_toolkit/content/service.py +278 -44
- unique_toolkit/content/smart_rules.py +301 -0
- unique_toolkit/content/utils.py +8 -3
- unique_toolkit/embedding/service.py +102 -11
- unique_toolkit/framework_utilities/__init__.py +1 -0
- unique_toolkit/framework_utilities/langchain/client.py +71 -0
- unique_toolkit/framework_utilities/langchain/history.py +19 -0
- unique_toolkit/framework_utilities/openai/__init__.py +6 -0
- unique_toolkit/framework_utilities/openai/client.py +83 -0
- unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
- unique_toolkit/framework_utilities/utils.py +23 -0
- unique_toolkit/language_model/__init__.py +3 -0
- unique_toolkit/language_model/builder.py +27 -11
- unique_toolkit/language_model/default_language_model.py +3 -0
- unique_toolkit/language_model/functions.py +327 -43
- unique_toolkit/language_model/infos.py +992 -50
- unique_toolkit/language_model/reference.py +242 -0
- unique_toolkit/language_model/schemas.py +475 -48
- unique_toolkit/language_model/service.py +228 -27
- unique_toolkit/protocols/support.py +145 -0
- unique_toolkit/services/__init__.py +7 -0
- unique_toolkit/services/chat_service.py +1630 -0
- unique_toolkit/services/knowledge_base.py +861 -0
- unique_toolkit/short_term_memory/service.py +178 -41
- unique_toolkit/smart_rules/__init__.py +0 -0
- unique_toolkit/smart_rules/compile.py +56 -0
- unique_toolkit/test_utilities/events.py +197 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
- unique_toolkit-1.23.0.dist-info/RECORD +182 -0
- unique_toolkit/evaluators/__init__.py +0 -1
- unique_toolkit/evaluators/config.py +0 -35
- unique_toolkit/evaluators/constants.py +0 -1
- unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
- unique_toolkit/evaluators/context_relevancy/service.py +0 -53
- unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
- unique_toolkit/evaluators/hallucination/constants.py +0 -41
- unique_toolkit-0.7.7.dist-info/RECORD +0 -64
- /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
unique_toolkit/app/schemas.py
CHANGED
|
@@ -1,23 +1,43 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from enum import StrEnum
|
|
2
|
-
from
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Generic, Optional, TypeVar, override
|
|
3
6
|
|
|
4
7
|
from humps import camelize
|
|
5
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
6
10
|
from typing_extensions import deprecated
|
|
7
11
|
|
|
12
|
+
from unique_toolkit._common.exception import ConfigurationException
|
|
13
|
+
from unique_toolkit.app.unique_settings import UniqueChatEventFilterOptions
|
|
14
|
+
from unique_toolkit.smart_rules.compile import UniqueQL, parse_uniqueql
|
|
15
|
+
|
|
16
|
+
FilterOptionsT = TypeVar("FilterOptionsT", bound=BaseSettings)
|
|
17
|
+
|
|
8
18
|
# set config to convert camelCase to snake_case
|
|
9
19
|
model_config = ConfigDict(
|
|
10
20
|
alias_generator=camelize,
|
|
11
21
|
populate_by_name=True,
|
|
12
22
|
arbitrary_types_allowed=True,
|
|
13
23
|
)
|
|
24
|
+
_logger = getLogger(__name__)
|
|
14
25
|
|
|
15
26
|
|
|
16
27
|
class EventName(StrEnum):
|
|
17
28
|
EXTERNAL_MODULE_CHOSEN = "unique.chat.external-module.chosen"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
USER_MESSAGE_CREATED = "unique.chat.user-message.created"
|
|
30
|
+
INGESTION_CONTENT_UPLOADED = "unique.ingestion.content.uploaded"
|
|
31
|
+
INGESTION_CONTENT_FINISHED = "unique.ingestion.content.finished"
|
|
32
|
+
MAGIC_TABLE_IMPORT_COLUMNS = "unique.magic-table.import-columns"
|
|
33
|
+
MAGIC_TABLE_ADD_META_DATA = "unique.magic-table.add-meta-data"
|
|
34
|
+
MAGIC_TABLE_ADD_DOCUMENT = "unique.magic-table.add-document"
|
|
35
|
+
MAGIC_TABLE_DELETE_ROW = "unique.magic-table.delete-row"
|
|
36
|
+
MAGIC_TABLE_DELETE_COLUMN = "unique.magic-table.delete-column"
|
|
37
|
+
MAGIC_TABLE_UPDATE_CELL = "unique.magic-table.update-cell"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BaseEvent(BaseModel, Generic[FilterOptionsT]):
|
|
21
41
|
model_config = model_config
|
|
22
42
|
|
|
23
43
|
id: str
|
|
@@ -25,6 +45,68 @@ class BaseEvent(BaseModel):
|
|
|
25
45
|
user_id: str
|
|
26
46
|
company_id: str
|
|
27
47
|
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_json_file(cls, file_path: Path) -> "BaseEvent":
|
|
50
|
+
if not file_path.exists():
|
|
51
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
52
|
+
with file_path.open("r", encoding="utf-8") as f:
|
|
53
|
+
data = json.load(f)
|
|
54
|
+
return cls.model_validate(data)
|
|
55
|
+
|
|
56
|
+
def filter_event(self, *, filter_options: FilterOptionsT | None = None) -> bool:
|
|
57
|
+
"""Determine if event should be filtered out and be neglected."""
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
###
|
|
62
|
+
# MCP schemas
|
|
63
|
+
###
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class McpTool(BaseModel):
|
|
67
|
+
model_config = model_config
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
description: Optional[str] = None
|
|
71
|
+
input_schema: dict[str, Any]
|
|
72
|
+
output_schema: Optional[dict[str, Any]] = None
|
|
73
|
+
annotations: Optional[dict[str, Any]] = None
|
|
74
|
+
title: Optional[str] = Field(
|
|
75
|
+
default=None,
|
|
76
|
+
description="The display title for a tool. This is a Unique specific field.",
|
|
77
|
+
)
|
|
78
|
+
icon: Optional[str] = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
description="An icon name from the Lucide icon set for the tool. This is a Unique specific field.",
|
|
81
|
+
)
|
|
82
|
+
system_prompt: Optional[str] = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
description="An optional system prompt for the tool. This is a Unique specific field.",
|
|
85
|
+
)
|
|
86
|
+
user_prompt: Optional[str] = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
description="An optional user prompt for the tool. This is a Unique specific field.",
|
|
89
|
+
)
|
|
90
|
+
is_connected: bool = Field(
|
|
91
|
+
description="Whether the tool is connected to the MCP server. This is a Unique specific field.",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class McpServer(BaseModel):
|
|
96
|
+
model_config = model_config
|
|
97
|
+
|
|
98
|
+
id: str
|
|
99
|
+
name: str
|
|
100
|
+
system_prompt: Optional[str] = Field(
|
|
101
|
+
default=None,
|
|
102
|
+
description="An optional system prompt for the MCP server.",
|
|
103
|
+
)
|
|
104
|
+
user_prompt: Optional[str] = Field(
|
|
105
|
+
default=None,
|
|
106
|
+
description="An optional user prompt for the MCP server.",
|
|
107
|
+
)
|
|
108
|
+
tools: list[McpTool] = []
|
|
109
|
+
|
|
28
110
|
|
|
29
111
|
###
|
|
30
112
|
# ChatEvent schemas
|
|
@@ -95,45 +177,130 @@ class ChatEventPayload(BaseModel):
|
|
|
95
177
|
assistant_id: str
|
|
96
178
|
user_message: ChatEventUserMessage
|
|
97
179
|
assistant_message: ChatEventAssistantMessage
|
|
98
|
-
text:
|
|
99
|
-
additional_parameters:
|
|
100
|
-
user_metadata:
|
|
101
|
-
|
|
102
|
-
|
|
180
|
+
text: str | None = None
|
|
181
|
+
additional_parameters: ChatEventAdditionalParameters | None = None
|
|
182
|
+
user_metadata: dict[str, Any] | None = Field(
|
|
183
|
+
default_factory=dict,
|
|
184
|
+
)
|
|
185
|
+
tool_choices: list[str] = Field(
|
|
186
|
+
default_factory=list,
|
|
103
187
|
description="A list containing the tool names the user has chosen to be activated.",
|
|
104
188
|
)
|
|
105
|
-
|
|
106
|
-
|
|
189
|
+
disabled_tools: list[str] = Field(
|
|
190
|
+
default_factory=list,
|
|
191
|
+
description="A list containing the tool names of tools that are disabled at the company level",
|
|
192
|
+
)
|
|
193
|
+
tool_parameters: dict[str, Any] = Field(
|
|
194
|
+
default_factory=dict,
|
|
195
|
+
description="Parameters extracted from module selection function calling the tool.",
|
|
196
|
+
)
|
|
197
|
+
# Default is None as empty dict triggers error in `backend-ingestion`
|
|
198
|
+
metadata_filter: dict[str, Any] | None = Field(
|
|
199
|
+
default=None,
|
|
200
|
+
description="Metadata filter compiled after module selection function calling and scope rules.",
|
|
201
|
+
)
|
|
202
|
+
raw_scope_rules: UniqueQL | None = Field(
|
|
203
|
+
default=None,
|
|
204
|
+
description="Raw UniqueQL rule that can be compiled to a metadata filter.",
|
|
205
|
+
)
|
|
206
|
+
mcp_servers: list[McpServer] = Field(
|
|
207
|
+
default_factory=list,
|
|
208
|
+
description="A list of MCP servers with tools available for the chat session.",
|
|
209
|
+
)
|
|
210
|
+
message_execution_id: str | None = Field(
|
|
211
|
+
default=None,
|
|
212
|
+
description="The message execution id for triggering the chat event. Originates from the message execution service.",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@field_validator("raw_scope_rules", mode="before")
|
|
216
|
+
def validate_scope_rules(cls, value: dict[str, Any] | None) -> UniqueQL | None:
|
|
217
|
+
if value:
|
|
218
|
+
return parse_uniqueql(value)
|
|
107
219
|
|
|
108
220
|
|
|
109
221
|
@deprecated("""Use `ChatEventPayload` instead.
|
|
110
222
|
This class will be removed in the next major version.""")
|
|
111
223
|
class EventPayload(ChatEventPayload):
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
224
|
+
pass
|
|
225
|
+
# user_message: EventUserMessage
|
|
226
|
+
# assistant_message: EventAssistantMessage
|
|
227
|
+
# additional_parameters: Optional[EventAdditionalParameters] = None
|
|
115
228
|
|
|
116
229
|
|
|
117
|
-
|
|
118
|
-
"""Use the more specific `ChatEvent` instead that has the same properties. \
|
|
119
|
-
This class will be removed in the next major version."""
|
|
120
|
-
)
|
|
121
|
-
class Event(BaseModel):
|
|
230
|
+
class ChatEvent(BaseEvent):
|
|
122
231
|
model_config = model_config
|
|
123
232
|
|
|
124
|
-
|
|
125
|
-
event: EventName
|
|
126
|
-
user_id: str
|
|
127
|
-
company_id: str
|
|
128
|
-
payload: EventPayload
|
|
233
|
+
payload: ChatEventPayload
|
|
129
234
|
created_at: Optional[int] = None
|
|
130
235
|
version: Optional[str] = None
|
|
131
236
|
|
|
237
|
+
@classmethod
|
|
238
|
+
def from_json_file(cls, file_path: Path) -> "ChatEvent":
|
|
239
|
+
if not file_path.exists():
|
|
240
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
241
|
+
with file_path.open("r", encoding="utf-8") as f:
|
|
242
|
+
data = json.load(f)
|
|
243
|
+
return cls.model_validate(data)
|
|
244
|
+
|
|
245
|
+
def get_initial_debug_info(self) -> dict[str, Any]:
|
|
246
|
+
"""Get the debug information for the chat event"""
|
|
247
|
+
|
|
248
|
+
# TODO: Make sure this coincides with what is shown in the first user message
|
|
249
|
+
return {
|
|
250
|
+
"user_metadata": self.payload.user_metadata,
|
|
251
|
+
"tool_parameters": self.payload.tool_parameters,
|
|
252
|
+
"chosen_module": self.payload.name,
|
|
253
|
+
"assistant": {"id": self.payload.assistant_id},
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@override
|
|
257
|
+
def filter_event(
|
|
258
|
+
self, *, filter_options: UniqueChatEventFilterOptions | None = None
|
|
259
|
+
) -> bool:
|
|
260
|
+
# Empty string evals to False
|
|
261
|
+
|
|
262
|
+
if filter_options is None:
|
|
263
|
+
return False # Don't filter when no options provided
|
|
264
|
+
|
|
265
|
+
if not filter_options.assistant_ids and not filter_options.references_in_code:
|
|
266
|
+
raise ConfigurationException(
|
|
267
|
+
"No filter options provided, all events will be filtered! \n"
|
|
268
|
+
"Please define: \n"
|
|
269
|
+
" - 'UNIQUE_CHAT_EVENT_FILTER_OPTIONS_ASSISTANT_IDS' \n"
|
|
270
|
+
" - 'UNIQUE_CHAT_EVENT_FILTER_OPTIONS_REFERENCES_IN_CODE' \n"
|
|
271
|
+
"in your environment variables."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Per reference in code there can be multiple assistants
|
|
275
|
+
if (
|
|
276
|
+
filter_options.assistant_ids
|
|
277
|
+
and self.payload.assistant_id not in filter_options.assistant_ids
|
|
278
|
+
):
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
if (
|
|
282
|
+
filter_options.references_in_code
|
|
283
|
+
and self.payload.name not in filter_options.references_in_code
|
|
284
|
+
):
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
return super().filter_event(filter_options=filter_options)
|
|
132
288
|
|
|
133
|
-
class ChatEvent(BaseEvent):
|
|
134
|
-
model_config = model_config
|
|
135
289
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
290
|
+
@deprecated(
|
|
291
|
+
"""Use the more specific `ChatEvent` instead that has the same properties. \
|
|
292
|
+
This class will be removed in the next major version."""
|
|
293
|
+
)
|
|
294
|
+
class Event(ChatEvent):
|
|
295
|
+
pass
|
|
296
|
+
# The below should only affect type hints
|
|
297
|
+
# event: EventName T
|
|
298
|
+
# payload: EventPayload
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def from_json_file(cls, file_path: Path) -> "Event":
|
|
302
|
+
if not file_path.exists():
|
|
303
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
304
|
+
with file_path.open("r", encoding="utf-8") as f:
|
|
305
|
+
data = json.load(f)
|
|
306
|
+
return cls.model_validate(data)
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Self, TypeVar
|
|
5
|
+
from urllib.parse import ParseResult, urlparse, urlunparse
|
|
6
|
+
|
|
7
|
+
import unique_sdk
|
|
8
|
+
from platformdirs import user_config_dir
|
|
9
|
+
from pydantic import AliasChoices, Field, SecretStr, model_validator
|
|
10
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from unique_toolkit.app.schemas import BaseEvent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T", bound=BaseSettings)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def warn_about_defaults(instance: T) -> T:
|
|
22
|
+
"""Log warnings for fields that are using default values."""
|
|
23
|
+
for field_name, model_field in instance.__class__.model_fields.items():
|
|
24
|
+
field_value = getattr(instance, field_name)
|
|
25
|
+
default_value = model_field.default
|
|
26
|
+
|
|
27
|
+
# Handle SecretStr comparison by comparing the secret values
|
|
28
|
+
if isinstance(field_value, SecretStr) and isinstance(default_value, SecretStr):
|
|
29
|
+
if field_value.get_secret_value() == default_value.get_secret_value():
|
|
30
|
+
logger.warning(
|
|
31
|
+
f"Using default value for '{field_name}': {default_value.get_secret_value()}"
|
|
32
|
+
)
|
|
33
|
+
elif field_value == default_value:
|
|
34
|
+
logger.warning(f"Using default value for '{field_name}': {default_value}")
|
|
35
|
+
return instance
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UniqueApp(BaseSettings):
|
|
39
|
+
id: SecretStr = Field(
|
|
40
|
+
default=SecretStr("dummy_id"),
|
|
41
|
+
validation_alias=AliasChoices(
|
|
42
|
+
"unique_app_id", "app_id", "UNIQUE_APP_ID", "APP_ID"
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
key: SecretStr = Field(
|
|
46
|
+
default=SecretStr("dummy_key"),
|
|
47
|
+
validation_alias=AliasChoices(
|
|
48
|
+
"unique_app_key", "key", "UNIQUE_APP_KEY", "KEY", "API_KEY", "api_key"
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
base_url: str = Field(
|
|
52
|
+
default="http://localhost:8092/",
|
|
53
|
+
deprecated="Use UniqueApi.base_url instead",
|
|
54
|
+
)
|
|
55
|
+
endpoint: str = Field(default="dummy")
|
|
56
|
+
|
|
57
|
+
endpoint_secret: SecretStr = Field(default=SecretStr("dummy_secret"))
|
|
58
|
+
|
|
59
|
+
@model_validator(mode="after")
|
|
60
|
+
def _warn_about_defaults(self) -> Self:
|
|
61
|
+
return warn_about_defaults(self)
|
|
62
|
+
|
|
63
|
+
model_config = SettingsConfigDict(
|
|
64
|
+
env_prefix="unique_app_",
|
|
65
|
+
env_file_encoding="utf-8",
|
|
66
|
+
case_sensitive=False,
|
|
67
|
+
extra="ignore",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class UniqueApi(BaseSettings):
|
|
72
|
+
base_url: str = Field(
|
|
73
|
+
default="http://localhost:8092/",
|
|
74
|
+
description="The base URL of the Unique API. Ask your admin to provide you with the correct URL.",
|
|
75
|
+
validation_alias=AliasChoices(
|
|
76
|
+
"unique_api_base_url",
|
|
77
|
+
"base_url",
|
|
78
|
+
"UNIQUE_API_BASE_URL",
|
|
79
|
+
"BASE_URL",
|
|
80
|
+
"API_BASE",
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
version: str = Field(
|
|
84
|
+
default="2023-12-06",
|
|
85
|
+
validation_alias=AliasChoices(
|
|
86
|
+
"unique_api_version", "version", "UNIQUE_API_VERSION", "VERSION"
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
model_config = SettingsConfigDict(
|
|
91
|
+
env_prefix="unique_api_",
|
|
92
|
+
env_file_encoding="utf-8",
|
|
93
|
+
case_sensitive=False,
|
|
94
|
+
extra="ignore",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@model_validator(mode="after")
|
|
98
|
+
def _warn_about_defaults(self) -> Self:
|
|
99
|
+
return warn_about_defaults(self)
|
|
100
|
+
|
|
101
|
+
def sse_url(self, subscriptions: list[str]) -> str:
|
|
102
|
+
parsed = urlparse(self.base_url)
|
|
103
|
+
return urlunparse(
|
|
104
|
+
parsed._replace(
|
|
105
|
+
path="/public/event-socket/events/stream",
|
|
106
|
+
query=f"subscriptions={','.join(subscriptions)}",
|
|
107
|
+
fragment=None,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def base_path(self) -> tuple[ParseResult, str]:
|
|
112
|
+
parsed = urlparse(self.base_url)
|
|
113
|
+
base_path = "/public/chat/"
|
|
114
|
+
|
|
115
|
+
if parsed.hostname and (
|
|
116
|
+
"gateway.qa.unique" in parsed.hostname
|
|
117
|
+
or "gateway.unique" in parsed.hostname
|
|
118
|
+
):
|
|
119
|
+
base_path = "/public/chat-gen2/"
|
|
120
|
+
|
|
121
|
+
if parsed.hostname and (
|
|
122
|
+
"localhost" in parsed.hostname or "svc.cluster.local" in parsed.hostname
|
|
123
|
+
):
|
|
124
|
+
base_path = "/public/"
|
|
125
|
+
|
|
126
|
+
return parsed, base_path
|
|
127
|
+
|
|
128
|
+
def sdk_url(self) -> str:
|
|
129
|
+
parsed, base_path = self.base_path()
|
|
130
|
+
return urlunparse(parsed._replace(path=base_path, query=None, fragment=None))
|
|
131
|
+
|
|
132
|
+
def openai_proxy_url(self) -> str:
|
|
133
|
+
parsed, base_path = self.base_path()
|
|
134
|
+
path = base_path + "openai-proxy/"
|
|
135
|
+
return urlunparse(parsed._replace(path=path, query=None, fragment=None))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class UniqueAuth(BaseSettings):
|
|
139
|
+
company_id: SecretStr = Field(
|
|
140
|
+
default=SecretStr("dummy_company_id"),
|
|
141
|
+
validation_alias=AliasChoices(
|
|
142
|
+
"unique_auth_company_id",
|
|
143
|
+
"company_id",
|
|
144
|
+
"UNIQUE_AUTH_COMPANY_ID",
|
|
145
|
+
"COMPANY_ID",
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
user_id: SecretStr = Field(
|
|
149
|
+
default=SecretStr("dummy_user_id"),
|
|
150
|
+
validation_alias=AliasChoices(
|
|
151
|
+
"unique_auth_user_id", "user_id", "UNIQUE_AUTH_USER_ID", "USER_ID"
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
model_config = SettingsConfigDict(
|
|
156
|
+
env_prefix="unique_auth_",
|
|
157
|
+
env_file_encoding="utf-8",
|
|
158
|
+
case_sensitive=False,
|
|
159
|
+
extra="ignore",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@model_validator(mode="after")
|
|
163
|
+
def _warn_about_defaults(self) -> Self:
|
|
164
|
+
return warn_about_defaults(self)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_event(cls, event: "BaseEvent") -> Self:
|
|
168
|
+
return cls(
|
|
169
|
+
company_id=SecretStr(event.company_id),
|
|
170
|
+
user_id=SecretStr(event.user_id),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class UniqueChatEventFilterOptions(BaseSettings):
|
|
175
|
+
# Empty string evals to False
|
|
176
|
+
assistant_ids: list[str] = Field(
|
|
177
|
+
default=[],
|
|
178
|
+
description="The assistant ids (space) to filter by. Default is all assistants.",
|
|
179
|
+
)
|
|
180
|
+
references_in_code: list[str] = Field(
|
|
181
|
+
default=[],
|
|
182
|
+
description="The module (reference) names in code to filter by. Default is all modules.",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
model_config = SettingsConfigDict(
|
|
186
|
+
env_prefix="unique_chat_event_filter_options_",
|
|
187
|
+
env_file_encoding="utf-8",
|
|
188
|
+
case_sensitive=False,
|
|
189
|
+
extra="ignore",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@model_validator(mode="after")
|
|
193
|
+
def _warn_about_defaults(self) -> Self:
|
|
194
|
+
return warn_about_defaults(self)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class EnvFileNotFoundError(FileNotFoundError):
|
|
198
|
+
"""Raised when no environment file can be found in any of the expected locations."""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class UniqueSettings:
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
auth: UniqueAuth,
|
|
205
|
+
app: UniqueApp,
|
|
206
|
+
api: UniqueApi,
|
|
207
|
+
*,
|
|
208
|
+
chat_event_filter_options: UniqueChatEventFilterOptions | None = None,
|
|
209
|
+
env_file: Path | None = None,
|
|
210
|
+
):
|
|
211
|
+
self._app = app
|
|
212
|
+
self._auth = auth
|
|
213
|
+
self._api = api
|
|
214
|
+
self._chat_event_filter_options = chat_event_filter_options
|
|
215
|
+
self._env_file: Path | None = (
|
|
216
|
+
env_file if (env_file and env_file.exists()) else None
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def _find_env_file(cls, filename: str = "unique.env") -> Path:
|
|
221
|
+
"""Find environment file using cross-platform fallback locations.
|
|
222
|
+
|
|
223
|
+
Search order:
|
|
224
|
+
1. UNIQUE_ENV_FILE environment variable
|
|
225
|
+
2. Current working directory
|
|
226
|
+
3. User config directory (cross-platform via platformdirs)
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
filename: Name of the environment file (default: 'unique.env')
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Path to the environment file.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
EnvFileNotFoundError: If no environment file is found in any location.
|
|
236
|
+
"""
|
|
237
|
+
locations = [
|
|
238
|
+
# 1. Explicit environment variable
|
|
239
|
+
Path(env_path) if (env_path := os.environ.get("UNIQUE_ENV_FILE")) else None,
|
|
240
|
+
# 2. Current working directory
|
|
241
|
+
Path.cwd() / filename,
|
|
242
|
+
# 3. User config directory (cross-platform)
|
|
243
|
+
Path(user_config_dir("unique", "unique-toolkit")) / filename,
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
for location in locations:
|
|
247
|
+
if location and location.exists() and location.is_file():
|
|
248
|
+
return location
|
|
249
|
+
|
|
250
|
+
# If no file found, provide helpful error message
|
|
251
|
+
searched_locations = [str(loc) for loc in locations if loc is not None]
|
|
252
|
+
raise EnvFileNotFoundError(
|
|
253
|
+
f"Environment file '{filename}' not found. Searched locations:\n"
|
|
254
|
+
+ "\n".join(f" - {loc}" for loc in searched_locations)
|
|
255
|
+
+ "\n\nTo fix this:\n"
|
|
256
|
+
+ f" 1. Create {filename} in one of the above locations, or\n"
|
|
257
|
+
+ f" 2. Set UNIQUE_ENV_FILE environment variable to point to your {filename} file"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def from_env(
|
|
262
|
+
cls,
|
|
263
|
+
env_file: Path | None = None,
|
|
264
|
+
) -> "UniqueSettings":
|
|
265
|
+
"""Initialize settings from environment variables and/or env file.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
env_file: Optional path to environment file. If provided, will load variables from this file.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
UniqueSettings instance with values loaded from environment/env file.
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
FileNotFoundError: If env_file is provided but does not exist.
|
|
275
|
+
ValidationError: If required environment variables are missing.
|
|
276
|
+
"""
|
|
277
|
+
if env_file and not env_file.exists():
|
|
278
|
+
raise FileNotFoundError(f"Environment file not found: {env_file}")
|
|
279
|
+
|
|
280
|
+
# Initialize settings with environment file if provided
|
|
281
|
+
env_file_str = str(env_file) if env_file else None
|
|
282
|
+
auth = UniqueAuth(_env_file=env_file_str) # type: ignore[call-arg]
|
|
283
|
+
app = UniqueApp(_env_file=env_file_str) # type: ignore[call-arg]
|
|
284
|
+
api = UniqueApi(_env_file=env_file_str) # type: ignore[call-arg]
|
|
285
|
+
event_filter_options = UniqueChatEventFilterOptions(_env_file=env_file_str) # type: ignore[call-arg]
|
|
286
|
+
return cls(
|
|
287
|
+
auth=auth,
|
|
288
|
+
app=app,
|
|
289
|
+
api=api,
|
|
290
|
+
chat_event_filter_options=event_filter_options,
|
|
291
|
+
env_file=env_file,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def from_env_auto(cls, filename: str = "unique.env") -> "UniqueSettings":
|
|
296
|
+
"""Initialize settings by automatically finding environment file.
|
|
297
|
+
|
|
298
|
+
This method will automatically search for an environment file in standard locations
|
|
299
|
+
and fall back to environment variables only if no file is found.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
filename: Name of the environment file to search for (default: '.env')
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
UniqueSettings instance with values loaded from found env file or environment variables.
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
env_file = cls._find_env_file(filename)
|
|
309
|
+
logger.info(f"Environment file found at {env_file}")
|
|
310
|
+
return cls.from_env(env_file=env_file)
|
|
311
|
+
except EnvFileNotFoundError:
|
|
312
|
+
logger.warning(
|
|
313
|
+
f"Environment file '{filename}' not found. Falling back to environment variables only."
|
|
314
|
+
)
|
|
315
|
+
# Fall back to environment variables only
|
|
316
|
+
return cls.from_env()
|
|
317
|
+
|
|
318
|
+
def init_sdk(self) -> None:
|
|
319
|
+
"""Initialize the unique_sdk global configuration with these settings.
|
|
320
|
+
|
|
321
|
+
This method configures the global unique_sdk module with the API key,
|
|
322
|
+
app ID, and base URL from these settings.
|
|
323
|
+
"""
|
|
324
|
+
unique_sdk.api_key = self._app.key.get_secret_value()
|
|
325
|
+
unique_sdk.app_id = self._app.id.get_secret_value()
|
|
326
|
+
unique_sdk.api_base = self._api.sdk_url()
|
|
327
|
+
|
|
328
|
+
@classmethod
|
|
329
|
+
def from_env_auto_with_sdk_init(
|
|
330
|
+
cls, filename: str = "unique.env"
|
|
331
|
+
) -> "UniqueSettings":
|
|
332
|
+
"""Initialize settings and SDK in one convenient call.
|
|
333
|
+
|
|
334
|
+
This method combines from_env_auto() and init_sdk() for the most common use case.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
filename: Name of the environment file to search for (default: '.env')
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
UniqueSettings instance with SDK already initialized.
|
|
341
|
+
"""
|
|
342
|
+
settings = cls.from_env_auto(filename)
|
|
343
|
+
settings.init_sdk()
|
|
344
|
+
return settings
|
|
345
|
+
|
|
346
|
+
def update_from_event(self, event: "BaseEvent") -> None:
|
|
347
|
+
self._auth = UniqueAuth.from_event(event)
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def api(self) -> UniqueApi:
|
|
351
|
+
return self._api
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def app(self) -> UniqueApp:
|
|
355
|
+
return self._app
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def auth(self) -> UniqueAuth:
|
|
359
|
+
return self._auth
|
|
360
|
+
|
|
361
|
+
@auth.setter
|
|
362
|
+
def auth(self, value: UniqueAuth) -> None:
|
|
363
|
+
self._auth = value
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def chat_event_filter_options(self) -> UniqueChatEventFilterOptions | None:
|
|
367
|
+
return self._chat_event_filter_options
|
unique_toolkit/chat/__init__.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
1
3
|
from .constants import DOMAIN_NAME as DOMAIN_NAME
|
|
2
4
|
from .schemas import ChatMessage as ChatMessage
|
|
3
5
|
from .schemas import ChatMessageAssessment as ChatMessageAssessment
|
|
@@ -5,7 +7,12 @@ from .schemas import ChatMessageAssessmentLabel as ChatMessageAssessmentLabel
|
|
|
5
7
|
from .schemas import ChatMessageAssessmentStatus as ChatMessageAssessmentStatus
|
|
6
8
|
from .schemas import ChatMessageAssessmentType as ChatMessageAssessmentType
|
|
7
9
|
from .schemas import ChatMessageRole as ChatMessageRole
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
# Import ChatService with deprecation warning suppressed for internal use
|
|
12
|
+
with warnings.catch_warnings():
|
|
13
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
14
|
+
from .service import ChatService as ChatService
|
|
15
|
+
|
|
9
16
|
from .utils import (
|
|
10
17
|
convert_chat_history_to_injectable_string as convert_chat_history_to_injectable_string,
|
|
11
18
|
)
|