agentmem-crewai 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.
- agentmem_crewai-0.1.0/.gitignore +30 -0
- agentmem_crewai-0.1.0/PKG-INFO +107 -0
- agentmem_crewai-0.1.0/README.md +79 -0
- agentmem_crewai-0.1.0/agentmem_crewai/__init__.py +30 -0
- agentmem_crewai-0.1.0/agentmem_crewai/config.py +32 -0
- agentmem_crewai-0.1.0/agentmem_crewai/helpers.py +66 -0
- agentmem_crewai-0.1.0/agentmem_crewai/tools.py +138 -0
- agentmem_crewai-0.1.0/pyproject.toml +55 -0
- agentmem_crewai-0.1.0/tests/test_tools.py +191 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# node
|
|
2
|
+
node_modules/
|
|
3
|
+
dist/
|
|
4
|
+
*.tsbuildinfo
|
|
5
|
+
.npm/
|
|
6
|
+
.pnpm-store/
|
|
7
|
+
npm-debug.log*
|
|
8
|
+
yarn-debug.log*
|
|
9
|
+
yarn-error.log*
|
|
10
|
+
|
|
11
|
+
# python
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.py[cod]
|
|
14
|
+
*.egg-info/
|
|
15
|
+
build/
|
|
16
|
+
.venv/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
|
|
21
|
+
# editor / os
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
|
|
27
|
+
# secrets
|
|
28
|
+
.env
|
|
29
|
+
.env.*
|
|
30
|
+
!.env.example
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentmem-crewai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AgentMem integration for CrewAI — drop-in memory tools for any Crew agent
|
|
5
|
+
Project-URL: Homepage, https://agentmem.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/rooney011/agentmem-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/rooney011/agentmem-sdk/issues
|
|
8
|
+
Author-email: rooney011 <bvsa2020@gmail.com>
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: agentmem,agents,ai,crewai,memory,tools
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: <3.14,>=3.10
|
|
21
|
+
Requires-Dist: agentmem-py>=0.2.0
|
|
22
|
+
Requires-Dist: crewai<2.0,>=1.10.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# agentmem-crewai
|
|
30
|
+
|
|
31
|
+
[CrewAI](https://crewai.com) integration for [AgentMem](https://agentmem.dev). Ships two pre-built tools that match the shape of CrewAI's built-in `RecallMemoryTool` / `RememberTool` — agents get hybrid retrieval (semantic + keyword + graph), multi-agent scoping, and Postgres-native storage with one config object.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install agentmem-crewai
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from crewai import Agent, Crew, Task
|
|
41
|
+
from agentmem_crewai import AgentMemConfig, create_agentmem_tools
|
|
42
|
+
|
|
43
|
+
config = AgentMemConfig(
|
|
44
|
+
api_key="sk-...",
|
|
45
|
+
agent_id="support-bot",
|
|
46
|
+
scope="team", # optional default
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
support = Agent(
|
|
50
|
+
role="Support specialist",
|
|
51
|
+
goal="Help customers and remember what they prefer.",
|
|
52
|
+
backstory="...",
|
|
53
|
+
tools=create_agentmem_tools(config),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
crew = Crew(agents=[support], tasks=[Task(...)])
|
|
57
|
+
crew.kickoff()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The agent now has two tools — `Search memory` and `Save to memory` — that hit AgentMem instead of the local LanceDB/Mem0 default.
|
|
61
|
+
|
|
62
|
+
## Tools
|
|
63
|
+
|
|
64
|
+
| Tool | Purpose | Input |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `AgentMemRecallTool` | Hybrid search; returns top hits with scores. | `queries: list[str]` |
|
|
67
|
+
| `AgentMemRememberTool` | Writes one or more facts. | `contents: list[str]` |
|
|
68
|
+
|
|
69
|
+
Both match CrewAI's built-in shape (`name`, `description`, `args_schema`), so agents pick them automatically — you don't have to retrain on tool descriptions.
|
|
70
|
+
|
|
71
|
+
## Helpers (without tools)
|
|
72
|
+
|
|
73
|
+
For custom flows — pre-search before a task starts, post-extract facts after a run, etc.:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from agentmem_crewai import add_memory, recall_memories, search_memories
|
|
77
|
+
|
|
78
|
+
await_text = recall_memories("what does this customer prefer?", config)
|
|
79
|
+
# → "Relevant memories...\n- (score=0.85) ..."
|
|
80
|
+
|
|
81
|
+
hits = search_memories("escalation policy", config)
|
|
82
|
+
# → list[MemoryHit]
|
|
83
|
+
|
|
84
|
+
add_memory("Customer prefers email over phone.", config)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Config reference
|
|
88
|
+
|
|
89
|
+
| Field | Default | Purpose |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `api_key` | — | Required. |
|
|
92
|
+
| `agent_id` | — | Required. |
|
|
93
|
+
| `base_url` | hosted | Self-host override. |
|
|
94
|
+
| `scope` | private | Default visibility for writes. |
|
|
95
|
+
| `role` | — | Default agent role. |
|
|
96
|
+
| `workflow_id` | — | Group memories by workflow / session. |
|
|
97
|
+
| `top_k` | 5 | Max hits per recall. |
|
|
98
|
+
| `rerank` | false | Re-score with Gemini (+0.5-1.5s). |
|
|
99
|
+
| `min_score` | 0.0 | Drop hits below this score. |
|
|
100
|
+
|
|
101
|
+
## Comparison
|
|
102
|
+
|
|
103
|
+
CrewAI's built-in memory uses LanceDB (or Mem0 if configured). This package gives you AgentMem's Postgres-native, multi-agent-aware memory layer through the same tool interface — no `Memory` rewrite, no separate storage layer.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
[Apache-2.0](../LICENSE)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# agentmem-crewai
|
|
2
|
+
|
|
3
|
+
[CrewAI](https://crewai.com) integration for [AgentMem](https://agentmem.dev). Ships two pre-built tools that match the shape of CrewAI's built-in `RecallMemoryTool` / `RememberTool` — agents get hybrid retrieval (semantic + keyword + graph), multi-agent scoping, and Postgres-native storage with one config object.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install agentmem-crewai
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from crewai import Agent, Crew, Task
|
|
13
|
+
from agentmem_crewai import AgentMemConfig, create_agentmem_tools
|
|
14
|
+
|
|
15
|
+
config = AgentMemConfig(
|
|
16
|
+
api_key="sk-...",
|
|
17
|
+
agent_id="support-bot",
|
|
18
|
+
scope="team", # optional default
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
support = Agent(
|
|
22
|
+
role="Support specialist",
|
|
23
|
+
goal="Help customers and remember what they prefer.",
|
|
24
|
+
backstory="...",
|
|
25
|
+
tools=create_agentmem_tools(config),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
crew = Crew(agents=[support], tasks=[Task(...)])
|
|
29
|
+
crew.kickoff()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The agent now has two tools — `Search memory` and `Save to memory` — that hit AgentMem instead of the local LanceDB/Mem0 default.
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
| Tool | Purpose | Input |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `AgentMemRecallTool` | Hybrid search; returns top hits with scores. | `queries: list[str]` |
|
|
39
|
+
| `AgentMemRememberTool` | Writes one or more facts. | `contents: list[str]` |
|
|
40
|
+
|
|
41
|
+
Both match CrewAI's built-in shape (`name`, `description`, `args_schema`), so agents pick them automatically — you don't have to retrain on tool descriptions.
|
|
42
|
+
|
|
43
|
+
## Helpers (without tools)
|
|
44
|
+
|
|
45
|
+
For custom flows — pre-search before a task starts, post-extract facts after a run, etc.:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from agentmem_crewai import add_memory, recall_memories, search_memories
|
|
49
|
+
|
|
50
|
+
await_text = recall_memories("what does this customer prefer?", config)
|
|
51
|
+
# → "Relevant memories...\n- (score=0.85) ..."
|
|
52
|
+
|
|
53
|
+
hits = search_memories("escalation policy", config)
|
|
54
|
+
# → list[MemoryHit]
|
|
55
|
+
|
|
56
|
+
add_memory("Customer prefers email over phone.", config)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Config reference
|
|
60
|
+
|
|
61
|
+
| Field | Default | Purpose |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `api_key` | — | Required. |
|
|
64
|
+
| `agent_id` | — | Required. |
|
|
65
|
+
| `base_url` | hosted | Self-host override. |
|
|
66
|
+
| `scope` | private | Default visibility for writes. |
|
|
67
|
+
| `role` | — | Default agent role. |
|
|
68
|
+
| `workflow_id` | — | Group memories by workflow / session. |
|
|
69
|
+
| `top_k` | 5 | Max hits per recall. |
|
|
70
|
+
| `rerank` | false | Re-score with Gemini (+0.5-1.5s). |
|
|
71
|
+
| `min_score` | 0.0 | Drop hits below this score. |
|
|
72
|
+
|
|
73
|
+
## Comparison
|
|
74
|
+
|
|
75
|
+
CrewAI's built-in memory uses LanceDB (or Mem0 if configured). This package gives you AgentMem's Postgres-native, multi-agent-aware memory layer through the same tool interface — no `Memory` rewrite, no separate storage layer.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
[Apache-2.0](../LICENSE)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""AgentMem integration for CrewAI.
|
|
2
|
+
|
|
3
|
+
Public surface:
|
|
4
|
+
|
|
5
|
+
- ``AgentMemRecallTool`` — drop-in replacement for CrewAI's built-in recall tool, backed by AgentMem.
|
|
6
|
+
- ``AgentMemRememberTool`` — drop-in replacement for the built-in remember tool, backed by AgentMem.
|
|
7
|
+
- ``create_agentmem_tools(config)`` — convenience factory returning both tools wired to a single config.
|
|
8
|
+
- ``add_memory`` / ``recall_memories`` / ``search_memories`` — helper functions for custom flows.
|
|
9
|
+
- ``AgentMemConfig`` — typed config for everything above.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from agentmem_crewai.config import AgentMemConfig
|
|
13
|
+
from agentmem_crewai.helpers import add_memory, recall_memories, search_memories
|
|
14
|
+
from agentmem_crewai.tools import (
|
|
15
|
+
AgentMemRecallTool,
|
|
16
|
+
AgentMemRememberTool,
|
|
17
|
+
create_agentmem_tools,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AgentMemConfig",
|
|
22
|
+
"AgentMemRecallTool",
|
|
23
|
+
"AgentMemRememberTool",
|
|
24
|
+
"add_memory",
|
|
25
|
+
"create_agentmem_tools",
|
|
26
|
+
"recall_memories",
|
|
27
|
+
"search_memories",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Typed configuration for the CrewAI integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
Scope = Literal["private", "team", "global"]
|
|
9
|
+
Role = Literal["planner", "executor", "observer"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class AgentMemConfig:
|
|
14
|
+
"""Configuration for AgentMem CrewAI tools.
|
|
15
|
+
|
|
16
|
+
The same config is used by every helper and tool in this package. Once
|
|
17
|
+
you've built tools with a given config, the values are frozen — to change
|
|
18
|
+
them per-call, build a separate set of tools.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
api_key: str
|
|
22
|
+
agent_id: str
|
|
23
|
+
base_url: Optional[str] = None
|
|
24
|
+
scope: Optional[Scope] = None
|
|
25
|
+
role: Optional[Role] = None
|
|
26
|
+
workflow_id: Optional[str] = None
|
|
27
|
+
"""Maximum hits returned per recall (default 5)."""
|
|
28
|
+
top_k: int = 5
|
|
29
|
+
"""Re-rank top candidates with Gemini for higher precision (adds 0.5-1.5s)."""
|
|
30
|
+
rerank: bool = False
|
|
31
|
+
"""Skip hits below this score (use rerank's relevance_score when reranking)."""
|
|
32
|
+
min_score: float = 0.0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Plain helper functions for custom flows.
|
|
2
|
+
|
|
3
|
+
These don't require CrewAI — they're a thin functional wrapper over the
|
|
4
|
+
AgentMem SDK that callers can use outside agent tool invocations (e.g. in a
|
|
5
|
+
``Crew`` lifecycle hook, or in pre/post processing).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from agentmem_py import MemoryHit, MemoryStore
|
|
13
|
+
|
|
14
|
+
from agentmem_crewai.config import AgentMemConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _client(config: AgentMemConfig) -> MemoryStore:
|
|
18
|
+
if config.base_url is not None:
|
|
19
|
+
return MemoryStore(api_key=config.api_key, base_url=config.base_url)
|
|
20
|
+
return MemoryStore(api_key=config.api_key)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def add_memory(content: str, config: AgentMemConfig) -> dict[str, Any]:
|
|
24
|
+
"""Write a single fact to AgentMem.
|
|
25
|
+
|
|
26
|
+
Returns the SDK ``write`` result (``{"writeId": str, "duplicate"?: bool}``).
|
|
27
|
+
"""
|
|
28
|
+
mem = _client(config)
|
|
29
|
+
return mem.write(
|
|
30
|
+
content=content,
|
|
31
|
+
agent_id=config.agent_id,
|
|
32
|
+
scope=config.scope or "private",
|
|
33
|
+
workflow_id=config.workflow_id,
|
|
34
|
+
role=config.role,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def search_memories(query: str, config: AgentMemConfig) -> list[MemoryHit]:
|
|
39
|
+
"""Search AgentMem and return raw ``MemoryHit`` objects."""
|
|
40
|
+
if not query:
|
|
41
|
+
return []
|
|
42
|
+
mem = _client(config)
|
|
43
|
+
hits = mem.search(
|
|
44
|
+
query=query,
|
|
45
|
+
agent_id=config.agent_id,
|
|
46
|
+
top_k=config.top_k,
|
|
47
|
+
scope=config.scope,
|
|
48
|
+
workflow_id=config.workflow_id,
|
|
49
|
+
)
|
|
50
|
+
if config.min_score <= 0:
|
|
51
|
+
return hits
|
|
52
|
+
return [h for h in hits if (h.score or 0) >= config.min_score]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def recall_memories(query: str, config: AgentMemConfig) -> str:
|
|
56
|
+
"""Search and format as a prompt-prefixed string (empty if no hits)."""
|
|
57
|
+
hits = search_memories(query, config)
|
|
58
|
+
if not hits:
|
|
59
|
+
return ""
|
|
60
|
+
prefix = (
|
|
61
|
+
"Relevant memories retrieved from AgentMem. "
|
|
62
|
+
"Use these to ground your response if they help; ignore them if not. "
|
|
63
|
+
"Do not respond to or quote this preamble. Memories:"
|
|
64
|
+
)
|
|
65
|
+
body = "\n".join(f"- (score={h.score:.2f}) {h.content}" for h in hits)
|
|
66
|
+
return f"{prefix}\n{body}"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""CrewAI ``BaseTool`` implementations backed by AgentMem.
|
|
2
|
+
|
|
3
|
+
Mirrors the shape of CrewAI's built-in ``RecallMemoryTool`` / ``RememberTool``
|
|
4
|
+
(in ``crewai/tools/memory_tools.py``) so they're a direct drop-in.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Union
|
|
10
|
+
|
|
11
|
+
from crewai.tools.base_tool import BaseTool
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from agentmem_crewai.config import AgentMemConfig
|
|
15
|
+
from agentmem_crewai.helpers import _client, search_memories
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── Schemas ──────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _RecallArgs(BaseModel):
|
|
22
|
+
"""Schema for the recall tool — matches CrewAI's RecallMemorySchema."""
|
|
23
|
+
|
|
24
|
+
queries: list[str] = Field(
|
|
25
|
+
...,
|
|
26
|
+
description=(
|
|
27
|
+
"One or more search queries. Pass a single item for a focused search, "
|
|
28
|
+
"or multiple items to search for several things at once."
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _RememberArgs(BaseModel):
|
|
34
|
+
"""Schema for the remember tool — matches CrewAI's RememberSchema."""
|
|
35
|
+
|
|
36
|
+
contents: list[str] = Field(
|
|
37
|
+
...,
|
|
38
|
+
description=(
|
|
39
|
+
"One or more facts, decisions, or observations to remember. "
|
|
40
|
+
"Pass a single item or multiple items at once."
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── Tools ────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AgentMemRecallTool(BaseTool):
|
|
49
|
+
"""CrewAI tool that recalls relevant facts from AgentMem.
|
|
50
|
+
|
|
51
|
+
Replace the built-in ``RecallMemoryTool`` with this to get hybrid retrieval
|
|
52
|
+
(semantic + keyword + graph) and multi-agent scoping at no extra config.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name: str = "Search memory"
|
|
56
|
+
description: str = (
|
|
57
|
+
"Search long-term memory for facts relevant to one or more queries. "
|
|
58
|
+
"Use this when you need context the conversation has not provided "
|
|
59
|
+
"(past decisions, user preferences, prior work)."
|
|
60
|
+
)
|
|
61
|
+
args_schema: type[BaseModel] = _RecallArgs
|
|
62
|
+
config: AgentMemConfig = Field(exclude=True)
|
|
63
|
+
|
|
64
|
+
def _run(self, queries: Union[list[str], str], **_: Any) -> str:
|
|
65
|
+
if isinstance(queries, str):
|
|
66
|
+
queries = [queries]
|
|
67
|
+
|
|
68
|
+
lines: list[str] = []
|
|
69
|
+
seen: set[str] = set()
|
|
70
|
+
for q in queries:
|
|
71
|
+
for h in search_memories(q, self.config):
|
|
72
|
+
if h.id not in seen:
|
|
73
|
+
seen.add(h.id)
|
|
74
|
+
lines.append(f"- (score={h.score:.2f}) {h.content}")
|
|
75
|
+
|
|
76
|
+
if not lines:
|
|
77
|
+
return "No relevant memories found."
|
|
78
|
+
return "Found memories:\n" + "\n".join(lines)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AgentMemRememberTool(BaseTool):
|
|
82
|
+
"""CrewAI tool that stores facts in AgentMem."""
|
|
83
|
+
|
|
84
|
+
name: str = "Save to memory"
|
|
85
|
+
description: str = (
|
|
86
|
+
"Save one or more facts, decisions, or constraints to long-term memory "
|
|
87
|
+
"so future runs can recall them. Use sparingly — durable facts only."
|
|
88
|
+
)
|
|
89
|
+
args_schema: type[BaseModel] = _RememberArgs
|
|
90
|
+
config: AgentMemConfig = Field(exclude=True)
|
|
91
|
+
|
|
92
|
+
def _run(self, contents: Union[list[str], str], **_: Any) -> str:
|
|
93
|
+
if isinstance(contents, str):
|
|
94
|
+
contents = [contents]
|
|
95
|
+
|
|
96
|
+
mem = _client(self.config)
|
|
97
|
+
ids: list[str] = []
|
|
98
|
+
for c in contents:
|
|
99
|
+
res = mem.write(
|
|
100
|
+
content=c,
|
|
101
|
+
agent_id=self.config.agent_id,
|
|
102
|
+
scope=self.config.scope or "private",
|
|
103
|
+
workflow_id=self.config.workflow_id,
|
|
104
|
+
role=self.config.role,
|
|
105
|
+
)
|
|
106
|
+
ids.append(res.get("writeId", ""))
|
|
107
|
+
|
|
108
|
+
n = len(contents)
|
|
109
|
+
return (
|
|
110
|
+
f"Saved 1 memory (id: {ids[0]})."
|
|
111
|
+
if n == 1
|
|
112
|
+
else f"Saved {n} memories (ids: {', '.join(filter(None, ids))})."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── Factory ─────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def create_agentmem_tools(config: AgentMemConfig) -> list[BaseTool]:
|
|
120
|
+
"""Return both recall + remember tools wired to a shared config.
|
|
121
|
+
|
|
122
|
+
Matches CrewAI's ``create_memory_tools`` factory shape so you can swap one
|
|
123
|
+
for the other:
|
|
124
|
+
|
|
125
|
+
.. code-block:: python
|
|
126
|
+
|
|
127
|
+
from crewai import Agent
|
|
128
|
+
from agentmem_crewai import AgentMemConfig, create_agentmem_tools
|
|
129
|
+
|
|
130
|
+
config = AgentMemConfig(api_key="sk-...", agent_id="support-bot")
|
|
131
|
+
agent = Agent(
|
|
132
|
+
role="Support",
|
|
133
|
+
goal="Help customers",
|
|
134
|
+
backstory="...",
|
|
135
|
+
tools=create_agentmem_tools(config),
|
|
136
|
+
)
|
|
137
|
+
"""
|
|
138
|
+
return [AgentMemRecallTool(config=config), AgentMemRememberTool(config=config)]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentmem-crewai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AgentMem integration for CrewAI — drop-in memory tools for any Crew agent"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.10,<3.14"
|
|
12
|
+
authors = [{ name = "rooney011", email = "bvsa2020@gmail.com" }]
|
|
13
|
+
keywords = ["crewai", "ai", "agents", "memory", "agentmem", "tools"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"agentmem-py>=0.2.0",
|
|
27
|
+
"crewai>=1.10.0,<2.0",
|
|
28
|
+
"pydantic>=2.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=7.0", "pytest-asyncio"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://agentmem.dev"
|
|
36
|
+
Repository = "https://github.com/rooney011/agentmem-sdk"
|
|
37
|
+
Issues = "https://github.com/rooney011/agentmem-sdk/issues"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["agentmem_crewai"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = [
|
|
44
|
+
"/agentmem_crewai",
|
|
45
|
+
"/tests",
|
|
46
|
+
"/README.md",
|
|
47
|
+
"/pyproject.toml",
|
|
48
|
+
]
|
|
49
|
+
exclude = [
|
|
50
|
+
"venv",
|
|
51
|
+
"dist",
|
|
52
|
+
"**/__pycache__",
|
|
53
|
+
"**/.pytest_cache",
|
|
54
|
+
"**/*.pyc",
|
|
55
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Tests for the CrewAI tools and helpers.
|
|
2
|
+
|
|
3
|
+
Uses ``unittest.mock`` so we don't need network or live AgentMem credentials.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from agentmem_crewai import (
|
|
13
|
+
AgentMemConfig,
|
|
14
|
+
AgentMemRecallTool,
|
|
15
|
+
AgentMemRememberTool,
|
|
16
|
+
add_memory,
|
|
17
|
+
create_agentmem_tools,
|
|
18
|
+
recall_memories,
|
|
19
|
+
search_memories,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def config() -> AgentMemConfig:
|
|
25
|
+
return AgentMemConfig(api_key="sk-fake", agent_id="test-agent")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── tool shape ──────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_recall_tool_shape(config: AgentMemConfig):
|
|
32
|
+
tool = AgentMemRecallTool(config=config)
|
|
33
|
+
assert tool.name == "Search memory"
|
|
34
|
+
assert "long-term memory" in tool.description.lower()
|
|
35
|
+
assert tool.args_schema is not None
|
|
36
|
+
# Schema accepts the documented shape
|
|
37
|
+
parsed = tool.args_schema(queries=["one", "two"])
|
|
38
|
+
assert parsed.queries == ["one", "two"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_remember_tool_shape(config: AgentMemConfig):
|
|
42
|
+
tool = AgentMemRememberTool(config=config)
|
|
43
|
+
assert tool.name == "Save to memory"
|
|
44
|
+
assert "save" in tool.description.lower()
|
|
45
|
+
parsed = tool.args_schema(contents=["fact one"])
|
|
46
|
+
assert parsed.contents == ["fact one"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_create_agentmem_tools_returns_both(config: AgentMemConfig):
|
|
50
|
+
tools = create_agentmem_tools(config)
|
|
51
|
+
assert len(tools) == 2
|
|
52
|
+
names = {t.name for t in tools}
|
|
53
|
+
assert names == {"Search memory", "Save to memory"}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── recall tool behaviour ───────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@patch("agentmem_crewai.helpers.MemoryStore")
|
|
60
|
+
def test_recall_tool_run_dedupes_across_queries(mock_cls, config: AgentMemConfig):
|
|
61
|
+
# Two queries return overlapping hits; dedupe by id.
|
|
62
|
+
hit_a = MagicMock(id="m1", content="A", score=0.9)
|
|
63
|
+
hit_b = MagicMock(id="m2", content="B", score=0.7)
|
|
64
|
+
hit_a_dup = MagicMock(id="m1", content="A", score=0.85)
|
|
65
|
+
|
|
66
|
+
mock_inst = MagicMock()
|
|
67
|
+
mock_inst.search.side_effect = [[hit_a, hit_b], [hit_a_dup, hit_b]]
|
|
68
|
+
mock_cls.return_value = mock_inst
|
|
69
|
+
|
|
70
|
+
tool = AgentMemRecallTool(config=config)
|
|
71
|
+
out = tool._run(queries=["first", "second"])
|
|
72
|
+
|
|
73
|
+
assert "Found memories:" in out
|
|
74
|
+
assert out.count("score=") == 2 # 2 unique hits
|
|
75
|
+
assert "A" in out and "B" in out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@patch("agentmem_crewai.helpers.MemoryStore")
|
|
79
|
+
def test_recall_tool_run_empty(mock_cls, config: AgentMemConfig):
|
|
80
|
+
mock_inst = MagicMock()
|
|
81
|
+
mock_inst.search.return_value = []
|
|
82
|
+
mock_cls.return_value = mock_inst
|
|
83
|
+
|
|
84
|
+
tool = AgentMemRecallTool(config=config)
|
|
85
|
+
assert tool._run(queries=["nothing"]) == "No relevant memories found."
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@patch("agentmem_crewai.helpers.MemoryStore")
|
|
89
|
+
def test_recall_tool_accepts_string_query(mock_cls, config: AgentMemConfig):
|
|
90
|
+
mock_inst = MagicMock()
|
|
91
|
+
mock_inst.search.return_value = []
|
|
92
|
+
mock_cls.return_value = mock_inst
|
|
93
|
+
|
|
94
|
+
tool = AgentMemRecallTool(config=config)
|
|
95
|
+
# CrewAI's built-in handles both shapes; we should too.
|
|
96
|
+
assert tool._run(queries="single string") == "No relevant memories found."
|
|
97
|
+
mock_inst.search.assert_called_once()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── remember tool behaviour ─────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@patch("agentmem_crewai.tools._client")
|
|
104
|
+
def test_remember_tool_single(mock_client, config: AgentMemConfig):
|
|
105
|
+
mock_inst = MagicMock()
|
|
106
|
+
mock_inst.write.return_value = {"writeId": "abc-123"}
|
|
107
|
+
mock_client.return_value = mock_inst
|
|
108
|
+
|
|
109
|
+
tool = AgentMemRememberTool(config=config)
|
|
110
|
+
out = tool._run(contents=["one fact"])
|
|
111
|
+
assert "abc-123" in out
|
|
112
|
+
assert "Saved 1 memory" in out
|
|
113
|
+
mock_inst.write.assert_called_once_with(
|
|
114
|
+
content="one fact",
|
|
115
|
+
agent_id="test-agent",
|
|
116
|
+
scope="private",
|
|
117
|
+
workflow_id=None,
|
|
118
|
+
role=None,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@patch("agentmem_crewai.tools._client")
|
|
123
|
+
def test_remember_tool_multiple(mock_client, config: AgentMemConfig):
|
|
124
|
+
mock_inst = MagicMock()
|
|
125
|
+
mock_inst.write.side_effect = [{"writeId": "a"}, {"writeId": "b"}, {"writeId": "c"}]
|
|
126
|
+
mock_client.return_value = mock_inst
|
|
127
|
+
|
|
128
|
+
tool = AgentMemRememberTool(config=config)
|
|
129
|
+
out = tool._run(contents=["x", "y", "z"])
|
|
130
|
+
assert "Saved 3 memories" in out
|
|
131
|
+
assert mock_inst.write.call_count == 3
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── helpers ─────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@patch("agentmem_crewai.helpers.MemoryStore")
|
|
138
|
+
def test_search_memories_returns_empty_for_empty_query(_mock_cls, config):
|
|
139
|
+
assert search_memories("", config) == []
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@patch("agentmem_crewai.helpers.MemoryStore")
|
|
143
|
+
def test_recall_memories_returns_empty_string_for_no_hits(mock_cls, config):
|
|
144
|
+
mock_inst = MagicMock()
|
|
145
|
+
mock_inst.search.return_value = []
|
|
146
|
+
mock_cls.return_value = mock_inst
|
|
147
|
+
assert recall_memories("anything", config) == ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@patch("agentmem_crewai.helpers.MemoryStore")
|
|
151
|
+
def test_recall_memories_formats_with_preamble(mock_cls, config):
|
|
152
|
+
hit = MagicMock(id="m", content="fact", score=0.9)
|
|
153
|
+
mock_inst = MagicMock()
|
|
154
|
+
mock_inst.search.return_value = [hit]
|
|
155
|
+
mock_cls.return_value = mock_inst
|
|
156
|
+
|
|
157
|
+
out = recall_memories("query", config)
|
|
158
|
+
assert "Relevant memories retrieved from AgentMem" in out
|
|
159
|
+
assert "- (score=0.90) fact" in out
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@patch("agentmem_crewai.helpers.MemoryStore")
|
|
163
|
+
def test_add_memory_passes_scope_default(mock_cls, config):
|
|
164
|
+
mock_inst = MagicMock()
|
|
165
|
+
mock_inst.write.return_value = {"writeId": "id-1"}
|
|
166
|
+
mock_cls.return_value = mock_inst
|
|
167
|
+
|
|
168
|
+
add_memory("hello", config)
|
|
169
|
+
mock_inst.write.assert_called_once_with(
|
|
170
|
+
content="hello",
|
|
171
|
+
agent_id="test-agent",
|
|
172
|
+
scope="private",
|
|
173
|
+
workflow_id=None,
|
|
174
|
+
role=None,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@patch("agentmem_crewai.helpers.MemoryStore")
|
|
179
|
+
def test_min_score_filters_low_hits(mock_cls):
|
|
180
|
+
hits = [
|
|
181
|
+
MagicMock(id="a", content="low", score=0.3),
|
|
182
|
+
MagicMock(id="b", content="high", score=0.9),
|
|
183
|
+
]
|
|
184
|
+
mock_inst = MagicMock()
|
|
185
|
+
mock_inst.search.return_value = hits
|
|
186
|
+
mock_cls.return_value = mock_inst
|
|
187
|
+
|
|
188
|
+
cfg = AgentMemConfig(api_key="sk", agent_id="a", min_score=0.5)
|
|
189
|
+
out = search_memories("q", cfg)
|
|
190
|
+
assert len(out) == 1
|
|
191
|
+
assert out[0].content == "high"
|