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.
@@ -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"