rosen-sdk 0.2.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.
rosen/__init__.py ADDED
@@ -0,0 +1,314 @@
1
+ """Rosen Python agent SDK (local MVP).
2
+
3
+ An agent is a function ``handler(ctx: Context) -> str | Result``. The Rosen
4
+ runtime invokes the agent as a subprocess, writing a JSON request to stdin and
5
+ reading a JSON response from stdout. ``run(handler)`` implements that protocol
6
+ so agent authors only write ``handler``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ import http.server
12
+ import threading
13
+ import os
14
+ import json
15
+ import sys
16
+ import traceback
17
+ from typing import Callable, Union, TypedDict
18
+ from .model import Engine, Model
19
+ from .tool import Tool, Tools, Transport
20
+ from .memory import Memories, Memory, MemoryBackend
21
+
22
+ __all__ = [
23
+ "Context",
24
+ "Engine",
25
+ "Result",
26
+ "Model",
27
+ "Tool",
28
+ "Tools",
29
+ "Transport",
30
+ "Memory",
31
+ "Memories",
32
+ "MemoryBackend",
33
+ "run",
34
+ "serve"]
35
+
36
+
37
+ @dataclasses.dataclass
38
+ class Context:
39
+ """Everything an agent receives for a single run."""
40
+
41
+ input: str
42
+ trace_id: str = ""
43
+ agent: str = ""
44
+ namespace: str = ""
45
+ conversation_id: str = ""
46
+ model: Model = dataclasses.field(default_factory=Model)
47
+ tools: Tools = dataclasses.field(default_factory=Tools)
48
+ memory: Memories = dataclasses.field(default_factory=Memories)
49
+
50
+ def log(self, message: str) -> None:
51
+ """Emit a trace line to stderr (stdout is the JSON result channel)."""
52
+ print(f"[rosen trace {self.trace_id}] {message}", file=sys.stderr)
53
+
54
+
55
+ @dataclasses.dataclass
56
+ class Result:
57
+ """An explicit agent result. Returning a plain ``str`` is also accepted."""
58
+
59
+ output: str
60
+
61
+
62
+ class ToolSpec(TypedDict, total=False):
63
+ name: str
64
+ transport: str
65
+ endpoint: str
66
+ permissions: str
67
+
68
+
69
+ class ModelSpec(TypedDict, total=False):
70
+ engine: str
71
+ model: str
72
+
73
+
74
+ class MemorySpec(TypedDict, total=False):
75
+ name: str
76
+ backend: str
77
+ endpoint: str
78
+ collection: str
79
+ mode: str
80
+ embedder: str
81
+
82
+
83
+ class Request(TypedDict, total=False): # total=False = every key optional
84
+ input: str
85
+ traceId: str
86
+ namespace: str
87
+ conversationId: str
88
+ model: ModelSpec
89
+ tools: list[ToolSpec]
90
+ memory: list[MemorySpec]
91
+
92
+
93
+ Handler = Callable[[Context], Union[str, Result]]
94
+
95
+
96
+ def _build_context(req: dict, memory: Memories | None = None) -> Context:
97
+ """
98
+ Build a Context from a decoded JSON request (shared by run and server).
99
+ """
100
+ model_info = req.get("model") or {}
101
+ tools = Tools([
102
+ Tool(
103
+ name=t.get("name", ""),
104
+ transport=t.get("transport", ""),
105
+ endpoint=t.get("endpoint", ""),
106
+ permissions=t.get("permissions", "")
107
+ )
108
+ for t in (req.get("tools") or [])
109
+ ])
110
+
111
+ if memory is None:
112
+ memory = _build_memories(req)
113
+
114
+ conversation_id = req.get("conversationId", "")
115
+
116
+ return Context(
117
+ input=req.get("input", ""),
118
+ trace_id=req.get("traceId", ""),
119
+ agent=req.get("agent", ""),
120
+ namespace=req.get("namespace", ""),
121
+ conversation_id=conversation_id,
122
+ model=Model(engine=model_info.get("engine", ""),
123
+ model=model_info.get("model", ""),
124
+ tools=tools,
125
+ memory=memory,
126
+ conversation_id=conversation_id),
127
+ tools=tools,
128
+ memory=memory
129
+ )
130
+
131
+
132
+ def _build_memories(req: dict) -> Memories:
133
+ return Memories([
134
+ Memory(
135
+ name=m.get("name", ""),
136
+ backend=m.get("backend", ""),
137
+ endpoint=m.get("endpoint", ""),
138
+ collection=m.get("collection", ""),
139
+ mode=m.get("mode", ""),
140
+ embedder=m.get("embedder")
141
+ )
142
+ for m in (req.get("memory") or [])
143
+ ])
144
+
145
+
146
+ def _invoke(
147
+ handler: Handler,
148
+ req: dict,
149
+ base: dict | None = None,
150
+ base_memory: Memories | None = None) -> dict:
151
+ """
152
+ Run the handler for one request, returning an {output}/{error} payload.
153
+ """
154
+ request_memory = "memory" in req
155
+ if base:
156
+ req = {**base, **req}
157
+ memory = None if request_memory else base_memory
158
+ ctx = _build_context(req, memory=memory)
159
+ try:
160
+ result = handler(ctx)
161
+ except Exception as exc:
162
+ traceback.print_exc(file=sys.stderr)
163
+ return {"error": str(exc)}
164
+ output = result.output if isinstance(result, Result) else str(result)
165
+ return {"output": output}
166
+
167
+
168
+ def run(handler: Handler, stdin=None, stdout=None) -> None:
169
+ """
170
+ Read one JSON request, invoke ``handler``, write one JSON response.
171
+
172
+ The request comes from ``stdin``; if that is empty (a one-shot k8s Job pod has
173
+ no stdin from the controller) it falls back to the ``ROSEN_REQUEST`` env var.
174
+ The response is written to ``stdout`` and, whe ``ROSEN_RESULT_PATH`` is set,
175
+ mirrored there (the Job's termination-log) so k8s surfaces it in the pod's
176
+ termination.message -- a structured result channel, no log scrapping.
177
+ """
178
+ stdin = stdin if stdin is not None else sys.stdin
179
+ stdout = stdout if stdout is not None else sys.stdout
180
+
181
+ raw = stdin.read()
182
+ if not raw.strip():
183
+ raw = os.environ.get("ROSEN_REQUEST", "")
184
+ try:
185
+ req = json.loads(raw) if raw.strip() else {}
186
+ except json.JSONDecodeError as exc:
187
+ result = {"error": f"invalid request json: {exc}"}
188
+ else:
189
+ result = _invoke(handler, req)
190
+ _write(stdout, result)
191
+ _mirror_result(result)
192
+
193
+
194
+ def serve(handler: Handler, addr: str | None = None) -> None:
195
+ """
196
+ Run the agent as a long-lived HTTP server (one process, many requests).
197
+
198
+ GET /healthz -> 200: the readiness signal the controller probes.
199
+ POST /invoke -> 202: accept a run, execute the handler on a background
200
+ thread, and ack immediatly with {runId, worker}.
201
+ The worker is this pod's own address so the controller polls
202
+ the exact pod that holds the result.
203
+ GET /result/{runId} -> 200 {status: running|done|error, ...} | 404 unknown.
204
+
205
+ Asynchronous by design: the handler never holds the response open, so a slow run
206
+ can't trip a client-side invoke timeout (the "Failed-but-persisted" split-brain).
207
+
208
+ The bind address somes from ``addr`` or ``ROSEN_SERVE_ADDR`` (default 127.0.0.1:8080);
209
+ ``ROSEN_POD_IP`` (injected by the controller via the downward API) is the address
210
+ advertised for polling, falling back to the bind host.
211
+ """
212
+
213
+ addr = addr or os.environ.get("ROSEN_SERVE_ADDR", "127.0.0.1:8080")
214
+ host, _, port = addr.rpartition(":")
215
+ worker = f"{os.environ.get('ROSEN_POD_IP', host)}:{port}"
216
+
217
+ raw_bindings = os.environ.get("ROSEN_AGENT_BINDINGS")
218
+ base = json.loads(raw_bindings) if raw_bindings else {}
219
+ base_memory = _build_memories(base)
220
+
221
+ results: dict[str, dict] = {}
222
+ lock = threading.Lock()
223
+
224
+ def _run(run_id: str, req: Request) -> None:
225
+ out = _invoke(handler, req, base, base_memory)
226
+ with lock:
227
+ if "error" in out:
228
+ results[run_id] = {"status": "error", "error": out["error"]}
229
+ else:
230
+ results[run_id] = {"status": "done", "output": out["output"]}
231
+
232
+ class _Handler(http.server.BaseHTTPRequestHandler):
233
+ def do_GET(self) -> None:
234
+ if self.path == "/healthz":
235
+ self._respond(200, {"status": "ok"})
236
+ elif self.path.startswith("/result/"):
237
+ run_id = self.path[len("/result/"):]
238
+ with lock:
239
+ result = results.get(run_id)
240
+ if result is None:
241
+ self._respond(404, {"status": "unknown"})
242
+ else:
243
+ self._respond(200, result)
244
+ else:
245
+ self._respond(404, {"error": "not found"})
246
+
247
+ def do_POST(self) -> None:
248
+ if self.path != "/invoke":
249
+ self._respond(404, {"error": "not found"})
250
+ return
251
+ lenght = int(self.headers.get("Content-Length", 0))
252
+ raw = self.rfile.read(lenght).decode("utf-8")
253
+ try:
254
+ req = json.loads(raw) if raw.strip() else {}
255
+ except json.JSONDecodeError as exc:
256
+ self._respond(400, {"error": f"invalid request json: {exc}"})
257
+ return
258
+
259
+ run_id = req.get("traceId")
260
+ if not run_id:
261
+ self._respond(400, {"error": "missing traceId (run id)"})
262
+ return
263
+
264
+ with lock:
265
+ fresh = run_id not in results
266
+ if fresh:
267
+ results[run_id] = {"status": "running"}
268
+ if fresh:
269
+ threading.Thread(target=_run, args=(run_id, req), daemon=True).start()
270
+
271
+ self._respond(202, {"runId": run_id, "worker": worker})
272
+
273
+ def _respond(self, code: int, payload: dict) -> None:
274
+ body = json.dumps(payload).encode("utf-8")
275
+ try:
276
+ self.send_response(code)
277
+ self.send_header("Content-Type", "application/json")
278
+ self.send_header("Content-Length", str(len(body)))
279
+ self.end_headers()
280
+ self.wfile.write(body)
281
+ except (BrokenPipeError, ConnectionResetError):
282
+ print(f"rosen: client disconnected before response (path={self.path})",
283
+ file=sys.stderr, flush=True)
284
+
285
+ def log_message(self, *args) -> None:
286
+ pass
287
+
288
+ server = http.server.ThreadingHTTPServer((host, int(port)), _Handler)
289
+ bound = server.server_address
290
+ print(f"rosen: serving on {bound[0]}:{bound[1]}", file=sys.stderr, flush=True)
291
+ server.serve_forever()
292
+
293
+
294
+ def _write(stdout, payload: dict) -> None:
295
+ json.dump(payload, stdout)
296
+ stdout.write("\n")
297
+ stdout.flush()
298
+
299
+
300
+ def _mirror_result(payload: dict) -> None:
301
+ """
302
+ Best-effort: mirror the result to ROSEN_RESULT_PATH (the Job's termination-log)
303
+ so k8s surfaces it in the pod's termination.message. A one-shot Job as no other
304
+ structured result channel; write failures (e.g. path unwritable locally) are logged
305
+ and ignored so they never mask the real result on stdout.
306
+ """
307
+ path = os.environ.get("ROSEN_RESULT_PATH")
308
+ if not path:
309
+ return
310
+ try:
311
+ with open(path, "w") as f:
312
+ f.write(json.dumps(payload))
313
+ except OSError as exc:
314
+ print(f"rosen: could not write result to {path}: {exc}", file=sys.stderr, flush=True)
rosen/embedder.py ADDED
@@ -0,0 +1,66 @@
1
+ import hashlib
2
+ import os
3
+
4
+
5
+ def _stable_hash(token: str) -> int:
6
+ # Python's hash() is salted per process. Vectors must be
7
+ # reproducible accross the serve subprocess boundry, so
8
+ # hash deterministically.
9
+ return int.from_bytes(hashlib.md5(token.encode()).digest()[:4], "big")
10
+
11
+
12
+ class Embedder:
13
+ dim = 0
14
+
15
+ def embed(self, texts: list) -> list:
16
+ raise NotImplementedError
17
+
18
+
19
+ class MockEmbedder(Embedder):
20
+ """Deterministic, offline embeddings for tests and local dev"""
21
+
22
+ def __init__(self, model: str = "", dim: int = 64):
23
+ self.model = model
24
+ self.dim = dim
25
+
26
+ def embed(self, texts: list) -> list:
27
+ vectors = []
28
+ for text in texts:
29
+ vec = [0.0] * self.dim
30
+ for token in text.lower().split():
31
+ vec[_stable_hash(token) % self.dim] += 1
32
+ vectors.append(vec)
33
+ return vectors
34
+
35
+
36
+ class OpenAIEmbedder(Embedder):
37
+
38
+ def __init__(self, model: str):
39
+ self.model = model
40
+ self.dim = 0
41
+
42
+ def _client(self):
43
+ base_url = os.environ.get("ROSEN_MODEL_BASE_URL")
44
+ api_key = os.environ.get("ROSEN_MODEL_API_KEY")
45
+ if not base_url or not api_key:
46
+ raise RuntimeError("ROSEN_MODEL_BASE_URL and ROSEN_MODEL_API_KEY must be set")
47
+ from openai import OpenAI
48
+ return OpenAI(base_url=base_url, api_key=api_key)
49
+
50
+ def embed(self, texts: list) -> list:
51
+ resp = self._client().embeddings.create(model=self.model, input=texts)
52
+ vectors = [d.embedding for d in resp.data]
53
+ if vectors:
54
+ self.dim = len(vectors[0])
55
+ return vectors
56
+
57
+
58
+ _EMBEDDERS = {
59
+ "mock": MockEmbedder,
60
+ "openai": OpenAIEmbedder,
61
+ "azure-openai": OpenAIEmbedder
62
+ }
63
+
64
+
65
+ def build_embedder(engine: str, model: str) -> Embedder:
66
+ return _EMBEDDERS.get(engine, MockEmbedder)(model)
rosen/memory.py ADDED
@@ -0,0 +1,277 @@
1
+ import json
2
+ import uuid
3
+
4
+ from .embedder import build_embedder
5
+
6
+
7
+ class MemoryBackend:
8
+ def remember(self, text, metadata=None, scope=None) -> str:
9
+ raise NotImplementedError
10
+
11
+ def recall(self, query, k=5, scope=None) -> list:
12
+ raise NotImplementedError
13
+
14
+ def history(self, scope, k=None) -> list:
15
+ raise NotImplementedError
16
+
17
+
18
+ class MockMemory(MemoryBackend):
19
+
20
+ def __init__(self, name: str, endpoint: str = "", collection: str = "", embedder=None):
21
+ self.name = name
22
+ self.endpoint = endpoint
23
+ self.collection = collection
24
+ self._items = []
25
+
26
+ def remember(self, text, metadata=None, scope=None) -> str:
27
+ item_id = str(len(self._items))
28
+ self._items.append(
29
+ {"id": item_id, "scope": scope, "text": text, "metadata": metadata or {}})
30
+ return item_id
31
+
32
+ def recall(self, query, k=5, scope=None) -> list:
33
+ needle = (query or "").lower()
34
+ hits = [
35
+ {"id": it["id"], "text": it["text"], "metadata": it["metadata"]}
36
+ for it in reversed(self._items)
37
+ if it["scope"] == scope and needle in it["text"].lower()
38
+ ]
39
+ return hits
40
+
41
+ def history(self, scope, k=None) -> list:
42
+ turns = [
43
+ {"id": it["id"], "text": it["text"], "metadata": it["metadata"]}
44
+ for it in self._items
45
+ if it["scope"] == scope
46
+ ]
47
+ return turns[-k:] if k else turns
48
+
49
+
50
+ class RedisMemory(MemoryBackend):
51
+ """Conversational memory over a shared Redis. Each scope (conversationId) is a
52
+ Redis list of turns, so a turn landing on any replica reads/writes the same
53
+ list -- conversations survive across replicas and restarts"""
54
+
55
+ def __init__(self, name: str, endpoint: str = "", collection: str = "", embedder=None):
56
+ import redis
57
+ self.name = name
58
+ self.endpoint = endpoint or "redis://127.0.0.1:6379/0"
59
+ self.collection = collection or "rosen"
60
+ self._client = redis.Redis.from_url(self.endpoint, decode_responses=True)
61
+
62
+ def _key(self, scope) -> str:
63
+ return f"{self.collection}:{self.name}:{scope or 'global'}"
64
+
65
+ def remember(self, text, metadata=None, scope=None) -> str:
66
+ item_id = uuid.uuid4().hex
67
+ self._client.rpush(
68
+ self._key(scope),
69
+ json.dumps({"id": item_id, "text": text, "metadata": metadata or {}}))
70
+ return item_id
71
+
72
+ def history(self, scope, k=None) -> list:
73
+ start = -k if k else 0
74
+ return [json.loads(r) for r in self._client.lrange(self._key(scope), start, -1)]
75
+
76
+ def recall(self, query, k=5, scope=None) -> list:
77
+ """simple keyword scan"""
78
+ needle = (query or "").lower()
79
+ items = [json.loads(r) for r in self._client.lrange(self._key(scope), 0, -1)]
80
+ return [it for it in reversed(items) if needle in it["text"].lower()][:k]
81
+
82
+
83
+ class FaissMemory(MemoryBackend):
84
+ """In-process semantic memory"""
85
+
86
+ def __init__(self, name: str, endpoint: str = "", collection: str = "", embedder=None):
87
+ self.name = name
88
+ self.endpoint = endpoint
89
+ self.collection = collection
90
+ self._embedder = embedder
91
+ self._items = []
92
+
93
+ def _embed(self, text: str) -> list:
94
+ if self._embedder is None:
95
+ raise RuntimeError("FaissMemory needs and embedder; bind one via Memory.spec.embedder")
96
+ return self._embedder.embed([text])[0]
97
+
98
+ def remember(self, text, metadata=None, scope=None) -> str:
99
+ item_id = str(len(self._items))
100
+ self._items.append({
101
+ "id": item_id, "scope": scope, "text": text,
102
+ "metadata": metadata or {}, "vector": self._embed(text)})
103
+
104
+ return item_id
105
+
106
+ def recall(self, query, k=5, scope=None) -> list:
107
+ import faiss
108
+ import numpy as np
109
+ hits = [it for it in self._items if it["scope"] == scope]
110
+ if not hits:
111
+ return []
112
+
113
+ vectors = np.array([it["vector"] for it in hits], dtype="float32")
114
+ faiss.normalize_L2(vectors)
115
+ index = faiss.IndexFlatIP(vectors.shape[1])
116
+ index.add(vectors)
117
+
118
+ q = np.array([self._embed(query)], dtype="float32")
119
+ faiss.normalize_L2(q)
120
+
121
+ _, idxs = index.search(q, min(k, len(hits)))
122
+ return [
123
+ {"id": hits[i]["id"], "text": hits[i]["text"], "metadata": hits[i]["metadata"]}
124
+ for i in idxs[0]
125
+ ]
126
+
127
+ def history(self, scope, k=None) -> list:
128
+ raise NotImplementedError("semantic memory has no transcript; use conversational store")
129
+
130
+
131
+ class PgVectorMemory(MemoryBackend):
132
+ """Shared/persistent vector memory over Postgres + pgvector"""
133
+
134
+ def __init__(self, name: str, endpoint: str = "", collection: str = "", embedder=None):
135
+ import psycopg
136
+ from psycopg import sql
137
+ from pgvector.psycopg import register_vector
138
+ self.name = name
139
+ self.endpoint = endpoint or "postgresql://127.0.0.1:5432/rosen"
140
+ self.collection = collection or "rosen_semantic"
141
+ self._embedder = embedder
142
+ self._table = sql.Identifier(self.collection)
143
+ self._conn = psycopg.connect(self.endpoint, autocommit=True)
144
+ self._conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
145
+ register_vector(self._conn)
146
+ self._ready = False
147
+
148
+ def _embed(self, text: str) -> list:
149
+ if self._embedder is None:
150
+ raise RuntimeError(
151
+ "PgVectorMemory needs an embedder; bind one via Memory.spec.embedder")
152
+ return self._embedder.embed([text])[0]
153
+
154
+ def _ensure_table(self, dim: int):
155
+ # Lazily create the table on first use: the dimension comes from the
156
+ # embedder's vectors, so we never hardcode it.
157
+ if self._ready:
158
+ return
159
+ from psycopg import sql
160
+ self._conn.execute(sql.SQL(
161
+ "CREATE TABLE IF NOT EXISTS {table} ("
162
+ "id TEXT PRIMARY KEY, scope TEXT, text TEXT, "
163
+ "metadata JSONB, embedding vector({dim}))"
164
+ ).format(table=self._table, dim=sql.Literal(int(dim))))
165
+ self._ready = True
166
+
167
+ def remember(self, text, metadata=None, scope=None) -> str:
168
+ from psycopg import sql
169
+ from psycopg.types.json import Jsonb
170
+ from pgvector import Vector
171
+ vector = self._embed(text)
172
+ self._ensure_table(len(vector))
173
+ item_id = uuid.uuid4().hex
174
+ self._conn.execute(
175
+ sql.SQL("INSERT INTO {table} (id, scope, text, metadata, embedding) "
176
+ "VALUES (%s, %s, %s, %s, %s)").format(table=self._table),
177
+ (item_id, scope, text, Jsonb(metadata or {}), Vector(vector)))
178
+ return item_id
179
+
180
+ def recall(self, query, k=5, scope=None) -> list:
181
+ from psycopg import sql
182
+ from pgvector import Vector
183
+ vector = self._embed(query)
184
+ self._ensure_table(len(vector))
185
+ where = sql.SQL("WHERE scope = %s") if scope is not None else sql.SQL("")
186
+ params = ([scope] if scope is not None else []) + [Vector(vector), k]
187
+ rows = self._conn.execute(
188
+ sql.SQL("SELECT id, text, metadata FROM {table} {where} "
189
+ "ORDER BY embedding <=> %s LIMIT %s").format(table=self._table, where=where),
190
+ params).fetchall()
191
+ return [{"id": r[0], "text": r[1], "metadata": r[2]} for r in rows]
192
+
193
+ def history(self, scope, k=None) -> list:
194
+ raise NotImplementedError("semantic memory has no transcript; use conversational store")
195
+
196
+ def close(self):
197
+ self._conn.close()
198
+
199
+
200
+ _BACKENDS = {
201
+ "mock": MockMemory,
202
+ "inmemory": MockMemory,
203
+ "redis": RedisMemory,
204
+ "faiss": FaissMemory,
205
+ "pgvector": PgVectorMemory
206
+ }
207
+
208
+
209
+ class Memory:
210
+
211
+ def __init__(self, name="", backend="", endpoint="", collection="", mode="", embedder=None):
212
+ self.name = name
213
+ self.backend = backend
214
+ self.endpoint = endpoint
215
+ self.collection = collection
216
+ self.mode = mode or "semantic"
217
+ built = build_embedder(embedder.get("engine", ""),
218
+ embedder.get("model", "")) if embedder else None
219
+ adapter_cls = _BACKENDS.get(backend, MockMemory)
220
+ self._adapter = adapter_cls(name, endpoint, collection, embedder=built)
221
+
222
+ def remember(self, text, metadata=None, scope=None) -> str:
223
+ return self._adapter.remember(text, metadata, scope)
224
+
225
+ def recall(self, query, k=5, scope=None) -> list:
226
+ return self._adapter.recall(query, k, scope)
227
+
228
+ def history(self, scope, k=None) -> list:
229
+ return self._adapter.history(scope, k)
230
+
231
+
232
+ class Memories:
233
+
234
+ def __init__(self, memories=None):
235
+ self._by_name = {m.name: m for m in (memories or [])}
236
+
237
+ def _by_mode(self, mode: str) -> Memory:
238
+ matches = [m for m in self._by_name.values() if m.mode == mode]
239
+ if not matches:
240
+ raise KeyError(f"no {mode} memory is bound to this agent")
241
+ if len(matches) > 1:
242
+ raise KeyError(f"multiple {mode} memory stores bound; address one by name")
243
+ return matches[0]
244
+
245
+ def has_mode(self, mode: str) -> bool:
246
+ return any(m.mode == mode for m in self._by_name.values())
247
+
248
+ def remember(self, text, metadata=None, scope=None) -> str:
249
+ mode = "conversational" if scope else "semantic"
250
+ return self._by_mode(mode).remember(text, metadata, scope)
251
+
252
+ def recall(self, query, k=5, scope=None) -> list:
253
+ return self._by_mode("semantic").recall(query, k, scope)
254
+
255
+ def history(self, scope, k=None) -> list:
256
+ return self._by_mode("conversational").history(scope, k)
257
+
258
+ def get(self, name: str) -> Memory:
259
+ try:
260
+ return self._by_name[name]
261
+ except KeyError:
262
+ raise KeyError(f"memory {name!r} not available to this agent")
263
+
264
+ def __getitem__(self, name: str) -> Memory:
265
+ return self.get(name)
266
+
267
+ def __contains__(self, name: str) -> bool:
268
+ return name in self._by_name
269
+
270
+ def __iter__(self):
271
+ return iter(self._by_name.values())
272
+
273
+ def __len__(self) -> int:
274
+ return len(self._by_name)
275
+
276
+ def name(self):
277
+ return list(self._by_name.keys())