merge: resolve conflicts with fix/openai-sdk-python-v4-api

Keep 7-tool parity while adopting singular container_tag, include.documents
forwarding, and v2.0.0 package metadata from the base branch.

Co-authored-by: Dhravya Shah <dhravya@supermemory.com>
This commit is contained in:
Cursor Agent 2026-08-14 18:25:14 +00:00
commit 0a3ff69f4c
No known key found for this signature in database
5 changed files with 76 additions and 38 deletions

View file

@ -4,6 +4,22 @@ Memory tools and middleware for OpenAI with Supermemory integration.
This package provides both **automatic memory injection middleware** and **manual memory tools** for the official [OpenAI Python SDK](https://github.com/openai/openai-python) using [Supermemory](https://supermemory.ai) capabilities.
## Breaking changes (v2.0)
Aligned with Supermemory v4 APIs:
- `SupermemoryToolsConfig.container_tags` (list) is **removed**. Use a single `container_tag` string instead.
- Search/add tool calls send `container_tag` (singular) to the API.
- `search_memories(..., include_full_docs=True)` maps to `include={"documents": True}` on `client.search.memories`.
```python
# Before (v1)
config = {"container_tags": ["user-123"]}
# After (v2)
config = {"container_tag": "user-123"}
```
## Installation
Install using uv (recommended):
@ -237,7 +253,7 @@ from supermemory_openai import SupermemoryTools
tools = SupermemoryTools(
api_key="your-supermemory-api-key",
config={
"project_id": "my-project", # or use container_tags
"project_id": "my-project", # or use container_tag="user-123"
"base_url": "https://custom-endpoint.com", # optional
}
)
@ -408,7 +424,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.6"
version = "2.0.0"
description = "Memory tools for OpenAI function calling with supermemory"
readme = "README.md"
license = "MIT"
@ -25,7 +25,7 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"openai>=1.102.0",
"supermemory>=3.16.0",
"supermemory>=3.50.0",
"typing-extensions>=4.0.0",
"requests>=2.25.0",
]

View file

