lightrag-memgraph 0.3.0__tar.gz → 0.3.2__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.
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/PKG-INFO +10 -5
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/README.md +7 -1
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/pyproject.toml +10 -12
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/src/lightrag_memgraph/__init__.py +9 -3
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/src/lightrag_memgraph/_connection.py +34 -16
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/src/lightrag_memgraph/core.py +32 -2
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/src/lightrag_memgraph/docstatus_impl.py +53 -33
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/src/lightrag_memgraph/kv_impl.py +23 -15
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/src/lightrag_memgraph/vector_impl.py +45 -34
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/tests/test_core.py +48 -1
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/tests/test_integration_memgraph.py +8 -4
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/.gitignore +0 -0
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/LICENSE +0 -0
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/example.py +0 -0
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/src/lightrag_memgraph/embeddings.py +0 -0
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/src/lightrag_memgraph/registry.py +0 -0
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/tests/__init__.py +0 -0
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/tests/test_embeddings.py +0 -0
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/tests/test_import.py +0 -0
- {lightrag_memgraph-0.3.0 → lightrag_memgraph-0.3.2}/tests/test_storages.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: lightrag-memgraph
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: LightRAG integration with Memgraph
|
|
5
5
|
Project-URL: Homepage, https://github.com/memgraph/ai-toolkit
|
|
6
6
|
Project-URL: Source, https://github.com/memgraph/ai-toolkit/tree/main/integrations/lightrag-memgraph
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: anthropic>=0.40.0
|
|
21
|
-
Requires-Dist: lightrag-hku
|
|
21
|
+
Requires-Dist: lightrag-hku==1.5.4
|
|
22
22
|
Requires-Dist: memgraph-toolbox>=0.1.11
|
|
23
23
|
Requires-Dist: numpy>=1.22.0
|
|
24
24
|
Requires-Dist: openai>=1.0.0
|
|
@@ -26,7 +26,6 @@ Requires-Dist: voyageai>=0.2.0
|
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: black>=26.3.1; extra == 'dev'
|
|
28
28
|
Requires-Dist: isort>=5.0.0; extra == 'dev'
|
|
29
|
-
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
30
29
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
31
30
|
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
32
31
|
Description-Content-Type: text/markdown
|
|
@@ -85,6 +84,12 @@ asyncio.run(main())
|
|
|
85
84
|
|
|
86
85
|
See `example.py` in this repo for a full run with sample texts and graph output.
|
|
87
86
|
|
|
87
|
+
`initialize(**lightrag_kwargs)` forwards anything to `LightRAG(...)`, and explicit
|
|
88
|
+
values override the Memgraph-storage defaults — e.g. `max_parallel_insert=8`, or
|
|
89
|
+
`addon_params={"entity_types_guidance": "..."}` to steer entity extraction toward a
|
|
90
|
+
specific vocabulary. [unstructured2graph](../../unstructured2graph/README.md#entity-typing--ontology)
|
|
91
|
+
produces that `addon_params` from an ontology file via `Ontology.addon_params()`.
|
|
92
|
+
|
|
88
93
|
## Storage / persistence
|
|
89
94
|
|
|
90
95
|
By default the wrapper persists **LightRAG's entire working state** into
|
|
@@ -184,7 +189,7 @@ https://platform.claude.com/docs/en/about-claude/models.
|
|
|
184
189
|
await wrapper.initialize(
|
|
185
190
|
working_dir="./lightrag_storage",
|
|
186
191
|
llm_model_func=anthropic_complete,
|
|
187
|
-
llm_model_name="claude-
|
|
192
|
+
llm_model_name="claude-sonnet-4-20250514", # any current Claude model id
|
|
188
193
|
# embedding_func omitted: defaults to Memgraph's local sentence-transformer,
|
|
189
194
|
# see Embeddings above. Anthropic itself has no embeddings API.
|
|
190
195
|
)
|
|
@@ -52,6 +52,12 @@ asyncio.run(main())
|
|
|
52
52
|
|
|
53
53
|
See `example.py` in this repo for a full run with sample texts and graph output.
|
|
54
54
|
|
|
55
|
+
`initialize(**lightrag_kwargs)` forwards anything to `LightRAG(...)`, and explicit
|
|
56
|
+
values override the Memgraph-storage defaults — e.g. `max_parallel_insert=8`, or
|
|
57
|
+
`addon_params={"entity_types_guidance": "..."}` to steer entity extraction toward a
|
|
58
|
+
specific vocabulary. [unstructured2graph](../../unstructured2graph/README.md#entity-typing--ontology)
|
|
59
|
+
produces that `addon_params` from an ontology file via `Ontology.addon_params()`.
|
|
60
|
+
|
|
55
61
|
## Storage / persistence
|
|
56
62
|
|
|
57
63
|
By default the wrapper persists **LightRAG's entire working state** into
|
|
@@ -151,7 +157,7 @@ https://platform.claude.com/docs/en/about-claude/models.
|
|
|
151
157
|
await wrapper.initialize(
|
|
152
158
|
working_dir="./lightrag_storage",
|
|
153
159
|
llm_model_func=anthropic_complete,
|
|
154
|
-
llm_model_name="claude-
|
|
160
|
+
llm_model_name="claude-sonnet-4-20250514", # any current Claude model id
|
|
155
161
|
# embedding_func omitted: defaults to Memgraph's local sentence-transformer,
|
|
156
162
|
# see Embeddings above. Anthropic itself has no embeddings API.
|
|
157
163
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lightrag-memgraph"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "LightRAG integration with Memgraph"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -22,10 +22,15 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
dependencies = [
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
25
|
+
# Pinned to exactly 1.5.4: 1.5.5 (2026-07-31) added new @abstractmethod
|
|
26
|
+
# requirements to DocStatusStorage (a "memory-bounded scheduling" API --
|
|
27
|
+
# get_docs_by_statuses_page/get_docs_by_ids/get_full_docs_by_ids/
|
|
28
|
+
# resolve_doc_source_strict) that MemgraphDocStatusStorage doesn't
|
|
29
|
+
# implement yet, so LightRAG() construction raises TypeError. See #250.
|
|
30
|
+
# Also still capped below 1.6 for the separate reason registry.py's
|
|
31
|
+
# compat guard documents (lightrag.kg internals this integration
|
|
32
|
+
# monkey-patches). Bump only after implementing the new interface.
|
|
33
|
+
"lightrag-hku==1.5.4",
|
|
29
34
|
"memgraph-toolbox>=0.1.11",
|
|
30
35
|
"numpy>=1.22.0",
|
|
31
36
|
"anthropic>=0.40.0",
|
|
@@ -39,7 +44,6 @@ dev = [
|
|
|
39
44
|
"pytest-asyncio>=0.21.0",
|
|
40
45
|
"black>=26.3.1",
|
|
41
46
|
"isort>=5.0.0",
|
|
42
|
-
"mypy>=1.0.0",
|
|
43
47
|
]
|
|
44
48
|
|
|
45
49
|
[project.urls]
|
|
@@ -66,9 +70,3 @@ target-version = ['py310']
|
|
|
66
70
|
[tool.isort]
|
|
67
71
|
profile = "black"
|
|
68
72
|
line_length = 88
|
|
69
|
-
|
|
70
|
-
[tool.mypy]
|
|
71
|
-
python_version = "3.10"
|
|
72
|
-
warn_return_any = true
|
|
73
|
-
warn_unused_configs = true
|
|
74
|
-
disallow_untyped_defs = true
|
|
@@ -25,7 +25,7 @@ def _patch_anthropic() -> None:
|
|
|
25
25
|
import os
|
|
26
26
|
from collections.abc import AsyncIterator
|
|
27
27
|
from importlib.metadata import version as get_version
|
|
28
|
-
from typing import Any
|
|
28
|
+
from typing import Any, cast
|
|
29
29
|
|
|
30
30
|
import lightrag.llm.anthropic as _mod
|
|
31
31
|
from anthropic import (
|
|
@@ -121,8 +121,14 @@ def _patch_anthropic() -> None:
|
|
|
121
121
|
parts.append(content)
|
|
122
122
|
return "".join(parts)
|
|
123
123
|
|
|
124
|
-
_wrapped
|
|
125
|
-
|
|
124
|
+
# _wrapped is a plain function object; cast(..., x) is the identity function at runtime,
|
|
125
|
+
# so this sets/reads the real attribute -- it's just how we spell "let me stash a marker
|
|
126
|
+
# attribute on a function" past a type checker that doesn't model dynamic attributes on
|
|
127
|
+
# function objects. Likewise, _wrapped's signature (no image_inputs=) is intentionally
|
|
128
|
+
# narrower than anthropic_complete_if_cache's -- it's real dynamic monkey-patching of a
|
|
129
|
+
# third-party module attribute, not something a structural type can express.
|
|
130
|
+
cast("Any", _wrapped)._lightrag_memgraph_patched = True
|
|
131
|
+
_mod.anthropic_complete_if_cache = cast("Any", _wrapped)
|
|
126
132
|
except Exception:
|
|
127
133
|
pass
|
|
128
134
|
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import os
|
|
15
15
|
import re
|
|
16
|
-
from typing import TYPE_CHECKING
|
|
16
|
+
from typing import TYPE_CHECKING, cast
|
|
17
17
|
|
|
18
18
|
from lightrag.utils import logger
|
|
19
19
|
|
|
@@ -21,6 +21,7 @@ from memgraph_toolbox.api.memgraph import AsyncMemgraph, memgraph_env
|
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from neo4j import AsyncDriver, AsyncSession
|
|
24
|
+
from typing_extensions import LiteralString
|
|
24
25
|
|
|
25
26
|
# Keyed by event-loop id so tests spinning up a fresh loop per test don't
|
|
26
27
|
# reuse a driver bound to a closed loop.
|
|
@@ -71,6 +72,19 @@ def sanitize_index_name(value: str) -> str:
|
|
|
71
72
|
return re.sub(r"[^A-Za-z0-9_]", "_", value)
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
def as_literal_query(text: str) -> LiteralString:
|
|
76
|
+
"""Assert that a dynamically-built Cypher string is safe to pass to ``AsyncSession.run``.
|
|
77
|
+
|
|
78
|
+
neo4j types ``run(query=...)`` as ``LiteralString | Query`` to steer callers away from
|
|
79
|
+
interpolating untrusted input into query text -- ``LiteralString`` can only be produced
|
|
80
|
+
from literal syntax, so the type checker can't see that a runtime f-string is safe. Every
|
|
81
|
+
query built in this package only interpolates our own backtick-escaped identifiers
|
|
82
|
+
(``sanitize_label``/``sanitize_index_name``) or passes values as bound parameters (``$id``
|
|
83
|
+
etc.), never raw external input concatenated into query text, so the cast is safe here.
|
|
84
|
+
"""
|
|
85
|
+
return cast("LiteralString", text)
|
|
86
|
+
|
|
87
|
+
|
|
74
88
|
def resolve_workspace(workspace: str | None) -> str:
|
|
75
89
|
"""Resolve the effective workspace for a storage instance.
|
|
76
90
|
|
|
@@ -84,7 +98,7 @@ def resolve_workspace(workspace: str | None) -> str:
|
|
|
84
98
|
async def create_index(session: AsyncSession, *, label: str, prop: str, workspace: str) -> None:
|
|
85
99
|
"""CREATE INDEX on :`label`(prop), swallowing "already exists" (Memgraph has no IF NOT EXISTS)."""
|
|
86
100
|
try:
|
|
87
|
-
await (await session.run(f"CREATE INDEX ON :`{label}`({prop})")).consume()
|
|
101
|
+
await (await session.run(as_literal_query(f"CREATE INDEX ON :`{label}`({prop})"))).consume()
|
|
88
102
|
except Exception as e: # index may already exist, which is not an error
|
|
89
103
|
logger.warning(f"[{workspace}] Index creation on :`{label}`({prop}) may have failed or already exists: {e}")
|
|
90
104
|
|
|
@@ -106,7 +120,7 @@ class MemgraphCrudMixin:
|
|
|
106
120
|
async def is_empty(self) -> bool:
|
|
107
121
|
driver = await get_driver()
|
|
108
122
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
109
|
-
result = await session.run(f"MATCH (n:`{self._label}`) RETURN n LIMIT 1")
|
|
123
|
+
result = await session.run(as_literal_query(f"MATCH (n:`{self._label}`) RETURN n LIMIT 1"))
|
|
110
124
|
record = await result.single()
|
|
111
125
|
await result.consume()
|
|
112
126
|
return record is None
|
|
@@ -118,13 +132,15 @@ class MemgraphCrudMixin:
|
|
|
118
132
|
driver = await get_driver()
|
|
119
133
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
120
134
|
result = await session.run(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
as_literal_query(
|
|
136
|
+
f"""
|
|
137
|
+
UNWIND $keys AS k
|
|
138
|
+
OPTIONAL MATCH (n:`{self._label}` {{id: k}})
|
|
139
|
+
WITH k, n
|
|
140
|
+
WHERE n IS NULL
|
|
141
|
+
RETURN k
|
|
142
|
+
"""
|
|
143
|
+
),
|
|
128
144
|
keys=list(keys),
|
|
129
145
|
)
|
|
130
146
|
missing = {record["k"] async for record in result}
|
|
@@ -138,11 +154,13 @@ class MemgraphCrudMixin:
|
|
|
138
154
|
async with driver.session(database=get_database()) as session:
|
|
139
155
|
await (
|
|
140
156
|
await session.run(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
157
|
+
as_literal_query(
|
|
158
|
+
f"""
|
|
159
|
+
UNWIND $ids AS target_id
|
|
160
|
+
MATCH (n:`{self._label}` {{id: target_id}})
|
|
161
|
+
DETACH DELETE n
|
|
162
|
+
"""
|
|
163
|
+
),
|
|
146
164
|
ids=list(ids),
|
|
147
165
|
)
|
|
148
166
|
).consume()
|
|
@@ -151,7 +169,7 @@ class MemgraphCrudMixin:
|
|
|
151
169
|
try:
|
|
152
170
|
driver = await get_driver()
|
|
153
171
|
async with driver.session(database=get_database()) as session:
|
|
154
|
-
await (await session.run(f"MATCH (n:`{self._label}`) DETACH DELETE n")).consume()
|
|
172
|
+
await (await session.run(as_literal_query(f"MATCH (n:`{self._label}`) DETACH DELETE n"))).consume()
|
|
155
173
|
logger.info(f"[{self.workspace}] Dropped Memgraph storage for {self.namespace}")
|
|
156
174
|
return {"status": "success", "message": "data dropped"}
|
|
157
175
|
except Exception as e:
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
from lightrag import LightRAG
|
|
5
|
-
from lightrag.kg.shared_storage import initialize_pipeline_status
|
|
5
|
+
from lightrag.kg.shared_storage import finalize_share_data, initialize_pipeline_status
|
|
6
6
|
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
|
|
7
7
|
from lightrag.utils import logger, setup_logger
|
|
8
8
|
|
|
@@ -101,6 +101,20 @@ class MemgraphLightRAGWrapper:
|
|
|
101
101
|
raise RuntimeError("LightRAG not initialized. Call initialize() first.")
|
|
102
102
|
return self.rag
|
|
103
103
|
|
|
104
|
+
@property
|
|
105
|
+
def workspace(self) -> str:
|
|
106
|
+
"""The Memgraph label LightRAG writes every extracted entity under.
|
|
107
|
+
|
|
108
|
+
Exposed directly (rather than making callers reach through
|
|
109
|
+
get_lightrag().chunk_entity_relation_graph) since it's the one piece
|
|
110
|
+
of LightRAG's internal state consumers outside this package actually
|
|
111
|
+
need -- e.g. unstructured2graph's LightRAGBackend.workspace_label.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
RuntimeError: if initialize() hasn't been called yet.
|
|
115
|
+
"""
|
|
116
|
+
return self.get_lightrag().chunk_entity_relation_graph.workspace
|
|
117
|
+
|
|
104
118
|
# https://github.com/HKUDS/LightRAG/blob/main/lightrag/lightrag.py
|
|
105
119
|
async def ainsert(self, **kwargs) -> None:
|
|
106
120
|
"""
|
|
@@ -114,6 +128,22 @@ class MemgraphLightRAGWrapper:
|
|
|
114
128
|
await self.rag.ainsert(**kwargs)
|
|
115
129
|
|
|
116
130
|
async def afinalize(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Finalize storages and reset LightRAG's process-global shared-storage
|
|
133
|
+
state (locks, shared dicts). Without the latter reset, a second
|
|
134
|
+
LightRAG instance created later in the same process reuses asyncio
|
|
135
|
+
locks bound to this instance's (by-then-closed) event loop and raises
|
|
136
|
+
"bound to a different event loop" -- e.g. two real-LightRAG tests
|
|
137
|
+
run back-to-back under pytest-asyncio's function-scoped event loop.
|
|
138
|
+
|
|
139
|
+
The reset runs in a finally block: if finalize_storages() raises
|
|
140
|
+
(e.g. a transient network error), the shared-storage state would
|
|
141
|
+
otherwise stay registered as initialized and break every subsequent
|
|
142
|
+
LightRAG instance in this process, not just this one's cleanup.
|
|
143
|
+
"""
|
|
117
144
|
if self.rag is None:
|
|
118
145
|
raise RuntimeError("LightRAG not initialized. Call initialize() first.")
|
|
119
|
-
|
|
146
|
+
try:
|
|
147
|
+
await self.rag.finalize_storages()
|
|
148
|
+
finally:
|
|
149
|
+
finalize_share_data()
|
|
@@ -11,13 +11,14 @@ from __future__ import annotations
|
|
|
11
11
|
import asyncio
|
|
12
12
|
import json
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
-
from typing import Any, final
|
|
14
|
+
from typing import Any, cast, final
|
|
15
15
|
|
|
16
16
|
from lightrag.base import DocProcessingStatus, DocStatus, DocStatusStorage
|
|
17
17
|
from lightrag.utils import logger
|
|
18
18
|
|
|
19
19
|
from ._connection import (
|
|
20
20
|
MemgraphCrudMixin,
|
|
21
|
+
as_literal_query,
|
|
21
22
|
close_driver,
|
|
22
23
|
create_index,
|
|
23
24
|
get_database,
|
|
@@ -74,7 +75,7 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
74
75
|
driver = await get_driver()
|
|
75
76
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
76
77
|
result = await session.run(
|
|
77
|
-
f"MATCH (n:`{self._label}` {{id: $id}}) RETURN n.data AS data",
|
|
78
|
+
as_literal_query(f"MATCH (n:`{self._label}` {{id: $id}}) RETURN n.data AS data"),
|
|
78
79
|
id=id,
|
|
79
80
|
)
|
|
80
81
|
record = await result.single()
|
|
@@ -90,11 +91,13 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
90
91
|
driver = await get_driver()
|
|
91
92
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
92
93
|
result = await session.run(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
as_literal_query(
|
|
95
|
+
f"""
|
|
96
|
+
UNWIND $ids AS target_id
|
|
97
|
+
OPTIONAL MATCH (n:`{self._label}` {{id: target_id}})
|
|
98
|
+
RETURN target_id AS id, n.data AS data
|
|
99
|
+
"""
|
|
100
|
+
),
|
|
98
101
|
ids=list(ids),
|
|
99
102
|
)
|
|
100
103
|
found: dict[str, str] = {}
|
|
@@ -102,7 +105,10 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
102
105
|
if record["data"] is not None:
|
|
103
106
|
found[record["id"]] = record["data"]
|
|
104
107
|
await result.consume()
|
|
105
|
-
|
|
108
|
+
# LightRAG's own BaseKVStorage.get_by_ids declares -> list[dict[str, Any]], but its
|
|
109
|
+
# reference JsonKVStorage impl (and this one, to match) legitimately puts None in the
|
|
110
|
+
# slot of any id that does not exist -- the upstream annotation itself is imprecise.
|
|
111
|
+
return cast("list[dict[str, Any]]", [json.loads(found[i]) if i in found else None for i in ids])
|
|
106
112
|
|
|
107
113
|
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
|
|
108
114
|
if not data:
|
|
@@ -123,11 +129,13 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
123
129
|
async with driver.session(database=get_database()) as session:
|
|
124
130
|
await (
|
|
125
131
|
await session.run(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
as_literal_query(
|
|
133
|
+
f"""
|
|
134
|
+
UNWIND $entries AS e
|
|
135
|
+
MERGE (n:`{self._label}` {{id: e.id}})
|
|
136
|
+
SET n.data = e.data, n += e.indexed
|
|
137
|
+
"""
|
|
138
|
+
),
|
|
131
139
|
entries=entries,
|
|
132
140
|
)
|
|
133
141
|
).consume()
|
|
@@ -141,7 +149,9 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
141
149
|
counts = {status.value: 0 for status in DocStatus}
|
|
142
150
|
driver = await get_driver()
|
|
143
151
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
144
|
-
result = await session.run(
|
|
152
|
+
result = await session.run(
|
|
153
|
+
as_literal_query(f"MATCH (n:`{self._label}`) RETURN n.status AS status, count(n) AS cnt")
|
|
154
|
+
)
|
|
145
155
|
async for record in result:
|
|
146
156
|
if record["status"] in counts:
|
|
147
157
|
counts[record["status"]] = record["cnt"]
|
|
@@ -164,11 +174,13 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
164
174
|
driver = await get_driver()
|
|
165
175
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
166
176
|
result = await session.run(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
as_literal_query(
|
|
178
|
+
f"""
|
|
179
|
+
MATCH (n:`{self._label}`)
|
|
180
|
+
WHERE n.status IN $statuses
|
|
181
|
+
RETURN n.id AS id, n.data AS data
|
|
182
|
+
"""
|
|
183
|
+
),
|
|
172
184
|
statuses=status_values,
|
|
173
185
|
)
|
|
174
186
|
records = [record async for record in result]
|
|
@@ -179,10 +191,12 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
179
191
|
driver = await get_driver()
|
|
180
192
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
181
193
|
result = await session.run(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
as_literal_query(
|
|
195
|
+
f"""
|
|
196
|
+
MATCH (n:`{self._label}` {{track_id: $track_id}})
|
|
197
|
+
RETURN n.id AS id, n.data AS data
|
|
198
|
+
"""
|
|
199
|
+
),
|
|
186
200
|
track_id=track_id,
|
|
187
201
|
)
|
|
188
202
|
records = [record async for record in result]
|
|
@@ -237,7 +251,7 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
237
251
|
async def _count() -> int:
|
|
238
252
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
239
253
|
result = await session.run(
|
|
240
|
-
f"MATCH (n:`{self._label}`) {where} RETURN count(n) AS total",
|
|
254
|
+
as_literal_query(f"MATCH (n:`{self._label}`) {where} RETURN count(n) AS total"),
|
|
241
255
|
**filter_params,
|
|
242
256
|
)
|
|
243
257
|
record = await result.single()
|
|
@@ -247,12 +261,14 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
247
261
|
async def _page() -> list:
|
|
248
262
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
249
263
|
result = await session.run(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
264
|
+
as_literal_query(
|
|
265
|
+
f"""
|
|
266
|
+
MATCH (n:`{self._label}`) {where}
|
|
267
|
+
RETURN n.id AS id, n.data AS data
|
|
268
|
+
ORDER BY {sort_expr} {order}
|
|
269
|
+
SKIP $skip LIMIT $limit
|
|
270
|
+
"""
|
|
271
|
+
),
|
|
256
272
|
**params,
|
|
257
273
|
)
|
|
258
274
|
records = [record async for record in result]
|
|
@@ -277,7 +293,7 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
277
293
|
driver = await get_driver()
|
|
278
294
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
279
295
|
result = await session.run(
|
|
280
|
-
f"MATCH (n:`{self._label}` {{file_path: $file_path}}) RETURN n.data AS data LIMIT 1",
|
|
296
|
+
as_literal_query(f"MATCH (n:`{self._label}` {{file_path: $file_path}}) RETURN n.data AS data LIMIT 1"),
|
|
281
297
|
file_path=file_path,
|
|
282
298
|
)
|
|
283
299
|
record = await result.single()
|
|
@@ -293,7 +309,9 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
293
309
|
driver = await get_driver()
|
|
294
310
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
295
311
|
result = await session.run(
|
|
296
|
-
|
|
312
|
+
as_literal_query(
|
|
313
|
+
f"MATCH (n:`{self._label}` {{file_path: $basename}}) RETURN n.id AS id, n.data AS data LIMIT 1"
|
|
314
|
+
),
|
|
297
315
|
basename=basename,
|
|
298
316
|
)
|
|
299
317
|
record = await result.single()
|
|
@@ -309,7 +327,9 @@ class MemgraphDocStatusStorage(MemgraphCrudMixin, DocStatusStorage):
|
|
|
309
327
|
driver = await get_driver()
|
|
310
328
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
311
329
|
result = await session.run(
|
|
312
|
-
|
|
330
|
+
as_literal_query(
|
|
331
|
+
f"MATCH (n:`{self._label}` {{content_hash: $content_hash}}) RETURN n.id AS id, n.data AS data LIMIT 1"
|
|
332
|
+
),
|
|
313
333
|
content_hash=content_hash,
|
|
314
334
|
)
|
|
315
335
|
record = await result.single()
|
|
@@ -11,13 +11,14 @@ from __future__ import annotations
|
|
|
11
11
|
import json
|
|
12
12
|
import time
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
-
from typing import Any, final
|
|
14
|
+
from typing import Any, cast, final
|
|
15
15
|
|
|
16
16
|
from lightrag.base import BaseKVStorage
|
|
17
17
|
from lightrag.utils import logger
|
|
18
18
|
|
|
19
19
|
from ._connection import (
|
|
20
20
|
MemgraphCrudMixin,
|
|
21
|
+
as_literal_query,
|
|
21
22
|
close_driver,
|
|
22
23
|
create_index,
|
|
23
24
|
get_database,
|
|
@@ -66,7 +67,7 @@ class MemgraphKVStorage(MemgraphCrudMixin, BaseKVStorage):
|
|
|
66
67
|
driver = await get_driver()
|
|
67
68
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
68
69
|
result = await session.run(
|
|
69
|
-
f"MATCH (n:`{self._label}` {{id: $id}}) RETURN n.data AS data",
|
|
70
|
+
as_literal_query(f"MATCH (n:`{self._label}` {{id: $id}}) RETURN n.data AS data"),
|
|
70
71
|
id=id,
|
|
71
72
|
)
|
|
72
73
|
record = await result.single()
|
|
@@ -80,11 +81,13 @@ class MemgraphKVStorage(MemgraphCrudMixin, BaseKVStorage):
|
|
|
80
81
|
driver = await get_driver()
|
|
81
82
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
82
83
|
result = await session.run(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
as_literal_query(
|
|
85
|
+
f"""
|
|
86
|
+
UNWIND $ids AS target_id
|
|
87
|
+
OPTIONAL MATCH (n:`{self._label}` {{id: target_id}})
|
|
88
|
+
RETURN target_id AS id, n.data AS data
|
|
89
|
+
"""
|
|
90
|
+
),
|
|
88
91
|
ids=list(ids),
|
|
89
92
|
)
|
|
90
93
|
found: dict[str, str] = {}
|
|
@@ -92,7 +95,10 @@ class MemgraphKVStorage(MemgraphCrudMixin, BaseKVStorage):
|
|
|
92
95
|
if record["data"] is not None:
|
|
93
96
|
found[record["id"]] = record["data"]
|
|
94
97
|
await result.consume()
|
|
95
|
-
|
|
98
|
+
# LightRAG's own BaseKVStorage.get_by_ids declares -> list[dict[str, Any]], but its
|
|
99
|
+
# reference JsonKVStorage impl (and this one, to match) legitimately puts None in the
|
|
100
|
+
# slot of any id that doesn't exist -- the upstream annotation itself is imprecise.
|
|
101
|
+
return cast("list[dict[str, Any]]", [self._decode(i, found.get(i)) for i in ids])
|
|
96
102
|
|
|
97
103
|
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
|
|
98
104
|
if not data:
|
|
@@ -126,13 +132,15 @@ class MemgraphKVStorage(MemgraphCrudMixin, BaseKVStorage):
|
|
|
126
132
|
async with driver.session(database=get_database()) as session:
|
|
127
133
|
await (
|
|
128
134
|
await session.run(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
as_literal_query(
|
|
136
|
+
f"""
|
|
137
|
+
UNWIND $entries AS e
|
|
138
|
+
MERGE (n:`{self._label}` {{id: e.id}})
|
|
139
|
+
ON CREATE SET n.data = e.new_data, n.created_at = e.ts
|
|
140
|
+
ON MATCH SET n.data = e.update_data
|
|
141
|
+
SET n.updated_at = e.ts
|
|
142
|
+
"""
|
|
143
|
+
),
|
|
136
144
|
entries=entries,
|
|
137
145
|
)
|
|
138
146
|
).consume()
|
|
@@ -19,6 +19,7 @@ from lightrag.base import BaseVectorStorage
|
|
|
19
19
|
from lightrag.utils import compute_mdhash_id, logger
|
|
20
20
|
|
|
21
21
|
from ._connection import (
|
|
22
|
+
as_literal_query,
|
|
22
23
|
close_driver,
|
|
23
24
|
create_index,
|
|
24
25
|
get_database,
|
|
@@ -130,18 +131,20 @@ class MemgraphVectorStorage(BaseVectorStorage):
|
|
|
130
131
|
await self._ensure_vector_index(session)
|
|
131
132
|
await (
|
|
132
133
|
await session.run(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
as_literal_query(
|
|
135
|
+
f"""
|
|
136
|
+
UNWIND $entries AS e
|
|
137
|
+
MERGE (n:`{self._label}` {{id: e.id}})
|
|
138
|
+
ON CREATE SET n.created_at = e.ts
|
|
139
|
+
SET n += e.props, n.embedding = e.embedding, n.updated_at = e.ts
|
|
140
|
+
"""
|
|
141
|
+
),
|
|
139
142
|
entries=entries,
|
|
140
143
|
)
|
|
141
144
|
).consume()
|
|
142
145
|
logger.debug(f"[{self.workspace}] Upserted {len(entries)} vectors to {self.namespace}")
|
|
143
146
|
|
|
144
|
-
async def query(self, query: str, top_k: int, query_embedding: list[float] = None) -> list[dict[str, Any]]:
|
|
147
|
+
async def query(self, query: str, top_k: int, query_embedding: list[float] | None = None) -> list[dict[str, Any]]:
|
|
145
148
|
"""Vector-search this namespace and return up to ``top_k`` live results.
|
|
146
149
|
|
|
147
150
|
By design, Memgraph's native vector index operates at ``READ_UNCOMMITTED``
|
|
@@ -176,15 +179,17 @@ class MemgraphVectorStorage(BaseVectorStorage):
|
|
|
176
179
|
# dropping it.
|
|
177
180
|
# Index name is a sanitized string literal (not accepted as a query param).
|
|
178
181
|
result = await session.run(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
as_literal_query(
|
|
183
|
+
f"""
|
|
184
|
+
CALL vector_search.search("{self._index_name}", $top_k, $embedding)
|
|
185
|
+
YIELD node, similarity
|
|
186
|
+
WITH collect({{node: node, similarity: similarity}}) AS cand_pairs
|
|
187
|
+
UNWIND cand_pairs AS cp
|
|
188
|
+
OPTIONAL MATCH (live:`{self._label}`) WHERE live = cp.node
|
|
189
|
+
RETURN live.id AS id, cp.similarity AS similarity,
|
|
190
|
+
CASE WHEN live IS NULL THEN NULL ELSE properties(live) END AS props
|
|
191
|
+
"""
|
|
192
|
+
),
|
|
188
193
|
top_k=int(top_k),
|
|
189
194
|
embedding=embedding,
|
|
190
195
|
)
|
|
@@ -225,7 +230,7 @@ class MemgraphVectorStorage(BaseVectorStorage):
|
|
|
225
230
|
driver = await get_driver()
|
|
226
231
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
227
232
|
result = await session.run(
|
|
228
|
-
f"MATCH (n:`{self._label}` {{id: $id}}) RETURN properties(n) AS props",
|
|
233
|
+
as_literal_query(f"MATCH (n:`{self._label}` {{id: $id}}) RETURN properties(n) AS props"),
|
|
229
234
|
id=id,
|
|
230
235
|
)
|
|
231
236
|
record = await result.single()
|
|
@@ -243,11 +248,13 @@ class MemgraphVectorStorage(BaseVectorStorage):
|
|
|
243
248
|
driver = await get_driver()
|
|
244
249
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
245
250
|
result = await session.run(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
as_literal_query(
|
|
252
|
+
f"""
|
|
253
|
+
MATCH (n:`{self._label}`)
|
|
254
|
+
WHERE n.id IN $ids
|
|
255
|
+
RETURN n.id AS id, properties(n) AS props
|
|
256
|
+
"""
|
|
257
|
+
),
|
|
251
258
|
ids=list(ids),
|
|
252
259
|
)
|
|
253
260
|
records = [record async for record in result]
|
|
@@ -266,11 +273,13 @@ class MemgraphVectorStorage(BaseVectorStorage):
|
|
|
266
273
|
driver = await get_driver()
|
|
267
274
|
async with driver.session(database=get_database(), default_access_mode="READ") as session:
|
|
268
275
|
result = await session.run(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
as_literal_query(
|
|
277
|
+
f"""
|
|
278
|
+
MATCH (n:`{self._label}`)
|
|
279
|
+
WHERE n.id IN $ids AND n.embedding IS NOT NULL
|
|
280
|
+
RETURN n.id AS id, n.embedding AS embedding
|
|
281
|
+
"""
|
|
282
|
+
),
|
|
274
283
|
ids=list(ids),
|
|
275
284
|
)
|
|
276
285
|
vectors = {record["id"]: list(record["embedding"]) async for record in result}
|
|
@@ -291,12 +300,14 @@ class MemgraphVectorStorage(BaseVectorStorage):
|
|
|
291
300
|
async with driver.session(database=get_database()) as session:
|
|
292
301
|
await (
|
|
293
302
|
await session.run(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
303
|
+
as_literal_query(
|
|
304
|
+
f"""
|
|
305
|
+
MATCH (n:`{self._label}`)
|
|
306
|
+
{where_clause}
|
|
307
|
+
SET n.embedding = NULL
|
|
308
|
+
DETACH DELETE n
|
|
309
|
+
"""
|
|
310
|
+
),
|
|
300
311
|
**params,
|
|
301
312
|
)
|
|
302
313
|
).consume()
|
|
@@ -323,7 +334,7 @@ class MemgraphVectorStorage(BaseVectorStorage):
|
|
|
323
334
|
driver = await get_driver()
|
|
324
335
|
async with driver.session(database=get_database()) as session:
|
|
325
336
|
try:
|
|
326
|
-
await (await session.run(f"DROP VECTOR INDEX {self._index_name}")).consume()
|
|
337
|
+
await (await session.run(as_literal_query(f"DROP VECTOR INDEX {self._index_name}"))).consume()
|
|
327
338
|
except Exception as e:
|
|
328
339
|
logger.debug(f"[{self.workspace}] Dropping vector index {self._index_name} skipped: {e}")
|
|
329
340
|
self._index_ready = False
|
|
@@ -9,12 +9,13 @@ runs an insert/query).
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
12
13
|
|
|
13
14
|
import pytest
|
|
14
15
|
from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed
|
|
15
16
|
from lightrag.utils import logger as lightrag_logger
|
|
16
17
|
|
|
17
|
-
from lightrag_memgraph.core import _apply_lightrag_defaults, _bridge_lightrag_env_names
|
|
18
|
+
from lightrag_memgraph.core import MemgraphLightRAGWrapper, _apply_lightrag_defaults, _bridge_lightrag_env_names
|
|
18
19
|
from lightrag_memgraph.embeddings import memgraph_sentence_embed
|
|
19
20
|
|
|
20
21
|
_MEMGRAPH_ENV_NAMES = ("MEMGRAPH_URL", "MEMGRAPH_URI", "MEMGRAPH_USER", "MEMGRAPH_USERNAME")
|
|
@@ -136,3 +137,49 @@ def test_no_openai_key_required_with_non_openai_llm_and_embedding_default(monkey
|
|
|
136
137
|
kwargs = {"llm_model_func": lambda *a, **kw: None}
|
|
137
138
|
_apply_lightrag_defaults(kwargs) # must not raise
|
|
138
139
|
assert kwargs["embedding_func"] is memgraph_sentence_embed
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.asyncio
|
|
143
|
+
async def test_afinalize_resets_shared_data_on_success():
|
|
144
|
+
wrapper = MemgraphLightRAGWrapper()
|
|
145
|
+
wrapper.rag = AsyncMock()
|
|
146
|
+
|
|
147
|
+
with patch("lightrag_memgraph.core.finalize_share_data") as mock_finalize_share_data:
|
|
148
|
+
await wrapper.afinalize()
|
|
149
|
+
|
|
150
|
+
wrapper.rag.finalize_storages.assert_awaited_once()
|
|
151
|
+
mock_finalize_share_data.assert_called_once()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.mark.asyncio
|
|
155
|
+
async def test_afinalize_resets_shared_data_even_if_finalize_storages_raises():
|
|
156
|
+
"""A transient error in finalize_storages() must not leave the
|
|
157
|
+
process-global shared-storage state stuck as initialized -- that would
|
|
158
|
+
break every subsequent LightRAG instance in this process, not just this
|
|
159
|
+
one's cleanup."""
|
|
160
|
+
wrapper = MemgraphLightRAGWrapper()
|
|
161
|
+
wrapper.rag = AsyncMock()
|
|
162
|
+
wrapper.rag.finalize_storages.side_effect = RuntimeError("transient network error")
|
|
163
|
+
|
|
164
|
+
with (
|
|
165
|
+
patch("lightrag_memgraph.core.finalize_share_data") as mock_finalize_share_data,
|
|
166
|
+
pytest.raises(RuntimeError, match="transient network error"),
|
|
167
|
+
):
|
|
168
|
+
await wrapper.afinalize()
|
|
169
|
+
|
|
170
|
+
mock_finalize_share_data.assert_called_once()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_workspace_reads_chunk_entity_relation_graph_workspace():
|
|
174
|
+
wrapper = MemgraphLightRAGWrapper()
|
|
175
|
+
wrapper.rag = MagicMock()
|
|
176
|
+
wrapper.rag.chunk_entity_relation_graph.workspace = "tenant-42"
|
|
177
|
+
|
|
178
|
+
assert wrapper.workspace == "tenant-42"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_workspace_raises_before_initialize():
|
|
182
|
+
wrapper = MemgraphLightRAGWrapper()
|
|
183
|
+
|
|
184
|
+
with pytest.raises(RuntimeError, match="not initialized"):
|
|
185
|
+
_ = wrapper.workspace
|
|
@@ -35,6 +35,7 @@ from lightrag.utils import EmbeddingFunc
|
|
|
35
35
|
|
|
36
36
|
from lightrag_memgraph import MemgraphLightRAGWrapper
|
|
37
37
|
from lightrag_memgraph._connection import (
|
|
38
|
+
as_literal_query,
|
|
38
39
|
close_driver,
|
|
39
40
|
get_database,
|
|
40
41
|
get_driver,
|
|
@@ -493,7 +494,8 @@ async def test_vector_query_logs_and_excludes_stale_candidates(
|
|
|
493
494
|
# DETACH DELETE leaves a stale entry in the native vector index.
|
|
494
495
|
driver = await get_driver()
|
|
495
496
|
async with driver.session(database=get_database()) as session:
|
|
496
|
-
|
|
497
|
+
query = as_literal_query(f"MATCH (n:`{store._label}` {{id: 'v-cat'}}) DETACH DELETE n")
|
|
498
|
+
await (await session.run(query)).consume()
|
|
497
499
|
|
|
498
500
|
# lightrag's logger has propagate=False, so caplog.at_level (which only
|
|
499
501
|
# relies on root-logger propagation) never sees its records; attach the
|
|
@@ -612,9 +614,11 @@ async def test_wrapper_defaults_embedding_func_to_working_memgraph_sentence_embe
|
|
|
612
614
|
# underlying callable, unwrapping the EmbeddingFunc nesting), so identity
|
|
613
615
|
# doesn't survive the round trip -- the declared model/dim and, most
|
|
614
616
|
# importantly, actual behaviour do.
|
|
615
|
-
|
|
616
|
-
assert
|
|
617
|
-
|
|
617
|
+
embedding_func = rag.embedding_func
|
|
618
|
+
assert embedding_func is not None
|
|
619
|
+
assert embedding_func.model_name == memgraph_sentence_embed.model_name
|
|
620
|
+
assert embedding_func.embedding_dim == DEFAULT_EMBEDDING_DIM
|
|
621
|
+
vectors = await embedding_func(["hello world", "graph databases are fast"])
|
|
618
622
|
assert vectors.shape == (2, DEFAULT_EMBEDDING_DIM)
|
|
619
623
|
finally:
|
|
620
624
|
if wrapper.rag is not None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|