unique_toolkit 2026.40.0.dev1__py3-none-any.whl → 2026.40.0.dev3__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.
@@ -203,7 +203,8 @@ def _compose_msgs(
203
203
  )
204
204
  system_msg = LanguageModelSystemMessage(content=system_msg_content)
205
205
 
206
- # Render user message
206
+ # Render user message. Pass history text only when history exists so the
207
+ # template can omit an empty conversation section.
207
208
  user_msg_content = render_template(
208
209
  user_template,
209
210
  input_text=input.input_text,
@@ -211,7 +212,7 @@ def _compose_msgs(
211
212
  if has_context
212
213
  else None,
213
214
  history_messages_text=input.get_joined_history_texts(tag_name="conversation")
214
- if has_context
215
+ if has_context and input.history_messages
215
216
  else None,
216
217
  output_text=input.output_text,
217
218
  )
@@ -1224,3 +1224,67 @@ def test_get_msgs__ignores_context_images__when_model_lacks_vision() -> None:
1224
1224
  # Assert
1225
1225
  assert result.root[0].content == no_context_result.root[0].content
1226
1226
  assert isinstance(result.root[1].content, str)
1227
+
1228
+
1229
+ @pytest.mark.unit
1230
+ def test_compose_msgs__omits_conversation_section__when_history_is_empty():
1231
+ """A single-turn evaluation must not be shown an empty conversation.
1232
+
1233
+ The template guards the section with `{% if history_messages_text %}`,
1234
+ and the getter returns a truthy placeholder when there is no history, so
1235
+ the guard used to never fire: every caller evaluating one turn shipped a
1236
+ `Conversation:` section whose body said there was no conversation, while
1237
+ the system prompt announced one.
1238
+ """
1239
+ msgs = _compose_msgs(
1240
+ EvaluationMetricInput(
1241
+ input_text="What is the policy rate?",
1242
+ context_texts=["The policy rate is 0.25%."],
1243
+ history_messages=[],
1244
+ output_text="0.25%.",
1245
+ ),
1246
+ HallucinationConfig(),
1247
+ has_context=True,
1248
+ )
1249
+
1250
+ user_content = msgs.root[-1].content
1251
+ assert isinstance(user_content, str)
1252
+ assert "Conversation:" not in user_content
1253
+ assert "<No conversation texts provided>" not in user_content
1254
+ # The references the model does have must still be there.
1255
+ assert "The policy rate is 0.25%." in user_content
1256
+
1257
+
1258
+ @pytest.mark.unit
1259
+ def test_compose_msgs__keeps_conversation_section__when_history_is_present():
1260
+ msgs = _compose_msgs(
1261
+ EvaluationMetricInput(
1262
+ input_text="And in March?",
1263
+ context_texts=["The March rate was 0.50%."],
1264
+ history_messages=[
1265
+ ChatMessage(
1266
+ id="msg_1",
1267
+ chat_id="chat_1",
1268
+ previous_message_id=None,
1269
+ role=LanguageModelMessageRole.USER,
1270
+ text="What is the policy rate?",
1271
+ ),
1272
+ ChatMessage(
1273
+ id="msg_2",
1274
+ chat_id="chat_1",
1275
+ previous_message_id="msg_1",
1276
+ role=LanguageModelMessageRole.ASSISTANT,
1277
+ text="0.25%.",
1278
+ ),
1279
+ ],
1280
+ output_text="0.50%.",
1281
+ ),
1282
+ HallucinationConfig(),
1283
+ has_context=True,
1284
+ )
1285
+
1286
+ user_content = msgs.root[-1].content
1287
+ assert isinstance(user_content, str)
1288
+ assert "Conversation:" in user_content
1289
+ assert "What is the policy rate?" in user_content
1290
+ assert "<No conversation texts provided>" not in user_content
@@ -188,6 +188,46 @@ async def modify_message_async(
188
188
  raise e
189
189
 
190
190
 
191
+ async def mark_turn_system_interrupted_async(
192
+ user_id: str,
193
+ company_id: str,
194
+ chat_id: str,
195
+ user_message_id: str,
196
+ ) -> ChatMessage:
197
+ """Marks the turn anchored on *user_message_id* as system-interrupted.
198
+
199
+ Persists ``turnInterruptionReason = SYSTEM_INTERRUPTED`` on the originating
200
+ USER message via a dedicated service-authenticated node-chat endpoint. The
201
+ server guarantees idempotency and never overwrites ``userAbortedAt`` (a
202
+ user-initiated stop wins if both race).
203
+
204
+ Args:
205
+ user_id (str): The user ID.
206
+ company_id (str): The company ID.
207
+ chat_id (str): The chat ID.
208
+ user_message_id (str): The originating USER message of the turn.
209
+
210
+ Returns:
211
+ ChatMessage: The USER message after the update (unchanged if the call
212
+ was a no-op due to idempotency or a racing user stop).
213
+
214
+ Raises:
215
+ Exception: If the request fails.
216
+
217
+ """
218
+ try:
219
+ message = await unique_sdk.Message.mark_turn_system_interrupted_async(
220
+ user_id=user_id,
221
+ company_id=company_id,
222
+ id=user_message_id,
223
+ chatId=chat_id,
224
+ )
225
+ return ChatMessage(**message)
226
+ except Exception as e:
227
+ _LOGGER.error(f"Failed to mark turn as system-interrupted: {e}")
228
+ raise e
229
+
230
+
191
231
  def map_references(references: list[ContentReference]) -> list[dict[str, Any]]:
192
232
  return [
193
233
  {
@@ -192,6 +192,12 @@ class MessageLogStatus(StrEnum):
192
192
  RUNNING = "RUNNING"
193
193
  COMPLETED = "COMPLETED"
194
194
  FAILED = "FAILED"
195
+ CANCELLED = "CANCELLED"
196
+ """The user pressed Stop while this thinking/tool step was in flight.
197
+
198
+ Use ``FAILED`` for system-side interruption (timeout, truncation, terminal
199
+ backend failure), not for user-initiated cancellation.
200
+ """
195
201
 
196
202
 
197
203
  class MessageExecutionStatus(StrEnum):
@@ -49,6 +49,7 @@ from unique_toolkit.chat.functions import (
49
49
  get_message_tools,
50
50
  get_message_tools_async,
51
51
  get_selection_from_history,
52
+ mark_turn_system_interrupted_async,
52
53
  modify_message,
53
54
  modify_message_assessment,
54
55
  modify_message_assessment_async,
@@ -383,6 +384,36 @@ class ChatService(ChatServiceDeprecated):
383
384
  set_completed_at=set_completed_at or False,
384
385
  )
385
386
 
387
+ async def mark_turn_system_interrupted_async(self) -> ChatMessage:
388
+ """Marks the current turn as system-interrupted.
389
+
390
+ Persists ``turnInterruptionReason = SYSTEM_INTERRUPTED`` on the current
391
+ chat context's originating USER message via a dedicated
392
+ service-authenticated node-chat endpoint.
393
+
394
+ Call this only for system-side unexpected endings: timeout, truncation,
395
+ terminal backend failure, and equivalents. Do not call it for
396
+ user-initiated cancellation or normal completion.
397
+
398
+ The operation is idempotent and never overwrites ``userAbortedAt``: if
399
+ the user pressed Stop and the system interruption race, the user stop
400
+ wins and this call is a server-side no-op.
401
+
402
+ Returns:
403
+ ChatMessage: The USER message anchoring the turn.
404
+
405
+ Raises:
406
+ Exception: If the request fails. Callers invoking this from
407
+ failure-handling paths should treat it as best-effort.
408
+
409
+ """
410
+ return await mark_turn_system_interrupted_async(
411
+ user_id=self._user_id,
412
+ company_id=self._company_id,
413
+ chat_id=self._chat_id,
414
+ user_message_id=self._user_message_id,
415
+ )
416
+
386
417
  def modify_assistant_message(
387
418
  self,
388
419
  content: str | None = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.40.0.dev1
3
+ Version: 2026.40.0.dev3
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.40.0.dev0,<2026.40.0rc0
19
+ Requires-Dist: unique-sdk>=2026.40.0.dev3,<2026.40.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,7 +99,7 @@ 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=QSPsRxx2H-YpSR2Jn7zJKtwZfZ7elTPAFQiYvM3gYfs,2618
102
- unique_toolkit/agentic/evaluation/hallucination/utils.py,sha256=QVXO0hFrsGxvbgTwC40RSfrLhE_1of1oBrrfcafk10Y,15531
102
+ unique_toolkit/agentic/evaluation/hallucination/utils.py,sha256=maiMxlUjT9_U0GAH9HvemMIUE-47MCGtwm2XMwDAShQ,15664
103
103
  unique_toolkit/agentic/evaluation/output_parser.py,sha256=0FDo8YY_Dc4qlTNeYyQkznzIFj9aX9wMrLOTbhhTl6g,1418
104
104
  unique_toolkit/agentic/evaluation/schemas.py,sha256=RT0O-AXSOf1zV3fDbUfn7V6bOIA01pkzui02Uad9E3Y,3815
105
105
  unique_toolkit/agentic/evaluation/tests/fixtures.py,sha256=Q-ughTfDiAdsMKbBVGzFiBucFdAx-FXgJ9iqp5xMyPs,2801
@@ -107,7 +107,7 @@ unique_toolkit/agentic/evaluation/tests/test_config.py,sha256=p7xFQ7KE_yU8jGpqYA
107
107
  unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py,sha256=-GRxH70vzvrZG6Hz0igH6i2CqM7Z9jjKt3396xWDh84,12011
108
108
  unique_toolkit/agentic/evaluation/tests/test_hallucination_constants.py,sha256=A77nRcniI7hUf640f1sTvCAibQbJE30ikbz1RX4fltw,20478
109
109
  unique_toolkit/agentic/evaluation/tests/test_hallucination_external_context.py,sha256=3JR9WfBWazX7a1h3uq77Q6kSKm8Mt-aAW8KUYxJUw14,4303
110
- unique_toolkit/agentic/evaluation/tests/test_hallucination_utils.py,sha256=JzFXSH3k1qnSDbZEQy5ftKiVnVL_jS1PTpp3H3DjewI,39218
110
+ unique_toolkit/agentic/evaluation/tests/test_hallucination_utils.py,sha256=TJcJf9SZcH1dsSq38NkvSBM0VZfBRkan49Una3UPQB0,41546
111
111
  unique_toolkit/agentic/evaluation/tests/test_output_parser.py,sha256=KfltytmvqnPWLhmZpBXqcRmnlYorw_USwM5rkLVv8so,5179
112
112
  unique_toolkit/agentic/evaluation/tests/test_prompt_loaders.py,sha256=zBREdlKf5tdDyB8XSaNgpQv3-tuZJoYteeJrp6WMWDM,11897
113
113
  unique_toolkit/agentic/evaluation/utils.py,sha256=HmyPaDV8wdW-_gOjjW-wDaMKgdrsP5-SHP7OqTmGI_A,264
@@ -260,10 +260,10 @@ unique_toolkit/chat/cancellation.py,sha256=k5HMjpeBmofIiInwq5TjOR_5k91oq1bWG00lJ
260
260
  unique_toolkit/chat/constants.py,sha256=05kq6zjqUVB2d6_P7s-90nbljpB3ryxwCI-CAz0r2O4,83
261
261
  unique_toolkit/chat/deprecated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
262
262
  unique_toolkit/chat/deprecated/service.py,sha256=8_XoopUWHT396Z_5gwMmsI6v4NGVgQirSJCYUW4Gx8c,9339
263
- unique_toolkit/chat/functions.py,sha256=1xpiRMTbl9XSz7fzzbUYYtsAa178bhZ0zIOAAzjDqQw,55773
263
+ unique_toolkit/chat/functions.py,sha256=fb4x_y4d5QwT1WUjVEP-opFpJ9CV8WaiLAOR1ktTmy0,57073
264
264
  unique_toolkit/chat/rendering.py,sha256=c8YiV9oADRrJQ5A_QBJ4_UFc0NZ-2vVaa7tupoMusso,880
265
265
  unique_toolkit/chat/responses_api.py,sha256=Swp6wi10vCPkBiSSj8NN58gpjTgYVkqRlwdOJRQHVjo,21135
266
- unique_toolkit/chat/schemas.py,sha256=DJ3-J2a-FK45UU_H2NaDOh-H0UEsPgmM_5eGfwN3HfI,8123
266
+ unique_toolkit/chat/schemas.py,sha256=tRU8vcT1SQWtktkHQmDO26CNbOgSynFHfFB9FR6D4OQ,8372
267
267
  unique_toolkit/chat/service.py,sha256=6D00OL4QrGafbOhTaC5zNXaNgg7gS5W_2ePVa4LhqpE,4439
268
268
  unique_toolkit/chat/state.py,sha256=Cjgwv_2vhDFbV69xxsn7SefhaoIAEqLx3ferdVFCnOg,1445
269
269
  unique_toolkit/chat/utils.py,sha256=CsD9IER2gA1IOV0fmUOl-N0LmKvA7JP9dGrOqeOY0Gg,1173
@@ -412,7 +412,7 @@ unique_toolkit/monitoring/registry.py,sha256=De2Bx5QBBRJQY2MM3xtntiJ2JU2FoKkVT7r
412
412
  unique_toolkit/monitoring/tracing.py,sha256=0lmLuQjyyC3iFpNTw8rOyCpEZ6PTvp16nVbzcO9d2sQ,10043
413
413
  unique_toolkit/protocols/support.py,sha256=x5edzbBmKAUuvm3oLFh52gkgQsqR5LU7M0t8RHK0DZk,5161
414
414
  unique_toolkit/services/__init__.py,sha256=b9Wl7Wvz1JmZDbDexIZzBoXZ0wscXuczGAH8LoL-Qak,572
415
- unique_toolkit/services/chat_service.py,sha256=8BncFKNJK0zR8QM3oFzUNdiPq5I4OLYbmQWJ06rIFTg,98932
415
+ unique_toolkit/services/chat_service.py,sha256=umPHKDn0FX7vG7y7Tw-88iMultPpw_r0fYlhQlqvLYY,100196
416
416
  unique_toolkit/services/factory.py,sha256=7gNeanyHkgX23Iz7RMCQTCkE9gI5asCI11nXxM0jjx4,9832
417
417
  unique_toolkit/services/knowledge_base.py,sha256=6dM4abcXR9y2VYoR6SK79gwjrzb83Wm8jMPPIt5z95o,43461
418
418
  unique_toolkit/short_term_memory/__init__.py,sha256=2mI3AUrffgH7Yt-xS57EGqnHf7jnn6xquoKEhJqk3Wg,185
@@ -423,6 +423,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
423
423
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
424
424
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
425
425
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
426
- unique_toolkit-2026.40.0.dev1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
427
- unique_toolkit-2026.40.0.dev1.dist-info/METADATA,sha256=BfWkREvwGUnak2qn1etchW2-YJZloIknxqqA_bOd-jA,7821
428
- unique_toolkit-2026.40.0.dev1.dist-info/RECORD,,
426
+ unique_toolkit-2026.40.0.dev3.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
427
+ unique_toolkit-2026.40.0.dev3.dist-info/METADATA,sha256=5lKBF9LaItGW_BMKf--Pm1cu_WyBA_yMPHpWd9XPrHs,7821
428
+ unique_toolkit-2026.40.0.dev3.dist-info/RECORD,,