From 8e1edf05f732261da33d15f4a8bf37ce212f23a5 Mon Sep 17 00:00:00 2001 From: Harry Li Date: Wed, 22 Jan 2025 14:50:35 -0500 Subject: [PATCH] Chat api restructure (#12) * progress restructuring * useMainChatAPI * rearranged runQuery to show results faster * edited styling * copyright, added LLM warning to query history names * added linkq status type, refactored LinkQChatMessageType * linkq status checkpoint * fixed type issues, added status badge * can edit API key through UI * separated settings component --------- Co-authored-by: Harry Li --- .env | 1 + src/App.module.scss | 4 - src/App.tsx | 94 ++++----------- src/components/ApiKeyWarning.tsx | 6 +- src/components/Chat/Chat.module.scss | 22 ++-- src/components/Chat/Chat.tsx | 78 +++++++----- src/components/LLMWarning.tsx | 2 +- src/components/QueryEditor/QueryEditor.tsx | 9 +- src/components/Results/Results.module.scss | 6 + src/components/Results/Results.tsx | 77 ++++++++++++ src/components/Settings/Settings.module.scss | 9 ++ src/components/Settings/Settings.tsx | 95 +++++++++++++++ src/hooks/useChatAPIInstance.ts | 31 +++++ src/hooks/useMainChatAPI.tsx | 34 ++++++ src/hooks/useMakeChatGPTAPIInstance.tsx | 29 ----- src/hooks/useRunQuery.tsx | 84 +++++++------ src/redux/chatHistorySlice.ts | 17 ++- src/redux/queryHistorySlice.ts | 38 ++++-- src/redux/resultsSlice.ts | 12 +- src/redux/settingsSlice.ts | 20 +++- src/types/linkQ.ts | 1 + src/utils/{ChatGPTAPI.ts => ChatAPI.ts} | 111 +++++++++--------- src/utils/demoData.ts | 93 ++++++++++----- .../mintaka-wikidata/mintakaEvaluation.ts | 24 ++-- src/utils/handleUserChat.ts | 12 +- src/utils/queryBuildingWorkflow.ts | 46 +++++--- src/utils/staticChat.ts | 12 +- src/utils/summarizeQueryResults.ts | 70 +++++++---- 28 files changed, 682 insertions(+), 355 deletions(-) create mode 100644 src/components/Results/Results.module.scss create mode 100644 src/components/Results/Results.tsx create mode 100644 src/components/Settings/Settings.module.scss create mode 100644 src/components/Settings/Settings.tsx create mode 100644 src/hooks/useChatAPIInstance.ts create mode 100644 src/hooks/useMainChatAPI.tsx delete mode 100644 src/hooks/useMakeChatGPTAPIInstance.tsx create mode 100644 src/types/linkQ.ts rename src/utils/{ChatGPTAPI.ts => ChatAPI.ts} (51%) diff --git a/.env b/.env index e0c98f1..94ca9cf 100644 --- a/.env +++ b/.env @@ -1,5 +1,6 @@ # duplicate this file and rename the new file as .env.local +VITE_BASE_URL= VITE_OPENAI_API_KEY= VITE_DEMO_MODE=false diff --git a/src/App.module.scss b/src/App.module.scss index 6f599c1..01f14d9 100644 --- a/src/App.module.scss +++ b/src/App.module.scss @@ -20,9 +20,5 @@ #results-content { color: white; padding: 1em; - - #empty-results-message { - text-align: center; - } } } diff --git a/src/App.tsx b/src/App.tsx index 02bee58..f0312ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,99 +1,47 @@ // Copyright (c) 2024 Massachusetts Institute of Technology // SPDX-License-Identifier: MIT -import { Title } from '@mantine/core'; import { ApiKeyWarning } from 'components/ApiKeyWarning'; import { Chat } from 'components/Chat/Chat'; import { DemoModeModal } from 'components/DemoModeModal'; -import { ErrorMessage } from 'components/ErrorMessage'; import { IDTableContainer } from 'components/IDTable/IDTable'; -import { InfoModal } from 'components/InfoModal'; -import { LLMWarning } from 'components/LLMWarning'; import { QueryEditor } from 'components/QueryEditor/QueryEditor' import { QueryVisualization } from "components/QueryVisualization/QueryVisualization"; -import { ResultsTable } from 'components/ResultsTable/ResultsTable'; +import { Results } from 'components/Results/Results'; -import { useAppSelector } from 'redux/store'; - -import { useRunQuery, RunQueryProvider } from 'hooks/useRunQuery'; +import { MainChatAPIProvider } from 'hooks/useMainChatAPI'; +import { RunQueryProvider } from 'hooks/useRunQuery'; import styles from 'App.module.scss' function App() { return ( - -
-
- -
- -
- + + +
+
+ +
+ +
+ - + - + -
- +
+ +
-
- - -
+ + + +
) } export default App - - -function Results() { - const { runQueryIsPending } = useRunQuery() - const results = useAppSelector(state => state.results.results) - - if(runQueryIsPending) { - return

Loading...

- } - else if(results?.error) { - return ( - <> -

There was an error running your query

-
{results.error}
- - ) - } - else if(results?.data) { - return ( - <> - Results Summary from LLM - {results.summary ? ( -
- -

This results summary was generated by an LLM that can make mistakes. Refer below to the Results Table from KG for ground-truth data.

-

Note that the absence of data does not necessairly mean that there is no data. It is possible that the query did not find what that you are looking for.

-
- -

{results.summary}

-
- ) : There was an error generating a summary.} - -
- - - Results Table from KG - <InfoModal title="Results Table from KG"> - <p>These are ground-truth results retrieved from the KG using the query you executed.</p> - <p>Note that the absence of data does not necessairly mean that there is no data. It is possible that the query did not find what that you are looking for.</p> - </InfoModal> - - - - ) - } - else { - return

Run a query to see results!

- } -} \ No newline at end of file diff --git a/src/components/ApiKeyWarning.tsx b/src/components/ApiKeyWarning.tsx index 47985a0..b5f30eb 100644 --- a/src/components/ApiKeyWarning.tsx +++ b/src/components/ApiKeyWarning.tsx @@ -3,6 +3,8 @@ import { useAppSelector } from "redux/store" +import { Settings } from "./Settings/Settings" + import { IS_DEMO_MODE } from "utils/demoData" export function ApiKeyWarning() { @@ -17,7 +19,9 @@ export function ApiKeyWarning() { display: "flex", justifyContent: "center", alignItems: "center", zIndex: 1, }}> -

