From 3404a665c5f96a9afa23f7cc44b13664b4680a47 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Thu, 1 Jun 2023 16:55:02 +0200 Subject: [PATCH 01/49] starting working on queryTranslator --- package.json | 1 + src/component/field/Field.tsx | 2 +- src/dashboard/drawer/DashboardDrawer.tsx | 1 + src/extensions/ExtensionConfig.tsx | 35 +++ .../query-translator/QueryTranslator.tsx | 63 ++++++ .../query-translator/QueryTranslatorConfig.ts | 96 ++++++++ src/extensions/query-translator/README.md | 7 + .../modelClients/ModelClient.ts | 19 ++ .../modelClients/OpenAi/OpenAiClient.ts | 83 +++++++ .../modelClients/OpenAi/OpenAiClientOLD.ts | 210 ++++++++++++++++++ .../modelClients/VertexAi/VertexAiClient.ts | 210 ++++++++++++++++++ .../query-translator/modelClients/const.ts | 42 ++++ .../settings/ClientSettings.tsx | 105 +++++++++ .../settings/QueryTranslatorSettingsModal.tsx | 79 +++++++ .../state/QueryTranslatorActions.ts | 46 ++++ .../state/QueryTranslatorReducer.ts | 70 ++++++ .../state/QueryTranslatorSelector.ts | 53 +++++ .../state/QueryTranslatorThunks.ts | 31 +++ src/modal/SaveModal.tsx | 3 + src/settings/SettingsModal.tsx | 19 +- yarn.lock | 28 ++- 21 files changed, 1198 insertions(+), 5 deletions(-) create mode 100644 src/extensions/query-translator/QueryTranslator.tsx create mode 100644 src/extensions/query-translator/QueryTranslatorConfig.ts create mode 100644 src/extensions/query-translator/README.md create mode 100644 src/extensions/query-translator/modelClients/ModelClient.ts create mode 100644 src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts create mode 100644 src/extensions/query-translator/modelClients/OpenAi/OpenAiClientOLD.ts create mode 100644 src/extensions/query-translator/modelClients/VertexAi/VertexAiClient.ts create mode 100644 src/extensions/query-translator/modelClients/const.ts create mode 100644 src/extensions/query-translator/settings/ClientSettings.tsx create mode 100644 src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx create mode 100644 src/extensions/query-translator/state/QueryTranslatorActions.ts create mode 100644 src/extensions/query-translator/state/QueryTranslatorReducer.ts create mode 100644 src/extensions/query-translator/state/QueryTranslatorSelector.ts create mode 100644 src/extensions/query-translator/state/QueryTranslatorThunks.ts diff --git a/package.json b/package.json index 36049893b..93e20607c 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "mui-color": "^2.0.0-beta.2", "mui-nested-menu": "^3.2.1", "neo4j-client-sso": "^1.2.2", + "openai": "^3.2.1", "postcss": "^8.4.21", "postcss-loader": "^7.2.4", "postcss-preset-env": "^8.3.0", diff --git a/src/component/field/Field.tsx b/src/component/field/Field.tsx index 8a0c27e6e..c57c184c8 100644 --- a/src/component/field/Field.tsx +++ b/src/component/field/Field.tsx @@ -30,8 +30,8 @@ const NeoField = ({ onChange: (newValue) => onChange(newValue.value), value: value != null ? { label: value, value: value } : { label: defaultValue, value: defaultValue }, menuPlacement: 'auto', + isDisabled: disabled, }} - disabled={disabled} helpText={helperText} placeholder={placeholder} size={size} diff --git a/src/dashboard/drawer/DashboardDrawer.tsx b/src/dashboard/drawer/DashboardDrawer.tsx index 8de21f669..d04c084eb 100644 --- a/src/dashboard/drawer/DashboardDrawer.tsx +++ b/src/dashboard/drawer/DashboardDrawer.tsx @@ -85,6 +85,7 @@ export const NeoDrawer = ({ dashboardSettings={dashboardSettings} updateDashboardSetting={updateDashboardSetting} navItemClass={navItemClass} + extensions={extensions} > diff --git a/src/extensions/ExtensionConfig.tsx b/src/extensions/ExtensionConfig.tsx index 56d3e9658..abf94e124 100644 --- a/src/extensions/ExtensionConfig.tsx +++ b/src/extensions/ExtensionConfig.tsx @@ -5,6 +5,9 @@ import { workflowReducer } from './workflows/state/WorkflowReducer'; import NeoWorkflowDrawerButton from './workflows/component/WorkflowDrawerButton'; import SidebarDrawerButton from './sidebar/SidebarDrawerButton'; import { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; +import { QUERY_TRANSLATOR_ACTION_PREFIX } from './query-translator/state/QueryTranslatorActions'; +import { queryTranslatorReducer } from './query-translator/state/QueryTranslatorReducer'; +import QueryTranslator from './query-translator/QueryTranslator'; // TODO: continue documenting interface interface Extension { @@ -18,6 +21,7 @@ interface Extension { reducerPrefix?: string; reducerObject?: any; drawerButton?: JSX.Element; + settingsModal?: JSX.Element; } // TODO: define extension config interface @@ -78,6 +82,18 @@ export const EXTENSIONS: Record = { 'An extension to create, manage, and run workflows consisting of Cypher queries. Workflows can be used to run ETL flows, complex query chains, or run graph data science workloads.', link: 'https://neo4j.com/professional-services/', }, + 'query-translator': { + name: 'query-translator', + label: 'Query Translator', + author: 'Neo4j Professional Services', + image: 'https://www.unfe.org/wp-content/uploads/2019/04/SM-placeholder.png', // TODO: Fix placeholder image. + enabled: true, + reducerPrefix: QUERY_TRANSLATOR_ACTION_PREFIX, + reducerObject: queryTranslatorReducer, + settingsModal: QueryTranslator, + description: 'ask queries in natural language (available only in english).', + link: 'https://neo4j.com/professional-services/', + }, }; /** @@ -117,5 +133,24 @@ function getExtensionDrawerButtons() { return buttons; } +/** + * At the start of the application, we want to collect programmatically the extensions that need to be added inside the SettingsModal. + * @returns + */ +function getExtensionSettingsModal() { + let modals = {}; + Object.values(EXTENSIONS).forEach((extension) => { + try { + if (extension.settingsModal) { + modals[extension.name] = extension.settingsModal; + } + } catch (e) { + console.log(`Something wrong happened while loading the Extensions settings modals : ${e}`); + } + }); + return modals; +} + export const EXTENSIONS_REDUCERS = getExtensionReducers(); export const EXTENSIONS_DRAWER_BUTTONS = getExtensionDrawerButtons(); +export const EXTENSIONS_SETTINGS_MODALS = getExtensionSettingsModal(); diff --git a/src/extensions/query-translator/QueryTranslator.tsx b/src/extensions/query-translator/QueryTranslator.tsx new file mode 100644 index 000000000..53aa9a023 --- /dev/null +++ b/src/extensions/query-translator/QueryTranslator.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { deleteAllMessageHistory, setGlobalModelClient } from './state/QueryTranslatorActions'; +import { getApiKey, getClientSettings, getModelProvider } from './state/QueryTranslatorSelector'; +import { Button } from '@neo4j-ndl/react'; +import TranslateIcon from '@mui/icons-material/Translate'; +import QueryTranslatorSettingsModal from './settings/QueryTranslatorSettingsModal'; +/** + * TODO: + * 1. The query translator should handle all the requests from the cards to the client + * 2. When changing the modeltype, reset all the history of messages + * 3. create system message from here to prevent fucking all up during the thunk, o each modelProvider change and at the start pull all the db schema + */ + +export const QueryTranslator = ({ + apiKey, + modelProvider, + clientSettings, + _deleteAllMessageHistory, + _setGlobalModelClient, +}) => { + const [open, setOpen] = React.useState(false); + // When changing provider, i will reset all the messages to prevent strage results + useEffect(() => { + if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { + console.log(`henlo ${[modelProvider, apiKey, JSON.stringify(clientSettings)]}`); + } + }, [modelProvider, apiKey, clientSettings]); + + const button = ( +
+ +
+ ); + + const component = ( +
+ {button} + {open ? : <>} +
+ ); + + return component; +}; + +const mapStateToProps = (state) => ({ + apiKey: getApiKey(state), + modelProvider: getModelProvider(state), + clientSettings: getClientSettings(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + deleteAllMessageHistory: () => { + dispatch(deleteAllMessageHistory()); + }, + setGlobalModelClient: (modelClient) => { + dispatch(setGlobalModelClient(modelClient)); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(QueryTranslator); diff --git a/src/extensions/query-translator/QueryTranslatorConfig.ts b/src/extensions/query-translator/QueryTranslatorConfig.ts new file mode 100644 index 000000000..f76f627a2 --- /dev/null +++ b/src/extensions/query-translator/QueryTranslatorConfig.ts @@ -0,0 +1,96 @@ +import { SELECTION_TYPES } from '../../config/CardConfig'; +import { ModelClient } from './modelClients/ModelClient'; +import { OpenAiClient } from './modelClients/OpenAi/OpenAiClient'; +import { VertexAiClient } from './modelClients/VertexAi/VertexAiClient'; + +interface ClientSettings { + apiKey: any; + modelType: any; + region?: any; +} + +interface ClientConfig { + clientName: string; + clientClass: ModelClient; + clientSettingsModal: JSX.Element; + settings: ClientSettings; +} + +interface AvailableClients { + openAi: ClientConfig; + vertexAi: ClientConfig; +} + +interface QueryTranslatorConfig { + availableClients: AvailableClients; +} + +export const QUERY_TRANSLATOR_CONFIG: QueryTranslatorConfig = { + availableClients: { + openAi: { + clientName: 'openAi', + clientClass: OpenAiClient, + settings: { + apiKey: { + label: 'Api Key to authenticate the client', + type: SELECTION_TYPES.TEXT, + default: '', + required: true, + }, + modelType: { + label: 'Select from the possible model types', + type: SELECTION_TYPES.LIST, + methodFromClient: 'getListModels', + default: '', + }, + }, + }, + // vertexAi: { + // clientName: "vertexAi", + // clientClass: VertexAiClient, + // settings: { + // apiKey: { + // label: 'Api Key to authenticate the client', + // type: SELECTION_TYPES.TEXT, + // default: '', + // }, + // modelType: { + // label: 'Select from the possible model types', + // type: SELECTION_TYPES.LIST, + // needsStateValues: true, + // default: "Insert your Api Key first", + // }, + // region: { + // label: 'GCP Region', + // type: SELECTION_TYPES.LIST, + // needsStateValues: true, + // default: [], + // } + // } + // }, + }, +}; + +/** + * Function to get the extension config + * @param extensionName Name of the desired extension + * @returns Predefined fields of configuration for an extension + */ +export function getQueryTranslatorDefaultConfig(providerName) { + return QUERY_TRANSLATOR_CONFIG.availableClients && + QUERY_TRANSLATOR_CONFIG.availableClients[providerName] && + QUERY_TRANSLATOR_CONFIG.availableClients[providerName].settings + ? QUERY_TRANSLATOR_CONFIG.availableClients[providerName].settings + : {}; +} + +/** + * Given the provider and the settings in input, return the related client object + * @param modelProvider Name of the provider (for example: OpenAi) + * @param settings Dictionary that will be unpacked by the client itself + * @returns Client object related to the provider + */ +export function getModelProviderObject(modelProvider, settings) { + let modelProviderClass = QUERY_TRANSLATOR_CONFIG.availableClients[modelProvider].clientClass; + return new modelProviderClass(settings); +} diff --git a/src/extensions/query-translator/README.md b/src/extensions/query-translator/README.md new file mode 100644 index 000000000..f5e00c7ec --- /dev/null +++ b/src/extensions/query-translator/README.md @@ -0,0 +1,7 @@ +Ideally we want to create a component agnostic to the type of model. + +Methods for model: +1. generate prompt +2. chat completion + +The history should be kept by the component, and passed down to the client when needed. \ No newline at end of file diff --git a/src/extensions/query-translator/modelClients/ModelClient.ts b/src/extensions/query-translator/modelClients/ModelClient.ts new file mode 100644 index 000000000..9a517f8bd --- /dev/null +++ b/src/extensions/query-translator/modelClients/ModelClient.ts @@ -0,0 +1,19 @@ +// A model client should just handle the communication +export interface ModelClient { + apiKey: string; + setApiKey: any; + modelType: string | undefined; + listAvailableModels: string[]; + chatCompletion: any; + createSystemMessage: any; + addUserMessage: any; + validateQuery: any; + driver; +} + +// to see if i need this +export enum ModelOperationState { + RUNNING, + DONE, + ERROR, +} diff --git a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts new file mode 100644 index 000000000..d2d05cd5e --- /dev/null +++ b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts @@ -0,0 +1,83 @@ +import { TroubleshootOutlined } from '@mui/icons-material'; +import { Configuration, OpenAIApi } from 'openai'; +import { ModelClient } from '../ModelClient'; + +const consoleLogAsync = async (message: string, other?: any) => { + await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); +}; + +export class OpenAiClient implements ModelClient { + apiKey: string; + + modelType: string | undefined; + + listAvailableModels: string[]; + + chatCompletion!: any; + + createSystemMessage!: any; + + addUserMessage!: any; + + validateQuery!: any; + + modelClient!: OpenAIApi; + + driver: any; + + constructor(settings) { + this.apiKey = settings.apiKey; + this.listAvailableModels = []; + this.setModelClient(); + } + + setModelClient() { + const configuration = new Configuration({ + apiKey: this.apiKey, + }); + this.modelClient = new OpenAIApi(configuration); + } + + async authenticate(setIsAuthenticated) { + try { + let tmp = await this.getListModels(); + setIsAuthenticated(tmp.length > 0); + return tmp.length > 0; + } catch (e) { + consoleLogAsync('Authentication went wrong: ', e); + return false; + } + } + + async getListModels() { + let res; + try { + if (!this.modelClient) { + throw new Error('no client defined'); + } + let req = await this.modelClient.listModels(); + // Extracting the names + res = req.data.data.map((x) => x.id); + } catch (e) { + consoleLogAsync('Error while loading the model list: ', e); + res = []; + } + return res; + } + + setApiKey(apiKey) { + this.apiKey = apiKey; + const configuration = new Configuration({ + apiKey: apiKey, + }); + this.modelClient = new OpenAIApi(configuration); + } + + setListAvailableModels(listModels) { + this.listAvailableModels = listModels; + } + + setModelType(modelType) { + this.modelType = modelType; + } +} diff --git a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClientOLD.ts b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClientOLD.ts new file mode 100644 index 000000000..78d8defe6 --- /dev/null +++ b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClientOLD.ts @@ -0,0 +1,210 @@ +import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from 'openai'; +import { REPORT_TYPES } from '../../../../config/ReportConfig'; +import { nodePropsQuery, relPropsQuery, relQuery, reportTypesToDesc } from '../const'; +import { ModelClient } from '../ModelClient'; + +const consoleLogAsync = async (message: string, other?: any) => { + await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); +}; + +export class OpenAiClient implements ModelClient { + messages: Array; + + apiKey: string; + + modelClient: OpenAIApi; + + driver; + + setMessages; + + database; + + nodeProps; + + relProps; + + rels; + + schemaText; + + reportType: string; + + constructor(apiKey, messages = [], setMessages, driver, database, reportType) { + this.messages = messages; + this.setMessages = setMessages; + this.apiKey = apiKey; + if (apiKey) { + this.setApiKey(apiKey); + } + this.driver = driver; + this.database = database; + this.generateSchema(); + this.updateReportType(reportType); + } + + updateReportType(reportType) { + this.reportType = REPORT_TYPES[reportType].label; + } + + resetClient() { + this.messages = []; + this.setMessages([]); + this.generateSchema(); + } + + setNodeProps(props) { + this.nodeProps = props; + } + + setRelProps(props) { + this.relProps = props; + } + + setRels(props) { + this.rels = props; + } + + setSchemaText() { + this.schemaText = ` + This is the schema representation of the Neo4j database. + Node properties are the following: + ${JSON.stringify(this.nodeProps)} + Relationship properties are the following: + ${JSON.stringify(this.relProps)} + Relationship point from source to target nodes + ${JSON.stringify(this.rels)} + Make sure to respect relationship types and directions + `; + } + + async queryDatabase(query) { + const session = this.driver.session({ database: this.database }); + const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); + + let res = await transaction + .run(query, undefined) + .then((res) => { + const { records } = res; + let elems = records.map((elem) => { + return elem.toObject()[elem.keys[0]]; + }); + records.length > 0 ?? elems.unshift(records[0].keys); + transaction.commit(); + return elems; + }) + .catch(async (e) => { + await consoleLogAsync(`Error while running ${query}`, e); + }); + return res; + } + + async generateSchema() { + this.setNodeProps(await this.queryDatabase(nodePropsQuery)); + this.setRelProps(await this.queryDatabase(relPropsQuery)); + this.setRels(await this.queryDatabase(relQuery)); + this.setSchemaText(); + } + + getSystemMessage() { + return ` + Task: Generate Cypher queries to query a Neo4j graph database based on the provided schema definition. These queries will be used inside NeoDash reports. + Documentation for NeoDash is here : https://neo4j.com/labs/neodash/2.2/ + Instructions: + Use only the provided relationship types and properties. + Do not use any other relationship types or properties that are not provided. + The Cypher RETURN clause must contained certain variables, based on the report type asked for. + Report types : + Table - Multiple variables representing property values of nodes and relationships. + Graph - Multiple variables representing nodes objects and relationships objects inside the graph. + Bar Chart - Two variables named category(a String value) and value(numeric value). + Line Chart - Two numeric variables named x and y. + Sunburst - Two variables named Path(list of strings) and value(a numerical value). + Circle Packing - Two variables named Path(a list of strings) and value(a numerical value). + Choropleth - Two variables named code(a String value) and value(a numerical value). + Area Map - Two variables named code(a String value) and value(a numerical value). + Treemap - Two variables named Path(a list of strings) and value(a numerical value). + Radar Chart - Multiple variables representing property values of nodes and relationships. + Sankey Chart - Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value). + Map - multiple variables representing nodes objects(should contain spatial propeties) and relationship objects. + Single Value - A single value of a single variable. + Gauge Chart - A single value of a single variable. + Raw JSON - The Cypher query must return a JSON object that will be displayed as raw JSON data. + Pie Chart - Two variables named category and value. + Schema: + ${this.schemaText} + `; + } + + setApiKey(apiKey) { + this.apiKey = apiKey; + const configuration = new Configuration({ + apiKey: apiKey, + }); + this.modelClient = new OpenAIApi(configuration); + } + + addUserMessage(content) { + let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${ + reportTypesToDesc[this.reportType] + } Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. `; + this.messages.push({ role: 'user', content: finalMessage }); + } + + addSystemMessage(content) { + this.messages.push({ role: 'assistant', content: content }); + } + + updateMessageHistory(message) { + this.messages.push(message); + this.setMessages(this.messages); + } + + async chatCompletion( + content, + setResponse = (res) => { + console.log(res); + } + ) { + try { + if (this.messages.length == 0) { + if (this.schemaText) { + this.addSystemMessage(this.getSystemMessage()); + } else { + await this.generateSchema(); + this.addSystemMessage(this.getSystemMessage()); + } + } + if (this.apiKey) { + this.addUserMessage(content); + + const completion = await this.modelClient.createChatCompletion({ + model: 'gpt-3.5-turbo', + messages: this.messages, + }); + + // If the status is correct + if ( + completion.status == 200 && + completion.data && + completion.data.choices && + completion.data.choices[0].message + ) { + let { message } = completion.data.choices[0]; + this.updateMessageHistory(message); + setResponse(message.content); + } else { + throw Error(`Request returned with status: ${completion.status}`); + } + } else { + throw Error('api key not present'); + } + } catch (error) { + setResponse(!this.apiKey ? 'key not present' : `${error}`); + await consoleLogAsync('error during query', error); + } finally { + // TODO: trigger availability of the card (we should stop clicking on the card to prevent strange misconfigurations here) + await consoleLogAsync('done', this); + } + } +} diff --git a/src/extensions/query-translator/modelClients/VertexAi/VertexAiClient.ts b/src/extensions/query-translator/modelClients/VertexAi/VertexAiClient.ts new file mode 100644 index 000000000..425d6ec60 --- /dev/null +++ b/src/extensions/query-translator/modelClients/VertexAi/VertexAiClient.ts @@ -0,0 +1,210 @@ +import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from 'openai'; +import { REPORT_TYPES } from '../../../../config/ReportConfig'; +import { nodePropsQuery, relPropsQuery, relQuery, reportTypesToDesc } from '../const'; +import { ModelClient } from '../ModelClient'; + +const consoleLogAsync = async (message: string, other?: any) => { + await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); +}; + +export class VertexAiClient implements ModelClient { + messages: Array; + + apiKey: string; + + modelClient: OpenAIApi; + + driver; + + setMessages; + + database; + + nodeProps; + + relProps; + + rels; + + schemaText; + + reportType: string; + + constructor(apiKey, messages = [], setMessages, driver, database, reportType) { + this.messages = messages; + this.setMessages = setMessages; + this.apiKey = apiKey; + if (apiKey) { + this.setApiKey(apiKey); + } + this.driver = driver; + this.database = database; + this.generateSchema(); + this.updateReportType(reportType); + } + + updateReportType(reportType) { + this.reportType = REPORT_TYPES[reportType].label; + } + + resetClient() { + this.messages = []; + this.setMessages([]); + this.generateSchema(); + } + + setNodeProps(props) { + this.nodeProps = props; + } + + setRelProps(props) { + this.relProps = props; + } + + setRels(props) { + this.rels = props; + } + + setSchemaText() { + this.schemaText = ` + This is the schema representation of the Neo4j database. + Node properties are the following: + ${JSON.stringify(this.nodeProps)} + Relationship properties are the following: + ${JSON.stringify(this.relProps)} + Relationship point from source to target nodes + ${JSON.stringify(this.rels)} + Make sure to respect relationship types and directions + `; + } + + async queryDatabase(query) { + const session = this.driver.session({ database: this.database }); + const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); + + let res = await transaction + .run(query, undefined) + .then((res) => { + const { records } = res; + let elems = records.map((elem) => { + return elem.toObject()[elem.keys[0]]; + }); + records.length > 0 ?? elems.unshift(records[0].keys); + transaction.commit(); + return elems; + }) + .catch(async (e) => { + await consoleLogAsync(`Error while running ${query}`, e); + }); + return res; + } + + async generateSchema() { + this.setNodeProps(await this.queryDatabase(nodePropsQuery)); + this.setRelProps(await this.queryDatabase(relPropsQuery)); + this.setRels(await this.queryDatabase(relQuery)); + this.setSchemaText(); + } + + getSystemMessage() { + return ` + Task: Generate Cypher queries to query a Neo4j graph database based on the provided schema definition. These queries will be used inside NeoDash reports. + Documentation for NeoDash is here : https://neo4j.com/labs/neodash/2.2/ + Instructions: + Use only the provided relationship types and properties. + Do not use any other relationship types or properties that are not provided. + The Cypher RETURN clause must contained certain variables, based on the report type asked for. + Report types : + Table - Multiple variables representing property values of nodes and relationships. + Graph - Multiple variables representing nodes objects and relationships objects inside the graph. + Bar Chart - Two variables named category(a String value) and value(numeric value). + Line Chart - Two numeric variables named x and y. + Sunburst - Two variables named Path(list of strings) and value(a numerical value). + Circle Packing - Two variables named Path(a list of strings) and value(a numerical value). + Choropleth - Two variables named code(a String value) and value(a numerical value). + Area Map - Two variables named code(a String value) and value(a numerical value). + Treemap - Two variables named Path(a list of strings) and value(a numerical value). + Radar Chart - Multiple variables representing property values of nodes and relationships. + Sankey Chart - Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value). + Map - multiple variables representing nodes objects(should contain spatial propeties) and relationship objects. + Single Value - A single value of a single variable. + Gauge Chart - A single value of a single variable. + Raw JSON - The Cypher query must return a JSON object that will be displayed as raw JSON data. + Pie Chart - Two variables named category and value. + Schema: + ${this.schemaText} + `; + } + + setApiKey(apiKey) { + this.apiKey = apiKey; + const configuration = new Configuration({ + apiKey: apiKey, + }); + this.modelClient = new OpenAIApi(configuration); + } + + addUserMessage(content) { + let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${ + reportTypesToDesc[this.reportType] + } Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. `; + this.messages.push({ role: 'user', content: finalMessage }); + } + + addSystemMessage(content) { + this.messages.push({ role: 'assistant', content: content }); + } + + updateMessageHistory(message) { + this.messages.push(message); + this.setMessages(this.messages); + } + + async chatCompletion( + content, + setResponse = (res) => { + console.log(res); + } + ) { + try { + if (this.messages.length == 0) { + if (this.schemaText) { + this.addSystemMessage(this.getSystemMessage()); + } else { + await this.generateSchema(); + this.addSystemMessage(this.getSystemMessage()); + } + } + if (this.apiKey) { + this.addUserMessage(content); + + const completion = await this.modelClient.createChatCompletion({ + model: 'gpt-3.5-turbo', + messages: this.messages, + }); + + // If the status is correct + if ( + completion.status == 200 && + completion.data && + completion.data.choices && + completion.data.choices[0].message + ) { + let { message } = completion.data.choices[0]; + this.updateMessageHistory(message); + setResponse(message.content); + } else { + throw Error(`Request returned with status: ${completion.status}`); + } + } else { + throw Error('api key not present'); + } + } catch (error) { + setResponse(!this.apiKey ? 'key not present' : `${error}`); + await consoleLogAsync('error during query', error); + } finally { + // TODO: trigger availability of the card (we should stop clicking on the card to prevent strange misconfigurations here) + await consoleLogAsync('done', this); + } + } +} diff --git a/src/extensions/query-translator/modelClients/const.ts b/src/extensions/query-translator/modelClients/const.ts new file mode 100644 index 000000000..e8fb96c65 --- /dev/null +++ b/src/extensions/query-translator/modelClients/const.ts @@ -0,0 +1,42 @@ +export const nodePropsQuery = `CALL apoc.meta.data() +YIELD label, other, elementType, type, property +WHERE NOT type = "RELATIONSHIP" AND elementType = "node" +WITH label AS nodeLabels, collect(property) AS properties +RETURN {labels: nodeLabels, properties: properties} AS output +`; + +export const relPropsQuery = ` +CALL apoc.meta.data() +YIELD label, other, elementType, type, property +WHERE NOT type = "RELATIONSHIP" AND elementType = "relationship" +WITH label AS nodeLabels, collect(property) AS properties +RETURN {type: nodeLabels, properties: properties} AS output +`; + +export const relQuery = ` +CALL apoc.meta.data() +YIELD label, other, elementType, type, property +WHERE type = "RELATIONSHIP" AND elementType = "node" +RETURN {source: label, relationship: property, target: other} AS output +`; + +export const reportTypesToDesc = { + Table: 'Multiple variables representing property values of nodes and relationships.', + Graph: + 'Multiple variables representing nodes objects and relationships objects inside the graph. Please return also the relationship objects.', + 'Bar Chart': 'Two variables named category(a String value) and value(numeric value).', + 'Line Chart': 'Two numeric variables named x and y.', + Sunburst: 'Two variables named Path(list of strings) and value(a numerical value).', + 'Circle Packing': 'Two variables named Path(a list of strings) and value(a numerical value).', + Choropleth: 'Two variables named code(a String value) and value(a numerical value).', + 'Area Map': 'Two variables named code(a String value) and value(a numerical value).', + Treemap: 'Two variables named Path(a list of strings) and value(a numerical value).', + 'Radar Chart': 'Multiple variables representing property values of nodes and relationships.', + 'Sankey Chart': + 'Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value).', + Map: 'multiple variables representing nodes objects(should contain spatial propeties) and relationship objects.', + 'Single Value': 'A single value of a single variable.', + 'Gauge Chart': 'A single value of a single variable.', + 'Raw JSON': 'The Cypher query must return a JSON object that will be displayed as raw JSON data.', + 'Pie Chart': 'Two variables named category and value.', +}; diff --git a/src/extensions/query-translator/settings/ClientSettings.tsx b/src/extensions/query-translator/settings/ClientSettings.tsx new file mode 100644 index 000000000..87d536f78 --- /dev/null +++ b/src/extensions/query-translator/settings/ClientSettings.tsx @@ -0,0 +1,105 @@ +import React, { useCallback, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { debounce, List, ListItem } from '@mui/material'; +import { getModelProviderObject, getQueryTranslatorDefaultConfig } from '../QueryTranslatorConfig'; +import { getClientSettings } from '../state/QueryTranslatorSelector'; +import NeoSetting from '../../../component/field/Setting'; +import { settingsReducer } from '../../../settings/SettingsReducer'; + +const update = (state, mutations) => Object.assign({}, state, mutations); + +// TODO - this is also very similar to the existing settings form in the card settings. +export const ClientSettings = ({ modelProvider, settingState, setSettingsState }) => { + const defaultSettings = getQueryTranslatorDefaultConfig(modelProvider); + const requiredSettings = Object.keys(defaultSettings).filter((setting) => defaultSettings[setting].required); + const [localSettings, setLocalSettings] = React.useState(settingState); + const [isAuthenticated, setIsAuthenticated] = React.useState(false); + const [localClient, setLocalClient] = React.useState(undefined); + const [settingChoices, setSettingChoices] = React.useState({}); + + const updateSpecificSettingChoices = (field: string, value: any) => { + const entry = {}; + entry[field] = value; + setSettingChoices(update(settingChoices, entry)); + }; + + const updateSpecificExtensionSetting = (field: string, value: any, local = false) => { + const entry = {}; + entry[field] = value; + local ? setLocalSettings(update(localSettings, entry)) : setSettingsState(update(settingState, entry)); + }; + const debouncedUpdateSpecificExtensionSetting = useCallback(debounce(updateSpecificExtensionSetting, 500), []); + + function checkIfDisabled(setting) { + let tmp = defaultSettings[setting]; + if (tmp.required || isAuthenticated) { + return false; + } + return !requiredSettings.every((e) => settingState[e]); + } + + useEffect(() => { + let clientObject = getModelProviderObject(modelProvider, settingState); + clientObject.authenticate(setIsAuthenticated); + }, [settingState.apiKey]); + + useEffect(() => { + let localClientTmp = getModelProviderObject(modelProvider, settingState); + if (isAuthenticated) { + setLocalClient(localClientTmp); + } + let tmpSettingsChoices = {}; + Object.keys(defaultSettings).map((setting) => { + tmpSettingsChoices[setting] = setChoices(setting, localClientTmp); + }); + }, [isAuthenticated]); + + function setChoices(setting, localClientTmp) { + let choices = defaultSettings[setting].values ? defaultSettings[setting].values : []; + let { methodFromClient } = defaultSettings[setting]; + if (methodFromClient && isAuthenticated) { + localClientTmp[methodFromClient]().then((value) => { + updateSpecificSettingChoices(setting, value); + }); + } else { + updateSpecificSettingChoices(setting, choices); + } + } + + const component = ( + + {Object.keys(defaultSettings).map((setting) => { + let disabled = checkIfDisabled(setting); + return ( + + { + updateSpecificExtensionSetting(setting, e, true); + debouncedUpdateSpecificExtensionSetting(setting, e); + }} + /> + + ); + })} + + ); + return component; +}; + +const mapStateToProps = (state) => ({ + settings: getClientSettings(state), +}); + +const mapDispatchToProps = (_dispatch) => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(ClientSettings); diff --git a/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx b/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx new file mode 100644 index 000000000..018e79057 --- /dev/null +++ b/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx @@ -0,0 +1,79 @@ +import { Badge, Dialog, DialogContent, DialogTitle, IconButton } from '@mui/material'; +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { setClientSettings, setModelProvider } from '../state/QueryTranslatorActions'; +import { getApiKey, getClientSettings, getModelProvider } from '../state/QueryTranslatorSelector'; +import SaveIcon from '@mui/icons-material/Save'; +import { SELECTION_TYPES } from '../../../config/CardConfig'; +import NeoSetting from '../../../component/field/Setting'; +import { QUERY_TRANSLATOR_CONFIG } from '../QueryTranslatorConfig'; +import ClientSettings from './ClientSettings'; + +export const QueryTranslatorSettingsModal = ({ + open, + setOpen, + apiKey, + modelProvider, + clientSettings, + updateClientSettings, + updateModelProvider, +}) => { + const [modelProviderState, setModelProviderState] = React.useState(modelProvider); + const [apiKeyState, setApiKeyState] = React.useState(apiKey); + const [settingsState, setSettingsState] = React.useState(clientSettings); + const handleClose = () => { + updateModelProvider(modelProviderState); + updateClientSettings(settingsState); + setOpen(false); + }; + + return ( + + + Henlo + + + + + + + + +
+ Select your model provider: + setModelProviderState(e)} + /> +
+ {modelProviderState ? ( + + ) : ( + <>Select one of the available clients. + )} +
+
+
+ ); +}; +const mapStateToProps = (state) => ({ + apiKey: getApiKey(state), + clientSettings: getClientSettings(state), + modelProvider: getModelProvider(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + updateClientSettings: (settings) => dispatch(setClientSettings(settings)), + updateModelProvider: (modelProvider) => dispatch(setModelProvider(modelProvider)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(QueryTranslatorSettingsModal); diff --git a/src/extensions/query-translator/state/QueryTranslatorActions.ts b/src/extensions/query-translator/state/QueryTranslatorActions.ts new file mode 100644 index 000000000..604fe0ca2 --- /dev/null +++ b/src/extensions/query-translator/state/QueryTranslatorActions.ts @@ -0,0 +1,46 @@ +import { ChatCompletionRequestMessage } from 'openai'; + +export const QUERY_TRANSLATOR_ACTION_PREFIX = 'DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/'; + +export const SET_MODEL_PROVIDER = `${QUERY_TRANSLATOR_ACTION_PREFIX}SET_MODEL_PROVIDER`; +export const setModelProvider = (modelProvider) => ({ + type: SET_MODEL_PROVIDER, + payload: { modelProvider }, +}); + +export const SET_CLIENT_SETTINGS = `${QUERY_TRANSLATOR_ACTION_PREFIX}SET_CLIENT_SETTINGS`; +export const setClientSettings = (settings) => ({ + type: SET_CLIENT_SETTINGS, + payload: { settings }, +}); + +export const SET_GLOBAL_MODEL_CLIENT = `${QUERY_TRANSLATOR_ACTION_PREFIX}SET_GLOBAL_MODEL_CLIENT`; +export const setGlobalModelClient = (modelClient) => ({ + type: SET_GLOBAL_MODEL_CLIENT, + payload: { modelClient }, +}); + +export const UPDATE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_EXTENSION_TITLE`; +/** + * Action to add a new message to the history + * @param message Object defined as {user: string, message: string} + * @param pageIndex Index of the page related to the card + * @param cardIndex Index of the card inside the page + * @returns + */ +export const updateMessageHistory = (message: ChatCompletionRequestMessage, pageIndex: number, cardIndex: number) => ({ + type: UPDATE_MESSAGE_HISTORY, + payload: { message, pageIndex, cardIndex }, +}); + +export const DELETE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}DELETE_MESSAGE_HISTORY`; +export const deleteMessageHistory = (pageIndex: number, cardIndex: number) => ({ + type: DELETE_MESSAGE_HISTORY, + payload: { pageIndex, cardIndex }, +}); + +export const DELETE_ALL_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}DELETE_ALL_MESSAGE_HISTORY`; +export const deleteAllMessageHistory = () => ({ + type: DELETE_ALL_MESSAGE_HISTORY, + payload: {}, +}); diff --git a/src/extensions/query-translator/state/QueryTranslatorReducer.ts b/src/extensions/query-translator/state/QueryTranslatorReducer.ts new file mode 100644 index 000000000..ca29d2885 --- /dev/null +++ b/src/extensions/query-translator/state/QueryTranslatorReducer.ts @@ -0,0 +1,70 @@ +/** + * Reducers define changes to the application state when a given action + */ + +import { + UPDATE_MESSAGE_HISTORY, + DELETE_MESSAGE_HISTORY, + SET_MODEL_PROVIDER, + DELETE_ALL_MESSAGE_HISTORY, + SET_GLOBAL_MODEL_CLIENT, + SET_CLIENT_SETTINGS, +} from './QueryTranslatorActions'; + +export const INITIAL_EXTENSION_STATE = { + modelProvider: '', + history: {}, + modelClient: '', + settings: {}, +}; + +const update = (state, mutations) => Object.assign({}, state, mutations); + +export const queryTranslatorReducer = (state = INITIAL_EXTENSION_STATE, action: { type: any; payload: any }) => { + const { type, payload } = action; + + switch (type) { + case SET_MODEL_PROVIDER: { + const { modelProvider } = payload; + state = update(state, { modelProvider: modelProvider }); + return state; + } + case SET_CLIENT_SETTINGS: { + const { settings } = payload; + state = update(state, { settings: settings }); + return state; + } + // Object used globally to run async tasks + case SET_GLOBAL_MODEL_CLIENT: { + const { modelClient } = payload; + state = update(state, { modelClient: modelClient }); + return state; + } + case UPDATE_MESSAGE_HISTORY: { + const { message, pageIndex, cardIndex } = payload; + let newHistory = { ...state.history }; + const history = newHistory[pageIndex] && newHistory[pageIndex][cardIndex] ? newHistory[pageIndex][cardIndex] : []; + // For now we always append a message to the history + history.push(message); + newHistory[pageIndex][cardIndex] = history; + state = update(state, { history: newHistory }); + return state; + } + case DELETE_MESSAGE_HISTORY: { + const { pageIndex, cardIndex } = payload; + let newHistory = { ...state.history }; + if (newHistory && newHistory[pageIndex] && newHistory[pageIndex][cardIndex]) { + delete newHistory[pageIndex][cardIndex]; + state = update(state, { history: newHistory }); + } + return state; + } + case DELETE_ALL_MESSAGE_HISTORY: { + state = update(state, { history: {} }); + return state; + } + default: { + return state; + } + } +}; diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts new file mode 100644 index 000000000..607402ddc --- /dev/null +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -0,0 +1,53 @@ +export const queryTranslatorExtensionName = 'query-translator'; + +/** + * The extension keeps, during one session, the history of messages between a user and a model. + * This method serves to get all the messages. + * @param state Current state of the session + * @returns history of messages between the user and the model within the context of that card + */ + +const checkExtensionConfig = (state: any) => { + return state.dashboard.extensionsConfig && state.dashboard.extensionsConfig[queryTranslatorExtensionName]; +}; +export const getHistory = (state: any) => { + let history = checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].history; + return history != undefined && history ? history : {}; +}; + +export const getModelClient = (state: any) => { + let modelClient = + checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].getModelClient; + return modelClient != undefined && modelClient ? modelClient : undefined; +}; + +/** + * The extension keeps, during one session, the history of messages between a user and a model + * @param state Current state of the session + * @param pageIndex Index of the page where the card lives + * @param cardIndex Index that identifies the card inside the page + * @returns history of messages between the user and the model within the context of that card + */ +export const getHistoryPerCard = (state: any, pageIndex, cardIndex) => { + let history = getHistory(state); + let cardHistory = history[pageIndex] && history[pageIndex][cardIndex]; + return cardHistory != undefined && cardHistory ? cardHistory : []; +}; + +export const getClientSettings = (state: any) => { + let clientSettings = + checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].settings; + return clientSettings != undefined && clientSettings ? clientSettings : {}; +}; + +export const getApiKey = (state: any) => { + let settings = getClientSettings(state); + + return settings.apiKey != undefined && settings.apiKey ? settings.apiKey : ''; +}; + +export const getModelProvider = (state: any) => { + let modelProvider = + checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].modelProvider; + return modelProvider != undefined && modelProvider ? modelProvider : ''; +}; diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts new file mode 100644 index 000000000..ed5411818 --- /dev/null +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -0,0 +1,31 @@ +import { getHistoryPerCard, getModelClient } from './QueryTranslatorSelector'; + +const consoleLogAsync = async (message: string, other?: any) => { + await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); +}; + +/** + * Method to handle the request to a model client for a query translation. + * @param pageIndex Index of the page where the card lives + * @param cardIndex Index that identifies a card inside its page + * @param message Message inserted by the user + * @param setQuery Function to set the query inside the card (i don't think i need this one) + * @returns + */ +export const queryTranslationThunk = + (pageIndex, cardIndex, message, reportType) => async (dispatch: any, getState: any) => { + const state = getState(); + const modelClient = getModelClient(state); + // if (!modelClient){ + + // } + const messageHistory = getHistoryPerCard(state, pageIndex, cardIndex); + try { + modelClient.chatCompletion(message, reportType, messageHistory); + } catch (e) { + await consoleLogAsync( + `Something wrong happened while calling the model client for the card number ${cardIndex} inside the page ${pageIndex}: \n`, + { e } + ); + } + }; diff --git a/src/modal/SaveModal.tsx b/src/modal/SaveModal.tsx index 13e868acd..df68ec915 100644 --- a/src/modal/SaveModal.tsx +++ b/src/modal/SaveModal.tsx @@ -83,7 +83,10 @@ export const NeoSaveModal = ({ 'settingsOpen', 'advancedSettingsOpen', 'collapseTimeout', + 'modelClient', + 'history', ]); + const dashboardString = JSON.stringify(filteredDashboard, null, 2); const downloadDashboard = () => { const element = document.createElement('a'); diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index f0f097e6c..e95f87635 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,8 +5,21 @@ import { DASHBOARD_SETTINGS } from '../config/DashboardConfig'; import { SideNavigationItem } from '@neo4j-ndl/react'; import { Cog6ToothIconOutline } from '@neo4j-ndl/react/icons'; import { Dialog } from '@neo4j-ndl/react'; +import { EXTENSIONS_SETTINGS_MODALS } from '../extensions/ExtensionConfig'; + +export const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting, navItemClass, extensions }) => { + function getExtensionSettingsModal() { + const res = ( + <> + {Object.keys(EXTENSIONS_SETTINGS_MODALS).map((name) => { + const Component = extensions[name] ? EXTENSIONS_SETTINGS_MODALS[name] : ''; + return Component ? : <>; + })} + + ); + return res; + } -export const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting, navItemClass }) => { const [open, setOpen] = React.useState(false); const handleClickOpen = () => { @@ -18,7 +31,7 @@ export const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting, na }; const settings = DASHBOARD_SETTINGS; - + const extensionSettings = getExtensionSettingsModal(); // Else, build the advanced settings view. const advancedDashboardSettings = (
@@ -61,6 +74,8 @@ export const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting, na

{advancedDashboardSettings} + {extensionSettings} +
diff --git a/yarn.lock b/yarn.lock index ab302ccfe..a9bbe0086 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3908,6 +3908,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== +axios@^0.26.0: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + babel-loader@^8.2.3: version "8.3.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" @@ -4462,7 +4469,7 @@ colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.19: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -6361,7 +6368,7 @@ focus-lock@^0.11.6: dependencies: tslib "^2.0.3" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.8: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -6398,6 +6405,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -8804,6 +8820,14 @@ open@^8.0.9, open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openai@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866" + integrity sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A== + dependencies: + axios "^0.26.0" + form-data "^4.0.0" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" From 6b19f3e84436c32b113d95744de338e19dcdac1f Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Thu, 1 Jun 2023 18:56:32 +0200 Subject: [PATCH 02/49] added comments and starting work on openAiClient definiton --- .../query-translator/QueryTranslator.tsx | 13 +- .../query-translator/QueryTranslatorConfig.ts | 2 +- .../modelClients/OpenAi/OpenAiClient.ts | 140 +++++++++++++++++- .../settings/ClientSettings.tsx | 67 +++++---- .../settings/QueryTranslatorSettingsModal.tsx | 5 +- .../state/QueryTranslatorThunks.ts | 8 +- 6 files changed, 193 insertions(+), 42 deletions(-) diff --git a/src/extensions/query-translator/QueryTranslator.tsx b/src/extensions/query-translator/QueryTranslator.tsx index 53aa9a023..2ec34d41e 100644 --- a/src/extensions/query-translator/QueryTranslator.tsx +++ b/src/extensions/query-translator/QueryTranslator.tsx @@ -5,8 +5,10 @@ import { getApiKey, getClientSettings, getModelProvider } from './state/QueryTra import { Button } from '@neo4j-ndl/react'; import TranslateIcon from '@mui/icons-material/Translate'; import QueryTranslatorSettingsModal from './settings/QueryTranslatorSettingsModal'; +import { getModelClientObject } from './QueryTranslatorConfig'; +import { queryTranslationThunk } from './state/QueryTranslatorThunks'; /** - * TODO: + * //TODO: * 1. The query translator should handle all the requests from the cards to the client * 2. When changing the modeltype, reset all the history of messages * 3. create system message from here to prevent fucking all up during the thunk, o each modelProvider change and at the start pull all the db schema @@ -17,13 +19,15 @@ export const QueryTranslator = ({ modelProvider, clientSettings, _deleteAllMessageHistory, - _setGlobalModelClient, + setGlobalModelClient, + queryTranslation, }) => { const [open, setOpen] = React.useState(false); // When changing provider, i will reset all the messages to prevent strage results useEffect(() => { if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { - console.log(`henlo ${[modelProvider, apiKey, JSON.stringify(clientSettings)]}`); + setGlobalModelClient(getModelClientObject(modelProvider, clientSettings)); + queryTranslation(0, 0, 'hello', 'graph'); } }, [modelProvider, apiKey, clientSettings]); @@ -58,6 +62,9 @@ const mapDispatchToProps = (dispatch) => ({ setGlobalModelClient: (modelClient) => { dispatch(setGlobalModelClient(modelClient)); }, + queryTranslation: (pageIndex, cardIndex, message, reportType) => { + dispatch(queryTranslationThunk(pageIndex, cardIndex, message, reportType)); + }, }); export default connect(mapStateToProps, mapDispatchToProps)(QueryTranslator); diff --git a/src/extensions/query-translator/QueryTranslatorConfig.ts b/src/extensions/query-translator/QueryTranslatorConfig.ts index f76f627a2..9dc6ea3a0 100644 --- a/src/extensions/query-translator/QueryTranslatorConfig.ts +++ b/src/extensions/query-translator/QueryTranslatorConfig.ts @@ -90,7 +90,7 @@ export function getQueryTranslatorDefaultConfig(providerName) { * @param settings Dictionary that will be unpacked by the client itself * @returns Client object related to the provider */ -export function getModelProviderObject(modelProvider, settings) { +export function getModelClientObject(modelProvider, settings) { let modelProviderClass = QUERY_TRANSLATOR_CONFIG.availableClients[modelProvider].clientClass; return new modelProviderClass(settings); } diff --git a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts index d2d05cd5e..4408a6b89 100644 --- a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts +++ b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts @@ -1,5 +1,6 @@ import { TroubleshootOutlined } from '@mui/icons-material'; import { Configuration, OpenAIApi } from 'openai'; +import { nodePropsQuery, relPropsQuery, relQuery, reportTypesToDesc } from '../const'; import { ModelClient } from '../ModelClient'; const consoleLogAsync = async (message: string, other?: any) => { @@ -13,12 +14,8 @@ export class OpenAiClient implements ModelClient { listAvailableModels: string[]; - chatCompletion!: any; - createSystemMessage!: any; - addUserMessage!: any; - validateQuery!: any; modelClient!: OpenAIApi; @@ -57,7 +54,7 @@ export class OpenAiClient implements ModelClient { } let req = await this.modelClient.listModels(); // Extracting the names - res = req.data.data.map((x) => x.id); + res = req.data.data.map((x) => x.id).filter((x) => x.includes('gpt-3.5')); } catch (e) { consoleLogAsync('Error while loading the model list: ', e); res = []; @@ -80,4 +77,137 @@ export class OpenAiClient implements ModelClient { setModelType(modelType) { this.modelType = modelType; } + + createSchemaText(nodeProps, relProps, rels) { + return ` + This is the schema representation of the Neo4j database. + Node properties are the following: + ${JSON.stringify(nodeProps)} + Relationship properties are the following: + ${JSON.stringify(relProps)} + Relationship point from source to target nodes + ${JSON.stringify(rels)} + Make sure to respect relationship types and directions + `; + } + + async queryDatabase(query, database) { + const session = this.driver.session({ database: database }); + const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); + + let res = await transaction + .run(query, undefined) + .then((res) => { + const { records } = res; + let elems = records.map((elem) => { + return elem.toObject()[elem.keys[0]]; + }); + records.length > 0 ?? elems.unshift(records[0].keys); + transaction.commit(); + return elems; + }) + .catch(async (e) => { + await consoleLogAsync(`Error while running ${query}`, e); + }); + return res; + } + + async generateSchema(database) { + let nodeProps = await this.queryDatabase(nodePropsQuery, database); + let relProps = await this.queryDatabase(relPropsQuery, database); + let rels = await this.queryDatabase(relQuery, database); + return this.createSchemaText(nodeProps, relProps, rels); + } + + getSystemMessage(schemaText) { + return ` + Task: Generate Cypher queries to query a Neo4j graph database based on the provided schema definition. These queries will be used inside NeoDash reports. + Documentation for NeoDash is here : https://neo4j.com/labs/neodash/2.2/ + Instructions: + Use only the provided relationship types and properties. + Do not use any other relationship types or properties that are not provided. + The Cypher RETURN clause must contained certain variables, based on the report type asked for. + Report types : + Table - Multiple variables representing property values of nodes and relationships. + Graph - Multiple variables representing nodes objects and relationships objects inside the graph. + Bar Chart - Two variables named category(a String value) and value(numeric value). + Line Chart - Two numeric variables named x and y. + Sunburst - Two variables named Path(list of strings) and value(a numerical value). + Circle Packing - Two variables named Path(a list of strings) and value(a numerical value). + Choropleth - Two variables named code(a String value) and value(a numerical value). + Area Map - Two variables named code(a String value) and value(a numerical value). + Treemap - Two variables named Path(a list of strings) and value(a numerical value). + Radar Chart - Multiple variables representing property values of nodes and relationships. + Sankey Chart - Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value). + Map - multiple variables representing nodes objects(should contain spatial propeties) and relationship objects. + Single Value - A single value of a single variable. + Gauge Chart - A single value of a single variable. + Raw JSON - The Cypher query must return a JSON object that will be displayed as raw JSON data. + Pie Chart - Two variables named category and value. + Schema: + ${schemaText} + `; + } + + // TODO: adapt to the new structure, no more persisting inside the object, passign everything down + addUserMessage(content, reportType) { + let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${reportTypesToDesc[reportType]} Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. `; + return { role: 'user', content: finalMessage }; + } + + addSystemMessage(content) { + return { role: 'assistant', content: content }; + } + + // updateMessageHistory(message) { + // this.messages.push(message); + // this.setMessages(this.messages); + // } + + async chatCompletion( + content, + messages, + database, + reportType, + setResponse = (res) => { + console.log(res); + } + ) { + try { + if (messages.length == 0) { + let schema = await this.generateSchema(database); + this.addSystemMessage(this.getSystemMessage(schema)); + } + if (this.apiKey) { + this.addUserMessage(content, reportType); + + const completion = await this.modelClient.createChatCompletion({ + model: this.modelType, + messages: messages, + }); + + // If the status is correct + if ( + completion.status == 200 && + completion.data && + completion.data.choices && + completion.data.choices[0].message + ) { + let { message } = completion.data.choices[0]; + this.updateMessageHistory(message); + setResponse(message.content); + } else { + throw Error(`Request returned with status: ${completion.status}`); + } + } else { + throw Error('api key not present'); + } + } catch (error) { + setResponse(!this.apiKey ? 'key not present' : `${error}`); + await consoleLogAsync('error during query', error); + } finally { + // TODO: trigger availability of the card (we should stop clicking on the card to prevent strange misconfigurations here) + await consoleLogAsync('done', this); + } + } } diff --git a/src/extensions/query-translator/settings/ClientSettings.tsx b/src/extensions/query-translator/settings/ClientSettings.tsx index 87d536f78..60eca2b80 100644 --- a/src/extensions/query-translator/settings/ClientSettings.tsx +++ b/src/extensions/query-translator/settings/ClientSettings.tsx @@ -1,35 +1,41 @@ import React, { useCallback, useEffect } from 'react'; import { connect } from 'react-redux'; import { debounce, List, ListItem } from '@mui/material'; -import { getModelProviderObject, getQueryTranslatorDefaultConfig } from '../QueryTranslatorConfig'; +import { getModelClientObject, getQueryTranslatorDefaultConfig } from '../QueryTranslatorConfig'; import { getClientSettings } from '../state/QueryTranslatorSelector'; import NeoSetting from '../../../component/field/Setting'; -import { settingsReducer } from '../../../settings/SettingsReducer'; const update = (state, mutations) => Object.assign({}, state, mutations); -// TODO - this is also very similar to the existing settings form in the card settings. +// TODO: the following +// 1. the settings modal should save only when all the required fields are defined and we can correctly authenticate export const ClientSettings = ({ modelProvider, settingState, setSettingsState }) => { const defaultSettings = getQueryTranslatorDefaultConfig(modelProvider); const requiredSettings = Object.keys(defaultSettings).filter((setting) => defaultSettings[setting].required); const [localSettings, setLocalSettings] = React.useState(settingState); const [isAuthenticated, setIsAuthenticated] = React.useState(false); - const [localClient, setLocalClient] = React.useState(undefined); const [settingChoices, setSettingChoices] = React.useState({}); - const updateSpecificSettingChoices = (field: string, value: any) => { + /** + * Method used to update a certain field inside a state object. + * @param field Name of the field to update + * @param value Value to set for the specified field + * @param stateObj Object to update + * @param setFunction Function used to update stateObj + */ + const updateSpecificFieldInStateObject = (field: string, value: any, stateObj, setFunction) => { const entry = {}; entry[field] = value; - setSettingChoices(update(settingChoices, entry)); + setFunction(update(stateObj, entry)); }; - const updateSpecificExtensionSetting = (field: string, value: any, local = false) => { - const entry = {}; - entry[field] = value; - local ? setLocalSettings(update(localSettings, entry)) : setSettingsState(update(settingState, entry)); - }; - const debouncedUpdateSpecificExtensionSetting = useCallback(debounce(updateSpecificExtensionSetting, 500), []); + const debouncedUpdateSpecificFieldInStateObject = useCallback(debounce(updateSpecificFieldInStateObject, 500), []); + /** + * Function used from each setting to understand if it needs to be disabled + * @param setting Name of the setting to check + * @returns False if not disabled, otherwise True + */ function checkIfDisabled(setting) { let tmp = defaultSettings[setting]; if (tmp.required || isAuthenticated) { @@ -38,31 +44,36 @@ export const ClientSettings = ({ modelProvider, settingState, setSettingsState } return !requiredSettings.every((e) => settingState[e]); } + // Effect used to authenticate the client when the apiKey changed useEffect(() => { - let clientObject = getModelProviderObject(modelProvider, settingState); + let clientObject = getModelClientObject(modelProvider, settingState); clientObject.authenticate(setIsAuthenticated); }, [settingState.apiKey]); + // Effect used to trigger the population of the settings when the user inserts a correct apiKey useEffect(() => { - let localClientTmp = getModelProviderObject(modelProvider, settingState); - if (isAuthenticated) { - setLocalClient(localClientTmp); - } + let localClientTmp = getModelClientObject(modelProvider, settingState); let tmpSettingsChoices = {}; Object.keys(defaultSettings).map((setting) => { tmpSettingsChoices[setting] = setChoices(setting, localClientTmp); }); }, [isAuthenticated]); - function setChoices(setting, localClientTmp) { + /** + * Function used to handle the definition of the choices param inside the settings form. + * If needed, it will get the choices from the client + * @param setting Name of the setting that we need to populate + * @param modelClient Client to call the AI model + */ + function setChoices(setting, modelClient) { let choices = defaultSettings[setting].values ? defaultSettings[setting].values : []; let { methodFromClient } = defaultSettings[setting]; if (methodFromClient && isAuthenticated) { - localClientTmp[methodFromClient]().then((value) => { - updateSpecificSettingChoices(setting, value); + modelClient[methodFromClient]().then((value) => { + updateSpecificFieldInStateObject(setting, value, settingChoices, setSettingChoices); }); } else { - updateSpecificSettingChoices(setting, choices); + updateSpecificFieldInStateObject(setting, choices, settingChoices, setSettingChoices); } } @@ -78,14 +89,18 @@ export const ClientSettings = ({ modelProvider, settingState, setSettingsState } value={localSettings[setting]} disabled={disabled} type={defaultSettings[setting].type} - label={`${defaultSettings[setting].label} - ${ - setting == 'apiKey' ? (isAuthenticated ? '( Authenticated )' : '( Not Authenticated )') : '' - }`} + label={ + // TODO: change this label for api to a button that verifies the auth, + // if verified, then show the other options.(should be verified for all REQUIRED options in defaultConfig) + `${defaultSettings[setting].label} - ${ + setting == 'apiKey' ? (isAuthenticated ? '( Authenticated )' : '( Not Authenticated )') : '' + }` + } defaultValue={defaultSettings[setting].default} choices={settingChoices[setting] ? settingChoices[setting] : []} onChange={(e) => { - updateSpecificExtensionSetting(setting, e, true); - debouncedUpdateSpecificExtensionSetting(setting, e); + updateSpecificFieldInStateObject(setting, e, localSettings, setLocalSettings); + debouncedUpdateSpecificFieldInStateObject(setting, e, settingState, setSettingsState); }} /> diff --git a/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx b/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx index 018e79057..5ffc36996 100644 --- a/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx +++ b/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx @@ -12,15 +12,15 @@ import ClientSettings from './ClientSettings'; export const QueryTranslatorSettingsModal = ({ open, setOpen, - apiKey, modelProvider, clientSettings, updateClientSettings, updateModelProvider, }) => { const [modelProviderState, setModelProviderState] = React.useState(modelProvider); - const [apiKeyState, setApiKeyState] = React.useState(apiKey); const [settingsState, setSettingsState] = React.useState(clientSettings); + + // TODO: a user shouldn't be able to save a configuration if it's not correct and it didn't fill all the requirements const handleClose = () => { updateModelProvider(modelProviderState); updateClientSettings(settingsState); @@ -66,7 +66,6 @@ export const QueryTranslatorSettingsModal = ({ ); }; const mapStateToProps = (state) => ({ - apiKey: getApiKey(state), clientSettings: getClientSettings(state), modelProvider: getModelProvider(state), }); diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index ed5411818..6dd685d0e 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -13,15 +13,15 @@ const consoleLogAsync = async (message: string, other?: any) => { * @returns */ export const queryTranslationThunk = - (pageIndex, cardIndex, message, reportType) => async (dispatch: any, getState: any) => { + (pageIndex, cardIndex, _message, _reportType) => async (dispatch: any, getState: any) => { const state = getState(); - const modelClient = getModelClient(state); + const _modelClient = getModelClient(state); // if (!modelClient){ // } - const messageHistory = getHistoryPerCard(state, pageIndex, cardIndex); + const _messageHistory = getHistoryPerCard(state, pageIndex, cardIndex); try { - modelClient.chatCompletion(message, reportType, messageHistory); + // modelClient.chatCompletion(message, reportType, messageHistory); } catch (e) { await consoleLogAsync( `Something wrong happened while calling the model client for the card number ${cardIndex} inside the page ${pageIndex}: \n`, From 5fa1d0ceff80b32c8d865fafea7127d705e7d21f Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Tue, 6 Jun 2023 00:40:13 +0200 Subject: [PATCH 03/49] defined thunk to connect each report with the model --- .../query-translator/QueryTranslator.tsx | 19 +- .../modelClients/ModelClient.ts | 10 + .../modelClients/OpenAi/OpenAiClient.ts | 155 ++++++++----- .../modelClients/OpenAi/OpenAiClientOLD.ts | 210 ------------------ .../query-translator/modelClients/const.ts | 2 + .../settings/ClientSettings.tsx | 13 +- .../state/QueryTranslatorActions.ts | 10 +- .../state/QueryTranslatorReducer.ts | 13 +- .../state/QueryTranslatorSelector.ts | 3 +- .../state/QueryTranslatorThunks.ts | 73 +++++- src/modal/SaveModal.tsx | 1 - 11 files changed, 217 insertions(+), 292 deletions(-) delete mode 100644 src/extensions/query-translator/modelClients/OpenAi/OpenAiClientOLD.ts diff --git a/src/extensions/query-translator/QueryTranslator.tsx b/src/extensions/query-translator/QueryTranslator.tsx index 2ec34d41e..029640a43 100644 --- a/src/extensions/query-translator/QueryTranslator.tsx +++ b/src/extensions/query-translator/QueryTranslator.tsx @@ -1,12 +1,13 @@ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; -import { deleteAllMessageHistory, setGlobalModelClient } from './state/QueryTranslatorActions'; +import { deleteAllMessageHistory, deleteMessageHistory, setGlobalModelClient } from './state/QueryTranslatorActions'; import { getApiKey, getClientSettings, getModelProvider } from './state/QueryTranslatorSelector'; import { Button } from '@neo4j-ndl/react'; import TranslateIcon from '@mui/icons-material/Translate'; import QueryTranslatorSettingsModal from './settings/QueryTranslatorSettingsModal'; import { getModelClientObject } from './QueryTranslatorConfig'; import { queryTranslationThunk } from './state/QueryTranslatorThunks'; +import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; /** * //TODO: * 1. The query translator should handle all the requests from the cards to the client @@ -21,13 +22,16 @@ export const QueryTranslator = ({ _deleteAllMessageHistory, setGlobalModelClient, queryTranslation, + _deleteMessageHistory, }) => { const [open, setOpen] = React.useState(false); + const { driver } = useContext(Neo4jContext); + // When changing provider, i will reset all the messages to prevent strage results useEffect(() => { + setGlobalModelClient(undefined); if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { - setGlobalModelClient(getModelClientObject(modelProvider, clientSettings)); - queryTranslation(0, 0, 'hello', 'graph'); + queryTranslation(0, 0, 'give me any query', 'Table', driver); } }, [modelProvider, apiKey, clientSettings]); @@ -62,8 +66,11 @@ const mapDispatchToProps = (dispatch) => ({ setGlobalModelClient: (modelClient) => { dispatch(setGlobalModelClient(modelClient)); }, - queryTranslation: (pageIndex, cardIndex, message, reportType) => { - dispatch(queryTranslationThunk(pageIndex, cardIndex, message, reportType)); + queryTranslation: (pagenumber, cardIndex, message, reportType, driver) => { + dispatch(queryTranslationThunk(pagenumber, cardIndex, message, reportType, driver)); + }, + deleteMessageHistory: (pagenumber, cardIndex) => { + dispatch(deleteMessageHistory(pagenumber, cardIndex)); }, }); diff --git a/src/extensions/query-translator/modelClients/ModelClient.ts b/src/extensions/query-translator/modelClients/ModelClient.ts index 9a517f8bd..36f13d6e7 100644 --- a/src/extensions/query-translator/modelClients/ModelClient.ts +++ b/src/extensions/query-translator/modelClients/ModelClient.ts @@ -1,5 +1,15 @@ +import { ChatCompletionRequestMessage } from 'openai'; +import { Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; + // A model client should just handle the communication export interface ModelClient { + setDriver(driver: any): unknown; + queryTranslation( + message: string, + messageHistory: ChatCompletionRequestMessage[], + database: string, + reportType: string + ): Promise; apiKey: string; setApiKey: any; modelType: string | undefined; diff --git a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts index 4408a6b89..024a33431 100644 --- a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts +++ b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts @@ -1,6 +1,5 @@ -import { TroubleshootOutlined } from '@mui/icons-material'; -import { Configuration, OpenAIApi } from 'openai'; -import { nodePropsQuery, relPropsQuery, relQuery, reportTypesToDesc } from '../const'; +import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai'; +import { nodePropsQuery, MAX_NUM_VALIDATION, relPropsQuery, relQuery, reportTypesToDesc } from '../const'; import { ModelClient } from '../ModelClient'; const consoleLogAsync = async (message: string, other?: any) => { @@ -14,9 +13,7 @@ export class OpenAiClient implements ModelClient { listAvailableModels: string[]; - createSystemMessage!: any; - - validateQuery!: any; + createSystemMessage: any; modelClient!: OpenAIApi; @@ -24,10 +21,27 @@ export class OpenAiClient implements ModelClient { constructor(settings) { this.apiKey = settings.apiKey; + this.modelType = settings.modelType; this.listAvailableModels = []; this.setModelClient(); } + async validateQuery(query, database) { + let isValid = false; + let errorMessage = ''; + try { + let res = await this.queryDatabase(`EXPLAIN ${query}`, database); + isValid = true; + } catch (e) { + isValid = false; + errorMessage = e.message; + } + return [isValid, errorMessage]; + } + + /** + * Function used to create the OpenAiApi object. + * */ setModelClient() { const configuration = new Configuration({ apiKey: this.apiKey, @@ -35,9 +49,20 @@ export class OpenAiClient implements ModelClient { this.modelClient = new OpenAIApi(configuration); } - async authenticate(setIsAuthenticated) { + /** + * + * @param setIsAuthenticated If defined, is a function used to set the authentication result (for example, set function of a state variable) + * @returns True if we client can authenticate, False otherwise + */ + async authenticate( + setIsAuthenticated = (boolean) => { + let x = boolean; + } + ) { try { let tmp = await this.getListModels(); + // Can be used in async mode without awaiting + // by passing down a function to set the authentication result setIsAuthenticated(tmp.length > 0); return tmp.length > 0; } catch (e) { @@ -46,6 +71,10 @@ export class OpenAiClient implements ModelClient { } } + /** + * Used also to check authentication + * @returns list of models available for this client + */ async getListModels() { let res; try { @@ -70,6 +99,10 @@ export class OpenAiClient implements ModelClient { this.modelClient = new OpenAIApi(configuration); } + setDriver(driver) { + this.driver = driver; + } + setListAvailableModels(listModels) { this.listAvailableModels = listModels; } @@ -107,16 +140,20 @@ export class OpenAiClient implements ModelClient { return elems; }) .catch(async (e) => { - await consoleLogAsync(`Error while running ${query}`, e); + throw e; }); return res; } async generateSchema(database) { - let nodeProps = await this.queryDatabase(nodePropsQuery, database); - let relProps = await this.queryDatabase(relPropsQuery, database); - let rels = await this.queryDatabase(relQuery, database); - return this.createSchemaText(nodeProps, relProps, rels); + try { + let nodeProps = await this.queryDatabase(nodePropsQuery, database); + let relProps = await this.queryDatabase(relPropsQuery, database); + let rels = await this.queryDatabase(relQuery, database); + return this.createSchemaText(nodeProps, relProps, rels); + } catch (e) { + throw Error(`Couldn't generate schema due to: ${e.message}`); + } } getSystemMessage(schemaText) { @@ -150,64 +187,74 @@ export class OpenAiClient implements ModelClient { } // TODO: adapt to the new structure, no more persisting inside the object, passign everything down - addUserMessage(content, reportType) { - let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${reportTypesToDesc[reportType]} Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. `; - return { role: 'user', content: finalMessage }; + addUserMessage(content, reportType, plain = false) { + let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${reportTypesToDesc[reportType]} Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result `; + return { role: ChatCompletionRequestMessageRoleEnum.User, content: plain ? content : finalMessage }; } addSystemMessage(content) { - return { role: 'assistant', content: content }; + return { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: content }; } - // updateMessageHistory(message) { - // this.messages.push(message); - // this.setMessages(this.messages); - // } + addErrorMessage(error) { + let finalMessage = `Please fix the query accordingly to this error: ${error}. Plain cypher code, no comments and no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result`; + return { role: ChatCompletionRequestMessageRoleEnum.User, content: finalMessage }; + } + + async chatCompletion(history) { + const completion = await this.modelClient.createChatCompletion({ + model: this.modelType, + messages: history, + }); + // If the status is correct + if (completion.status == 200 && completion.data && completion.data.choices && completion.data.choices[0].message) { + let { message } = completion.data.choices[0]; + return message; + } + throw Error(`Request returned with status: ${completion.status}`); + + } + + async queryTranslation(inputMessage, history, database, reportType) { + // Creating a copy of the history + let newHistory: ChatCompletionRequestMessage[] = [...history]; - async chatCompletion( - content, - messages, - database, - reportType, - setResponse = (res) => { - console.log(res); - } - ) { try { - if (messages.length == 0) { + if (history.length == 0) { let schema = await this.generateSchema(database); - this.addSystemMessage(this.getSystemMessage(schema)); + newHistory.push(this.addSystemMessage(this.getSystemMessage(schema))); } - if (this.apiKey) { - this.addUserMessage(content, reportType); - const completion = await this.modelClient.createChatCompletion({ - model: this.modelType, - messages: messages, - }); + let tmpHistory = [...newHistory]; + tmpHistory.push(this.addUserMessage(inputMessage, reportType)); - // If the status is correct - if ( - completion.status == 200 && - completion.data && - completion.data.choices && - completion.data.choices[0].message - ) { - let { message } = completion.data.choices[0]; - this.updateMessageHistory(message); - setResponse(message.content); + let retries = 0; + let isValidated = false; + let errorMessage = ''; + // Creating a tmp history to prevent updating the history with erroneous messages + // While is not validated and we didn't exceed the maximum retry number + while (!isValidated && retries < MAX_NUM_VALIDATION) { + retries += 1; + // Get the answer to the question + let newMessage = await this.chatCompletion(tmpHistory); + tmpHistory.push(newMessage); + // and try to validate it + let res = await this.validateQuery(newMessage.content, database); + isValidated = res[0]; + errorMessage = res[1]; + if (!isValidated) { + tmpHistory.push(this.addErrorMessage(errorMessage)); } else { - throw Error(`Request returned with status: ${completion.status}`); + newHistory.push(this.addUserMessage(inputMessage, reportType, true)); + newHistory.push(newMessage); } - } else { - throw Error('api key not present'); + } + if (!isValidated) { + throw Error(`The model couldn't translate your request: ${inputMessage}`); } } catch (error) { - setResponse(!this.apiKey ? 'key not present' : `${error}`); await consoleLogAsync('error during query', error); - } finally { - // TODO: trigger availability of the card (we should stop clicking on the card to prevent strange misconfigurations here) - await consoleLogAsync('done', this); } + return newHistory; } } diff --git a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClientOLD.ts b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClientOLD.ts deleted file mode 100644 index 78d8defe6..000000000 --- a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClientOLD.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from 'openai'; -import { REPORT_TYPES } from '../../../../config/ReportConfig'; -import { nodePropsQuery, relPropsQuery, relQuery, reportTypesToDesc } from '../const'; -import { ModelClient } from '../ModelClient'; - -const consoleLogAsync = async (message: string, other?: any) => { - await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); -}; - -export class OpenAiClient implements ModelClient { - messages: Array; - - apiKey: string; - - modelClient: OpenAIApi; - - driver; - - setMessages; - - database; - - nodeProps; - - relProps; - - rels; - - schemaText; - - reportType: string; - - constructor(apiKey, messages = [], setMessages, driver, database, reportType) { - this.messages = messages; - this.setMessages = setMessages; - this.apiKey = apiKey; - if (apiKey) { - this.setApiKey(apiKey); - } - this.driver = driver; - this.database = database; - this.generateSchema(); - this.updateReportType(reportType); - } - - updateReportType(reportType) { - this.reportType = REPORT_TYPES[reportType].label; - } - - resetClient() { - this.messages = []; - this.setMessages([]); - this.generateSchema(); - } - - setNodeProps(props) { - this.nodeProps = props; - } - - setRelProps(props) { - this.relProps = props; - } - - setRels(props) { - this.rels = props; - } - - setSchemaText() { - this.schemaText = ` - This is the schema representation of the Neo4j database. - Node properties are the following: - ${JSON.stringify(this.nodeProps)} - Relationship properties are the following: - ${JSON.stringify(this.relProps)} - Relationship point from source to target nodes - ${JSON.stringify(this.rels)} - Make sure to respect relationship types and directions - `; - } - - async queryDatabase(query) { - const session = this.driver.session({ database: this.database }); - const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); - - let res = await transaction - .run(query, undefined) - .then((res) => { - const { records } = res; - let elems = records.map((elem) => { - return elem.toObject()[elem.keys[0]]; - }); - records.length > 0 ?? elems.unshift(records[0].keys); - transaction.commit(); - return elems; - }) - .catch(async (e) => { - await consoleLogAsync(`Error while running ${query}`, e); - }); - return res; - } - - async generateSchema() { - this.setNodeProps(await this.queryDatabase(nodePropsQuery)); - this.setRelProps(await this.queryDatabase(relPropsQuery)); - this.setRels(await this.queryDatabase(relQuery)); - this.setSchemaText(); - } - - getSystemMessage() { - return ` - Task: Generate Cypher queries to query a Neo4j graph database based on the provided schema definition. These queries will be used inside NeoDash reports. - Documentation for NeoDash is here : https://neo4j.com/labs/neodash/2.2/ - Instructions: - Use only the provided relationship types and properties. - Do not use any other relationship types or properties that are not provided. - The Cypher RETURN clause must contained certain variables, based on the report type asked for. - Report types : - Table - Multiple variables representing property values of nodes and relationships. - Graph - Multiple variables representing nodes objects and relationships objects inside the graph. - Bar Chart - Two variables named category(a String value) and value(numeric value). - Line Chart - Two numeric variables named x and y. - Sunburst - Two variables named Path(list of strings) and value(a numerical value). - Circle Packing - Two variables named Path(a list of strings) and value(a numerical value). - Choropleth - Two variables named code(a String value) and value(a numerical value). - Area Map - Two variables named code(a String value) and value(a numerical value). - Treemap - Two variables named Path(a list of strings) and value(a numerical value). - Radar Chart - Multiple variables representing property values of nodes and relationships. - Sankey Chart - Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value). - Map - multiple variables representing nodes objects(should contain spatial propeties) and relationship objects. - Single Value - A single value of a single variable. - Gauge Chart - A single value of a single variable. - Raw JSON - The Cypher query must return a JSON object that will be displayed as raw JSON data. - Pie Chart - Two variables named category and value. - Schema: - ${this.schemaText} - `; - } - - setApiKey(apiKey) { - this.apiKey = apiKey; - const configuration = new Configuration({ - apiKey: apiKey, - }); - this.modelClient = new OpenAIApi(configuration); - } - - addUserMessage(content) { - let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${ - reportTypesToDesc[this.reportType] - } Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. `; - this.messages.push({ role: 'user', content: finalMessage }); - } - - addSystemMessage(content) { - this.messages.push({ role: 'assistant', content: content }); - } - - updateMessageHistory(message) { - this.messages.push(message); - this.setMessages(this.messages); - } - - async chatCompletion( - content, - setResponse = (res) => { - console.log(res); - } - ) { - try { - if (this.messages.length == 0) { - if (this.schemaText) { - this.addSystemMessage(this.getSystemMessage()); - } else { - await this.generateSchema(); - this.addSystemMessage(this.getSystemMessage()); - } - } - if (this.apiKey) { - this.addUserMessage(content); - - const completion = await this.modelClient.createChatCompletion({ - model: 'gpt-3.5-turbo', - messages: this.messages, - }); - - // If the status is correct - if ( - completion.status == 200 && - completion.data && - completion.data.choices && - completion.data.choices[0].message - ) { - let { message } = completion.data.choices[0]; - this.updateMessageHistory(message); - setResponse(message.content); - } else { - throw Error(`Request returned with status: ${completion.status}`); - } - } else { - throw Error('api key not present'); - } - } catch (error) { - setResponse(!this.apiKey ? 'key not present' : `${error}`); - await consoleLogAsync('error during query', error); - } finally { - // TODO: trigger availability of the card (we should stop clicking on the card to prevent strange misconfigurations here) - await consoleLogAsync('done', this); - } - } -} diff --git a/src/extensions/query-translator/modelClients/const.ts b/src/extensions/query-translator/modelClients/const.ts index e8fb96c65..f95bc3d98 100644 --- a/src/extensions/query-translator/modelClients/const.ts +++ b/src/extensions/query-translator/modelClients/const.ts @@ -40,3 +40,5 @@ export const reportTypesToDesc = { 'Raw JSON': 'The Cypher query must return a JSON object that will be displayed as raw JSON data.', 'Pie Chart': 'Two variables named category and value.', }; + +export const MAX_NUM_VALIDATION = 5; diff --git a/src/extensions/query-translator/settings/ClientSettings.tsx b/src/extensions/query-translator/settings/ClientSettings.tsx index 60eca2b80..b56c50537 100644 --- a/src/extensions/query-translator/settings/ClientSettings.tsx +++ b/src/extensions/query-translator/settings/ClientSettings.tsx @@ -4,6 +4,7 @@ import { debounce, List, ListItem } from '@mui/material'; import { getModelClientObject, getQueryTranslatorDefaultConfig } from '../QueryTranslatorConfig'; import { getClientSettings } from '../state/QueryTranslatorSelector'; import NeoSetting from '../../../component/field/Setting'; +import { setGlobalModelClient } from '../state/QueryTranslatorActions'; const update = (state, mutations) => Object.assign({}, state, mutations); @@ -45,6 +46,7 @@ export const ClientSettings = ({ modelProvider, settingState, setSettingsState } } // Effect used to authenticate the client when the apiKey changed + // TODO: change to modelClientInitializationThunk when having a button to set the state globally and not only local as right now useEffect(() => { let clientObject = getModelClientObject(modelProvider, settingState); clientObject.authenticate(setIsAuthenticated); @@ -53,6 +55,11 @@ export const ClientSettings = ({ modelProvider, settingState, setSettingsState } // Effect used to trigger the population of the settings when the user inserts a correct apiKey useEffect(() => { let localClientTmp = getModelClientObject(modelProvider, settingState); + if (isAuthenticated) { + setGlobalModelClient(localClientTmp); + } else { + setGlobalModelClient(undefined); + } let tmpSettingsChoices = {}; Object.keys(defaultSettings).map((setting) => { tmpSettingsChoices[setting] = setChoices(setting, localClientTmp); @@ -115,6 +122,10 @@ const mapStateToProps = (state) => ({ settings: getClientSettings(state), }); -const mapDispatchToProps = (_dispatch) => ({}); +const mapDispatchToProps = (dispatch) => ({ + setGlobalModelClient: (modelClient) => { + dispatch(setGlobalModelClient(modelClient)); + }, +}); export default connect(mapStateToProps, mapDispatchToProps)(ClientSettings); diff --git a/src/extensions/query-translator/state/QueryTranslatorActions.ts b/src/extensions/query-translator/state/QueryTranslatorActions.ts index 604fe0ca2..20c6fb7d8 100644 --- a/src/extensions/query-translator/state/QueryTranslatorActions.ts +++ b/src/extensions/query-translator/state/QueryTranslatorActions.ts @@ -23,14 +23,18 @@ export const setGlobalModelClient = (modelClient) => ({ export const UPDATE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_EXTENSION_TITLE`; /** * Action to add a new message to the history - * @param message Object defined as {user: string, message: string} + * @param history History of messages between a card and the model * @param pageIndex Index of the page related to the card * @param cardIndex Index of the card inside the page * @returns */ -export const updateMessageHistory = (message: ChatCompletionRequestMessage, pageIndex: number, cardIndex: number) => ({ +export const updateMessageHistory = ( + cardHistory: ChatCompletionRequestMessage[], + pageIndex: number, + cardIndex: number +) => ({ type: UPDATE_MESSAGE_HISTORY, - payload: { message, pageIndex, cardIndex }, + payload: { cardHistory, pageIndex, cardIndex }, }); export const DELETE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}DELETE_MESSAGE_HISTORY`; diff --git a/src/extensions/query-translator/state/QueryTranslatorReducer.ts b/src/extensions/query-translator/state/QueryTranslatorReducer.ts index ca29d2885..ef3256d53 100644 --- a/src/extensions/query-translator/state/QueryTranslatorReducer.ts +++ b/src/extensions/query-translator/state/QueryTranslatorReducer.ts @@ -41,12 +41,15 @@ export const queryTranslatorReducer = (state = INITIAL_EXTENSION_STATE, action: return state; } case UPDATE_MESSAGE_HISTORY: { - const { message, pageIndex, cardIndex } = payload; + const { cardHistory, pageIndex, cardIndex } = payload; let newHistory = { ...state.history }; - const history = newHistory[pageIndex] && newHistory[pageIndex][cardIndex] ? newHistory[pageIndex][cardIndex] : []; - // For now we always append a message to the history - history.push(message); - newHistory[pageIndex][cardIndex] = history; + + if (newHistory && !newHistory[pageIndex]) { + newHistory[pageIndex] = {}; + newHistory[pageIndex][cardIndex] = cardHistory; + } else { + newHistory[pageIndex][cardIndex] = cardHistory; + } state = update(state, { history: newHistory }); return state; } diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts index 607402ddc..b536c9c7a 100644 --- a/src/extensions/query-translator/state/QueryTranslatorSelector.ts +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -10,6 +10,7 @@ export const queryTranslatorExtensionName = 'query-translator'; const checkExtensionConfig = (state: any) => { return state.dashboard.extensionsConfig && state.dashboard.extensionsConfig[queryTranslatorExtensionName]; }; + export const getHistory = (state: any) => { let history = checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].history; return history != undefined && history ? history : {}; @@ -17,7 +18,7 @@ export const getHistory = (state: any) => { export const getModelClient = (state: any) => { let modelClient = - checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].getModelClient; + checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].modelClient; return modelClient != undefined && modelClient ? modelClient : undefined; }; diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index 6dd685d0e..8ed569d5e 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -1,30 +1,81 @@ -import { getHistoryPerCard, getModelClient } from './QueryTranslatorSelector'; +import { getDatabase } from '../../../settings/SettingsSelectors'; +import { ModelClient } from '../modelClients/ModelClient'; +import { getModelClientObject } from '../QueryTranslatorConfig'; +import { setGlobalModelClient, updateMessageHistory } from './QueryTranslatorActions'; +import { getClientSettings, getHistoryPerCard, getModelClient, getModelProvider } from './QueryTranslatorSelector'; const consoleLogAsync = async (message: string, other?: any) => { await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); }; +/** + * Thunk used to initialize the client model. + * @returns True if the client is created, otherwise False + */ +const modelClientInitializationThunk = + ( + setIsAuthenticated = (boolean) => { + let x = boolean; + } + ) => + async (dispatch: any, getState: any) => { + const state = getState(); + let modelProvider = getModelProvider(state); + let settings = getClientSettings(state); + if (modelProvider && settings) { + let tmpClient = getModelClientObject(modelProvider, settings); + let isAuthenticated = await tmpClient.authenticate(setIsAuthenticated); + if (isAuthenticated) { + dispatch(setGlobalModelClient(tmpClient)); + return tmpClient; + } + } + return undefined; + }; + +/** + * Wrapper to recreate the model client if it doesn't exists + * @returns An instance of the model client + */ +const getModelClientThunk = () => async (dispatch: any, getState: any) => { + const state = getState(); + let modelClient = getModelClient(state); + + if (!modelClient) { + let newClient = await dispatch(modelClientInitializationThunk()); + if (newClient) { + return newClient; + } + } + return modelClient; +}; + /** * Method to handle the request to a model client for a query translation. - * @param pageIndex Index of the page where the card lives + * @param pagenumber Index of the page where the card lives * @param cardIndex Index that identifies a card inside its page * @param message Message inserted by the user * @param setQuery Function to set the query inside the card (i don't think i need this one) * @returns */ export const queryTranslationThunk = - (pageIndex, cardIndex, _message, _reportType) => async (dispatch: any, getState: any) => { - const state = getState(); - const _modelClient = getModelClient(state); - // if (!modelClient){ - - // } - const _messageHistory = getHistoryPerCard(state, pageIndex, cardIndex); + (pagenumber, cardIndex, message, reportType, driver) => async (dispatch: any, getState: any) => { try { - // modelClient.chatCompletion(message, reportType, messageHistory); + const state = getState(); + const database = getDatabase(state, pagenumber, cardIndex); + + let client: ModelClient = await dispatch(getModelClientThunk()); + client.setDriver(driver); + + const messageHistory = getHistoryPerCard(state, pagenumber, cardIndex); + let newMessages = await client.queryTranslation(message, messageHistory, database, reportType); + consoleLogAsync('newHistory', newMessages); + if (message.length !== newMessages.length) { + dispatch(updateMessageHistory(newMessages, pagenumber, cardIndex)); + } } catch (e) { await consoleLogAsync( - `Something wrong happened while calling the model client for the card number ${cardIndex} inside the page ${pageIndex}: \n`, + `Something wrong happened while calling the model client for the card number ${cardIndex} inside the page ${pagenumber}: \n`, { e } ); } diff --git a/src/modal/SaveModal.tsx b/src/modal/SaveModal.tsx index df68ec915..ad8b2eca2 100644 --- a/src/modal/SaveModal.tsx +++ b/src/modal/SaveModal.tsx @@ -84,7 +84,6 @@ export const NeoSaveModal = ({ 'advancedSettingsOpen', 'collapseTimeout', 'modelClient', - 'history', ]); const dashboardString = JSON.stringify(filteredDashboard, null, 2); From b1b103e7afab511f8d32daa35df4b26b62f74aa2 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Tue, 6 Jun 2023 13:47:05 +0200 Subject: [PATCH 04/49] working on abstract class --- .../query-translator/QueryTranslator.tsx | 3 +- .../query-translator/QueryTranslatorConfig.ts | 4 +- .../modelClients/ModelClient.ts | 168 ++++++++++- .../modelClients/OpenAi/OpenAiClient.ts | 260 ------------------ .../modelClients/OpenAiClient.ts | 139 ++++++++++ .../{VertexAi => }/VertexAiClient.ts | 6 +- .../query-translator/modelClients/const.ts | 24 ++ .../state/QueryTranslatorThunks.ts | 1 - 8 files changed, 323 insertions(+), 282 deletions(-) delete mode 100644 src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts create mode 100644 src/extensions/query-translator/modelClients/OpenAiClient.ts rename src/extensions/query-translator/modelClients/{VertexAi => }/VertexAiClient.ts (98%) diff --git a/src/extensions/query-translator/QueryTranslator.tsx b/src/extensions/query-translator/QueryTranslator.tsx index 029640a43..eaaa2c16b 100644 --- a/src/extensions/query-translator/QueryTranslator.tsx +++ b/src/extensions/query-translator/QueryTranslator.tsx @@ -19,7 +19,7 @@ export const QueryTranslator = ({ apiKey, modelProvider, clientSettings, - _deleteAllMessageHistory, + deleteAllMessageHistory, setGlobalModelClient, queryTranslation, _deleteMessageHistory, @@ -30,6 +30,7 @@ export const QueryTranslator = ({ // When changing provider, i will reset all the messages to prevent strage results useEffect(() => { setGlobalModelClient(undefined); + deleteAllMessageHistory(); if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { queryTranslation(0, 0, 'give me any query', 'Table', driver); } diff --git a/src/extensions/query-translator/QueryTranslatorConfig.ts b/src/extensions/query-translator/QueryTranslatorConfig.ts index 9dc6ea3a0..b4c34410b 100644 --- a/src/extensions/query-translator/QueryTranslatorConfig.ts +++ b/src/extensions/query-translator/QueryTranslatorConfig.ts @@ -1,7 +1,7 @@ import { SELECTION_TYPES } from '../../config/CardConfig'; import { ModelClient } from './modelClients/ModelClient'; -import { OpenAiClient } from './modelClients/OpenAi/OpenAiClient'; -import { VertexAiClient } from './modelClients/VertexAi/VertexAiClient'; +import { OpenAiClient } from './modelClients/OpenAiClient'; +import { VertexAiClient } from './modelClients/VertexAiClient'; interface ClientSettings { apiKey: any; diff --git a/src/extensions/query-translator/modelClients/ModelClient.ts b/src/extensions/query-translator/modelClients/ModelClient.ts index 36f13d6e7..0b20ae666 100644 --- a/src/extensions/query-translator/modelClients/ModelClient.ts +++ b/src/extensions/query-translator/modelClients/ModelClient.ts @@ -1,24 +1,162 @@ -import { ChatCompletionRequestMessage } from 'openai'; -import { Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; +import { MAX_NUM_VALIDATION, nodePropsQuery, relPropsQuery, relQuery, TASK_DEFINITION } from './const'; + +const notImplementedError = (functionName) => { + throw new Error(`Not Implemented: ${functionName}`); +}; +const consoleLogAsync = async (message: string, other?: any) => { + await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); +}; // A model client should just handle the communication -export interface ModelClient { - setDriver(driver: any): unknown; - queryTranslation( - message: string, - messageHistory: ChatCompletionRequestMessage[], - database: string, - reportType: string - ): Promise; +export abstract class ModelClient { apiKey: string; - setApiKey: any; + modelType: string | undefined; + listAvailableModels: string[]; - chatCompletion: any; + createSystemMessage: any; - addUserMessage: any; - validateQuery: any; - driver; + + modelClient: any; + + driver: any; + + constructor(settings) { + this.apiKey = settings.apiKey; + this.modelType = settings.modelType; + this.listAvailableModels = []; + this.setModelClient(); + } + + setModelClient() { + notImplementedError('setModelClient'); + } + + async queryDatabase(query, database) { + const session = this.driver.session({ database: database }); + const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); + + let res = await transaction + .run(query, undefined) + .then((res) => { + const { records } = res; + let elems = records.map((elem) => { + return elem.toObject()[elem.keys[0]]; + }); + records.length > 0 ?? elems.unshift(records[0].keys); + transaction.commit(); + return elems; + }) + .catch(async (e) => { + throw e; + }); + return res; + } + + createSchemaText(nodeProps, relProps, rels) { + return ` + This is the schema representation of the Neo4j database. + Node properties are the following: + ${JSON.stringify(nodeProps)} + Relationship properties are the following: + ${JSON.stringify(relProps)} + Relationship point from source to target nodes + ${JSON.stringify(rels)} + Make sure to respect relationship types and directions + `; + } + + async generateSchema(database) { + try { + let nodeProps = await this.queryDatabase(nodePropsQuery, database); + let relProps = await this.queryDatabase(relPropsQuery, database); + let rels = await this.queryDatabase(relQuery, database); + return this.createSchemaText(nodeProps, relProps, rels); + } catch (e) { + throw Error(`Couldn't generate schema due to: ${e.message}`); + } + } + + getSystemMessage(schemaText) { + return `${TASK_DEFINITION} + Schema: + ${schemaText} + `; + } + + async validateQuery(_message, _database) { + notImplementedError('validateQuery'); + } + + setDriver(driver: any) { + this.driver = driver; + } + + async queryTranslation(inputMessage, history, database, reportType) { + // Creating a copy of the history + let newHistory = [...history]; + + try { + if (history.length == 0) { + let schema = await this.generateSchema(database); + newHistory.push(this.addSystemMessage(this.getSystemMessage(schema))); + } + + let tmpHistory = [...newHistory]; + tmpHistory.push(this.addUserMessage(inputMessage, reportType)); + + let retries = 0; + let isValidated = false; + let errorMessage = ''; + // Creating a tmp history to prevent updating the history with erroneous messages + // While is not validated and we didn't exceed the maximum retry number + while (!isValidated && retries < MAX_NUM_VALIDATION) { + retries += 1; + // Get the answer to the question + let newMessage = await this.chatCompletion(tmpHistory); + tmpHistory.push(newMessage); + await consoleLogAsync(`tmpHistory step: ${retries}`, tmpHistory); + + // and try to validate it + let res = await this.validateQuery(newMessage, database); + isValidated = res[0]; + errorMessage = res[1]; + if (!isValidated) { + tmpHistory.push(this.addErrorMessage(errorMessage)); + } else { + newHistory.push(this.addUserMessage(inputMessage, reportType, true)); + newHistory.push(newMessage); + } + } + if (!isValidated) { + throw Error(`The model couldn't translate your request: ${inputMessage}`); + } + } catch (error) { + await consoleLogAsync('error during query', error); + } + return newHistory; + } + + async chatCompletion(_history) { + notImplementedError('chatCompletion'); + } + + // TODO: adapt to the new structure, no more persisting inside the object, passign everything down + addUserMessage(_content, _reportType, _plain = false) { + notImplementedError('addUserMessage'); + } + + addSystemMessage(_content) { + notImplementedError('addSystemMessage'); + } + + addAssistantMessage(_content) { + notImplementedError('addAssistantMessage'); + } + + addErrorMessage(_error) { + notImplementedError('addErrorMessage'); + } } // to see if i need this diff --git a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts b/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts deleted file mode 100644 index 024a33431..000000000 --- a/src/extensions/query-translator/modelClients/OpenAi/OpenAiClient.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai'; -import { nodePropsQuery, MAX_NUM_VALIDATION, relPropsQuery, relQuery, reportTypesToDesc } from '../const'; -import { ModelClient } from '../ModelClient'; - -const consoleLogAsync = async (message: string, other?: any) => { - await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); -}; - -export class OpenAiClient implements ModelClient { - apiKey: string; - - modelType: string | undefined; - - listAvailableModels: string[]; - - createSystemMessage: any; - - modelClient!: OpenAIApi; - - driver: any; - - constructor(settings) { - this.apiKey = settings.apiKey; - this.modelType = settings.modelType; - this.listAvailableModels = []; - this.setModelClient(); - } - - async validateQuery(query, database) { - let isValid = false; - let errorMessage = ''; - try { - let res = await this.queryDatabase(`EXPLAIN ${query}`, database); - isValid = true; - } catch (e) { - isValid = false; - errorMessage = e.message; - } - return [isValid, errorMessage]; - } - - /** - * Function used to create the OpenAiApi object. - * */ - setModelClient() { - const configuration = new Configuration({ - apiKey: this.apiKey, - }); - this.modelClient = new OpenAIApi(configuration); - } - - /** - * - * @param setIsAuthenticated If defined, is a function used to set the authentication result (for example, set function of a state variable) - * @returns True if we client can authenticate, False otherwise - */ - async authenticate( - setIsAuthenticated = (boolean) => { - let x = boolean; - } - ) { - try { - let tmp = await this.getListModels(); - // Can be used in async mode without awaiting - // by passing down a function to set the authentication result - setIsAuthenticated(tmp.length > 0); - return tmp.length > 0; - } catch (e) { - consoleLogAsync('Authentication went wrong: ', e); - return false; - } - } - - /** - * Used also to check authentication - * @returns list of models available for this client - */ - async getListModels() { - let res; - try { - if (!this.modelClient) { - throw new Error('no client defined'); - } - let req = await this.modelClient.listModels(); - // Extracting the names - res = req.data.data.map((x) => x.id).filter((x) => x.includes('gpt-3.5')); - } catch (e) { - consoleLogAsync('Error while loading the model list: ', e); - res = []; - } - return res; - } - - setApiKey(apiKey) { - this.apiKey = apiKey; - const configuration = new Configuration({ - apiKey: apiKey, - }); - this.modelClient = new OpenAIApi(configuration); - } - - setDriver(driver) { - this.driver = driver; - } - - setListAvailableModels(listModels) { - this.listAvailableModels = listModels; - } - - setModelType(modelType) { - this.modelType = modelType; - } - - createSchemaText(nodeProps, relProps, rels) { - return ` - This is the schema representation of the Neo4j database. - Node properties are the following: - ${JSON.stringify(nodeProps)} - Relationship properties are the following: - ${JSON.stringify(relProps)} - Relationship point from source to target nodes - ${JSON.stringify(rels)} - Make sure to respect relationship types and directions - `; - } - - async queryDatabase(query, database) { - const session = this.driver.session({ database: database }); - const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); - - let res = await transaction - .run(query, undefined) - .then((res) => { - const { records } = res; - let elems = records.map((elem) => { - return elem.toObject()[elem.keys[0]]; - }); - records.length > 0 ?? elems.unshift(records[0].keys); - transaction.commit(); - return elems; - }) - .catch(async (e) => { - throw e; - }); - return res; - } - - async generateSchema(database) { - try { - let nodeProps = await this.queryDatabase(nodePropsQuery, database); - let relProps = await this.queryDatabase(relPropsQuery, database); - let rels = await this.queryDatabase(relQuery, database); - return this.createSchemaText(nodeProps, relProps, rels); - } catch (e) { - throw Error(`Couldn't generate schema due to: ${e.message}`); - } - } - - getSystemMessage(schemaText) { - return ` - Task: Generate Cypher queries to query a Neo4j graph database based on the provided schema definition. These queries will be used inside NeoDash reports. - Documentation for NeoDash is here : https://neo4j.com/labs/neodash/2.2/ - Instructions: - Use only the provided relationship types and properties. - Do not use any other relationship types or properties that are not provided. - The Cypher RETURN clause must contained certain variables, based on the report type asked for. - Report types : - Table - Multiple variables representing property values of nodes and relationships. - Graph - Multiple variables representing nodes objects and relationships objects inside the graph. - Bar Chart - Two variables named category(a String value) and value(numeric value). - Line Chart - Two numeric variables named x and y. - Sunburst - Two variables named Path(list of strings) and value(a numerical value). - Circle Packing - Two variables named Path(a list of strings) and value(a numerical value). - Choropleth - Two variables named code(a String value) and value(a numerical value). - Area Map - Two variables named code(a String value) and value(a numerical value). - Treemap - Two variables named Path(a list of strings) and value(a numerical value). - Radar Chart - Multiple variables representing property values of nodes and relationships. - Sankey Chart - Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value). - Map - multiple variables representing nodes objects(should contain spatial propeties) and relationship objects. - Single Value - A single value of a single variable. - Gauge Chart - A single value of a single variable. - Raw JSON - The Cypher query must return a JSON object that will be displayed as raw JSON data. - Pie Chart - Two variables named category and value. - Schema: - ${schemaText} - `; - } - - // TODO: adapt to the new structure, no more persisting inside the object, passign everything down - addUserMessage(content, reportType, plain = false) { - let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${reportTypesToDesc[reportType]} Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result `; - return { role: ChatCompletionRequestMessageRoleEnum.User, content: plain ? content : finalMessage }; - } - - addSystemMessage(content) { - return { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: content }; - } - - addErrorMessage(error) { - let finalMessage = `Please fix the query accordingly to this error: ${error}. Plain cypher code, no comments and no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result`; - return { role: ChatCompletionRequestMessageRoleEnum.User, content: finalMessage }; - } - - async chatCompletion(history) { - const completion = await this.modelClient.createChatCompletion({ - model: this.modelType, - messages: history, - }); - // If the status is correct - if (completion.status == 200 && completion.data && completion.data.choices && completion.data.choices[0].message) { - let { message } = completion.data.choices[0]; - return message; - } - throw Error(`Request returned with status: ${completion.status}`); - - } - - async queryTranslation(inputMessage, history, database, reportType) { - // Creating a copy of the history - let newHistory: ChatCompletionRequestMessage[] = [...history]; - - try { - if (history.length == 0) { - let schema = await this.generateSchema(database); - newHistory.push(this.addSystemMessage(this.getSystemMessage(schema))); - } - - let tmpHistory = [...newHistory]; - tmpHistory.push(this.addUserMessage(inputMessage, reportType)); - - let retries = 0; - let isValidated = false; - let errorMessage = ''; - // Creating a tmp history to prevent updating the history with erroneous messages - // While is not validated and we didn't exceed the maximum retry number - while (!isValidated && retries < MAX_NUM_VALIDATION) { - retries += 1; - // Get the answer to the question - let newMessage = await this.chatCompletion(tmpHistory); - tmpHistory.push(newMessage); - // and try to validate it - let res = await this.validateQuery(newMessage.content, database); - isValidated = res[0]; - errorMessage = res[1]; - if (!isValidated) { - tmpHistory.push(this.addErrorMessage(errorMessage)); - } else { - newHistory.push(this.addUserMessage(inputMessage, reportType, true)); - newHistory.push(newMessage); - } - } - if (!isValidated) { - throw Error(`The model couldn't translate your request: ${inputMessage}`); - } - } catch (error) { - await consoleLogAsync('error during query', error); - } - return newHistory; - } -} diff --git a/src/extensions/query-translator/modelClients/OpenAiClient.ts b/src/extensions/query-translator/modelClients/OpenAiClient.ts new file mode 100644 index 000000000..19805ac49 --- /dev/null +++ b/src/extensions/query-translator/modelClients/OpenAiClient.ts @@ -0,0 +1,139 @@ +import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai'; +import { nodePropsQuery, MAX_NUM_VALIDATION, relPropsQuery, relQuery, reportTypesToDesc } from './const'; +import { ModelClient } from './ModelClient'; + +const consoleLogAsync = async (message: string, other?: any) => { + await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); +}; + +export class OpenAiClient extends ModelClient { + modelType: string | undefined; + + createSystemMessage: any; + + modelClient!: OpenAIApi; + + driver: any; + + constructor(settings) { + super(settings); + } + + async validateQuery(message, database) { + let query = message.content; + let isValid = false; + let errorMessage = ''; + try { + let res = await this.queryDatabase(`EXPLAIN ${query}`, database); + isValid = true; + } catch (e) { + isValid = false; + errorMessage = e.message; + } + return [isValid, errorMessage]; + } + + /** + * Function used to create the OpenAiApi object. + * */ + setModelClient() { + const configuration = new Configuration({ + apiKey: this.apiKey, + }); + this.modelClient = new OpenAIApi(configuration); + } + + /** + * + * @param setIsAuthenticated If defined, is a function used to set the authentication result (for example, set function of a state variable) + * @returns True if we client can authenticate, False otherwise + */ + async authenticate( + setIsAuthenticated = (boolean) => { + let x = boolean; + } + ) { + try { + let tmp = await this.getListModels(); + // Can be used in async mode without awaiting + // by passing down a function to set the authentication result + setIsAuthenticated(tmp.length > 0); + return tmp.length > 0; + } catch (e) { + consoleLogAsync('Authentication went wrong: ', e); + return false; + } + } + + /** + * Used also to check authentication + * @returns list of models available for this client + */ + async getListModels() { + let res; + try { + if (!this.modelClient) { + throw new Error('no client defined'); + } + let req = await this.modelClient.listModels(); + // Extracting the names + res = req.data.data.map((x) => x.id).filter((x) => x.includes('gpt-3.5')); + } catch (e) { + consoleLogAsync('Error while loading the model list: ', e); + res = []; + } + return res; + } + + setApiKey(apiKey) { + this.apiKey = apiKey; + const configuration = new Configuration({ + apiKey: apiKey, + }); + this.modelClient = new OpenAIApi(configuration); + } + + setDriver(driver) { + this.driver = driver; + } + + setListAvailableModels(listModels) { + this.listAvailableModels = listModels; + } + + setModelType(modelType) { + this.modelType = modelType; + } + + // TODO: adapt to the new structure, no more persisting inside the object, passign everything down + addUserMessage(content, reportType, plain = false) { + let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${reportTypesToDesc[reportType]} Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result `; + return { role: ChatCompletionRequestMessageRoleEnum.User, content: plain ? content : finalMessage }; + } + + addSystemMessage(content) { + return { role: ChatCompletionRequestMessageRoleEnum.System, content: content }; + } + + addAssistantMessage(content) { + return { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: content }; + } + + addErrorMessage(error) { + let finalMessage = `Please fix the query accordingly to this error: ${error}. Plain cypher code, no comments and no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result`; + return { role: ChatCompletionRequestMessageRoleEnum.User, content: finalMessage }; + } + + async chatCompletion(history) { + const completion = await this.modelClient.createChatCompletion({ + model: this.modelType, + messages: history, + }); + // If the status is correct + if (completion.status == 200 && completion.data && completion.data.choices && completion.data.choices[0].message) { + let { message } = completion.data.choices[0]; + return message; + } + throw Error(`Request returned with status: ${completion.status}`); + } +} diff --git a/src/extensions/query-translator/modelClients/VertexAi/VertexAiClient.ts b/src/extensions/query-translator/modelClients/VertexAiClient.ts similarity index 98% rename from src/extensions/query-translator/modelClients/VertexAi/VertexAiClient.ts rename to src/extensions/query-translator/modelClients/VertexAiClient.ts index 425d6ec60..4f1a37f23 100644 --- a/src/extensions/query-translator/modelClients/VertexAi/VertexAiClient.ts +++ b/src/extensions/query-translator/modelClients/VertexAiClient.ts @@ -1,7 +1,7 @@ import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from 'openai'; -import { REPORT_TYPES } from '../../../../config/ReportConfig'; -import { nodePropsQuery, relPropsQuery, relQuery, reportTypesToDesc } from '../const'; -import { ModelClient } from '../ModelClient'; +import { REPORT_TYPES } from '../../../config/ReportConfig'; +import { nodePropsQuery, relPropsQuery, relQuery, reportTypesToDesc } from './const'; +import { ModelClient } from './ModelClient'; const consoleLogAsync = async (message: string, other?: any) => { await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); diff --git a/src/extensions/query-translator/modelClients/const.ts b/src/extensions/query-translator/modelClients/const.ts index f95bc3d98..e7f5f5f47 100644 --- a/src/extensions/query-translator/modelClients/const.ts +++ b/src/extensions/query-translator/modelClients/const.ts @@ -41,4 +41,28 @@ export const reportTypesToDesc = { 'Pie Chart': 'Two variables named category and value.', }; +export const TASK_DEFINITION = `Task: Generate Cypher queries to query a Neo4j graph database based on the provided schema definition. These queries will be used inside NeoDash reports. +Documentation for NeoDash is here : https://neo4j.com/labs/neodash/2.2/ +Instructions: +Use only the provided relationship types and properties. +Do not use any other relationship types or properties that are not provided. +The Cypher RETURN clause must contained certain variables, based on the report type asked for. +Report types : +Table - Multiple variables representing property values of nodes and relationships. +Graph - Multiple variables representing nodes objects and relationships objects inside the graph. +Bar Chart - Two variables named category(a String value) and value(numeric value). +Line Chart - Two numeric variables named x and y. +Sunburst - Two variables named Path(list of strings) and value(a numerical value). +Circle Packing - Two variables named Path(a list of strings) and value(a numerical value). +Choropleth - Two variables named code(a String value) and value(a numerical value). +Area Map - Two variables named code(a String value) and value(a numerical value). +Treemap - Two variables named Path(a list of strings) and value(a numerical value). +Radar Chart - Multiple variables representing property values of nodes and relationships. +Sankey Chart - Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value). +Map - multiple variables representing nodes objects(should contain spatial propeties) and relationship objects. +Single Value - A single value of a single variable. +Gauge Chart - A single value of a single variable. +Raw JSON - The Cypher query must return a JSON object that will be displayed as raw JSON data. +Pie Chart - Two variables named category and value.`; + export const MAX_NUM_VALIDATION = 5; diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index 8ed569d5e..dadeb65d4 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -63,7 +63,6 @@ export const queryTranslationThunk = try { const state = getState(); const database = getDatabase(state, pagenumber, cardIndex); - let client: ModelClient = await dispatch(getModelClientThunk()); client.setDriver(driver); From ccfcd65adb9528d647f4cdcf58adbaa50beaa4b3 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Tue, 6 Jun 2023 17:42:29 +0200 Subject: [PATCH 05/49] fixing state inconsistency when reloading and commenting --- .../query-translator/QueryTranslator.tsx | 11 +- .../modelClients/ModelClient.ts | 87 +++++--- .../modelClients/OpenAiClient.ts | 4 +- .../modelClients/VertexAiClient.ts | 205 +----------------- .../state/QueryTranslatorSelector.ts | 29 +-- .../state/QueryTranslatorThunks.ts | 37 ++-- 6 files changed, 100 insertions(+), 273 deletions(-) diff --git a/src/extensions/query-translator/QueryTranslator.tsx b/src/extensions/query-translator/QueryTranslator.tsx index eaaa2c16b..42016e2e8 100644 --- a/src/extensions/query-translator/QueryTranslator.tsx +++ b/src/extensions/query-translator/QueryTranslator.tsx @@ -5,7 +5,6 @@ import { getApiKey, getClientSettings, getModelProvider } from './state/QueryTra import { Button } from '@neo4j-ndl/react'; import TranslateIcon from '@mui/icons-material/Translate'; import QueryTranslatorSettingsModal from './settings/QueryTranslatorSettingsModal'; -import { getModelClientObject } from './QueryTranslatorConfig'; import { queryTranslationThunk } from './state/QueryTranslatorThunks'; import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; /** @@ -19,7 +18,7 @@ export const QueryTranslator = ({ apiKey, modelProvider, clientSettings, - deleteAllMessageHistory, + _deleteAllMessageHistory, setGlobalModelClient, queryTranslation, _deleteMessageHistory, @@ -29,10 +28,14 @@ export const QueryTranslator = ({ // When changing provider, i will reset all the messages to prevent strage results useEffect(() => { + // TODO: can't recast correctly the model client when refreshing the session, it should be removed at every session restart setGlobalModelClient(undefined); - deleteAllMessageHistory(); + }, []); + + // When changing provider, i will reset all the messages to prevent strage results + useEffect(() => { if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { - queryTranslation(0, 0, 'give me any query', 'Table', driver); + queryTranslation(0, '2afa79af-9ac2-4473-b424-47db58db46af', 'give me any query', 'Table', driver); } }, [modelProvider, apiKey, clientSettings]); diff --git a/src/extensions/query-translator/modelClients/ModelClient.ts b/src/extensions/query-translator/modelClients/ModelClient.ts index 0b20ae666..0ba513aa4 100644 --- a/src/extensions/query-translator/modelClients/ModelClient.ts +++ b/src/extensions/query-translator/modelClients/ModelClient.ts @@ -32,25 +32,35 @@ export abstract class ModelClient { notImplementedError('setModelClient'); } + /** + * Function to query the db directly from the client + * @param query Query to run + * @param database Selected database + * @returns The records results if the query runs correctly, otherwise the function will throw an error + */ async queryDatabase(query, database) { - const session = this.driver.session({ database: database }); - const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); - - let res = await transaction - .run(query, undefined) - .then((res) => { - const { records } = res; - let elems = records.map((elem) => { - return elem.toObject()[elem.keys[0]]; + if (this.driver) { + const session = this.driver.session({ database: database }); + const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); + + let res = await transaction + .run(query, undefined) + .then((res) => { + const { records } = res; + let elems = records.map((elem) => { + return elem.toObject()[elem.keys[0]]; + }); + records.length > 0 ?? elems.unshift(records[0].keys); + transaction.commit(); + return elems; + }) + .catch(async (e) => { + throw e; }); - records.length > 0 ?? elems.unshift(records[0].keys); - transaction.commit(); - return elems; - }) - .catch(async (e) => { - throw e; - }); - return res; + return res; + } + throw new Error('Driver not present'); + } createSchemaText(nodeProps, relProps, rels) { @@ -84,46 +94,55 @@ export abstract class ModelClient { `; } - async validateQuery(_message, _database) { - notImplementedError('validateQuery'); - } - setDriver(driver: any) { this.driver = driver; } + /** + * Method responsible to ask the model to translate the message. + * @param inputMessage + * @param history History of messages exchanged between a card and the model client + * @param database Databased used from the report, it will be used to fetch the schema + * @param reportType Type of report asking that requires the translation + * @returns The new history to assign to the card. If there was no possibility of validating the query, the + * method will return the same history passed in input + */ async queryTranslation(inputMessage, history, database, reportType) { // Creating a copy of the history let newHistory = [...history]; - + // Creating a tmp history to prevent updating the history with erroneous messages + let tmpHistory = [...newHistory]; + let schema = ''; try { - if (history.length == 0) { - let schema = await this.generateSchema(database); - newHistory.push(this.addSystemMessage(this.getSystemMessage(schema))); + // If empty, the first message will be the task definition + if (tmpHistory.length == 0) { + schema = await this.generateSchema(database); + tmpHistory.push(this.addSystemMessage(this.getSystemMessage(schema))); } - - let tmpHistory = [...newHistory]; tmpHistory.push(this.addUserMessage(inputMessage, reportType)); let retries = 0; let isValidated = false; let errorMessage = ''; - // Creating a tmp history to prevent updating the history with erroneous messages + // While is not validated and we didn't exceed the maximum retry number while (!isValidated && retries < MAX_NUM_VALIDATION) { retries += 1; + // Get the answer to the question let newMessage = await this.chatCompletion(tmpHistory); tmpHistory.push(newMessage); - await consoleLogAsync(`tmpHistory step: ${retries}`, tmpHistory); // and try to validate it - let res = await this.validateQuery(newMessage, database); - isValidated = res[0]; - errorMessage = res[1]; + let validationResult = await this.validateQuery(newMessage, database); + isValidated = validationResult[0]; + errorMessage = validationResult[1]; if (!isValidated) { tmpHistory.push(this.addErrorMessage(errorMessage)); } else { + if (newHistory.length == 0 && schema) { + newHistory.push(this.addSystemMessage(this.getSystemMessage(schema))); + } newHistory.push(this.addUserMessage(inputMessage, reportType, true)); newHistory.push(newMessage); } @@ -137,6 +156,10 @@ export abstract class ModelClient { return newHistory; } + async validateQuery(_message, _database) { + notImplementedError('validateQuery'); + } + async chatCompletion(_history) { notImplementedError('chatCompletion'); } diff --git a/src/extensions/query-translator/modelClients/OpenAiClient.ts b/src/extensions/query-translator/modelClients/OpenAiClient.ts index 19805ac49..a6d1e21cc 100644 --- a/src/extensions/query-translator/modelClients/OpenAiClient.ts +++ b/src/extensions/query-translator/modelClients/OpenAiClient.ts @@ -1,5 +1,5 @@ -import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai'; -import { nodePropsQuery, MAX_NUM_VALIDATION, relPropsQuery, relQuery, reportTypesToDesc } from './const'; +import { ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai'; +import { reportTypesToDesc } from './const'; import { ModelClient } from './ModelClient'; const consoleLogAsync = async (message: string, other?: any) => { diff --git a/src/extensions/query-translator/modelClients/VertexAiClient.ts b/src/extensions/query-translator/modelClients/VertexAiClient.ts index 4f1a37f23..40d0caa0f 100644 --- a/src/extensions/query-translator/modelClients/VertexAiClient.ts +++ b/src/extensions/query-translator/modelClients/VertexAiClient.ts @@ -1,210 +1,7 @@ -import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from 'openai'; -import { REPORT_TYPES } from '../../../config/ReportConfig'; -import { nodePropsQuery, relPropsQuery, relQuery, reportTypesToDesc } from './const'; import { ModelClient } from './ModelClient'; const consoleLogAsync = async (message: string, other?: any) => { await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); }; -export class VertexAiClient implements ModelClient { - messages: Array; - - apiKey: string; - - modelClient: OpenAIApi; - - driver; - - setMessages; - - database; - - nodeProps; - - relProps; - - rels; - - schemaText; - - reportType: string; - - constructor(apiKey, messages = [], setMessages, driver, database, reportType) { - this.messages = messages; - this.setMessages = setMessages; - this.apiKey = apiKey; - if (apiKey) { - this.setApiKey(apiKey); - } - this.driver = driver; - this.database = database; - this.generateSchema(); - this.updateReportType(reportType); - } - - updateReportType(reportType) { - this.reportType = REPORT_TYPES[reportType].label; - } - - resetClient() { - this.messages = []; - this.setMessages([]); - this.generateSchema(); - } - - setNodeProps(props) { - this.nodeProps = props; - } - - setRelProps(props) { - this.relProps = props; - } - - setRels(props) { - this.rels = props; - } - - setSchemaText() { - this.schemaText = ` - This is the schema representation of the Neo4j database. - Node properties are the following: - ${JSON.stringify(this.nodeProps)} - Relationship properties are the following: - ${JSON.stringify(this.relProps)} - Relationship point from source to target nodes - ${JSON.stringify(this.rels)} - Make sure to respect relationship types and directions - `; - } - - async queryDatabase(query) { - const session = this.driver.session({ database: this.database }); - const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 }); - - let res = await transaction - .run(query, undefined) - .then((res) => { - const { records } = res; - let elems = records.map((elem) => { - return elem.toObject()[elem.keys[0]]; - }); - records.length > 0 ?? elems.unshift(records[0].keys); - transaction.commit(); - return elems; - }) - .catch(async (e) => { - await consoleLogAsync(`Error while running ${query}`, e); - }); - return res; - } - - async generateSchema() { - this.setNodeProps(await this.queryDatabase(nodePropsQuery)); - this.setRelProps(await this.queryDatabase(relPropsQuery)); - this.setRels(await this.queryDatabase(relQuery)); - this.setSchemaText(); - } - - getSystemMessage() { - return ` - Task: Generate Cypher queries to query a Neo4j graph database based on the provided schema definition. These queries will be used inside NeoDash reports. - Documentation for NeoDash is here : https://neo4j.com/labs/neodash/2.2/ - Instructions: - Use only the provided relationship types and properties. - Do not use any other relationship types or properties that are not provided. - The Cypher RETURN clause must contained certain variables, based on the report type asked for. - Report types : - Table - Multiple variables representing property values of nodes and relationships. - Graph - Multiple variables representing nodes objects and relationships objects inside the graph. - Bar Chart - Two variables named category(a String value) and value(numeric value). - Line Chart - Two numeric variables named x and y. - Sunburst - Two variables named Path(list of strings) and value(a numerical value). - Circle Packing - Two variables named Path(a list of strings) and value(a numerical value). - Choropleth - Two variables named code(a String value) and value(a numerical value). - Area Map - Two variables named code(a String value) and value(a numerical value). - Treemap - Two variables named Path(a list of strings) and value(a numerical value). - Radar Chart - Multiple variables representing property values of nodes and relationships. - Sankey Chart - Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value). - Map - multiple variables representing nodes objects(should contain spatial propeties) and relationship objects. - Single Value - A single value of a single variable. - Gauge Chart - A single value of a single variable. - Raw JSON - The Cypher query must return a JSON object that will be displayed as raw JSON data. - Pie Chart - Two variables named category and value. - Schema: - ${this.schemaText} - `; - } - - setApiKey(apiKey) { - this.apiKey = apiKey; - const configuration = new Configuration({ - apiKey: apiKey, - }); - this.modelClient = new OpenAIApi(configuration); - } - - addUserMessage(content) { - let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${ - reportTypesToDesc[this.reportType] - } Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. `; - this.messages.push({ role: 'user', content: finalMessage }); - } - - addSystemMessage(content) { - this.messages.push({ role: 'assistant', content: content }); - } - - updateMessageHistory(message) { - this.messages.push(message); - this.setMessages(this.messages); - } - - async chatCompletion( - content, - setResponse = (res) => { - console.log(res); - } - ) { - try { - if (this.messages.length == 0) { - if (this.schemaText) { - this.addSystemMessage(this.getSystemMessage()); - } else { - await this.generateSchema(); - this.addSystemMessage(this.getSystemMessage()); - } - } - if (this.apiKey) { - this.addUserMessage(content); - - const completion = await this.modelClient.createChatCompletion({ - model: 'gpt-3.5-turbo', - messages: this.messages, - }); - - // If the status is correct - if ( - completion.status == 200 && - completion.data && - completion.data.choices && - completion.data.choices[0].message - ) { - let { message } = completion.data.choices[0]; - this.updateMessageHistory(message); - setResponse(message.content); - } else { - throw Error(`Request returned with status: ${completion.status}`); - } - } else { - throw Error('api key not present'); - } - } catch (error) { - setResponse(!this.apiKey ? 'key not present' : `${error}`); - await consoleLogAsync('error during query', error); - } finally { - // TODO: trigger availability of the card (we should stop clicking on the card to prevent strange misconfigurations here) - await consoleLogAsync('done', this); - } - } -} +export class VertexAiClient extends ModelClient {} diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts index b536c9c7a..44e88d5b8 100644 --- a/src/extensions/query-translator/state/QueryTranslatorSelector.ts +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -1,24 +1,23 @@ -export const queryTranslatorExtensionName = 'query-translator'; - -/** - * The extension keeps, during one session, the history of messages between a user and a model. - * This method serves to get all the messages. - * @param state Current state of the session - * @returns history of messages between the user and the model within the context of that card - */ +export const QUERY_TRANSLATOR_EXTENSION_NAME = 'query-translator'; const checkExtensionConfig = (state: any) => { - return state.dashboard.extensionsConfig && state.dashboard.extensionsConfig[queryTranslatorExtensionName]; + return state.dashboard.extensions && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME]; }; export const getHistory = (state: any) => { - let history = checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].history; + let history = checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].history; return history != undefined && history ? history : {}; }; +export const getModelProvider = (state: any) => { + let modelProvider = + checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].modelProvider; + return modelProvider != undefined && modelProvider ? modelProvider : ''; +}; + export const getModelClient = (state: any) => { let modelClient = - checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].modelClient; + checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].modelClient; return modelClient != undefined && modelClient ? modelClient : undefined; }; @@ -37,7 +36,7 @@ export const getHistoryPerCard = (state: any, pageIndex, cardIndex) => { export const getClientSettings = (state: any) => { let clientSettings = - checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].settings; + checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].settings; return clientSettings != undefined && clientSettings ? clientSettings : {}; }; @@ -46,9 +45,3 @@ export const getApiKey = (state: any) => { return settings.apiKey != undefined && settings.apiKey ? settings.apiKey : ''; }; - -export const getModelProvider = (state: any) => { - let modelProvider = - checkExtensionConfig(state) && state.dashboard.extensionsConfig[queryTranslatorExtensionName].modelProvider; - return modelProvider != undefined && modelProvider ? modelProvider : ''; -}; diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index dadeb65d4..da9790ac4 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -9,7 +9,8 @@ const consoleLogAsync = async (message: string, other?: any) => { }; /** - * Thunk used to initialize the client model. + * Thunk used to initialize the client model. To inizialize the client, we need to check that + * it can authenticate to it's service by calling its authenticate function * @returns True if the client is created, otherwise False */ const modelClientInitializationThunk = @@ -20,11 +21,17 @@ const modelClientInitializationThunk = ) => async (dispatch: any, getState: any) => { const state = getState(); + // Fetching the client properties from the state let modelProvider = getModelProvider(state); let settings = getClientSettings(state); if (modelProvider && settings) { + // Getting the correct ModelClient object let tmpClient = getModelClientObject(modelProvider, settings); + + // Try authentication let isAuthenticated = await tmpClient.authenticate(setIsAuthenticated); + + // If the authentication runs smoothly, store the client inside the application state if (isAuthenticated) { dispatch(setGlobalModelClient(tmpClient)); return tmpClient; @@ -34,7 +41,8 @@ const modelClientInitializationThunk = }; /** - * Wrapper to recreate the model client if it doesn't exists + * Wrapper to get the model client from the state if already exists, otherwise it will recreate it and check that + * the authentication still works * @returns An instance of the model client */ const getModelClientThunk = () => async (dispatch: any, getState: any) => { @@ -51,30 +59,33 @@ const getModelClientThunk = () => async (dispatch: any, getState: any) => { }; /** - * Method to handle the request to a model client for a query translation. + * Thunk used to handle the request to a model client for a query translation. * @param pagenumber Index of the page where the card lives - * @param cardIndex Index that identifies a card inside its page + * @param cardId Index that identifies a card inside its page * @param message Message inserted by the user - * @param setQuery Function to set the query inside the card (i don't think i need this one) - * @returns + * @param reportType Type of report used by the card calling the thunk + * @param driver Neo4j Driver used to fetch the schema from the database */ export const queryTranslationThunk = - (pagenumber, cardIndex, message, reportType, driver) => async (dispatch: any, getState: any) => { + (pagenumber, cardId, message, reportType, driver) => async (dispatch: any, getState: any) => { try { const state = getState(); - const database = getDatabase(state, pagenumber, cardIndex); + const database = getDatabase(state, pagenumber, cardId); + + // Retrieving the model client from the state let client: ModelClient = await dispatch(getModelClientThunk()); client.setDriver(driver); - const messageHistory = getHistoryPerCard(state, pagenumber, cardIndex); + const messageHistory = getHistoryPerCard(state, pagenumber, cardId); let newMessages = await client.queryTranslation(message, messageHistory, database, reportType); - consoleLogAsync('newHistory', newMessages); - if (message.length !== newMessages.length) { - dispatch(updateMessageHistory(newMessages, pagenumber, cardIndex)); + + // The history will be updated only if the length is different (otherwise, it's the same history) + if (messageHistory.length < newMessages.length) { + dispatch(updateMessageHistory(newMessages, pagenumber, cardId)); } } catch (e) { await consoleLogAsync( - `Something wrong happened while calling the model client for the card number ${cardIndex} inside the page ${pagenumber}: \n`, + `Something wrong happened while calling the model client for the card number ${cardId} inside the page ${pagenumber}: \n`, { e } ); } From cc7757ad61c4bd3f20d9e3058d414a71022c6b02 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Tue, 6 Jun 2023 19:15:01 +0200 Subject: [PATCH 06/49] more comments and added card query thunk inside translation --- .../query-translator/QueryTranslator.tsx | 7 +++-- .../query-translator/QueryTranslatorConfig.ts | 14 +++++++--- .../modelClients/ModelClient.ts | 22 +++++++++------ .../modelClients/OpenAiClient.ts | 6 ++++- .../state/QueryTranslatorActions.ts | 6 +---- .../state/QueryTranslatorReducer.ts | 8 +++--- .../state/QueryTranslatorSelector.ts | 13 +++++++-- .../state/QueryTranslatorThunks.ts | 27 ++++++++++++++----- 8 files changed, 72 insertions(+), 31 deletions(-) diff --git a/src/extensions/query-translator/QueryTranslator.tsx b/src/extensions/query-translator/QueryTranslator.tsx index 42016e2e8..1ba225bbb 100644 --- a/src/extensions/query-translator/QueryTranslator.tsx +++ b/src/extensions/query-translator/QueryTranslator.tsx @@ -33,9 +33,12 @@ export const QueryTranslator = ({ }, []); // When changing provider, i will reset all the messages to prevent strage results + // TODO: remove this effect is just for testing useEffect(() => { if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { - queryTranslation(0, '2afa79af-9ac2-4473-b424-47db58db46af', 'give me any query', 'Table', driver); + [].forEach((cardId) => { + queryTranslation(0, cardId, 'give me any query', 'Table', driver); + }); } }, [modelProvider, apiKey, clientSettings]); @@ -50,7 +53,7 @@ export const QueryTranslator = ({ const component = (
{button} - {open ? : <>} + {open ? : <>}
); diff --git a/src/extensions/query-translator/QueryTranslatorConfig.ts b/src/extensions/query-translator/QueryTranslatorConfig.ts index b4c34410b..023633e00 100644 --- a/src/extensions/query-translator/QueryTranslatorConfig.ts +++ b/src/extensions/query-translator/QueryTranslatorConfig.ts @@ -3,10 +3,18 @@ import { ModelClient } from './modelClients/ModelClient'; import { OpenAiClient } from './modelClients/OpenAiClient'; import { VertexAiClient } from './modelClients/VertexAiClient'; +interface ClientSettingEntry { + label: string; + type: SELECTION_TYPES; + default: any; + required?: boolean; // Required for authentication, the user should insert all the required fields before trying to authenticate + methodFromClient?: string; // String that contains the name of the client function to call to retrieve the data needed to fill the option +} + interface ClientSettings { - apiKey: any; - modelType: any; - region?: any; + apiKey: ClientSettingEntry; + modelType: ClientSettingEntry; + region?: ClientSettingEntry; } interface ClientConfig { diff --git a/src/extensions/query-translator/modelClients/ModelClient.ts b/src/extensions/query-translator/modelClients/ModelClient.ts index 0ba513aa4..b3e58888e 100644 --- a/src/extensions/query-translator/modelClients/ModelClient.ts +++ b/src/extensions/query-translator/modelClients/ModelClient.ts @@ -58,9 +58,8 @@ export abstract class ModelClient { throw e; }); return res; - } - throw new Error('Driver not present'); - + } + throw new Error('Driver not present'); } createSchemaText(nodeProps, relProps, rels) { @@ -98,6 +97,11 @@ export abstract class ModelClient { this.driver = driver; } + getMessageContent(_message: any) { + notImplementedError('getMessageContent'); + return ''; + } + /** * Method responsible to ask the model to translate the message. * @param inputMessage @@ -113,6 +117,7 @@ export abstract class ModelClient { // Creating a tmp history to prevent updating the history with erroneous messages let tmpHistory = [...newHistory]; let schema = ''; + let query = ''; try { // If empty, the first message will be the task definition if (tmpHistory.length == 0) { @@ -130,11 +135,11 @@ export abstract class ModelClient { retries += 1; // Get the answer to the question - let newMessage = await this.chatCompletion(tmpHistory); - tmpHistory.push(newMessage); + let modelAnswer = await this.chatCompletion(tmpHistory); + tmpHistory.push(modelAnswer); // and try to validate it - let validationResult = await this.validateQuery(newMessage, database); + let validationResult = await this.validateQuery(modelAnswer, database); isValidated = validationResult[0]; errorMessage = validationResult[1]; if (!isValidated) { @@ -144,7 +149,8 @@ export abstract class ModelClient { newHistory.push(this.addSystemMessage(this.getSystemMessage(schema))); } newHistory.push(this.addUserMessage(inputMessage, reportType, true)); - newHistory.push(newMessage); + newHistory.push(modelAnswer); + query = this.getMessageContent(modelAnswer); } } if (!isValidated) { @@ -153,7 +159,7 @@ export abstract class ModelClient { } catch (error) { await consoleLogAsync('error during query', error); } - return newHistory; + return [query, newHistory]; } async validateQuery(_message, _database) { diff --git a/src/extensions/query-translator/modelClients/OpenAiClient.ts b/src/extensions/query-translator/modelClients/OpenAiClient.ts index a6d1e21cc..a3683ea03 100644 --- a/src/extensions/query-translator/modelClients/OpenAiClient.ts +++ b/src/extensions/query-translator/modelClients/OpenAiClient.ts @@ -1,4 +1,4 @@ -import { ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai'; +import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai'; import { reportTypesToDesc } from './const'; import { ModelClient } from './ModelClient'; @@ -124,6 +124,10 @@ export class OpenAiClient extends ModelClient { return { role: ChatCompletionRequestMessageRoleEnum.User, content: finalMessage }; } + getMessageContent(message: ChatCompletionRequestMessage) { + return message.content; + } + async chatCompletion(history) { const completion = await this.modelClient.createChatCompletion({ model: this.modelType, diff --git a/src/extensions/query-translator/state/QueryTranslatorActions.ts b/src/extensions/query-translator/state/QueryTranslatorActions.ts index 20c6fb7d8..1ea8716b1 100644 --- a/src/extensions/query-translator/state/QueryTranslatorActions.ts +++ b/src/extensions/query-translator/state/QueryTranslatorActions.ts @@ -28,11 +28,7 @@ export const UPDATE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_E * @param cardIndex Index of the card inside the page * @returns */ -export const updateMessageHistory = ( - cardHistory: ChatCompletionRequestMessage[], - pageIndex: number, - cardIndex: number -) => ({ +export const updateMessageHistory = (cardHistory: any[], pageIndex: number, cardIndex: number) => ({ type: UPDATE_MESSAGE_HISTORY, payload: { cardHistory, pageIndex, cardIndex }, }); diff --git a/src/extensions/query-translator/state/QueryTranslatorReducer.ts b/src/extensions/query-translator/state/QueryTranslatorReducer.ts index ef3256d53..bb5b53734 100644 --- a/src/extensions/query-translator/state/QueryTranslatorReducer.ts +++ b/src/extensions/query-translator/state/QueryTranslatorReducer.ts @@ -12,10 +12,10 @@ import { } from './QueryTranslatorActions'; export const INITIAL_EXTENSION_STATE = { - modelProvider: '', - history: {}, - modelClient: '', - settings: {}, + modelProvider: '', // Name of the provider (defined in the config) + history: {}, // Objects that keeps, for every card, their history (to move to session store) + modelClient: '', // Object to connect with the model API (to move to session store) + settings: {}, // Settings needed by the client to operate }; const update = (state, mutations) => Object.assign({}, state, mutations); diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts index 44e88d5b8..544f71c94 100644 --- a/src/extensions/query-translator/state/QueryTranslatorSelector.ts +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -15,6 +15,14 @@ export const getModelProvider = (state: any) => { return modelProvider != undefined && modelProvider ? modelProvider : ''; }; +/** + * The extension keeps, during one session, the client to connect to the model API. + * The client is kept only during the session, so every refresh it is deleted. + * @param state Current state of the session + * @param pageIndex Index of the page where the card lives + * @param cardIndex Index that identifies the card inside the page + * @returns history of messages between the user and the model within the context of that card (defaulted to undefined) + */ export const getModelClient = (state: any) => { let modelClient = checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].modelClient; @@ -22,11 +30,12 @@ export const getModelClient = (state: any) => { }; /** - * The extension keeps, during one session, the history of messages between a user and a model + * The extension keeps, during one session, the history of messages between a user and a model. + * The history is kept only during the session, so every refresh it is deleted. * @param state Current state of the session * @param pageIndex Index of the page where the card lives * @param cardIndex Index that identifies the card inside the page - * @returns history of messages between the user and the model within the context of that card + * @returns history of messages between the user and the model within the context of that card (defaulted to []) */ export const getHistoryPerCard = (state: any, pageIndex, cardIndex) => { let history = getHistory(state); diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index da9790ac4..7c3b9bf1f 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -1,3 +1,4 @@ +import { updateReportQueryThunk } from '../../../card/CardThunks'; import { getDatabase } from '../../../settings/SettingsSelectors'; import { ModelClient } from '../modelClients/ModelClient'; import { getModelClientObject } from '../QueryTranslatorConfig'; @@ -21,6 +22,7 @@ const modelClientInitializationThunk = ) => async (dispatch: any, getState: any) => { const state = getState(); + // Fetching the client properties from the state let modelProvider = getModelProvider(state); let settings = getClientSettings(state); @@ -49,6 +51,7 @@ const getModelClientThunk = () => async (dispatch: any, getState: any) => { const state = getState(); let modelClient = getModelClient(state); + // If not persisted in the current session, try to initialize a new model if (!modelClient) { let newClient = await dispatch(modelClientInitializationThunk()); if (newClient) { @@ -68,25 +71,37 @@ const getModelClientThunk = () => async (dispatch: any, getState: any) => { */ export const queryTranslationThunk = (pagenumber, cardId, message, reportType, driver) => async (dispatch: any, getState: any) => { + let query; try { const state = getState(); const database = getDatabase(state, pagenumber, cardId); // Retrieving the model client from the state let client: ModelClient = await dispatch(getModelClientThunk()); - client.setDriver(driver); + if (client) { + // If missing, pass down the driver to persist it inside the client + if (!client.driver) { + client.setDriver(driver); + } - const messageHistory = getHistoryPerCard(state, pagenumber, cardId); - let newMessages = await client.queryTranslation(message, messageHistory, database, reportType); + const messageHistory = getHistoryPerCard(state, pagenumber, cardId); + let translationRes = await client.queryTranslation(message, messageHistory, database, reportType); + query = translationRes[0]; + let newHistory = translationRes[1]; - // The history will be updated only if the length is different (otherwise, it's the same history) - if (messageHistory.length < newMessages.length) { - dispatch(updateMessageHistory(newMessages, pagenumber, cardId)); + // The history will be updated only if the length is different (otherwise, it's the same history) + if (messageHistory.length < newHistory.length && query) { + dispatch(updateMessageHistory(newHistory, pagenumber, cardId)); + dispatch(updateReportQueryThunk(cardId, query)); + } + } else { + throw new Error("Couldn't get the Model Client for the translation, please check your credentials."); } } catch (e) { await consoleLogAsync( `Something wrong happened while calling the model client for the card number ${cardId} inside the page ${pagenumber}: \n`, { e } ); + throw e; } }; From e828425dae8791a56b6e360a0f48f286991a4151 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Thu, 8 Jun 2023 11:02:08 +0200 Subject: [PATCH 07/49] Updating interface --- src/extensions/ExtensionConfig.tsx | 9 +- .../query-translator/QueryTranslatorConfig.ts | 29 ++-- .../{modelClients => clients}/ModelClient.ts | 2 +- .../{modelClients => clients}/OpenAiClient.ts | 0 .../VertexAiClient.ts | 0 .../{modelClients => clients}/const.ts | 0 .../ClientSettings.tsx | 134 +++++++++++++----- .../{ => component}/QueryTranslator.tsx | 31 ++-- .../QueryTranslatorSettingsModal.tsx | 61 ++++---- .../state/QueryTranslatorThunks.ts | 7 +- src/settings/SettingsModal.tsx | 17 +-- 11 files changed, 174 insertions(+), 116 deletions(-) rename src/extensions/query-translator/{modelClients => clients}/ModelClient.ts (99%) rename src/extensions/query-translator/{modelClients => clients}/OpenAiClient.ts (100%) rename src/extensions/query-translator/{modelClients => clients}/VertexAiClient.ts (100%) rename src/extensions/query-translator/{modelClients => clients}/const.ts (100%) rename src/extensions/query-translator/{settings => component}/ClientSettings.tsx (51%) rename src/extensions/query-translator/{ => component}/QueryTranslator.tsx (75%) rename src/extensions/query-translator/{settings => component}/QueryTranslatorSettingsModal.tsx (55%) diff --git a/src/extensions/ExtensionConfig.tsx b/src/extensions/ExtensionConfig.tsx index abf94e124..b7b0f17d9 100644 --- a/src/extensions/ExtensionConfig.tsx +++ b/src/extensions/ExtensionConfig.tsx @@ -7,7 +7,8 @@ import SidebarDrawerButton from './sidebar/SidebarDrawerButton'; import { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; import { QUERY_TRANSLATOR_ACTION_PREFIX } from './query-translator/state/QueryTranslatorActions'; import { queryTranslatorReducer } from './query-translator/state/QueryTranslatorReducer'; -import QueryTranslator from './query-translator/QueryTranslator'; +import QueryTranslatorButton from './query-translator/component/QueryTranslator'; +import TranslatorButton from './query-translator/component/TranslatorButton'; // TODO: continue documenting interface interface Extension { @@ -61,7 +62,7 @@ export const EXTENSIONS: Record = { label: 'Node Sidebar', author: 'Neo4j Professional Services', image: 'https://www.unfe.org/wp-content/uploads/2019/04/SM-placeholder.png', // TODO: Fix placeholder image. - enabled: false, + enabled: true, reducerPrefix: NODE_SIDEBAR_ACTION_PREFIX, reducerObject: sidebarReducer, drawerButton: SidebarDrawerButton, @@ -84,13 +85,13 @@ export const EXTENSIONS: Record = { }, 'query-translator': { name: 'query-translator', - label: 'Query Translator', + label: 'Natural Language Queries', author: 'Neo4j Professional Services', image: 'https://www.unfe.org/wp-content/uploads/2019/04/SM-placeholder.png', // TODO: Fix placeholder image. enabled: true, reducerPrefix: QUERY_TRANSLATOR_ACTION_PREFIX, reducerObject: queryTranslatorReducer, - settingsModal: QueryTranslator, + drawerButton: QueryTranslatorButton, description: 'ask queries in natural language (available only in english).', link: 'https://neo4j.com/professional-services/', }, diff --git a/src/extensions/query-translator/QueryTranslatorConfig.ts b/src/extensions/query-translator/QueryTranslatorConfig.ts index 023633e00..d1934c061 100644 --- a/src/extensions/query-translator/QueryTranslatorConfig.ts +++ b/src/extensions/query-translator/QueryTranslatorConfig.ts @@ -1,13 +1,14 @@ import { SELECTION_TYPES } from '../../config/CardConfig'; -import { ModelClient } from './modelClients/ModelClient'; -import { OpenAiClient } from './modelClients/OpenAiClient'; -import { VertexAiClient } from './modelClients/VertexAiClient'; +import { ModelClient } from './clients/ModelClient'; +import { OpenAiClient } from './clients/OpenAiClient'; +import { VertexAiClient } from './clients/VertexAiClient'; interface ClientSettingEntry { label: string; type: SELECTION_TYPES; default: any; - required?: boolean; // Required for authentication, the user should insert all the required fields before trying to authenticate + authentication?: boolean; // Required for authentication, the user should insert all the required fields before trying to authenticate + hasAuthButton?: boolean; // Append a button at the end of the selector to trigger an auth request. methodFromClient?: string; // String that contains the name of the client function to call to retrieve the data needed to fill the option } @@ -25,7 +26,7 @@ interface ClientConfig { } interface AvailableClients { - openAi: ClientConfig; + OpenAI: ClientConfig; vertexAi: ClientConfig; } @@ -35,21 +36,23 @@ interface QueryTranslatorConfig { export const QUERY_TRANSLATOR_CONFIG: QueryTranslatorConfig = { availableClients: { - openAi: { - clientName: 'openAi', + OpenAI: { + clientName: 'OpenAI', clientClass: OpenAiClient, settings: { apiKey: { - label: 'Api Key to authenticate the client', + label: 'OpenAI API Key', type: SELECTION_TYPES.TEXT, default: '', - required: true, + hasAuthButton: true, + authentication: true, }, modelType: { - label: 'Select from the possible model types', + label: 'Model', type: SELECTION_TYPES.LIST, methodFromClient: 'getListModels', default: '', + authentication: false, }, }, }, @@ -99,6 +102,10 @@ export function getQueryTranslatorDefaultConfig(providerName) { * @returns Client object related to the provider */ export function getModelClientObject(modelProvider, settings) { - let modelProviderClass = QUERY_TRANSLATOR_CONFIG.availableClients[modelProvider].clientClass; + let providerDetails = QUERY_TRANSLATOR_CONFIG.availableClients[modelProvider]; + if (providerDetails === undefined) { + throw Error(`Invalid provider name${ modelProvider}`); + } + let modelProviderClass = providerDetails.clientClass; return new modelProviderClass(settings); } diff --git a/src/extensions/query-translator/modelClients/ModelClient.ts b/src/extensions/query-translator/clients/ModelClient.ts similarity index 99% rename from src/extensions/query-translator/modelClients/ModelClient.ts rename to src/extensions/query-translator/clients/ModelClient.ts index b3e58888e..6e28b8ffb 100644 --- a/src/extensions/query-translator/modelClients/ModelClient.ts +++ b/src/extensions/query-translator/clients/ModelClient.ts @@ -189,7 +189,7 @@ export abstract class ModelClient { } // to see if i need this -export enum ModelOperationState { +export enum ModelConnectionState { RUNNING, DONE, ERROR, diff --git a/src/extensions/query-translator/modelClients/OpenAiClient.ts b/src/extensions/query-translator/clients/OpenAiClient.ts similarity index 100% rename from src/extensions/query-translator/modelClients/OpenAiClient.ts rename to src/extensions/query-translator/clients/OpenAiClient.ts diff --git a/src/extensions/query-translator/modelClients/VertexAiClient.ts b/src/extensions/query-translator/clients/VertexAiClient.ts similarity index 100% rename from src/extensions/query-translator/modelClients/VertexAiClient.ts rename to src/extensions/query-translator/clients/VertexAiClient.ts diff --git a/src/extensions/query-translator/modelClients/const.ts b/src/extensions/query-translator/clients/const.ts similarity index 100% rename from src/extensions/query-translator/modelClients/const.ts rename to src/extensions/query-translator/clients/const.ts diff --git a/src/extensions/query-translator/settings/ClientSettings.tsx b/src/extensions/query-translator/component/ClientSettings.tsx similarity index 51% rename from src/extensions/query-translator/settings/ClientSettings.tsx rename to src/extensions/query-translator/component/ClientSettings.tsx index b56c50537..e18e8c2ea 100644 --- a/src/extensions/query-translator/settings/ClientSettings.tsx +++ b/src/extensions/query-translator/component/ClientSettings.tsx @@ -4,13 +4,38 @@ import { debounce, List, ListItem } from '@mui/material'; import { getModelClientObject, getQueryTranslatorDefaultConfig } from '../QueryTranslatorConfig'; import { getClientSettings } from '../state/QueryTranslatorSelector'; import NeoSetting from '../../../component/field/Setting'; -import { setGlobalModelClient } from '../state/QueryTranslatorActions'; +import { + deleteAllMessageHistory, + setClientSettings, + setGlobalModelClient, + setModelProvider, +} from '../state/QueryTranslatorActions'; +import { + ExpandIcon, + ShrinkIcon, + DragIcon, + QuestionMarkCircleIconOutline, + TrashIconOutline, + DocumentDuplicateIconOutline, + PlayCircleIconSolid, + CheckCircleIconSolid, +} from '@neo4j-ndl/react/icons'; +import { Button, IconButton } from '@neo4j-ndl/react'; +import { modelClientInitializationThunk } from '../state/QueryTranslatorThunks'; const update = (state, mutations) => Object.assign({}, state, mutations); // TODO: the following // 1. the settings modal should save only when all the required fields are defined and we can correctly authenticate -export const ClientSettings = ({ modelProvider, settingState, setSettingsState }) => { +export const ClientSettings = ({ + modelProvider, + settingState, + setSettingsState, + authenticate, + updateModelProvider, + updateClientSettings, + deleteAllMessageHistory, +}) => { const defaultSettings = getQueryTranslatorDefaultConfig(modelProvider); const requiredSettings = Object.keys(defaultSettings).filter((setting) => defaultSettings[setting].required); const [localSettings, setLocalSettings] = React.useState(settingState); @@ -45,13 +70,6 @@ export const ClientSettings = ({ modelProvider, settingState, setSettingsState } return !requiredSettings.every((e) => settingState[e]); } - // Effect used to authenticate the client when the apiKey changed - // TODO: change to modelClientInitializationThunk when having a button to set the state globally and not only local as right now - useEffect(() => { - let clientObject = getModelClientObject(modelProvider, settingState); - clientObject.authenticate(setIsAuthenticated); - }, [settingState.apiKey]); - // Effect used to trigger the population of the settings when the user inserts a correct apiKey useEffect(() => { let localClientTmp = getModelClientObject(modelProvider, settingState); @@ -84,35 +102,65 @@ export const ClientSettings = ({ modelProvider, settingState, setSettingsState } } } + const authButton = ( + { + e.preventDefault(); + console.log('Clicked auth button...'); + updateModelProvider(modelProvider); + updateClientSettings(settingState); + authenticate(setIsAuthenticated); + }} + clean + style={{ marginTop: 24, marginRight: 28, color: 'white', backgroundColor: !isAuthenticated ? 'orange' : 'green' }} + size='medium' + > + {!isAuthenticated ? : } + + ); + const component = ( - - {Object.keys(defaultSettings).map((setting) => { - let disabled = checkIfDisabled(setting); - return ( - - { - updateSpecificFieldInStateObject(setting, e, localSettings, setLocalSettings); - debouncedUpdateSpecificFieldInStateObject(setting, e, settingState, setSettingsState); - }} - /> - - ); - })} + + {/* Only render the base settings (required for auth) if no authentication is available. */} + {Object.keys(defaultSettings) + .filter((setting) => defaultSettings[setting].authentication == true || isAuthenticated) + .map((setting) => { + let disabled = checkIfDisabled(setting); + return ( + + { + updateSpecificFieldInStateObject(setting, e, localSettings, setLocalSettings); + debouncedUpdateSpecificFieldInStateObject(setting, e, settingState, setSettingsState); + + if (defaultSettings[setting].hasAuthButton) { + setIsAuthenticated(false); + } + }} + /> + {/* TODO: Only show auth button if all required fields are filled in. */} + {defaultSettings[setting].hasAuthButton == true ? authButton : <>} + + ); + })} +
+ {isAuthenticated ? ( + + ) : ( + <> + )}
); return component; @@ -126,6 +174,18 @@ const mapDispatchToProps = (dispatch) => ({ setGlobalModelClient: (modelClient) => { dispatch(setGlobalModelClient(modelClient)); }, + authenticate: (setIsAuthenticated) => { + dispatch(modelClientInitializationThunk(setIsAuthenticated)); + }, + updateModelProvider: (modelProviderState) => { + dispatch(setModelProvider(modelProviderState)); + }, + updateClientSettings: (settingState) => { + dispatch(setClientSettings(settingState)); + }, + deleteAllMessageHistory: () => { + dispatch(deleteAllMessageHistory()); + }, }); export default connect(mapStateToProps, mapDispatchToProps)(ClientSettings); diff --git a/src/extensions/query-translator/QueryTranslator.tsx b/src/extensions/query-translator/component/QueryTranslator.tsx similarity index 75% rename from src/extensions/query-translator/QueryTranslator.tsx rename to src/extensions/query-translator/component/QueryTranslator.tsx index 1ba225bbb..ed630e859 100644 --- a/src/extensions/query-translator/QueryTranslator.tsx +++ b/src/extensions/query-translator/component/QueryTranslator.tsx @@ -1,12 +1,14 @@ import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; -import { deleteAllMessageHistory, deleteMessageHistory, setGlobalModelClient } from './state/QueryTranslatorActions'; -import { getApiKey, getClientSettings, getModelProvider } from './state/QueryTranslatorSelector'; -import { Button } from '@neo4j-ndl/react'; +import { deleteAllMessageHistory, deleteMessageHistory, setGlobalModelClient } from '../state/QueryTranslatorActions'; +import { getApiKey, getClientSettings, getModelProvider } from '../state/QueryTranslatorSelector'; +import { Button, SideNavigationItem } from '@neo4j-ndl/react'; import TranslateIcon from '@mui/icons-material/Translate'; -import QueryTranslatorSettingsModal from './settings/QueryTranslatorSettingsModal'; -import { queryTranslationThunk } from './state/QueryTranslatorThunks'; +import QueryTranslatorSettingsModal from './QueryTranslatorSettingsModal'; +import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; +import { Tooltip } from '@mui/material'; +import { ChatBubbleOvalLeftIconOutline, LanguageIconSolid } from '@neo4j-ndl/react/icons'; /** * //TODO: * 1. The query translator should handle all the requests from the cards to the client @@ -14,7 +16,7 @@ import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; * 3. create system message from here to prevent fucking all up during the thunk, o each modelProvider change and at the start pull all the db schema */ -export const QueryTranslator = ({ +export const QueryTranslatorButton = ({ apiKey, modelProvider, clientSettings, @@ -44,9 +46,18 @@ export const QueryTranslator = ({ const button = (
- + + setOpen(true)} + icon={ + + } + > + Query Translator + +
); @@ -81,4 +92,4 @@ const mapDispatchToProps = (dispatch) => ({ }, }); -export default connect(mapStateToProps, mapDispatchToProps)(QueryTranslator); +export default connect(mapStateToProps, mapDispatchToProps)(QueryTranslatorButton); diff --git a/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx b/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx similarity index 55% rename from src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx rename to src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx index 5ffc36996..283405baa 100644 --- a/src/extensions/query-translator/settings/QueryTranslatorSettingsModal.tsx +++ b/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx @@ -1,4 +1,4 @@ -import { Badge, Dialog, DialogContent, DialogTitle, IconButton } from '@mui/material'; +import { Badge, IconButton } from '@mui/material'; import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { setClientSettings, setModelProvider } from '../state/QueryTranslatorActions'; @@ -8,6 +8,7 @@ import { SELECTION_TYPES } from '../../../config/CardConfig'; import NeoSetting from '../../../component/field/Setting'; import { QUERY_TRANSLATOR_CONFIG } from '../QueryTranslatorConfig'; import ClientSettings from './ClientSettings'; +import { Button, Dialog } from '@neo4j-ndl/react'; export const QueryTranslatorSettingsModal = ({ open, @@ -28,40 +29,32 @@ export const QueryTranslatorSettingsModal = ({ }; return ( - - - Henlo - - - - - - - - -
- Select your model provider: - setModelProviderState(e)} + + LLM-Powered Natural Language Queries + + This extensions lets you create reports with natural language. Your queries (in English) are translated to + Cypher by a LLM provider of your choice. +
+ setModelProviderState(e)} + /> + {modelProviderState ? ( + -
- {modelProviderState ? ( - - ) : ( - <>Select one of the available clients. - )} -
-
+ ) : ( + <>Select one of the available clients. + )} +
); }; diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index 7c3b9bf1f..ce410f931 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -1,6 +1,6 @@ import { updateReportQueryThunk } from '../../../card/CardThunks'; import { getDatabase } from '../../../settings/SettingsSelectors'; -import { ModelClient } from '../modelClients/ModelClient'; +import { ModelClient } from '../clients/ModelClient'; import { getModelClientObject } from '../QueryTranslatorConfig'; import { setGlobalModelClient, updateMessageHistory } from './QueryTranslatorActions'; import { getClientSettings, getHistoryPerCard, getModelClient, getModelProvider } from './QueryTranslatorSelector'; @@ -14,10 +14,10 @@ const consoleLogAsync = async (message: string, other?: any) => { * it can authenticate to it's service by calling its authenticate function * @returns True if the client is created, otherwise False */ -const modelClientInitializationThunk = +export const modelClientInitializationThunk = ( setIsAuthenticated = (boolean) => { - let x = boolean; + return boolean; } ) => async (dispatch: any, getState: any) => { @@ -26,6 +26,7 @@ const modelClientInitializationThunk = // Fetching the client properties from the state let modelProvider = getModelProvider(state); let settings = getClientSettings(state); + // console.log(modelProvider, settings) if (modelProvider && settings) { // Getting the correct ModelClient object let tmpClient = getModelClientObject(modelProvider, settings); diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index e95f87635..314c5e507 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,21 +5,8 @@ import { DASHBOARD_SETTINGS } from '../config/DashboardConfig'; import { SideNavigationItem } from '@neo4j-ndl/react'; import { Cog6ToothIconOutline } from '@neo4j-ndl/react/icons'; import { Dialog } from '@neo4j-ndl/react'; -import { EXTENSIONS_SETTINGS_MODALS } from '../extensions/ExtensionConfig'; - -export const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting, navItemClass, extensions }) => { - function getExtensionSettingsModal() { - const res = ( - <> - {Object.keys(EXTENSIONS_SETTINGS_MODALS).map((name) => { - const Component = extensions[name] ? EXTENSIONS_SETTINGS_MODALS[name] : ''; - return Component ? : <>; - })} - - ); - return res; - } +export const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting, navItemClass }) => { const [open, setOpen] = React.useState(false); const handleClickOpen = () => { @@ -31,7 +18,6 @@ export const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting, na }; const settings = DASHBOARD_SETTINGS; - const extensionSettings = getExtensionSettingsModal(); // Else, build the advanced settings view. const advancedDashboardSettings = (
@@ -74,7 +60,6 @@ export const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting, na

{advancedDashboardSettings} - {extensionSettings}
From f954ae36fff918c2a60f58006a3c28da783837a8 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Thu, 8 Jun 2023 11:56:40 +0200 Subject: [PATCH 08/49] Styling of extensions --- public/translator.png | Bin 0 -> 287082 bytes src/extensions/ExtensionConfig.tsx | 57 +++++++++--------- src/extensions/ExtensionsModal.tsx | 5 +- .../component/QueryTranslator.tsx | 4 +- 4 files changed, 34 insertions(+), 32 deletions(-) create mode 100644 public/translator.png diff --git a/public/translator.png b/public/translator.png new file mode 100644 index 0000000000000000000000000000000000000000..59df348f119a0dc763523930f984fc728387af22 GIT binary patch literal 287082 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%0DdhP~wrJ9P!*ZaE-D?wMvQhpAGLS+Mq2$1kjf& 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%0DwfJqh^hFu`A!2K~&cmMaH= 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 = { 'Report actions let dashboard builders add extra interactivity into dashboards. For example, setting parameter values when a cell in a table or a node in a graph is clicked.', link: 'https://neo4j.com/professional-services/', }, - 'node-sidebar': { - name: 'node-sidebar', - label: 'Node Sidebar', - author: 'Neo4j Professional Services', - image: 'https://www.unfe.org/wp-content/uploads/2019/04/SM-placeholder.png', // TODO: Fix placeholder image. - enabled: true, - reducerPrefix: NODE_SIDEBAR_ACTION_PREFIX, - reducerObject: sidebarReducer, - drawerButton: SidebarDrawerButton, - description: - 'The node sidebar allows you to create a customer drawer on the side of the page. This drawer will contain nodes from the graph, which can be inspected, and drilled down into by setting dashboard parameters.', - link: 'https://neo4j.com/professional-services/', - }, - workflows: { - name: 'workflows', - label: 'Cypher Workflows', - author: 'Neo4j Professional Services', - image: 'https://www.unfe.org/wp-content/uploads/2019/04/SM-placeholder.png', // TODO: Fix placeholder image. - enabled: false, - reducerPrefix: WORKFLOWS_ACTION_PREFIX, - reducerObject: workflowReducer, - drawerButton: NeoWorkflowDrawerButton, - description: - 'An extension to create, manage, and run workflows consisting of Cypher queries. Workflows can be used to run ETL flows, complex query chains, or run graph data science workloads.', - link: 'https://neo4j.com/professional-services/', - }, + // 'node-sidebar': { + // name: 'node-sidebar', + // label: 'Node Sidebar', + // author: 'Neo4j Professional Services', + // image: 'https://www.unfe.org/wp-content/uploads/2019/04/SM-placeholder.png', // TODO: Fix placeholder image. + // enabled: true, + // reducerPrefix: NODE_SIDEBAR_ACTION_PREFIX, + // reducerObject: sidebarReducer, + // drawerButton: SidebarDrawerButton, + // description: + // 'The node sidebar allows you to create a customer drawer on the side of the page. This drawer will contain nodes from the graph, which can be inspected, and drilled down into by setting dashboard parameters.', + // link: 'https://neo4j.com/professional-services/', + // }, + // workflows: { + // name: 'workflows', + // label: 'Cypher Workflows', + // author: 'Neo4j Professional Services', + // image: 'https://www.unfe.org/wp-content/uploads/2019/04/SM-placeholder.png', // TODO: Fix placeholder image. + // enabled: false, + // reducerPrefix: WORKFLOWS_ACTION_PREFIX, + // reducerObject: workflowReducer, + // drawerButton: NeoWorkflowDrawerButton, + // description: + // 'An extension to create, manage, and run workflows consisting of Cypher queries. Workflows can be used to run ETL flows, complex query chains, or run graph data science workloads.', + // link: 'https://neo4j.com/professional-services/', + // }, 'query-translator': { name: 'query-translator', label: 'Natural Language Queries', author: 'Neo4j Professional Services', - image: 'https://www.unfe.org/wp-content/uploads/2019/04/SM-placeholder.png', // TODO: Fix placeholder image. + image: 'translator.png', enabled: true, reducerPrefix: QUERY_TRANSLATOR_ACTION_PREFIX, reducerObject: queryTranslatorReducer, drawerButton: QueryTranslatorButton, - description: 'ask queries in natural language (available only in english).', + description: + 'Use natural language to generate Cypher queries in NeoDash. Connect your own LLM through an API, and let NeoDash use your database schema + the report types to generate queries automatically.', link: 'https://neo4j.com/professional-services/', }, }; diff --git a/src/extensions/ExtensionsModal.tsx b/src/extensions/ExtensionsModal.tsx index 546941426..f9f83b0bb 100644 --- a/src/extensions/ExtensionsModal.tsx +++ b/src/extensions/ExtensionsModal.tsx @@ -114,6 +114,7 @@ const NeoExtensionsModal = ({ +

{e.description}


@@ -121,9 +122,9 @@ const NeoExtensionsModal = ({

- +
- + diff --git a/src/extensions/query-translator/component/QueryTranslator.tsx b/src/extensions/query-translator/component/QueryTranslator.tsx index ed630e859..48458d318 100644 --- a/src/extensions/query-translator/component/QueryTranslator.tsx +++ b/src/extensions/query-translator/component/QueryTranslator.tsx @@ -46,7 +46,7 @@ export const QueryTranslatorButton = ({ const button = (
- + setOpen(true)} icon={ @@ -55,7 +55,7 @@ export const QueryTranslatorButton = ({ /> } > - Query Translator + Natural Language Queries
From 846735e6f722212b6654615de5976d90bb62028d Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Thu, 8 Jun 2023 11:58:46 +0200 Subject: [PATCH 09/49] Style tweak --- src/dashboard/drawer/DashboardDrawer.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dashboard/drawer/DashboardDrawer.tsx b/src/dashboard/drawer/DashboardDrawer.tsx index d04c084eb..e27bda65d 100644 --- a/src/dashboard/drawer/DashboardDrawer.tsx +++ b/src/dashboard/drawer/DashboardDrawer.tsx @@ -48,7 +48,7 @@ export const NeoDrawer = ({ * are enabled and present a button (EX: node-sidebar) * @returns JSX element containing all the buttons related to their enabled extensions */ - function renderDrawerExtensionsButton() { + function renderDrawerExtensionsButtons() { const res = ( <> {Object.keys(EXTENSIONS_DRAWER_BUTTONS).map((name) => { @@ -92,6 +92,7 @@ export const NeoDrawer = ({ + {renderDrawerExtensionsButtons()} Learn - {renderDrawerExtensionsButton()} + Date: Thu, 8 Jun 2023 14:33:31 +0200 Subject: [PATCH 10/49] Added dual-language support for card settings content --- src/card/settings/CardSettingsContent.tsx | 54 ++++++++++++++++--- src/extensions/ExtensionConfig.tsx | 23 +++++++- .../component/LanguageToggleSwitch.tsx | 48 +++++++++++++++++ 3 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 src/extensions/query-translator/component/LanguageToggleSwitch.tsx diff --git a/src/card/settings/CardSettingsContent.tsx b/src/card/settings/CardSettingsContent.tsx index f1bbe0114..012fd5147 100644 --- a/src/card/settings/CardSettingsContent.tsx +++ b/src/card/settings/CardSettingsContent.tsx @@ -5,6 +5,11 @@ import { useCallback } from 'react'; import NeoCodeEditorComponent from '../../component/editor/CodeEditorComponent'; import { getReportTypes } from '../../extensions/ExtensionUtils'; import { Dropdown } from '@neo4j-ndl/react'; +import { NeoLanguageToggleSwitch } from '../../extensions/query-translator/component/LanguageToggleSwitch'; +import { + EXTENSIONS_CARD_SETTINGS_COMPONENTS, + getExtensionCardSettingsComponents, +} from '../../extensions/ExtensionConfig'; const NeoCardSettingsContent = ({ query, @@ -18,24 +23,49 @@ const NeoCardSettingsContent = ({ onTypeUpdate, onDatabaseChanged, // When the database related to a report is changed it must be stored in the report state }) => { - // Ensure that we only trigger a text update event after the user has stopped typing. - const [queryText, setQueryText] = React.useState(query); + // Store a generic dictionary of query text variables in a dictionary, where the key is the language. + const [queryTexts, setQueryTexts] = React.useState({ Cypher: query }); const debouncedQueryUpdate = useCallback(debounce(onQueryUpdate, 250), []); // State to manage the current database entry inside the form const [databaseText, setDatabaseText] = React.useState(database); const debouncedDatabaseUpdate = useCallback(debounce(onDatabaseChanged, 250), []); + const [languageName, setLanguageName] = React.useState('Cypher'); + useEffect(() => { // Reset text to the dashboard state when the page gets reorganized. - if (query !== queryText) { - setQueryText(query); + if (query !== queryTexts.Cypher) { + const newQueryTexts = { ...queryTexts }; + newQueryTexts.Cypher = query; + setQueryTexts(newQueryTexts); } }, [query]); + useEffect(() => { + // Reset text to the dashboard state when the page gets reorganized. + if (reportSettings.naturalLanguageQuery !== queryTexts.English) { + const newQueryTexts = { ...queryTexts }; + newQueryTexts.English = reportSettings.naturalLanguageQuery; + setQueryTexts(newQueryTexts); + } + }, [reportSettings.naturalLanguageQuery]); + const reportTypes = getReportTypes(extensions); const SettingsComponent = reportTypes[type] && reportTypes[type].settingsComponent; + function renderExtensionsComponents() { + const res = ( + <> + {Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENTS).map((name) => { + const Component = extensions[name] ? EXTENSIONS_CARD_SETTINGS_COMPONENTS[name] : ''; + return Component ? : <>; + })} + + ); + return res; + } + return ( ) : (
+ {renderExtensionsComponents()} { - debouncedQueryUpdate(value); - setQueryText(value); + if (languageName == 'Cypher') { + debouncedQueryUpdate(value); + } else { + onReportSettingUpdate('naturalLanguageQuery', value); + } + + const newQueryText = { ...queryTexts }; + newQueryText[languageName] = value; + setQueryTexts(newQueryText); }} - placeholder={'Enter Cypher here...'} + placeholder={`Enter ${ languageName } here...`} />
= { enabled: true, reducerPrefix: QUERY_TRANSLATOR_ACTION_PREFIX, reducerObject: queryTranslatorReducer, + cardSettingsComponent: NeoLanguageToggleSwitch, drawerButton: QueryTranslatorButton, description: - 'Use natural language to generate Cypher queries in NeoDash. Connect your own LLM through an API, and let NeoDash use your database schema + the report types to generate queries automatically.', + 'Use natural language to generate Cypher queries in NeoDash. Connect to an LLM through an API, and let NeoDash use your database schema + the report types to generate queries automatically.', link: 'https://neo4j.com/professional-services/', }, }; @@ -135,6 +138,23 @@ function getExtensionDrawerButtons() { return buttons; } +/** + * Gets components to inject inside the card settings content, before the Cypher query box. + */ +export function getExtensionCardSettingsComponents() { + let components = {}; + Object.values(EXTENSIONS).forEach((extension) => { + try { + if (extension.cardSettingsComponent) { + components[extension.name] = extension.cardSettingsComponent; + } + } catch (e) { + console.log(`Something wrong happened while loading the extension components. : ${e}`); + } + }); + return components; +} + /** * At the start of the application, we want to collect programmatically the extensions that need to be added inside the SettingsModal. * @returns @@ -156,3 +176,4 @@ function getExtensionSettingsModal() { export const EXTENSIONS_REDUCERS = getExtensionReducers(); export const EXTENSIONS_DRAWER_BUTTONS = getExtensionDrawerButtons(); export const EXTENSIONS_SETTINGS_MODALS = getExtensionSettingsModal(); +export const EXTENSIONS_CARD_SETTINGS_COMPONENTS = getExtensionCardSettingsComponents(); diff --git a/src/extensions/query-translator/component/LanguageToggleSwitch.tsx b/src/extensions/query-translator/component/LanguageToggleSwitch.tsx new file mode 100644 index 000000000..e79d5691c --- /dev/null +++ b/src/extensions/query-translator/component/LanguageToggleSwitch.tsx @@ -0,0 +1,48 @@ +import ReportIcon from '@mui/icons-material/Report'; +import React from 'react'; +import { connect } from 'react-redux'; +import { ListItem, ListItemIcon, ListItemText } from '@mui/material'; +import { IconButton, Switch } from '@neo4j-ndl/react'; + +// TODO - rename to 'Node Sidebar Extension button' to reflect better the functionality. +export const NeoLanguageToggleSwitch = ({ setLanguageName }) => { + enum Language { + ENGLISH, + CYPHER, + } + + const [language, setLanguage] = React.useState(Language.CYPHER); + + return ( +
+ + + + + + +
Cypher + { + if (language == Language.ENGLISH) { + setLanguage(Language.CYPHER); + setLanguageName('Cypher'); + } else { + setLanguage(Language.ENGLISH); + setLanguageName('English'); + } + }} + className='n-ml-2' + /> +   English
+
+ ); +}; + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(NeoLanguageToggleSwitch); From c9ed53d3c005a479321d9e58728a25fb0e8f6b5b Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Thu, 8 Jun 2023 18:18:49 +0200 Subject: [PATCH 11/49] refactoring some variable names, added reducer for lastMessage --- .../component/ClientSettings.tsx | 17 +++------ .../component/QueryTranslator.tsx | 29 +++++++-------- .../QueryTranslatorSettingsModal.tsx | 10 +++--- .../state/QueryTranslatorActions.ts | 28 ++++++++++----- .../state/QueryTranslatorReducer.ts | 30 +++++++++++----- .../state/QueryTranslatorSelector.ts | 36 ++++++++++++++----- .../state/QueryTranslatorThunks.ts | 29 +++++++++++---- 7 files changed, 111 insertions(+), 68 deletions(-) diff --git a/src/extensions/query-translator/component/ClientSettings.tsx b/src/extensions/query-translator/component/ClientSettings.tsx index e18e8c2ea..cb94f8c2e 100644 --- a/src/extensions/query-translator/component/ClientSettings.tsx +++ b/src/extensions/query-translator/component/ClientSettings.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react'; import { connect } from 'react-redux'; import { debounce, List, ListItem } from '@mui/material'; import { getModelClientObject, getQueryTranslatorDefaultConfig } from '../QueryTranslatorConfig'; -import { getClientSettings } from '../state/QueryTranslatorSelector'; +import { getQueryTranslatorSettings } from '../state/QueryTranslatorSelector'; import NeoSetting from '../../../component/field/Setting'; import { deleteAllMessageHistory, @@ -10,16 +10,7 @@ import { setGlobalModelClient, setModelProvider, } from '../state/QueryTranslatorActions'; -import { - ExpandIcon, - ShrinkIcon, - DragIcon, - QuestionMarkCircleIconOutline, - TrashIconOutline, - DocumentDuplicateIconOutline, - PlayCircleIconSolid, - CheckCircleIconSolid, -} from '@neo4j-ndl/react/icons'; +import { PlayCircleIconSolid, CheckCircleIconSolid } from '@neo4j-ndl/react/icons'; import { Button, IconButton } from '@neo4j-ndl/react'; import { modelClientInitializationThunk } from '../state/QueryTranslatorThunks'; @@ -102,12 +93,12 @@ export const ClientSettings = ({ } } + // Prevent authentication if all required fields are not full (EX: look at checkIfDisabled) const authButton = ( { e.preventDefault(); - console.log('Clicked auth button...'); updateModelProvider(modelProvider); updateClientSettings(settingState); authenticate(setIsAuthenticated); @@ -167,7 +158,7 @@ export const ClientSettings = ({ }; const mapStateToProps = (state) => ({ - settings: getClientSettings(state), + settings: getQueryTranslatorSettings(state), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/src/extensions/query-translator/component/QueryTranslator.tsx b/src/extensions/query-translator/component/QueryTranslator.tsx index 48458d318..a1d081537 100644 --- a/src/extensions/query-translator/component/QueryTranslator.tsx +++ b/src/extensions/query-translator/component/QueryTranslator.tsx @@ -1,14 +1,13 @@ import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { deleteAllMessageHistory, deleteMessageHistory, setGlobalModelClient } from '../state/QueryTranslatorActions'; -import { getApiKey, getClientSettings, getModelProvider } from '../state/QueryTranslatorSelector'; -import { Button, SideNavigationItem } from '@neo4j-ndl/react'; -import TranslateIcon from '@mui/icons-material/Translate'; +import { getApiKey, getQueryTranslatorSettings, getModelProvider } from '../state/QueryTranslatorSelector'; +import { SideNavigationItem } from '@neo4j-ndl/react'; import QueryTranslatorSettingsModal from './QueryTranslatorSettingsModal'; import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; import { Tooltip } from '@mui/material'; -import { ChatBubbleOvalLeftIconOutline, LanguageIconSolid } from '@neo4j-ndl/react/icons'; +import { LanguageIconSolid } from '@neo4j-ndl/react/icons'; /** * //TODO: * 1. The query translator should handle all the requests from the cards to the client @@ -20,10 +19,8 @@ export const QueryTranslatorButton = ({ apiKey, modelProvider, clientSettings, - _deleteAllMessageHistory, setGlobalModelClient, queryTranslation, - _deleteMessageHistory, }) => { const [open, setOpen] = React.useState(false); const { driver } = useContext(Neo4jContext); @@ -37,10 +34,14 @@ export const QueryTranslatorButton = ({ // When changing provider, i will reset all the messages to prevent strage results // TODO: remove this effect is just for testing useEffect(() => { - if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { - [].forEach((cardId) => { - queryTranslation(0, cardId, 'give me any query', 'Table', driver); - }); + try { + if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { + ['d7e3f139-9c03-445a-846e-c6b6eb7787b2'].forEach((cardId) => { + queryTranslation(0, cardId, 'give me any query', 'Table', driver); + }); + } + } catch (e) { + console.log(e); } }, [modelProvider, apiKey, clientSettings]); @@ -74,22 +75,16 @@ export const QueryTranslatorButton = ({ const mapStateToProps = (state) => ({ apiKey: getApiKey(state), modelProvider: getModelProvider(state), - clientSettings: getClientSettings(state), + clientSettings: getQueryTranslatorSettings(state), }); const mapDispatchToProps = (dispatch) => ({ - deleteAllMessageHistory: () => { - dispatch(deleteAllMessageHistory()); - }, setGlobalModelClient: (modelClient) => { dispatch(setGlobalModelClient(modelClient)); }, queryTranslation: (pagenumber, cardIndex, message, reportType, driver) => { dispatch(queryTranslationThunk(pagenumber, cardIndex, message, reportType, driver)); }, - deleteMessageHistory: (pagenumber, cardIndex) => { - dispatch(deleteMessageHistory(pagenumber, cardIndex)); - }, }); export default connect(mapStateToProps, mapDispatchToProps)(QueryTranslatorButton); diff --git a/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx b/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx index 283405baa..73639657b 100644 --- a/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx +++ b/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx @@ -1,14 +1,12 @@ -import { Badge, IconButton } from '@mui/material'; -import React, { useEffect } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import { setClientSettings, setModelProvider } from '../state/QueryTranslatorActions'; -import { getApiKey, getClientSettings, getModelProvider } from '../state/QueryTranslatorSelector'; -import SaveIcon from '@mui/icons-material/Save'; +import { getQueryTranslatorSettings, getModelProvider } from '../state/QueryTranslatorSelector'; import { SELECTION_TYPES } from '../../../config/CardConfig'; import NeoSetting from '../../../component/field/Setting'; import { QUERY_TRANSLATOR_CONFIG } from '../QueryTranslatorConfig'; import ClientSettings from './ClientSettings'; -import { Button, Dialog } from '@neo4j-ndl/react'; +import { Dialog } from '@neo4j-ndl/react'; export const QueryTranslatorSettingsModal = ({ open, @@ -59,7 +57,7 @@ export const QueryTranslatorSettingsModal = ({ ); }; const mapStateToProps = (state) => ({ - clientSettings: getClientSettings(state), + clientSettings: getQueryTranslatorSettings(state), modelProvider: getModelProvider(state), }); diff --git a/src/extensions/query-translator/state/QueryTranslatorActions.ts b/src/extensions/query-translator/state/QueryTranslatorActions.ts index 1ea8716b1..a8b86f637 100644 --- a/src/extensions/query-translator/state/QueryTranslatorActions.ts +++ b/src/extensions/query-translator/state/QueryTranslatorActions.ts @@ -1,6 +1,5 @@ -import { ChatCompletionRequestMessage } from 'openai'; - export const QUERY_TRANSLATOR_ACTION_PREFIX = 'DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/'; +export const QUERY_TRANSLATOR_TEMPORARY_ACTION_PREFIX = 'DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/TEMPORARY'; export const SET_MODEL_PROVIDER = `${QUERY_TRANSLATOR_ACTION_PREFIX}SET_MODEL_PROVIDER`; export const setModelProvider = (modelProvider) => ({ @@ -24,19 +23,32 @@ export const UPDATE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_E /** * Action to add a new message to the history * @param history History of messages between a card and the model - * @param pageIndex Index of the page related to the card - * @param cardIndex Index of the card inside the page + * @param pagenumber Index of the page related to the card + * @param cardId Id of the card inside the page * @returns */ -export const updateMessageHistory = (cardHistory: any[], pageIndex: number, cardIndex: number) => ({ +export const updateMessageHistory = (cardHistory: any[], pagenumber: number, cardId: string) => ({ type: UPDATE_MESSAGE_HISTORY, - payload: { cardHistory, pageIndex, cardIndex }, + payload: { cardHistory, pagenumber, cardId }, +}); + +export const UPDATE_LAST_MESSAGE = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_LAST_MESSAGE`; +/** + * Action to store the last message sent between a user and the query translator + * @param message History of messages between a card and the model + * @param pagenumber Index of the page related to the card + * @param cardId Id of the card inside the page + * @returns + */ +export const updateLastMessage = (message: string, pagenumber: number, cardId: string) => ({ + type: UPDATE_LAST_MESSAGE, + payload: { message, pagenumber, cardId }, }); export const DELETE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}DELETE_MESSAGE_HISTORY`; -export const deleteMessageHistory = (pageIndex: number, cardIndex: number) => ({ +export const deleteMessageHistory = (pagenumber: number, cardId: string) => ({ type: DELETE_MESSAGE_HISTORY, - payload: { pageIndex, cardIndex }, + payload: { pagenumber, cardId }, }); export const DELETE_ALL_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}DELETE_ALL_MESSAGE_HISTORY`; diff --git a/src/extensions/query-translator/state/QueryTranslatorReducer.ts b/src/extensions/query-translator/state/QueryTranslatorReducer.ts index bb5b53734..9137ee6e4 100644 --- a/src/extensions/query-translator/state/QueryTranslatorReducer.ts +++ b/src/extensions/query-translator/state/QueryTranslatorReducer.ts @@ -9,6 +9,7 @@ import { DELETE_ALL_MESSAGE_HISTORY, SET_GLOBAL_MODEL_CLIENT, SET_CLIENT_SETTINGS, + UPDATE_LAST_MESSAGE, } from './QueryTranslatorActions'; export const INITIAL_EXTENSION_STATE = { @@ -16,6 +17,7 @@ export const INITIAL_EXTENSION_STATE = { history: {}, // Objects that keeps, for every card, their history (to move to session store) modelClient: '', // Object to connect with the model API (to move to session store) settings: {}, // Settings needed by the client to operate + lastMessages: {}, }; const update = (state, mutations) => Object.assign({}, state, mutations); @@ -41,23 +43,35 @@ export const queryTranslatorReducer = (state = INITIAL_EXTENSION_STATE, action: return state; } case UPDATE_MESSAGE_HISTORY: { - const { cardHistory, pageIndex, cardIndex } = payload; + const { cardHistory, pagenumber, cardId } = payload; let newHistory = { ...state.history }; - if (newHistory && !newHistory[pageIndex]) { - newHistory[pageIndex] = {}; - newHistory[pageIndex][cardIndex] = cardHistory; + if (newHistory && !newHistory[pagenumber]) { + newHistory[pagenumber] = {}; + newHistory[pagenumber][cardId] = cardHistory; } else { - newHistory[pageIndex][cardIndex] = cardHistory; + newHistory[pagenumber][cardId] = cardHistory; } state = update(state, { history: newHistory }); return state; } + case UPDATE_LAST_MESSAGE: { + const { message, pagenumber, cardId } = payload; + let newLastMessages = { ...state.lastMessages }; + if (newLastMessages && !newLastMessages[pagenumber]) { + newLastMessages[pagenumber] = {}; + newLastMessages[pagenumber][cardId] = message; + } else { + newLastMessages[pagenumber][cardId] = message; + } + state = update(state, { lastMessages: newLastMessages }); + return state; + } case DELETE_MESSAGE_HISTORY: { - const { pageIndex, cardIndex } = payload; + const { pagenumber, cardId } = payload; let newHistory = { ...state.history }; - if (newHistory && newHistory[pageIndex] && newHistory[pageIndex][cardIndex]) { - delete newHistory[pageIndex][cardIndex]; + if (newHistory && newHistory[pagenumber] && newHistory[pagenumber][cardId]) { + delete newHistory[pagenumber][cardId]; state = update(state, { history: newHistory }); } return state; diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts index 544f71c94..825f92c80 100644 --- a/src/extensions/query-translator/state/QueryTranslatorSelector.ts +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -9,6 +9,18 @@ export const getHistory = (state: any) => { return history != undefined && history ? history : {}; }; +export const getLastMessages = (state: any) => { + let lastMessages = + checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].lastMessages; + return lastMessages != undefined && lastMessages ? lastMessages : {}; +}; + +export const getQueryTranslatorSettings = (state: any) => { + let clientSettings = + checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].settings; + return clientSettings != undefined && clientSettings ? clientSettings : {}; +}; + export const getModelProvider = (state: any) => { let modelProvider = checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].modelProvider; @@ -33,24 +45,30 @@ export const getModelClient = (state: any) => { * The extension keeps, during one session, the history of messages between a user and a model. * The history is kept only during the session, so every refresh it is deleted. * @param state Current state of the session - * @param pageIndex Index of the page where the card lives + * @param pagenumber Index of the page where the card lives * @param cardIndex Index that identifies the card inside the page * @returns history of messages between the user and the model within the context of that card (defaulted to []) */ -export const getHistoryPerCard = (state: any, pageIndex, cardIndex) => { +export const getHistoryPerCard = (state: any, pagenumber, cardIndex) => { let history = getHistory(state); - let cardHistory = history[pageIndex] && history[pageIndex][cardIndex]; + let cardHistory = history[pagenumber] && history[pagenumber][cardIndex]; return cardHistory != undefined && cardHistory ? cardHistory : []; }; -export const getClientSettings = (state: any) => { - let clientSettings = - checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].settings; - return clientSettings != undefined && clientSettings ? clientSettings : {}; +/** + * We persist the last message sent from the user to the model. + * @param state State of the application + * @param pagenumber Number of the page where the card lives + * @param cardIndex Unique identifier of the card + * @returns + */ +export const getLastMessage = (state: any, pagenumber, cardIndex) => { + let messages = getLastMessages(state); + let lastMessage = messages[pagenumber] && messages[pagenumber][cardIndex]; + return lastMessage != undefined && lastMessage ? lastMessage : []; }; export const getApiKey = (state: any) => { - let settings = getClientSettings(state); - + let settings = getQueryTranslatorSettings(state); return settings.apiKey != undefined && settings.apiKey ? settings.apiKey : ''; }; diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index ce410f931..97bc7222e 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -2,8 +2,13 @@ import { updateReportQueryThunk } from '../../../card/CardThunks'; import { getDatabase } from '../../../settings/SettingsSelectors'; import { ModelClient } from '../clients/ModelClient'; import { getModelClientObject } from '../QueryTranslatorConfig'; -import { setGlobalModelClient, updateMessageHistory } from './QueryTranslatorActions'; -import { getClientSettings, getHistoryPerCard, getModelClient, getModelProvider } from './QueryTranslatorSelector'; +import { setGlobalModelClient, updateLastMessage, updateMessageHistory } from './QueryTranslatorActions'; +import { + getQueryTranslatorSettings, + getHistoryPerCard, + getModelClient, + getModelProvider, +} from './QueryTranslatorSelector'; const consoleLogAsync = async (message: string, other?: any) => { await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other)); @@ -25,7 +30,7 @@ export const modelClientInitializationThunk = // Fetching the client properties from the state let modelProvider = getModelProvider(state); - let settings = getClientSettings(state); + let settings = getQueryTranslatorSettings(state); // console.log(modelProvider, settings) if (modelProvider && settings) { // Getting the correct ModelClient object @@ -71,12 +76,22 @@ const getModelClientThunk = () => async (dispatch: any, getState: any) => { * @param driver Neo4j Driver used to fetch the schema from the database */ export const queryTranslationThunk = - (pagenumber, cardId, message, reportType, driver) => async (dispatch: any, getState: any) => { + ( + pagenumber, + cardId, + message, + reportType, + driver, + setErrorMessage = (e) => { + console.log(e); + } + ) => + async (dispatch: any, getState: any) => { let query; try { const state = getState(); const database = getDatabase(state, pagenumber, cardId); - + dispatch(updateLastMessage(message, pagenumber, cardId)); // Retrieving the model client from the state let client: ModelClient = await dispatch(getModelClientThunk()); if (client) { @@ -89,7 +104,7 @@ export const queryTranslationThunk = let translationRes = await client.queryTranslation(message, messageHistory, database, reportType); query = translationRes[0]; let newHistory = translationRes[1]; - + await consoleLogAsync('eho', newHistory); // The history will be updated only if the length is different (otherwise, it's the same history) if (messageHistory.length < newHistory.length && query) { dispatch(updateMessageHistory(newHistory, pagenumber, cardId)); @@ -103,6 +118,6 @@ export const queryTranslationThunk = `Something wrong happened while calling the model client for the card number ${cardId} inside the page ${pagenumber}: \n`, { e } ); - throw e; + setErrorMessage(e); } }; From bffd9bd22b09f207c1dfeec9ed797226a6fc1592 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Thu, 8 Jun 2023 18:19:03 +0200 Subject: [PATCH 12/49] refactoring some variable names, added reducer for lastMessage --- src/extensions/query-translator/state/QueryTranslatorThunks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index 97bc7222e..268771e99 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -104,7 +104,6 @@ export const queryTranslationThunk = let translationRes = await client.queryTranslation(message, messageHistory, database, reportType); query = translationRes[0]; let newHistory = translationRes[1]; - await consoleLogAsync('eho', newHistory); // The history will be updated only if the length is different (otherwise, it's the same history) if (messageHistory.length < newHistory.length && query) { dispatch(updateMessageHistory(newHistory, pagenumber, cardId)); From 8f2279b7b68fef99feb9993a3fc032f8ad7a317e Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Fri, 9 Jun 2023 19:13:35 +0200 Subject: [PATCH 13/49] plugged sessionStorage, it will be flushed after a new connection --- src/application/Application.tsx | 2 + src/extensions/ExtensionsReducer.ts | 21 -------- .../component/QueryTranslator.tsx | 2 +- .../state/QueryTranslatorActions.ts | 50 +++++++++--------- .../state/QueryTranslatorReducer.ts | 29 ----------- .../state/QueryTranslatorSelector.ts | 15 ++++-- .../state/QueryTranslatorThunks.ts | 1 + src/sessionState/SessionStorageActions.ts | 25 +++++++++ src/sessionState/SessionStorageReducer.ts | 52 +++++++++++++++++++ src/sessionState/SessionStorageSelectors.ts | 6 +++ src/store.ts | 2 + 11 files changed, 125 insertions(+), 80 deletions(-) delete mode 100644 src/extensions/ExtensionsReducer.ts create mode 100644 src/sessionState/SessionStorageActions.ts create mode 100644 src/sessionState/SessionStorageReducer.ts create mode 100644 src/sessionState/SessionStorageSelectors.ts diff --git a/src/application/Application.tsx b/src/application/Application.tsx index abb3c6a95..6ba927ac3 100644 --- a/src/application/Application.tsx +++ b/src/application/Application.tsx @@ -49,6 +49,7 @@ import NeoReportHelpModal from '../modal/ReportHelpModal'; import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { ThemeProvider } from '@mui/material/styles'; import lightTheme from '../component/theme/Themes'; +import { resetSessionStorage } from '../sessionState/SessionStorageActions'; /** * This is the main application component for NeoDash. @@ -175,6 +176,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => ({ createConnection: (protocol, url, port, database, username, password) => { dispatch(setConnected(false)); + dispatch(resetSessionStorage()); dispatch(createConnectionThunk(protocol, url, port, database, username, password)); }, createConnectionFromDesktopIntegration: () => { diff --git a/src/extensions/ExtensionsReducer.ts b/src/extensions/ExtensionsReducer.ts deleted file mode 100644 index de715f429..000000000 --- a/src/extensions/ExtensionsReducer.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Reducers define changes to the application state when a given action is performed. - */ - -export const initialState = {}; - -const update = (state, mutations) => Object.assign({}, state, mutations); - -export const extensionsReducer = (state = initialState, action: { type: any; payload: any }) => { - const { type, payload } = action; - - if (!action.type.startsWith('DASHBOARD/EXTENSIONS')) { - return state; - } - - switch (type) { - default: { - return state; - } - } -}; diff --git a/src/extensions/query-translator/component/QueryTranslator.tsx b/src/extensions/query-translator/component/QueryTranslator.tsx index a1d081537..ea3fa7050 100644 --- a/src/extensions/query-translator/component/QueryTranslator.tsx +++ b/src/extensions/query-translator/component/QueryTranslator.tsx @@ -36,7 +36,7 @@ export const QueryTranslatorButton = ({ useEffect(() => { try { if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { - ['d7e3f139-9c03-445a-846e-c6b6eb7787b2'].forEach((cardId) => { + ['eb204bd5-7dd1-4cb4-9a34-111976db0b0e', '4d017b2f-261a-4d21-a187-ab8cce6ec31d'].forEach((cardId) => { queryTranslation(0, cardId, 'give me any query', 'Table', driver); }); } diff --git a/src/extensions/query-translator/state/QueryTranslatorActions.ts b/src/extensions/query-translator/state/QueryTranslatorActions.ts index a8b86f637..5d1b2d377 100644 --- a/src/extensions/query-translator/state/QueryTranslatorActions.ts +++ b/src/extensions/query-translator/state/QueryTranslatorActions.ts @@ -1,5 +1,16 @@ +import { + deleteAllKeysInSessionStorageWithPrefix, + deleteSessionStorageValue, + DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE, + DELETE_VALUE_SESSION_STORAGE, + SESSION_STORAGE_PREFIX, + setSessionStorageValue, + STORE_VALUE_SESSION_STORAGE, +} from '../../../sessionState/SessionStorageActions'; +import { getSessionStorageHistoryKey, QUERY_TRANSLATOR_HISTORY_PREFIX } from './QueryTranslatorSelector'; + export const QUERY_TRANSLATOR_ACTION_PREFIX = 'DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/'; -export const QUERY_TRANSLATOR_TEMPORARY_ACTION_PREFIX = 'DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/TEMPORARY'; +export const QUERY_TRANSLATOR_SESSION_STORAGE_ACTION_PREFIX = `DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/${SESSION_STORAGE_PREFIX}/`; export const SET_MODEL_PROVIDER = `${QUERY_TRANSLATOR_ACTION_PREFIX}SET_MODEL_PROVIDER`; export const setModelProvider = (modelProvider) => ({ @@ -19,19 +30,6 @@ export const setGlobalModelClient = (modelClient) => ({ payload: { modelClient }, }); -export const UPDATE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_EXTENSION_TITLE`; -/** - * Action to add a new message to the history - * @param history History of messages between a card and the model - * @param pagenumber Index of the page related to the card - * @param cardId Id of the card inside the page - * @returns - */ -export const updateMessageHistory = (cardHistory: any[], pagenumber: number, cardId: string) => ({ - type: UPDATE_MESSAGE_HISTORY, - payload: { cardHistory, pagenumber, cardId }, -}); - export const UPDATE_LAST_MESSAGE = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_LAST_MESSAGE`; /** * Action to store the last message sent between a user and the query translator @@ -45,14 +43,18 @@ export const updateLastMessage = (message: string, pagenumber: number, cardId: s payload: { message, pagenumber, cardId }, }); -export const DELETE_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}DELETE_MESSAGE_HISTORY`; -export const deleteMessageHistory = (pagenumber: number, cardId: string) => ({ - type: DELETE_MESSAGE_HISTORY, - payload: { pagenumber, cardId }, -}); +/** + * Action to add a new message to the history + * @param history History of messages between a card and the model + * @param pagenumber Index of the page related to the card + * @param cardId Id of the card inside the page + * @returns + */ -export const DELETE_ALL_MESSAGE_HISTORY = `${QUERY_TRANSLATOR_ACTION_PREFIX}DELETE_ALL_MESSAGE_HISTORY`; -export const deleteAllMessageHistory = () => ({ - type: DELETE_ALL_MESSAGE_HISTORY, - payload: {}, -}); +export const updateMessageHistory = (cardHistory: any[], pagenumber: number, cardId: string) => + setSessionStorageValue(getSessionStorageHistoryKey(pagenumber, cardId), cardHistory); + +export const deleteMessageHistory = (pagenumber: number, cardId: string) => + deleteSessionStorageValue(getSessionStorageHistoryKey(pagenumber, cardId)); + +export const deleteAllMessageHistory = () => deleteAllKeysInSessionStorageWithPrefix(QUERY_TRANSLATOR_HISTORY_PREFIX); diff --git a/src/extensions/query-translator/state/QueryTranslatorReducer.ts b/src/extensions/query-translator/state/QueryTranslatorReducer.ts index 9137ee6e4..77f789a1c 100644 --- a/src/extensions/query-translator/state/QueryTranslatorReducer.ts +++ b/src/extensions/query-translator/state/QueryTranslatorReducer.ts @@ -3,10 +3,7 @@ */ import { - UPDATE_MESSAGE_HISTORY, - DELETE_MESSAGE_HISTORY, SET_MODEL_PROVIDER, - DELETE_ALL_MESSAGE_HISTORY, SET_GLOBAL_MODEL_CLIENT, SET_CLIENT_SETTINGS, UPDATE_LAST_MESSAGE, @@ -42,19 +39,6 @@ export const queryTranslatorReducer = (state = INITIAL_EXTENSION_STATE, action: state = update(state, { modelClient: modelClient }); return state; } - case UPDATE_MESSAGE_HISTORY: { - const { cardHistory, pagenumber, cardId } = payload; - let newHistory = { ...state.history }; - - if (newHistory && !newHistory[pagenumber]) { - newHistory[pagenumber] = {}; - newHistory[pagenumber][cardId] = cardHistory; - } else { - newHistory[pagenumber][cardId] = cardHistory; - } - state = update(state, { history: newHistory }); - return state; - } case UPDATE_LAST_MESSAGE: { const { message, pagenumber, cardId } = payload; let newLastMessages = { ...state.lastMessages }; @@ -67,19 +51,6 @@ export const queryTranslatorReducer = (state = INITIAL_EXTENSION_STATE, action: state = update(state, { lastMessages: newLastMessages }); return state; } - case DELETE_MESSAGE_HISTORY: { - const { pagenumber, cardId } = payload; - let newHistory = { ...state.history }; - if (newHistory && newHistory[pagenumber] && newHistory[pagenumber][cardId]) { - delete newHistory[pagenumber][cardId]; - state = update(state, { history: newHistory }); - } - return state; - } - case DELETE_ALL_MESSAGE_HISTORY: { - state = update(state, { history: {} }); - return state; - } default: { return state; } diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts index 825f92c80..f827df2a4 100644 --- a/src/extensions/query-translator/state/QueryTranslatorSelector.ts +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -1,5 +1,10 @@ -export const QUERY_TRANSLATOR_EXTENSION_NAME = 'query-translator'; +import { getSessionStorageValue } from '../../../sessionState/SessionStorageSelectors'; +export const QUERY_TRANSLATOR_HISTORY_PREFIX = 'query_translator_history__'; +export const QUERY_TRANSLATOR_EXTENSION_NAME = 'query-translator'; +export const getSessionStorageHistoryKey = (pagenumber, cardIndex) => { + return `${QUERY_TRANSLATOR_HISTORY_PREFIX}__${pagenumber}__${cardIndex}`; +}; const checkExtensionConfig = (state: any) => { return state.dashboard.extensions && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME]; }; @@ -46,12 +51,12 @@ export const getModelClient = (state: any) => { * The history is kept only during the session, so every refresh it is deleted. * @param state Current state of the session * @param pagenumber Index of the page where the card lives - * @param cardIndex Index that identifies the card inside the page + * @param cardId Index that identifies the card inside the page * @returns history of messages between the user and the model within the context of that card (defaulted to []) */ -export const getHistoryPerCard = (state: any, pagenumber, cardIndex) => { - let history = getHistory(state); - let cardHistory = history[pagenumber] && history[pagenumber][cardIndex]; +export const getHistoryPerCard = (state: any, pagenumber, cardId) => { + let tmpKey = getSessionStorageHistoryKey(pagenumber, cardId); + let cardHistory = getSessionStorageValue(state, tmpKey); return cardHistory != undefined && cardHistory ? cardHistory : []; }; diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index 268771e99..a2fe5b952 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -104,6 +104,7 @@ export const queryTranslationThunk = let translationRes = await client.queryTranslation(message, messageHistory, database, reportType); query = translationRes[0]; let newHistory = translationRes[1]; + await consoleLogAsync('apwmda0owj', newHistory); // The history will be updated only if the length is different (otherwise, it's the same history) if (messageHistory.length < newHistory.length && query) { dispatch(updateMessageHistory(newHistory, pagenumber, cardId)); diff --git a/src/sessionState/SessionStorageActions.ts b/src/sessionState/SessionStorageActions.ts new file mode 100644 index 000000000..b4273e73e --- /dev/null +++ b/src/sessionState/SessionStorageActions.ts @@ -0,0 +1,25 @@ +export const SESSION_STORAGE_PREFIX = 'NEODASH_SESSION_STORAGE'; + +export const RESET_STATE = `${SESSION_STORAGE_PREFIX}/RESET_STATE`; +export const resetSessionStorage = () => ({ + type: RESET_STATE, + payload: {}, +}); + +export const STORE_VALUE_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/STORE_VALUE`; +export const setSessionStorageValue = (key, value) => ({ + type: STORE_VALUE_SESSION_STORAGE, + payload: { key, value }, +}); + +export const DELETE_VALUE_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/DELETE_VALUE`; +export const deleteSessionStorageValue = (key) => ({ + type: DELETE_VALUE_SESSION_STORAGE, + payload: { key }, +}); + +export const DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/DELETE_ALL_KEYS_WITH_PREFIX`; +export const deleteAllKeysInSessionStorageWithPrefix = (prefix) => ({ + type: DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE, + payload: { prefix }, +}); diff --git a/src/sessionState/SessionStorageReducer.ts b/src/sessionState/SessionStorageReducer.ts new file mode 100644 index 000000000..6283859f3 --- /dev/null +++ b/src/sessionState/SessionStorageReducer.ts @@ -0,0 +1,52 @@ +/** + * Reducers define changes to the application state when a given action + */ + +import { + DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE, + DELETE_VALUE_SESSION_STORAGE, + RESET_STATE, + STORE_VALUE_SESSION_STORAGE, +} from './SessionStorageActions'; + +export const initialState = {}; + +const update = (state, mutations) => Object.assign({}, state, mutations); + +export const sessionStorageReducer = (state = initialState, action: { type: any; payload: any }) => { + const { type, payload } = action; + + switch (type) { + case RESET_STATE: { + return {}; + } + case STORE_VALUE_SESSION_STORAGE: { + const { key, value } = payload; + console.log(payload); + let newValue = {}; + newValue[key] = value; + return update(state, newValue); + } + + case DELETE_VALUE_SESSION_STORAGE: { + const { key } = payload; + let newState = { ...state }; + delete newState[key]; + return newState; + } + case DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE: { + const { prefix } = payload; + let newState = { ...state }; + // Deleting all the values that elements that present that + Object.keys(newState).map((key) => { + if (key.startsWith(prefix)) { + delete newState[key]; + } + }); + return newState; + } + default: { + return state; + } + } +}; diff --git a/src/sessionState/SessionStorageSelectors.ts b/src/sessionState/SessionStorageSelectors.ts new file mode 100644 index 000000000..951a4d76b --- /dev/null +++ b/src/sessionState/SessionStorageSelectors.ts @@ -0,0 +1,6 @@ +const getSessionStorage = (state: any) => state.sessionStorage; + +export const getSessionStorageValue = (state: any, key: any) => { + const sessionStorage = getSessionStorage(state); + return sessionStorage[key] ? sessionStorage[key] : undefined; +}; diff --git a/src/store.ts b/src/store.ts index 4aeb976e4..06305ecbe 100644 --- a/src/store.ts +++ b/src/store.ts @@ -6,6 +6,7 @@ import thunk from 'redux-thunk'; import { composeWithDevTools } from '@redux-devtools/extension'; import { dashboardReducer } from './dashboard/DashboardReducer'; import { applicationReducer } from './application/ApplicationReducer'; +import { sessionStorageReducer } from './sessionState/SessionStorageReducer'; /** * Set up the store (browser cache), as well as the reducers that can update application state. @@ -19,6 +20,7 @@ const persistConfig = { const reducers = { dashboard: dashboardReducer, application: applicationReducer, + sessionStorage: sessionStorageReducer, }; const rootReducer = combineReducers(reducers); From 95d79d67690fa6481096ec63d7b625d02708e30d Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Sun, 11 Jun 2023 17:55:21 +0200 Subject: [PATCH 14/49] refactoring folder name, added comments --- src/application/Application.tsx | 2 +- .../state/QueryTranslatorActions.ts | 5 +---- .../state/QueryTranslatorSelector.ts | 10 +++++----- .../SessionStorageActions.ts | 13 +++++++++++++ .../SessionStorageReducer.ts | 1 + .../SessionStorageSelectors.ts | 0 src/store.ts | 2 +- 7 files changed, 22 insertions(+), 11 deletions(-) rename src/{sessionState => sessionStorage}/SessionStorageActions.ts (68%) rename src/{sessionState => sessionStorage}/SessionStorageReducer.ts (98%) rename src/{sessionState => sessionStorage}/SessionStorageSelectors.ts (100%) diff --git a/src/application/Application.tsx b/src/application/Application.tsx index 6ba927ac3..f7d4226f3 100644 --- a/src/application/Application.tsx +++ b/src/application/Application.tsx @@ -49,7 +49,7 @@ import NeoReportHelpModal from '../modal/ReportHelpModal'; import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { ThemeProvider } from '@mui/material/styles'; import lightTheme from '../component/theme/Themes'; -import { resetSessionStorage } from '../sessionState/SessionStorageActions'; +import { resetSessionStorage } from '../sessionStorage/SessionStorageActions'; /** * This is the main application component for NeoDash. diff --git a/src/extensions/query-translator/state/QueryTranslatorActions.ts b/src/extensions/query-translator/state/QueryTranslatorActions.ts index 5d1b2d377..ca4f7374b 100644 --- a/src/extensions/query-translator/state/QueryTranslatorActions.ts +++ b/src/extensions/query-translator/state/QueryTranslatorActions.ts @@ -1,12 +1,9 @@ import { deleteAllKeysInSessionStorageWithPrefix, deleteSessionStorageValue, - DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE, - DELETE_VALUE_SESSION_STORAGE, SESSION_STORAGE_PREFIX, setSessionStorageValue, - STORE_VALUE_SESSION_STORAGE, -} from '../../../sessionState/SessionStorageActions'; +} from '../../../sessionStorage/SessionStorageActions'; import { getSessionStorageHistoryKey, QUERY_TRANSLATOR_HISTORY_PREFIX } from './QueryTranslatorSelector'; export const QUERY_TRANSLATOR_ACTION_PREFIX = 'DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/'; diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts index f827df2a4..8e74145fb 100644 --- a/src/extensions/query-translator/state/QueryTranslatorSelector.ts +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -1,9 +1,9 @@ -import { getSessionStorageValue } from '../../../sessionState/SessionStorageSelectors'; +import { getSessionStorageValue } from '../../../sessionStorage/SessionStorageSelectors'; export const QUERY_TRANSLATOR_HISTORY_PREFIX = 'query_translator_history__'; export const QUERY_TRANSLATOR_EXTENSION_NAME = 'query-translator'; -export const getSessionStorageHistoryKey = (pagenumber, cardIndex) => { - return `${QUERY_TRANSLATOR_HISTORY_PREFIX}__${pagenumber}__${cardIndex}`; +export const getSessionStorageHistoryKey = (pagenumber, cardId) => { + return `${QUERY_TRANSLATOR_HISTORY_PREFIX}__${pagenumber}__${cardId}`; }; const checkExtensionConfig = (state: any) => { return state.dashboard.extensions && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME]; @@ -55,8 +55,8 @@ export const getModelClient = (state: any) => { * @returns history of messages between the user and the model within the context of that card (defaulted to []) */ export const getHistoryPerCard = (state: any, pagenumber, cardId) => { - let tmpKey = getSessionStorageHistoryKey(pagenumber, cardId); - let cardHistory = getSessionStorageValue(state, tmpKey); + let sessionStorageKey = getSessionStorageHistoryKey(pagenumber, cardId); + let cardHistory = getSessionStorageValue(state, sessionStorageKey); return cardHistory != undefined && cardHistory ? cardHistory : []; }; diff --git a/src/sessionState/SessionStorageActions.ts b/src/sessionStorage/SessionStorageActions.ts similarity index 68% rename from src/sessionState/SessionStorageActions.ts rename to src/sessionStorage/SessionStorageActions.ts index b4273e73e..1d300939b 100644 --- a/src/sessionState/SessionStorageActions.ts +++ b/src/sessionStorage/SessionStorageActions.ts @@ -7,17 +7,30 @@ export const resetSessionStorage = () => ({ }); export const STORE_VALUE_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/STORE_VALUE`; +/** + * Sets a value with the key passed in input + * @param key Key to use to access the SessionStorage + * @param value Value to add inside the SessionStorage + */ export const setSessionStorageValue = (key, value) => ({ type: STORE_VALUE_SESSION_STORAGE, payload: { key, value }, }); +/** + * Deletes a key from the SessionStorage + * @param key Key to use to access the SessionStorage + */ export const DELETE_VALUE_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/DELETE_VALUE`; export const deleteSessionStorageValue = (key) => ({ type: DELETE_VALUE_SESSION_STORAGE, payload: { key }, }); +/** + * Deletes all the keys that start with the prefix passed in input + * @param prefix Prefix used to match the keys inside the SessionStorage + */ export const DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/DELETE_ALL_KEYS_WITH_PREFIX`; export const deleteAllKeysInSessionStorageWithPrefix = (prefix) => ({ type: DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE, diff --git a/src/sessionState/SessionStorageReducer.ts b/src/sessionStorage/SessionStorageReducer.ts similarity index 98% rename from src/sessionState/SessionStorageReducer.ts rename to src/sessionStorage/SessionStorageReducer.ts index 6283859f3..85aa855f6 100644 --- a/src/sessionState/SessionStorageReducer.ts +++ b/src/sessionStorage/SessionStorageReducer.ts @@ -31,6 +31,7 @@ export const sessionStorageReducer = (state = initialState, action: { type: any; case DELETE_VALUE_SESSION_STORAGE: { const { key } = payload; let newState = { ...state }; + console.log(key); delete newState[key]; return newState; } diff --git a/src/sessionState/SessionStorageSelectors.ts b/src/sessionStorage/SessionStorageSelectors.ts similarity index 100% rename from src/sessionState/SessionStorageSelectors.ts rename to src/sessionStorage/SessionStorageSelectors.ts diff --git a/src/store.ts b/src/store.ts index 06305ecbe..5ae34a0e2 100644 --- a/src/store.ts +++ b/src/store.ts @@ -6,7 +6,7 @@ import thunk from 'redux-thunk'; import { composeWithDevTools } from '@redux-devtools/extension'; import { dashboardReducer } from './dashboard/DashboardReducer'; import { applicationReducer } from './application/ApplicationReducer'; -import { sessionStorageReducer } from './sessionState/SessionStorageReducer'; +import { sessionStorageReducer } from './sessionStorage/SessionStorageReducer'; /** * Set up the store (browser cache), as well as the reducers that can update application state. From 838ac69728bac1728e508b03fa7b9dd5c9da4db1 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Sun, 11 Jun 2023 18:01:01 +0200 Subject: [PATCH 15/49] refactoring folder name, added comments --- .../query-translator/state/QueryTranslatorSelector.ts | 6 +++++- src/sessionStorage/README.adoc | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/sessionStorage/README.adoc diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts index 8e74145fb..c9e0305e6 100644 --- a/src/extensions/query-translator/state/QueryTranslatorSelector.ts +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -1,7 +1,11 @@ import { getSessionStorageValue } from '../../../sessionStorage/SessionStorageSelectors'; -export const QUERY_TRANSLATOR_HISTORY_PREFIX = 'query_translator_history__'; export const QUERY_TRANSLATOR_EXTENSION_NAME = 'query-translator'; +export const QUERY_TRANSLATOR_HISTORY_PREFIX = `${QUERY_TRANSLATOR_EXTENSION_NAME}_history__`; + +/** + * Creates a new composite key for RW operations against the SessionStorage. + */ export const getSessionStorageHistoryKey = (pagenumber, cardId) => { return `${QUERY_TRANSLATOR_HISTORY_PREFIX}__${pagenumber}__${cardId}`; }; diff --git a/src/sessionStorage/README.adoc b/src/sessionStorage/README.adoc new file mode 100644 index 000000000..7b0e017ef --- /dev/null +++ b/src/sessionStorage/README.adoc @@ -0,0 +1,5 @@ += Session Storage + +This reducer serves only to store data that we want to reset at each new session. +To connect to it, just define a key and use the predefined actions to set a new pair (key,value) inside of it. +Inside the actions there is also an action to delete all the keys that match a precise prefix, it can be useful, for example, to wipe the sessionStorage state for a certain extension, if it stores the data inside the sessionStorage using a prefix (for example look at the query-translator extension at getSessionStorageHistoryKey ) \ No newline at end of file From 690337f01a7cd54bcb193788462d13ea01fe0633 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Sun, 11 Jun 2023 18:13:53 +0200 Subject: [PATCH 16/49] moved docs inside the developer guide --- docs/modules/ROOT/nav.adoc | 1 + .../modules/ROOT/pages/developer-guide/session-storage.adoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename src/sessionStorage/README.adoc => docs/modules/ROOT/pages/developer-guide/session-storage.adoc (97%) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index c75d6f6a6..3dd7baeb8 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -39,6 +39,7 @@ ** xref:developer-guide/design.adoc[Design] ** xref:developer-guide/adding-visualizations.adoc[Adding Visualizations] ** xref:developer-guide/state-management.adoc[State Management] +** xref:developer-guide/session-storage.adoc[Session Storage] ** xref:developer-guide/testing.adoc[Testing] ** xref:developer-guide/contributing.adoc[Contributing] diff --git a/src/sessionStorage/README.adoc b/docs/modules/ROOT/pages/developer-guide/session-storage.adoc similarity index 97% rename from src/sessionStorage/README.adoc rename to docs/modules/ROOT/pages/developer-guide/session-storage.adoc index 7b0e017ef..a4861a4cf 100644 --- a/src/sessionStorage/README.adoc +++ b/docs/modules/ROOT/pages/developer-guide/session-storage.adoc @@ -2,4 +2,4 @@ This reducer serves only to store data that we want to reset at each new session. To connect to it, just define a key and use the predefined actions to set a new pair (key,value) inside of it. -Inside the actions there is also an action to delete all the keys that match a precise prefix, it can be useful, for example, to wipe the sessionStorage state for a certain extension, if it stores the data inside the sessionStorage using a prefix (for example look at the query-translator extension at getSessionStorageHistoryKey ) \ No newline at end of file +Inside the actions there is also an action to delete all the keys that match a precise prefix, it can be useful, for example, to wipe the sessionStorage state for a certain extension, if it stores the data inside the sessionStorage using a prefix (for example look at the query-translator extension at getSessionStorageHistoryKey). \ No newline at end of file From f4e8de5c423e56f985f37de341c7f732bf7d4d4b Mon Sep 17 00:00:00 2001 From: aleksandarneo4j Date: Mon, 12 Jun 2023 08:06:04 +0200 Subject: [PATCH 17/49] added reportexamples in the user message --- .../query-translator/clients/OpenAiClient.ts | 17 +++++++-- .../query-translator/clients/const.ts | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/extensions/query-translator/clients/OpenAiClient.ts b/src/extensions/query-translator/clients/OpenAiClient.ts index a3683ea03..04e1bebcc 100644 --- a/src/extensions/query-translator/clients/OpenAiClient.ts +++ b/src/extensions/query-translator/clients/OpenAiClient.ts @@ -1,5 +1,5 @@ import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai'; -import { reportTypesToDesc } from './const'; +import { reportTypesToDesc, reportExampleQueries } from './const'; import { ModelClient } from './ModelClient'; const consoleLogAsync = async (message: string, other?: any) => { @@ -105,9 +105,19 @@ export class OpenAiClient extends ModelClient { this.modelType = modelType; } + // TODO: adapt to the new structure, no more persisting inside the object, passign everything down + /* addUserMessage(content, reportType, plain = false) { + // let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${reportTypesToDesc[reportType]} Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result `; + let finalMessage = `User Query: ${content}. Generate a Cypher query that retrieves data for ${reportTypesToDesc[reportType]}. Ensure the query adheres to the provided schema and follows the expected format. Please remove any comments, explanations, or unnecessary symbols from the query result.`; + return { role: ChatCompletionRequestMessageRoleEnum.User, content: plain ? content : finalMessage }; + } +*/ // TODO: adapt to the new structure, no more persisting inside the object, passign everything down addUserMessage(content, reportType, plain = false) { - let finalMessage = `${content}. The Cypher RETURN clause must contained certain variables, in this case ${reportTypesToDesc[reportType]} Plain cypher code, no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result `; + let queryExample = reportExampleQueries[reportType]; + let finalMessage = `${content}. Please use the following query structure as an example for ${reportTypesToDesc[reportType]}: + ${queryExample} + Remember to respect the schema and remove any unnecessary comments or explanations from your result.`; return { role: ChatCompletionRequestMessageRoleEnum.User, content: plain ? content : finalMessage }; } @@ -120,7 +130,8 @@ export class OpenAiClient extends ModelClient { } addErrorMessage(error) { - let finalMessage = `Please fix the query accordingly to this error: ${error}. Plain cypher code, no comments and no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result`; + //let finalMessage = `Please fix the query accordingly to this error: ${error}. Plain cypher code, no comments and no explanations and no unrequired symbols. Remember to respect the schema. Please remove any comment or explanation from your result`; + let finalMessage = `Error: ${error}. Please correct the query based on the provided error message. Ensure the query follows the expected format, adheres to the schema, and does not contain any comments, explanations, or unnecessary symbols. Please remove any comments or explanations from the query result.`; return { role: ChatCompletionRequestMessageRoleEnum.User, content: finalMessage }; } diff --git a/src/extensions/query-translator/clients/const.ts b/src/extensions/query-translator/clients/const.ts index e7f5f5f47..972b278f9 100644 --- a/src/extensions/query-translator/clients/const.ts +++ b/src/extensions/query-translator/clients/const.ts @@ -41,6 +41,42 @@ export const reportTypesToDesc = { 'Pie Chart': 'Two variables named category and value.', }; +export const reportExampleQueries = { + Table: 'MATCH (n:Movie)<-[:ACTED_IN]-(p:Person) RETURN n.title, n.released, count(p) as actors', + Graph: `MATCH (p:Person)-[a:ACTED_IN]->(m:Movie) WHERE m.title = 'The Matrix' RETURN p, a, m`, + 'Bar Chart': 'MATCH (p:Person)-[e]->(m:Movie) RETURN m.title as Title, COUNT(p) as People', + 'Line Chart': 'MATCH (p:Person) RETURN (p.born/10)*10 as Decade, COUNT(p) as People ORDER BY Decade ASC', + Sunburst: `MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department) WITH nodes(path) as no WITH no, last(no) as leaf WITH [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val RETURN result, val`, + 'Circle Packing': `MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department) WITH nodes(path) as no WITH no, last(no) as leaf WITH [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val RETURN result, val`, + Choropleth: `MATCH (:Company{name:'NeoDash'})-[:HAS_DEPARTMENT]->(:Department)<-[:IN_DEPARTMENT]-(e:Employee),(e)-[:LIVES_IN]->(c:Country) WITH c.code as code, count(e) as value RETURN code, value`, + 'Area Map': `MATCH (:Company{name:'NeoDash'})-[:HAS_DEPARTMENT]->(:Department)<-[:IN_DEPARTMENT]-(e:Employee), + (e)-[:LIVES]->(city:City)-[:IN_COUNTRY]->(country:Country) + WITH city, country + CALL { + WITH country + RETURN country.countryCode as code, count(*) as value + UNION + WITH city + RETURN city.countryCode as code, count(*) as value + } + WITH code, sum(value) as totalCount + RETURN code,totalCount`, + Treemap: `MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department) WITH nodes(path) as no WITH no, last(no) as leaf WITH [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val RETURN result, val`, + 'Radar Chart': `MATCH (s:Skill) + MATCH (:Player{name:"Messi"})-[h1:HAS_SKILL]->(s) + MATCH (:Player{name:"Mbappe"})-[h2:HAS_SKILL]->(s) + MATCH (:Player{name:"Benzema"})-[h3:HAS_SKILL]->(s) + MATCH (:Player{name:"C Ronaldo"})-[h4:HAS_SKILL]->(s) + MATCH (:Player{name:"Lewandowski"})-[h5:HAS_SKILL]->(s) + RETURN s.name as Skill, h1.value as Messi, h2.value as Mbappe, h3.value as Benzema, h4.value as Ronaldo, h5.value as Lewandowski`, + 'Sankey Chart': 'MATCH (p:Person)-[r:RATES]->(m:Movie) RETURN p, r, m', + Map: 'MATCH (b:Brewery) RETURN b', + 'Single Value': 'MATCH (n) RETURN COUNT(n)', + 'Gauge Chart': 'MATCH (c:CPU) WHERE c.id = 1 RETURN c.load_percentage * 100', + 'Raw JSON': 'MATCH (n) RETURN COUNT(n)', + 'Pie Chart': 'Match (n:Person)-[e]->(m:Movie) RETURN m.title as Title, COUNT(p) as People LIMIT 10', +}; + export const TASK_DEFINITION = `Task: Generate Cypher queries to query a Neo4j graph database based on the provided schema definition. These queries will be used inside NeoDash reports. Documentation for NeoDash is here : https://neo4j.com/labs/neodash/2.2/ Instructions: From 79cc1b7b58948d30d59294ee134df3e34389c5d2 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Mon, 12 Jun 2023 11:46:18 +0200 Subject: [PATCH 18/49] Break out query component --- src/card/settings/CardSettingsContent.tsx | 83 +++++++++++++---------- src/extensions/ExtensionConfig.tsx | 3 +- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/card/settings/CardSettingsContent.tsx b/src/card/settings/CardSettingsContent.tsx index 012fd5147..a3d715340 100644 --- a/src/card/settings/CardSettingsContent.tsx +++ b/src/card/settings/CardSettingsContent.tsx @@ -7,7 +7,7 @@ import { getReportTypes } from '../../extensions/ExtensionUtils'; import { Dropdown } from '@neo4j-ndl/react'; import { NeoLanguageToggleSwitch } from '../../extensions/query-translator/component/LanguageToggleSwitch'; import { - EXTENSIONS_CARD_SETTINGS_COMPONENTS, + EXTENSIONS_CARD_SETTINGS_COMPONENT, getExtensionCardSettingsComponents, } from '../../extensions/ExtensionConfig'; @@ -54,11 +54,19 @@ const NeoCardSettingsContent = ({ const reportTypes = getReportTypes(extensions); const SettingsComponent = reportTypes[type] && reportTypes[type].settingsComponent; + function hasExtensionComponents() { + return ( + Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENT).filter( + (name) => extensions[name] && EXTENSIONS_CARD_SETTINGS_COMPONENT[name] + ).length > 0 + ); + } + function renderExtensionsComponents() { const res = ( <> - {Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENTS).map((name) => { - const Component = extensions[name] ? EXTENSIONS_CARD_SETTINGS_COMPONENTS[name] : ''; + {Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENT).map((name) => { + const Component = extensions[name] ? EXTENSIONS_CARD_SETTINGS_COMPONENT[name] : ''; return Component ? : <>; })} @@ -66,6 +74,41 @@ const NeoCardSettingsContent = ({ return res; } + const defaultQueryBoxComponent = ( + <> + { + if (languageName == 'Cypher') { + debouncedQueryUpdate(value); + } else { + onReportSettingUpdate('naturalLanguageQuery', value); + } + + const newQueryText = { ...queryTexts }; + newQueryText[languageName] = value; + setQueryTexts(newQueryText); + }} + placeholder={`Enter ${languageName} here...`} + /> +
+ {reportTypes[type] && reportTypes[type].helperText} +
+ + ); + return ( ) : ( -
- {renderExtensionsComponents()} - { - if (languageName == 'Cypher') { - debouncedQueryUpdate(value); - } else { - onReportSettingUpdate('naturalLanguageQuery', value); - } - - const newQueryText = { ...queryTexts }; - newQueryText[languageName] = value; - setQueryTexts(newQueryText); - }} - placeholder={`Enter ${ languageName } here...`} - /> -
- {reportTypes[type] && reportTypes[type].helperText} -
-
+
{hasExtensionComponents() ? renderExtensionsComponents() : defaultQueryBoxComponent}
)}
); diff --git a/src/extensions/ExtensionConfig.tsx b/src/extensions/ExtensionConfig.tsx index 8402847b0..e707a1f86 100644 --- a/src/extensions/ExtensionConfig.tsx +++ b/src/extensions/ExtensionConfig.tsx @@ -8,7 +8,6 @@ import { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; import { QUERY_TRANSLATOR_ACTION_PREFIX } from './query-translator/state/QueryTranslatorActions'; import { queryTranslatorReducer } from './query-translator/state/QueryTranslatorReducer'; import QueryTranslatorButton from './query-translator/component/QueryTranslator'; -import TranslatorButton from './query-translator/component/TranslatorButton'; import { NeoLanguageToggleSwitch } from './query-translator/component/LanguageToggleSwitch'; // TODO: continue documenting interface @@ -176,4 +175,4 @@ function getExtensionSettingsModal() { export const EXTENSIONS_REDUCERS = getExtensionReducers(); export const EXTENSIONS_DRAWER_BUTTONS = getExtensionDrawerButtons(); export const EXTENSIONS_SETTINGS_MODALS = getExtensionSettingsModal(); -export const EXTENSIONS_CARD_SETTINGS_COMPONENTS = getExtensionCardSettingsComponents(); +export const EXTENSIONS_CARD_SETTINGS_COMPONENT = getExtensionCardSettingsComponents(); From 40a1fb869702aa7989fa535e1c2e54469fc4327b Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Mon, 12 Jun 2023 11:46:54 +0200 Subject: [PATCH 19/49] added migration for new extension architecture from 2.2 to 2.3 and added automatic fetch of cardIds to test openAi features in queryTranslator --- src/dashboard/DashboardThunks.ts | 2 ++ .../query-translator/component/QueryTranslator.tsx | 11 ++++++++--- src/sessionStorage/SessionStorageReducer.ts | 2 -- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/dashboard/DashboardThunks.ts b/src/dashboard/DashboardThunks.ts index 163ce1c99..1a05b3575 100644 --- a/src/dashboard/DashboardThunks.ts +++ b/src/dashboard/DashboardThunks.ts @@ -323,6 +323,8 @@ export function upgradeDashboardVersion(dashboard: any, origin: string, target: styling: { active: true, }, + active: true, + activeReducers: [], }; dashboard.version = '2.3'; diff --git a/src/extensions/query-translator/component/QueryTranslator.tsx b/src/extensions/query-translator/component/QueryTranslator.tsx index ea3fa7050..d31334c0f 100644 --- a/src/extensions/query-translator/component/QueryTranslator.tsx +++ b/src/extensions/query-translator/component/QueryTranslator.tsx @@ -8,6 +8,7 @@ import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; import { Tooltip } from '@mui/material'; import { LanguageIconSolid } from '@neo4j-ndl/react/icons'; +import { getReports } from '../../../page/PageSelectors'; /** * //TODO: * 1. The query translator should handle all the requests from the cards to the client @@ -19,6 +20,7 @@ export const QueryTranslatorButton = ({ apiKey, modelProvider, clientSettings, + reports, // TODO: REMOVE IT JUST FOR TEST setGlobalModelClient, queryTranslation, }) => { @@ -36,9 +38,11 @@ export const QueryTranslatorButton = ({ useEffect(() => { try { if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { - ['eb204bd5-7dd1-4cb4-9a34-111976db0b0e', '4d017b2f-261a-4d21-a187-ab8cce6ec31d'].forEach((cardId) => { - queryTranslation(0, cardId, 'give me any query', 'Table', driver); - }); + reports + .map((card) => card.id) + .forEach((cardId) => { + queryTranslation(0, cardId, 'give me any query', 'Table', driver); + }); } } catch (e) { console.log(e); @@ -76,6 +80,7 @@ const mapStateToProps = (state) => ({ apiKey: getApiKey(state), modelProvider: getModelProvider(state), clientSettings: getQueryTranslatorSettings(state), + reports: getReports(state), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/src/sessionStorage/SessionStorageReducer.ts b/src/sessionStorage/SessionStorageReducer.ts index 85aa855f6..b54b82ec3 100644 --- a/src/sessionStorage/SessionStorageReducer.ts +++ b/src/sessionStorage/SessionStorageReducer.ts @@ -22,7 +22,6 @@ export const sessionStorageReducer = (state = initialState, action: { type: any; } case STORE_VALUE_SESSION_STORAGE: { const { key, value } = payload; - console.log(payload); let newValue = {}; newValue[key] = value; return update(state, newValue); @@ -31,7 +30,6 @@ export const sessionStorageReducer = (state = initialState, action: { type: any; case DELETE_VALUE_SESSION_STORAGE: { const { key } = payload; let newState = { ...state }; - console.log(key); delete newState[key]; return newState; } From 80c93293ba4977283055c0b3625f557b492ed046 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Mon, 12 Jun 2023 11:51:05 +0200 Subject: [PATCH 20/49] Revert --- src/card/settings/CardSettingsContent.tsx | 38 +++++-------------- .../component/LanguageToggleSwitch.tsx | 4 +- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/src/card/settings/CardSettingsContent.tsx b/src/card/settings/CardSettingsContent.tsx index a3d715340..88f3f314f 100644 --- a/src/card/settings/CardSettingsContent.tsx +++ b/src/card/settings/CardSettingsContent.tsx @@ -23,34 +23,21 @@ const NeoCardSettingsContent = ({ onTypeUpdate, onDatabaseChanged, // When the database related to a report is changed it must be stored in the report state }) => { - // Store a generic dictionary of query text variables in a dictionary, where the key is the language. - const [queryTexts, setQueryTexts] = React.useState({ Cypher: query }); + // Ensure that we only trigger a text update event after the user has stopped typing. + const [queryText, setQueryText] = React.useState(query); const debouncedQueryUpdate = useCallback(debounce(onQueryUpdate, 250), []); // State to manage the current database entry inside the form const [databaseText, setDatabaseText] = React.useState(database); const debouncedDatabaseUpdate = useCallback(debounce(onDatabaseChanged, 250), []); - const [languageName, setLanguageName] = React.useState('Cypher'); - useEffect(() => { // Reset text to the dashboard state when the page gets reorganized. - if (query !== queryTexts.Cypher) { - const newQueryTexts = { ...queryTexts }; - newQueryTexts.Cypher = query; - setQueryTexts(newQueryTexts); + if (query !== queryText) { + setQueryText(query); } }, [query]); - useEffect(() => { - // Reset text to the dashboard state when the page gets reorganized. - if (reportSettings.naturalLanguageQuery !== queryTexts.English) { - const newQueryTexts = { ...queryTexts }; - newQueryTexts.English = reportSettings.naturalLanguageQuery; - setQueryTexts(newQueryTexts); - } - }, [reportSettings.naturalLanguageQuery]); - const reportTypes = getReportTypes(extensions); const SettingsComponent = reportTypes[type] && reportTypes[type].settingsComponent; @@ -67,7 +54,7 @@ const NeoCardSettingsContent = ({ <> {Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENT).map((name) => { const Component = extensions[name] ? EXTENSIONS_CARD_SETTINGS_COMPONENT[name] : ''; - return Component ? : <>; + return Component ? : <>; })} ); @@ -77,21 +64,14 @@ const NeoCardSettingsContent = ({ const defaultQueryBoxComponent = ( <> { - if (languageName == 'Cypher') { - debouncedQueryUpdate(value); - } else { - onReportSettingUpdate('naturalLanguageQuery', value); - } - - const newQueryText = { ...queryTexts }; - newQueryText[languageName] = value; - setQueryTexts(newQueryText); + debouncedQueryUpdate(value); + setQueryText(value); }} - placeholder={`Enter ${languageName} here...`} + placeholder={`Enter Cypher here...`} />
{ +export const NeoLanguageToggleSwitch = () => { enum Language { ENGLISH, CYPHER, @@ -25,10 +25,8 @@ export const NeoLanguageToggleSwitch = ({ setLanguageName }) => { onChange={() => { if (language == Language.ENGLISH) { setLanguage(Language.CYPHER); - setLanguageName('Cypher'); } else { setLanguage(Language.ENGLISH); - setLanguageName('English'); } }} className='n-ml-2' From 7e2f69d9e0658949dfd716ce755140736cffefea Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Mon, 12 Jun 2023 12:07:59 +0200 Subject: [PATCH 21/49] Generalizing --- src/card/settings/CardSettingsContent.tsx | 39 ++++++++++--------- src/component/editor/CodeEditorComponent.tsx | 10 +++++ src/extensions/ExtensionConfig.tsx | 4 +- ...Switch.tsx => OverrideCardQueryEditor.tsx} | 21 +++++++++- 4 files changed, 52 insertions(+), 22 deletions(-) rename src/extensions/query-translator/component/{LanguageToggleSwitch.tsx => OverrideCardQueryEditor.tsx} (55%) diff --git a/src/card/settings/CardSettingsContent.tsx b/src/card/settings/CardSettingsContent.tsx index 88f3f314f..645c78438 100644 --- a/src/card/settings/CardSettingsContent.tsx +++ b/src/card/settings/CardSettingsContent.tsx @@ -2,10 +2,12 @@ import React, { useEffect } from 'react'; import CardContent from '@mui/material/CardContent'; import debounce from 'lodash/debounce'; import { useCallback } from 'react'; -import NeoCodeEditorComponent from '../../component/editor/CodeEditorComponent'; +import NeoCodeEditorComponent, { + DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE, +} from '../../component/editor/CodeEditorComponent'; import { getReportTypes } from '../../extensions/ExtensionUtils'; import { Dropdown } from '@neo4j-ndl/react'; -import { NeoLanguageToggleSwitch } from '../../extensions/query-translator/component/LanguageToggleSwitch'; +import { NeoOverrideCardQueryEditor } from '../../extensions/query-translator/component/OverrideCardQueryEditor'; import { EXTENSIONS_CARD_SETTINGS_COMPONENT, getExtensionCardSettingsComponents, @@ -49,12 +51,26 @@ const NeoCardSettingsContent = ({ ); } + function updateCypherQuery(value) { + debouncedQueryUpdate(value); + setQueryText(value); + } + function renderExtensionsComponents() { const res = ( <> {Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENT).map((name) => { const Component = extensions[name] ? EXTENSIONS_CARD_SETTINGS_COMPONENT[name] : ''; - return Component ? : <>; + return Component ? ( + + ) : ( + <> + ); })} ); @@ -68,24 +84,11 @@ const NeoCardSettingsContent = ({ editable={true} language={reportTypes[type] && reportTypes[type].inputMode ? reportTypes[type].inputMode : 'cypher'} onChange={(value) => { - debouncedQueryUpdate(value); - setQueryText(value); + updateCypherQuery(value); }} placeholder={`Enter Cypher here...`} /> -
- {reportTypes[type] && reportTypes[type].helperText} -
+
{reportTypes[type] && reportTypes[type].helperText}
); diff --git a/src/component/editor/CodeEditorComponent.tsx b/src/component/editor/CodeEditorComponent.tsx index 72f87ef08..f185de195 100644 --- a/src/component/editor/CodeEditorComponent.tsx +++ b/src/component/editor/CodeEditorComponent.tsx @@ -3,6 +3,16 @@ import { CypherEditor, CypherEditorProps } from '@neo4j-cypher/react-codemirror' import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; // import { languages } from '@codemirror/language-data'; +export const DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE = { + color: 'grey', + fontSize: 12, + paddingLeft: '5px', + borderBottom: '1px solid lightgrey', + borderLeft: '1px solid lightgrey', + borderRight: '1px solid lightgrey', + marginTop: '0px', +}; + const markdownExtensions = [ markdown({ base: markdownLanguage, // Support GFM diff --git a/src/extensions/ExtensionConfig.tsx b/src/extensions/ExtensionConfig.tsx index e707a1f86..839a8786c 100644 --- a/src/extensions/ExtensionConfig.tsx +++ b/src/extensions/ExtensionConfig.tsx @@ -8,7 +8,7 @@ import { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; import { QUERY_TRANSLATOR_ACTION_PREFIX } from './query-translator/state/QueryTranslatorActions'; import { queryTranslatorReducer } from './query-translator/state/QueryTranslatorReducer'; import QueryTranslatorButton from './query-translator/component/QueryTranslator'; -import { NeoLanguageToggleSwitch } from './query-translator/component/LanguageToggleSwitch'; +import { NeoOverrideCardQueryEditor } from './query-translator/component/OverrideCardQueryEditor'; // TODO: continue documenting interface interface Extension { @@ -92,7 +92,7 @@ export const EXTENSIONS: Record = { enabled: true, reducerPrefix: QUERY_TRANSLATOR_ACTION_PREFIX, reducerObject: queryTranslatorReducer, - cardSettingsComponent: NeoLanguageToggleSwitch, + cardSettingsComponent: NeoOverrideCardQueryEditor, drawerButton: QueryTranslatorButton, description: 'Use natural language to generate Cypher queries in NeoDash. Connect to an LLM through an API, and let NeoDash use your database schema + the report types to generate queries automatically.', diff --git a/src/extensions/query-translator/component/LanguageToggleSwitch.tsx b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx similarity index 55% rename from src/extensions/query-translator/component/LanguageToggleSwitch.tsx rename to src/extensions/query-translator/component/OverrideCardQueryEditor.tsx index 4c6faf193..a979df79f 100644 --- a/src/extensions/query-translator/component/LanguageToggleSwitch.tsx +++ b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx @@ -3,15 +3,20 @@ import React from 'react'; import { connect } from 'react-redux'; import { ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { IconButton, Switch } from '@neo4j-ndl/react'; +import NeoCodeEditorComponent, { + DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE, +} from '../../../component/editor/CodeEditorComponent'; +import { getReportTypes } from '../../ExtensionUtils'; // TODO - rename to 'Node Sidebar Extension button' to reflect better the functionality. -export const NeoLanguageToggleSwitch = () => { +export const NeoOverrideCardQueryEditor = ({ cypherQuery, extensions, reportType, updateCypherQuery }) => { enum Language { ENGLISH, CYPHER, } const [language, setLanguage] = React.useState(Language.CYPHER); + const reportTypes = getReportTypes(extensions); return (
@@ -35,6 +40,18 @@ export const NeoLanguageToggleSwitch = () => {   English + updateCypherQuery(value)} + placeholder={`Enter Cypher here...`} + /> +
+ {reportTypes[reportType] && reportTypes[reportType].helperText} +
); }; @@ -43,4 +60,4 @@ const mapStateToProps = () => ({}); const mapDispatchToProps = () => ({}); -export default connect(mapStateToProps, mapDispatchToProps)(NeoLanguageToggleSwitch); +export default connect(mapStateToProps, mapDispatchToProps)(NeoOverrideCardQueryEditor); From 27d5acd2b86b01fe650c3672427ea168c4c4915e Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Mon, 12 Jun 2023 12:38:49 +0200 Subject: [PATCH 22/49] Props drilling --- src/card/Card.tsx | 2 + src/card/settings/CardSettings.tsx | 4 + src/card/settings/CardSettingsContent.tsx | 4 + .../component/OverrideCardQueryEditor.tsx | 76 ++++++++++++++++--- .../component/QueryTranslator.tsx | 25 +----- 5 files changed, 75 insertions(+), 36 deletions(-) diff --git a/src/card/Card.tsx b/src/card/Card.tsx index 0c3825c04..b5281a780 100644 --- a/src/card/Card.tsx +++ b/src/card/Card.tsx @@ -165,6 +165,8 @@ const NeoCard = ({ { +export const NeoOverrideCardQueryEditor = ({ + pagenumber, + reportId, + cypherQuery, + extensions, + reportType, + updateCypherQuery, + translateQuery, + updateEnglishQuery, +}) => { enum Language { ENGLISH, CYPHER, } + console.log(pagenumber, reportId); const [language, setLanguage] = React.useState(Language.CYPHER); + const [englishQuestion, setEnglishQuestion] = React.useState(''); + const debouncedEnglishQuestionUpdate = useCallback(debounce(updateEnglishQuery, 250), []); + const reportTypes = getReportTypes(extensions); + const cypherEditor = ( + updateCypherQuery(value)} + placeholder={`Enter Cypher here...`} + /> + ); + + function updateEnglishQuestion(value) { + debouncedEnglishQuestionUpdate(pagenumber, reportId, value); + setEnglishQuestion(value); + } + + // To prevent a bug with the code editor component, we wrap it in an extra enclosing bracket. + const englishEditor = ( + <> + { + updateEnglishQuestion(value); + }} + placeholder={`Enter English here...`} + /> + + ); + + const { driver } = useContext(Neo4jContext); + return (
@@ -29,6 +80,8 @@ export const NeoOverrideCardQueryEditor = ({ cypherQuery, extensions, reportType checked={language == Language.ENGLISH} onChange={() => { if (language == Language.ENGLISH) { + // Trigger a translation + translateQuery(pagenumber, reportId, englishQuestion, reportType, driver); setLanguage(Language.CYPHER); } else { setLanguage(Language.ENGLISH); @@ -40,15 +93,7 @@ export const NeoOverrideCardQueryEditor = ({ cypherQuery, extensions, reportType
  English
- updateCypherQuery(value)} - placeholder={`Enter Cypher here...`} - /> + {language == Language.CYPHER ? cypherEditor : englishEditor}
{reportTypes[reportType] && reportTypes[reportType].helperText}
@@ -58,6 +103,13 @@ export const NeoOverrideCardQueryEditor = ({ cypherQuery, extensions, reportType const mapStateToProps = () => ({}); -const mapDispatchToProps = () => ({}); +const mapDispatchToProps = (dispatch) => ({ + translateQuery: (pagenumber, reportId, text, reportType, driver) => { + dispatch(queryTranslationThunk(pagenumber, reportId, text, reportType, driver)); + }, + updateEnglishQuery: (pagenumber, reportId, message) => { + dispatch(updateLastMessage(message, pagenumber, reportId)); + }, +}); export default connect(mapStateToProps, mapDispatchToProps)(NeoOverrideCardQueryEditor); diff --git a/src/extensions/query-translator/component/QueryTranslator.tsx b/src/extensions/query-translator/component/QueryTranslator.tsx index d31334c0f..b4142a2c1 100644 --- a/src/extensions/query-translator/component/QueryTranslator.tsx +++ b/src/extensions/query-translator/component/QueryTranslator.tsx @@ -16,14 +16,7 @@ import { getReports } from '../../../page/PageSelectors'; * 3. create system message from here to prevent fucking all up during the thunk, o each modelProvider change and at the start pull all the db schema */ -export const QueryTranslatorButton = ({ - apiKey, - modelProvider, - clientSettings, - reports, // TODO: REMOVE IT JUST FOR TEST - setGlobalModelClient, - queryTranslation, -}) => { +export const QueryTranslatorButton = ({ setGlobalModelClient }) => { const [open, setOpen] = React.useState(false); const { driver } = useContext(Neo4jContext); @@ -33,22 +26,6 @@ export const QueryTranslatorButton = ({ setGlobalModelClient(undefined); }, []); - // When changing provider, i will reset all the messages to prevent strage results - // TODO: remove this effect is just for testing - useEffect(() => { - try { - if (modelProvider && apiKey && Object.keys(clientSettings).length > 0) { - reports - .map((card) => card.id) - .forEach((cardId) => { - queryTranslation(0, cardId, 'give me any query', 'Table', driver); - }); - } - } catch (e) { - console.log(e); - } - }, [modelProvider, apiKey, clientSettings]); - const button = (
From ed07686abb7965d5ca61970db28754a2274d6256 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Mon, 12 Jun 2023 12:52:44 +0200 Subject: [PATCH 23/49] Plugged translation on switch --- src/card/settings/CardSettingsContent.tsx | 1 - src/extensions/ExtensionConfig.tsx | 2 +- .../query-translator/component/OverrideCardQueryEditor.tsx | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/card/settings/CardSettingsContent.tsx b/src/card/settings/CardSettingsContent.tsx index f86b035de..1f15794cf 100644 --- a/src/card/settings/CardSettingsContent.tsx +++ b/src/card/settings/CardSettingsContent.tsx @@ -7,7 +7,6 @@ import NeoCodeEditorComponent, { } from '../../component/editor/CodeEditorComponent'; import { getReportTypes } from '../../extensions/ExtensionUtils'; import { Dropdown } from '@neo4j-ndl/react'; -import { NeoOverrideCardQueryEditor } from '../../extensions/query-translator/component/OverrideCardQueryEditor'; import { EXTENSIONS_CARD_SETTINGS_COMPONENT, getExtensionCardSettingsComponents, diff --git a/src/extensions/ExtensionConfig.tsx b/src/extensions/ExtensionConfig.tsx index 839a8786c..495497562 100644 --- a/src/extensions/ExtensionConfig.tsx +++ b/src/extensions/ExtensionConfig.tsx @@ -8,7 +8,7 @@ import { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; import { QUERY_TRANSLATOR_ACTION_PREFIX } from './query-translator/state/QueryTranslatorActions'; import { queryTranslatorReducer } from './query-translator/state/QueryTranslatorReducer'; import QueryTranslatorButton from './query-translator/component/QueryTranslator'; -import { NeoOverrideCardQueryEditor } from './query-translator/component/OverrideCardQueryEditor'; +import NeoOverrideCardQueryEditor from './query-translator/component/OverrideCardQueryEditor'; // TODO: continue documenting interface interface Extension { diff --git a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx index db9295914..9f174dc99 100644 --- a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx +++ b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx @@ -28,7 +28,6 @@ export const NeoOverrideCardQueryEditor = ({ CYPHER, } - console.log(pagenumber, reportId); const [language, setLanguage] = React.useState(Language.CYPHER); const [englishQuestion, setEnglishQuestion] = React.useState(''); const debouncedEnglishQuestionUpdate = useCallback(debounce(updateEnglishQuery, 250), []); From 96f79821e3b5f798a272836cde01580dbeddad33 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Mon, 12 Jun 2023 13:17:12 +0200 Subject: [PATCH 24/49] UX improvements --- .../component/OverrideCardQueryEditor.tsx | 86 +++++++++++++------ .../component/QueryTranslator.tsx | 4 - .../QueryTranslatorSettingsModal.tsx | 8 ++ .../state/QueryTranslatorThunks.ts | 8 +- 4 files changed, 72 insertions(+), 34 deletions(-) diff --git a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx index 9f174dc99..db49a8467 100644 --- a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx +++ b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx @@ -11,6 +11,7 @@ import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; import debounce from 'lodash/debounce'; import { updateLastMessage } from '../state/QueryTranslatorActions'; +import { createNotification } from '../../../application/ApplicationActions'; // TODO - rename to 'Node Sidebar Extension button' to reflect better the functionality. export const NeoOverrideCardQueryEditor = ({ @@ -22,6 +23,7 @@ export const NeoOverrideCardQueryEditor = ({ updateCypherQuery, translateQuery, updateEnglishQuery, + displayError, }) => { enum Language { ENGLISH, @@ -29,6 +31,7 @@ export const NeoOverrideCardQueryEditor = ({ } const [language, setLanguage] = React.useState(Language.CYPHER); + const [runningTranslation, setRunningTranslation] = React.useState(false); const [englishQuestion, setEnglishQuestion] = React.useState(''); const debouncedEnglishQuestionUpdate = useCallback(debounce(updateEnglishQuery, 250), []); @@ -68,34 +71,58 @@ export const NeoOverrideCardQueryEditor = ({ const { driver } = useContext(Neo4jContext); + function triggerTranslation() { + setRunningTranslation(true); + translateQuery( + pagenumber, + reportId, + englishQuestion, + reportType, + driver, + () => { + setRunningTranslation(false); + }, + (e) => { + setRunningTranslation(false); + console.log(e); + displayError(e); + } + ); + } + return (
- - - - - - -
Cypher - { - if (language == Language.ENGLISH) { - // Trigger a translation - translateQuery(pagenumber, reportId, englishQuestion, reportType, driver); - setLanguage(Language.CYPHER); - } else { - setLanguage(Language.ENGLISH); - } - }} - className='n-ml-2' - /> -   English
- {language == Language.CYPHER ? cypherEditor : englishEditor} -
- {reportTypes[reportType] && reportTypes[reportType].helperText} -
+ {runningTranslation ? ( + <>Running... + ) : ( + <> + + + + + + +
Cypher + { + if (language == Language.ENGLISH) { + triggerTranslation(); + setLanguage(Language.CYPHER); + } else { + setLanguage(Language.ENGLISH); + } + }} + className='n-ml-2' + /> +   English
+ {language == Language.CYPHER ? cypherEditor : englishEditor} +
+ {reportTypes[reportType] && reportTypes[reportType].helperText} +
+ + )}
); }; @@ -103,12 +130,15 @@ export const NeoOverrideCardQueryEditor = ({ const mapStateToProps = () => ({}); const mapDispatchToProps = (dispatch) => ({ - translateQuery: (pagenumber, reportId, text, reportType, driver) => { - dispatch(queryTranslationThunk(pagenumber, reportId, text, reportType, driver)); + translateQuery: (pagenumber, reportId, text, reportType, driver, onComplete, onError) => { + dispatch(queryTranslationThunk(pagenumber, reportId, text, reportType, driver, onComplete, onError)); }, updateEnglishQuery: (pagenumber, reportId, message) => { dispatch(updateLastMessage(message, pagenumber, reportId)); }, + displayError: (message) => { + dispatch(createNotification('Error when translating the natural language query', message)); + }, }); export default connect(mapStateToProps, mapDispatchToProps)(NeoOverrideCardQueryEditor); diff --git a/src/extensions/query-translator/component/QueryTranslator.tsx b/src/extensions/query-translator/component/QueryTranslator.tsx index b4142a2c1..1e5c4ee45 100644 --- a/src/extensions/query-translator/component/QueryTranslator.tsx +++ b/src/extensions/query-translator/component/QueryTranslator.tsx @@ -4,7 +4,6 @@ import { deleteAllMessageHistory, deleteMessageHistory, setGlobalModelClient } f import { getApiKey, getQueryTranslatorSettings, getModelProvider } from '../state/QueryTranslatorSelector'; import { SideNavigationItem } from '@neo4j-ndl/react'; import QueryTranslatorSettingsModal from './QueryTranslatorSettingsModal'; -import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; import { Tooltip } from '@mui/material'; import { LanguageIconSolid } from '@neo4j-ndl/react/icons'; @@ -64,9 +63,6 @@ const mapDispatchToProps = (dispatch) => ({ setGlobalModelClient: (modelClient) => { dispatch(setGlobalModelClient(modelClient)); }, - queryTranslation: (pagenumber, cardIndex, message, reportType, driver) => { - dispatch(queryTranslationThunk(pagenumber, cardIndex, message, reportType, driver)); - }, }); export default connect(mapStateToProps, mapDispatchToProps)(QueryTranslatorButton); diff --git a/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx b/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx index 73639657b..f71ad5061 100644 --- a/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx +++ b/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx @@ -33,6 +33,14 @@ export const QueryTranslatorSettingsModal = ({ This extensions lets you create reports with natural language. Your queries (in English) are translated to Cypher by a LLM provider of your choice.
+
+ Keep in mind that the following data will be sent to a external API: +
    +
  • - Your database schema, including label names, relationship types, and property keys.
  • +
  • - Any natural language question that a user writes.
  • +
+
+
{ + onComplete = (e) => { + console.log(e); + }, + onError = (e) => { console.log(e); } ) => @@ -109,6 +112,7 @@ export const queryTranslationThunk = if (messageHistory.length < newHistory.length && query) { dispatch(updateMessageHistory(newHistory, pagenumber, cardId)); dispatch(updateReportQueryThunk(cardId, query)); + onComplete(query); } } else { throw new Error("Couldn't get the Model Client for the translation, please check your credentials."); @@ -118,6 +122,6 @@ export const queryTranslationThunk = `Something wrong happened while calling the model client for the card number ${cardId} inside the page ${pagenumber}: \n`, { e } ); - setErrorMessage(e); + onError(e); } }; From 8855a7828685a7de581dcb72d46354ebce8925ac Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Mon, 12 Jun 2023 13:28:34 +0200 Subject: [PATCH 25/49] Better UX --- public/style.css | 17 +++++++++++++++++ .../component/OverrideCardQueryEditor.tsx | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/public/style.css b/public/style.css index 012f2f331..b4e0a4c3e 100644 --- a/public/style.css +++ b/public/style.css @@ -198,3 +198,20 @@ .cm-tooltip-autocomplete { margin-top: 4px; } + +@keyframes pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); + } +} \ No newline at end of file diff --git a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx index db49a8467..cbfe46b1c 100644 --- a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx +++ b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx @@ -93,7 +93,15 @@ export const NeoOverrideCardQueryEditor = ({ return (
{runningTranslation ? ( - <>Running... +
+
+ +
+ Calling GPT-3.5-Turbo... +
) : ( <> From 9ccb5e1c8efe441d32635fb5ab3118390733154b Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Mon, 12 Jun 2023 15:13:36 +0200 Subject: [PATCH 26/49] added correct error throwing in modelClient --- src/extensions/query-translator/clients/ModelClient.ts | 1 + src/extensions/query-translator/clients/const.ts | 2 +- src/extensions/query-translator/state/QueryTranslatorThunks.ts | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/query-translator/clients/ModelClient.ts b/src/extensions/query-translator/clients/ModelClient.ts index 6e28b8ffb..9ff5d0574 100644 --- a/src/extensions/query-translator/clients/ModelClient.ts +++ b/src/extensions/query-translator/clients/ModelClient.ts @@ -158,6 +158,7 @@ export abstract class ModelClient { } } catch (error) { await consoleLogAsync('error during query', error); + throw error; } return [query, newHistory]; } diff --git a/src/extensions/query-translator/clients/const.ts b/src/extensions/query-translator/clients/const.ts index 972b278f9..bb2441a6b 100644 --- a/src/extensions/query-translator/clients/const.ts +++ b/src/extensions/query-translator/clients/const.ts @@ -101,4 +101,4 @@ Gauge Chart - A single value of a single variable. Raw JSON - The Cypher query must return a JSON object that will be displayed as raw JSON data. Pie Chart - Two variables named category and value.`; -export const MAX_NUM_VALIDATION = 5; +export const MAX_NUM_VALIDATION = 3; diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index 9bc1ca73d..3dd8921dd 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -102,7 +102,6 @@ export const queryTranslationThunk = if (!client.driver) { client.setDriver(driver); } - const messageHistory = getHistoryPerCard(state, pagenumber, cardId); let translationRes = await client.queryTranslation(message, messageHistory, database, reportType); query = translationRes[0]; From cea77bf13cc7be028c7615d44e6cab3c85765324 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Mon, 12 Jun 2023 16:55:03 +0200 Subject: [PATCH 27/49] UX fix --- .../component/ClientSettings.tsx | 21 +++++++++++++++---- .../component/OverrideCardQueryEditor.tsx | 2 +- .../QueryTranslatorSettingsModal.tsx | 1 + 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/extensions/query-translator/component/ClientSettings.tsx b/src/extensions/query-translator/component/ClientSettings.tsx index cb94f8c2e..07c86d5cb 100644 --- a/src/extensions/query-translator/component/ClientSettings.tsx +++ b/src/extensions/query-translator/component/ClientSettings.tsx @@ -10,7 +10,7 @@ import { setGlobalModelClient, setModelProvider, } from '../state/QueryTranslatorActions'; -import { PlayCircleIconSolid, CheckCircleIconSolid } from '@neo4j-ndl/react/icons'; +import { PlayCircleIconSolid, CheckCircleIconSolid, PlayIconSolid } from '@neo4j-ndl/react/icons'; import { Button, IconButton } from '@neo4j-ndl/react'; import { modelClientInitializationThunk } from '../state/QueryTranslatorThunks'; @@ -26,6 +26,7 @@ export const ClientSettings = ({ updateModelProvider, updateClientSettings, deleteAllMessageHistory, + setOpen, }) => { const defaultSettings = getQueryTranslatorDefaultConfig(modelProvider); const requiredSettings = Object.keys(defaultSettings).filter((setting) => defaultSettings[setting].required); @@ -146,9 +147,21 @@ export const ClientSettings = ({ })}
{isAuthenticated ? ( - + <> + + + ) : ( <> )} diff --git a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx index cbfe46b1c..aa76275d2 100644 --- a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx +++ b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx @@ -100,7 +100,7 @@ export const NeoOverrideCardQueryEditor = ({ src='https://seeklogo.com/images/O/open-ai-logo-8B9BFEDC26-seeklogo.com.png' >
- Calling GPT-3.5-Turbo... + Calling OpenAI... ) : ( <> diff --git a/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx b/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx index f71ad5061..b5504bdc1 100644 --- a/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx +++ b/src/extensions/query-translator/component/QueryTranslatorSettingsModal.tsx @@ -53,6 +53,7 @@ export const QueryTranslatorSettingsModal = ({ /> {modelProviderState ? ( Date: Tue, 13 Jun 2023 11:00:58 +0200 Subject: [PATCH 28/49] Minor UX improvement for settings of extension --- src/extensions/query-translator/component/ClientSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/query-translator/component/ClientSettings.tsx b/src/extensions/query-translator/component/ClientSettings.tsx index 07c86d5cb..5bf8f2578 100644 --- a/src/extensions/query-translator/component/ClientSettings.tsx +++ b/src/extensions/query-translator/component/ClientSettings.tsx @@ -146,7 +146,7 @@ export const ClientSettings = ({ ); })}
- {isAuthenticated ? ( + {isAuthenticated && Object.keys(defaultSettings).every((n) => localSettings[n] !== undefined) ? ( <>
{language == Language.CYPHER ? cypherEditor : englishEditor}
- {reportTypes[reportType] && reportTypes[reportType].helperText} + {language == Language.ENGLISH ? ( + <> + For prompting tips, check out the{' '} + + Documentation + + . + + ) : ( + reportTypes[reportType] && reportTypes[reportType].helperText + )}
)} From 0c32b8e8be02ad4974e1a3689da3289ebc442591 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Tue, 13 Jun 2023 12:19:36 +0200 Subject: [PATCH 30/49] added function in queryTranslation to get back the current validationStep --- .../query-translator/clients/ModelClient.ts | 17 ++++++++++++++--- .../state/QueryTranslatorThunks.ts | 16 +++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/extensions/query-translator/clients/ModelClient.ts b/src/extensions/query-translator/clients/ModelClient.ts index 9ff5d0574..5f298def9 100644 --- a/src/extensions/query-translator/clients/ModelClient.ts +++ b/src/extensions/query-translator/clients/ModelClient.ts @@ -108,10 +108,19 @@ export abstract class ModelClient { * @param history History of messages exchanged between a card and the model client * @param database Databased used from the report, it will be used to fetch the schema * @param reportType Type of report asking that requires the translation + * @param setValidationStep Function to set the current validation step outside the function * @returns The new history to assign to the card. If there was no possibility of validating the query, the * method will return the same history passed in input */ - async queryTranslation(inputMessage, history, database, reportType) { + async queryTranslation( + inputMessage, + history, + database, + reportType, + setValidationStep = (value) => { + let x = value; + } + ) { // Creating a copy of the history let newHistory = [...history]; // Creating a tmp history to prevent updating the history with erroneous messages @@ -133,6 +142,7 @@ export abstract class ModelClient { // While is not validated and we didn't exceed the maximum retry number while (!isValidated && retries < MAX_NUM_VALIDATION) { retries += 1; + setValidationStep(retries); // Get the answer to the question let modelAnswer = await this.chatCompletion(tmpHistory); @@ -142,6 +152,8 @@ export abstract class ModelClient { let validationResult = await this.validateQuery(modelAnswer, database); isValidated = validationResult[0]; errorMessage = validationResult[1]; + + // If you can't validate the query, send the model a message to try to fix it if (!isValidated) { tmpHistory.push(this.addErrorMessage(errorMessage)); } else { @@ -157,7 +169,7 @@ export abstract class ModelClient { throw Error(`The model couldn't translate your request: ${inputMessage}`); } } catch (error) { - await consoleLogAsync('error during query', error); + await consoleLogAsync('Error during query', error); throw error; } return [query, newHistory]; @@ -171,7 +183,6 @@ export abstract class ModelClient { notImplementedError('chatCompletion'); } - // TODO: adapt to the new structure, no more persisting inside the object, passign everything down addUserMessage(_content, _reportType, _plain = false) { notImplementedError('addUserMessage'); } diff --git a/src/extensions/query-translator/state/QueryTranslatorThunks.ts b/src/extensions/query-translator/state/QueryTranslatorThunks.ts index 3dd8921dd..29b4e8b80 100644 --- a/src/extensions/query-translator/state/QueryTranslatorThunks.ts +++ b/src/extensions/query-translator/state/QueryTranslatorThunks.ts @@ -74,6 +74,10 @@ const getModelClientThunk = () => async (dispatch: any, getState: any) => { * @param message Message inserted by the user * @param reportType Type of report used by the card calling the thunk * @param driver Neo4j Driver used to fetch the schema from the database + * @param onComplete Function used to bring the query back to the calling component + * @param onError Function used to bring the error back to the calling component + * @param setValidationStep Function used to bring the current validation step counter + * back to the calling component */ export const queryTranslationThunk = ( @@ -87,6 +91,9 @@ export const queryTranslationThunk = }, onError = (e) => { console.log(e); + }, + setValidationStep = (e) => { + console.log(e); } ) => async (dispatch: any, getState: any) => { @@ -102,8 +109,15 @@ export const queryTranslationThunk = if (!client.driver) { client.setDriver(driver); } + await consoleLogAsync('modelClient', client); const messageHistory = getHistoryPerCard(state, pagenumber, cardId); - let translationRes = await client.queryTranslation(message, messageHistory, database, reportType); + let translationRes = await client.queryTranslation( + message, + messageHistory, + database, + reportType, + setValidationStep + ); query = translationRes[0]; let newHistory = translationRes[1]; await consoleLogAsync('apwmda0owj', newHistory); From b1afe63a424f80a2d148e11191859560b1097f0c Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Tue, 13 Jun 2023 20:04:38 +0200 Subject: [PATCH 31/49] Added skeleton for injecting a pre-report-populating translation function --- src/extensions/ExtensionConfig.tsx | 3 +++ src/extensions/query-translator/util/Util.ts | 7 +++++++ src/report/Report.tsx | 10 ++++++++++ 3 files changed, 20 insertions(+) create mode 100644 src/extensions/query-translator/util/Util.ts diff --git a/src/extensions/ExtensionConfig.tsx b/src/extensions/ExtensionConfig.tsx index 495497562..22b16e11a 100644 --- a/src/extensions/ExtensionConfig.tsx +++ b/src/extensions/ExtensionConfig.tsx @@ -9,6 +9,7 @@ import { QUERY_TRANSLATOR_ACTION_PREFIX } from './query-translator/state/QueryTr import { queryTranslatorReducer } from './query-translator/state/QueryTranslatorReducer'; import QueryTranslatorButton from './query-translator/component/QueryTranslator'; import NeoOverrideCardQueryEditor from './query-translator/component/OverrideCardQueryEditor'; +import { translateQuery } from './query-translator/util/Util'; // TODO: continue documenting interface interface Extension { @@ -24,6 +25,7 @@ interface Extension { drawerButton?: JSX.Element; cardSettingsComponent?: JSX.Element; settingsModal?: JSX.Element; + prepopulateReportFunction: any; // function } // TODO: define extension config interface @@ -93,6 +95,7 @@ export const EXTENSIONS: Record = { reducerPrefix: QUERY_TRANSLATOR_ACTION_PREFIX, reducerObject: queryTranslatorReducer, cardSettingsComponent: NeoOverrideCardQueryEditor, + prepopulateReportFunction: translateQuery, drawerButton: QueryTranslatorButton, description: 'Use natural language to generate Cypher queries in NeoDash. Connect to an LLM through an API, and let NeoDash use your database schema + the report types to generate queries automatically.', diff --git a/src/extensions/query-translator/util/Util.ts b/src/extensions/query-translator/util/Util.ts new file mode 100644 index 000000000..0d2dd1529 --- /dev/null +++ b/src/extensions/query-translator/util/Util.ts @@ -0,0 +1,7 @@ +/** + * TODO: placeholder of function that gets injected before report populating logic. + */ +export function translateQuery(original, setResult) { + console.log('okay'); + setResult(original); +} diff --git a/src/report/Report.tsx b/src/report/Report.tsx index 4a1848b9a..c79d87d75 100644 --- a/src/report/Report.tsx +++ b/src/report/Report.tsx @@ -15,6 +15,7 @@ import { LoadingSpinner } from '@neo4j-ndl/react'; import { ExclamationTriangleIconSolid } from '@neo4j-ndl/react/icons'; import { connect } from 'react-redux'; import { setPageNumberThunk } from '../settings/SettingsThunks'; +import { EXTENSIONS } from '../extensions/ExtensionConfig'; export const NeoReport = ({ database = 'neo4j', // The Neo4j database to run queries onto. @@ -89,6 +90,15 @@ export const NeoReport = ({ const useNodePropsAsFields = reportTypes[type].useNodePropsAsFields == true; const useReturnValuesAsFields = reportTypes[type].useReturnValuesAsFields == true; + // Dynamically run injected extension functions before the report is populated. + Object.keys(extensions) + .filter((e) => extensions[e].active && EXTENSIONS[e].prepopulateReportFunction !== null) + .forEach((e) => { + // TODO: Pass in something here (?) and use the callback to update the query before running the execution of the report population. + EXTENSIONS[e].prepopulateReportFunction('hello', () => {}); + }); + + // const prePopulationFunctions = extensions/ if (debounced) { setStatus(QueryStatus.RUNNING); debouncedRunCypherQuery( From 9a9ab5c204601e654fe3cfbf54237250c725500c Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Wed, 14 Jun 2023 08:55:40 +0200 Subject: [PATCH 32/49] Add skeleton for pre-report population logic --- src/card/Card.tsx | 1 + src/card/view/CardView.tsx | 2 ++ .../state/QueryTranslatorThunks.ts | 4 +++- src/extensions/query-translator/util/Util.ts | 14 +++++++++++--- .../component/SidebarNodeInspectionModal.tsx | 1 + src/modal/ReportExamplesModal.tsx | 1 + src/report/Report.tsx | 16 +++++++++++++--- src/report/ReportWrapper.tsx | 2 ++ 8 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/card/Card.tsx b/src/card/Card.tsx index b5281a780..cef4b65ca 100644 --- a/src/card/Card.tsx +++ b/src/card/Card.tsx @@ -128,6 +128,7 @@ const NeoCard = ({ {active ? ( { + dispatch(createNotification('Error when translating the natural language query', error)); + }) + ); } diff --git a/src/extensions/sidebar/component/SidebarNodeInspectionModal.tsx b/src/extensions/sidebar/component/SidebarNodeInspectionModal.tsx index f000c66a9..599d16b10 100644 --- a/src/extensions/sidebar/component/SidebarNodeInspectionModal.tsx +++ b/src/extensions/sidebar/component/SidebarNodeInspectionModal.tsx @@ -84,6 +84,7 @@ const SidebarNodeInspectionModal = ({
{/* TODO: add missing parameters or make them optional in NeoReportWrapper */} { diff --git a/src/modal/ReportExamplesModal.tsx b/src/modal/ReportExamplesModal.tsx index 985883b2a..b6e5e00d5 100644 --- a/src/modal/ReportExamplesModal.tsx +++ b/src/modal/ReportExamplesModal.tsx @@ -59,6 +59,7 @@ export const NeoReportExamplesModal = ({ database, examples, extensions, navItem }} > {}, ChartType = NeoTableChart, // The report component to render with the query results. }) => { const [records, setRecords] = useState(null); @@ -94,8 +98,9 @@ export const NeoReport = ({ Object.keys(extensions) .filter((e) => extensions[e].active && EXTENSIONS[e].prepopulateReportFunction !== null) .forEach((e) => { - // TODO: Pass in something here (?) and use the callback to update the query before running the execution of the report population. - EXTENSIONS[e].prepopulateReportFunction('hello', () => {}); + EXTENSIONS[e].prepopulateReportFunction(driver, getCustomDispatcher(), pagenumber, id, {}, (result) => { + alert(result); + }); }); // const prePopulationFunctions = extensions/ @@ -285,12 +290,17 @@ export const NeoReport = ({ ); }; -const mapStateToProps = () => ({}); +const mapStateToProps = (state) => ({ + pagenumber: getPageNumber(state), +}); const mapDispatchToProps = (dispatch) => ({ setPageNumber: (index: number) => { dispatch(setPageNumberThunk(index)); }, + getCustomDispatcher: () => { + return dispatch; + }, }); export default connect(mapStateToProps, mapDispatchToProps)(NeoReport); diff --git a/src/report/ReportWrapper.tsx b/src/report/ReportWrapper.tsx index 40d9c7be3..12519c8be 100644 --- a/src/report/ReportWrapper.tsx +++ b/src/report/ReportWrapper.tsx @@ -28,6 +28,7 @@ const ErrorBoundary = withErrorBoundary(({ children, resetTrigger }) => { }); export const NeoReportWrapper = ({ + id, database, query, lastRunTimestamp, @@ -52,6 +53,7 @@ export const NeoReportWrapper = ({ return ( Date: Wed, 14 Jun 2023 09:16:03 +0200 Subject: [PATCH 33/49] Bumped OpenAI lib version --- package.json | 2 +- src/card/settings/CardSettingsHeader.tsx | 5 +++-- src/report/Report.tsx | 1 - yarn.lock | 15 +++++---------- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index e70a0f11b..2ffdcf99d 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "mui-color": "^2.0.0-beta.2", "mui-nested-menu": "^3.2.1", "neo4j-client-sso": "^1.2.2", - "openai": "^3.2.1", + "openai": "^3.3.0", "postcss": "^8.4.21", "postcss-loader": "^7.2.4", "postcss-preset-env": "^8.3.0", diff --git a/src/card/settings/CardSettingsHeader.tsx b/src/card/settings/CardSettingsHeader.tsx index 379279ee1..baa3be970 100644 --- a/src/card/settings/CardSettingsHeader.tsx +++ b/src/card/settings/CardSettingsHeader.tsx @@ -9,6 +9,7 @@ import { TrashIconOutline, DocumentDuplicateIconOutline, PlayCircleIconSolid, + EllipsisVerticalIconOutline, } from '@neo4j-ndl/react/icons'; const NeoCardSettingsHeader = ({ @@ -59,7 +60,7 @@ const NeoCardSettingsHeader = ({ action={ <> {fullscreenEnabled ? expanded ? unMaximizeButton : maximizeButton : <>} - + { @@ -69,7 +70,7 @@ const NeoCardSettingsHeader = ({ clean size='medium' > - + diff --git a/src/report/Report.tsx b/src/report/Report.tsx index 4d08603b9..4a2cc727e 100644 --- a/src/report/Report.tsx +++ b/src/report/Report.tsx @@ -103,7 +103,6 @@ export const NeoReport = ({ }); }); - // const prePopulationFunctions = extensions/ if (debounced) { setStatus(QueryStatus.RUNNING); debouncedRunCypherQuery( diff --git a/yarn.lock b/yarn.lock index 491e7ab4b..ef650d11e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8825,10 +8825,10 @@ open@^8.0.9, open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" -openai@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866" - integrity sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A== +openai@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-3.3.0.tgz#a6408016ad0945738e1febf43f2fccca83a3f532" + integrity sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ== dependencies: axios "^0.26.0" form-data "^4.0.0" @@ -11686,12 +11686,7 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.1.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" - integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== - -yaml@^2.1.3, yaml@^2.2.1: +yaml@^2.1.1, yaml@^2.1.3, yaml@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== From be721bba8ac5966379bbad810830e6e888ae4b79 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Wed, 14 Jun 2023 09:28:28 +0200 Subject: [PATCH 34/49] Fixes --- src/extensions/query-translator/util/Util.ts | 20 +++++++++++++++++--- src/report/Report.tsx | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/extensions/query-translator/util/Util.ts b/src/extensions/query-translator/util/Util.ts index 8279837dd..b1f982e2e 100644 --- a/src/extensions/query-translator/util/Util.ts +++ b/src/extensions/query-translator/util/Util.ts @@ -1,4 +1,5 @@ import { createNotification } from '../../../application/ApplicationActions'; +import { updateReportQueryThunk } from '../../../card/CardThunks'; import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; /** @@ -7,9 +8,22 @@ import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; export function translateQuery(driver, dispatch, pagenumber, id, extensions, setResult) { // TODO get english question from extensions config. // TODO - only trigger the translation if the latest english wasn't already translated, or if the english query is ''. + console.log(extensions); dispatch( - queryTranslationThunk(pagenumber, id, 'show me a movie with tom hanks', 'table', driver, setResult, (error) => { - dispatch(createNotification('Error when translating the natural language query', error)); - }) + queryTranslationThunk( + pagenumber, + id, + 'show me a movie with tom hanks', + 'table', + driver, + (result) => { + alert(result); + // dispatch(updateReportQueryThunk(id, result)); + setResult(result); + }, + (error) => { + dispatch(createNotification('Error when translating the natural language query', error)); + } + ) ); } diff --git a/src/report/Report.tsx b/src/report/Report.tsx index 4a2cc727e..352a6af66 100644 --- a/src/report/Report.tsx +++ b/src/report/Report.tsx @@ -98,7 +98,7 @@ export const NeoReport = ({ Object.keys(extensions) .filter((e) => extensions[e].active && EXTENSIONS[e].prepopulateReportFunction !== null) .forEach((e) => { - EXTENSIONS[e].prepopulateReportFunction(driver, getCustomDispatcher(), pagenumber, id, {}, (result) => { + EXTENSIONS[e].prepopulateReportFunction(driver, getCustomDispatcher(), pagenumber, id, extensions, (result) => { alert(result); }); }); From 0ee9077ba492964476ec4080c933fccaac35643e Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Wed, 14 Jun 2023 11:42:27 +0200 Subject: [PATCH 35/49] Fixed glitchy caching in english editor --- .../component/OverrideCardQueryEditor.tsx | 19 +++++++++++++++---- .../state/QueryTranslatorSelector.ts | 8 ++++---- src/extensions/query-translator/util/Util.ts | 9 +++++---- src/report/Report.tsx | 14 +++++++++++--- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx index 87de11773..35d3da521 100644 --- a/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx +++ b/src/extensions/query-translator/component/OverrideCardQueryEditor.tsx @@ -1,5 +1,5 @@ import ReportIcon from '@mui/icons-material/Report'; -import React, { useCallback, useContext } from 'react'; +import React, { useCallback, useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { IconButton, Switch } from '@neo4j-ndl/react'; @@ -12,6 +12,7 @@ import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; import debounce from 'lodash/debounce'; import { updateLastMessage } from '../state/QueryTranslatorActions'; import { createNotification } from '../../../application/ApplicationActions'; +import { getLastMessage } from '../state/QueryTranslatorSelector'; // TODO - rename to 'Node Sidebar Extension button' to reflect better the functionality. export const NeoOverrideCardQueryEditor = ({ @@ -21,6 +22,7 @@ export const NeoOverrideCardQueryEditor = ({ extensions, reportType, updateCypherQuery, + lastMessage, translateQuery, updateEnglishQuery, displayError, @@ -35,6 +37,13 @@ export const NeoOverrideCardQueryEditor = ({ const [englishQuestion, setEnglishQuestion] = React.useState(''); const debouncedEnglishQuestionUpdate = useCallback(debounce(updateEnglishQuery, 250), []); + useEffect(() => { + // Reset text to the dashboard state when the page gets reorganized. + if (lastMessage !== englishQuestion) { + setEnglishQuestion(lastMessage); + } + }, [lastMessage]); + const reportTypes = getReportTypes(extensions); const cypherEditor = ( @@ -56,7 +65,7 @@ export const NeoOverrideCardQueryEditor = ({ // To prevent a bug with the code editor component, we wrap it in an extra enclosing bracket. const englishEditor = ( - <> +
- +
); const { driver } = useContext(Neo4jContext); @@ -148,7 +157,9 @@ export const NeoOverrideCardQueryEditor = ({ ); }; -const mapStateToProps = () => ({}); +const mapStateToProps = (state, ownProps) => ({ + lastMessage: getLastMessage(state, ownProps.pagenumber, ownProps.reportId), +}); const mapDispatchToProps = (dispatch) => ({ translateQuery: (pagenumber, reportId, text, reportType, driver, onComplete, onError) => { diff --git a/src/extensions/query-translator/state/QueryTranslatorSelector.ts b/src/extensions/query-translator/state/QueryTranslatorSelector.ts index c9e0305e6..e3bbf5b79 100644 --- a/src/extensions/query-translator/state/QueryTranslatorSelector.ts +++ b/src/extensions/query-translator/state/QueryTranslatorSelector.ts @@ -68,13 +68,13 @@ export const getHistoryPerCard = (state: any, pagenumber, cardId) => { * We persist the last message sent from the user to the model. * @param state State of the application * @param pagenumber Number of the page where the card lives - * @param cardIndex Unique identifier of the card + * @param id Unique identifier of the card * @returns */ -export const getLastMessage = (state: any, pagenumber, cardIndex) => { +export const getLastMessage = (state: any, pagenumber, id) => { let messages = getLastMessages(state); - let lastMessage = messages[pagenumber] && messages[pagenumber][cardIndex]; - return lastMessage != undefined && lastMessage ? lastMessage : []; + let lastMessage = messages[pagenumber] && messages[pagenumber][id]; + return lastMessage !== undefined ? lastMessage : ''; }; export const getApiKey = (state: any) => { diff --git a/src/extensions/query-translator/util/Util.ts b/src/extensions/query-translator/util/Util.ts index b1f982e2e..e4d323e22 100644 --- a/src/extensions/query-translator/util/Util.ts +++ b/src/extensions/query-translator/util/Util.ts @@ -5,20 +5,21 @@ import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; /** * TODO: placeholder of function that gets injected before report populating logic. */ -export function translateQuery(driver, dispatch, pagenumber, id, extensions, setResult) { +export function translateQuery(driver, dispatch, pagenumber, id, type, extensions, setResult) { // TODO get english question from extensions config. // TODO - only trigger the translation if the latest english wasn't already translated, or if the english query is ''. console.log(extensions); + + const message = extensions['query-translator'].lastMessages[pagenumber][id]; dispatch( queryTranslationThunk( pagenumber, id, - 'show me a movie with tom hanks', - 'table', + message, + type, driver, (result) => { alert(result); - // dispatch(updateReportQueryThunk(id, result)); setResult(result); }, (error) => { diff --git a/src/report/Report.tsx b/src/report/Report.tsx index 352a6af66..f181c3764 100644 --- a/src/report/Report.tsx +++ b/src/report/Report.tsx @@ -98,9 +98,17 @@ export const NeoReport = ({ Object.keys(extensions) .filter((e) => extensions[e].active && EXTENSIONS[e].prepopulateReportFunction !== null) .forEach((e) => { - EXTENSIONS[e].prepopulateReportFunction(driver, getCustomDispatcher(), pagenumber, id, extensions, (result) => { - alert(result); - }); + EXTENSIONS[e].prepopulateReportFunction( + driver, + getCustomDispatcher(), + pagenumber, + id, + type, + extensions, + (result) => { + alert(result); + } + ); }); if (debounced) { From ab839cfd04c96e6915fd433515b7340a33348de9 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Wed, 14 Jun 2023 11:53:06 +0200 Subject: [PATCH 36/49] Fix bugs --- src/extensions/query-translator/util/Util.ts | 2 - src/report/Report.tsx | 85 +++++++++++--------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/extensions/query-translator/util/Util.ts b/src/extensions/query-translator/util/Util.ts index e4d323e22..de1c76faf 100644 --- a/src/extensions/query-translator/util/Util.ts +++ b/src/extensions/query-translator/util/Util.ts @@ -6,7 +6,6 @@ import { queryTranslationThunk } from '../state/QueryTranslatorThunks'; * TODO: placeholder of function that gets injected before report populating logic. */ export function translateQuery(driver, dispatch, pagenumber, id, type, extensions, setResult) { - // TODO get english question from extensions config. // TODO - only trigger the translation if the latest english wasn't already translated, or if the english query is ''. console.log(extensions); @@ -19,7 +18,6 @@ export function translateQuery(driver, dispatch, pagenumber, id, type, extension type, driver, (result) => { - alert(result); setResult(result); }, (error) => { diff --git a/src/report/Report.tsx b/src/report/Report.tsx index f181c3764..9819fb5bb 100644 --- a/src/report/Report.tsx +++ b/src/report/Report.tsx @@ -94,10 +94,53 @@ export const NeoReport = ({ const useNodePropsAsFields = reportTypes[type].useNodePropsAsFields == true; const useReturnValuesAsFields = reportTypes[type].useReturnValuesAsFields == true; + // Logic to run a query + const executeQuery = (newQuery) => { + if (debounced) { + setStatus(QueryStatus.RUNNING); + debouncedRunCypherQuery( + driver, + database, + newQuery, + parameters, + rowLimit, + setStatus, + setRecords, + setFields, + fields, + useNodePropsAsFields, + useReturnValuesAsFields, + HARD_ROW_LIMITING, + queryTimeLimit + ); + } else { + runCypherQuery( + driver, + database, + newQuery, + parameters, + rowLimit, + setStatus, + setRecords, + setFields, + fields, + useNodePropsAsFields, + useReturnValuesAsFields, + HARD_ROW_LIMITING, + queryTimeLimit + ); + } + }; + // Dynamically run injected extension functions before the report is populated. - Object.keys(extensions) - .filter((e) => extensions[e].active && EXTENSIONS[e].prepopulateReportFunction !== null) - .forEach((e) => { + // If a custom prepopulating function is present... + // ... Await for the prepopulating function to complete before running the (normal) query logic. + const prepopulationFunctions = Object.keys(extensions).filter( + (e) => extensions[e].active && EXTENSIONS[e].prepopulateReportFunction !== null + ); + + if (prepopulationFunctions.length > 0) { + prepopulationFunctions.forEach((e) => { EXTENSIONS[e].prepopulateReportFunction( driver, getCustomDispatcher(), @@ -106,44 +149,12 @@ export const NeoReport = ({ type, extensions, (result) => { - alert(result); + executeQuery(result); } ); }); - - if (debounced) { - setStatus(QueryStatus.RUNNING); - debouncedRunCypherQuery( - driver, - database, - query, - parameters, - rowLimit, - setStatus, - setRecords, - setFields, - fields, - useNodePropsAsFields, - useReturnValuesAsFields, - HARD_ROW_LIMITING, - queryTimeLimit - ); } else { - runCypherQuery( - driver, - database, - query, - parameters, - rowLimit, - setStatus, - setRecords, - setFields, - fields, - useNodePropsAsFields, - useReturnValuesAsFields, - HARD_ROW_LIMITING, - queryTimeLimit - ); + executeQuery(query); } }; From 4083b63bc12744de6b08791d2df4d351f67e8933 Mon Sep 17 00:00:00 2001 From: Alfred Rubin Date: Wed, 14 Jun 2023 13:02:14 +0200 Subject: [PATCH 37/49] moved model client inside SessionStorage, fixed error in query-translator utils and fixed error while updating client settings from querytranslator.tsx --- .../query-translator/clients/ModelClient.ts | 1 + .../component/ClientSettings.tsx | 4 +-- .../component/QueryTranslator.tsx | 29 +++------------ .../QueryTranslatorSettingsModal.tsx | 17 +++++++-- .../state/QueryTranslatorActions.ts | 12 ++++--- .../state/QueryTranslatorReducer.ts | 6 ---- .../state/QueryTranslatorSelector.ts | 12 ++++--- .../state/QueryTranslatorThunks.ts | 2 -- src/extensions/query-translator/util/Util.ts | 35 ++++++++++--------- 9 files changed, 54 insertions(+), 64 deletions(-) diff --git a/src/extensions/query-translator/clients/ModelClient.ts b/src/extensions/query-translator/clients/ModelClient.ts index 5f298def9..9e283ef5c 100644 --- a/src/extensions/query-translator/clients/ModelClient.ts +++ b/src/extensions/query-translator/clients/ModelClient.ts @@ -148,6 +148,7 @@ export abstract class ModelClient { let modelAnswer = await this.chatCompletion(tmpHistory); tmpHistory.push(modelAnswer); + await consoleLogAsync('tmpHistory', tmpHistory); // and try to validate it let validationResult = await this.validateQuery(modelAnswer, database); isValidated = validationResult[0]; diff --git a/src/extensions/query-translator/component/ClientSettings.tsx b/src/extensions/query-translator/component/ClientSettings.tsx index 5bf8f2578..01e0a4c9e 100644 --- a/src/extensions/query-translator/component/ClientSettings.tsx +++ b/src/extensions/query-translator/component/ClientSettings.tsx @@ -26,7 +26,7 @@ export const ClientSettings = ({ updateModelProvider, updateClientSettings, deleteAllMessageHistory, - setOpen, + handleClose, }) => { const defaultSettings = getQueryTranslatorDefaultConfig(modelProvider); const requiredSettings = Object.keys(defaultSettings).filter((setting) => defaultSettings[setting].required); @@ -151,7 +151,7 @@ export const ClientSettings = ({