diff --git a/docker-compose.yml b/docker-compose.yml index 012922e..a5ad422 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,7 @@ services: - VITE_REACT_APP_AAD_APP_CLIENT_ID=${AAD_APP_CLIENT_ID} - VITE_REACT_APP_AAD_APP_TENANT_ID=${AAD_APP_TENANT_ID} - VITE_REACT_APP_AAD_APP_REDIRECT_URI=${AAD_APP_REDIRECT_URI} - - VITE_REACT_APP_AAD_APP_SCOPE_URI=${AAD_APP_SCOPE_URI} + - VITE_REACT_APP_ADD_APP_SCOPE_URI=${ADD_APP_SCOPE_URI} - VITE_REACT_APP_SERVER_HOST=${SERVER_HOST} - VITE_REACT_APP_INSIGHTS_KEY=${APPINSIGHTS_INSTRUMENTATIONKEY} - VITE_REACT_EDITOR=code diff --git a/docs/tutorials/quickstart.md b/docs/tutorials/quickstart.md index 46e0f02..4fbcd0f 100644 --- a/docs/tutorials/quickstart.md +++ b/docs/tutorials/quickstart.md @@ -34,6 +34,11 @@ ADD_APP_SCOPE_URI="api:///Users.Create" ISSUER="https://sts.windows.net//" ``` +4. Install libgraphviz-dev +```bash +sudo apt install libgraphviz-dev +``` + ## Instructions 1. **Start Docker:** @@ -44,7 +49,7 @@ ISSUER="https://sts.windows.net//" 2. **Open Bash Terminal:** - For Windows, use WSL2. **Note:** PowerShell will not work. -You need to setup 3 repositories to start the PwR Studio. Follow the instructions below to clone the repositories. Keep the repositories in the same directory as siblings. **Note:** Do not clone in your Windows directory as that will change the line endings. +You need to setup 4 repositories to start the PwR Studio. Follow the instructions below to clone the repositories. Keep the repositories in the same directory as siblings. **Note:** Do not clone in your Windows directory as that will change the line endings. 3. **Clone PwR-Studio Repository:** ```bash @@ -71,16 +76,23 @@ You need to setup 3 repositories to start the PwR Studio. Follow the instruction git clone git@github.com:microsoft/PwR-NL2DSL.git ``` + +6. **Clone [Jugalbandi-Manager](https://github.com/OpenNyAI/Jugalbandi-Manager) Repository:** + ```bash + git clone git@github.com:OpenNyAI/Jugalbandi-Manager.git + ``` + Great job! You have successfully cloned the repositories. 🎉 Your directory structure should look like this: ```bash + ├── Jugalbandi-Manager ├── Jugalbandi-Studio-Engine ├── PwR-NL2DSL └── PwR-Studio ``` -6. **Setup Local Environment Variables:** +7. **Setup Local Environment Variables:** 1. Go into `PwR-Studio` repository ```bash diff --git a/lib/pwr_studio/engines/__init__.py b/lib/pwr_studio/engines/__init__.py index 5b2cedc..824578e 100644 --- a/lib/pwr_studio/engines/__init__.py +++ b/lib/pwr_studio/engines/__init__.py @@ -46,8 +46,7 @@ def __init__( assert isinstance(kwargs["correlation_id"], str) self._correlation_id = kwargs["correlation_id"] - if len(self._project.representations.keys()) == 0: - self.initalize() + self.initalize() @abstractmethod def _get_representations(self) -> List[PwRStudioRepresentation]: @@ -58,7 +57,9 @@ def initalize(self): for pwrRep in pwrRepresentations: name = pwrRep.get_name() data = pwrRep.get_data() - self._project.representations[name] = data + + if name not in self._project.representations: + self._project.representations[name] = data async def process_utterance(self, text: str, **kwargs): allowed_args = {"chat_history", "files"} diff --git a/server/app/main.py b/server/app/main.py index b5ee02f..21aeed3 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -965,6 +965,26 @@ async def get_output_response( print( f"#3S - main.py Starting Output Webhook with {output_response.type}:{output_response.message}; PwR-RID={output_response.correlation_id}" ) + + if output_response.project: + project = output_response.project + # if project.current_iteration: + # iterations_row['description'] = project.current_iteration.description + + for name in project.representations: + representation = project.representations[name] + rep_dict = { + "name": name, + "type": representation.type, + "text": representation.text, + "project_id": pid, + "is_pwr_viewable": representation.is_pwr_viewable, + "is_user_viewable": True, + "is_editable": representation.is_editable, + "sort_order": representation.sort_order, + } + crud.create_representation(db, rep_dict) + if output_response.type == "output" or output_response.type == "debug": crud.create_output_message( db, diff --git a/studio/src/components/Chat/TestBot.tsx b/studio/src/components/Chat/TestBot.tsx index c216d4d..206e588 100644 --- a/studio/src/components/Chat/TestBot.tsx +++ b/studio/src/components/Chat/TestBot.tsx @@ -7,13 +7,16 @@ import TypingLoader from "./TypingLoader"; import { sendRequest } from '../../api'; import TestBotFooter from "./TestBotFooter"; import { useId } from '@fluentui/react-hooks'; - +import FormInput from '../FormInput'; +import DropdownInput from '../DropdownInput'; interface props { id: string | undefined, token: any, userId: any, - setOnlineState: Function + setOnlineState: Function, + resetChat: bool, + resetChatToggle: Function } const testBotStyles = makeStyles(theme => ({ @@ -34,6 +37,10 @@ const testBotStyles = makeStyles(theme => ({ background: '#AFD8FF', color: '#000' }, + plugin: { + background: '#C9CCCF', + color: '#000' + }, input: { alignSelf: 'flex-end', backgroundColor: '#f0949e', @@ -75,6 +82,7 @@ const TestBot = (props: props) => { const classUser = mergeStyles(classes.message, classes.user); const classInput = mergeStyles(classes.message, classes.input); const classAgent = mergeStyles(classes.message, classes.agent); + const classPlugin = mergeStyles(classes.message, classes.plugin); const userTypes = ["instruction", "feedback"] const noBackground = mergeStyles(classes.message, classes.noBackground) const debugClass = mergeStyles(classes.message, classes.debug); @@ -83,6 +91,7 @@ const TestBot = (props: props) => { const [disableSend, setDisableSend] = React.useState(false); const [callBackMode, setCallBackMode] = React.useState(false); + const botRestartmessage = "_reset_chat_" const { getWebSocket, @@ -119,7 +128,7 @@ const TestBot = (props: props) => { scrollChatToBottom(); } }, [lastJsonMessage, addMessages]); - + React.useEffect(() => { if (readyState === ReadyState.OPEN) { props.setOnlineState(true) @@ -135,11 +144,13 @@ const TestBot = (props: props) => { if (messageType === 'debug') { return debugClass } - if (message.indexOf('\xa1') > -1) { - return classInput - } + //if (message.indexOf('\xa1') > -1) { + // return classInput + //} if (userTypes.includes(messageType)) { return classUser + } else if (message.startsWith('##plugin')) { + return classPlugin } else { return classAgent } @@ -228,6 +239,44 @@ const TestBot = (props: props) => { return msg; } + const isCustomInputNeeded = (messages:any, type_check:string) => { + if (messages != null && messages.length > 1) { + if (messages[0].type === 'end' + && messages[1].type === 'output' + && messages[1].message.indexOf("\xa1") > -1) { + const msgBody = messages[1].message.split('\xa1')[0]; + const jobj = JSON.parse(msgBody); + return jobj["type"] === type_check; + } + } + return false; + } + + const isFromInputNeeded = (messages:any) => { + return isCustomInputNeeded(messages, "form"); + } + + const getFormFields = (messages:any) => { + const msgBody = messages[1].message.split('\xa1')[0]; + return JSON.parse(msgBody)["vars"] + } + + const isDropdownInputNeeded = (messages:any) => { + return isCustomInputNeeded(messages, "dropdown"); + } + + const getDropdownFields = (messages:any) => { + const msgBody = messages[1].message.split('\xa1')[0]; + return JSON.parse(msgBody)["options"] + } + + React.useEffect(() => { + if (props.resetChat) { + sendMessageToWss(botRestartmessage); + props.resetChatToggle(false); + } + }, [props.resetChat]); + return ( @@ -239,7 +288,7 @@ const TestBot = (props: props) => { {messages ? messages.map((message: any, index: string) => ( //
<> - {message.message.trim() !== '' && !['representation_edit', 'files'].includes(message.type) && !(message.type === 'thought' && ['input', 'event'].includes(message.message.trim())) && + {message.message.trim() !== '' && !['representation_edit', 'files'].includes(message.type) && !(message.type === 'thought' && ['input', 'event'].includes(message.message.trim())) && message.message.trim() !== botRestartmessage &&
{ {message.type === 'thought' && } {message.type === 'error' && } - +
- {message.type === 'output' ? + {message.type === 'output' ?
: getMessage(message.message)}
- +
} @@ -271,7 +320,14 @@ const TestBot = (props: props) => {
- + { isFromInputNeeded(messages) ? + : + ( + isDropdownInputNeeded(messages) ? + : + + ) + } ) diff --git a/studio/src/components/DropdownInput/index.tsx b/studio/src/components/DropdownInput/index.tsx new file mode 100644 index 0000000..65858ff --- /dev/null +++ b/studio/src/components/DropdownInput/index.tsx @@ -0,0 +1,50 @@ +import '@/styles/components/publishModal.scss'; +import { DefaultButton, Dropdown, Label, Stack } from '@fluentui/react'; +import React from 'react'; + +interface props { + choices: any, + sendChoice: Function +} + +export const DropdownInput = (props: props) => { + const optionsList = props.choices ?? []; + const options = optionsList.map((x, i) => ({'key': '_' + i, 'text': x})); + + if (options.length > 0) { + options[0].selected = true; + } + + const [selectedOption, setSelectedOption] = React.useState(0); + + const setChoice = (e, o, i) => { + setSelectedOption(i); + } + + const updateChoice = () => { + if (options.length > 0) + props.sendChoice(optionsList[selectedOption]); + } + + return ( + + + + + + + + + + Submit + + + + + ); +} + +export default DropdownInput; \ No newline at end of file diff --git a/studio/src/components/FormInput/index.tsx b/studio/src/components/FormInput/index.tsx new file mode 100644 index 0000000..9d8d8a4 --- /dev/null +++ b/studio/src/components/FormInput/index.tsx @@ -0,0 +1,59 @@ +import '@/styles/components/publishModal.scss'; +import { DefaultButton, Label, Stack, TextField } from '@fluentui/react'; +import React from 'react'; + +interface props { + variableNames: any, + sendVariableValues: Function +} + +export const FormInput = (props: props) => { + let valueDict = {} + for (var idx = 0; idx < props.variableNames.length; idx++) { + valueDict[idx] = React.useState(''); + } + + const sendFromData = () => { + let formData = {} + formData["type"] = "form" + formData["vars"] = {} + + let simpleVersion = "" + for (var k in valueDict) { + formData["vars"][props.variableNames[k]] = valueDict[k][0]; + simpleVersion += props.variableNames[k] + simpleVersion += " : " + simpleVersion += valueDict[k][0] + simpleVersion += "\n" + } + const stringVersion = JSON.stringify(formData) + "\xa1" + simpleVersion; + props.sendVariableValues(stringVersion); + } + + return ( + + + {props.variableNames.map((vname: string, index: number) => ( + + + + + + { valueDict[index][1](value || '') }} + value={valueDict[index][0]}/> + + + )) + } + + + sendFromData()}>Submit + + + + + ); +} + +export default FormInput; \ No newline at end of file diff --git a/studio/src/components/PublishModal/index.tsx b/studio/src/components/PublishModal/index.tsx index 6f433ce..b92702c 100644 --- a/studio/src/components/PublishModal/index.tsx +++ b/studio/src/components/PublishModal/index.tsx @@ -1,11 +1,54 @@ import '@/styles/components/publishModal.scss'; import { DefaultButton, Icon, Label, Modal, Stack, TextField } from '@fluentui/react'; import React from 'react'; +import { sendRequest } from '../../api'; + interface props { - isOpen: boolean - hideModal: Function + isOpen: boolean, + hideModal: Function, + representations: any, + dslName: string } + +const uploadProgram = (url: string, secret: string, name: string, representations: any) => { + const dslContentText = representations?.find(r => r.name == 'dsl')?.text || '{}'; + const codeContent = representations?.find(r => r.name == 'code')?.text || '' + const dslContent = JSON.parse(dslContentText); + const credentials = dslContent.config_vars.map(x => x.name); + ; + + const requestBody = { + 'name': dslContent.fsm_name, + 'dsl': dslContentText, + 'code': codeContent, + 'requirements': '', + 'required_credentials': credentials, + 'index_urls':[''], + 'version': '1.0.0' + + } + + sendRequest({ + url: url, + method: "POST", + accessToken: secret, + body: JSON.stringify(requestBody), + headers: { + 'Content-Type': 'application/json' + } + }); +} + export const PublishModal = (props: props) => { + const [message, setMessage] = React.useState(''); + const [secretKey, setSecretKey] = React.useState(''); + + const resetModal = () => { + setMessage(''); + setSecretKey(''); + props.hideModal(); + } + return( <> { - + { setMessage(value || '') }} + value={message}/> @@ -32,7 +78,10 @@ export const PublishModal = (props: props) => { - + { setSecretKey(value || '') }} + value={secretKey}/> @@ -53,10 +102,13 @@ before you do wide-scale release. - props.hideModal()}>Publish + { + uploadProgram(message, secretKey, props.dslName, props.representations); + resetModal(); + }}>Publish - props.hideModal()}>Cancel + resetModal()}>Cancel diff --git a/studio/src/components/index.tsx b/studio/src/components/index.tsx index 129e034..7c65acb 100644 --- a/studio/src/components/index.tsx +++ b/studio/src/components/index.tsx @@ -5,6 +5,8 @@ import InPlaceEditing from './InPlaceEditing'; import PluginList from './PluginList'; import DevBot from './Chat/DevBot'; import Mermaid from './Mermaid'; +import FormInput from './FormInput'; +import DropdownInput from './DropdownInput'; export { AudioRecorder, @@ -13,5 +15,7 @@ export { InPlaceEditing, PluginList, DevBot, - Mermaid + Mermaid, + FormInput, + DropdownInput }; \ No newline at end of file diff --git a/studio/src/pages/editor-page.tsx b/studio/src/pages/editor-page.tsx index 86f8001..5ea9634 100644 --- a/studio/src/pages/editor-page.tsx +++ b/studio/src/pages/editor-page.tsx @@ -78,6 +78,7 @@ export const EditorPage: React.FunctionComponent = () => { const [inputText, setInputText] = React.useState(''); const fileInput = React.createRef(); const [dslImportLoader, setDslImportLoader] = React.useState(false); + const [resetTestChat, setResetTestChat] = React.useState(false); React.useEffect(() => { if (token && params.id) { sendRequest({ @@ -97,7 +98,7 @@ export const EditorPage: React.FunctionComponent = () => { }, [params.id, token, userId]) React.useEffect(() => { - if (account && inProgress === "none") { + if (account && inProgress === "none") { instance.acquireTokenSilent({ scopes: [import.meta.env.VITE_REACT_APP_ADD_APP_SCOPE_URI || ''], account: account @@ -260,7 +261,7 @@ export const EditorPage: React.FunctionComponent = () => { } const renderRep = (item: any, index: number) => { - // if (item.name !== 'code') { + if (item.name !== 'fsm_state') { return ( @@ -271,13 +272,13 @@ export const EditorPage: React.FunctionComponent = () => { {item.name} ) - // } + } } return ( - + { {}, - }, { - key: 'callbackForm', - name: 'Callback form', - iconProps: { iconName: 'FormLibrary' }, - onClick: () => { }, + key: 'clearData', + name: 'Start / Reset bot', + iconProps: { iconName: 'Rerun' }, + onClick: () => { + setResetTestChat(true); + }, } ]} onRenderOverflowButton={onRenderOverflowButton} @@ -438,7 +437,7 @@ export const EditorPage: React.FunctionComponent = () => { setRefreshIR(refreshIR + 1) } pluginStoreToggle={showPluginStore} userId={userId} setOnlineState={setDevChatStatus} id={params.id} token={token} />
- +