Merge pull request #1003 from danielaskdd/add-graph-search-mode
Browse files- lightrag/api/routers/graph_routes.py +15 -6
- lightrag/api/webui/assets/{index-rP-YlyR1.css → index-CH-3l4_Z.css} +0 -0
- lightrag/api/webui/assets/{index-DbuMPJAD.js → index-CJz72b6Q.js} +0 -0
- lightrag/api/webui/index.html +0 -0
- lightrag/base.py +1 -1
- lightrag/kg/networkx_impl.py +47 -20
- lightrag/lightrag.py +32 -4
- lightrag_webui/src/api/lightrag.ts +6 -2
- lightrag_webui/src/components/graph/GraphControl.tsx +10 -7
- lightrag_webui/src/components/graph/Settings.tsx +17 -2
- lightrag_webui/src/components/ui/Input.tsx +1 -1
- lightrag_webui/src/hooks/useLightragGraph.tsx +10 -6
- lightrag_webui/src/stores/settings.ts +6 -0
- lightrag_webui/src/vite-env.d.ts +10 -0
- lightrag_webui/tsconfig.json +1 -1
- lightrag_webui/vite.config.ts +16 -1
lightrag/api/routers/graph_routes.py
CHANGED
|
@@ -3,7 +3,6 @@ This module contains all graph-related routes for the LightRAG API.
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
from typing import Optional
|
| 6 |
-
|
| 7 |
from fastapi import APIRouter, Depends
|
| 8 |
|
| 9 |
from ..utils_api import get_api_key_dependency, get_auth_dependency
|
|
@@ -25,23 +24,33 @@ def create_graph_routes(rag, api_key: Optional[str] = None):
|
|
| 25 |
return await rag.get_graph_labels()
|
| 26 |
|
| 27 |
@router.get("/graphs", dependencies=[Depends(optional_api_key)])
|
| 28 |
-
async def get_knowledge_graph(
|
|
|
|
|
|
|
| 29 |
"""
|
| 30 |
Retrieve a connected subgraph of nodes where the label includes the specified label.
|
| 31 |
Maximum number of nodes is constrained by the environment variable `MAX_GRAPH_NODES` (default: 1000).
|
| 32 |
When reducing the number of nodes, the prioritization criteria are as follows:
|
| 33 |
-
1.
|
| 34 |
-
2.
|
| 35 |
-
3.
|
|
|
|
| 36 |
Maximum number of nodes is limited to env MAX_GRAPH_NODES(default: 1000)
|
| 37 |
|
| 38 |
Args:
|
| 39 |
label (str): Label to get knowledge graph for
|
| 40 |
max_depth (int, optional): Maximum depth of graph. Defaults to 3.
|
|
|
|
|
|
|
| 41 |
|
| 42 |
Returns:
|
| 43 |
Dict[str, List[str]]: Knowledge graph for label
|
| 44 |
"""
|
| 45 |
-
return await rag.get_knowledge_graph(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
return router
|
|
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
from typing import Optional
|
|
|
|
| 6 |
from fastapi import APIRouter, Depends
|
| 7 |
|
| 8 |
from ..utils_api import get_api_key_dependency, get_auth_dependency
|
|
|
|
| 24 |
return await rag.get_graph_labels()
|
| 25 |
|
| 26 |
@router.get("/graphs", dependencies=[Depends(optional_api_key)])
|
| 27 |
+
async def get_knowledge_graph(
|
| 28 |
+
label: str, max_depth: int = 3, min_degree: int = 0, inclusive: bool = False
|
| 29 |
+
):
|
| 30 |
"""
|
| 31 |
Retrieve a connected subgraph of nodes where the label includes the specified label.
|
| 32 |
Maximum number of nodes is constrained by the environment variable `MAX_GRAPH_NODES` (default: 1000).
|
| 33 |
When reducing the number of nodes, the prioritization criteria are as follows:
|
| 34 |
+
1. min_degree does not affect nodes directly connected to the matching nodes
|
| 35 |
+
2. Label matching nodes take precedence
|
| 36 |
+
3. Followed by nodes directly connected to the matching nodes
|
| 37 |
+
4. Finally, the degree of the nodes
|
| 38 |
Maximum number of nodes is limited to env MAX_GRAPH_NODES(default: 1000)
|
| 39 |
|
| 40 |
Args:
|
| 41 |
label (str): Label to get knowledge graph for
|
| 42 |
max_depth (int, optional): Maximum depth of graph. Defaults to 3.
|
| 43 |
+
inclusive_search (bool, optional): If True, search for nodes that include the label. Defaults to False.
|
| 44 |
+
min_degree (int, optional): Minimum degree of nodes. Defaults to 0.
|
| 45 |
|
| 46 |
Returns:
|
| 47 |
Dict[str, List[str]]: Knowledge graph for label
|
| 48 |
"""
|
| 49 |
+
return await rag.get_knowledge_graph(
|
| 50 |
+
node_label=label,
|
| 51 |
+
max_depth=max_depth,
|
| 52 |
+
inclusive=inclusive,
|
| 53 |
+
min_degree=min_degree,
|
| 54 |
+
)
|
| 55 |
|
| 56 |
return router
|
lightrag/api/webui/assets/{index-rP-YlyR1.css → index-CH-3l4_Z.css}
RENAMED
|
Binary files a/lightrag/api/webui/assets/index-rP-YlyR1.css and b/lightrag/api/webui/assets/index-CH-3l4_Z.css differ
|
|
|
lightrag/api/webui/assets/{index-DbuMPJAD.js → index-CJz72b6Q.js}
RENAMED
|
Binary files a/lightrag/api/webui/assets/index-DbuMPJAD.js and b/lightrag/api/webui/assets/index-CJz72b6Q.js differ
|
|
|
lightrag/api/webui/index.html
CHANGED
|
Binary files a/lightrag/api/webui/index.html and b/lightrag/api/webui/index.html differ
|
|
|
lightrag/base.py
CHANGED
|
@@ -204,7 +204,7 @@ class BaseGraphStorage(StorageNameSpace, ABC):
|
|
| 204 |
|
| 205 |
@abstractmethod
|
| 206 |
async def get_knowledge_graph(
|
| 207 |
-
self, node_label: str, max_depth: int =
|
| 208 |
) -> KnowledgeGraph:
|
| 209 |
"""Retrieve a subgraph of the knowledge graph starting from a given node."""
|
| 210 |
|
|
|
|
| 204 |
|
| 205 |
@abstractmethod
|
| 206 |
async def get_knowledge_graph(
|
| 207 |
+
self, node_label: str, max_depth: int = 3
|
| 208 |
) -> KnowledgeGraph:
|
| 209 |
"""Retrieve a subgraph of the knowledge graph starting from a given node."""
|
| 210 |
|
lightrag/kg/networkx_impl.py
CHANGED
|
@@ -232,19 +232,26 @@ class NetworkXStorage(BaseGraphStorage):
|
|
| 232 |
return sorted(list(labels))
|
| 233 |
|
| 234 |
async def get_knowledge_graph(
|
| 235 |
-
self,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
) -> KnowledgeGraph:
|
| 237 |
"""
|
| 238 |
Retrieve a connected subgraph of nodes where the label includes the specified `node_label`.
|
| 239 |
Maximum number of nodes is constrained by the environment variable `MAX_GRAPH_NODES` (default: 1000).
|
| 240 |
When reducing the number of nodes, the prioritization criteria are as follows:
|
| 241 |
-
1.
|
| 242 |
-
2.
|
| 243 |
-
3.
|
|
|
|
| 244 |
|
| 245 |
Args:
|
| 246 |
node_label: Label of the starting node
|
| 247 |
max_depth: Maximum depth of the subgraph
|
|
|
|
|
|
|
| 248 |
|
| 249 |
Returns:
|
| 250 |
KnowledgeGraph object containing nodes and edges
|
|
@@ -255,6 +262,10 @@ class NetworkXStorage(BaseGraphStorage):
|
|
| 255 |
|
| 256 |
graph = await self._get_graph()
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
# Handle special case for "*" label
|
| 259 |
if node_label == "*":
|
| 260 |
# For "*", return the entire graph including all nodes and edges
|
|
@@ -262,11 +273,16 @@ class NetworkXStorage(BaseGraphStorage):
|
|
| 262 |
graph.copy()
|
| 263 |
) # Create a copy to avoid modifying the original graph
|
| 264 |
else:
|
| 265 |
-
# Find nodes with matching node id
|
| 266 |
nodes_to_explore = []
|
| 267 |
for n, attr in graph.nodes(data=True):
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
if not nodes_to_explore:
|
| 272 |
logger.warning(f"No nodes found with label {node_label}")
|
|
@@ -277,26 +293,37 @@ class NetworkXStorage(BaseGraphStorage):
|
|
| 277 |
for start_node in nodes_to_explore:
|
| 278 |
node_subgraph = nx.ego_graph(graph, start_node, radius=max_depth)
|
| 279 |
combined_subgraph = nx.compose(combined_subgraph, node_subgraph)
|
| 280 |
-
subgraph = combined_subgraph
|
| 281 |
-
|
| 282 |
-
# Check if number of nodes exceeds max_graph_nodes
|
| 283 |
-
if len(subgraph.nodes()) > MAX_GRAPH_NODES:
|
| 284 |
-
origin_nodes = len(subgraph.nodes())
|
| 285 |
-
|
| 286 |
-
node_degrees = dict(subgraph.degree())
|
| 287 |
-
|
| 288 |
-
start_nodes = set()
|
| 289 |
-
direct_connected_nodes = set()
|
| 290 |
|
| 291 |
-
|
|
|
|
| 292 |
start_nodes = set(nodes_to_explore)
|
| 293 |
# Get nodes directly connected to all start nodes
|
| 294 |
for start_node in start_nodes:
|
| 295 |
-
direct_connected_nodes.update(
|
|
|
|
|
|
|
| 296 |
|
| 297 |
# Remove start nodes from directly connected nodes (avoid duplicates)
|
| 298 |
direct_connected_nodes -= start_nodes
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
def priority_key(node_item):
|
| 301 |
node, degree = node_item
|
| 302 |
# Priority order: start(2) > directly connected(1) > other nodes(0)
|
|
@@ -356,7 +383,7 @@ class NetworkXStorage(BaseGraphStorage):
|
|
| 356 |
result.edges.append(
|
| 357 |
KnowledgeGraphEdge(
|
| 358 |
id=edge_id,
|
| 359 |
-
type="
|
| 360 |
source=str(source),
|
| 361 |
target=str(target),
|
| 362 |
properties=edge_data,
|
|
|
|
| 232 |
return sorted(list(labels))
|
| 233 |
|
| 234 |
async def get_knowledge_graph(
|
| 235 |
+
self,
|
| 236 |
+
node_label: str,
|
| 237 |
+
max_depth: int = 3,
|
| 238 |
+
min_degree: int = 0,
|
| 239 |
+
inclusive: bool = False,
|
| 240 |
) -> KnowledgeGraph:
|
| 241 |
"""
|
| 242 |
Retrieve a connected subgraph of nodes where the label includes the specified `node_label`.
|
| 243 |
Maximum number of nodes is constrained by the environment variable `MAX_GRAPH_NODES` (default: 1000).
|
| 244 |
When reducing the number of nodes, the prioritization criteria are as follows:
|
| 245 |
+
1. min_degree does not affect nodes directly connected to the matching nodes
|
| 246 |
+
2. Label matching nodes take precedence
|
| 247 |
+
3. Followed by nodes directly connected to the matching nodes
|
| 248 |
+
4. Finally, the degree of the nodes
|
| 249 |
|
| 250 |
Args:
|
| 251 |
node_label: Label of the starting node
|
| 252 |
max_depth: Maximum depth of the subgraph
|
| 253 |
+
min_degree: Minimum degree of nodes to include. Defaults to 0
|
| 254 |
+
inclusive: Do an inclusive search if true
|
| 255 |
|
| 256 |
Returns:
|
| 257 |
KnowledgeGraph object containing nodes and edges
|
|
|
|
| 262 |
|
| 263 |
graph = await self._get_graph()
|
| 264 |
|
| 265 |
+
# Initialize sets for start nodes and direct connected nodes
|
| 266 |
+
start_nodes = set()
|
| 267 |
+
direct_connected_nodes = set()
|
| 268 |
+
|
| 269 |
# Handle special case for "*" label
|
| 270 |
if node_label == "*":
|
| 271 |
# For "*", return the entire graph including all nodes and edges
|
|
|
|
| 273 |
graph.copy()
|
| 274 |
) # Create a copy to avoid modifying the original graph
|
| 275 |
else:
|
| 276 |
+
# Find nodes with matching node id based on search_mode
|
| 277 |
nodes_to_explore = []
|
| 278 |
for n, attr in graph.nodes(data=True):
|
| 279 |
+
node_str = str(n)
|
| 280 |
+
if not inclusive:
|
| 281 |
+
if node_label == node_str: # Use exact matching
|
| 282 |
+
nodes_to_explore.append(n)
|
| 283 |
+
else: # inclusive mode
|
| 284 |
+
if node_label in node_str: # Use partial matching
|
| 285 |
+
nodes_to_explore.append(n)
|
| 286 |
|
| 287 |
if not nodes_to_explore:
|
| 288 |
logger.warning(f"No nodes found with label {node_label}")
|
|
|
|
| 293 |
for start_node in nodes_to_explore:
|
| 294 |
node_subgraph = nx.ego_graph(graph, start_node, radius=max_depth)
|
| 295 |
combined_subgraph = nx.compose(combined_subgraph, node_subgraph)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
+
# Get start nodes and direct connected nodes
|
| 298 |
+
if nodes_to_explore:
|
| 299 |
start_nodes = set(nodes_to_explore)
|
| 300 |
# Get nodes directly connected to all start nodes
|
| 301 |
for start_node in start_nodes:
|
| 302 |
+
direct_connected_nodes.update(
|
| 303 |
+
combined_subgraph.neighbors(start_node)
|
| 304 |
+
)
|
| 305 |
|
| 306 |
# Remove start nodes from directly connected nodes (avoid duplicates)
|
| 307 |
direct_connected_nodes -= start_nodes
|
| 308 |
|
| 309 |
+
subgraph = combined_subgraph
|
| 310 |
+
|
| 311 |
+
# Filter nodes based on min_degree, but keep start nodes and direct connected nodes
|
| 312 |
+
if min_degree > 0:
|
| 313 |
+
nodes_to_keep = [
|
| 314 |
+
node
|
| 315 |
+
for node, degree in subgraph.degree()
|
| 316 |
+
if node in start_nodes
|
| 317 |
+
or node in direct_connected_nodes
|
| 318 |
+
or degree >= min_degree
|
| 319 |
+
]
|
| 320 |
+
subgraph = subgraph.subgraph(nodes_to_keep)
|
| 321 |
+
|
| 322 |
+
# Check if number of nodes exceeds max_graph_nodes
|
| 323 |
+
if len(subgraph.nodes()) > MAX_GRAPH_NODES:
|
| 324 |
+
origin_nodes = len(subgraph.nodes())
|
| 325 |
+
node_degrees = dict(subgraph.degree())
|
| 326 |
+
|
| 327 |
def priority_key(node_item):
|
| 328 |
node, degree = node_item
|
| 329 |
# Priority order: start(2) > directly connected(1) > other nodes(0)
|
|
|
|
| 383 |
result.edges.append(
|
| 384 |
KnowledgeGraphEdge(
|
| 385 |
id=edge_id,
|
| 386 |
+
type="DIRECTED",
|
| 387 |
source=str(source),
|
| 388 |
target=str(target),
|
| 389 |
properties=edge_data,
|
lightrag/lightrag.py
CHANGED
|
@@ -504,11 +504,39 @@ class LightRAG:
|
|
| 504 |
return text
|
| 505 |
|
| 506 |
async def get_knowledge_graph(
|
| 507 |
-
self,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
) -> KnowledgeGraph:
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
def _get_storage_class(self, storage_name: str) -> Callable[..., Any]:
|
| 514 |
import_path = STORAGES[storage_name]
|
|
|
|
| 504 |
return text
|
| 505 |
|
| 506 |
async def get_knowledge_graph(
|
| 507 |
+
self,
|
| 508 |
+
node_label: str,
|
| 509 |
+
max_depth: int = 3,
|
| 510 |
+
min_degree: int = 0,
|
| 511 |
+
inclusive: bool = False,
|
| 512 |
) -> KnowledgeGraph:
|
| 513 |
+
"""Get knowledge graph for a given label
|
| 514 |
+
|
| 515 |
+
Args:
|
| 516 |
+
node_label (str): Label to get knowledge graph for
|
| 517 |
+
max_depth (int): Maximum depth of graph
|
| 518 |
+
min_degree (int, optional): Minimum degree of nodes to include. Defaults to 0.
|
| 519 |
+
inclusive (bool, optional): Whether to use inclusive search mode. Defaults to False.
|
| 520 |
+
|
| 521 |
+
Returns:
|
| 522 |
+
KnowledgeGraph: Knowledge graph containing nodes and edges
|
| 523 |
+
"""
|
| 524 |
+
# get params supported by get_knowledge_graph of specified storage
|
| 525 |
+
import inspect
|
| 526 |
+
|
| 527 |
+
storage_params = inspect.signature(
|
| 528 |
+
self.chunk_entity_relation_graph.get_knowledge_graph
|
| 529 |
+
).parameters
|
| 530 |
+
|
| 531 |
+
kwargs = {"node_label": node_label, "max_depth": max_depth}
|
| 532 |
+
|
| 533 |
+
if "min_degree" in storage_params and min_degree > 0:
|
| 534 |
+
kwargs["min_degree"] = min_degree
|
| 535 |
+
|
| 536 |
+
if "inclusive" in storage_params:
|
| 537 |
+
kwargs["inclusive"] = inclusive
|
| 538 |
+
|
| 539 |
+
return await self.chunk_entity_relation_graph.get_knowledge_graph(**kwargs)
|
| 540 |
|
| 541 |
def _get_storage_class(self, storage_name: str) -> Callable[..., Any]:
|
| 542 |
import_path = STORAGES[storage_name]
|
lightrag_webui/src/api/lightrag.ts
CHANGED
|
@@ -161,8 +161,12 @@ axiosInstance.interceptors.response.use(
|
|
| 161 |
)
|
| 162 |
|
| 163 |
// API methods
|
| 164 |
-
export const queryGraphs = async (
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
return response.data
|
| 167 |
}
|
| 168 |
|
|
|
|
| 161 |
)
|
| 162 |
|
| 163 |
// API methods
|
| 164 |
+
export const queryGraphs = async (
|
| 165 |
+
label: string,
|
| 166 |
+
maxDepth: number,
|
| 167 |
+
minDegree: number
|
| 168 |
+
): Promise<LightragGraphType> => {
|
| 169 |
+
const response = await axiosInstance.get(`/graphs?label=${encodeURIComponent(label)}&max_depth=${maxDepth}&min_degree=${minDegree}`)
|
| 170 |
return response.data
|
| 171 |
}
|
| 172 |
|
lightrag_webui/src/components/graph/GraphControl.tsx
CHANGED
|
@@ -40,18 +40,21 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|
| 40 |
const focusedEdge = useGraphStore.use.focusedEdge()
|
| 41 |
|
| 42 |
/**
|
| 43 |
-
* When component mount
|
| 44 |
-
* => load the graph
|
| 45 |
*/
|
| 46 |
useEffect(() => {
|
| 47 |
// Create & load the graph
|
| 48 |
const graph = lightrageGraph()
|
| 49 |
loadGraph(graph)
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
Object.assign(graph, { __force_applied: true })
|
| 53 |
-
}
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
| 56 |
useGraphStore.getState()
|
| 57 |
|
|
@@ -87,7 +90,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|
| 87 |
},
|
| 88 |
clickStage: () => clearSelection()
|
| 89 |
})
|
| 90 |
-
}, [
|
| 91 |
|
| 92 |
/**
|
| 93 |
* When component mount or hovered node change
|
|
|
|
| 40 |
const focusedEdge = useGraphStore.use.focusedEdge()
|
| 41 |
|
| 42 |
/**
|
| 43 |
+
* When component mount or maxIterations changes
|
| 44 |
+
* => load the graph and apply layout
|
| 45 |
*/
|
| 46 |
useEffect(() => {
|
| 47 |
// Create & load the graph
|
| 48 |
const graph = lightrageGraph()
|
| 49 |
loadGraph(graph)
|
| 50 |
+
assignLayout()
|
| 51 |
+
}, [assignLayout, loadGraph, lightrageGraph, maxIterations])
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
/**
|
| 54 |
+
* When component mount
|
| 55 |
+
* => register events
|
| 56 |
+
*/
|
| 57 |
+
useEffect(() => {
|
| 58 |
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
| 59 |
useGraphStore.getState()
|
| 60 |
|
|
|
|
| 90 |
},
|
| 91 |
clickStage: () => clearSelection()
|
| 92 |
})
|
| 93 |
+
}, [registerEvents])
|
| 94 |
|
| 95 |
/**
|
| 96 |
* When component mount or hovered node change
|
lightrag_webui/src/components/graph/Settings.tsx
CHANGED
|
@@ -90,9 +90,12 @@ const LabeledNumberInput = ({
|
|
| 90 |
{label}
|
| 91 |
</label>
|
| 92 |
<Input
|
| 93 |
-
|
|
|
|
| 94 |
onChange={onValueChange}
|
| 95 |
-
className="h-6 w-full min-w-0"
|
|
|
|
|
|
|
| 96 |
onBlur={onBlur}
|
| 97 |
onKeyDown={(e) => {
|
| 98 |
if (e.key === 'Enter') {
|
|
@@ -119,6 +122,7 @@ export default function Settings() {
|
|
| 119 |
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
| 120 |
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
|
| 121 |
const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth()
|
|
|
|
| 122 |
const graphLayoutMaxIterations = useSettingsStore.use.graphLayoutMaxIterations()
|
| 123 |
|
| 124 |
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
|
@@ -177,6 +181,11 @@ export default function Settings() {
|
|
| 177 |
useSettingsStore.setState({ graphQueryMaxDepth: depth })
|
| 178 |
}, [])
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
const setGraphLayoutMaxIterations = useCallback((iterations: number) => {
|
| 181 |
if (iterations < 1) return
|
| 182 |
useSettingsStore.setState({ graphLayoutMaxIterations: iterations })
|
|
@@ -266,6 +275,12 @@ export default function Settings() {
|
|
| 266 |
value={graphQueryMaxDepth}
|
| 267 |
onEditFinished={setGraphQueryMaxDepth}
|
| 268 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
<LabeledNumberInput
|
| 270 |
label="Max Layout Iterations"
|
| 271 |
min={1}
|
|
|
|
| 90 |
{label}
|
| 91 |
</label>
|
| 92 |
<Input
|
| 93 |
+
type="number"
|
| 94 |
+
value={currentValue === null ? '' : currentValue}
|
| 95 |
onChange={onValueChange}
|
| 96 |
+
className="h-6 w-full min-w-0 pr-1"
|
| 97 |
+
min={min}
|
| 98 |
+
max={max}
|
| 99 |
onBlur={onBlur}
|
| 100 |
onKeyDown={(e) => {
|
| 101 |
if (e.key === 'Enter') {
|
|
|
|
| 122 |
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
| 123 |
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
|
| 124 |
const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth()
|
| 125 |
+
const graphMinDegree = useSettingsStore.use.graphMinDegree()
|
| 126 |
const graphLayoutMaxIterations = useSettingsStore.use.graphLayoutMaxIterations()
|
| 127 |
|
| 128 |
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
|
|
|
| 181 |
useSettingsStore.setState({ graphQueryMaxDepth: depth })
|
| 182 |
}, [])
|
| 183 |
|
| 184 |
+
const setGraphMinDegree = useCallback((degree: number) => {
|
| 185 |
+
if (degree < 0) return
|
| 186 |
+
useSettingsStore.setState({ graphMinDegree: degree })
|
| 187 |
+
}, [])
|
| 188 |
+
|
| 189 |
const setGraphLayoutMaxIterations = useCallback((iterations: number) => {
|
| 190 |
if (iterations < 1) return
|
| 191 |
useSettingsStore.setState({ graphLayoutMaxIterations: iterations })
|
|
|
|
| 275 |
value={graphQueryMaxDepth}
|
| 276 |
onEditFinished={setGraphQueryMaxDepth}
|
| 277 |
/>
|
| 278 |
+
<LabeledNumberInput
|
| 279 |
+
label="Minimum Degree"
|
| 280 |
+
min={0}
|
| 281 |
+
value={graphMinDegree}
|
| 282 |
+
onEditFinished={setGraphMinDegree}
|
| 283 |
+
/>
|
| 284 |
<LabeledNumberInput
|
| 285 |
label="Max Layout Iterations"
|
| 286 |
min={1}
|
lightrag_webui/src/components/ui/Input.tsx
CHANGED
|
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|
| 7 |
<input
|
| 8 |
type={type}
|
| 9 |
className={cn(
|
| 10 |
-
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
| 11 |
className
|
| 12 |
)}
|
| 13 |
ref={ref}
|
|
|
|
| 7 |
<input
|
| 8 |
type={type}
|
| 9 |
className={cn(
|
| 10 |
+
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm [&::-webkit-inner-spin-button]:opacity-100 [&::-webkit-outer-spin-button]:opacity-100',
|
| 11 |
className
|
| 12 |
)}
|
| 13 |
ref={ref}
|
lightrag_webui/src/hooks/useLightragGraph.tsx
CHANGED
|
@@ -50,11 +50,11 @@ export type NodeType = {
|
|
| 50 |
}
|
| 51 |
export type EdgeType = { label: string }
|
| 52 |
|
| 53 |
-
const fetchGraph = async (label: string, maxDepth: number) => {
|
| 54 |
let rawData: any = null
|
| 55 |
|
| 56 |
try {
|
| 57 |
-
rawData = await queryGraphs(label, maxDepth)
|
| 58 |
} catch (e) {
|
| 59 |
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
|
| 60 |
return null
|
|
@@ -161,13 +161,14 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|
| 161 |
return graph
|
| 162 |
}
|
| 163 |
|
| 164 |
-
const lastQueryLabel = { label: '', maxQueryDepth: 0 }
|
| 165 |
|
| 166 |
const useLightrangeGraph = () => {
|
| 167 |
const queryLabel = useSettingsStore.use.queryLabel()
|
| 168 |
const rawGraph = useGraphStore.use.rawGraph()
|
| 169 |
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
| 170 |
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
|
|
|
| 171 |
|
| 172 |
const getNode = useCallback(
|
| 173 |
(nodeId: string) => {
|
|
@@ -185,13 +186,16 @@ const useLightrangeGraph = () => {
|
|
| 185 |
|
| 186 |
useEffect(() => {
|
| 187 |
if (queryLabel) {
|
| 188 |
-
if (lastQueryLabel.label !== queryLabel ||
|
|
|
|
|
|
|
| 189 |
lastQueryLabel.label = queryLabel
|
| 190 |
lastQueryLabel.maxQueryDepth = maxQueryDepth
|
|
|
|
| 191 |
|
| 192 |
const state = useGraphStore.getState()
|
| 193 |
state.reset()
|
| 194 |
-
fetchGraph(queryLabel, maxQueryDepth).then((data) => {
|
| 195 |
// console.debug('Query label: ' + queryLabel)
|
| 196 |
state.setSigmaGraph(createSigmaGraph(data))
|
| 197 |
data?.buildDynamicMap()
|
|
@@ -203,7 +207,7 @@ const useLightrangeGraph = () => {
|
|
| 203 |
state.reset()
|
| 204 |
state.setSigmaGraph(new DirectedGraph())
|
| 205 |
}
|
| 206 |
-
}, [queryLabel, maxQueryDepth])
|
| 207 |
|
| 208 |
const lightrageGraph = useCallback(() => {
|
| 209 |
if (sigmaGraph) {
|
|
|
|
| 50 |
}
|
| 51 |
export type EdgeType = { label: string }
|
| 52 |
|
| 53 |
+
const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => {
|
| 54 |
let rawData: any = null
|
| 55 |
|
| 56 |
try {
|
| 57 |
+
rawData = await queryGraphs(label, maxDepth, minDegree)
|
| 58 |
} catch (e) {
|
| 59 |
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
|
| 60 |
return null
|
|
|
|
| 161 |
return graph
|
| 162 |
}
|
| 163 |
|
| 164 |
+
const lastQueryLabel = { label: '', maxQueryDepth: 0, minDegree: 0 }
|
| 165 |
|
| 166 |
const useLightrangeGraph = () => {
|
| 167 |
const queryLabel = useSettingsStore.use.queryLabel()
|
| 168 |
const rawGraph = useGraphStore.use.rawGraph()
|
| 169 |
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
| 170 |
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
| 171 |
+
const minDegree = useSettingsStore.use.graphMinDegree()
|
| 172 |
|
| 173 |
const getNode = useCallback(
|
| 174 |
(nodeId: string) => {
|
|
|
|
| 186 |
|
| 187 |
useEffect(() => {
|
| 188 |
if (queryLabel) {
|
| 189 |
+
if (lastQueryLabel.label !== queryLabel ||
|
| 190 |
+
lastQueryLabel.maxQueryDepth !== maxQueryDepth ||
|
| 191 |
+
lastQueryLabel.minDegree !== minDegree) {
|
| 192 |
lastQueryLabel.label = queryLabel
|
| 193 |
lastQueryLabel.maxQueryDepth = maxQueryDepth
|
| 194 |
+
lastQueryLabel.minDegree = minDegree
|
| 195 |
|
| 196 |
const state = useGraphStore.getState()
|
| 197 |
state.reset()
|
| 198 |
+
fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
|
| 199 |
// console.debug('Query label: ' + queryLabel)
|
| 200 |
state.setSigmaGraph(createSigmaGraph(data))
|
| 201 |
data?.buildDynamicMap()
|
|
|
|
| 207 |
state.reset()
|
| 208 |
state.setSigmaGraph(new DirectedGraph())
|
| 209 |
}
|
| 210 |
+
}, [queryLabel, maxQueryDepth, minDegree])
|
| 211 |
|
| 212 |
const lightrageGraph = useCallback(() => {
|
| 213 |
if (sigmaGraph) {
|
lightrag_webui/src/stores/settings.ts
CHANGED
|
@@ -22,6 +22,9 @@ interface SettingsState {
|
|
| 22 |
graphQueryMaxDepth: number
|
| 23 |
setGraphQueryMaxDepth: (depth: number) => void
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
graphLayoutMaxIterations: number
|
| 26 |
setGraphLayoutMaxIterations: (iterations: number) => void
|
| 27 |
|
|
@@ -66,6 +69,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
| 66 |
enableEdgeEvents: false,
|
| 67 |
|
| 68 |
graphQueryMaxDepth: 3,
|
|
|
|
| 69 |
graphLayoutMaxIterations: 10,
|
| 70 |
|
| 71 |
queryLabel: defaultQueryLabel,
|
|
@@ -107,6 +111,8 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
| 107 |
|
| 108 |
setGraphQueryMaxDepth: (depth: number) => set({ graphQueryMaxDepth: depth }),
|
| 109 |
|
|
|
|
|
|
|
| 110 |
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
| 111 |
|
| 112 |
setApiKey: (apiKey: string | null) => set({ apiKey }),
|
|
|
|
| 22 |
graphQueryMaxDepth: number
|
| 23 |
setGraphQueryMaxDepth: (depth: number) => void
|
| 24 |
|
| 25 |
+
graphMinDegree: number
|
| 26 |
+
setGraphMinDegree: (degree: number) => void
|
| 27 |
+
|
| 28 |
graphLayoutMaxIterations: number
|
| 29 |
setGraphLayoutMaxIterations: (iterations: number) => void
|
| 30 |
|
|
|
|
| 69 |
enableEdgeEvents: false,
|
| 70 |
|
| 71 |
graphQueryMaxDepth: 3,
|
| 72 |
+
graphMinDegree: 0,
|
| 73 |
graphLayoutMaxIterations: 10,
|
| 74 |
|
| 75 |
queryLabel: defaultQueryLabel,
|
|
|
|
| 111 |
|
| 112 |
setGraphQueryMaxDepth: (depth: number) => set({ graphQueryMaxDepth: depth }),
|
| 113 |
|
| 114 |
+
setGraphMinDegree: (degree: number) => set({ graphMinDegree: degree }),
|
| 115 |
+
|
| 116 |
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
| 117 |
|
| 118 |
setApiKey: (apiKey: string | null) => set({ apiKey }),
|
lightrag_webui/src/vite-env.d.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
| 1 |
/// <reference types="vite/client" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/// <reference types="vite/client" />
|
| 2 |
+
|
| 3 |
+
interface ImportMetaEnv {
|
| 4 |
+
readonly VITE_API_PROXY: string
|
| 5 |
+
readonly VITE_API_ENDPOINTS: string
|
| 6 |
+
readonly VITE_BACKEND_URL: string
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
interface ImportMeta {
|
| 10 |
+
readonly env: ImportMetaEnv
|
| 11 |
+
}
|
lightrag_webui/tsconfig.json
CHANGED
|
@@ -26,5 +26,5 @@
|
|
| 26 |
"@/*": ["./src/*"]
|
| 27 |
}
|
| 28 |
},
|
| 29 |
-
"include": ["src"]
|
| 30 |
}
|
|
|
|
| 26 |
"@/*": ["./src/*"]
|
| 27 |
}
|
| 28 |
},
|
| 29 |
+
"include": ["src", "vite.config.ts"]
|
| 30 |
}
|
lightrag_webui/vite.config.ts
CHANGED
|
@@ -14,6 +14,21 @@ export default defineConfig({
|
|
| 14 |
},
|
| 15 |
base: './',
|
| 16 |
build: {
|
| 17 |
-
outDir: path.resolve(__dirname, '../lightrag/api/webui')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
})
|
|
|
|
| 14 |
},
|
| 15 |
base: './',
|
| 16 |
build: {
|
| 17 |
+
outDir: path.resolve(__dirname, '../lightrag/api/webui'),
|
| 18 |
+
emptyOutDir: true
|
| 19 |
+
},
|
| 20 |
+
server: {
|
| 21 |
+
proxy: import.meta.env.VITE_API_PROXY === 'true' && import.meta.env.VITE_API_ENDPOINTS ?
|
| 22 |
+
Object.fromEntries(
|
| 23 |
+
import.meta.env.VITE_API_ENDPOINTS.split(',').map(endpoint => [
|
| 24 |
+
endpoint,
|
| 25 |
+
{
|
| 26 |
+
target: import.meta.env.VITE_BACKEND_URL || 'http://localhost:9621',
|
| 27 |
+
changeOrigin: true,
|
| 28 |
+
rewrite: endpoint === '/api' ?
|
| 29 |
+
(path) => path.replace(/^\/api/, '') : undefined
|
| 30 |
+
}
|
| 31 |
+
])
|
| 32 |
+
) : {}
|
| 33 |
}
|
| 34 |
})
|