Files
Home_assistant/backend/app/chat/notices.py
2026-06-09 14:18:02 +03:00

246 lines
8.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from typing import Any
from app.db.models import PomodoroSession
from app.pomodoro.cycle import PHASE_LONG_BREAK, PHASE_SHORT_BREAK, PHASE_WORK
PHASE_LABELS = {
PHASE_WORK: "Работа",
PHASE_SHORT_BREAK: "Короткий перерыв",
PHASE_LONG_BREAK: "Длинный перерыв",
}
def _format_time(seconds: int) -> str:
minutes, secs = divmod(max(0, seconds), 60)
return f"{minutes:02d}:{secs:02d}"
def format_phase_completed_notice(
session: PomodoroSession,
next_phase: str | None,
) -> str:
phase_label = PHASE_LABELS.get(session.phase, session.phase)
task = session.task_note or "без описания"
lines = [f"⏱ **{phase_label} завершена** · {session.duration_min} мин · _{task}_"]
if next_phase == PHASE_SHORT_BREAK:
lines.append("Дальше: короткий перерыв ☕")
elif next_phase == PHASE_LONG_BREAK:
lines.append("Дальше: длинный перерыв 🌴 · цикл почти завершён")
elif next_phase == PHASE_WORK:
lines.append("Дальше: снова работа 💪")
else:
lines.append("Цикл сброшен. Можно отдохнуть и начать заново.")
return "\n".join(lines)
POMODORO_TOOL_NAMES = frozenset({
"get_pomodoro_status",
"start_pomodoro",
"start_short_break",
"start_long_break",
"stop_pomodoro",
"skip_pomodoro_phase",
"reset_pomodoro_cycle",
"get_pomodoro_history",
})
# Не засорять чат служебными ответами
TOOLS_SKIP_CHAT_NOTICE = frozenset({
"get_pomodoro_status",
})
def format_tool_notice(tool_name: str, raw_result: str) -> str | None:
if tool_name in TOOLS_SKIP_CHAT_NOTICE:
return None
try:
data = json.loads(raw_result)
except json.JSONDecodeError:
return None
if isinstance(data, dict) and "error" in data:
prefix = "" if tool_name in POMODORO_TOOL_NAMES else "📋"
return f"{prefix} {data['error']}"
if tool_name == "reset_pomodoro_cycle":
cycle = data.get("cycle", data)
return (
"⏱ **Цикл помидоро сброшен** · "
f"прогресс: {cycle.get('completed_work_sessions', 0)}/"
f"{cycle.get('sessions_until_long_break', 4)}"
)
if tool_name in (
"get_pomodoro_status",
"start_pomodoro",
"start_work",
"start_short_break",
"start_long_break",
"stop_pomodoro",
"skip_pomodoro_phase",
):
return _format_status_notice(data)
if tool_name == "get_pomodoro_history":
return _format_history_notice(data)
if tool_name == "create_work_item":
return _format_work_item_notice(data)
if tool_name == "list_work_items":
return _format_work_items_list_notice(data)
if tool_name == "list_taiga_tasks":
return _format_taiga_tasks_notice(data)
if tool_name == "sync_taiga_projects":
return f"📋 Синхронизировано проектов Taiga: **{len(data)}**"
if tool_name == "list_taiga_projects":
if not isinstance(data, list) or not data:
return "📋 Проекты Taiga не найдены. Вызовите sync_taiga_projects."
lines = ["📋 **Проекты:**"]
for p in data:
gitea = f"{p.get('gitea_owner')}/{p.get('gitea_repo')}" if p.get("gitea_configured") else ""
lines.append(f"- `{p.get('slug')}`: {p.get('name')} · Gitea: {gitea}")
return "\n".join(lines)
return None
def _format_work_item_notice(data: dict[str, Any]) -> str | None:
if data.get("error"):
return f"📋 {data['error']}"
if not data.get("ok"):
return None
taiga = data.get("taiga", {})
gitea = data.get("gitea", {})
lines = [
"📋 **Создано:**",
f"- Taiga: #{taiga.get('ref')}{taiga.get('subject')}",
f"- URL: {taiga.get('url')}",
]
if gitea.get("url"):
lines.append(f"- Gitea: {gitea.get('url')}")
if data.get("branch"):
lines.append(f"- Ветка: `{data['branch']}`")
subtasks = data.get("subtasks") or []
if subtasks:
lines.append("**Подзадачи:**")
for t in subtasks:
lines.append(f"- #{t.get('ref')} {t.get('subject')}")
return "\n".join(lines)
def _format_work_items_list_notice(data: Any) -> str | None:
if not isinstance(data, list) or not data:
return "📋 Локальных work items (созданных ассистентом) нет."
lines = ["📋 **Work items ассистента:**"]
for item in data[:15]:
lines.append(
f"- [{item.get('status')}] #{item.get('taiga_ref')} {item.get('title')} "
f"({item.get('taiga_slug')})"
)
return "\n".join(lines)
def _format_taiga_tasks_notice(data: Any) -> str | None:
if not isinstance(data, dict):
return None
if data.get("error"):
return f"📋 {data['error']}"
blocks = data.get("projects") or []
total_stories = data.get("total_stories", 0)
total_tasks = data.get("total_tasks", 0)
if not blocks or (total_stories == 0 and total_tasks == 0):
slug = blocks[0].get("slug") if len(blocks) == 1 else None
if slug:
return f"📋 В `{slug}` нет открытых user stories и tasks в Taiga."
return "📋 Открытых задач в Taiga не найдено."
lines = [f"📋 **Открытые задачи Taiga** (stories: {total_stories}, tasks: {total_tasks}):"]
for block in blocks:
stories = block.get("stories") or []
tasks = block.get("tasks") or []
if not stories and not tasks:
continue
lines.append(f"**{block.get('name')}** (`{block.get('slug')}`):")
for s in stories:
lines.append(f"- story #{s.get('ref')} {s.get('subject')}")
for t in tasks:
lines.append(f"- task #{t.get('ref')} {t.get('subject')}")
return "\n".join(lines)
def _format_status_notice(data: dict[str, Any]) -> str:
status = data.get("status", "idle")
phase = data.get("phase", PHASE_WORK)
phase_label = PHASE_LABELS.get(phase, phase)
task = data.get("task_note") or "без описания"
remaining = data.get("remaining_seconds", 0)
duration = data.get("duration_min", 25)
cycle = data.get("cycle", {})
cycle_info = ""
if cycle:
cycle_info = (
f" · цикл {cycle.get('completed_work_sessions', 0)}/"
f"{cycle.get('sessions_until_long_break', 4)}"
)
if status == "idle":
return f"⏱ **Помидоро:** таймер не запущен{cycle_info}."
if status == "running":
return (
f"⏱ **{phase_label}** · осталось **{_format_time(remaining)}** "
f"из {duration} мин · _{task}_{cycle_info}"
)
if status == "paused":
elapsed = data.get("elapsed_seconds", 0)
return (
f"⏱ **{phase_label} на паузе** · прошло {_format_time(elapsed)} "
f"из {duration} мин · _{task}_{cycle_info}"
)
if status == "completed":
return f"⏱ **{phase_label} завершена** · {duration} мин · _{task}_"
if status == "cancelled":
return f"⏱ **{phase_label} отменена** · _{task}_"
return f"⏱ Помидоро: {status}"
def _format_history_notice(data: Any) -> str:
if not isinstance(data, list) or not data:
return "⏱ **История помидоро** пуста."
lines = ["⏱ **История помидоро:**"]
for item in data[:10]:
task = item.get("task_note") or "без описания"
phase = PHASE_LABELS.get(item.get("phase", ""), item.get("phase", "?"))
duration = item.get("duration_min", "?")
lines.append(f"- {phase}: {task} ({duration} мин)")
return "\n".join(lines)
def format_pomodoro_context(status: dict[str, Any]) -> str:
notice = _format_status_notice(status)
cycle = status.get("cycle", {})
extra = ""
if cycle:
extra = (
f"\nНастройки цикла: работа {cycle.get('work_duration_min')} мин, "
f"перерыв {cycle.get('short_break_min')} мин, "
f"длинный {cycle.get('long_break_min')} мин."
)
return f"[Актуальный статус помидоро]\n{notice}{extra}"