unique_toolkit 2026.36.0.dev0__py3-none-any.whl → 2026.36.0.dev2__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.
@@ -4,9 +4,10 @@ import json
4
4
  import logging
5
5
  import mimetypes
6
6
  import uuid
7
- from typing import Any, Dict
7
+ from typing import Any
8
8
 
9
9
  import unique_sdk
10
+ from pydantic import ValidationError
10
11
 
11
12
  from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
12
13
  from unique_toolkit.agentic.feature_flags import FeatureFlagNames
@@ -21,6 +22,7 @@ from unique_toolkit.app.schemas import ChatEvent, McpServer, McpTool
21
22
  from unique_toolkit.chat.schemas import MessageLog, MessageLogStatus
22
23
  from unique_toolkit.chat.service import ChatService
23
24
  from unique_toolkit.content.functions import upload_content_from_bytes
25
+ from unique_toolkit.content.schemas import ContentChunk, ContentReference
24
26
  from unique_toolkit.experimental.resources.feature_flags import is_flag_enabled
25
27
  from unique_toolkit.language_model import LanguageModelService
26
28
  from unique_toolkit.language_model.schemas import (
@@ -30,6 +32,8 @@ from unique_toolkit.language_model.schemas import (
30
32
 
31
33
  logger = logging.getLogger(__name__)
32
34
 
35
+ REFERENCE_META_KEY = "unique.app/reference"
36
+
33
37
 
34
38
  class MCPToolWrapper(Tool[MCPToolConfig]):
35
39
  """Wrapper class for MCP tools that implements the Tool interface"""
@@ -72,7 +76,7 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
72
76
  parameters=self._mcp_tool.input_schema,
73
77
  )
74
78
 
75
- def _json_schema_to_python_type(self, schema: Dict[str, Any]) -> type:
79
+ def _json_schema_to_python_type(self, schema: dict[str, Any]) -> type:
76
80
  """Convert JSON schema type to Python type"""
77
81
  json_type = schema.get("type", "string")
78
82
 
@@ -152,9 +156,14 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
152
156
  # Use SDK to call the public API
153
157
  result = await self._call_mcp_tool_via_sdk(arguments)
154
158
 
159
+ content_chunks, references = self._extract_structured_references(result)
155
160
  content_str, image_data_urls = await asyncio.to_thread(
156
161
  self._process_mcp_result, result
157
162
  )
163
+ if content_chunks:
164
+ # Structured references already carry the text; keep any
165
+ # extracted images but drop the redundant flat text.
166
+ content_str = ""
158
167
 
159
168
  # TODO: Why result here not applied directly to the body of the tool_response? like so how does it know the results in the history?
160
169
  tool_response = ToolCallResponse(
@@ -167,6 +176,7 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
167
176
  },
168
177
  error_message="",
169
178
  content=content_str,
179
+ content_chunks=content_chunks or None,
170
180
  image_data_urls=image_data_urls,
171
181
  )
172
182
 
@@ -187,6 +197,7 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
187
197
  progress_message="_Completed MCP tool_",
188
198
  status=MessageLogStatus.COMPLETED,
189
199
  active_message_log=active_message_log,
200
+ references=references,
190
201
  )
191
202
 
192
203
  return tool_response
@@ -230,17 +241,19 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
230
241
  progress_message: str | None = None,
231
242
  status: MessageLogStatus = MessageLogStatus.RUNNING,
232
243
  active_message_log: MessageLog | None = None,
244
+ references: list[ContentReference] | None = None,
233
245
  ) -> MessageLog | None:
234
246
  return self._message_step_logger.create_or_update_message_log(
235
247
  active_message_log=active_message_log,
236
248
  header=self.display_name(),
237
249
  progress_message=progress_message,
238
250
  status=status,
251
+ references=references or [],
239
252
  )
240
253
 
