diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetDomainsTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetDomainsTool.java new file mode 100644 index 0000000..c53fcf0 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetDomainsTool.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.agent.tools; + +import io.agentscope.core.tool.Tool; +import io.github.malonetalk.service.DomainService; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@AllArgsConstructor +public class GetDomainsTool implements MarkAgentTool { + + private final DomainService domainService; + + @Tool( + name = "get_domains", + description = + "Get available data domains in the datasource. Call this tool first to discover" + + " what domains are available before querying tables.") + public List getDomains() { + return domainService.listDomainNames(); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java index c98eb91..3ee500a 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java @@ -18,6 +18,7 @@ package io.github.malonetalk.agent.tools; import io.agentscope.core.tool.Tool; +import io.agentscope.core.tool.ToolParam; import io.github.malonetalk.entity.Datasource; import io.github.malonetalk.entity.TableInfo; import io.github.malonetalk.enums.Status; @@ -37,8 +38,22 @@ public class GetTablesTool implements MarkAgentTool { private final DatasourceService dataSourceService; private final TableSemanticService tableSemanticService; - @Tool(name = "get_tables", description = "获取数据库中的表信息,包括表名和表描述") - public List getTables() { + @Tool( + name = "get_tables", + description = + "Get table information from the database, including table name and description." + + " Optionally filter by domains; returns all tables if domains is not" + + " provided. It is recommended to call get_domains first to discover" + + " available domains.") + public List getTables( + @ToolParam( + name = "domains", + description = + "Optional list of domain names. Only tables belonging to these" + + " domains will be returned. If not provided or empty," + + " returns all tables.", + required = false) + List domains) { List activeDataSources = dataSourceService.findByStatus(Status.ACTIVE.getCode()); @@ -54,6 +69,6 @@ public List getTables() { } Datasource dataSource = activeDataSources.get(0); - return tableSemanticService.listTableInfosByDatasourceId(dataSource.getId()); + return tableSemanticService.listTableInfosByDomains(dataSource.getId(), domains); } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/common/SemanticConstants.java b/data-agent-backend/src/main/java/io/github/malonetalk/common/SemanticConstants.java index 66a72ba..4f08ded 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/common/SemanticConstants.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/common/SemanticConstants.java @@ -22,6 +22,7 @@ public final class SemanticConstants { public static final String RELATION_KEY_SEPARATOR = "|"; public static final String SORT_ORDER_ASC = "asc"; public static final String SORT_ORDER_DESC = "desc"; + public static final String DEFAULT_DOMAIN = "default"; private SemanticConstants() {} } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/config/DataSourceConfig.java b/data-agent-backend/src/main/java/io/github/malonetalk/config/DataSourceConfig.java index e9e8c23..0a8c7ba 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/config/DataSourceConfig.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/config/DataSourceConfig.java @@ -17,6 +17,7 @@ */ package io.github.malonetalk.config; +import com.fasterxml.jackson.databind.ObjectMapper; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; @@ -66,4 +67,9 @@ public DataSource dataSource() { return new HikariDataSource(config); } + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/DomainController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DomainController.java new file mode 100644 index 0000000..3f905c1 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DomainController.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.controller; + +import io.github.malonetalk.common.Result; +import io.github.malonetalk.dto.DomainCreateRequest; +import io.github.malonetalk.dto.DomainPageQuery; +import io.github.malonetalk.dto.DomainUpdateRequest; +import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.entity.DomainInfo; +import io.github.malonetalk.service.DomainService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/domains") +@RequiredArgsConstructor +public class DomainController { + + private final DomainService domainService; + + @GetMapping + public Result> findDomains(@Valid DomainPageQuery query) { + return Result.success(domainService.getDomainPage(query)); + } + + @GetMapping("/{id}") + public Result findById(@PathVariable Integer id) { + if (id == null || id < 0) { + return Result.error(400, "ID必须为非负数"); + } + DomainInfo domain = domainService.findById(id); + if (domain == null) { + return Result.error(404, "领域不存在"); + } + return Result.success(domain); + } + + @PostMapping + public Result create(@Valid @RequestBody DomainCreateRequest request) { + return Result.success(domainService.create(request)); + } + + @PutMapping("/{id}") + public Result update( + @PathVariable Integer id, @Valid @RequestBody DomainUpdateRequest request) { + if (id == null || id < 0) { + return Result.error(400, "ID必须为非负数"); + } + return Result.success(domainService.update(id, request)); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + if (id == null || id < 0) { + return Result.error(400, "ID必须为非负数"); + } + domainService.delete(id); + return Result.success(true); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainCreateRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainCreateRequest.java new file mode 100644 index 0000000..7f77fb6 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainCreateRequest.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.NotBlank; + +public record DomainCreateRequest( + @NotBlank(message = "领域名称不能为空") String name, String description) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainPageQuery.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainPageQuery.java new file mode 100644 index 0000000..d35bebb --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainPageQuery.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; + +public record DomainPageQuery( + @Min(1) Integer page, + @Min(1) Integer pageSize, + String keyword, + @Pattern(regexp = "^(?i)(asc|desc)$", message = "sortOrder must be asc or desc.") + String sortOrder) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainUpdateRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainUpdateRequest.java new file mode 100644 index 0000000..eb6f472 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/DomainUpdateRequest.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.NotBlank; + +public record DomainUpdateRequest( + @NotBlank(message = "领域名称不能为空") String name, String description) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/DomainInfo.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/DomainInfo.java new file mode 100644 index 0000000..fad3e82 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/DomainInfo.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.entity; + +import java.time.LocalDateTime; +import lombok.Data; + +@Data +public class DomainInfo { + + private Integer id; + private String name; + private String description; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/DomainInfoMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/DomainInfoMapper.java new file mode 100644 index 0000000..d727b62 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/DomainInfoMapper.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.mapper; + +import io.github.malonetalk.dto.DomainPageQuery; +import io.github.malonetalk.entity.DomainInfo; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface DomainInfoMapper { + + int insert(DomainInfo domainInfo); + + int update(DomainInfo domainInfo); + + int deleteByIds(@Param("ids") List ids); + + List selectAll(); + + List selectPage( + @Param("query") DomainPageQuery query, @Param("sortDescending") boolean sortDescending); + + DomainInfo selectById(@Param("id") Integer id); + + DomainInfo selectByName(@Param("name") String name); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableInfoMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableInfoMapper.java index 9c41458..92bfee3 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableInfoMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/TableInfoMapper.java @@ -41,4 +41,9 @@ List selectPageByDatasourceId( TableInfo selectByDatasourceIdAndTableName( @Param("datasourceId") Integer datasourceId, @Param("tableName") String tableName); + + List selectByDatasourceIdAndDomains( + @Param("datasourceId") Integer datasourceId, @Param("domains") List domains); + + int countByDomain(@Param("domain") String domain); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DomainService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DomainService.java new file mode 100644 index 0000000..cfb6117 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DomainService.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.dto.DomainCreateRequest; +import io.github.malonetalk.dto.DomainPageQuery; +import io.github.malonetalk.dto.DomainUpdateRequest; +import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.entity.DomainInfo; +import java.util.List; + +public interface DomainService { + + PageResponse getDomainPage(DomainPageQuery query); + + DomainInfo findById(Integer id); + + DomainInfo create(DomainCreateRequest request); + + DomainInfo update(Integer id, DomainUpdateRequest request); + + void delete(Integer id); + + List listDomainNames(); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DomainServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DomainServiceImpl.java new file mode 100644 index 0000000..3f68bc1 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DomainServiceImpl.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import io.github.malonetalk.common.SemanticConstants; +import io.github.malonetalk.dto.DomainCreateRequest; +import io.github.malonetalk.dto.DomainPageQuery; +import io.github.malonetalk.dto.DomainUpdateRequest; +import io.github.malonetalk.dto.pagination.PageResponse; +import io.github.malonetalk.entity.DomainInfo; +import io.github.malonetalk.mapper.DomainInfoMapper; +import io.github.malonetalk.mapper.TableInfoMapper; +import io.github.malonetalk.utils.SemanticUtils; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DomainServiceImpl implements DomainService { + + private final DomainInfoMapper domainInfoMapper; + private final TableInfoMapper tableInfoMapper; + + @Override + public PageResponse getDomainPage(DomainPageQuery query) { + int pageNumber = PageResponse.resolvePage(query.page()); + int pageSize = PageResponse.resolvePageSize(query.pageSize()); + SemanticUtils.validateSortOrder(query.sortOrder()); + boolean sortDescending = + SemanticConstants.SORT_ORDER_DESC.equalsIgnoreCase(query.sortOrder()); + PageHelper.startPage(pageNumber, pageSize); + @SuppressWarnings("unchecked") + Page page = + (Page) + domainInfoMapper.selectPage( + new DomainPageQuery( + pageNumber, + pageSize, + SemanticUtils.normalizeBlankToNull(query.keyword()), + query.sortOrder()), + sortDescending); + List items = page.getResult(); + long total = page.getTotal(); + return PageResponse.of(items, total, pageNumber, pageSize); + } + + @Override + public DomainInfo findById(Integer id) { + if (id == null) { + throw new IllegalArgumentException("id 不能为空"); + } + return domainInfoMapper.selectById(id); + } + + @Override + public DomainInfo create(DomainCreateRequest request) { + String normalizedName = SemanticUtils.requireName(request.name(), "领域名称"); + DomainInfo existing = domainInfoMapper.selectByName(normalizedName); + if (existing != null) { + throw new IllegalArgumentException("领域名称已存在: " + normalizedName); + } + DomainInfo domainInfo = new DomainInfo(); + domainInfo.setName(normalizedName); + domainInfo.setDescription(SemanticUtils.normalizeBlankToNull(request.description())); + domainInfo.setCreateTime(LocalDateTime.now()); + domainInfo.setUpdateTime(LocalDateTime.now()); + domainInfoMapper.insert(domainInfo); + return domainInfo; + } + + @Override + public DomainInfo update(Integer id, DomainUpdateRequest request) { + if (id == null) { + throw new IllegalArgumentException("id 不能为空"); + } + DomainInfo existing = findById(id); + if (existing == null) { + throw new IllegalArgumentException("领域不存在: id=" + id); + } + String normalizedName = SemanticUtils.requireName(request.name(), "领域名称"); + DomainInfo nameConflict = domainInfoMapper.selectByName(normalizedName); + if (nameConflict != null && !nameConflict.getId().equals(id)) { + throw new IllegalArgumentException("领域名称已存在: " + normalizedName); + } + existing.setName(normalizedName); + existing.setDescription(SemanticUtils.normalizeBlankToNull(request.description())); + existing.setUpdateTime(LocalDateTime.now()); + domainInfoMapper.update(existing); + return existing; + } + + @Override + public void delete(Integer id) { + if (id == null) { + throw new IllegalArgumentException("id 不能为空"); + } + DomainInfo existing = findById(id); + if (existing == null) { + throw new IllegalArgumentException("领域不存在: id=" + id); + } + if (SemanticConstants.DEFAULT_DOMAIN.equalsIgnoreCase(existing.getName())) { + throw new IllegalArgumentException("无法删除的领域" + existing.getName()); + } + int referenceCount = tableInfoMapper.countByDomain(existing.getName()); + if (referenceCount > 0) { + throw new IllegalArgumentException("领域正在被占用" + existing.getName()); + } + domainInfoMapper.deleteByIds(List.of(id)); + } + + @Override + public List listDomainNames() { + return domainInfoMapper.selectAll().stream() + .map(DomainInfo::getName) + .distinct() + .sorted(String::compareToIgnoreCase) + .toList(); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticService.java index 83e8717..0d80e2e 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticService.java @@ -34,6 +34,8 @@ public interface TableSemanticService { void updateTableSemantic(TableSemanticUpdateRequest request); + List listTableInfosByDomains(Integer datasourceId, List domains); + void resetTableSemantic(Integer datasourceId, String tableName); int resetTableSemantics(Integer datasourceId, List tableNames); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticServiceImpl.java index e1223e3..d064889 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/table/TableSemanticServiceImpl.java @@ -94,6 +94,18 @@ public List listTableInfosByDatasourceId(Integer datasourceId) { return tableInfoMapper.selectByDatasourceId(datasourceId); } + @Override + public List listTableInfosByDomains(Integer datasourceId, List domains) { + SemanticUtils.requireDatasourceId(datasourceId); + if (datasourceService.findById(datasourceId) == null) { + return List.of(); + } + if (domains == null || domains.isEmpty()) { + return listTableInfosByDatasourceId(datasourceId); + } + return tableInfoMapper.selectByDatasourceIdAndDomains(datasourceId, domains); + } + @Override public void updateTableSemantic(TableSemanticUpdateRequest request) { requireDatasource(request.datasourceId()); @@ -107,7 +119,7 @@ public void updateTableSemantic(TableSemanticUpdateRequest request) { tableInfo.setTableName(normalizedTableName); tableInfo.setTableDescription( SemanticUtils.normalizeBlankToNull(request.tableDescription())); - tableInfo.setDomain(SemanticUtils.normalizeBlankToNull(request.domain())); + tableInfo.setDomain(normalizeDomain(request.domain())); tableInfo.setIsVisible(request.isVisible()); tableInfo.setCreateTime(LocalDateTime.now()); tableInfo.setUpdateTime(LocalDateTime.now()); @@ -117,7 +129,7 @@ public void updateTableSemantic(TableSemanticUpdateRequest request) { existing.setTableName(normalizedTableName); existing.setTableDescription( SemanticUtils.normalizeBlankToNull(request.tableDescription())); - existing.setDomain(SemanticUtils.normalizeBlankToNull(request.domain())); + existing.setDomain(normalizeDomain(request.domain())); existing.setIsVisible(request.isVisible()); existing.setUpdateTime(LocalDateTime.now()); tableInfoMapper.update(existing); @@ -190,4 +202,9 @@ private TableSemanticResponse mapResponse(TableInfo tableInfo) { private String normalizeName(String value) { return SemanticUtils.requireName(value, "tableName").toLowerCase(Locale.ROOT); } + + private String normalizeDomain(String value) { + String normalized = SemanticUtils.normalizeBlankToNull(value); + return normalized == null ? SemanticConstants.DEFAULT_DOMAIN : normalized; + } } diff --git a/data-agent-backend/src/main/resources/mapper/DomainInfoMapper.xml b/data-agent-backend/src/main/resources/mapper/DomainInfoMapper.xml new file mode 100644 index 0000000..9116ce8 --- /dev/null +++ b/data-agent-backend/src/main/resources/mapper/DomainInfoMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + INSERT INTO domain_info ( + name, description, + create_time, update_time + ) VALUES ( + #{name}, #{description}, + #{createTime}, #{updateTime} + ) + + + + UPDATE domain_info + + name = #{name}, + description = #{description}, + update_time = #{updateTime}, + + WHERE id = #{id} + + + + DELETE FROM domain_info + WHERE id IN + + #{id} + + + + diff --git a/data-agent-backend/src/main/resources/mapper/TableInfoMapper.xml b/data-agent-backend/src/main/resources/mapper/TableInfoMapper.xml index c308e2f..56d5e26 100644 --- a/data-agent-backend/src/main/resources/mapper/TableInfoMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/TableInfoMapper.xml @@ -66,6 +66,21 @@ WHERE id = #{id} + + + + DELETE FROM table_info WHERE datasource_id = #{datasourceId} diff --git a/data-agent-frontend/src/api/domain.ts b/data-agent-frontend/src/api/domain.ts new file mode 100644 index 0000000..b4a37be --- /dev/null +++ b/data-agent-frontend/src/api/domain.ts @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import request from './request'; +import type { ApiResponse } from './request'; + +export interface PageResponse { + page: number; + pageSize: number; + total: number; + totalPages: number; + hasPrevious: boolean; + hasNext: boolean; + items: T[]; +} + +export interface DomainInfo { + id: number; + name: string; + description: string | null; + createTime: string; + updateTime: string; +} + +export interface DomainPageQuery { + page?: number; + pageSize?: number; + keyword?: string; + sortOrder?: 'asc' | 'desc'; +} + +export interface DomainCreateRequest { + name: string; + description?: string; +} + +export interface DomainUpdateRequest { + name: string; + description?: string; +} + +export function getDomainPage(query: DomainPageQuery) { + return request.get>>('/domains', { params: query }); +} + +export function getDomainById(id: number) { + return request.get>(`/domains/${id}`); +} + +export function createDomain(data: DomainCreateRequest) { + return request.post>('/domains', data); +} + +export function updateDomain(id: number, data: DomainUpdateRequest) { + return request.put>(`/domains/${id}`, data); +} + +export function deleteDomain(id: number) { + return request.delete>(`/domains/${id}`); +} diff --git a/data-agent-frontend/src/components/layout/AppSidebar.vue b/data-agent-frontend/src/components/layout/AppSidebar.vue index d936f73..a09a76b 100644 --- a/data-agent-frontend/src/components/layout/AppSidebar.vue +++ b/data-agent-frontend/src/components/layout/AppSidebar.vue @@ -27,6 +27,7 @@ const menuItems = [ { path: '/chat', title: 'AI 智能分析', icon: 'ChatDotRound' }, { path: '/data-source', title: '数据源管理', icon: 'Connection' }, + { path: '/semantic', title: '语义管理', icon: 'Collection' }, ]; const activeMenu = computed(() => route.path); diff --git a/data-agent-frontend/src/router/index.ts b/data-agent-frontend/src/router/index.ts index ad13e31..6892db4 100644 --- a/data-agent-frontend/src/router/index.ts +++ b/data-agent-frontend/src/router/index.ts @@ -41,6 +41,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/data-source/DataSource.vue'), meta: { title: '数据源管理' }, }, + { + path: '/semantic', + name: 'SemanticManage', + component: () => import('@/views/semantic/SemanticManage.vue'), + meta: { title: '语义管理' }, + }, ]; const router = createRouter({ diff --git a/data-agent-frontend/src/views/semantic/SemanticManage.vue b/data-agent-frontend/src/views/semantic/SemanticManage.vue new file mode 100644 index 0000000..e4d0444 --- /dev/null +++ b/data-agent-frontend/src/views/semantic/SemanticManage.vue @@ -0,0 +1,145 @@ + + + + + + + diff --git a/data-agent-frontend/src/views/semantic/components/DomainManage.vue b/data-agent-frontend/src/views/semantic/components/DomainManage.vue new file mode 100644 index 0000000..94a9b10 --- /dev/null +++ b/data-agent-frontend/src/views/semantic/components/DomainManage.vue @@ -0,0 +1,298 @@ + + + + + + + diff --git a/data-agent-frontend/vite.config.ts b/data-agent-frontend/vite.config.ts index 7b949a3..90eab16 100644 --- a/data-agent-frontend/vite.config.ts +++ b/data-agent-frontend/vite.config.ts @@ -18,7 +18,7 @@ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import eslint from 'vite-plugin-eslint'; -import { resolve } from 'path'; +import { fileURLToPath, URL } from 'node:url'; /** * Vite configuration file @@ -32,7 +32,7 @@ export default defineConfig({ // Allows importing like: import xxx from '@/components/xxx' resolve: { alias: { - '@': resolve(__dirname, 'src'), + '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, // Development server configuration diff --git a/skills/data-query/SKILL.md b/skills/data-query/SKILL.md index afcb7f8..139f988 100644 --- a/skills/data-query/SKILL.md +++ b/skills/data-query/SKILL.md @@ -7,14 +7,16 @@ description: A skill for querying database tables and generating SQL queries bas You are a data query assistant. When the user asks a data-related question, follow these steps: -1. Use the `get_tables` tool to get available database tables -2. Use the `get_table_schema` tool to get the schema of relevant tables -3. Generate a SELECT SQL statement based on the table structure -4. Use the `execute_sql` tool to execute the query -5. Summarize the query results for the user +1. Use the `get_domains` tool to get available data domains +2. Based on the user's question, select the relevant domain(s), then use `get_tables(domains=[...])` to get tables in those domains +3. Use the `get_table_schema` tool to get the schema of relevant tables +4. Generate a SELECT SQL statement based on the table structure +5. Use the `execute_sql` tool to execute the query +6. Summarize the query results for the user ## Notes - Only SELECT queries are supported, no modification operations - Always check the table schema before generating SQL to ensure column names and types are correct - If the query result is empty, suggest the user check the query conditions +- When the user's question is ambiguous about which domain to use, query multiple domains to increase coverage diff --git a/sql/data_source.sql b/sql/data_source.sql index 9822f29..d66aae3 100644 --- a/sql/data_source.sql +++ b/sql/data_source.sql @@ -74,3 +74,17 @@ CREATE TABLE IF NOT EXISTS `logical_table_relation` ( (`datasource_id`, `source_table_name`, `target_table_name`, `id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='逻辑表关系表'; +CREATE TABLE IF NOT EXISTS `domain_info` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(255) NOT NULL COMMENT '领域名称', + `description` VARCHAR(500) DEFAULT NULL COMMENT '领域描述', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_domain_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据领域表'; + +INSERT INTO `domain_info` (`name`, `description`, `create_time`, `update_time`) +SELECT 'default', '默认领域', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM `domain_info` WHERE `name` = 'default'); +