Coverage for src / lilbee / app / memory.py: 100%
47 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-06-28 01:01 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-06-28 01:01 +0000
1"""Use-case orchestration for long-term chat memory, shared by every surface.
3Surfaces (TUI, CLI, MCP, REST, Python API) call these functions rather than
4constructing ``MemoryRow`` objects or building owner predicates themselves, so
5embedding, id/timestamp assignment, and scoping live in one place.
6"""
8from __future__ import annotations
10import uuid
11from collections.abc import Callable
12from dataclasses import dataclass
13from datetime import UTC, datetime
15from lilbee.app.services import get_services
16from lilbee.core.config import cfg
17from lilbee.data.store import (
18 LOCAL_OWNER,
19 MemoryKind,
20 MemoryRow,
21 MemorySource,
22 agent_recall_predicate,
23 escape_sql_string,
24 local_owner_predicate,
25)
28def make_memory_row(
29 text: str,
30 embed: Callable[[str], list[float]],
31 *,
32 owner: str = LOCAL_OWNER,
33 kind: MemoryKind = MemoryKind.FACT,
34 source: MemorySource = MemorySource.MANUAL,
35 shared: bool = False,
36) -> MemoryRow:
37 """Build a fully populated ``MemoryRow`` with a fresh id, timestamps, and
38 an embedded vector. The single id/timestamp/embedding assignment point, so
39 callers supply only their own ``embed`` and store the result.
40 """
41 now = datetime.now(UTC).isoformat()
42 return MemoryRow(
43 id=uuid.uuid4().hex,
44 owner=owner,
45 shared=shared,
46 kind=kind,
47 source=source,
48 text=text,
49 vector=embed(text),
50 created_at=now,
51 updated_at=now,
52 )
55MEMORY_DISABLED_HINT = (
56 "Memory is off. Turn it on with /set memory_enabled true in the TUI, "
57 "settings_set via MCP, or memory_enabled = true in config.toml."
58)
61def memory_enabled() -> bool:
62 """True when the memory subsystem is switched on (off by default)."""
63 return cfg.memory_enabled
66def remember(
67 text: str,
68 *,
69 owner: str = LOCAL_OWNER,
70 kind: MemoryKind = MemoryKind.FACT,
71 source: MemorySource = MemorySource.MANUAL,
72 shared: bool = False,
73) -> str:
74 """Embed *text* and store it as a memory; returns the stored id."""
75 services = get_services()
76 record = make_memory_row(
77 text,
78 services.embedder.embed,
79 owner=owner,
80 kind=kind,
81 source=source,
82 shared=shared,
83 )
84 return services.store.add_memory(record)
87def recall(query: str, owner: str = LOCAL_OWNER, *, top_k: int | None = None) -> list[MemoryRow]:
88 """Recall *owner*'s facts (plus human-shared facts for agents)."""
89 services = get_services()
90 predicate = local_owner_predicate() if owner == LOCAL_OWNER else agent_recall_predicate(owner)
91 return services.store.search_memories(
92 services.embedder.embed(query),
93 owner_predicate=predicate,
94 top_k=cfg.memory_top_k if top_k is None else top_k,
95 max_distance=cfg.memory_max_distance,
96 )
99def list_memories(owner: str = LOCAL_OWNER) -> list[MemoryRow]:
100 """List all of *owner*'s memories (any kind), newest first."""
101 predicate = (
102 local_owner_predicate() if owner == LOCAL_OWNER else f"owner = '{escape_sql_string(owner)}'"
103 )
104 return get_services().store.get_memories(owner_predicate=predicate)
107def forget(memory_id: str) -> None:
108 """Delete a memory by id."""
109 get_services().store.delete_memory(memory_id)
112def set_memory_shared(memory_id: str, *, shared: bool) -> bool:
113 """Set a memory's shared-with-agents flag; returns True when the id exists."""
114 return get_services().store.update_memory(memory_id, shared=shared)
117def auto_extract_enabled() -> bool:
118 """True when auto-extraction is on (requires the master gate too)."""
119 return cfg.memory_enabled and cfg.memory_auto_extract
122@dataclass(frozen=True, slots=True)
123class SavedMemory:
124 """A memory created by auto-extraction: its stored id, kind, and text."""
126 id: str
127 kind: MemoryKind
128 text: str
131def auto_extract(question: str, answer: str) -> list[SavedMemory]:
132 """Extract durable memories from a chat turn and store them.
134 Returns one :class:`SavedMemory` per stored memory. Stored memories are
135 ``source=EXTRACTED`` and are recalled like any other; the user manages them
136 in ``/memories``. A no-op (returns ``[]``) unless both the master gate and
137 ``memory_auto_extract`` are on.
138 """
139 from lilbee.retrieval.query.memory_extract import extract_memories
141 if not auto_extract_enabled():
142 return []
143 services = get_services()
144 extracted = extract_memories(question, answer, services.provider.chat)
145 stored: list[SavedMemory] = []
146 for memory in extracted:
147 memory_id = remember(memory.text, kind=memory.kind, source=MemorySource.EXTRACTED)
148 stored.append(SavedMemory(id=memory_id, kind=memory.kind, text=memory.text))
149 return stored