crewai-memory-dynamodb 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.
@@ -0,0 +1,216 @@
1
+ """Amazon DynamoDB ``StorageBackend`` for CrewAI unified ``Memory``.
2
+
3
+ One table, ``PK`` = scope path, ``SK`` = record id, with a DynamoDB **vector
4
+ index** on ``embedding`` (cosine) whose search schema declares ``PK`` as an
5
+ inline filter. ``search`` runs DynamoDB's native ``SearchVectors`` — one call
6
+ per concrete scope under the requested prefix (DynamoDB allows only equality on
7
+ a string search-schema attribute) — and the core merges by similarity and
8
+ applies category / metadata filters. Built on ``crewai-memory-core``.
9
+
10
+ Requires ``boto3>=1.43.78`` and a region where DynamoDB vector search is
11
+ available. The vector index can only be declared at table creation.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from decimal import Decimal
17
+ from typing import Any, List, Optional, Tuple
18
+
19
+ import boto3
20
+ from boto3.dynamodb.conditions import Attr, Key
21
+ from boto3.dynamodb.types import TypeDeserializer
22
+
23
+ from crewai_memory_core import MemoryBackend, ROW_FIELDS, in_scope, norm_scope
24
+
25
+ __all__ = ["DynamoDBMemoryBackend"]
26
+
27
+
28
+ def _plain(obj: Any) -> Any:
29
+ if isinstance(obj, Decimal):
30
+ return int(obj) if obj == obj.to_integral_value() else float(obj)
31
+ if isinstance(obj, dict):
32
+ return {k: _plain(v) for k, v in obj.items()}
33
+ if isinstance(obj, list):
34
+ return [_plain(v) for v in obj]
35
+ return obj
36
+
37
+
38
+ def _ddb(obj: Any) -> Any:
39
+ if isinstance(obj, float):
40
+ return Decimal(repr(obj))
41
+ if isinstance(obj, dict):
42
+ return {k: _ddb(v) for k, v in obj.items()}
43
+ if isinstance(obj, list):
44
+ return [_ddb(v) for v in obj]
45
+ return obj
46
+
47
+
48
+ class DynamoDBMemoryBackend(MemoryBackend):
49
+ """CrewAI ``StorageBackend`` on DynamoDB with native vector search.
50
+
51
+ Example:
52
+ ```python
53
+ from crewai.memory import Memory
54
+ from crewai_memory_dynamodb import DynamoDBMemoryBackend
55
+
56
+ backend = DynamoDBMemoryBackend(table_name="crewai-memory", dimensions=3072)
57
+ memory = Memory(storage=backend) # embedder default: OpenAI text-embedding-3-large (3072)
58
+ ```
59
+
60
+ ``dimensions`` must match the embedder configured on ``Memory``.
61
+ """
62
+
63
+ supports_native_vector_search = True
64
+
65
+ def __init__(
66
+ self,
67
+ table_name: str,
68
+ *,
69
+ dimensions: int = 3072,
70
+ index_name: str = "vector_index",
71
+ region_name: Optional[str] = None,
72
+ boto_session: Optional["boto3.Session"] = None,
73
+ endpoint_url: Optional[str] = None,
74
+ ) -> None:
75
+ session = boto_session or boto3.Session(region_name=region_name)
76
+ self._client = session.client("dynamodb", endpoint_url=endpoint_url)
77
+ self._resource = session.resource("dynamodb", endpoint_url=endpoint_url)
78
+ self.table_name = table_name
79
+ self.dimensions = dimensions
80
+ self._index_name = index_name
81
+ self._deser = TypeDeserializer()
82
+ self._ensure_table()
83
+ self.table = self._resource.Table(table_name)
84
+
85
+ # ------------------------------------------------------------------ setup
86
+ def _ensure_table(self) -> None:
87
+ try:
88
+ self._client.describe_table(TableName=self.table_name)
89
+ return
90
+ except self._client.exceptions.ResourceNotFoundException:
91
+ pass
92
+ self._client.create_table(
93
+ TableName=self.table_name,
94
+ KeySchema=[
95
+ {"AttributeName": "PK", "KeyType": "HASH"},
96
+ {"AttributeName": "SK", "KeyType": "RANGE"},
97
+ ],
98
+ AttributeDefinitions=[
99
+ {"AttributeName": "PK", "AttributeType": "S"},
100
+ {"AttributeName": "SK", "AttributeType": "S"},
101
+ ],
102
+ BillingMode="PAY_PER_REQUEST",
103
+ GlobalSecondaryIndexes=[
104
+ {
105
+ "IndexName": "by_id",
106
+ "KeySchema": [{"AttributeName": "SK", "KeyType": "HASH"}],
107
+ "Projection": {"ProjectionType": "ALL"},
108
+ }
109
+ ],
110
+ VectorIndexes=[
111
+ {
112
+ "IndexName": self._index_name,
113
+ "VectorAttribute": {"AttributeName": "embedding"},
114
+ "Dimensions": self.dimensions,
115
+ "DistanceFunction": "COSINE",
116
+ "Projection": {"ProjectionType": "ALL"},
117
+ "SearchSchema": [{"AttributeName": "PK", "SearchSchemaElementType": "INLINE_FILTER"}],
118
+ }
119
+ ],
120
+ )
121
+ self._client.get_waiter("table_exists").wait(TableName=self.table_name)
122
+
123
+ # ------------------------------------------------------------------ rows
124
+ def _to_row(self, item: dict) -> dict:
125
+ item = _plain(item)
126
+ row = {f: item.get(f) for f in ROW_FIELDS}
127
+ row["id"] = item["SK"]
128
+ row["scope"] = item["PK"]
129
+ row["embedding"] = [float(v) for v in item["embedding"]] if item.get("embedding") else None
130
+ return row
131
+
132
+ def _to_item(self, row: dict) -> dict:
133
+ item = {"PK": norm_scope(row["scope"]), "SK": row["id"]}
134
+ for f in ROW_FIELDS:
135
+ if f in ("id", "scope", "embedding"):
136
+ continue
137
+ if row.get(f) is not None:
138
+ item[f] = _ddb(row[f])
139
+ if row.get("embedding"):
140
+ item["embedding"] = [Decimal(repr(float(v))) for v in row["embedding"]]
141
+ return item
142
+
143
+ def _paginate(self, fn, **kwargs) -> List[dict]:
144
+ items: List[dict] = []
145
+ while True:
146
+ resp = fn(**kwargs)
147
+ items.extend(resp.get("Items", []))
148
+ lek = resp.get("LastEvaluatedKey")
149
+ if not lek:
150
+ return items
151
+ kwargs["ExclusiveStartKey"] = lek
152
+
153
+ # ------------------------------------------------------------------ primitives
154
+ def _put(self, rows: List[dict]) -> None:
155
+ with self.table.batch_writer() as bw:
156
+ for row in rows:
157
+ existing = self._get(row["id"])
158
+ if existing and norm_scope(existing["scope"]) != norm_scope(row["scope"]):
159
+ bw.delete_item(Key={"PK": existing["scope"], "SK": row["id"]}) # scope moved
160
+ bw.put_item(Item=self._to_item(row))
161
+
162
+ def _get(self, record_id: str) -> Optional[dict]:
163
+ resp = self.table.query(IndexName="by_id", KeyConditionExpression=Key("SK").eq(record_id), Limit=1)
164
+ items = resp.get("Items", [])
165
+ return self._to_row(items[0]) if items else None
166
+
167
+ def _delete_ids(self, ids: List[str]) -> int:
168
+ n = 0
169
+ with self.table.batch_writer() as bw:
170
+ for rid in ids:
171
+ row = self._get(rid)
172
+ if row:
173
+ bw.delete_item(Key={"PK": row["scope"], "SK": rid})
174
+ n += 1
175
+ return n
176
+
177
+ def _scan(self, scope_prefix: Optional[str]) -> List[dict]:
178
+ p = norm_scope(scope_prefix)
179
+ if p == "/":
180
+ items = self._paginate(self.table.scan)
181
+ else:
182
+ items = self._paginate(self.table.query, KeyConditionExpression=Key("PK").eq(p))
183
+ items += self._paginate(self.table.scan, FilterExpression=Attr("PK").begins_with(p + "/"))
184
+ return [self._to_row(i) for i in items]
185
+
186
+ def _scopes_under(self, scope_prefix: Optional[str]) -> List[str]:
187
+ p = norm_scope(scope_prefix)
188
+ kwargs: dict = {"ProjectionExpression": "PK"}
189
+ if p != "/":
190
+ kwargs["FilterExpression"] = Attr("PK").eq(p) | Attr("PK").begins_with(p + "/")
191
+ return sorted({i["PK"] for i in self._paginate(self.table.scan, **kwargs)})
192
+
193
+ def _vector_search(
194
+ self, vector: List[float], scope_prefix: Optional[str], limit: int
195
+ ) -> List[Tuple[dict, float]]:
196
+ scopes = self._scopes_under(scope_prefix)
197
+ if not scopes:
198
+ return []
199
+ qv = [{"N": repr(float(v))} for v in vector]
200
+ out: List[Tuple[dict, float]] = []
201
+ for sc in scopes:
202
+ resp = self._client.search_vectors(
203
+ TableName=self.table_name,
204
+ IndexName=self._index_name,
205
+ SearchVector=qv,
206
+ TopK=int(limit),
207
+ SearchConditionExpression="#pk = :s",
208
+ ExpressionAttributeNames={"#pk": "PK"},
209
+ ExpressionAttributeValues={":s": {"S": sc}},
210
+ )
211
+ for r in resp.get("SearchResults", []):
212
+ item = {k: self._deser.deserialize(v) for k, v in r["Item"].items()}
213
+ # DynamoDB returns the cosine *distance* (0 = identical).
214
+ out.append((self._to_row(item), 1.0 - float(r.get("Score", 1.0))))
215
+ out.sort(key=lambda rs: -rs[1])
216
+ return out[:limit]
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.5
2
+ Name: crewai-memory-dynamodb
3
+ Version: 0.1.0
4
+ Summary: Amazon DynamoDB StorageBackend for CrewAI unified Memory — hierarchical scopes, categories, metadata filters and native vector search (SearchVectors)
5
+ Project-URL: Homepage, https://github.com/skamalj/crewai-memory
6
+ Project-URL: Repository, https://github.com/skamalj/crewai-memory.git
7
+ Project-URL: Documentation, https://skamalj.github.io/agentstate-reducer/
8
+ Author-email: Kamal <skamalj@gmail.com>
9
+ Keywords: agent-memory,aws,crewai,dynamodb,long-term-memory,memory,storage-backend,vector-search
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: boto3>=1.43.78
16
+ Requires-Dist: crewai-memory-core>=0.1.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # crewai-memory-dynamodb
20
+
21
+ An **Amazon DynamoDB** `StorageBackend` for [CrewAI](https://docs.crewai.com/en/concepts/memory)'s unified `Memory` — hierarchical scopes, categories, metadata filters, importance/recency, and **native vector search** via DynamoDB `SearchVectors`. No external vector database.
22
+
23
+ ```bash
24
+ pip install crewai-memory-dynamodb
25
+ ```
26
+
27
+ ```python
28
+ from crewai import Crew
29
+ from crewai.memory import Memory
30
+ from crewai_memory_dynamodb import DynamoDBMemoryBackend
31
+
32
+ backend = DynamoDBMemoryBackend(table_name="crewai-memory", dimensions=3072) # match your embedder
33
+ memory = Memory(storage=backend) # CrewAI does LLM analysis, scoping, scoring
34
+ crew = Crew(agents=[...], tasks=[...], memory=memory)
35
+
36
+ memory.remember("The customer prefers email over phone", scope="/customers/acme", categories=["preference"])
37
+ memory.recall("how should we contact acme?", scope="/customers")
38
+ ```
39
+
40
+ Or register it once for every `Crew(memory=True)`:
41
+
42
+ ```python
43
+ from crewai.memory.storage.factory import set_memory_storage_factory
44
+ set_memory_storage_factory(lambda spec: DynamoDBMemoryBackend("crewai-memory", dimensions=3072))
45
+ ```
46
+
47
+ ## How it works
48
+
49
+ - One table: `PK` = scope path, `SK` = record id, plus a `by_id` GSI and a **vector index** on `embedding` (cosine, `dimensions`) whose search schema declares `PK` as an inline filter. Auto-created (`PAY_PER_REQUEST`); the vector index can only be declared at creation, so use a new table to change `dimensions`.
50
+ - `search` runs native `SearchVectors` once per concrete scope under the requested prefix (DynamoDB allows only equality on a string search-schema attribute), merges by similarity (`1 - distance`), then applies `categories` / `metadata_filter` / `min_score` on the candidates (oversampled ×3 when filtering).
51
+ - Scope tree, category counts, `list_records`, `delete(older_than=...)`, `reset`, `touch_records` are query/scan based — sized for agent-memory volumes.
52
+ - `dimensions` must equal the `Memory` embedder's output size (CrewAI default `text-embedding-3-large` = 3072; Bedrock Titan v2 = 1024).
53
+
54
+ Requires `boto3>=1.43.78` and a region where DynamoDB vector search is available. Permissions: `DescribeTable`, `CreateTable`, `GetItem`, `PutItem`, `DeleteItem`, `BatchWriteItem`, `Query`, `Scan`, `SearchVectors`.
55
+
56
+ Docs: <https://skamalj.github.io/agentstate-reducer/> · part of [crewai-memory](https://github.com/skamalj/crewai-memory)
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,4 @@
1
+ crewai_memory_dynamodb/__init__.py,sha256=WDuO3DUgohcvKxt3i52kvFTwtFQ3gvfZ4nyoZ5rKIHI,8619
2
+ crewai_memory_dynamodb-0.1.0.dist-info/METADATA,sha256=RUwmJKeq4W9w_ruIrw-7qnf_0ibVJeHt3oR6NgxmHTw,3385
3
+ crewai_memory_dynamodb-0.1.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
4
+ crewai_memory_dynamodb-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any