Skip to content

Commit

Permalink
adds support for export / import of assistant configs, including part…
Browse files Browse the repository at this point in the history
…ials imports, in json and yaml (microsoft#129)
  • Loading branch information
bkrabach authored Oct 16, 2024
1 parent 2484c60 commit 2c6cc01
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 0 deletions.
2 changes: 2 additions & 0 deletions workbench-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"deepmerge": "^4.3.1",
"diff": "^7.0.0",
"js-tiktoken": "^1.0.12",
"js-yaml": "^4.1.0",
"lexical": "^0.17.1",
"mermaid": "^10.9.0",
"microsoft-cognitiveservices-speech-sdk": "^1.38.0",
Expand All @@ -71,6 +72,7 @@
"@rollup/plugin-replace": "^5.0.7",
"@types/debug": "^4.1.12",
"@types/diff": "^5.2.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.10.3",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
Expand Down
11 changes: 11 additions & 0 deletions workbench-app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from '@fluentui/react-components';
import YAML from 'js-yaml';
import React from 'react';

interface AssistantConfigExportButtonProps {
config: object;
assistantId: string;
}

export const AssistantConfigExportButton: React.FC<AssistantConfigExportButtonProps> = ({ config, assistantId }) => {
const exportConfig = (format: 'json' | 'yaml') => {
let content = '';
let filename = `config_${assistantId}`;

try {
if (format === 'yaml') {
content = YAML.dump(config);
filename += '.yaml';
} else {
content = JSON.stringify(config, null, 2);
filename += '.json';
}

const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();

URL.revokeObjectURL(url);
} catch (error) {
console.error('Error while generating config file:', error);
}
};

const primaryActionButtonProps = {
onClick: () => exportConfig('json'),
};

return (
<Menu positioning="below-end">
<MenuTrigger disableButtonEnhancement>
{(triggerProps) => (
<SplitButton menuButton={triggerProps} primaryActionButton={primaryActionButtonProps}>
Export Config
</SplitButton>
)}
</MenuTrigger>

<MenuPopover>
<MenuList>
<MenuItem onClick={() => exportConfig('json')}>JSON Format</MenuItem>
<MenuItem onClick={() => exportConfig('yaml')}>YAML Format</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Button } from '@fluentui/react-components';
import YAML from 'js-yaml';
import React, { useRef } from 'react';
import { useAppDispatch } from '../../redux/app/hooks'; // Import the relevant hooks
import { addError } from '../../redux/features/app/appSlice'; // Import the error action

interface AssistantConfigImportButtonProps {
onImport: (config: object) => void;
}

export const AssistantConfigImportButton: React.FC<AssistantConfigImportButtonProps> = ({ onImport }) => {
const fileInputRef = useRef<HTMLInputElement | null>(null);
const dispatch = useAppDispatch(); // Use the dispatch hook

const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target?.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result as string;
let importedConfig;

if (file.name.endsWith('.yaml') || file.name.endsWith('.yml')) {
importedConfig = YAML.load(content);
} else {
importedConfig = JSON.parse(content);
}

if (typeof importedConfig === 'object' && importedConfig !== null) {
onImport(importedConfig as object);
} else {
throw new Error('Invalid configuration format');
}
} catch (error) {
console.error('Error reading configuration file:', error);
dispatch(
addError({
title: 'Import Error',
message:
'There was an error importing the configuration file. Please check the file format and contents.',
}),
);
}
};
reader.readAsText(file);
}
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};

return (
<div>
<input type="file" hidden ref={fileInputRef} accept=".json,.yaml,.yml" onChange={handleFileChange} />
<Button onClick={() => fileInputRef.current?.click()}>Import Config</Button>
</div>
);
};
11 changes: 11 additions & 0 deletions workbench-app/src/components/Assistants/AssistantEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { CustomizedObjectFieldTemplate } from '../App/FormWidgets/CustomizedObje
import { InspectableWidget } from '../App/FormWidgets/InspectableWidget';
import { Loading } from '../App/Loading';
import { ApplyConfigButton } from './ApplyConfigButton';
import { AssistantConfigExportButton } from './AssistantConfigExportButton';
import { AssistantConfigImportButton } from './AssistantConfigImportButton';

const log = debug(Constants.debug.root).extend('AssistantEdit');

Expand Down Expand Up @@ -114,6 +116,13 @@ export const AssistantEdit: React.FC<AssistantInstanceEditProps> = (props) => {
setFormData(config);
};

const mergeConfigurations = (uploadedConfig: object) => {
if (!config) return;

const updatedConfig = Utility.deepMerge(config.config, uploadedConfig);
setFormData(updatedConfig);
};

const widgets: RegistryWidgetsType = {
inspectable: InspectableWidget,
baseModelEditor: BaseModelEditorWidget,
Expand Down Expand Up @@ -160,6 +169,8 @@ export const AssistantEdit: React.FC<AssistantInstanceEditProps> = (props) => {
newConfig={defaults}
onApply={restoreConfig}
/>
<AssistantConfigExportButton config={config?.config || {}} assistantId={assistant.id} />
<AssistantConfigImportButton onImport={mergeConfigurations} />
{!isValid && (
<div className={classes.warning}>
<Warning24Filled /> Configuration has missing or invalid values
Expand Down

0 comments on commit 2c6cc01

Please sign in to comment.