@ -130,11 +130,14 @@ ALL_TOOL_NAMES = (
class SupermemoryToolsConfig(TypedDict, total=False):
"""Configuration for Supermemory tools.
Only one of `project_id` or `container_tags` can be provided.
Only one of `project_id` or `container_tag` can be provided.
Breaking change: `container_tags` (list) was removed in favor of a single
`container_tag` string, matching the Supermemory v4 API.
"""
base_url: Optional[str]
container_tags: Optional[List[str]]
container_tag: Optional[str]
project_id: Optional[str]
@ -347,16 +350,16 @@ MEMORY_TOOL_SCHEMAS: Dict[str, ChatCompletionFunctionToolParam] = {
}
def _resolve_container_tags(config: SupermemoryToolsConfig) -> List[str]:
if config.get("project_id") is not None and config.get("container_tags") is not None:
def _resolve_container_tag(config: SupermemoryToolsConfig) -> str:
if config.get("project_id") is not None and config.get("container_tag") is not None:
raise SupermemoryConfigurationError(
"Supermemory tools config accepts either project_id or container_tags, not both."
"Supermemory tools config accepts either project_id or container_tag, not both."
)
if config.get("project_id"):
return [f"sm_project_{config['project_id']}"]
if config.get("container_tags"):
return config["container_tags"]
return ["sm_project_default"]
return f"sm_project_{config['project_id']}"
if config.get("container_tag"):
return config["container_tag"]
return "sm_project_default"
def _tool_definition(name: str) -> ChatCompletionToolParam:
@ -386,10 +389,10 @@ class SupermemoryTools:
client_kwargs["base_url"] = config["base_url"]
self.client = supermemory.AsyncSupermemory(**client_kwargs)
self.container_tags = _resolve_container_tags(config)
self.container_tag = _resolve_container_tag(config)
def _primary_container_tag(self, container_tag: Optional[str] = None) -> str:
return container_tag or self.container_tags[0]
return container_tag or self.container_tag
def get_tool_definitions(self) -> List[ChatCompletionFunctionToolParam]:
"""Get OpenAI function definitions for all memory tools."""
@ -431,10 +434,11 @@ class SupermemoryTools:
try:
response: SearchMemoriesResponse = await self.client.search.memories(
q=information_to_get,
container_tags=self.container_tags,
container_tag=self.container_tag,
limit=limit,
threshold=DEFAULT_CHUNK_THRESHOLD,
search_mode="hybrid",
include={"documents": include_full_docs},
)
results = response.results or []
@ -459,7 +463,7 @@ class SupermemoryTools:
try:
response: AddResponse = await self.client.add(
content=memory,
container_tags=self.container_tags,
container_tag=self.container_tag,
)
return MemoryAddResult(
@ -582,7 +586,7 @@ class SupermemoryTools:
kwargs: Dict[str, Any] = {
"content": content,
"container_tags": self.container_tags,
"container_tag": self.container_tag,
}
if metadata:
kwargs["metadata"] = metadata

View file

@ -125,14 +125,15 @@ class TestToolInitialization:
assert tools is not None
assert len(tools.get_tool_definitions()) == EXPECTED_TOOL_COUNT
def test_create_tools_with_custom_container_tags(self, test_api_key: str):
"""Test creating tools with custom container tags."""
def test_create_tools_with_custom_container_tag(self, test_api_key: str):
"""Test creating tools with a custom container tag."""
config: SupermemoryToolsConfig = {
"container_tags": ["custom-tag-1", "custom-tag-2"],
"container_tag": "custom-tag-1",
}
tools = SupermemoryTools(test_api_key, config)
assert tools is not None
assert tools.container_tag == "custom-tag-1"
assert len(tools.get_tool_definitions()) == EXPECTED_TOOL_COUNT
@ -180,7 +181,7 @@ class TestMemoryOperationsUnit:
from types import SimpleNamespace
from unittest.mock import AsyncMock
tools = SupermemoryTools("test-key", {"container_tags": ["unit-tag"]})
tools = SupermemoryTools("test-key", {"container_tag": "unit-tag"})
tools.client.add = AsyncMock(
return_value=SimpleNamespace(
id="doc_123",
@ -195,7 +196,7 @@ class TestMemoryOperationsUnit:
assert result["memory"]["id"] == "doc_123"
tools.client.add.assert_awaited_once_with(
content="User likes tea",
container_tags=["unit-tag"],
container_tag="unit-tag",
)
@pytest.mark.asyncio
@ -204,7 +205,7 @@ class TestMemoryOperationsUnit:
from types import SimpleNamespace
from unittest.mock import AsyncMock
tools = SupermemoryTools("test-key", {"container_tags": ["unit-tag"]})
tools = SupermemoryTools("test-key", {"container_tag": "unit-tag"})
tools.client.search.memories = AsyncMock(
return_value=SimpleNamespace(
results=[SimpleNamespace(model_dump=lambda: {"memory": "likes tea"})]
@ -218,9 +219,26 @@ class TestMemoryOperationsUnit:
tools.client.search.memories.assert_awaited_once()
kwargs = tools.client.search.memories.await_args.kwargs
assert kwargs["q"] == "tea"
assert kwargs["container_tags"] == ["unit-tag"]
assert kwargs["container_tag"] == "unit-tag"
assert kwargs["limit"] == 3
assert kwargs["search_mode"] == "hybrid"
assert kwargs["include"] == {"documents": True}
@pytest.mark.asyncio
async def test_search_memories_forwards_include_full_docs_false(self):
"""include_full_docs=False must set include.documents to False."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
tools = SupermemoryTools("test-key", {"container_tag": "unit-tag"})
tools.client.search.memories = AsyncMock(
return_value=SimpleNamespace(results=[])
)
await tools.search_memories("tea", include_full_docs=False)
kwargs = tools.client.search.memories.await_args.kwargs
assert kwargs["include"] == {"documents": False}
@pytest.mark.asyncio
async def test_get_profile_uses_client_profile(self):
@ -228,7 +246,7 @@ class TestMemoryOperationsUnit:
from types import SimpleNamespace
from unittest.mock import AsyncMock
tools = SupermemoryTools("test-key", {"container_tags": ["unit-tag"]})
tools = SupermemoryTools("test-key", {"container_tag": "unit-tag"})
tools.client.profile = AsyncMock(
return_value=SimpleNamespace(
profile={"static": ["likes tea"], "dynamic": []},
@ -250,7 +268,7 @@ class TestMemoryOperationsUnit:
from types import SimpleNamespace
from unittest.mock import AsyncMock
tools = SupermemoryTools("test-key", {"container_tags": ["unit-tag"]})
tools = SupermemoryTools("test-key", {"container_tag": "unit-tag"})
tools.client.documents.list = AsyncMock(
return_value=SimpleNamespace(
memories=[{"id": "doc_1"}],
@ -270,14 +288,14 @@ class TestMemoryOperationsUnit:
@pytest.mark.asyncio
async def test_memory_forget_requires_id_or_content(self):
"""memory_forget must reject calls without memory_id or memory_content."""
tools = SupermemoryTools("test-key", {"container_tags": ["unit-tag"]})
tools = SupermemoryTools("test-key", {"container_tag": "unit-tag"})
result = await tools.memory_forget()
assert result["success"] is False
assert "memory_id or memory_content" in result["error"]
def test_rejects_project_id_and_container_tags(self):
"""Config must reject both project_id and container_tags."""
def test_rejects_project_id_and_container_tag(self):
"""Config must reject both project_id and container_tag."""
from supermemory_openai.exceptions import SupermemoryConfigurationError
with pytest.raises(SupermemoryConfigurationError):
@ -285,7 +303,7 @@ class TestMemoryOperationsUnit:
"test-key",
{
"project_id": "abc",
"container_tags": ["tag-a"],
"container_tag": "tag-a",
},
)
@ -324,7 +342,7 @@ class TestMemoryOperations:
async def test_add_memory(self, test_api_key: str, test_base_url: str):
"""Test adding memory."""
config: SupermemoryToolsConfig = {
"container_tags": ["test-add-memory"],
"container_tag": "test-add-memory",
}
if test_base_url:
config["base_url"] = test_base_url
@ -460,7 +478,7 @@ class TestOpenAIIntegration:
):
"""Test handling multiple tool calls."""
tools_config: SupermemoryToolsConfig = {
"container_tags": ["test-multi-tools"],
"container_tag": "test-multi-tools",
}
if test_base_url:
tools_config["base_url"] = test_base_url

View file

@ -377,7 +377,7 @@ resolution-markers = [
"python_full_version < '3.10'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
wheels = [
@ -392,7 +392,7 @@ resolution-markers = [
"python_full_version >= '3.10'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
@ -422,7 +422,7 @@ name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
@ -1372,7 +1372,7 @@ wheels = [
[[package]]
name = "supermemory-openai-sdk"
version = "1.0.6"
version = "2.0.0"
source = { editable = "." }
dependencies = [
{ name = "openai" },
@ -1402,7 +1402,7 @@ requires-dist = [
{ name = "aiohttp", marker = "extra == 'async'", specifier = ">=3.8.0" },
{ name = "openai", specifier = ">=1.102.0" },
{ name = "requests", specifier = ">=2.25.0" },
{ name = "supermemory", specifier = ">=3.16.0" },
{ name = "supermemory", specifier = ">=3.50.0" },
{ name = "typing-extensions", specifier = ">=4.0.0" },
]
provides-extras = ["async"]