Skip to content

Commit c55d042

Browse files
authored
Merge pull request #42 from helloissariel/fix/api-call
Add Mem0 memory tools to spoon-toolkit and declare mem0ai dependency
2 parents ff59deb + d7270e8 commit c55d042

File tree

3 files changed

+207
-1
lines changed

3 files changed

+207
-1
lines changed

spoon_toolkits/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@
132132
EvmSwapQuoteTool,
133133
)
134134

135+
from .memory.mem0_tools import AddMemoryTool, SearchMemoryTool, GetAllMemoryTool
136+
135137

136138

137139
__all__ = [
@@ -277,4 +279,9 @@
277279
"EvmErc20TransferTool",
278280
"EvmBalanceTool",
279281
"EvmSwapQuoteTool",
280-
]
282+
283+
# Memory tools
284+
"AddMemoryTool",
285+
"SearchMemoryTool",
286+
"GetAllMemoryTool",
287+
]

spoon_toolkits/memory/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .mem0_tools import AddMemoryTool, SearchMemoryTool, GetAllMemoryTool
2+
3+
__all__ = ["AddMemoryTool", "SearchMemoryTool", "GetAllMemoryTool"]
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"""Mem0-powered memory tools."""
2+
3+
import logging
4+
from typing import Any, Dict, List, Optional
5+
6+
from pydantic import Field
7+
8+
from spoon_ai.memory.mem0_client import SpoonMem0
9+
from spoon_ai.tools.base import BaseTool, ToolResult
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class Mem0ToolBase(BaseTool):
15+
"""Shared Mem0 tool wiring that supports client injection or config-based init."""
16+
17+
mem0_config: Dict[str, Any] = Field(default_factory=dict, description="Mem0 client configuration")
18+
mem0_client: Optional[SpoonMem0] = Field(default=None, description="Optional injected SpoonMem0 client")
19+
default_user_id: Optional[str] = Field(
20+
default=None, description="Default user/agent id used when none is provided at call time"
21+
)
22+
23+
def model_post_init(self, __context: Any = None) -> None:
24+
super().model_post_init(__context)
25+
if self.mem0_client is None:
26+
self.mem0_client = SpoonMem0(self.mem0_config)
27+
if self.default_user_id is None:
28+
self.default_user_id = (
29+
self.mem0_config.get("user_id")
30+
or self.mem0_config.get("agent_id")
31+
or (self.mem0_client.user_id if self.mem0_client else None)
32+
)
33+
34+
def _resolve_user(self, user_id: Optional[str]) -> Optional[str]:
35+
return user_id or self.default_user_id
36+
37+
def _get_client(self) -> Optional[SpoonMem0]:
38+
client = self.mem0_client
39+
if client and client.is_ready():
40+
return client
41+
return None
42+
43+
44+
class AddMemoryTool(Mem0ToolBase):
45+
name: str = "add_memory"
46+
description: str = "Store text or conversation snippets in Mem0 for long-term recall."
47+
parameters: dict = {
48+
"type": "object",
49+
"properties": {
50+
"content": {
51+
"type": "string",
52+
"description": "Text to store as memory when not supplying structured messages",
53+
},
54+
"messages": {
55+
"type": "array",
56+
"description": "Optional list of role/content dicts to store together",
57+
"items": {
58+
"type": "object",
59+
"properties": {
60+
"role": {"type": "string", "description": "Message role (e.g., user, assistant, system)"},
61+
"content": {"type": "string", "description": "Message text"},
62+
},
63+
},
64+
},
65+
"role": {
66+
"type": "string",
67+
"description": "Role tag used when content is provided directly",
68+
"default": "user",
69+
},
70+
"user_id": {
71+
"type": "string",
72+
"description": "Optional user/agent id override for this write",
73+
},
74+
"metadata": {
75+
"type": "object",
76+
"description": "Additional metadata to attach to the stored memory",
77+
},
78+
},
79+
}
80+
81+
async def execute(
82+
self,
83+
content: Optional[str] = None,
84+
messages: Optional[List[Any]] = None,
85+
role: str = "user",
86+
user_id: Optional[str] = None,
87+
metadata: Optional[Dict[str, Any]] = None,
88+
) -> ToolResult:
89+
client = self._get_client()
90+
if not client:
91+
return ToolResult(error="Mem0 client is not initialized; install mem0ai and set MEM0_API_KEY.")
92+
93+
payload: List[Dict[str, str]] = []
94+
if messages:
95+
for message in messages:
96+
if isinstance(message, dict):
97+
msg_role = str(message.get("role") or "user")
98+
msg_content = message.get("content")
99+
else:
100+
msg_role = "user"
101+
msg_content = str(message)
102+
if msg_content:
103+
payload.append({"role": msg_role, "content": str(msg_content)})
104+
105+
if content:
106+
payload.append({"role": role or "user", "content": content})
107+
108+
if not payload:
109+
return ToolResult(error="No memory content provided; pass content or messages.")
110+
111+
try:
112+
active_user = self._resolve_user(user_id)
113+
client.add_memory(payload, user_id=active_user, metadata=metadata)
114+
return ToolResult(output={"status": "stored", "count": len(payload), "user_id": active_user})
115+
except Exception as exc: # pragma: no cover - defensive
116+
logger.warning("Failed to add memory via Mem0: %s", exc)
117+
return ToolResult(error=f"Failed to add memory: {exc}")
118+
119+
120+
class SearchMemoryTool(Mem0ToolBase):
121+
name: str = "search_memory"
122+
description: str = "Search Mem0 for relevant stored memories using a natural language query."
123+
parameters: dict = {
124+
"type": "object",
125+
"properties": {
126+
"query": {
127+
"type": "string",
128+
"description": "Natural language query describing what to recall from memory",
129+
},
130+
"user_id": {
131+
"type": "string",
132+
"description": "Optional user/agent id override for this search",
133+
},
134+
"limit": {
135+
"type": "integer",
136+
"description": "Maximum number of memories to return",
137+
},
138+
},
139+
"required": ["query"],
140+
}
141+
142+
async def execute(
143+
self,
144+
query: str,
145+
user_id: Optional[str] = None,
146+
limit: Optional[int] = None,
147+
) -> ToolResult:
148+
client = self._get_client()
149+
if not client:
150+
return ToolResult(error="Mem0 client is not initialized; install mem0ai and set MEM0_API_KEY.")
151+
if not query:
152+
return ToolResult(error="Query is required to search memories.")
153+
154+
try:
155+
memories = client.search_memory(query=query, user_id=self._resolve_user(user_id), limit=limit)
156+
return ToolResult(output={"memories": memories})
157+
except Exception as exc: # pragma: no cover - defensive
158+
logger.warning("Mem0 search failed: %s", exc)
159+
return ToolResult(error=f"Failed to search memories: {exc}")
160+
161+
162+
class GetAllMemoryTool(Mem0ToolBase):
163+
name: str = "get_all_memory"
164+
description: str = "Fetch all stored memories for the configured or provided user."
165+
parameters: dict = {
166+
"type": "object",
167+
"properties": {
168+
"user_id": {
169+
"type": "string",
170+
"description": "Optional user/agent id override for the retrieval",
171+
},
172+
"limit": {
173+
"type": "integer",
174+
"description": "Maximum number of memories to return",
175+
},
176+
},
177+
}
178+
179+
async def execute(
180+
self,
181+
user_id: Optional[str] = None,
182+
limit: Optional[int] = None,
183+
) -> ToolResult:
184+
client = self._get_client()
185+
if not client:
186+
return ToolResult(error="Mem0 client is not initialized; install mem0ai and set MEM0_API_KEY.")
187+
188+
try:
189+
memories = client.get_all_memory(user_id=self._resolve_user(user_id), limit=limit)
190+
return ToolResult(output={"memories": memories})
191+
except Exception as exc: # pragma: no cover - defensive
192+
logger.warning("Mem0 get_all failed: %s", exc)
193+
return ToolResult(error=f"Failed to fetch memories: {exc}")
194+
195+
196+
__all__ = ["AddMemoryTool", "SearchMemoryTool", "GetAllMemoryTool"]

0 commit comments

Comments
 (0)