diff --git a/backend/Dockerfile b/backend/Dockerfile index 5249ac53c..c36f8ce2e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -5,6 +5,7 @@ EXPOSE 8000 # Install dependencies and clean up in one layer RUN apt-get update && \ apt-get install -y --no-install-recommends \ + libmagic1 \ libgl1-mesa-glx \ libreoffice \ cmake \ diff --git a/backend/requirements.txt b/backend/requirements.txt index de1fc1136..1df0ff4de 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,7 +2,7 @@ asyncio==3.4.3 boto3==1.35.69 botocore==1.35.69 certifi==2024.8.30 -fastapi==0.115.5 +fastapi==0.115.6 fastapi-health==0.4.0 google-api-core==2.23.0 google-auth==2.36.0 @@ -32,6 +32,7 @@ opencv-python==4.10.0.84 psutil==6.1.0 pydantic==2.9.0 python-dotenv==1.0.1 +python-magic==0.4.27 PyPDF2==3.0.1 PyMuPDF==1.24.14 starlette==0.41.3 @@ -39,6 +40,9 @@ sse-starlette==2.1.3 starlette-session==0.4.3 tqdm==4.67.1 unstructured[all-docs]==0.16.6 +unstructured==0.16.6 +unstructured-client==0.26.2 +unstructured-inference==0.8.1 urllib3==2.2.2 uvicorn==0.32.1 gunicorn==23.0.0 diff --git a/backend/score.py b/backend/score.py index cacbcc791..737a82afe 100644 --- a/backend/score.py +++ b/backend/score.py @@ -993,7 +993,7 @@ async def backend_connection_configuation(): message="Unable to connect backend DB" error_message = str(e) logging.exception(f'{error_message}') - return create_api_response(job_status, message=message, error=error_message + ' or fill from the login dialog', data=graph_connection) + return create_api_response(job_status, message=message, error=error_message.rstrip('.') + ', or fill from the login dialog.', data=graph_connection) finally: gc.collect() diff --git a/backend/src/ragas_eval.py b/backend/src/ragas_eval.py index c9f447242..29f7e026c 100644 --- a/backend/src/ragas_eval.py +++ b/backend/src/ragas_eval.py @@ -5,7 +5,7 @@ from datasets import Dataset from dotenv import load_dotenv from ragas import evaluate -from ragas.metrics import answer_relevancy, faithfulness +from ragas.metrics import answer_relevancy, faithfulness,context_entity_recall from src.shared.common_fn import load_embedding_model from ragas.dataset_schema import SingleTurnSample from ragas.metrics import RougeScore, SemanticSimilarity, ContextEntityRecall @@ -24,25 +24,29 @@ def get_ragas_metrics(question: str, context: list, answer: list, model: str): try: start_time = time.time() dataset = Dataset.from_dict( - {"question": [question] * len(answer), "answer": answer, "contexts": [[ctx] for ctx in context]} + {"question": [question] * len(answer),"reference": answer, "answer": answer, "contexts": [[ctx] for ctx in context]} ) logging.info("Evaluation dataset created successfully.") if ("diffbot" in model) or ("ollama" in model): raise ValueError(f"Unsupported model for evaluation: {model}") + elif ("gemini" in model): + llm, model_name = get_llm(model=model) + llm = LangchainLLMWrapper(llm,is_finished_parser=custom_is_finished_parser) else: llm, model_name = get_llm(model=model) + llm = LangchainLLMWrapper(llm) logging.info(f"Evaluating with model: {model_name}") score = evaluate( dataset=dataset, - metrics=[faithfulness, answer_relevancy], + metrics=[faithfulness, answer_relevancy,context_entity_recall], llm=llm, embeddings=EMBEDDING_FUNCTION, ) score_dict = ( - score.to_pandas()[["faithfulness", "answer_relevancy"]] + score.to_pandas()[["faithfulness", "answer_relevancy","context_entity_recall"]] .fillna(0) .round(4) .to_dict(orient="list") @@ -67,13 +71,10 @@ async def get_additional_metrics(question: str, contexts: list, answers: list, r if ("diffbot" in model_name) or ("ollama" in model_name): raise ValueError(f"Unsupported model for evaluation: {model_name}") llm, model_name = get_llm(model=model_name) - ragas_llm = LangchainLLMWrapper(llm) embeddings = EMBEDDING_FUNCTION embedding_model = LangchainEmbeddingsWrapper(embeddings=embeddings) rouge_scorer = RougeScore() semantic_scorer = SemanticSimilarity() - entity_recall_scorer = ContextEntityRecall() - entity_recall_scorer.llm = ragas_llm semantic_scorer.embeddings = embedding_model metrics = [] for response, context in zip(answers, contexts): @@ -82,18 +83,35 @@ async def get_additional_metrics(question: str, contexts: list, answers: list, r rouge_score = round(rouge_score,4) semantic_score = await semantic_scorer.single_turn_ascore(sample) semantic_score = round(semantic_score, 4) - if "gemini" in model_name: - entity_recall_score = "Not Available" - else: - entity_sample = SingleTurnSample(reference=reference, retrieved_contexts=[context]) - entity_recall_score = await entity_recall_scorer.single_turn_ascore(entity_sample) - entity_recall_score = round(entity_recall_score, 4) metrics.append({ "rouge_score": rouge_score, "semantic_score": semantic_score, - "context_entity_recall_score": entity_recall_score }) return metrics except Exception as e: logging.exception("Error in get_additional_metrics") - return {"error": str(e)} \ No newline at end of file + return {"error": str(e)} + + +def custom_is_finished_parser(response): + is_finished_list = [] + for g in response.flatten(): + resp = g.generations[0][0] + if resp.generation_info is not None: + if resp.generation_info.get("finish_reason") is not None: + is_finished_list.append( + resp.generation_info.get("finish_reason") == "STOP" + ) + + elif ( + isinstance(resp, ChatGeneration) + and t.cast(ChatGeneration, resp).message is not None + ): + resp_message: BaseMessage = t.cast(ChatGeneration, resp).message + if resp_message.response_metadata.get("finish_reason") is not None: + is_finished_list.append( + resp_message.response_metadata.get("finish_reason") == "STOP" + ) + else: + is_finished_list.append(True) + return all(is_finished_list) \ No newline at end of file diff --git a/frontend/src/components/ChatBot/ChatModesSwitch.tsx b/frontend/src/components/ChatBot/ChatModesSwitch.tsx index 5ff01a919..1327d5a7f 100644 --- a/frontend/src/components/ChatBot/ChatModesSwitch.tsx +++ b/frontend/src/components/ChatBot/ChatModesSwitch.tsx @@ -29,7 +29,7 @@ export default function ChatModesSwitch({ onClick={() => switchToOtherMode(currentModeIndex - 1)} ariaLabel='left' > - +
switchToOtherMode(currentModeIndex + 1)} ariaLabel='right' > - + ); diff --git a/frontend/src/components/ChatBot/Chatbot.tsx b/frontend/src/components/ChatBot/Chatbot.tsx index aa84764bc..877dcdab4 100644 --- a/frontend/src/components/ChatBot/Chatbot.tsx +++ b/frontend/src/components/ChatBot/Chatbot.tsx @@ -249,7 +249,7 @@ const Chatbot: FC = (props) => { } else { setListMessages((prev) => prev.map((msg) => - (msg.id === chatbotMessageId ? { ...msg, modes: { ...msg.modes, [mode]: responseMode } } : msg) + msg.id === chatbotMessageId ? { ...msg, modes: { ...msg.modes, [mode]: responseMode } } : msg ) ); } @@ -264,7 +264,7 @@ const Chatbot: FC = (props) => { } else { setListMessages((prev) => prev.map((msg) => - (msg.id === chatbotMessageId ? { ...msg, modes: { ...msg.modes, [mode]: responseMode } } : msg) + msg.id === chatbotMessageId ? { ...msg, modes: { ...msg.modes, [mode]: responseMode } } : msg ) ); } @@ -273,7 +273,7 @@ const Chatbot: FC = (props) => { console.error(`API call failed for mode ${mode}:`, result.reason); setListMessages((prev) => prev.map((msg) => - (msg.id === chatbotMessageId + msg.id === chatbotMessageId ? { ...msg, modes: { @@ -281,7 +281,7 @@ const Chatbot: FC = (props) => { [mode]: { message: 'Failed to fetch response for this mode.', error: result.reason }, }, } - : msg) + : msg ) ); } @@ -294,7 +294,7 @@ const Chatbot: FC = (props) => { if (error instanceof Error) { setListMessages((prev) => prev.map((msg) => - (msg.id === chatbotMessageId + msg.id === chatbotMessageId ? { ...msg, isLoading: false, @@ -306,7 +306,7 @@ const Chatbot: FC = (props) => { }, }, } - : msg) + : msg ) ); } diff --git a/frontend/src/components/ChatBot/CommonChatActions.tsx b/frontend/src/components/ChatBot/CommonChatActions.tsx index d1a5c6243..9f7acc8b9 100644 --- a/frontend/src/components/ChatBot/CommonChatActions.tsx +++ b/frontend/src/components/ChatBot/CommonChatActions.tsx @@ -43,7 +43,7 @@ export default function CommonActions({ disabled={chat.isTyping || chat.isLoading} aria-label='copy text' > - + - {chat.speaking ? : } + {chat.speaking ? ( + + ) : ( + + )} ); diff --git a/frontend/src/components/ChatBot/MetricsTab.tsx b/frontend/src/components/ChatBot/MetricsTab.tsx index b39292d94..627c60ec3 100644 --- a/frontend/src/components/ChatBot/MetricsTab.tsx +++ b/frontend/src/components/ChatBot/MetricsTab.tsx @@ -119,23 +119,7 @@ function MetricsTab({ }} /> ), - PaginationNumericButton: ({ isSelected, innerProps, ...restProps }) => { - return ( - - ); - }, + Navigation: null, }} isKeyboardNavigable={false} /> diff --git a/frontend/src/components/ChatBot/MultiModeMetrics.tsx b/frontend/src/components/ChatBot/MultiModeMetrics.tsx index 8bd89f1d8..5be629c1c 100644 --- a/frontend/src/components/ChatBot/MultiModeMetrics.tsx +++ b/frontend/src/components/ChatBot/MultiModeMetrics.tsx @@ -102,7 +102,7 @@ export default function MultiModeMetrics({ ), }), - columnHelper.accessor((row) => row.context_entity_recall_score as number, { + columnHelper.accessor((row) => row.context_entity_recall as number, { id: 'Entity Recall Score', cell: (info) => { const value = isNaN(info.getValue()) ? 'N.A' : info.getValue()?.toFixed(2); @@ -122,7 +122,7 @@ export default function MultiModeMetrics({ - Determines the recall of entities present in both reference and retrieved contexts. + Determines the recall of entities present in both generated answer and retrieved contexts. @@ -201,7 +201,7 @@ export default function MultiModeMetrics({ }); useEffect(() => { if (isWithAdditionalMetrics === false) { - table.setColumnVisibility({ 'Recall Score': false, 'Semantic Score': false, 'Rouge Score': false }); + table.setColumnVisibility({ 'Semantic Score': false, 'Rouge Score': false }); } else { table.resetColumnVisibility(true); } @@ -235,23 +235,7 @@ export default function MultiModeMetrics({ }} /> ), - PaginationNumericButton: ({ isSelected, innerProps, ...restProps }) => { - return ( - - ); - }, + Navigation: null, }} isKeyboardNavigable={false} /> diff --git a/frontend/src/components/ChatBot/chatInfo.ts b/frontend/src/components/ChatBot/chatInfo.ts index c7e990ae7..26f2e765c 100644 --- a/frontend/src/components/ChatBot/chatInfo.ts +++ b/frontend/src/components/ChatBot/chatInfo.ts @@ -1,5 +1,6 @@ import { getNeighbors } from '../../services/GraphQuery'; import { NeoNode, NeoRelationship, UserCredentials } from '../../types'; +import { showNormalToast } from '../../utils/toasts'; export const handleGraphNodeClick = async ( userCredentials: UserCredentials, @@ -29,6 +30,8 @@ export const handleGraphNodeClick = async ( setNeoRels(relationships); setOpenGraphView(true); setViewPoint('chatInfoView'); + } else { + showNormalToast('No nodes or relationships found for the selected node.'); } } catch (error: any) { console.error('Error fetching neighbors:', error); diff --git a/frontend/src/components/Layout/PageLayout.tsx b/frontend/src/components/Layout/PageLayout.tsx index 12cbc9cbe..cb29ceb01 100644 --- a/frontend/src/components/Layout/PageLayout.tsx +++ b/frontend/src/components/Layout/PageLayout.tsx @@ -63,12 +63,10 @@ const PageLayout: React.FC = () => { showDisconnectButton, } = useCredentials(); const { cancel } = useSpeechSynthesis(); - + useEffect(() => { async function initializeConnection() { const session = localStorage.getItem('neo4j.connection'); - const environment = process.env.VITE_ENV; - const isDev = environment === 'DEV'; // Fetch backend health status try { const response = await healthStatus(); @@ -97,7 +95,7 @@ const PageLayout: React.FC = () => { password: atob(parsedConnection.password), database: parsedConnection.database, }); - setGdsActive(parsedConnection.isGDS); + setGdsActive(parsedConnection.isgdsActive); setIsReadOnlyUser(parsedConnection.isReadOnlyUser); } else { console.error('Invalid parsed session data:', parsedConnection); @@ -126,7 +124,7 @@ const PageLayout: React.FC = () => { database: envCredentials.database, userDbVectorIndex: 384, isReadOnlyUser: envCredentials.isReadonlyUser, - isGDS: envCredentials.isGds, + isgdsActive: envCredentials.isgdsActive, }) ); return true; @@ -137,63 +135,53 @@ const PageLayout: React.FC = () => { return false; } }; - // Handle case where session exists + // Handle connection initialization let backendApiResponse; try { - if (isDev) { - backendApiResponse = await envConnectionAPI(); - const connectionData = backendApiResponse.data; - const envCredentials = { - uri: connectionData.data.uri, - password: atob(connectionData.data.password), - userName: connectionData.data.user_name, - database: connectionData.data.database, - isReadonlyUser: !connectionData.data.write_access, - isGds: connectionData.data.gds_status, - }; - if (session && isDev) { - const updated = updateSessionIfNeeded(envCredentials, session); - if (!updated) { - setUserCredentialsFromSession(session); // Using stored session if no update is needed - } - setConnectionStatus(Boolean(connectionData.data.graph_connection)); - setIsBackendConnected(true); - handleDisconnectButtonState(false); - } else if (!session) { - setUserCredentials(envCredentials); - localStorage.setItem( - 'neo4j.connection', - JSON.stringify({ - uri: envCredentials.uri, - user: envCredentials.userName, - password: btoa(envCredentials.password), - database: envCredentials.database, - userDbVectorIndex: 384, - isReadOnlyUser: envCredentials.isReadonlyUser, - isGDS: envCredentials.isGds, - }) - ); - setConnectionStatus(true); - setGdsActive(envCredentials.isGds); - setIsReadOnlyUser(envCredentials.isReadonlyUser); - handleDisconnectButtonState(false); + backendApiResponse = await envConnectionAPI(); + const connectionData = backendApiResponse.data; + const envCredentials = { + uri: connectionData.data.uri, + password: atob(connectionData.data.password), + userName: connectionData.data.user_name, + database: connectionData.data.database, + isReadonlyUser: !connectionData.data.write_access, + isgdsActive: connectionData.data.gds_status, + }; + if (session) { + const updated = updateSessionIfNeeded(envCredentials, session); + if (!updated) { + setUserCredentialsFromSession(session); // Use stored session if no update is needed } - } else if (session && !isDev) { - // For PROD, picking the session values - setUserCredentialsFromSession(session as string); - setConnectionStatus(true); - handleDisconnectButtonState(true); + setConnectionStatus(Boolean(connectionData.data.graph_connection)); + setIsBackendConnected(true); + handleDisconnectButtonState(false); } else { - setOpenConnection((prev) => ({ ...prev, openPopUp: true })); - handleDisconnectButtonState(true); + setUserCredentials(envCredentials); + localStorage.setItem( + 'neo4j.connection', + JSON.stringify({ + uri: envCredentials.uri, + user: envCredentials.userName, + password: btoa(envCredentials.password), + database: envCredentials.database, + userDbVectorIndex: 384, + isReadOnlyUser: envCredentials.isReadonlyUser, + isgdsActive: envCredentials.isgdsActive, + }) + ); + setConnectionStatus(true); + setGdsActive(envCredentials.isgdsActive); + setIsReadOnlyUser(envCredentials.isReadonlyUser); + handleDisconnectButtonState(false); } } catch (error) { - console.error('Error in DEV session handling:', error); + console.error('Error during backend API call:', error); if (session) { - setUserCredentialsFromSession(session as string); + setUserCredentialsFromSession(session); setConnectionStatus(true); } else { - setErrorMessage(backendApiResponse?.data.error); + setErrorMessage(backendApiResponse?.data?.error); setOpenConnection((prev) => ({ ...prev, openPopUp: true })); } handleDisconnectButtonState(true); diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/Deduplication/index.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/Deduplication/index.tsx index d59b46e21..df30c25c1 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/Deduplication/index.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/Deduplication/index.tsx @@ -104,12 +104,12 @@ export default function DeduplicationTab() { const onRemove = (nodeid: string, similarNodeId: string) => { setDuplicateNodes((prev) => { return prev.map((d) => - (d.e.elementId === nodeid + d.e.elementId === nodeid ? { ...d, similar: d.similar.filter((n) => n.elementId != similarNodeId), } - : d) + : d ); }); }; diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/DeleteTabForOrphanNodes/index.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/DeleteTabForOrphanNodes/index.tsx index 5d23cc7c4..de8cdc186 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/DeleteTabForOrphanNodes/index.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/DeleteTabForOrphanNodes/index.tsx @@ -122,13 +122,13 @@ export default function DeletePopUpForOrphanNodes({ return (
handleOrphanNodeClick(info.row.id, 'chatInfoView'), - title: info.getValue(), + title: info.getValue() ? info.getValue() : info.row.id, }} > - {info.getValue()} + {info.getValue() ? info.getValue() : info.row.id}
); diff --git a/frontend/src/components/Popups/GraphEnhancementDialog/PostProcessingCheckList/SelectedJobList.tsx b/frontend/src/components/Popups/GraphEnhancementDialog/PostProcessingCheckList/SelectedJobList.tsx index 447b1f846..fc5f39206 100644 --- a/frontend/src/components/Popups/GraphEnhancementDialog/PostProcessingCheckList/SelectedJobList.tsx +++ b/frontend/src/components/Popups/GraphEnhancementDialog/PostProcessingCheckList/SelectedJobList.tsx @@ -11,11 +11,11 @@ export default function SelectedJobList({ }) { const ongoingPostProcessingTasks = useMemo( () => - (isGdsActive + isGdsActive ? postProcessingTasks.includes('enable_communities') ? postProcessingTasks : postProcessingTasks.filter((s) => s != 'enable_communities') - : postProcessingTasks.filter((s) => s != 'enable_communities')), + : postProcessingTasks.filter((s) => s != 'enable_communities'), [isGdsActive, postProcessingTasks] ); return ( diff --git a/frontend/src/utils/Constants.ts b/frontend/src/utils/Constants.ts index 6044cb748..d577194eb 100644 --- a/frontend/src/utils/Constants.ts +++ b/frontend/src/utils/Constants.ts @@ -13,12 +13,12 @@ export const llms = process.env?.VITE_LLM_MODELS?.trim() != '' ? (process.env.VITE_LLM_MODELS?.split(',') as string[]) : [ - 'diffbot', 'openai_gpt_3.5', 'openai_gpt_4o', 'openai_gpt_4o_mini', 'gemini_1.5_pro', 'gemini_1.5_flash', + 'diffbot', 'azure_ai_gpt_35', 'azure_ai_gpt_4o', 'ollama_llama3', @@ -366,5 +366,5 @@ export const metricsinfo: Record = { answer_relevancy: "Determines How well the answer addresses the user's question.", rouge_score: 'Determines How much the generated answer matches the reference answer, word-for-word.', semantic_score: 'Determines How well the generated answer understands the meaning of the reference answer.', - context_entity_recall_score: 'Determines the recall of entities present in both reference and retrieved contexts', + context_entity_recall: 'Determines the recall of entities present in both generated answer and retrieved contexts', }; diff --git a/frontend/src/utils/Utils.ts b/frontend/src/utils/Utils.ts index 7314c55e7..82217535d 100644 --- a/frontend/src/utils/Utils.ts +++ b/frontend/src/utils/Utils.ts @@ -395,7 +395,7 @@ export const isFileCompleted = (waitingFile: CustomFile, item: SourceNode) => waitingFile && item.status === 'Completed'; export const calculateProcessedCount = (prev: number, batchSize: number) => - (prev === batchSize ? batchSize - 1 : prev + 1); + prev === batchSize ? batchSize - 1 : prev + 1; export const isProcessingFileValid = (item: SourceNode, userCredentials: UserCredentials) => { return item.status === 'Processing' && item.fileName != undefined && userCredentials && userCredentials.database;