unique_toolkit 2026.28.0.dev6__py3-none-any.whl → 2026.28.0.dev8__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.
@@ -71,7 +71,7 @@ class ChunkRelevancySorter:
71
71
  company_id: str | None = None,
72
72
  user_id: str | None = None,
73
73
  ):
74
- if isinstance(event, (ChatEvent, BaseEvent)):
74
+ if isinstance(event, BaseEvent):
75
75
  self.chunk_relevancy_evaluator = ContextRelevancyEvaluator.from_event(event)
76
76
  else:
77
77
  [company_id, user_id] = validate_required_values([company_id, user_id])
@@ -81,7 +81,7 @@ class ContextRelevancyEvaluator:
81
81
  company_id: str | None = None,
82
82
  user_id: str | None = None,
83
83
  ):
84
- if isinstance(event, (ChatEvent, BaseEvent)):
84
+ if isinstance(event, BaseEvent):
85
85
  self.language_model_service = LanguageModelService.from_event(event)
86
86
  else:
87
87
  [company_id, user_id] = validate_required_values([company_id, user_id])
@@ -116,7 +116,7 @@ class RetrieveSearchScopeTool(Tool[RetrieveSearchScopeConfig]):
116
116
  )
117
117
 
118
118
  try:
119
- settings = UniqueSettings.from_chat_event(self._event)
119
+ settings = UniqueSettings.from_chat_event(self.event)
120
120
  kb_service = UniqueServiceFactory(
121
121
  settings=settings
122
122
  ).knowledge_base_service()
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+ from typing_extensions import Self
5
+
6
+ from unique_toolkit.app.schemas import ChatEvent
7
+
8
+
9
+ class ToolRunContext(BaseModel):
10
+ """Tool-manager runtime state decoupled from a raw :class:`ChatEvent`."""
11
+
12
+ model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
13
+
14
+ tool_choices: list[str] = Field(default_factory=list)
15
+ disabled_tools: list[str] = Field(default_factory=list)
16
+ tool_init_event: ChatEvent | None = None
17
+
18
+ @classmethod
19
+ def from_chat_event(cls, event: ChatEvent) -> Self:
20
+ return cls(
21
+ tool_choices=list(event.payload.tool_choices),
22
+ disabled_tools=list(event.payload.disabled_tools),
23
+ tool_init_event=event,
24
+ )
@@ -18,7 +18,8 @@ from unique_toolkit.app.schemas import ChatEvent
18
18
  from unique_toolkit.language_model import LanguageModelToolDescription
19
19
 
20
20
  if TYPE_CHECKING:
21
- pass
21
+ from unique_toolkit.language_model.service import LanguageModelService
22
+ from unique_toolkit.services.chat_service import ChatService
22
23
  from unique_toolkit.language_model.schemas import (
23
24
  LanguageModelFunction,
24
25
  )
@@ -169,6 +170,9 @@ class Tool(ABC, Generic[ConfigType]):
169
170
  def __init__(self, config: ConfigType) -> None: ...
170
171
 
171
172
  @overload
173
+ @deprecated(
174
+ "Passing event is deprecated. Use Tool(config) and inject context in run()."
175
+ )
172
176
  def __init__(
173
177
  self,
174
178
  config: ConfigType,
@@ -176,17 +180,32 @@ class Tool(ABC, Generic[ConfigType]):
176
180
  tool_progress_reporter: ToolProgressReporter | None = ...,
177
181
  ) -> None: ...
178
182
 
183
+ @overload
184
+ def __init__(
185
+ self,
186
+ config: ConfigType,
187
+ *,
188
+ chat_service: ChatService,
189
+ language_model_service: LanguageModelService,
190
+ tool_progress_reporter: ToolProgressReporter | None = ...,
191
+ ) -> None: ...
192
+
179
193
  def __init__(
180
194
  self,
181
195
  config: ConfigType,
182
196
  event: ChatEvent | None = None,
183
197
  tool_progress_reporter: ToolProgressReporter | None = None,
198
+ *,
199
+ chat_service: ChatService | None = None,
200
+ language_model_service: LanguageModelService | None = None,
184
201
  ) -> None:
185
202
  """Initialize the tool.
186
203
 
187
- Preferred (decoupled): `Tool(config)` — configuration only.
204
+ Preferred (decoupled): ``Tool(config)`` — configuration only.
188
205
 
189
- Backward compatible: `Tool(config, event, tool_progress_reporter)` — creates
206
+ Service injection: ``Tool(config, chat_service=..., language_model_service=...)``.
207
+
208
+ Backward compatible: ``Tool(config, event, tool_progress_reporter)`` — creates
190
209
  deprecated chat_service, language_model_service, message_step_logger for
191
210
  legacy subclasses.
192
211
  """
@@ -200,6 +219,25 @@ class Tool(ABC, Generic[ConfigType]):
200
219
  self.logger = getLogger(f"{module_name}.{__name__}")
201
220
  self.debug_info: dict[str, Any] = {}
202
221
 
222
+ if (chat_service is None) != (language_model_service is None):
223
+ raise ValueError(
224
+ "chat_service and language_model_service must be injected together; "
225
+ "supplying only one is not supported."
226
+ )
227
+
228
+ if chat_service is not None and language_model_service is not None:
229
+ from unique_toolkit.agentic.message_log_manager.service import (
230
+ MessageStepLogger,
231
+ )
232
+
233
+ self._tool_progress_reporter = tool_progress_reporter
234
+ self._chat_service = chat_service
235
+ self._language_model_service = language_model_service
236
+ self._message_step_logger = MessageStepLogger(
237
+ chat_service=self._chat_service,
238
+ )
239
+ return
240
+
203
241
  if event is not None:
204
242
  from unique_toolkit.agentic.message_log_manager.service import (
205
243
  MessageStepLogger,
@@ -220,11 +258,12 @@ class Tool(ABC, Generic[ConfigType]):
220
258
  "Never reuse event. Dangerous. Prefer Tool(config) and inject context in run()."
221
259
  )
222
260
  def event(self) -> ChatEvent:
223
- if not hasattr(self, "_event"):
261
+ chat_event = getattr(self, "_event", None)
262
+ if chat_event is None:
224
263
  raise AttributeError(
225
264
  "event not available (tool was initialized with config only). "
226
265
  )
227
- return self._event
266
+ return chat_event
228
267
 
229
268
  @property
