mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
Feat: The query variables of the subsequent operators can reference the structured variables defined in the agent operator. #10866 (#10902)
### What problem does this PR solve? Feat: The query variables of the subsequent operators can reference the structured variables defined in the agent operator. #10866 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import i18n from '@/locales/config';
|
import i18n from '@/locales/config';
|
||||||
import { BeginId } from '@/pages/flow/constant';
|
import { BeginId } from '@/pages/agent/constant';
|
||||||
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
|
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
const prefix = BeginId + '@';
|
const prefix = BeginId + '@';
|
||||||
|
|||||||
@ -114,7 +114,7 @@ export default function VariablePickerMenuPlugin({
|
|||||||
minLength: 0,
|
minLength: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [queryString, setQueryString] = React.useState<string| null>('');
|
const [queryString, setQueryString] = React.useState<string | null>('');
|
||||||
|
|
||||||
const options = useBuildComponentIdSelectOptions(node?.id, node?.parentId);
|
const options = useBuildComponentIdSelectOptions(node?.id, node?.parentId);
|
||||||
|
|
||||||
|
|||||||
@ -1623,7 +1623,7 @@ This delimiter is used to split the input text into several text pieces echo of
|
|||||||
extractorDescription:
|
extractorDescription:
|
||||||
'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.',
|
'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.',
|
||||||
outputFormat: 'Output format',
|
outputFormat: 'Output format',
|
||||||
fileFormats: 'File format',
|
fileFormats: 'File type',
|
||||||
fileFormatOptions: {
|
fileFormatOptions: {
|
||||||
pdf: 'PDF',
|
pdf: 'PDF',
|
||||||
spreadsheet: 'Spreadsheet',
|
spreadsheet: 'Spreadsheet',
|
||||||
@ -1644,7 +1644,7 @@ This delimiter is used to split the input text into several text pieces echo of
|
|||||||
searchMethodTip: `Defines how the content can be searched — by full-text, embedding, or both.
|
searchMethodTip: `Defines how the content can be searched — by full-text, embedding, or both.
|
||||||
The Indexer will store the content in the corresponding data structures for the selected methods.`,
|
The Indexer will store the content in the corresponding data structures for the selected methods.`,
|
||||||
// file: 'File',
|
// file: 'File',
|
||||||
parserMethod: 'Parsing method',
|
parserMethod: 'PDF parser',
|
||||||
// systemPrompt: 'System Prompt',
|
// systemPrompt: 'System Prompt',
|
||||||
systemPromptPlaceholder:
|
systemPromptPlaceholder:
|
||||||
'Enter system prompt for image analysis, if empty the system default value will be used',
|
'Enter system prompt for image analysis, if empty the system default value will be used',
|
||||||
|
|||||||
@ -1529,7 +1529,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
|||||||
extractorDescription:
|
extractorDescription:
|
||||||
'使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',
|
'使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',
|
||||||
outputFormat: '输出格式',
|
outputFormat: '输出格式',
|
||||||
fileFormats: '文件格式',
|
fileFormats: '文件类型',
|
||||||
fields: '字段',
|
fields: '字段',
|
||||||
addParser: '增加解析器',
|
addParser: '增加解析器',
|
||||||
hierarchy: '层次结构',
|
hierarchy: '层次结构',
|
||||||
|
|||||||
@ -617,7 +617,10 @@ export const initialAgentValues = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
[AgentStructuredOutputField]: {},
|
[AgentStructuredOutputField]: {
|
||||||
|
type: 'Object Array String Number Boolean',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -31,20 +31,15 @@ import * as ReactDOM from 'react-dom';
|
|||||||
import { $createVariableNode } from './variable-node';
|
import { $createVariableNode } from './variable-node';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
useFilterStructuredOutputByValue,
|
||||||
HoverCardContent,
|
useFindAgentStructuredOutputLabel,
|
||||||
HoverCardTrigger,
|
useShowSecondaryMenu,
|
||||||
} from '@/components/ui/hover-card';
|
} from '@/pages/agent/hooks/use-build-structured-output';
|
||||||
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 { 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 { PromptIdentity } from '../../agent-form/use-build-prompt-options';
|
||||||
|
import { StructuredOutputSecondaryMenu } from '../structured-output-secondary-menu';
|
||||||
import { ProgrammaticTag } from './constant';
|
import { ProgrammaticTag } from './constant';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import { filterAgentStructuredOutput } from './utils';
|
|
||||||
class VariableInnerOption extends MenuOption {
|
class VariableInnerOption extends MenuOption {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
@ -82,10 +77,6 @@ class VariableOption extends MenuOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeId(value: string) {
|
|
||||||
return value.split('@').at(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VariablePickerMenuItem({
|
function VariablePickerMenuItem({
|
||||||
index,
|
index,
|
||||||
option,
|
option,
|
||||||
@ -97,58 +88,9 @@ function VariablePickerMenuItem({
|
|||||||
option: VariableOption | VariableInnerOption,
|
option: VariableOption | VariableInnerOption,
|
||||||
) => void;
|
) => void;
|
||||||
}) {
|
}) {
|
||||||
const { getOperatorTypeFromId, getNode, clickedNodeId } = useGraphStore(
|
const filterStructuredOutput = useFilterStructuredOutputByValue();
|
||||||
(state) => state,
|
|
||||||
);
|
|
||||||
|
|
||||||
const showSecondaryMenu = useCallback(
|
const showSecondaryMenu = useShowSecondaryMenu();
|
||||||
(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 (
|
|
||||||
<ul className="border-l">
|
|
||||||
{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 (
|
|
||||||
<li key={key} className="pl-1">
|
|
||||||
<div
|
|
||||||
onClick={() => selectOptionAndCleanUp(nextOption)}
|
|
||||||
className="hover:bg-bg-card p-1 text-text-primary rounded-sm flex justify-between"
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
<span className="text-text-secondary">{dataType}</span>
|
|
||||||
</div>
|
|
||||||
{dataType === 'object' &&
|
|
||||||
renderAgentStructuredOutput(value, nextOption)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div></div>;
|
|
||||||
},
|
|
||||||
[selectOptionAndCleanUp],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@ -165,39 +107,20 @@ function VariablePickerMenuItem({
|
|||||||
const shouldShowSecondary = showSecondaryMenu(x.value, x.label);
|
const shouldShowSecondary = showSecondaryMenu(x.value, x.label);
|
||||||
|
|
||||||
if (shouldShowSecondary) {
|
if (shouldShowSecondary) {
|
||||||
const node = getNode(getNodeId(x.value));
|
const filteredStructuredOutput = filterStructuredOutput(x.value);
|
||||||
const structuredOutput = get(
|
|
||||||
node,
|
|
||||||
`data.form.outputs.${AgentStructuredOutputField}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredStructuredOutput = filterAgentStructuredOutput(
|
|
||||||
structuredOutput,
|
|
||||||
getOperatorTypeFromId(clickedNodeId),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard key={x.value} openDelay={100} closeDelay={100}>
|
<StructuredOutputSecondaryMenu
|
||||||
<HoverCardTrigger asChild>
|
key={x.value}
|
||||||
<li className="hover:bg-bg-card p-1 text-text-primary rounded-sm">
|
data={x}
|
||||||
{x.label}
|
click={(y) =>
|
||||||
</li>
|
selectOptionAndCleanUp({
|
||||||
</HoverCardTrigger>
|
...x,
|
||||||
<HoverCardContent
|
...y,
|
||||||
side="left"
|
} as VariableInnerOption)
|
||||||
align="start"
|
}
|
||||||
className={cn(
|
filteredStructuredOutput={filteredStructuredOutput}
|
||||||
'min-w-[140px] border border-border rounded-md shadow-lg p-0',
|
></StructuredOutputSecondaryMenu>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<section className="p-2">
|
|
||||||
<div className="p-1">
|
|
||||||
{x.parentLabel} structured output:
|
|
||||||
</div>
|
|
||||||
{renderAgentStructuredOutput(filteredStructuredOutput, x)}
|
|
||||||
</section>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,9 +162,8 @@ export default function VariablePickerMenuPlugin({
|
|||||||
baseOptions,
|
baseOptions,
|
||||||
}: VariablePickerMenuPluginProps): JSX.Element {
|
}: VariablePickerMenuPluginProps): JSX.Element {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const getOperatorTypeFromId = useGraphStore(
|
|
||||||
(state) => state.getOperatorTypeFromId,
|
const findAgentStructuredOutputLabel = useFindAgentStructuredOutputLabel();
|
||||||
);
|
|
||||||
|
|
||||||
// const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
// const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||||
// minLength: 0,
|
// minLength: 0,
|
||||||
@ -313,27 +235,17 @@ export default function VariablePickerMenuPlugin({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// agent structured output
|
// agent structured output
|
||||||
const fields = value.split('@');
|
const agentStructuredOutput = findAgentStructuredOutputLabel(
|
||||||
if (
|
value,
|
||||||
getOperatorTypeFromId(fields.at(0)) === Operator.Agent &&
|
children,
|
||||||
fields.at(1)?.startsWith(AgentStructuredOutputField)
|
);
|
||||||
) {
|
if (agentStructuredOutput) {
|
||||||
// is agent structured output
|
return agentStructuredOutput;
|
||||||
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);
|
return children.find((x) => x.value === value);
|
||||||
},
|
},
|
||||||
[getOperatorTypeFromId, options],
|
[findAgentStructuredOutputLabel, options],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectOption = useCallback(
|
const onSelectOption = useCallback(
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
@ -12,6 +11,7 @@ import { useFormContext } from 'react-hook-form';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { VariableType } from '../../constant';
|
import { VariableType } from '../../constant';
|
||||||
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
|
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
|
||||||
|
import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu';
|
||||||
|
|
||||||
type QueryVariableProps = {
|
type QueryVariableProps = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -52,11 +52,11 @@ export function QueryVariable({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
)}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectWithSearch
|
<GroupedSelectWithSecondaryMenu
|
||||||
options={finalOptions}
|
options={finalOptions}
|
||||||
{...field}
|
{...field}
|
||||||
allowClear
|
// allowClear
|
||||||
></SelectWithSearch>
|
></GroupedSelectWithSecondaryMenu>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -0,0 +1,206 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@/components/ui/command';
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@/components/ui/hover-card';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { ChevronDown, X } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
useFilterStructuredOutputByValue,
|
||||||
|
useFindAgentStructuredOutputLabel,
|
||||||
|
useShowSecondaryMenu,
|
||||||
|
} from '../../hooks/use-build-structured-output';
|
||||||
|
import { StructuredOutputSecondaryMenu } from './structured-output-secondary-menu';
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
children?: Item[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Group = {
|
||||||
|
label: string | React.ReactNode;
|
||||||
|
options: Option[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GroupedSelectWithSecondaryMenuProps {
|
||||||
|
options: Group[];
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupedSelectWithSecondaryMenu({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = 'Select an option...',
|
||||||
|
}: GroupedSelectWithSecondaryMenuProps) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const showSecondaryMenu = useShowSecondaryMenu();
|
||||||
|
const filterStructuredOutput = useFilterStructuredOutputByValue();
|
||||||
|
const findAgentStructuredOutputLabel = useFindAgentStructuredOutputLabel();
|
||||||
|
|
||||||
|
// Find the label of the selected item
|
||||||
|
const flattenedOptions = options.flatMap((g) => g.options);
|
||||||
|
let selectedLabel =
|
||||||
|
flattenedOptions
|
||||||
|
.flatMap((o) => [o, ...(o.children || [])])
|
||||||
|
.find((o) => o.value === value)?.label || '';
|
||||||
|
|
||||||
|
if (!selectedLabel && value) {
|
||||||
|
selectedLabel =
|
||||||
|
findAgentStructuredOutputLabel(value, flattenedOptions)?.label ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle clear click
|
||||||
|
const handleClear = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onChange?.('');
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecondaryMenuClick = useCallback(
|
||||||
|
(record: Item) => {
|
||||||
|
onChange?.(record.value);
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className={cn(
|
||||||
|
'w-full justify-between text-sm font-normal',
|
||||||
|
!value && 'text-muted-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{selectedLabel || placeholder}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{value && (
|
||||||
|
<X
|
||||||
|
className="h-4 w-4 text-muted-foreground hover:text-foreground cursor-pointer"
|
||||||
|
onClick={handleClear}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search..." />
|
||||||
|
<CommandList className="overflow-visible">
|
||||||
|
{options.map((group, idx) => (
|
||||||
|
<CommandGroup key={idx} heading={group.label}>
|
||||||
|
{group.options.map((option) => {
|
||||||
|
const shouldShowSecondary = showSecondaryMenu(
|
||||||
|
option.value,
|
||||||
|
option.label,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldShowSecondary) {
|
||||||
|
const filteredStructuredOutput = filterStructuredOutput(
|
||||||
|
option.value,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<StructuredOutputSecondaryMenu
|
||||||
|
key={option.value}
|
||||||
|
data={option}
|
||||||
|
click={handleSecondaryMenuClick}
|
||||||
|
filteredStructuredOutput={filteredStructuredOutput}
|
||||||
|
></StructuredOutputSecondaryMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return option.children ? (
|
||||||
|
<HoverCard
|
||||||
|
key={option.value}
|
||||||
|
openDelay={100}
|
||||||
|
closeDelay={150}
|
||||||
|
>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {}}
|
||||||
|
className="flex items-center justify-between cursor-default"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
<span className="ml-auto text-muted-foreground">
|
||||||
|
›
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent
|
||||||
|
side="right"
|
||||||
|
align="start"
|
||||||
|
className="w-[180px] p-1"
|
||||||
|
>
|
||||||
|
{option.children.map((child) => (
|
||||||
|
<div
|
||||||
|
key={child.value}
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground',
|
||||||
|
value === child.value &&
|
||||||
|
'bg-accent text-accent-foreground',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
onChange?.(child.value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{child.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
) : (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => {
|
||||||
|
onChange?.(option.value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
value === option.value &&
|
||||||
|
'bg-accent text-accent-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import { JSONSchema } from '@/components/jsonjoy-builder';
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@/components/ui/hover-card';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { get, isPlainObject } from 'lodash';
|
||||||
|
import { PropsWithChildren, ReactNode, useCallback } from 'react';
|
||||||
|
|
||||||
|
type DataItem = { label: ReactNode; value: string; parentLabel?: ReactNode };
|
||||||
|
|
||||||
|
type StructuredOutputSecondaryMenuProps = {
|
||||||
|
data: DataItem;
|
||||||
|
click(option: { label: ReactNode; value: string }): void;
|
||||||
|
filteredStructuredOutput: JSONSchema;
|
||||||
|
} & PropsWithChildren;
|
||||||
|
export function StructuredOutputSecondaryMenu({
|
||||||
|
data,
|
||||||
|
click,
|
||||||
|
filteredStructuredOutput,
|
||||||
|
}: StructuredOutputSecondaryMenuProps) {
|
||||||
|
const renderAgentStructuredOutput = useCallback(
|
||||||
|
(values: any, option: { label: ReactNode; value: string }) => {
|
||||||
|
if (isPlainObject(values) && 'properties' in values) {
|
||||||
|
return (
|
||||||
|
<ul className="border-l">
|
||||||
|
{Object.entries(values.properties).map(([key, value]) => {
|
||||||
|
const nextOption = {
|
||||||
|
label: option.label + `.${key}`,
|
||||||
|
value: option.value + `.${key}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataType = get(value, 'type');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={key} className="pl-1">
|
||||||
|
<div
|
||||||
|
onClick={() => click(nextOption)}
|
||||||
|
className="hover:bg-bg-card p-1 text-text-primary rounded-sm flex justify-between"
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
<span className="text-text-secondary">{dataType}</span>
|
||||||
|
</div>
|
||||||
|
{dataType === 'object' &&
|
||||||
|
renderAgentStructuredOutput(value, nextOption)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div></div>;
|
||||||
|
},
|
||||||
|
[click],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverCard key={data.value} openDelay={100} closeDelay={100}>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<li className="hover:bg-bg-card p-1 text-text-primary rounded-sm">
|
||||||
|
{data.label}
|
||||||
|
</li>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent
|
||||||
|
side="left"
|
||||||
|
align="start"
|
||||||
|
className={cn(
|
||||||
|
'min-w-[140px] border border-border rounded-md shadow-lg p-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<section className="p-2">
|
||||||
|
<div className="p-1">{data?.parentLabel} structured output:</div>
|
||||||
|
{renderAgentStructuredOutput(filteredStructuredOutput, data)}
|
||||||
|
</section>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { Collapse } from '@/components/collapse';
|
|||||||
import { FormContainer } from '@/components/form-container';
|
import { FormContainer } from '@/components/form-container';
|
||||||
import NumberInput from '@/components/originui/number-input';
|
import NumberInput from '@/components/originui/number-input';
|
||||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -86,6 +87,8 @@ function InvokeForm({ node }: INextOperatorForm) {
|
|||||||
|
|
||||||
const variables = useWatch({ control: form.control, name: 'variables' });
|
const variables = useWatch({ control: form.control, name: 'variables' });
|
||||||
|
|
||||||
|
const isDarkTheme = useIsDarkTheme();
|
||||||
|
|
||||||
useWatchFormChange(node?.id, form);
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -147,7 +150,7 @@ function InvokeForm({ node }: INextOperatorForm) {
|
|||||||
<Editor
|
<Editor
|
||||||
height={200}
|
height={200}
|
||||||
defaultLanguage="json"
|
defaultLanguage="json"
|
||||||
theme="vs-dark"
|
theme={isDarkTheme ? 'vs-dark' : undefined}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
92
web/src/pages/agent/hooks/use-build-structured-output.ts
Normal file
92
web/src/pages/agent/hooks/use-build-structured-output.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { get } from 'lodash';
|
||||||
|
import { ReactNode, useCallback } from 'react';
|
||||||
|
import { AgentStructuredOutputField, Operator } from '../constant';
|
||||||
|
import useGraphStore from '../store';
|
||||||
|
import { filterAgentStructuredOutput } from '../utils/filter-agent-structured-output';
|
||||||
|
|
||||||
|
function getNodeId(value: string) {
|
||||||
|
return value.split('@').at(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShowSecondaryMenu() {
|
||||||
|
const { getOperatorTypeFromId } = useGraphStore((state) => state);
|
||||||
|
|
||||||
|
const showSecondaryMenu = useCallback(
|
||||||
|
(value: string, outputLabel: string) => {
|
||||||
|
const nodeId = getNodeId(value);
|
||||||
|
return (
|
||||||
|
getOperatorTypeFromId(nodeId) === Operator.Agent &&
|
||||||
|
outputLabel === AgentStructuredOutputField
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[getOperatorTypeFromId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return showSecondaryMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFilterStructuredOutputByValue() {
|
||||||
|
const { getOperatorTypeFromId, getNode, clickedNodeId } = useGraphStore(
|
||||||
|
(state) => state,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterStructuredOutput = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const node = getNode(getNodeId(value));
|
||||||
|
const structuredOutput = get(
|
||||||
|
node,
|
||||||
|
`data.form.outputs.${AgentStructuredOutputField}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredStructuredOutput = filterAgentStructuredOutput(
|
||||||
|
structuredOutput,
|
||||||
|
getOperatorTypeFromId(clickedNodeId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredStructuredOutput;
|
||||||
|
},
|
||||||
|
[clickedNodeId, getNode, getOperatorTypeFromId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return filterStructuredOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFindAgentStructuredOutputLabel() {
|
||||||
|
const getOperatorTypeFromId = useGraphStore(
|
||||||
|
(state) => state.getOperatorTypeFromId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const findAgentStructuredOutputLabel = useCallback(
|
||||||
|
(
|
||||||
|
value: string,
|
||||||
|
options: Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
parentLabel?: string | ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
// 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 = options.find((x) => value.includes(x.value));
|
||||||
|
const jsonSchemaFields = fields
|
||||||
|
.at(1)
|
||||||
|
?.slice(AgentStructuredOutputField.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...agentOption,
|
||||||
|
label: (agentOption?.label ?? '') + jsonSchemaFields,
|
||||||
|
value: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getOperatorTypeFromId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return findAgentStructuredOutputLabel;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user