diff --git a/web/src/pages/agent/constant/index.tsx b/web/src/pages/agent/constant/index.tsx index b6b427c1f..0f9de04ed 100644 --- a/web/src/pages/agent/constant/index.tsx +++ b/web/src/pages/agent/constant/index.tsx @@ -584,6 +584,8 @@ export const initialCodeValues = { export const initialWaitingDialogueValues = {}; +export const AgentStructuredOutputField = 'structured'; + export const initialAgentValues = { ...initialLlmBaseValues, description: '', @@ -615,7 +617,7 @@ export const initialAgentValues = { type: 'string', value: '', }, - structured: {}, + [AgentStructuredOutputField]: {}, }, }; diff --git a/web/src/pages/agent/form/agent-form/structured-output-dialog.tsx b/web/src/pages/agent/form/agent-form/structured-output-dialog.tsx index 2c81d1c8d..6ce305bff 100644 --- a/web/src/pages/agent/form/agent-form/structured-output-dialog.tsx +++ b/web/src/pages/agent/form/agent-form/structured-output-dialog.tsx @@ -34,7 +34,7 @@ export function StructuredOutputDialog({ {t('flow.structuredOutput.configuration')} -
+
diff --git a/web/src/pages/agent/form/components/prompt-editor/index.css b/web/src/pages/agent/form/components/prompt-editor/index.css index a7abebc5f..aff304f73 100644 --- a/web/src/pages/agent/form/components/prompt-editor/index.css +++ b/web/src/pages/agent/form/components/prompt-editor/index.css @@ -28,10 +28,10 @@ cursor: pointer; line-height: 16px; font-size: 15px; - display: flex; + /* display: flex; align-content: center; flex-direction: row; - flex-shrink: 0; + flex-shrink: 0; */ border: 0; } diff --git a/web/src/pages/agent/form/components/prompt-editor/utils.ts b/web/src/pages/agent/form/components/prompt-editor/utils.ts new file mode 100644 index 000000000..3f51158e9 --- /dev/null +++ b/web/src/pages/agent/form/components/prompt-editor/utils.ts @@ -0,0 +1,44 @@ +import { JSONSchema } from '@/components/jsonjoy-builder'; +import { Operator } from '@/constants/agent'; +import { isPlainObject } from 'lodash'; + +export function filterAgentStructuredOutput( + structuredOutput: JSONSchema, + operator?: string, +) { + if (typeof structuredOutput === 'boolean') { + return structuredOutput; + } + if ( + structuredOutput.properties && + isPlainObject(structuredOutput.properties) + ) { + const filterByPredicate = (predicate: (value: JSONSchema) => boolean) => { + const properties = Object.entries({ + ...structuredOutput.properties, + }).reduce( + (pre, [key, value]) => { + if (predicate(value)) { + pre[key] = value; + } + return pre; + }, + {} as Record, + ); + + return { ...structuredOutput, properties }; + }; + + if (operator === Operator.Iteration) { + return filterByPredicate( + (value) => typeof value !== 'boolean' && value.type === 'array', + ); + } else { + return filterByPredicate( + (value) => typeof value !== 'boolean' && value.type !== 'array', + ); + } + } + + return structuredOutput; +} diff --git a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx index 42f06b2cc..92679be71 100644 --- a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx +++ b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx @@ -30,10 +30,21 @@ import * as ReactDOM from 'react-dom'; import { $createVariableNode } from './variable-node'; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from '@/components/ui/hover-card'; +import { Operator } from '@/constants/agent'; +import { cn } from '@/lib/utils'; +import { AgentStructuredOutputField } from '@/pages/agent/constant'; import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query'; +import useGraphStore from '@/pages/agent/store'; +import { get, isPlainObject } from 'lodash'; import { PromptIdentity } from '../../agent-form/use-build-prompt-options'; import { ProgrammaticTag } from './constant'; import './index.css'; +import { filterAgentStructuredOutput } from './utils'; class VariableInnerOption extends MenuOption { label: string; value: string; @@ -71,6 +82,10 @@ class VariableOption extends MenuOption { } } +function getNodeId(value: string) { + return value.split('@').at(0); +} + function VariablePickerMenuItem({ index, option, @@ -82,6 +97,59 @@ function VariablePickerMenuItem({ option: VariableOption | VariableInnerOption, ) => void; }) { + const { getOperatorTypeFromId, getNode, clickedNodeId } = useGraphStore( + (state) => state, + ); + + const showSecondaryMenu = useCallback( + (value: string, outputLabel: string) => { + const nodeId = getNodeId(value); + return ( + getOperatorTypeFromId(nodeId) === Operator.Agent && + outputLabel === AgentStructuredOutputField + ); + }, + [getOperatorTypeFromId], + ); + + const renderAgentStructuredOutput = useCallback( + (values: any, option: VariableInnerOption) => { + if (isPlainObject(values) && 'properties' in values) { + return ( +
    + {Object.entries(values.properties).map(([key, value]) => { + const nextOption = new VariableInnerOption( + option.label + `.${key}`, + option.value + `.${key}`, + option.parentLabel, + option.icon, + ); + + const dataType = get(value, 'type'); + + return ( +
  • +
    selectOptionAndCleanUp(nextOption)} + className="hover:bg-bg-card p-1 text-text-primary rounded-sm flex justify-between" + > + {key} + {dataType} +
    + {dataType === 'object' && + renderAgentStructuredOutput(value, nextOption)} +
  • + ); + })} +