230
269
  @deprecated(
@@ -12,6 +12,7 @@ from openai.types.responses import (
12
12
  response_create_params,
13
13
  )
14
14
  from pydantic import BaseModel, Field
15
+ from typing_extensions import Self
15
16
 
16
17
  from unique_toolkit._common.execution import (
17
18
  Result,
@@ -31,6 +32,7 @@ from unique_toolkit.agentic.tools.openai_builtin.base import (
31
32
  OpenAIBuiltInToolName,
32
33
  )
33
34
  from unique_toolkit.agentic.tools.openai_builtin.manager import OpenAIBuiltInToolManager
35
+ from unique_toolkit.agentic.tools.run_context import ToolRunContext
34
36
  from unique_toolkit.agentic.tools.schemas import ToolCallResponse, ToolPrompts
35
37
  from unique_toolkit.agentic.tools.tool import Tool
36
38
  from unique_toolkit.agentic.tools.tool_progress_reporter import ToolProgressReporter
@@ -88,13 +90,62 @@ class _ToolManager(Generic[_ApiMode]):
88
90
  a2a_manager: A2AManager,
89
91
  api_mode: _ApiMode,
90
92
  builtin_tool_manager: OpenAIBuiltInToolManager | None = None,
93
+ ) -> None:
94
+ self._setup(
95
+ logger=logger,
96
+ config=config,
97
+ run_context=ToolRunContext.from_chat_event(event),
98
+ tool_progress_reporter=tool_progress_reporter,
99
+ mcp_manager=mcp_manager,
100
+ a2a_manager=a2a_manager,
101
+ api_mode=api_mode,
102
+ builtin_tool_manager=builtin_tool_manager,
103
+ )
104
+
105
+ @classmethod
106
+ def from_run_context(
107
+ cls,
108
+ logger: Logger,
109
+ config: ToolManagerConfig,
110
+ *,
111
+ run_context: ToolRunContext,
112
+ tool_progress_reporter: ToolProgressReporter,
113
+ mcp_manager: MCPManager,
114
+ a2a_manager: A2AManager,
115
+ api_mode: _ApiMode,
116
+ builtin_tool_manager: OpenAIBuiltInToolManager | None = None,
117
+ ) -> Self:
118
+ instance = cls.__new__(cls)
119
+ instance._setup(
120
+ logger=logger,
121
+ config=config,
122
+ run_context=run_context,
123
+ tool_progress_reporter=tool_progress_reporter,
124
+ mcp_manager=mcp_manager,
125
+ a2a_manager=a2a_manager,
126
+ api_mode=api_mode,
127
+ builtin_tool_manager=builtin_tool_manager,
128
+ )
129
+ return instance
130
+
131
+ def _setup(
132
+ self,
133
+ *,
134
+ logger: Logger,
135
+ config: ToolManagerConfig,
136
+ run_context: ToolRunContext,
137
+ tool_progress_reporter: ToolProgressReporter,
138
+ mcp_manager: MCPManager,
139
+ a2a_manager: A2AManager,
140
+ api_mode: _ApiMode,
141
+ builtin_tool_manager: OpenAIBuiltInToolManager | None,
91
142
  ) -> None:
92
143
  self._logger = logger
93
144
  self._config = config
94
145
  self._tool_progress_reporter = tool_progress_reporter
95
146
  self._tools: list[Tool[Any] | OpenAIBuiltInTool[Any]] = []
96
- self._tool_choices = event.payload.tool_choices
97
- self._disabled_tools = event.payload.disabled_tools
147
+ self._tool_choices = run_context.tool_choices
148
+ self._disabled_tools = run_context.disabled_tools
98
149
  self._exclusive_tools = [
99
150
  tool.name for tool in self._config.tools if tool.is_exclusive
100
151
  ]
@@ -104,17 +155,33 @@ class _ToolManager(Generic[_ApiMode]):
104
155
  self._a2a_manager = a2a_manager
105
156
  self._builtin_tool_manager = builtin_tool_manager
106
157
  self._api_mode = api_mode
107
- self._init__tools(event)
108
-
109
- def _init__tools(self, event: ChatEvent) -> None:
158
+ self._sub_agents: list[SubAgentTool] = []
159
+ self._builtin_tools: list[OpenAIBuiltInTool[Any]] = []
160
+ self._mcp_tools: list[Tool[Any]] = []
161
+ self._internal_tools: list[Tool[Any]] = []
162
+ self.available_tools: list[
163
+ Tool[Any] | OpenAIBuiltInTool[Any] | SubAgentTool
164
+ ] = []
165
+ self._init__tools(run_context.tool_init_event)
166
+
167
+ def _init__tools(self, tool_init_event: ChatEvent | None) -> None:
110
168
  tool_choices = self._tool_choices
111
169
  tool_configs = self._config.tools
112
170
  self._logger.info("Initializing tool definitions...")
113
171
  self._logger.info(f"Tool choices: {tool_choices}")
114
172
 
115
- tool_configs, sub_agents = self._a2a_manager.get_all_sub_agents(
116
- tool_configs, event
117
- )
173
+ if tool_init_event is not None:
174
+ tool_configs, sub_agents = self._a2a_manager.get_all_sub_agents(
175
+ tool_configs,
176
+ tool_init_event,
177
+ )
178
+ else:
179
+ tool_configs = [
180
+ tool_config
181
+ for tool_config in tool_configs
182
+ if not tool_config.is_sub_agent
183
+ ]
184
+ sub_agents = []
118
185
  self._sub_agents = sub_agents
119
186
 
120
187
  registered_tool_names = set(t.name for t in self._sub_agents)
@@ -133,18 +200,27 @@ class _ToolManager(Generic[_ApiMode]):
133
200
  registered_tool_names.update(t.name for t in self._mcp_tools)
134
201
 
135
202
  # Build internal tools from configurations
136
- self._internal_tools = [
137
- ToolFactory.build_tool_with_settings(
138
- t.name,
139
- t,
140
- t.configuration,
141
- event,
142
- tool_progress_reporter=self._tool_progress_reporter,
143
- )
144
- for t in tool_configs
145
- if t.name not in registered_tool_names # Skip already handled tools
146
- and t.name not in OpenAIBuiltInToolName # Safeguard
147
- ]
203
+ self._internal_tools.clear()
204
+ for t in tool_configs:
205
+ if t.name in registered_tool_names:
206
+ continue
207
+ if t.name in OpenAIBuiltInToolName:
208
+ continue
209
+ if tool_init_event is not None:
210
+ built_tool = ToolFactory.build_tool_with_settings(
211
+ t.name,
212
+ t,
213
+ t.configuration,
214
+ tool_init_event,
215
+ tool_progress_reporter=self._tool_progress_reporter,
216
+ )
217
+ else:
218
+ self._logger.info(
219
+ "Skipping internal tool '%s' (requires chat event for initialization)",
220
+ t.name,
221
+ )
222
+ continue
223
+ self._internal_tools.append(built_tool)
148
224
 
149
225
  # Combine all types of tools
150
226
  self.available_tools = (
@@ -607,6 +683,30 @@ class ToolManager(_ToolManager[Literal["completions"]]):
607
683
  builtin_tool_manager=None,
608
684
  )
609
685
 
686
+ @classmethod
687
+ def from_run_context(
688
+ cls,
689
+ logger: Logger,
690
+ config: ToolManagerConfig,
691
+ *,
692
+ run_context: ToolRunContext,
693
+ tool_progress_reporter: ToolProgressReporter,
694
+ mcp_manager: MCPManager,
695
+ a2a_manager: A2AManager,
696
+ api_mode: Literal["completions"] = "completions",
697
+ builtin_tool_manager: OpenAIBuiltInToolManager | None = None,
698
+ ) -> Self:
699
+ return super().from_run_context(
700
+ logger=logger,
701
+ config=config,
702
+ run_context=run_context,
703
+ tool_progress_reporter=tool_progress_reporter,
704
+ mcp_manager=mcp_manager,
705
+ a2a_manager=a2a_manager,
706
+ api_mode=api_mode,
707
+ builtin_tool_manager=builtin_tool_manager,
708
+ )
709
+
610
710
 
611
711
  class ResponsesApiToolManager(_ToolManager[Literal["responses"]]):
612
712
  def __init__(
@@ -631,6 +731,33 @@ class ResponsesApiToolManager(_ToolManager[Literal["responses"]]):
631
731
  builtin_tool_manager=builtin_tool_manager,
632
732
  )
633
733
 
734
+ @classmethod
735
+ def from_run_context(
736
+ cls,
737
+ logger: Logger,
738
+ config: ToolManagerConfig,
739
+ *,
740
+ run_context: ToolRunContext,
741
+ tool_progress_reporter: ToolProgressReporter,
742
+ mcp_manager: MCPManager,
743
+ a2a_manager: A2AManager,
744
+ api_mode: Literal["responses"] = "responses",
745
+ builtin_tool_manager: OpenAIBuiltInToolManager | None = None,
746
+ ) -> Self:
747
+ if builtin_tool_manager is None:
748
+ msg = "builtin_tool_manager is required for ResponsesApiToolManager"
749
+ raise ValueError(msg)
750
+ return super().from_run_context(
751
+ logger=logger,
752
+ config=config,
753
+ run_context=run_context,
754
+ tool_progress_reporter=tool_progress_reporter,
755
+ mcp_manager=mcp_manager,
756
+ a2a_manager=a2a_manager,
757
+ api_mode=api_mode,
758
+ builtin_tool_manager=builtin_tool_manager,
759
+ )
760
+
634
761
  def get_required_include_params(self) -> list[ResponseIncludable]:
635
762
  """Return Responses API include params required by all active built-in tools."""
636
763
  return self._builtin_tool_manager.get_required_include_params()
@@ -1,5 +1,5 @@
1
1
  from enum import StrEnum
2
- from typing import Annotated, Any, Generic, Literal, TypeVar
2
+ from typing import Annotated, Any, Generic, Literal, TypeVar, override
3
3
 
4
4
  from pydantic import (
5
5
  AliasChoices,
@@ -20,13 +20,13 @@ from unique_sdk.api_resources._agentic_table import (
20
20
  SheetType,
21
21
  )
22
22
 
23
+ from unique_toolkit._common.exception import ConfigurationException
23
24
  from unique_toolkit._common.pydantic_helpers import get_configuration_dict
24
25
  from unique_toolkit.app.schemas import (
25
- ChatEvent,
26
- ChatEventAssistantMessage,
27
- ChatEventUserMessage,
28
- Correlation,
26
+ AssistantWebhookEvent,
27
+ BaseEventPayload,
29
28
  )
29
+ from unique_toolkit.app.unique_settings import UniqueChatEventFilterOptions
30
30
  from unique_toolkit.language_model.schemas import (
31
31
  LanguageModelMessageRole,
32
32
  LanguageModelMessages,
@@ -112,27 +112,27 @@ A = TypeVar("A", bound=MagicTableAction)
112
112
  T = TypeVar("T", bound=BaseMetadata)
113
113
 
114
114
 
115
- class MagicTableBasePayload(BaseModel, Generic[A, T]):
115
+ class MagicTableBasePayload(BaseEventPayload, Generic[A, T]):
116
+ """Magic-table-specific payload.
117
+
118
+ Inherits the common envelope fields (``name``, ``chat_id``, ``assistant_id``,
119
+ ``configuration``, ``metadata_filter``, ``correlation``) from
120
+ :class:`BaseEventPayload` and adds magic-table-only fields.
121
+
122
+ Note: previously this carried stub-empty ``user_message`` /
123
+ ``assistant_message`` defaults so that ``MagicTableEvent`` could pose as
124
+ a ``ChatEvent``. That coupling is gone; magic-table flows that need a
125
+ chat context should construct a :class:`UniqueContext` explicitly.
126
+ """
127
+
116
128
  model_config = get_configuration_dict()
117
- name: str = Field(description="The name of the module")
118
- sheet_name: str
119
129
 
130
+ # Optional on magic-table (BaseEventPayload requires it for chat).
131
+ configuration: dict[str, Any] = Field(default_factory=dict)
132
+ sheet_name: str
120
133
  action: A
121
- chat_id: str
122
- assistant_id: str
123
134
  table_id: str
124
- user_message: ChatEventUserMessage = Field(
125
- default=ChatEventUserMessage(
126
- id="", text="", original_text="", created_at="", language=""
127
- )
128
- )
129
- assistant_message: ChatEventAssistantMessage = Field(
130
- default=ChatEventAssistantMessage(id="", created_at="")
131
- )
132
- configuration: dict[str, Any] = {}
133
135
  metadata: T
134
- metadata_filter: dict[str, Any] | None = None
135
- correlation: Correlation | None = None
136
136
 
137
137
 
138
138
  ########### Specialized Payload definitions ###########
@@ -263,10 +263,48 @@ PayloadTypes = (
263
263
  MagicTablePayloadTypes = Annotated[PayloadTypes, Field(discriminator="action")]
264
264
 
265
265
 
266
- class MagicTableEvent(ChatEvent):
267
- # TODO(UN-19532): investigate making ChatEvent generic to avoid these overrides
266
+ class MagicTableEvent(
267
+ AssistantWebhookEvent[UniqueChatEventFilterOptions, MagicTablePayloadTypes]
268
+ ):
269
+ """Magic-table webhook event, sibling of :class:`ChatEvent`.
270
+
271
+ No longer extends ``ChatEvent``; both events are siblings under
272
+ :class:`AssistantWebhookEvent`. This makes ``isinstance(magic_table_event,
273
+ ChatEvent)`` correctly return ``False`` and prevents silently passing a
274
+ magic-table event to services that require chat-only fields.
275
+ """
276
+
268
277
  event: MagicTableEventTypes # pyright: ignore[reportIncompatibleVariableOverride]
269
- payload: MagicTablePayloadTypes # pyright: ignore[reportIncompatibleVariableOverride]
278
+
279
+ @override
280
+ def filter_event(
281
+ self, *, filter_options: UniqueChatEventFilterOptions | None = None
282
+ ) -> bool:
283
+ if filter_options is None:
284
+ return False
285
+
286
+ if not filter_options.assistant_ids and not filter_options.references_in_code:
287
+ raise ConfigurationException(
288
+ "No filter options provided, all events will be filtered! \n"
289
+ "Please define: \n"
290
+ " - 'UNIQUE_CHAT_EVENT_FILTER_OPTIONS_ASSISTANT_IDS' \n"
291
+ " - 'UNIQUE_CHAT_EVENT_FILTER_OPTIONS_REFERENCES_IN_CODE' \n"
292
+ "in your environment variables."
293
+ )
294
+
295
+ if (
296
+ filter_options.assistant_ids
297
+ and self.payload.assistant_id not in filter_options.assistant_ids
298
+ ):
299
+ return True
300
+
301
+ if (
302
+ filter_options.references_in_code
303
+ and self.payload.name not in filter_options.references_in_code
304
+ ):
305
+ return True
306
+
307
+ return super().filter_event(filter_options=filter_options)
270
308
 
271
309
 
272
310
  class LogDetail(BaseModel):
@@ -0,0 +1,67 @@
1
+ """Synthetic :class:`ChatEvent` for magic-table runs with chat-typed toolkit APIs."""
2
+
3
+ from unique_toolkit.agentic_table.schemas import MagicTableEvent
4
+ from unique_toolkit.app.schemas import (
5
+ ChatEvent,
6
+ ChatEventAssistantMessage,
7
+ ChatEventPayload,
8
+ ChatEventUserMessage,
9
+ EventName,
10
+ )
11
+ from unique_toolkit.app.unique_settings import (
12
+ MAGIC_TABLE_STREAMING_ASSISTANT_MESSAGE_ID,
13
+ MAGIC_TABLE_STREAMING_USER_MESSAGE_ID,
14
+ )
15
+
16
+ _MAGIC_TABLE_STREAMING_MESSAGE_CREATED_AT = "1970-01-01T00:00:00Z"
17
+
18
+
19
+ def build_synthetic_chat_event(
20
+ event: MagicTableEvent,
21
+ *,
22
+ event_id: str | None = None,
23
+ description: str = "magic-table",
24
+ ) -> ChatEvent:
25
+ """Build a :class:`ChatEvent` for evaluators/tools that still require chat typing.
26
+
27
+ Discouraged for new code: prefer :meth:`UniqueContext.from_magic_table_event`
28
+ and ``Service.from_context(...)`` / :meth:`ToolManager.from_run_context`
29
+ so callers do not depend on a fabricated chat envelope.
30
+
31
+ Magic-table runs create messages dynamically; placeholder message ids are
32
+ sufficient for search tooling and hallucination evaluation scoping.
33
+ """
34
+ payload = event.payload
35
+ chat_id = payload.chat_id
36
+ if not chat_id:
37
+ msg = (
38
+ "Magic-table event is missing chat_id; "
39
+ "chat-scoped services cannot be built."
40
+ )
41
+ raise ValueError(msg)
42
+ return ChatEvent(
43
+ id=event_id or event.id,
44
+ event=EventName.EXTERNAL_MODULE_CHOSEN,
45
+ user_id=event.user_id,
46
+ company_id=event.company_id,
47
+ payload=ChatEventPayload(
48
+ name=payload.name,
49
+ chat_id=chat_id,
50
+ assistant_id=payload.assistant_id,
51
+ description=description,
52
+ configuration=payload.configuration,
53
+ metadata_filter=payload.metadata_filter,
54
+ correlation=payload.correlation,
55
+ user_message=ChatEventUserMessage(
56
+ id=MAGIC_TABLE_STREAMING_USER_MESSAGE_ID,
57
+ text="",
58
+ original_text="",
59
+ created_at=_MAGIC_TABLE_STREAMING_MESSAGE_CREATED_AT,
60
+ language="EN",
61
+ ),
62
+ assistant_message=ChatEventAssistantMessage(
63
+ id=MAGIC_TABLE_STREAMING_ASSISTANT_MESSAGE_ID,
64
+ created_at=_MAGIC_TABLE_STREAMING_MESSAGE_CREATED_AT,
65
+ ),
66
+ ),
67
+ )
@@ -250,14 +250,36 @@ class ChatEventSkill(BaseModel):
250
250
  SkillReference = ChatEventSkill
251
251
 
252
252
 
253
- class ChatEventPayload(BaseModel):
253
+ class BaseEventPayload(BaseModel):
254
+ """Common payload fields shared by all assistant-context webhook events.
255
+
256
+ Both :class:`ChatEventPayload` and :class:`MagicTableBasePayload` extend
257
+ this. Services that only need the common envelope (``chat_id``,
258
+ ``assistant_id``, ``metadata_filter``, ``correlation``, ...) should
259
+ accept :class:`AssistantWebhookEvent` and read the payload off this
260
+ shared shape; services that need chat-only fields (``user_message``,
261
+ ``assistant_message``) must accept :class:`ChatEvent` explicitly.
262
+ """
263
+
254
264
  model_config = model_config
255
265
 
256
- name: str
257
- description: str
258
- configuration: dict[str, Any]
266
+ name: str = Field(description="The module / action name driving the event.")
259
267
  chat_id: str
260
268
  assistant_id: str
269
+ configuration: dict[str, Any]
270
+ # Default is None as empty dict triggers error in `backend-ingestion`
271
+ metadata_filter: dict[str, Any] | None = Field(
272
+ default=None,
273
+ description="Metadata filter compiled after module selection function calling and scope rules.",
274
+ )
275
+ correlation: Correlation | None = Field(
276
+ default=None,
277
+ description="The correlation to parent message in another chat.",
278
+ )
279
+
280
+
281
+ class ChatEventPayload(BaseEventPayload):
282
+ description: str
261
283
  user_message: ChatEventUserMessage
262
284
  assistant_message: ChatEventAssistantMessage
263
285
  text: str | None = None
@@ -296,11 +318,6 @@ class ChatEventPayload(BaseModel):
296
318
  default_factory=dict,
297
319
  description="Parameters extracted from module selection function calling the tool.",
298
320
  )
299
- # Default is None as empty dict triggers error in `backend-ingestion`
300
- metadata_filter: dict[str, Any] | None = Field(
301
- default=None,
302
- description="Metadata filter compiled after module selection function calling and scope rules.",
303
- )
304
321
  raw_scope_rules: UniqueQL | None = Field(
305
322
  default=None,
306
323
  description="Raw UniqueQL rule that can be compiled to a metadata filter.",
@@ -317,10 +334,6 @@ class ChatEventPayload(BaseModel):
317
334
  default=None,
318
335
  description="The session configuration for the chat session.",
319
336
  )
320
- correlation: Correlation | None = Field(
321
- default=None,
322
- description="The correlation to parent message in another chat.",
323
- )
324
337
 
325
338
  @field_validator("raw_scope_rules", mode="before")
326
339
  def validate_scope_rules(cls, value: dict[str, Any] | None) -> UniqueQL | None:
@@ -341,13 +354,39 @@ class EventPayload(ChatEventPayload):
341
354
  # additional_parameters: Optional[EventAdditionalParameters] = None
342
355
 
343
356
 
344
- class ChatEvent(BaseEvent[UniqueChatEventFilterOptions]):
357
+ PayloadT_co = TypeVar("PayloadT_co", bound=BaseEventPayload, covariant=True)
358
+
359
+
360
+ class AssistantWebhookEvent(
361
+ BaseEvent[FilterOptionsT], Generic[FilterOptionsT, PayloadT_co]
362
+ ):
363
+ """Umbrella envelope for webhook events that carry assistant/chat context.
364
+
365
+ The payload is constrained to :class:`BaseEventPayload`, giving consumers
366
+ a typed view of the shared envelope fields (``chat_id``, ``assistant_id``,
367
+ ``metadata_filter``, ``correlation``, ...). Concrete events specialize
368
+ the payload type:
369
+
370
+ * :class:`ChatEvent` → :class:`ChatEventPayload`
371
+ * ``MagicTableEvent`` → ``MagicTablePayloadTypes`` (discriminated union)
372
+
373
+ ``PayloadT_co`` is covariant so that ``AssistantWebhookEvent[..., ChildPayload]``
374
+ is a subtype of ``AssistantWebhookEvent[..., ParentPayload]``. This lets a
375
+ service signature accept *any* assistant-context event while still
376
+ accessing the common ``BaseEventPayload`` shape in a type-checked way.
377
+ """
378
+
345
379
  model_config = model_config
346
380
 
347
- payload: ChatEventPayload
381
+ # Covariant payload must stay read-only; reassignment through a widened type is unsound.
382
+ payload: PayloadT_co = Field(frozen=True)
348
383
  created_at: Optional[int] = None
349
384
  version: Optional[str] = None
350
385
 
386
+
387
+ class ChatEvent(AssistantWebhookEvent[UniqueChatEventFilterOptions, ChatEventPayload]):
388
+ model_config = model_config
389
+
351
390
  class FilterOptions(BaseSettings):
352
391
  assistant_ids: list[str] = Field(
353
392
  default=[],
@@ -26,8 +26,13 @@ from unique_toolkit.app.feature_flags import UNIQUE_TOOLKIT_FEATURE_FLAGS
26
26
  from unique_toolkit.app.find_env_file import EnvFileNotFoundError, find_env_file
27
27
 
28
28
  if TYPE_CHECKING:
29
+ from unique_toolkit.agentic_table.schemas import MagicTableEvent
29
30
  from unique_toolkit.app.schemas import BaseEvent, ChatEvent
30
31
 
32
+ # Placeholder message ids for magic-table runs that create messages dynamically.
33
+ MAGIC_TABLE_STREAMING_ASSISTANT_MESSAGE_ID = "magic-table-streaming-assistant"
34
+ MAGIC_TABLE_STREAMING_USER_MESSAGE_ID = "magic-table-streaming-user"
35
+
31
36
 
32
37
  logger = getLogger(__name__)
33
38
 
@@ -485,6 +490,39 @@ class UniqueContext:
485
490
  chat=ChatContext.from_chat_event(event),
486
491
  )
487
492
 
493
+ @classmethod
494
+ def from_magic_table_event(cls, event: MagicTableEvent) -> Self:
495
+ """Build auth + chat context from a magic-table webhook event.
496
+
497
+ Magic-table payloads carry ``chat_id`` and ``assistant_id`` but no live
498
+ user/assistant message objects. Placeholder message ids are used so
499
+ chat-scoped services (e.g. :class:`ChatService`) can be constructed via
500
+ :meth:`from_context`; do not use this context for short-term memory
501
+ writes that require a real message id.
502
+ """
503
+ payload = event.payload
504
+ if not payload.chat_id:
505
+ msg = (
506
+ "Magic-table event is missing chat_id; "
507
+ "chat-scoped services cannot be built."
508
+ )
509
+ raise ValueError(msg)
510
+ parent_chat_id = (
511
+ payload.correlation.parent_chat_id if payload.correlation else None
512
+ )
513
+ return cls(
514
+ auth=AuthContext.from_event(event),
515
+ chat=ChatContext(
516
+ chat_id=payload.chat_id,
517
+ assistant_id=payload.assistant_id,
518
+ last_assistant_message_id=MAGIC_TABLE_STREAMING_ASSISTANT_MESSAGE_ID,
519
+ last_user_message_id=MAGIC_TABLE_STREAMING_USER_MESSAGE_ID,
520
+ last_user_message_text="",
521
+ metadata_filter=payload.metadata_filter,
522
+ parent_chat_id=parent_chat_id,
523
+ ),
524
+ )
525
+
488
526
  @classmethod
489
527
  def from_event(cls, event: BaseEvent[Any]) -> Self:
490
528
  """Build an auth-only context from any BaseEvent."""
@@ -180,6 +180,7 @@ def map_references(references: list[ContentReference]) -> list[dict[str, Any]]:
180
180
  return [
181
181
  {
182
182
  "name": ref.name,
183
+ "description": ref.description,
183
184
  "url": ref.url,
184
185
  "sequenceNumber": ref.sequence_number,
185
186
  "sourceId": ref.source_id,
@@ -199,6 +200,7 @@ def map_references_with_original_index(
199
200
  return [
200
201
  {
201
202
  "name": ref.name,
203
+ "description": ref.description,
202
204
  "url": ref.url,
203
205
  "sequenceNumber": ref.sequence_number,
204
206
  "sourceId": ref.source_id,
@@ -215,6 +215,17 @@ class ContentReference(BaseModel):
215
215
  sequence_number: int
216
216
  source: str
217
217
  source_id: str
218
+ description: str | None = Field(
219
+ default=None,
220
+ description=(
221
+ "Optional free-text/structured description carried alongside the "
222
+ "reference. Used by MCP tool references (UN-22310) to carry an "
223
+ 'enriched ``{"mcp": {connectorName, title, details?, mcpServerId?}}`` '
224
+ "JSON payload the chat frontend renders as a card. Nullable "
225
+ "end-to-end (node-chat persists it as a nullable column; empty "
226
+ "values are dropped before sending)."
227
+ ),
228
+ )
218
229
  url: str | None = Field(
219
230
  default=None,
220
231
  description=(
@@ -241,6 +252,8 @@ class ContentReference(BaseModel):
241
252
  "source": reference["source"],
242
253
  "source_id": reference["sourceId"],
243
254
  }
255
+ if "description" in reference:
256
+ kwargs["description"] = reference["description"]
244
257
  if "originalIndex" in reference:
245
258
  kwargs["original_index"] = reference["originalIndex"]
246
259
 
@@ -4,12 +4,12 @@ from typing import Any, overload
4
4
 
5
5
  import unique_sdk
6
6
  from requests import Response
7
- from typing_extensions import deprecated
7
+ from typing_extensions import Self, deprecated
8
8
 
9
9
  from unique_toolkit._common.utils.files import is_file_content, is_image_content
10
10
  from unique_toolkit._common.validate_required_values import validate_required_values
11
- from unique_toolkit.app.schemas import BaseEvent, ChatEvent, Correlation, Event
12
- from unique_toolkit.app.unique_settings import UniqueSettings
11
+ from unique_toolkit.app.schemas import AssistantWebhookEvent, BaseEvent, Correlation
12
+ from unique_toolkit.app.unique_settings import UniqueContext, UniqueSettings
13
13
  from unique_toolkit.content import DOMAIN_NAME
14
14
  from unique_toolkit.content.constants import DEFAULT_SEARCH_LANGUAGE
15
15
  from unique_toolkit.content.functions import (
@@ -46,7 +46,7 @@ class ContentService:
46
46
  "Use __init__ with company_id, user_id and chat_id instead or use the classmethod `from_event`"
47
47
  )
48
48
  @overload
49
- def __init__(self, event: Event | ChatEvent | BaseEvent[Any]): ...
49
+ def __init__(self, event: BaseEvent[Any]): ...
50
50
 
51
51
  """
52
52
  Initialize the ContentService with an event (deprecated)
@@ -68,7 +68,7 @@ class ContentService:
68
68
 
69
69
  def __init__(
70
70
  self,
71
- event: Event | BaseEvent[Any] | None = None,
71
+ event: BaseEvent[Any] | None = None,
72
72
  company_id: str | None = None,
73
73
  user_id: str | None = None,
74
74
  chat_id: str | None = None,
@@ -78,23 +78,32 @@ class ContentService:
78
78
  Initialize the ContentService with a company_id, user_id and chat_id.
79
79
  """
80
80
 
81
- self._event = event # Changed to protected attribute
81
+ if event is not None:
82
+ if isinstance(event, AssistantWebhookEvent):
83
+ delegated = type(self).from_event(event)
84
+ self._event = event
85
+ self._company_id = delegated._company_id
86
+ self._user_id = delegated._user_id
87
+ self._chat_id = delegated._chat_id
88
+ self._metadata_filter = delegated._metadata_filter
89
+ return
90
+
91
+ self._event = event
92
+ self._metadata_filter = None
93
+ self._company_id = event.company_id
94
+ self._user_id = event.user_id
95
+ return
96
+
97
+ self._event = None
82
98
  self._metadata_filter = None
83
- if event:
84
- self._company_id: str = event.company_id
85
- self._user_id: str = event.user_id
86
- if isinstance(event, (ChatEvent, Event)):
87
- self._metadata_filter = event.payload.metadata_filter
88
- self._chat_id: str | None = event.payload.chat_id
89
- else:
90
- [company_id, user_id] = validate_required_values([company_id, user_id])
91
- self._company_id = company_id
92
- self._user_id = user_id
93
- self._chat_id = chat_id
94
- self._metadata_filter = metadata_filter
99
+ [company_id, user_id] = validate_required_values([company_id, user_id])
100
+ self._company_id = company_id
101
+ self._user_id = user_id
102
+ self._chat_id = chat_id
103
+ self._metadata_filter = metadata_filter
95
104
 
96
105
  @classmethod
97
- def from_event(cls, event: Event | ChatEvent | BaseEvent[Any]):
106
+ def from_event(cls, event: BaseEvent[Any]):
98
107
  """Initialize the ContentService with an event.
99
108
 
100
109
  When the event has a correlation (e.g. subagent run), delegates to
@@ -109,32 +118,26 @@ class ContentService:
109
118
  ContentService: Instance scoped to the event's chat or, when
110
119
  correlation is present, the parent chat.
111
120
  """
112
- if (
113
- isinstance(event, (ChatEvent, Event))
114
- and getattr(event.payload, "correlation", None) is not None
115
- ):
116
- if event.payload.correlation is None:
117
- raise ValueError(
118
- "correlation attribute is not defined in the event payload"
121
+ if isinstance(event, AssistantWebhookEvent):
122
+ payload = event.payload
123
+ if payload.correlation is not None:
124
+ return cls.from_correlation(
125
+ event.company_id,
126
+ event.user_id,
127
+ payload.correlation,
128
+ metadata_filter=payload.metadata_filter,
119
129
  )
120
- return cls.from_correlation(
121
- event.company_id,
122
- event.user_id,
123
- event.payload.correlation,
124
- metadata_filter=getattr(event.payload, "metadata_filter", None),
125
- )
126
- chat_id = None
127
- metadata_filter = None
128
130
 
129
- if isinstance(event, (ChatEvent | Event)):
130
- chat_id = event.payload.chat_id
131
- metadata_filter = event.payload.metadata_filter
131
+ return cls(
132
+ company_id=event.company_id,
133
+ user_id=event.user_id,
134
+ chat_id=payload.chat_id,
135
+ metadata_filter=payload.metadata_filter,
136
+ )
132
137
 
133
138
  return cls(
134
139
  company_id=event.company_id,
135
140
  user_id=event.user_id,
136
- chat_id=chat_id,
137
- metadata_filter=metadata_filter,
138
141
  )
139
142
 
140
143
  @classmethod
@@ -167,11 +170,41 @@ class ContentService:
167
170
  metadata_filter=metadata_filter,
168
171
  )
169
172
 
173
+ @classmethod
174
+ def from_context(
175
+ cls,
176
+ context: UniqueContext,
177
+ *,
178
+ metadata_filter: dict[str, Any] | None = None,
179
+ ) -> Self:
180
+ """Create a ContentService from a :class:`UniqueContext`.
181
+
182
+ When ``context.chat.parent_chat_id`` is set (subagent correlation),
183
+ content operations are scoped to the parent chat.
184
+ """
185
+ if context.chat is None:
186
+ msg = "ContentService requires chat context (context.chat is None)"
187
+ raise ValueError(msg)
188
+ chat = context.chat
189
+ effective_filter = (
190
+ metadata_filter if metadata_filter is not None else chat.metadata_filter
191
+ )
192
+ chat_id = (
193
+ chat.parent_chat_id if chat.parent_chat_id is not None else chat.chat_id
194
+ )
195
+ return cls(
196
+ company_id=context.auth.get_confidential_company_id(),
197
+ user_id=context.auth.get_confidential_user_id(),
198
+ chat_id=chat_id,
199
+ metadata_filter=effective_filter,
200
+ )
201
+
170
202
  @classmethod
171
203
  def from_settings(
172
204
  cls,
173
205
  settings: UniqueSettings | str | None = None,
174
206
  metadata_filter: dict[str, Any] | None = None,
207
+ **kwargs: Any,
175
208
  ):
176
209
  """
177
210
  Initialize the ContentService with a settings object and metadata filter.
@@ -182,6 +215,12 @@ class ContentService:
182
215
  elif isinstance(settings, str):
183
216
  settings = UniqueSettings.from_env_auto_with_sdk_init(filename=settings)
184
217
 
218
+ if settings.context.chat is not None:
219
+ return cls.from_context(
220
+ settings.context,
221
+ metadata_filter=metadata_filter,
222
+ )
223
+
185
224
  return cls(
186
225
  company_id=settings.auth.company_id.get_secret_value(),
187
226
  user_id=settings.auth.user_id.get_secret_value(),
@@ -192,7 +231,7 @@ class ContentService:
192
231
  @deprecated(
193
232
  "The event property is deprecated and will be removed in a future version."
194
233
  )
195
- def event(self) -> Event | BaseEvent[Any] | None:
234
+ def event(self) -> BaseEvent[Any] | None:
196
235
  """
197
236
  Get the event object (deprecated).
198
237
 
@@ -5,7 +5,7 @@ from pydantic import BaseModel
5
5
  from typing_extensions import deprecated
6
6
 
7
7
  from unique_toolkit._common.validate_required_values import validate_required_values
8
- from unique_toolkit.app.schemas import BaseEvent, ChatEvent, Event
8
+ from unique_toolkit.app.schemas import AssistantWebhookEvent, BaseEvent
9
9
  from unique_toolkit.app.unique_settings import UniqueSettings
10
10
  from unique_toolkit.content.schemas import ContentChunk
11
11
  from unique_toolkit.language_model.constants import (
@@ -40,7 +40,7 @@ class LanguageModelService:
40
40
  "Use __init__ with company_id and user_id instead or use the classmethod `from_event`"
41
41
  )
42
42
  @overload
43
- def __init__(self, event: Event | ChatEvent | BaseEvent[Any]): ...
43
+ def __init__(self, event: BaseEvent[Any]): ...
44
44
 
45
45
  """
46
46
  Initialize the LanguageModelService with an event (deprecated)
@@ -55,30 +55,30 @@ class LanguageModelService:
55
55
 
56
56
  def __init__(
57
57
  self,
58
- event: Event | ChatEvent | BaseEvent[Any] | None = None,
58
+ event: BaseEvent[Any] | None = None,
59
59
  company_id: str | None = None,
60
60
  user_id: str | None = None,
61
61
  **kwargs: dict[str, Any], # only here for backward compatibility
62
62
  ):
63
- if isinstance(event, (ChatEvent, Event)):
63
+ if event is not None:
64
+ auth_scoped = type(self).from_event(event)
65
+ self._company_id = auth_scoped._company_id
66
+ self._user_id = auth_scoped._user_id
64
67
  self._event = event
65
- self._chat_id = event.payload.chat_id
66
- self._assistant_id = event.payload.assistant_id
67
- self._company_id = event.company_id
68
- self._user_id = event.user_id
69
- elif isinstance(event, BaseEvent):
70
- self._event = event
71
- self._company_id = event.company_id
72
- self._user_id = event.user_id
73
68
  self._chat_id = None
74
69
  self._assistant_id = None
75
- else:
76
- [company_id, user_id] = validate_required_values([company_id, user_id])
77
- self._event = None
78
- self._company_id: str = company_id
79
- self._user_id: str = user_id
80
- self._chat_id: str | None = None
81
- self._assistant_id: str | None = None
70
+ if isinstance(event, AssistantWebhookEvent):
71
+ payload = event.payload
72
+ self._chat_id = payload.chat_id
73
+ self._assistant_id = payload.assistant_id
74
+ return
75
+
76
+ [company_id, user_id] = validate_required_values([company_id, user_id])
77
+ self._event = None
78
+ self._company_id: str = company_id
79
+ self._user_id: str = user_id
80
+ self._chat_id: str | None = None
81
+ self._assistant_id: str | None = None
82
82
 
83
83
  @classmethod
84
84
  def from_event(cls, event: BaseEvent[Any]):
@@ -88,7 +88,7 @@ class LanguageModelService:
88
88
  return cls(company_id=event.company_id, user_id=event.user_id)
89
89
 
90
90
  @classmethod
91
- def from_settings(cls, settings: UniqueSettings | str | None = None):
91
+ def from_settings(cls, settings: UniqueSettings | str | None = None, **kwargs: Any):
92
92
  """
93
93
  Initialize the LanguageModelService with a settings object.
94
94
  """
@@ -106,7 +106,7 @@ class LanguageModelService:
106
106
  @deprecated(
107
107
  "The event property is deprecated and will be removed in a future version."
108
108
  )
109
- def event(self) -> Event | BaseEvent[Any] | None:
109
+ def event(self) -> BaseEvent[Any] | None:
110
110
  """
111
111
  Get the event object (deprecated).
112
112
 
@@ -138,7 +138,8 @@ class UniqueServiceFactory:
138
138
  def register_known_services(cls) -> None:
139
139
  """Register the stable toolkit services with the factory.
140
140
 
141
- Currently registers :class:`KnowledgeBaseService` and :class:`ChatService`.
141
+ Currently registers :class:`KnowledgeBaseService`, :class:`ChatService`,
142
+ :class:`ContentService`, and :class:`LanguageModelService`.
142
143
  :class:`ChunkRelevancySorter` is intentionally excluded to avoid circular
143
144
  imports at package init time — use the ``chunk_relevancy_sorter()``
144
145
  convenience method instead.
@@ -149,10 +150,17 @@ class UniqueServiceFactory:
149
150
  example, :meth:`Identity.from_settings`) so that the experimental
150
151
  dependency is visible at every call site.
151
152
  """
153
+ from unique_toolkit.content.service import ContentService
154
+ from unique_toolkit.language_model.service import LanguageModelService
152
155
  from unique_toolkit.services.chat_service import ChatService
153
156
  from unique_toolkit.services.knowledge_base import KnowledgeBaseService
154
157
 
155
- for service_class in [KnowledgeBaseService, ChatService]:
158
+ for service_class in [
159
+ KnowledgeBaseService,
160
+ ChatService,
161
+ ContentService,
162
+ LanguageModelService,
163
+ ]:
156
164
  if service_class.__name__ not in cls._registry:
157
165
  cls.register(service_class=service_class)
158
166
 
@@ -16,7 +16,7 @@ from unique_toolkit._common.metadata_filter_scope import (
16
16
  merge_scope_clause_into_metadata_filter,
17
17
  )
18
18
  from unique_toolkit._common.validate_required_values import validate_required_values
19
- from unique_toolkit.app.schemas import BaseEvent, ChatEvent, Event
19
+ from unique_toolkit.app.schemas import AssistantWebhookEvent, BaseEvent
20
20
  from unique_toolkit.app.unique_settings import UniqueSettings
21
21
  from unique_toolkit.content.constants import (
22
22
  DEFAULT_SEARCH_LANGUAGE,
@@ -93,7 +93,7 @@ class KnowledgeBaseService:
93
93
  """
94
94
  metadata_filter = None
95
95
 
96
- if isinstance(event, (ChatEvent | Event)):
96
+ if isinstance(event, AssistantWebhookEvent):
97
97
  metadata_filter = event.payload.metadata_filter
98
98
 
99
99
  return cls(
@@ -52,23 +52,33 @@ class ShortTermMemoryService:
52
52
  chat_id: str | None = None,
53
53
  message_id: str | None = None,
54
54
  ):
55
- self._event = event
56
- if event:
57
- self._company_id: str = event.company_id
58
- self._user_id: str = event.user_id
59
- if isinstance(event, (ChatEvent, Event)):
60
- self._chat_id = event.payload.chat_id
61
- self._message_id = event.payload.user_message.id
62
- else:
63
- [company_id, user_id] = validate_required_values([company_id, user_id])
64
-
65
- if not (chat_id or message_id):
66
- raise ValueError("Chat_id or message_id must be provided")
67
-
68
- self._company_id = company_id
69
- self._user_id = user_id
70
- self._chat_id = chat_id
71
- self._message_id = message_id
55
+ if event is not None:
56
+ if isinstance(event, ChatEvent):
57
+ delegated = type(self).from_event(event)
58
+ self._event = event
59
+ self._company_id = delegated._company_id
60
+ self._user_id = delegated._user_id
61
+ self._chat_id = delegated._chat_id
62
+ self._message_id = delegated._message_id
63
+ return
64
+
65
+ self._event = event
66
+ self._company_id = event.company_id
67
+ self._user_id = event.user_id
68
+ self._chat_id = None
69
+ self._message_id = None
70
+ return
71
+
72
+ self._event = None
73
+ [company_id, user_id] = validate_required_values([company_id, user_id])
74
+
75
+ if not (chat_id or message_id):
76
+ raise ValueError("Chat_id or message_id must be provided")
77
+
78
+ self._company_id = company_id
79
+ self._user_id = user_id
80
+ self._chat_id = chat_id
81
+ self._message_id = message_id
72
82
 
73
83
  @classmethod
74
84
  def from_event(cls, event: ChatEvent):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.28.0.dev6
3
+ Version: 2026.28.0.dev8
4
4
  Summary:
5
5
  Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
6
6
  Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
@@ -16,7 +16,7 @@ Requires-Dist: sseclient>=0.0.27,<0.0.28
16
16
  Requires-Dist: openai>=1.105.0,<3
17
17
  Requires-Dist: platformdirs>=4.0.0,<5
18
18
  Requires-Dist: pillow>=12.1.1
19
- Requires-Dist: unique-sdk>=2026.28.0.dev8,<2026.28.0rc0
19
+ Requires-Dist: unique-sdk>=2026.28.0.dev11,<2026.28.0rc0
20
20
  Requires-Dist: jambo>=0.1.2,<0.2
21
21
  Requires-Dist: jinja2>=3.1.6,<4
22
22
  Requires-Dist: docxtpl>=0.20.1,<0.21
@@ -7,7 +7,7 @@ unique_toolkit/_common/base_model_type_attribute.py,sha256=3t2InDT3nxG9YMEg0M90_
7
7
  unique_toolkit/_common/chunk_relevancy_sorter/config.py,sha256=_9ACfqI-69DzN2azdt9dUhBRvS6NbTjqqNfm77Qn_5M,1889
8
8
  unique_toolkit/_common/chunk_relevancy_sorter/exception.py,sha256=1mY4zjbvnXsd5oIxwiVsma09bS2XRnHrxW8KJBGtgCM,126
9
9
  unique_toolkit/_common/chunk_relevancy_sorter/schemas.py,sha256=Cg9vNDPmkn5V_j6ssV58ADcEaDURNP5TeUa1UmRtdHw,1343
10
- unique_toolkit/_common/chunk_relevancy_sorter/service.py,sha256=AmowH3VA3PC1MmSto6x5HO3GUlM8deT2BNFnkPQcB_M,14770
10
+ unique_toolkit/_common/chunk_relevancy_sorter/service.py,sha256=pCwRJRdCqXK_2a7kAm3K9VZRCjaLntek8OtoCyRpHQE,14757
11
11
  unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py,sha256=giD9b5W8A0xP18dZCcrLUruoGi38BeBvPnO1phY7Sp0,8892
12
12
  unique_toolkit/_common/config_checker/README.md,sha256=4WXBP5DIyEwklSi8RkVpKYYUPo_qOvm_8zEAz9WsdcU,11564
13
13
  unique_toolkit/_common/config_checker/__init__.py,sha256=iGtDFILDQrjSpxC_hNut_H6hQxQ_exDp6GQrgMSdn0U,1204
@@ -90,7 +90,7 @@ unique_toolkit/agentic/evaluation/context_relevancy/prompts/__init__.py,sha256=I
90
90
  unique_toolkit/agentic/evaluation/context_relevancy/prompts/system_prompt.j2,sha256=PFT9Y7QQ85h-ronwK8sq6zJJrxfrZhvZkYHXspMdDHU,985
91
91
  unique_toolkit/agentic/evaluation/context_relevancy/prompts/user_prompt.j2,sha256=9HXm0Qg0xAPIE5mnaUiHrscolD_qO7f83qlIjXiP1n4,150
92
92
  unique_toolkit/agentic/evaluation/context_relevancy/schema.py,sha256=lZd0TPzH43ifgWWGg3WO6b1AQX8aK2R9y51yH0d1DHM,2919
93
- unique_toolkit/agentic/evaluation/context_relevancy/service.py,sha256=i6WA_BwHyhHDfnvK59UC0z1FgZ8EPW5OJyYLbSxNRic,8760
93
+ unique_toolkit/agentic/evaluation/context_relevancy/service.py,sha256=OYr5_uQmovFtvsXVItqDw1VUW5rP-LED9XElx-kh5MM,8747
94
94
  unique_toolkit/agentic/evaluation/evaluation_manager.py,sha256=4HeKsDg6tLkJXNw3kzjmVLbD-86IRzKLQSuPwYtfYBk,7992
95
95
  unique_toolkit/agentic/evaluation/exception.py,sha256=7lcVbCyoN4Md1chNJDFxpUYyWbVrcr9dcc3TxWykJTc,115
96
96
  unique_toolkit/agentic/evaluation/hallucination/constants.py,sha256=uEQR-CNKmeXizDaLOLHhwTZ0iYUmFwoKeMLHPm4M0sY,3208
@@ -187,7 +187,7 @@ unique_toolkit/agentic/tools/experimental/open_file_tool/runtime.py,sha256=SOIFV
187
187
  unique_toolkit/agentic/tools/experimental/open_file_tool/tool.py,sha256=BXsEeyJ1Njswl_PChfO-vVz2MzFzmDcN2Few9WBWdXU,3774
188
188
  unique_toolkit/agentic/tools/experimental/retrieve_search_scope_tool/__init__.py,sha256=kD6BaQWgOWk8fPYVBuCUnfHWcJNuH4KjWAreORAkfWI,357
189
189
  unique_toolkit/agentic/tools/experimental/retrieve_search_scope_tool/config.py,sha256=NI-dk1Sai-t32PD5AUIyeGguPeV-qZhn4F8-nIHXAhA,4271
190
- unique_toolkit/agentic/tools/experimental/retrieve_search_scope_tool/tool.py,sha256=Egxrq_9TVNOaCvBtEuLsaolOdeGyxaJxYU6ibOMfC2A,7514
190
+ unique_toolkit/agentic/tools/experimental/retrieve_search_scope_tool/tool.py,sha256=4r9nX6iFvmYtnV1VnsWaINcnTLvU665ha3pgQSquh8U,7513
191
191
  unique_toolkit/agentic/tools/experimental/todo/__init__.py,sha256=QzaC8tqBFrjfZbYSxvWJkWyFYel1ejrbx8IN63d0YCo,583
192
192
  unique_toolkit/agentic/tools/experimental/todo/config.py,sha256=52GamaMU6VrCd__JgyRoJfEL_Dp0y8CPf3jwYCkXVBg,3506
193
193
  unique_toolkit/agentic/tools/experimental/todo/prompts/__init__.py,sha256=DlBk0Ex5w41XtcmnLdGFK9r4mVfBHweFwc8dZB5tXVI,379
@@ -214,11 +214,12 @@ unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/gene
214
214
  unique_toolkit/agentic/tools/openai_builtin/code_interpreter/schemas.py,sha256=W7egCmYrPrzic0aXsu9wY5fvQ8y_cl4Tzq_cjVs0mZs,542
215
215
  unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py,sha256=k10VBHL2vVlEylNF3EWGvm8_wZ_44K1pJ5ngGirUzOA,14275
216
216
  unique_toolkit/agentic/tools/openai_builtin/manager.py,sha256=CkfMRsjrRDv_X-zfJHZnxxOdpCK1qzf6RGGxKtUHp9k,3519
217
+ unique_toolkit/agentic/tools/run_context.py,sha256=lk9tS1wn97NcSByNZpJWunnnZTos9H2SsxUkKKAvW_A,783
217
218
  unique_toolkit/agentic/tools/schemas.py,sha256=4BTJ0q7fiAVQ2Uy9kVJnS0rcKeKKaYTkOFq8XGZwMJ0,5291
218
219
  unique_toolkit/agentic/tools/test/test_mcp_manager.py,sha256=uWrTmnenQ70s5hrTfMYVah2vDT-jZq4nQmvhutypEK8,19195
219
220
  unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py,sha256=XHNezB8itj9KzpQgD0cwRtp2AgRUfUkQsHc3bTyPj6c,15801
220
- unique_toolkit/agentic/tools/tool.py,sha256=l92-yE8zgwYoaZGm3VmgKCw6CfyFeMiiWxPHqgRjNB4,9176
221
- unique_toolkit/agentic/tools/tool_manager.py,sha256=lsFfKPVJgJwlDc1YE80o0z6Ktfo13us4OhO2m9EZikc,23355
221
+ unique_toolkit/agentic/tools/tool.py,sha256=jDGXRD9H0aagibLARWjJaX8DGMFjICeCojgONs40ghY,10721
222
+ unique_toolkit/agentic/tools/tool_manager.py,sha256=FNf_Yfi-Nt8YfGA3A_ZHUStzSuIJiY_rSdRJLVGu6z8,27719
222
223
  unique_toolkit/agentic/tools/tool_progress_reporter.py,sha256=GaR0oqDUJZvBB9WCUVYYh0Zvs6U-LMygJCCrqPlitgA,10296
223
224
  unique_toolkit/agentic/tools/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
224
225
  unique_toolkit/agentic/tools/utils/source_handling/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -226,8 +227,9 @@ unique_toolkit/agentic/tools/utils/source_handling/schema.py,sha256=iHBKuks6tUy8
226
227
  unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py,sha256=uZ0QXqrPWgId3ZA67dvjHQ6xrW491LK1xxx_sVJmFHg,9160
227
228
  unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py,sha256=EA8iVvb3L91OFk2XMbGcFuhe2etqm3Sx9QCYDGiOSOM,6995
228
229
  unique_toolkit/agentic_table/__init__.py,sha256=bTMTBUomZysXhMtP9Rnzk6hVa2lZ7Xmrn-THZzE8uWo,1737
229
- unique_toolkit/agentic_table/schemas.py,sha256=CClyrQnI847iVBbhz1LDzTyiR39Qq2jP7z-FR4qjCU8,10988
230
+ unique_toolkit/agentic_table/schemas.py,sha256=IhVVSWZyDVIeyVoiP-FWp26MWwXv8oL4KFPTzeQquCc,12568
230
231
  unique_toolkit/agentic_table/service.py,sha256=26hblR33Vp8FWWMgtaQCJ0IQT8hmGzGCQy86BDi2wOo,19670
232
+ unique_toolkit/agentic_table/synthetic_chat_event.py,sha256=MylvYuhx5CVt2DbPetzV0sR8GlkL218DmNFi7A6RYVs,2373
231
233
  unique_toolkit/app/__init__.py,sha256=VnyvvCugDYZzEK93vVpSL1zHGduuev6yOvLJOGzpmMc,1568
232
234
  unique_toolkit/app/chat_event_filter_options_settings.py,sha256=W0Ydio1UeED5_xqaTzbcGzK2tH4EpA1WHUzdF-iWpqI,364
233
235
  unique_toolkit/app/dev_util.py,sha256=cHDuC6jtaptdVISbtMmOCqRTTWBPeCoIUQvUQvcmVyY,5517
@@ -238,15 +240,15 @@ unique_toolkit/app/init_logging.py,sha256=Lv0FuEiUmxvtV2yovjD0gWNIvLdsi2xdMFeEiN
238
240
  unique_toolkit/app/init_sdk.py,sha256=5_oDoETr6akwYyBCb0ivTdMNu3SVgPSkrXcDS6ELyY8,2269
239
241
  unique_toolkit/app/performance/async_tasks.py,sha256=8Xv5kEmmFDJO7YyyBxv4e7bcNgq-3z_E2_4z1Yn9EqU,1970
240
242
  unique_toolkit/app/performance/async_wrapper.py,sha256=yVVcRDkcdyfjsxro-N29SBvi-7773wnfDplef6-y8xw,1077
241
- unique_toolkit/app/schemas.py,sha256=f5h9BqCuqHvrXUAB7tjrx0PSLmuJJKlZ89othqOsuSY,14762
242
- unique_toolkit/app/unique_settings.py,sha256=XrRcKobm0DHFIFJvScEeRVBDmRotBZTz9qctg34FbY8,23364
243
+ unique_toolkit/app/schemas.py,sha256=NWaj7cpNiXaxU6W-U-bw911cdpXjqsbSUGeVoN4Q3Js,16534
244
+ unique_toolkit/app/unique_settings.py,sha256=nN_5PxBepHXUSd3yL2r7Ex1cVUf8r79uvRk2qa9exh0,25079
243
245
  unique_toolkit/app/verification.py,sha256=GxFFwcJMy25fCA_Xe89wKW7bgqOu8PAs5y8QpHF0GSc,3861
244
246
  unique_toolkit/app/webhook.py,sha256=evpxLDnvz7P3D7-TV1XBkl8WcoldXpjOkw3tvFRxHr0,2368
245
247
  unique_toolkit/chat/__init__.py,sha256=N9RubtxWN7UF0DlxP7YgeNTgjjk6p7AwkbGaVvuZCv0,938
246
248
  unique_toolkit/chat/cancellation.py,sha256=HvGZK-daCMSr0M0AnvMY3EBPH1N_UqQPfrbTjwr8owk,5645
247
249
  unique_toolkit/chat/constants.py,sha256=05kq6zjqUVB2d6_P7s-90nbljpB3ryxwCI-CAz0r2O4,83
248
250
  unique_toolkit/chat/deprecated/service.py,sha256=8_XoopUWHT396Z_5gwMmsI6v4NGVgQirSJCYUW4Gx8c,9339
249
- unique_toolkit/chat/functions.py,sha256=vj0sdeBVm-vsrZOwsHAfCUtpdTwQNgr-aVayvRhUCp4,52360
251
+ unique_toolkit/chat/functions.py,sha256=8PXqo4X2BJhsbHkD9X5Ykqz-iVqbRJ1UlWuCNl1XSX8,52448
250
252
  unique_toolkit/chat/rendering.py,sha256=c8YiV9oADRrJQ5A_QBJ4_UFc0NZ-2vVaa7tupoMusso,880
251
253
  unique_toolkit/chat/responses_api.py,sha256=QtCOe3TI242N1JChhoFikxUCvGY6xfTvSmaE9Ta8HSM,19811
252
254
  unique_toolkit/chat/schemas.py,sha256=l-Epugu27RoajiX5OkFO-BrVNey-wfTmaXiEmNNmG7s,7396
@@ -256,8 +258,8 @@ unique_toolkit/chat/utils.py,sha256=CsD9IER2gA1IOV0fmUOl-N0LmKvA7JP9dGrOqeOY0Gg,
256
258
  unique_toolkit/content/__init__.py,sha256=nnQDfnNTEvI8-3dgWvIojoFDYU7edwd8Kloi6lMDG0M,1586
257
259
  unique_toolkit/content/constants.py,sha256=1iy4Y67xobl5VTnJB6SxSyuoBWbdLl9244xfVMUZi5o,60
258
260
  unique_toolkit/content/functions.py,sha256=avnlEQ1FkVz2NTbXUkafU04jzygqOJtUMQL605rEL4c,34019
259
- unique_toolkit/content/schemas.py,sha256=00qbe-DlUjqlf-n3QVOotZjpsj0LZNnUoPmdwyMjVHY,10667
260
- unique_toolkit/content/service.py,sha256=fR93_76aPczx2voxR7cvYDs_-pxD-Mc9WP6-5IqA8ko,28301
261
+ unique_toolkit/content/schemas.py,sha256=AmnlqMpA_JgbgsaZF6Xdy8kt9lcY0YCqlfqpG1tYWeE,11306
262
+ unique_toolkit/content/service.py,sha256=S_sFtG1hJd-zz7XyP-6Gu-B8_Xx-r5v46qZLWdRRyRs,29477
261
263
  unique_toolkit/content/smart_rules.py,sha256=9GlHXm63POTi4jy5hW0qUjeqy-JSeL1wIbURM1GKhc4,11481
262
264
  unique_toolkit/content/utils.py,sha256=keDkqu_ZzQY0y_g7g_XLOza3p4XsvfqVVDI9ylM91KY,10115
263
265
  unique_toolkit/data_extraction/README.md,sha256=5KxzqPHC29wxBiWJ-87wBQisSgRBkuSnPTb38pGH1wA,3138
@@ -383,7 +385,7 @@ unique_toolkit/language_model/infos.py,sha256=V3gKj4DyZwapbLAzDBPjw2D8LlWY9UaSAP
383
385
  unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
384
386
  unique_toolkit/language_model/reference.py,sha256=T3wr2jP_uRg5wKVIbKqLbxJlcgxfbZQGq3XdjbQ2kSo,6984
385
387
  unique_toolkit/language_model/schemas.py,sha256=JZbIT3w1cj5fKrYaQ55X7sjXmCo6i_GqgJ198V3RXSg,29474
386
- unique_toolkit/language_model/service.py,sha256=mzHYppmbFxXRHzsmQ4i_z9N4fheIEjomJeuGaEOFs9Y,12025
388
+ unique_toolkit/language_model/service.py,sha256=hDoJBBgG_wWs8WuC_c8tnmK8cNu8o5EhJJXRvgwhkFQ,11956
387
389
  unique_toolkit/language_model/settings.py,sha256=xkoMv3ATb2ft5V3MA8JvqW-EO8AnZ5t-6rvdbTx6lFU,1490
388
390
  unique_toolkit/language_model/stream_transform.py,sha256=iNNGpLY_7G6Bpp9Y4KtbIkQm3WB4yZt75BXcqx0DVkI,2775
389
391
  unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
@@ -394,16 +396,16 @@ unique_toolkit/monitoring/registry.py,sha256=8r9jqaQoRHZveGRZe_GQvBzvQpmUV4cpw8E
394
396
  unique_toolkit/protocols/support.py,sha256=x5edzbBmKAUuvm3oLFh52gkgQsqR5LU7M0t8RHK0DZk,5161
395
397
  unique_toolkit/services/__init__.py,sha256=b9Wl7Wvz1JmZDbDexIZzBoXZ0wscXuczGAH8LoL-Qak,572
396
398
  unique_toolkit/services/chat_service.py,sha256=mh9lZr_AiNR0JSdiEU4kh2vkh5YQG4tASlQIRpeeEQI,95567
397
- unique_toolkit/services/factory.py,sha256=tZo49glnXyW0wc76PXG_c95p9SE2DL9V1UwrcaVXFPo,9525
398
- unique_toolkit/services/knowledge_base.py,sha256=wuH0KYLHpvpyq-ZCYleAe19AobMSYgd5I60jLNq-mwo,43439
399
+ unique_toolkit/services/factory.py,sha256=7gNeanyHkgX23Iz7RMCQTCkE9gI5asCI11nXxM0jjx4,9832
400
+ unique_toolkit/services/knowledge_base.py,sha256=a9itNRxIrGDIuXkLiTLBsA-m2ncy8F8DwCLCXtWm_GY,43446
399
401
  unique_toolkit/short_term_memory/__init__.py,sha256=2mI3AUrffgH7Yt-xS57EGqnHf7jnn6xquoKEhJqk3Wg,185
400
402
  unique_toolkit/short_term_memory/constants.py,sha256=698CL6-wjup2MvU19RxSmQk3gX7aqW_OOpZB7sbz_Xg,34
401
403
  unique_toolkit/short_term_memory/functions.py,sha256=mbEeWkJ5Jr0cvg1_m6l9aS1L6nWE5ZhviNzpKlCs-a8,7907
402
404
  unique_toolkit/short_term_memory/schemas.py,sha256=C6gopsnuIgWJBG7XF87gxMwzLJtqbSGmsKXUf2h1sn4,1666
403
- unique_toolkit/short_term_memory/service.py,sha256=Qo-c9yxzC1ZEEHJyIG_vnRs_ef0SKbjCAm-iH_JeJCk,8979
405
+ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74xZ1hckAzj97n4M,9264
404
406
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
405
407
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
406
408
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
407
- unique_toolkit-2026.28.0.dev6.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
408
- unique_toolkit-2026.28.0.dev6.dist-info/METADATA,sha256=hoO0ZUgIV4theG_01eSZ0nk_8iU5vBqbxVk5St6xIW0,7426
409
- unique_toolkit-2026.28.0.dev6.dist-info/RECORD,,
409
+ unique_toolkit-2026.28.0.dev8.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
410
+ unique_toolkit-2026.28.0.dev8.dist-info/METADATA,sha256=v9-siqdK8nGyMVGx7bXMB2dX4Z3RTGeS7KZZd4v069U,7427
411
+ unique_toolkit-2026.28.0.dev8.dist-info/RECORD,,