unique_toolkit 1.28.8__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 (44) hide show
  1. unique_toolkit/__init__.py +12 -6
  2. unique_toolkit/_common/docx_generator/service.py +8 -32
  3. unique_toolkit/_common/utils/jinja/helpers.py +10 -0
  4. unique_toolkit/_common/utils/jinja/render.py +18 -0
  5. unique_toolkit/_common/utils/jinja/schema.py +65 -0
  6. unique_toolkit/_common/utils/jinja/utils.py +80 -0
  7. unique_toolkit/agentic/message_log_manager/service.py +9 -0
  8. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +58 -3
  9. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +11 -0
  10. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +33 -0
  11. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +99 -15
  12. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
  13. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +768 -0
  14. unique_toolkit/agentic/tools/a2a/tool/config.py +77 -1
  15. unique_toolkit/agentic/tools/a2a/tool/service.py +67 -3
  16. unique_toolkit/agentic/tools/config.py +5 -45
  17. unique_toolkit/agentic/tools/openai_builtin/base.py +4 -0
  18. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +4 -0
  19. unique_toolkit/agentic/tools/tool_manager.py +16 -19
  20. unique_toolkit/app/__init__.py +3 -0
  21. unique_toolkit/app/fast_api_factory.py +131 -0
  22. unique_toolkit/app/webhook.py +77 -0
  23. unique_toolkit/chat/functions.py +1 -1
  24. unique_toolkit/content/functions.py +4 -4
  25. unique_toolkit/content/service.py +1 -1
  26. unique_toolkit/data_extraction/README.md +96 -0
  27. unique_toolkit/data_extraction/__init__.py +11 -0
  28. unique_toolkit/data_extraction/augmented/__init__.py +5 -0
  29. unique_toolkit/data_extraction/augmented/service.py +93 -0
  30. unique_toolkit/data_extraction/base.py +25 -0
  31. unique_toolkit/data_extraction/basic/__init__.py +11 -0
  32. unique_toolkit/data_extraction/basic/config.py +18 -0
  33. unique_toolkit/data_extraction/basic/prompt.py +13 -0
  34. unique_toolkit/data_extraction/basic/service.py +55 -0
  35. unique_toolkit/embedding/service.py +1 -1
  36. unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
  37. unique_toolkit/framework_utilities/openai/client.py +2 -1
  38. unique_toolkit/language_model/infos.py +22 -1
  39. unique_toolkit/services/knowledge_base.py +4 -6
  40. {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/METADATA +51 -2
  41. {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/RECORD +43 -27
  42. unique_toolkit/agentic/tools/test/test_tool_manager.py +0 -1686
  43. {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
  44. {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/WHEEL +0 -0
@@ -1,10 +1,74 @@
1
- from typing import Literal
1
+ import re
2
+ from enum import StrEnum
3
+ from typing import Annotated, Generic, Literal, TypeVar
2
4
 
3
5
  from pydantic import Field
6
+ from pydantic.main import BaseModel
4
7
 
5
8
  from unique_toolkit._common.pydantic_helpers import get_configuration_dict
6
9
  from unique_toolkit.agentic.tools.schemas import BaseToolConfig
7
10
 
11
+
12
+ class SubAgentSystemReminderType(StrEnum):
13
+ FIXED = "fixed"
14
+ REGEXP = "regexp"
15
+ REFERENCE = "reference"
16
+
17
+
18
+ T = TypeVar("T", bound=SubAgentSystemReminderType)
19
+
20
+
21
+ class SystemReminderConfig(BaseModel, Generic[T]):
22
+ model_config = get_configuration_dict()
23
+
24
+ type: T
25
+
26
+
27
+ _SYSTEM_REMINDER_FIELD_DESCRIPTION = """
28
+ The reminder to add to the tool response. The reminder can be a Jinja template and can contain the following placeholders:
29
+ - {{ display_name }}: The display name of the sub agent.
30
+ - {{ tool_name }}: The tool name.
31
+ """.strip()
32
+
33
+
34
+ class ReferenceSystemReminderConfig(SystemReminderConfig):
35
+ type: Literal[SubAgentSystemReminderType.REFERENCE] = (
36
+ SubAgentSystemReminderType.REFERENCE
37
+ )
38
+ reminder: str = Field(
39
+ default="Rememeber to properly reference EACH fact from sub agent {{ display_name }}'s response with the correct format INLINE.",
40
+ description=_SYSTEM_REMINDER_FIELD_DESCRIPTION,
41
+ )
42
+
43
+
44
+ class FixedSystemReminderConfig(SystemReminderConfig):
45
+ type: Literal[SubAgentSystemReminderType.FIXED] = SubAgentSystemReminderType.FIXED
46
+ reminder: str = Field(
47
+ description=_SYSTEM_REMINDER_FIELD_DESCRIPTION,
48
+ )
49
+
50
+
51
+ _REGEXP_DETECTED_REMINDER_FIELD_DESCRIPTION = """
52
+ The reminder to add to the tool response. The reminder can be a Jinja template and can contain the following placeholders:
53
+ - {{ display_name }}: The display name of the sub agent.
54
+ - {{ tool_name }}: The tool name.
55
+ - {{ text_matches }}: Will be replaced with the portions of the text that triggered the reminder.
56
+ """.strip()
57
+
58
+
59
+ class RegExpDetectedSystemReminderConfig(SystemReminderConfig):
60
+ """A system reminder that is only added if the sub agent response matches a regular expression."""
61
+
62
+ type: Literal[SubAgentSystemReminderType.REGEXP] = SubAgentSystemReminderType.REGEXP
63
+
64
+ regexp: re.Pattern[str] = Field(
65
+ description="The regular expression to use to detect whether the system reminder should be added.",
66
+ )
67
+ reminder: str = Field(
68
+ description=_REGEXP_DETECTED_REMINDER_FIELD_DESCRIPTION,
69
+ )
70
+
71
+
8
72
  DEFAULT_PARAM_DESCRIPTION_SUB_AGENT_USER_MESSAGE = """
9
73
  This is the message that will be sent to the sub-agent.
10
74
  """.strip()
@@ -80,3 +144,15 @@ class SubAgentToolConfig(BaseToolConfig):
80
144
  default=False,
81
145
  description="If set, the sub-agent response will be interpreted as a list of content chunks.",
82
146
  )
147
+
148
+ system_reminders_config: list[
149
+ Annotated[
150
+ FixedSystemReminderConfig
151
+ | RegExpDetectedSystemReminderConfig
152
+ | ReferenceSystemReminderConfig,
153
+ Field(discriminator="type"),
154
+ ]
155
+ ] = Field(
156
+ default=[],
157
+ description="Configuration for the system reminders to add to the tool response.",
158
+ )
@@ -4,7 +4,7 @@ import json
4
4
  import logging
5
5
  import re
6
6
  from datetime import datetime
7
- from typing import override
7
+ from typing import cast, override
8
8
 
9
9
  import unique_sdk
10
10
  from pydantic import Field, TypeAdapter, create_model
@@ -15,6 +15,7 @@ from unique_toolkit._common.referencing import (
15
15
  remove_all_refs,
16
16
  replace_ref_number,
17
17
  )
18
+ from unique_toolkit._common.utils.jinja.render import render_template
18
19
  from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
19
20
  from unique_toolkit.agentic.tools.a2a.response_watcher import SubAgentResponseWatcher
20
21
  from unique_toolkit.agentic.tools.a2a.tool._memory import (
@@ -25,6 +26,8 @@ from unique_toolkit.agentic.tools.a2a.tool._schema import (
25
26
  SubAgentToolInput,
26
27
  )
27
28
  from unique_toolkit.agentic.tools.a2a.tool.config import (
29
+ RegExpDetectedSystemReminderConfig,
30
+ SubAgentSystemReminderType,
28
31
  SubAgentToolConfig,
29
32
  )
30
33
  from unique_toolkit.agentic.tools.factory import ToolFactory
@@ -215,12 +218,63 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
215
218
  )
216
219
 
217
220
  return ToolCallResponse(
218
- id=tool_call.id, # type: ignore
221
+ id=tool_call.id,
219
222
  name=tool_call.name,
220
- content=content,
223
+ content=_format_response(
224
+ tool_name=self.name,
225
+ text=content,
226
+ system_reminders=self._get_system_reminders(response),
227
+ ),
221
228
  content_chunks=content_chunks,
222
229
  )
223
230
 
231
+ def _get_system_reminders(self, message: unique_sdk.Space.Message) -> list[str]:
232
+ reminders = []
233
+ for reminder_config in self.config.system_reminders_config:
234
+ if reminder_config.type == SubAgentSystemReminderType.FIXED:
235
+ reminders.append(
236
+ render_template(
237
+ reminder_config.reminder,
238
+ display_name=self.display_name(),
239
+ tool_name=self.name,
240
+ )
241
+ )
242
+ elif (
243
+ reminder_config.type == SubAgentSystemReminderType.REFERENCE
244
+ and self.config.use_sub_agent_references
245
+ and message["references"] is not None
246
+ and len(message["references"]) > 0
247
+ ):
248
+ reminders.append(
249
+ render_template(
250
+ reminder_config.reminder,
251
+ display_name=self.display_name(),
252
+ tool_name=self.name,
253
+ )
254
+ )
255
+ elif (
256
+ reminder_config.type == SubAgentSystemReminderType.REGEXP
257
+ and message["text"] is not None
258
+ ):
259
+ reminder_config = cast(
260
+ RegExpDetectedSystemReminderConfig, reminder_config
261
+ )
262
+ text_matches = [
263
+ match.group(0)
264
+ for match in reminder_config.regexp.finditer(message["text"])
265
+ ]
266
+ if len(text_matches) > 0:
267
+ reminders.append(
268
+ render_template(
269
+ reminder_config.reminder,
270
+ display_name=self.display_name(),
271
+ tool_name=self.name,
272
+ text_matches=text_matches,
273
+ )
274
+ )
275
+
276
+ return reminders
277
+
224
278
  async def _get_chat_id(self) -> str | None:
225
279
  if not self.config.reuse_chat:
226
280
  return None
@@ -326,4 +380,14 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
326
380
  ) from e
327
381
 
328
382
 
383
+ def _format_response(tool_name: str, text: str, system_reminders: list[str]) -> str:
384
+ if len(system_reminders) == 0:
385
+ return text
386
+
387
+ reponse_key = f"{tool_name} response"
388
+ response = {reponse_key: text, "SYSTEM_REMINDERS": system_reminders}
389
+
390
+ return json.dumps(response, indent=2)
391
+
392
+
329
393
  ToolFactory.register_tool(SubAgentTool, SubAgentToolConfig)
@@ -1,12 +1,12 @@
1
- import json
2
1
  from enum import StrEnum
3
- from typing import Annotated, Any, Dict
2
+ from typing import Annotated, Any
4
3
 
5
4
  from pydantic import (
6
5
  BaseModel,
7
6
  BeforeValidator,
8
7
  Field,
9
8
  ValidationInfo,
9
+ field_serializer,
10
10
  model_validator,
11
11
  )
12
12
 
@@ -123,46 +123,6 @@ class ToolBuildConfig(BaseModel):
123
123
  value["configuration"] = config
124
124
  return value
125
125
 
126
- def model_dump(self) -> Dict[str, Any]:
127
- """
128
- Returns a dict representation of the tool config that preserves
129
- subclass fields from `configuration` by delegating to its own
130
- model_dump. This prevents `{}` when `configuration` is typed
131
- as `BaseToolConfig` but holds a subclass instance.
132
- """
133
- data: Dict[str, Any] = {
134
- "name": self.name,
135
- "configuration": self.configuration.model_dump()
136
- if self.configuration
137
- else None,
138
- "display_name": self.display_name,
139
- "icon": self.icon,
140
- "selection_policy": self.selection_policy,
141
- "is_exclusive": self.is_exclusive,
142
- "is_sub_agent": self.is_sub_agent,
143
- "is_enabled": self.is_enabled,
144
- }
145
- return data
146
-
147
- def model_dump_json(self) -> str:
148
- """
149
- Returns a JSON string representation of the tool config.
150
- Ensures `configuration` is fully serialized by using the
151
- subclass's `model_dump_json()` when available.
152
- """
153
- config_json = (
154
- self.configuration.model_dump_json() if self.configuration else None
155
- )
156
- config = json.loads(config_json) if config_json else None
157
-
158
- data: Dict[str, Any] = {
159
- "name": self.name,
160
- "configuration": config,
161
- "display_name": self.display_name,
162
- "icon": self.icon,
163
- "selection_policy": self.selection_policy,
164
- "is_exclusive": self.is_exclusive,
165
- "is_sub_agent": self.is_sub_agent,
166
- "is_enabled": self.is_enabled,
167
- }
168
- return json.dumps(data)
126
+ @field_serializer("configuration")
127
+ def serialize_config(self, value: BaseToolConfig) -> dict[str, Any]:
128
+ return value.__class__.model_dump(value)
@@ -40,3 +40,7 @@ class OpenAIBuiltInTool(ABC, Generic[ToolType]):
40
40
  @abstractmethod
41
41
  def takes_control(self) -> bool:
42
42
  raise NotImplementedError()
43
+
44
+ @abstractmethod
45
+ def display_name(self) -> str:
46
+ raise NotImplementedError()
@@ -244,3 +244,7 @@ class OpenAICodeInterpreterTool(OpenAIBuiltInTool[CodeInterpreter]):
244
244
  tool_format_information_for_user_prompt=self._config.tool_format_information_for_user_prompt,
245
245
  input_model={},
246
246
  )
247
+
248
+ @override
249
+ def display_name(self) -> str:
250
+ return self.DISPLAY_NAME
@@ -231,25 +231,6 @@ class _ToolManager(Generic[_ApiMode]):
231
231
  self,
232
232
  tool_calls: list[LanguageModelFunction],
233
233
  ) -> list[ToolCallResponse]:
