nelgraph 1.0.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.
Files changed (37) hide show
  1. nelgraph-1.0.0/.gitignore +21 -0
  2. nelgraph-1.0.0/PKG-INFO +94 -0
  3. nelgraph-1.0.0/README.md +66 -0
  4. nelgraph-1.0.0/nelgraph/__init__.py +185 -0
  5. nelgraph-1.0.0/nelgraph/cli.py +144 -0
  6. nelgraph-1.0.0/nelgraph/community/__init__.py +1 -0
  7. nelgraph-1.0.0/nelgraph/community/detector.py +68 -0
  8. nelgraph-1.0.0/nelgraph/community/summarizer.py +208 -0
  9. nelgraph-1.0.0/nelgraph/config.py +79 -0
  10. nelgraph-1.0.0/nelgraph/core/__init__.py +1 -0
  11. nelgraph-1.0.0/nelgraph/core/init_pipeline.py +341 -0
  12. nelgraph-1.0.0/nelgraph/core/sync_pipeline.py +212 -0
  13. nelgraph-1.0.0/nelgraph/docker-compose.yml +24 -0
  14. nelgraph-1.0.0/nelgraph/embeddings/__init__.py +1 -0
  15. nelgraph-1.0.0/nelgraph/embeddings/chroma_client.py +222 -0
  16. nelgraph-1.0.0/nelgraph/embeddings/embedder.py +92 -0
  17. nelgraph-1.0.0/nelgraph/extractors/__init__.py +1 -0
  18. nelgraph-1.0.0/nelgraph/extractors/llm_extractor.py +182 -0
  19. nelgraph-1.0.0/nelgraph/extractors/testing_enricher.py +327 -0
  20. nelgraph-1.0.0/nelgraph/graph/__init__.py +1 -0
  21. nelgraph-1.0.0/nelgraph/graph/builder.py +274 -0
  22. nelgraph-1.0.0/nelgraph/graph/neo4j_client.py +161 -0
  23. nelgraph-1.0.0/nelgraph/graph/schema.py +43 -0
  24. nelgraph-1.0.0/nelgraph/initialize_graph.py +174 -0
  25. nelgraph-1.0.0/nelgraph/knowledge_base.py +255 -0
  26. nelgraph-1.0.0/nelgraph/parsers/__init__.py +1 -0
  27. nelgraph-1.0.0/nelgraph/parsers/ast_parser.py +11 -0
  28. nelgraph-1.0.0/nelgraph/parsers/base_parser.py +529 -0
  29. nelgraph-1.0.0/nelgraph/parsers/doc_parser.py +44 -0
  30. nelgraph-1.0.0/nelgraph/parsers/git_parser.py +105 -0
  31. nelgraph-1.0.0/nelgraph/parsers/php_parser.py +396 -0
  32. nelgraph-1.0.0/nelgraph/query/__init__.py +1 -0
  33. nelgraph-1.0.0/nelgraph/query/engine.py +157 -0
  34. nelgraph-1.0.0/nelgraph/updater/__init__.py +1 -0
  35. nelgraph-1.0.0/nelgraph/updater/git_hook.py +109 -0
  36. nelgraph-1.0.0/nelgraph/updater/watcher.py +70 -0
  37. nelgraph-1.0.0/pyproject.toml +42 -0
