Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] AI Assistant [2] #2777

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/demo-app/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<link href="https://unpkg.com/maplibre-gl@^3/dist/maplibre-gl.css" rel="stylesheet">
<link rel="stylesheet" href="/bundle.css">
<style type="text/css">
body {margin: 0; padding: 0; overflow: hidden;}
body {margin: 0; padding: 0; overflow: hidden;}
</style>
</head>
<body>
Expand Down
3 changes: 2 additions & 1 deletion examples/demo-app/esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ const config = {
'process.env.FoursquareClientId': JSON.stringify(process.env.FoursquareClientId || ''),
'process.env.FoursquareDomain': JSON.stringify(process.env.FoursquareDomain || ''),
'process.env.FoursquareAPIURL': JSON.stringify(process.env.FoursquareAPIURL || ''),
'process.env.FoursquareUserMapsURL': JSON.stringify(process.env.FoursquareUserMapsURL || '')
'process.env.FoursquareUserMapsURL': JSON.stringify(process.env.FoursquareUserMapsURL || ''),
'process.env.OpenAIToken': JSON.stringify(process.env.OpenAIToken || '')
},
plugins: [
// automatically injected kepler.gl package version into the bundle
Expand Down
3 changes: 2 additions & 1 deletion examples/demo-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
},
"packageManager": "[email protected]",
"devDependencies": {
"esbuild-plugin-replace": "^1.4.0"
"esbuild-plugin-replace": "^1.4.0",
"redux-logger": "^3.0.6"
}
}
32 changes: 22 additions & 10 deletions examples/demo-app/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import {combineReducers, createStore, applyMiddleware, compose} from 'redux';
import {routerReducer, routerMiddleware} from 'react-router-redux';
import {browserHistory} from 'react-router';
import {enhanceReduxMiddleware} from '@kepler.gl/reducers';
import {createLogger} from 'redux-logger';
import {enhanceReduxMiddleware} from '@kepler.gl/reducers';
import thunk from 'redux-thunk';
// eslint-disable-next-line no-unused-vars
import window from 'global/window';
Expand All @@ -17,16 +17,28 @@ const reducers = combineReducers({
routing: routerReducer
});

export const middlewares = enhanceReduxMiddleware([thunk, routerMiddleware(browserHistory)]);
// add redux-logger
const loggerMiddleware = createLogger({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be a separate PR?

predicate: (_getState, action) => {
// skip logging in production mode
if (process.env.NODE_ENV === 'production') {
return false;
}
const skipLogging = [
'@@kepler.gl/LAYER_HOVER',
'@@kepler.gl/MOUSE_MOVE',
'@@kepler.gl/UPDATE_MAP'
];
return !skipLogging.includes(action.type);
},
collapsed: () => true // Collapse all actions for more compact log
});

// eslint-disable-next-line no-process-env
if (NODE_ENV === 'local') {
// Redux logger
const logger = createLogger({
collapsed: () => true // Collapse all actions for more compact log
});
middlewares.push(logger);
}
export const middlewares = enhanceReduxMiddleware([
thunk,
routerMiddleware(browserHistory),
loggerMiddleware
]);

export const enhancers = [applyMiddleware(...middlewares)];

