Feat: HTTP componant supports variables (#10432)

### What problem does this PR solve?

HTTP component supports variables. #10382




![http1](https://github.com/user-attachments/assets/196a2a5b-461c-455c-8896-ec2efe7c0a13)


![http2](https://github.com/user-attachments/assets/0ab97cb0-323c-456e-b556-6f416d52e59f)


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
Yongteng Lei
2025-10-09 16:05:58 +08:00
committed by GitHub
parent 2078d88c28
commit 66d0d44a00
14 changed files with 127 additions and 36 deletions

View File

@ -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:

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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.',

View File

@ -1098,6 +1098,8 @@ export default {
cleanHtml: 'HTMLをクリーン',
cleanHtmlTip:
'応答がHTML形式であり、主要なコンテンツのみが必要な場合は、これをオンにしてください。',
invalidUrl:
'有効なURLまたは{variable_name}または{component@variable}形式の変数プレースホルダーを含むURLである必要があります',
reference: '参照',
input: '入力',
output: '出力',

View File

@ -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',

View File

@ -1327,6 +1327,8 @@ export default {
cleanHtml: 'Очистить HTML',
cleanHtmlTip:
'Включите, если нужен только основной контент из HTML-ответа.',
invalidUrl:
'Должен быть действительный URL или URL с заполнителями переменных в формате {имя_переменной} или {компонент@переменная}',
reference: 'Ссылка',
input: 'Вход',
output: 'Выход',

View File

@ -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',

View File

@ -1153,6 +1153,8 @@ export default {
headers: '請求頭',
cleanHtml: '清除 HTML',
cleanHtmlTip: '如果回應是 HTML 格式並且只需要主要內容,請將其開啟。',
invalidUrl:
'必須是有效的 URL 或包含變量佔位符的 URL格式為 {variable_name} 或 {component@variable}',
reference: '引用',
input: '輸入',
output: '輸出',

View File

@ -1359,6 +1359,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
headers: '请求头',
cleanHtml: '清除 HTML',
cleanHtmlTip: '如果响应是 HTML 格式且只需要主要内容,请将其打开。',
invalidUrl:
'必须是有效的 URL 或包含变量占位符的 URL格式为 {variable_name} 或 {component@variable}',
reference: '引用',
input: '输入',
output: '输出',

View File

@ -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) {
<FormItem>
<FormLabel>{t('flow.url')}</FormLabel>
<FormControl>
<Input {...field} placeholder="http://" />
<PromptEditor
value={field.value}
onChange={field.onChange}
placeholder="http://"
showToolbar={false}
multiLine={false}
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@ -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(),