@@ -0,0 +1,21 @@
1
+ .venv/
2
+ chroma_db/
3
+ data/
4
+ .graphrag_data/
5
+ server_data/
6
+ workspace/
7
+ __pycache__/
8
+ *.pyc
9
+ .env
10
+ node_modules/
11
+ dist/
12
+ build/
13
+ *.log
14
+ .DS_Store
15
+ /updates/
16
+ ngrok.exe
17
+ ngrok_tmp.exe
18
+
19
+ # MCP compiled JS outputs
20
+ mcp/server.js
21
+ mcp/tools.js
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: nelgraph
3
+ Version: 1.0.0
4
+ Summary: GraphRAG knowledge base for codebases
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: chromadb
8
+ Requires-Dist: click
9
+ Requires-Dist: fastapi
10
+ Requires-Dist: gitpython
11
+ Requires-Dist: igraph
12
+ Requires-Dist: json-repair
13
+ Requires-Dist: leidenalg
14
+ Requires-Dist: neo4j
15
+ Requires-Dist: networkx
16
+ Requires-Dist: openai
17
+ Requires-Dist: python-dotenv
18
+ Requires-Dist: requests
19
+ Requires-Dist: rich
20
+ Requires-Dist: tree-sitter
21
+ Requires-Dist: tree-sitter-javascript
22
+ Requires-Dist: tree-sitter-php
23
+ Requires-Dist: tree-sitter-python
24
+ Requires-Dist: tree-sitter-typescript
25
+ Requires-Dist: uvicorn
26
+ Requires-Dist: watchdog
27
+ Description-Content-Type: text/markdown
28
+
29
+ # nelgraph 🚀
30
+
31
+ An autonomous, zero-configuration Knowledge Graph builder and semantic search engine optimized for local codebases and AI testing agents. It ingests source code, AST Call Graphs, and Git history into a hybrid Graph-Vector database (**Neo4j** + **ChromaDB**) using **DeepSeek V4-Flash**.
32
+
33
+ ## 🛠️ Installation
34
+
35
+ ```bash
36
+ pip install nelgraph
37
+ ```
38
+
39
+ ## 🚀 CLI Usage
40
+
41
+ ### 1. Initialize GraphRAG for your project
42
+ Navigate to your project directory and run:
43
+
44
+ ```bash
45
+ nelgraph init
46
+ ```
47
+
48
+ During initialization, it will prompt you for your OpenRouter API key, configure the local `.env` and `.graphrag_data/` directory, start Neo4j inside a local Docker container, build structural nodes and indexes, and automatically install a git post-commit hook so the graph auto-syncs.
49
+
50
+ ### 2. Manual Synchronization
51
+ If you want to manually trigger incremental synchronization:
52
+
53
+ ```bash
54
+ nelgraph sync
55
+ ```
56
+
57
+ ### 3. Check Status
58
+ View current database metrics, indexed function counts, and enrichment coverage:
59
+
60
+ ```bash
61
+ nelgraph status
62
+ ```
63
+
64
+ ### 4. Run Watcher
65
+ Run a file watcher that auto-syncs the graph in the background when files change:
66
+
67
+ ```bash
68
+ nelgraph watch
69
+ ```
70
+
71
+ ## 🔌 Programmatic Python API
72
+
73
+ You can import `nelgraph` directly into your scripts or AI testing agents:
74
+
75
+ ```python
76
+ import nelgraph
77
+
78
+ # Configure (if .env is not present or needs custom config)
79
+ nelgraph.configure(
80
+ codebase_path="/absolute/path/to/project",
81
+ openrouter_api_key="your-openrouter-key"
82
+ )
83
+
84
+ # 1. Get snapshot of prioritized functions
85
+ snapshot = nelgraph.get_snapshot()
86
+ print(f"Total functions: {snapshot['total']}")
87
+
88
+ # 2. Retrieve detailed context for a function
89
+ ctx = nelgraph.get_function_context("process_order")
90
+ print(ctx.get("function", {}).get("raw_code"))
91
+
92
+ # 3. Mark function as tested/verified
93
+ nelgraph.mark_tested("process_order")
94
+ ```
@@ -0,0 +1,66 @@
1
+ # nelgraph 🚀
2
+
3
+ An autonomous, zero-configuration Knowledge Graph builder and semantic search engine optimized for local codebases and AI testing agents. It ingests source code, AST Call Graphs, and Git history into a hybrid Graph-Vector database (**Neo4j** + **ChromaDB**) using **DeepSeek V4-Flash**.
4
+
5
+ ## 🛠️ Installation
6
+
7
+ ```bash
8
+ pip install nelgraph
9
+ ```
10
+
11
+ ## 🚀 CLI Usage
12
+
13
+ ### 1. Initialize GraphRAG for your project
14
+ Navigate to your project directory and run:
15
+
16
+ ```bash
17
+ nelgraph init
18
+ ```
19
+
20
+ During initialization, it will prompt you for your OpenRouter API key, configure the local `.env` and `.graphrag_data/` directory, start Neo4j inside a local Docker container, build structural nodes and indexes, and automatically install a git post-commit hook so the graph auto-syncs.
21
+
22
+ ### 2. Manual Synchronization
23
+ If you want to manually trigger incremental synchronization:
24
+
25
+ ```bash
26
+ nelgraph sync
27
+ ```
28
+
29
+ ### 3. Check Status
30
+ View current database metrics, indexed function counts, and enrichment coverage:
31
+
32
+ ```bash
33
+ nelgraph status
34
+ ```
35
+
36
+ ### 4. Run Watcher
37
+ Run a file watcher that auto-syncs the graph in the background when files change:
38
+
39
+ ```bash
40
+ nelgraph watch
41
+ ```
42
+
43
+ ## 🔌 Programmatic Python API
44
+
45
+ You can import `nelgraph` directly into your scripts or AI testing agents:
46
+
47
+ ```python
48
+ import nelgraph
49
+
50
+ # Configure (if .env is not present or needs custom config)
51
+ nelgraph.configure(
52
+ codebase_path="/absolute/path/to/project",
53
+ openrouter_api_key="your-openrouter-key"
54
+ )
55
+
56
+ # 1. Get snapshot of prioritized functions
57
+ snapshot = nelgraph.get_snapshot()
58
+ print(f"Total functions: {snapshot['total']}")
59
+
60
+ # 2. Retrieve detailed context for a function
61
+ ctx = nelgraph.get_function_context("process_order")
62
+ print(ctx.get("function", {}).get("raw_code"))
63
+
64
+ # 3. Mark function as tested/verified
65
+ nelgraph.mark_tested("process_order")
66
+ ```
@@ -0,0 +1,185 @@
1
+ """
2
+ GraphRAG Knowledge Base — Internal Python Module
3
+
4
+ Cách dùng nhanh nhất:
5
+
6
+ import graphrag
7
+ graphrag.configure(codebase_path="/path/to/project", openrouter_api_key="sk-...")
8
+ graphrag.run_init()
9
+
10
+ ctx = graphrag.get_function_context("processOrder")
11
+ snap = graphrag.get_snapshot()
12
+ changes = graphrag.get_changes("abc123f")
13
+ graphrag.mark_tested("processOrder")
14
+ """
15
+
16
+ # --- Public API ---
17
+ from nelgraph.knowledge_base import (
18
+ get_function_context,
19
+ get_snapshot,
20
+ get_changes,
21
+ mark_tested,
22
+ search,
23
+ run_init,
24
+ run_sync,
25
+ )
26
+
27
+ __version__ = "1.0.0"
28
+
29
+ __all__ = [
30
+ "configure",
31
+ "get_function_context",
32
+ "get_snapshot",
33
+ "get_changes",
34
+ "mark_tested",
35
+ "search",
36
+ "run_init",
37
+ "run_sync",
38
+ ]
39
+
40
+
41
+ def configure(
42
+ codebase_path: str = None,
43
+ openrouter_api_key: str = None,
44
+ neo4j_uri: str = None,
45
+ neo4j_password: str = None,
46
+ neo4j_user: str = None,
47
+ llm_model: str = None,
48
+ embedding_model: str = None,
49
+ embedding_dimensions: int = None,
50
+ ):
51
+ """
52
+ Cấu hình graphrag bằng code thay vì .env file.
53
+ Gọi hàm này TRƯỚC khi dùng bất kỳ function nào khác.
54
+
55
+ Args:
56
+ codebase_path: Đường dẫn tuyệt đối đến codebase cần analyze.
57
+ openrouter_api_key: API key của OpenRouter.
58
+ neo4j_uri: URI kết nối Neo4j (default: bolt://127.0.0.1:7687).
59
+ neo4j_password: Password Neo4j.
60
+ neo4j_user: Username Neo4j (default: neo4j).
61
+ llm_model: Model ID trên OpenRouter cho LLM enrichment.
62
+ embedding_model: Model ID trên OpenRouter cho embeddings.
63
+ embedding_dimensions: Số chiều vector (default: 512).
64
+
65
+ Ví dụ:
66
+ graphrag.configure(
67
+ codebase_path="/home/user/opensourcepos",
68
+ openrouter_api_key="sk-or-...",
69
+ )
70
+ """
71
+ import os
72
+ import nelgraph.config as _cfg
73
+
74
+ if codebase_path:
75
+ import os as _os
76
+ codebase_path = _os.path.abspath(codebase_path).replace("\\", "/")
77
+ _cfg.CODEBASE_PATH = codebase_path
78
+ os.environ["CODEBASE_PATH"] = codebase_path
79
+
80
+ # Recalculate dependent paths
81
+ _cfg.GRAPHRAG_DATA_DIR = _os.path.join(codebase_path, ".graphrag_data").replace("\\", "/")
82
+ _cfg.NEO4J_DATA_DIR = _os.path.join(_cfg.GRAPHRAG_DATA_DIR, "neo4j", "data").replace("\\", "/")
83
+ _cfg.NEO4J_LOGS_DIR = _os.path.join(_cfg.GRAPHRAG_DATA_DIR, "neo4j", "logs").replace("\\", "/")
84
+ _cfg.CHROMA_PATH = _os.path.join(_cfg.GRAPHRAG_DATA_DIR, "chromadb").replace("\\", "/")
85
+ _cfg.SYNC_STATE_PATH = _os.path.join(_cfg.GRAPHRAG_DATA_DIR, "sync_state.json").replace("\\", "/")
86
+
87
+ if openrouter_api_key:
88
+ _cfg.OPENROUTER_API_KEY = openrouter_api_key
89
+ os.environ["OPENROUTER_API_KEY"] = openrouter_api_key
90
+ # Reset lazy clients so they pick up the new key
91
+ _reset_ai_clients()
92
+
93
+ if neo4j_uri:
94
+ _cfg.NEO4J_URI = neo4j_uri
95
+ os.environ["NEO4J_URI"] = neo4j_uri
96
+
97
+ if neo4j_password:
98
+ _cfg.NEO4J_PASSWORD = neo4j_password
99
+ os.environ["NEO4J_PASSWORD"] = neo4j_password
100
+ # Reset Neo4j singleton
101
+ import nelgraph.graph.neo4j_client as _nc
102
+ _nc._client = None
103
+
104
+ if neo4j_user:
105
+ _cfg.NEO4J_USER = neo4j_user
106
+
107
+ if llm_model:
108
+ _cfg.LLM_MODEL = llm_model
109
+
110
+ if embedding_model:
111
+ _cfg.EMBEDDING_MODEL = embedding_model
112
+
113
+ if embedding_dimensions:
114
+ _cfg.EMBEDDING_DIMENSIONS = embedding_dimensions
115
+
116
+
117
+ def _reset_ai_clients():
118
+ """Reset tất cả lazy OpenAI client singletons để pick up config mới."""
119
+ try:
120
+ import nelgraph.embeddings.embedder as _emb
121
+ _emb._openai_client = None
122
+ except Exception:
123
+ pass
124
+ try:
125
+ import nelgraph.extractors.testing_enricher as _te
126
+ _te._client_ai = None
127
+ except Exception:
128
+ pass
129
+ try:
130
+ import nelgraph.extractors.llm_extractor as _le
131
+ _le._client = None
132
+ except Exception:
133
+ pass
134
+ try:
135
+ import nelgraph.community.summarizer as _sm
136
+ _sm._client_ai = None
137
+ except Exception:
138
+ pass
139
+
140
+
141
+ def status() -> dict:
142
+ """
143
+ Trả về trạng thái hiện tại của graph.
144
+ Không cần Neo4j đang chạy — nếu không kết nối được thì báo offline.
145
+
146
+ Returns:
147
+ {
148
+ "neo4j": "connected" | "offline",
149
+ "codebase_path": "...",
150
+ "last_sync": "...",
151
+ "total_functions": 0,
152
+ "enriched_functions": 0,
153
+ }
154
+ """
155
+ import nelgraph.config as _cfg
156
+ from nelgraph.initialize_graph import _load_sync_state
157
+
158
+ result = {
159
+ "neo4j": "offline",
160
+ "codebase_path": _cfg.CODEBASE_PATH,
161
+ "last_sync": None,
162
+ "total_functions": 0,
163
+ "enriched_functions": 0,
164
+ }
165
+
166
+ sync_state = _load_sync_state()
167
+ if sync_state:
168
+ result["last_sync"] = sync_state.get("last_sync_time")
169
+
170
+ try:
171
+ from nelgraph.graph.neo4j_client import get_client
172
+ client = get_client()
173
+ stats = client.run("""
174
+ OPTIONAL MATCH (f:Function) WITH count(f) as total
175
+ OPTIONAL MATCH (f2:Function) WHERE f2.how_it_works IS NOT NULL
176
+ RETURN total, count(f2) as enriched
177
+ """)
178
+ if stats:
179
+ result["neo4j"] = "connected"
180
+ result["total_functions"] = stats[0]["total"]
181
+ result["enriched_functions"] = stats[0]["enriched"]
182
+ except Exception:
183
+ pass
184
+
185
+ return result
@@ -0,0 +1,144 @@
1
+ import click
2
+ from rich.console import Console
3
+ import os
4
+ import sys
5
+ import requests
6
+ import nelgraph
7
+
8
+ if sys.platform.startswith("win"):
9
+ try:
10
+ sys.stdout.reconfigure(encoding="utf-8")
11
+ sys.stderr.reconfigure(encoding="utf-8")
12
+ except Exception:
13
+ pass
14
+
15
+ console = Console()
16
+
17
+ def _check_for_updates():
18
+ try:
19
+ # Check PyPI version dynamically (timeout after 2s to prevent CLI blocking)
20
+ res = requests.get("https://pypi.org/pypi/nelgraph/json", timeout=2)
21
+ latest = res.json()["info"]["version"]
22
+ from nelgraph import __version__
23
+ if latest != __version__:
24
+ console.print(
25
+ f"[yellow]Update available: {__version__} → {latest}[/yellow]\n"
26
+ f"Run: [bold]pip install --upgrade nelgraph[/bold]"
27
+ )
28
+ except Exception:
29
+ pass
30
+
31
+ @click.group()
32
+ def main():
33
+ """nelgraph — Codebase Knowledge Graph & Semantic Search CLI"""
34
+ _check_for_updates()
35
+
36
+ def _install_git_hook(codebase_path: str):
37
+ git_dir = os.path.join(codebase_path, ".git")
38
+ if not os.path.exists(git_dir):
39
+ return
40
+ hooks_dir = os.path.join(git_dir, "hooks")
41
+ os.makedirs(hooks_dir, exist_ok=True)
42
+ hook_path = os.path.join(hooks_dir, "post-commit")
43
+
44
+ # Simple shell hook to run nelgraph sync in background silently
45
+ hook_content = """#!/bin/sh
46
+ # Auto-sync graph in background after commit
47
+ nelgraph sync --silent &
48
+ """
49
+ try:
50
+ with open(hook_path, "w", newline="\n", encoding="utf-8") as f:
51
+ f.write(hook_content)
52
+ # Make hook executable
53
+ import stat
54
+ st = os.stat(hook_path)
55
+ os.chmod(hook_path, st.st_mode | stat.S_IEXEC)
56
+ console.print("[green]✓ Git post-commit hook installed successfully.[/green]")
57
+ except Exception as e:
58
+ console.print(f"[yellow]⚠ Warning: Could not install Git post-commit hook: {e}[/yellow]")
59
+
60
+ @main.command()
61
+ @click.option("--key", help="OpenRouter API key")
62
+ @click.option("--path", default=".", help="Path to codebase (default: current dir)")
63
+ def init(key, path):
64
+ """
65
+ Khởi tạo GraphRAG cho project hiện tại.
66
+ Tạo .env, start Neo4j Docker, parse + enrich toàn bộ codebase.
67
+ """
68
+ abs_path = os.path.abspath(path).replace("\\", "/")
69
+
70
+ # Load env from target path if exists
71
+ from dotenv import load_dotenv
72
+ env_path = os.path.join(abs_path, ".env")
73
+ if os.path.exists(env_path):
74
+ load_dotenv(env_path)
75
+
76
+ api_key = key or os.getenv("OPENROUTER_API_KEY")
77
+ if not api_key:
78
+ api_key = click.prompt("OpenRouter API key", hide_input=True)
79
+
80
+ # Write or update .env in target directory
81
+ lines = []
82
+ if os.path.exists(env_path):
83
+ with open(env_path, "r", encoding="utf-8") as f:
84
+ lines = f.readlines()
85
+
86
+ has_key = False
87
+ has_path = False
88
+ new_lines = []
89
+ for line in lines:
90
+ if line.strip().startswith("OPENROUTER_API_KEY="):
91
+ new_lines.append(f"OPENROUTER_API_KEY={api_key}\n")
92
+ has_key = True
93
+ elif line.strip().startswith("CODEBASE_PATH="):
94
+ new_lines.append(f"CODEBASE_PATH={abs_path}\n")
95
+ has_path = True
96
+ else:
97
+ new_lines.append(line)
98
+
99
+ if not has_key:
100
+ new_lines.append(f"OPENROUTER_API_KEY={api_key}\n")
101
+ if not has_path:
102
+ new_lines.append(f"CODEBASE_PATH={abs_path}\n")
103
+
104
+ with open(env_path, "w", encoding="utf-8") as f:
105
+ f.writelines(new_lines)
106
+
107
+ console.print(f"[green]✓ Configured {env_path}[/green]")
108
+
109
+ # Programmatically configure nelgraph
110
+ nelgraph.configure(codebase_path=abs_path, openrouter_api_key=api_key)
111
+
112
+ # Run full initialization pipeline
113
+ nelgraph.run_init()
114
+
115
+ # Install git post-commit hook
116
+ _install_git_hook(abs_path)
117
+
118
+ @main.command()
119
+ @click.option("--silent", is_flag=True, help="Run silently without printing to stdout")
120
+ def sync(silent):
121
+ """Sync thủ công — parse files đã thay đổi kể từ lần sync cuối."""
122
+ if silent:
123
+ # Redirect stdout/stderr to devnull
124
+ sys.stdout = open(os.devnull, 'w')
125
+ sys.stderr = open(os.devnull, 'w')
126
+
127
+ # Run sync pipeline
128
+ nelgraph.run_sync()
129
+
130
+ @main.command()
131
+ def status():
132
+ """Xem trạng thái graph hiện tại."""
133
+ # Run status helper
134
+ from nelgraph.initialize_graph import run_status
135
+ run_status()
136
+
137
+ @main.command()
138
+ def watch():
139
+ """Chạy file watcher — tự sync khi có file thay đổi."""
140
+ from nelgraph.updater.watcher import start_watcher
141
+ start_watcher()
142
+
143
+ if __name__ == "__main__":
144
+ main()
@@ -0,0 +1 @@
1
+ # GraphRAG Community Detection and Summarization Package
@@ -0,0 +1,68 @@
1
+ import networkx as nx
2
+ import igraph as ig
3
+ import leidenalg
4
+ from nelgraph.graph.neo4j_client import get_client
5
+
6
+
7
+ def build_networkx_graph() -> nx.Graph:
8
+ """Convert Neo4j graph to NetworkX graph for algorithms."""
9
+ client = get_client()
10
+
11
+ G = nx.Graph()
12
+
13
+ # Get all nodes
14
+ nodes = client.run("MATCH (n) WHERE n.name IS NOT NULL RETURN elementId(n) as id, labels(n) as labels, n.name as name")
15
+ for record in nodes:
16
+ G.add_node(record["id"], name=record["name"], label=record["labels"][0] if record["labels"] else "Unknown")
17
+
18
+ # Get all edges
19
+ edges = client.run("MATCH (a)-[r]->(b) WHERE a.name IS NOT NULL AND b.name IS NOT NULL RETURN elementId(a) as from_id, elementId(b) as to_id, type(r) as rel_type")
20
+ for record in edges:
21
+ G.add_edge(record["from_id"], record["to_id"], rel_type=record["rel_type"])
22
+
23
+ print(f"[Community] NetworkX graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges")
24
+ return G
25
+
26
+
27
+ def detect_communities() -> dict:
28
+ """
29
+ Run Leiden algorithm via igraph/leidenalg.
30
+ Returns mapping node_id -> community_id.
31
+ Saves community_id to Neo4j nodes.
32
+ """
33
+ G = build_networkx_graph()
34
+ if G.number_of_nodes() == 0:
35
+ print("[Community] Empty graph, skipping community detection.")
36
+ return {}
37
+
38
+ # Convert NetworkX to igraph
39
+ nx_nodes = list(G.nodes())
40
+ node_id_map = {n: i for i, n in enumerate(nx_nodes)}
41
+
42
+ ig_graph = ig.Graph()
43
+ ig_graph.add_vertices(len(nx_nodes))
44
+
45
+ for u, v in G.edges():
46
+ ig_graph.add_edge(node_id_map[u], node_id_map[v])
47
+
48
+ # Run Leiden algorithm
49
+ partition = leidenalg.find_partition(ig_graph, leidenalg.ModularityVertexPartition)
50
+
51
+ # Build result mapping: neo4j_node_id -> community_id
52
+ result = {}
53
+ for community_id, members in enumerate(partition):
54
+ for member_idx in members:
55
+ neo4j_id = nx_nodes[member_idx]
56
+ result[neo4j_id] = community_id
57
+
58
+ print(f"[Community] Detected {len(partition)} communities.")
59
+
60
+ # Save community IDs to Neo4j
61
+ client = get_client()
62
+ for node_id, community_id in result.items():
63
+ client.run("""
64
+ MATCH (n) WHERE elementId(n) = $node_id
65
+ SET n.community_id = $community_id
66
+ """, {"node_id": node_id, "community_id": community_id})
67
+
68
+ return result