diff --git a/agent/component/invoke.py b/agent/component/invoke.py index a6f6cd5eeb..d31c7ed250 100644 --- a/agent/component/invoke.py +++ b/agent/component/invoke.py @@ -19,11 +19,12 @@ import os import re import time from abc import ABC + import requests +from agent.component.base import ComponentBase, ComponentParamBase from api.utils.api_utils import timeout from deepdoc.parser import HtmlParser -from agent.component.base import ComponentBase, ComponentParamBase class InvokeParam(ComponentParamBase): @@ -43,11 +44,11 @@ class InvokeParam(ComponentParamBase): self.datatype = "json" # New parameter to determine data posting type def check(self): - self.check_valid_value(self.method.lower(), "Type of content from the crawler", ['get', 'post', 'put']) + self.check_valid_value(self.method.lower(), "Type of content from the crawler", ["get", "post", "put"]) self.check_empty(self.url, "End point URL") self.check_positive_integer(self.timeout, "Timeout time in second") self.check_boolean(self.clean_html, "Clean HTML") - self.check_valid_value(self.datatype.lower(), "Data post type", ['json', 'formdata']) # Check for valid datapost value + self.check_valid_value(self.datatype.lower(), "Data post type", ["json", "formdata"]) # Check for valid datapost value class Invoke(ComponentBase, ABC): @@ -63,6 +64,18 @@ class Invoke(ComponentBase, ABC): args[para["key"]] = self._canvas.get_variable_value(para["ref"]) url = self._param.url.strip() + + def replace_variable(match): + var_name = match.group(1) + try: + value = self._canvas.get_variable_value(var_name) + return str(value or "") + except Exception: + return "" + + # {base_url} or {component_id@variable_name} + url = re.sub(r"\{([a-zA-Z_][a-zA-Z0-9_.@-]*)\}", replace_variable, url) + if url.find("http") != 0: url = "http://" + url @@ -75,52 +88,32 @@ class Invoke(ComponentBase, ABC): proxies = {"http": self._param.proxy, "https": self._param.proxy} last_e = "" - for _ in range(self._param.max_retries+1): + for _ in range(self._param.max_retries + 1): try: - if method == 'get': - response = requests.get(url=url, - params=args, - headers=headers, - proxies=proxies, - timeout=self._param.timeout) + if method == "get": + response = requests.get(url=url, params=args, headers=headers, proxies=proxies, timeout=self._param.timeout) if self._param.clean_html: sections = HtmlParser()(None, response.content) self.set_output("result", "\n".join(sections)) else: self.set_output("result", response.text) - if method == 'put': - if self._param.datatype.lower() == 'json': - response = requests.put(url=url, - json=args, - headers=headers, - proxies=proxies, - timeout=self._param.timeout) + if method == "put": + if self._param.datatype.lower() == "json": + response = requests.put(url=url, json=args, headers=headers, proxies=proxies, timeout=self._param.timeout) else: - response = requests.put(url=url, - data=args, - headers=headers, - proxies=proxies, - timeout=self._param.timeout) + response = requests.put(url=url, data=args, headers=headers, proxies=proxies, timeout=self._param.timeout) if self._param.clean_html: sections = HtmlParser()(None, response.content) self.set_output("result", "\n".join(sections)) else: self.set_output("result", response.text) - if method == 'post': - if self._param.datatype.lower() == 'json': - response = requests.post(url=url, - json=args, - headers=headers, - proxies=proxies, - timeout=self._param.timeout) + if method == "post": + if self._param.datatype.lower() == "json": + response = requests.post(url=url, json=args, headers=headers, proxies=proxies, timeout=self._param.timeout) else: - response = requests.post(url=url, - data=args, - headers=headers, - proxies=proxies, - timeout=self._param.timeout) + response = requests.post(url=url, data=args, headers=headers, proxies=proxies, timeout=self._param.timeout) if self._param.clean_html: self.set_output("result", "\n".join(sections)) else: diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index e00f1fb06c..ee841cbc28 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -1170,6 +1170,8 @@ export default { cleanHtml: 'HTML bereinigen', cleanHtmlTip: 'Wenn die Antwort im HTML-Format vorliegt und nur der Hauptinhalt gewünscht wird, schalten Sie dies bitte ein.', + invalidUrl: + 'Muss eine gültige URL oder eine URL mit Variablenplatzhaltern im Format {Variablenname} oder {Komponente@Variable} sein', reference: 'Referenz', input: 'Eingabe', output: 'Ausgabe', diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index e160af555b..22d593c9a4 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1397,6 +1397,8 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s cleanHtml: 'Clean HTML', cleanHtmlTip: 'If the response is HTML formatted and only the primary content wanted, please toggle it on.', + invalidUrl: + 'Must be a valid URL or URL with variable placeholders in the format {variable_name} or {component@variable}', reference: 'Reference', input: 'Input', output: 'Output', diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index a946515400..8d7c5da735 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -866,6 +866,19 @@ export default { noteDescription: 'Nota', notePlaceholder: 'Por favor ingresa una nota', runningHintText: 'está corriendo...🕞', + + invoke: 'Solicitud HTTP', + invokeDescription: + 'Un componente capaz de llamar a servicios remotos, utilizando las salidas de otros componentes o constantes como entradas.', + url: 'Url', + method: 'Método', + timeout: 'Tiempo de espera', + headers: 'Encabezados', + cleanHtml: 'Limpiar HTML', + cleanHtmlTip: + 'Si la respuesta está formateada en HTML y solo se desea el contenido principal, actívelo.', + invalidUrl: + 'Debe ser una URL válida o una URL con marcadores de posición de variables en el formato {nombre_variable} o {componente@variable}', }, footer: { profile: 'Todos los derechos reservados @ React', diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index 40e02b2a83..6203c10696 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -1096,6 +1096,8 @@ export default { cleanHtml: 'Nettoyer le HTML', cleanHtmlTip: 'Si la réponse est au format HTML et que seul le contenu principal est souhaité, activez cette option.', + invalidUrl: + 'Doit être une URL valide ou une URL avec des espaces réservés de variables au format {nom_variable} ou {composant@variable}', reference: 'Référence', input: 'Entrée', output: 'Sortie', diff --git a/web/src/locales/id.ts b/web/src/locales/id.ts index 57dbae0e96..c59185350a 100644 --- a/web/src/locales/id.ts +++ b/web/src/locales/id.ts @@ -1051,6 +1051,20 @@ export default { note: 'Catatan', noteDescription: 'Catatan', notePlaceholder: 'Silakan masukkan catatan', + + invoke: 'Permintaan HTTP', + invokeDescription: + 'Komponen yang mampu memanggil layanan remote, menggunakan output komponen lain atau konstanta sebagai input.', + url: 'Url', + method: 'Metode', + timeout: 'Waktu habis', + headers: 'Header', + cleanHtml: 'Bersihkan HTML', + cleanHtmlTip: + 'Jika respons diformat HTML dan hanya ingin konten utama, aktifkan opsi ini.', + invalidUrl: + 'Harus berupa URL yang valid atau URL dengan placeholder variabel dalam format {nama_variabel} atau {komponen@variabel}', + prompt: 'Prompt', promptTip: 'Gunakan prompt sistem untuk menjelaskan tugas untuk LLM, tentukan bagaimana harus merespons, dan menguraikan persyaratan lainnya. Prompt sistem sering digunakan bersama dengan kunci (variabel), yang berfungsi sebagai berbagai input data untuk LLM. Gunakan garis miring `/` atau tombol (x) untuk menampilkan kunci yang digunakan.', diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index 1d71fb7340..8a48b4fe6c 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -1098,6 +1098,8 @@ export default { cleanHtml: 'HTMLをクリーン', cleanHtmlTip: '応答がHTML形式であり、主要なコンテンツのみが必要な場合は、これをオンにしてください。', + invalidUrl: + '有効なURLまたは{variable_name}または{component@variable}形式の変数プレースホルダーを含むURLである必要があります', reference: '参照', input: '入力', output: '出力', diff --git a/web/src/locales/pt-br.ts b/web/src/locales/pt-br.ts index 64f9edb3ab..d2113dc769 100644 --- a/web/src/locales/pt-br.ts +++ b/web/src/locales/pt-br.ts @@ -1066,6 +1066,8 @@ export default { cleanHtml: 'Limpar HTML', cleanHtmlTip: 'Se a resposta for formatada em HTML e apenas o conteúdo principal for desejado, ative esta opção.', + invalidUrl: + 'Deve ser uma URL válida ou uma URL com marcadores de posição de variáveis no formato {nome_variável} ou {componente@variável}', reference: 'Referência', input: 'Entrada', diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index 296e878c8b..f922c3d01f 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -1327,6 +1327,8 @@ export default { cleanHtml: 'Очистить HTML', cleanHtmlTip: 'Включите, если нужен только основной контент из HTML-ответа.', + invalidUrl: + 'Должен быть действительный URL или URL с заполнителями переменных в формате {имя_переменной} или {компонент@переменная}', reference: 'Ссылка', input: 'Вход', output: 'Выход', diff --git a/web/src/locales/vi.ts b/web/src/locales/vi.ts index b09738d605..403dfd1a12 100644 --- a/web/src/locales/vi.ts +++ b/web/src/locales/vi.ts @@ -1128,6 +1128,8 @@ export default { cleanHtml: 'Làm sạch HTML', cleanHtmlTip: 'Nếu phản hồi được định dạng HTML và chỉ muốn nội dung chính, hãy bật nó lên.', + invalidUrl: + 'Phải là URL hợp lệ hoặc URL có chứa các biến theo định dạng {ten_bien} hoặc {thanh_phan@bien}', reference: 'Tham khảo', input: 'Đầu vào', output: 'Đầu ra', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index e589a45998..076cd4cb44 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -1153,6 +1153,8 @@ export default { headers: '請求頭', cleanHtml: '清除 HTML', cleanHtmlTip: '如果回應是 HTML 格式並且只需要主要內容,請將其開啟。', + invalidUrl: + '必須是有效的 URL 或包含變量佔位符的 URL,格式為 {variable_name} 或 {component@variable}', reference: '引用', input: '輸入', output: '輸出', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index a572831929..6ed526e198 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1359,6 +1359,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 headers: '请求头', cleanHtml: '清除 HTML', cleanHtmlTip: '如果响应是 HTML 格式且只需要主要内容,请将其打开。', + invalidUrl: + '必须是有效的 URL 或包含变量占位符的 URL,格式为 {variable_name} 或 {component@variable}', reference: '引用', input: '输入', output: '输出', diff --git a/web/src/pages/agent/form/invoke-form/index.tsx b/web/src/pages/agent/form/invoke-form/index.tsx index 3d67ec0304..594eb1b080 100644 --- a/web/src/pages/agent/form/invoke-form/index.tsx +++ b/web/src/pages/agent/form/invoke-form/index.tsx @@ -26,6 +26,7 @@ import { INextOperatorForm } from '../../interface'; import { buildOutputList } from '../../utils/build-output-list'; import { FormWrapper } from '../components/form-wrapper'; import { Output } from '../components/output'; +import { PromptEditor } from '../components/prompt-editor'; import { FormSchema, FormSchemaType } from './schema'; import { useEditVariableRecord } from './use-edit-variable'; import { VariableDialog } from './variable-dialog'; @@ -98,7 +99,13 @@ function InvokeForm({ node }: INextOperatorForm) { {t('flow.url')} - + diff --git a/web/src/pages/agent/form/invoke-form/schema.ts b/web/src/pages/agent/form/invoke-form/schema.ts index a3b11aff26..3f3b86ccf4 100644 --- a/web/src/pages/agent/form/invoke-form/schema.ts +++ b/web/src/pages/agent/form/invoke-form/schema.ts @@ -6,8 +6,54 @@ export const VariableFormSchema = z.object({ value: z.string(), }); +// {user_id} or {component@variable} +const placeholderRegex = /\{([a-zA-Z_][a-zA-Z0-9_.@-]*)\}/g; + +// URL validation schema that accepts: +// 1. Standard URLs (e.g. https://example.com/api) +// 2. URLs with variable placeholders in curly braces (e.g. https://api/{user_id}/posts) +const urlValidation = z.string().refine( + (val) => { + if (!val) return false; + + const hasPlaceholders = val.includes('{') && val.includes('}'); + const matches = [...val.matchAll(placeholderRegex)]; + + if (hasPlaceholders) { + if ( + !matches.length || + matches.some((m) => !/^[a-zA-Z_][a-zA-Z0-9_.@-]*$/.test(m[1])) + ) + return false; + + if ((val.match(/{/g) || []).length !== (val.match(/}/g) || []).length) + return false; + + const testURL = val.replace(placeholderRegex, 'placeholder'); + + return isValidURL(testURL); + } + + return isValidURL(val); + }, + { + message: 'Must be a valid URL or URL with variable placeholders', + }, +); + +function isValidURL(str: string): boolean { + try { + // Try to construct a full URL; prepend http:// if protocol is missing + new URL(str.startsWith('http') ? str : `http://${str}`); + return true; + } catch { + // Allow relative paths (e.g. /api/users) if needed + return /^\/[a-zA-Z0-9]/.test(str); + } +} + export const FormSchema = z.object({ - url: z.string().url(), + url: urlValidation, method: z.string(), timeout: z.number(), headers: z.string(),