Skip to content

Commit

Permalink
feat(entities-plugins): support new ai plugins (#1517)
Browse files Browse the repository at this point in the history
* feat(entities-plugins): support new ai plugins

* fix(entities-plugin): nested record fields under array fields

* chore(*): hook in inspectors

* test(*): fix test case

* fix(*): null fieldValue

* fix(*): deep object

* chore(*): delete com

* fix(*): logo name

* chore(*): revert scope changes

* fix(*): elements default value

* fix(*): ai-prompt-decorator required fields

* fix(*): more required fields

* fix(*): nested label

* fix(*): cleanup

---------

Co-authored-by: Makito <i@maki.to>
  • Loading branch information
Leopoldthecoder and sumimakito authored Jul 31, 2024
1 parent 47f2e28 commit b9ffef7
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 18 deletions.
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

0 comments on commit b9ffef7

Please sign in to comment.