Skip to content

Commit

Permalink
Add a prototype tab; support ingest and search with guardrails (#139)
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
(cherry picked from commit e18bb11)
  • Loading branch information
ohltyler authored and github-actions[bot] committed Apr 19, 2024
1 parent e805ea7 commit 371e0dc
Show file tree
Hide file tree
Showing 13 changed files with 851 additions and 3 deletions.
2 changes: 2 additions & 0 deletions common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const BASE_NODE_API_PATH = '/api/flow_framework';
// OpenSearch node APIs
export const BASE_OPENSEARCH_NODE_API_PATH = `${BASE_NODE_API_PATH}/opensearch`;
export const CAT_INDICES_NODE_API_PATH = `${BASE_OPENSEARCH_NODE_API_PATH}/catIndices`;
export const SEARCH_INDEX_NODE_API_PATH = `${BASE_OPENSEARCH_NODE_API_PATH}/search`;
export const INGEST_NODE_API_PATH = `${BASE_OPENSEARCH_NODE_API_PATH}/ingest`;

// Flow Framework node APIs
export const BASE_WORKFLOW_NODE_API_PATH = `${BASE_NODE_API_PATH}/workflow`;
Expand Down
6 changes: 6 additions & 0 deletions public/pages/workflow_detail/prototype/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './prototype';
218 changes: 218 additions & 0 deletions public/pages/workflow_detail/prototype/ingestor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState, useEffect } from 'react';
import {
EuiButton,
EuiCodeEditor,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiText,
} from '@elastic/eui';
import {
USE_CASE,
Workflow,
getIndexName,
getSemanticSearchValues,
} from '../../../../common';
import { ingest, useAppDispatch } from '../../../store';
import { getCore } from '../../../services';
import { getFormattedJSONString } from './utils';

interface IngestorProps {
workflow: Workflow;
}

type WorkflowValues = {
modelId: string;
};

type SemanticSearchValues = WorkflowValues & {
inputField: string;
vectorField: string;
};

type DocGeneratorFn = (
queryText: string,
workflowValues: SemanticSearchValues
) => {};

