agentrecall-db 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentrecall/__init__.py +33 -0
- agentrecall/cli.py +209 -0
- agentrecall/config.py +42 -0
- agentrecall/embeddings.py +84 -0
- agentrecall/errors.py +36 -0
- agentrecall/mcp_server.py +70 -0
- agentrecall/memory.py +261 -0
- agentrecall/models.py +70 -0
- agentrecall/search.py +82 -0
- agentrecall/store.py +493 -0
- agentrecall_db-0.1.0.dist-info/METADATA +243 -0
- agentrecall_db-0.1.0.dist-info/RECORD +15 -0
- agentrecall_db-0.1.0.dist-info/WHEEL +4 -0
- agentrecall_db-0.1.0.dist-info/entry_points.txt +2 -0
- agentrecall_db-0.1.0.dist-info/licenses/LICENSE +21 -0
agentrecall/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""agentrecall — agent memory in a single SQLite file.
|
|
2
|
+
|
|
3
|
+
No vector DB, no server, no cloud. Keyword recall works out of the box on stdlib alone;
|
|
4
|
+
install ``agentrecall[semantic]`` for torch-free hybrid semantic search.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .embeddings import Embedder, Model2VecEmbedder, get_default_embedder
|
|
10
|
+
from .errors import (
|
|
11
|
+
AgentRecallError,
|
|
12
|
+
EmbeddingsUnavailable,
|
|
13
|
+
MemoryNotFound,
|
|
14
|
+
StoreError,
|
|
15
|
+
)
|
|
16
|
+
from .memory import Memory
|
|
17
|
+
from .models import MemoryHit, MemoryRecord
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Memory",
|
|
23
|
+
"MemoryRecord",
|
|
24
|
+
"MemoryHit",
|
|
25
|
+
"Embedder",
|
|
26
|
+
"Model2VecEmbedder",
|
|
27
|
+
"get_default_embedder",
|
|
28
|
+
"AgentRecallError",
|
|
29
|
+
"MemoryNotFound",
|
|
30
|
+
"EmbeddingsUnavailable",
|
|
31
|
+
"StoreError",
|
|
32
|
+
"__version__",
|
|
33
|
+
]
|
agentrecall/cli.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""``agentrecall`` command-line interface (argparse, stdlib only).
|
|
2
|
+
|
|
3
|
+
All subcommands honour ``--db`` and the ``AGENTRECALL_*`` environment variables. Use
|
|
4
|
+
``--json`` on read commands for machine-readable output.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .config import Settings
|
|
16
|
+
from .memory import Memory
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _split_tags(value: str | None) -> list[str] | None:
|
|
20
|
+
if not value:
|
|
21
|
+
return None
|
|
22
|
+
tags = [t.strip() for t in value.split(",") if t.strip()]
|
|
23
|
+
return tags or None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_memory(args: argparse.Namespace) -> Memory:
|
|
27
|
+
settings = Settings.from_env()
|
|
28
|
+
db_path = getattr(args, "db", None) or settings.db_path
|
|
29
|
+
namespace = getattr(args, "namespace", None) or settings.namespace
|
|
30
|
+
embeddings = settings.embeddings_value()
|
|
31
|
+
if getattr(args, "embeddings", None):
|
|
32
|
+
embeddings = {"auto": "auto", "true": True, "false": False}[args.embeddings]
|
|
33
|
+
return Memory(db_path, namespace=namespace, embeddings=embeddings)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _truncate(text: str, width: int = 80) -> str:
|
|
37
|
+
text = text.replace("\n", " ")
|
|
38
|
+
return text if len(text) <= width else text[: width - 1] + "…"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _cmd_add(args: argparse.Namespace) -> int:
|
|
42
|
+
with _build_memory(args) as mem:
|
|
43
|
+
record = mem.add(
|
|
44
|
+
args.content,
|
|
45
|
+
tags=_split_tags(args.tags),
|
|
46
|
+
importance=args.importance,
|
|
47
|
+
)
|
|
48
|
+
print(f"#{record.id} {_truncate(record.content)}")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cmd_search(args: argparse.Namespace) -> int:
|
|
53
|
+
with _build_memory(args) as mem:
|
|
54
|
+
hits = mem.search(args.query, k=args.k, tags=_split_tags(args.tags))
|
|
55
|
+
if args.json:
|
|
56
|
+
print(json.dumps([h.to_dict() for h in hits], ensure_ascii=False, indent=2))
|
|
57
|
+
return 0
|
|
58
|
+
if not hits:
|
|
59
|
+
print("(no matches)")
|
|
60
|
+
return 0
|
|
61
|
+
for hit in hits:
|
|
62
|
+
print(f"{hit.score:6.3f} #{hit.id:<4} {_truncate(hit.content)}")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _cmd_list(args: argparse.Namespace) -> int:
|
|
67
|
+
with _build_memory(args) as mem:
|
|
68
|
+
records = mem.all(limit=args.limit, tags=_split_tags(args.tags))
|
|
69
|
+
if args.json:
|
|
70
|
+
print(json.dumps([r.to_dict() for r in records], ensure_ascii=False, indent=2))
|
|
71
|
+
return 0
|
|
72
|
+
if not records:
|
|
73
|
+
print("(empty)")
|
|
74
|
+
return 0
|
|
75
|
+
for record in records:
|
|
76
|
+
tags = f" [{', '.join(record.tags)}]" if record.tags else ""
|
|
77
|
+
print(f"#{record.id:<4} {_truncate(record.content)}{tags}")
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _cmd_get(args: argparse.Namespace) -> int:
|
|
82
|
+
from .errors import MemoryNotFound
|
|
83
|
+
|
|
84
|
+
with _build_memory(args) as mem:
|
|
85
|
+
try:
|
|
86
|
+
record = mem.get(args.id)
|
|
87
|
+
except MemoryNotFound:
|
|
88
|
+
print(f"no memory with id {args.id}", file=sys.stderr)
|
|
89
|
+
return 1
|
|
90
|
+
print(json.dumps(record.to_dict(), ensure_ascii=False, indent=2))
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _cmd_delete(args: argparse.Namespace) -> int:
|
|
95
|
+
with _build_memory(args) as mem:
|
|
96
|
+
ok = mem.delete(args.id)
|
|
97
|
+
print("deleted" if ok else "not found")
|
|
98
|
+
return 0 if ok else 1
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cmd_forget(args: argparse.Namespace) -> int:
|
|
102
|
+
before = datetime.fromisoformat(args.before) if args.before else None
|
|
103
|
+
with _build_memory(args) as mem:
|
|
104
|
+
removed = mem.forget(before=before, keep_last=args.keep_last)
|
|
105
|
+
print(f"forgot {removed} memorie(s)")
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cmd_stats(args: argparse.Namespace) -> int:
|
|
110
|
+
with _build_memory(args) as mem:
|
|
111
|
+
data = {
|
|
112
|
+
"namespace": mem.namespace,
|
|
113
|
+
"count": mem.count(),
|
|
114
|
+
"semantic": mem.semantic_enabled,
|
|
115
|
+
}
|
|
116
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _cmd_export(args: argparse.Namespace) -> int:
|
|
121
|
+
with _build_memory(args) as mem:
|
|
122
|
+
records = mem.all()
|
|
123
|
+
if args.format == "json":
|
|
124
|
+
print(json.dumps([r.to_dict() for r in records], ensure_ascii=False, indent=2))
|
|
125
|
+
else: # markdown
|
|
126
|
+
for record in records:
|
|
127
|
+
tags = f" `{' '.join(record.tags)}`" if record.tags else ""
|
|
128
|
+
print(f"- **#{record.id}**{tags} — {record.content}")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
133
|
+
from .mcp_server import serve
|
|
134
|
+
|
|
135
|
+
mem = _build_memory(args)
|
|
136
|
+
try:
|
|
137
|
+
serve(mem, transport=args.transport)
|
|
138
|
+
finally:
|
|
139
|
+
mem.close()
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
144
|
+
parser = argparse.ArgumentParser(
|
|
145
|
+
prog="agentrecall",
|
|
146
|
+
description="Agent memory in a single SQLite file.",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument("--version", action="version", version=f"agentrecall {__version__}")
|
|
149
|
+
parser.add_argument("--db", help="database file (env AGENTRECALL_DB)")
|
|
150
|
+
parser.add_argument("--namespace", help="namespace (env AGENTRECALL_NAMESPACE)")
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--embeddings", choices=["auto", "true", "false"], help="semantic search mode"
|
|
153
|
+
)
|
|
154
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
155
|
+
|
|
156
|
+
p_add = sub.add_parser("add", help="store a memory")
|
|
157
|
+
p_add.add_argument("content")
|
|
158
|
+
p_add.add_argument("--tags", help="comma-separated tags")
|
|
159
|
+
p_add.add_argument("--importance", type=float, default=1.0)
|
|
160
|
+
p_add.set_defaults(func=_cmd_add)
|
|
161
|
+
|
|
162
|
+
p_search = sub.add_parser("search", help="search memories")
|
|
163
|
+
p_search.add_argument("query")
|
|
164
|
+
p_search.add_argument("-k", type=int, default=5)
|
|
165
|
+
p_search.add_argument("--tags", help="comma-separated tags filter")
|
|
166
|
+
p_search.add_argument("--json", action="store_true")
|
|
167
|
+
p_search.set_defaults(func=_cmd_search)
|
|
168
|
+
|
|
169
|
+
p_list = sub.add_parser("list", help="list memories")
|
|
170
|
+
p_list.add_argument("--limit", type=int, default=20)
|
|
171
|
+
p_list.add_argument("--tags", help="comma-separated tags filter")
|
|
172
|
+
p_list.add_argument("--json", action="store_true")
|
|
173
|
+
p_list.set_defaults(func=_cmd_list)
|
|
174
|
+
|
|
175
|
+
p_get = sub.add_parser("get", help="show one memory")
|
|
176
|
+
p_get.add_argument("id", type=int)
|
|
177
|
+
p_get.set_defaults(func=_cmd_get)
|
|
178
|
+
|
|
179
|
+
p_delete = sub.add_parser("delete", help="delete one memory")
|
|
180
|
+
p_delete.add_argument("id", type=int)
|
|
181
|
+
p_delete.set_defaults(func=_cmd_delete)
|
|
182
|
+
|
|
183
|
+
p_forget = sub.add_parser("forget", help="bulk-delete by age or count")
|
|
184
|
+
p_forget.add_argument("--before", help="ISO-8601 datetime; delete older")
|
|
185
|
+
p_forget.add_argument("--keep-last", type=int, dest="keep_last")
|
|
186
|
+
p_forget.set_defaults(func=_cmd_forget)
|
|
187
|
+
|
|
188
|
+
p_stats = sub.add_parser("stats", help="show counts and config")
|
|
189
|
+
p_stats.set_defaults(func=_cmd_stats)
|
|
190
|
+
|
|
191
|
+
p_export = sub.add_parser("export", help="dump memories")
|
|
192
|
+
p_export.add_argument("--format", choices=["json", "md"], default="json")
|
|
193
|
+
p_export.set_defaults(func=_cmd_export)
|
|
194
|
+
|
|
195
|
+
p_serve = sub.add_parser("serve", help="run the MCP server")
|
|
196
|
+
p_serve.add_argument("--transport", default="stdio")
|
|
197
|
+
p_serve.set_defaults(func=_cmd_serve)
|
|
198
|
+
|
|
199
|
+
return parser
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main(argv: list[str] | None = None) -> int:
|
|
203
|
+
parser = build_parser()
|
|
204
|
+
args = parser.parse_args(argv)
|
|
205
|
+
return args.func(args)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__": # pragma: no cover
|
|
209
|
+
raise SystemExit(main())
|
agentrecall/config.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Lightweight settings read from ``AGENTRECALL_*`` environment variables.
|
|
2
|
+
|
|
3
|
+
Stdlib only — no pydantic, no extra dependency. Used by the CLI and MCP server so the
|
|
4
|
+
same database can be configured once via the environment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from .embeddings import DEFAULT_MODEL
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Settings:
|
|
17
|
+
db_path: str = "agentrecall.db"
|
|
18
|
+
namespace: str = "default"
|
|
19
|
+
embeddings: str = "auto" # "auto" | "true" | "false"
|
|
20
|
+
model: str = DEFAULT_MODEL
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_env(cls) -> Settings:
|
|
24
|
+
env = os.environ
|
|
25
|
+
return cls(
|
|
26
|
+
db_path=env.get("AGENTRECALL_DB", cls.db_path),
|
|
27
|
+
namespace=env.get("AGENTRECALL_NAMESPACE", cls.namespace),
|
|
28
|
+
embeddings=env.get("AGENTRECALL_EMBEDDINGS", cls.embeddings).lower(),
|
|
29
|
+
model=env.get("AGENTRECALL_MODEL", cls.model),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def embeddings_value(self) -> bool | str:
|
|
33
|
+
"""Map the string setting to the ``Memory(embeddings=...)`` argument."""
|
|
34
|
+
value = self.embeddings.lower()
|
|
35
|
+
if value == "true":
|
|
36
|
+
return True
|
|
37
|
+
if value == "false":
|
|
38
|
+
return False
|
|
39
|
+
return "auto"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["Settings"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Embeddings for semantic recall — optional and torch-free.
|
|
2
|
+
|
|
3
|
+
The default :class:`Model2VecEmbedder` wraps `model2vec <https://github.com/MinishLab/model2vec>`_
|
|
4
|
+
static embeddings: ~10 MB models, CPU-only, no PyTorch. Bring your own embedder by passing
|
|
5
|
+
any object that satisfies the :class:`Embedder` protocol to ``Memory(embedder=...)``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
from .errors import EmbeddingsUnavailable
|
|
13
|
+
|
|
14
|
+
DEFAULT_MODEL = "minishlab/potion-base-8M"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class Embedder(Protocol):
|
|
19
|
+
"""Anything that can turn texts into fixed-length vectors.
|
|
20
|
+
|
|
21
|
+
Implementations must be deterministic for a given input and return one vector per
|
|
22
|
+
input text, each of length :attr:`dim`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def dim(self) -> int: ...
|
|
27
|
+
|
|
28
|
+
def embed(self, texts: list[str]) -> list[list[float]]: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Model2VecEmbedder:
|
|
32
|
+
"""Static-embedding embedder (no torch, no GPU).
|
|
33
|
+
|
|
34
|
+
The model is loaded lazily on the first :meth:`embed` call so that constructing a
|
|
35
|
+
:class:`~agentrecall.memory.Memory` in ``embeddings="auto"`` mode is cheap. Raises
|
|
36
|
+
:class:`~agentrecall.errors.EmbeddingsUnavailable` if ``model2vec`` is not installed.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, model: str = DEFAULT_MODEL) -> None:
|
|
40
|
+
self._model_name = model
|
|
41
|
+
self._model = None
|
|
42
|
+
self._dim: int | None = None
|
|
43
|
+
|
|
44
|
+
def _ensure_loaded(self) -> None:
|
|
45
|
+
if self._model is not None:
|
|
46
|
+
return
|
|
47
|
+
try:
|
|
48
|
+
from model2vec import StaticModel
|
|
49
|
+
except ImportError as exc: # pragma: no cover - exercised via guarded tests
|
|
50
|
+
raise EmbeddingsUnavailable(
|
|
51
|
+
"Semantic search needs the optional 'semantic' extra. "
|
|
52
|
+
"Install it with: pip install 'agentrecall[semantic]'"
|
|
53
|
+
) from exc
|
|
54
|
+
self._model = StaticModel.from_pretrained(self._model_name)
|
|
55
|
+
# model2vec exposes the embedding dimension on the loaded model.
|
|
56
|
+
self._dim = int(self._model.dim)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def dim(self) -> int:
|
|
60
|
+
self._ensure_loaded()
|
|
61
|
+
assert self._dim is not None
|
|
62
|
+
return self._dim
|
|
63
|
+
|
|
64
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
65
|
+
self._ensure_loaded()
|
|
66
|
+
assert self._model is not None
|
|
67
|
+
vectors = self._model.encode(list(texts))
|
|
68
|
+
# model2vec returns a numpy array; normalise to plain Python floats.
|
|
69
|
+
return [[float(x) for x in row] for row in vectors]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_default_embedder() -> Embedder | None:
|
|
73
|
+
"""Return a :class:`Model2VecEmbedder` if ``model2vec`` is importable, else ``None``.
|
|
74
|
+
|
|
75
|
+
Never raises — this is the probe used by ``embeddings="auto"``.
|
|
76
|
+
"""
|
|
77
|
+
import importlib.util
|
|
78
|
+
|
|
79
|
+
if importlib.util.find_spec("model2vec") is None:
|
|
80
|
+
return None
|
|
81
|
+
return Model2VecEmbedder()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["Embedder", "Model2VecEmbedder", "get_default_embedder", "DEFAULT_MODEL"]
|
agentrecall/errors.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Exception hierarchy for agentrecall.
|
|
2
|
+
|
|
3
|
+
All errors raised by the library subclass :class:`AgentRecallError`, so callers can
|
|
4
|
+
``except AgentRecallError`` to catch anything the library throws.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentRecallError(Exception):
|
|
11
|
+
"""Base class for all agentrecall errors."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryNotFound(AgentRecallError):
|
|
15
|
+
"""Raised by ``get()`` / ``update()`` / ``delete()`` when the id does not exist."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, memory_id: int) -> None:
|
|
18
|
+
self.memory_id = memory_id
|
|
19
|
+
super().__init__(f"No memory with id {memory_id!r}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EmbeddingsUnavailable(AgentRecallError):
|
|
23
|
+
"""Raised when semantic mode is required (``embeddings=True``) but the optional
|
|
24
|
+
``[semantic]`` extra (model2vec / sqlite-vec) is not installed or cannot load."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StoreError(AgentRecallError):
|
|
28
|
+
"""Wraps an unexpected SQLite-level failure."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AgentRecallError",
|
|
33
|
+
"MemoryNotFound",
|
|
34
|
+
"EmbeddingsUnavailable",
|
|
35
|
+
"StoreError",
|
|
36
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Expose a :class:`~agentrecall.memory.Memory` as an MCP server.
|
|
2
|
+
|
|
3
|
+
This is a drop-in, embeddings-capable alternative to the official memory MCP server
|
|
4
|
+
(which persists to a keyword-only JSONL flat file). Here memories live in a portable
|
|
5
|
+
SQLite file and recall can be hybrid keyword + semantic.
|
|
6
|
+
|
|
7
|
+
Requires the optional ``mcp`` extra: ``pip install 'agentrecall[mcp]'``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .memory import Memory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_mcp_server(memory: Memory, *, name: str = "agentrecall"):
|
|
16
|
+
"""Return a configured FastMCP server exposing remember/recall/forget tools."""
|
|
17
|
+
try:
|
|
18
|
+
from mcp.server.fastmcp import FastMCP
|
|
19
|
+
except ImportError as exc: # pragma: no cover - exercised only without the extra
|
|
20
|
+
raise ImportError(
|
|
21
|
+
"The agentrecall MCP server needs the optional 'mcp' extra. "
|
|
22
|
+
"Install it with: pip install 'agentrecall[mcp]'"
|
|
23
|
+
) from exc
|
|
24
|
+
|
|
25
|
+
server = FastMCP(name)
|
|
26
|
+
|
|
27
|
+
@server.tool()
|
|
28
|
+
def remember(
|
|
29
|
+
content: str,
|
|
30
|
+
tags: list[str] | None = None,
|
|
31
|
+
metadata: dict | None = None,
|
|
32
|
+
importance: float = 1.0,
|
|
33
|
+
) -> dict:
|
|
34
|
+
"""Store a new memory verbatim and return the stored record."""
|
|
35
|
+
record = memory.add(content, tags=tags, metadata=metadata, importance=importance)
|
|
36
|
+
return record.to_dict()
|
|
37
|
+
|
|
38
|
+
@server.tool()
|
|
39
|
+
def recall(query: str, k: int = 5, tags: list[str] | None = None) -> list[dict]:
|
|
40
|
+
"""Search stored memories; returns the most relevant first."""
|
|
41
|
+
return [hit.to_dict() for hit in memory.search(query, k=k, tags=tags)]
|
|
42
|
+
|
|
43
|
+
@server.tool()
|
|
44
|
+
def forget(memory_id: int) -> bool:
|
|
45
|
+
"""Delete a memory by id. Returns True if a row was removed."""
|
|
46
|
+
return memory.delete(memory_id)
|
|
47
|
+
|
|
48
|
+
@server.tool()
|
|
49
|
+
def list_memories(limit: int = 20) -> list[dict]:
|
|
50
|
+
"""List the most recent memories."""
|
|
51
|
+
return [record.to_dict() for record in memory.all(limit=limit)]
|
|
52
|
+
|
|
53
|
+
@server.tool()
|
|
54
|
+
def memory_stats() -> dict:
|
|
55
|
+
"""Return counts and the active configuration."""
|
|
56
|
+
return {
|
|
57
|
+
"count": memory.count(),
|
|
58
|
+
"namespace": memory.namespace,
|
|
59
|
+
"semantic": memory.semantic_enabled,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return server
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def serve(memory: Memory, *, transport: str = "stdio") -> None:
|
|
66
|
+
"""Build and run the MCP server (blocking)."""
|
|
67
|
+
build_mcp_server(memory).run(transport=transport)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["build_mcp_server", "serve"]
|