Skip to content

Commit e22b5b0

Browse files
authored
feat: add redis backend for ObjectStore (#164)
* test: add object store integration tests * chore: license headers
1 parent b1b8683 commit e22b5b0

11 files changed

Lines changed: 741 additions & 240 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ dev = [
6464
"pytest",
6565
"pytest-asyncio",
6666
"python-dotenv",
67+
"docker"
6768
]
6869
lint = [
6970
"ruff",

src/deepset_mcp/tools/tokonomics/__init__.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
Key Features:
1414
- TTL-based object storage for temporary results
1515
- Rich object exploration with multiple rendering modes
16-
- Reference-based parameter passing (@obj_001.path.to.value)
16+
- Reference-based parameter passing (@obj_id.path.to.value)
1717
- Type-safe decorators that preserve function signatures
1818
- Configurable preview truncation and custom rendering callbacks
1919
@@ -30,7 +30,7 @@
3030
>>>
3131
>>> result = get_data()
3232
>>> print(result) # Shows rich preview
33-
>>> result.obj_id # "obj_001"
33+
>>> result.obj_id # "obj_123"
3434
>>> result.value # Original data
3535
3636
Referenceable tool that accepts references:
@@ -45,28 +45,27 @@
4545
>>> process_users([{"name": "Bob"}])
4646
>>>
4747
>>> # Use with reference
48-
>>> process_users("@obj_001.users")
48+
>>> process_users("@obj_123.users")
4949
5050
Exploration utilities:
5151
5252
>>> from deepset_mcp.tools.tokonomics import explore, search
5353
>>>
5454
>>> # Explore object structure
55-
>>> explore("obj_001", mode="tree")
55+
>>> explore("obj_123", mode="tree")
5656
>>>
5757
>>> # Search within objects
58-
>>> search("obj_001", "Alice")
58+
>>> search("obj_123", "Alice")
5959
"""
6060

6161
from .decorators import explorable, explorable_and_referenceable, referenceable
6262
from .explorer import RichExplorer
63-
from .object_store import Explorable, InMemoryBackend, ObjectRef, ObjectStore
63+
from .object_store import Explorable, InMemoryBackend, ObjectStore
6464

6565
__all__ = [
6666
# Core classes
6767
"Explorable",
6868
"InMemoryBackend",
69-
"ObjectRef",
7069
"ObjectStore",
7170
"RichExplorer",
7271
# Decorators

src/deepset_mcp/tools/tokonomics/decorators.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@
1919

2020
from glom import GlomError, glom
2121

22-
from .explorer import RichExplorer
23-
from .object_store import Explorable, ObjectRef, ObjectStore
22+
from deepset_mcp.tools.tokonomics.explorer import RichExplorer
23+
from deepset_mcp.tools.tokonomics.object_store import Explorable, ObjectStore
2424

2525
F = TypeVar("F", bound=Callable[..., Any])
2626

2727

2828
def _is_reference(value: Any) -> bool:
2929
"""Check if a value is a reference string."""
30-
return isinstance(value, str) and ObjectRef.parse(value) is not None
30+
return isinstance(value, str) and value.startswith("@") and len(value) > 1
3131

3232

3333
def _type_allows_str(annotation: Any) -> bool:
@@ -87,10 +87,10 @@ def _enhance_docstring_for_references(original: str, param_info: dict[str, dict[
8787
f" {func_name}(data={{'key': 'value'}}, threshold=10)",
8888
"",
8989
" # Call with references",
90-
f" {func_name}(data='@obj_001', threshold='@obj_002.config.threshold')",
90+
f" {func_name}(data='@obj_123', threshold='@obj_456.config.threshold')",
9191
"",
9292
" # Mixed call",
93-
f" {func_name}(data='@obj_001.items', threshold=10)",
93+
f" {func_name}(data='@obj_123.items', threshold=10)",
9494
]
9595
)
9696

@@ -141,7 +141,7 @@ def explorable(
141141
... return {"processed": data}
142142
...
143143
>>> result = process_data({"input": "value"})
144-
>>> # result contains a preview and object ID like "@obj_001"
144+
>>> # result contains a preview and object ID like "@obj_123"
145145
"""
146146

147147
def decorator(func: F) -> F:
@@ -180,7 +180,7 @@ def referenceable(
180180
) -> Callable[[F], F]:
181181
"""Decorator factory that enables parameters to accept object references.
182182
183-
Parameters can accept reference strings like '@obj_001' or '@obj_001.path.to.value'
183+
Parameters can accept reference strings like '@obj_id' or '@obj_id.path.to.value'
184184
which are automatically resolved before calling the function.
185185
186186
:param object_store: The object store instance to use for lookups.
@@ -200,27 +200,25 @@ def referenceable(
200200
>>> process_data({"a": 1, "b": 2}, 10)
201201
>>>
202202
>>> # Call with references
203-
>>> process_data("@obj_001", "@obj_002.config.threshold")
203+
>>> process_data("@obj_123", "@obj_456.config.threshold")
204204
"""
205205

206206
def resolve_reference(ref_str: str) -> Any:
207207
"""Resolve a reference string to its actual value."""
208-
ref = ObjectRef.parse(ref_str)
209-
if ref is None:
210-
raise ValueError(f"Invalid reference format: {ref_str}")
208+
obj_id, path = explorer.parse_reference(ref_str)
211209

212-
obj = object_store.get(ref.obj_id)
210+
obj = object_store.get(obj_id)
213211
if obj is None:
214-
raise ValueError(f"Object @{ref.obj_id} not found or expired")
212+
raise ValueError(f"Object @{obj_id} not found or expired")
215213

216-
if ref.path:
214+
if path:
217215
try:
218-
explorer._validate_path(ref.path)
219-
return glom(obj, explorer._parse_path(ref.path))
216+
explorer._validate_path(path)
217+
return glom(obj, explorer._parse_path(path))
220218
except GlomError as exc:
221-
raise ValueError(f"Navigation error at {ref.path}: {exc}") from exc
219+
raise ValueError(f"Navigation error at {path}: {exc}") from exc
222220
except ValueError as exc:
223-
raise ValueError(f"Invalid path {ref.path}: {exc}") from exc
221+
raise ValueError(f"Invalid path {path}: {exc}") from exc
224222

225223
return obj
226224

@@ -362,7 +360,7 @@ def explorable_and_referenceable(
362360
... return {**data1, **data2}
363361
...
364362
>>> # Accepts references and returns preview with object ID
365-
>>> result = merge_data("@obj_001", {"new": "data"})
363+
>>> result = merge_data("@obj_123", {"new": "data"})
366364
>>> # result contains a preview and can be referenced as "@obj_002"
367365
"""
368366

src/deepset_mcp/tools/tokonomics/explorer.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from rich.console import Console
1919
from rich.pretty import Pretty
2020

21-
from .object_store import ObjectRef, ObjectStore
21+
from deepset_mcp.tools.tokonomics.object_store import ObjectStore
2222

2323

2424
class RichExplorer:
@@ -58,11 +58,26 @@ def __init__(
5858
# Validation pattern for allowed attributes
5959
self.allowed_attr_regex = re.compile(r"[A-Za-z][A-Za-z0-9_]*\Z")
6060

61+
def parse_reference(self, ref_str: str) -> tuple[str, str]:
62+
"""Parse @obj_id.path into (obj_id, path).
63+
64+
:param ref_str: Reference string like @obj_id.path or obj_id
65+
:return: Tuple of (obj_id, path)
66+
"""
67+
if not ref_str.startswith("@"):
68+
return ref_str, "" # Not a reference, return as-is
69+
70+
ref_str = ref_str[1:] # Remove @
71+
if "." in ref_str:
72+
obj_id, path = ref_str.split(".", 1)
73+
return obj_id, path
74+
return ref_str, ""
75+
6176
def explore(self, obj_id: str, path: str = "") -> str:
6277
"""Return a string preview of the requested object.
6378
6479
:param obj_id: Identifier obtained from the store.
65-
:param path: Navigation path using ``.`` or ``[...]`` notation (e.g. ``@obj_001.path.to.attribute``).
80+
:param path: Navigation path using ``.`` or ``[...]`` notation (e.g. ``@obj_id.path.to.attribute``).
6681
:return: String representation of the object.
6782
"""
6883
obj = self._get_object_at_path(obj_id, path)
@@ -187,12 +202,11 @@ def _get_object_at_path(self, obj_id: str, path: str) -> Any:
187202
:param path: Navigation path (optional).
188203
:return: Object at path or error string.
189204
"""
190-
ref = ObjectRef.parse(obj_id)
191-
# We accept @obj_001 as well as obj_001
192-
if ref is None:
193-
resolved_obj_id = obj_id
194-
else:
195-
resolved_obj_id = ref.obj_id
205+
resolved_obj_id, ref_path = self.parse_reference(obj_id)
206+
207+
# If there's a path from the reference, combine it with the provided path
208+
if ref_path:
209+
path = f"{ref_path}.{path}" if path else ref_path
196210

197211
obj = self.store.get(resolved_obj_id)
198212
if obj is None:

src/deepset_mcp/tools/tokonomics/object_store.py

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from __future__ import annotations
2222

2323
import logging
24-
import re
2524
import time
2625
import uuid
2726
from typing import (
@@ -229,42 +228,3 @@ def get(self, obj_id: str) -> Any | None:
229228
def delete(self, obj_id: str) -> bool:
230229
"""Delete object."""
231230
return self._backend.delete(obj_id)
232-
233-
234-
# =============================================================================
235-
# 4 · Object references
236-
# =============================================================================
237-
238-
239-
class ObjectRef:
240-
"""Lightweight parser for reference strings of the form.
241-
242-
Examples::
243-
244-
@obj_042.settings.theme
245-
@obj_123["settings"]["theme"]
246-
"""
247-
248-
_PATTERN = re.compile(r"^@(\w+)(.*)$")
249-
250-
def __init__(self, obj_id: str, path: str = "") -> None:
251-
"""Initialize ObjectRef with object ID and optional path."""
252-
self.obj_id = obj_id
253-
self.path = path
254-
255-
# --------------------------------------------------------------------- #
256-
# Factory
257-
# --------------------------------------------------------------------- #
258-
259-
@classmethod
260-
def parse(cls, ref: str | Any) -> ObjectRef | None:
261-
"""Parse a reference string into an ObjectRef instance."""
262-
if not isinstance(ref, str):
263-
return None
264-
m = cls._PATTERN.match(ref)
265-
if m is None:
266-
return None
267-
obj_id, path = m.group(1), m.group(2) or ""
268-
if path.startswith("."):
269-
path = path[1:]
270-
return cls(obj_id, path)

test/integration/tools/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-FileCopyrightText: 2025-present deepset GmbH <info@deepset.ai>
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-FileCopyrightText: 2025-present deepset GmbH <info@deepset.ai>
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+

0 commit comments

Comments
 (0)