File size: 5,262 Bytes
2552437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
145
146
147
148
149
150
151
152
153
from __future__ import annotations

from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession

from ..config import settings
from ..models import ChatMemory, PendingAction
from .inventory import now_iso


SEMANTIC_HINTS = ("prefiero", "quiero", "no quiero", "recuerda", "me gusta", "no uses", "sin comandos")


async def remember_message(session: AsyncSession, chat_id: str, role: str, content: str, kind: str = "short") -> None:
    content = (content or "").strip()
    if not content:
        return

    session.add(
        ChatMemory(
            chat_id=str(chat_id),
            kind=kind,
            role=role,
            content=content[:4000],
            created_at=now_iso(),
        )
    )
    await session.flush()

    if kind == "short":
        result = await session.execute(
            select(ChatMemory)
            .where(ChatMemory.chat_id == str(chat_id), ChatMemory.kind == "short")
            .order_by(ChatMemory.id.desc())
        )
        messages = list(result.scalars().all())
        for stale in messages[settings.memory_short_limit * 2 :]:
            await session.delete(stale)


async def capture_semantic_memory(session: AsyncSession, chat_id: str, user_text: str) -> None:
    lowered = (user_text or "").lower()
    if not any(hint in lowered for hint in SEMANTIC_HINTS):
        return

    normalized = user_text.strip()[:400]
    result = await session.execute(
        select(ChatMemory)
        .where(ChatMemory.chat_id == str(chat_id), ChatMemory.kind == "semantic")
        .order_by(ChatMemory.id.desc())
        .limit(5)
    )
    if normalized in {item.content for item in result.scalars().all()}:
        return

    await remember_message(session, chat_id, "user", normalized, kind="semantic")


async def remember_episode(session: AsyncSession, chat_id: str, content: str) -> None:
    await remember_message(session, chat_id, "system", content, kind="episodic")


async def build_memory_context(session: AsyncSession, chat_id: str) -> str:
    fragments: list[str] = []

    semantic_result = await session.execute(
        select(ChatMemory)
        .where(ChatMemory.chat_id == str(chat_id), ChatMemory.kind == "semantic")
        .order_by(ChatMemory.id.desc())
        .limit(6)
    )
    semantic = list(reversed(list(semantic_result.scalars().all())))
    if semantic:
        fragments.append("Preferencias y hechos utiles:")
        fragments.extend(f"- {item.content}" for item in semantic)

    short_result = await session.execute(
        select(ChatMemory)
        .where(ChatMemory.chat_id == str(chat_id), ChatMemory.kind == "short")
        .order_by(ChatMemory.id.desc())
        .limit(settings.memory_short_limit)
    )
    short_messages = list(reversed(list(short_result.scalars().all())))
    if short_messages:
        fragments.append("Contexto reciente:")
        fragments.extend(f"- {item.role}: {item.content}" for item in short_messages)

    episode_result = await session.execute(
        select(ChatMemory)
        .where(ChatMemory.chat_id == str(chat_id), ChatMemory.kind == "episodic")
        .order_by(ChatMemory.id.desc())
        .limit(4)
    )
    episodes = list(reversed(list(episode_result.scalars().all())))
    if episodes:
        fragments.append("Eventos relevantes:")
        fragments.extend(f"- {item.content}" for item in episodes)

    return "\n".join(fragments).strip()


async def recall_recent_chat(session: AsyncSession, chat_id: str) -> str:
    short_result = await session.execute(
        select(ChatMemory)
        .where(ChatMemory.chat_id == str(chat_id), ChatMemory.kind == "short")
        .order_by(ChatMemory.id.desc())
        .limit(settings.memory_short_limit)
    )
    messages = list(reversed(list(short_result.scalars().all())))
    if not messages:
        return "No tengo contexto reciente guardado en este chat."

    return "Esto es lo ultimo relevante del chat:\n" + "\n".join(
        f"- {item.role}: {item.content}" for item in messages
    )


async def get_pending_action(session: AsyncSession, chat_id: str) -> PendingAction | None:
    return await session.get(PendingAction, str(chat_id))


async def set_pending_action(session: AsyncSession, chat_id: str, action_type: str, raw_text: str, question: str) -> None:
    item = await session.get(PendingAction, str(chat_id))
    timestamp = now_iso()
    if item is None:
        session.add(
            PendingAction(
                chat_id=str(chat_id),
                action_type=action_type,
                raw_text=raw_text[:4000],
                question=question,
                created_at=timestamp,
                updated_at=timestamp,
            )
        )
        return

    item.action_type = action_type
    item.raw_text = raw_text[:4000]
    item.question = question
    item.updated_at = timestamp


async def clear_pending_action(session: AsyncSession, chat_id: str) -> None:
    item = await session.get(PendingAction, str(chat_id))
    if item is not None:
        await session.delete(item)


async def clear_chat_memory(session: AsyncSession, chat_id: str) -> None:
    await session.execute(delete(ChatMemory).where(ChatMemory.chat_id == str(chat_id)))
    await clear_pending_action(session, chat_id)