|
|
@@ -0,0 +1,157 @@
|
|
|
+package com.zsjz.ai.module.agent.tools;
|
|
|
+
|
|
|
+import io.agentscope.core.tool.Tool;
|
|
|
+import io.agentscope.core.tool.ToolParam;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.lang.reflect.Modifier;
|
|
|
+import java.lang.reflect.Parameter;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 工具参数 schema 的契约测试。
|
|
|
+ *
|
|
|
+ * <p>大模型只按 JSON Schema 的 {@code required} 和参数 description 生成调用参数:
|
|
|
+ * <ul>
|
|
|
+ * <li>description 里写了「必填 / 必传」的字段,必须同步 {@code required = true},
|
|
|
+ * 否则模型会把它当可选字段漏传;</li>
|
|
|
+ * <li>反过来,{@code required = true} 的字段必须在 description 里明确写「必填 / 必传」,
|
|
|
+ * 让模型知道为什么必须给;</li>
|
|
|
+ * <li>条件必填(如「lx 为 2/3 时必传」)和「建议必传」不算硬必填,由工具内校验兜底。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p>这个测试覆盖所有 {@code *ToolSpecs} 的嵌套参数类,新增字段时会被自动纳入检查。
|
|
|
+ */
|
|
|
+class ToolSpecRequiredFieldContractTest {
|
|
|
+
|
|
|
+ private static final List<Class<?>> SPEC_GROUPS = List.of(
|
|
|
+ CallToolSpecs.class,
|
|
|
+ FileToolSpecs.class,
|
|
|
+ GraphToolSpecs.class,
|
|
|
+ PersonToolSpecs.class,
|
|
|
+ TrackToolSpecs.class,
|
|
|
+ TransToolSpecs.class);
|
|
|
+
|
|
|
+ private static final List<Class<?>> TOOL_CLASSES = List.of(
|
|
|
+ CallAnalysisTool.class,
|
|
|
+ FileAnalysisTool.class,
|
|
|
+ GovernTableTool.class,
|
|
|
+ GraphAnalysisTool.class,
|
|
|
+ PersonAnalysisTool.class,
|
|
|
+ PythonAnalysisTool.class,
|
|
|
+ RagSchemaSearchTool.class,
|
|
|
+ SqlAnalysisTool.class,
|
|
|
+ TrackAnalysisTool.class,
|
|
|
+ TransAnalysisTool.class,
|
|
|
+ WorkspaceInfoTool.class);
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("参数 description 写「必填/必传」时,required 必须为 true(条件必填/建议必传除外)")
|
|
|
+ void requiredFlagMatchesDescription() {
|
|
|
+ List<String> problems = new ArrayList<>();
|
|
|
+ forEachToolParam((owner, field, param) -> {
|
|
|
+ String desc = param.description();
|
|
|
+ boolean hardRequired = desc.contains("必填") || desc.contains("必传");
|
|
|
+ if (!hardRequired || param.required()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ boolean conditional = desc.contains("时必") || desc.contains("建议必")
|
|
|
+ || desc.contains("至少") || desc.contains("二选一");
|
|
|
+ if (!conditional) {
|
|
|
+ problems.add(owner + "." + field.getName() + " 描述写了必填但 required=false");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ assertTrue(problems.isEmpty(), String.join("\n", problems));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("required=true 的参数必须在 description 里明确写「必填/必传」")
|
|
|
+ void requiredParameterDescriptionIsExplicit() {
|
|
|
+ List<String> problems = new ArrayList<>();
|
|
|
+ forEachToolParam((owner, field, param) -> {
|
|
|
+ if (!param.required()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String desc = param.description();
|
|
|
+ if (!desc.contains("必填") && !desc.contains("必传")) {
|
|
|
+ problems.add(owner + "." + field.getName() + " required=true 但描述未写明必填");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ assertTrue(problems.isEmpty(), String.join("\n", problems));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("每个 ToolParam 都必须有非空 description")
|
|
|
+ void everyParamHasDescription() {
|
|
|
+ List<String> problems = new ArrayList<>();
|
|
|
+ forEachToolParam((owner, field, param) -> {
|
|
|
+ if (param.description() == null || param.description().isBlank()) {
|
|
|
+ problems.add(owner + "." + field.getName() + " 缺少 description");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ assertTrue(problems.isEmpty(), String.join("\n", problems));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("@Tool 方法参数:必须有 description,描述写必填时 required 不能是 false")
|
|
|
+ void toolMethodParamsHaveDescriptionAndRequiredConsistency() {
|
|
|
+ List<String> problems = new ArrayList<>();
|
|
|
+ for (Class<?> toolClass : TOOL_CLASSES) {
|
|
|
+ for (Method method : toolClass.getDeclaredMethods()) {
|
|
|
+ Tool tool = method.getAnnotation(Tool.class);
|
|
|
+ if (tool == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (tool.name().isBlank() || tool.description().isBlank()) {
|
|
|
+ problems.add(toolClass.getSimpleName() + "." + method.getName() + " 的 @Tool name/description 为空");
|
|
|
+ }
|
|
|
+ for (Parameter parameter : method.getParameters()) {
|
|
|
+ ToolParam param = parameter.getAnnotation(ToolParam.class);
|
|
|
+ if (param == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String desc = param.description();
|
|
|
+ String where = toolClass.getSimpleName() + "." + method.getName() + "." + param.name();
|
|
|
+ if (desc == null || desc.isBlank()) {
|
|
|
+ problems.add(where + " 缺少 description");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ boolean hardRequired = desc.contains("必填") || desc.contains("必传");
|
|
|
+ boolean conditional = desc.contains("时必") || desc.contains("建议必")
|
|
|
+ || desc.contains("至少") || desc.contains("二选一");
|
|
|
+ if (hardRequired && !conditional && !param.required()) {
|
|
|
+ problems.add(where + " 描述写了必填但 required=false");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(problems.isEmpty(), String.join("\n", problems));
|
|
|
+ }
|
|
|
+
|
|
|
+ private interface ParamVisitor {
|
|
|
+ void visit(String owner, Field field, ToolParam param);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void forEachToolParam(ParamVisitor visitor) {
|
|
|
+ for (Class<?> group : SPEC_GROUPS) {
|
|
|
+ for (Class<?> spec : group.getDeclaredClasses()) {
|
|
|
+ if (!Modifier.isPublic(spec.getModifiers())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (Field field : spec.getDeclaredFields()) {
|
|
|
+ ToolParam param = field.getAnnotation(ToolParam.class);
|
|
|
+ if (param == null || Modifier.isStatic(field.getModifiers())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ visitor.visit(spec.getSimpleName(), field, param);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|