supermemory-agent-framework 1.0.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,60 @@
1
+ """Supermemory Agent Framework - Memory tools and middleware for Microsoft Agent Framework."""
2
+
3
+ from .connection import (
4
+ AgentSupermemory,
5
+ )
6
+
7
+ from .tools import (
8
+ SupermemoryTools,
9
+ MemorySearchResult,
10
+ MemoryAddResult,
11
+ ProfileResult,
12
+ )
13
+
14
+ from .middleware import (
15
+ SupermemoryChatMiddleware,
16
+ SupermemoryMiddlewareOptions,
17
+ )
18
+
19
+ from .context_provider import (
20
+ SupermemoryContextProvider,
21
+ )
22
+
23
+ from .utils import (
24
+ Logger,
25
+ create_logger,
26
+ deduplicate_memories,
27
+ DeduplicatedMemories,
28
+ convert_profile_to_markdown,
29
+ )
30
+
31
+ from .exceptions import (
32
+ SupermemoryError,
33
+ SupermemoryConfigurationError,
34
+ SupermemoryAPIError,
35
+ SupermemoryMemoryOperationError,
36
+ SupermemoryTimeoutError,
37
+ SupermemoryNetworkError,
38
+ )
39
+
40
+ __all__ = [
41
+ "AgentSupermemory",
42
+ "SupermemoryTools",
43
+ "MemorySearchResult",
44
+ "MemoryAddResult",
45
+ "ProfileResult",
46
+ "SupermemoryChatMiddleware",
47
+ "SupermemoryMiddlewareOptions",
48
+ "SupermemoryContextProvider",
49
+ "Logger",
50
+ "create_logger",
51
+ "deduplicate_memories",
52
+ "DeduplicatedMemories",
53
+ "convert_profile_to_markdown",
54
+ "SupermemoryError",
55
+ "SupermemoryConfigurationError",
56
+ "SupermemoryAPIError",
57
+ "SupermemoryMemoryOperationError",
58
+ "SupermemoryTimeoutError",
59
+ "SupermemoryNetworkError",
60
+ ]
@@ -0,0 +1,63 @@
1
+ """Shared connection class for Supermemory Agent Framework integrations.
2
+
3
+ Provides a single connection object that holds the SDK client, container tag,
4
+ conversation ID, and entity context — shared across middleware, tools, and
5
+ context providers.
6
+ """
7
+
8
+ import os
9
+ import uuid
10
+ from typing import Optional
11
+
12
+ import supermemory
13
+
14
+ from .exceptions import SupermemoryConfigurationError
15
+
16
+
17
+ class AgentSupermemory:
18
+ """Shared Supermemory connection for middleware, tools, and context providers.
19
+
20
+ Centralizes API client creation, container tag, conversation ID, and
21
+ entity context so that all integration points share the same session.
22
+
23
+ Example:
24
+ ```python
25
+ from supermemory_agent_framework import AgentSupermemory
26
+
27
+ conn = AgentSupermemory(
28
+ api_key="your-key",
29
+ container_tag="user-123",
30
+ entity_context="The user is a Python developer who prefers async code.",
31
+ )
32
+ ```
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ api_key: Optional[str] = None,
38
+ container_tag: str = "msft_agent_chat",
39
+ entity_context: Optional[str] = None,
40
+ conversation_id: Optional[str] = None,
41
+ ) -> None:
42
+ """Initialize the shared Supermemory connection.
43
+
44
+ Args:
45
+ api_key: Supermemory API key. Falls back to SUPERMEMORY_API_KEY env var.
46
+ container_tag: Unique identifier for memory scope (e.g., user ID).
47
+ entity_context: Custom context about the user/entity to prepend to memories.
48
+ conversation_id: Conversation ID for grouping messages. Auto-generated if None.
49
+ """
50
+ resolved_api_key = api_key or os.getenv("SUPERMEMORY_API_KEY")
51
+ if not resolved_api_key:
52
+ raise SupermemoryConfigurationError(
53
+ "SUPERMEMORY_API_KEY environment variable is required but not set. "
54
+ "Pass api_key parameter or set the environment variable."
55
+ )
56
+
57
+ self.client: supermemory.AsyncSupermemory = supermemory.AsyncSupermemory(
58
+ api_key=resolved_api_key
59
+ )
60
+ self.container_tag: str = container_tag
61
+ self.conversation_id: str = conversation_id or str(uuid.uuid4())
62
+ self.custom_id: str = f"conversation_{self.conversation_id}"
63
+ self.entity_context: Optional[str] = entity_context
@@ -0,0 +1,330 @@
1
+ """Supermemory context provider for Microsoft Agent Framework.
2
+
3
+ Provides a BaseContextProvider subclass that automatically injects relevant
4
+ memories before LLM invocation and stores conversations after.
5
+
6
+ This is the idiomatic way to integrate persistent memory in Agent Framework,
7
+ following the same pattern as the built-in Mem0 integration.
8
+ """
9
+
10
+ from typing import Any, Literal, Optional
11
+
12
+ from agent_framework import BaseContextProvider
13
+
14
+ from .connection import AgentSupermemory
15
+ from .utils import (
16
+ convert_profile_to_markdown,
17
+ create_logger,
18
+ deduplicate_memories,
19
+ wrap_memory_injection,
20
+ )
21
+
22
+
23
+ class SupermemoryContextProvider(BaseContextProvider):
24
+ """Context provider that integrates Supermemory into the agent pipeline.
25
+
26
+ Automatically searches for relevant memories before the model is invoked
27
+ and optionally stores conversations after the model responds.
28
+
29
+ This follows the same pattern as the built-in Mem0 context provider,
30
+ making it the most idiomatic way to add persistent memory to agents.
31
+
32
+ Example:
33
+ ```python
34
+ from agent_framework import Agent, AgentSession
35
+ from agent_framework.openai import OpenAIResponsesClient
36
+ from supermemory_agent_framework import (
37
+ AgentSupermemory,
38
+ SupermemoryContextProvider,
39
+ )
40
+
41
+ conn = AgentSupermemory(api_key="your-key", container_tag="user-123")
42
+
43
+ provider = SupermemoryContextProvider(
44
+ conn,
45
+ mode="full",
46
+ store_conversations=True,
47
+ )
48
+
49
+ agent = OpenAIResponsesClient().as_agent(
50
+ name="MemoryAgent",
51
+ instructions="You are a helpful assistant with memory.",
52
+ context_providers=[provider],
53
+ )
54
+
55
+ session = AgentSession()
56
+ response = await agent.run(
57
+ "What's my favorite programming language?",
58
+ session=session,
59
+ )
60
+ ```
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ connection: AgentSupermemory,
66
+ *,
67
+ mode: Literal["profile", "query", "full"] = "full",
68
+ store_conversations: bool = False,
69
+ context_prompt: str = "",
70
+ verbose: bool = False,
71
+ source_id: str = "supermemory",
72
+ ) -> None:
73
+ """Initialize the Supermemory context provider.
74
+
75
+ Args:
76
+ connection: Shared AgentSupermemory connection.
77
+ mode: Memory retrieval mode - "profile", "query", or "full".
78
+ store_conversations: Whether to store conversations after each run.
79
+ context_prompt: Header text prepended to memory content.
80
+ verbose: Enable detailed logging.
81
+ source_id: Unique identifier for this provider instance.
82
+ """
83
+ super().__init__(source_id=source_id)
84
+
85
+ self._connection = connection
86
+ self._container_tag = connection.container_tag
87
+ self._mode = mode
88
+ self._store_conversations = store_conversations
89
+ self._context_prompt = context_prompt
90
+ self._logger = create_logger(verbose)
91
+ self._client = connection.client
92
+
93
+ async def before_run(
94
+ self,
95
+ *,
96
+ agent: Any,
97
+ session: Any,
98
+ context: Any,
99
+ state: dict[str, Any],
100
+ ) -> None:
101
+ """Search Supermemory for relevant memories and inject into context."""
102
+ # Extract query text from input messages
103
+ query_text = ""
104
+ if self._mode != "profile":
105
+ query_text = self._extract_query_from_context(context)
106
+ if not query_text and self._mode == "query":
107
+ self._logger.debug("No user message found, skipping memory search")
108
+ return
109
+
110
+ self._logger.info(
111
+ "Searching Supermemory for memories",
112
+ {
113
+ "container_tag": self._container_tag,
114
+ "mode": self._mode,
115
+ "query_preview": query_text[:100] if query_text else "",
116
+ },
117
+ )
118
+
119
+ try:
120
+ memories_text = await self._fetch_memories(query_text)
121
+ except Exception as e:
122
+ self._logger.error(
123
+ "Failed to fetch memories, proceeding without",
124
+ {"error": str(e)},
125
+ )
126
+ return
127
+
128
+ if not memories_text:
129
+ self._logger.debug("No memories found")
130
+ return
131
+
132
+ # Prepend entity context if available
133
+ if self._connection.entity_context:
134
+ memories_text = f"{self._connection.entity_context}\n\n{memories_text}"
135
+
136
+ # Inject memories into the session context
137
+ full_text = wrap_memory_injection(memories_text, self._context_prompt)
138
+
139
+ self._logger.debug(
140
+ "Injecting memories into context",
141
+ {"length": len(memories_text)},
142
+ )
143
+
144
+ # Use extend_instructions to add memory context
145
+ if hasattr(context, "extend_instructions"):
146
+ context.extend_instructions(full_text, source=self.source_id)
147
+ elif hasattr(context, "extend_messages"):
148
+ # Fallback: add as a system message
149
+ context.extend_messages(
150
+ [{"role": "system", "content": full_text}],
151
+ source=self.source_id,
152
+ )
153
+
154
+ async def after_run(
155
+ self,
156
+ *,
157
+ agent: Any,
158
+ session: Any,
159
+ context: Any,
160
+ state: dict[str, Any],
161
+ ) -> None:
162
+ """Store conversation messages to Supermemory for future retrieval."""
163
+ if not self._store_conversations:
164
+ return
165
+
166
+ try:
167
+ conversation_text = self._extract_conversation_from_context(context)
168
+ if not conversation_text:
169
+ self._logger.debug("No conversation content to store")
170
+ return
171
+
172
+ self._logger.info(
173
+ "Storing conversation to Supermemory",
174
+ {
175
+ "container_tag": self._container_tag,
176
+ "content_length": len(conversation_text),
177
+ },
178
+ )
179
+
180
+ add_params: dict[str, Any] = {
181
+ "content": conversation_text,
182
+ "container_tag": self._container_tag,
183
+ "custom_id": self._connection.custom_id,
184
+ }
185
+
186
+ await self._client.add(**add_params)
187
+
188
+ self._logger.info("Conversation stored successfully")
189
+
190
+ except Exception as e:
191
+ self._logger.error(
192
+ "Failed to store conversation",
193
+ {"error": str(e)},
194
+ )
195
+
196
+ async def _fetch_memories(self, query_text: str = "") -> str:
197
+ """Fetch and format memories from Supermemory."""
198
+ kwargs: dict[str, Any] = {"container_tag": self._container_tag}
199
+ if query_text:
200
+ kwargs["q"] = query_text
201
+
202
+ response = await self._client.profile(**kwargs)
203
+
204
+ profile = response.profile if response.profile else None
205
+ static = list(profile.static) if profile and profile.static else []
206
+ dynamic = list(profile.dynamic) if profile and profile.dynamic else []
207
+ search_results_raw = (
208
+ list(response.search_results.results)
209
+ if response.search_results and response.search_results.results
210
+ else []
211
+ )
212
+
213
+ deduplicated = deduplicate_memories(
214
+ static=static,
215
+ dynamic=dynamic,
216
+ search_results=search_results_raw,
217
+ )
218
+
219
+ # Build formatted text based on mode
220
+ profile_text = ""
221
+ if self._mode != "query":
222
+ profile_text = convert_profile_to_markdown(
223
+ {
224
+ "profile": {
225
+ "static": deduplicated.static,
226
+ "dynamic": deduplicated.dynamic,
227
+ },
228
+ "searchResults": {"results": []},
229
+ }
230
+ )
231
+
232
+ search_text = ""
233
+ if self._mode != "profile" and deduplicated.search_results:
234
+ search_text = "Search results for user's recent message:\n" + "\n".join(
235
+ f"- {memory}" for memory in deduplicated.search_results
236
+ )
237
+
238
+ return f"{profile_text}\n{search_text}".strip()
239
+
240
+ def _extract_query_from_context(self, context: Any) -> str:
241
+ """Extract the last user message from the session context."""
242
+ messages = None
243
+
244
+ if hasattr(context, "input_messages"):
245
+ messages = context.input_messages
246
+ elif hasattr(context, "messages"):
247
+ messages = context.messages
248
+
249
+ if not messages:
250
+ return ""
251
+
252
+ for msg in reversed(list(messages)):
253
+ role = None
254
+ content = None
255
+
256
+ if hasattr(msg, "role"):
257
+ role = msg.role
258
+ elif isinstance(msg, dict):
259
+ role = msg.get("role")
260
+
261
+ if role == "user":
262
+ if hasattr(msg, "text"):
263
+ content = msg.text
264
+ elif hasattr(msg, "content"):
265
+ content = msg.content
266
+ elif isinstance(msg, dict):
267
+ content = msg.get("content", "") or msg.get("text", "")
268
+
269
+ if isinstance(content, str):
270
+ return content
271
+ if isinstance(content, list):
272
+ parts = []
273
+ for part in content:
274
+ if isinstance(part, dict) and part.get("type") == "text":
275
+ parts.append(part.get("text", ""))
276
+ elif isinstance(part, str):
277
+ parts.append(part)
278
+ return " ".join(parts)
279
+ return ""
280
+
281
+ def _extract_conversation_from_context(self, context: Any) -> str:
282
+ """Extract conversation text from context for storage."""
283
+ messages: list[Any] = []
284
+
285
+ # Gather input messages
286
+ if hasattr(context, "input_messages"):
287
+ messages.extend(context.input_messages or [])
288
+ elif hasattr(context, "messages"):
289
+ messages.extend(context.messages or [])
290
+
291
+ # Gather response messages
292
+ if hasattr(context, "response") and context.response:
293
+ resp = context.response
294
+ if hasattr(resp, "text") and resp.text:
295
+ messages.append({"role": "assistant", "content": resp.text})
296
+ elif hasattr(resp, "messages"):
297
+ messages.extend(resp.messages or [])
298
+
299
+ if not messages:
300
+ return ""
301
+
302
+ parts = []
303
+ for msg in messages:
304
+ role = None
305
+ content = None
306
+
307
+ if hasattr(msg, "role"):
308
+ role = msg.role
309
+ elif isinstance(msg, dict):
310
+ role = msg.get("role")
311
+
312
+ if role not in ("user", "assistant", "system"):
313
+ continue
314
+
315
+ if hasattr(msg, "text"):
316
+ content = msg.text
317
+ elif hasattr(msg, "content"):
318
+ content = msg.content
319
+ elif isinstance(msg, dict):
320
+ content = msg.get("content", "") or msg.get("text", "")
321
+
322
+ if isinstance(content, str) and content.strip():
323
+ display = {
324
+ "user": "User",
325
+ "assistant": "Assistant",
326
+ "system": "System",
327
+ }.get(role, str(role))
328
+ parts.append(f"{display}: {content}")
329
+
330
+ return "\n\n".join(parts)
@@ -0,0 +1,66 @@
1
+ """Custom exceptions for Supermemory Agent Framework integration."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class SupermemoryError(Exception):
7
+ """Base exception for all Supermemory-related errors."""
8
+
9
+ def __init__(self, message: str, original_error: Optional[Exception] = None):
10
+ super().__init__(message)
11
+ self.message = message
12
+ self.original_error = original_error
13
+
14
+ def __str__(self) -> str:
15
+ if self.original_error:
16
+ return f"{self.message}: {self.original_error}"
17
+ return self.message
18
+
19
+
20
+ class SupermemoryConfigurationError(SupermemoryError):
21
+ """Raised when there are configuration issues (e.g., missing API key)."""
22
+
23
+ pass
24
+
25
+
26
+ class SupermemoryAPIError(SupermemoryError):
27
+ """Raised when Supermemory API requests fail."""
28
+
29
+ def __init__(
30
+ self,
31
+ message: str,
32
+ status_code: Optional[int] = None,
33
+ response_text: Optional[str] = None,
34
+ original_error: Optional[Exception] = None,
35
+ ):
36
+ super().__init__(message, original_error)
37
+ self.status_code = status_code
38
+ self.response_text = response_text
39
+
40
+ def __str__(self) -> str:
41
+ parts = [self.message]
42
+ if self.status_code:
43
+ parts.append(f"Status: {self.status_code}")
44
+ if self.response_text:
45
+ parts.append(f"Response: {self.response_text}")
46
+ if self.original_error:
47
+ parts.append(f"Cause: {self.original_error}")
48
+ return " | ".join(parts)
49
+
50
+
51
+ class SupermemoryMemoryOperationError(SupermemoryError):
52
+ """Raised when memory operations (search, add) fail."""
53
+
54
+ pass
55
+
56
+
57
+ class SupermemoryTimeoutError(SupermemoryError):
58
+ """Raised when operations timeout."""
59
+
60
+ pass
61
+
62
+
63
+ class SupermemoryNetworkError(SupermemoryError):
64
+ """Raised when network operations fail."""
65
+
66
+ pass