Skip to content

Commit

Permalink
feat(chat): handle required schema buttons, fix name regenerate, impr…
Browse files Browse the repository at this point in the history
…ove schema validation (Issue #2980, #2901, #2972, #2986) (#3005)

Co-authored-by: Magomed-Elbi Dzhukalaev <[email protected]>
  • Loading branch information
Gimir and Magomed-Elbi Dzhukalaev authored Jan 24, 2025
1 parent a2bc02a commit 952be60
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 20 deletions.
20 changes: 19 additions & 1 deletion apps/chat/src/components/Chat/ChatInput/ChatInputMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { usePromptSelection } from '@/src/hooks/usePromptSelection';
import { useTokenizer } from '@/src/hooks/useTokenizer';

import { getUserCustomContent } from '@/src/utils/app/file';
import {
getConversationSchema,
isFormValueValid,
} from '@/src/utils/app/form-schema';
import { isMobile } from '@/src/utils/app/mobile';
import { getPromptLimitDescription } from '@/src/utils/app/modals';

Expand Down Expand Up @@ -182,6 +186,16 @@ export const ChatInputMessage = Inversify.register(
selectedPrompt,
} = usePromptSelection(maxTokensLength, modelTokenizer, '');

const isSchemaValueValid = useMemo(() => {
const schema =
selectedConversations.map(getConversationSchema)?.[0] ??
configurationSchema;

if (!schema) return true;

return isFormValueValid(schema, chatFormValue);
}, [selectedConversations, configurationSchema, chatFormValue]);

const isInputEmpty = useMemo(() => {
return (
!content.trim().length &&
Expand All @@ -202,7 +216,8 @@ export const ChatInputMessage = Inversify.register(
!isModelsLoaded ||
isUploadingFilePresent ||
isConversationNameInvalid ||
isConversationPathInvalid;
isConversationPathInvalid ||
!isSchemaValueValid;

const canAttach =
(canAttachFiles || canAttachFolders || canAttachLinks) &&
Expand Down Expand Up @@ -454,6 +469,9 @@ export const ChatInputMessage = Inversify.register(
if (isConversationPathInvalid) {
return t(errorsMessages.entityPathInvalid);
}
if (!isSchemaValueValid) {
return t('Please select one of the options above');
}
return t('Please type a message');
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ import { memo, useCallback } from 'react';

import { useTranslation } from 'next-i18next';

import { getMessageSchema } from '@/src/utils/app/form-schema';
import {
getMessageSchema,
isFormSchemaValid,
} from '@/src/utils/app/form-schema';

import { Translation } from '@/src/types/translation';

import { ChatActions } from '@/src/store/chat/chat.reducer';
import { ChatSelectors } from '@/src/store/chat/chat.selectors';
import { ConversationsSelectors } from '@/src/store/conversations/conversations.reducers';
import { useAppDispatch, useAppSelector } from '@/src/store/hooks';

import { FormSchema } from '@/src/components/Chat/ChatMessage/MessageSchema/FormSchema';
import { ErrorMessage } from '@/src/components/Common/ErrorMessage';

import {
DialSchemaProperties,
Expand All @@ -29,6 +34,7 @@ const AssistantSchemaView = ({ schema }: AssistantSchemaViewProps) => {
const isPlayback = useAppSelector(
ConversationsSelectors.selectIsPlaybackSelectedConversations,
);
const formValue = useAppSelector(ChatSelectors.selectChatFormValue);

const handleChange = useCallback(
(property: string, value: MessageFormValueType, submit?: boolean) => {
Expand All @@ -54,6 +60,8 @@ const AssistantSchemaView = ({ schema }: AssistantSchemaViewProps) => {
schema={schema}
onChange={handleChange}
disabled={isPlayback}
formValue={formValue}
showSelected
/>
</div>
);
Expand All @@ -74,6 +82,13 @@ export const AssistantSchema = memo(function AssistantSchema({

if (!schema) return null;

if (!isFormSchemaValid(schema))
return (
<div className="mt-2">
<ErrorMessage error={t('Form schema is invalid') ?? ''} />
</div>
);

if (
!isLastMessage &&
!message.content &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ export const FormSchema = memo(function FormSchema({
buttonClassName,
}: FormSchemaProps) {
return (
<div className={classNames('flex flex-col gap-2', wrapperClassName)}>
<div
data-no-context-menu
className={classNames('flex flex-col gap-2', wrapperClassName)}
>
{Object.entries(schema.properties).map(([name, property]) => (
<PropertyRenderer
property={property}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,12 @@ const InvalidSchemaMessage = () => {
);
};

const config = {
errorLogMessage: 'Invalid schema error:',
};

export const UserSchema = withErrorBoundary(
MemoUserSchema,
<InvalidSchemaMessage />,
config,
);

export const AssistantSchema = withErrorBoundary(
MemoAssistantSchema,
<InvalidSchemaMessage />,
config,
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getConfigurationSchema,
getFormButtonType,
getMessageSchema,
isFormSchemaValid,
} from '@/src/utils/app/form-schema';

import { FormButtonType } from '@/src/types/chat';
Expand All @@ -19,6 +20,7 @@ import { ErrorMessage } from '@/src/components/Common/ErrorMessage';
import {
DialSchemaProperties,
Message,
MessageFormSchema,
MessageFormValue,
MessageFormValueType,
} from '@epam/ai-dial-shared';
Expand All @@ -32,25 +34,20 @@ interface UserSchemaProps {
setFormValue?: (value: MessageFormValue) => void;
onSubmit?: (formValue?: MessageFormValue, content?: string) => void;
disabled?: boolean;
schema?: MessageFormSchema;
}

export const UserSchema = memo(function UserSchema({
messageIndex,
allMessages,
const UserSchemaView = memo(function UserSchemaView({
isEditing,
setInputValue,
formValue,
setFormValue,
onSubmit,
disabled,
schema,
}: UserSchemaProps) {
const { t } = useTranslation(Translation.Chat);

const schema = useMemo(() => {
if (messageIndex === 0) return getConfigurationSchema(allMessages[0]);
return getMessageSchema(allMessages[messageIndex - 1]);
}, [allMessages, messageIndex]);

const handleChange = useCallback(
(property: string, value: MessageFormValueType, submit?: boolean) => {
if (schema && formValue) {
Expand Down Expand Up @@ -129,3 +126,22 @@ export const UserSchema = memo(function UserSchema({
</div>
) : null;
});

export const UserSchema = memo(function UserSchema(props: UserSchemaProps) {
const { t } = useTranslation(Translation.Chat);

const schema = useMemo(() => {
if (props.messageIndex === 0)
return getConfigurationSchema(props.allMessages[0]);
return getMessageSchema(props.allMessages[props.messageIndex - 1]);
}, [props.allMessages, props.messageIndex]);

if (schema && !isFormSchemaValid(schema))
return (
<div className="mt-2">
<ErrorMessage error={t('Form schema is invalid') ?? ''} />
</div>
);

return <UserSchemaView {...props} schema={schema} />;
});
7 changes: 5 additions & 2 deletions apps/chat/src/components/Chat/EmptyChatDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,12 @@ const EmptyChatDescriptionView = ({
const versions = useMemo(
() =>
models.filter(
(m) => installedModelIds.has(m.reference) && m.name === model?.name,
(m) =>
(installedModelIds.has(m.reference) ||
model?.reference === m.reference) &&
m.name === model?.name,
),
[installedModelIds, model?.name, models],
[installedModelIds, model?.name, model?.reference, models],
);

const incorrectModel = !model;
Expand Down
14 changes: 13 additions & 1 deletion apps/chat/src/utils/app/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import {
isEntityNameOrPathInvalid,
prepareEntityName,
} from '@/src/utils/app/common';
import { isConversationWithFormSchema } from '@/src/utils/app/form-schema';
import {
getConfigurationSchema,
getConfigurationValue,
getFormValueDefinitions,
isConversationWithFormSchema,
} from '@/src/utils/app/form-schema';

import { Conversation, Replay } from '@/src/types/chat';
import { EntityType, PartialBy } from '@/src/types/common';
Expand Down Expand Up @@ -98,12 +103,19 @@ export const getNewConversationName = (
const convName = prepareEntityName(conversation.name);
const content = prepareEntityName(message.content);

const formValue = getConfigurationValue(message);
const configurationSchema = getConfigurationSchema(message);

if (content.length > 0) {
return content;
} else if (message.custom_content?.attachments?.length) {
const { title, reference_url } = message.custom_content.attachments[0];

return prepareEntityName(!title && reference_url ? reference_url : title);
} else if (formValue && configurationSchema) {
const definitions = getFormValueDefinitions(formValue, configurationSchema);

if (definitions.length) return prepareEntityName(definitions[0].title);
}

return convName;
Expand Down
70 changes: 70 additions & 0 deletions apps/chat/src/utils/app/form-schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { TypeValidator } from '@/src/utils/app/typeValidator';

import { Conversation, FormButtonType } from '@/src/types/chat';

import {
DialSchemaProperties,
FormSchemaButtonOption,
Message,
MessageFormSchema,
MessageFormValue,
} from '@epam/ai-dial-shared';
import { mapValues, omit } from 'lodash';

Expand All @@ -18,6 +21,12 @@ export const getConfigurationSchema = (message?: Message) =>
export const getConfigurationValue = (message?: Message) =>
message?.custom_content?.configuration_value;

export const getConversationSchema = (conversation: Conversation) => {
return getMessageSchema(
conversation.messages[conversation.messages.length - 1],
);
};

export const getFormButtonType = (option: FormSchemaButtonOption) => {
if (option[DialSchemaProperties.DialWidgetOptions]?.submit)
return FormButtonType.Submit;
Expand Down Expand Up @@ -55,3 +64,64 @@ export const removeDescriptionsFromSchema = (
omit(value, ['description']),
),
});

export const getFormValueMissingProperties = (
schema: MessageFormSchema,
value: MessageFormValue,
) => {
return schema.required?.filter((property) => !(property in value)) ?? [];
};

export const isFormValueValid = (
schema: MessageFormSchema,
value?: MessageFormValue,
) => {
return !getFormValueMissingProperties(schema, value ?? {}).length;
};

export const isFormSchemaValid = TypeValidator.shape({
type: TypeValidator.string(),
required: TypeValidator.optional(TypeValidator.array(TypeValidator.string())),
[DialSchemaProperties.DialChatMessageInputDisabled]: TypeValidator.optional(
TypeValidator.boolean(),
),
properties: TypeValidator.map(
TypeValidator.string(),
TypeValidator.shape({
type: TypeValidator.string(),
description: TypeValidator.optional(TypeValidator.string()),
oneOf: TypeValidator.optional(
TypeValidator.array(
TypeValidator.shape({
title: TypeValidator.string(),
const: TypeValidator.number(),
[DialSchemaProperties.DialWidgetOptions]: TypeValidator.optional(
TypeValidator.shape({
confirmationMessage: TypeValidator.optional(
TypeValidator.string(),
),
populateText: TypeValidator.optional(TypeValidator.string()),
submit: TypeValidator.optional(TypeValidator.boolean()),
}),
),
}),
),
),
}),
),
});

export const getFormValueDefinitions = (
value: MessageFormValue,
schema?: MessageFormSchema,
) => {
if (!schema || !isFormSchemaValid(schema)) return [];

return Object.entries(value)
.map(([key, value]) => {
return schema.properties[key].oneOf?.find(
(option) => option.const === value,
);
})
.filter(Boolean) as FormSchemaButtonOption[];
};
45 changes: 45 additions & 0 deletions apps/chat/src/utils/app/typeValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { isArray, isBoolean, isNumber, isObject, isString } from 'lodash';

type Validator = (v: unknown) => boolean;

export class TypeValidator {
static number() {
return (v: unknown) => isNumber(v);
}

static string() {
return (v: unknown) => isString(v);
}

static boolean() {
return (v: unknown) => isBoolean(v);
}

static array(childType: Validator) {
return (v: unknown) => isArray(v) && v.every(childType);
}

static oneOf(options: unknown[]) {
return (v: unknown) => options.includes(v);
}

static shape(shapeType: Record<string, Validator>) {
return (v: unknown) =>
isObject(v) &&
Object.entries(shapeType).every(([key, validator]) =>
validator((v as Record<string, unknown>)[key]),
);
}

static map(keyValidator: Validator, valueValidator: Validator) {
return (v: unknown) =>
isObject(v) &&
Object.entries(v).every(
([key, value]) => keyValidator(key) && valueValidator(value),
);
}

static optional(validator: Validator) {
return (v: unknown) => v === undefined || v === null || validator(v);
}
}

0 comments on commit 952be60

Please sign in to comment.