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.
Files changed (190) hide show
  1. unique_toolkit/__init__.py +36 -3
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +357 -0
  3. unique_toolkit/_common/base_model_type_attribute.py +303 -0
  4. unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
  5. unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
  6. unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
  7. unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
  8. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
  9. unique_toolkit/_common/default_language_model.py +12 -0
  10. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  11. unique_toolkit/_common/docx_generator/config.py +12 -0
  12. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  13. unique_toolkit/_common/docx_generator/service.py +225 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +368 -0
  16. unique_toolkit/_common/endpoint_requestor.py +480 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/experimental/endpoint_builder.py +368 -0
  19. unique_toolkit/_common/experimental/endpoint_requestor.py +488 -0
  20. unique_toolkit/_common/feature_flags/schema.py +9 -0
  21. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  22. unique_toolkit/_common/pydantic_helpers.py +174 -0
  23. unique_toolkit/_common/referencing.py +53 -0
  24. unique_toolkit/_common/string_utilities.py +140 -0
  25. unique_toolkit/_common/tests/test_referencing.py +521 -0
  26. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  27. unique_toolkit/_common/token/image_token_counting.py +67 -0
  28. unique_toolkit/_common/token/token_counting.py +204 -0
  29. unique_toolkit/_common/utils/__init__.py +1 -0
  30. unique_toolkit/_common/utils/files.py +43 -0
  31. unique_toolkit/_common/utils/image/encode.py +25 -0
  32. unique_toolkit/_common/utils/jinja/helpers.py +10 -0
  33. unique_toolkit/_common/utils/jinja/render.py +18 -0
  34. unique_toolkit/_common/utils/jinja/schema.py +65 -0
  35. unique_toolkit/_common/utils/jinja/utils.py +80 -0
  36. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  37. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  38. unique_toolkit/_common/utils/write_configuration.py +51 -0
  39. unique_toolkit/_common/validators.py +101 -4
  40. unique_toolkit/agentic/__init__.py +1 -0
  41. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  42. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  43. unique_toolkit/agentic/evaluation/config.py +36 -0
  44. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  45. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  46. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  47. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  48. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  49. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +112 -0
  50. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  51. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +20 -16
  52. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +32 -21
  53. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  54. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  55. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  56. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  57. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +298 -0
  58. unique_toolkit/agentic/history_manager/history_manager.py +241 -0
  59. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  60. unique_toolkit/agentic/history_manager/utils.py +96 -0
  61. unique_toolkit/agentic/message_log_manager/__init__.py +5 -0
  62. unique_toolkit/agentic/message_log_manager/service.py +93 -0
  63. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  64. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  65. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  66. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +71 -0
  67. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +297 -0
  68. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  69. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  70. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  71. unique_toolkit/agentic/tools/__init__.py +1 -0
  72. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  73. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  74. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  75. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  76. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  77. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  78. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  79. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  80. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  81. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +240 -0
  82. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +84 -0
  83. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +78 -0
  84. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +264 -0
  85. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  86. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
  87. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +2103 -0
  88. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  89. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  90. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  91. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  92. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  93. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  94. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  95. unique_toolkit/agentic/tools/a2a/tool/config.py +158 -0
  96. unique_toolkit/agentic/tools/a2a/tool/service.py +393 -0
  97. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  98. unique_toolkit/agentic/tools/config.py +128 -0
  99. unique_toolkit/agentic/tools/factory.py +44 -0
  100. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  101. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  102. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  103. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  104. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  105. unique_toolkit/agentic/tools/openai_builtin/base.py +46 -0
  106. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  107. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +88 -0
  108. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +250 -0
  109. unique_toolkit/agentic/tools/openai_builtin/manager.py +79 -0
  110. unique_toolkit/agentic/tools/schemas.py +145 -0
  111. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  112. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  113. unique_toolkit/agentic/tools/tool.py +187 -0
  114. unique_toolkit/agentic/tools/tool_manager.py +492 -0
  115. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  116. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  117. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  118. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  119. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  120. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  121. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  122. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  123. unique_toolkit/app/__init__.py +9 -0
  124. unique_toolkit/app/dev_util.py +180 -0
  125. unique_toolkit/app/fast_api_factory.py +131 -0
  126. unique_toolkit/app/init_sdk.py +32 -1
  127. unique_toolkit/app/schemas.py +206 -31
  128. unique_toolkit/app/unique_settings.py +367 -0
  129. unique_toolkit/app/webhook.py +77 -0
  130. unique_toolkit/chat/__init__.py +8 -1
  131. unique_toolkit/chat/deprecated/service.py +232 -0
  132. unique_toolkit/chat/functions.py +648 -78
  133. unique_toolkit/chat/rendering.py +34 -0
  134. unique_toolkit/chat/responses_api.py +461 -0
  135. unique_toolkit/chat/schemas.py +134 -2
  136. unique_toolkit/chat/service.py +115 -767
  137. unique_toolkit/content/functions.py +353 -8
  138. unique_toolkit/content/schemas.py +128 -15
  139. unique_toolkit/content/service.py +321 -45
  140. unique_toolkit/content/smart_rules.py +301 -0
  141. unique_toolkit/content/utils.py +10 -3
  142. unique_toolkit/data_extraction/README.md +96 -0
  143. unique_toolkit/data_extraction/__init__.py +11 -0
  144. unique_toolkit/data_extraction/augmented/__init__.py +5 -0
  145. unique_toolkit/data_extraction/augmented/service.py +93 -0
  146. unique_toolkit/data_extraction/base.py +25 -0
  147. unique_toolkit/data_extraction/basic/__init__.py +11 -0
  148. unique_toolkit/data_extraction/basic/config.py +18 -0
  149. unique_toolkit/data_extraction/basic/prompt.py +13 -0
  150. unique_toolkit/data_extraction/basic/service.py +55 -0
  151. unique_toolkit/embedding/service.py +103 -12
  152. unique_toolkit/framework_utilities/__init__.py +1 -0
  153. unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
  154. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  155. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  156. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  157. unique_toolkit/framework_utilities/openai/client.py +84 -0
  158. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  159. unique_toolkit/framework_utilities/utils.py +23 -0
  160. unique_toolkit/language_model/__init__.py +3 -0
  161. unique_toolkit/language_model/_responses_api_utils.py +93 -0
  162. unique_toolkit/language_model/builder.py +27 -11
  163. unique_toolkit/language_model/default_language_model.py +3 -0
  164. unique_toolkit/language_model/functions.py +345 -43
  165. unique_toolkit/language_model/infos.py +1288 -46
  166. unique_toolkit/language_model/reference.py +242 -0
  167. unique_toolkit/language_model/schemas.py +481 -49
  168. unique_toolkit/language_model/service.py +229 -28
  169. unique_toolkit/protocols/support.py +145 -0
  170. unique_toolkit/services/__init__.py +7 -0
  171. unique_toolkit/services/chat_service.py +1631 -0
  172. unique_toolkit/services/knowledge_base.py +1094 -0
  173. unique_toolkit/short_term_memory/service.py +178 -41
  174. unique_toolkit/smart_rules/__init__.py +0 -0
  175. unique_toolkit/smart_rules/compile.py +56 -0
  176. unique_toolkit/test_utilities/events.py +197 -0
  177. unique_toolkit-1.33.3.dist-info/METADATA +1145 -0
  178. unique_toolkit-1.33.3.dist-info/RECORD +205 -0
  179. unique_toolkit/evaluators/__init__.py +0 -1
  180. unique_toolkit/evaluators/config.py +0 -35
  181. unique_toolkit/evaluators/constants.py +0 -1
  182. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  183. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  184. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  185. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  186. unique_toolkit-0.7.9.dist-info/METADATA +0 -413
  187. unique_toolkit-0.7.9.dist-info/RECORD +0 -64
  188. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  189. {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
  190. {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
@@ -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
- from .service import ChatService as ChatService
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
+ )