/**
* A basic and flexible UI for ingesting some documents against an index. Sets up guardrails to limit
* what is customized in the document, and setting readonly values based on the workflow's use case
* and details.
*
* For example, given a semantic search workflow configured on index A, with model B, input field C, and vector field D,
* the UI will enforce the ingested document to include C, and ingest it against A.
*/
export function Ingestor(props: IngestorProps) {
const dispatch = useAppDispatch();
// query state
const [workflowValues, setWorkflowValues] = useState<WorkflowValues>();
const [docGeneratorFn, setDocGeneratorFn] = useState<DocGeneratorFn>();
const [indexName, setIndexName] = useState<string>('');
const [docObj, setDocObj] = useState<{}>({});
const [formattedDoc, setFormattedDoc] = useState<string>('');
const [userInput, setUserInput] = useState<string>('');

// results state
const [response, setResponse] = useState<{}>({});
const [formattedResponse, setFormattedResponse] = useState<string>('');

// hook to set all of the workflow-related fields based on the use case
useEffect(() => {
setWorkflowValues(getWorkflowValues(props.workflow));
setDocGeneratorFn(getDocGeneratorFn(props.workflow));
setIndexName(getIndexName(props.workflow));
}, [props.workflow]);

// hook to generate the query once all dependent input vars are available
useEffect(() => {
if (docGeneratorFn && workflowValues) {
setDocObj(docGeneratorFn(userInput, workflowValues));
}
}, [userInput, docGeneratorFn, workflowValues]);

// hooks to persist the formatted data. this is so we don't
// re-execute the JSON formatting unless necessary
useEffect(() => {
setFormattedResponse(getFormattedJSONString(response));
}, [response]);
useEffect(() => {
setFormattedDoc(getFormattedJSONString(docObj));
}, [docObj]);

//
function onExecuteIngest() {
dispatch(ingest({ index: indexName, doc: docObj }))
.unwrap()
.then(async (result) => {
setResponse(result);
})
.catch((error: any) => {
getCore().notifications.toasts.addDanger(error);
setResponse({});
});
}

return (
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiText size="s">Ingest some sample data to get started.</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiFieldText
placeholder={'Enter some plaintext...'}
compressed={false}
value={userInput}
onChange={(e) => {
setUserInput(e.target.value);
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={onExecuteIngest} fill={false}>
Ingest
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="50vh"
value={formattedDoc}
onChange={() => {}}
readOnly={true}
setOptions={{
fontSize: '14px',
}}
aria-label="Code Editor"
tabSize={2}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}></EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiText size="s">Response</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiFieldText
placeholder={indexName}
prepend="Index:"
compressed={false}
disabled={true}
readOnly={true}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="50vh"
value={formattedResponse}
onChange={() => {}}
readOnly={true}
setOptions={{
fontSize: '14px',
}}
aria-label="Code Editor"
tabSize={2}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}

// getting the appropriate doc generator function based on the use case
function getDocGeneratorFn(workflow: Workflow): DocGeneratorFn {
let fn;
switch (workflow.use_case) {
case USE_CASE.SEMANTIC_SEARCH:
default: {
fn = () => generateSemanticSearchDoc;
}
}
return fn;
}

// getting the appropriate static values from the workflow based on the use case
function getWorkflowValues(workflow: Workflow): WorkflowValues {
let values;
switch (workflow.use_case) {
case USE_CASE.SEMANTIC_SEARCH:
default: {
values = getSemanticSearchValues(workflow);
}
}
return values;
}

// utility fn to generate a document suited for semantic search
function generateSemanticSearchDoc(
docValue: string,
workflowValues: SemanticSearchValues
): {} {
return {
[workflowValues.inputField]: docValue,
};
}
103 changes: 103 additions & 0 deletions public/pages/workflow_detail/prototype/prototype.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState } from 'react';
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiPageContent,
EuiSpacer,
EuiTab,
EuiTabs,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { Workflow } from '../../../../common';
import { QueryExecutor } from './query_executor';
import { Ingestor } from './ingestor';

interface PrototypeProps {
workflow?: Workflow;
}

enum TAB_ID {
INGEST = 'ingest',
QUERY = 'query',
}

const inputTabs = [
{
id: TAB_ID.INGEST,
name: '1. Ingest Data',
disabled: false,
},
{
id: TAB_ID.QUERY,
name: '2. Query data',
disabled: false,
},
];

/**
* A simple prototyping page to perform ingest and search.
*/
export function Prototype(props: PrototypeProps) {
const [selectedTabId, setSelectedTabId] = useState<string>(TAB_ID.INGEST);
return (
<EuiPageContent>
<EuiTitle>
<h2>Prototype</h2>
</EuiTitle>
<EuiSpacer size="m" />
{props.workflow?.resourcesCreated &&
props.workflow?.resourcesCreated.length > 0 ? (
<>
<EuiTabs size="m" expand={false}>
{inputTabs.map((tab, idx) => {
return (
<EuiTab
onClick={() => setSelectedTabId(tab.id)}
isSelected={tab.id === selectedTabId}
disabled={tab.disabled}
key={idx}
>
{tab.name}
</EuiTab>
);
})}
</EuiTabs>
<EuiSpacer size="m" />
<EuiFlexGroup direction="column">
{selectedTabId === TAB_ID.INGEST && (
<EuiFlexItem>
<Ingestor workflow={props.workflow} />
</EuiFlexItem>
)}
{selectedTabId === TAB_ID.QUERY && (
<EuiFlexItem>
<QueryExecutor workflow={props.workflow} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</>
) : (
<EuiEmptyPrompt
iconType={'cross'}
title={<h2>No resources available</h2>}
titleSize="s"
body={
<>
<EuiText>
Provision the workflow to generate resources in order to start
prototyping.
</EuiText>
</>
}
/>
)}
</EuiPageContent>
);
}
Loading

0 comments on commit 371e0dc

Please sign in to comment.