strands-dakera 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.
- strands_dakera-0.1.0/.gitignore +13 -0
- strands_dakera-0.1.0/PKG-INFO +65 -0
- strands_dakera-0.1.0/README.md +33 -0
- strands_dakera-0.1.0/pyproject.toml +101 -0
- strands_dakera-0.1.0/src/strands_dakera/__init__.py +20 -0
- strands_dakera-0.1.0/src/strands_dakera/memory.py +420 -0
- strands_dakera-0.1.0/tests/test_memory.py +294 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strands-dakera
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Persistent, decay-weighted memory for Strands agents, backed by a self-hosted Dakera server.
|
|
5
|
+
Project-URL: Homepage, https://dakera.ai
|
|
6
|
+
Project-URL: Documentation, https://github.com/dakera-ai/strands-dakera#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/dakera-ai/strands-dakera
|
|
8
|
+
Project-URL: Issues, https://github.com/dakera-ai/strands-dakera/issues
|
|
9
|
+
Author-email: Dakera <hello@dakera.ai>
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
Keywords: agents,ai,dakera,decay-weighted,memory,strands,strands-agents,vector-search
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: dakera>=0.12.0
|
|
23
|
+
Requires-Dist: rich>=13.0.0
|
|
24
|
+
Requires-Dist: strands-agents>=1.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy<2.0.0,>=1.15.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio<1.0.0,>=0.25.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest<9.0.0,>=8.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff<1.0.0,>=0.11.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# strands-dakera (Python)
|
|
34
|
+
|
|
35
|
+
Persistent, decay-weighted memory for [Strands Agents](https://github.com/strands-agents/sdk-python),
|
|
36
|
+
backed by a self-hosted [Dakera](https://github.com/dakera-ai/dakera-deploy) server.
|
|
37
|
+
|
|
38
|
+
See the [repository README](../README.md) for full usage. Quick start:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install strands-dakera
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from strands import Agent
|
|
46
|
+
from strands_dakera import dakera_memory
|
|
47
|
+
|
|
48
|
+
agent = Agent(tools=[dakera_memory])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Local development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install hatch
|
|
55
|
+
hatch run test # run the test suite (mocked client, no live server)
|
|
56
|
+
hatch run lint # ruff
|
|
57
|
+
hatch run typecheck # mypy
|
|
58
|
+
hatch run prepare # format + lint + typecheck + test
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Release
|
|
62
|
+
|
|
63
|
+
Tag a GitHub release with a `python-v*` tag (e.g. `python-v0.1.0`); the
|
|
64
|
+
`publish-python` workflow builds the wheel and publishes it to PyPI via trusted
|
|
65
|
+
publishing.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# strands-dakera (Python)
|
|
2
|
+
|
|
3
|
+
Persistent, decay-weighted memory for [Strands Agents](https://github.com/strands-agents/sdk-python),
|
|
4
|
+
backed by a self-hosted [Dakera](https://github.com/dakera-ai/dakera-deploy) server.
|
|
5
|
+
|
|
6
|
+
See the [repository README](../README.md) for full usage. Quick start:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install strands-dakera
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from strands import Agent
|
|
14
|
+
from strands_dakera import dakera_memory
|
|
15
|
+
|
|
16
|
+
agent = Agent(tools=[dakera_memory])
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Local development
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install hatch
|
|
23
|
+
hatch run test # run the test suite (mocked client, no live server)
|
|
24
|
+
hatch run lint # ruff
|
|
25
|
+
hatch run typecheck # mypy
|
|
26
|
+
hatch run prepare # format + lint + typecheck + test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Release
|
|
30
|
+
|
|
31
|
+
Tag a GitHub release with a `python-v*` tag (e.g. `python-v0.1.0`); the
|
|
32
|
+
`publish-python` workflow builds the wheel and publishes it to PyPI via trusted
|
|
33
|
+
publishing.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "strands-dakera"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Persistent, decay-weighted memory for Strands agents, backed by a self-hosted Dakera server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Dakera", email = "hello@dakera.ai"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["strands", "strands-agents", "agents", "ai", "memory", "dakera", "vector-search", "decay-weighted"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dependencies = [
|
|
29
|
+
"strands-agents>=1.0.0",
|
|
30
|
+
"dakera>=0.12.0",
|
|
31
|
+
"rich>=13.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://dakera.ai"
|
|
36
|
+
Documentation = "https://github.com/dakera-ai/strands-dakera#readme"
|
|
37
|
+
Repository = "https://github.com/dakera-ai/strands-dakera"
|
|
38
|
+
Issues = "https://github.com/dakera-ai/strands-dakera/issues"
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=8.0.0,<9.0.0",
|
|
43
|
+
"pytest-asyncio>=0.25.0,<1.0.0",
|
|
44
|
+
"ruff>=0.11.0,<1.0.0",
|
|
45
|
+
"mypy>=1.15.0,<2.0.0",
|
|
46
|
+
"hatch",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version]
|
|
50
|
+
source = "vcs"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.version.raw-options]
|
|
53
|
+
# The git repo lives one level up (monorepo-style layout with a python/ subdir,
|
|
54
|
+
# matching the Strands extension-template). Tell hatch-vcs where the .git is.
|
|
55
|
+
root = ".."
|
|
56
|
+
# Match release tags like `python-v0.1.0` and strip the `python-v` prefix so the
|
|
57
|
+
# published package version is a clean SemVer string.
|
|
58
|
+
tag_regex = "^python-v(?P<version>.+)$"
|
|
59
|
+
# hatch-vcs refuses a non-default `root` unless given a reference file next to it.
|
|
60
|
+
relative_to = "pyproject.toml"
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.wheel]
|
|
63
|
+
packages = ["src/strands_dakera"]
|
|
64
|
+
|
|
65
|
+
[tool.hatch.envs.default]
|
|
66
|
+
dependencies = [
|
|
67
|
+
"pytest>=8.0.0,<9.0.0",
|
|
68
|
+
"pytest-asyncio>=0.25.0,<1.0.0",
|
|
69
|
+
"ruff>=0.11.0,<1.0.0",
|
|
70
|
+
"mypy>=1.15.0,<2.0.0",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.hatch.envs.default.scripts]
|
|
74
|
+
test = "pytest {args}"
|
|
75
|
+
lint = "ruff check src tests"
|
|
76
|
+
format = "ruff format src tests"
|
|
77
|
+
typecheck = "mypy src"
|
|
78
|
+
prepare = ["format", "lint", "typecheck", "test"]
|
|
79
|
+
|
|
80
|
+
[tool.ruff]
|
|
81
|
+
line-length = 120
|
|
82
|
+
include = ["src/**/*.py", "tests/**/*.py"]
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint]
|
|
85
|
+
select = [
|
|
86
|
+
"E", # pycodestyle
|
|
87
|
+
"F", # pyflakes
|
|
88
|
+
"I", # isort
|
|
89
|
+
"B", # flake8-bugbear
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[tool.mypy]
|
|
93
|
+
python_version = "3.10"
|
|
94
|
+
warn_return_any = true
|
|
95
|
+
warn_unused_configs = true
|
|
96
|
+
ignore_missing_imports = true
|
|
97
|
+
|
|
98
|
+
[tool.pytest.ini_options]
|
|
99
|
+
testpaths = ["tests"]
|
|
100
|
+
pythonpath = ["src"]
|
|
101
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Strands Dakera — persistent, decay-weighted memory for Strands agents.
|
|
2
|
+
|
|
3
|
+
Backed by a self-hosted `Dakera <https://github.com/dakera-ai/dakera-deploy>`_
|
|
4
|
+
memory server. Exposes the ``dakera_memory`` tool for store / retrieve / get /
|
|
5
|
+
update / delete operations with importance-weighted, decay-aware recall.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from strands_dakera.memory import (
|
|
9
|
+
TOOL_SPEC,
|
|
10
|
+
DakeraServiceClient,
|
|
11
|
+
dakera_memory,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"dakera_memory",
|
|
16
|
+
"DakeraServiceClient",
|
|
17
|
+
"TOOL_SPEC",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool for managing agent memories using Dakera (store, retrieve, get, update, delete).
|
|
3
|
+
|
|
4
|
+
This module provides persistent, decay-weighted vector memory for Strands agents,
|
|
5
|
+
backed by a self-hosted Dakera memory server. It mirrors the structure of the
|
|
6
|
+
``mem0_memory`` tool so it slots naturally into the Strands tools ecosystem.
|
|
7
|
+
|
|
8
|
+
Dakera is a self-hosted AI agent memory server. Run it locally with the
|
|
9
|
+
``dakera-ai/dakera-deploy`` docker-compose stack (server + MinIO); the REST API
|
|
10
|
+
listens on port 3000 by default. See https://github.com/dakera-ai/dakera-deploy.
|
|
11
|
+
|
|
12
|
+
Key Features:
|
|
13
|
+
------------
|
|
14
|
+
1. Memory Management:
|
|
15
|
+
- store: Add a new memory for an agent, with importance and metadata
|
|
16
|
+
- retrieve: Semantic (decay-weighted) recall across an agent's memories
|
|
17
|
+
- get: Fetch a specific memory by its ID
|
|
18
|
+
- update: Update the content/metadata of an existing memory
|
|
19
|
+
- delete: Remove a memory by its ID
|
|
20
|
+
|
|
21
|
+
2. Safety Features:
|
|
22
|
+
- User confirmation for mutative operations (store, update, delete)
|
|
23
|
+
- Content previews before storage
|
|
24
|
+
- Warning messages before deletion
|
|
25
|
+
- BYPASS_TOOL_CONSENT mode for bypassing confirmations in tests
|
|
26
|
+
|
|
27
|
+
3. Advanced Capabilities:
|
|
28
|
+
- Decay-weighted, access-aware ranking (recall favors important, fresh memories)
|
|
29
|
+
- Importance scoring (0.0-1.0) and typed memories (episodic/semantic/procedural)
|
|
30
|
+
- Structured metadata storage
|
|
31
|
+
- Rich output formatting
|
|
32
|
+
|
|
33
|
+
Configuration (environment variables):
|
|
34
|
+
- DAKERA_BASE_URL: Dakera server URL (default: http://localhost:3000)
|
|
35
|
+
- DAKERA_API_KEY: API key for the Dakera server (dk-...)
|
|
36
|
+
|
|
37
|
+
Usage Examples:
|
|
38
|
+
--------------
|
|
39
|
+
```python
|
|
40
|
+
from strands import Agent
|
|
41
|
+
from strands_dakera import dakera_memory
|
|
42
|
+
|
|
43
|
+
agent = Agent(tools=[dakera_memory])
|
|
44
|
+
|
|
45
|
+
# Store a memory
|
|
46
|
+
agent.tool.dakera_memory(
|
|
47
|
+
action="store",
|
|
48
|
+
agent_id="alex",
|
|
49
|
+
content="Important information to remember",
|
|
50
|
+
importance=0.8,
|
|
51
|
+
metadata={"category": "meeting_notes"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Retrieve memories with decay-weighted semantic search
|
|
55
|
+
agent.tool.dakera_memory(
|
|
56
|
+
action="retrieve",
|
|
57
|
+
agent_id="alex",
|
|
58
|
+
query="meeting information",
|
|
59
|
+
top_k=5,
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
import json
|
|
65
|
+
import logging
|
|
66
|
+
import os
|
|
67
|
+
from typing import Any
|
|
68
|
+
|
|
69
|
+
from rich.console import Console
|
|
70
|
+
from rich.panel import Panel
|
|
71
|
+
from rich.table import Table
|
|
72
|
+
from rich.text import Text
|
|
73
|
+
from strands.types.tools import ToolResult, ToolResultContent, ToolUse
|
|
74
|
+
|
|
75
|
+
logger = logging.getLogger(__name__)
|
|
76
|
+
console = Console()
|
|
77
|
+
|
|
78
|
+
TOOL_SPEC = {
|
|
79
|
+
"name": "dakera_memory",
|
|
80
|
+
"description": (
|
|
81
|
+
"Persistent, decay-weighted memory for agents, backed by a self-hosted Dakera server.\n\n"
|
|
82
|
+
"Actions:\n"
|
|
83
|
+
"- store: Store a new memory (requires agent_id and content)\n"
|
|
84
|
+
"- retrieve: Decay-weighted semantic search (requires agent_id and query)\n"
|
|
85
|
+
"- get: Get a memory by ID (requires agent_id and memory_id)\n"
|
|
86
|
+
"- update: Update a memory's content/metadata (requires agent_id and memory_id)\n"
|
|
87
|
+
"- delete: Delete a memory by ID (requires agent_id and memory_id)\n\n"
|
|
88
|
+
"Configure the server via DAKERA_BASE_URL (default http://localhost:3000) and DAKERA_API_KEY."
|
|
89
|
+
),
|
|
90
|
+
"inputSchema": {
|
|
91
|
+
"json": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"action": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "Action to perform (store, retrieve, get, update, delete)",
|
|
97
|
+
"enum": ["store", "retrieve", "get", "update", "delete"],
|
|
98
|
+
},
|
|
99
|
+
"agent_id": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "Agent identifier that owns the memories (required for all actions)",
|
|
102
|
+
},
|
|
103
|
+
"content": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "Memory content (required for store; optional for update)",
|
|
106
|
+
},
|
|
107
|
+
"memory_id": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Memory ID (required for get, update, delete actions)",
|
|
110
|
+
},
|
|
111
|
+
"query": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "Search query (required for retrieve action)",
|
|
114
|
+
},
|
|
115
|
+
"top_k": {
|
|
116
|
+
"type": "integer",
|
|
117
|
+
"description": "Number of results to return for retrieve (default: 5)",
|
|
118
|
+
},
|
|
119
|
+
"importance": {
|
|
120
|
+
"type": "number",
|
|
121
|
+
"description": "Importance score 0.0-1.0 for store action",
|
|
122
|
+
},
|
|
123
|
+
"memory_type": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"description": "Memory type for store/update (episodic, semantic, procedural, working)",
|
|
126
|
+
"enum": ["episodic", "semantic", "procedural", "working"],
|
|
127
|
+
},
|
|
128
|
+
"metadata": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"description": "Optional metadata to store with the memory",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
"required": ["action", "agent_id"],
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DakeraServiceClient:
|
|
140
|
+
"""Thin wrapper around the Dakera Python SDK for the memory tool."""
|
|
141
|
+
|
|
142
|
+
def __init__(self) -> None:
|
|
143
|
+
"""Initialize the Dakera client from environment configuration.
|
|
144
|
+
|
|
145
|
+
Reads DAKERA_BASE_URL (default http://localhost:3000) and DAKERA_API_KEY.
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
from dakera import DakeraClient
|
|
149
|
+
except ImportError as err:
|
|
150
|
+
raise ImportError(
|
|
151
|
+
"The dakera package is required for the dakera_memory tool. "
|
|
152
|
+
"Install it with: pip install 'strands-dakera'"
|
|
153
|
+
) from err
|
|
154
|
+
|
|
155
|
+
base_url = os.environ.get("DAKERA_BASE_URL", "http://localhost:3000")
|
|
156
|
+
api_key = os.environ.get("DAKERA_API_KEY")
|
|
157
|
+
self.client = DakeraClient(base_url=base_url, api_key=api_key)
|
|
158
|
+
|
|
159
|
+
def store_memory(
|
|
160
|
+
self,
|
|
161
|
+
agent_id: str,
|
|
162
|
+
content: str,
|
|
163
|
+
importance: float | None = None,
|
|
164
|
+
memory_type: str = "episodic",
|
|
165
|
+
metadata: dict[str, Any] | None = None,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Store a memory for an agent."""
|
|
168
|
+
result: dict[str, Any] = self.client.store_memory(
|
|
169
|
+
agent_id=agent_id,
|
|
170
|
+
content=content,
|
|
171
|
+
memory_type=memory_type,
|
|
172
|
+
importance=importance,
|
|
173
|
+
metadata=metadata,
|
|
174
|
+
)
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
def get_memory(self, agent_id: str, memory_id: str) -> dict[str, Any]:
|
|
178
|
+
"""Get a memory by ID."""
|
|
179
|
+
result: dict[str, Any] = self.client.get_memory(agent_id, memory_id)
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
def update_memory(
|
|
183
|
+
self,
|
|
184
|
+
agent_id: str,
|
|
185
|
+
memory_id: str,
|
|
186
|
+
content: str | None = None,
|
|
187
|
+
metadata: dict[str, Any] | None = None,
|
|
188
|
+
memory_type: str | None = None,
|
|
189
|
+
) -> dict[str, Any]:
|
|
190
|
+
"""Update an existing memory."""
|
|
191
|
+
result: dict[str, Any] = self.client.update_memory(
|
|
192
|
+
agent_id=agent_id,
|
|
193
|
+
memory_id=memory_id,
|
|
194
|
+
content=content,
|
|
195
|
+
metadata=metadata,
|
|
196
|
+
memory_type=memory_type,
|
|
197
|
+
)
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
def search_memories(self, agent_id: str, query: str, top_k: int = 5) -> list[dict[str, Any]]:
|
|
201
|
+
"""Decay-weighted semantic recall for an agent."""
|
|
202
|
+
# `recall` returns a RecallResponse (with `.memories`); we defensively also
|
|
203
|
+
# accept a raw iterable, so treat the result as untyped for duck-typing.
|
|
204
|
+
response: Any = self.client.recall(agent_id=agent_id, query=query, top_k=top_k)
|
|
205
|
+
memories = getattr(response, "memories", response)
|
|
206
|
+
return [_memory_to_dict(m) for m in memories]
|
|
207
|
+
|
|
208
|
+
def delete_memory(self, agent_id: str, memory_id: str) -> dict[str, Any]:
|
|
209
|
+
"""Delete a memory by ID."""
|
|
210
|
+
result: dict[str, Any] = self.client.forget(agent_id, memory_id)
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _memory_to_dict(memory: Any) -> dict[str, Any]:
|
|
215
|
+
"""Normalize an SDK memory object (or dict) into a plain dict."""
|
|
216
|
+
if isinstance(memory, dict):
|
|
217
|
+
return memory
|
|
218
|
+
return {
|
|
219
|
+
"id": getattr(memory, "id", None),
|
|
220
|
+
"content": getattr(memory, "content", None),
|
|
221
|
+
"importance": getattr(memory, "importance", None),
|
|
222
|
+
"score": getattr(memory, "score", None),
|
|
223
|
+
"memory_type": getattr(memory, "memory_type", None),
|
|
224
|
+
"metadata": getattr(memory, "metadata", None),
|
|
225
|
+
"created_at": getattr(memory, "created_at", None),
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def format_store_response(memory: dict[str, Any]) -> Panel:
|
|
230
|
+
"""Format a store response."""
|
|
231
|
+
content = [
|
|
232
|
+
"✅ Memory stored successfully:",
|
|
233
|
+
f"🔑 Memory ID: {memory.get('id', 'unknown')}",
|
|
234
|
+
f"📊 Importance: {memory.get('importance', 'default')}",
|
|
235
|
+
]
|
|
236
|
+
text = memory.get("content")
|
|
237
|
+
if text:
|
|
238
|
+
preview = text[:100] + "..." if len(text) > 100 else text
|
|
239
|
+
content.append(f"\n📄 Content: {preview}")
|
|
240
|
+
return Panel("\n".join(content), title="[bold green]Memory Stored", border_style="green")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def format_get_response(memory: dict[str, Any]) -> Panel:
|
|
244
|
+
"""Format a get/update response."""
|
|
245
|
+
result = [
|
|
246
|
+
"✅ Memory retrieved successfully:",
|
|
247
|
+
f"🔑 Memory ID: {memory.get('id', 'unknown')}",
|
|
248
|
+
f"📊 Importance: {memory.get('importance', 'Unknown')}",
|
|
249
|
+
f"🕒 Created: {memory.get('created_at', 'Unknown')}",
|
|
250
|
+
]
|
|
251
|
+
metadata = memory.get("metadata")
|
|
252
|
+
if metadata:
|
|
253
|
+
result.append(f"📋 Metadata: {json.dumps(metadata, indent=2)}")
|
|
254
|
+
result.append(f"\n📄 Memory: {memory.get('content', 'No content available')}")
|
|
255
|
+
return Panel("\n".join(result), title="[bold green]Memory Retrieved", border_style="green")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def format_retrieve_response(memories: list[dict[str, Any]]) -> Panel:
|
|
259
|
+
"""Format a retrieve (semantic search) response."""
|
|
260
|
+
if not memories:
|
|
261
|
+
return Panel(
|
|
262
|
+
"No memories found matching the query.",
|
|
263
|
+
title="[bold yellow]No Matches",
|
|
264
|
+
border_style="yellow",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
table = Table(title="Search Results", show_header=True, header_style="bold magenta")
|
|
268
|
+
table.add_column("ID", style="cyan")
|
|
269
|
+
table.add_column("Memory", style="yellow", width=50)
|
|
270
|
+
table.add_column("Score", style="green")
|
|
271
|
+
table.add_column("Importance", style="blue")
|
|
272
|
+
|
|
273
|
+
for memory in memories:
|
|
274
|
+
content = memory.get("content") or "No content available"
|
|
275
|
+
preview = content[:100] + "..." if len(content) > 100 else content
|
|
276
|
+
table.add_row(
|
|
277
|
+
str(memory.get("id", "unknown")),
|
|
278
|
+
preview,
|
|
279
|
+
str(memory.get("score", "N/A")),
|
|
280
|
+
str(memory.get("importance", "N/A")),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return Panel(table, title="[bold green]Search Results", border_style="green")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def format_delete_response(memory_id: str) -> Panel:
|
|
287
|
+
"""Format a delete response."""
|
|
288
|
+
content = [
|
|
289
|
+
"✅ Memory deleted successfully:",
|
|
290
|
+
f"🔑 Memory ID: {memory_id}",
|
|
291
|
+
]
|
|
292
|
+
return Panel("\n".join(content), title="[bold green]Memory Deleted", border_style="green")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def dakera_memory(tool: ToolUse, **kwargs: Any) -> ToolResult:
|
|
296
|
+
"""Persistent decay-weighted memory management for agents, backed by Dakera.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
tool: ToolUse object containing the input fields (action, agent_id, content,
|
|
300
|
+
memory_id, query, top_k, importance, memory_type, metadata).
|
|
301
|
+
**kwargs: Additional keyword arguments.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
ToolResult containing status and response content.
|
|
305
|
+
"""
|
|
306
|
+
tool_use_id = tool.get("toolUseId", "default-id")
|
|
307
|
+
try:
|
|
308
|
+
tool_input = tool.get("input", {})
|
|
309
|
+
|
|
310
|
+
action = tool_input.get("action")
|
|
311
|
+
if not action:
|
|
312
|
+
raise ValueError("action parameter is required")
|
|
313
|
+
|
|
314
|
+
agent_id = tool_input.get("agent_id")
|
|
315
|
+
if not agent_id:
|
|
316
|
+
raise ValueError("agent_id parameter is required")
|
|
317
|
+
|
|
318
|
+
client = DakeraServiceClient()
|
|
319
|
+
bypass_consent = os.environ.get("BYPASS_TOOL_CONSENT", "").lower() == "true"
|
|
320
|
+
|
|
321
|
+
mutative_actions = {"store", "update", "delete"}
|
|
322
|
+
needs_confirmation = action in mutative_actions and not bypass_consent
|
|
323
|
+
|
|
324
|
+
if needs_confirmation:
|
|
325
|
+
if action == "store":
|
|
326
|
+
if not tool_input.get("content"):
|
|
327
|
+
raise ValueError("content is required for store action")
|
|
328
|
+
preview = tool_input["content"][:15000]
|
|
329
|
+
console.print(Panel(preview, title=f"[bold green]Memory for agent {agent_id}", border_style="green"))
|
|
330
|
+
elif action in {"update", "delete"}:
|
|
331
|
+
if not tool_input.get("memory_id"):
|
|
332
|
+
raise ValueError(f"memory_id is required for {action} action")
|
|
333
|
+
console.print(
|
|
334
|
+
Panel(
|
|
335
|
+
f"Memory ID: {tool_input['memory_id']}",
|
|
336
|
+
title=f"[bold red]⚠️ Memory to be {action}d",
|
|
337
|
+
border_style="red",
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if action == "store":
|
|
342
|
+
if not tool_input.get("content"):
|
|
343
|
+
raise ValueError("content is required for store action")
|
|
344
|
+
memory = client.store_memory(
|
|
345
|
+
agent_id=agent_id,
|
|
346
|
+
content=tool_input["content"],
|
|
347
|
+
importance=tool_input.get("importance"),
|
|
348
|
+
memory_type=tool_input.get("memory_type", "episodic"),
|
|
349
|
+
metadata=tool_input.get("metadata"),
|
|
350
|
+
)
|
|
351
|
+
console.print(format_store_response(memory))
|
|
352
|
+
return ToolResult(
|
|
353
|
+
toolUseId=tool_use_id,
|
|
354
|
+
status="success",
|
|
355
|
+
content=[ToolResultContent(text=json.dumps(memory, indent=2, default=str))],
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if action == "retrieve":
|
|
359
|
+
if not tool_input.get("query"):
|
|
360
|
+
raise ValueError("query is required for retrieve action")
|
|
361
|
+
memories = client.search_memories(
|
|
362
|
+
agent_id=agent_id,
|
|
363
|
+
query=tool_input["query"],
|
|
364
|
+
top_k=tool_input.get("top_k", 5),
|
|
365
|
+
)
|
|
366
|
+
console.print(format_retrieve_response(memories))
|
|
367
|
+
return ToolResult(
|
|
368
|
+
toolUseId=tool_use_id,
|
|
369
|
+
status="success",
|
|
370
|
+
content=[ToolResultContent(text=json.dumps(memories, indent=2, default=str))],
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if action == "get":
|
|
374
|
+
if not tool_input.get("memory_id"):
|
|
375
|
+
raise ValueError("memory_id is required for get action")
|
|
376
|
+
memory = client.get_memory(agent_id, tool_input["memory_id"])
|
|
377
|
+
console.print(format_get_response(memory))
|
|
378
|
+
return ToolResult(
|
|
379
|
+
toolUseId=tool_use_id,
|
|
380
|
+
status="success",
|
|
381
|
+
content=[ToolResultContent(text=json.dumps(memory, indent=2, default=str))],
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if action == "update":
|
|
385
|
+
if not tool_input.get("memory_id"):
|
|
386
|
+
raise ValueError("memory_id is required for update action")
|
|
387
|
+
memory = client.update_memory(
|
|
388
|
+
agent_id=agent_id,
|
|
389
|
+
memory_id=tool_input["memory_id"],
|
|
390
|
+
content=tool_input.get("content"),
|
|
391
|
+
metadata=tool_input.get("metadata"),
|
|
392
|
+
memory_type=tool_input.get("memory_type"),
|
|
393
|
+
)
|
|
394
|
+
console.print(format_get_response(memory))
|
|
395
|
+
return ToolResult(
|
|
396
|
+
toolUseId=tool_use_id,
|
|
397
|
+
status="success",
|
|
398
|
+
content=[ToolResultContent(text=json.dumps(memory, indent=2, default=str))],
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if action == "delete":
|
|
402
|
+
if not tool_input.get("memory_id"):
|
|
403
|
+
raise ValueError("memory_id is required for delete action")
|
|
404
|
+
client.delete_memory(agent_id, tool_input["memory_id"])
|
|
405
|
+
console.print(format_delete_response(tool_input["memory_id"]))
|
|
406
|
+
return ToolResult(
|
|
407
|
+
toolUseId=tool_use_id,
|
|
408
|
+
status="success",
|
|
409
|
+
content=[ToolResultContent(text=f"Memory {tool_input['memory_id']} deleted successfully")],
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
raise ValueError(f"Invalid action: {action}")
|
|
413
|
+
|
|
414
|
+
except Exception as e:
|
|
415
|
+
console.print(Panel(Text(str(e), style="red"), title="❌ Memory Operation Error", border_style="red"))
|
|
416
|
+
return ToolResult(
|
|
417
|
+
toolUseId=tool_use_id,
|
|
418
|
+
status="error",
|
|
419
|
+
content=[ToolResultContent(text=f"Error: {str(e)}")],
|
|
420
|
+
)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the dakera_memory tool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from strands.types.tools import ToolUse
|
|
11
|
+
|
|
12
|
+
from strands_dakera.memory import DakeraServiceClient, dakera_memory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def mock_tool():
|
|
17
|
+
"""Create a mock ToolUse object."""
|
|
18
|
+
mock = MagicMock(spec=ToolUse)
|
|
19
|
+
mock.get.side_effect = lambda key, default=None: {"toolUseId": "test-id", "input": {}}.get(key, default)
|
|
20
|
+
return mock
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_tool(input_data: dict) -> MagicMock:
|
|
24
|
+
"""Helper: build a mock ToolUse with given input dict."""
|
|
25
|
+
mock = MagicMock(spec=ToolUse)
|
|
26
|
+
mock.get.side_effect = lambda key, default=None: {
|
|
27
|
+
"toolUseId": "test-id",
|
|
28
|
+
"input": input_data,
|
|
29
|
+
}.get(key, default)
|
|
30
|
+
return mock
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# DakeraServiceClient init
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_service_client_raises_on_missing_package():
|
|
39
|
+
"""ImportError when dakera package is not installed."""
|
|
40
|
+
with patch("builtins.__import__", side_effect=ImportError("No module named 'dakera'")):
|
|
41
|
+
with pytest.raises(ImportError, match="dakera package is required"):
|
|
42
|
+
DakeraServiceClient()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@patch.dict(os.environ, {"DAKERA_BASE_URL": "http://localhost:3000", "DAKERA_API_KEY": "dk-test"})
|
|
46
|
+
def test_service_client_init():
|
|
47
|
+
"""Client initialises from env vars."""
|
|
48
|
+
mock_dakera_client = MagicMock()
|
|
49
|
+
with patch("strands_dakera.memory.DakeraServiceClient.__init__", return_value=None) as patched:
|
|
50
|
+
patched.return_value = None
|
|
51
|
+
client = DakeraServiceClient.__new__(DakeraServiceClient)
|
|
52
|
+
client.client = mock_dakera_client
|
|
53
|
+
assert client.client is mock_dakera_client
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# store action
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@patch.dict(os.environ, {"BYPASS_TOOL_CONSENT": "true"})
|
|
62
|
+
def test_store_memory():
|
|
63
|
+
"""store action returns success with memory dict."""
|
|
64
|
+
stored = {
|
|
65
|
+
"id": "mem-abc",
|
|
66
|
+
"content": "Test memory content",
|
|
67
|
+
"importance": 0.8,
|
|
68
|
+
"created_at": "2026-07-02T00:00:00Z",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
with patch("strands_dakera.memory.DakeraServiceClient") as MockClient:
|
|
72
|
+
instance = MockClient.return_value
|
|
73
|
+
instance.store_memory.return_value = stored
|
|
74
|
+
|
|
75
|
+
tool = make_tool(
|
|
76
|
+
{
|
|
77
|
+
"action": "store",
|
|
78
|
+
"agent_id": "alex",
|
|
79
|
+
"content": "Test memory content",
|
|
80
|
+
"importance": 0.8,
|
|
81
|
+
"metadata": {"category": "notes"},
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
result = dakera_memory(tool=tool)
|
|
85
|
+
|
|
86
|
+
assert result["status"] == "success"
|
|
87
|
+
body = json.loads(result["content"][0]["text"])
|
|
88
|
+
assert body["id"] == "mem-abc"
|
|
89
|
+
instance.store_memory.assert_called_once_with(
|
|
90
|
+
agent_id="alex",
|
|
91
|
+
content="Test memory content",
|
|
92
|
+
importance=0.8,
|
|
93
|
+
memory_type="episodic",
|
|
94
|
+
metadata={"category": "notes"},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_store_missing_content():
|
|
99
|
+
"""store without content returns error."""
|
|
100
|
+
tool = make_tool({"action": "store", "agent_id": "alex"})
|
|
101
|
+
with patch("strands_dakera.memory.DakeraServiceClient"):
|
|
102
|
+
result = dakera_memory(tool=tool)
|
|
103
|
+
assert result["status"] == "error"
|
|
104
|
+
assert "content is required for store action" in result["content"][0]["text"]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# retrieve action
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@patch.dict(os.environ, {"BYPASS_TOOL_CONSENT": "true"})
|
|
113
|
+
def test_retrieve_memories():
|
|
114
|
+
"""retrieve returns a list of matching memories."""
|
|
115
|
+
memories = [
|
|
116
|
+
{"id": "mem-1", "content": "hello world", "score": 0.95, "importance": 0.7},
|
|
117
|
+
{"id": "mem-2", "content": "another fact", "score": 0.80, "importance": 0.5},
|
|
118
|
+
]
|
|
119
|
+
with patch("strands_dakera.memory.DakeraServiceClient") as MockClient:
|
|
120
|
+
instance = MockClient.return_value
|
|
121
|
+
instance.search_memories.return_value = memories
|
|
122
|
+
|
|
123
|
+
tool = make_tool({"action": "retrieve", "agent_id": "alex", "query": "hello", "top_k": 2})
|
|
124
|
+
result = dakera_memory(tool=tool)
|
|
125
|
+
|
|
126
|
+
assert result["status"] == "success"
|
|
127
|
+
body = json.loads(result["content"][0]["text"])
|
|
128
|
+
assert len(body) == 2
|
|
129
|
+
assert body[0]["id"] == "mem-1"
|
|
130
|
+
instance.search_memories.assert_called_once_with(agent_id="alex", query="hello", top_k=2)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_retrieve_missing_query():
|
|
134
|
+
"""retrieve without query returns error."""
|
|
135
|
+
tool = make_tool({"action": "retrieve", "agent_id": "alex"})
|
|
136
|
+
with patch("strands_dakera.memory.DakeraServiceClient"):
|
|
137
|
+
result = dakera_memory(tool=tool)
|
|
138
|
+
assert result["status"] == "error"
|
|
139
|
+
assert "query is required" in result["content"][0]["text"]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_retrieve_empty_results():
|
|
143
|
+
"""retrieve with no matches returns success with empty list."""
|
|
144
|
+
with patch("strands_dakera.memory.DakeraServiceClient") as MockClient:
|
|
145
|
+
instance = MockClient.return_value
|
|
146
|
+
instance.search_memories.return_value = []
|
|
147
|
+
|
|
148
|
+
tool = make_tool({"action": "retrieve", "agent_id": "alex", "query": "xyz"})
|
|
149
|
+
result = dakera_memory(tool=tool)
|
|
150
|
+
|
|
151
|
+
assert result["status"] == "success"
|
|
152
|
+
body = json.loads(result["content"][0]["text"])
|
|
153
|
+
assert body == []
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# get action
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_get_memory():
|
|
162
|
+
"""get returns the memory dict."""
|
|
163
|
+
memory = {
|
|
164
|
+
"id": "mem-abc",
|
|
165
|
+
"content": "stored fact",
|
|
166
|
+
"importance": 0.9,
|
|
167
|
+
"created_at": "2026-07-02T00:00:00Z",
|
|
168
|
+
"metadata": {},
|
|
169
|
+
}
|
|
170
|
+
with patch("strands_dakera.memory.DakeraServiceClient") as MockClient:
|
|
171
|
+
instance = MockClient.return_value
|
|
172
|
+
instance.get_memory.return_value = memory
|
|
173
|
+
|
|
174
|
+
tool = make_tool({"action": "get", "agent_id": "alex", "memory_id": "mem-abc"})
|
|
175
|
+
result = dakera_memory(tool=tool)
|
|
176
|
+
|
|
177
|
+
assert result["status"] == "success"
|
|
178
|
+
body = json.loads(result["content"][0]["text"])
|
|
179
|
+
assert body["id"] == "mem-abc"
|
|
180
|
+
instance.get_memory.assert_called_once_with("alex", "mem-abc")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_get_missing_memory_id():
|
|
184
|
+
"""get without memory_id returns error."""
|
|
185
|
+
tool = make_tool({"action": "get", "agent_id": "alex"})
|
|
186
|
+
with patch("strands_dakera.memory.DakeraServiceClient"):
|
|
187
|
+
result = dakera_memory(tool=tool)
|
|
188
|
+
assert result["status"] == "error"
|
|
189
|
+
assert "memory_id is required for get action" in result["content"][0]["text"]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# update action
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@patch.dict(os.environ, {"BYPASS_TOOL_CONSENT": "true"})
|
|
198
|
+
def test_update_memory():
|
|
199
|
+
"""update returns updated memory dict."""
|
|
200
|
+
updated = {
|
|
201
|
+
"id": "mem-abc",
|
|
202
|
+
"content": "updated content",
|
|
203
|
+
"importance": 0.9,
|
|
204
|
+
"created_at": "2026-07-02T00:00:00Z",
|
|
205
|
+
"metadata": {"tag": "v2"},
|
|
206
|
+
}
|
|
207
|
+
with patch("strands_dakera.memory.DakeraServiceClient") as MockClient:
|
|
208
|
+
instance = MockClient.return_value
|
|
209
|
+
instance.update_memory.return_value = updated
|
|
210
|
+
|
|
211
|
+
tool = make_tool(
|
|
212
|
+
{
|
|
213
|
+
"action": "update",
|
|
214
|
+
"agent_id": "alex",
|
|
215
|
+
"memory_id": "mem-abc",
|
|
216
|
+
"content": "updated content",
|
|
217
|
+
"metadata": {"tag": "v2"},
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
result = dakera_memory(tool=tool)
|
|
221
|
+
|
|
222
|
+
assert result["status"] == "success"
|
|
223
|
+
body = json.loads(result["content"][0]["text"])
|
|
224
|
+
assert body["content"] == "updated content"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_update_missing_memory_id():
|
|
228
|
+
"""update without memory_id returns error."""
|
|
229
|
+
tool = make_tool({"action": "update", "agent_id": "alex", "content": "new"})
|
|
230
|
+
with patch("strands_dakera.memory.DakeraServiceClient"):
|
|
231
|
+
result = dakera_memory(tool=tool)
|
|
232
|
+
assert result["status"] == "error"
|
|
233
|
+
assert "memory_id is required for update action" in result["content"][0]["text"]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# delete action
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@patch.dict(os.environ, {"BYPASS_TOOL_CONSENT": "true"})
|
|
242
|
+
def test_delete_memory():
|
|
243
|
+
"""delete returns success text with memory_id."""
|
|
244
|
+
with patch("strands_dakera.memory.DakeraServiceClient") as MockClient:
|
|
245
|
+
instance = MockClient.return_value
|
|
246
|
+
instance.delete_memory.return_value = {"status": "ok"}
|
|
247
|
+
|
|
248
|
+
tool = make_tool({"action": "delete", "agent_id": "alex", "memory_id": "mem-abc"})
|
|
249
|
+
result = dakera_memory(tool=tool)
|
|
250
|
+
|
|
251
|
+
assert result["status"] == "success"
|
|
252
|
+
assert "mem-abc" in result["content"][0]["text"]
|
|
253
|
+
instance.delete_memory.assert_called_once_with("alex", "mem-abc")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_delete_missing_memory_id():
|
|
257
|
+
"""delete without memory_id returns error."""
|
|
258
|
+
tool = make_tool({"action": "delete", "agent_id": "alex"})
|
|
259
|
+
with patch("strands_dakera.memory.DakeraServiceClient"):
|
|
260
|
+
result = dakera_memory(tool=tool)
|
|
261
|
+
assert result["status"] == "error"
|
|
262
|
+
assert "memory_id is required for delete action" in result["content"][0]["text"]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Guard: missing required params
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_missing_action():
|
|
271
|
+
"""No action returns error."""
|
|
272
|
+
tool = make_tool({"agent_id": "alex"})
|
|
273
|
+
with patch("strands_dakera.memory.DakeraServiceClient"):
|
|
274
|
+
result = dakera_memory(tool=tool)
|
|
275
|
+
assert result["status"] == "error"
|
|
276
|
+
assert "action parameter is required" in result["content"][0]["text"]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_missing_agent_id():
|
|
280
|
+
"""No agent_id returns error."""
|
|
281
|
+
tool = make_tool({"action": "retrieve", "query": "test"})
|
|
282
|
+
with patch("strands_dakera.memory.DakeraServiceClient"):
|
|
283
|
+
result = dakera_memory(tool=tool)
|
|
284
|
+
assert result["status"] == "error"
|
|
285
|
+
assert "agent_id parameter is required" in result["content"][0]["text"]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_invalid_action():
|
|
289
|
+
"""Unknown action returns error."""
|
|
290
|
+
tool = make_tool({"action": "explode", "agent_id": "alex"})
|
|
291
|
+
with patch("strands_dakera.memory.DakeraServiceClient"):
|
|
292
|
+
result = dakera_memory(tool=tool)
|
|
293
|
+
assert result["status"] == "error"
|
|
294
|
+
assert "Invalid action" in result["content"][0]["text"]
|