241
254
  def _extract_and_validate_arguments(
242
255
  self, tool_call: LanguageModelFunction
243
- ) -> Dict[str, Any]:
256
+ ) -> dict[str, Any]:
244
257
  """
245
258
  Extract and validate arguments from tool call, handling various formats robustly.
246
259
 
@@ -287,7 +300,39 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
287
300
  f"Unexpected arguments type for MCP tool {self.name}: {type(raw_arguments)}"
288
301
  )
289
302
 
290
- def _process_mcp_result(self, result: Dict[str, Any]) -> tuple[str, list[str]]:
303
+ def _extract_structured_references(
304
+ self, result: dict[str, Any]
305
+ ) -> tuple[list[ContentChunk], list[ContentReference]]:
306
+ """Opt-in: content items with valid ``unique.app/reference`` + chunk ``_meta``."""
307
+ chunks: list[ContentChunk] = []
308
+ references: list[ContentReference] = []
309
+ content_items = self._extract_content_items(result)
310
+ for item in content_items:
311
+ if not isinstance(item, dict) or item.get("type") != "text":
312
+ continue
313
+ meta = item.get("_meta")
314
+ if not isinstance(meta, dict):
315
+ continue
316
+ ref_payload = meta.get(REFERENCE_META_KEY)
317
+ if not isinstance(ref_payload, dict):
318
+ continue
319
+ try:
320
+ reference = ContentReference.model_validate(ref_payload)
321
+ chunk = ContentChunk.model_validate(
322
+ {k: v for k, v in meta.items() if k != REFERENCE_META_KEY}
323
+ )
324
+ except ValidationError:
325
+ self.logger.warning(
326
+ "MCP tool %s: ignoring invalid %s metadata",
327
+ self.name,
328
+ REFERENCE_META_KEY,
329
+ )
330
+ continue
331
+ references.append(reference)
332
+ chunks.append(chunk)
333
+ return chunks, references
334
+
335
+ def _process_mcp_result(self, result: dict[str, Any]) -> tuple[str, list[str]]:
291
336
  """Parse MCP result content array, upload images, return content string and data URLs."""
292
337
  content_items = self._extract_content_items(result)
293
338
  if not content_items:
@@ -327,7 +372,7 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
327
372
  content_str = json.dumps(result)
328
373
  return content_str, image_data_urls
329
374
 
330
- def _extract_content_items(self, result: Dict[str, Any]) -> list[Any]:
375
+ def _extract_content_items(self, result: dict[str, Any]) -> list[Any]:
331
376
  """Extract content array from MCP result (handles various response shapes)."""
332
377
  if "content" in result and isinstance(result["content"], list):
333
378
  return result["content"]
@@ -378,7 +423,7 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
378
423
 
379
424
  return data_url, content_id
380
425
 
381
- async def _call_mcp_tool_via_sdk(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
426
+ async def _call_mcp_tool_via_sdk(self, arguments: dict[str, Any]) -> dict[str, Any]:
382
427
  """Call MCP tool via SDK to public API"""
383
428
  try:
384
429
  self.logger.info(
@@ -46,12 +46,15 @@ logger = logging.getLogger(f"toolkit.language_model.{__name__}")
46
46
  def _completion_headers(
47
47
  chat_id: str | None,
48
48
  assistant_id: str | None,
49
+ assistant_message_id: str | None = None,
49
50
  ) -> dict[str, str] | None:
50
51
  headers: dict[str, str] = {}
51
52
  if chat_id:
52
53
  headers["x-chat-id"] = chat_id
53
54
  if assistant_id:
54
55
  headers["x-assistant-id"] = assistant_id
56
+ if assistant_message_id:
57
+ headers["x-assistant-message-id"] = assistant_message_id
55
58
  return headers or None
56
59
 
57
60
 
@@ -68,6 +71,7 @@ def complete(
68
71
  user_id: str | None = None,
69
72
  chat_id: str | None = None,
70
73
  assistant_id: str | None = None,
74
+ assistant_message_id: str | None = None,
71
75
  ) -> LanguageModelResponse:
72
76
  """Call the completion endpoint synchronously without streaming the response.
73
77
 
@@ -103,7 +107,7 @@ def complete(
103
107
  )
104
108
 
105
109
  try:
106
- headers = _completion_headers(chat_id, assistant_id)
110
+ headers = _completion_headers(chat_id, assistant_id, assistant_message_id)
107
111
  if headers is None:
