This commit is contained in:
Dhravya Shah 2026-08-27 21:31:29 +00:00 committed by GitHub
commit 740053d4ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 205 additions and 1351 deletions

View file

@ -245,8 +245,7 @@ tools = SupermemoryTools(
# Search memories
result = await tools.search_memories(
information_to_get="user preferences",
limit=10,
include_full_docs=True
limit=10
)
# Add memory
@ -260,6 +259,10 @@ result = await tools.fetch_memory(
)
```
`include_full_docs` is retained as a deprecated Python argument for compatibility,
but v4 search returns relevant memories and chunks instead of full source documents.
It is no longer exposed in the OpenAI tool schema.
### Individual Tools
```python
@ -408,7 +411,7 @@ Optional for testing:
### Required
- `openai>=1.102.0` - Official OpenAI Python SDK
- `supermemory>=3.1.0` - Supermemory client
- `supermemory>=3.50.0` - Supermemory client
- `requests>=2.25.0` - HTTP requests (fallback)
### Optional

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "supermemory-openai-sdk"
version = "1.0.5"
version = "1.0.6"
description = "Memory tools for OpenAI function calling with supermemory"
readme = "README.md"
license = "MIT"
@ -15,7 +15,6 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@ -23,10 +22,10 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
requires-python = ">=3.8.1"
requires-python = ">=3.9"
dependencies = [
"openai>=1.102.0",
"supermemory>=3.1.0,<3.5.0",
"supermemory>=3.50.0",
"typing-extensions>=4.0.0",
"requests>=2.25.0",
]
@ -62,7 +61,7 @@ multi_line_output = 3
line_length = 88
[tool.mypy]
python_version = "3.8"
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true

View file

@ -222,15 +222,15 @@ async def add_memory_tool(
) -> None:
"""Add a new memory to the SuperMemory system."""
try:
add_params = {
"content": content,
"container_tags": [container_tag],
}
if custom_id is not None:
add_params["custom_id"] = custom_id
# Handle both sync and async supermemory clients
result = client.memories.add(**add_params)
if custom_id is None:
result = client.add(content=content, container_tag=container_tag)
else:
result = client.add(
content=content,
container_tag=container_tag,
custom_id=custom_id,
)
if inspect.isawaitable(result):
response = await result
else:
@ -242,7 +242,7 @@ async def add_memory_tool(
"container_tag": container_tag,
"custom_id": custom_id,
"content_length": len(content),
"memory_id": response.id,
"memory_id": getattr(response, "id", None),
},
)
except (OSError, ConnectionError) as network_error:

View file

