fixed timer
This commit is contained in:
@@ -5,21 +5,40 @@ from sqlalchemy import select
|
|||||||
from app.db.base import SessionLocal
|
from app.db.base import SessionLocal
|
||||||
from app.db.models import ChatSession, Message
|
from app.db.models import ChatSession, Message
|
||||||
|
|
||||||
|
DISPLAY_ONLY_ROLES = frozenset({"notice", "character"})
|
||||||
|
|
||||||
|
|
||||||
|
def _latest_chat_session(db) -> ChatSession:
|
||||||
|
session = db.scalar(
|
||||||
|
select(ChatSession).order_by(ChatSession.updated_at.desc()).limit(1)
|
||||||
|
)
|
||||||
|
if not session:
|
||||||
|
session = ChatSession(title="Уведомления")
|
||||||
|
db.add(session)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(session)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
def post_notice_to_latest_chat(content: str) -> int | None:
|
def post_notice_to_latest_chat(content: str) -> int | None:
|
||||||
"""Сохраняет notice в последний активный чат. Возвращает session_id."""
|
"""Сохраняет notice в последний активный чат. Возвращает session_id."""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
session = db.scalar(
|
session = _latest_chat_session(db)
|
||||||
select(ChatSession).order_by(ChatSession.updated_at.desc()).limit(1)
|
|
||||||
)
|
|
||||||
if not session:
|
|
||||||
session = ChatSession(title="Уведомления")
|
|
||||||
db.add(session)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(session)
|
|
||||||
db.add(Message(session_id=session.id, role="notice", content=content))
|
db.add(Message(session_id=session.id, role="notice", content=content))
|
||||||
db.commit()
|
db.commit()
|
||||||
return session.id
|
return session.id
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def post_character_comment_to_latest_chat(content: str) -> int | None:
|
||||||
|
"""Реплика персонажа в UI; не попадает в контекст LLM (в отличие от assistant)."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
session = _latest_chat_session(db)
|
||||||
|
db.add(Message(session_id=session.id, role="character", content=content))
|
||||||
|
db.commit()
|
||||||
|
return session.id
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from app.config import get_settings
|
|||||||
from app.db.base import SessionLocal
|
from app.db.base import SessionLocal
|
||||||
from app.character.service import CharacterService
|
from app.character.service import CharacterService
|
||||||
from app.chat.history import sanitize_openai_messages, strip_historical_reasoning
|
from app.chat.history import sanitize_openai_messages, strip_historical_reasoning
|
||||||
|
from app.chat.notice_inbox import DISPLAY_ONLY_ROLES
|
||||||
from app.chat.notices import (
|
from app.chat.notices import (
|
||||||
POMODORO_TOOL_NAMES,
|
POMODORO_TOOL_NAMES,
|
||||||
format_pomodoro_context,
|
format_pomodoro_context,
|
||||||
@@ -112,7 +113,7 @@ class ChatService:
|
|||||||
|
|
||||||
def _build_messages(self, session: ChatSession) -> list[dict[str, Any]]:
|
def _build_messages(self, session: ChatSession) -> list[dict[str, Any]]:
|
||||||
system_prompt = self._build_system_prompt(session.id)
|
system_prompt = self._build_system_prompt(session.id)
|
||||||
all_chat = [m for m in session.messages if m.role != "notice"]
|
all_chat = [m for m in session.messages if m.role not in DISPLAY_ONLY_ROLES]
|
||||||
last_user = next((m.content for m in reversed(all_chat) if m.role == "user"), "")
|
last_user = next((m.content for m in reversed(all_chat) if m.role == "user"), "")
|
||||||
if last_user:
|
if last_user:
|
||||||
memory_snapshot = get_memory_snapshot(self.db, session.id)
|
memory_snapshot = get_memory_snapshot(self.db, session.id)
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import logging
|
|||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.chat.notice_inbox import post_notice_to_latest_chat
|
from app.character.service import CharacterService
|
||||||
|
from app.chat.notice_inbox import post_character_comment_to_latest_chat, post_notice_to_latest_chat
|
||||||
from app.chat.notices import format_phase_completed_notice
|
from app.chat.notices import format_phase_completed_notice
|
||||||
from app.db.models import PomodoroSession
|
from app.db.models import PomodoroSession
|
||||||
|
from app.llm.client import LLMClient
|
||||||
from app.pomodoro.cycle import PHASE_LONG_BREAK, PHASE_SHORT_BREAK, PHASE_WORK, CycleManager
|
from app.pomodoro.cycle import PHASE_LONG_BREAK, PHASE_SHORT_BREAK, PHASE_WORK, CycleManager
|
||||||
from app.pomodoro.service import PomodoroService
|
from app.pomodoro.service import PomodoroService
|
||||||
|
|
||||||
@@ -22,6 +24,38 @@ class PomodoroCompletionHandler:
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.pomodoro = PomodoroService(db)
|
self.pomodoro = PomodoroService(db)
|
||||||
self.cycle = CycleManager(db)
|
self.cycle = CycleManager(db)
|
||||||
|
self.llm = LLMClient()
|
||||||
|
self.character = CharacterService()
|
||||||
|
|
||||||
|
async def _generate_llm_comment(
|
||||||
|
self,
|
||||||
|
session: PomodoroSession,
|
||||||
|
next_phase: str | None,
|
||||||
|
) -> str:
|
||||||
|
cycle = self.cycle.to_dict()
|
||||||
|
phase_label = PHASE_LABELS.get(session.phase, session.phase)
|
||||||
|
next_label = PHASE_LABELS.get(next_phase, "пауза") if next_phase else "отдых, цикл сброшен"
|
||||||
|
work_done = cycle["completed_work_sessions"]
|
||||||
|
if session.phase == PHASE_WORK:
|
||||||
|
work_done += 1
|
||||||
|
|
||||||
|
system = self.character.get_system_prompt()
|
||||||
|
user_prompt = f"""Фаза помидоро «{phase_label}» только что завершилась.
|
||||||
|
Задача: {session.task_note or 'без описания'}
|
||||||
|
Прогресс цикла: {work_done}/{cycle['sessions_until_long_break']} работ.
|
||||||
|
Следующая фаза: {next_label}.
|
||||||
|
|
||||||
|
Напиши пользователю короткое сообщение (2-4 предложения) на русском: поздравь, поддержи или предложи отдохнуть. Без markdown и без эмодзи."""
|
||||||
|
|
||||||
|
result = await self.llm.complete(
|
||||||
|
[
|
||||||
|
{"role": "system", "content": system},
|
||||||
|
{"role": "user", "content": user_prompt},
|
||||||
|
],
|
||||||
|
temperature=0.8,
|
||||||
|
visible_reply=True,
|
||||||
|
)
|
||||||
|
return (result.get("content") or "").strip() or "Фаза завершена. Хорошая работа."
|
||||||
|
|
||||||
def _resolve_next_phase(self, session: PomodoroSession) -> str | None:
|
def _resolve_next_phase(self, session: PomodoroSession) -> str | None:
|
||||||
phase = session.phase
|
phase = session.phase
|
||||||
@@ -42,11 +76,16 @@ class PomodoroCompletionHandler:
|
|||||||
|
|
||||||
next_phase = self._resolve_next_phase(session)
|
next_phase = self._resolve_next_phase(session)
|
||||||
notice = format_phase_completed_notice(session, next_phase)
|
notice = format_phase_completed_notice(session, next_phase)
|
||||||
|
|
||||||
# Только notice — role=assistant ломает tool/reasoning цепочки OpenRouter.
|
|
||||||
post_notice_to_latest_chat(notice)
|
post_notice_to_latest_chat(notice)
|
||||||
|
|
||||||
|
try:
|
||||||
|
comment = await self._generate_llm_comment(session, next_phase)
|
||||||
|
if comment:
|
||||||
|
post_character_comment_to_latest_chat(comment)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Pomodoro LLM comment failed (phase=%s)", session.phase)
|
||||||
|
|
||||||
self.cycle.bump_notify_seq()
|
self.cycle.bump_notify_seq()
|
||||||
self.pomodoro.mark_notified(session)
|
self.pomodoro.mark_notified(session)
|
||||||
self.pomodoro.advance_after_completion(session)
|
self.pomodoro.advance_after_completion(session)
|
||||||
logger.info("Pomodoro phase completed notice posted (phase=%s)", session.phase)
|
logger.info("Pomodoro phase completed (phase=%s, next=%s)", session.phase, next_phase)
|
||||||
|
|||||||
@@ -27,10 +27,16 @@ function noticeLabel(content: string): string {
|
|||||||
|
|
||||||
function roleLabel(role: string, content = ""): string {
|
function roleLabel(role: string, content = ""): string {
|
||||||
if (role === "notice") return noticeLabel(content);
|
if (role === "notice") return noticeLabel(content);
|
||||||
|
if (role === "character") return "assistant";
|
||||||
if (role === "user") return "вы";
|
if (role === "user") return "вы";
|
||||||
return role;
|
return role;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function messageClassName(role: string): string {
|
||||||
|
if (role === "character") return "assistant";
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
const [activeId, setActiveId] = useState<number | null>(null);
|
const [activeId, setActiveId] = useState<number | null>(null);
|
||||||
@@ -273,10 +279,10 @@ export default function Chat() {
|
|||||||
onClick={dismissKeyboard}
|
onClick={dismissKeyboard}
|
||||||
>
|
>
|
||||||
{visibleMessages.map((msg) => (
|
{visibleMessages.map((msg) => (
|
||||||
<div key={msg.id} className={`message message-${msg.role}`}>
|
<div key={msg.id} className={`message message-${messageClassName(msg.role)}`}>
|
||||||
<div className="message-role">{roleLabel(msg.role, msg.content)}</div>
|
<div className="message-role">{roleLabel(msg.role, msg.content)}</div>
|
||||||
<div className="message-content">
|
<div className="message-content">
|
||||||
{msg.role === "assistant" || msg.role === "notice" ? (
|
{msg.role === "assistant" || msg.role === "notice" || msg.role === "character" ? (
|
||||||
<ReactMarkdown>{msg.content}</ReactMarkdown>
|
<ReactMarkdown>{msg.content}</ReactMarkdown>
|
||||||
) : (
|
) : (
|
||||||
msg.content
|
msg.content
|
||||||
|
|||||||
Reference in New Issue
Block a user