108
112
  response = unique_sdk.ChatCompletion.create(
109
113
  company_id=company_id,
@@ -142,6 +146,7 @@ async def complete_async(
142
146
  structured_output_enforce_schema: bool = False,
143
147
  chat_id: str | None = None,
144
148
  assistant_id: str | None = None,
149
+ assistant_message_id: str | None = None,
145
150
  ) -> LanguageModelResponse:
146
151
  """Call the completion endpoint asynchronously without streaming the response.
147
152
 
@@ -186,7 +191,7 @@ async def complete_async(
186
191
  )
187
192
 
188
193
  try:
189
- headers = _completion_headers(chat_id, assistant_id)
194
+ headers = _completion_headers(chat_id, assistant_id, assistant_message_id)
190
195
  if headers is None:
191
196
  response = await unique_sdk.ChatCompletion.create_async(
192
197
  company_id=company_id,
@@ -502,6 +507,7 @@ def complete_with_references(
502
507
  other_options: dict[str, Any] | None = None,
503
508
  chat_id: str | None = None,
504
509
  assistant_id: str | None = None,
510
+ assistant_message_id: str | None = None,
505
511
  ) -> LanguageModelStreamResponse:
506
512
  # Use toolkit language model functions for chat completion
507
513
 
@@ -516,6 +522,7 @@ def complete_with_references(
516
522
  other_options=other_options,
517
523
  chat_id=chat_id,
518
524
  assistant_id=assistant_id,
525
+ assistant_message_id=assistant_message_id,
519
526
  )
520
527
 
521
528
  return _create_language_model_stream_response_with_references(
@@ -539,6 +546,7 @@ async def complete_with_references_async(
539
546
  other_options: dict[str, Any] | None = None,
540
547
  chat_id: str | None = None,
541
548
  assistant_id: str | None = None,
549
+ assistant_message_id: str | None = None,
542
550
  ) -> LanguageModelStreamResponse:
543
551
  # Use toolkit language model functions for chat completion
544
552
  response = await complete_async(
@@ -552,6 +560,7 @@ async def complete_with_references_async(
552
560
  other_options=other_options,
553
561
  chat_id=chat_id,
554
562
  assistant_id=assistant_id,
563
+ assistant_message_id=assistant_message_id,
555
564
  )
556
565
 
557
566
  return _create_language_model_stream_response_with_references(
@@ -5,7 +5,11 @@ 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 AssistantWebhookEvent, BaseEvent
8
+ from unique_toolkit.app.schemas import (
9
+ AssistantWebhookEvent,
10
+ BaseEvent,
11
+ ChatEventPayload,
12
+ )
9
13
  from unique_toolkit.app.unique_settings import UniqueSettings
10
14
  from unique_toolkit.content.schemas import ContentChunk
11
15
  from unique_toolkit.language_model.constants import (
@@ -67,10 +71,13 @@ class LanguageModelService:
67
71
  self._event = event
68
72
  self._chat_id = None
69
73
  self._assistant_id = None
74
+ self._assistant_message_id = None
70
75
  if isinstance(event, AssistantWebhookEvent):
71
76
  payload = event.payload
72
77
  self._chat_id = payload.chat_id
73
78
  self._assistant_id = payload.assistant_id
79
+ if isinstance(payload, ChatEventPayload):
80
+ self._assistant_message_id = payload.assistant_message.id
74
81
  return
75
82
 
76
83
  [company_id, user_id] = validate_required_values([company_id, user_id])
@@ -79,6 +86,7 @@ class LanguageModelService:
79
86
  self._user_id: str = user_id
80
87
  self._chat_id: str | None = None
81
88
  self._assistant_id: str | None = None
89
+ self._assistant_message_id: str | None = None
82
90
 
83
91
  @classmethod
84
92
  def from_event(cls, event: BaseEvent[Any]):
@@ -89,6 +97,8 @@ class LanguageModelService:
89
97
  if isinstance(event, AssistantWebhookEvent):
90
98
  service._chat_id = event.payload.chat_id
91
99
  service._assistant_id = event.payload.assistant_id
100
+ if isinstance(event.payload, ChatEventPayload):
101
+ service._assistant_message_id = event.payload.assistant_message.id
92
102
  return service
93
103
 
94
104
  @classmethod
@@ -251,6 +261,7 @@ class LanguageModelService:
251
261
  structured_output_enforce_schema=structured_output_enforce_schema,
252
262
  chat_id=self._chat_id,
253
263
  assistant_id=self._assistant_id,
264
+ assistant_message_id=self._assistant_message_id,
254
265
  )
255
266
 
256
267
  async def complete_async(
@@ -281,6 +292,7 @@ class LanguageModelService:
281
292
  structured_output_enforce_schema=structured_output_enforce_schema,
282
293
  chat_id=self._chat_id,
283
294
  assistant_id=self._assistant_id,
295
+ assistant_message_id=self._assistant_message_id,
284
296
  )
285
297
 
286
298
  @classmethod
@@ -344,6 +356,7 @@ class LanguageModelService:
344
356
  start_text=start_text,
345
357
  chat_id=self._chat_id,
346
358
  assistant_id=self._assistant_id,
359
+ assistant_message_id=self._assistant_message_id,
347
360
  )
348
361
 
349
362
  async def complete_with_references_async(
@@ -371,4 +384,5 @@ class LanguageModelService:
371
384
  start_text=start_text,
372
385
  chat_id=self._chat_id,
373
386
  assistant_id=self._assistant_id,
387
+ assistant_message_id=self._assistant_message_id,
374
388
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.36.0.dev0
3
+ Version: 2026.36.0.dev2
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>
@@ -201,7 +201,7 @@ unique_toolkit/agentic/tools/factory.py,sha256=YK9uh2EqotQFb9stsXs-A8lsNaxklTCd_
201
201
  unique_toolkit/agentic/tools/mcp/__init__.py,sha256=RLF_p-LDRC7GhiB3fdCi4u3bh6V9PY_w26fg61BLyco,122
202
202
  unique_toolkit/agentic/tools/mcp/manager.py,sha256=EfWbfNhB8Lnx2EqBgUXhK7klFLaBv2eezvYq8J0TXyo,3808
203
203
  unique_toolkit/agentic/tools/mcp/models.py,sha256=DaBVzwJ4dqtgzm8Mv0-NOGBU04zyVe-RgWntBhkpmnE,742
204
- unique_toolkit/agentic/tools/mcp/tool_wrapper.py,sha256=fE5nVALJ-wVZkkbbJ_ySRLBhAAR6sCAAu1lUxQQVWC4,15596
204
+ unique_toolkit/agentic/tools/mcp/tool_wrapper.py,sha256=-WZaPjhbxozrPWgwZDD48hfYIp7s1cGAKU3IXqLXFVA,17575
205
205
  unique_toolkit/agentic/tools/names.py,sha256=MDF0ONLrxeC-iz--EtyrHAox_RN6zasxo5MSbXQYGyw,412
206
206
  unique_toolkit/agentic/tools/openai_builtin/__init__.py,sha256=Q6d9mhsLRVF3erT2yMxLld62D3FTZ8nbovI4vLv4Nso,701
207
207
  unique_toolkit/agentic/tools/openai_builtin/base.py,sha256=QWF-GL_fkJqzGPpxvtg69-TB_IjsrxokeJVQESzH-18,1415
@@ -391,14 +391,14 @@ unique_toolkit/language_model/_responses_api_utils.py,sha256=w2ZyleOQcWLpOWGJNNb
391
391
  unique_toolkit/language_model/builder.py,sha256=KwBzsD6-fvQnB4p2I7M-5Tk6j4tmQ973MYwU1ulXFb4,4526
392
392
  unique_toolkit/language_model/constants.py,sha256=B-topqW0r83dkC_25DeQfnPk3n53qzIHUCBS7YJ0-1U,119
393
393
  unique_toolkit/language_model/default_language_model.py,sha256=mcyuFs_V6tp52C8xJSHiEHO4kFGj6xed1gvozvCRD7k,1181
394
- unique_toolkit/language_model/functions.py,sha256=1J-7p9fvVbU1agrVZu-_UGiQHDSBEX0_eZ4SidntdFM,26226
394
+ unique_toolkit/language_model/functions.py,sha256=W6JC4LUh-LZ9waDgph--xVmKeQ7Ls6NRAqDx9DwQdeY,26691
395
395
  unique_toolkit/language_model/infos.py,sha256=zs0UFhds4UkSRr3zxyTJ-jJ9u2O2x9VgZx3a9d4zbYU,146221
396
396
  unique_toolkit/language_model/invocation_stats.py,sha256=XAM_qckI-BoMBSgDpVcxFD1cFSdi1-GHZToHz_jLynM,2491
397
397
  unique_toolkit/language_model/model_costs.py,sha256=WM1u2fWAKpPB_creS-ew8YAzc1V8oiaCbvhpSJaO8B0,3789
398
398
  unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
399
399
  unique_toolkit/language_model/reference.py,sha256=T3wr2jP_uRg5wKVIbKqLbxJlcgxfbZQGq3XdjbQ2kSo,6984
400
400
  unique_toolkit/language_model/schemas.py,sha256=IUmp_JMHx0czadJlmsLL9nmc-nCPmVRVLp2T5DSYsqY,35402
401
- unique_toolkit/language_model/service.py,sha256=9Q1xQCGjGVHZVMjkXaAPKAc7fLY2lGY6sFgdasoRQZE,12616
401
+ unique_toolkit/language_model/service.py,sha256=lgpzWhqxLBOeFacCafQi4e7BqAxJbOlr8GnCYcKeTdM,13274
402
402
  unique_toolkit/language_model/settings.py,sha256=7szohExI-qmTfUf2Zp5AB5TzrsZy1UekAGyXx_UaPMc,1524
403
403
  unique_toolkit/language_model/stream_transform.py,sha256=iNNGpLY_7G6Bpp9Y4KtbIkQm3WB4yZt75BXcqx0DVkI,2775
404
404
  unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
@@ -420,6 +420,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
420
420
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
421
421
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
422
422
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
423
- unique_toolkit-2026.36.0.dev0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
424
- unique_toolkit-2026.36.0.dev0.dist-info/METADATA,sha256=FDHFO1DJ6xFdOS6tl_Nise6nYmhJmGppXAW5CkEmlL4,7821
425
- unique_toolkit-2026.36.0.dev0.dist-info/RECORD,,
423
+ unique_toolkit-2026.36.0.dev2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
424
+ unique_toolkit-2026.36.0.dev2.dist-info/METADATA,sha256=Caxi8wvrymP225m-SxkPfSMbMQ4agwTI-81eGleVGXw,7821
425
+ unique_toolkit-2026.36.0.dev2.dist-info/RECORD,,