Skip to content
Merged
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
5 changes: 5 additions & 0 deletions common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export enum PROCESSOR_TYPE {
TEXT_EMBEDDING = 'text_embedding',
TEXT_IMAGE_EMBEDDING = 'text_image_embedding',
COPY = 'copy',
AGENTIC_QUERY_TRANSLATOR = 'agentic_query_translator',
AGENTIC_CONTEXT = 'agentic_context',
}

export enum MODEL_TYPE {
Expand Down Expand Up @@ -1083,4 +1085,7 @@ export const DEFAULT_AGENT = {
type: TOOL_TYPE.QUERY_PLANNING,
},
],
memory: {
type: AGENT_MEMORY_TYPE.CONVERSATION_INDEX,
},
} as Partial<Agent>;
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ export function AgentConfiguration(props: AgentConfigurationProps) {
<>
<EuiFlexItem>
<EuiFormRow
label={'Memory (optional)'}
label={'Memory'}
labelAppend={
<EuiText size="xs">
<EuiLink
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Provider } from 'react-redux';
import { IndexSelector } from './index_selector';
import { AGENT_TYPE } from '../../../../../common';
import { FormikContext, FormikContextType } from 'formik';
import configureStore from 'redux-mock-store';
import { INITIAL_OPENSEARCH_STATE } from '../../../../store';

jest.mock('../../../../services', () => {
const { mockCoreServices } = require('../../../../../test/mocks');
return {
...jest.requireActual('../../../../services'),
...mockCoreServices,
};
});

jest.mock('../../../../utils', () => ({
...jest.requireActual('../../../../utils'),
getDataSourceId: jest.fn().mockReturnValue('test-datasource-id'),
}));

jest.mock('../../../../store', () => {
return {
...jest.requireActual('../../../../store'),
useAppDispatch: jest
.fn()
.mockReturnValue(
jest
.fn()
.mockImplementation(() =>
Promise.resolve({ unwrap: () => Promise.resolve() })
)
),
};
});

const mockStore = configureStore([]);

describe('IndexSelector', () => {
const mockIndices = {
index1: {
name: 'index1',
health: 'green',
status: 'open',
docsCount: '100',
},
index2: {
name: 'index2',
health: 'yellow',
status: 'open',
docsCount: '50',
},
'.system': {
name: '.system',
health: 'green',
status: 'open',
docsCount: '10',
},
};

const initialState = {
opensearch: {
...INITIAL_OPENSEARCH_STATE,
indices: mockIndices,
},
workflows: {},
presets: {},
ml: { models: {}, loading: false, errorMessage: '' },
errors: { loading: false, errorMessage: '' },
};

const mockFormikContext: FormikContextType<any> = {
values: {
search: {
index: {
name: '',
},
},
},
} as FormikContextType<any>;

// Reusable render function
const renderIndexSelector = (agentType?: AGENT_TYPE) => {
const store = mockStore(initialState);
return render(
<Provider store={store}>
<FormikContext.Provider value={mockFormikContext}>
<IndexSelector agentType={agentType} />
</FormikContext.Provider>
</Provider>
);
};

beforeEach(() => {
jest.clearAllMocks();
});

test('renders the component', () => {
renderIndexSelector(AGENT_TYPE.FLOW);

expect(screen.getByText('Index')).toBeInTheDocument();

const viewDetailsButton = screen.getByTestId('viewIndexDetailsButton');
expect(viewDetailsButton).toBeInTheDocument();

const indexSelector = screen.getByTestId('indexSelector');
expect(indexSelector).toBeInTheDocument();
});

test('system indices hidden', () => {
renderIndexSelector(AGENT_TYPE.FLOW);

expect(screen.getByText('Index')).toBeInTheDocument();

const indexSelector = screen.getByTestId('indexSelector');
indexSelector.click();

expect(screen.queryByText('.system')).not.toBeInTheDocument();
});

test('all indices option is hidden for flow agents', () => {
renderIndexSelector(AGENT_TYPE.FLOW);

expect(screen.getByText('Index')).toBeInTheDocument();

const indexSelector = screen.getByTestId('indexSelector');
indexSelector.click();

expect(screen.getByText('index1')).toBeInTheDocument();
expect(screen.getByText('index2')).toBeInTheDocument();
expect(screen.queryByText('All indices')).not.toBeInTheDocument();
});

test('all indices option is visible for conversational agents', () => {
renderIndexSelector(AGENT_TYPE.CONVERSATIONAL);

expect(screen.getByText('Index')).toBeInTheDocument();

const indexSelector = screen.getByTestId('indexSelector');
indexSelector.click();

expect(screen.getByText('index1')).toBeInTheDocument();
expect(screen.getByText('index2')).toBeInTheDocument();
expect(screen.getByText('All indices')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ import {
} from '../../../../store';
import { getDataSourceId } from '../../../../utils';
import {
AGENT_TYPE,
OMIT_SYSTEM_INDEX_PATTERN,
WorkflowFormValues,
} from '../../../../../common';
import { IndexDetailsModal } from './index_details_modal';
import { NoIndicesCallout } from '../components';

interface IndexSelectorProps {}
interface IndexSelectorProps {
agentType?: AGENT_TYPE;
}

const INDEX_NAME_PATH = 'search.index.name';
const ALL_INDICES = 'All indices';
Expand All @@ -55,20 +58,28 @@ export function IndexSelector(props: IndexSelectorProps) {
const [indexOptions, setIndexOptions] = useState<
{ value: string; text: string }[]
>([]);

// Optionally add an "ALL INDICES" option for eligible agent types (conversational)
useEffect(() => {
setIndexOptions([
{
text: ALL_INDICES,
value: '',
},
let eligibleIndexOptions = [
...Object.values(indices || {})
.filter((index) => !index.name.startsWith('.')) // Filter out system indices
.map((index) => ({
value: index.name,
text: index.name,
})),
]);
}, [indices]);
];
if (props.agentType === AGENT_TYPE.CONVERSATIONAL) {
eligibleIndexOptions = [
{
text: ALL_INDICES,
value: '',
},
...eligibleIndexOptions,
];
}
setIndexOptions(eligibleIndexOptions);
}, [indices, props.agentType]);

return (
<>
Expand Down Expand Up @@ -103,6 +114,7 @@ export function IndexSelector(props: IndexSelectorProps) {
<EuiFlexGroup direction="row">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-testid="viewIndexDetailsButton"
size="s"
disabled={isEmpty(selectedIndexName)}
onClick={async () => {
Expand All @@ -129,23 +141,29 @@ export function IndexSelector(props: IndexSelectorProps) {
{isDetailsModalVisible && (
<IndexDetailsModal
onClose={() => setIsDetailsModalVisible(false)}
indexName={getIn(values, 'search.index.name')}
indexName={selectedIndexName}
/>
)}
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiSelect
data-testid="indexSelector"
options={indexOptions}
value={
isEmpty(selectedIndexName) ? ALL_INDICES : selectedIndexName
isEmpty(selectedIndexName)
? props.agentType === AGENT_TYPE.FLOW ||
isEmpty(props.agentType)
? undefined
: ALL_INDICES
: selectedIndexName
}
onChange={(e) => {
const value = e.target.value;
setFieldValue(INDEX_NAME_PATH, value);
setFieldTouched(INDEX_NAME_PATH, true);
}}
aria-label="Select index"
hasNoInitialSelection={false}
hasNoInitialSelection={true}
fullWidth
compressed
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
WorkflowConfig,
IndexMappings,
AGENT_ID_PATH,
PROCESSOR_TYPE,
} from '../../../../../common';

interface SearchQueryProps {
Expand Down Expand Up @@ -56,11 +57,19 @@ export function SearchQuery(props: SearchQueryProps) {
const [autoPipeline, setAutoPipeline] = useState<{}>({
request_processors: [
{
agentic_query_translator: {
[PROCESSOR_TYPE.AGENTIC_QUERY_TRANSLATOR]: {
agent_id: '',
},
},
],
response_processors: [
{
[PROCESSOR_TYPE.AGENTIC_CONTEXT]: {
agent_steps_summary: true,
dsl_query: true,
},
},
],
});
const [customPipeline, setCustomPipeline] = useState<{}>({
request_processors: [],
Expand All @@ -86,26 +95,34 @@ export function SearchQuery(props: SearchQueryProps) {
setAutoPipeline({
request_processors: [
{
agentic_query_translator: {
[PROCESSOR_TYPE.AGENTIC_QUERY_TRANSLATOR]: {
agent_id: selectedAgentId,
},
},
],
response_processors: [
{
[PROCESSOR_TYPE.AGENTIC_CONTEXT]: {
agent_steps_summary: true,
dsl_query: true,
},
},
],
});
// try to also update the agent ID if the user is building a custom pipeline
if (
!isEmpty(
getIn(
customPipeline,
'request_processors.0.agentic_query_translator.agent_id',
`request_processors.0.${PROCESSOR_TYPE.AGENTIC_QUERY_TRANSLATOR}.agent_id`,
undefined
)
)
) {
let customPipelineUpdated = cloneDeep(customPipeline);
set(
customPipelineUpdated,
'request_processors.0.agentic_query_translator.agent_id',
`request_processors.0.${PROCESSOR_TYPE.AGENTIC_QUERY_TRANSLATOR}.agent_id`,
selectedAgentId
);
setCustomPipeline(customPipelineUpdated);
Expand Down
Loading
Loading