From 85e92172fe22c0e688f79292f6127f5059fb7573 Mon Sep 17 00:00:00 2001 From: kartikpersistent <101251502+kartikpersistent@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:35:24 +0530 Subject: [PATCH 001/132] Dev (#537) * format fixes and graph schema indication fix * Update README.md * added chat modes variable in env updated the readme * spell fix * added the chat mode in env table * added the logos * fixed the overflow issues * removed the extra fix * Fixed specific scenario "when the text from schema closes it should reopen the previous modal" * readme changes * removed dev console logs * added new retrieval query (#533) * format fixes and tab rendering fix * fixed the setting modal reopen issue --------- Co-authored-by: Prakriti Solankey <156313631+prakriti-solankey@users.noreply.github.com> Co-authored-by: vasanthasaikalluri <165021735+vasanthasaikalluri@users.noreply.github.com> From 3591d2eb817d68173f818beac5a4b1b674519ab4 Mon Sep 17 00:00:00 2001 From: kartikpersistent <101251502+kartikpersistent@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:07:33 +0000 Subject: [PATCH 002/132] disabled the sumbit buttom on loading --- frontend/src/components/Content.tsx | 2 +- frontend/src/components/Layout/PageLayout.tsx | 1 - .../components/Popups/GraphEnhancementDialog/index.tsx | 10 +++++----- .../src/components/WebSources/GenericSourceModal.tsx | 6 +++--- frontend/src/components/WebSources/Web/WebInput.tsx | 10 ++++++++-- .../components/WebSources/WikiPedia/WikipediaInput.tsx | 4 +++- .../src/components/WebSources/Youtube/YoutubeInput.tsx | 4 +++- frontend/src/types.ts | 2 +- 8 files changed, 24 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/Content.tsx b/frontend/src/components/Content.tsx index da71cc9bd..614afbf2c 100644 --- a/frontend/src/components/Content.tsx +++ b/frontend/src/components/Content.tsx @@ -32,7 +32,7 @@ const Content: React.FC = ({ setIsSchema, showEnhancementDialog, setshowEnhancementDialog, - closeSettingModal + closeSettingModal, }) => { const [init, setInit] = useState(false); const [openConnection, setOpenConnection] = useState(false); diff --git a/frontend/src/components/Layout/PageLayout.tsx b/frontend/src/components/Layout/PageLayout.tsx index 53b101ca5..e2472ca48 100644 --- a/frontend/src/components/Layout/PageLayout.tsx +++ b/frontend/src/components/Layout/PageLayout.tsx @@ -127,7 +127,6 @@ export default function PageLayoutNew({ showEnhancementDialog={showEnhancementDialog} setshowEnhancementDialog={setshowEnhancementDialog} closeSettingModal={closeSettingModal} - /> {showDrawerChatbot && ( diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx index e85ca660f..67f6644ba 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx @@ -13,7 +13,7 @@ import { useFileContext } from '../../../context/UsersFiles'; export default function GraphEnhancementDialog({ open, onClose, - closeSettingModal + closeSettingModal, }: { open: boolean; onClose: () => void; @@ -21,7 +21,7 @@ export default function GraphEnhancementDialog({ alertmsg: string, alerttype: OverridableStringUnion | undefined ) => void; - closeSettingModal:()=>void + closeSettingModal: () => void; }) { const [orphanDeleteAPIloading, setorphanDeleteAPIloading] = useState(false); const { setShowTextFromSchemaDialog } = useFileContext(); @@ -38,9 +38,9 @@ export default function GraphEnhancementDialog({ } }; useEffect(() => { - closeSettingModal() - }, []) - + closeSettingModal(); + }, []); + const [activeTab, setactiveTab] = useState(0); return ( {APP_SOURCES != undefined && APP_SOURCES.includes('youtube') && ( - + )} {APP_SOURCES != undefined && APP_SOURCES.includes('wiki') && ( - + )} {APP_SOURCES != undefined && APP_SOURCES.includes('web') && ( - + )} diff --git a/frontend/src/components/WebSources/Web/WebInput.tsx b/frontend/src/components/WebSources/Web/WebInput.tsx index 994949840..b6440ab79 100644 --- a/frontend/src/components/WebSources/Web/WebInput.tsx +++ b/frontend/src/components/WebSources/Web/WebInput.tsx @@ -2,7 +2,13 @@ import { webLinkValidation } from '../../../utils/Utils'; import useSourceInput from '../../../hooks/useSourceInput'; import CustomSourceInput from '../CustomSourceInput'; -export default function WebInput({ setIsLoading }: { setIsLoading: React.Dispatch> }) { +export default function WebInput({ + setIsLoading, + loading, +}: { + setIsLoading: React.Dispatch>; + loading: boolean; +}) { const { inputVal, onChangeHandler, @@ -21,7 +27,7 @@ export default function WebInput({ setIsLoading }: { setIsLoading: React.Dispatc onCloseHandler={onClose} isFocused={isFocused} isValid={isValid} - disabledCheck={false} + disabledCheck={loading ? true : false} label='Website Link' placeHolder='https://neo4j.com/' value={inputVal} diff --git a/frontend/src/components/WebSources/WikiPedia/WikipediaInput.tsx b/frontend/src/components/WebSources/WikiPedia/WikipediaInput.tsx index cf90d8349..582e06bee 100644 --- a/frontend/src/components/WebSources/WikiPedia/WikipediaInput.tsx +++ b/frontend/src/components/WebSources/WikiPedia/WikipediaInput.tsx @@ -3,9 +3,11 @@ import useSourceInput from '../../../hooks/useSourceInput'; import CustomSourceInput from '../CustomSourceInput'; export default function WikipediaInput({ + loading, setIsLoading, }: { setIsLoading: React.Dispatch>; + loading: boolean; }) { const { inputVal, @@ -25,7 +27,7 @@ export default function WikipediaInput({ onCloseHandler={onClose} isFocused={isFocused} isValid={isValid} - disabledCheck={false} + disabledCheck={loading ? true : false} label='Wikipedia Link' placeHolder='https://en.wikipedia.org/wiki/Albert_Einstein' value={inputVal} diff --git a/frontend/src/components/WebSources/Youtube/YoutubeInput.tsx b/frontend/src/components/WebSources/Youtube/YoutubeInput.tsx index 8652d7f92..77e22bda6 100644 --- a/frontend/src/components/WebSources/Youtube/YoutubeInput.tsx +++ b/frontend/src/components/WebSources/Youtube/YoutubeInput.tsx @@ -3,8 +3,10 @@ import useSourceInput from '../../../hooks/useSourceInput'; import { youtubeLinkValidation } from '../../../utils/Utils'; export default function YoutubeInput({ + loading, setIsLoading, }: { + loading: boolean; setIsLoading: React.Dispatch>; }) { const { @@ -25,7 +27,7 @@ export default function YoutubeInput({ onCloseHandler={onClose} isFocused={isFocused} isValid={isValid} - disabledCheck={false} + disabledCheck={loading ? true : false} label='Youtube Link' placeHolder='https://www.youtube.com/watch?v=2W9HM1xBibo' value={inputVal} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index dc0cadf1a..0dcaeb7f8 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -156,7 +156,7 @@ export interface ContentProps { setIsSchema: Dispatch>; showEnhancementDialog: boolean; setshowEnhancementDialog: Dispatch>; - closeSettingModal:()=>void + closeSettingModal: () => void; } export interface FileTableProps { From 92188a384d2fc0a7e555c847c0de3becd715b076 Mon Sep 17 00:00:00 2001 From: kartikpersistent <101251502+kartikpersistent@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:28:56 +0530 Subject: [PATCH 003/132] Deduplication tab (#566) * de-duplication API * Update De-Duplicate query * created the Deduplication tab * added the API service * added the removeable tags for similar nodes in deduplication tab * Integrate Tag * added GraphLabel * added loader state * added the merge service * integrated the merge API * Merge Query issue fixed * Auto refresh the duplicate nodes after merging operation * added the description for de duplication * reset on merging --------- Co-authored-by: Pravesh Kumar <121786590+praveshkumar1988@users.noreply.github.com> --- backend/example.env | 2 + backend/score.py | 36 +++ backend/src/graphDB_dataAccess.py | 59 ++++ .../src/components/ChatBot/ChatInfoModal.tsx | 2 +- .../Deduplication/index.tsx | 294 ++++++++++++++++++ .../EntityExtractionSetting.tsx | 16 +- .../Popups/GraphEnhancementDialog/index.tsx | 9 +- .../Popups/Settings/SettingModal.tsx | 2 +- frontend/src/components/UI/Legend.tsx | 6 +- .../components/WebSources/Web/WebInput.tsx | 2 +- .../WebSources/WikiPedia/WikipediaInput.tsx | 2 +- .../WebSources/Youtube/YoutubeInput.tsx | 2 +- frontend/src/services/GetDuplicateNodes.ts | 18 ++ .../src/services/MergeDuplicateEntities.ts | 20 ++ frontend/src/types.ts | 26 +- 15 files changed, 477 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/Popups/GraphEnhancementDialog/Deduplication/index.tsx rename frontend/src/components/Popups/{Settings => GraphEnhancementDialog/EnitityExtraction}/EntityExtractionSetting.tsx (96%) create mode 100644 frontend/src/services/GetDuplicateNodes.ts create mode 100644 frontend/src/services/MergeDuplicateEntities.ts diff --git a/backend/example.env b/backend/example.env index 0bbbf2403..1d14cfae4 100644 --- a/backend/example.env +++ b/backend/example.env @@ -25,6 +25,8 @@ NEO4J_USER_AGENT="" ENABLE_USER_AGENT = "" LLM_MODEL_CONFIG_model_version="" ENTITY_EMBEDDING="" True or False +DUPLICATE_SCORE_VALUE = "" +DUPLICATE_TEXT_DISTANCE = "" #examples LLM_MODEL_CONFIG_azure_ai_gpt_35="azure_deployment_name,azure_endpoint or base_url,azure_api_key,api_version" LLM_MODEL_CONFIG_azure_ai_gpt_4o="gpt-4o,https://YOUR-ENDPOINT.openai.azure.com/,azure_api_key,api_version" diff --git a/backend/score.py b/backend/score.py index b8de56b6b..646d78e73 100644 --- a/backend/score.py +++ b/backend/score.py @@ -600,6 +600,42 @@ async def get_unconnected_nodes_list(uri=Form(), userName=Form(), password=Form( if graph is not None: close_db_connection(graph,"delete_unconnected_nodes") gc.collect() + +@app.post("/get_duplicate_nodes") +async def get_duplicate_nodes(uri=Form(), userName=Form(), password=Form(), database=Form()): + try: + graph = create_graph_database_connection(uri, userName, password, database) + graphDb_data_Access = graphDBdataAccess(graph) + nodes_list, total_nodes = graphDb_data_Access.get_duplicate_nodes_list() + return create_api_response('Success',data=nodes_list, message=total_nodes) + except Exception as e: + job_status = "Failed" + message="Unable to get the list of duplicate nodes" + error_message = str(e) + logging.exception(f'Exception in getting list of duplicate nodes:{error_message}') + return create_api_response(job_status, message=message, error=error_message) + finally: + if graph is not None: + close_db_connection(graph,"get_duplicate_nodes") + gc.collect() + +@app.post("/merge_duplicate_nodes") +async def merge_duplicate_nodes(uri=Form(), userName=Form(), password=Form(), database=Form(),duplicate_nodes_list=Form()): + try: + graph = create_graph_database_connection(uri, userName, password, database) + graphDb_data_Access = graphDBdataAccess(graph) + result = graphDb_data_Access.merge_duplicate_nodes(duplicate_nodes_list) + return create_api_response('Success',data=result,message="Duplicate entities merged successfully") + except Exception as e: + job_status = "Failed" + message="Unable to merge the duplicate nodes" + error_message = str(e) + logging.exception(f'Exception in merge the duplicate nodes:{error_message}') + return create_api_response(job_status, message=message, error=error_message) + finally: + if graph is not None: + close_db_connection(graph,"merge_duplicate_nodes") + gc.collect() if __name__ == "__main__": uvicorn.run(app) \ No newline at end of file diff --git a/backend/src/graphDB_dataAccess.py b/backend/src/graphDB_dataAccess.py index 1d4dfb8d9..84f919301 100644 --- a/backend/src/graphDB_dataAccess.py +++ b/backend/src/graphDB_dataAccess.py @@ -241,4 +241,63 @@ def delete_unconnected_nodes(self,unconnected_entities_list): DETACH DELETE e """ param = {"elementIds":entities_list} + return self.execute_query(query,param) + + def get_duplicate_nodes_list(self): + score_value = float(os.environ.get('DUPLICATE_SCORE_VALUE')) + text_distance = int(os.environ.get('DUPLICATE_TEXT_DISTANCE')) + query_duplicate_nodes = """ + MATCH (n:!Chunk&!Document) with n + WHERE n.embedding is not null and n.id is not null // and size(n.id) > 3 + WITH n ORDER BY count {{ (n)--() }} DESC, size(n.id) DESC // updated + WITH collect(n) as nodes + UNWIND nodes as n + WITH n, [other in nodes + // only one pair, same labels e.g. Person with Person + WHERE elementId(n) < elementId(other) and labels(n) = labels(other) + // at least embedding similarity of X + AND + (vector.similarity.cosine(other.embedding, n.embedding) > $duplicate_score_value + OR + // either contains each other as substrings or has a text edit distinct of less than 3 + toLower(n.id) CONTAINS toLower(other.id) OR + toLower(other.id) CONTAINS toLower(n.id) + OR (size(n.id)>5 AND apoc.text.distance(toLower(n.id), toLower(other.id)) < $duplicate_text_distance) + )] as similar + WHERE size(similar) > 0 + OPTIONAL MATCH (doc:Document)<-[:PART_OF]-(c:Chunk)-[:HAS_ENTITY]->(n) + {return_statement} + """ + return_query_duplicate_nodes = """ + RETURN n {.*, embedding:null, elementId:elementId(n), labels:labels(n)} as e, + [s in similar | s {.id, .description, labels:labels(s), elementId: elementId(s)}] as similar, + collect(distinct doc.fileName) as documents, count(distinct c) as chunkConnections + ORDER BY e.id ASC + """ + return_query_duplicate_nodes_total = "RETURN COUNT(DISTINCT(n)) as total" + + param = {"duplicate_score_value": score_value, "duplicate_text_distance" : text_distance} + + nodes_list = self.execute_query(query_duplicate_nodes.format(return_statement=return_query_duplicate_nodes),param=param) + total_nodes = self.execute_query(query_duplicate_nodes.format(return_statement=return_query_duplicate_nodes_total),param=param) + return nodes_list, total_nodes[0] + + def merge_duplicate_nodes(self,duplicate_nodes_list): + nodes_list = json.loads(duplicate_nodes_list) + print(f'Nodes list to merge {nodes_list}') + query = """ + UNWIND $rows AS row + CALL { with row + MATCH (first) WHERE elementId(first) = row.firstElementId + MATCH (rest) WHERE elementId(rest) IN row.similarElementIds + WITH first, collect (rest) as rest + WITH [first] + rest as nodes + CALL apoc.refactor.mergeNodes(nodes, + {properties:"discard",mergeRels:true, produceSelfRel:false, preserveExistingSelfRels:false, singleElementAsArray:true}) + YIELD node + RETURN size(nodes) as mergedCount + } + RETURN sum(mergedCount) as totalMerged + """ + param = {"rows":nodes_list} return self.execute_query(query,param) \ No newline at end of file diff --git a/frontend/src/components/ChatBot/ChatInfoModal.tsx b/frontend/src/components/ChatBot/ChatInfoModal.tsx index e82693eb2..eb2704dc2 100644 --- a/frontend/src/components/ChatBot/ChatInfoModal.tsx +++ b/frontend/src/components/ChatBot/ChatInfoModal.tsx @@ -293,7 +293,7 @@ const ChatInfoModal: React.FC = ({ >
{ - //@ts-ignore + // @ts-ignore label[Object.keys(label)[0]].id ?? Object.keys(label)[0] }
diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/Deduplication/index.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/Deduplication/index.tsx new file mode 100644 index 000000000..e12429112 --- /dev/null +++ b/frontend/src/components/Popups/GraphEnhancementDialog/Deduplication/index.tsx @@ -0,0 +1,294 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { getDuplicateNodes } from '../../../../services/GetDuplicateNodes'; +import { useCredentials } from '../../../../context/UserCredentials'; +import { dupNodes, selectedDuplicateNodes, UserCredentials } from '../../../../types'; +import { + useReactTable, + getCoreRowModel, + createColumnHelper, + getFilteredRowModel, + getPaginationRowModel, + Table, + Row, + getSortedRowModel, +} from '@tanstack/react-table'; +import { Checkbox, DataGrid, DataGridComponents, Flex, Tag, Typography } from '@neo4j-ndl/react'; +import Legend from '../../../UI/Legend'; +import { DocumentIconOutline } from '@neo4j-ndl/react/icons'; +import { calcWordColor } from '@neo4j-devtools/word-color'; +import ButtonWithToolTip from '../../../UI/ButtonWithToolTip'; +import mergeDuplicateNodes from '../../../../services/MergeDuplicateEntities'; + +export default function DeduplicationTab() { + const { userCredentials } = useCredentials(); + const [duplicateNodes, setDuplicateNodes] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + const [isLoading, setLoading] = useState(false); + const [mergeAPIloading, setmergeAPIloading] = useState(false); + const tableRef = useRef(null); + const fetchDuplicateNodes = useCallback(async () => { + try { + setLoading(true); + const duplicateNodesData = await getDuplicateNodes(userCredentials as UserCredentials); + setLoading(false); + if (duplicateNodesData.data.status === 'Failed') { + throw new Error(duplicateNodesData.data.error); + } + if (duplicateNodesData.data.data.length) { + console.log({ duplicateNodesData }); + setDuplicateNodes(duplicateNodesData.data.data); + } + } catch (error) { + setLoading(false); + console.log(error); + } + }, [userCredentials]); + useEffect(() => { + (async () => { + await fetchDuplicateNodes(); + })(); + }, [userCredentials]); + + const clickHandler = async () => { + try { + const selectedNodeMap = table.getSelectedRowModel().rows.map( + (r): selectedDuplicateNodes => ({ + firstElementId: r.id, + similarElementIds: r.original.similar.map((s) => s.elementId), + }) + ); + setmergeAPIloading(true); + const response = await mergeDuplicateNodes(userCredentials as UserCredentials, selectedNodeMap); + table.resetRowSelection() + table.resetPagination() + setmergeAPIloading(false); + if (response.data.status === 'Failed') { + throw new Error(response.data.error); + } + } catch (error) { + setmergeAPIloading(false); + console.log(error); + } + }; + + const columnHelper = createColumnHelper(); + const onRemove = (nodeid: string, similarNodeId: string) => { + setDuplicateNodes((prev) => { + return prev.map((d) => + d.e.elementId === nodeid + ? { + ...d, + similar: d.similar.filter((n) => n.elementId != similarNodeId), + } + : d + ); + }); + }; + const columns = useMemo( + () => [ + { + id: 'Check to Delete All Files', + header: ({ table }: { table: Table }) => { + return ( + + ); + }, + cell: ({ row }: { row: Row }) => { + return ( +
+ +
+ ); + }, + size: 80, + }, + columnHelper.accessor((row) => row.e.id, { + id: 'Id', + cell: (info) => { + return ( +
+ {info.getValue()} +
+ ); + }, + header: () => ID, + footer: (info) => info.column.id, + }), + columnHelper.accessor((row) => row.similar, { + id: 'Similar Nodes', + cell: (info) => { + return ( + + {info.getValue().map((s, index) => ( + { + onRemove(info.row.original.e.elementId, s.elementId); + }} + removeable={true} + type='default' + size='medium' + > + {s.id} + + ))} + + ); + }, + }), + columnHelper.accessor((row) => row.e.labels, { + id: 'Labels', + cell: (info) => { + return ( + + {info.getValue().map((l, index) => ( + + ))} + + ); + }, + header: () => Labels, + footer: (info) => info.column.id, + }), + columnHelper.accessor((row) => row.documents, { + id: 'Connnected Documents', + cell: (info) => { + return ( + + {Array.from(new Set([...info.getValue()])).map((d, index) => ( + + + + + {d} + + ))} + + ); + }, + header: () => Related Documents , + footer: (info) => info.column.id, + }), + columnHelper.accessor((row) => row.chunkConnections, { + id: 'Connected Chunks', + cell: (info) => {info?.getValue()}, + header: () => Connected Chunks, + footer: (info) => info.column.id, + }), + ], + [] + ); + const table = useReactTable({ + data: duplicateNodes, + columns, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + state: { + rowSelection, + }, + onRowSelectionChange: setRowSelection, + enableGlobalFilter: false, + autoResetPageIndex: false, + enableRowSelection: true, + enableMultiRowSelection: true, + getRowId: (row) => row.e.elementId, + enableSorting: true, + getSortedRowModel: getSortedRowModel(), + initialState: { + pagination: { + pageSize: 5, + }, + }, + }); + const selectedFilesCheck =mergeAPIloading + ? 'Merging...': table.getSelectedRowModel().rows.length + ? `Merge Duplicate Nodes (${table.getSelectedRowModel().rows.length})` + : 'Select Node(s) to Merge'; + return ( +
+ + Refine Your Knowledge Graph: Merge Duplicate Entities: + + Identify and merge similar entries like "Apple" and "Apple Inc." to eliminate redundancy and improve the + accuracy and clarity of your knowledge graph. + + {duplicateNodes.length > 0 && ( + Total Duplicate Nodes: {duplicateNodes.length} + )} + + , + PaginationNumericButton: ({ isSelected, innerProps, ...restProps }) => { + return ( + + ); + }, + }} + /> + + { + await clickHandler(); + await fetchDuplicateNodes(); + }} + size='large' + loading={mergeAPIloading} + text={ + isLoading + ? 'Fetching Duplicate Nodes' + : !isLoading && !duplicateNodes.length + ? 'No Nodes Found' + : !table.getSelectedRowModel().rows.length + ? 'No Nodes Selected' + : mergeAPIloading + ? 'Merging' + : `Merge Selected Nodes (${table.getSelectedRowModel().rows.length})` + } + label='Merge Duplicate Node Button' + disabled={!table.getSelectedRowModel().rows.length} + placement='top' + > + {selectedFilesCheck} + + +
+ ); +} diff --git a/frontend/src/components/Popups/Settings/EntityExtractionSetting.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/EntityExtractionSetting.tsx similarity index 96% rename from frontend/src/components/Popups/Settings/EntityExtractionSetting.tsx rename to frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/EntityExtractionSetting.tsx index a44ae6ba7..5336ee0d6 100644 --- a/frontend/src/components/Popups/Settings/EntityExtractionSetting.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/EnitityExtraction/EntityExtractionSetting.tsx @@ -1,14 +1,14 @@ import { MouseEventHandler, useCallback, useEffect, useState } from 'react'; -import ButtonWithToolTip from '../../UI/ButtonWithToolTip'; -import { appLabels, buttonCaptions, tooltips } from '../../../utils/Constants'; +import ButtonWithToolTip from '../../../UI/ButtonWithToolTip'; +import { appLabels, buttonCaptions, tooltips } from '../../../../utils/Constants'; import { Dropdown, Flex, Typography } from '@neo4j-ndl/react'; -import { useCredentials } from '../../../context/UserCredentials'; -import { useFileContext } from '../../../context/UsersFiles'; +import { useCredentials } from '../../../../context/UserCredentials'; +import { useFileContext } from '../../../../context/UsersFiles'; import { OnChangeValue, ActionMeta } from 'react-select'; -import { OptionType, OptionTypeForExamples, schema, UserCredentials } from '../../../types'; -import { useAlertContext } from '../../../context/Alert'; -import { getNodeLabelsAndRelTypes } from '../../../services/GetNodeLabelsRelTypes'; -import schemaExamples from '../../../assets/schemas.json'; +import { OptionType, OptionTypeForExamples, schema, UserCredentials } from '../../../../types'; +import { useAlertContext } from '../../../../context/Alert'; +import { getNodeLabelsAndRelTypes } from '../../../../services/GetNodeLabelsRelTypes'; +import schemaExamples from '../../../../assets/schemas.json'; export default function EntityExtractionSetting({ view, diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx index 67f6644ba..9681fea83 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/index.tsx @@ -5,10 +5,11 @@ import DeletePopUpForOrphanNodes from './DeleteTabForOrphanNodes'; import deleteOrphanAPI from '../../../services/DeleteOrphanNodes'; import { UserCredentials } from '../../../types'; import { useCredentials } from '../../../context/UserCredentials'; -import EntityExtractionSettings from '../Settings/EntityExtractionSetting'; +import EntityExtractionSettings from './EnitityExtraction/EntityExtractionSetting'; import { AlertColor, AlertPropsColorOverrides } from '@mui/material'; import { OverridableStringUnion } from '@mui/types'; import { useFileContext } from '../../../context/UsersFiles'; +import DeduplicationTab from './Deduplication'; export default function GraphEnhancementDialog({ open, @@ -72,6 +73,9 @@ export default function GraphEnhancementDialog({ Disconnected Nodes + + De-Duplication Of Nodes + @@ -94,6 +98,9 @@ export default function GraphEnhancementDialog({ + + +
); diff --git a/frontend/src/components/Popups/Settings/SettingModal.tsx b/frontend/src/components/Popups/Settings/SettingModal.tsx index f5602a6d2..7884bfd6c 100644 --- a/frontend/src/components/Popups/Settings/SettingModal.tsx +++ b/frontend/src/components/Popups/Settings/SettingModal.tsx @@ -1,6 +1,6 @@ import { Dialog } from '@neo4j-ndl/react'; import { SettingsModalProps } from '../../../types'; -import EntityExtractionSetting from './EntityExtractionSetting'; +import EntityExtractionSetting from '../GraphEnhancementDialog/EnitityExtraction/EntityExtractionSetting'; const SettingsModal: React.FC = ({ open, onClose, openTextSchema, onContinue, settingView }) => { return ( diff --git a/frontend/src/components/UI/Legend.tsx b/frontend/src/components/UI/Legend.tsx index 5c7110a06..d798ff4f4 100644 --- a/frontend/src/components/UI/Legend.tsx +++ b/frontend/src/components/UI/Legend.tsx @@ -1,3 +1,5 @@ +import { GraphLabel } from '@neo4j-ndl/react'; + export default function Legend({ bgColor, title, @@ -8,9 +10,9 @@ export default function Legend({ chunkCount?: number; }) { return ( -
+ {title} {chunkCount && `(${chunkCount})`} -
+ ); } diff --git a/frontend/src/components/WebSources/Web/WebInput.tsx b/frontend/src/components/WebSources/Web/WebInput.tsx index b6440ab79..a4d6764bd 100644 --- a/frontend/src/components/WebSources/Web/WebInput.tsx +++ b/frontend/src/components/WebSources/Web/WebInput.tsx @@ -27,7 +27,7 @@ export default function WebInput({ onCloseHandler={onClose} isFocused={isFocused} isValid={isValid} - disabledCheck={loading ? true : false} + disabledCheck={Boolean(loading)} label='Website Link' placeHolder='https://neo4j.com/' value={inputVal} diff --git a/frontend/src/components/WebSources/WikiPedia/WikipediaInput.tsx b/frontend/src/components/WebSources/WikiPedia/WikipediaInput.tsx index 582e06bee..34a1e22e3 100644 --- a/frontend/src/components/WebSources/WikiPedia/WikipediaInput.tsx +++ b/frontend/src/components/WebSources/WikiPedia/WikipediaInput.tsx @@ -27,7 +27,7 @@ export default function WikipediaInput({ onCloseHandler={onClose} isFocused={isFocused} isValid={isValid} - disabledCheck={loading ? true : false} + disabledCheck={Boolean(loading)} label='Wikipedia Link' placeHolder='https://en.wikipedia.org/wiki/Albert_Einstein' value={inputVal} diff --git a/frontend/src/components/WebSources/Youtube/YoutubeInput.tsx b/frontend/src/components/WebSources/Youtube/YoutubeInput.tsx index 77e22bda6..bb8479013 100644 --- a/frontend/src/components/WebSources/Youtube/YoutubeInput.tsx +++ b/frontend/src/components/WebSources/Youtube/YoutubeInput.tsx @@ -27,7 +27,7 @@ export default function YoutubeInput({ onCloseHandler={onClose} isFocused={isFocused} isValid={isValid} - disabledCheck={loading ? true : false} + disabledCheck={Boolean(loading)} label='Youtube Link' placeHolder='https://www.youtube.com/watch?v=2W9HM1xBibo' value={inputVal} diff --git a/frontend/src/services/GetDuplicateNodes.ts b/frontend/src/services/GetDuplicateNodes.ts new file mode 100644 index 000000000..3cb27a970 --- /dev/null +++ b/frontend/src/services/GetDuplicateNodes.ts @@ -0,0 +1,18 @@ +import axios from 'axios'; +import { url } from '../utils/Utils'; +import { duplicateNodesData, UserCredentials } from '../types'; + +export const getDuplicateNodes = async (userCredentials: UserCredentials) => { + const formData = new FormData(); + formData.append('uri', userCredentials?.uri ?? ''); + formData.append('database', userCredentials?.database ?? ''); + formData.append('userName', userCredentials?.userName ?? ''); + formData.append('password', userCredentials?.password ?? ''); + try { + const response = await axios.post(`${url()}/get_duplicate_nodes`, formData); + return response; + } catch (error) { + console.log(error); + throw error; + } +}; diff --git a/frontend/src/services/MergeDuplicateEntities.ts b/frontend/src/services/MergeDuplicateEntities.ts new file mode 100644 index 000000000..ee4e0fffb --- /dev/null +++ b/frontend/src/services/MergeDuplicateEntities.ts @@ -0,0 +1,20 @@ +import axios from 'axios'; +import { url } from '../utils/Utils'; +import { commonserverresponse, selectedDuplicateNodes, UserCredentials } from '../types'; + +const mergeDuplicateNodes = async (userCredentials: UserCredentials, selectedNodes: selectedDuplicateNodes[]) => { + try { + const formData = new FormData(); + formData.append('uri', userCredentials?.uri ?? ''); + formData.append('database', userCredentials?.database ?? ''); + formData.append('userName', userCredentials?.userName ?? ''); + formData.append('password', userCredentials?.password ?? ''); + formData.append('duplicate_nodes_list', JSON.stringify(selectedNodes)); + const response = await axios.post(`${url()}/merge_duplicate_nodes`, formData); + return response; + } catch (error) { + console.log('Error Merging the duplicate nodes:', error); + throw error; + } +}; +export default mergeDuplicateNodes; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 0dcaeb7f8..2eb20b6f1 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -352,13 +352,14 @@ export interface orphanNode { elementId: string; description: string; labels: string[]; - embedding: null | string; + embedding?: null | string; } export interface orphanNodeProps { documents: string[]; chunkConnections: number; e: orphanNode; checked?: boolean; + similar?: orphanNode[]; } export interface labelsAndTypes { labels: string[]; @@ -372,15 +373,34 @@ export interface commonserverresponse { error?: string; message?: string | orphanTotalNodes; file_name?: string; - data?: labelsAndTypes | labelsAndTypes[] | uploadData | orphanNodeProps[]; + data?: labelsAndTypes | labelsAndTypes[] | uploadData | orphanNodeProps[] | dupNodes[]; +} +export interface dupNodeProps { + id: string; + elementId: string; + labels: string[]; + embedding?: null | string; +} +export interface dupNodes { + e: dupNodeProps; + similar: dupNodeProps[]; + documents: string[]; + chunkConnections: number; +} +export interface selectedDuplicateNodes { + firstElementId: string; + similarElementIds: string[]; } - export interface ScehmaFromText extends Partial { data: labelsAndTypes; } + export interface ServerData extends Partial { data: labelsAndTypes[]; } +export interface duplicateNodesData extends Partial { + data: dupNodes[]; +} export interface OrphanNodeResponse extends Partial { data: orphanNodeProps[]; } From 40650d0ab1af8e111f777a006e389dd9309d0162 Mon Sep 17 00:00:00 2001 From: Prakriti Solankey <156313631+prakriti-solankey@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:38:06 +0530 Subject: [PATCH 004/132] Update frontend_docs.adoc (#538) * Update frontend_docs.adoc * doc update * Images * Images folder change * Images folder change * test image * Update frontend_docs.adoc * image change * Update frontend_docs.adoc * Update frontend_docs.adoc * added the Graph Mode SS * added the Query SS * Update frontend_docs.adoc * conflics fix * conflict fix * Update frontend_docs.adoc --------- Co-authored-by: aashipandya <156318202+aashipandya@users.noreply.github.com> Co-authored-by: kartikpersistent <101251502+kartikpersistent@users.noreply.github.com> --- docs/frontend/frontend_docs.adoc | 609 +++++++++++++++++++ docs/frontend/images/ChatInfoModal.jpg | Bin 0 -> 230606 bytes docs/frontend/images/ChatModes.jpg | Bin 0 -> 197248 bytes docs/frontend/images/ChatResponse.jpg | Bin 0 -> 230606 bytes docs/frontend/images/ChunksInfo.jpg | Bin 0 -> 278157 bytes docs/frontend/images/ConnectionModal.jpg | Bin 0 -> 142122 bytes docs/frontend/images/DarkMode.jpg | Bin 0 -> 193556 bytes docs/frontend/images/DeleteFiles.jpg | Bin 0 -> 158247 bytes docs/frontend/images/DeleteOrphanNodes.jpg | Bin 0 -> 154909 bytes docs/frontend/images/Dropdown.jpg | Bin 0 -> 166348 bytes docs/frontend/images/EntitiesInfo.jpg | Bin 0 -> 224797 bytes docs/frontend/images/EntityGraph.jpg | Bin 0 -> 99849 bytes docs/frontend/images/ExistingSchema.jpg | Bin 0 -> 248650 bytes docs/frontend/images/GCSbucketFiles.jpg | Bin 0 -> 147628 bytes docs/frontend/images/GEDeleteOrphanNodes.jpg | Bin 0 -> 182439 bytes docs/frontend/images/Gcloud_auth.jpg | Bin 0 -> 202973 bytes docs/frontend/images/GenerateGraph.jpg | Bin 0 -> 179290 bytes docs/frontend/images/GraphEnhacements.jpg | Bin 0 -> 188066 bytes docs/frontend/images/GraphModeDetails.png | Bin 0 -> 199124 bytes docs/frontend/images/GraphModeQuery.png | Bin 0 -> 209420 bytes docs/frontend/images/GraphVectorMode.jpg | Bin 0 -> 287488 bytes docs/frontend/images/KnowledgeGraph.jpg | Bin 0 -> 140917 bytes docs/frontend/images/LexicalGraph.jpg | Bin 0 -> 109415 bytes docs/frontend/images/LightMode.jpg | Bin 0 -> 169094 bytes docs/frontend/images/NoFiles.jpg | Bin 0 -> 134934 bytes docs/frontend/images/PredefinedSchema.jpg | Bin 0 -> 191663 bytes docs/frontend/images/S3BucketScan.jpg | Bin 0 -> 139384 bytes docs/frontend/images/ScanningSource.jpg | Bin 0 -> 136415 bytes docs/frontend/images/SourcesInfo.jpg | Bin 0 -> 193541 bytes docs/frontend/images/UploadLocalFile.jpg | Bin 0 -> 182334 bytes docs/frontend/images/UploadingStatus.jpg | Bin 0 -> 117696 bytes docs/frontend/images/UserDefinedSchema.jpg | Bin 0 -> 179886 bytes docs/frontend/images/VectorMode.jpg | Bin 0 -> 209859 bytes docs/frontend/images/WebSources.jpg | Bin 0 -> 127102 bytes docs/frontend/images/WithFiles.jpg | Bin 0 -> 188586 bytes 35 files changed, 609 insertions(+) create mode 100644 docs/frontend/images/ChatInfoModal.jpg create mode 100644 docs/frontend/images/ChatModes.jpg create mode 100644 docs/frontend/images/ChatResponse.jpg create mode 100644 docs/frontend/images/ChunksInfo.jpg create mode 100644 docs/frontend/images/ConnectionModal.jpg create mode 100644 docs/frontend/images/DarkMode.jpg create mode 100644 docs/frontend/images/DeleteFiles.jpg create mode 100644 docs/frontend/images/DeleteOrphanNodes.jpg create mode 100644 docs/frontend/images/Dropdown.jpg create mode 100644 docs/frontend/images/EntitiesInfo.jpg create mode 100644 docs/frontend/images/EntityGraph.jpg create mode 100644 docs/frontend/images/ExistingSchema.jpg create mode 100644 docs/frontend/images/GCSbucketFiles.jpg create mode 100644 docs/frontend/images/GEDeleteOrphanNodes.jpg create mode 100644 docs/frontend/images/Gcloud_auth.jpg create mode 100644 docs/frontend/images/GenerateGraph.jpg create mode 100644 docs/frontend/images/GraphEnhacements.jpg create mode 100644 docs/frontend/images/GraphModeDetails.png create mode 100644 docs/frontend/images/GraphModeQuery.png create mode 100644 docs/frontend/images/GraphVectorMode.jpg create mode 100644 docs/frontend/images/KnowledgeGraph.jpg create mode 100644 docs/frontend/images/LexicalGraph.jpg create mode 100644 docs/frontend/images/LightMode.jpg create mode 100644 docs/frontend/images/NoFiles.jpg create mode 100644 docs/frontend/images/PredefinedSchema.jpg create mode 100644 docs/frontend/images/S3BucketScan.jpg create mode 100644 docs/frontend/images/ScanningSource.jpg create mode 100644 docs/frontend/images/SourcesInfo.jpg create mode 100644 docs/frontend/images/UploadLocalFile.jpg create mode 100644 docs/frontend/images/UploadingStatus.jpg create mode 100644 docs/frontend/images/UserDefinedSchema.jpg create mode 100644 docs/frontend/images/VectorMode.jpg create mode 100644 docs/frontend/images/WebSources.jpg create mode 100644 docs/frontend/images/WithFiles.jpg diff --git a/docs/frontend/frontend_docs.adoc b/docs/frontend/frontend_docs.adoc index e69de29bb..9eaf1e4bc 100644 --- a/docs/frontend/frontend_docs.adoc +++ b/docs/frontend/frontend_docs.adoc @@ -0,0 +1,609 @@ += LLM Knowledge Graph Builder Frontend + +== Objective + +This document provides a comprehensive guide for developers on how we build a React application integrated with Neo4j Aura for graph database functionalities. The application allows users to connect to a Neo4j Aura instance and we show you how to automatically create a graph from the unstructured text. We allow users to upload documents locally and from cloud buckets, YouTube videos, and Wikipedia pages, configure a graph schema, extract the lexical, entity and knowledge graph, visualize the extracted graph, ask questions and see the details that were used to generate the answers. + +== Architecture Structure + +* For Knowledge Graph builder App: + ** React JS – Application logic. + ** Axios – for network calls and handling responses + ** Styled Components – To handle CSS in JS – Where we write all CSS ourselves, Or Tailwind CSS – 3rd party CSS classes to speed up development. + ** LongPooling: Long polling can be conceptualized as the simplest way to maintain a steady connection between a client and a server.It holds the request for a period if it has no response to send it back.It regularly updates clients with new information like updating a status, processed chunks every minute with new data. + ** SSEs are the best options when the server generates the data in a loop and sends multiple events to the clients and if we need real-time traffic from the server to the client. + +== Folders + + . + ├── Components + | ├─ ChatBot + | | ├─ ChatBotInfoModal + | | ├─ ChatModeToggle + | | ├─ ExpandedChatButtonContainer + | ├─ Data Sources + | | ├─ AWS + | | ├─ GCS + | | ├─ Local + | | ├─ WebSources + | | | ├─Web + | | | ├─Wikipedia + | | | ├─Youtube + | ├─ Graph + | | ├─ GraphViewButton + | | ├─ GraphViewModal + | | ├─ LegendsChip + | ├─ Layout + | | ├─ Content + | | ├─ DrawerChatbot + | | ├─ DrawerDropzone + | | ├─ Header + | | ├─ PageLayout + | | ├─ SideNav + | ├─ Popups + | | ├─ ConnectionModal + | | ├─ DeletePopup + | | ├─ GraphEnhancementDialog + | | ├─ LargeFilePopup + | | ├─ Settings + | ├─ UI + | | ├─ Alert + | | ├─ ButtonWithTooltip + | | ├─ CustomButton + | | ├─ CustomModal + | | ├─ CustomProgressBar + | | ├─ CustomSourceInput + | | ├─ Dropdown + | | ├─ ErrorBoundary + | | ├─ FileTable + | | ├─ GenericSourceButton + | | ├─ GenericSourceModal + | | ├─ HoverableLink + | | ├─ IconButtonTooltip + | | ├─ Legend + | | ├─ Menu + | | ├─ QuickStarter + ├── HOC + | ├─ SettingModalHOC + ├── Assets + | ├─ images + | | ├─ Application Images + | ├─ chatbotMessages.json + | ├─ schema.json + ├── Context + | ├─ Alert + | ├─ ThemeWrapper + | ├─ UserCredentials + | ├─ UserMessages + | ├─ UserFiles + ├── Hooks + | ├─ useSourceInput + | ├─ useSpeech + | ├─ useSSE + ├── Services + ├── Styling + | ├─ info + ├── Utils + | ├─ constants + | ├─ FileAPI + | ├─ Loader + | ├─ Types + | ├─ utils + └── README.md + +== Application + +== 1. Setup and Installation: +Added Node.js with version v21.1.0 and npm on the development machine. +Install necessary dependencies by running yarn install, such as axios for making HTTP requests and others to interact with the graph. + +== 2. Connect to the Neo4j Aura instance: +Created a connection modal by adding details including protocol, URI, database name, username, and password. Added a submit button that triggers an API: ***/connect*** and accepts params like uri, password, username and database to establish a connection to the Neo4j Aura instance. Handled the authentication and error scenarios appropriately, by displaying relevant messages. To check whether the backend connection is up and working we hit the API: ***/health*** + +* Before Connection : + +image::images/ConnectionModal.jpg[NoConnection, 600] + + * After connection: + +image::images/NoFiles.jpg[Connection, 600] + + +== 3. File Source integration: +Implemented various file source integrations including drag-and-drop, web sources search that includes YouTube video, Wikipedia link, Amazon S3 file access, and Google Cloud Storage (GCS) file access. This allows users to upload PDF files from local storage or directly from the integrated sources. +The Api’s are as follows: + +* ***/source_list:*** + ** to fetch the list of files in the DB + +image::images/WithFiles.jpg[Connected, 600] + +* ***/upload:*** + ** to upload files from Local + +image::images/UploadLocalFile.jpg[Local File, 600] + + + ** status 'Uploading' while file is get uploaded. + +image::images/UploadingStatus.jpg[Upload Status, 600] + + +* ***/url/scan:*** + ** to scan the link or sources of YouTube, Wikipedia, and Web Sources + +image::images/WebSources.jpg[WebSources, 600] + +* ***/url/scan:*** + ** to scan the files of S3 and GCS. + *** Add the respective Bucket URL, access key and secret key to access ***S3 files***. + +image::images/S3BucketScan.jpg[S3 scan, 600] + + **** Add the respective Project ID, Bucket name, and folder to access ***GCS files***. User gets a redirect to the authentication page to authenticate their google account. + +image::images/GCSbucketFiles.jpg[GCS scan, 600] + +image::images/Gcloud_auth.jpg[auth login scan, 600] + + +== 4. File Source Extraction: +* ***/extract*** + ** to fetch the number of nodes and relationships created. + *** During Extraction the selected files or all files in ‘New’ state go into ‘Processing’ state and then ‘Completed’ state if there are no failures. + +image::images/GenerateGraph.jpg[Generate Graph, 600] + + +== 5. Graph Generation: +* Created a component for generating graphs based on the files in the table, to extract nodes and relationships. When the user clicks on the Preview Graph or on the Table View icon the user can see that the graph model holds three options for viewing: Lexical Graph, Entity Graph and Knowledge Graph. We utilized Neo4j's graph library to visualize the extracted nodes and relationships in the form of a graph query API: ***/graph_query***. There are options for customizing the graph visualization such as layout algorithms [zoom in, zoom out, fit, refresh], node styling, relationship types. + +image::images/KnowledgeGraph.jpg[Knowledge Graph, 600] +image::images/EntityGraph.jpg[Entity Graph, 600] +image::images/EntityGraph.jpg[Entity Graph, 600] + +== 6. Chatbot: +* Created a Chatbot Component which has state variables to manage user input and chat messages. Once the user asks the question and clicks on the Ask button API: ***/chatbot*** is triggered to send user input to the backend and receive the response. The chat also has options for users to see more details about the chat, text to speech and copy the response. + +image::images/ChatResponse.jpg[ChatResponse, 600] + +* ***/chunk_entities:*** + + ** to fetch the number of sources, entities and chunks + +***Sources*** + +image::images/ChatInfoModal.jpg[ChatInfoModal, 600] + +***Entities*** + +image::images/EntitiesInfo.jpg[EntitiesInfo, 600] + +***Chunks*** + +image::images/ChunksInfo.jpg[ChunksInfo, 600] + +* There are three modes ***Vector***, ***Graph***, ***Graph+Vector*** that can be provided to the chat to retrieve the answers. + +image::images/ChatModes.jpg[ChatModes, 600] + + • In Vector mode, we only get the sources and chunks . + +image::images/VectorMode.jpg[VectorMode, 600] + + • Graph Mode: Cypher query and Entities [DEV] + +image::images/GraphModeDetails.png[GraphMode, 600] +image::images/GraphModeQuery.png[GraphMode, 600] + + • Graph+Vector Mode: Sources, Chunks and Entities + +image::images/GraphVectorMode.jpg[GraphVectorMode, 600] + +== 6. Graph Enhancement Settings: +Users can now set their own Schema for nodes and relations or can already be an existing schema. + +* ***/schema:*** + ** to fetch the existing schema that already exists in the db. + +image::images/PredefinedSchema.jpg[PredefinedSchema, 600] + +* ***/populate_graph_schema:*** + ** to fetch the schema from user entered document text + +image::images/UserDefinedSchema.jpg[UserDefinedSchema, 600] + +* ***/delete_unconnected_nodes:*** + ** to remove the lonely entities. + +image::images/DeleteOrphanNodes.jpg[DeleteOrphanNodes, 600] + +== 7. Settings: + +* ***LLM Model*** + +User can select desired LLM models + +image::images/Dropdown.jpg[Dropdown, 600] + +* ***Dark/Light Mode*** + +User can choose the application view : both in dark and light mode + +image::images/DarkMode.jpg[DarkMode, 600] + + +image::images/LightMode.jpg[LightMode, 600] + +* ***Delete Files*** + +User can delete all number/selected files from the table. + +image::images/DeleteFiles.jpg[DeleteFiles, 600] + +== 8. Interface Design: +Designed a user-friendly interface that guides users through the process of connecting to Neo4j Aura, accessing file sources, uploading PDF files, and generating graphs. + +* ***Components:*** @neo4j-ndl/react +* ***Icons:*** @neo4j-ndl/react/icons +* ***Graph Visualization:*** @neo4j-nvl/react. +* ***NVL:*** @neo4j-nvl/core +* ***CSS:*** Inline styling, tailwind CSS + +== 9. Deployment: +Followed best practices for optimizing performance and security of the deployed application. + +* ***Local Deployment:*** + ** Running through docker-compose + ** By default only OpenAI and Diffbot are enabled since Gemini requires extra GCP configurations. + ** In your root folder, create a .env file with your OPENAI and DIFFBOT keys (if you want to use both), + ** By default, the input sources will be: Local files, Youtube, Wikipedia ,AWS S3 and Webpages. As this default config is applied: + ** By default,all of the chat modes will be available: vector, graph+vector and graph. If none of the mode is mentioned in the chat modes variable all modes will be available: + ** You can then run Docker Compose to build and start all components: + +[source,indent=0] +---- + * LLM_MODELS="diffbot,openai-gpt-3.5,openai-gpt-4o" + * REACT_APP_SOURCES="local,youtube,wiki,s3,gcs,web" + * GOOGLE_CLIENT_ID="xxxx" [For Google GCS integration] + * CHAT_MODES="vector,graph+vector" + * CHUNK_SIZE=5242880 + * TIME_PER_BYTE=2 + * TIME_PER_PAGE=50 + * TIME_PER_CHUNK=4 + * LARGE_FILE_SIZE=5242880 + * ENV="PROD"/ ‘DEV’ + * NEO4J_USER_AGENT="LLM-Graph-Builder/v0.2-dev" + * BACKEND_API_URL= + * BLOOM_URL= + * NPM_TOKEN= + * BACKEND_PROCESSING_URL= +---- +* ***Cloud Deployment:*** + ** To deploy the app install the gcloud cli , run the following command in the terminal specifically from frontend root folder. + *** gcloud run deploy + *** source location current directory > Frontend + *** region : 32 [us-central 1] + *** Allow unauthenticated request : Yes + + +== 10. API Reference +----- +POST /connect +----- + +Neo4j database connection on frontend is done with this API. + +**API Parameters :** + +* `uri`= Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name + +=== Upload Files from Local +---- +POST /upload +---- + +The upload endpoint is designed to handle the uploading of large files by breaking them into smaller chunks. This method ensures that large files can be uploaded efficiently without overloading the server. + +**API Parameters :** + +* `file`=The file to be uploaded, received in chunks, +* `chunkNumber`=The current chunk number being uploaded, +* `totalChunks`=The total number of chunks the file is divided into (each chunk of 1Mb size), +* `originalname`=The original name of the file, +* `model`=The model associated with the file, +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name + + +=== User Defined Schema +---- +POST /schema +---- + +User can set schema for graph generation (i.e. Nodes and relationship labels) in settings panel or get existing db schema through this API. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name + +=== Graph schema from Input Text +---- +POST /populate_graph_schema +---- + +The API is used to populate a graph schema based on the provided input text, model, and schema description flag. + +**API Parameters :** + +* `input_text`=The input text used to populate the graph schema. +* `model`=The model to be used for populating the graph schema. +* `is_schema_description_checked`=A flag indicating whether the schema description should be considered. + +=== Unstructured Sources +---- +POST /url/scan +---- + +Create Document node for other sources - s3 bucket, gcs bucket, wikipedia, youtube url and web pages. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name +* `model`= LLM model, +* `source_url`= , +* `aws_access_key_id`= AWS access key, +* `aws_secret_access_key`= AWS secret key, +* `wiki_query`= Wikipedia query sources, +* `gcs_project_id`= GCS project id, +* `gcs_bucket_name`= GCS bucket name, +* `gcs_bucket_folder`= GCS bucket folder, +* `source_type`= s3 bucket/ gcs bucket/ youtube/Wikipedia as source type +* `gcs_project_id`=Form(None), +* `access_token`=Form(None) + + +=== Extration of Nodes and Relations from Data +---- +POST /extract +---- + +This API is responsible for - + +** Reading the content of source provided in the form of langchain Document object from respective langchain loaders + +** Dividing the document into multiple chunks, and make below relations - +*** PART_OF - relation from Document node to all chunk nodes +*** FIRST_CHUNK - relation from document node to first chunk node +*** NEXT_CHUNK - relation from a chunk pointing to next chunk of the document. +*** HAS_ENTITY - relation between chunk node and entities extracted from LLM. + +** Extracting nodes and relations in the form of GraphDocument from respective LLM. + +** Update embedding of chunks and create vector index. + +** Update K-Nearest Neighbors graph for similar chunks. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name +* `model`= LLM model, +* `file_name` = File uploaded from device +* `source_url`= , +* `aws_access_key_id`= AWS access key, +* `aws_secret_access_key`= AWS secret key, +* `wiki_query`= Wikipedia query sources, +* `gcs_project_id`=GCS project id, +* `gcs_bucket_name`= GCS bucket name, +* `gcs_bucket_folder`= GCS bucket folder, +* `gcs_blob_filename` = GCS file name, +* `source_type`= local file/ s3 bucket/ gcs bucket/ youtube/ Wikipedia as source, +allowedNodes=Node labels passed from settings panel, +* `allowedRelationship`=Relationship labels passed from settings panel, +* `language`=Language in which wikipedia content will be extracted + +=== Get list of sources +---- +GET /sources_list +---- + +List all sources (Document nodes) present in Neo4j graph database. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name + + +=== Post processing after graph generation +---- +POST /post_processing : +---- + +This API is called at the end of processing of whole document to get create k-nearest neighbor relations between similar chunks of document based on KNN_MIN_SCORE which is 0.8 by default and to drop and create a full text index on db labels. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name +* `tasks`= List of tasks to perform + +=== Chat with Data +---- +POST /chat_bot +---- + +The API responsible for a chatbot system designed to leverage multiple AI models and a Neo4j graph database, providing answers to user queries. It interacts with AI models from OpenAI and Google's Vertex AI and utilizes embedding models to enhance the retrieval of relevant information. + +**Components :** + +** Embedding Models - Includes OpenAI Embeddings, VertexAI Embeddings, and SentenceTransformer Embeddings to support vector-based query operations. +** AI Models - OpenAI GPT 3.5, GPT 4o, Gemini Pro, Gemini 1.5 Pro and Groq llama3 can be configured for the chatbot backend to generate responses and process natural language. +** Graph Database (Neo4jGraph) - Manages interactions with the Neo4j database, retrieving, and storing conversation histories. +** Response Generation - Utilizes Vector Embeddings from the Neo4j database, chat history, and the knowledge base of the LLM used. + +**API Parameters :** + +* `uri`= Neo4j uri +* `userName`= Neo4j database username +* `password`= Neo4j database password +* `model`= LLM model +* `question`= User query for the chatbot +* `session_id`= Session ID used to maintain the history of chats during the user's connection + +=== Get entities from chunks +---- +POST/chunk_entities +---- + +This API is used to get the entities and relations associated with a particular chunk and chunk metadata. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name +* `chunk_ids` = Chunk ids of document + + +=== Clear chat history +---- +POST /clear_chat_bot +---- + +This API is used to clear the chat history which is saved in Neo4j DB. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name, +* `session_id` = User session id for QA chat + +=== View graph for a file +---- +POST /graph_query +---- + +This API is used to view graph for a particular file. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `query_type`= Neo4j database name +* `document_names` = File name for which user wants to view graph + +=== SSE event to update processing status +---- +GET /update_extract_status +---- + +The API provides a continuous update on the extraction status of a specified file. It uses Server-Sent Events (SSE) to stream updates to the client. + +**API Parameters :** + +* `file_name`=The name of the file whose extraction status is being tracked, +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name + +---- +GET /document_status +---- + +The API gives the extraction status of a specified file. It uses Server-Sent Events (SSE) to stream updates to the client. + +**API Parameters :** + +* `file_name`=The name of the file whose extraction status is being tracked, +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name + +=== Delete selected documents +---- +POST /delete_document_and_entities +---- + +Deleteion of nodes and relations for multiple files is done through this API. User can choose multiple documents to be deleted, also user have option to delete only 'Document' and 'Chunk' nodes and keep the entities extracted from that document. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name, +* `filenames`= List of files to be deleted, +* `source_types`= Document sources(Wikipedia, youtube, etc.), +* `deleteEntities`= Boolean value to check entities deletion is requested or not + +=== Cancel processing job +---- +POST/cancelled_job +---- + +This API is responsible for cancelling an in process job. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name, +* `filenames`= Name of the file whose processing need to be stopped, +* `source_types`= Source of the file + +=== Deletion of orpahn nodes +---- +POST /delete_unconnected_nodes +---- + +The API is used to delete unconnected entities from database. + +**API Parameters :** + +* `uri`=Neo4j uri, +* `userName`= Neo4j db username, +* `password`= Neo4j db password, +* `database`= Neo4j database name, +* `unconnected_entities_list`=selected entities list to delete of unconnected entities. + + +== 11. Conclusion: +In conclusion, this technical document outlines the process of building a React application with Neo4j Aura integration for graph database functionalities. + + +== 12. Referral Links: +* Dev env : https://dev-frontend-dcavk67s4a-uc.a.run.app/ +* Staging env: https://staging-frontend-dcavk67s4a-uc.a.run.app/ +* Prod env: https://prod-frontend-dcavk67s4a-uc.a.run.app/ + + + + + + diff --git a/docs/frontend/images/ChatInfoModal.jpg b/docs/frontend/images/ChatInfoModal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72c1198008cffec331a273a2876394e5c7bffb81 GIT binary patch literal 230606 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