234
- tool_calls = tool_calls
235
-
236
- tool_calls = self.filter_duplicate_tool_calls(
237
- tool_calls=tool_calls,
238
- )
239
- num_tool_calls = len(tool_calls)
240
-
241
- if num_tool_calls > self._config.max_tool_calls:
242
- self._logger.warning(
243
- (
244
- "Number of tool calls %s exceeds the allowed maximum of %s."
245
- "The tool calls will be reduced to the first %s."
246
- ),
247
- num_tool_calls,
248
- self._config.max_tool_calls,
249
- self._config.max_tool_calls,
250
- )
251
- tool_calls = tool_calls[: self._config.max_tool_calls]
252
-
253
234
  tool_call_responses = await self._execute_parallelized(tool_calls)
254
235
  return tool_call_responses
255
236
 
@@ -358,6 +339,22 @@ class _ToolManager(Generic[_ApiMode]):
358
339
  )
359
340
  return unique_tool_calls
360
341
 
342
+ def filter_tool_calls_by_max_tool_calls_allowed(
343
+ self, tool_calls: list[LanguageModelFunction]
344
+ ) -> list[LanguageModelFunction]:
345
+ if len(tool_calls) > self._config.max_tool_calls:
346
+ self._logger.warning(
347
+ (
348
+ "Number of tool calls %s exceeds the allowed maximum of %s."
349
+ "The tool calls will be reduced to the first %s."
350
+ ),
351
+ len(tool_calls),
352
+ self._config.max_tool_calls,
353
+ self._config.max_tool_calls,
354
+ )
355
+ return tool_calls[: self._config.max_tool_calls]
356
+ return tool_calls
357
+
361
358
  @overload
