Spaces:
Sleeping
Sleeping
File size: 4,854 Bytes
48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 b0d952a 48b5418 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | """Fire-and-forget inline memory persistence triggered by Soul mid-conversation.
Call via ``asyncio.create_task(save_inline_memory(...))``. The coroutine opens
its own DB session so the caller's session is already closed by the time this
runs. Failures are logged at WARNING — inline memory loss is visible in logs
but never propagates to the caller.
"""
from __future__ import annotations
import logging
import math
from sqlalchemy import select
from app.db import SessionLocal
from app.memory.embeddings import build_memory_embedding_input, embed_texts
from app.models.memory import Memory, MemoryScope, MemoryType
from app.schemas.agents import InlineMemoryRequest
from app.schemas.memory import MemoryCreate
logger = logging.getLogger(__name__)
DEDUP_COSINE_THRESHOLD = 0.92
def _cosine_sim(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
mag_a = math.sqrt(sum(x * x for x in a))
mag_b = math.sqrt(sum(x * x for x in b))
if mag_a == 0.0 or mag_b == 0.0:
return 0.0
return dot / (mag_a * mag_b)
def _is_near_duplicate(
session,
*,
character_id: str | None = None,
agent_id: str | None = None,
player_id: str,
proposed_embedding: list[float],
) -> bool:
if agent_id is not None:
scope_filter = Memory.agent_id == agent_id
elif character_id is not None:
scope_filter = Memory.character_id == character_id
else:
raise ValueError("_is_near_duplicate requires character_id or agent_id")
stmt = (
select(Memory.embedding)
.where(
scope_filter,
Memory.player_id == player_id,
Memory.scope == MemoryScope.OPPONENT_SPECIFIC,
Memory.embedding.is_not(None),
)
)
for (emb,) in session.execute(stmt):
if emb and _cosine_sim(proposed_embedding, emb) > DEDUP_COSINE_THRESHOLD:
return True
return False
async def save_inline_memory(
request: InlineMemoryRequest,
*,
character_id: str | None = None,
agent_id: str | None = None,
player_id: str,
match_id: str | None = None,
) -> None:
"""Embed and persist a Soul-generated inline memory.
Fire-and-forget: call as ``asyncio.create_task(save_inline_memory(...))``.
Survives client disconnect. Logs and returns silently on any error.
Pass either ``character_id`` (character room) or ``agent_id`` (agent room).
"""
scope_label = agent_id or character_id
try:
embed_input = build_memory_embedding_input(
narrative_text=request.narrative_text,
triggers=list(request.triggers),
relevance_tags=list(request.relevance_tags),
)
vectors = embed_texts([embed_input])
proposed_embedding = vectors[0]
with SessionLocal() as session:
if _is_near_duplicate(
session,
character_id=character_id,
agent_id=agent_id,
player_id=player_id,
proposed_embedding=proposed_embedding,
):
logger.info(
"inline_memory_dedup_skipped player=%s scope=%s match=%s",
player_id, scope_label, match_id,
)
return
item = MemoryCreate(
scope=MemoryScope.OPPONENT_SPECIFIC,
type=MemoryType(request.type),
emotional_valence=request.emotional_valence,
triggers=list(request.triggers),
narrative_text=request.narrative_text,
relevance_tags=list(request.relevance_tags),
player_id=player_id,
match_id=match_id,
)
# bulk_create handles embedding; pass embed=False since we already have it.
row = Memory(
character_id=character_id,
agent_id=agent_id,
player_id=item.player_id,
match_id=item.match_id,
scope=item.scope,
type=item.type,
emotional_valence=item.emotional_valence,
triggers=list(item.triggers),
narrative_text=item.narrative_text,
relevance_tags=list(item.relevance_tags),
embedding=proposed_embedding,
surface_count=0,
last_surfaced_at=None,
)
session.add(row)
session.commit()
logger.info(
"inline_memory_saved player=%s scope=%s match=%s triggers=%s",
player_id, scope_label, match_id, list(request.triggers),
)
except Exception:
logger.warning(
"inline_memory_failed player=%s scope=%s match=%s — skipping",
player_id, scope_label, match_id,
exc_info=True,
)
|