added memmory
This commit is contained in:
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
@@ -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] = {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user