diff --git a/agent/component/invoke.py b/agent/component/invoke.py index a6f6cd5ee..d31c7ed25 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 e00f1fb06..ee841cbc2 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 e160af555..22d593c9a 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 a94651540..8d7c5da73 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 40e02b2a8..6203c1069 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 57dbae0e9..c59185350 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 1d71fb734..8a48b4fe6 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 64f9edb3a..d2113dc76 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 296e878c8..f922c3d01 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 b09738d60..403dfd1a1 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 e589a4599..076cd4cb4 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 a57283192..6ed526e19 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 3d67ec030..594eb1b08 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 a3b11aff2..3f3b86ccf 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(),