362
359
  def get_tool_by_name(
363
360
  self: "_ToolManager[Literal['completions']]", name: str
@@ -47,5 +47,8 @@ from .schemas import (
47
47
  from .verification import (
48
48
  verify_signature_and_construct_event as verify_signature_and_construct_event,
49
49
  )
50
+ from .webhook import (
51
+ is_webhook_signature_valid as is_webhook_signature_valid,
52
+ )
50
53
 
51
54
  DOMAIN_NAME = "app"
@@ -0,0 +1,131 @@
1
+ import json
2
+ from logging import getLogger
3
+ from typing import TYPE_CHECKING, Any, Callable, TypeVar
4
+
5
+ from pydantic import ValidationError
6
+
7
+ if TYPE_CHECKING:
8
+ from fastapi import BackgroundTasks, FastAPI, Request, status
9
+ from fastapi.responses import JSONResponse
10
+ else:
11
+ try:
12
+ from fastapi import BackgroundTasks, FastAPI, Request, status
13
+ from fastapi.responses import JSONResponse
14
+ except ImportError:
15
+ FastAPI = None # type: ignore[assignment, misc]
16
+ Request = None # type: ignore[assignment, misc]
17
+ status = None # type: ignore[assignment, misc]
18
+ JSONResponse = None # type: ignore[assignment, misc]
19
+ BackgroundTasks = None # type: ignore[assignment, misc]
20
+
21
+ from unique_toolkit.app.schemas import BaseEvent, ChatEvent, EventName
22
+ from unique_toolkit.app.unique_settings import UniqueSettings
23
+
24
+ logger = getLogger(__name__)
25
+
26
+
27
+ def default_event_handler(event: Any) -> int:
28
+ logger.info("Event received at event handler")
29
+ if status is not None:
30
+ return status.HTTP_200_OK
31
+ else:
32
+ # No fastapi installed
33
+ return 200
34
+
35
+
36
+ T = TypeVar("T", bound=BaseEvent)
37
+
38
+
39
+ def build_unique_custom_app(
40
+ *,
41
+ title: str = "Unique Chat App",
42
+ webhook_path: str = "/webhook",
43
+ settings: UniqueSettings,
44
+ event_handler: Callable[[T], int] = default_event_handler,
45
+ event_constructor: Callable[..., T] = ChatEvent,
46
+ subscribed_event_names: list[str] | None = None,
47
+ ) -> "FastAPI":
48
+ """Factory class for creating FastAPI apps with Unique webhook handling."""
49
+ if FastAPI is None:
50
+ raise ImportError(
51
+ "FastAPI is not installed. Install it with: poetry install --with fastapi"
52
+ )
53
+
54
+ app = FastAPI(title=title)
55
+
56
+ if subscribed_event_names is None:
57
+ subscribed_event_names = [EventName.EXTERNAL_MODULE_CHOSEN]
58
+
59
+ @app.get(path="/")
60
+ async def health_check() -> JSONResponse:
61
+ """Health check endpoint."""
62
+ return JSONResponse(content={"status": "healthy", "service": title})
63
+
64
+ @app.post(path=webhook_path)
65
+ async def webhook_handler(
66
+ request: Request, background_tasks: BackgroundTasks
67
+ ) -> JSONResponse:
68
+ """
69
+ Webhook endpoint for receiving events from Unique platform.
70
+
71
+ This endpoint:
72
+ 1. Verifies the webhook signature
73
+ 2. Constructs an event from the payload
74
+ 3. Calls the configured event handler
75
+ """
76
+ # Get raw body and headers
77
+ body = await request.body()
78
+ headers = dict(request.headers)
79
+
80
+ from unique_toolkit.app.webhook import is_webhook_signature_valid
81
+
82
+ if not is_webhook_signature_valid(
83
+ headers=headers,
84
+ payload=body,
85
+ endpoint_secret=settings.app.endpoint_secret.get_secret_value(),
86
+ ):
87
+ return JSONResponse(
88
+ status_code=status.HTTP_401_UNAUTHORIZED,
89
+ content={"error": "Invalid webhook signature"},
90
+ )
91
+
92
+ try:
93
+ event_data = json.loads(body.decode(encoding="utf-8"))
94
+ except json.JSONDecodeError as e:
95
+ logger.error(f"Error parsing event: {e}", exc_info=True)
96
+ return JSONResponse(
97
+ status_code=status.HTTP_400_BAD_REQUEST,
98
+ content={"error": f"Invalid event format: {str(e)}"},
99
+ )
100
+
101
+ if event_data["event"] not in subscribed_event_names:
102
+ return JSONResponse(
103
+ status_code=status.HTTP_400_BAD_REQUEST,
104
+ content={"error": "Not subscribed event"},
105
+ )
106
+
107
+ try:
108
+ event = event_constructor(**event_data)
109
+ if event.filter_event(filter_options=settings.chat_event_filter_options):
110
+ return JSONResponse(
111
+ status_code=status.HTTP_200_OK,
112
+ content={"error": "Event filtered out"},
113
+ )
114
+ except ValidationError as e:
115
+ # pydantic errors https://docs.pydantic.dev/2.10/errors/errors/
116
+ logger.error(f"Validation error with model: {e.json()}", exc_info=True)
117
+ raise e
118
+ except ValueError as e:
119
+ logger.error(f"Error deserializing event: {e}", exc_info=True)
120
+ return JSONResponse(
121
+ status_code=status.HTTP_400_BAD_REQUEST,
122
+ content={"error": "Invalid event"},
123
+ )
124
+
125
+ # Run the task in background so that we don't block for long running tasks
126
+ background_tasks.add_task(event_handler, event)
127
+ return JSONResponse(
128
+ status_code=status.HTTP_200_OK, content={"message": "Event received"}
129
+ )
130
+
131
+ return app
@@ -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
@@ -263,7 +263,7 @@ def create_message(
263
263
  references: list[ContentReference] | None = None,
264
264
  debug_info: dict | None = None,
265
265
  set_completed_at: bool | None = False,
266
- ):
266
+ ) -> ChatMessage:
267
267
  """Creates a message in the chat session synchronously.
268
268
 
269
269
  Args:
@@ -144,7 +144,7 @@ def search_contents(
144
144
  chat_id: str,
145
145
  where: dict,
146
146
  include_failed_content: bool = False,
147
- ):
147
+ ) -> list[Content]:
148
148
  """
149
149
  Performs an asynchronous search for content files in the knowledge base by filter.
150
150
 
@@ -297,7 +297,7 @@ def upload_content_from_bytes(
297
297
  skip_ingestion: bool = False,
298
298
  ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
299
299
  metadata: dict[str, Any] | None = None,
300
- ):
300
+ ) -> Content:
301
301
  """
302
302
  Uploads content to the knowledge base.
303
303
 
@@ -347,7 +347,7 @@ def upload_content(
347
347
  skip_excel_ingestion: bool = False,
348
348
  ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
349
349
  metadata: dict[str, Any] | None = None,
350
- ):
350
+ ) -> Content:
351
351
  """
352
352
  Uploads content to the knowledge base.
353
353
 
@@ -399,7 +399,7 @@ def _trigger_upload_content(
399
399
  skip_excel_ingestion: bool = False,
400
400
  ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
401
401
  metadata: dict[str, Any] | None = None,
402
- ):
402
+ ) -> Content:
403
403
  """
404
404
  Uploads content to the knowledge base.
405
405
 
@@ -528,7 +528,7 @@ class ContentService:
528
528
  skip_excel_ingestion: bool = False,
529
529
  ingestion_config: unique_sdk.Content.IngestionConfig | None = None,
530
530
  metadata: dict[str, Any] | None = None,
531
- ):
531
+ ) -> Content:
532
532
  """
533
533
  Uploads content to the knowledge base.
534
534