feat(api)!: memory single chain — async MemoryService, strict eval closure
Route all memory ingest/retrieve/enrichment/compaction through async MemoryService. Remove legacy sync memory implementations (ingest/retrieve/compaction); Celery and memoir Phase2 call asyncio.run into MemoryService-backed helpers. Memoir Phase1 batch ingest uses MemoryService.ingest_transcripts_batch; drop chapters. evidence_bundle_json mirror (Alembic 0015). Evaluation uses snapshot/link-only bundles; raise EvidenceClosureMissing instead of partial/fallback lineage tiers. Split memoir state into NarrativeCoverageState and InterviewControlState; delete the _interview_meta_store adapter layer. Remove rolling-query and recent-fact fallback settings from config and evidence assembly. Update judges, docs, tests, and PlaygroundPage alignment. Made-with: Cursor
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Memory compaction:增量 chunk 近重复检测,软排除(is_excluded + MemoryCurationAction)。
|
||||
|
||||
仅依赖 repo / settings;供 Celery 同步任务调用。
|
||||
仅依赖 repo / settings;供 async MemoryService 调用。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -11,19 +11,19 @@ import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logging import get_logger
|
||||
from app.features.memory.models import MemoryChunk, MemorySource
|
||||
from app.features.memory.repo import (
|
||||
create_curation_action_sync,
|
||||
get_first_chunk_after_cursor_sync,
|
||||
get_memory_chunk_sync,
|
||||
list_incremental_chunks_for_compaction_sync,
|
||||
mark_facts_stale_for_excluded_chunk_sync,
|
||||
search_nearest_chunks_for_compaction_sync,
|
||||
set_chunk_excluded_sync,
|
||||
create_curation_action,
|
||||
get_first_chunk_after_cursor,
|
||||
get_memory_chunk_for_user,
|
||||
list_incremental_chunks_for_compaction,
|
||||
mark_facts_stale_for_excluded_chunk,
|
||||
search_nearest_chunks_for_compaction,
|
||||
set_chunk_excluded,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -90,8 +90,8 @@ def canonical_score(
|
||||
return score + bonus
|
||||
|
||||
|
||||
def _source_type_for_chunk(session: Session, chunk: MemoryChunk) -> str | None:
|
||||
src = session.get(MemorySource, chunk.source_id)
|
||||
async def _source_type_for_chunk(db: AsyncSession, chunk: MemoryChunk) -> str | None:
|
||||
src = await db.get(MemorySource, chunk.source_id)
|
||||
return src.source_type if src else None
|
||||
|
||||
|
||||
@@ -149,8 +149,8 @@ def count_duplicate_layers(
|
||||
return n
|
||||
|
||||
|
||||
def _advance_cursor_past_excluded_only_sync(
|
||||
session: Session,
|
||||
async def _advance_cursor_past_excluded_only(
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
cursor_ts: datetime,
|
||||
cursor_id: str,
|
||||
@@ -166,8 +166,8 @@ def _advance_cursor_past_excluded_only_sync(
|
||||
advanced = False
|
||||
ts, cid = cursor_ts, cursor_id
|
||||
for _ in range(max_steps):
|
||||
nxt = get_first_chunk_after_cursor_sync(
|
||||
session,
|
||||
nxt = await get_first_chunk_after_cursor(
|
||||
db,
|
||||
user_id=user_id,
|
||||
after_cursor_ts=ts,
|
||||
after_chunk_id=cid,
|
||||
@@ -200,8 +200,8 @@ def _advance_cursor_past_excluded_only_sync(
|
||||
return (ts, cid) if advanced else None
|
||||
|
||||
|
||||
def run_memory_compaction_sync(
|
||||
session: Session, user_id: str, context: dict[str, Any] | None
|
||||
async def run_memory_compaction(
|
||||
db: AsyncSession, user_id: str, context: dict[str, Any] | None
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
对增量 chunk 做近重复软排除;调用方负责 commit。
|
||||
@@ -228,8 +228,8 @@ def run_memory_compaction_sync(
|
||||
has_candidate_filter = "candidate_chunk_ids" in ctx or "candidate_source_ids" in ctx
|
||||
|
||||
max_chunks = settings.memory_compaction_max_chunks_per_run
|
||||
incremental = list_incremental_chunks_for_compaction_sync(
|
||||
session,
|
||||
incremental = await list_incremental_chunks_for_compaction(
|
||||
db,
|
||||
user_id=user_id,
|
||||
after_cursor_ts=cursor_ts,
|
||||
after_chunk_id=cursor_id,
|
||||
@@ -242,8 +242,8 @@ def run_memory_compaction_sync(
|
||||
ms = (time.perf_counter() - t0) * 1000
|
||||
# 候选 id/source 收窄时,空集可能仅表示「交集为空」,不能盲目前进全局游标
|
||||
if not has_candidate_filter:
|
||||
tail = _advance_cursor_past_excluded_only_sync(
|
||||
session,
|
||||
tail = await _advance_cursor_past_excluded_only(
|
||||
db,
|
||||
user_id,
|
||||
cursor_ts,
|
||||
cursor_id,
|
||||
@@ -313,7 +313,7 @@ def run_memory_compaction_sync(
|
||||
if chunk.id in local_excluded:
|
||||
last_cursor_chunk = chunk
|
||||
continue
|
||||
row = get_memory_chunk_sync(session, chunk.id, user_id)
|
||||
row = await get_memory_chunk_for_user(db, chunk.id, user_id)
|
||||
if row is None or row.is_excluded:
|
||||
last_cursor_chunk = chunk
|
||||
continue
|
||||
@@ -348,9 +348,9 @@ def run_memory_compaction_sync(
|
||||
}
|
||||
|
||||
chunks_scanned_this_run += 1
|
||||
st_c = _source_type_for_chunk(session, row)
|
||||
neighbors = search_nearest_chunks_for_compaction_sync(
|
||||
session,
|
||||
st_c = await _source_type_for_chunk(db, row)
|
||||
neighbors = await search_nearest_chunks_for_compaction(
|
||||
db,
|
||||
user_id=user_id,
|
||||
chunk_id=row.id,
|
||||
query_embedding=list(emb),
|
||||
@@ -363,7 +363,7 @@ def run_memory_compaction_sync(
|
||||
nid = nb["id"]
|
||||
if nid == row.id or nid in local_excluded:
|
||||
continue
|
||||
other = get_memory_chunk_sync(session, nid, user_id)
|
||||
other = await get_memory_chunk_for_user(db, nid, user_id)
|
||||
if other is None or other.is_excluded:
|
||||
continue
|
||||
|
||||
@@ -409,15 +409,15 @@ def run_memory_compaction_sync(
|
||||
else:
|
||||
loser_id, winner_id = row.id, nid
|
||||
|
||||
loser = get_memory_chunk_sync(session, loser_id, user_id)
|
||||
loser = await get_memory_chunk_for_user(db, loser_id, user_id)
|
||||
if loser is None or loser.is_excluded:
|
||||
continue
|
||||
|
||||
ok = set_chunk_excluded_sync(session, loser_id, user_id, True)
|
||||
ok = await set_chunk_excluded(db, loser_id, user_id, True)
|
||||
if not ok:
|
||||
continue
|
||||
stale_n = mark_facts_stale_for_excluded_chunk_sync(
|
||||
session, user_id=user_id, chunk_id=loser_id
|
||||
stale_n = await mark_facts_stale_for_excluded_chunk(
|
||||
db, user_id=user_id, chunk_id=loser_id
|
||||
)
|
||||
if stale_n:
|
||||
logger.info(
|
||||
@@ -426,8 +426,8 @@ def run_memory_compaction_sync(
|
||||
loser_id,
|
||||
stale_n,
|
||||
)
|
||||
create_curation_action_sync(
|
||||
session,
|
||||
await create_curation_action(
|
||||
db,
|
||||
user_id=user_id,
|
||||
action_type="exclude",
|
||||
target_type="chunk",
|
||||
|
||||
Reference in New Issue
Block a user