001/**
002 * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.gnu.org/licenses/lgpl-3.0.txt
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package dev.tinyflow.core.node;
017
018import com.agentsflex.core.chain.Chain;
019import com.agentsflex.core.chain.DataType;
020import com.agentsflex.core.chain.Parameter;
021import com.agentsflex.core.chain.node.BaseNode;
022import com.agentsflex.core.llm.client.OkHttpClientUtil;
023import com.agentsflex.core.prompt.template.TextPromptTemplate;
024import com.agentsflex.core.util.StringUtil;
025import com.alibaba.fastjson.JSON;
026import com.alibaba.fastjson.JSONObject;
027import okhttp3.*;
028
029import java.io.IOException;
030import java.io.UnsupportedEncodingException;
031import java.net.URLEncoder;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035
036public class HttpNode extends BaseNode {
037
038    private String url;
039    private String method;
040
041    private List<Parameter> headers;
042
043    private String bodyType;
044    private List<Parameter> formData;
045    private List<Parameter> formUrlencoded;
046    private String bodyJson;
047    private String rawBody;
048
049    public static String mapToQueryString(Map<String, Object> map) {
050        if (map == null || map.isEmpty()) {
051            return "";
052        }
053
054        StringBuilder stringBuilder = new StringBuilder();
055
056        for (String key : map.keySet()) {
057            if (StringUtil.noText(key)) {
058                continue;
059            }
060            if (stringBuilder.length() > 0) {
061                stringBuilder.append("&");
062            }
063            stringBuilder.append(key.trim());
064            stringBuilder.append("=");
065            Object value = map.get(key);
066            stringBuilder.append(value == null ? "" : urlEncode(value.toString().trim()));
067        }
068        return stringBuilder.toString();
069    }
070
071    public static String urlEncode(String string) {
072        try {
073            return URLEncoder.encode(string, "UTF-8");
074        } catch (UnsupportedEncodingException e) {
075            throw new RuntimeException(e);
076        }
077    }
078
079    public String getUrl() {
080        return url;
081    }
082
083    public void setUrl(String url) {
084        this.url = url;
085    }
086
087    public String getMethod() {
088        return method;
089    }
090
091    public void setMethod(String method) {
092        this.method = method;
093    }
094
095    public List<Parameter> getHeaders() {
096        return headers;
097    }
098
099    public void setHeaders(List<Parameter> headers) {
100        this.headers = headers;
101    }
102
103    public String getBodyType() {
104        return bodyType;
105    }
106
107    public void setBodyType(String bodyType) {
108        this.bodyType = bodyType;
109    }
110
111    public List<Parameter> getFormData() {
112        return formData;
113    }
114
115    public void setFormData(List<Parameter> formData) {
116        this.formData = formData;
117    }
118
119    public List<Parameter> getFormUrlencoded() {
120        return formUrlencoded;
121    }
122
123    public void setFormUrlencoded(List<Parameter> formUrlencoded) {
124        this.formUrlencoded = formUrlencoded;
125    }
126
127    public String getBodyJson() {
128        return bodyJson;
129    }
130
131    public void setBodyJson(String bodyJson) {
132        this.bodyJson = bodyJson;
133    }
134
135    public String getRawBody() {
136        return rawBody;
137    }
138
139    public void setRawBody(String rawBody) {
140        this.rawBody = rawBody;
141    }
142
143    @Override
144    protected Map<String, Object> execute(Chain chain) {
145
146        Map<String, Object> argsMap = chain.getParameterValues(this);
147        String newUrl = TextPromptTemplate.of(url).formatToString(argsMap);
148
149        Request.Builder reqBuilder = new Request.Builder().url(newUrl);
150
151        Map<String, Object> headersMap = chain.getParameterValues(this, headers, argsMap);
152        headersMap.forEach((s, o) -> reqBuilder.addHeader(s, String.valueOf(o)));
153
154        if (StringUtil.noText(method) || "GET".equalsIgnoreCase(method)) {
155            reqBuilder.method("GET", null);
156        } else {
157            reqBuilder.method(method.toUpperCase(), getRequestBody(chain, argsMap));
158        }
159
160
161        OkHttpClient okHttpClient = OkHttpClientUtil.buildDefaultClient();
162        try (Response response = okHttpClient.newCall(reqBuilder.build()).execute()) {
163
164            Map<String, Object> result = new HashMap<>();
165            result.put("statusCode", response.code());
166
167            Map<String, String> responseHeaders = new HashMap<>();
168            for (String name : response.headers().names()) {
169                responseHeaders.put(name, response.header(name));
170            }
171            result.put("headers", responseHeaders);
172
173            ResponseBody body = response.body();
174            String bodyString = null;
175            if (body != null) bodyString = body.string();
176
177            if (StringUtil.hasText(bodyString)) {
178                DataType outDataType = null;
179                List<Parameter> outputDefs = getOutputDefs();
180                if (outputDefs != null) {
181                    for (Parameter outputDef : outputDefs) {
182                        if ("body".equalsIgnoreCase(outputDef.getName())) {
183                            outDataType = outputDef.getDataType();
184                            break;
185                        }
186                    }
187                }
188
189                if (outDataType != null && (outDataType == DataType.Object || outDataType
190                        .getValue().startsWith("Array"))) {
191                    result.put("body", JSON.parse(bodyString));
192                } else {
193                    result.put("body", bodyString);
194                }
195            }
196            return result;
197        } catch (IOException e) {
198            throw new RuntimeException(e);
199        }
200    }
201
202    private RequestBody getRequestBody(Chain chain, Map<String, Object> formatArgs) {
203        if ("json".equals(bodyType)) {
204            String bodyJsonString = TextPromptTemplate.of(bodyJson).formatToString(formatArgs);
205            JSONObject jsonObject = JSON.parseObject(bodyJsonString);
206            return RequestBody.create(jsonObject.toString(), MediaType.parse("application/json"));
207        }
208
209        if ("x-www-form-urlencoded".equals(bodyType)) {
210            Map<String, Object> formUrlencodedMap = chain.getParameterValues(this, formUrlencoded);
211            String bodyString = mapToQueryString(formUrlencodedMap);
212            return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded"));
213        }
214
215        if ("form-data".equals(bodyType)) {
216            Map<String, Object> formDataMap = chain.getParameterValues(this, formData, formatArgs);
217
218            MultipartBody.Builder builder = new MultipartBody.Builder()
219                    .setType(MultipartBody.FORM);
220
221            formDataMap.forEach((s, o) -> {
222//                if (o instanceof File) {
223//                    File f = (File) o;
224//                    RequestBody body = RequestBody.create(f, MediaType.parse("application/octet-stream"));
225//                    builder.addFormDataPart(s, f.getName(), body);
226//                } else if (o instanceof InputStream) {
227//                    RequestBody body = new HttpClient.InputStreamRequestBody(MediaType.parse("application/octet-stream"), (InputStream) o);
228//                    builder.addFormDataPart(s, s, body);
229//                } else if (o instanceof byte[]) {
230//                    builder.addFormDataPart(s, s, RequestBody.create((byte[]) o));
231//                } else {
232//                    builder.addFormDataPart(s, String.valueOf(o));
233//                }
234                builder.addFormDataPart(s, String.valueOf(o));
235            });
236
237            return builder.build();
238        }
239
240        if ("raw".equals(bodyType)) {
241            String rawBodyString = TextPromptTemplate.of(rawBody).formatToString(formatArgs);
242            return RequestBody.create(rawBodyString, null);
243        }
244        //none
245        return RequestBody.create("", null);
246    }
247
248    @Override
249    public String toString() {
250        return "HttpNode{" +
251                "url='" + url + '\'' +
252                ", method='" + method + '\'' +
253                ", headers=" + headers +
254                ", bodyType='" + bodyType + '\'' +
255                ", fromData=" + formData +
256                ", fromUrlencoded=" + formUrlencoded +
257                ", bodyJson='" + bodyJson + '\'' +
258                ", rawBody='" + rawBody + '\'' +
259                ", parameters=" + parameters +
260                ", outputDefs=" + outputDefs +
261                ", id='" + id + '\'' +
262                ", name='" + name + '\'' +
263                ", description='" + description + '\'' +
264                ", async=" + async +
265                ", inwardEdges=" + inwardEdges +
266                ", outwardEdges=" + outwardEdges +
267                ", condition=" + condition +
268                ", memory=" + memory +
269                ", nodeStatus=" + nodeStatus +
270                '}';
271    }
272}