You need to configure the VITE_OPENAI_API_KEY environment variable in your .env.local file.

+ + +

You need to configure the VITE_OPENAI_API_KEY environment variable in your .env.local file or update your API key in the settings menu.

) } diff --git a/src/components/Chat/Chat.module.scss b/src/components/Chat/Chat.module.scss index 221b1ff..860a06c 100644 --- a/src/components/Chat/Chat.module.scss +++ b/src/components/Chat/Chat.module.scss @@ -6,15 +6,8 @@ height: 100vh; position: relative; - #chat-settings-button { - position: absolute; - top: 0.5rem; - left: 0.5rem; - z-index: 1; - } - #chat-scroll-container { - height: calc(100% - 35px - 1rem); + height: calc(100% - 35px - 2.5rem); overflow-y: auto; margin-bottom: 3px; padding: 0.5rem; @@ -41,8 +34,15 @@ .chat { background-color: #444; padding: 0.5rem; - margin-bottom: 0.5rem; border-radius: 5px; + + &:not(:last-child) { + margin-bottom: 0.5rem; + } + + button { + margin-top: 0.5rem; + } } &.user { @@ -55,10 +55,6 @@ &.system { justify-content: flex-end; - - .chat { - // background-color: #154360; - } } .copy-query-buttons { diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 3d6e6cf..a4bdd60 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -4,23 +4,23 @@ import CodeMirror from '@uiw/react-codemirror'; import { StreamLanguage } from '@codemirror/language'; import { sparql } from '@codemirror/legacy-modes/mode/sparql'; -import { ActionIcon, Button, Checkbox, Modal, TextInput } from "@mantine/core"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { Badge, Button, Modal, TextInput } from "@mantine/core"; +import { useEffect, useRef, useState } from "react"; import { useMutation } from "@tanstack/react-query"; -import { IconCaretRight, IconSettings, IconZoomCode } from '@tabler/icons-react'; +import { IconCaretRight, IconZoomCode } from '@tabler/icons-react'; import { ErrorMessage } from 'components/ErrorMessage'; import { LLMWarning } from 'components/LLMWarning'; +import { Settings } from 'components/Settings/Settings'; -import { useMakeChatGPTAPIInstance } from 'hooks/useMakeChatGPTAPIInstance'; +import { useMainChatAPI } from 'hooks/useMainChatAPI'; import { useRunQuery } from 'hooks/useRunQuery'; -import { addMessageToSimpleChatHistory, toggleShowFullChatHistory } from 'redux/chatHistorySlice'; +import { addMessageToSimpleChatHistory } from 'redux/chatHistorySlice'; import { setQueryValue } from 'redux/queryValueSlice'; import { useAppDispatch, useAppSelector } from 'redux/store'; import { handleUserChat } from 'utils/handleUserChat'; -import { INITIAL_SYSTEM_MESSAGE } from 'utils/knowledgeBase/prompts'; import { tryParsingOutQuery } from 'utils/tryParsingOutQuery'; import styles from "./Chat.module.scss" @@ -44,19 +44,11 @@ export function Chat() { } }, [chatHistory.length]) - const [showSettingsModal, setShowSettingsModal] = useState(false) - const closeSettingsModal = () => setShowSettingsModal(false) // const [inputText, setInputText] = useState("Who won the 2023 Formula One Championship?"); // prefill the chat const [inputText, setInputText] = useState(""); - const makeChatGPTAPIInstance = useMakeChatGPTAPIInstance() - const chatGPT = useMemo(() => { - return makeChatGPTAPIInstance({ - chatId: 0, - systemMessage: INITIAL_SYSTEM_MESSAGE, - }) - },[]) + const chatAPI = useMainChatAPI() const {error, isPending, mutate:submitChat, reset} = useMutation({ mutationKey: ['submit-chat'], @@ -68,13 +60,14 @@ export function Chat() { mutationFn: async (text:string) => { //add the user's message to the simple chat history dispatch(addMessageToSimpleChatHistory({ - chatId: chatGPT.chatId, + chatId: chatAPI.chatId, content: text, name: "user", role: "user", + stage: "Question Refinement", })) - const llmResponse = await handleUserChat(text, chatGPT) + const llmResponse = await handleUserChat(text, chatAPI) //add the LLM's final response to the simple chat dispatch(addMessageToSimpleChatHistory(llmResponse)) @@ -86,17 +79,7 @@ export function Chat() { return (
- - dispatch(toggleShowFullChatHistory())} - label="Show full chat history" - /> - - - setShowSettingsModal(true)}> - - +
{chatHistory.map((c, i) => { @@ -121,7 +104,8 @@ export function Chat() { {error.message} )} - {isPending &&

Loading...

} + {/* {isPending &&

Loading...

} */} +
{ @@ -156,6 +140,7 @@ function RenderLLMResponse({

This was generated by an LLM that can make mistakes.

+
{post}

- +
- +
) +} + + + +function LinkQStatus({ + chatIsPending, +}:{ + chatIsPending: boolean, +}) { + const fullChatHistory = useAppSelector(state => state.chatHistory.fullChatHistory) + const { runQueryIsPending, summarizeResultsIsPending } = useRunQuery() + + let color = "blue" + let displayMessage = "Waiting for User Input" + const stage = fullChatHistory.at(-1)?.stage + if(chatIsPending) { + color = "yellow" + displayMessage = stage || "" + } + else if(runQueryIsPending) { + color = "yellow" + displayMessage = "Executing Query" + } + else if(summarizeResultsIsPending) { + color = "yellow" + displayMessage = "Query Summarization" + } + + return ( +

{displayMessage}

+ ) } \ No newline at end of file diff --git a/src/components/LLMWarning.tsx b/src/components/LLMWarning.tsx index e88357a..bb70ad5 100644 --- a/src/components/LLMWarning.tsx +++ b/src/components/LLMWarning.tsx @@ -15,7 +15,7 @@ export const LLMWarning = ({ {children} - + diff --git a/src/components/QueryEditor/QueryEditor.tsx b/src/components/QueryEditor/QueryEditor.tsx index 4f10fe9..353e298 100644 --- a/src/components/QueryEditor/QueryEditor.tsx +++ b/src/components/QueryEditor/QueryEditor.tsx @@ -15,6 +15,7 @@ import { useAppDispatch, useAppSelector } from 'redux/store'; import { useRunQuery } from 'hooks/useRunQuery'; import styles from "./QueryEditor.module.scss" +import { LLMWarning } from 'components/LLMWarning'; export function QueryEditor() { const dispatch = useAppDispatch() @@ -48,7 +49,13 @@ export function QueryEditor() {
- Query History + + Query History + + <LLMWarning> + <p>These query names are generated by an LLM</p> + </LLMWarning> + {queryHistory.map((record,i) => { return ( diff --git a/src/components/Results/Results.module.scss b/src/components/Results/Results.module.scss new file mode 100644 index 0000000..dd96a50 --- /dev/null +++ b/src/components/Results/Results.module.scss @@ -0,0 +1,6 @@ +/* Copyright (c) 2024 Massachusetts Institute of Technology */ +/* SPDX-License-Identifier: MIT */ + +#empty-results-message { + text-align: center; +} \ No newline at end of file diff --git a/src/components/Results/Results.tsx b/src/components/Results/Results.tsx new file mode 100644 index 0000000..1fa215f --- /dev/null +++ b/src/components/Results/Results.tsx @@ -0,0 +1,77 @@ +// Copyright (c) 2024 Massachusetts Institute of Technology +// SPDX-License-Identifier: MIT +import { Title } from "@mantine/core" +import { ErrorMessage } from "components/ErrorMessage" +import { InfoModal } from "components/InfoModal" +import { LLMWarning } from "components/LLMWarning" +import { ResultsTable } from "components/ResultsTable/ResultsTable" +import { useRunQuery } from "hooks/useRunQuery" +import { useAppSelector } from "redux/store" + +import styles from "./Results.module.scss" + +export function Results() { + const { runQueryIsPending } = useRunQuery() + const results = useAppSelector(state => state.results.results) + + const summaryContent = (() => { + if(results) { + if(results.summary) { + return ( +
+ +

This results summary was generated by an LLM that can make mistakes. Refer below to the Results Table from KG for ground-truth data.

+

Note that the absence of data does not necessairly mean that there is actually no data in the data source. It is possible that the query did not find what that you are looking for.

+
+ +

{results.summary}

+
+ ) + } + else { + return

Loading...

+ } + } + })() + + const resultsContent = (() => { + if(results?.data) { + return + } + else if(results?.error) { + return ( + <> + There was an error running your query: +
{results.error}
+ + ) + } + return null + })() + + if(runQueryIsPending) { + return

Loading...

+ } + else if(results?.error || results?.data) { + return ( + <> + Results Summary from LLM + {summaryContent} + +
+ + + Results Table from KG + <InfoModal title="Results Table from KG"> + <p>These are ground-truth results retrieved from the KG using the query you executed.</p> + <p>Note that the absence of data does not necessairly mean that there is actually no data in the data source. It is possible that the query did not find what that you are looking for.</p> + </InfoModal> + + {resultsContent} + + ) + } + else { + return

Run a query to see results!

+ } +} \ No newline at end of file diff --git a/src/components/Settings/Settings.module.scss b/src/components/Settings/Settings.module.scss new file mode 100644 index 0000000..b8d122b --- /dev/null +++ b/src/components/Settings/Settings.module.scss @@ -0,0 +1,9 @@ +/* Copyright (c) 2024 Massachusetts Institute of Technology */ +/* SPDX-License-Identifier: MIT */ + +.settings-button { + position: absolute; + top: 0.5rem; + left: 0.5rem; + z-index: 1; +} \ No newline at end of file diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx new file mode 100644 index 0000000..e6d1aa0 --- /dev/null +++ b/src/components/Settings/Settings.tsx @@ -0,0 +1,95 @@ +import { useEffect, useState } from "react" + +import { useQuery } from "@tanstack/react-query" + +import { ActionIcon, Checkbox, Modal, Select, TextInput } from "@mantine/core" +import { IconSettings } from "@tabler/icons-react" + +import { ErrorMessage } from "components/ErrorMessage" + +import { useMainChatAPI } from "hooks/useMainChatAPI" + +import { setApiKey, setBaseURL, setModel } from "redux/settingsSlice" +import { toggleShowFullChatHistory } from "redux/chatHistorySlice" +import { useAppDispatch, useAppSelector } from "redux/store" + +import styles from "./Settings.module.scss" + +export function Settings() { + const dispatch = useAppDispatch() + + const apiKey = useAppSelector(state => state.settings.apiKey) + const baseURL = useAppSelector(state => state.settings.baseURL) + const model = useAppSelector(state => state.settings.model) + const showFullChatHistory = useAppSelector(state => state.chatHistory.showFullChatHistory) + + const [showSettingsModal, setShowSettingsModal] = useState(false) + const closeSettingsModal = () => setShowSettingsModal(false) + + const chatAPI = useMainChatAPI() + + const {data, error, isLoading} = useQuery({ + queryKey: [baseURL], + queryFn: async () => { + //https://platform.openai.com/docs/api-reference/models/list + const list = await chatAPI.openAI.models.list() + return list.data.map(({id}) => id).sort() + }, + }) + useEffect(() => { + //if the model chosen is no longer in the list + if(data && !data.includes(model) && data[0]) { + //auto-pick the first option + dispatch(setModel(data[0])) + } + }, [data]) + + return ( + <> + + dispatch(toggleShowFullChatHistory())} + label="Show full chat history" + /> +
+ { + //IDK if this is necessary, but we don't want to accidentally send an API key to the wrong baseURL + dispatch(setApiKey("")) + dispatch(setBaseURL(event.currentTarget.value)) + }} + /> +
+ dispatch(setApiKey(event.currentTarget.value))} + /> +
+ {isLoading ?

Loading models...

: ( + <> +