初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+304
View File
@@ -0,0 +1,304 @@
"""记忆服务。
集成 ChromaDB 向量存储:
- create_memory / 大/小周天自动创建的 Memory 写入后异步生成 embedding 存向量
- retrieve_memories 支持向量相似度检索,失败时回退 SQL 排序检索
"""
from __future__ import annotations
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.errors import AppError
from app.core.logging import logger
from app.core.time import utc_now
from app.db.session import AsyncSessionLocal
from app.models.memory import Memory
from app.modules.memory.context import MemoryRetrievalResult
from app.repositories import memory as memory_repo
from app.repositories.json_utils import dumps
from app.schemas.common import Page
from app.schemas.memory import MemoryCreateRequest, MemoryResponse, MemoryUpdateRequest
from app.services.audit import record_audit_event
from app.services.characters import get_active_character_model
from app.services.serializers import memory_to_response
def _get_embedding_service(manager):
"""尝试创建 EmbeddingService,失败时返回 None。"""
try:
models = manager.model_registry.list_by_capability("embedding")
if not models:
return None
from app.ai_provider.embedding import EmbeddingService
return EmbeddingService(manager)
except Exception: # noqa: BLE001
return None
async def _embed_and_store(memory: Memory) -> None:
"""异步后台任务:为记忆生成 embedding 并存入向量库。
embedding 服务和 ChromaDB 均不可用时静默跳过——不影响主流程。
"""
text = memory.content or memory.title or ""
if not text:
return
try:
from app.ai_provider import ProviderManagerFactory
manager = ProviderManagerFactory.create()
service = _get_embedding_service(manager)
if service is None:
return
import asyncio as _asyncio
response = await _asyncio.wait_for(
service.embed(text),
timeout=10.0,
)
if not response.data or not response.data[0].embedding:
return
embedding = response.data[0].embedding
from app.integrations.vector_store import get_vector_store
store = get_vector_store()
if store is None:
return
store.add_memory(
character_id=memory.character_id,
memory_id=memory.id,
text=text,
embedding=embedding,
metadata={
"type": memory.type,
"source": memory.source,
"title": memory.title or "",
},
)
except Exception: # noqa: BLE001
logger.warning(
"memory_embed_failed",
memory_id=memory.id,
character_id=memory.character_id,
)
async def embed_memory_async(memory: Memory) -> None:
"""公开接口:为单条记忆生成 embedding 并存入向量库(供大/小周天后台任务调用)。"""
await _embed_and_store(memory)
# ── CRUD ──────────────────────────────────────────────────────────────────
async def list_memory_page(
*,
page: int = 1,
page_size: int = 20,
memory_type: str | None = None,
source: str | None = None,
owner_scope: str = settings.default_owner_scope,
) -> Page[MemoryResponse]:
async with AsyncSessionLocal() as session:
character = await get_active_character_model(session, owner_scope)
safe_page_size = min(max(page_size, 1), 100)
safe_page = max(page, 1)
items, total = await memory_repo.list_memories(
session,
character.id,
memory_type=memory_type,
source=source,
page=safe_page,
page_size=safe_page_size,
)
return Page(
items=[memory_to_response(item) for item in items],
page=safe_page,
page_size=safe_page_size,
total=total,
)
async def create_memory(
session: AsyncSession,
payload: MemoryCreateRequest,
*,
owner_scope: str = settings.default_owner_scope,
) -> MemoryResponse:
character = await get_active_character_model(session, owner_scope)
character_id = payload.character_id or character.id
if character_id != character.id:
raise AppError("not_found", "人物不存在。")
memory = Memory(
character_id=character_id,
type=payload.type,
title=payload.title,
content=payload.content,
source=payload.source,
confidence=payload.confidence,
importance=payload.importance,
sensitivity=payload.sensitivity,
evidence_ids_json=dumps(payload.evidence_ids),
status="active",
)
session.add(memory)
await session.flush()
# 异步 embedding
if settings.vector_db_enabled:
import asyncio
asyncio.ensure_future(_embed_and_store(memory))
await record_audit_event(
session,
action="memory.created",
character_id=character_id,
actor="user",
decision="auto",
input_summary=payload.title,
resource_type="memory",
resource_id=memory.id,
)
await session.commit()
await session.refresh(memory)
return memory_to_response(memory)
async def update_memory(
memory_id: str, payload: MemoryUpdateRequest, *, owner_scope: str = settings.default_owner_scope
) -> MemoryResponse:
async with AsyncSessionLocal() as session:
character = await get_active_character_model(session, owner_scope)
memory = await memory_repo.get_memory(session, memory_id)
if memory is None or memory.character_id != character.id or memory.deleted_at is not None:
raise AppError("not_found", "记忆不存在。")
updates = payload.model_dump(exclude_unset=True)
evidence_ids = updates.pop("evidence_ids", None)
for key, value in updates.items():
setattr(memory, key, value)
if evidence_ids is not None:
memory.evidence_ids_json = dumps(evidence_ids)
memory.updated_at = utc_now()
# 如果内容变了,异步更新向量
if ("content" in updates or "title" in updates) and settings.vector_db_enabled:
import asyncio
asyncio.ensure_future(_embed_and_store(memory))
await record_audit_event(
session,
action="memory.updated",
character_id=character.id,
actor="user",
decision="auto",
input_summary=memory.title,
resource_type="memory",
resource_id=memory.id,
)
await session.commit()
await session.refresh(memory)
return memory_to_response(memory)
async def delete_memory(
memory_id: str, *, owner_scope: str = settings.default_owner_scope
) -> MemoryResponse:
async with AsyncSessionLocal() as session:
character = await get_active_character_model(session, owner_scope)
memory = await memory_repo.get_memory(session, memory_id)
if memory is None or memory.character_id != character.id or memory.deleted_at is not None:
raise AppError("not_found", "记忆不存在。")
memory.status = "deleted"
memory.deleted_at = utc_now()
memory.updated_at = utc_now()
# 同步删除向量
if settings.vector_db_enabled:
try:
from app.integrations.vector_store import get_vector_store
store = get_vector_store()
if store is not None:
store.delete_memory(character.id, memory_id)
except Exception: # noqa: BLE001
pass
await record_audit_event(
session,
action="memory.deleted",
character_id=character.id,
actor="user",
decision="auto",
input_summary=memory.title,
resource_type="memory",
resource_id=memory.id,
)
await session.commit()
await session.refresh(memory)
return memory_to_response(memory)
async def memory_context(
session: AsyncSession,
character_id: str,
*,
limit: int,
_owner_scope: str = settings.default_owner_scope,
) -> str:
"""获取记忆上下文文本(供叩心枢开场、辰正等场景使用)。"""
memories = await memory_repo.list_injectable_memories(session, character_id, limit=limit)
if not memories:
return ""
lines = [f"- {item.title}: {item.content}" for item in memories]
return "相关记忆:\n" + "\n".join(lines)
async def retrieve_memories(
session: AsyncSession,
character_id: str,
*,
limit: int,
_owner_scope: str = settings.default_owner_scope,
) -> MemoryRetrievalResult:
"""检索可注入上下文的相关记忆。
优先尝试向量相似度检索,失败时回退 SQL 排序检索。
"""
# 尝试向量检索(5s 超时,避免 embedding endpoint 不可达时阻塞消息流)
if settings.vector_db_enabled:
try:
from app.ai_provider import ProviderManagerFactory
manager = ProviderManagerFactory.create()
models = manager.model_registry.list_by_capability("embedding")
if models:
import asyncio as _asyncio
from app.ai_provider.embedding import EmbeddingService
from app.integrations.vector_store import get_vector_store
query_text = f"检索 {character_id} 的最新相关记忆"
service = EmbeddingService(manager)
response = await _asyncio.wait_for(
service.embed(query_text),
timeout=5.0,
)
if response.data and response.data[0].embedding:
store = get_vector_store()
if store is not None:
vector_results = store.search(
character_id,
response.data[0].embedding,
limit=limit,
)
if vector_results:
ids = [r["id"] for r in vector_results]
memories = await memory_repo.get_memories_by_ids(session, ids)
if memories:
return MemoryRetrievalResult(memories=memories)
except Exception: # noqa: BLE001
logger.warning("vector_retrieval_failed,回退 SQL 检索")
# 回退 SQL
memories = await memory_repo.list_injectable_memories(session, character_id, limit=limit)
return MemoryRetrievalResult(memories=memories)