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}