[AC-MIGRATION] feat(db): 新增用户记忆迁移和工具脚本

- 新增 003_user_memories 迁移脚本支持用户记忆表
- 新增 clear_kb_vectors 脚本用于清理知识库向量
- 新增 svg 资源目录
This commit is contained in:
MerCry 2026-03-11 19:11:44 +08:00
parent a6276522c8
commit 60e16d65c9
5 changed files with 256 additions and 0 deletions

View File

@ -0,0 +1,75 @@
"""
Database Migration: User Memories Table.
[AC-IDMP-14] 用户级记忆滚动摘要表
创建时间: 2025-03-08
变更说明:
- 新增 user_memories 表用于存储滚动摘要与事实/偏好/未解决问题
执行方式:
- SQLModel 会自动创建表通过 init_db
- 此脚本用于手动迁移或回滚
SQL DDL:
```sql
CREATE TABLE user_memories (
id UUID PRIMARY KEY,
tenant_id VARCHAR NOT NULL,
user_id VARCHAR NOT NULL,
summary TEXT,
facts JSON,
preferences JSON,
open_issues JSON,
summary_version INTEGER NOT NULL DEFAULT 1,
last_turn_id VARCHAR,
expires_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_user_memories_tenant_user ON user_memories(tenant_id, user_id);
CREATE INDEX ix_user_memories_tenant_user_updated ON user_memories(tenant_id, user_id, updated_at);
```
回滚 SQL:
```sql
DROP TABLE IF EXISTS user_memories;
```
"""
USER_MEMORIES_DDL = """
CREATE TABLE IF NOT EXISTS user_memories (
id UUID PRIMARY KEY,
tenant_id VARCHAR NOT NULL,
user_id VARCHAR NOT NULL,
summary TEXT,
facts JSON,
preferences JSON,
open_issues JSON,
summary_version INTEGER NOT NULL DEFAULT 1,
last_turn_id VARCHAR,
expires_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
"""
USER_MEMORIES_INDEXES = """
CREATE INDEX IF NOT EXISTS ix_user_memories_tenant_user ON user_memories(tenant_id, user_id);
CREATE INDEX IF NOT EXISTS ix_user_memories_tenant_user_updated ON user_memories(tenant_id, user_id, updated_at);
"""
USER_MEMORIES_ROLLBACK = """
DROP TABLE IF EXISTS user_memories;
"""
async def upgrade(conn):
"""执行迁移"""
await conn.execute(USER_MEMORIES_DDL)
await conn.execute(USER_MEMORIES_INDEXES)
async def downgrade(conn):
"""回滚迁移"""
await conn.execute(USER_MEMORIES_ROLLBACK)

View File

