unique_orchestrator 2026.30.0.dev4__tar.gz → 2026.30.0.dev5__tar.gz
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.
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/PKG-INFO +2 -2
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/pyproject.toml +2 -2
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +28 -4
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_loop_debug_params.py +119 -3
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/unique_ai.py +68 -13
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/README.md +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/__init__.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/_builders/__init__.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/_builders/history_manager.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/_builders/open_file_setup.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/_builders/skill_setup.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/config.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/settings.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_agent_config_rjsf_ui_schema.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_config_services_validators.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/tests/test_utils_resolve_other_options.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/unique_ai_builder.py +0 -0
- {unique_orchestrator-2026.30.0.dev4 → unique_orchestrator-2026.30.0.dev5}/unique_orchestrator/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-orchestrator
|
|
3
|
-
Version: 2026.30.0.
|
|
3
|
+
Version: 2026.30.0.dev5
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Andreas Hauri
|
|
6
6
|
Author-email: Andreas Hauri <andreas.hauri@unique.ai>
|
|
@@ -9,7 +9,7 @@ Requires-Dist: pydantic>=2.8.2,<3
|
|
|
9
9
|
Requires-Dist: pydantic-settings>=2.10.1,<3
|
|
10
10
|
Requires-Dist: typing-extensions>=4.9.0,<5
|
|
11
11
|
Requires-Dist: jinja2>=3.1.0,<4
|
|
12
|
-
Requires-Dist: unique-toolkit>=2026.30.0.
|
|
12
|
+
Requires-Dist: unique-toolkit>=2026.30.0.dev15,<2026.30.0rc0
|
|
13
13
|
Requires-Dist: unique-stock-ticker>=2026.30.0.dev0,<2026.30.0rc0
|
|
14
14
|
Requires-Dist: unique-follow-up-questions>=2026.30.0.dev0,<2026.30.0rc0
|
|
15
15
|
Requires-Dist: unique-internal-search>=2026.30.0.dev0,<2026.30.0rc0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_orchestrator"
|
|
3
|
-
version = "2026.30.0.
|
|
3
|
+
version = "2026.30.0.dev5"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Proprietary" }
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
"pydantic-settings>=2.10.1,<3",
|
|
14
14
|
"typing-extensions>=4.9.0,<5",
|
|
15
15
|
"jinja2>=3.1.0,<4",
|
|
16
|
-
"unique-toolkit>=2026.30.0.
|
|
16
|
+
"unique-toolkit>=2026.30.0.dev15,<2026.30.0rc0",
|
|
17
17
|
"unique-stock-ticker>=2026.30.0.dev0,<2026.30.0rc0",
|
|
18
18
|
"unique-follow-up-questions>=2026.30.0.dev0,<2026.30.0rc0",
|
|
19
19
|
"unique-internal-search>=2026.30.0.dev0,<2026.30.0rc0",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
|
+
from datetime import datetime, timezone
|
|
2
3
|
from unittest.mock import AsyncMock, MagicMock
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -735,6 +736,27 @@ class TestRunExecutionTimingIntegration:
|
|
|
735
736
|
assert "loop_iterations" in payload
|
|
736
737
|
assert "total_time" in payload
|
|
737
738
|
|
|
739
|
+
@pytest.mark.ai
|
|
740
|
+
@pytest.mark.asyncio
|
|
741
|
+
async def test_run__analytics__includes_total_time_to_answer_ms(
|
|
742
|
+
self, monkeypatch
|
|
743
|
+
) -> None:
|
|
744
|
+
"""
|
|
745
|
+
Purpose: Verify analytics measures from user creation through completion.
|
|
746
|
+
Why this matters: Internal loop timing excludes queueing and completion latency.
|
|
747
|
+
Setup summary: Return a known completion timestamp and assert the full delta.
|
|
748
|
+
"""
|
|
749
|
+
ua = self._build_run_ua(monkeypatch)
|
|
750
|
+
ua._event.payload.user_message.created_at = "2026-07-13T20:00:00Z"
|
|
751
|
+
ua._chat_service.modify_assistant_message_async.return_value = MagicMock(
|
|
752
|
+
completed_at=datetime(2026, 7, 13, 20, 0, 2, 345000, tzinfo=timezone.utc)
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
await ua.run()
|
|
756
|
+
|
|
757
|
+
analytics_call = ua._debug_info_manager.add_analytics.call_args
|
|
758
|
+
assert analytics_call.kwargs["total_time_to_answer_ms"] == 2345
|
|
759
|
+
|
|
738
760
|
@pytest.mark.ai
|
|
739
761
|
@pytest.mark.asyncio
|
|
740
762
|
async def test_run__execution_times__contains_one_entry_after_single_iteration(
|
|
@@ -836,10 +858,10 @@ class TestRunExecutionTimingIntegration:
|
|
|
836
858
|
) -> None:
|
|
837
859
|
"""
|
|
838
860
|
Purpose: Verify that a failure in update_debug_info_async propagates to the caller.
|
|
839
|
-
Why this matters:
|
|
840
|
-
|
|
861
|
+
Why this matters: Debug failures remain visible without preventing answer
|
|
862
|
+
completion.
|
|
841
863
|
Setup summary: Make update_debug_info_async raise; run; assert RuntimeError
|
|
842
|
-
propagates
|
|
864
|
+
propagates after modify_assistant_message_async completes the answer.
|
|
843
865
|
"""
|
|
844
866
|
ua = self._build_run_ua(monkeypatch)
|
|
845
867
|
ua._chat_service.update_debug_info_async.side_effect = RuntimeError("boom")
|
|
@@ -847,7 +869,9 @@ class TestRunExecutionTimingIntegration:
|
|
|
847
869
|
with pytest.raises(RuntimeError, match="boom"):
|
|
848
870
|
await ua.run()
|
|
849
871
|
|
|
850
|
-
ua._chat_service.modify_assistant_message_async.
|
|
872
|
+
ua._chat_service.modify_assistant_message_async.assert_awaited_once_with(
|
|
873
|
+
set_completed_at=True
|
|
874
|
+
)
|
|
851
875
|
|
|
852
876
|
@pytest.mark.ai
|
|
853
877
|
@pytest.mark.asyncio
|
|
@@ -76,6 +76,7 @@ def _make_loop_response() -> MagicMock:
|
|
|
76
76
|
response.message = MagicMock()
|
|
77
77
|
response.message.references = []
|
|
78
78
|
response.message.text = "response text"
|
|
79
|
+
response.message.original_text = "original response text"
|
|
79
80
|
response.is_empty.return_value = False
|
|
80
81
|
return response
|
|
81
82
|
|
|
@@ -256,6 +257,9 @@ class TestRunLoopDebugParams:
|
|
|
256
257
|
mock_chat_service.cancellation = mock_cancellation
|
|
257
258
|
mock_chat_service.get_debug_info_async = AsyncMock(return_value={})
|
|
258
259
|
mock_chat_service.update_debug_info_async = AsyncMock(return_value=None)
|
|
260
|
+
# Returns None by default → no completed_at, so total_time_to_answer_ms
|
|
261
|
+
# resolves to None. Tests that exercise timing override this return value
|
|
262
|
+
# with a MagicMock(completed_at=...).
|
|
259
263
|
mock_chat_service.modify_assistant_message_async = AsyncMock(return_value=None)
|
|
260
264
|
mock_chat_service.create_assistant_message_async = AsyncMock(
|
|
261
265
|
return_value=MagicMock(id="assist_new")
|
|
@@ -279,6 +283,9 @@ class TestRunLoopDebugParams:
|
|
|
279
283
|
mock_config = MagicMock()
|
|
280
284
|
mock_config.effective_max_loop_iterations = 1
|
|
281
285
|
mock_config.agent.prompt_config.user_metadata = []
|
|
286
|
+
mock_config.space.language_model.name = "AZURE_GPT_5_2025_0807"
|
|
287
|
+
mock_config.space.language_model.family = "openai"
|
|
288
|
+
mock_config.space.language_model.provider = "AZURE"
|
|
282
289
|
|
|
283
290
|
mock_history_manager = MagicMock()
|
|
284
291
|
mock_history_manager.get_history_for_model_call = AsyncMock(
|
|
@@ -345,6 +352,10 @@ class TestRunLoopDebugParams:
|
|
|
345
352
|
as the first argument.
|
|
346
353
|
"""
|
|
347
354
|
ua = self._build_run_ua(monkeypatch)
|
|
355
|
+
tool = MagicMock()
|
|
356
|
+
tool.name = "InternalSearch"
|
|
357
|
+
tool.display_name.return_value = "Knowledge Base Search"
|
|
358
|
+
ua._tool_manager.available_tools = [tool]
|
|
348
359
|
|
|
349
360
|
await ua.run()
|
|
350
361
|
|
|
@@ -353,14 +364,119 @@ class TestRunLoopDebugParams:
|
|
|
353
364
|
]
|
|
354
365
|
assert "loop_params" in keys_written
|
|
355
366
|
assert "skills" in keys_written
|
|
367
|
+
# This test owns one contract: run() forwards the same skills value it
|
|
368
|
+
# wrote under "skills" as the first positional arg to add_analytics.
|
|
369
|
+
# Per-field kwargs (references / timing / lengths) are covered by the
|
|
370
|
+
# focused sibling tests below, so we don't re-pin the whole call here.
|
|
356
371
|
skills_call = next(
|
|
357
372
|
c
|
|
358
373
|
for c in ua._debug_info_manager.add.call_args_list
|
|
359
374
|
if c.args[0] == "skills"
|
|
360
375
|
)
|
|
361
|
-
ua._debug_info_manager.add_analytics.
|
|
362
|
-
|
|
363
|
-
|
|
376
|
+
ua._debug_info_manager.add_analytics.assert_called_once()
|
|
377
|
+
analytics_call = ua._debug_info_manager.add_analytics.call_args
|
|
378
|
+
assert analytics_call.args[0] == skills_call.args[1]
|
|
379
|
+
assert analytics_call.kwargs["tool_display_names"] == {
|
|
380
|
+
"InternalSearch": "Knowledge Base Search"
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@pytest.mark.ai
|
|
384
|
+
@pytest.mark.asyncio
|
|
385
|
+
async def test_run__analytics__counts_unique_reference_sources(
|
|
386
|
+
self, monkeypatch
|
|
387
|
+
) -> None:
|
|
388
|
+
ua = self._build_run_ua(monkeypatch)
|
|
389
|
+
references = [
|
|
390
|
+
MagicMock(url="source_1"),
|
|
391
|
+
MagicMock(url="source_1"),
|
|
392
|
+
MagicMock(url="source_2"),
|
|
393
|
+
MagicMock(url="source_3"),
|
|
394
|
+
MagicMock(url="source_3"),
|
|
395
|
+
]
|
|
396
|
+
ua._loop_iteration_runner.return_value.message.references = references
|
|
397
|
+
ua._reference_manager.get_references.return_value = [references]
|
|
398
|
+
|
|
399
|
+
await ua.run()
|
|
400
|
+
|
|
401
|
+
analytics_call = ua._debug_info_manager.add_analytics.call_args
|
|
402
|
+
assert analytics_call.kwargs["references"] == 3
|
|
403
|
+
|
|
404
|
+
@pytest.mark.ai
|
|
405
|
+
@pytest.mark.asyncio
|
|
406
|
+
async def test_run__analytics__counts_references_across_iterations(
|
|
407
|
+
self, monkeypatch
|
|
408
|
+
) -> None:
|
|
409
|
+
"""
|
|
410
|
+
Purpose: Verify analytics includes reference sources from every loop iteration.
|
|
411
|
+
Why this matters: Earlier citations belong to the same run and must not be
|
|
412
|
+
omitted when a later iteration appends another reference batch.
|
|
413
|
+
Setup summary: Return two stored batches with one duplicate source and assert
|
|
414
|
+
that all three distinct sources are counted.
|
|
415
|
+
"""
|
|
416
|
+
ua = self._build_run_ua(monkeypatch)
|
|
417
|
+
first_iteration_references = [
|
|
418
|
+
MagicMock(url="source_1"),
|
|
419
|
+
MagicMock(url="source_2"),
|
|
420
|
+
]
|
|
421
|
+
latest_iteration_references = [
|
|
422
|
+
MagicMock(url="source_2"),
|
|
423
|
+
MagicMock(url="source_3"),
|
|
424
|
+
]
|
|
425
|
+
ua._reference_manager.get_references.return_value = [
|
|
426
|
+
first_iteration_references,
|
|
427
|
+
latest_iteration_references,
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
await ua.run()
|
|
431
|
+
|
|
432
|
+
analytics_call = ua._debug_info_manager.add_analytics.call_args
|
|
433
|
+
assert analytics_call.kwargs["references"] == 3
|
|
434
|
+
|
|
435
|
+
@pytest.mark.ai
|
|
436
|
+
@pytest.mark.asyncio
|
|
437
|
+
async def test_run__analytics__counts_distinct_references_without_urls(
|
|
438
|
+
self, monkeypatch
|
|
439
|
+
) -> None:
|
|
440
|
+
"""
|
|
441
|
+
Purpose: Verify URL-less references are identified by source and source ID.
|
|
442
|
+
Why this matters: MCP citations can omit URLs and must not collapse into one
|
|
443
|
+
analytics source.
|
|
444
|
+
Setup summary: Run with two distinct URL-less references and assert both count.
|
|
445
|
+
"""
|
|
446
|
+
ua = self._build_run_ua(monkeypatch)
|
|
447
|
+
references = [
|
|
448
|
+
MagicMock(url=None, source="mcp", source_id="resource_1"),
|
|
449
|
+
MagicMock(url=None, source="mcp", source_id="resource_2"),
|
|
450
|
+
]
|
|
451
|
+
ua._loop_iteration_runner.return_value.message.references = references
|
|
452
|
+
ua._reference_manager.get_references.return_value = [references]
|
|
453
|
+
|
|
454
|
+
await ua.run()
|
|
455
|
+
|
|
456
|
+
analytics_call = ua._debug_info_manager.add_analytics.call_args
|
|
457
|
+
assert analytics_call.kwargs["references"] == 2
|
|
458
|
+
|
|
459
|
+
@pytest.mark.ai
|
|
460
|
+
@pytest.mark.asyncio
|
|
461
|
+
async def test_run__analytics__total_time_to_answer_ms_none_without_completed_at(
|
|
462
|
+
self, monkeypatch
|
|
463
|
+
) -> None:
|
|
464
|
+
"""
|
|
465
|
+
Purpose: Verify total_time_to_answer_ms is None when the completed message
|
|
466
|
+
carries no completed_at.
|
|
467
|
+
Why this matters: Makes the None case deliberate — run() must degrade to
|
|
468
|
+
None (not crash) when completion time is unavailable, e.g. a tool took
|
|
469
|
+
control. Pairs with test_run__analytics__includes_total_time_to_answer_ms
|
|
470
|
+
(execution-timing suite) which covers the populated case.
|
|
471
|
+
Setup summary: modify_assistant_message_async returns None (fixture default);
|
|
472
|
+
assert the kwarg is present and None.
|
|
473
|
+
"""
|
|
474
|
+
ua = self._build_run_ua(monkeypatch)
|
|
475
|
+
|
|
476
|
+
await ua.run()
|
|
477
|
+
|
|
478
|
+
analytics_call = ua._debug_info_manager.add_analytics.call_args
|
|
479
|
+
assert analytics_call.kwargs["total_time_to_answer_ms"] is None
|
|
364
480
|
|
|
365
481
|
@pytest.mark.ai
|
|
366
482
|
@pytest.mark.asyncio
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
4
|
from logging import Logger
|
|
5
5
|
from typing import Any, cast, overload
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@ import jinja2
|
|
|
8
8
|
from typing_extensions import deprecated
|
|
9
9
|
from unique_skill_tool.service import SkillTool
|
|
10
10
|
from unique_toolkit.agentic.debug_info_manager.debug_info_manager import (
|
|
11
|
+
AnalyticsLanguageModel,
|
|
11
12
|
DebugInfoManager,
|
|
12
13
|
)
|
|
13
14
|
from unique_toolkit.agentic.evaluation.evaluation_manager import EvaluationManager
|
|
@@ -261,9 +262,8 @@ class UniqueAI:
|
|
|
261
262
|
self._finalize_loop_timing(loop_start)
|
|
262
263
|
break
|
|
263
264
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
)
|
|
265
|
+
references = loop_response.message.references or []
|
|
266
|
+
self._reference_manager.add_references(references)
|
|
267
267
|
self._last_assistant_text = (
|
|
268
268
|
loop_response.message.original_text or loop_response.message.text
|
|
269
269
|
)
|
|
@@ -318,12 +318,53 @@ class UniqueAI:
|
|
|
318
318
|
self._debug_info_manager.add("loop_params", self._loop_debug_params)
|
|
319
319
|
skills_debug_info = self._get_activated_skills_debug_info()
|
|
320
320
|
self._debug_info_manager.add("skills", skills_debug_info)
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
reference_sources = {
|
|
322
|
+
("url", reference.url)
|
|
323
|
+
if reference.url is not None
|
|
324
|
+
else ("source", reference.source, reference.source_id)
|
|
325
|
+
for references in self._reference_manager.get_references()
|
|
326
|
+
for reference in references
|
|
327
|
+
}
|
|
328
|
+
language_model = self._config.space.language_model
|
|
329
|
+
tool_display_names = {
|
|
330
|
+
str(tool.name): tool.display_name() or str(tool.name)
|
|
331
|
+
for tool in self._tool_manager.available_tools
|
|
332
|
+
}
|
|
323
333
|
tool_names = [
|
|
324
334
|
tool["name"] for tool in self._debug_info_manager.get()["tools"]
|
|
325
335
|
]
|
|
326
336
|
|
|
337
|
+
total_time_to_answer_ms: int | None = None
|
|
338
|
+
if not self._chat_service.cancellation.is_cancelled:
|
|
339
|
+
if self._config.agent.input_token_distribution.enable_tool_call_persistence:
|
|
340
|
+
await self._persist_tool_calls()
|
|
341
|
+
completed_message = (
|
|
342
|
+
await self._chat_service.modify_assistant_message_async(
|
|
343
|
+
set_completed_at=not self._tool_took_control,
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
total_time_to_answer_ms = self._calculate_total_time_to_answer_ms(
|
|
347
|
+
user_message_created_at=self._event.payload.user_message.created_at,
|
|
348
|
+
assistant_completed_at=getattr(
|
|
349
|
+
completed_message, "completed_at", None
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
self._debug_info_manager.add_analytics(
|
|
354
|
+
skills_debug_info,
|
|
355
|
+
language_model=AnalyticsLanguageModel(
|
|
356
|
+
name=str(language_model.name),
|
|
357
|
+
family=str(language_model.family),
|
|
358
|
+
provider=str(language_model.provider),
|
|
359
|
+
),
|
|
360
|
+
tool_display_names=tool_display_names,
|
|
361
|
+
references=len(reference_sources),
|
|
362
|
+
user_prompt_length=len(self._event.payload.user_message.text),
|
|
363
|
+
answer_length=len(self._last_assistant_text or ""),
|
|
364
|
+
loop_iteration_count=len(self._execution_times),
|
|
365
|
+
total_time_to_answer_ms=total_time_to_answer_ms,
|
|
366
|
+
)
|
|
367
|
+
|
|
327
368
|
# Get current debug info from chat service and add debug info from run. Do not update if DeepResearch is in the tool names.
|
|
328
369
|
if "DeepResearch" not in tool_names:
|
|
329
370
|
debug_info = {
|
|
@@ -331,16 +372,30 @@ class UniqueAI:
|
|
|
331
372
|
**self._debug_info_manager.get(),
|
|
332
373
|
}
|
|
333
374
|
await self._chat_service.update_debug_info_async(debug_info=debug_info)
|
|
334
|
-
|
|
335
|
-
if not self._chat_service.cancellation.is_cancelled:
|
|
336
|
-
if self._config.agent.input_token_distribution.enable_tool_call_persistence:
|
|
337
|
-
await self._persist_tool_calls()
|
|
338
|
-
await self._chat_service.modify_assistant_message_async(
|
|
339
|
-
set_completed_at=not self._tool_took_control,
|
|
340
|
-
)
|
|
341
375
|
finally:
|
|
342
376
|
sub.cancel()
|
|
343
377
|
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _calculate_total_time_to_answer_ms(
|
|
380
|
+
user_message_created_at: str,
|
|
381
|
+
assistant_completed_at: datetime | None,
|
|
382
|
+
) -> int | None:
|
|
383
|
+
if assistant_completed_at is None:
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
user_created_at = datetime.fromisoformat(user_message_created_at)
|
|
388
|
+
except (ValueError, TypeError):
|
|
389
|
+
# ValueError: malformed timestamp; TypeError: created_at not a str.
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
if user_created_at.tzinfo is None:
|
|
393
|
+
user_created_at = user_created_at.replace(tzinfo=timezone.utc)
|
|
394
|
+
if assistant_completed_at.tzinfo is None:
|
|
395
|
+
assistant_completed_at = assistant_completed_at.replace(tzinfo=timezone.utc)
|
|
396
|
+
|
|
397
|
+
return round((assistant_completed_at - user_created_at).total_seconds() * 1000)
|
|
398
|
+
|
|
344
399
|
def _record_loop_debug_params(self, other_options: dict) -> None:
|
|
345
400
|
reasoning = other_options.get("reasoning")
|
|
346
401
|
thinking_level: str = (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|