Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(entities-plugins): support new ai plugins #1517

Merged
merged 14 commits into from
Jul 31, 2024
Merged
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ const getModel = (): Record<string, any> => {

break

// Handle values that aren't strings but should be.
// Handle values that aren't strings but should be.
case 'string':
fieldValue = (fieldValue == null) ? '' : String(fieldValue)
break
Expand All @@ -359,6 +359,23 @@ const getModel = (): Record<string, any> => {
}
}

// FIXME: Special treatment for AI plugins with complexly nested array fields
if (fieldSchema.type === 'array' && fieldSchema.nestedFields) {
const deepOmitNil = (o: Record<string, any>) => {
Object.keys(o).forEach(key => {
if (o[key] && typeof o[key] === 'object' && o[key] !== null) {
deepOmitNil(o[key])
} else if (o[key] === undefined || o[key] === null || (typeof o[key] === 'number' && isNaN(o[key]))
|| (typeof o[key] === 'string' && o[key].trim().length === 0)) {
delete o[key]
}
})
}
if (fieldValue && typeof fieldValue === 'object') {
deepOmitNil(fieldValue)
}
}

// Format Advanced Object for submission
if (fieldSchema.type === 'object-advanced' && fieldSchema.fields && fieldValue !== null) {
if (Object.entries(fieldValue).length === 0) {
Expand Down Expand Up @@ -544,7 +561,7 @@ const initFormModel = (): void => {
} else if (props.record.config) { // typical plugins
// scope fields
if ((props.record.consumer_id || props.record.consumer) || (props.record.service_id || props.record.service) ||
(props.record.route_id || props.record.route) || (props.record.consumer_group_id || props.record.consumer_group)) {
(props.record.route_id || props.record.route) || (props.record.consumer_group_id || props.record.consumer_group)) {
updateModel({
service_id: props.record.service_id || props.record.service,
route_id: props.record.route_id || props.record.route,
Expand Down Expand Up @@ -604,9 +621,11 @@ watch(() => props.schema, (newSchema, oldSchema) => {

Object.assign(formModel, form.model)

formSchema.value = { fields: formSchema.value?.fields?.map((r: Record<string, any>) => {
return { ...r, disabled: r.disabled || false }
}) }
formSchema.value = {
fields: formSchema.value?.fields?.map((r: Record<string, any>) => {
return { ...r, disabled: r.disabled || false }
}),
}
Object.assign(originalModel, JSON.parse(JSON.stringify(form.model)))
sharedFormName.value = getSharedFormName(form.model.name)

Expand Down
63 changes: 52 additions & 11 deletions packages/entities/entities-plugins/src/components/PluginForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ const getArrayType = (list: unknown[]): string => {
return uniqueTypes.length > 1 ? 'string' : uniqueTypes[0]
}

const buildFormSchema = (parentKey: string, response: Record<string, any>, initialFormSchema: Record<string, any>) => {
const buildFormSchema = (parentKey: string, response: Record<string, any>, initialFormSchema: Record<string, any>, arrayNested?: boolean) => {
let schema = (response && response.fields) || []
const pluginSchema = customSchemas[props.pluginType as keyof typeof customSchemas]
const credentialSchema = CREDENTIAL_METADATA[props.pluginType]?.schema?.fields
Expand Down Expand Up @@ -524,10 +524,20 @@ const buildFormSchema = (parentKey: string, response: Record<string, any>, initi
}

if (scheme.fields) {
if (arrayNested && scheme.type === 'record') {
initialFormSchema[field] = {
type: 'object',
model: key,
schema: {
fields: Object.values(buildFormSchema(field, scheme, {}, true)),
},
}
return initialFormSchema
}
return buildFormSchema(field, scheme, initialFormSchema)
}

initialFormSchema[field] = { id: field } // each field's key will be set as the id
initialFormSchema[field] = { id: field, model: key } // each field's key will be set as the id
initialFormSchema[field].type = scheme.type === 'boolean' ? 'checkbox' : 'input'
initialFormSchema[field].required = scheme.required

Expand Down Expand Up @@ -669,16 +679,18 @@ const buildFormSchema = (parentKey: string, response: Record<string, any>, initi
// If itemFields is not defined, it means no custom schema for this field is defined
// This usually happens for a custom plugin, so we need to build the schema
if (!itemFields) {
initialFormSchema[field].type = 'array'
initialFormSchema[field].newElementButtonLabelClasses = 'kong-form-new-element-button-label'
initialFormSchema[field].fieldClasses = 'array-card-container-wrapper'
initialFormSchema[field].itemContainerComponent = 'FieldArrayCardContainer'

initialFormSchema[field].items = {
type: 'object',
schema: {
fields: Object.values(buildFormSchema(field, scheme.elements, {})),
fields: Object.values(buildFormSchema(field, scheme.elements, {}, true)),
},
}
initialFormSchema[field].type = 'array'
initialFormSchema[field].newElementButtonLabelClasses = 'kong-form-new-element-button-label'

// Set the model to the field name, and the label to the formatted field name
initialFormSchema[field].items.schema.fields.forEach(
(field: { id?: string, model?: string, label?: string }) => {
Expand All @@ -693,16 +705,45 @@ const buildFormSchema = (parentKey: string, response: Record<string, any>, initi
}
},
)

if (scheme.elements.type === 'record') {
/**
* FIXME Special treatment for nested fields in AI plugins
* Tell PluginEntityForm that this field is nested (not flatten), and eliminate the null
* fields from the payload
*/
initialFormSchema[field].nestedFields = true
}
}

if (!initialFormSchema[field].nestedFields) {
// If the field is an array of objects, set the default value to an object
// with the default values of the nested fields
initialFormSchema[field].items.default = () =>
scheme.elements.fields.reduce((acc: Record<string, any>, current: Record<string, { default?: string }>) => {
const key = Object.keys(current)[0]
acc[key] = current[key].default
return acc
}, {})
initialFormSchema[field].items.default = () =>
scheme.elements.fields.reduce((acc: Record<string, any>, current: Record<string, { default?: string }>) => {
const key = Object.keys(current)[0]
acc[key] = current[key].default
return acc
}, {})
} else { // FIXME: Special treatment for building default values for nested fields in AI plugins
const visit = (currField: any, defaultValue: Record<string, any>) => {
if (currField.type === 'object') {
if (currField.model) {
defaultValue[currField.model] = {}
}
for (const childField of currField.schema.fields) {
visit(childField, currField.model ? defaultValue[currField.model] : defaultValue)
}
} else if (currField.model) {
defaultValue[currField.model] = currField.default
}
}
initialFormSchema[field].items.default = () => {
const defaultValue: Record<string, any> = {}
visit(initialFormSchema[field].items, defaultValue)
return defaultValue
}
}
}

if (treatAsCredential.value && credentialSchema) {
Expand Down
28 changes: 26 additions & 2 deletions packages/entities/entities-plugins/src/definitions/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export const PLUGIN_METADATA: Record<string, Omit<PluginMetaData<I18nMessageSour
group: PluginGroup.AI,
isEnterprise: true,
nameKey: 'plugins.meta.ai-prompt-decorator.name',
scope: [PluginScope.GLOBAL, PluginScope.SERVICE, PluginScope.ROUTE, PluginScope.CONSUMER],
scope: [PluginScope.GLOBAL, PluginScope.SERVICE, PluginScope.ROUTE, PluginScope.CONSUMER, PluginScope.CONSUMER_GROUP],
useLegacyForm: true,
},
'ai-prompt-template': {
Expand All @@ -311,7 +311,7 @@ export const PLUGIN_METADATA: Record<string, Omit<PluginMetaData<I18nMessageSour
group: PluginGroup.AI,
isEnterprise: true,
nameKey: 'plugins.meta.ai-prompt-guard.name',
scope: [PluginScope.GLOBAL, PluginScope.SERVICE, PluginScope.ROUTE, PluginScope.CONSUMER],
scope: [PluginScope.GLOBAL, PluginScope.SERVICE, PluginScope.ROUTE, PluginScope.CONSUMER, PluginScope.CONSUMER_GROUP],
useLegacyForm: true,
},
'ai-request-transformer': {
Expand Down Expand Up @@ -702,6 +702,30 @@ export const PLUGIN_METADATA: Record<string, Omit<PluginMetaData<I18nMessageSour
nameKey: 'plugins.meta.standard-webhooks.name',
scope: [PluginScope.GLOBAL, PluginScope.SERVICE, PluginScope.ROUTE],
},
'ai-proxy-advanced': {
descriptionKey: 'plugins.meta.ai-proxy-advanced.description',
group: PluginGroup.AI,
isEnterprise: true,
nameKey: 'plugins.meta.ai-proxy-advanced.name',
scope: [PluginScope.GLOBAL, PluginScope.SERVICE, PluginScope.ROUTE, PluginScope.CONSUMER, PluginScope.CONSUMER_GROUP],
useLegacyForm: true,
},
'ai-semantic-caching': {
descriptionKey: 'plugins.meta.ai-semantic-caching.description',
group: PluginGroup.AI,
isEnterprise: true,
nameKey: 'plugins.meta.ai-semantic-caching.name',
scope: [PluginScope.GLOBAL, PluginScope.SERVICE, PluginScope.ROUTE, PluginScope.CONSUMER],
useLegacyForm: true,
},
'ai-semantic-prompt-guard': {
descriptionKey: 'plugins.meta.ai-semantic-prompt-guard.description',
group: PluginGroup.AI,
isEnterprise: true,
nameKey: 'plugins.meta.ai-semantic-prompt-guard.name',
scope: [PluginScope.GLOBAL, PluginScope.SERVICE, PluginScope.ROUTE, PluginScope.CONSUMER, PluginScope.CONSUMER_GROUP],
useLegacyForm: true,
},
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const aiPromptDecoratorSchema: AIPromptDecoratorSchema = {
model: 'role',
help: 'LLM message role',
type: 'select',
required: true,
values: [
'system',
'assistant',
Expand All @@ -22,6 +23,7 @@ export const aiPromptDecoratorSchema: AIPromptDecoratorSchema = {
label: 'Content',
model: 'content',
type: 'input',
required: true,
help: 'LLM message content',
inputType: 'text',
}],
Expand All @@ -40,6 +42,7 @@ export const aiPromptDecoratorSchema: AIPromptDecoratorSchema = {
model: 'role',
help: 'LLM message role',
type: 'select',
required: true,
values: [
'system',
'assistant',
Expand All @@ -49,6 +52,7 @@ export const aiPromptDecoratorSchema: AIPromptDecoratorSchema = {
label: 'Content',
model: 'content',
type: 'input',
required: true,
help: 'LLM message content',
inputType: 'text',
}],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export const aiPromptTemplateSchema: AIPromptTemplateSchema = {
model: 'name',
help: 'Template Name, for reference in user requests',
type: 'input',
required: true,
inputType: 'text',
}, {
label: 'Template String',
model: 'template',
help: 'Template string content, containing {{placeholders}} for variable substitution',
type: 'textArea',
required: true,
inputType: 'text',
}],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export const aiRateLimitingAdvancedSchema: AIRateLimitingAdvancedSchema = {
model: 'window_size',
help: 'Window size to apply a limit to (defined in seconds)',
type: 'input',
required: true,
inputType: 'number',
}, {
label: 'Name',
model: 'name',
help: 'The llm providers.',
type: 'select',
required: true,
values: [
'anthropic',
'azure',
Expand All @@ -33,6 +35,7 @@ export const aiRateLimitingAdvancedSchema: AIRateLimitingAdvancedSchema = {
model: 'limit',
help: 'Limit applied to the llm provider.',
type: 'input',
required: true,
inputType: 'number',
}],
},
Expand Down
12 changes: 12 additions & 0 deletions packages/entities/entities-plugins/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,18 @@
"standard-webhooks": {
"name": "Standard Webhooks",
"description": "Validate incoming webhooks adhere to the Standard Webhooks specification, to which Kong is a contributor"
},
"ai-proxy-advanced": {
"name": "AI Proxy Advanced",
"description": "Route across different LLMs and models using advanced load balancing algorithms, including semantic routing."
},
"ai-semantic-caching": {
"name": "AI Semantic Cache",
"description": "Semantically cache AI requests to any LLM to reduce latency, improve end-user experiences and optimize GenAI costs."
},
"ai-semantic-prompt-guard": {
"name": "AI Semantic Prompt Guard",
"description": "Semantically and intelligently create allow and deny lists of topics that can be requested across every LLM."
}
},
"fields": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface Field {
values?: string[]
id?: string
default?: string,
required?: boolean,
placeholder?: string,
hint?: string,
help?: string,
Expand Down
Loading