@ -0,0 +1,178 @@
"""
Script to cleanup vector data for a specific knowledge base.
Clears the Qdrant collection for the given KB ID, allowing re-indexing.
"""
import asyncio
import logging
import sys
sys.path.insert(0, "Q:\\agentProject\\ai-robot-core\\ai-service")
from app.core.config import get_settings
from app.core.qdrant_client import get_qdrant_client
from app.core.database import get_session
from app.models.entities import KnowledgeBase, Document
from sqlalchemy import select
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
async def get_knowledge_base_info(kb_id: str) -> dict | None:
"""Get knowledge base information from database."""
async for session in get_session():
stmt = select(KnowledgeBase).where(KnowledgeBase.id == kb_id)
result = await session.execute(stmt)
kb = result.scalar_one_or_none()
if kb:
doc_stmt = select(Document).where(Document.kb_id == kb_id)
doc_result = await session.execute(doc_stmt)
documents = doc_result.scalars().all()
return {
"id": str(kb.id),
"tenant_id": kb.tenant_id,
"name": kb.name,
"doc_count": len(documents),
"document_ids": [str(doc.id) for doc in documents]
}
return None
async def list_kb_collections(tenant_id: str, kb_id: str) -> list[str]:
"""List all collections that might be related to the KB."""
client = await get_qdrant_client()
qdrant = await client.get_client()
collections = await qdrant.get_collections()
all_names = [c.name for c in collections.collections]
safe_tenant = tenant_id.replace('@', '_')
safe_kb = kb_id.replace('-', '_')[:8]
matching = [
name for name in all_names
if safe_kb in name or kb_id.replace('-', '')[:8] in name.replace('_', '')
]
return matching
async def clear_kb_vector_data(tenant_id: str, kb_id: str, delete_docs: bool = False) -> bool:
"""
Clear vector data for a specific knowledge base.
Args:
tenant_id: Tenant identifier
kb_id: Knowledge base ID
delete_docs: Whether to also delete document records from database
Returns:
True if successful
"""
client = await get_qdrant_client()
qdrant = await client.get_client()
collection_name = client.get_kb_collection_name(tenant_id, kb_id)
try:
exists = await qdrant.collection_exists(collection_name)
if exists:
await qdrant.delete_collection(collection_name=collection_name)
logger.info(f"Deleted Qdrant collection: {collection_name}")
else:
logger.info(f"Collection {collection_name} does not exist")
if delete_docs:
async for session in get_session():
doc_stmt = select(Document).where(Document.kb_id == kb_id)
doc_result = await session.execute(doc_stmt)
documents = doc_result.scalars().all()
for doc in documents:
await session.delete(doc)
stmt = select(KnowledgeBase).where(KnowledgeBase.id == kb_id)
result = await session.execute(stmt)
kb = result.scalar_one_or_none()
if kb:
kb.doc_count = 0
kb.updated_at = datetime.utcnow()
await session.commit()
logger.info(f"Deleted {len(documents)} document records from database")
break
return True
except Exception as e:
logger.error(f"Failed to clear KB vector data: {e}")
return False
async def main(kb_id: str, delete_docs: bool = False):
"""Main function to clear KB vector data."""
logger.info(f"Starting cleanup for knowledge base: {kb_id}")
kb_info = await get_knowledge_base_info(kb_id)
if not kb_info:
logger.error(f"Knowledge base not found: {kb_id}")
return False
logger.info(f"Found knowledge base:")
logger.info(f" - ID: {kb_info['id']}")
logger.info(f" - Name: {kb_info['name']}")
logger.info(f" - Tenant: {kb_info['tenant_id']}")
logger.info(f" - Document count: {kb_info['doc_count']}")
matching_collections = await list_kb_collections(kb_info['tenant_id'], kb_id)
if matching_collections:
logger.info(f" - Related collections: {matching_collections}")
print()
print("=" * 60)
print("WARNING: This will delete all vector data for this knowledge base!")
print(f"Collection to delete: kb_{kb_info['tenant_id'].replace('@', '_')}_{kb_id.replace('-', '_')[:8]}")
if delete_docs:
print("Document records in database will also be deleted!")
print("=" * 60)
print()
confirm = input("Continue? (yes/no): ")
if confirm.lower() != "yes":
print("Cancelled")
return False
success = await clear_kb_vector_data(
tenant_id=kb_info['tenant_id'],
kb_id=kb_id,
delete_docs=delete_docs
)
if success:
logger.info(f"Successfully cleared vector data for KB: {kb_id}")
logger.info("You can now re-index the knowledge base documents.")
else:
logger.error(f"Failed to clear vector data for KB: {kb_id}")
return success
if __name__ == "__main__":
import argparse
from datetime import datetime
parser = argparse.ArgumentParser(description="Clear vector data for a knowledge base")
parser.add_argument("kb_id", help="Knowledge base ID to clear")
parser.add_argument("--delete-docs", action="store_true",
help="Also delete document records from database")
args = parser.parse_args()
asyncio.run(main(args.kb_id, args.delete_docs))