@ -1,7 +1,8 @@
"""Supermemory tools for OpenAI function calling."""
import json
from typing import Dict, List, Optional, TypedDict, Union
import warnings
from typing import Dict, List, Optional, TypedDict
import supermemory
from openai.types.chat import (
@ -9,12 +10,7 @@ from openai.types.chat import (
ChatCompletionMessageToolCall,
ChatCompletionToolMessageParam,
)
from supermemory.types import (
MemoryAddResponse,
MemoryGetResponse,
SearchExecuteResponse,
)
from supermemory.types.search_execute_response import Result
from supermemory.types import AddResponse, SearchMemoriesResponse
from .exceptions import (
SupermemoryConfigurationError,
@ -27,6 +23,8 @@ class SupermemoryToolsConfig(TypedDict, total=False):
"""Configuration for Supermemory tools.
Only one of `project_id` or `container_tags` can be provided.
The first container tag is the primary v4 search scope; all configured tags
are applied when adding a memory.
"""
base_url: Optional[str]
@ -35,14 +33,14 @@ class SupermemoryToolsConfig(TypedDict, total=False):
# Type aliases using inferred types from supermemory package
MemoryObject = Union[MemoryGetResponse, MemoryAddResponse]
MemoryObject = AddResponse
class MemorySearchResult(TypedDict, total=False):
"""Result type for memory search operations."""
success: bool
results: Optional[List[Result]]
results: Optional[List[Dict[str, object]]]
count: Optional[int]
error: Optional[str]
@ -51,7 +49,7 @@ class MemoryAddResult(TypedDict, total=False):
"""Result type for memory add operations."""
success: bool
memory: Optional[MemoryAddResponse]
memory: Optional[Dict[str, object]]
error: Optional[str]
@ -69,14 +67,6 @@ MEMORY_TOOL_SCHEMAS: Dict[str, ChatCompletionFunctionToolParam] = {
"type": "string",
"description": "Terms to search for in the user's memories",
},
"include_full_docs": {
"type": "boolean",
"description": (
"Whether to include the full document content in the response. "
"Defaults to true for better AI context."
),
"default": True,
},
"limit": {
"type": "number",
"description": "Maximum number of results to return",
@ -173,32 +163,42 @@ class SupermemoryTools:
async def search_memories(
self,
information_to_get: str,
include_full_docs: bool = True,
include_full_docs: Optional[bool] = None,
limit: int = 10,
) -> MemorySearchResult:
"""Search memories.
Args:
information_to_get: Terms to search for
include_full_docs: Whether to include full document content
include_full_docs: Deprecated compatibility argument. V4 search
returns relevant memories and chunks, not full source documents.
limit: Maximum number of results
Returns:
MemorySearchResult
"""
try:
response: SearchExecuteResponse = await self.client.search.execute(
q=information_to_get,
container_tags=self.container_tags,
limit=limit,
chunk_threshold=0.6,
include_full_docs=include_full_docs,
if include_full_docs is not None:
warnings.warn(
"include_full_docs is deprecated and ignored because v4 search "
"does not return full source documents",
DeprecationWarning,
stacklevel=2,
)
try:
response: SearchMemoriesResponse = await self.client.search.memories(
q=information_to_get,
container_tag=self.container_tags[0],
limit=limit,
threshold=0.6,
search_mode="hybrid",
)
results = response.results or []
return MemorySearchResult(
success=True,
results=[r.model_dump() for r in response.results],
count=len(response.results),
results=[r.model_dump() for r in results],
count=len(results),
)
except (OSError, ConnectionError) as network_error:
return MemorySearchResult(
@ -221,12 +221,10 @@ class SupermemoryTools:
MemoryAddResult
"""
try:
add_params = {
"content": memory,
"container_tags": self.container_tags,
}
response: MemoryAddResponse = await self.client.memories.add(**add_params)
response: AddResponse = await self.client.add(
content=memory,
container_tags=self.container_tags,
)
return MemoryAddResult(
success=True,
@ -322,7 +320,7 @@ class SearchMemoriesTool:
async def execute(
self,
information_to_get: str,
include_full_docs: bool = True,
include_full_docs: Optional[bool] = None,
limit: int = 10,
) -> MemorySearchResult:
"""Execute search memories."""

View file

@ -212,14 +212,19 @@ def deduplicate_memories(
def extract_memory_text(item: Any) -> Optional[str]:
if item is None:
return None
if isinstance(item, str):
trimmed = item.strip()
return trimmed if trimmed else None
if isinstance(item, dict):
memory = item.get("memory")
if isinstance(memory, str):
trimmed = memory.strip()
return trimmed if trimmed else None
return None
if isinstance(item, str):
trimmed = item.strip()
# Stainless SDK returns pydantic models (attribute access, snake_case).
memory = getattr(item, "memory", None)
if isinstance(memory, str):
trimmed = memory.strip()
return trimmed if trimmed else None
return None

View file

@ -159,6 +159,10 @@ class TestToolDefinitions:
assert search_tool is not None
assert search_tool["type"] == "function"
assert "information_to_get" in search_tool["function"]["parameters"]["required"]
assert (
"include_full_docs"
not in search_tool["function"]["parameters"]["properties"]
)
# Check addMemory
add_tool = next(
@ -177,6 +181,65 @@ class TestToolDefinitions:
assert class_definitions == helper_definitions
class TestMemoryOperationsUnit:
"""Unit tests for memory operations (no live API)."""
@pytest.mark.asyncio
async def test_add_memory_uses_client_add(self):
"""add_memory must call client.add (memories.add was removed in supermemory 3.50)."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
tools = SupermemoryTools("test-key", {"container_tags": ["unit-tag"]})
tools.client.add = AsyncMock(
return_value=SimpleNamespace(
id="doc_123",
status="queued",
model_dump=lambda: {"id": "doc_123", "status": "queued"},
)
)
result = await tools.add_memory("User likes tea")
assert result["success"] is True
assert result["memory"]["id"] == "doc_123"
tools.client.add.assert_awaited_once_with(
content="User likes tea",
container_tags=["unit-tag"],
)
@pytest.mark.asyncio
async def test_search_memories_uses_search_memories_hybrid(self):
"""V4 search must use the primary singular tag and hybrid mode."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
tools = SupermemoryTools(
"test-key", {"container_tags": ["primary-tag", "secondary-tag"]}
)
tools.client.search.memories = AsyncMock(
return_value=SimpleNamespace(
results=[SimpleNamespace(model_dump=lambda: {"memory": "likes tea"})]
)
)
with pytest.warns(DeprecationWarning, match="include_full_docs"):
result = await tools.search_memories(
"tea", include_full_docs=False, limit=3
)
assert result["success"] is True
assert result["count"] == 1
tools.client.search.memories.assert_awaited_once()
kwargs = tools.client.search.memories.await_args.kwargs
assert kwargs["q"] == "tea"
assert kwargs["container_tag"] == "primary-tag"
assert "container_tags" not in kwargs
assert "include_full_docs" not in kwargs
assert kwargs["limit"] == 3
assert kwargs["search_mode"] == "hybrid"
class TestMemoryOperations:
"""Test memory operations."""

File diff suppressed because it is too large Load diff