topoteretes--cognee
c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
611 行
25 KiB
Python
611 行
25 KiB
Python
"""Factory function to get the appropriate graph client based on the graph type."""
|
|
|
|
import inspect
|
|
import os
|
|
from numbers import Number
|
|
|
|
from cognee.infrastructure.databases.dataset_queue.pinning import dataset_queue_pin_predicate
|
|
from cognee.infrastructure.databases.utils.closing_lru_cache import closing_lru_cache
|
|
from cognee.shared.lru_cache import DATABASE_MAX_LRU_CACHE_SIZE
|
|
from cognee.shared.logging_utils import get_logger
|
|
|
|
from .kuzu.adapter import DEFAULT_KUZU_BUFFER_POOL_SIZE, DEFAULT_KUZU_MAX_DB_SIZE
|
|
from .config import get_graph_context_config
|
|
from .graph_db_interface import GraphDBInterface
|
|
from .supported_databases import supported_databases
|
|
|
|
logger = get_logger("GraphEngine")
|
|
|
|
|
|
def _normalize_graph_database_provider(provider: str) -> str:
|
|
return provider.lower() if isinstance(provider, str) else provider
|
|
|
|
|
|
def _get_create_graph_engine_optional_defaults() -> dict:
|
|
"""Return default values for optional create_graph_engine parameters."""
|
|
signature = inspect.signature(create_graph_engine)
|
|
return {
|
|
name: parameter.default
|
|
for name, parameter in signature.parameters.items()
|
|
if parameter.default is not inspect.Parameter.empty
|
|
}
|
|
|
|
|
|
def _normalize_optional_create_graph_engine_params(params: dict) -> dict:
|
|
"""
|
|
Normalize optional create_graph_engine parameters:
|
|
- replace None with the function defaults
|
|
- convert numeric graph_database_port values to string
|
|
"""
|
|
defaults = _get_create_graph_engine_optional_defaults()
|
|
normalized = dict(params)
|
|
|
|
for key, default_value in defaults.items():
|
|
if normalized.get(key) is None:
|
|
normalized[key] = default_value
|
|
|
|
if isinstance(normalized.get("graph_database_port"), Number) and not isinstance(
|
|
normalized["graph_database_port"], bool
|
|
):
|
|
normalized["graph_database_port"] = str(normalized["graph_database_port"])
|
|
|
|
if not normalized.get("graph_dataset_database_handler"):
|
|
normalized["graph_dataset_database_handler"] = os.getenv(
|
|
"GRAPH_DATASET_DATABASE_HANDLER", "ladybug"
|
|
)
|
|
|
|
return normalized
|
|
|
|
|
|
class _GraphEngineHandle:
|
|
"""Stable reference to the current graph engine that survives cache invalidation.
|
|
|
|
Database engine instances are cached via ``closing_lru_cache``. Several
|
|
operations invalidate that cache — ``prune_system`` calls ``cache_clear()``,
|
|
``delete_dataset`` evicts individual entries, and the ``__aexit__`` of
|
|
``set_database_global_context_variables`` evicts subprocess-mode engines to
|
|
release file locks. When an entry is evicted the underlying adapter is
|
|
closed, so any direct proxy reference becomes a dead object that raises
|
|
"adapter is closed" on use.
|
|
|
|
This handle solves the problem by deferring resolution: every attribute
|
|
access calls ``create_graph_engine(**config)`` which either returns the
|
|
existing cached proxy (fast path) or transparently creates a fresh adapter
|
|
if the old one was evicted (recovery path). Code that stores the return
|
|
value of ``get_graph_engine()`` — even across ``cognify``, ``search``,
|
|
``prune``, or ``delete`` calls — always reaches a live adapter without
|
|
needing to re-call ``get_graph_engine()``.
|
|
|
|
For adapters that expose ``initialize()`` (Postgres, Neo4j), the handle
|
|
tracks which engine proxy was last initialized and re-runs the idempotent
|
|
schema setup when the underlying engine changes.
|
|
|
|
Known limitation (subprocess + exclusive file lock, e.g. Ladybug): the cache
|
|
leases a single shared proxy per entry, so two concurrently-held handles for
|
|
the same DB path pin the *same* proxy. If that entry is evicted while an
|
|
idle second handle keeps holding the proxy, the old worker's close stays
|
|
deferred (it does not release the file lock) until that holder lets go or
|
|
is garbage-collected. Creators for the same path deliberately do NOT wait
|
|
for such a deferred close — an idle holder can pin it indefinitely, and
|
|
waiting on it from a handle's own re-resolution self-deadlocks (this hung
|
|
CI) — so a fresh engine relies on the worker's open-retry
|
|
(``SUBPROCESS_OPEN_LOCK_RETRIES``) for the overlap. Once a close is
|
|
actually in flight, creators wait for it deterministically; the primary
|
|
multi-tenant teardown path (``dataset_queue._teardown_subprocess_engines``)
|
|
also ``await``s ``engine.close()`` to completion before any re-creation.
|
|
"""
|
|
|
|
__slots__ = ("_config", "_last_initialized_id", "_pinned")
|
|
|
|
def __init__(self, config: dict):
|
|
object.__setattr__(self, "_config", config)
|
|
object.__setattr__(self, "_last_initialized_id", None)
|
|
# Pinned leased engine proxy. Holding it avoids re-entering the cache on
|
|
# every attribute access (and the create-vs-close race that re-entry
|
|
# caused). It is dropped + re-resolved once the pin is no longer the
|
|
# live cache entry (see ``_pin_is_live``) so prune/delete eviction still
|
|
# recovers a fresh engine instead of keeping an evicted DB worker alive.
|
|
object.__setattr__(self, "_pinned", None)
|
|
|
|
@staticmethod
|
|
def _pin_is_live(engine) -> bool:
|
|
"""Whether a pinned engine is still the live cached value and safe to
|
|
reuse. A leased proxy whose entry was evicted must be released so its
|
|
deferred close can run (otherwise a pinned handle would keep an evicted
|
|
Ladybug worker alive holding the file lock, blocking a new worker)."""
|
|
active = getattr(engine, "_leased_entry_active", None)
|
|
if active is not None:
|
|
try:
|
|
if not active():
|
|
return False
|
|
except Exception:
|
|
# ``_leased_entry_active`` is two attribute reads and should
|
|
# never raise; if it does, surface it — then treat the pin as
|
|
# stale, which safely re-resolves through the cache.
|
|
logger.warning(
|
|
"Unexpected error while checking pinned engine liveness", exc_info=True
|
|
)
|
|
return False
|
|
# Subprocess adapters latch ``_permanently_closed`` on close.
|
|
if getattr(engine, "_permanently_closed", False):
|
|
return False
|
|
return True
|
|
|
|
def _release_stale_pin(self, pinned) -> None:
|
|
"""Drop the stale pinned proxy BEFORE re-resolving a replacement.
|
|
|
|
Critical for the lock race: the pinned proxy is (typically) the last
|
|
reference keeping an evicted adapter alive. Releasing it lets the
|
|
deferred close start — and a subprocess adapter's close runs off-loop,
|
|
releasing the on-disk file lock — *before* a new worker opens the same
|
|
path. Holding the pin across the re-resolution would keep the old worker
|
|
alive and the new one would fail to take the lock.
|
|
"""
|
|
object.__setattr__(self, "_pinned", None)
|
|
del pinned
|
|
|
|
def _engine(self):
|
|
"""Synchronous resolution used on the hot attribute-access path. Reuses
|
|
the pin when live; otherwise drops it and re-resolves through the (sync)
|
|
cache. A mid-flow re-resolution can't await an in-flight close — the
|
|
off-loop close + worker open-retry backstop cover that residual race."""
|
|
pinned = self._pinned
|
|
if pinned is not None and self._pin_is_live(pinned):
|
|
return pinned
|
|
if pinned is not None:
|
|
self._release_stale_pin(pinned)
|
|
pinned = None
|
|
engine = create_graph_engine(**self._config)
|
|
object.__setattr__(self, "_pinned", engine)
|
|
return engine
|
|
|
|
async def _aengine(self):
|
|
"""Async resolution used at initialization. Goes through the cache's
|
|
async acquisition path so it waits for any in-flight close of the same
|
|
key before constructing a new engine + pinning it."""
|
|
pinned = self._pinned
|
|
if pinned is not None and self._pin_is_live(pinned):
|
|
return pinned
|
|
if pinned is not None:
|
|
self._release_stale_pin(pinned)
|
|
pinned = None
|
|
engine = await acreate_graph_engine(**self._config)
|
|
object.__setattr__(self, "_pinned", engine)
|
|
return engine
|
|
|
|
async def _ensure_initialized(self):
|
|
engine = await self._aengine()
|
|
engine_id = id(engine)
|
|
if engine_id != self._last_initialized_id and hasattr(engine, "initialize"):
|
|
await engine.initialize()
|
|
object.__setattr__(self, "_last_initialized_id", engine_id)
|
|
|
|
@property
|
|
def __class__(self):
|
|
return self._engine().__class__
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self._engine(), name)
|
|
|
|
def __repr__(self):
|
|
return f"<GraphEngineHandle config={self._config!r}>"
|
|
|
|
|
|
async def get_graph_engine() -> GraphDBInterface:
|
|
"""Factory function to get the appropriate graph client based on the graph type."""
|
|
config = get_graph_context_config()
|
|
handle = _GraphEngineHandle(config)
|
|
await handle._ensure_initialized()
|
|
return handle
|
|
|
|
|
|
# def _make_pghybrid_adapter():
|
|
# """Build the uncached Postgres hybrid adapter used when
|
|
# ``USE_UNIFIED_PROVIDER=pghybrid``. Not cached — the caller owns it, matching
|
|
# the original inline behavior."""
|
|
# from .postgres.adapter import PostgresAdapter
|
|
# from cognee.infrastructure.databases.relational.get_relational_engine import (
|
|
# get_relational_engine,
|
|
# )
|
|
#
|
|
# return PostgresAdapter(connection_string=get_relational_engine().db_uri)
|
|
|
|
|
|
def _resolve_graph_engine_args(params: dict) -> tuple:
|
|
"""Normalize the engine parameters and return the positional argument tuple
|
|
passed to ``_create_graph_engine``.
|
|
|
|
Shared by the sync (:func:`create_graph_engine`) and async
|
|
(:func:`acreate_graph_engine`) entry points so both produce the *identical*
|
|
cache key (the positional tuple) — and so it matches the key built by
|
|
``evict_graph_engine`` / ``is_graph_engine_cached``.
|
|
"""
|
|
normalized = _normalize_optional_create_graph_engine_params(params)
|
|
return (
|
|
_normalize_graph_database_provider(params.get("graph_database_provider")),
|
|
params.get("graph_file_path"),
|
|
normalized["graph_database_url"],
|
|
normalized["graph_database_name"],
|
|
normalized["graph_database_username"],
|
|
normalized["graph_database_password"],
|
|
normalized["graph_database_host"],
|
|
normalized["graph_database_allow_anonymous"],
|
|
normalized["graph_database_port"],
|
|
normalized["graph_database_key"],
|
|
normalized["graph_dataset_database_handler"],
|
|
normalized["graph_database_subprocess_enabled"],
|
|
normalized["kuzu_num_threads"],
|
|
normalized["kuzu_buffer_pool_size"],
|
|
normalized["kuzu_max_db_size"],
|
|
)
|
|
|
|
|
|
def create_graph_engine(
|
|
graph_database_provider,
|
|
graph_file_path,
|
|
graph_database_url="",
|
|
graph_database_name="",
|
|
graph_database_username="",
|
|
graph_database_password="",
|
|
graph_database_host="",
|
|
graph_database_allow_anonymous=False,
|
|
graph_database_port="",
|
|
graph_database_key="",
|
|
graph_dataset_database_handler="",
|
|
graph_database_subprocess_enabled=False,
|
|
kuzu_num_threads=0,
|
|
kuzu_buffer_pool_size=DEFAULT_KUZU_BUFFER_POOL_SIZE,
|
|
kuzu_max_db_size=DEFAULT_KUZU_MAX_DB_SIZE,
|
|
):
|
|
"""
|
|
Wrapper function to call create graph engine with caching.
|
|
For a detailed description, see _create_graph_engine.
|
|
"""
|
|
# Check USE_UNIFIED_PROVIDER outside the cache so it's always re-read
|
|
# if os.environ.get("USE_UNIFIED_PROVIDER", "") == "pghybrid":
|
|
# return _make_pghybrid_adapter()
|
|
|
|
return _create_graph_engine(*_resolve_graph_engine_args(locals()))
|
|
|
|
|
|
async def acreate_graph_engine(**kwargs):
|
|
"""Async counterpart of :func:`create_graph_engine` that waits for any
|
|
in-flight close of the same cache key before constructing a new engine.
|
|
|
|
Used by ``get_graph_engine``'s handle at initialization so a freshly evicted
|
|
subprocess engine's worker has fully exited (releasing its file lock) before
|
|
a new worker opens the same DB path.
|
|
"""
|
|
# if os.environ.get("USE_UNIFIED_PROVIDER", "") == "pghybrid":
|
|
# return _make_pghybrid_adapter()
|
|
|
|
return await _create_graph_engine.acall(*_resolve_graph_engine_args(kwargs))
|
|
|
|
|
|
def evict_graph_engine(**kwargs) -> bool:
|
|
"""Evict a cached graph engine entry created via ``create_graph_engine``.
|
|
|
|
Mirrors ``create_graph_engine``'s normalization so the cache key
|
|
matches. Used by per-dataset deletion paths to drop the leased
|
|
adapter (and trigger its ``close()``) without disturbing the rest
|
|
of the cache.
|
|
|
|
Returns True if the entry existed.
|
|
"""
|
|
normalized = _normalize_optional_create_graph_engine_params(kwargs)
|
|
provider = _normalize_graph_database_provider(kwargs.get("graph_database_provider"))
|
|
return _create_graph_engine.cache_evict(
|
|
provider,
|
|
kwargs.get("graph_file_path"),
|
|
normalized["graph_database_url"],
|
|
normalized["graph_database_name"],
|
|
normalized["graph_database_username"],
|
|
normalized["graph_database_password"],
|
|
normalized["graph_database_host"],
|
|
normalized["graph_database_allow_anonymous"],
|
|
normalized["graph_database_port"],
|
|
normalized["graph_database_key"],
|
|
normalized["graph_dataset_database_handler"],
|
|
normalized["graph_database_subprocess_enabled"],
|
|
normalized["kuzu_num_threads"],
|
|
normalized["kuzu_buffer_pool_size"],
|
|
normalized["kuzu_max_db_size"],
|
|
)
|
|
|
|
|
|
def evict_graph_engines_for_database(graph_database_name: str) -> int:
|
|
"""Evict every cached graph engine bound to *graph_database_name*.
|
|
|
|
The same per-dataset database can be cached under multiple keys: the
|
|
dataset-handler creation key and the pipeline's context-config key differ
|
|
in ``graph_file_path`` and ``graph_dataset_database_handler``, so key-exact
|
|
``evict_graph_engine`` misses the pipeline's entry and leaves an engine
|
|
whose connection pool died with the dropped database. Per-dataset database
|
|
names are dataset UUIDs, so matching the name against key fields cannot
|
|
collide with other entries.
|
|
|
|
Returns the number of evicted entries.
|
|
"""
|
|
if not graph_database_name:
|
|
raise ValueError("graph_database_name must be a non-empty database name")
|
|
return _create_graph_engine.cache_evict_matching(graph_database_name=graph_database_name)
|
|
|
|
|
|
async def aevict_graph_engines_for_database(graph_database_name: str) -> int:
|
|
"""Evict every cached graph engine bound to *graph_database_name* and wait
|
|
until their IN-FLIGHT closes have completed (workers exited, file locks
|
|
released). Use before removing the database's files so a teardown that is
|
|
already running cannot race the removal.
|
|
|
|
A close still deferred behind a live caller proxy (an idle engine handle)
|
|
is NOT waited on — see the ``closing_lru_cache`` module docstring. In that
|
|
case files are removed under an engine that closes later; on POSIX the
|
|
unlinked files stay valid for the holder and the eventual close writes to
|
|
nowhere, which is acceptable for a dataset being deleted.
|
|
|
|
Returns the number of evicted entries.
|
|
"""
|
|
evicted = evict_graph_engines_for_database(graph_database_name)
|
|
await _create_graph_engine.cache_await_closed(graph_database_name=graph_database_name)
|
|
return evicted
|
|
|
|
|
|
def is_graph_engine_cached(**kwargs) -> bool:
|
|
"""Check whether a graph engine entry exists in the cache without creating."""
|
|
normalized = _normalize_optional_create_graph_engine_params(kwargs)
|
|
provider = _normalize_graph_database_provider(kwargs.get("graph_database_provider"))
|
|
return _create_graph_engine.cache_contains(
|
|
provider,
|
|
kwargs.get("graph_file_path"),
|
|
normalized["graph_database_url"],
|
|
normalized["graph_database_name"],
|
|
normalized["graph_database_username"],
|
|
normalized["graph_database_password"],
|
|
normalized["graph_database_host"],
|
|
normalized["graph_database_allow_anonymous"],
|
|
normalized["graph_database_port"],
|
|
normalized["graph_database_key"],
|
|
normalized["graph_dataset_database_handler"],
|
|
normalized["graph_database_subprocess_enabled"],
|
|
normalized["kuzu_num_threads"],
|
|
normalized["kuzu_buffer_pool_size"],
|
|
normalized["kuzu_max_db_size"],
|
|
)
|
|
|
|
|
|
@closing_lru_cache(
|
|
maxsize=DATABASE_MAX_LRU_CACHE_SIZE,
|
|
pinned_predicate=dataset_queue_pin_predicate("graph_database_name"),
|
|
)
|
|
def _create_graph_engine(
|
|
graph_database_provider,
|
|
graph_file_path,
|
|
graph_database_url="",
|
|
graph_database_name="",
|
|
graph_database_username="",
|
|
graph_database_password="",
|
|
graph_database_host="",
|
|
graph_database_allow_anonymous=False,
|
|
graph_database_port="",
|
|
graph_database_key="",
|
|
graph_dataset_database_handler="",
|
|
graph_database_subprocess_enabled=False,
|
|
kuzu_num_threads=0,
|
|
kuzu_buffer_pool_size=DEFAULT_KUZU_BUFFER_POOL_SIZE,
|
|
kuzu_max_db_size=DEFAULT_KUZU_MAX_DB_SIZE,
|
|
):
|
|
"""
|
|
Create a graph engine based on the specified provider type.
|
|
|
|
This factory function initializes and returns the appropriate graph client depending on
|
|
the database provider specified. It validates required parameters and raises an
|
|
EnvironmentError if any are missing for the respective provider implementations.
|
|
|
|
Parameters:
|
|
-----------
|
|
|
|
- graph_database_provider: The type of graph database provider to use (e.g., neo4j, falkor, ladybug).
|
|
- graph_database_url: The URL for the graph database instance. Required for neo4j and falkordb providers.
|
|
- graph_database_username: The username for authentication with the graph database.
|
|
Required for neo4j provider.
|
|
- graph_database_password: The password for authentication with the graph database.
|
|
Required for neo4j provider.
|
|
- graph_database_port: The port number for the graph database connection. Required
|
|
for the falkordb provider
|
|
- graph_file_path: The filesystem path to the graph file. Required for the ladybug
|
|
provider.
|
|
|
|
Returns:
|
|
--------
|
|
|
|
Returns an instance of the appropriate graph adapter depending on the provider type
|
|
specified.
|
|
"""
|
|
|
|
if graph_database_provider in supported_databases:
|
|
adapter = supported_databases[graph_database_provider]
|
|
|
|
return adapter(
|
|
graph_database_url=graph_database_url,
|
|
graph_database_username=graph_database_username,
|
|
graph_database_password=graph_database_password,
|
|
graph_database_port=graph_database_port,
|
|
graph_database_key=graph_database_key,
|
|
database_name=graph_database_name,
|
|
)
|
|
|
|
if graph_database_provider == "neo4j":
|
|
if not graph_database_url:
|
|
raise EnvironmentError("Missing required Neo4j URL.")
|
|
|
|
from .neo4j_driver.adapter import Neo4jAdapter
|
|
|
|
return Neo4jAdapter(
|
|
graph_database_url=graph_database_url,
|
|
graph_database_username=graph_database_username or None,
|
|
graph_database_password=graph_database_password or None,
|
|
graph_database_name=graph_database_name or None,
|
|
graph_database_allow_anonymous=graph_database_allow_anonymous,
|
|
)
|
|
|
|
elif graph_database_provider == "postgres":
|
|
from cognee.context_global_variables import backend_access_control_enabled
|
|
|
|
if backend_access_control_enabled():
|
|
if not (
|
|
graph_database_host
|
|
and graph_database_port
|
|
and graph_database_username
|
|
and graph_database_password
|
|
):
|
|
raise EnvironmentError("Missing required Postgres graph credentials.")
|
|
|
|
connection_string: str = (
|
|
f"postgresql+asyncpg://{graph_database_username}:{graph_database_password}"
|
|
f"@{graph_database_host}:{graph_database_port}/{graph_database_name}"
|
|
)
|
|
else:
|
|
if (
|
|
graph_database_port
|
|
and graph_database_username
|
|
and graph_database_password
|
|
and graph_database_host
|
|
and graph_database_name
|
|
):
|
|
connection_string: str = (
|
|
f"postgresql+asyncpg://{graph_database_username}:{graph_database_password}"
|
|
f"@{graph_database_host}:{graph_database_port}/{graph_database_name}"
|
|
)
|
|
else:
|
|
from cognee.infrastructure.databases.relational import get_relational_config
|
|
|
|
logger.warning(
|
|
"Postgres graph credentials are not fully configured; "
|
|
"falling back to the relational database configuration. "
|
|
"Set GRAPH_DATABASE_HOST/PORT/USERNAME/PASSWORD/NAME explicitly "
|
|
"to avoid this fallback."
|
|
)
|
|
|
|
relational_config = get_relational_config()
|
|
db_username = relational_config.db_username
|
|
db_password = relational_config.db_password
|
|
db_host = relational_config.db_host
|
|
db_port = relational_config.db_port
|
|
db_name = relational_config.db_name
|
|
|
|
if not (db_host and db_port and db_name and db_username and db_password):
|
|
raise EnvironmentError("Missing required Postgres graph credentials!")
|
|
|
|
connection_string: str = (
|
|
f"postgresql+asyncpg://{db_username}:{db_password}"
|
|
f"@{db_host}:{db_port}/{db_name}"
|
|
)
|
|
|
|
from .postgres.adapter import PostgresAdapter
|
|
|
|
return PostgresAdapter(connection_string=connection_string)
|
|
|
|
elif graph_database_provider in ("ladybug", "kuzu"):
|
|
if not graph_file_path:
|
|
raise EnvironmentError("Missing required Ladybug database path.")
|
|
|
|
from .ladybug.adapter import LadybugAdapter
|
|
|
|
if graph_database_subprocess_enabled:
|
|
return LadybugAdapter.create_subprocess(
|
|
db_path=graph_file_path,
|
|
kuzu_num_threads=kuzu_num_threads,
|
|
kuzu_buffer_pool_size=kuzu_buffer_pool_size,
|
|
kuzu_max_db_size=kuzu_max_db_size,
|
|
)
|
|
|
|
return LadybugAdapter(
|
|
db_path=graph_file_path,
|
|
kuzu_num_threads=kuzu_num_threads,
|
|
kuzu_buffer_pool_size=kuzu_buffer_pool_size,
|
|
kuzu_max_db_size=kuzu_max_db_size,
|
|
)
|
|
|
|
elif graph_database_provider in ("ladybug-remote", "kuzu-remote"):
|
|
if not graph_database_url:
|
|
raise EnvironmentError("Missing required Ladybug remote URL.")
|
|
|
|
from .ladybug.remote_ladybug_adapter import RemoteLadybugAdapter
|
|
|
|
return RemoteLadybugAdapter(
|
|
api_url=graph_database_url,
|
|
username=graph_database_username,
|
|
password=graph_database_password,
|
|
)
|
|
elif graph_database_provider == "neptune":
|
|
try:
|
|
from langchain_aws import NeptuneAnalyticsGraph
|
|
except ImportError:
|
|
raise ImportError(
|
|
"langchain_aws is not installed. Please install it with 'pip install langchain_aws'"
|
|
)
|
|
|
|
if not graph_database_url:
|
|
raise EnvironmentError("Missing Neptune endpoint.")
|
|
|
|
from .neptune_driver.adapter import NeptuneGraphDB, NEPTUNE_ENDPOINT_URL
|
|
|
|
if not graph_database_url.startswith(NEPTUNE_ENDPOINT_URL):
|
|
raise ValueError(
|
|
f"Neptune endpoint must have the format {NEPTUNE_ENDPOINT_URL}<GRAPH_ID>"
|
|
)
|
|
|
|
graph_identifier = graph_database_url.replace(NEPTUNE_ENDPOINT_URL, "")
|
|
|
|
return NeptuneGraphDB(
|
|
graph_id=graph_identifier,
|
|
)
|
|
|
|
elif graph_database_provider == "neptune_analytics":
|
|
"""
|
|
Creates a graph DB from config
|
|
We want to use a hybrid (graph & vector) DB and we should update this
|
|
to make a single instance of the hybrid configuration (with embedder)
|
|
instead of creating the hybrid object twice.
|
|
"""
|
|
try:
|
|
from langchain_aws import NeptuneAnalyticsGraph
|
|
except ImportError:
|
|
raise ImportError(
|
|
"langchain_aws is not installed. Please install it with 'pip install langchain_aws'"
|
|
)
|
|
|
|
if not graph_database_url:
|
|
raise EnvironmentError("Missing Neptune endpoint.")
|
|
|
|
from ..hybrid.neptune_analytics.NeptuneAnalyticsAdapter import (
|
|
NeptuneAnalyticsAdapter,
|
|
NEPTUNE_ANALYTICS_ENDPOINT_URL,
|
|
)
|
|
|
|
if not graph_database_url.startswith(NEPTUNE_ANALYTICS_ENDPOINT_URL):
|
|
raise ValueError(
|
|
f"Neptune endpoint must have the format '{NEPTUNE_ANALYTICS_ENDPOINT_URL}<GRAPH_ID>'"
|
|
)
|
|
|
|
graph_identifier = graph_database_url.replace(NEPTUNE_ANALYTICS_ENDPOINT_URL, "")
|
|
|
|
return NeptuneAnalyticsAdapter(
|
|
graph_id=graph_identifier,
|
|
)
|
|
|
|
all_providers = list(supported_databases.keys()) + [
|
|
"neo4j",
|
|
"ladybug",
|
|
"ladybug-remote",
|
|
"kuzu",
|
|
"kuzu-remote",
|
|
"postgres",
|
|
"neptune",
|
|
"neptune_analytics",
|
|
]
|
|
raise EnvironmentError(
|
|
f"Unsupported graph database provider: {graph_database_provider}. "
|
|
f"Supported providers are: {', '.join(all_providers)}"
|
|
)
|