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 |
})
|