unique_toolkit 2026.28.0.dev5__py3-none-any.whl → 2026.28.0.dev6__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.
@@ -52,6 +52,18 @@ class HallucinationConfig(EvaluationMetricConfig):
52
52
  default={},
53
53
  description="Additional options to pass to the language model.",
54
54
  )
55
+ max_external_context_chars_per_source: SkipJsonSchema[int] = Field(
56
+ default=20_000,
57
+ ge=0,
58
+ description=(
59
+ "Per-source character cap applied to external (non-chunk) "
60
+ "context texts, e.g. MCP tool output. Raw/large tool results "
61
+ "inflate the judge prompt and the eval-LLM call; each external "
62
+ "text is truncated to this many characters before being added "
63
+ "to the context. 0 disables truncation. Does not affect "
64
+ "chunk-derived context."
65
+ ),
66
+ )
55
67
  score_to_label: dict[str, str] = {
56
68
  "LOW": "GREEN",
57
69
  "MEDIUM": "YELLOW",
@@ -54,6 +54,17 @@ class HallucinationEvaluation(Evaluation):
54
54
  reference_pattern=self.config.reference_pattern,
55
55
  )
56
56
 
57
+ # Append non-chunk groundedness context (e.g. MCP tool output). These
58
+ # are added in full ("all retrieved output as context") rather than via
59
+ # source-marker selection, since they carry no [sourceN] markers.
60
+ external_context_texts = self._reference_manager.get_external_context_texts()
61
+ max_chars = self.config.max_external_context_chars_per_source
62
+ if max_chars > 0:
63
+ external_context_texts = [
64
+ text[:max_chars] for text in external_context_texts
65
+ ]
66
+ context_texts = context_texts + external_context_texts
67
+
57
68
  evaluation_result: EvaluationMetricResult = await check_hallucination(
58
69
  company_id=self._company_id,
59
70
  user_id=self._user_id,
@@ -0,0 +1,122 @@
1
+ """Tests that HallucinationEvaluation grounds against non-chunk context.
2
+
3
+ UN-21951: external context texts (e.g. MCP tool output) registered on the
4
+ ReferenceManager must be appended to the evaluation context — in addition to
5
+ chunk-derived context, capped by ``max_external_context_chars_per_source`` —
6
+ without being converted to ContentChunks.
7
+ """
8
+
9
+ from types import SimpleNamespace
10
+
11
+ import pytest
12
+
13
+ from unique_toolkit.agentic.evaluation.hallucination import (
14
+ hallucination_evaluation as he,
15
+ )
16
+ from unique_toolkit.agentic.evaluation.hallucination.constants import (
17
+ HallucinationConfig,
18
+ SourceSelectionMode,
19
+ )
20
+ from unique_toolkit.agentic.evaluation.schemas import (
21
+ EvaluationMetricName,
22
+ EvaluationMetricResult,
23
+ )
24
+ from unique_toolkit.agentic.reference_manager.reference_manager import (
25
+ ReferenceManager,
26
+ )
27
+ from unique_toolkit.chat.schemas import ChatMessageRole
28
+ from unique_toolkit.language_model.schemas import (
29
+ LanguageModelStreamResponse,
30
+ LanguageModelStreamResponseMessage,
31
+ )
32
+
33
+
34
+ def _make_event() -> SimpleNamespace:
35
+ return SimpleNamespace(
36
+ company_id="company_1",
37
+ user_id="user_1",
38
+ payload=SimpleNamespace(
39
+ user_message=SimpleNamespace(text="what is ACME revenue?"),
40
+ ),
41
+ )
42
+
43
+
44
+ def _make_response(text: str = "ACME revenue is 1M.") -> LanguageModelStreamResponse:
45
+ return LanguageModelStreamResponse(
46
+ message=LanguageModelStreamResponseMessage(
47
+ id="msg_1",
48
+ previous_message_id=None,
49
+ role=ChatMessageRole.ASSISTANT,
50
+ text=text,
51
+ chat_id="chat_1",
52
+ original_text=text,
53
+ references=None,
54
+ ),
55
+ tool_calls=None,
56
+ )
57
+
58
+
59
+ @pytest.fixture
60
+ def captured_context(monkeypatch: pytest.MonkeyPatch) -> dict:
61
+ """Patch check_hallucination to capture the context_texts it receives."""
62
+ captured: dict = {}
63
+
64
+ async def _fake_check_hallucination(*, company_id, user_id, input, config):
65
+ captured["context_texts"] = list(input.context_texts or [])
66
+ return EvaluationMetricResult(
67
+ name=EvaluationMetricName.HALLUCINATION,
68
+ value="LOW",
69
+ reason="grounded",
70
+ )
71
+
72
+ monkeypatch.setattr(he, "check_hallucination", _fake_check_hallucination)
73
+ return captured
74
+
75
+
76
+ async def test_external_context_is_appended(captured_context: dict) -> None:
77
+ manager = ReferenceManager()
78
+ manager.add_external_context_texts(["ACME revenue: 1M"])
79
+ evaluation = he.HallucinationEvaluation(
80
+ HallucinationConfig(source_selection_mode=SourceSelectionMode.FROM_IDS),
81
+ _make_event(), # type: ignore[arg-type]
82
+ manager,
83
+ )
84
+
85
+ result = await evaluation.run(_make_response())
86
+
87
+ # No chunks → context comes solely from the external (MCP) source.
88
+ assert captured_context["context_texts"] == ["ACME revenue: 1M"]
89
+ assert result.value == "LOW"
90
+ assert result.is_positive is True
91
+
92
+
93
+ async def test_external_context_is_truncated(captured_context: dict) -> None:
94
+ manager = ReferenceManager()
95
+ manager.add_external_context_texts(["x" * 100])
96
+ evaluation = he.HallucinationEvaluation(
97
+ HallucinationConfig(
98
+ source_selection_mode=SourceSelectionMode.FROM_IDS,
99
+ max_external_context_chars_per_source=10,
100
+ ),
101
+ _make_event(), # type: ignore[arg-type]
102
+ manager,
103
+ )
104
+
105
+ await evaluation.run(_make_response())
106
+
107
+ assert captured_context["context_texts"] == ["x" * 10]
108
+
109
+
110
+ async def test_no_external_context_preserves_chunk_only_behaviour(
111
+ captured_context: dict,
112
+ ) -> None:
113
+ manager = ReferenceManager() # no chunks, no external context
114
+ evaluation = he.HallucinationEvaluation(
115
+ HallucinationConfig(source_selection_mode=SourceSelectionMode.FROM_IDS),
116
+ _make_event(), # type: ignore[arg-type]
117
+ manager,
118
+ )
119
+
120
+ await evaluation.run(_make_response())
121
+
122
+ assert captured_context["context_texts"] == []
@@ -32,6 +32,7 @@ class ReferenceManager:
32
32
  self._tool_chunks: dict[str, tool_chunks] = {}
33
33
  self._chunks: list[ContentChunk] = []
34
34
  self._references: list[list[ContentReference]] = []
35
+ self._external_context_texts: list[str] = []
35
36
 
36
37
  def extract_referenceable_chunks(
37
38
  self, tool_responses: list[ToolCallResponse]
@@ -47,6 +48,21 @@ class ReferenceManager:
47
48
  def get_chunks(self) -> list[ContentChunk]:
48
49
  return self._chunks
49
50
 
51
+ def add_external_context_texts(self, texts: list[str]) -> None:
52
+ """Register groundedness context that is not backed by ContentChunks.
53
+
54
+ Some retrieval sources (e.g. MCP tool output) provide text the answer
55
+ is built on but are deliberately *not* modelled as ContentChunks — they
56
+ are a different entity from ingested document chunks and must not flow
57
+ through the chunk/reference pipeline. They still need to ground the
58
+ hallucination check, so they are stored here and appended to the
59
+ evaluation context alongside the chunk-derived texts.
60
+ """
61
+ self._external_context_texts.extend(text for text in texts if text)
62
+
63
+ def get_external_context_texts(self) -> list[str]:
64
+ return self._external_context_texts
65
+
50
66
  def get_tool_chunks(self) -> dict[str, tool_chunks]:
51
67
  return self._tool_chunks
52
68
 
@@ -0,0 +1,35 @@
1
+ """Tests for non-chunk groundedness context on the ReferenceManager.
2
+
3
+ External context texts (e.g. MCP tool output, UN-21951) ground the
4
+ hallucination check without being modelled as ContentChunks.
5
+ """
6
+
7
+ from unique_toolkit.agentic.reference_manager.reference_manager import (
8
+ ReferenceManager,
9
+ )
10
+
11
+
12
+ def test_external_context_starts_empty() -> None:
13
+ manager = ReferenceManager()
14
+ assert manager.get_external_context_texts() == []
15
+
16
+
17
+ def test_add_external_context_texts_accumulates() -> None:
18
+ manager = ReferenceManager()
19
+ manager.add_external_context_texts(["a", "b"])
20
+ manager.add_external_context_texts(["c"])
21
+ assert manager.get_external_context_texts() == ["a", "b", "c"]
22
+
23
+
24
+ def test_add_external_context_texts_drops_empty_strings() -> None:
25
+ manager = ReferenceManager()
26
+ manager.add_external_context_texts(["keep", "", "also"])
27
+ assert manager.get_external_context_texts() == ["keep", "also"]
28
+
29
+
30
+ def test_external_context_is_independent_of_chunks() -> None:
31
+ manager = ReferenceManager()
32
+ manager.add_external_context_texts(["mcp output"])
33
+ # External context must not leak into the chunk pipeline.
34
+ assert manager.get_chunks() == []
35
+ assert manager.get_external_context_texts() == ["mcp output"]
@@ -215,7 +215,15 @@ class ContentReference(BaseModel):
215
215
  sequence_number: int
216
216
  source: str
217
217
  source_id: str
218
- url: str
218
+ url: str | None = Field(
219
+ default=None,
220
+ description=(
221
+ "Link target for the reference. Optional: some sources (e.g. an "
222
+ "opaque MCP tool result with no resource link) have no URL and "
223
+ "render as a display-only, non-clickable citation. Nullable "
224
+ "end-to-end (node-chat persists it as a nullable column)."
225
+ ),
226
+ )
219
227
  original_index: list[int] = Field(
220
228
  default=[],
221
229
  description="List of indices in the ChatMessage original_text this reference refers to. This is usually the id in the functionCallResponse. List type due to implementation in node-chat",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.28.0.dev5
3
+ Version: 2026.28.0.dev6
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.dev6,<2026.28.0rc0
19
+ Requires-Dist: unique-sdk>=2026.28.0.dev8,<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
@@ -93,8 +93,8 @@ unique_toolkit/agentic/evaluation/context_relevancy/schema.py,sha256=lZd0TPzH43i
93
93
  unique_toolkit/agentic/evaluation/context_relevancy/service.py,sha256=i6WA_BwHyhHDfnvK59UC0z1FgZ8EPW5OJyYLbSxNRic,8760
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
- unique_toolkit/agentic/evaluation/hallucination/constants.py,sha256=CBWc2CEvSrTyzn1KryxDp7-vp9RLgF16WJ7Ch2JcWNY,2652
97
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py,sha256=Fj4FxKtiY91V5P_IJGlUdmJBT7GJ93SelR6XGoQJUQU,3949
96
+ unique_toolkit/agentic/evaluation/hallucination/constants.py,sha256=uEQR-CNKmeXizDaLOLHhwTZ0iYUmFwoKeMLHPm4M0sY,3208
97
+ unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py,sha256=HvaaRJ4D4pz_77IT2ZbL-OMfVSEOcmcalecOPnBsiRI,4548
98
98
  unique_toolkit/agentic/evaluation/hallucination/prompts/__init__.py,sha256=4KFYMZsB3fJUKzoiUJE1npZ0gueWgvceB32EUrN-v7A,343
99
99
  unique_toolkit/agentic/evaluation/hallucination/prompts/system_prompt.j2,sha256=sDUX6G645Ba40D_qKu4cUI8g-sJOfG8JpZreTNFgf7M,2616
100
100
  unique_toolkit/agentic/evaluation/hallucination/prompts/user_prompt.j2,sha256=mD_qE9fOkyc1XXrebFt097ddx8bTlA6lbY04hKSQmWs,273
@@ -106,6 +106,7 @@ unique_toolkit/agentic/evaluation/tests/fixtures.py,sha256=Q-ughTfDiAdsMKbBVGzFi
106
106
  unique_toolkit/agentic/evaluation/tests/test_config.py,sha256=p7xFQ7KE_yU8jGpqYA7ntAYe5Vln33wd6nwv3FM9XfI,8327
107
107
  unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py,sha256=NcSOyBJ_lqYehtlraZPo9RLutCitTP76kvkuyogSD2A,9477
108
108
  unique_toolkit/agentic/evaluation/tests/test_hallucination_constants.py,sha256=A77nRcniI7hUf640f1sTvCAibQbJE30ikbz1RX4fltw,20478
109
+ unique_toolkit/agentic/evaluation/tests/test_hallucination_external_context.py,sha256=mIHxQDxW7oUbvOGH5bMcoV_yKr9BZvJlgE0JYicLdsM,3949
109
110
  unique_toolkit/agentic/evaluation/tests/test_hallucination_utils.py,sha256=ogds7jUwwGJHdabhayhEY_1JbiaVv7Fxh58TMwgbqgw,37537
110
111
  unique_toolkit/agentic/evaluation/tests/test_output_parser.py,sha256=KfltytmvqnPWLhmZpBXqcRmnlYorw_USwM5rkLVv8so,5179
111
112
  unique_toolkit/agentic/evaluation/tests/test_prompt_loaders.py,sha256=zBREdlKf5tdDyB8XSaNgpQv3-tuZJoYteeJrp6WMWDM,11897
@@ -138,7 +139,8 @@ unique_toolkit/agentic/message_log_manager/__init__.py,sha256=3-KY_sGkPbNoSnrzwP
138
139
  unique_toolkit/agentic/message_log_manager/service.py,sha256=yXeV_70ATUJrFi_KYRJ8PRmRbo27kQjY5cjazxDFkpI,9840
139
140
  unique_toolkit/agentic/message_log_order.py,sha256=GaZOlAkFS2BpTc72af6Nit9inm6LskBYTze5Q7dd9cs,1041
140
141
  unique_toolkit/agentic/postprocessor/postprocessor_manager.py,sha256=zOKnyzt1JwN7Godb4g3RQUOvwaR-Eg4ygm66gkmDM50,8125
141
- unique_toolkit/agentic/reference_manager/reference_manager.py,sha256=x51CT0D8HHu2LzgXdHGy0leOYpjnsxVbPZ2nc28G9mA,4005
142
+ unique_toolkit/agentic/reference_manager/reference_manager.py,sha256=axqjSNFoR6oGVKg_fK1D2IsA9sxvreyPutRxK17dBxI,4844
143
+ unique_toolkit/agentic/reference_manager/tests/test_external_context.py,sha256=FlCr8rcvrJDkVbKW6pDylSTmP0v0luH_3TGBvXm4KZw,1246
142
144
  unique_toolkit/agentic/responses_api/__init__.py,sha256=9WTO-ef7fGE9Y1QtZJFm8Q_jkwK8Srtl-HWvpAD2Wxs,668
143
145
  unique_toolkit/agentic/responses_api/postprocessors/code_display.py,sha256=CboLgZ4Rncz_kxRwqoq2C93pZsdylHK4jU145YodEkM,936
144
146
  unique_toolkit/agentic/responses_api/postprocessors/generated_files.py,sha256=yVwFT8ObMVJZpUufO1jEHofAdrXm5ndsqbSepuUkNPE,1064
@@ -254,7 +256,7 @@ unique_toolkit/chat/utils.py,sha256=CsD9IER2gA1IOV0fmUOl-N0LmKvA7JP9dGrOqeOY0Gg,
254
256
  unique_toolkit/content/__init__.py,sha256=nnQDfnNTEvI8-3dgWvIojoFDYU7edwd8Kloi6lMDG0M,1586
255
257
  unique_toolkit/content/constants.py,sha256=1iy4Y67xobl5VTnJB6SxSyuoBWbdLl9244xfVMUZi5o,60
256
258
  unique_toolkit/content/functions.py,sha256=avnlEQ1FkVz2NTbXUkafU04jzygqOJtUMQL605rEL4c,34019
257
- unique_toolkit/content/schemas.py,sha256=iBorQu7OM9d1gNwky1oJv7krsAqbTst2_TrE7cQH0GA,10290
259
+ unique_toolkit/content/schemas.py,sha256=00qbe-DlUjqlf-n3QVOotZjpsj0LZNnUoPmdwyMjVHY,10667
258
260
  unique_toolkit/content/service.py,sha256=fR93_76aPczx2voxR7cvYDs_-pxD-Mc9WP6-5IqA8ko,28301
259
261
  unique_toolkit/content/smart_rules.py,sha256=9GlHXm63POTi4jy5hW0qUjeqy-JSeL1wIbURM1GKhc4,11481
260
262
  unique_toolkit/content/utils.py,sha256=keDkqu_ZzQY0y_g7g_XLOza3p4XsvfqVVDI9ylM91KY,10115
@@ -402,6 +404,6 @@ unique_toolkit/short_term_memory/service.py,sha256=Qo-c9yxzC1ZEEHJyIG_vnRs_ef0SK
402
404
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
403
405
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
404
406
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
405
- unique_toolkit-2026.28.0.dev5.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
406
- unique_toolkit-2026.28.0.dev5.dist-info/METADATA,sha256=2C5KgFuTF2BY0kFsCSw0GLePoG1n6mLY7eIxAbsKl04,7426
407
- unique_toolkit-2026.28.0.dev5.dist-info/RECORD,,
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,,