unique_toolkit 0.7.9__py3-none-any.whl → 1.33.3__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.
- unique_toolkit/__init__.py +36 -3
- unique_toolkit/_common/api_calling/human_verification_manager.py +357 -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 +225 -0
- unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
- unique_toolkit/_common/endpoint_builder.py +368 -0
- unique_toolkit/_common/endpoint_requestor.py +480 -0
- unique_toolkit/_common/exception.py +24 -0
- unique_toolkit/_common/experimental/endpoint_builder.py +368 -0
- unique_toolkit/_common/experimental/endpoint_requestor.py +488 -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 +174 -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/image/encode.py +25 -0
- unique_toolkit/_common/utils/jinja/helpers.py +10 -0
- unique_toolkit/_common/utils/jinja/render.py +18 -0
- unique_toolkit/_common/utils/jinja/schema.py +65 -0
- unique_toolkit/_common/utils/jinja/utils.py +80 -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 +112 -0
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +20 -16
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +32 -21
- 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 +298 -0
- unique_toolkit/agentic/history_manager/history_manager.py +241 -0
- unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
- unique_toolkit/agentic/history_manager/utils.py +96 -0
- unique_toolkit/agentic/message_log_manager/__init__.py +5 -0
- unique_toolkit/agentic/message_log_manager/service.py +93 -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 +71 -0
- unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +297 -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 +240 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +84 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +78 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +264 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +2103 -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 +158 -0
- unique_toolkit/agentic/tools/a2a/tool/service.py +393 -0
- unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
- unique_toolkit/agentic/tools/config.py +128 -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 +46 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +88 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +250 -0
- unique_toolkit/agentic/tools/openai_builtin/manager.py +79 -0
- unique_toolkit/agentic/tools/schemas.py +145 -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 +187 -0
- unique_toolkit/agentic/tools/tool_manager.py +492 -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 +9 -0
- unique_toolkit/app/dev_util.py +180 -0
- unique_toolkit/app/fast_api_factory.py +131 -0
- unique_toolkit/app/init_sdk.py +32 -1
- unique_toolkit/app/schemas.py +206 -31
- unique_toolkit/app/unique_settings.py +367 -0
- unique_toolkit/app/webhook.py +77 -0
- unique_toolkit/chat/__init__.py +8 -1
- unique_toolkit/chat/deprecated/service.py +232 -0
- unique_toolkit/chat/functions.py +648 -78
- unique_toolkit/chat/rendering.py +34 -0
- unique_toolkit/chat/responses_api.py +461 -0
- unique_toolkit/chat/schemas.py +134 -2
- unique_toolkit/chat/service.py +115 -767
- unique_toolkit/content/functions.py +353 -8
- unique_toolkit/content/schemas.py +128 -15
- unique_toolkit/content/service.py +321 -45
- unique_toolkit/content/smart_rules.py +301 -0
- unique_toolkit/content/utils.py +10 -3
- unique_toolkit/data_extraction/README.md +96 -0
- unique_toolkit/data_extraction/__init__.py +11 -0
- unique_toolkit/data_extraction/augmented/__init__.py +5 -0
- unique_toolkit/data_extraction/augmented/service.py +93 -0
- unique_toolkit/data_extraction/base.py +25 -0
- unique_toolkit/data_extraction/basic/__init__.py +11 -0
- unique_toolkit/data_extraction/basic/config.py +18 -0
- unique_toolkit/data_extraction/basic/prompt.py +13 -0
- unique_toolkit/data_extraction/basic/service.py +55 -0
- unique_toolkit/embedding/service.py +103 -12
- unique_toolkit/framework_utilities/__init__.py +1 -0
- unique_toolkit/framework_utilities/langchain/__init__.py +10 -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 +84 -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/_responses_api_utils.py +93 -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 +345 -43
- unique_toolkit/language_model/infos.py +1288 -46
- unique_toolkit/language_model/reference.py +242 -0
- unique_toolkit/language_model/schemas.py +481 -49
- unique_toolkit/language_model/service.py +229 -28
- unique_toolkit/protocols/support.py +145 -0
- unique_toolkit/services/__init__.py +7 -0
- unique_toolkit/services/chat_service.py +1631 -0
- unique_toolkit/services/knowledge_base.py +1094 -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-1.33.3.dist-info/METADATA +1145 -0
- unique_toolkit-1.33.3.dist-info/RECORD +205 -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.9.dist-info/METADATA +0 -413
- unique_toolkit-0.7.9.dist-info/RECORD +0 -64
- /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
- {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
- {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webhook signature verification for Unique platform.
|
|
3
|
+
|
|
4
|
+
Extracted from unique_sdk to provide standalone verification without event construction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_webhook_signature_valid(
|
|
16
|
+
headers: dict[str, str],
|
|
17
|
+
payload: bytes,
|
|
18
|
+
endpoint_secret: str,
|
|
19
|
+
tolerance: int = 300,
|
|
20
|
+
) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Verify webhook signature from Unique platform.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
headers: Request headers with X-Unique-Signature and X-Unique-Created-At
|
|
26
|
+
payload: Raw request body bytes
|
|
27
|
+
endpoint_secret: App endpoint secret from Unique platform
|
|
28
|
+
tolerance: Max seconds between timestamp and now (default: 300)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if signature is valid, False otherwise
|
|
32
|
+
"""
|
|
33
|
+
# Extract headers
|
|
34
|
+
signature = headers.get("X-Unique-Signature") or headers.get("x-unique-signature")
|
|
35
|
+
timestamp_str = headers.get("X-Unique-Created-At") or headers.get(
|
|
36
|
+
"x-unique-created-at"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if not signature:
|
|
40
|
+
_LOGGER.error("Missing X-Unique-Signature header")
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
if not timestamp_str:
|
|
44
|
+
_LOGGER.error("Missing X-Unique-Created-At header")
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
# Convert timestamp to int
|
|
48
|
+
try:
|
|
49
|
+
timestamp = int(timestamp_str)
|
|
50
|
+
except ValueError:
|
|
51
|
+
_LOGGER.error(f"Invalid timestamp: {timestamp_str}")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Decode payload if bytes
|
|
55
|
+
message = payload.decode("utf-8") if isinstance(payload, bytes) else payload
|
|
56
|
+
|
|
57
|
+
# Compute expected signature: HMAC-SHA256(message, secret)
|
|
58
|
+
expected_signature = hmac.new(
|
|
59
|
+
endpoint_secret.encode("utf-8"),
|
|
60
|
+
msg=message.encode("utf-8"),
|
|
61
|
+
digestmod=hashlib.sha256,
|
|
62
|
+
).hexdigest()
|
|
63
|
+
|
|
64
|
+
# Compare signatures (constant-time to prevent timing attacks)
|
|
65
|
+
if not hmac.compare_digest(expected_signature, signature):
|
|
66
|
+
_LOGGER.error("Signature mismatch. Ensure you're using the raw request body.")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Check timestamp tolerance (prevent replay attacks)
|
|
70
|
+
if tolerance and timestamp < time.time() - tolerance:
|
|
71
|
+
_LOGGER.error(
|
|
72
|
+
f"Timestamp outside tolerance ({tolerance}s). Possible replay attack."
|
|
73
|
+
)
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
_LOGGER.debug("✅ Webhook signature verified successfully")
|
|
77
|
+
return True
|
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
|
)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from typing_extensions import deprecated
|
|
2
|
+
|
|
3
|
+
from unique_toolkit.app.schemas import ChatEvent, Event
|
|
4
|
+
from unique_toolkit.chat.functions import (
|
|
5
|
+
modify_message,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChatServiceDeprecated:
|
|
10
|
+
def __init__(self, event: ChatEvent | Event):
|
|
11
|
+
self._event = event
|
|
12
|
+
self._company_id: str = event.company_id
|
|
13
|
+
self._user_id: str = event.user_id
|
|
14
|
+
self._assistant_message_id: str = event.payload.assistant_message.id
|
|
15
|
+
self._user_message_id: str = event.payload.user_message.id
|
|
16
|
+
self._chat_id: str = event.payload.chat_id
|
|
17
|
+
self._assistant_id: str = event.payload.assistant_id
|
|
18
|
+
self._user_message_text: str = event.payload.user_message.text
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@deprecated(
|
|
22
|
+
"The event property is deprecated and will be removed in a future version.",
|
|
23
|
+
)
|
|
24
|
+
def event(self) -> Event | ChatEvent:
|
|
25
|
+
"""Get the event object (deprecated).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Event | BaseEvent | None: The event object.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
return self._event
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
@deprecated(
|
|
35
|
+
"The company_id property is deprecated and will be removed in a future version.",
|
|
36
|
+
)
|
|
37
|
+
def company_id(self) -> str:
|
|
38
|
+
"""Get the company identifier (deprecated).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str | None: The company identifier.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
return self._company_id
|
|
45
|
+
|
|
46
|
+
@company_id.setter
|
|
47
|
+
@deprecated(
|
|
48
|
+
"The company_id setter is deprecated and will be removed in a future version.",
|
|
49
|
+
)
|
|
50
|
+
def company_id(self, value: str) -> None:
|
|
51
|
+
"""Set the company identifier (deprecated).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
value (str | None): The company identifier.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
self._company_id = value
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
@deprecated(
|
|
61
|
+
"The user_id property is deprecated and will be removed in a future version.",
|
|
62
|
+
)
|
|
63
|
+
def user_id(self) -> str:
|
|
64
|
+
"""Get the user identifier (deprecated).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
str | None: The user identifier.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
return self._user_id
|
|
71
|
+
|
|
72
|
+
@user_id.setter
|
|
73
|
+
@deprecated(
|
|
74
|
+
"The user_id setter is deprecated and will be removed in a future version.",
|
|
75
|
+
)
|
|
76
|
+
def user_id(self, value: str) -> None:
|
|
77
|
+
"""Set the user identifier (deprecated).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
value (str | None): The user identifier.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
self._user_id = value
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
@deprecated(
|
|
87
|
+
"The assistant_message_id property is deprecated and will be removed in a future version.",
|
|
88
|
+
)
|
|
89
|
+
def assistant_message_id(self) -> str:
|
|
90
|
+
"""Get the assistant message identifier (deprecated).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
str | None: The assistant message identifier.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
return self._assistant_message_id
|
|
97
|
+
|
|
98
|
+
@assistant_message_id.setter
|
|
99
|
+
@deprecated(
|
|
100
|
+
"The assistant_message_id setter is deprecated and will be removed in a future version.",
|
|
101
|
+
)
|
|
102
|
+
def assistant_message_id(self, value: str) -> None:
|
|
103
|
+
"""Set the assistant message identifier (deprecated).
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
value (str | None): The assistant message identifier.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
self._assistant_message_id = value
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
@deprecated(
|
|
113
|
+
"The user_message_id property is deprecated and will be removed in a future version.",
|
|
114
|
+
)
|
|
115
|
+
def user_message_id(self) -> str:
|
|
116
|
+
"""Get the user message identifier (deprecated).
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
str | None: The user message identifier.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
return self._user_message_id
|
|
123
|
+
|
|
124
|
+
@user_message_id.setter
|
|
125
|
+
@deprecated(
|
|
126
|
+
"The user_message_id setter is deprecated and will be removed in a future version.",
|
|
127
|
+
)
|
|
128
|
+
def user_message_id(self, value: str) -> None:
|
|
129
|
+
"""Set the user message identifier (deprecated).
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
value (str | None): The user message identifier.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
self._user_message_id = value
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
@deprecated(
|
|
139
|
+
"The chat_id property is deprecated and will be removed in a future version.",
|
|
140
|
+
)
|
|
141
|
+
def chat_id(self) -> str:
|
|
142
|
+
"""Get the chat identifier (deprecated).
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
str | None: The chat identifier.
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
return self._chat_id
|
|
149
|
+
|
|
150
|
+
@chat_id.setter
|
|
151
|
+
@deprecated(
|
|
152
|
+
"The chat_id setter is deprecated and will be removed in a future version.",
|
|
153
|
+
)
|
|
154
|
+
def chat_id(self, value: str) -> None:
|
|
155
|
+
"""Set the chat identifier (deprecated).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
value (str | None): The chat identifier.
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
self._chat_id = value
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
@deprecated(
|
|
165
|
+
"The assistant_id property is deprecated and will be removed in a future version.",
|
|
166
|
+
)
|
|
167
|
+
def assistant_id(self) -> str:
|
|
168
|
+
"""Get the assistant identifier (deprecated).
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
str | None: The assistant identifier.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
return self._assistant_id
|
|
175
|
+
|
|
176
|
+
@assistant_id.setter
|
|
177
|
+
@deprecated(
|
|
178
|
+
"The assistant_id setter is deprecated and will be removed in a future version.",
|
|
179
|
+
)
|
|
180
|
+
def assistant_id(self, value: str) -> None:
|
|
181
|
+
"""Set the assistant identifier (deprecated).
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
value (str | None): The assistant identifier.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
self._assistant_id = value
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
@deprecated(
|
|
191
|
+
"The user_message_text property is deprecated and will be removed in a future version.",
|
|
192
|
+
)
|
|
193
|
+
def user_message_text(self) -> str:
|
|
194
|
+
"""Get the user message text (deprecated).
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
str | None: The user message text.
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
return self._user_message_text
|
|
201
|
+
|
|
202
|
+
@user_message_text.setter
|
|
203
|
+
@deprecated(
|
|
204
|
+
"The user_message_text setter is deprecated and will be removed in a future version.",
|
|
205
|
+
)
|
|
206
|
+
def user_message_text(self, value: str) -> None:
|
|
207
|
+
"""Set the user message text (deprecated).
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
value (str | None): The user message text.
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
self._user_message_text = value
|
|
214
|
+
|
|
215
|
+
@deprecated("Use `replace_debug_info`")
|
|
216
|
+
def update_debug_info(self, debug_info: dict):
|
|
217
|
+
"""Updates the debug information for the chat session.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
debug_info (dict): The new debug information.
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
return modify_message(
|
|
224
|
+
user_id=self._user_id,
|
|
225
|
+
company_id=self._company_id,
|
|
226
|
+
assistant_message_id=self._assistant_message_id,
|
|
227
|
+
chat_id=self._chat_id,
|
|
228
|
+
user_message_id=self._user_message_id,
|
|
229
|
+
user_message_text=self._user_message_text,
|
|
230
|
+
assistant=False,
|
|
231
|
+
debug_info=debug_info,
|
|
232
|
+
)
|