Expand Down
4 changes: 3 additions & 1 deletion src/ai-assistant/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
"@kepler.gl/layers": "3.1.0-alpha.0",
"@kepler.gl/types": "3.1.0-alpha.0",
"@kepler.gl/utils": "3.1.0-alpha.0",
"color-interpolate": "^1.0.5",
"echarts": "^5.5.1",
"global": "^4.3.0",
"react-ai-assist": "0.0.8"
"react-ai-assist": "0.0.20"
},
"nyc": {
"sourceMap": false,
Expand Down
22 changes: 22 additions & 0 deletions src/ai-assistant/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const UPDATE_AI_ASSISTANT_CONFIG = `${ACTION_PREFIX}UPDATE_AI_ASSISTANT_C
export const UPDATE_AI_ASSISTANT_MESSAGES = `${ACTION_PREFIX}UPDATE_AI_ASSISTANT_MESSAGES`;
export const SET_START_SCREEN_CAPTURE = `${ACTION_PREFIX}SET_START_SCREEN_CAPTURE`;
export const SET_SCREEN_CAPTURED = `${ACTION_PREFIX}SET_SCREEN_CAPTURED`;
export const ADD_DATASET_CONTEXT = `${ACTION_PREFIX}ADD_DATASET_CONTEXT`;
export const REMOVE_DATASET_CONTEXT = `${ACTION_PREFIX}REMOVE_DATASET_CONTEXT`;
export const RESET_DATASET_CONTEXT = `${ACTION_PREFIX}RESET_DATASET_CONTEXT`;

// Action creators
export function updateAiAssistantConfig(config: AiAssistantConfig) {
Expand Down Expand Up @@ -39,3 +42,22 @@ export function setScreenCaptured(screenshot: string) {
payload: screenshot
};
}

export function addDatasetContext(datasetId: string) {
return {
type: ADD_DATASET_CONTEXT,
payload: datasetId
};
}

export function removeDatasetContext(datasetId: string) {
return {
type: REMOVE_DATASET_CONTEXT,
payload: datasetId
};
}
export function resetDatasetContext() {
return {
type: RESET_DATASET_CONTEXT
};
}
76 changes: 60 additions & 16 deletions src/ai-assistant/src/components/ai-assistant-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@

import React, {useEffect} from 'react';
import styled, {withTheme} from 'styled-components';
import {AiAssistant, MessageModel, useAssistant} from 'react-ai-assist';
import 'react-ai-assist/dist/index.css';
import {
AiAssistant,
MessageModel,
useAssistant,
histogramFunctionDefinition
} from 'react-ai-assist';
import 'react-ai-assist/../dist/index.css';

import {textColorLT} from '@kepler.gl/styles';
import {ActionHandler, addDataToMap, loadFiles, mapStyleChange} from '@kepler.gl/actions';
import {ActionHandler} from '@kepler.gl/actions';
import {MapStyle} from '@kepler.gl/reducers';
import {Loader} from '@loaders.gl/loader-utils';
import {VisState} from '@kepler.gl/schemas';

import {basemapFunctionDefinition} from '../tools/basemap-functions';
import {loadUrlFunctionDefinition} from '../tools/loadurl-function';
Expand All @@ -23,23 +28,21 @@ import {
INSTRUCTIONS,
WELCOME_MESSAGE
} from '../constants';
import {filterFunctionDefinition} from '../tools/filter-function';
import {addLayerFunctionDefinition} from '../tools/layer-creation-function';
import {updateLayerColorFunctionDefinition} from '../tools/layer-style-function';
import {SelectedKeplerGlActions} from './ai-assistant-manager';
import {getDatasetContext, getValuesFromDataset, highlightRows} from '../tools/utils';

export type AiAssistantComponentProps = {
theme: any;
aiAssistant: AiAssistantState;
updateAiAssistantMessages: ActionHandler<typeof updateAiAssistantMessages>;
setStartScreenCapture: ActionHandler<typeof setStartScreenCapture>;
setScreenCaptured: ActionHandler<typeof setScreenCaptured>;
keplerGlActions: {
mapStyleChange: ActionHandler<typeof mapStyleChange>;
loadFiles: ActionHandler<typeof loadFiles>;
addDataToMap: ActionHandler<typeof addDataToMap>;
};
keplerGlActions: SelectedKeplerGlActions;
mapStyle: MapStyle;
visState: {
loaders: Loader[];
loadOptions: object;
};
visState: VisState;
};

const StyledAiAssistantComponent = styled.div`
Expand Down Expand Up @@ -70,6 +73,33 @@ function AiAssistantComponentFactory() {
addDataToMap: keplerGlActions.addDataToMap,
loaders: visState.loaders,
loadOptions: visState.loadOptions
}),
addLayerFunctionDefinition({
addLayer: keplerGlActions.addLayer,
datasets: visState.datasets
}),
updateLayerColorFunctionDefinition({
layerVisualChannelConfigChange: keplerGlActions.layerVisualChannelConfigChange,
layers: visState.layers
}),
filterFunctionDefinition({
datasets: visState.datasets,
filters: visState.filters,
createOrUpdateFilter: keplerGlActions.createOrUpdateFilter,
setFilter: keplerGlActions.setFilter,
setFilterPlot: keplerGlActions.setFilterPlot
}),
histogramFunctionDefinition({
getValues: (datasetName: string, variableName: string): number[] =>
getValuesFromDataset(visState.datasets, datasetName, variableName),
onSelected: (datasetName: string, selectedRowIndices: number[]) =>
highlightRows(
visState.datasets,
visState.layers,
datasetName,
selectedRowIndices,
keplerGlActions.layerSetIsValid
)
})
];

