001/*
002 *  Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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 com.agentsflex.core.model.chat.tool;
017
018import com.agentsflex.core.model.chat.tool.annotation.ToolDef;
019import com.agentsflex.core.model.chat.tool.annotation.ToolParam;
020import com.agentsflex.core.util.JsonSchemaTypeMapper;
021import com.agentsflex.core.util.TypeConverter;
022import com.alibaba.fastjson2.JSONArray;
023import com.alibaba.fastjson2.JSONObject;
024import org.jetbrains.annotations.NotNull;
025
026import java.lang.reflect.Field;
027import java.lang.reflect.InvocationTargetException;
028import java.lang.reflect.Method;
029import java.lang.reflect.Modifier;
030import java.lang.reflect.Parameter;
031import java.lang.reflect.Type;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.HashSet;
036import java.util.LinkedHashMap;
037import java.util.List;
038import java.util.Map;
039import java.util.Set;
040
041/**
042 * 基于反射的方法工具实现
043 *
044 * @author fuhai
045 * @since 2023/10/01
046 */
047public class MethodTool extends BaseTool {
048
049    private Class<?> clazz;
050    private Object object;
051    private Method method;
052
053    /**
054     * 线程本地缓存,防止循环引用导致栈溢出
055     */
056    private static final ThreadLocal<Set<Class<?>>> RESOLVING_PROPERTIES =
057        ThreadLocal.withInitial(HashSet::new);
058
059    public Class<?> getClazz() {
060        return clazz;
061    }
062
063    public void setClazz(Class<?> clazz) {
064        this.clazz = clazz;
065    }
066
067    public Object getObject() {
068        return object;
069    }
070
071    public void setObject(Object object) {
072        this.object = object;
073    }
074
075    public Method getMethod() {
076        return method;
077    }
078
079    public void setMethod(Method method) {
080        this.method = method;
081
082        ToolDef toolDef = method.getAnnotation(ToolDef.class);
083        this.name = toolDef.name();
084        this.description = toolDef.description();
085
086        List<MethodParameter> parameterList = new ArrayList<>();
087        Parameter[] methodParameters = method.getParameters();
088        Type[] genericParameterTypes = method.getGenericParameterTypes();
089        int index = 0;
090        for (Parameter methodParameter : methodParameters) {
091            MethodParameter parameter = getParameter(methodParameter, genericParameterTypes[index++]);
092            parameterList.add(parameter);
093        }
094        this.parameters = parameterList.toArray(new MethodParameter[]{});
095    }
096
097    @NotNull
098    private static MethodParameter getParameter(Parameter methodParameter, Type genericParameterType) {
099        ToolParam toolParam = methodParameter.getAnnotation(ToolParam.class);
100        MethodParameter parameter = new MethodParameter();
101        parameter.setName(toolParam.name());
102        parameter.setDescription(toolParam.description());
103
104        Class<?> paramType = methodParameter.getType();
105        String schemaType = JsonSchemaTypeMapper.mapToSchemaType(paramType);
106
107        parameter.setType(schemaType);
108        parameter.setTypeClass(genericParameterType);
109        parameter.setRequired(toolParam.required());
110
111        // 处理数组/集合类型的 items
112        if ("array".equals(schemaType)) {
113            String arrayItemType = JsonSchemaTypeMapper.resolveArrayItemType(genericParameterType);
114            MethodParameter itemParam = new MethodParameter();
115            itemParam.setType(arrayItemType);
116            itemParam.setDescription("Array items");
117            parameter.addChild(itemParam);
118        }
119
120        // 处理枚举
121        String[] enums = toolParam.enums();
122        if (enums != null && enums.length > 0) {
123            parameter.setEnums(enums);
124        } else if (genericParameterType instanceof Class && ((Class<?>) genericParameterType).isEnum()) {
125            Object[] enumConstants = ((Class<?>) genericParameterType).getEnumConstants();
126            String[] enumNames = new String[enumConstants.length];
127            for (int i = 0; i < enumConstants.length; i++) {
128                enumNames[i] = ((Enum<?>) enumConstants[i]).name();
129            }
130            parameter.setEnums(enumNames);
131        }
132
133        // 如果类型是 object,解析其带注解的属性
134        if ("object".equals(schemaType)) {
135            Map<String, Object> properties = resolveAnnotatedProperties(paramType);
136            if (!properties.isEmpty()) {
137                parameter.setProperties(properties);
138            }
139        }
140
141        return parameter;
142    }
143
144    /**
145     * 递归解析实体类中带 @ToolParam 注解的字段
146     *
147     * @param clazz 要解析的类
148     * @return 字段名 → schema map 的映射
149     */
150    @NotNull
151    private static Map<String, Object> resolveAnnotatedProperties(@NotNull Class<?> clazz) {
152
153        // 防止循环引用:如果当前类正在解析中,返回空避免死循环
154        if (RESOLVING_PROPERTIES.get().contains(clazz)) {
155            return Collections.emptyMap();
156        }
157        RESOLVING_PROPERTIES.get().add(clazz);
158
159        try {
160            Map<String, Object> properties = new LinkedHashMap<>();
161
162            // 遍历所有字段(包括父类)
163            for (Field field : getAllFields(clazz)) {
164                // 跳过 static/transient/合成字段
165                int modifiers = field.getModifiers();
166                if (Modifier.isStatic(modifiers) ||
167                    Modifier.isTransient(modifiers) ||
168                    field.isSynthetic()) {
169                    continue;
170                }
171
172                // 只解析有 @ToolParam 注解的字段
173                if (!field.isAnnotationPresent(ToolParam.class)) {
174                    continue;
175                }
176
177                ToolParam param = field.getAnnotation(ToolParam.class);
178                String fieldName = param.name();  // 使用注解指定的名称
179                Class<?> fieldType = field.getType();
180                Type genericType = field.getGenericType();
181
182                // 构建字段的 schema
183                Map<String, Object> fieldSchema = new LinkedHashMap<>();
184                fieldSchema.put("type", JsonSchemaTypeMapper.mapToSchemaType(fieldType));
185
186                // 描述
187                if (!param.description().isEmpty()) {
188                    fieldSchema.put("description", param.description());
189                }
190
191                // 枚举:优先使用注解配置,其次自动提取枚举类值
192                if (param.enums().length > 0) {
193                    fieldSchema.put("enum", Arrays.asList(param.enums()));
194                } else if (fieldType.isEnum()) {
195                    Object[] constants = fieldType.getEnumConstants();
196                    String[] names = new String[constants.length];
197                    for (int i = 0; i < constants.length; i++) {
198                        names[i] = ((Enum<?>) constants[i]).name();
199                    }
200                    fieldSchema.put("enum", Arrays.asList(names));
201                }
202
203                // 递归:如果字段类型是 object 且有 @ToolParam,继续解析其属性
204                if ("object".equals(fieldSchema.get("type"))) {
205                    Map<String, Object> nestedProps = resolveAnnotatedProperties(fieldType);
206                    if (!nestedProps.isEmpty()) {
207                        fieldSchema.put("properties", nestedProps);
208                    }
209                }
210
211                // 数组元素类型
212                if ("array".equals(fieldSchema.get("type"))) {
213                    String itemType = JsonSchemaTypeMapper.resolveArrayItemType(genericType);
214                    Map<String, Object> items = new LinkedHashMap<>();
215                    items.put("type", itemType);
216                    fieldSchema.put("items", items);
217                }
218
219                properties.put(fieldName, fieldSchema);
220            }
221
222            return properties;
223
224        } finally {
225            // 解析完成后移除,避免影响其他解析
226            RESOLVING_PROPERTIES.get().remove(clazz);
227        }
228    }
229
230    /**
231     * 获取类及其父类的所有字段
232     */
233    @NotNull
234    private static List<Field> getAllFields(@NotNull Class<?> clazz) {
235        List<Field> fields = new ArrayList<>();
236        Class<?> current = clazz;
237        while (current != null && current != Object.class) {
238            Collections.addAll(fields, current.getDeclaredFields());
239            current = current.getSuperclass();
240        }
241        return fields;
242    }
243
244    public Object invoke(Map<String, Object> argsMap) {
245        try {
246            Object[] args = new Object[this.parameters.length];
247            for (int i = 0; i < this.parameters.length; i++) {
248                MethodParameter parameter = (MethodParameter) this.parameters[i];
249                Object value = argsMap.get(parameter.getName());
250                if (value instanceof JSONArray) {
251                    args[i] = ((JSONArray) value).to(parameter.getTypeClass());
252                } else if (value instanceof JSONObject) {
253                    args[i] = ((JSONObject) value).to(parameter.getTypeClass());
254                } else {
255                    args[i] = TypeConverter.convert(value, parameter.getTypeClass());
256                }
257            }
258            return method.invoke(object, args);
259        } catch (IllegalAccessException | InvocationTargetException e) {
260            throw new RuntimeException(e);
261        }
262    }
263}