Skip to content

Commit ad25cf8

Browse files
authored
feat(memory): Add Kùzu embedded graph database as alternative to Neo4j Docker (#1617)
feat(memory): Add Kùzu embedded graph database with seamless auto-install ## Summary Adds Kùzu as a zero-infrastructure alternative to Neo4j Docker for the memory system, with seamless auto-installation. ## Key Features - **Zero-config experience**: Kùzu auto-installs when needed (no separate `pip install amplihack[kuzu]` step) - **CLI flags**: `--graph-backend {kuzu,neo4j,auto}` for explicit backend selection - **Security opt-out**: `AMPLIHACK_NO_AUTO_INSTALL=1` env var to disable auto-install - **Same interface**: KuzuConnector matches Neo4jConnector for compatibility ## Backend Selection Priority 1. `AMPLIHACK_GRAPH_BACKEND` env var (explicit override) 2. Existing Neo4j container (if already running) 3. Kùzu (if installed or auto-install enabled) - preferred for simplicity 4. Neo4j Docker (if Docker available) ## Usage ```bash # Zero-config (auto-installs kuzu if needed) amplihack launch # Explicit kuzu backend amplihack launch --graph-backend kuzu # Disable auto-install export AMPLIHACK_NO_AUTO_INSTALL=1 amplihack launch ``` Closes #1613 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 956ff39 commit ad25cf8

File tree

8 files changed

+1169
-41
lines changed

8 files changed

+1169
-41
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ ui = [
2929
tui-testing = [
3030
"pytest-asyncio>=0.21.0",
3131
]
32+
kuzu = [
33+
"kuzu>=0.11.0", # Embedded graph database alternative to Neo4j Docker
34+
]
3235
dev = [
3336
"pytest>=7.0.0",
3437
"pytest-cov>=4.0.0",

src/amplihack/cli.py

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,23 @@ def launch_command(args: argparse.Namespace, claude_args: Optional[List[str]] =
9595
Returns:
9696
Exit code.
9797
"""
98-
# Handle backwards compatibility: Check for deprecated --use-graph-mem flag
99-
use_graph_mem = getattr(args, "use_graph_mem", False)
98+
# Handle graph backend selection (new unified approach)
99+
graph_backend = getattr(args, "graph_backend", "auto")
100100
enable_neo4j = getattr(args, "enable_neo4j_memory", False)
101-
102-
# Set environment variable for Neo4j opt-in (Why: Makes flag accessible to session hooks and launcher)
103-
if use_graph_mem or enable_neo4j:
101+
use_graph_mem = getattr(args, "use_graph_mem", False) # Deprecated
102+
103+
# Set environment variable for graph backend selection
104+
if graph_backend != "auto":
105+
os.environ["AMPLIHACK_GRAPH_BACKEND"] = graph_backend
106+
print(f"Graph backend set to: {graph_backend}")
107+
elif enable_neo4j or use_graph_mem:
108+
os.environ["AMPLIHACK_GRAPH_BACKEND"] = "neo4j"
104109
os.environ["AMPLIHACK_ENABLE_NEO4J_MEMORY"] = "1"
105110
if use_graph_mem:
106111
print(
107-
"WARNING: --use-graph-mem is deprecated. Please use --enable-neo4j-memory instead."
112+
"WARNING: --use-graph-mem is deprecated. Please use --graph-backend neo4j instead."
108113
)
109-
print("Neo4j graph memory enabled via --use-graph-mem flag (deprecated)")
110-
else:
111-
print("Neo4j graph memory enabled via --enable-neo4j-memory flag")
114+
print("Graph backend set to: neo4j")
112115

113116
# Set container name if provided
114117
if getattr(args, "use_memory_db", None):
@@ -244,7 +247,9 @@ def handle_auto_mode(
244247
# Extract timeout from args
245248
query_timeout = getattr(args, "query_timeout_minutes", 5.0)
246249

247-
auto = AutoMode(sdk, prompt, args.max_turns, ui_mode=ui_mode, query_timeout_minutes=query_timeout)
250+
auto = AutoMode(
251+
sdk, prompt, args.max_turns, ui_mode=ui_mode, query_timeout_minutes=query_timeout
252+
)
248253
return auto.run()
249254

250255

@@ -427,6 +432,30 @@ def add_neo4j_args(parser: argparse.ArgumentParser) -> None:
427432
)
428433

429434

435+
def add_graph_backend_args(parser: argparse.ArgumentParser) -> None:
436+
"""Add graph backend selection arguments to a parser.
437+
438+
Args:
439+
parser: ArgumentParser to add arguments to.
440+
"""
441+
parser.add_argument(
442+
"--graph-backend",
443+
choices=["kuzu", "neo4j", "auto"],
444+
default="auto",
445+
metavar="BACKEND",
446+
help=(
447+
"Select graph database backend for memory system. "
448+
"Options: kuzu (embedded, zero-config), neo4j (Docker), auto (default). "
449+
"Kùzu is auto-installed if needed."
450+
),
451+
)
452+
parser.add_argument(
453+
"--enable-neo4j-memory",
454+
action="store_true",
455+
help="Enable Neo4j graph memory (alias for --graph-backend neo4j).",
456+
)
457+
458+
430459
def create_parser() -> argparse.ArgumentParser:
431460
"""Create the argument parser for amplihack CLI.
432461
@@ -469,25 +498,27 @@ def create_parser() -> argparse.ArgumentParser:
469498
add_claude_specific_args(launch_parser)
470499
add_auto_mode_args(launch_parser)
471500
add_neo4j_args(launch_parser)
501+
add_graph_backend_args(launch_parser)
472502
add_common_sdk_args(launch_parser)
473503
launch_parser.add_argument(
474504
"--profile",
475505
type=str,
476506
default=None,
477-
help="Profile URI to use for this launch (overrides configured profile)"
507+
help="Profile URI to use for this launch (overrides configured profile)",
478508
)
479509

480510
# Claude command (alias for launch)
481511
claude_parser = subparsers.add_parser("claude", help="Launch Claude Code (alias for launch)")
482512
add_claude_specific_args(claude_parser)
483513
add_auto_mode_args(claude_parser)
484514
add_neo4j_args(claude_parser)
515+
add_graph_backend_args(claude_parser)
485516
add_common_sdk_args(claude_parser)
486517
claude_parser.add_argument(
487518
"--profile",
488519
type=str,
489520
default=None,
490-
help="Profile URI to use for this launch (overrides configured profile)"
521+
help="Profile URI to use for this launch (overrides configured profile)",
491522
)
492523

493524
# Copilot command
@@ -547,7 +578,7 @@ def create_parser() -> argparse.ArgumentParser:
547578
"--profile",
548579
type=str,
549580
default=None,
550-
help="Profile URI to use for this install (overrides configured profile)"
581+
help="Profile URI to use for this install (overrides configured profile)",
551582
)
552583

553584
return parser
@@ -610,7 +641,7 @@ def main(argv: Optional[List[str]] = None) -> int:
610641
# Find the amplihack package location
611642
import amplihack
612643

613-
from . import copytree_manifest, ESSENTIAL_DIRS
644+
from . import ESSENTIAL_DIRS, copytree_manifest
614645

615646
amplihack_src = os.path.dirname(os.path.abspath(amplihack.__file__))
616647

@@ -625,21 +656,19 @@ def main(argv: Optional[List[str]] = None) -> int:
625656
if os.path.exists(claude_tools_path):
626657
sys.path.insert(0, claude_tools_path)
627658
from profile_management.staging import create_staging_manifest
659+
628660
manifest = create_staging_manifest(ESSENTIAL_DIRS, profile_uri)
629-
if manifest.profile_name != "all" and not manifest.profile_name.endswith("(fallback)"):
661+
if manifest.profile_name != "all" and not manifest.profile_name.endswith(
662+
"(fallback)"
663+
):
630664
print(f"📦 Using profile: {manifest.profile_name}")
631665
except Exception as e:
632666
# Fall back to full staging on errors
633667
print(f"ℹ️ Profile loading failed ({e}), using full staging")
634668

635669
# Copy .claude contents to temp .claude directory
636670
# Note: copytree_manifest copies TO the dst, not INTO dst/.claude
637-
copied = copytree_manifest(
638-
amplihack_src,
639-
temp_claude_dir,
640-
".claude",
641-
manifest=manifest
642-
)
671+
copied = copytree_manifest(amplihack_src, temp_claude_dir, ".claude", manifest=manifest)
643672

644673
# Smart PROJECT.md initialization for UVX mode
645674
if copied:
@@ -672,12 +701,12 @@ def main(argv: Optional[List[str]] = None) -> int:
672701
def replace_paths(obj):
673702
if isinstance(obj, dict):
674703
for key, value in obj.items():
675-
if key == "command" and isinstance(value, str) and value.startswith(
676-
".claude/"
704+
if (
705+
key == "command"
706+
and isinstance(value, str)
707+
and value.startswith(".claude/")
677708
):
678-
obj[key] = value.replace(
679-
".claude/", "$CLAUDE_PROJECT_DIR/.claude/"
680-
)
709+
obj[key] = value.replace(".claude/", "$CLAUDE_PROJECT_DIR/.claude/")
681710
else:
682711
replace_paths(value)
683712
elif isinstance(obj, list):
@@ -761,21 +790,20 @@ def replace_paths(obj):
761790
# _local_install expects repo root, so pass package_dir (which contains .claude/)
762791
_local_install(str(package_dir))
763792
return 0
764-
else:
765-
# Fallback: Clone from GitHub (for old installations)
766-
import subprocess
767-
import tempfile
768-
769-
print("⚠️ Package .claude/ not found, cloning from GitHub...")
770-
with tempfile.TemporaryDirectory() as tmp:
771-
repo_url = "https://github.com/rysweet/MicrosoftHackathon2025-AgenticCoding"
772-
try:
773-
subprocess.check_call(["git", "clone", "--depth", "1", repo_url, tmp])
774-
_local_install(tmp)
775-
return 0
776-
except subprocess.CalledProcessError as e:
777-
print(f"Failed to install: {e}")
778-
return 1
793+
# Fallback: Clone from GitHub (for old installations)
794+
import subprocess
795+
import tempfile
796+
797+
print("⚠️ Package .claude/ not found, cloning from GitHub...")
798+
with tempfile.TemporaryDirectory() as tmp:
799+
repo_url = "https://github.com/rysweet/MicrosoftHackathon2025-AgenticCoding"
800+
try:
801+
subprocess.check_call(["git", "clone", "--depth", "1", repo_url, tmp])
802+
_local_install(tmp)
803+
return 0
804+
except subprocess.CalledProcessError as e:
805+
print(f"Failed to install: {e}")
806+
return 1
779807

780808
elif args.command == "uninstall":
781809
uninstall()

src/amplihack/memory/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
33
Provides persistent memory storage for AI agents with session isolation,
44
thread-safe operations, and efficient retrieval.
5+
6+
Supports multiple graph database backends:
7+
- Neo4j (Docker-based): Full-featured graph database
8+
- Kùzu (embedded): Zero-infrastructure, file-based graph database
9+
10+
Use auto_backend for automatic backend selection:
11+
from amplihack.memory.auto_backend import get_connector
12+
with get_connector() as conn:
13+
results = conn.execute_query("MATCH (n) RETURN count(n)")
514
"""
615

716
from .database import MemoryDatabase

0 commit comments

Comments
 (0)