crewai-memory-dynamodb 0.1.0__tar.gz
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.
- crewai_memory_dynamodb-0.1.0/.gitignore +9 -0
- crewai_memory_dynamodb-0.1.0/PKG-INFO +60 -0
- crewai_memory_dynamodb-0.1.0/README.md +42 -0
- crewai_memory_dynamodb-0.1.0/pyproject.toml +36 -0
- crewai_memory_dynamodb-0.1.0/src/crewai_memory_dynamodb/__init__.py +216 -0
- crewai_memory_dynamodb-0.1.0/tests/test_dynamodb_backend.py +66 -0
|
@@ -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,42 @@
|
|
|
1
|
+
# crewai-memory-dynamodb
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install crewai-memory-dynamodb
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from crewai import Crew
|
|
11
|
+
from crewai.memory import Memory
|
|
12
|
+
from crewai_memory_dynamodb import DynamoDBMemoryBackend
|
|
13
|
+
|
|
14
|
+
backend = DynamoDBMemoryBackend(table_name="crewai-memory", dimensions=3072) # match your embedder
|
|
15
|
+
memory = Memory(storage=backend) # CrewAI does LLM analysis, scoping, scoring
|
|
16
|
+
crew = Crew(agents=[...], tasks=[...], memory=memory)
|
|
17
|
+
|
|
18
|
+
memory.remember("The customer prefers email over phone", scope="/customers/acme", categories=["preference"])
|
|
19
|
+
memory.recall("how should we contact acme?", scope="/customers")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or register it once for every `Crew(memory=True)`:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from crewai.memory.storage.factory import set_memory_storage_factory
|
|
26
|
+
set_memory_storage_factory(lambda spec: DynamoDBMemoryBackend("crewai-memory", dimensions=3072))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
- 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`.
|
|
32
|
+
- `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).
|
|
33
|
+
- Scope tree, category counts, `list_records`, `delete(older_than=...)`, `reset`, `touch_records` are query/scan based — sized for agent-memory volumes.
|
|
34
|
+
- `dimensions` must equal the `Memory` embedder's output size (CrewAI default `text-embedding-3-large` = 3072; Bedrock Titan v2 = 1024).
|
|
35
|
+
|
|
36
|
+
Requires `boto3>=1.43.78` and a region where DynamoDB vector search is available. Permissions: `DescribeTable`, `CreateTable`, `GetItem`, `PutItem`, `DeleteItem`, `BatchWriteItem`, `Query`, `Scan`, `SearchVectors`.
|
|
37
|
+
|
|
38
|
+
Docs: <https://skamalj.github.io/agentstate-reducer/> · part of [crewai-memory](https://github.com/skamalj/crewai-memory)
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "crewai-memory-dynamodb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Amazon DynamoDB StorageBackend for CrewAI unified Memory — hierarchical scopes, categories, metadata filters and native vector search (SearchVectors)"
|
|
9
|
+
authors = [{name = "Kamal", email = "skamalj@gmail.com"}]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"crewai-memory-core>=0.1.0",
|
|
14
|
+
"boto3>=1.43.78",
|
|
15
|
+
]
|
|
16
|
+
keywords = ["crewai", "memory", "storage-backend", "dynamodb", "aws", "long-term-memory", "agent-memory", "vector-search"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/skamalj/crewai-memory"
|
|
26
|
+
Repository = "https://github.com/skamalj/crewai-memory.git"
|
|
27
|
+
Documentation = "https://skamalj.github.io/agentstate-reducer/"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["pytest>=7.0", "pytest-asyncio"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
asyncio_mode = "auto"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/crewai_memory_dynamodb"]
|
|
@@ -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,66 @@
|
|
|
1
|
+
"""DynamoDB StorageBackend: the shared contract suite against a real table (us-east-1)."""
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from crewai_memory_core.contract import * # noqa: F401,F403
|
|
10
|
+
from crewai_memory_core.contract import DIMS
|
|
11
|
+
from crewai_memory_dynamodb import DynamoDBMemoryBackend
|
|
12
|
+
|
|
13
|
+
REGION = os.environ.get("CREWAI_DDB_REGION", "us-east-1")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _aws_available() -> bool:
|
|
17
|
+
try:
|
|
18
|
+
boto3.client("sts", region_name=REGION).get_caller_identity()
|
|
19
|
+
return True
|
|
20
|
+
except Exception:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
pytestmark = pytest.mark.skipif(not _aws_available(), reason="AWS credentials not available")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="module")
|
|
28
|
+
def _table():
|
|
29
|
+
name = f"crewai_mem_test_{uuid.uuid4().hex[:8]}"
|
|
30
|
+
b = DynamoDBMemoryBackend(name, dimensions=DIMS, region_name=REGION)
|
|
31
|
+
ddb = boto3.client("dynamodb", region_name=REGION)
|
|
32
|
+
for _ in range(60):
|
|
33
|
+
d = ddb.describe_table(TableName=name)["Table"]
|
|
34
|
+
vi = d.get("VectorIndexes") or []
|
|
35
|
+
gsi = d.get("GlobalSecondaryIndexes") or []
|
|
36
|
+
if vi and all(v.get("IndexStatus", "ACTIVE") == "ACTIVE" for v in vi) and all(g.get("IndexStatus") == "ACTIVE" for g in gsi):
|
|
37
|
+
break
|
|
38
|
+
time.sleep(5)
|
|
39
|
+
yield b
|
|
40
|
+
try:
|
|
41
|
+
ddb.delete_table(TableName=name)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture()
|
|
47
|
+
def backend(_table):
|
|
48
|
+
_table.reset()
|
|
49
|
+
yield _table
|
|
50
|
+
_table.reset()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_native_vector_path_used(backend, monkeypatch):
|
|
54
|
+
from crewai_memory_core.contract import _seed, EMB
|
|
55
|
+
_seed(backend)
|
|
56
|
+
calls = []
|
|
57
|
+
orig = backend._client.search_vectors
|
|
58
|
+
|
|
59
|
+
def spy(**kw):
|
|
60
|
+
calls.append(kw["SearchConditionExpression"])
|
|
61
|
+
return orig(**kw)
|
|
62
|
+
|
|
63
|
+
monkeypatch.setattr(backend._client, "search_vectors", spy)
|
|
64
|
+
hits = backend.search(EMB(["sushi"])[0], scope_prefix="/crew/support/user", limit=5)
|
|
65
|
+
assert calls and all(c == "#pk = :s" for c in calls) and len(calls) == 2 # one call per child scope
|
|
66
|
+
assert hits
|