diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SchemaReader.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SchemaReader.java index cc92d53..7f30363 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SchemaReader.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/datasource/SchemaReader.java @@ -22,18 +22,19 @@ import io.github.malonetalk.dto.datasource.PhysicalTableInfo; import io.github.malonetalk.entity.Datasource; import io.github.malonetalk.exception.BusinessException; +import io.github.malonetalk.utils.SemanticUtils; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -63,8 +64,10 @@ public List getTableSchema(Datasource datasource, String tab javax.sql.DataSource ds = dynamicDataSourceManager.getOrCreateDataSource(datasource); try (Connection conn = ds.getConnection()) { - Set primaryKeys = getPrimaryKeys(conn, tableName); - return getColumns(conn, tableName, primaryKeys); + List primaryKeys = getPrimaryKeys(conn, tableName); + Map> indexesByColumn = + getIndexesByColumn(conn, tableName, primaryKeys); + return getColumns(conn, tableName, primaryKeys, indexesByColumn); } catch (SQLException e) { log.error("Failed to read schema for table {}: {}", tableName, e.getMessage(), e); throw BusinessException.of( @@ -123,22 +126,66 @@ private List getTables(Connection conn) throws SQLException { return tables; } - private Set getPrimaryKeys(Connection conn, String tableName) throws SQLException { - Set pkColumns = new HashSet<>(); + private List getPrimaryKeys(Connection conn, String tableName) throws SQLException { + Map pkColumns = new TreeMap<>(); DatabaseMetaData metaData = conn.getMetaData(); try (ResultSet rs = metaData.getPrimaryKeys(conn.getCatalog(), conn.getSchema(), tableName)) { while (rs.next()) { - pkColumns.add(rs.getString("COLUMN_NAME")); + String columnName = rs.getString("COLUMN_NAME"); + if (columnName != null) { + pkColumns.put(rs.getShort("KEY_SEQ"), normalizeColumnName(columnName)); + } + } + } + + return List.copyOf(pkColumns.values()); + } + + private Map> getIndexesByColumn( + Connection conn, String tableName, List primaryKeys) throws SQLException { + Map indexes = new LinkedHashMap<>(); + DatabaseMetaData metaData = conn.getMetaData(); + + try (ResultSet rs = + metaData.getIndexInfo( + conn.getCatalog(), conn.getSchema(), tableName, false, false)) { + while (rs.next()) { + String indexName = rs.getString("INDEX_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + if (indexName == null || columnName == null) { + continue; + } + boolean unique = !rs.getBoolean("NON_UNIQUE"); + short position = rs.getShort("ORDINAL_POSITION"); + indexes.computeIfAbsent(indexName, key -> new IndexParts(indexName, unique)) + .columns() + .put(position, columnName); } } - return pkColumns; + Map> indexesByColumn = new LinkedHashMap<>(); + for (IndexParts index : indexes.values()) { + if (!primaryKeys.isEmpty() && index.normalizedColumnNames().equals(primaryKeys)) { + continue; + } + String description = index.description(); + for (String column : index.columns().values()) { + indexesByColumn + .computeIfAbsent(normalizeColumnName(column), key -> new ArrayList<>()) + .add(description); + } + } + return indexesByColumn; } private List getColumns( - Connection conn, String tableName, Set primaryKeys) throws SQLException { + Connection conn, + String tableName, + List primaryKeys, + Map> indexesByColumn) + throws SQLException { List columns = new ArrayList<>(); DatabaseMetaData metaData = conn.getMetaData(); @@ -148,24 +195,52 @@ private List getColumns( String columnName = rs.getString("COLUMN_NAME"); String typeName = rs.getString("TYPE_NAME"); int columnSize = rs.getInt("COLUMN_SIZE"); + int decimalDigits = rs.getInt("DECIMAL_DIGITS"); String nullableStr = rs.getString("IS_NULLABLE"); boolean nullable = "YES".equalsIgnoreCase(nullableStr); String defaultValue = rs.getString("COLUMN_DEF"); String remarks = rs.getString("REMARKS"); - boolean isPk = primaryKeys.contains(columnName); + String normalizedColumnName = normalizeColumnName(columnName); + boolean isPk = primaryKeys.contains(normalizedColumnName); columns.add( new PhysicalColumnInfo( columnName, typeName, columnSize, + decimalDigits, nullable, defaultValue, isPk, - remarks)); + remarks, + indexesByColumn.getOrDefault(normalizedColumnName, List.of()))); } } return columns; } + + private static String normalizeColumnName(String columnName) { + return SemanticUtils.normalizeObjectName( + columnName, "Missing column name while reading schema."); + } + + private record IndexParts(String name, boolean unique, Map columns) { + + private IndexParts(String name, boolean unique) { + this(name, unique, new TreeMap<>()); + } + + private String description() { + return (unique ? "UNIQUE " : "") + + name + + "(" + + String.join(", ", columns.values()) + + ")"; + } + + private List normalizedColumnNames() { + return columns.values().stream().map(SchemaReader::normalizeColumnName).toList(); + } + } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java index 604b1dd..a45afef 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java @@ -43,12 +43,9 @@ public class GetTableSchemaTool implements MarkAgentTool { name = "get_table_schema", description = """ - Get the schema information of the specified table, including column name, data \ - type, whether it is primary key, whether it allows null, default value \ - and column comments. Returns semantic-first merged column information \ - (uses semantic layer if available, falls back to physical layer \ - otherwise). This tool should be called to understand the table \ - structure before generating SQL.\ + Get synced semantic-layer schema information for the specified table, \ + including column name, data type, primary key flag, index hints and column \ + descriptions. Call this tool before generating SQL.\ """) public ToolResultBlock getTableSchema( @ToolParam(name = "table_name", description = "The table name to query schema for") 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 ecf24ea..51b7227 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 @@ -42,10 +42,8 @@ public class GetTablesTool implements MarkAgentTool { name = "get_tables", description = """ - Get table information from the database, including table name, domain, \ - description and relations. Returns semantic-first merged table \ - information (uses semantic layer if available, falls back to physical \ - layer otherwise).\ + Get synced semantic-layer table information, including table name, domain, \ + description and enabled relations.\ """) public ToolResultBlock getTables( @ToolParam( diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/convertor/PromptConverter.java b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/PromptConverter.java index bb25339..9996727 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/convertor/PromptConverter.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/PromptConverter.java @@ -17,9 +17,6 @@ */ package io.github.malonetalk.convertor; -import io.github.malonetalk.common.SemanticConstants; -import io.github.malonetalk.dto.datasource.PhysicalColumnInfo; -import io.github.malonetalk.dto.datasource.PhysicalTableInfo; import io.github.malonetalk.dto.prompt.ColumnPromptResponse; import io.github.malonetalk.dto.prompt.TablePromptResponse; import io.github.malonetalk.dto.prompt.TableRelationPromptResponse; @@ -29,85 +26,40 @@ import io.github.malonetalk.service.semantic.enums.UsageLevelEnum; import io.github.malonetalk.utils.SemanticUtils; import java.util.List; -import java.util.Map; -/** 物理层/语义层 → Agent Prompt DTO 的统一转换器,集中管理所有面向 LLM 的 DTO 映射逻辑。 */ +/** Converts synced semantic-layer snapshots into Agent-facing prompt DTOs. */ public final class PromptConverter { private PromptConverter() {} - /** 将物理列信息与语义列信息合并,转换为面向 Agent 的列响应 DTO */ - public static ColumnPromptResponse mapColumnPrompt( - PhysicalColumnInfo physicalColumn, Map semanticByName) { - ColumnInfo semanticColumn = - semanticByName.get( - SemanticUtils.normalizeObjectName( - physicalColumn.columnName(), - "Missing physical column name for prompt conversion.")); - // null = 没有语义列记录,视为纯物理列、纳入 prompt; - // 仅在确有语义记录且不可用时才跳过 - if (semanticColumn != null - && !SemanticAvailabilityHelper.isColumnAvailable( - semanticColumn, UsageLevelEnum.AI_PROMPT)) { + public static ColumnPromptResponse mapColumnPrompt(ColumnInfo column) { + if (!SemanticAvailabilityHelper.isColumnAvailable(column, UsageLevelEnum.AI_PROMPT)) { return null; } - - String description = - SemanticUtils.firstNonBlank( - semanticColumn == null ? null : semanticColumn.getColumnDescription(), - physicalColumn.remarks()); - - StringBuilder typeBuilder = new StringBuilder(physicalColumn.typeName()); - if (physicalColumn.columnSize() > 0) { - typeBuilder.append("(").append(physicalColumn.columnSize()).append(")"); - } - return ColumnPromptResponse.builder() - .name(physicalColumn.columnName()) - .type(typeBuilder.toString()) - .primaryKey(physicalColumn.primaryKey()) - .nullable(physicalColumn.nullable()) - .defaultValue(SemanticUtils.trimToNull(physicalColumn.defaultValue())) - .description(description) + .name(column.getColumnName()) + .type(SemanticUtils.trimToNull(column.getTypeName())) + .primaryKey(column.getPrimaryKey()) + .description( + SemanticUtils.firstNonBlank( + column.getColumnDescription(), + column.getPhysicalColumnDescription())) + .indexInfo(SemanticUtils.trimToNull(column.getIndexInfo())) .build(); } - /** 将物理表信息与语义表信息合并,转换为面向 Agent 的表响应 DTO */ public static TablePromptResponse mapTablePrompt( - PhysicalTableInfo physicalTable, - Map semanticByName, - List resolvedRelations) { - TableInfo semanticTable = - semanticByName.get( - SemanticUtils.normalizeObjectName( - physicalTable.tableName(), - "Missing physical table name for prompt conversion.")); - // ponytail: null = 没有语义表记录,视为纯物理表、纳入 prompt; - // 仅在确有语义记录且不可用时才跳过 - if (semanticTable != null - && !SemanticAvailabilityHelper.isTableAvailable( - semanticTable, UsageLevelEnum.AI_PROMPT)) { + TableInfo table, List resolvedRelations) { + if (!SemanticAvailabilityHelper.isTableAvailable(table, UsageLevelEnum.AI_PROMPT)) { return null; } - return TablePromptResponse.builder() - .name(physicalTable.tableName()) - .domain(resolveDomain(semanticTable)) - .description(resolveDescription(physicalTable, semanticTable)) + .name(table.getTableName()) + .domain(SemanticUtils.normalizeDomain(table.getDomain())) + .description( + SemanticUtils.firstNonBlank( + table.getTableDescription(), table.getPhysicalTableDescription())) .relations(resolvedRelations) .build(); } - - private static String resolveDomain(TableInfo semanticTable) { - return semanticTable == null - ? SemanticConstants.DEFAULT_DOMAIN - : SemanticUtils.normalizeDomain(semanticTable.getDomain()); - } - - private static String resolveDescription( - PhysicalTableInfo physicalTable, TableInfo semanticTable) { - return SemanticUtils.firstNonBlank( - semanticTable == null ? null : semanticTable.getTableDescription(), - physicalTable.remarks()); - } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/convertor/SemanticConverter.java b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/SemanticConverter.java index 8cf96d8..5c2e4e6 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/convertor/SemanticConverter.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/convertor/SemanticConverter.java @@ -70,6 +70,7 @@ public ColumnSemanticResponse toResponse(ColumnInfo columnInfo) { .columnDescription(columnInfo.getColumnDescription()) .typeName(SemanticUtils.trimToNull(columnInfo.getTypeName())) .primaryKey(columnInfo.getPrimaryKey()) + .indexInfo(SemanticUtils.trimToNull(columnInfo.getIndexInfo())) .isVisible(columnInfo.getIsVisible()) .hasPhysicalColumn(hasPhysicalColumn) .effective( diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/datasource/PhysicalColumnInfo.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/datasource/PhysicalColumnInfo.java index 31503fe..993f28c 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/datasource/PhysicalColumnInfo.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/datasource/PhysicalColumnInfo.java @@ -17,23 +17,53 @@ */ package io.github.malonetalk.dto.datasource; +import java.util.List; +import java.util.Locale; + /** 物理数据源列信息,由 SchemaReader 从 JDBC 元数据读取 */ public record PhysicalColumnInfo( String columnName, String typeName, int columnSize, + int decimalDigits, boolean nullable, String defaultValue, boolean primaryKey, - String remarks) { + String remarks, + List indexes) { + + public String formattedTypeName() { + if (typeName == null || typeName.isBlank()) { + return null; + } + String trimmedTypeName = typeName.trim(); + if (columnSize <= 0) { + return trimmedTypeName; + } + return switch (trimmedTypeName.toUpperCase(Locale.ROOT)) { + case "CHAR", "VARCHAR" -> trimmedTypeName + "(" + columnSize + ")"; + case "DECIMAL", "NUMERIC" -> + trimmedTypeName + + "(" + + columnSize + + (decimalDigits > 0 ? "," + decimalDigits : "") + + ")"; + default -> trimmedTypeName; + }; + } + + public String formattedIndexInfo() { + if (indexes == null || indexes.isEmpty()) { + return null; + } + String indexInfo = String.join(", ", indexes).trim(); + return indexInfo.isEmpty() ? null : indexInfo; + } @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(columnName).append(" ").append(typeName); - if (columnSize > 0) { - sb.append("(").append(columnSize).append(")"); - } + sb.append(columnName).append(" ").append(formattedTypeName()); if (primaryKey) { sb.append(" PRIMARY KEY"); } @@ -46,6 +76,10 @@ public String toString() { if (remarks != null && !remarks.isEmpty()) { sb.append(" COMMENT '").append(remarks).append("'"); } + String indexInfo = formattedIndexInfo(); + if (indexInfo != null) { + sb.append(" INDEX ").append(indexInfo); + } return sb.toString(); } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/prompt/ColumnPromptResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/prompt/ColumnPromptResponse.java index 8ef02e5..d26b94f 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/prompt/ColumnPromptResponse.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/prompt/ColumnPromptResponse.java @@ -22,9 +22,4 @@ /** Agent-facing DTO for LLM prompt formatting. */ @Builder public record ColumnPromptResponse( - String name, - String type, - Boolean primaryKey, - Boolean nullable, - String defaultValue, - String description) {} + String name, String type, Boolean primaryKey, String description, String indexInfo) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/ColumnSemanticResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/ColumnSemanticResponse.java index e1db962..4eea5ca 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/ColumnSemanticResponse.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/semantic/ColumnSemanticResponse.java @@ -28,6 +28,7 @@ public record ColumnSemanticResponse( String columnDescription, String typeName, Boolean primaryKey, + String indexInfo, Boolean isVisible, Boolean hasPhysicalColumn, // 最终可用性(推导值,非存储字段):effective = isVisible && hasPhysicalColumn。 diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnInfo.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnInfo.java index e3df009..e87f991 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnInfo.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ColumnInfo.java @@ -30,6 +30,7 @@ public class ColumnInfo { private String physicalColumnDescription; private String typeName; private Boolean primaryKey; + private String indexInfo; private String columnDescription; private Boolean isVisible; private Boolean physicalStatus; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/SemanticMergeService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/SemanticMergeService.java index dadfae6..df46ee8 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/SemanticMergeService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/SemanticMergeService.java @@ -17,11 +17,9 @@ */ package io.github.malonetalk.service.semantic; -import io.github.malonetalk.agent.datasource.SchemaReader; import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.common.SemanticConstants; import io.github.malonetalk.convertor.PromptConverter; -import io.github.malonetalk.dto.datasource.PhysicalColumnInfo; import io.github.malonetalk.dto.prompt.ColumnPromptResponse; import io.github.malonetalk.dto.prompt.TablePromptResponse; import io.github.malonetalk.dto.prompt.TableRelationPromptResponse; @@ -43,6 +41,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -52,7 +51,6 @@ @RequiredArgsConstructor public class SemanticMergeService { - private final SchemaReader schemaReader; private final TableInfoMapper tableInfoMapper; private final ColumnSemanticInfoMapper columnSemanticInfoMapper; private final LogicalTableRelationMapper logicalTableRelationMapper; @@ -70,54 +68,53 @@ public List listVisibleTablesByDomains( RelationSourceIndex.of( logicalTableRelationMapper.selectByDatasourceId(datasource.getId())); - return schemaReader.getTables(datasource).stream() + return tableIndex.asList().stream() .map( - physical -> + table -> PromptConverter.mapTablePrompt( - physical, - tableIndex.asMap(), + table, resolveVisibleRelations( - physical.tableName(), + table.getTableName(), tableIndex, columnIndex, relationIndex))) - .filter(java.util.Objects::nonNull) + .filter(Objects::nonNull) .filter(table -> domainMatches(table.domain(), normalizedDomains)) .toList(); } public List getTableSchema(Datasource datasource, String tableName) { String normalizedTableName = - SemanticUtils.requireTrimmed( + SemanticUtils.normalizeObjectName( tableName, "Missing tableName for merged table schema lookup."); - List physicalColumns = - schemaReader.getTableSchema(datasource, normalizedTableName); - if (physicalColumns.isEmpty()) { + TableInfo semanticTable = + tableInfoMapper.selectByDatasourceIdAndTableName( + datasource.getId(), normalizedTableName); + if (semanticTable == null || !SemanticAvailabilityHelper.hasPhysicalTable(semanticTable)) { throw BusinessException.of( ErrorCode.RESOURCE_NOT_FOUND, "The physical table does not exist or is unavailable. Synchronize the table" + " schema and try again."); } + if (!Boolean.TRUE.equals(semanticTable.getIsVisible())) { + throw BusinessException.of(ErrorCode.TABLE_HIDDEN); + } - TableInfo semanticTable = - tableInfoMapper.selectByDatasourceIdAndTableName( + List columns = + columnSemanticInfoMapper.selectByDatasourceIdAndTableName( datasource.getId(), normalizedTableName); - if (semanticTable != null && !SemanticAvailabilityHelper.hasPhysicalTable(semanticTable)) { + if (columns.isEmpty()) { throw BusinessException.of( ErrorCode.RESOURCE_NOT_FOUND, - "Table " + normalizedTableName + " does not exist physically."); - } - if (semanticTable != null && !Boolean.TRUE.equals(semanticTable.getIsVisible())) { - throw BusinessException.of(ErrorCode.TABLE_HIDDEN); + "No synced columns found for table " + + normalizedTableName + + ". Synchronize the table schema and try again."); } - Map semanticColumnIndex = - buildSemanticColumnIndex(datasource.getId(), normalizedTableName); - - return physicalColumns.stream() - .map(physical -> PromptConverter.mapColumnPrompt(physical, semanticColumnIndex)) - .filter(java.util.Objects::nonNull) + return columns.stream() + .map(PromptConverter::mapColumnPrompt) + .filter(Objects::nonNull) .toList(); } @@ -126,9 +123,9 @@ private List resolveVisibleRelations( TableNameIndex tableIndex, TableColumnIndex columnIndex, RelationSourceIndex relationIndex) { - List logicalRelations = relationIndex.get(sourceTableName); List visibleRelations = - filterVisibleLogicalRelations(logicalRelations, tableIndex, columnIndex); + filterVisibleLogicalRelations( + relationIndex.get(sourceTableName), tableIndex, columnIndex); return deduplicateRelations(visibleRelations); } @@ -141,36 +138,18 @@ private List filterVisibleLogicalRelations( if (!Boolean.TRUE.equals(relation.getIsEnabled())) { continue; } - if (tableIndex.isHidden(relation.getSourceTableName()) - || tableIndex.isHidden(relation.getTargetTableName())) { - continue; - } - List sourceColumns; - try { - sourceColumns = - logicalTableRelationHelper.fromJson( - relation.getSourceColumnNamesJson(), "sourceColumnNames"); - } catch (BusinessException e) { - log.warn( - "Skip relation id={}: invalid source columns - {}", - relation.getId(), - e.getMessage()); + if (tableIndex.isUnavailable(relation.getSourceTableName()) + || tableIndex.isUnavailable(relation.getTargetTableName())) { continue; } - List targetColumns; - try { - targetColumns = - logicalTableRelationHelper.fromJson( - relation.getTargetColumnNamesJson(), "targetColumnNames"); - } catch (BusinessException e) { - log.warn( - "Skip relation id={}: invalid target columns - {}", - relation.getId(), - e.getMessage()); + List sourceColumns = parseRelationColumns(relation, true); + List targetColumns = parseRelationColumns(relation, false); + if (sourceColumns == null || targetColumns == null) { continue; } - if (columnIndex.hasHiddenColumn(relation.getSourceTableName(), sourceColumns) - || columnIndex.hasHiddenColumn(relation.getTargetTableName(), targetColumns)) { + if (columnIndex.hasUnavailableColumn(relation.getSourceTableName(), sourceColumns) + || columnIndex.hasUnavailableColumn( + relation.getTargetTableName(), targetColumns)) { continue; } visibleRelations.add( @@ -179,12 +158,28 @@ private List filterVisibleLogicalRelations( return visibleRelations; } + private List parseRelationColumns(LogicalTableRelation relation, boolean source) { + String fieldName = source ? "sourceColumnNames" : "targetColumnNames"; + String json = + source ? relation.getSourceColumnNamesJson() : relation.getTargetColumnNamesJson(); + try { + return logicalTableRelationHelper.fromJson(json, fieldName); + } catch (BusinessException e) { + log.warn( + "Skip relation id={}: invalid {} - {}", + relation.getId(), + fieldName, + e.getMessage()); + return null; + } + } + private List deduplicateRelations( List relations) { LinkedHashMap merged = new LinkedHashMap<>(); for (ResolvedLogicalRelation relation : relations) { String key = - buildRelationMergeKey( + logicalTableRelationHelper.buildRelationKey( relation.sourceTableName(), relation.sourceColumns(), relation.targetTableName(), @@ -205,37 +200,13 @@ private TableRelationPromptResponse toPromptResponse(ResolvedLogicalRelation rel relation.relation().getDescription()); } - private String buildRelationMergeKey( - String sourceTable, - List sourceColumns, - String targetTable, - List targetColumns) { - return logicalTableRelationHelper.buildRelationKey( - sourceTable, sourceColumns, targetTable, targetColumns); - } - - private Map buildSemanticColumnIndex( - Integer datasourceId, String tableName) { - Map result = new HashMap<>(); - for (ColumnInfo column : - columnSemanticInfoMapper.selectByDatasourceIdAndTableName( - datasourceId, tableName)) { - result.put( - SemanticUtils.normalizeObjectName( - column.getColumnName(), - "Missing columnName while building semantic column index."), - column); - } - return result; - } - private List normalizeDomains(List domains) { if (domains == null || domains.isEmpty()) { return List.of(); } return domains.stream() .map(SemanticUtils::trimToNull) - .filter(java.util.Objects::nonNull) + .filter(Objects::nonNull) .distinct() .toList(); } @@ -262,7 +233,7 @@ private String targetTableName() { private record TableNameIndex(Map index) { private static TableNameIndex of(List tables) { - Map map = new HashMap<>(); + Map map = new LinkedHashMap<>(); for (TableInfo table : tables) { map.put( SemanticUtils.normalizeObjectName( @@ -273,8 +244,8 @@ private static TableNameIndex of(List tables) { return new TableNameIndex(map); } - private Map asMap() { - return index; + private List asList() { + return List.copyOf(index.values()); } private TableInfo get(String tableName) { @@ -283,14 +254,11 @@ private TableInfo get(String tableName) { tableName, "Missing tableName while reading semantic table index.")); } - private boolean isHidden(String tableName) { + private boolean isUnavailable(String tableName) { TableInfo tableInfo = get(tableName); - if (tableInfo == null) { - // 表不在已加载的语义索引里:当作“未隐藏”保留,避免误丢合法关系 - return false; - } - return !SemanticAvailabilityHelper.isTableAvailable( - tableInfo, UsageLevelEnum.AI_PROMPT); + return tableInfo == null + || !SemanticAvailabilityHelper.isTableAvailable( + tableInfo, UsageLevelEnum.AI_PROMPT); } } @@ -327,15 +295,12 @@ private ColumnInfo get(String tableName, String columnName) { "Missing columnName while reading semantic column index.")); } - private boolean hasHiddenColumn(String tableName, List columnNames) { + private boolean hasUnavailableColumn(String tableName, List columnNames) { for (String columnName : columnNames) { ColumnInfo columnInfo = get(tableName, columnName); - if (columnInfo == null) { - // 列不在已加载的语义索引里:当作“未隐藏”保留 - return false; - } - if (!SemanticAvailabilityHelper.isColumnAvailable( - columnInfo, UsageLevelEnum.AI_PROMPT)) { + if (columnInfo == null + || !SemanticAvailabilityHelper.isColumnAvailable( + columnInfo, UsageLevelEnum.AI_PROMPT)) { return true; } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/sync/SemanticSyncApplyService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/sync/SemanticSyncApplyService.java index e94edfc..074a9af 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/sync/SemanticSyncApplyService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/sync/SemanticSyncApplyService.java @@ -237,7 +237,8 @@ private SyncTableResult buildPresentTableResult( } if (!Objects.equals(existingColumn.getPhysicalColumnDescription(), column.description()) || !Objects.equals(existingColumn.getTypeName(), column.typeName()) - || !Objects.equals(existingColumn.getPrimaryKey(), column.primaryKey())) { + || !Objects.equals(existingColumn.getPrimaryKey(), column.primaryKey()) + || !Objects.equals(existingColumn.getIndexInfo(), column.indexInfo())) { updatedColumns++; } } @@ -368,6 +369,7 @@ private ColumnInfo buildPhysicalColumnInfo( columnInfo.setPhysicalColumnDescription(column.description()); columnInfo.setTypeName(column.typeName()); columnInfo.setPrimaryKey(column.primaryKey()); + columnInfo.setIndexInfo(column.indexInfo()); columnInfo.setIsVisible(Boolean.TRUE); columnInfo.setPhysicalStatus(Boolean.TRUE); return columnInfo; @@ -377,5 +379,9 @@ public record TableSyncSource( String tableName, String description, List columns) {} public record ColumnSyncSource( - String columnName, String description, String typeName, Boolean primaryKey) {} + String columnName, + String description, + String typeName, + Boolean primaryKey, + String indexInfo) {} } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/sync/SemanticSyncServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/sync/SemanticSyncServiceImpl.java index 764be02..caa5461 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/sync/SemanticSyncServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/semantic/sync/SemanticSyncServiceImpl.java @@ -205,8 +205,9 @@ private TableSyncSource readTableSyncSource( SemanticUtils.requireTrimmed( column.columnName(), "Missing physical columnName."), SemanticUtils.trimToNull(column.remarks()), - SemanticUtils.trimToNull(column.typeName()), - column.primaryKey())); + column.formattedTypeName(), + column.primaryKey(), + column.formattedIndexInfo())); } return new TableSyncSource( physicalTable.tableName(), diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/utils/SemanticUtils.java b/data-agent-backend/src/main/java/io/github/malonetalk/utils/SemanticUtils.java index 97e4390..ca87433 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/utils/SemanticUtils.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/utils/SemanticUtils.java @@ -101,10 +101,14 @@ public static String formatTableSchema(String tableName, List + @@ -100,10 +101,11 @@ INSERT INTO column_info ( datasource_id, table_name, column_name, physical_column_description, type_name, - primary_key, column_description, is_visible, physical_status, create_time, update_time + primary_key, index_info, column_description, is_visible, physical_status, + create_time, update_time ) VALUES ( #{datasourceId}, #{tableName}, #{columnName}, #{physicalColumnDescription}, - #{typeName}, COALESCE(#{primaryKey}, 0), #{columnDescription}, + #{typeName}, COALESCE(#{primaryKey}, 0), #{indexInfo}, #{columnDescription}, COALESCE(#{isVisible}, 1), COALESCE(#{physicalStatus}, 1), COALESCE(#{createTime}, NOW()), COALESCE(#{updateTime}, NOW()) ) @@ -112,13 +114,15 @@ INSERT INTO column_info ( datasource_id, table_name, column_name, physical_column_description, type_name, - primary_key, column_description, is_visible, physical_status, create_time, update_time + primary_key, index_info, column_description, is_visible, physical_status, + create_time, update_time ) VALUES ( #{column.datasourceId}, #{column.tableName}, #{column.columnName}, #{column.physicalColumnDescription}, #{column.typeName}, - COALESCE(#{column.primaryKey}, 0), #{column.columnDescription}, + COALESCE(#{column.primaryKey}, 0), #{column.indexInfo}, + #{column.columnDescription}, COALESCE(#{column.isVisible}, 1), COALESCE(#{column.physicalStatus}, 1), COALESCE(#{column.createTime}, NOW()), COALESCE(#{column.updateTime}, NOW()) ) @@ -127,6 +131,7 @@ physical_column_description = VALUES(physical_column_description), type_name = VALUES(type_name), primary_key = VALUES(primary_key), + index_info = VALUES(index_info), physical_status = VALUES(physical_status), update_time = VALUES(update_time) @@ -151,6 +156,7 @@ physical_column_description = #{physicalColumnDescription}, type_name = #{typeName}, primary_key = #{primaryKey}, + index_info = #{indexInfo}, physical_status = #{physicalStatus}, update_time = #{updateTime}, diff --git a/data-agent-frontend/src/api/semantic.ts b/data-agent-frontend/src/api/semantic.ts index ff49e95..c220c6b 100644 --- a/data-agent-frontend/src/api/semantic.ts +++ b/data-agent-frontend/src/api/semantic.ts @@ -46,6 +46,7 @@ export interface ColumnSemanticResponse { columnDescription: string | null; typeName: string | null; primaryKey: boolean | null; + indexInfo: string | null; isVisible: boolean; hasPhysicalColumn: boolean; effective: boolean; diff --git a/data-agent-frontend/src/views/chat/components/TracePanel.vue b/data-agent-frontend/src/views/chat/components/TracePanel.vue index b0159fa..5df9560 100644 --- a/data-agent-frontend/src/views/chat/components/TracePanel.vue +++ b/data-agent-frontend/src/views/chat/components/TracePanel.vue @@ -29,7 +29,7 @@ (e: 'previewReport', content: string): void; }>(); - const isExpanded = ref(true); + const isExpanded = ref(false); function toggleExpand() { isExpanded.value = !isExpanded.value; @@ -132,7 +132,8 @@
> - Agent 思考与执行链路 + + {{ message.isStreaming ? '思考中' : '思维链' }} {{ summaryLabel }}
@@ -218,6 +219,15 @@ white-space: nowrap; } + .trace-panel__spinner { + width: 10px; + height: 10px; + border: 2px solid var(--app-border); + border-top-color: var(--app-accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; + } + .trace-panel__summary { color: var(--app-text-muted); font-weight: normal; @@ -317,4 +327,10 @@ line-height: 1.5; margin: 0; } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } diff --git a/data-agent-frontend/src/views/semantic/components/ColumnSemanticManage.vue b/data-agent-frontend/src/views/semantic/components/ColumnSemanticManage.vue index c24865a..8932168 100644 --- a/data-agent-frontend/src/views/semantic/components/ColumnSemanticManage.vue +++ b/data-agent-frontend/src/views/semantic/components/ColumnSemanticManage.vue @@ -231,6 +231,11 @@ PK + + +