added memmory

This commit is contained in:
2026-06-10 08:23:45 +03:00
parent 2c86a634bb
commit 5a9d26fbf4
13 changed files with 629 additions and 11 deletions
+33 -1
View File
@@ -165,9 +165,41 @@ frontend/ React + Vite, чат и таймер
data/ SQLite БД (создаётся автоматически) 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`
- Проактивные чаты по расписанию - Проактивные чаты по расписанию
- Фитнес-трекер - Фитнес-трекер
+2 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter 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 = APIRouter(prefix="/api/v1")
api_router.include_router(health.router, tags=["health"]) 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(pomodoro.router, prefix="/pomodoro", tags=["pomodoro"])
api_router.include_router(character.router, tags=["character"]) api_router.include_router(character.router, tags=["character"])
api_router.include_router(projects.router, tags=["projects"]) api_router.include_router(projects.router, tags=["projects"])
api_router.include_router(memory.router, tags=["memory"])
api_router.include_router(webhooks.router, tags=["webhooks"]) api_router.include_router(webhooks.router, tags=["webhooks"])
+101
View File
@@ -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
+4 -1
View File
@@ -12,7 +12,10 @@ TOOLS_INSTRUCTIONS = """
- «Какие задачи» / «покажи задачи проекта» → list_taiga_tasks (живые данные Taiga). - «Какие задачи» / «покажи задачи проекта» → list_taiga_tasks (живые данные Taiga).
- list_work_items — ТОЛЬКО задачи, созданные через create_work_item (локальная БД). - list_work_items — ТОЛЬКО задачи, созданные через create_work_item (локальная БД).
- create_work_item — при «заведи баг/фичу»; передай полный текст и project_slug. - create_work_item — при «заведи баг/фичу»; передай полный текст и project_slug.
- Снимок проектов/задач есть в контексте, но для актуализации вызывай tools. Никогда не пиши «ожидаю ответа от системы». - Память: remember_fact, recall_memories, forget_memory, update_profile, update_session_summary.
- «Запомни» → remember_fact. «Что помнишь» → recall_memories или снимок памяти в контексте.
- Снимок проектов/задач и памяти есть в контексте, но для записи/поиска вызывай tools.
- Никогда не пиши «ожидаю ответа от системы».
""".strip() """.strip()
DEFAULT_CARD: dict[str, Any] = { DEFAULT_CARD: dict[str, Any] = {
+30 -1
View File
@@ -47,9 +47,18 @@ POMODORO_TOOL_NAMES = frozenset({
"get_pomodoro_history", "get_pomodoro_history",
}) })
MEMORY_TOOL_NAMES = frozenset({
"remember_fact",
"recall_memories",
"forget_memory",
"update_profile",
"update_session_summary",
})
# Не засорять чат служебными ответами # Не засорять чат служебными ответами
TOOLS_SKIP_CHAT_NOTICE = frozenset({ TOOLS_SKIP_CHAT_NOTICE = frozenset({
"get_pomodoro_status", "get_pomodoro_status",
"recall_memories",
}) })
@@ -63,7 +72,12 @@ def format_tool_notice(tool_name: str, raw_result: str) -> str | None:
return None return None
if isinstance(data, dict) and "error" in data: 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']}" return f"{prefix} {data['error']}"
if tool_name == "reset_pomodoro_cycle": 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}") lines.append(f"- `{p.get('slug')}`: {p.get('name')} · Gitea: {gitea}")
return "\n".join(lines) 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 return None
+18 -6
View File
@@ -11,6 +11,7 @@ from app.chat.notices import (
format_pomodoro_context, format_pomodoro_context,
format_tool_notice, 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.projects.context import format_projects_context, get_projects_snapshot
from app.db.models import ChatSession, Message from app.db.models import ChatSession, Message
from app.llm.client import LLMClient 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 from app.tools.registry import TOOL_DEFINITIONS, execute_tool
MAX_TOOL_ROUNDS = 5 MAX_TOOL_ROUNDS = 5
MAX_HISTORY_MESSAGES = 40
class ChatService: class ChatService:
@@ -48,23 +50,31 @@ class ChatService:
self.db.commit() self.db.commit()
return True 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() status = PomodoroService(self.db).get_status()
memory_snapshot = get_memory_snapshot(self.db, session_id)
projects_snapshot = get_projects_snapshot(self.db) projects_snapshot = get_projects_snapshot(self.db)
return ( return (
f"{self.character.get_system_prompt()}\n\n" 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_pomodoro_context(status)}\n\n"
f"{format_projects_context(projects_snapshot)}" f"{format_projects_context(projects_snapshot)}"
) )
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)
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]] = [ messages: list[dict[str, Any]] = [
{"role": "system", "content": self._build_system_prompt()} {"role": "system", "content": system_prompt}
] ]
for msg in session.messages: chat_messages = all_chat[-MAX_HISTORY_MESSAGES:] if len(all_chat) > MAX_HISTORY_MESSAGES else all_chat
if msg.role == "notice":
continue
for msg in chat_messages:
content = msg.content or None content = msg.content or None
entry: dict[str, Any] = {"role": msg.role, "content": content} entry: dict[str, Any] = {"role": msg.role, "content": content}
if msg.tool_calls_json: if msg.tool_calls_json:
@@ -136,7 +146,9 @@ class ChatService:
for tool_call in tool_calls: for tool_call in tool_calls:
fn = tool_call["function"] fn = tool_call["function"]
args = LLMClient.parse_tool_arguments(fn.get("arguments", "")) 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 = { tool_message = {
"role": "tool", "role": "tool",
"tool_call_id": tool_call["id"], "tool_call_id": tool_call["id"],
+42
View File
@@ -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): class WorkItem(Base):
__tablename__ = "work_items" __tablename__ = "work_items"
View File
+58
View File
@@ -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)
+204
View File
@@ -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),
}
+130 -1
View File
@@ -3,6 +3,7 @@ from typing import Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.memory.service import MemoryService
from app.pomodoro.service import PomodoroService from app.pomodoro.service import PomodoroService
from app.projects.service import ProjectService 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", "type": "function",
"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) pomodoro = PomodoroService(db)
projects = ProjectService(db) projects = ProjectService(db)
memory = MemoryService(db)
try: try:
if name == "get_pomodoro_status": 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), limit=arguments.get("limit", 20),
status=arguments.get("status"), 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: else:
return json.dumps({"error": f"Unknown tool: {name}"}, ensure_ascii=False) return json.dumps({"error": f"Unknown tool: {name}"}, ensure_ascii=False)
+6
View File
@@ -11,3 +11,9 @@
Когда спрашивает что делал — get_pomodoro_history. Когда спрашивает что делал — get_pomodoro_history.
Не выдумывай данные о таймере — всегда используй инструменты. Не выдумывай данные о таймере — всегда используй инструменты.
Память:
- «Запомни» → remember_fact
- «Что ты помнишь» → recall_memories или факты из контекста
- Имя, часовой пояс → update_profile
- Не выдумывай факты о пользователе
+1
View File
@@ -15,6 +15,7 @@ function noticeLabel(content: string): string {
if (content.startsWith("⏱")) return "таймер"; if (content.startsWith("⏱")) return "таймер";
if (content.startsWith("📋")) return "задачи"; if (content.startsWith("📋")) return "задачи";
if (content.startsWith("🔀")) return "git"; if (content.startsWith("🔀")) return "git";
if (content.startsWith("🧠")) return "память";
return "система"; return "система";
} }