strands-dakera 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,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,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,5 @@
1
+ strands_dakera/__init__.py,sha256=OLL17uvk-fXHbAQacHRzDFiK-1P4yAQdC83daUye_NM,513
2
+ strands_dakera/memory.py,sha256=zv_YrgAXxQfxVZbBa1Q43yCgNEEE6L0J9JbHQaq0vqA,16334
3
+ strands_dakera-0.1.0.dist-info/METADATA,sha256=SKUGZVYdiYFgwgaz715LtB5bW8xjnQ_1A2ORDu9kSQk,2337
4
+ strands_dakera-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ strands_dakera-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any