unique_toolkit 2026.28.0.dev2__py3-none-any.whl → 2026.28.0.dev4__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.
@@ -16,6 +16,8 @@ from unique_toolkit.agentic.evaluation.schemas import (
16
16
  )
17
17
  from unique_toolkit.content import ContentReference
18
18
  from unique_toolkit.content.schemas import ContentChunk
19
+ from unique_toolkit.language_model.builder import MessagesBuilder
20
+ from unique_toolkit.language_model.infos import ModelCapabilities
19
21
  from unique_toolkit.language_model.schemas import (
20
22
  LanguageModelMessages,
21
23
  LanguageModelStreamResponse,
@@ -104,19 +106,28 @@ def _get_msgs(
104
106
  """
105
107
  Composes the messages for hallucination analysis based on the provided input and configuration.
106
108
 
107
- This method composes messages with or without context based on the availability of context texts
108
- and history message texts in the input.
109
+ This method composes messages with or without context based on the availability of context texts,
110
+ history message texts, and (for vision-capable models) context images in the input.
109
111
 
110
112
  Args:
111
- input (EvaluationMetricInput): The input data that includes context texts and history message texts
112
- for the analysis.
113
+ input (EvaluationMetricInput): The input data that includes context texts, history message texts,
114
+ and context images for the analysis.
113
115
  config (EvaluationMetricConfig): The configuration settings for composing messages.
114
116
  logger (Optional[logging.Logger], optional): The logger used for logging debug information.
115
117
 
116
118
  Returns:
117
119
  The composed messages as per the provided input and configuration.
118
120
  """
119
- has_context = bool(input.context_texts or input.history_messages)
121
+ # Context images only count as grounding when the model can actually consume
122
+ # them; otherwise _compose_msgs drops them and the message would be text-only.
123
+ model_supports_vision = (
124
+ ModelCapabilities.VISION in config.language_model.capabilities
125
+ )
126
+ has_context = bool(
127
+ input.context_texts
128
+ or input.history_messages
129
+ or (input.context_images and model_supports_vision)
130
+ )
120
131
 
121
132
  if has_context:
122
133
  _LOGGER.debug("Using context / history for hallucination evaluation.")
@@ -165,6 +176,36 @@ def _compose_msgs(
165
176
  else None,
166
177
  output_text=input.output_text,
167
178
  )
179
+
180
+ # When images are provided AND the configured model is vision-capable, build
181
+ # a multimodal user message (rendered user text + one image part per data
182
+ # URL) so the model can ground the check against the rendered pages. If the
183
+ # model lacks VISION we drop the images and fall back to the text-only
184
+ # message: attaching images to a non-vision model would make the LLM call
185
+ # fail. Without images the message stays the exact text-only form, so
186
+ # existing callers are unaffected.
187
+ context_images = input.context_images or []
188
+ model_supports_vision = (
189
+ ModelCapabilities.VISION in config.language_model.capabilities
190
+ )
191
+ # When has_context is True for an images-only input, the caller already
192
+ # confirmed the model is vision-capable, so this branch stays unreachable in
193
+ # that case and only logs for a genuine non-vision misconfiguration.
194
+ if context_images and not model_supports_vision:
195
+ _LOGGER.warning(
196
+ "Hallucination model %s lacks VISION capability; ignoring "
197
+ "%d context image(s) and grounding on text only.",
198
+ config.language_model.name,
199
+ len(context_images),
200
+ )
201
+ if context_images and model_supports_vision:
202
+ return (
203
+ MessagesBuilder()
204
+ .append(system_msg)
205
+ .image_message_append(content=user_msg_content, images=context_images)
206
+ .build()
207
+ )
208
+
168
209
  user_msg = LanguageModelUserMessage(content=user_msg_content)
169
210
 
170
211
  return LanguageModelMessages([system_msg, user_msg])
@@ -34,6 +34,16 @@ class EvaluationMetricInput(BaseModel):
34
34
  context_texts: Optional[list[str]] = None
35
35
  history_messages: Optional[list[ChatMessage]] = None
36
36
  output_text: Optional[str] = None
37
+ context_images: Optional[list[str]] = Field(
38
+ default=None,
39
+ description=(
40
+ "Optional base64 data URLs (e.g. 'data:image/png;base64,...') of "
41
+ "context the metric should evaluate against visually, in addition "
42
+ "to context_texts. Used by metrics that support a multimodal "
43
+ "(vision) check; ignored by metrics that do not. Defaults to None "
44
+ "so existing text-only callers are unaffected."
45
+ ),
46
+ )
37
47
 
38
48
  def get_joined_context_texts(self, tag_name: str = "reference") -> str:
39
49
  """
@@ -20,6 +20,10 @@ from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricInput
20
20
  from unique_toolkit.chat.schemas import ChatMessage
21
21
  from unique_toolkit.content import ContentReference
22
22
  from unique_toolkit.content.schemas import ContentChunk
23
+ from unique_toolkit.language_model.infos import (
24
+ LanguageModelName,
25
+ ModelCapabilities,
26
+ )
23
27
  from unique_toolkit.language_model.schemas import (
24
28
  LanguageModelMessageRole,
25
29
  LanguageModelMessages,
@@ -1013,3 +1017,162 @@ def test_compose_msgs__generates_non_empty_messages__from_config_prompts(
1013
1017
  # Assert
1014
1018
  assert len(result.root[0].content) > 0
1015
1019
  assert len(result.root[1].content) > 0
1020
+
1021
+
1022
+ _IMAGE_DATA_URL = "data:image/png;base64,QQ=="
1023
+
1024
+
1025
+ @pytest.mark.ai
1026
+ def test_compose_msgs__builds_multimodal_user_message__with_context_images(
1027
+ hallucination_config: HallucinationConfig,
1028
+ ) -> None:
1029
+ """
1030
+ Purpose: Verify that context_images produce a multimodal user message.
1031
+ Why this matters: Vision faithfulness needs the rendered pages as image parts.
1032
+ Setup summary: Provide context_images, assert the user message carries text +
1033
+ one image part per data URL.
1034
+ """
1035
+ # Arrange
1036
+ input_data: EvaluationMetricInput = EvaluationMetricInput(
1037
+ input_text="Question",
1038
+ output_text="Output",
1039
+ context_images=[_IMAGE_DATA_URL],
1040
+ )
1041
+
1042
+ # Act
1043
+ result: LanguageModelMessages = _compose_msgs(
1044
+ input_data, hallucination_config, has_context=True
1045
+ )
1046
+
1047
+ # Assert
1048
+ assert len(result.root) == 2
1049
+ user_content = result.root[1].content
1050
+ assert isinstance(user_content, list)
1051
+ text_parts = [p for p in user_content if p.get("type") == "text"]
1052
+ image_parts = [p for p in user_content if p.get("type") == "image_url"]
1053
+ assert len(text_parts) == 1
1054
+ assert len(image_parts) == 1
1055
+ assert image_parts[0]["imageUrl"]["url"] == _IMAGE_DATA_URL
1056
+
1057
+
1058
+ @pytest.mark.ai
1059
+ def test_compose_msgs__stays_text_only__without_context_images(
1060
+ hallucination_config: HallucinationConfig,
1061
+ ) -> None:
1062
+ """
1063
+ Purpose: Verify that omitting context_images keeps the exact text-only message.
1064
+ Why this matters: Backward compatibility for all existing (non-vision) callers.
1065
+ Setup summary: No context_images -> user message content is a plain string.
1066
+ """
1067
+ # Arrange
1068
+ input_data: EvaluationMetricInput = EvaluationMetricInput(
1069
+ input_text="Question",
1070
+ context_texts=["Context 1"],
1071
+ output_text="Output",
1072
+ )
1073
+
1074
+ # Act
1075
+ result: LanguageModelMessages = _compose_msgs(
1076
+ input_data, hallucination_config, has_context=True
1077
+ )
1078
+
1079
+ # Assert
1080
+ assert isinstance(result.root[1].content, str)
1081
+
1082
+
1083
+ @pytest.mark.ai
1084
+ def test_compose_msgs__drops_images__when_model_lacks_vision() -> None:
1085
+ """
1086
+ Purpose: Verify images are dropped when the configured model is not vision-capable.
1087
+ Why this matters: Attaching images to a non-vision model makes the LLM call fail;
1088
+ we must degrade gracefully to a text-only message instead.
1089
+ Setup summary: Configure a text-only model (no VISION capability) with
1090
+ context_images, assert the user message content stays a plain string.
1091
+ """
1092
+ # Arrange
1093
+ text_only_config = HallucinationConfig(
1094
+ enabled=True,
1095
+ language_model=LanguageModelName.AZURE_GPT_4_0613,
1096
+ )
1097
+ assert ModelCapabilities.VISION not in (
1098
+ text_only_config.language_model.capabilities
1099
+ )
1100
+ input_data: EvaluationMetricInput = EvaluationMetricInput(
1101
+ input_text="Question",
1102
+ output_text="Output",
1103
+ context_images=[_IMAGE_DATA_URL],
1104
+ )
1105
+
1106
+ # Act
1107
+ result: LanguageModelMessages = _compose_msgs(
1108
+ input_data, text_only_config, has_context=True
1109
+ )
1110
+
1111
+ # Assert
1112
+ assert isinstance(result.root[1].content, str)
1113
+
1114
+
1115
+ @pytest.mark.ai
1116
+ def test_get_msgs__treats_context_images_as_context(
1117
+ hallucination_config: HallucinationConfig,
1118
+ ) -> None:
1119
+ """
1120
+ Purpose: Verify that context_images alone count as context in _get_msgs.
1121
+ Why this matters: A vision-only turn must use the grounded (has_context) prompt
1122
+ and build the multimodal message.
1123
+ Setup summary: Provide only context_images; assert the system prompt matches the
1124
+ has_context variant and the user message is multimodal.
1125
+ """
1126
+ # Arrange
1127
+ image_only: EvaluationMetricInput = EvaluationMetricInput(
1128
+ input_text="Question",
1129
+ output_text="Output",
1130
+ context_images=[_IMAGE_DATA_URL],
1131
+ )
1132
+ grounded: EvaluationMetricInput = EvaluationMetricInput(
1133
+ input_text="Question",
1134
+ context_texts=["Context 1"],
1135
+ output_text="Output",
1136
+ )
1137
+
1138
+ # Act
1139
+ result: LanguageModelMessages = _get_msgs(image_only, hallucination_config)
1140
+ grounded_result: LanguageModelMessages = _get_msgs(grounded, hallucination_config)
1141
+
1142
+ # Assert
1143
+ assert result.root[0].content == grounded_result.root[0].content
1144
+ assert isinstance(result.root[1].content, list)
1145
+
1146
+
1147
+ @pytest.mark.ai
1148
+ def test_get_msgs__ignores_context_images__when_model_lacks_vision() -> None:
1149
+ """
1150
+ Purpose: Verify image-only input is not treated as context for a non-vision model.
1151
+ Why this matters: _compose_msgs drops the images for a non-vision model, so the
1152
+ grounded prompt would otherwise claim references that are never sent. has_context
1153
+ must reflect the message actually delivered.
1154
+ Setup summary: Configure a text-only model with only context_images; assert the
1155
+ system prompt matches the ungrounded variant and the user message is text-only.
1156
+ """
1157
+ # Arrange
1158
+ text_only_config = HallucinationConfig(
1159
+ enabled=True,
1160
+ language_model=LanguageModelName.AZURE_GPT_4_0613,
1161
+ )
1162
+ image_only: EvaluationMetricInput = EvaluationMetricInput(
1163
+ input_text="Question",
1164
+ output_text="Output",
1165
+ context_images=[_IMAGE_DATA_URL],
1166
+ )
1167
+ no_context: EvaluationMetricInput = EvaluationMetricInput(
1168
+ input_text="Question",
1169
+ output_text="Output",
1170
+ )
1171
+
1172
+ # Act
1173
+ result: LanguageModelMessages = _get_msgs(image_only, text_only_config)
1174
+ no_context_result: LanguageModelMessages = _get_msgs(no_context, text_only_config)
1175
+
1176
+ # Assert
1177
+ assert result.root[0].content == no_context_result.root[0].content
1178
+ assert isinstance(result.root[1].content, str)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.28.0.dev2
3
+ Version: 2026.28.0.dev4
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.dev3,<2026.28.0rc0
19
+ Requires-Dist: unique-sdk>=2026.28.0.dev5,<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
@@ -99,14 +99,14 @@ unique_toolkit/agentic/evaluation/hallucination/prompts/__init__.py,sha256=4KFYM
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
101
101
  unique_toolkit/agentic/evaluation/hallucination/service.py,sha256=WJF1f45uHnYLx1S4TW31bSFobFpV-YlOS3G_zMhuBVU,2512
102
- unique_toolkit/agentic/evaluation/hallucination/utils.py,sha256=nXbKyQksHVkmJIFWXW53-QaET_WrVYJOiUS4eDvQGaE,12091
102
+ unique_toolkit/agentic/evaluation/hallucination/utils.py,sha256=3j2iQ89pMV9DFZ-7asBiHMZfZqSt0gMalZT_d1GRhuo,14068
103
103
  unique_toolkit/agentic/evaluation/output_parser.py,sha256=0FDo8YY_Dc4qlTNeYyQkznzIFj9aX9wMrLOTbhhTl6g,1418
104
- unique_toolkit/agentic/evaluation/schemas.py,sha256=m9JMCUmeqP8KhsJOVEzsz6dRXUe1uKw-bxRDtn5qwvM,3156
104
+ unique_toolkit/agentic/evaluation/schemas.py,sha256=dadCyJjHbl469Crph2kEWll1kC_jpVkHaaY9wyXAK-E,3640
105
105
  unique_toolkit/agentic/evaluation/tests/fixtures.py,sha256=Q-ughTfDiAdsMKbBVGzFiBucFdAx-FXgJ9iqp5xMyPs,2801
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_utils.py,sha256=Mii94lYFh8H_OaJyZSApdh45Sz3CN5r5q9GVsQgOTYA,31856
109
+ unique_toolkit/agentic/evaluation/tests/test_hallucination_utils.py,sha256=ogds7jUwwGJHdabhayhEY_1JbiaVv7Fxh58TMwgbqgw,37537
110
110
  unique_toolkit/agentic/evaluation/tests/test_output_parser.py,sha256=KfltytmvqnPWLhmZpBXqcRmnlYorw_USwM5rkLVv8so,5179
111
111
  unique_toolkit/agentic/evaluation/tests/test_prompt_loaders.py,sha256=zBREdlKf5tdDyB8XSaNgpQv3-tuZJoYteeJrp6WMWDM,11897
112
112
  unique_toolkit/agentic/evaluation/utils.py,sha256=HmyPaDV8wdW-_gOjjW-wDaMKgdrsP5-SHP7OqTmGI_A,264
@@ -402,6 +402,6 @@ unique_toolkit/short_term_memory/service.py,sha256=Qo-c9yxzC1ZEEHJyIG_vnRs_ef0SK
402
402
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
403
403
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
404
404
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
405
- unique_toolkit-2026.28.0.dev2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
406
- unique_toolkit-2026.28.0.dev2.dist-info/METADATA,sha256=-L08qC8wJBPOovWUmGDdBRkhEdhxxYe-8LGLw6dW_Rs,7426
407
- unique_toolkit-2026.28.0.dev2.dist-info/RECORD,,
405
+ unique_toolkit-2026.28.0.dev4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
406
+ unique_toolkit-2026.28.0.dev4.dist-info/METADATA,sha256=zwb866qNcc6wQI3bmlKEsLMjuGurNHVXbuFbAcCeDuE,7426
407
+ unique_toolkit-2026.28.0.dev4.dist-info/RECORD,,