Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
FALKORDB_HOST=localhost
FALKORDB_PORT=6379

# Database backend. Use "lite" to run against an embedded FalkorDBLite
# instance instead of an external FalkorDB host/port.
CODE_GRAPH_DB_BACKEND=falkordb
FALKORDB_LITE_PATH=~/.cache/code-graph/falkordblite.rdb
# Optional: expose FalkorDBLite on localhost for host/port-only integrations
# such as GraphRAG chat. Structural CodeGraph/MCP tools do not need this.
# FALKORDB_LITE_HOST=127.0.0.1
# FALKORDB_LITE_PORT=6379

# Optional FalkorDB authentication
FALKORDB_USERNAME=
FALKORDB_PASSWORD=
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ code-graph/
- Python `>=3.12,<3.14`
- Node.js 20+
- [`uv`](https://docs.astral.sh/uv/)
- A FalkorDB instance (local or cloud)
- A FalkorDB instance (local/cloud) or the optional FalkorDBLite backend

### 1. Start FalkorDB

Expand All @@ -68,6 +68,16 @@ code-graph/
docker run -p 6379:6379 -it --rm falkordb/falkordb
```

**Option C:** Use embedded FalkorDBLite:

```bash
uv sync --extra light
export CODE_GRAPH_DB_BACKEND=lite
export FALKORDB_LITE_PATH=~/.cache/code-graph/falkordblite.rdb
```

FalkorDBLite runs a local embedded server over a private Unix socket by default. Set `FALKORDB_LITE_PORT` only when a host/port-only integration, such as GraphRAG chat, must connect to the embedded database.

### 2. Configure environment variables

Copy the template and adjust it for your setup:
Expand All @@ -78,10 +88,14 @@ cp .env.template .env

| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `CODE_GRAPH_DB_BACKEND` | Database backend: `falkordb` or `lite` | No | `falkordb` |
| `FALKORDB_HOST` | FalkorDB hostname | No | `localhost` |
| `FALKORDB_PORT` | FalkorDB port | No | `6379` |
| `FALKORDB_USERNAME` | Optional FalkorDB username | No | empty |
| `FALKORDB_PASSWORD` | Optional FalkorDB password | No | empty |
| `FALKORDB_LITE_PATH` | FalkorDBLite database file path | No | `~/.cache/code-graph/falkordblite.rdb` |
| `FALKORDB_LITE_HOST` | Host used when exposing FalkorDBLite over TCP | No | `127.0.0.1` |
| `FALKORDB_LITE_PORT` | Optional TCP port for FalkorDBLite host/port clients | No | empty |
| `SECRET_TOKEN` | Token checked by protected endpoints | No | empty |
| `CODE_GRAPH_PUBLIC` | Set `1` to skip auth on read-only endpoints | No | `0` |
| `ALLOWED_ANALYSIS_DIR` | Root path allowed for `/api/analyze_folder` | No | repository root |
Expand Down
12 changes: 12 additions & 0 deletions api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ def _check_connection(host: str, port: int) -> bool:
def ensure_db() -> None:
"""Ensure FalkorDB is running, auto-starting a Docker container if needed."""

from .db import create_falkordb, is_lite_backend

if is_lite_backend():
try:
db = create_falkordb()
db.connection.ping()
except Exception as e:
_json_error(f"Failed to initialize FalkorDBLite: {e}")
_stderr("FalkorDBLite embedded backend is ready")
_json_out({"status": "ok", "backend": "lite"})
return

host = os.getenv("FALKORDB_HOST", "localhost")
try:
port = int(os.getenv("FALKORDB_PORT", "6379"))
Expand Down
157 changes: 157 additions & 0 deletions api/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""Database backend selection for CodeGraph.

The default backend is a regular FalkorDB server addressed by host/port.
Set ``CODE_GRAPH_DB_BACKEND=lite`` to use FalkorDBLite's embedded server.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any


LITE_BACKENDS = {"lite", "falkordblite"}
DEFAULT_LITE_PATH = "~/.cache/code-graph/falkordblite.rdb"


def is_lite_backend() -> bool:
"""Return whether CodeGraph should use the embedded FalkorDBLite backend."""
backend = os.getenv("CODE_GRAPH_DB_BACKEND", "falkordb").strip().lower()
return backend in LITE_BACKENDS


def _lite_db_path() -> str:
path = Path(os.getenv("FALKORDB_LITE_PATH", DEFAULT_LITE_PATH)).expanduser()
path.parent.mkdir(parents=True, exist_ok=True)
return str(path)


def _lite_serverconfig() -> dict[str, str]:
"""Return FalkorDBLite server config from env.

FalkorDBLite defaults to a private Unix socket. Supplying
``FALKORDB_LITE_PORT`` additionally exposes a local TCP port, which is
useful for libraries that only accept host/port connection settings.
"""
port = os.getenv("FALKORDB_LITE_PORT")
if not port:
return {}
return {
"bind": os.getenv("FALKORDB_LITE_HOST", "127.0.0.1"),
"port": port,
}


def create_falkordb() -> Any:
"""Create a sync FalkorDB client for the configured backend."""
if is_lite_backend():
try:
from redislite.falkordb_client import FalkorDB as LiteFalkorDB
except ImportError as e:
raise RuntimeError(
"CODE_GRAPH_DB_BACKEND=lite requires the optional "
"`falkordblite` dependency. Install with "
"`uv sync --extra light` or `pip install 'falkordb-code-graph[light]'`."
) from e

return LiteFalkorDB(_lite_db_path(), serverconfig=_lite_serverconfig())

from falkordb import FalkorDB

return FalkorDB(
host=os.getenv("FALKORDB_HOST", "localhost"),
port=os.getenv("FALKORDB_PORT", 6379),
username=os.getenv("FALKORDB_USERNAME", None),
password=os.getenv("FALKORDB_PASSWORD", None),
)


def create_async_falkordb() -> Any:
"""Create an async FalkorDB client for the configured backend."""
if is_lite_backend():
try:
from redislite.async_falkordb_client import AsyncFalkorDB as LiteAsyncFalkorDB
except ImportError as e:
raise RuntimeError(
"CODE_GRAPH_DB_BACKEND=lite requires the optional "
"`falkordblite` dependency. Install with "
"`uv sync --extra light` or `pip install 'falkordb-code-graph[light]'`."
) from e

client = LiteAsyncFalkorDB(_lite_db_path(), serverconfig=_lite_serverconfig())
if not hasattr(client, "aclose"):
client.aclose = client.close
return client

from falkordb.asyncio import FalkorDB as AsyncFalkorDB

return AsyncFalkorDB(
host=os.getenv("FALKORDB_HOST", "localhost"),
port=int(os.getenv("FALKORDB_PORT", 6379)),
username=os.getenv("FALKORDB_USERNAME", None),
password=os.getenv("FALKORDB_PASSWORD", None),
)


def create_redis_connection() -> Any:
"""Create a sync Redis-compatible connection for metadata operations."""
if is_lite_backend():
return create_falkordb().connection

import redis

return redis.Redis(
host=os.getenv("FALKORDB_HOST", "localhost"),
port=int(os.getenv("FALKORDB_PORT", "6379")),
username=os.getenv("FALKORDB_USERNAME"),
password=os.getenv("FALKORDB_PASSWORD"),
decode_responses=True,
)


def create_async_redis_connection() -> Any:
"""Create an async Redis-compatible connection for metadata operations."""
if is_lite_backend():
return create_async_falkordb().connection

import redis.asyncio as aioredis

return aioredis.Redis(
host=os.getenv("FALKORDB_HOST", "localhost"),
port=int(os.getenv("FALKORDB_PORT", "6379")),
username=os.getenv("FALKORDB_USERNAME"),
password=os.getenv("FALKORDB_PASSWORD"),
decode_responses=True,
)


def graphrag_connection_kwargs() -> dict[str, Any]:
"""Return host/port kwargs for GraphRAG SDK constructors.

GraphRAG SDK accepts host/port only. FalkorDBLite's private Unix socket is
therefore usable for structural tools but not for GraphRAG unless a local
TCP port is explicitly enabled with ``FALKORDB_LITE_PORT``.
"""
if is_lite_backend():
port = os.getenv("FALKORDB_LITE_PORT")
if not port:
raise RuntimeError(
"GraphRAG requires host/port access. When using "
"CODE_GRAPH_DB_BACKEND=lite, set FALKORDB_LITE_PORT to expose "
"the embedded FalkorDBLite instance on localhost."
)
create_falkordb()
return {
"host": os.getenv("FALKORDB_LITE_HOST", "127.0.0.1"),
"port": int(port),
"username": None,
"password": None,
}

return {
"host": os.getenv("FALKORDB_HOST", "localhost"),
"port": int(os.getenv("FALKORDB_PORT", 6379)),
"username": os.getenv("FALKORDB_USERNAME", None),
"password": os.getenv("FALKORDB_PASSWORD", None),
}
18 changes: 4 additions & 14 deletions api/git_utils/git_graph.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import os
import logging
from falkordb import FalkorDB, Node
from falkordb.asyncio import FalkorDB as AsyncFalkorDB
from falkordb import Node
from typing import List, Optional

from api.db import create_async_falkordb, create_falkordb
from pygit2 import Commit

# Configure logging
Expand All @@ -19,10 +18,7 @@ class GitGraph():

def __init__(self, name: str):

self.db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'),
port=os.getenv('FALKORDB_PORT', 6379),
username=os.getenv('FALKORDB_USERNAME', None),
password=os.getenv('FALKORDB_PASSWORD', None))
self.db = create_falkordb()

self.g = self.db.select_graph(name)

Expand Down Expand Up @@ -182,12 +178,7 @@ class AsyncGitGraph:
"""Async read-only git graph for endpoint use."""

def __init__(self, name: str):
self.db = AsyncFalkorDB(
host=os.getenv('FALKORDB_HOST', 'localhost'),
port=int(os.getenv('FALKORDB_PORT', 6379)),
username=os.getenv('FALKORDB_USERNAME', None),
password=os.getenv('FALKORDB_PASSWORD', None),
)
self.db = create_async_falkordb()
self.g = self.db.select_graph(name)

def _commit_from_node(self, node: Node) -> dict:
Expand All @@ -205,4 +196,3 @@ async def list_commits(self) -> List[dict]:

async def close(self) -> None:
await self.db.aclose()

43 changes: 12 additions & 31 deletions api/graph.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import os
import re
import time
from .entities import *
from typing import Optional
from falkordb import FalkorDB, Path, Node, QueryResult
from falkordb.asyncio import FalkorDB as AsyncFalkorDB
from falkordb import Path, Node, QueryResult
from .db import create_async_falkordb, create_falkordb
from .entities import File, encode_edge, encode_node

# Configure the logger
import logging
Expand Down Expand Up @@ -62,10 +61,7 @@ def parse_graph_name(graph_name: str) -> Optional[tuple[str, str]]:


def graph_exists(name: str):
db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'),
port=os.getenv('FALKORDB_PORT', 6379),
username=os.getenv('FALKORDB_USERNAME', None),
password=os.getenv('FALKORDB_PASSWORD', None))
db = create_falkordb()

return name in db.list_graphs()

Expand All @@ -86,10 +82,7 @@ def get_repos() -> list[dict]:
single graph until the migration is run.
"""

db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'),
port=os.getenv('FALKORDB_PORT', 6379),
username=os.getenv('FALKORDB_USERNAME', None),
password=os.getenv('FALKORDB_PASSWORD', None))
db = create_falkordb()

repos = []
for g in db.list_graphs():
Expand Down Expand Up @@ -140,10 +133,7 @@ def __init__(self, name: str, branch: Optional[str] = None) -> None:
self.branch = branch or DEFAULT_BRANCH
self.name = compose_graph_name(self.project, self.branch)

self.db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'),
port=os.getenv('FALKORDB_PORT', 6379),
username=os.getenv('FALKORDB_USERNAME', None),
password=os.getenv('FALKORDB_PASSWORD', None))
self.db = create_falkordb()
self.g = self.db.select_graph(self.name)

# Initialize the backlog as disabled by default
Expand Down Expand Up @@ -180,10 +170,7 @@ def from_raw_name(cls, raw_name: str) -> "Graph":
obj.branch = DEFAULT_BRANCH
else:
obj.project, obj.branch = parsed
obj.db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'),
port=os.getenv('FALKORDB_PORT', 6379),
username=os.getenv('FALKORDB_USERNAME', None),
password=os.getenv('FALKORDB_PASSWORD', None))
obj.db = create_falkordb()
obj.g = obj.db.select_graph(raw_name)
obj.backlog = None
return obj
Expand Down Expand Up @@ -297,7 +284,7 @@ def _query(self, q: str, params: Optional[dict] = None) -> QueryResult:

return result_set

def get_sub_graph(self, l: int) -> dict:
def get_sub_graph(self, limit: int) -> dict:

q = """MATCH (src)
OPTIONAL MATCH (src)-[e]->(dest)
Expand All @@ -306,7 +293,7 @@ def get_sub_graph(self, l: int) -> dict:

sub_graph = {'nodes': [], 'edges': [] }

result_set = self._query(q, {'limit': l}).result_set
result_set = self._query(q, {'limit': limit}).result_set
for row in result_set:
src = row[0]
e = row[1]
Expand Down Expand Up @@ -592,7 +579,7 @@ def set_file_coverage(self, path: str, name: str, ext: str, coverage: float) ->

params = {'path': path, 'name': name, 'ext': ext, 'coverage': coverage}

res = self._query(q, params)
self._query(q, params)

def connect_entities(self, relation: str, src_id: int, dest_id: int, properties: dict = {}) -> None:
"""
Expand Down Expand Up @@ -789,14 +776,9 @@ def unreachable_entities(self, lbl: Optional[str], rel: Optional[str]) -> list[d
# Async helpers and read-only async graph wrapper
# ---------------------------------------------------------------------------

def _async_db() -> AsyncFalkorDB:
def _async_db():
"""Create an async FalkorDB connection using environment config."""
return AsyncFalkorDB(
host=os.getenv('FALKORDB_HOST', 'localhost'),
port=int(os.getenv('FALKORDB_PORT', 6379)),
username=os.getenv('FALKORDB_USERNAME', None),
password=os.getenv('FALKORDB_PASSWORD', None),
)
return create_async_falkordb()


async def async_graph_exists(name: str) -> bool:
Expand Down Expand Up @@ -952,4 +934,3 @@ async def stats(self) -> dict:

async def close(self) -> None:
await self.db.aclose()

Loading
Loading