Этап 6: отдельные запросы вместо диалога

- Каждый запрос уходит в модель отдельно: системный промпт, текущие дата и время, запрос
- Окно уточнений: цепочка обменов с паузами меньше follow_up_seconds (по умолчанию 120 с),
  не больше трёх последних
- Окно чата стало журналом с временем и пометкой уточнений, «Очистить журнал» вместо «Новый диалог»
- Настройка окна уточнений в окне настроек
- Тесты контекста уточнений и сборки запроса

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nid
2026-09-17 04:19:33 +03:00
co-authored by Claude Opus 5
parent 20a666e0d4
commit 542389c0f9
9 changed files with 218 additions and 66 deletions
+67 -10
View File
@@ -1,19 +1,76 @@
from agr_assistent.core.assistant import build_messages
from datetime import datetime
from agr_assistent.core.context import (
MAX_FOLLOW_UP_EXCHANGES,
FollowUpContext,
build_messages,
format_datetime,
)
def _history(count: int) -> list[dict[str, str]]:
return [{"role": "user", "content": str(i)} for i in range(count)]
class FakeClock:
def __init__(self) -> None:
self.now = 1000.0
def __call__(self) -> float:
return self.now
def test_build_messages_adds_system_prompt_and_trims_history() -> None:
messages = build_messages(" be nice ", _history(5), max_history_messages=2)
def test_build_messages_adds_date_context_and_request() -> None:
context = [{"role": "user", "content": "погода?"}, {"role": "assistant", "content": "солнце"}]
messages = build_messages(" будь краток ", context, "а завтра?", datetime(2026, 9, 17, 4, 5))
assert messages == [
{"role": "system", "content": "be nice"},
{"role": "user", "content": "3"},
{"role": "user", "content": "4"},
{"role": "system", "content": "будь краток\n\nСейчас четверг, 17 сентября 2026 года, 04:05."},
*context,
{"role": "user", "content": "а завтра?"},
]
def test_build_messages_without_limit_or_prompt() -> None:
assert build_messages("", _history(3), max_history_messages=0) == _history(3)
def test_format_datetime() -> None:
assert format_datetime(datetime(2026, 3, 1, 23, 59)) == "воскресенье, 1 марта 2026 года, 23:59"
def test_follow_up_is_available_only_within_window() -> None:
clock = FakeClock()
context = FollowUpContext(window_seconds=120, clock=clock)
context.remember("погода?", "солнце")
clock.now += 119
assert [m["content"] for m in context.recent()] == ["погода?", "солнце"]
clock.now += 2
assert context.recent() == []
def test_follow_up_chain_extends_window_and_keeps_last_exchanges() -> None:
clock = FakeClock()
context = FollowUpContext(window_seconds=60, clock=clock)
for index in range(MAX_FOLLOW_UP_EXCHANGES + 2):
context.remember(f"вопрос {index}", f"ответ {index}")
clock.now += 50 # каждый следующий вопрос успевает в окно предыдущего
requests = [m["content"] for m in context.recent() if m["role"] == "user"]
assert requests == [f"вопрос {i}" for i in range(2, MAX_FOLLOW_UP_EXCHANGES + 2)]
def test_chain_restarts_after_pause() -> None:
clock = FakeClock()
context = FollowUpContext(window_seconds=60, clock=clock)
context.remember("старый", "ответ")
clock.now += 61
context.remember("новый", "ответ")
assert [m["content"] for m in context.recent()] == ["новый", "ответ"]
def test_zero_window_disables_follow_ups_and_empty_replies_are_ignored() -> None:
clock = FakeClock()
assert FollowUpContext(window_seconds=0, clock=clock).recent() == []
context = FollowUpContext(window_seconds=60, clock=clock)
context.remember("вопрос", " ")
assert context.recent() == []