diff --git a/README.md b/README.md index 6e78b60..c66306a 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,41 @@ frontend/ React + Vite, чат и таймер data/ SQLite БД (создаётся автоматически) ``` +## Память и контекст (фаза 3a) + +Долгосрочная память в SQLite, без векторов: + +| Слой | Что хранит | +|------|------------| +| **Профиль** | имя, timezone, language, notes | +| **Факты** | устойчивые знания с категорией и важностью | +| **Сводка чата** | краткое содержание длинной сессии | + +В system prompt на каждый ответ: персонаж → **память** → помидоро → проекты. +История чата обрезается до 40 последних сообщений; раннее — в `session_summaries`. + +### Tools + +- `remember_fact` — «запомни, что…» +- `recall_memories` — поиск по памяти +- `forget_memory` — удалить факт по id +- `update_profile` — имя, часовой пояс и т.д. +- `update_session_summary` — сжать тему длинного чата + +### API + +| Method | Path | Описание | +|--------|------|----------| +| GET | `/api/v1/memory` | снимок памяти (+ `?session_id=`) | +| GET/PUT | `/api/v1/profile` | профиль | +| GET/POST | `/api/v1/memory/facts` | список / создать факт | +| DELETE | `/api/v1/memory/facts/{id}` | забыть | +| PUT | `/api/v1/memory/sessions/{id}/summary` | сводка чата | + ## Следующие фазы -- RAG с Qdrant для документов +- Qdrant: семантический поиск по фактам и документам +- RAG: загрузка файлов, `search_documents` - Проактивные чаты по расписанию - Фитнес-трекер diff --git a/backend/app/api/routes/__init__.py b/backend/app/api/routes/__init__.py index 1bd22b9..335372c 100644 --- a/backend/app/api/routes/__init__.py +++ b/backend/app/api/routes/__init__.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.routes import character, chat, health, pomodoro, projects, webhooks +from app.api.routes import character, chat, health, memory, pomodoro, projects, webhooks api_router = APIRouter(prefix="/api/v1") api_router.include_router(health.router, tags=["health"]) @@ -8,4 +8,5 @@ api_router.include_router(chat.router, prefix="/chat", tags=["chat"]) api_router.include_router(pomodoro.router, prefix="/pomodoro", tags=["pomodoro"]) api_router.include_router(character.router, tags=["character"]) api_router.include_router(projects.router, tags=["projects"]) +api_router.include_router(memory.router, tags=["memory"]) api_router.include_router(webhooks.router, tags=["webhooks"]) diff --git a/backend/app/api/routes/memory.py b/backend/app/api/routes/memory.py new file mode 100644 index 0000000..5277d05 --- /dev/null +++ b/backend/app/api/routes/memory.py @@ -0,0 +1,101 @@ +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field +from sqlalchemy.orm import Session + +from app.db.base import get_db +from app.memory.service import MemoryService + +router = APIRouter() + + +class ProfileUpdate(BaseModel): + updates: dict[str, Any] = Field(default_factory=dict) + + +class FactCreate(BaseModel): + content: str = Field(min_length=1) + category: str = "fact" + importance: int = Field(default=3, ge=1, le=5) + session_id: int | None = None + + +class SessionSummaryUpdate(BaseModel): + summary: str = Field(min_length=1) + message_count: int = 0 + + +@router.get("/memory") +def get_memory_snapshot( + session_id: int | None = None, + db: Session = Depends(get_db), +) -> dict[str, Any]: + return MemoryService(db).snapshot(session_id) + + +@router.get("/profile") +def get_profile(db: Session = Depends(get_db)) -> dict[str, Any]: + return MemoryService(db).get_profile() + + +@router.put("/profile") +def update_profile( + payload: ProfileUpdate, + db: Session = Depends(get_db), +) -> dict[str, Any]: + try: + return MemoryService(db).update_profile(payload.updates) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@router.get("/memory/facts") +def list_facts( + query: str | None = None, + category: str | None = None, + limit: int = 30, + db: Session = Depends(get_db), +) -> list[dict[str, Any]]: + return MemoryService(db).recall_memories(query=query, category=category, limit=limit) + + +@router.post("/memory/facts") +def create_fact( + payload: FactCreate, + db: Session = Depends(get_db), +) -> dict[str, Any]: + try: + return MemoryService(db).remember_fact( + payload.content, + category=payload.category, + session_id=payload.session_id, + importance=payload.importance, + source="api", + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@router.delete("/memory/facts/{memory_id}") +def forget_fact(memory_id: int, db: Session = Depends(get_db)) -> dict[str, Any]: + try: + return MemoryService(db).forget_memory(memory_id) + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@router.put("/memory/sessions/{session_id}/summary") +def update_session_summary( + session_id: int, + payload: SessionSummaryUpdate, + db: Session = Depends(get_db), +) -> dict[str, Any]: + try: + return MemoryService(db).update_session_summary( + session_id, + payload.summary, + message_count=payload.message_count, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc diff --git a/backend/app/character/card.py b/backend/app/character/card.py index 48e4acf..5397d73 100644 --- a/backend/app/character/card.py +++ b/backend/app/character/card.py @@ -12,7 +12,10 @@ TOOLS_INSTRUCTIONS = """ - «Какие задачи» / «покажи задачи проекта» → list_taiga_tasks (живые данные Taiga). - list_work_items — ТОЛЬКО задачи, созданные через create_work_item (локальная БД). - create_work_item — при «заведи баг/фичу»; передай полный текст и project_slug. -- Снимок проектов/задач есть в контексте, но для актуализации вызывай tools. Никогда не пиши «ожидаю ответа от системы». +- Память: remember_fact, recall_memories, forget_memory, update_profile, update_session_summary. +- «Запомни» → remember_fact. «Что помнишь» → recall_memories или снимок памяти в контексте. +- Снимок проектов/задач и памяти есть в контексте, но для записи/поиска вызывай tools. +- Никогда не пиши «ожидаю ответа от системы». """.strip() DEFAULT_CARD: dict[str, Any] = { diff --git a/backend/app/chat/notices.py b/backend/app/chat/notices.py index 6e54121..570dd4c 100644 --- a/backend/app/chat/notices.py +++ b/backend/app/chat/notices.py @@ -47,9 +47,18 @@ POMODORO_TOOL_NAMES = frozenset({ "get_pomodoro_history", }) +MEMORY_TOOL_NAMES = frozenset({ + "remember_fact", + "recall_memories", + "forget_memory", + "update_profile", + "update_session_summary", +}) + # Не засорять чат служебными ответами TOOLS_SKIP_CHAT_NOTICE = frozenset({ "get_pomodoro_status", + "recall_memories", }) @@ -63,7 +72,12 @@ def format_tool_notice(tool_name: str, raw_result: str) -> str | None: return None if isinstance(data, dict) and "error" in data: - prefix = "⏱" if tool_name in POMODORO_TOOL_NAMES else "📋" + if tool_name in POMODORO_TOOL_NAMES: + prefix = "⏱" + elif tool_name in MEMORY_TOOL_NAMES: + prefix = "🧠" + else: + prefix = "📋" return f"{prefix} {data['error']}" if tool_name == "reset_pomodoro_cycle": @@ -109,6 +123,21 @@ def format_tool_notice(tool_name: str, raw_result: str) -> str | None: lines.append(f"- `{p.get('slug')}`: {p.get('name')} · Gitea: {gitea}") return "\n".join(lines) + if tool_name == "remember_fact" and data.get("ok"): + action = "обновлено" if data.get("action") == "updated" else "сохранено" + return f"🧠 **Память {action}** · #{data.get('memory_id')}: {data.get('content')}" + + if tool_name == "forget_memory" and data.get("ok"): + return f"🧠 **Забыто** · #{data.get('memory_id')}: {data.get('forgotten')}" + + if tool_name == "update_profile" and data.get("ok"): + profile = data.get("profile") or {} + parts = [f"{k}={v}" for k, v in profile.items() if v] + return f"🧠 **Профиль обновлён** · {', '.join(parts) or 'пусто'}" + + if tool_name == "update_session_summary" and data.get("ok"): + return "🧠 **Сводка чата сохранена**" + return None diff --git a/backend/app/chat/service.py b/backend/app/chat/service.py index 2975451..de73c9a 100644 --- a/backend/app/chat/service.py +++ b/backend/app/chat/service.py @@ -11,6 +11,7 @@ from app.chat.notices import ( format_pomodoro_context, format_tool_notice, ) +from app.memory.context import format_memory_context, get_memory_snapshot from app.projects.context import format_projects_context, get_projects_snapshot from app.db.models import ChatSession, Message from app.llm.client import LLMClient @@ -18,6 +19,7 @@ from app.pomodoro.service import PomodoroService from app.tools.registry import TOOL_DEFINITIONS, execute_tool MAX_TOOL_ROUNDS = 5 +MAX_HISTORY_MESSAGES = 40 class ChatService: @@ -48,23 +50,31 @@ class ChatService: self.db.commit() return True - def _build_system_prompt(self) -> str: + def _build_system_prompt(self, session_id: int | None = None) -> str: status = PomodoroService(self.db).get_status() + memory_snapshot = get_memory_snapshot(self.db, session_id) projects_snapshot = get_projects_snapshot(self.db) return ( f"{self.character.get_system_prompt()}\n\n" + f"{format_memory_context(memory_snapshot)}\n\n" f"{format_pomodoro_context(status)}\n\n" f"{format_projects_context(projects_snapshot)}" ) def _build_messages(self, session: ChatSession) -> list[dict[str, Any]]: + system_prompt = self._build_system_prompt(session.id) + all_chat = [m for m in session.messages if m.role != "notice"] + if len(all_chat) > MAX_HISTORY_MESSAGES: + system_prompt += ( + f"\n\n[История чата: в контексте последние {MAX_HISTORY_MESSAGES} " + f"из {len(all_chat)} сообщений. Раннее — в сводке сессии, если сохранена.]" + ) messages: list[dict[str, Any]] = [ - {"role": "system", "content": self._build_system_prompt()} + {"role": "system", "content": system_prompt} ] - for msg in session.messages: - if msg.role == "notice": - continue + chat_messages = all_chat[-MAX_HISTORY_MESSAGES:] if len(all_chat) > MAX_HISTORY_MESSAGES else all_chat + for msg in chat_messages: content = msg.content or None entry: dict[str, Any] = {"role": msg.role, "content": content} if msg.tool_calls_json: @@ -136,7 +146,9 @@ class ChatService: for tool_call in tool_calls: fn = tool_call["function"] args = LLMClient.parse_tool_arguments(fn.get("arguments", "")) - result = await execute_tool(self.db, fn["name"], args) + result = await execute_tool( + self.db, fn["name"], args, session_id=session_id + ) tool_message = { "role": "tool", "tool_call_id": tool_call["id"], diff --git a/backend/app/db/models.py b/backend/app/db/models.py index b52ce9e..7a488d9 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -93,6 +93,48 @@ class ProjectBinding(Base): ) +class UserProfile(Base): + __tablename__ = "user_profile" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + data_json: Mapped[str] = mapped_column(Text, default="{}") + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + +class MemoryFact(Base): + __tablename__ = "memory_facts" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + category: Mapped[str] = mapped_column(String(64), default="fact", index=True) + content: Mapped[str] = mapped_column(Text) + source: Mapped[str] = mapped_column(String(32), default="user") + session_id: Mapped[int | None] = mapped_column( + ForeignKey("chat_sessions.id", ondelete="SET NULL"), nullable=True, index=True + ) + importance: Mapped[int] = mapped_column(Integer, default=3) + active: Mapped[bool] = mapped_column(Boolean, default=True, index=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + +class SessionSummary(Base): + __tablename__ = "session_summaries" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + session_id: Mapped[int] = mapped_column( + ForeignKey("chat_sessions.id", ondelete="CASCADE"), unique=True, index=True + ) + summary: Mapped[str] = mapped_column(Text, default="") + message_count: Mapped[int] = mapped_column(Integer, default=0) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + class WorkItem(Base): __tablename__ = "work_items" diff --git a/backend/app/memory/__init__.py b/backend/app/memory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/memory/context.py b/backend/app/memory/context.py new file mode 100644 index 0000000..08310aa --- /dev/null +++ b/backend/app/memory/context.py @@ -0,0 +1,58 @@ +from typing import Any + +from sqlalchemy.orm import Session + +from app.memory.service import MemoryService + +MAX_FACTS_IN_CONTEXT = 25 +PROFILE_KEYS = ("name", "timezone", "language", "notes") + + +def get_memory_snapshot(db: Session, session_id: int | None = None) -> dict[str, Any]: + return MemoryService(db).snapshot(session_id) + + +def format_memory_context(snapshot: dict[str, Any]) -> str: + lines = ["[Память и профиль — долгосрочный контекст]"] + + profile = snapshot.get("profile") or {} + profile_lines = [] + for key in PROFILE_KEYS: + value = (profile.get(key) or "").strip() + if value: + profile_lines.append(f"- {key}: {value}") + if profile_lines: + lines.append("Профиль пользователя:") + lines.extend(profile_lines) + else: + lines.append("Профиль: не заполнен (можно уточнить имя, часовой пояс).") + + summary = (snapshot.get("session_summary") or "").strip() + if summary: + lines.append("") + lines.append("Сводка текущего чата (ранние сообщения):") + lines.append(summary) + + facts = snapshot.get("facts") or [] + if facts: + lines.append("") + lines.append(f"Запомненные факты ({snapshot.get('total_facts', len(facts))}):") + for fact in facts[:MAX_FACTS_IN_CONTEXT]: + lines.append( + f"- [{fact.get('category')}] #{fact.get('id')} {fact.get('content')}" + ) + else: + lines.append("") + lines.append("Запомненные факты: пока нет.") + + lines.append("") + lines.append( + "Правила памяти: " + "«запомни» → remember_fact. " + "«что ты помнишь» → recall_memories или ответ из снимка выше. " + "«забудь #N» → forget_memory. " + "Профиль (имя, timezone) → update_profile. " + "Длинный чат — update_session_summary с краткой сводкой темы. " + "Не выдумывай факты — только то, что в профиле/фактах или сказал пользователь." + ) + return "\n".join(lines) diff --git a/backend/app/memory/service.py b/backend/app/memory/service.py new file mode 100644 index 0000000..ec06657 --- /dev/null +++ b/backend/app/memory/service.py @@ -0,0 +1,204 @@ +import json +from datetime import datetime, timezone +from typing import Any + +from sqlalchemy import func, or_, select +from sqlalchemy.orm import Session + +from app.db.models import MemoryFact, SessionSummary, UserProfile + +DEFAULT_PROFILE: dict[str, Any] = { + "name": "", + "timezone": "", + "language": "ru", + "notes": "", +} + + +class MemoryService: + def __init__(self, db: Session): + self.db = db + + def get_profile(self) -> dict[str, Any]: + row = self.db.scalar(select(UserProfile).limit(1)) + if not row: + return dict(DEFAULT_PROFILE) + try: + data = json.loads(row.data_json or "{}") + except json.JSONDecodeError: + data = {} + merged = dict(DEFAULT_PROFILE) + merged.update(data) + return merged + + def update_profile(self, updates: dict[str, Any]) -> dict[str, Any]: + row = self.db.scalar(select(UserProfile).limit(1)) + if not row: + row = UserProfile(data_json="{}") + self.db.add(row) + self.db.flush() + + current = self.get_profile() + for key, value in updates.items(): + if value is None: + current.pop(key, None) + else: + current[key] = value + + row.data_json = json.dumps(current, ensure_ascii=False) + row.updated_at = datetime.now(timezone.utc) + self.db.commit() + return {"ok": True, "profile": current} + + def remember_fact( + self, + content: str, + *, + category: str = "fact", + source: str = "user", + session_id: int | None = None, + importance: int = 3, + ) -> dict[str, Any]: + text = content.strip() + if not text: + raise ValueError("Пустой факт") + + existing = self.db.scalar( + select(MemoryFact).where( + MemoryFact.active.is_(True), + func.lower(MemoryFact.content) == text.lower(), + ) + ) + if existing: + existing.category = category or existing.category + existing.importance = max(existing.importance, min(5, max(1, importance))) + existing.updated_at = datetime.now(timezone.utc) + if session_id: + existing.session_id = session_id + self.db.commit() + return { + "ok": True, + "action": "updated", + "memory_id": existing.id, + "content": existing.content, + "category": existing.category, + } + + fact = MemoryFact( + category=(category or "fact")[:64], + content=text[:2000], + source=source[:32], + session_id=session_id, + importance=min(5, max(1, importance)), + ) + self.db.add(fact) + self.db.commit() + self.db.refresh(fact) + return { + "ok": True, + "action": "created", + "memory_id": fact.id, + "content": fact.content, + "category": fact.category, + } + + def recall_memories( + self, + *, + query: str | None = None, + category: str | None = None, + limit: int = 20, + active_only: bool = True, + ) -> list[dict[str, Any]]: + stmt = select(MemoryFact).order_by( + MemoryFact.importance.desc(), + MemoryFact.updated_at.desc(), + ) + if active_only: + stmt = stmt.where(MemoryFact.active.is_(True)) + if category: + stmt = stmt.where(MemoryFact.category == category) + if query: + pattern = f"%{query.strip()}%" + stmt = stmt.where( + or_( + MemoryFact.content.ilike(pattern), + MemoryFact.category.ilike(pattern), + ) + ) + facts = self.db.scalars(stmt.limit(min(limit, 50))).all() + return [ + { + "id": f.id, + "category": f.category, + "content": f.content, + "importance": f.importance, + "source": f.source, + "updated_at": f.updated_at.isoformat() if f.updated_at else None, + } + for f in facts + ] + + def forget_memory(self, memory_id: int) -> dict[str, Any]: + fact = self.db.get(MemoryFact, memory_id) + if not fact: + raise ValueError(f"Память #{memory_id} не найдена") + fact.active = False + fact.updated_at = datetime.now(timezone.utc) + self.db.commit() + return {"ok": True, "memory_id": memory_id, "forgotten": fact.content} + + def get_active_facts(self, limit: int = 25) -> list[MemoryFact]: + return list( + self.db.scalars( + select(MemoryFact) + .where(MemoryFact.active.is_(True)) + .order_by(MemoryFact.importance.desc(), MemoryFact.updated_at.desc()) + .limit(limit) + ).all() + ) + + def get_session_summary(self, session_id: int) -> SessionSummary | None: + return self.db.scalar( + select(SessionSummary).where(SessionSummary.session_id == session_id) + ) + + def update_session_summary( + self, + session_id: int, + summary: str, + *, + message_count: int = 0, + ) -> dict[str, Any]: + text = summary.strip() + if not text: + raise ValueError("Пустая сводка") + + row = self.get_session_summary(session_id) + if not row: + row = SessionSummary(session_id=session_id) + self.db.add(row) + + row.summary = text[:4000] + row.message_count = message_count + row.updated_at = datetime.now(timezone.utc) + self.db.commit() + return {"ok": True, "session_id": session_id, "summary": row.summary} + + def snapshot(self, session_id: int | None = None) -> dict[str, Any]: + facts = self.get_active_facts() + summary_row = self.get_session_summary(session_id) if session_id else None + return { + "profile": self.get_profile(), + "facts": [ + { + "id": f.id, + "category": f.category, + "content": f.content, + "importance": f.importance, + } + for f in facts + ], + "session_summary": summary_row.summary if summary_row else "", + "total_facts": len(facts), + } diff --git a/backend/app/tools/registry.py b/backend/app/tools/registry.py index 417e72f..796ef72 100644 --- a/backend/app/tools/registry.py +++ b/backend/app/tools/registry.py @@ -3,6 +3,7 @@ from typing import Any from sqlalchemy.orm import Session +from app.memory.service import MemoryService from app.pomodoro.service import PomodoroService from app.projects.service import ProjectService @@ -173,6 +174,99 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [ }, }, }, + { + "type": "function", + "function": { + "name": "remember_fact", + "description": ( + "Сохранить факт в долгосрочную память. " + "Когда пользователь просит «запомни», или сообщает устойчивое предпочтение/факт." + ), + "parameters": { + "type": "object", + "properties": { + "content": {"type": "string", "description": "Что запомнить"}, + "category": { + "type": "string", + "description": "preference, person, habit, project, fact", + }, + "importance": {"type": "integer", "description": "1-5, по умолчанию 3"}, + }, + "required": ["content"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "recall_memories", + "description": ( + "Поиск в долгосрочной памяти. " + "Когда спрашивают «что ты помнишь», «что я говорил про X»." + ), + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Подстрока для поиска"}, + "category": {"type": "string"}, + "limit": {"type": "integer"}, + }, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "forget_memory", + "description": "Удалить (деактивировать) факт по id из recall_memories или снимка памяти.", + "parameters": { + "type": "object", + "properties": { + "memory_id": {"type": "integer"}, + }, + "required": ["memory_id"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "update_profile", + "description": ( + "Обновить профиль пользователя: name, timezone, language, notes. " + "Передавай только изменившиеся поля." + ), + "parameters": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "timezone": {"type": "string"}, + "language": {"type": "string"}, + "notes": {"type": "string"}, + }, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "update_session_summary", + "description": ( + "Сохранить краткую сводку темы текущего чата " + "(когда диалог длинный или пользователь просит «сожми контекст»)." + ), + "parameters": { + "type": "object", + "properties": { + "summary": {"type": "string", "description": "2-5 предложений о теме чата"}, + "session_id": {"type": "integer"}, + }, + "required": ["summary", "session_id"], + }, + }, + }, { "type": "function", "function": { @@ -194,9 +288,16 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [ ] -async def execute_tool(db: Session, name: str, arguments: dict[str, Any]) -> str: +async def execute_tool( + db: Session, + name: str, + arguments: dict[str, Any], + *, + session_id: int | None = None, +) -> str: pomodoro = PomodoroService(db) projects = ProjectService(db) + memory = MemoryService(db) try: if name == "get_pomodoro_status": @@ -240,6 +341,34 @@ async def execute_tool(db: Session, name: str, arguments: dict[str, Any]) -> str limit=arguments.get("limit", 20), status=arguments.get("status"), ) + elif name == "remember_fact": + result = memory.remember_fact( + arguments.get("content", ""), + category=arguments.get("category", "fact"), + importance=arguments.get("importance", 3), + session_id=session_id, + source="tool", + ) + elif name == "recall_memories": + result = memory.recall_memories( + query=arguments.get("query"), + category=arguments.get("category"), + limit=arguments.get("limit", 20), + ) + elif name == "forget_memory": + result = memory.forget_memory(int(arguments["memory_id"])) + elif name == "update_profile": + updates = { + k: arguments[k] + for k in ("name", "timezone", "language", "notes") + if k in arguments and arguments[k] is not None + } + result = memory.update_profile(updates) + elif name == "update_session_summary": + result = memory.update_session_summary( + int(arguments["session_id"]), + arguments.get("summary", ""), + ) else: return json.dumps({"error": f"Unknown tool: {name}"}, ensure_ascii=False) diff --git a/backend/prompts/assistant.md b/backend/prompts/assistant.md index 8691583..e86008b 100644 --- a/backend/prompts/assistant.md +++ b/backend/prompts/assistant.md @@ -11,3 +11,9 @@ Когда спрашивает что делал — get_pomodoro_history. Не выдумывай данные о таймере — всегда используй инструменты. + +Память: +- «Запомни» → remember_fact +- «Что ты помнишь» → recall_memories или факты из контекста +- Имя, часовой пояс → update_profile +- Не выдумывай факты о пользователе diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx index 1d641a1..2ad5b8b 100644 --- a/frontend/src/pages/Chat.tsx +++ b/frontend/src/pages/Chat.tsx @@ -15,6 +15,7 @@ function noticeLabel(content: string): string { if (content.startsWith("⏱")) return "таймер"; if (content.startsWith("📋")) return "задачи"; if (content.startsWith("🔀")) return "git"; + if (content.startsWith("🧠")) return "память"; return "система"; }