+ ); + } + + return
; + }, + [selectOptionAndCleanUp], + ); + return (
  • {option.title}
      - {option.options.map((x) => ( -
    • selectOptionAndCleanUp(x)} - className="hover:bg-bg-card p-1 text-text-primary rounded-sm" - > - {x.label} -
    • - ))} + {option.options.map((x) => { + const shouldShowSecondary = showSecondaryMenu(x.value, x.label); + + if (shouldShowSecondary) { + const node = getNode(getNodeId(x.value)); + const structuredOutput = get( + node, + `data.form.outputs.${AgentStructuredOutputField}`, + ); + + const filteredStructuredOutput = filterAgentStructuredOutput( + structuredOutput, + getOperatorTypeFromId(clickedNodeId), + ); + + return ( + + +
    • + {x.label} +
    • +
      + +
      +
      + {x.parentLabel} structured output: +
      + {renderAgentStructuredOutput(filteredStructuredOutput, x)} +
      +
      +
      + ); + } + + return ( +
    • selectOptionAndCleanUp(x)} + className="hover:bg-bg-card p-1 text-text-primary rounded-sm" + > + {x.label} +
    • + ); + })}
  • @@ -130,6 +239,9 @@ export default function VariablePickerMenuPlugin({ baseOptions, }: VariablePickerMenuPluginProps): JSX.Element { const [editor] = useLexicalComposerContext(); + const getOperatorTypeFromId = useGraphStore( + (state) => state.getOperatorTypeFromId, + ); // const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', { // minLength: 0, @@ -200,9 +312,28 @@ export default function VariablePickerMenuPlugin({ return pre.concat(cur.options); }, []); + // agent structured output + const fields = value.split('@'); + if ( + getOperatorTypeFromId(fields.at(0)) === Operator.Agent && + fields.at(1)?.startsWith(AgentStructuredOutputField) + ) { + // is agent structured output + const agentOption = children.find((x) => value.includes(x.value)); + const jsonSchemaFields = fields + .at(1) + ?.slice(AgentStructuredOutputField.length); + + return { + ...agentOption, + label: (agentOption?.label ?? '') + jsonSchemaFields, + value: value, + }; + } + return children.find((x) => x.value === value); }, - [options], + [getOperatorTypeFromId, options], ); const onSelectOption = useCallback(