mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(tools): preserve datasource id tool inputs
This commit is contained in:
parent
7bf18e6af8
commit
7a34f3a502
@ -61,7 +61,7 @@ public class DatasourceTool {
|
|||||||
""")
|
""")
|
||||||
public String query_datasource(
|
public String query_datasource(
|
||||||
@ToolParam(description = "动作:list_datasources / list_tables / describe_table") String action,
|
@ToolParam(description = "动作:list_datasources / list_tables / describe_table") String action,
|
||||||
@ToolParam(description = "数据源 ID(list_tables 和 describe_table 时必填)", required = false) Long datasourceId,
|
@ToolParam(description = "数据源 ID(list_tables 和 describe_table 时必填)。必须作为字符串传入,避免大整数精度丢失", required = false) String datasourceId,
|
||||||
@ToolParam(description = "表名(describe_table 时必填)", required = false) String tableName) {
|
@ToolParam(description = "表名(describe_table 时必填)", required = false) String tableName) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -208,4 +208,24 @@ public class DatasourceTool {
|
|||||||
private String error(String message) {
|
private String error(String message) {
|
||||||
return JSONUtil.toJsonStr(new JSONObject().set("error", message));
|
return JSONUtil.toJsonStr(new JSONObject().set("error", message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Long parseDatasourceId(String datasourceId, String action) {
|
||||||
|
String trimmed = datasourceId != null ? datasourceId.trim() : "";
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(action + " 需要 datasourceId 参数");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(trimmed);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("datasourceId 必须是数字字符串");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String listTables(String datasourceId) throws SQLException {
|
||||||
|
return listTables(parseDatasourceId(datasourceId, "list_tables"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String describeTable(String datasourceId, String tableName) throws SQLException {
|
||||||
|
return describeTable(parseDatasourceId(datasourceId, "describe_table"), tableName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,16 +47,17 @@ public class SqlQueryTool {
|
|||||||
如果数据适合可视化,会自动附带一个 echarts 图表配置代码块,前端会自动渲染为交互式图表。
|
如果数据适合可视化,会自动附带一个 echarts 图表配置代码块,前端会自动渲染为交互式图表。
|
||||||
""")
|
""")
|
||||||
public String execute_sql(
|
public String execute_sql(
|
||||||
@ToolParam(description = "目标数据源 ID") Long datasourceId,
|
@ToolParam(description = "目标数据源 ID。必须作为字符串传入,避免大整数精度丢失") String datasourceId,
|
||||||
@ToolParam(description = "要执行的 SQL 查询(仅允许 SELECT)") String sql) {
|
@ToolParam(description = "要执行的 SQL 查询(仅允许 SELECT)") String sql) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Long parsedDatasourceId = parseDatasourceId(datasourceId);
|
||||||
// 1. 验证并规范化 SQL
|
// 1. 验证并规范化 SQL
|
||||||
String safeSql = sqlValidationService.validateAndNormalize(sql);
|
String safeSql = sqlValidationService.validateAndNormalize(sql);
|
||||||
log.info("执行 SQL 查询 [数据源 {}]: {}", datasourceId, safeSql);
|
log.info("执行 SQL 查询 [数据源 {}]: {}", parsedDatasourceId, safeSql);
|
||||||
|
|
||||||
// 2. 获取数据源连接
|
// 2. 获取数据源连接
|
||||||
DatasourceEntity entity = datasourceService.getDecrypted(datasourceId);
|
DatasourceEntity entity = datasourceService.getDecrypted(parsedDatasourceId);
|
||||||
|
|
||||||
// 3. 执行查询
|
// 3. 执行查询
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
@ -79,6 +80,18 @@ public class SqlQueryTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Long parseDatasourceId(String datasourceId) {
|
||||||
|
String trimmed = datasourceId != null ? datasourceId.trim() : "";
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("datasourceId is required");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(trimmed);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("datasourceId must be a numeric string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String formatResult(ResultSet rs, String sql, long elapsedMs) throws SQLException {
|
private String formatResult(ResultSet rs, String sql, long elapsedMs) throws SQLException {
|
||||||
ResultSetMetaData meta = rs.getMetaData();
|
ResultSetMetaData meta = rs.getMetaData();
|
||||||
int colCount = meta.getColumnCount();
|
int colCount = meta.getColumnCount();
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
package vip.mate.tool.builtin;
|
package vip.mate.tool.builtin;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.ai.support.ToolCallbacks;
|
||||||
|
import org.springframework.ai.tool.ToolCallback;
|
||||||
import vip.mate.datasource.model.DatasourceEntity;
|
import vip.mate.datasource.model.DatasourceEntity;
|
||||||
import vip.mate.datasource.service.DatasourceConnectionManager;
|
import vip.mate.datasource.service.DatasourceConnectionManager;
|
||||||
import vip.mate.datasource.service.DatasourceService;
|
import vip.mate.datasource.service.DatasourceService;
|
||||||
|
import vip.mate.datasource.service.SqlValidationService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -56,4 +60,36 @@ class DatasourceToolIdPrecisionTest {
|
|||||||
assertFalse(out.contains(": " + bigId) || out.contains(":" + bigId),
|
assertFalse(out.contains(": " + bigId) || out.contains(":" + bigId),
|
||||||
"id must NOT appear as a bare JSON number (precision-lossy across double/JS Number)");
|
"id must NOT appear as a bare JSON number (precision-lossy across double/JS Number)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("datasource tools publish datasourceId as a string parameter so LLM tool calls preserve precision")
|
||||||
|
void datasourceIdSchemasAreString() throws Exception {
|
||||||
|
DatasourceTool datasourceTool = new DatasourceTool(
|
||||||
|
mock(DatasourceService.class),
|
||||||
|
mock(DatasourceConnectionManager.class),
|
||||||
|
idSafeMapper());
|
||||||
|
SqlQueryTool sqlQueryTool = new SqlQueryTool(
|
||||||
|
mock(DatasourceService.class),
|
||||||
|
mock(DatasourceConnectionManager.class),
|
||||||
|
mock(SqlValidationService.class));
|
||||||
|
|
||||||
|
assertDatasourceIdIsString(datasourceTool, "query_datasource");
|
||||||
|
assertDatasourceIdIsString(sqlQueryTool, "execute_sql");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertDatasourceIdIsString(Object tool, String name) throws Exception {
|
||||||
|
JsonNode root = idSafeMapper().readTree(callback(tool, name).getToolDefinition().inputSchema());
|
||||||
|
|
||||||
|
assertTrue("string".equals(root.at("/properties/datasourceId/type").asText()),
|
||||||
|
name + " datasourceId must be a string schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ToolCallback callback(Object tool, String name) {
|
||||||
|
for (ToolCallback callback : ToolCallbacks.from(tool)) {
|
||||||
|
if (name.equals(callback.getToolDefinition().name())) {
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Missing tool callback: " + name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class DatasourceToolPostgresqlViewDiscoveryTest {
|
|||||||
DatasourceTool tool = new DatasourceTool(service, connectionManager, JsonMapper.builder().build());
|
DatasourceTool tool = new DatasourceTool(service, connectionManager, JsonMapper.builder().build());
|
||||||
|
|
||||||
// When the agent asks MateClaw to discover available relations.
|
// When the agent asks MateClaw to discover available relations.
|
||||||
String output = tool.query_datasource("list_tables", 1L, null);
|
String output = tool.query_datasource("list_tables", "1", null);
|
||||||
|
|
||||||
// Then the metadata query must include views and return the discovered view.
|
// Then the metadata query must include views and return the discovered view.
|
||||||
ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
|
ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user