Expand All @@ -87,12 +117,25 @@ function AiAssistantComponentFactory() {
functions
};

const {initializeAssistant} = useAssistant(assistantProps);
const {initializeAssistant, addAdditionalContext} = useAssistant(assistantProps);

const initializeAssistantWithContext = async () => {
await initializeAssistant();
const context = await getDatasetContext(visState.datasets);
addAdditionalContext({context});
};

useEffect(() => {
initializeAssistant();
initializeAssistantWithContext();
// re-initialize assistant when datasets, filters or layers change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [visState.datasets, visState.filters, visState.layers]);

const onRestartAssistant = () => {
// clean up aiAssistant state
updateAiAssistantMessages([]);
initializeAssistantWithContext();
};

const onMessagesUpdated = (messages: MessageModel[]) => {
updateAiAssistantMessages(messages);
Expand Down Expand Up @@ -121,6 +164,7 @@ function AiAssistantComponentFactory() {
onScreenshotClick={onScreenshotClick}
screenCapturedBase64={aiAssistant.screenshotToAsk.screenCaptured}
onRemoveScreenshot={onRemoveScreenshot}
onRestartChat={onRestartAssistant}
fontSize={'text-tiny'}
botMessageClassName={''}
githubIssueLink={'https://github.com/keplergl/kepler.gl/issues'}
Expand Down
2 changes: 1 addition & 1 deletion src/ai-assistant/src/components/ai-assistant-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ function AiAssistantConfigFactory(RangeSlider: ReturnType<typeof RangeSliderFact
}) => {
const [provider, setProvider] = useState(aiAssistantConfig.provider || 'openai');
const [model, setModel] = useState(aiAssistantConfig.model || PROVIDER_MODELS[provider][0]);
const [apiKey, setApiKey] = useState(aiAssistantConfig.apiKey || '');
const [apiKey, setApiKey] = useState(aiAssistantConfig.apiKey || process.env.OpenAIToken || '');
const [temperature, setTemperature] = useState(aiAssistantConfig.temperature || 0.8);
const [topP, setTopP] = useState(aiAssistantConfig.topP || 0.8);
const [baseUrl, setBaseUrl] = useState(aiAssistantConfig.baseUrl || 'http://localhost:11434');
Expand Down
58 changes: 41 additions & 17 deletions src/ai-assistant/src/components/ai-assistant-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,19 @@ import React, {useCallback} from 'react';
import styled from 'styled-components';
import {injectIntl, IntlShape} from 'react-intl';

import {MapStyle} from '@kepler.gl/reducers';
import {ActionHandler, mapStyleChange, loadFiles, addDataToMap} from '@kepler.gl/actions';
import {MapStyle, mapStyleLens, visStateLens} from '@kepler.gl/reducers';
import {
ActionHandler,
mapStyleChange,
loadFiles,
addDataToMap,
addLayer,
createOrUpdateFilter,
setFilter,
setFilterPlot,
layerSetIsValid,
layerVisualChannelConfigChange
} from '@kepler.gl/actions';
import {withState, SidePanelTitleFactory, Icons} from '@kepler.gl/components';
import {VisState} from '@kepler.gl/schemas';

Expand All @@ -20,18 +31,26 @@ import {
import AiAssistantConfigFactory from './ai-assistant-config';
import AiAssistantComponentFactory from './ai-assistant-component';

export type SelectedKeplerGlActions = {
mapStyleChange: ActionHandler<typeof mapStyleChange>;
loadFiles: ActionHandler<typeof loadFiles>;
addDataToMap: ActionHandler<typeof addDataToMap>;
addLayer: ActionHandler<typeof addLayer>;
layerVisualChannelConfigChange: ActionHandler<typeof layerVisualChannelConfigChange>;
createOrUpdateFilter: ActionHandler<typeof createOrUpdateFilter>;
setFilter: ActionHandler<typeof setFilter>;
setFilterPlot: ActionHandler<typeof setFilterPlot>;
layerSetIsValid: ActionHandler<typeof layerSetIsValid>;
};

export type AiAssistantManagerState = {
aiAssistantActions: {
updateAiAssistantConfig: ActionHandler<typeof updateAiAssistantConfig>;
updateAiAssistantMessages: ActionHandler<typeof updateAiAssistantMessages>;
setStartScreenCapture: ActionHandler<typeof setStartScreenCapture>;
setScreenCaptured: ActionHandler<typeof setScreenCaptured>;
};
keplerGlActions: {
mapStyleChange: ActionHandler<typeof mapStyleChange>;
loadFiles: ActionHandler<typeof loadFiles>;
addDataToMap: ActionHandler<typeof addDataToMap>;
};
keplerGlActions: SelectedKeplerGlActions;
aiAssistant: AiAssistantState;
mapStyle: MapStyle;
visState: VisState;
Expand All @@ -50,6 +69,8 @@ const StyledAiAssistantPanelContainer = styled.div`
justify-content: space-between;
overflow: hidden;
height: 100%;
min-width: ${props => props.theme.aiAssistantPanelWidth}px;
max-width: ${props => props.theme.aiAssistantPanelWidth}px;
& > * {
/* all children should allow input */
pointer-events: all;
Expand All @@ -68,7 +89,6 @@ const StyledAiAssistantPanel = styled.div`
const StyledAiAssistantPanelHeader = styled.div`
padding: 16px 16px 4px 16px;
border-bottom: 1px solid ${props => props.theme.borderColor};
min-width: 345px;
color: ${props => props.theme.subtextColorActive};
`;

Expand Down Expand Up @@ -142,22 +162,26 @@ function AiAssistantManagerFactory(
};

return withState(
[],
[visStateLens, mapStyleLens],
state => {
// todo: find a better way to get the state key
const stateKey = Object.keys(state)[0];
const mapKey = Object.keys(state[stateKey].keplerGl)[0];
return {
aiAssistant: state[stateKey].aiAssistant,
mapStyle: state[stateKey].keplerGl[mapKey].mapStyle,
visState: {
loaders: state[stateKey].keplerGl[mapKey].visState.loaders,
loadOptions: state[stateKey].keplerGl[mapKey].visState.loadOptions
}
aiAssistant: state[stateKey].aiAssistant
};
},
{
keplerGlActions: {mapStyleChange, loadFiles, addDataToMap},
keplerGlActions: {
mapStyleChange,
loadFiles,
addDataToMap,
addLayer,
createOrUpdateFilter,
setFilter,
setFilterPlot,
layerSetIsValid,
layerVisualChannelConfigChange
},
aiAssistantActions: {
updateAiAssistantConfig,
updateAiAssistantMessages,
Expand Down
27 changes: 21 additions & 6 deletions src/ai-assistant/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

export const TASK_LIST = '1. Change the basemap style.\n2. Load data from url.';
export const TASK_LIST =
'1. Change the basemap style.\n2. Load data from url.\n3. Create a map layer using variable.\n4. Filter the data of a variable.\n5. Create a histogram.';

export const WELCOME_MESSAGE = `Hi, I am Kepler.gl AI Assistant!\nHere are some tasks I can help you with:\n\n${TASK_LIST}`;

export const INSTRUCTIONS = `You are a helpful assistant that can answer questions and help with tasks. If you can use tools, please use them. If the parameters of functions are not provided, please ask user to specify them. Otherwise, just answer the question with plain textdirectly. Do not include any programming code in your response. You can do the following tasks: ${TASK_LIST}`;
export const INSTRUCTIONS = `You are a Kepler.gl AI Assistant that can answer questions and help with tasks of mapping and spatial data analysis.

When responding to user queries:
1. Analyze if the task requires one or multiple function calls
2. For each required function:
- Identify the appropriate function to call
- Determine all required parameters
- If parameters are missing, ask the user to provide them
- Execute functions in a sequential order

You can execute multiple functions to complete complex tasks, but execute them one at a time in a logical sequence. Always validate the success of each function call before proceeding to the next one.

Remember to:
- Return function calls in a structured format that can be parsed and executed
- Wait for confirmation of each function's completion before proceeding
- Prompt user to proceed to the next function if needed
- Provide clear feedback about what action is being taken
- Do not include raw programming code in responses to users`;

export const ASSISTANT_NAME = 'kepler-gl-ai-assistant';

export const ASSISTANT_DESCRIPTION = 'A Kepler.gl AI Assistant';

export const ASSISTANT_VERSION = '0.0.1-1';
export const ASSISTANT_VERSION = '0.0.1-9';
Loading
Loading