// // Copyright 2026 The InfiniFlow Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Package component — UserFillUp component (T3). // // UserFillUp is the user-interaction / form-filling node. It // renders an optional `tips` template (with `{{key}}` placeholders // resolved against the form's input map) and passes each form // field through to its downstream outputs. File-type inputs // (value.type starts with "file") emit a stable "" stub // so the run keeps flowing while the FileService integration // surfaces the actual bytes via the storage layer. // package component import ( "context" "encoding/json" "fmt" "regexp" "strings" "ragflow/internal/agent/runtime" ) const componentNameUserFillUp = "UserFillUp" // defaultUserFillUpTips is used when the operator omits the `tips` param. const defaultUserFillUpTips = "Please fill up the form" // fileStubPrefix is prepended to the form-field key when an input is // classified as a file-type input. The FileService.get_files payload // surfaces the actual bytes via the storage layer. const fileStubPrefix = "" stubs. Plain string fields use their value // verbatim; non-string values are coerced via fmt.Sprintf("%v", ...). func renderTips(template string, fields map[string]any) string { if template == "" { return "" } return tipsPlaceholderPattern.ReplaceAllStringFunc(template, func(match string) string { sub := tipsPlaceholderPattern.FindStringSubmatch(match) if len(sub) < 2 { return match } key := sub[1] raw, ok := fields[key] if !ok { return "" } return fieldValueToString(key, raw) }) } // resolveFieldValue converts one form-field payload into the value // that should appear in the component's output map. // // Rules: // - dict with type starting with "file" → "" stub // - dict with type=="object" and string value → parsed JSON // - dict with optional=true and value==nil → nil // - dict with a `value` field → the inner value // - anything else → pass through unchanged func resolveFieldValue(key string, raw any) any { m, ok := raw.(map[string]any) if !ok { return raw } if isFileType(m) { return fileStubPrefix + key + ">" } if opt, _ := m["optional"].(bool); opt { if v, present := m["value"]; !present || v == nil { return nil } } if isObjectType(m) { if rawStr, ok := m["value"].(string); ok && strings.TrimSpace(rawStr) != "" { var parsed any if err := json.Unmarshal([]byte(rawStr), &parsed); err != nil { return rawStr } return parsed } } if v, present := m["value"]; present { return v } return m } // isFileType reports whether the form-field payload's `type` field // starts with "file" (case-insensitive). func isFileType(m map[string]any) bool { t, _ := m["type"].(string) return strings.HasPrefix(strings.ToLower(t), "file") } // isObjectType reports whether the form-field payload's `type` field // equals "object". func isObjectType(m map[string]any) bool { t, _ := m["type"].(string) return t == "object" } // fieldValueToString is the tips-substitution variant of // resolveFieldValue: it returns a string suitable for direct insertion // into the rendered template. File stubs are emitted here too so the // tips template can reference a file field without crashing. func fieldValueToString(key string, raw any) string { if m, ok := raw.(map[string]any); ok { if isFileType(m) { return fileStubPrefix + key + ">" } if v, present := m["value"]; present { return stringifyField(v) } return "" } return stringifyField(raw) } // stringifyField renders a single form-field value as a string for // template substitution. nil → "", strings stay verbatim, everything // else uses %v. func stringifyField(v any) string { if v == nil { return "" } if s, ok := v.(string); ok { return s } return fmt.Sprintf("%v", v) } // init registers UserFillUp with the orchestrator-owned registry. func init() { Register(componentNameUserFillUp, func(params map[string]any) (Component, error) { var p userFillUpParam if err := p.Update(params); err != nil { return nil, err } return NewUserFillUpComponent(p), nil }) }