singlestore-langchain-core 0.1.0__tar.gz
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.
- singlestore_langchain_core-0.1.0/PKG-INFO +38 -0
- singlestore_langchain_core-0.1.0/README.md +17 -0
- singlestore_langchain_core-0.1.0/pyproject.toml +58 -0
- singlestore_langchain_core-0.1.0/singlestore_langchain_core/__init__.py +48 -0
- singlestore_langchain_core-0.1.0/singlestore_langchain_core/_connection.py +154 -0
- singlestore_langchain_core-0.1.0/singlestore_langchain_core/_filter.py +247 -0
- singlestore_langchain_core-0.1.0/singlestore_langchain_core/_utils.py +103 -0
- singlestore_langchain_core-0.1.0/singlestore_langchain_core/py.typed +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: singlestore-langchain-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared SingleStore helpers for langchain-singlestore and langgraph-singlestore
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10,<4.0
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: singlestoredb (>=1.16.9,<2.0.0)
|
|
15
|
+
Requires-Dist: sqlalchemy (>=2.0.40,<3.0.0)
|
|
16
|
+
Project-URL: Repository, https://github.com/singlestore-labs/langchain-singlestore
|
|
17
|
+
Project-URL: Release Notes, https://github.com/singlestore-labs/langchain-singlestore/releases
|
|
18
|
+
Project-URL: Source Code, https://github.com/singlestore-labs/langchain-singlestore/tree/main/libs/singlestore-langchain-core
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# singlestore-langchain-core
|
|
22
|
+
|
|
23
|
+
Shared, internal helpers used by the SingleStore integrations for LangChain
|
|
24
|
+
and LangGraph:
|
|
25
|
+
|
|
26
|
+
- Connection attribute helpers (`set_connector_attributes`,
|
|
27
|
+
`compute_connector_version`).
|
|
28
|
+
- SingleStore capability enums (`DistanceStrategy`, `FullTextIndexVersion`,
|
|
29
|
+
`FullTextScoringMode`).
|
|
30
|
+
- Metadata filter DSL (`FilterTypedDict`, `_parse_filter`) shared by vector
|
|
31
|
+
stores and stores.
|
|
32
|
+
|
|
33
|
+
This package is not intended to be depended on directly by application code.
|
|
34
|
+
Install one of the higher-level packages instead:
|
|
35
|
+
|
|
36
|
+
- [`langchain-singlestore`](../langchain-singlestore)
|
|
37
|
+
- [`langgraph-singlestore`](../langgraph-singlestore)
|
|
38
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# singlestore-langchain-core
|
|
2
|
+
|
|
3
|
+
Shared, internal helpers used by the SingleStore integrations for LangChain
|
|
4
|
+
and LangGraph:
|
|
5
|
+
|
|
6
|
+
- Connection attribute helpers (`set_connector_attributes`,
|
|
7
|
+
`compute_connector_version`).
|
|
8
|
+
- SingleStore capability enums (`DistanceStrategy`, `FullTextIndexVersion`,
|
|
9
|
+
`FullTextScoringMode`).
|
|
10
|
+
- Metadata filter DSL (`FilterTypedDict`, `_parse_filter`) shared by vector
|
|
11
|
+
stores and stores.
|
|
12
|
+
|
|
13
|
+
This package is not intended to be depended on directly by application code.
|
|
14
|
+
Install one of the higher-level packages instead:
|
|
15
|
+
|
|
16
|
+
- [`langchain-singlestore`](../langchain-singlestore)
|
|
17
|
+
- [`langgraph-singlestore`](../langgraph-singlestore)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "singlestore-langchain-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Shared SingleStore helpers for langchain-singlestore and langgraph-singlestore"
|
|
9
|
+
authors = []
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
repository = "https://github.com/singlestore-labs/langchain-singlestore"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
packages = [{ include = "singlestore_langchain_core" }]
|
|
14
|
+
|
|
15
|
+
[tool.poetry.urls]
|
|
16
|
+
"Source Code" = "https://github.com/singlestore-labs/langchain-singlestore/tree/main/libs/singlestore-langchain-core"
|
|
17
|
+
"Release Notes" = "https://github.com/singlestore-labs/langchain-singlestore/releases"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.dependencies]
|
|
20
|
+
python = ">=3.10,<4.0"
|
|
21
|
+
singlestoredb = "^1.16.9"
|
|
22
|
+
sqlalchemy = "^2.0.40"
|
|
23
|
+
|
|
24
|
+
[tool.mypy]
|
|
25
|
+
disallow_untyped_defs = "True"
|
|
26
|
+
|
|
27
|
+
[tool.ruff.lint]
|
|
28
|
+
select = ["E", "F", "I", "T201"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
addopts = "--strict-markers --strict-config --durations=5"
|
|
32
|
+
asyncio_mode = "auto"
|
|
33
|
+
|
|
34
|
+
[tool.poetry.group.test]
|
|
35
|
+
optional = true
|
|
36
|
+
|
|
37
|
+
[tool.poetry.group.test_integration]
|
|
38
|
+
optional = true
|
|
39
|
+
|
|
40
|
+
[tool.poetry.group.lint]
|
|
41
|
+
optional = true
|
|
42
|
+
|
|
43
|
+
[tool.poetry.group.typing]
|
|
44
|
+
optional = true
|
|
45
|
+
|
|
46
|
+
[tool.poetry.group.test.dependencies]
|
|
47
|
+
pytest = ">=7.4.3,<10.0.0"
|
|
48
|
+
pytest-asyncio = ">=0.23.2,<2.0.0"
|
|
49
|
+
pytest-socket = "^0.7.0"
|
|
50
|
+
|
|
51
|
+
[tool.poetry.group.test_integration.dependencies]
|
|
52
|
+
docker = "^7.1.0"
|
|
53
|
+
|
|
54
|
+
[tool.poetry.group.lint.dependencies]
|
|
55
|
+
ruff = "^0.5"
|
|
56
|
+
|
|
57
|
+
[tool.poetry.group.typing.dependencies]
|
|
58
|
+
mypy = "^1.10"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Shared SingleStore helpers used by ``langchain-singlestore`` and
|
|
2
|
+
``langgraph-singlestore``.
|
|
3
|
+
|
|
4
|
+
This package contains only SingleStore/SQL primitives with no dependency on
|
|
5
|
+
``langchain-core`` or ``langgraph``. It is intended to be an internal
|
|
6
|
+
implementation detail; direct use by application code is not supported.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib import metadata
|
|
10
|
+
|
|
11
|
+
from singlestore_langchain_core._connection import (
|
|
12
|
+
create_connection_pool,
|
|
13
|
+
)
|
|
14
|
+
from singlestore_langchain_core._filter import (
|
|
15
|
+
FilterTypedDict,
|
|
16
|
+
_get_match_param_function,
|
|
17
|
+
_parse_filter,
|
|
18
|
+
)
|
|
19
|
+
from singlestore_langchain_core._utils import (
|
|
20
|
+
DEFAULT_CONNECTOR_NAME,
|
|
21
|
+
DistanceStrategy,
|
|
22
|
+
FullTextIndexVersion,
|
|
23
|
+
FullTextScoringMode,
|
|
24
|
+
compute_connector_version,
|
|
25
|
+
hash,
|
|
26
|
+
set_connector_attributes,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
__version__ = metadata.version(__package__)
|
|
31
|
+
except metadata.PackageNotFoundError:
|
|
32
|
+
__version__ = ""
|
|
33
|
+
del metadata
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"DEFAULT_CONNECTOR_NAME",
|
|
37
|
+
"DistanceStrategy",
|
|
38
|
+
"FilterTypedDict",
|
|
39
|
+
"FullTextIndexVersion",
|
|
40
|
+
"FullTextScoringMode",
|
|
41
|
+
"_get_match_param_function",
|
|
42
|
+
"_parse_filter",
|
|
43
|
+
"compute_connector_version",
|
|
44
|
+
"hash",
|
|
45
|
+
"set_connector_attributes",
|
|
46
|
+
"create_connection_pool",
|
|
47
|
+
"__version__",
|
|
48
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Connection-pool utilities for SingleStore integrations.
|
|
2
|
+
|
|
3
|
+
Two SQLAlchemy ``Pool`` implementations are provided:
|
|
4
|
+
|
|
5
|
+
* :class:`SingleConnectionPool` — always hands out the same, caller-owned
|
|
6
|
+
connection. Useful when the caller manages the connection lifecycle itself
|
|
7
|
+
(tests, notebooks, a shared long-lived connection).
|
|
8
|
+
* :class:`QueueConnectionPool` — a thin wrapper around
|
|
9
|
+
:class:`sqlalchemy.pool.QueuePool` that lazily opens
|
|
10
|
+
:func:`singlestoredb.connect` connections using a stored kwargs mapping.
|
|
11
|
+
|
|
12
|
+
Use :func:`create_connection_pool` as the single entry point; it picks the
|
|
13
|
+
right implementation based on the arguments the caller supplied.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from singlestoredb.connection import Connection, connect
|
|
19
|
+
from sqlalchemy.pool import Pool, QueuePool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _CallerOwnedConnection:
|
|
23
|
+
"""DBAPI-like proxy over a caller-owned :class:`Connection`.
|
|
24
|
+
|
|
25
|
+
Consumers of a pool follow the ``connect()``/``close()`` idiom, but for
|
|
26
|
+
:class:`SingleConnectionPool` the underlying connection is owned by the
|
|
27
|
+
caller and must outlive the checkout. This proxy forwards every attribute
|
|
28
|
+
to the wrapped connection while making ``close()`` a no-op so subsequent
|
|
29
|
+
checkouts keep working.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__slots__ = ("_connection",)
|
|
33
|
+
|
|
34
|
+
def __init__(self, connection: Connection) -> None:
|
|
35
|
+
object.__setattr__(self, "_connection", connection)
|
|
36
|
+
|
|
37
|
+
def close(self) -> None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def __getattr__(self, name: str) -> Any:
|
|
41
|
+
if name == "_connection":
|
|
42
|
+
raise AttributeError(name)
|
|
43
|
+
return getattr(self._connection, name)
|
|
44
|
+
|
|
45
|
+
def __enter__(self) -> Connection:
|
|
46
|
+
return self._connection
|
|
47
|
+
|
|
48
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SingleConnectionPool(Pool):
|
|
53
|
+
"""Pool that returns the same pre-established connection on every call.
|
|
54
|
+
|
|
55
|
+
The connection is owned by the caller; this class does not open or close
|
|
56
|
+
it. Intended for scenarios where a single, long-lived connection is
|
|
57
|
+
reused across operations. Each :meth:`connect` returns a lightweight
|
|
58
|
+
proxy so callers may follow the standard ``connect()``/``close()``
|
|
59
|
+
idiom without tearing down the shared connection.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, connection: Connection) -> None:
|
|
63
|
+
self._connection = connection
|
|
64
|
+
|
|
65
|
+
def connect(self) -> Any: # type: ignore[override]
|
|
66
|
+
return _CallerOwnedConnection(self._connection)
|
|
67
|
+
|
|
68
|
+
def dispose(self) -> None: # type: ignore[override]
|
|
69
|
+
# Caller owns the underlying connection; nothing to release here.
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class QueueConnectionPool(Pool):
|
|
74
|
+
"""Lazy, size-bounded pool backed by :class:`sqlalchemy.pool.QueuePool`.
|
|
75
|
+
|
|
76
|
+
Connections are opened on demand via :func:`singlestoredb.connect` using
|
|
77
|
+
``connection_kwargs``. See the ``singlestoredb.connect`` documentation for
|
|
78
|
+
the full list of accepted keyword arguments (``host``, ``user``,
|
|
79
|
+
``password``, ``port``, ``database``, TLS options, etc.).
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
pool_size: Number of persistent connections kept in the pool.
|
|
83
|
+
max_overflow: Additional connections that may be opened beyond
|
|
84
|
+
``pool_size`` under load.
|
|
85
|
+
timeout: Seconds to wait for a free connection before raising.
|
|
86
|
+
connection_kwargs: Keyword arguments forwarded to
|
|
87
|
+
:func:`singlestoredb.connect` when a new connection is opened.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
pool_size: int = 5,
|
|
93
|
+
max_overflow: int = 10,
|
|
94
|
+
timeout: float = 30,
|
|
95
|
+
connection_kwargs: Optional[dict] = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
self._pool_size = pool_size
|
|
98
|
+
self._max_overflow = max_overflow
|
|
99
|
+
self._timeout = timeout
|
|
100
|
+
self._connection_kwargs: dict = dict(connection_kwargs or {})
|
|
101
|
+
self._pool = QueuePool(
|
|
102
|
+
self._open_connection,
|
|
103
|
+
pool_size=self._pool_size,
|
|
104
|
+
max_overflow=self._max_overflow,
|
|
105
|
+
timeout=self._timeout,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _open_connection(self) -> Any:
|
|
109
|
+
return connect(**self._connection_kwargs)
|
|
110
|
+
|
|
111
|
+
def connect(self) -> Any: # type: ignore[override]
|
|
112
|
+
return self._pool.connect()
|
|
113
|
+
|
|
114
|
+
def dispose(self) -> None: # type: ignore[override]
|
|
115
|
+
self._pool.dispose()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_connection_pool(
|
|
119
|
+
connection: Optional[Connection] = None,
|
|
120
|
+
connection_pool: Optional[Pool] = None,
|
|
121
|
+
pool_size: int = 5,
|
|
122
|
+
max_overflow: int = 10,
|
|
123
|
+
timeout: float = 30,
|
|
124
|
+
connection_kwargs: Optional[dict] = None,
|
|
125
|
+
) -> Pool:
|
|
126
|
+
"""Return the connection pool that matches the supplied arguments.
|
|
127
|
+
|
|
128
|
+
Dispatch rules (checked in order):
|
|
129
|
+
|
|
130
|
+
1. If both ``connection`` and ``connection_pool`` are given, raise
|
|
131
|
+
:class:`ValueError` — the caller must pick one.
|
|
132
|
+
2. If ``connection`` is given, wrap it in a :class:`SingleConnectionPool`.
|
|
133
|
+
3. If ``connection_pool`` is given, return it unchanged.
|
|
134
|
+
4. Otherwise build a :class:`QueueConnectionPool` from ``pool_size``,
|
|
135
|
+
``max_overflow``, ``timeout`` and ``connection_kwargs``.
|
|
136
|
+
|
|
137
|
+
See :class:`QueueConnectionPool` and :func:`singlestoredb.connect` for the
|
|
138
|
+
meaning of the remaining arguments.
|
|
139
|
+
"""
|
|
140
|
+
if connection is not None and connection_pool is not None:
|
|
141
|
+
raise ValueError("Cannot specify both a connection and a connection pool.")
|
|
142
|
+
|
|
143
|
+
if connection is not None:
|
|
144
|
+
return SingleConnectionPool(connection)
|
|
145
|
+
|
|
146
|
+
if connection_pool is not None:
|
|
147
|
+
return connection_pool
|
|
148
|
+
|
|
149
|
+
return QueueConnectionPool(
|
|
150
|
+
pool_size=pool_size,
|
|
151
|
+
max_overflow=max_overflow,
|
|
152
|
+
timeout=timeout,
|
|
153
|
+
connection_kwargs=connection_kwargs,
|
|
154
|
+
)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter module for VectorStore.
|
|
3
|
+
|
|
4
|
+
This module provides typed filter definitions and utilities to convert
|
|
5
|
+
filter dictionaries into SQL query fragments for metadata filtering.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Dict, List, Literal, Tuple, Union, cast
|
|
12
|
+
|
|
13
|
+
# Type definitions for filter values
|
|
14
|
+
FieldValue = Union[str, int, float, bool]
|
|
15
|
+
NumericFieldValue = Union[int, float]
|
|
16
|
+
|
|
17
|
+
# Simple filter types
|
|
18
|
+
ExactMatchFilter = Dict[str, FieldValue]
|
|
19
|
+
|
|
20
|
+
# Comparison operators
|
|
21
|
+
EqFilter = Dict[Literal["$eq"], FieldValue]
|
|
22
|
+
NeFilter = Dict[Literal["$ne"], FieldValue]
|
|
23
|
+
GtFilter = Dict[Literal["$gt"], NumericFieldValue]
|
|
24
|
+
GteFilter = Dict[Literal["$gte"], NumericFieldValue]
|
|
25
|
+
LtFilter = Dict[Literal["$lt"], NumericFieldValue]
|
|
26
|
+
LteFilter = Dict[Literal["$lte"], NumericFieldValue]
|
|
27
|
+
|
|
28
|
+
# Collection operators
|
|
29
|
+
InFilter = Dict[Literal["$in"], List[FieldValue]]
|
|
30
|
+
NinFilter = Dict[Literal["$nin"], List[FieldValue]]
|
|
31
|
+
|
|
32
|
+
# Existence operator
|
|
33
|
+
ExistsFilter = Dict[Literal["$exists"], bool]
|
|
34
|
+
|
|
35
|
+
# Combined filter types
|
|
36
|
+
FieldFilter = Union[
|
|
37
|
+
EqFilter,
|
|
38
|
+
NeFilter,
|
|
39
|
+
GtFilter,
|
|
40
|
+
GteFilter,
|
|
41
|
+
LtFilter,
|
|
42
|
+
LteFilter,
|
|
43
|
+
InFilter,
|
|
44
|
+
NinFilter,
|
|
45
|
+
ExistsFilter,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
SimpleFilter = Union[
|
|
49
|
+
ExactMatchFilter,
|
|
50
|
+
Dict[str, FieldFilter],
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Logical operators
|
|
54
|
+
AndFilter = Dict[Literal["$and"], List["FilterTypedDict"]]
|
|
55
|
+
OrFilter = Dict[Literal["$or"], List["FilterTypedDict"]]
|
|
56
|
+
|
|
57
|
+
# Overall filter type
|
|
58
|
+
FilterTypedDict = Union[SimpleFilter, AndFilter, OrFilter]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_match_param_function(value: Any) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Determine the appropriate match parameter function based on value type.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
value: The value to match against
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
String representing the SQL match function to use
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If the value type is not supported
|
|
73
|
+
"""
|
|
74
|
+
# Check bool first since bool is a subclass of int
|
|
75
|
+
if isinstance(value, bool):
|
|
76
|
+
return "MATCH_PARAM_BOOL_STRICT()"
|
|
77
|
+
elif isinstance(value, str):
|
|
78
|
+
return "MATCH_PARAM_STRING_STRICT()"
|
|
79
|
+
elif isinstance(value, (int, float)):
|
|
80
|
+
return "MATCH_PARAM_DOUBLE_STRICT()"
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(f"Unsupported value type: {type(value)}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_filter(
|
|
86
|
+
filter_dict: FilterTypedDict, metadata_field: str
|
|
87
|
+
) -> tuple[str, list[Any]]:
|
|
88
|
+
"""
|
|
89
|
+
Parse a filter dictionary into an SQL query fragment and parameters.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
filter_dict: Filter specification following the FilterTypedDict schema
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Tuple containing:
|
|
96
|
+
- SQL query fragment string
|
|
97
|
+
- List of parameter values to be substituted into the query
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If the filter format is invalid
|
|
101
|
+
"""
|
|
102
|
+
if not isinstance(filter_dict, dict):
|
|
103
|
+
raise ValueError("Filter must be a dictionary")
|
|
104
|
+
|
|
105
|
+
if len(filter_dict) != 1:
|
|
106
|
+
raise ValueError("Filter must contain exactly one key")
|
|
107
|
+
|
|
108
|
+
# Handle logical operators
|
|
109
|
+
if "$and" in filter_dict:
|
|
110
|
+
return _handle_and_filter( # type: ignore[arg-type]
|
|
111
|
+
cast(AndFilter, filter_dict), metadata_field
|
|
112
|
+
)
|
|
113
|
+
elif "$or" in filter_dict:
|
|
114
|
+
return _handle_or_filter( # type: ignore[arg-type]
|
|
115
|
+
cast(OrFilter, filter_dict), metadata_field
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
# Handle field filters
|
|
119
|
+
field_name = next(iter(filter_dict))
|
|
120
|
+
field_value = filter_dict[field_name] # type: ignore[index]
|
|
121
|
+
|
|
122
|
+
if isinstance(field_value, dict):
|
|
123
|
+
# Handle operator-based field filter
|
|
124
|
+
return _handle_operator_filter(field_name, field_value, metadata_field)
|
|
125
|
+
else:
|
|
126
|
+
# Handle exact match filter
|
|
127
|
+
match_func = _get_match_param_function(field_value)
|
|
128
|
+
return (
|
|
129
|
+
f"JSON_MATCH_ANY({match_func} = %s, {metadata_field}, %s)",
|
|
130
|
+
[field_value, field_name],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _handle_and_filter(
|
|
135
|
+
filter_dict: AndFilter, metadata_field: str
|
|
136
|
+
) -> tuple[str, list[Any]]:
|
|
137
|
+
"""Handle $and operator filter."""
|
|
138
|
+
sub_filters = filter_dict["$and"] # type: ignore[index]
|
|
139
|
+
if not isinstance(sub_filters, list):
|
|
140
|
+
raise ValueError("$and must be a list of filters")
|
|
141
|
+
|
|
142
|
+
# Process each sub-filter
|
|
143
|
+
parsed_filters = [
|
|
144
|
+
_parse_filter(sub_filter, metadata_field) for sub_filter in sub_filters
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Join conditions with AND
|
|
148
|
+
query = " AND ".join(item[0] for item in parsed_filters)
|
|
149
|
+
|
|
150
|
+
# Flatten parameter lists
|
|
151
|
+
params = [param for parsed_filter in parsed_filters for param in parsed_filter[1]]
|
|
152
|
+
|
|
153
|
+
return query, params
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _handle_or_filter(
|
|
157
|
+
filter_dict: OrFilter, metadata_field: str
|
|
158
|
+
) -> tuple[str, list[Any]]:
|
|
159
|
+
"""Handle $or operator filter."""
|
|
160
|
+
sub_filters = filter_dict["$or"] # type: ignore[index]
|
|
161
|
+
if not isinstance(sub_filters, list):
|
|
162
|
+
raise ValueError("$or must be a list of filters")
|
|
163
|
+
|
|
164
|
+
# Process each sub-filter
|
|
165
|
+
parsed_filters = [
|
|
166
|
+
_parse_filter(sub_filter, metadata_field) for sub_filter in sub_filters
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
# Join conditions with OR and wrap in parentheses
|
|
170
|
+
query = "(" + " OR ".join(item[0] for item in parsed_filters) + ")"
|
|
171
|
+
|
|
172
|
+
# Flatten parameter lists
|
|
173
|
+
params = [param for parsed_filter in parsed_filters for param in parsed_filter[1]]
|
|
174
|
+
|
|
175
|
+
return query, params
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _handle_operator_filter(
|
|
179
|
+
field_name: str, field_filter: Dict, metadata_field: str
|
|
180
|
+
) -> Tuple[str, List[Any]]:
|
|
181
|
+
"""Handle operator-based field filters like $eq, $gt, etc."""
|
|
182
|
+
if len(field_filter) != 1:
|
|
183
|
+
raise ValueError("Field filter must contain exactly one key")
|
|
184
|
+
|
|
185
|
+
operator = next(iter(field_filter))
|
|
186
|
+
field_value = field_filter[operator]
|
|
187
|
+
|
|
188
|
+
# Equal operator
|
|
189
|
+
if operator == "$eq":
|
|
190
|
+
match_func = _get_match_param_function(field_value)
|
|
191
|
+
return (
|
|
192
|
+
f"JSON_MATCH_ANY({match_func} = %s, {metadata_field}, %s)",
|
|
193
|
+
[field_value, field_name],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Not equal operator
|
|
197
|
+
elif operator == "$ne":
|
|
198
|
+
match_func = _get_match_param_function(field_value)
|
|
199
|
+
return (
|
|
200
|
+
f"NOT JSON_MATCH_ANY({match_func} = %s, {metadata_field}, %s) AND "
|
|
201
|
+
f"JSON_MATCH_ANY_EXISTS({metadata_field}, %s)",
|
|
202
|
+
[field_value, field_name, field_name],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Numeric comparison operators
|
|
206
|
+
elif operator in ("$gt", "$gte", "$lt", "$lte"):
|
|
207
|
+
if not isinstance(field_value, (int, float)):
|
|
208
|
+
raise ValueError(f"{operator} must be a numeric value")
|
|
209
|
+
|
|
210
|
+
comparison_op = {"$gt": ">", "$gte": ">=", "$lt": "<", "$lte": "<="}[operator]
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
f"JSON_EXTRACT_DOUBLE({metadata_field}, %s) {comparison_op} %s",
|
|
214
|
+
[field_name, field_value],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Collection operators
|
|
218
|
+
elif operator in ("$in", "$nin"):
|
|
219
|
+
if not isinstance(field_value, list):
|
|
220
|
+
raise ValueError(f"{operator} must be a list")
|
|
221
|
+
|
|
222
|
+
if operator == "$in":
|
|
223
|
+
return (
|
|
224
|
+
f"JSON_MATCH_ANY(JSON_ARRAY_CONTAINS_JSON(%s, MATCH_PARAM_JSON()), "
|
|
225
|
+
f"{metadata_field}, %s)",
|
|
226
|
+
[json.dumps(field_value), field_name],
|
|
227
|
+
)
|
|
228
|
+
else: # $nin
|
|
229
|
+
return (
|
|
230
|
+
f"NOT JSON_MATCH_ANY(JSON_ARRAY_CONTAINS_JSON(%s, MATCH_PARAM_JSON()), "
|
|
231
|
+
f"{metadata_field}, %s) AND "
|
|
232
|
+
f"JSON_MATCH_ANY_EXISTS({metadata_field}, %s)",
|
|
233
|
+
[json.dumps(field_value), field_name, field_name],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Existence operator
|
|
237
|
+
elif operator == "$exists":
|
|
238
|
+
if not isinstance(field_value, bool):
|
|
239
|
+
raise ValueError("$exists must be a boolean")
|
|
240
|
+
|
|
241
|
+
if field_value:
|
|
242
|
+
return f"JSON_MATCH_ANY_EXISTS({metadata_field}, %s)", [field_name]
|
|
243
|
+
else:
|
|
244
|
+
return f"NOT JSON_MATCH_ANY_EXISTS({metadata_field}, %s)", [field_name]
|
|
245
|
+
|
|
246
|
+
else:
|
|
247
|
+
raise ValueError(f"Unsupported operator: {operator}")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""SingleStore connection helpers and shared enums.
|
|
2
|
+
|
|
3
|
+
No LangChain / LangGraph imports live in this module so it can be shared by
|
|
4
|
+
every SingleStore integration package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
DEFAULT_CONNECTOR_NAME = "langchain python sdk"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_connector_version(package_name: str, *, fallback: str = "3.0.0") -> str:
|
|
16
|
+
"""Return the connector version to advertise to SingleStore.
|
|
17
|
+
|
|
18
|
+
Historically the connector version is the package version with ``2`` added
|
|
19
|
+
to the major component (``1.5.0`` -> ``3.5.0``). This preserves the wire
|
|
20
|
+
contract that pre-monorepo releases used.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
pkg_version = version(package_name)
|
|
24
|
+
version_parts = pkg_version.split(".")
|
|
25
|
+
major_version = int(version_parts[0]) + 2
|
|
26
|
+
version_parts[0] = str(major_version)
|
|
27
|
+
return ".".join(version_parts)
|
|
28
|
+
except (PackageNotFoundError, ValueError):
|
|
29
|
+
return fallback
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def set_connector_attributes(
|
|
33
|
+
connection_kwargs: dict,
|
|
34
|
+
*,
|
|
35
|
+
connector_name: str = DEFAULT_CONNECTOR_NAME,
|
|
36
|
+
connector_version: Optional[str] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Stamp connector identity onto ``connection_kwargs['conn_attrs']``.
|
|
39
|
+
|
|
40
|
+
``connector_version`` may be ``None`` when the caller has no versioned
|
|
41
|
+
package to report; the attribute is simply omitted in that case.
|
|
42
|
+
"""
|
|
43
|
+
if "conn_attrs" not in connection_kwargs:
|
|
44
|
+
connection_kwargs["conn_attrs"] = {}
|
|
45
|
+
|
|
46
|
+
connection_kwargs["conn_attrs"]["_connector_name"] = connector_name
|
|
47
|
+
if connector_version is not None:
|
|
48
|
+
connection_kwargs["conn_attrs"]["_connector_version"] = connector_version
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DistanceStrategy(str, Enum):
|
|
52
|
+
"""Distance strategies for calculating similarity between vectors.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
EUCLIDEAN_DISTANCE: Computes the Euclidean (L2) distance between vectors.
|
|
56
|
+
Lower scores indicate more similar vectors. Not compatible with
|
|
57
|
+
WEIGHTED_SUM search strategy.
|
|
58
|
+
DOT_PRODUCT: Computes the dot product (inner product) between vectors.
|
|
59
|
+
Higher scores indicate more similar vectors. This is the default
|
|
60
|
+
and recommended strategy for most embedding models.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
EUCLIDEAN_DISTANCE = "EUCLIDEAN_DISTANCE"
|
|
64
|
+
DOT_PRODUCT = "DOT_PRODUCT"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FullTextIndexVersion(str, Enum):
|
|
68
|
+
"""Full-text index versions supported by SingleStore.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
V1: Original full-text index implementation. Compatible with all
|
|
72
|
+
SingleStore versions that support full-text search. Only supports
|
|
73
|
+
MATCH scoring mode.
|
|
74
|
+
V2: New full-text index implementation available in SingleStore 8.7+.
|
|
75
|
+
Offers improved performance and supports additional scoring modes
|
|
76
|
+
(BM25, BM25_GLOBAL).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
V1 = "V1"
|
|
80
|
+
V2 = "V2"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FullTextScoringMode(str, Enum):
|
|
84
|
+
"""Scoring algorithms for full-text search ranking.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
MATCH: Uses SingleStore's native MATCH() AGAINST() function.
|
|
88
|
+
Compatible with both V1 and V2 full-text indexes.
|
|
89
|
+
BM25: Best Matching 25 algorithm with TF-IDF scoring and document
|
|
90
|
+
length normalization. Requires V2 full-text index.
|
|
91
|
+
BM25_GLOBAL: BM25 with global IDF statistics across all partitions.
|
|
92
|
+
Provides consistent scoring in distributed environments.
|
|
93
|
+
Requires V2 full-text index.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
MATCH = "MATCH"
|
|
97
|
+
BM25 = "BM25"
|
|
98
|
+
BM25_GLOBAL = "BM25_GLOBAL"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def hash(_input: str) -> str:
|
|
102
|
+
"""Use a deterministic hashing approach."""
|
|
103
|
+
return hashlib.md5(_input.encode()).hexdigest()
|
|
File without changes
|