1
ai-service/svg/kefu.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#1296db" d="M894.1 355.6h-1.7C853 177.6 687.6 51.4 498.1 54.9S148.2 190.5 115.9 369.7c-35.2 5.6-61.1 36-61.1 71.7v143.4c0.9 40.4 34.3 72.5 74.7 71.7 21.7-0.3 42.2-10 56-26.7 33.6 84.5 99.9 152 183.8 187 1.1-2 2.3-3.9 3.7-5.7 0.9-1.5 2.4-2.6 4.1-3 1.3 0 2.5 0.5 3.6 1.2a318.46 318.46 0 0 1-105.3-187.1c-5.1-44.4 24.1-85.4 67.6-95.2 64.3-11.7 128.1-24.7 192.4-35.9 37.9-5.3 70.4-29.8 85.7-64.9 6.8-15.9 11-32.8 12.5-50 0.5-3.1 2.9-5.6 5.9-6.2 3.1-0.7 6.4 0.5 8.2 3l1.7-1.1c25.4 35.9 74.7 114.4 82.7 197.2 8.2 94.8 3.7 160-71.4 226.5-1.1 1.1-1.7 2.6-1.7 4.1 0.1 2 1.1 3.8 2.8 4.8h4.8l3.2-1.8c75.6-40.4 132.8-108.2 159.9-189.5 11.4 16.1 28.5 27.1 47.8 30.8C846 783.9 716.9 871.6 557.2 884.9c-12-28.6-42.5-44.8-72.9-38.6-33.6 5.4-56.6 37-51.2 70.6 4.4 27.6 26.8 48.8 54.5 51.6 30.6 4.6 60.3-13 70.8-42.2 184.9-14.5 333.2-120.8 364.2-286.9 27.8-10.8 46.3-37.4 46.6-67.2V428.7c-0.1-19.5-8.1-38.2-22.3-51.6-14.5-13.8-33.8-21.4-53.8-21.3l1-0.2zM825.9 397c-71.1-176.9-272.1-262.7-449-191.7-86.8 34.9-155.7 103.4-191 190-2.5-2.8-5.2-5.4-8-7.9 25.3-154.6 163.8-268.6 326.8-269.2s302.3 112.6 328.7 267c-2.9 3.8-5.4 7.7-7.5 11.8z" /></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
ai-service/svg/user.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#1296db" d="M573.9 516.2L512 640l-61.9-123.8C232 546.4 64 733.6 64 960h896c0-226.4-168-413.6-386.1-443.8zM480 384h64c17.7 0 32.1 14.4 32.1 32.1 0 17.7-14.4 32.1-32.1 32.1h-64c-11.9 0-22.3-6.5-27.8-16.1H356c34.9 48.5 91.7 80 156 80 106 0 192-86 192-192s-86-192-192-192-192 86-192 192c0 28.5 6.2 55.6 17.4 80h114.8c5.5-9.6 15.9-16.1 27.8-16.1z" /><path fill="#1296db" d="M272 432.1h84c-4.2-5.9-8.1-12-11.7-18.4-2.3-4.1-4.4-8.3-6.4-12.5-0.2-0.4-0.4-0.7-0.5-1.1H288c-8.8 0-16-7.2-16-16v-48.4c0-64.1 25-124.3 70.3-169.6S447.9 95.8 512 95.8s124.3 25 169.7 70.3c38.3 38.3 62.1 87.2 68.5 140.2-8.4 4-14.2 12.5-14.2 22.4v78.6c0 13.7 11.1 24.8 24.8 24.8h14.6c13.7 0 24.8-11.1 24.8-24.8v-78.6c0-11.3-7.6-20.9-18-23.8-6.9-60.9-33.9-117.4-78-161.3C652.9 92.1 584.6 63.9 512 63.9s-140.9 28.3-192.3 79.6C268.3 194.8 240 263.1 240 335.7v64.4c0 17.7 14.3 32 32 32z" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1773157868702" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12129" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M657.066667 558.933333c-34.133333 25.6-76.8 38.4-123.733334 38.4s-85.333333-12.8-123.733333-38.4c0 4.266667-4.266667 4.266667-8.533333 4.266667C315.733333 614.4 256 708.266667 256 810.666667c0 12.8-8.533333 21.333333-21.333333 21.333333S213.333333 823.466667 213.333333 810.666667c0-119.466667 64-226.133333 166.4-281.6-38.4-38.4-59.733333-89.6-59.733333-145.066667 0-119.466667 93.866667-213.333333 213.333333-213.333333s213.333333 93.866667 213.333334 213.333333c0 55.466667-21.333333 106.666667-59.733334 145.066667 102.4 55.466667 166.4 162.133333 166.4 281.6 0 12.8-8.533333 21.333333-21.333333 21.333333s-21.333333-8.533333-21.333333-21.333333c0-102.4-59.733333-196.266667-149.333334-247.466667 0 0-4.266667 0-4.266666-4.266667z m-123.733334-4.266666c93.866667 0 170.666667-76.8 170.666667-170.666667s-76.8-170.666667-170.666667-170.666667-170.666667 76.8-170.666666 170.666667 76.8 170.666667 170.666666 170.666667z" p-id="12130" fill="#1296db"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB