AI Update: روی حالت های تمامی حافظه های موجود پروژه های کاربران رو همر
This commit is contained in:
@@ -84,3 +84,9 @@ apt install htop
|
||||
htop
|
||||
ذفخح
|
||||
btop
|
||||
agyy
|
||||
agy
|
||||
ls
|
||||
cd telegram-agy-bot/
|
||||
ls
|
||||
agy
|
||||
|
||||
@@ -36,5 +36,12 @@
|
||||
## Preferred Programming Language & Tech Stack (PHP)
|
||||
- ALWAYS use **PHP** (e.g. PHP 8.x, modern PHP, Laravel/Symfony/Slim or standalone PHP scripts/APIs) as the primary/default programming language and technology stack for all projects and backend services whenever feasible and applicable, unless the user explicitly requests another language or the nature of the task strictly requires a different technology.
|
||||
|
||||
## Memory & Instruction Precedence Hierarchy
|
||||
- ALWAYS resolve conflicts and prioritize instructions according to the strict 3-tier hierarchy:
|
||||
1. **Tier 1 (Highest Authority):** Global System Rules & Memories (قوانین و دستورات عمومی سیستم) - Always strictly supersedes all private/user/project instructions.
|
||||
2. **Tier 2 (Intermediate Authority):** Active Project Memories & Decisions (دستورات و خاطرات اختصاصی هر پروژه) - Supersedes general user profile preferences when operating inside that project.
|
||||
3. **Tier 3 (Baseline Authority):** General User Personal Preferences (دستورات و ترجیحات کلی کاربر) - Applied when not overridden by project or global rules.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1326,10 +1326,14 @@ class AGYEngine:
|
||||
except Exception as ex:
|
||||
logger.warning(f"Failed to load bot actions instruction: {ex}")
|
||||
|
||||
# AI Long-Term Persistent Memory Instruction (Global + User Private)
|
||||
# AI Long-Term Persistent Hierarchical Memory (Global + User + Active Project)
|
||||
try:
|
||||
from memory_manager import memory_manager
|
||||
memory_instruction = memory_manager.format_memories_for_prompt(user_id=session.chat_id)
|
||||
active_p_name = session.active_project or (session.current_project.name if session.current_project else None)
|
||||
memory_instruction = memory_manager.format_memories_for_prompt(
|
||||
user_id=session.chat_id,
|
||||
project_name=active_p_name,
|
||||
)
|
||||
if memory_instruction:
|
||||
system_instructions.append(memory_instruction)
|
||||
except Exception as mex:
|
||||
|
||||
+131
-36
@@ -1105,19 +1105,26 @@ def process_ai_schedule_actions(raw_text: str, chat_id: int, project_name: str,
|
||||
return processed_text, created_tasks
|
||||
|
||||
|
||||
def build_memory_menu(chat_id: int, view_type: str = "private", page: int = 0) -> tuple[str, InlineKeyboardMarkup]:
|
||||
"""Builds interactive AI Long-Term Memory manager dashboard with tabs (private vs global), pagination, and deletion."""
|
||||
def build_memory_menu(chat_id: int, view_type: str = "project", page: int = 0) -> tuple[str, InlineKeyboardMarkup]:
|
||||
"""Builds interactive 3-tier AI Memory manager dashboard with tabs (project, user, global), pagination, and deletion."""
|
||||
from memory_manager import memory_manager
|
||||
session = session_manager.get_or_create(chat_id)
|
||||
curr_proj = session_manager.get_current_project(chat_id)
|
||||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||||
is_admin_user = settings.is_admin(chat_id)
|
||||
|
||||
view_type = "global" if view_type == "global" else "private"
|
||||
proj_name = curr_proj.name if curr_proj else "default"
|
||||
|
||||
if view_type == "global":
|
||||
memories = memory_manager.get_global_memories(limit=100)
|
||||
else:
|
||||
# Validate view_type
|
||||
if view_type not in ("project", "user", "global"):
|
||||
view_type = "project" if curr_proj else "user"
|
||||
|
||||
if view_type == "project":
|
||||
memories = memory_manager.get_project_memories(user_id=chat_id, project_name=proj_name, limit=100)
|
||||
elif view_type == "user":
|
||||
memories = memory_manager.get_user_memories(user_id=chat_id, limit=100)
|
||||
else: # global
|
||||
memories = memory_manager.get_global_memories(limit=100)
|
||||
|
||||
total_cnt = len(memories)
|
||||
page_size = 5
|
||||
@@ -1125,19 +1132,29 @@ def build_memory_menu(chat_id: int, view_type: str = "private", page: int = 0) -
|
||||
page = max(0, min(page, max_pages - 1))
|
||||
paged_memories = memories[page * page_size : (page + 1) * page_size]
|
||||
|
||||
stats = memory_manager.get_stats()
|
||||
stats = memory_manager.get_stats(user_id=chat_id, project_name=proj_name)
|
||||
|
||||
if is_fa:
|
||||
tab_name = "🌐 خاطرات عمومی (کل سیستم)" if view_type == "global" else "👤 خاطرات خصوصی (اختصاصی شما)"
|
||||
if view_type == "project":
|
||||
tab_name = f"📁 خاطرات اختصاصی پروژه (<code>{escape_html(proj_name)}</code>)"
|
||||
tab_desc = "این خاطرات <b>فقط هنگام فعالیت در همین پروژه</b> به هوش مصنوعی تزریق میشوند."
|
||||
elif view_type == "user":
|
||||
tab_name = "👤 خاطرات شخصی من (سراسری)"
|
||||
tab_desc = "این خاطرات مربوط به شخص شماست و در <b>تمام پروژهها</b> اعمال میگردد."
|
||||
else:
|
||||
tab_name = "🌐 قوانین و خاطرات عمومی (کل سرور و کاربران)"
|
||||
tab_desc = "این قوانین برای <b>همه کاربران</b> تزریق میشوند (ویرایش و حذف فقط توسط مدیر)."
|
||||
|
||||
header = (
|
||||
f"🧠 <b>کنترلپنل حافظه هوشمند هوش مصنوعی (AI Long-Term Memory)</b>\n\n"
|
||||
f"🧠 <b>کنترلپنل حافظه هوشمند ۳ لایه (AI Memory Dashboard)</b>\n\n"
|
||||
f"• 📂 <b>بخش فعال:</b> {tab_name}\n"
|
||||
f"• 📊 <b>آمار کل:</b> <code>{stats['total_memories']}</code> خاطره (<code>{stats['global_memories']}</code> عمومی | <code>{stats['private_memories']}</code> خصوصی)\n\n"
|
||||
f"• ℹ️ {tab_desc}\n"
|
||||
f"• 📊 <b>آمار:</b> <code>{stats['current_project_memories']}</code> پروژه | <code>{stats['current_user_memories']}</code> کاربر | <code>{stats['global_memories']}</code> عمومی\n\n"
|
||||
)
|
||||
if not memories:
|
||||
body = (
|
||||
f"ℹ️ <i>در این بخش هنوز هیچ خاطرهای ثبت نشده است.</i>\n\n"
|
||||
f"💡 <b>یادگیری خودکار:</b> هوش مصنوعی به صورت خودکار در خلال گفتگوها، عادات، نام و نکات مهم شما را استخراج و ذخیره میکند."
|
||||
f"💡 <b>یادگیری خودکار:</b> هوش مصنوعی در حین گفتگوها نکات مهم مربوط به این بخش را خودکار استخراج و ثبت میکند."
|
||||
)
|
||||
else:
|
||||
body_lines = [f"<b>لیست خاطرات (صفحه {page + 1} از {max_pages}):</b>\n"]
|
||||
@@ -1149,16 +1166,26 @@ def build_memory_menu(chat_id: int, view_type: str = "private", page: int = 0) -
|
||||
)
|
||||
body = "\n\n".join(body_lines)
|
||||
else:
|
||||
tab_name = "🌐 Global System Memory" if view_type == "global" else "👤 Private User Memory"
|
||||
if view_type == "project":
|
||||
tab_name = f"📁 Project Memory (<code>{escape_html(proj_name)}</code>)"
|
||||
tab_desc = "Injected only when working in this project."
|
||||
elif view_type == "user":
|
||||
tab_name = "👤 User Profile Memory"
|
||||
tab_desc = "Applies to you across all your projects."
|
||||
else:
|
||||
tab_name = "🌐 Global System Memory"
|
||||
tab_desc = "Applies to all users (admin-managed)."
|
||||
|
||||
header = (
|
||||
f"🧠 <b>AI Long-Term Memory Dashboard</b>\n\n"
|
||||
f"🧠 <b>3-Tier AI Memory Dashboard</b>\n\n"
|
||||
f"• 📂 <b>Active Tab:</b> {tab_name}\n"
|
||||
f"• 📊 <b>Stats:</b> <code>{stats['total_memories']}</code> total (<code>{stats['global_memories']}</code> global | <code>{stats['private_memories']}</code> private)\n\n"
|
||||
f"• ℹ️ {tab_desc}\n"
|
||||
f"• 📊 <b>Stats:</b> <code>{stats['current_project_memories']}</code> project | <code>{stats['current_user_memories']}</code> user | <code>{stats['global_memories']}</code> global\n\n"
|
||||
)
|
||||
if not memories:
|
||||
body = "ℹ️ <i>No memories recorded in this section yet.</i>\n\n💡 <i>The AI automatically extracts durable facts and preferences during conversations.</i>"
|
||||
body = "ℹ️ <i>No memories recorded in this section yet.</i>"
|
||||
else:
|
||||
body_lines = [f"<b>Memories list (Page {page + 1} of {max_pages}):</b>\n"]
|
||||
body_lines = [f"<b>Memories (Page {page + 1} of {max_pages}):</b>\n"]
|
||||
for idx, m in enumerate(paged_memories, start=page * page_size + 1):
|
||||
cat_badge = f"[{m.category}]" if m.category and m.category != "general" else ""
|
||||
body_lines.append(
|
||||
@@ -1172,20 +1199,23 @@ def build_memory_menu(chat_id: int, view_type: str = "private", page: int = 0) -
|
||||
# Build keyboard
|
||||
keyboard = []
|
||||
|
||||
# 1. Tab buttons
|
||||
tab_priv_text = "👤 خاطرات من (خصوصی) ✓" if view_type == "private" else "👤 خاطرات من (خصوصی)"
|
||||
tab_glob_text = "🌐 خاطرات عمومی سیستم ✓" if view_type == "global" else "🌐 خاطرات عمومی سیستم"
|
||||
# 1. 3 Tab buttons
|
||||
t_proj = f"📁 پروژه ({proj_name[:10]}) ✓" if view_type == "project" else f"📁 پروژه ({proj_name[:10]})"
|
||||
t_user = "👤 کاربر (من) ✓" if view_type == "user" else "👤 کاربر (من)"
|
||||
t_glob = "🌐 عمومی ✓" if view_type == "global" else "🌐 عمومی"
|
||||
if not is_fa:
|
||||
tab_priv_text = "👤 My Memory ✓" if view_type == "private" else "👤 My Memory"
|
||||
tab_glob_text = "🌐 Global Memory ✓" if view_type == "global" else "🌐 Global Memory"
|
||||
t_proj = f"📁 Project ✓" if view_type == "project" else f"📁 Project"
|
||||
t_user = "👤 User ✓" if view_type == "user" else "👤 User"
|
||||
t_glob = "🌐 Global ✓" if view_type == "global" else "🌐 Global"
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(tab_priv_text, callback_data="mem_tab:private:0"),
|
||||
InlineKeyboardButton(tab_glob_text, callback_data="mem_tab:global:0"),
|
||||
InlineKeyboardButton(t_proj, callback_data="mem_tab:project:0"),
|
||||
InlineKeyboardButton(t_user, callback_data="mem_tab:user:0"),
|
||||
InlineKeyboardButton(t_glob, callback_data="mem_tab:global:0"),
|
||||
])
|
||||
|
||||
# 2. Item delete buttons (for paged memories)
|
||||
can_delete_current = (view_type == "private") or (view_type == "global" and is_admin_user)
|
||||
# 2. Item delete buttons
|
||||
can_delete_current = (view_type in ("project", "user")) or (view_type == "global" and is_admin_user)
|
||||
if can_delete_current and paged_memories:
|
||||
del_row = []
|
||||
for idx, m in enumerate(paged_memories, start=page * page_size + 1):
|
||||
@@ -1206,7 +1236,7 @@ def build_memory_menu(chat_id: int, view_type: str = "private", page: int = 0) -
|
||||
pag_row.append(InlineKeyboardButton("بعدی ➡️" if is_fa else "Next ➡️", callback_data=f"mem_tab:{view_type}:{page + 1}"))
|
||||
keyboard.append(pag_row)
|
||||
|
||||
# 4. Clear memory button (if applicable)
|
||||
# 4. Clear memory button
|
||||
action_row = []
|
||||
if can_delete_current and total_cnt > 0:
|
||||
clear_label = "🧹 پاکسازی این بخش" if is_fa else "🧹 Clear this section"
|
||||
@@ -3385,39 +3415,96 @@ async def usage_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
async def memory_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
chat_id = update.effective_chat.id
|
||||
session = session_manager.get_or_create(chat_id)
|
||||
curr_proj = session_manager.get_current_project(chat_id)
|
||||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||||
is_admin_user = settings.is_admin(chat_id)
|
||||
from memory_manager import memory_manager
|
||||
|
||||
if not context.args:
|
||||
text, markup = build_memory_menu(chat_id, view_type="private", page=0)
|
||||
text, markup = build_memory_menu(chat_id, view_type="project" if curr_proj else "user", page=0)
|
||||
await update.message.reply_html(text, reply_markup=markup)
|
||||
return
|
||||
|
||||
subcmd = context.args[0].lower().strip()
|
||||
from memory_manager import memory_manager
|
||||
|
||||
if subcmd in ("project", "proj", "p"):
|
||||
text, markup = build_memory_menu(chat_id, view_type="project", page=0)
|
||||
await update.message.reply_html(text, reply_markup=markup)
|
||||
return
|
||||
|
||||
if subcmd in ("user", "u", "me", "profile", "private"):
|
||||
text, markup = build_memory_menu(chat_id, view_type="user", page=0)
|
||||
await update.message.reply_html(text, reply_markup=markup)
|
||||
return
|
||||
|
||||
if subcmd in ("global", "g", "public", "system"):
|
||||
text, markup = build_memory_menu(chat_id, view_type="global", page=0)
|
||||
await update.message.reply_html(text, reply_markup=markup)
|
||||
return
|
||||
|
||||
if subcmd in ("private", "p", "my", "user"):
|
||||
text, markup = build_memory_menu(chat_id, view_type="private", page=0)
|
||||
await update.message.reply_html(text, reply_markup=markup)
|
||||
# Add memory manually: /memory add <global|user|project> <key> | <content>
|
||||
if subcmd in ("add", "set", "save") and len(context.args) > 2:
|
||||
target_type = context.args[1].lower().strip()
|
||||
if target_type not in ("global", "user", "project"):
|
||||
target_type = "user"
|
||||
rest = " ".join(context.args[1:]).strip()
|
||||
else:
|
||||
rest = " ".join(context.args[2:]).strip()
|
||||
|
||||
if target_type == "global" and not is_admin_user:
|
||||
await update.message.reply_html("🚫 <b>خطای دسترسی: فقط مدیر سیستم میتواند خاطره عمومی ثبت کند.</b>" if is_fa else "🚫 <b>Access Denied.</b>")
|
||||
return
|
||||
|
||||
if "|" in rest:
|
||||
key_part, content_part = rest.split("|", 1)
|
||||
else:
|
||||
parts = rest.split(None, 1)
|
||||
key_part = parts[0]
|
||||
content_part = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
key_clean = key_part.strip().lower()
|
||||
content_clean = content_part.strip()
|
||||
if not key_clean or not content_clean:
|
||||
await update.message.reply_html(
|
||||
"💡 <i>راهنمای افزودن دستی خاطره:</i>\n<code>/memory add [global|user|project] <key> | <محتوا></code>\n\n<b>مثال:</b>\n<code>/memory add global default_php | همیشه از PHP مدرن استفاده شود</code>"
|
||||
if is_fa
|
||||
else "💡 <i>Usage:</i> <code>/memory add [global|user|project] <key> | <content></code>"
|
||||
)
|
||||
return
|
||||
|
||||
p_name = curr_proj.name if (curr_proj and target_type == "project") else "default"
|
||||
item, is_created = memory_manager.save_or_update(
|
||||
type_=target_type,
|
||||
key=key_clean,
|
||||
content=content_clean,
|
||||
user_id=chat_id,
|
||||
project_name=p_name if target_type == "project" else None,
|
||||
category="rule" if target_type == "global" else "preference",
|
||||
importance=5 if target_type == "global" else 3,
|
||||
created_by=chat_id,
|
||||
)
|
||||
msg = f"✅ <b>خاطره با موفقیت ثبت شد:</b>\n• 🏷️ بخش: {item.type}\n• 🔑 کلید: <code>{escape_html(item.key)}</code>\n• 📝 محتوا: {escape_html(item.content)}" if is_fa else f"✅ <b>Memory saved:</b> [{item.type}] {item.key}"
|
||||
text, markup = build_memory_menu(chat_id, view_type=target_type, page=0)
|
||||
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
|
||||
return
|
||||
|
||||
if subcmd in ("clear", "clean", "purge"):
|
||||
target_type = "global" if len(context.args) > 1 and context.args[1].lower() in ("global", "g") else "private"
|
||||
target_type = context.args[1].lower() if len(context.args) > 1 else "project"
|
||||
if target_type not in ("global", "user", "project"):
|
||||
target_type = "project" if curr_proj else "user"
|
||||
p_name = curr_proj.name if curr_proj else "default"
|
||||
try:
|
||||
cnt = memory_manager.clear_memories(type_=target_type, user_id=chat_id, is_admin=is_admin_user)
|
||||
type_label = "عمومی" if target_type == "global" else "خصوصی"
|
||||
cnt = memory_manager.clear_memories(
|
||||
type_=target_type, user_id=chat_id, project_name=p_name, is_admin=is_admin_user
|
||||
)
|
||||
type_label = f"پروژه {p_name}" if target_type == "project" else ("عمومی" if target_type == "global" else "شخصی کاربر")
|
||||
msg = f"🧹 <b>تعداد {cnt} خاطره {type_label} با موفقیت پاکسازی شد.</b>" if is_fa else f"🧹 <b>{cnt} {target_type} memories cleared.</b>"
|
||||
except PermissionError:
|
||||
msg = "🚫 <b>خطای دسترسی: فقط ادمین مجاز به پاکسازی خاطرات عمومی است.</b>" if is_fa else "🚫 <b>Access denied.</b>"
|
||||
await update.message.reply_html(msg)
|
||||
return
|
||||
|
||||
text, markup = build_memory_menu(chat_id, view_type="private", page=0)
|
||||
text, markup = build_memory_menu(chat_id, view_type="project" if curr_proj else "user", page=0)
|
||||
await update.message.reply_html(text, reply_markup=markup)
|
||||
|
||||
|
||||
@@ -4078,6 +4165,7 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
session = session_manager.get_or_create(chat_id)
|
||||
curr_proj = session_manager.get_current_project(chat_id)
|
||||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||||
is_admin_user = settings.is_admin(chat_id) or settings.is_admin(user_id)
|
||||
data = query.data
|
||||
|
||||
if data == "btn_dashboard":
|
||||
@@ -5190,8 +5278,15 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
elif data.startswith("mem_clear_do:"):
|
||||
v_type = data.split(":")[1]
|
||||
from memory_manager import memory_manager
|
||||
curr_p = session_manager.get_current_project(chat_id)
|
||||
p_name = curr_p.name if curr_p else "default"
|
||||
try:
|
||||
cnt = memory_manager.clear_memories(type_=v_type, user_id=chat_id, is_admin=is_admin_user)
|
||||
cnt = memory_manager.clear_memories(
|
||||
type_=v_type,
|
||||
user_id=chat_id,
|
||||
project_name=p_name if v_type == "project" else None,
|
||||
is_admin=is_admin_user,
|
||||
)
|
||||
await query.answer(f"🧹 تعداد {cnt} خاطره پاک شد." if is_fa else f"🧹 Cleared {cnt} memories.")
|
||||
except PermissionError:
|
||||
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
|
||||
|
||||
@@ -1087,11 +1087,13 @@ async def execute_action(
|
||||
elif act in ("SAVE_MEMORY", "STORE_MEMORY", "SET_MEMORY", "ADD_MEMORY"):
|
||||
from memory_manager import memory_manager
|
||||
|
||||
mem_type = (attrs.get("type") or "private").lower().strip()
|
||||
raw_type = (attrs.get("type") or "user").lower().strip()
|
||||
key = attrs.get("key") or attrs.get("slug") or attrs.get("name")
|
||||
content = attrs.get("content") or attrs.get("text") or attrs.get("value") or attrs.get("_default")
|
||||
category = (attrs.get("category") or "general").lower().strip()
|
||||
importance_raw = attrs.get("importance") or "1"
|
||||
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
||||
|
||||
try:
|
||||
importance = int(importance_raw)
|
||||
except Exception:
|
||||
@@ -1106,10 +1108,18 @@ async def execute_action(
|
||||
created_task,
|
||||
)
|
||||
|
||||
# Security check: Non-admins cannot create global memories
|
||||
if mem_type == "global" and not is_admin:
|
||||
logger.warning("Non-admin user %s attempted to create global memory %s; downgraded to private.", chat_id, key)
|
||||
mem_type = "private"
|
||||
# Map types: 'project' -> project tier, 'user'/'private' -> user tier, 'global' -> rejected/downgraded for AI
|
||||
if raw_type in ("project", "proj"):
|
||||
mem_type = "project"
|
||||
proj_name = req_project
|
||||
elif raw_type == "global":
|
||||
# Global memories are strictly bot-managed by Admin. AI cannot auto-save global memories.
|
||||
logger.info("AI attempted global memory save for user %s; safely converting to user memory.", chat_id)
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
else:
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
|
||||
try:
|
||||
item, is_created = memory_manager.save_or_update(
|
||||
@@ -1117,21 +1127,28 @@ async def execute_action(
|
||||
key=key,
|
||||
content=content,
|
||||
user_id=chat_id,
|
||||
project_name=proj_name,
|
||||
category=category,
|
||||
importance=importance,
|
||||
created_by=chat_id,
|
||||
)
|
||||
type_label = "🌐 عمومی (سیستم)" if item.is_global else "👤 خصوصی (شما)"
|
||||
if item.is_project:
|
||||
type_label = f"📁 پروژه اختصاصی (<code>{escape_html(proj_name or '')}</code>)"
|
||||
elif item.is_user:
|
||||
type_label = "👤 کاربر (سراسری برای شما)"
|
||||
else:
|
||||
type_label = "🌐 عمومی (سیستم)"
|
||||
|
||||
badge = (
|
||||
f"\n\n🧠 <b>خاطره هوش مصنوعی ثبت شد:</b>\n"
|
||||
f"• 🏷️ <b>نوع:</b> {type_label}\n"
|
||||
f"• 🏷️ <b>بخش:</b> {type_label}\n"
|
||||
f"• 🔑 <b>کلید:</b> <code>{escape_html(item.key)}</code>\n"
|
||||
f"• 📂 <b>دسته:</b> <code>{escape_html(item.category)}</code>\n"
|
||||
f"• 📝 <b>محتوا:</b> {escape_html(item.content)}"
|
||||
if is_fa
|
||||
else
|
||||
f"\n\n🧠 <b>AI Memory Saved:</b>\n"
|
||||
f"• 🏷️ <b>Type:</b> {'Global' if item.is_global else 'Private'}\n"
|
||||
f"• 🏷️ <b>Scope:</b> {'Project (' + str(proj_name) + ')' if item.is_project else 'User'} \n"
|
||||
f"• 🔑 <b>Key:</b> <code>{escape_html(item.key)}</code>\n"
|
||||
f"• 📂 <b>Category:</b> <code>{escape_html(item.category)}</code>\n"
|
||||
f"• 📝 <b>Content:</b> {escape_html(item.content)}"
|
||||
@@ -1146,11 +1163,12 @@ async def execute_action(
|
||||
elif act in ("UPDATE_MEMORY", "EDIT_MEMORY"):
|
||||
from memory_manager import memory_manager
|
||||
|
||||
mem_type = (attrs.get("type") or "private").lower().strip()
|
||||
raw_type = (attrs.get("type") or "user").lower().strip()
|
||||
key = attrs.get("key") or attrs.get("slug")
|
||||
content = attrs.get("content") or attrs.get("text") or attrs.get("_default")
|
||||
category = attrs.get("category")
|
||||
importance_raw = attrs.get("importance")
|
||||
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
||||
|
||||
if not key or not content:
|
||||
return (
|
||||
@@ -1161,11 +1179,15 @@ async def execute_action(
|
||||
created_task,
|
||||
)
|
||||
|
||||
if mem_type == "global" and not is_admin:
|
||||
mem_type = "private"
|
||||
if raw_type in ("project", "proj"):
|
||||
mem_type = "project"
|
||||
proj_name = req_project
|
||||
else:
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
|
||||
try:
|
||||
existing = memory_manager.get_by_key(type_=mem_type, key=key, user_id=chat_id)
|
||||
existing = memory_manager.get_by_key(type_=mem_type, key=key, user_id=chat_id, project_name=proj_name)
|
||||
cat = category or (existing.category if existing else "general")
|
||||
imp = int(importance_raw) if importance_raw and importance_raw.isdigit() else (existing.importance if existing else 1)
|
||||
item, is_created = memory_manager.save_or_update(
|
||||
@@ -1173,20 +1195,21 @@ async def execute_action(
|
||||
key=key,
|
||||
content=content,
|
||||
user_id=chat_id,
|
||||
project_name=proj_name,
|
||||
category=cat,
|
||||
importance=imp,
|
||||
created_by=chat_id,
|
||||
)
|
||||
type_label = "🌐 عمومی (سیستم)" if item.is_global else "👤 خصوصی (شما)"
|
||||
type_label = f"📁 پروژه (<code>{escape_html(proj_name or '')}</code>)" if item.is_project else "👤 کاربر (سراسری)"
|
||||
badge = (
|
||||
f"\n\n🧠 <b>خاطره هوش مصنوعی بهروزرسانی شد:</b>\n"
|
||||
f"• 🏷️ <b>نوع:</b> {type_label}\n"
|
||||
f"• 🏷️ <b>بخش:</b> {type_label}\n"
|
||||
f"• 🔑 <b>کلید:</b> <code>{escape_html(item.key)}</code>\n"
|
||||
f"• 📝 <b>محتوای جدید:</b> {escape_html(item.content)}"
|
||||
if is_fa
|
||||
else
|
||||
f"\n\n🧠 <b>AI Memory Updated:</b>\n"
|
||||
f"• 🏷️ <b>Type:</b> {'Global' if item.is_global else 'Private'}\n"
|
||||
f"• 🏷️ <b>Scope:</b> {type_label}\n"
|
||||
f"• 🔑 <b>Key:</b> <code>{escape_html(item.key)}</code>\n"
|
||||
f"• 📝 <b>New Content:</b> {escape_html(item.content)}"
|
||||
)
|
||||
@@ -1202,13 +1225,19 @@ async def execute_action(
|
||||
|
||||
mem_id_raw = attrs.get("id")
|
||||
key = attrs.get("key") or attrs.get("slug") or attrs.get("_default")
|
||||
mem_type = (attrs.get("type") or "private").lower().strip()
|
||||
raw_type = (attrs.get("type") or "user").lower().strip()
|
||||
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
||||
|
||||
mem_type = "project" if raw_type in ("project", "proj") else "user"
|
||||
proj_name = req_project if mem_type == "project" else None
|
||||
|
||||
try:
|
||||
if mem_id_raw and mem_id_raw.isdigit():
|
||||
ok = memory_manager.delete_by_id(int(mem_id_raw), user_id=chat_id, is_admin=is_admin)
|
||||
elif key:
|
||||
ok = memory_manager.delete_by_key(type_=mem_type, key=key, user_id=chat_id, is_admin=is_admin)
|
||||
ok = memory_manager.delete_by_key(
|
||||
type_=mem_type, key=key, user_id=chat_id, project_name=proj_name, is_admin=is_admin
|
||||
)
|
||||
else:
|
||||
return "\n⚠️ لطفاً شناسه یا کلید خاطره جهت حذف را مشخص کنید." if is_fa else "\n⚠️ Please specify memory id or key to delete.", side_effects, created_task
|
||||
|
||||
@@ -1217,7 +1246,7 @@ async def execute_action(
|
||||
else:
|
||||
badge = f"\n⚠️ خاطرهای با این مشخصات یافت نشد." if is_fa else f"\n⚠️ Memory not found."
|
||||
except PermissionError:
|
||||
badge = "\n🚫 <i>خطای دسترسی: ویرایش یا حذف خاطرات عمومی تنها توسط مدیر سیستم امکانپذیر است.</i>" if is_fa else "\n🚫 <i>Access Denied: Only administrators can modify global memories.</i>"
|
||||
badge = "\n🚫 <i>خطای دسترسی: فقط مالک یا مدیر میتواند این خاطره را حذف کند.</i>" if is_fa else "\n🚫 <i>Access Denied.</i>"
|
||||
except Exception as e:
|
||||
badge = f"\n⚠️ خطا در حذف خاطره: {escape_html(str(e))}"
|
||||
return badge, side_effects, created_task
|
||||
@@ -1228,18 +1257,36 @@ async def execute_action(
|
||||
elif act in ("CLEAR_MEMORY", "PURGE_MEMORY", "RESET_MEMORY"):
|
||||
from memory_manager import memory_manager
|
||||
|
||||
mem_type = (attrs.get("type") or "private").lower().strip()
|
||||
raw_type = (attrs.get("type") or "user").lower().strip()
|
||||
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
||||
|
||||
if raw_type in ("project", "proj"):
|
||||
mem_type = "project"
|
||||
proj_name = req_project
|
||||
type_label = f"پروژه {proj_name}"
|
||||
elif raw_type == "global":
|
||||
if not is_admin:
|
||||
return "\n🚫 <i>خطای دسترسی: پاکسازی خاطرات عمومی تنها توسط مدیر امکانپذیر است.</i>" if is_fa else "\n🚫 <i>Access Denied: Only admins can clear global memories.</i>", side_effects, created_task
|
||||
mem_type = "global"
|
||||
proj_name = None
|
||||
type_label = "عمومی سیستم"
|
||||
else:
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
type_label = "شخصی کاربر"
|
||||
|
||||
try:
|
||||
count = memory_manager.clear_memories(type_=mem_type, user_id=chat_id, is_admin=is_admin)
|
||||
type_label = "عمومی" if mem_type == "global" else "خصوصی"
|
||||
count = memory_manager.clear_memories(
|
||||
type_=mem_type, user_id=chat_id, project_name=proj_name, is_admin=is_admin
|
||||
)
|
||||
badge = (
|
||||
f"\n\n🧹 <b>حافظه {type_label} پاکسازی شد ({count} مورد حذف گردید).</b>"
|
||||
if is_fa
|
||||
else
|
||||
f"\n\n🧹 <b>{mem_type.capitalize()} memory cleared ({count} items purged).</b>"
|
||||
f"\n\n🧹 <b>{type_label} memory cleared ({count} items purged).</b>"
|
||||
)
|
||||
except PermissionError:
|
||||
badge = "\n🚫 <i>خطای دسترسی: پاکسازی خاطرات عمومی تنها توسط مدیر امکانپذیر است.</i>" if is_fa else "\n🚫 <i>Access Denied: Only admins can clear global memories.</i>"
|
||||
badge = "\n🚫 <i>خطای دسترسی: عملیات مجاز نیست.</i>" if is_fa else "\n🚫 <i>Access Denied.</i>"
|
||||
except Exception as e:
|
||||
badge = f"\n⚠️ خطا در پاکسازی حافظه: {escape_html(str(e))}"
|
||||
return badge, side_effects, created_task
|
||||
@@ -1392,11 +1439,11 @@ def get_ai_bot_actions_instruction(lang: str = "fa") -> str:
|
||||
" [[GIT_INIT: project=\"<project_optional>\"]]\n\n"
|
||||
"17. Restart Telegram Bot Service:\n"
|
||||
" [[RESTART_BOT]]\n\n"
|
||||
"18. Long-Term AI Memory Management:\n"
|
||||
" [[SAVE_MEMORY: type=\"private|global\", key=\"<unique_slug>\", category=\"<category>\", content=\"<concise_summary>\"]]\n"
|
||||
" (Saves durable facts/preferences. type defaults to 'private'. Only admin can create 'global').\n"
|
||||
" [[UPDATE_MEMORY: key=\"<slug>\", content=\"<new_content>\"]]\n"
|
||||
" [[DELETE_MEMORY: key=\"<slug>\"]]\n"
|
||||
" [[CLEAR_MEMORY: type=\"private|global\"]]\n\n"
|
||||
"18. Long-Term AI Memory Management (3-Tier: User & Project):\n"
|
||||
" [[SAVE_MEMORY: type=\"user|project\", key=\"<unique_slug>\", category=\"<category>\", content=\"<concise_summary>\"]]\n"
|
||||
" (Use type='user' for personal user profile/habits, or type='project' for current project architecture/decisions. Global memory is read-only for AI).\n"
|
||||
" [[UPDATE_MEMORY: type=\"user|project\", key=\"<slug>\", content=\"<new_content>\"]]\n"
|
||||
" [[DELETE_MEMORY: type=\"user|project\", key=\"<slug>\"]]\n"
|
||||
" [[CLEAR_MEMORY: type=\"user|project\"]]\n\n"
|
||||
"Always explain to the user in a friendly and professional tone what actions have been performed."
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -17,11 +17,12 @@ DB_PATH = DATA_DIR / "memory.db"
|
||||
@dataclass
|
||||
class MemoryItem:
|
||||
id: int
|
||||
type: str # 'global' or 'private'
|
||||
user_id: Optional[int] # None for global, chat_id for private
|
||||
key: str
|
||||
category: str # 'rule', 'preference', 'tech_stack', 'fact', 'workflow', 'general'
|
||||
content: str
|
||||
type: str # 'global', 'user', 'project'
|
||||
user_id: Optional[int] = None # None for global, chat_id for user/project
|
||||
project_name: Optional[str] = None # None for global/user, project_name for project
|
||||
key: str = ""
|
||||
category: str = "general" # 'rule', 'preference', 'tech_stack', 'fact', 'workflow', 'general'
|
||||
content: str = ""
|
||||
importance: int = 1
|
||||
created_by: Optional[int] = None
|
||||
created_at: float = field(default_factory=time.time)
|
||||
@@ -35,8 +36,12 @@ class MemoryItem:
|
||||
return self.type == "global"
|
||||
|
||||
@property
|
||||
def is_private(self) -> bool:
|
||||
return self.type == "private"
|
||||
def is_user(self) -> bool:
|
||||
return self.type == "user"
|
||||
|
||||
@property
|
||||
def is_project(self) -> bool:
|
||||
return self.type == "project"
|
||||
|
||||
|
||||
class MemoryManager:
|
||||
@@ -53,41 +58,81 @@ class MemoryManager:
|
||||
return conn
|
||||
|
||||
def _init_db(self):
|
||||
"""Initializes tables and indexes if they do not exist."""
|
||||
"""Initializes tables and indexes, migrating from old schema if needed."""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL CHECK(type IN ('global', 'private')),
|
||||
user_id INTEGER,
|
||||
key TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'general',
|
||||
content TEXT NOT NULL,
|
||||
importance INTEGER NOT NULL DEFAULT 1,
|
||||
created_by INTEGER,
|
||||
created_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL
|
||||
);
|
||||
""")
|
||||
# Partial unique indexes to properly handle NULL user_id for global memories
|
||||
# Check if old memories table exists and needs migration
|
||||
cur = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='memories';")
|
||||
table_exists = cur.fetchone() is not None
|
||||
|
||||
if table_exists:
|
||||
# Check columns in existing table
|
||||
cur = conn.execute("PRAGMA table_info(memories);")
|
||||
cols = [row["name"] for row in cur.fetchall()]
|
||||
if "project_name" not in cols:
|
||||
logger.info("Migrating memories table to support 3-tier hierarchy (adding project_name)...")
|
||||
# Migrate table
|
||||
conn.execute("""
|
||||
CREATE TABLE memories_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL CHECK(type IN ('global', 'user', 'project', 'private')),
|
||||
user_id INTEGER,
|
||||
project_name TEXT,
|
||||
key TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'general',
|
||||
content TEXT NOT NULL,
|
||||
importance INTEGER NOT NULL DEFAULT 1,
|
||||
created_by INTEGER,
|
||||
created_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL
|
||||
);
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO memories_new (id, type, user_id, project_name, key, category, content, importance, created_by, created_at, updated_at)
|
||||
SELECT id, CASE WHEN type='private' THEN 'user' ELSE type END, user_id, NULL, key, category, content, importance, created_by, created_at, updated_at
|
||||
FROM memories;
|
||||
""")
|
||||
conn.execute("DROP TABLE memories;")
|
||||
conn.execute("ALTER TABLE memories_new RENAME TO memories;")
|
||||
else:
|
||||
conn.execute("""
|
||||
CREATE TABLE memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL CHECK(type IN ('global', 'user', 'project', 'private')),
|
||||
user_id INTEGER,
|
||||
project_name TEXT,
|
||||
key TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'general',
|
||||
content TEXT NOT NULL,
|
||||
importance INTEGER NOT NULL DEFAULT 1,
|
||||
created_by INTEGER,
|
||||
created_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
# Create partial unique indexes for each tier
|
||||
conn.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_global_key
|
||||
ON memories(type, key) WHERE type = 'global';
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_private_key
|
||||
ON memories(type, user_id, key) WHERE type = 'private';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_user_key
|
||||
ON memories(type, user_id, key) WHERE type IN ('user', 'private');
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_user_type
|
||||
ON memories(type, user_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_project_key
|
||||
ON memories(type, user_id, project_name, key) WHERE type = 'project';
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_lookup
|
||||
ON memories(type, user_id, project_name);
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_category
|
||||
ON memories(category);
|
||||
""")
|
||||
logger.info("Memory database initialized successfully at %s", self.db_path)
|
||||
logger.info("3-Tier Memory database initialized successfully at %s", self.db_path)
|
||||
except Exception as e:
|
||||
logger.error("Failed to initialize memory database: %s", e)
|
||||
raise
|
||||
@@ -98,22 +143,32 @@ class MemoryManager:
|
||||
key: str,
|
||||
content: str,
|
||||
user_id: Optional[int] = None,
|
||||
project_name: Optional[str] = None,
|
||||
category: str = "general",
|
||||
importance: int = 1,
|
||||
created_by: Optional[int] = None,
|
||||
) -> Tuple[MemoryItem, bool]:
|
||||
"""
|
||||
Saves or updates a memory.
|
||||
Saves or updates a memory in one of the 3 tiers (global, user, project).
|
||||
Returns: (MemoryItem, is_created: bool)
|
||||
"""
|
||||
type_ = type_.lower().strip()
|
||||
if type_ not in ("global", "private"):
|
||||
raise ValueError("Memory type must be 'global' or 'private'")
|
||||
if type_ == "private":
|
||||
type_ = "user"
|
||||
if type_ not in ("global", "user", "project"):
|
||||
raise ValueError("Memory type must be 'global', 'user', or 'project'")
|
||||
|
||||
if type_ == "global":
|
||||
user_id = None
|
||||
elif user_id is None:
|
||||
raise ValueError("Private memory requires a valid user_id")
|
||||
project_name = None
|
||||
elif type_ == "user":
|
||||
if user_id is None:
|
||||
raise ValueError("User memory requires a valid user_id")
|
||||
project_name = None
|
||||
elif type_ == "project":
|
||||
if user_id is None or not project_name:
|
||||
raise ValueError("Project memory requires both user_id and project_name")
|
||||
project_name = project_name.strip()
|
||||
|
||||
key = key.strip().lower()
|
||||
if not key:
|
||||
@@ -128,17 +183,22 @@ class MemoryManager:
|
||||
now = time.time()
|
||||
|
||||
with self._get_connection() as conn:
|
||||
# Check if exists
|
||||
# Check if existing item exists
|
||||
if type_ == "global":
|
||||
cur = conn.execute(
|
||||
"SELECT id, created_at, created_by FROM memories WHERE type = 'global' AND key = ?",
|
||||
(key,),
|
||||
)
|
||||
else:
|
||||
elif type_ == "user":
|
||||
cur = conn.execute(
|
||||
"SELECT id, created_at, created_by FROM memories WHERE type = 'private' AND user_id = ? AND key = ?",
|
||||
"SELECT id, created_at, created_by FROM memories WHERE type IN ('user', 'private') AND user_id = ? AND key = ?",
|
||||
(user_id, key),
|
||||
)
|
||||
else: # project
|
||||
cur = conn.execute(
|
||||
"SELECT id, created_at, created_by FROM memories WHERE type = 'project' AND user_id = ? AND project_name = ? AND key = ?",
|
||||
(user_id, project_name, key),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
if row:
|
||||
@@ -148,10 +208,10 @@ class MemoryManager:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE memories
|
||||
SET category = ?, content = ?, importance = ?, updated_at = ?
|
||||
SET type = ?, category = ?, content = ?, importance = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(category, content, importance, now, mem_id),
|
||||
(type_, category, content, importance, now, mem_id),
|
||||
)
|
||||
is_created = False
|
||||
created_at = orig_created_at
|
||||
@@ -159,10 +219,10 @@ class MemoryManager:
|
||||
else:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO memories (type, user_id, key, category, content, importance, created_by, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO memories (type, user_id, project_name, key, category, content, importance, created_by, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(type_, user_id, key, category, content, importance, created_by, now, now),
|
||||
(type_, user_id, project_name, key, category, content, importance, created_by, now, now),
|
||||
)
|
||||
mem_id = cur.lastrowid
|
||||
is_created = True
|
||||
@@ -173,6 +233,7 @@ class MemoryManager:
|
||||
id=mem_id,
|
||||
type=type_,
|
||||
user_id=user_id,
|
||||
project_name=project_name,
|
||||
key=key,
|
||||
category=category,
|
||||
content=content,
|
||||
@@ -182,12 +243,13 @@ class MemoryManager:
|
||||
updated_at=now,
|
||||
)
|
||||
logger.info(
|
||||
"%s memory [id=%d, key=%s, type=%s, user=%s]",
|
||||
"%s memory [id=%d, type=%s, key=%s, user=%s, proj=%s]",
|
||||
"Created" if is_created else "Updated",
|
||||
mem_id,
|
||||
key,
|
||||
type_,
|
||||
key,
|
||||
user_id,
|
||||
project_name,
|
||||
)
|
||||
return item, is_created
|
||||
|
||||
@@ -199,17 +261,26 @@ class MemoryManager:
|
||||
return MemoryItem(**dict(row))
|
||||
return None
|
||||
|
||||
def get_by_key(self, type_: str, key: str, user_id: Optional[int] = None) -> Optional[MemoryItem]:
|
||||
def get_by_key(
|
||||
self, type_: str, key: str, user_id: Optional[int] = None, project_name: Optional[str] = None
|
||||
) -> Optional[MemoryItem]:
|
||||
type_ = type_.lower().strip()
|
||||
if type_ == "private":
|
||||
type_ = "user"
|
||||
key = key.lower().strip()
|
||||
with self._get_connection() as conn:
|
||||
if type_ == "global":
|
||||
cur = conn.execute("SELECT * FROM memories WHERE type = 'global' AND key = ?", (key,))
|
||||
else:
|
||||
elif type_ == "user":
|
||||
cur = conn.execute(
|
||||
"SELECT * FROM memories WHERE type = 'private' AND user_id = ? AND key = ?",
|
||||
"SELECT * FROM memories WHERE type IN ('user', 'private') AND user_id = ? AND key = ?",
|
||||
(user_id, key),
|
||||
)
|
||||
else: # project
|
||||
cur = conn.execute(
|
||||
"SELECT * FROM memories WHERE type = 'project' AND user_id = ? AND project_name = ? AND key = ?",
|
||||
(user_id, project_name, key),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return MemoryItem(**dict(row))
|
||||
@@ -235,16 +306,32 @@ class MemoryManager:
|
||||
with self._get_connection() as conn:
|
||||
if category:
|
||||
cur = conn.execute(
|
||||
"SELECT * FROM memories WHERE type = 'private' AND user_id = ? AND category = ? ORDER BY importance DESC, updated_at DESC LIMIT ?",
|
||||
"SELECT * FROM memories WHERE type IN ('user', 'private') AND user_id = ? AND category = ? ORDER BY importance DESC, updated_at DESC LIMIT ?",
|
||||
(user_id, category.lower().strip(), limit),
|
||||
)
|
||||
else:
|
||||
cur = conn.execute(
|
||||
"SELECT * FROM memories WHERE type = 'private' AND user_id = ? ORDER BY importance DESC, updated_at DESC LIMIT ?",
|
||||
"SELECT * FROM memories WHERE type IN ('user', 'private') AND user_id = ? ORDER BY importance DESC, updated_at DESC LIMIT ?",
|
||||
(user_id, limit),
|
||||
)
|
||||
return [MemoryItem(**dict(r)) for r in cur.fetchall()]
|
||||
|
||||
def get_project_memories(
|
||||
self, user_id: int, project_name: str, category: Optional[str] = None, limit: int = 100
|
||||
) -> List[MemoryItem]:
|
||||
with self._get_connection() as conn:
|
||||
if category:
|
||||
cur = conn.execute(
|
||||
"SELECT * FROM memories WHERE type = 'project' AND user_id = ? AND project_name = ? AND category = ? ORDER BY importance DESC, updated_at DESC LIMIT ?",
|
||||
(user_id, project_name, category.lower().strip(), limit),
|
||||
)
|
||||
else:
|
||||
cur = conn.execute(
|
||||
"SELECT * FROM memories WHERE type = 'project' AND user_id = ? AND project_name = ? ORDER BY importance DESC, updated_at DESC LIMIT ?",
|
||||
(user_id, project_name, limit),
|
||||
)
|
||||
return [MemoryItem(**dict(r)) for r in cur.fetchall()]
|
||||
|
||||
def delete_by_id(self, memory_id: int, user_id: Optional[int] = None, is_admin: bool = False) -> bool:
|
||||
with self._get_connection() as conn:
|
||||
cur = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,))
|
||||
@@ -254,52 +341,86 @@ class MemoryManager:
|
||||
mem = MemoryItem(**dict(row))
|
||||
if mem.type == "global" and not is_admin:
|
||||
raise PermissionError("Only administrators can delete global memories")
|
||||
if mem.type == "private" and mem.user_id != user_id and not is_admin:
|
||||
raise PermissionError("Cannot delete another user's private memory")
|
||||
if mem.type in ("user", "project", "private") and mem.user_id != user_id and not is_admin:
|
||||
raise PermissionError("Cannot delete another user's memory")
|
||||
|
||||
conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
|
||||
logger.info("Deleted memory [id=%d, key=%s, type=%s]", memory_id, mem.key, mem.type)
|
||||
return True
|
||||
|
||||
def delete_by_key(
|
||||
self, type_: str, key: str, user_id: Optional[int] = None, is_admin: bool = False
|
||||
self,
|
||||
type_: str,
|
||||
key: str,
|
||||
user_id: Optional[int] = None,
|
||||
project_name: Optional[str] = None,
|
||||
is_admin: bool = False,
|
||||
) -> bool:
|
||||
type_ = type_.lower().strip()
|
||||
if type_ == "private":
|
||||
type_ = "user"
|
||||
key = key.lower().strip()
|
||||
with self._get_connection() as conn:
|
||||
if type_ == "global":
|
||||
if not is_admin:
|
||||
raise PermissionError("Only administrators can delete global memories")
|
||||
cur = conn.execute("DELETE FROM memories WHERE type = 'global' AND key = ?", (key,))
|
||||
else:
|
||||
elif type_ == "user":
|
||||
if user_id is None:
|
||||
return False
|
||||
cur = conn.execute(
|
||||
"DELETE FROM memories WHERE type = 'private' AND user_id = ? AND key = ?",
|
||||
"DELETE FROM memories WHERE type IN ('user', 'private') AND user_id = ? AND key = ?",
|
||||
(user_id, key),
|
||||
)
|
||||
else: # project
|
||||
if user_id is None or not project_name:
|
||||
return False
|
||||
cur = conn.execute(
|
||||
"DELETE FROM memories WHERE type = 'project' AND user_id = ? AND project_name = ? AND key = ?",
|
||||
(user_id, project_name, key),
|
||||
)
|
||||
deleted = cur.rowcount > 0
|
||||
if deleted:
|
||||
logger.info("Deleted memory by key [key=%s, type=%s, user=%s]", key, type_, user_id)
|
||||
logger.info("Deleted memory by key [key=%s, type=%s, user=%s, proj=%s]", key, type_, user_id, project_name)
|
||||
return deleted
|
||||
|
||||
def clear_memories(self, type_: str, user_id: Optional[int] = None, is_admin: bool = False) -> int:
|
||||
def clear_memories(
|
||||
self,
|
||||
type_: str,
|
||||
user_id: Optional[int] = None,
|
||||
project_name: Optional[str] = None,
|
||||
is_admin: bool = False,
|
||||
) -> int:
|
||||
type_ = type_.lower().strip()
|
||||
if type_ == "private":
|
||||
type_ = "user"
|
||||
with self._get_connection() as conn:
|
||||
if type_ == "global":
|
||||
if not is_admin:
|
||||
raise PermissionError("Only administrators can clear global memories")
|
||||
cur = conn.execute("DELETE FROM memories WHERE type = 'global'")
|
||||
else:
|
||||
elif type_ == "user":
|
||||
if user_id is None:
|
||||
return 0
|
||||
cur = conn.execute("DELETE FROM memories WHERE type = 'private' AND user_id = ?", (user_id,))
|
||||
cur = conn.execute("DELETE FROM memories WHERE type IN ('user', 'private') AND user_id = ?", (user_id,))
|
||||
else: # project
|
||||
if user_id is None or not project_name:
|
||||
return 0
|
||||
cur = conn.execute(
|
||||
"DELETE FROM memories WHERE type = 'project' AND user_id = ? AND project_name = ?",
|
||||
(user_id, project_name),
|
||||
)
|
||||
count = cur.rowcount
|
||||
logger.info("Cleared %d %s memories for user %s", count, type_, user_id)
|
||||
logger.info("Cleared %d %s memories for user %s (proj: %s)", count, type_, user_id, project_name)
|
||||
return count
|
||||
|
||||
def search_memories(
|
||||
self, query: str, user_id: Optional[int] = None, type_: Optional[str] = None, limit: int = 50
|
||||
self,
|
||||
query: str,
|
||||
user_id: Optional[int] = None,
|
||||
project_name: Optional[str] = None,
|
||||
type_: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
) -> List[MemoryItem]:
|
||||
q = f"%{query.strip().lower()}%"
|
||||
with self._get_connection() as conn:
|
||||
@@ -312,24 +433,33 @@ class MemoryManager:
|
||||
""",
|
||||
(q, q, q, limit),
|
||||
)
|
||||
elif type_ == "private" and user_id is not None:
|
||||
elif type_ == "user" and user_id is not None:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
SELECT * FROM memories
|
||||
WHERE type = 'private' AND user_id = ? AND (key LIKE ? OR content LIKE ? OR category LIKE ?)
|
||||
WHERE type IN ('user', 'private') AND user_id = ? AND (key LIKE ? OR content LIKE ? OR category LIKE ?)
|
||||
ORDER BY importance DESC, updated_at DESC LIMIT ?
|
||||
""",
|
||||
(user_id, q, q, q, limit),
|
||||
)
|
||||
elif type_ == "project" and user_id is not None and project_name:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
SELECT * FROM memories
|
||||
WHERE type = 'project' AND user_id = ? AND project_name = ? AND (key LIKE ? OR content LIKE ? OR category LIKE ?)
|
||||
ORDER BY importance DESC, updated_at DESC LIMIT ?
|
||||
""",
|
||||
(user_id, project_name, q, q, q, limit),
|
||||
)
|
||||
elif user_id is not None:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
SELECT * FROM memories
|
||||
WHERE (type = 'global' OR (type = 'private' AND user_id = ?))
|
||||
WHERE (type = 'global' OR (type IN ('user', 'private') AND user_id = ?) OR (type = 'project' AND user_id = ? AND project_name = ?))
|
||||
AND (key LIKE ? OR content LIKE ? OR category LIKE ?)
|
||||
ORDER BY type DESC, importance DESC, updated_at DESC LIMIT ?
|
||||
ORDER BY importance DESC, updated_at DESC LIMIT ?
|
||||
""",
|
||||
(user_id, q, q, q, limit),
|
||||
(user_id, user_id, project_name or "", q, q, q, limit),
|
||||
)
|
||||
else:
|
||||
cur = conn.execute(
|
||||
@@ -342,66 +472,102 @@ class MemoryManager:
|
||||
)
|
||||
return [MemoryItem(**dict(r)) for r in cur.fetchall()]
|
||||
|
||||
def format_memories_for_prompt(self, user_id: Optional[int] = None) -> Optional[str]:
|
||||
def format_memories_for_prompt(
|
||||
self, user_id: Optional[int] = None, project_name: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Formats all applicable memories (Global + User's Private) into a concise, token-efficient
|
||||
system instruction block for the AI model.
|
||||
Returns None if no memories are found.
|
||||
Formats all 3 tiers of memories into a structured, concise instruction block for AI:
|
||||
1. 🌐 Global System Rules
|
||||
2. 👤 User Personal Preferences & Profile
|
||||
3. 📁 Active Project Specific Knowledge
|
||||
"""
|
||||
globals_list = self.get_global_memories(limit=50)
|
||||
privates_list = self.get_user_memories(user_id=user_id, limit=50) if user_id else []
|
||||
users_list = self.get_user_memories(user_id=user_id, limit=50) if user_id else []
|
||||
projects_list = (
|
||||
self.get_project_memories(user_id=user_id, project_name=project_name, limit=50)
|
||||
if user_id and project_name
|
||||
else []
|
||||
)
|
||||
|
||||
if not globals_list and not privates_list:
|
||||
if not globals_list and not users_list and not projects_list:
|
||||
return None
|
||||
|
||||
lines = [
|
||||
"[SYSTEM INSTRUCTION: AI PERSISTENT MEMORY & CONTINUOUS LEARNING]",
|
||||
"You have access to persistent long-term memory containing verified system rules and user context.",
|
||||
"Use this knowledge to maintain continuity and customize all your responses accordingly.\n"
|
||||
"[SYSTEM INSTRUCTION: AI PERSISTENT HIERARCHICAL MEMORY & CONTINUOUS LEARNING]",
|
||||
"You have access to persistent 3-tier long-term memory containing verified system rules, project context, and user profile.",
|
||||
"Use this knowledge to maintain continuity and customize all your responses accordingly.\n",
|
||||
"⚖️ STRICT PRECEDENCE HIERARCHY (سلسلهمراتب قطعی اولویت و حل تعارض):",
|
||||
"1. 🥇 Tier 1 (Highest Authority): Global System Rules ALWAYS strictly supersede all Project and User instructions.",
|
||||
"2. 🥈 Tier 2 (Intermediate Authority): Active Project Knowledge & Rules ALWAYS supersede general User preferences inside this project.",
|
||||
"3. 🥉 Tier 3 (Baseline Authority): User Personal Preferences apply across projects as defaults when not overridden by Project or Global rules.\n"
|
||||
]
|
||||
|
||||
if globals_list:
|
||||
lines.append("🌐 GLOBAL SYSTEM MEMORY (قوانین و اطلاعات عمومی سیستم):")
|
||||
lines.append("🌐 1. GLOBAL SYSTEM RULES & KNOWLEDGE (قوانین عمومی سیستم - بالاترین اولویت):")
|
||||
for m in globals_list:
|
||||
cat = f"[{m.category}]" if m.category and m.category != "general" else ""
|
||||
lines.append(f"• [key: {m.key}] {cat} {m.content}")
|
||||
lines.append("")
|
||||
|
||||
if privates_list:
|
||||
lines.append(f"👤 USER PRIVATE MEMORY (خاطرات و ترجیحات اختصاصی این کاربر):")
|
||||
for m in privates_list:
|
||||
if projects_list:
|
||||
lines.append(f"📁 2. ACTIVE PROJECT KNOWLEDGE [پروژه: {project_name}] (دانش و تصمیمات اختصاصی این پروژه - اولویت دوم):")
|
||||
for m in projects_list:
|
||||
cat = f"[{m.category}]" if m.category and m.category != "general" else ""
|
||||
lines.append(f"• [key: {m.key}] {cat} {m.content}")
|
||||
lines.append("")
|
||||
|
||||
if users_list:
|
||||
lines.append("👤 3. USER PERSONAL PROFILE & PREFERENCES (ترجیحات و مشخصات شخصی کاربر - اولویت پایه):")
|
||||
for m in users_list:
|
||||
cat = f"[{m.category}]" if m.category and m.category != "general" else ""
|
||||
lines.append(f"• [key: {m.key}] {cat} {m.content}")
|
||||
lines.append("")
|
||||
|
||||
lines.extend([
|
||||
"🧠 AUTONOMOUS MEMORY MANAGEMENT RULES:",
|
||||
"1. When the user shares important facts, personal preferences, coding conventions, or rules that should persist, output the action tag:",
|
||||
' [[SAVE_MEMORY: type="private|global", key="<short_slug>", category="preference|rule|tech_stack|fact", content="<concise memory summary>"]]',
|
||||
"2. Note: Non-admin users can ONLY save 'private' memories. Only system administrators can set 'global' memories.",
|
||||
"3. If a past memory is superseded or changed, update it with the same key or use [[UPDATE_MEMORY: key=\"<slug>\", content=\"<new>\"]].",
|
||||
"4. If the user explicitly asks to forget something, use [[DELETE_MEMORY: key=\"<slug>\"]].",
|
||||
"1. When user shares durable facts, choose the appropriate tier:",
|
||||
' • User-level (developer habits, preferred tools, personal info): type="user"',
|
||||
' • Project-level (architecture, database schema, libraries, API designs, fixed bugs in this project): type="project"',
|
||||
' Action Tag: [[SAVE_MEMORY: type="user|project", key="<short_slug>", category="preference|rule|tech_stack|fact", content="<concise summary>"]]',
|
||||
"2. Note: Global memory is read-only for AI and managed exclusively by system administrators via the Telegram Bot UI.",
|
||||
"3. If past memory is superseded, update it: [[UPDATE_MEMORY: type=\"user|project\", key=\"<slug>\", content=\"<new_content>\"]].",
|
||||
"4. If user asks to forget something: [[DELETE_MEMORY: type=\"user|project\", key=\"<slug>\"]].",
|
||||
"5. Do NOT store temporary or trivial small-talk. Only store enduring and valuable knowledge."
|
||||
])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
def get_stats(self, user_id: Optional[int] = None, project_name: Optional[str] = None) -> Dict[str, Any]:
|
||||
with self._get_connection() as conn:
|
||||
cur = conn.execute("SELECT COUNT(*) as cnt FROM memories WHERE type = 'global'")
|
||||
global_count = cur.fetchone()["cnt"]
|
||||
|
||||
cur = conn.execute("SELECT COUNT(*) as cnt FROM memories WHERE type = 'private'")
|
||||
private_count = cur.fetchone()["cnt"]
|
||||
|
||||
cur = conn.execute("SELECT COUNT(DISTINCT user_id) as cnt FROM memories WHERE type = 'private'")
|
||||
cur = conn.execute("SELECT COUNT(*) as cnt FROM memories WHERE type IN ('user', 'private')")
|
||||
user_count = cur.fetchone()["cnt"]
|
||||
|
||||
cur = conn.execute("SELECT COUNT(*) as cnt FROM memories WHERE type = 'project'")
|
||||
project_count = cur.fetchone()["cnt"]
|
||||
|
||||
cur = conn.execute("SELECT COUNT(DISTINCT user_id) as cnt FROM memories WHERE type IN ('user', 'project', 'private')")
|
||||
users_with_mem = cur.fetchone()["cnt"]
|
||||
|
||||
this_user_count = 0
|
||||
this_project_count = 0
|
||||
if user_id:
|
||||
cur = conn.execute("SELECT COUNT(*) as cnt FROM memories WHERE type IN ('user', 'private') AND user_id = ?", (user_id,))
|
||||
this_user_count = cur.fetchone()["cnt"]
|
||||
if project_name:
|
||||
cur = conn.execute("SELECT COUNT(*) as cnt FROM memories WHERE type = 'project' AND user_id = ? AND project_name = ?", (user_id, project_name))
|
||||
this_project_count = cur.fetchone()["cnt"]
|
||||
|
||||
return {
|
||||
"total_memories": global_count + private_count,
|
||||
"total_memories": global_count + user_count + project_count,
|
||||
"global_memories": global_count,
|
||||
"private_memories": private_count,
|
||||
"users_with_memory": user_count,
|
||||
"user_memories": user_count,
|
||||
"project_memories": project_count,
|
||||
"users_with_memory": users_with_mem,
|
||||
"current_user_memories": this_user_count,
|
||||
"current_project_memories": this_project_count,
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
Reference in New Issue
Block a user