mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-11-10 21:32:43 +08:00
fix(agent): support String and Object Invoke parameters (#21953)
This commit is contained in:
1 parent
06d8605d4c
commit
6aeb92deff
6 files changed
+171
-22
No files matched your search
@@ -230,6 +230,18 @@ func (i *InvokeComponent) Invoke(ctx context.Context, db *gorm.DB, inputs map[st
|
||||
}
|
||||
}
|
||||
}
|
||||
if variable["type"] == "object" {
|
||||
if text, ok := value.(string); ok {
|
||||
var object map[string]any
|
||||
if err := json.Unmarshal([]byte(text), &object); err != nil || object == nil {
|
||||
return nil, fmt.Errorf("Invoke: parameter %q must be a valid JSON object", key)
|
||||
}
|
||||
value = object
|
||||
}
|
||||
if object, ok := value.(map[string]any); !ok || object == nil {
|
||||
return nil, fmt.Errorf("Invoke: parameter %q must be a valid JSON object", key)
|
||||
}
|
||||
}
|
||||
fields[key] = value
|
||||
}
|
||||
if len(fields) > 0 {
|
||||
|
||||
@@ -18,6 +18,7 @@ package component
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -699,3 +700,78 @@ func TestInvoke_ProxyRejectsHostnameTarget(t *testing.T) {
|
||||
t.Errorf("hostname+proxy target: _ERROR = %q, want %q", got, "URL not valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvoke_ParameterTypes(t *testing.T) {
|
||||
setupAllowAnyHost(t, true)
|
||||
for _, tc := range []struct {
|
||||
name, kind string
|
||||
value any
|
||||
invalid bool
|
||||
}{
|
||||
{"legacy", "", `{"nested":{"items":[1,true]}}`, false},
|
||||
{"string", "string", `{"nested":{"items":[1,true]}}`, false},
|
||||
{"object", "object", `{"nested":{"items":[1,true]}}`, false},
|
||||
{"native", "object", map[string]any{"nested": map[string]any{"items": []any{float64(1), true}}}, false},
|
||||
{"invalid", "object", `{`, true},
|
||||
{"array", "object", `[]`, true},
|
||||
{"null", "object", `null`, true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, ref := range []string{"", "source@result"} {
|
||||
var captured any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
captured = body["input"]
|
||||
w.Write([]byte(`ok`))
|
||||
}))
|
||||
c, err := NewInvokeComponent(map[string]any{"url": srv.URL, "method": "POST", "variables": []any{map[string]any{"key": "input", "type": tc.kind, "value": tc.value, "ref": ref}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = c.Invoke(t.Context(), nil, map[string]any{"source@result": tc.value})
|
||||
srv.Close()
|
||||
if tc.invalid {
|
||||
if err == nil || !strings.Contains(err.Error(), `parameter "input" must be a valid JSON object`) {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := tc.value
|
||||
if tc.kind == "object" {
|
||||
expected = map[string]any{"nested": map[string]any{"items": []any{float64(1), true}}}
|
||||
}
|
||||
if !reflect.DeepEqual(captured, expected) {
|
||||
t.Fatalf("wire value = %#v, want %#v", captured, expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvoke_ExplicitBodyIgnoresParameterTypes(t *testing.T) {
|
||||
setupAllowAnyHost(t, true)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if string(body) != `{"explicit":true}` {
|
||||
t.Errorf("body = %s", body)
|
||||
}
|
||||
w.Write([]byte("ok"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
c, err := NewInvokeComponent(map[string]any{
|
||||
"url": srv.URL, "method": "PUT", "body": `{"explicit":true}`,
|
||||
"variables": []any{map[string]any{"key": "ignored", "type": "object", "value": "invalid JSON"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := c.Invoke(t.Context(), nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -515,6 +515,11 @@ export const initialCrawlerValues = {
|
||||
query: '',
|
||||
};
|
||||
|
||||
export const InvokeParameterTypeOptions = [
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Object', value: 'object' },
|
||||
];
|
||||
|
||||
export const initialInvokeValues = {
|
||||
url: '',
|
||||
method: 'GET',
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const VariableFormSchema = z.object({
|
||||
key: z.string(),
|
||||
ref: z.string(),
|
||||
value: z.string(),
|
||||
});
|
||||
export const VariableFormSchema = z
|
||||
.object({
|
||||
key: z.string(),
|
||||
ref: z.string(),
|
||||
value: z.string(),
|
||||
type: z.enum(['string', 'object']).optional(),
|
||||
})
|
||||
.superRefine(({ type, ref, value }, context) => {
|
||||
if (type !== 'object' || ref) return;
|
||||
|
||||
try {
|
||||
const object = JSON.parse(value);
|
||||
if (
|
||||
object !== null &&
|
||||
typeof object === 'object' &&
|
||||
!Array.isArray(object)
|
||||
)
|
||||
return;
|
||||
} catch {
|
||||
// Report malformed JSON and non-object JSON with the same field error.
|
||||
}
|
||||
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['value'],
|
||||
message: 'Enter a valid JSON object.',
|
||||
});
|
||||
});
|
||||
|
||||
// {user_id} or {component@variable}
|
||||
const placeholderRegex = /\{([a-zA-Z_][a-zA-Z0-9_.@-]*)\}/g;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -15,15 +16,17 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { InvokeParameterTypeOptions } from '../../constant';
|
||||
import { QueryVariable } from '../components/query-variable';
|
||||
import { VariableFormSchemaType } from './schema';
|
||||
import { VariableFormSchema, VariableFormSchemaType } from './schema';
|
||||
|
||||
type ModalFormProps = {
|
||||
initialValue: VariableFormSchemaType;
|
||||
@@ -39,18 +42,17 @@ function VariableForm({
|
||||
submit,
|
||||
}: ModalFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const FormSchema = z.object({
|
||||
key: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine(
|
||||
(value) =>
|
||||
!value || !otherThanCurrentQuery.some((x) => x.key === value),
|
||||
{ message: 'The key cannot be repeated!' },
|
||||
),
|
||||
ref: z.string(),
|
||||
value: z.string(),
|
||||
const FormSchema = VariableFormSchema.superRefine(({ key }, context) => {
|
||||
if (
|
||||
!key.trim() ||
|
||||
otherThanCurrentQuery.some((x) => x.key === key.trim())
|
||||
) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['key'],
|
||||
message: !key.trim() ? 'Required' : 'The key cannot be repeated!',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
@@ -60,19 +62,22 @@ function VariableForm({
|
||||
key: '',
|
||||
value: '',
|
||||
ref: '',
|
||||
type: 'string',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(initialValue)) {
|
||||
form.reset(initialValue);
|
||||
form.reset({ type: 'string', ...initialValue });
|
||||
}
|
||||
}, [form, initialValue]);
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
submit(data);
|
||||
submit({ ...data, key: data.key.trim() });
|
||||
}
|
||||
|
||||
const parameterType = useWatch({ control: form.control, name: 'type' });
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -95,6 +100,23 @@ function VariableForm({
|
||||
)}
|
||||
/>
|
||||
<QueryVariable name="ref" label={t('flow.ref')}></QueryVariable>
|
||||
<FormField
|
||||
name="type"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('flow.type')}</FormLabel>
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
value={field.value ?? 'string'}
|
||||
onChange={field.onChange}
|
||||
options={InvokeParameterTypeOptions}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
name="value"
|
||||
control={form.control}
|
||||
@@ -102,7 +124,11 @@ function VariableForm({
|
||||
<FormItem>
|
||||
<FormLabel>{t('flow.value')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
{parameterType === 'object' ? (
|
||||
<Textarea {...field} className="font-mono" rows={5} />
|
||||
) : (
|
||||
<Input {...field} />
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -76,6 +76,13 @@ export function VariableTable({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: t('flow.type'),
|
||||
cell: ({ row }) => (
|
||||
<div>{row.original.type === 'object' ? 'Object' : 'String'}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'ref',
|
||||
header: t('flow.ref'),
|
||||
|
||||
Reference in new issue
Block a user