22 Commits

Author SHA1 Message Date
Jun Zhang
ec3fb53b85 Merge branch 'master' into master 2025-07-09 17:20:40 +08:00
FredTsang
11d1264d38 (fix)(chat-sdk): Export data based on queryColumns (#2297) 2025-07-09 17:19:16 +08:00
FredTsang
32675387d7 (fix)(chat-sdk) rm table defaultSortOrder (#2295) 2025-07-09 17:18:45 +08:00
mroldx
e408204690 Update sql-update-mysql.sql (#2293) 2025-07-09 17:18:08 +08:00
wangyong
269f146c11 (fix)(headless-server)语意建模-X模型-维度管理-维度搜索带key查询时,返回的维度结果范围超出了X模型范围 (#2328)
Some checks failed
supersonic CentOS CI / build (21) (push) Has been cancelled
supersonic mac CI / build (21) (push) Has been cancelled
supersonic ubuntu CI / build (21) (push) Has been cancelled
supersonic windows CI / build (21) (push) Has been cancelled
2025-07-07 17:19:59 +08:00
jerryjzhang
6f497b142e [fix][chat]Memory deletion should disable it first.
Some checks failed
supersonic CentOS CI / build (21) (push) Has been cancelled
supersonic mac CI / build (21) (push) Has been cancelled
supersonic ubuntu CI / build (21) (push) Has been cancelled
supersonic windows CI / build (21) (push) Has been cancelled
2025-07-02 18:59:56 +08:00
jerryjzhang
79a44b27ee [fix][heaadless]Fix NPE issues.
Some checks failed
supersonic CentOS CI / build (21) (push) Has been cancelled
supersonic mac CI / build (21) (push) Has been cancelled
supersonic ubuntu CI / build (21) (push) Has been cancelled
supersonic windows CI / build (21) (push) Has been cancelled
2025-06-28 08:51:04 +08:00
jerryjzhang
76cc5ee111 [opt][heaadless]Add user field to QueryStatement for consistency.
Some checks are pending
supersonic CentOS CI / build (21) (push) Waiting to run
supersonic mac CI / build (21) (push) Waiting to run
supersonic ubuntu CI / build (21) (push) Waiting to run
supersonic windows CI / build (21) (push) Waiting to run
2025-06-27 19:46:00 +08:00
jerryjzhang
320fcf04bd [fix][heaadless]bizName of ModelResp is not necessarily equal to table name.
Some checks failed
supersonic CentOS CI / build (21) (push) Has been cancelled
supersonic mac CI / build (21) (push) Has been cancelled
supersonic ubuntu CI / build (21) (push) Has been cancelled
supersonic windows CI / build (21) (push) Has been cancelled
2025-06-25 23:14:16 +08:00
jerryjzhang
75fc83010c [fix][heaadless]Fix dim value replacement. 2025-06-25 22:22:12 +08:00
superhero
37673c82da Merge pull request #2303 from Willy-J/fixbug2
Some checks failed
supersonic CentOS CI / build (21) (push) Has been cancelled
supersonic mac CI / build (21) (push) Has been cancelled
supersonic ubuntu CI / build (21) (push) Has been cancelled
supersonic windows CI / build (21) (push) Has been cancelled
(fix)(headless) add empty check for metric alias to prevent incorrect DataFormatType assignment
2025-06-23 17:31:03 +08:00
superhero
3ae0d645a7 Merge pull request #2307 from guilinlewis/lewis-pr
(improvement)(common|headless|chat|auth) 鉴权优化与召回优化
2025-06-23 17:29:21 +08:00
superhero
256a6bcb3f Merge pull request #2306 from 1985312383/master
(improvement)(headless | chat | launchers | webapp) Added assistant function! Call LLM to optimize SQL code performance before final SQL execution
2025-06-23 17:25:43 +08:00
guilinlewis
1faf84e372 (improvement)(common|headless|chat|auth) 鉴权优化与召回优化
1 修复生成的用户token 一生成就失效的问题
2 如果用户设置的token ,需校验是否数据库存在,因为用户可设置一年的token 有泄露风险
3 结果解析优化, 去除不可以解析的情况,解析问题需要改写后的问,
4 召回样例,用相似度,保住至少有一个样例是高相似度的
5 数据集召回,填加完全匹配格式筛选逻辑
2025-06-23 10:03:17 +08:00
guilinlewis
7e6639df83 (improvement)(common|headless|chat|auth) 鉴权优化与召回优化
1 修复生成的用户token 一生成就失效的问题
2 如果用户设置的token ,需校验是否数据库存在,因为用户可设置一年的token 有泄露风险
3 结果解析优化, 去除不可以解析的情况,解析问题需要改写后的问,
4 召回样例,用相似度,保住至少有一个样例是高相似度的
5 数据集召回,填加完全匹配格式筛选逻辑
2025-06-23 09:47:48 +08:00
柯慕灵
075ae4c0af Merge branch 'master' of https://github.com/1985312383/supersonic 2025-06-21 05:19:52 +08:00
柯慕灵
08133ccbfb Update LLMPhysicalSqlCorrector.java
修正为合适的prompt
2025-06-21 05:19:42 +08:00
柯慕灵
164d2a9e23 update supersonic-build.bat
增加路径创建时的判断,增加环境检测,修正打包的路径问题,与supersonic-daemen.bat 命令,符合文档中的运行方式
2025-06-21 05:00:54 +08:00
柯慕灵
f899d23b63 add new chat corrector
在助理最终执行物理SQL前,加入一步LLM优化性能功能
2025-06-21 04:57:04 +08:00
柯慕灵
944beddafc Merge branch 'tencentmusic:master' into master 2025-06-21 04:50:36 +08:00
Willy-J
019d737f07 (fix)(headless) add empty check for metric alias to prevent incorrect DataFormatType assignment 2025-06-18 15:03:40 +08:00
柯慕灵
87355533b4 fix windows daemon.bat path configuration 2025-06-11 09:20:09 +08:00
35 changed files with 412 additions and 106 deletions

View File

@@ -43,10 +43,26 @@ if "%service%"=="webapp" (
call mvn -f %projectDir% clean package -DskipTests -Dspotless.skip=true
IF ERRORLEVEL 1 (
ECHO Failed to build backend Java modules.
ECHO Please check Maven and Java versions are compatible.
ECHO Current Java: %JAVA_HOME%
ECHO Current Maven: %MAVEN_HOME%
EXIT /B 1
)
REM extract and copy files to deployment directory
cd %projectDir%\launchers\%model_name%\target
if exist "launchers-%model_name%-%MVN_VERSION%-bin.tar.gz" (
echo "Extracting launchers-%model_name%-%MVN_VERSION%-bin.tar.gz..."
tar -xf "launchers-%model_name%-%MVN_VERSION%-bin.tar.gz"
if exist "launchers-%model_name%-%MVN_VERSION%" (
echo "Copying files to deployment directory..."
xcopy /E /Y "launchers-%model_name%-%MVN_VERSION%\*" "%buildDir%\supersonic-%model_name%-%MVN_VERSION%\"
)
)
copy /y %projectDir%\launchers\%model_name%\target\*.tar.gz %buildDir%\
echo "finished building supersonic-%model_name% service"
cd %baseDir%
goto :EOF
@@ -72,22 +88,55 @@ if "%service%"=="webapp" (
cd %buildDir%
if exist %release_dir% rmdir /s /q %release_dir%
if exist %release_dir%.zip del %release_dir%.zip
mkdir %release_dir%
rem package webapp
tar xvf supersonic-webapp.tar.gz
move /y supersonic-webapp webapp
echo {"env": ""} > webapp\supersonic.config.json
move /y webapp %release_dir%
rem package java service
tar xvf %service_name%-bin.tar.gz
for /d %%D in ("%service_name%\*") do (
move "%%D" "%release_dir%"
rem check if release directory already exists from buildJavaService
if exist %release_dir% (
echo "Release directory already prepared by buildJavaService"
) else (
mkdir %release_dir%
rem package java service
tar xvf %service_name%-bin.tar.gz 2>nul
if errorlevel 1 (
echo "Warning: tar command failed, trying PowerShell extraction..."
powershell -Command "Expand-Archive -Path '%service_name%-bin.tar.gz' -DestinationPath '.' -Force"
)
for /d %%D in ("%service_name%\*") do (
move "%%D" "%release_dir%"
)
rmdir /s /q %service_name% 2>nul
)
rem package webapp
if exist supersonic-webapp.tar.gz (
tar xvf supersonic-webapp.tar.gz 2>nul
if errorlevel 1 (
echo "Warning: tar command failed, trying PowerShell extraction..."
powershell -Command "Expand-Archive -Path 'supersonic-webapp.tar.gz' -DestinationPath '.' -Force"
)
move /y supersonic-webapp webapp
echo {"env": ""} > webapp\supersonic.config.json
move /y webapp %release_dir%
del supersonic-webapp.tar.gz 2>nul
)
rem verify deployment structure
if exist "%release_dir%\lib\launchers-%model_name%-%MVN_VERSION%.jar" (
echo "Deployment structure verified successfully"
) else (
echo "Warning: Main jar file not found in deployment structure"
echo "Expected: %release_dir%\lib\launchers-%model_name%-%MVN_VERSION%.jar"
)
rem generate zip file
powershell Compress-Archive -Path %release_dir% -DestinationPath %release_dir%.zip
del %service_name%-bin.tar.gz
del supersonic-webapp.tar.gz
rmdir /s /q %service_name%
powershell -Command "Compress-Archive -Path '%release_dir%' -DestinationPath '%release_dir%.zip' -Force"
if errorlevel 1 (
echo "Warning: PowerShell compression failed, release directory still available: %release_dir%"
) else (
echo "Successfully created release package: %release_dir%.zip"
)
del %service_name%-bin.tar.gz 2>nul
echo "finished packaging supersonic release"
goto :EOF

View File

@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -222,8 +223,9 @@ public class DefaultUserAdaptor implements UserAdaptor {
new UserWithPassword(userDO.getId(), userDO.getName(), userDO.getDisplayName(),
userDO.getEmail(), userDO.getPassword(), userDO.getIsAdmin());
String token =
tokenService.generateToken(UserWithPassword.convert(userWithPassword), expireTime);
// 使用令牌名称作为生成key 这样可以区分正常请求和api 请求api 的令牌失效时间很长,需考虑令牌泄露的情况
String token = tokenService.generateToken(UserWithPassword.convert(userWithPassword),
"SysDbToken:" + name, (new Date().getTime() + expireTime));
UserTokenDO userTokenDO = saveUserToken(name, userName, token, expireTime);
return convertUserToken(userTokenDO);
}

View File

@@ -21,6 +21,8 @@ public interface UserRepository {
UserTokenDO getUserToken(Long tokenId);
UserTokenDO getUserTokenByName(String tokenName);
void deleteUserTokenByName(String userName);
void deleteUserToken(Long tokenId);

View File

@@ -65,6 +65,13 @@ public class UserRepositoryImpl implements UserRepository {
return userTokenDOMapper.selectById(tokenId);
}
@Override
public UserTokenDO getUserTokenByName(String tokenName) {
QueryWrapper<UserTokenDO> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserTokenDO::getName, tokenName);
return userTokenDOMapper.selectOne(queryWrapper);
}
@Override
public void deleteUserTokenByName(String userName) {
QueryWrapper<UserTokenDO> queryWrapper = new QueryWrapper<>();

View File

@@ -6,7 +6,10 @@ import javax.crypto.spec.SecretKeySpec;
import com.tencent.supersonic.auth.api.authentication.config.AuthenticationConfig;
import com.tencent.supersonic.auth.api.authentication.pojo.UserWithPassword;
import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserTokenDO;
import com.tencent.supersonic.auth.authentication.persistence.repository.UserRepository;
import com.tencent.supersonic.common.pojo.exception.AccessException;
import com.tencent.supersonic.common.util.ContextUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@@ -71,6 +74,7 @@ public class TokenService {
return generateToken(UserWithPassword.convert(appUser), request);
}
public Optional<Claims> getClaims(HttpServletRequest request) {
String token = request.getHeader(authenticationConfig.getTokenHttpHeaderKey());
String appKey = getAppKey(request);
@@ -90,6 +94,14 @@ public class TokenService {
public Optional<Claims> getClaims(String token, String appKey) {
try {
if (StringUtils.isNotBlank(appKey) && appKey.startsWith("SysDbToken:")) {// 如果是配置的长期令牌,需校验数据库是否存在该配置
UserRepository userRepository = ContextUtils.getBean(UserRepository.class);
UserTokenDO dbToken =
userRepository.getUserTokenByName(appKey.substring("SysDbToken:".length()));
if (dbToken == null || !dbToken.getToken().equals(token.replace("Bearer ", ""))) {
throw new AccessException("Token does not exist :" + appKey);
}
}
String tokenSecret = getTokenSecret(appKey);
Claims claims =
Jwts.parser().setSigningKey(tokenSecret.getBytes(StandardCharsets.UTF_8))
@@ -122,6 +134,16 @@ public class TokenService {
Map<String, String> appKeyToSecretMap = authenticationConfig.getAppKeyToSecretMap();
String secret = appKeyToSecretMap.get(appKey);
if (StringUtils.isBlank(secret)) {
if (StringUtils.isNotBlank(appKey) && appKey.startsWith("SysDbToken:")) { // 是配置的长期令牌
String realAppKey = appKey.substring("SysDbToken:".length());
String tmp =
"WIaO9YRRVt+7QtpPvyWsARFngnEcbaKBk783uGFwMrbJBaochsqCH62L4Kijcb0sZCYoSsiKGV/zPml5MnZ3uQ==";
if (tmp.length() <= realAppKey.length()) {
return realAppKey;
} else {
return realAppKey + tmp.substring(realAppKey.length());
}
}
throw new AccessException("get secret from appKey failed :" + appKey);
}
return secret;

View File

@@ -75,8 +75,12 @@ public class SqlExecutor implements ChatQueryExecutor {
return null;
}
QuerySqlReq sqlReq =
QuerySqlReq.builder().sql(parseInfo.getSqlInfo().getCorrectedS2SQL()).build();
// 使用querySQL它已经包含了所有修正包括物理SQL修正
String finalSql = StringUtils.isNotBlank(parseInfo.getSqlInfo().getQuerySQL())
? parseInfo.getSqlInfo().getQuerySQL()
: parseInfo.getSqlInfo().getCorrectedS2SQL();
QuerySqlReq sqlReq = QuerySqlReq.builder().sql(finalSql).build();
sqlReq.setSqlInfo(parseInfo.getSqlInfo());
sqlReq.setDataSetId(parseInfo.getDataSetId());
@@ -90,7 +94,7 @@ public class SqlExecutor implements ChatQueryExecutor {
queryResult.setQueryTimeCost(System.currentTimeMillis() - startTime);
if (queryResp != null) {
queryResult.setQueryAuthorization(queryResp.getQueryAuthorization());
queryResult.setQuerySql(queryResp.getSql());
queryResult.setQuerySql(finalSql);
queryResult.setQueryResults(queryResp.getResultList());
queryResult.setQueryColumns(queryResp.getColumns());
queryResult.setQueryState(QueryState.SUCCESS);

View File

@@ -47,7 +47,8 @@ public class DataInterpretProcessor implements ExecuteResultProcessor {
Agent agent = executeContext.getAgent();
ChatApp chatApp = agent.getChatAppConfig().get(APP_KEY);
return Objects.nonNull(chatApp) && chatApp.isEnable()
&& StringUtils.isNotBlank(executeContext.getResponse().getTextResult()); // 如果都没结果,则无法处理,直接跳过
&& StringUtils.isNotBlank(executeContext.getResponse().getTextResult()) // 如果都没结果,则无法处理
&& StringUtils.isBlank(executeContext.getResponse().getTextSummary()); // 如果已经有汇总的结果了,无法再次处理
}
@Override
@@ -57,7 +58,16 @@ public class DataInterpretProcessor implements ExecuteResultProcessor {
ChatApp chatApp = agent.getChatAppConfig().get(APP_KEY);
Map<String, Object> variable = new HashMap<>();
variable.put("question", executeContext.getRequest().getQueryText());
String question = executeContext.getResponse().getTextResult();// 结果解析应该用改写的问题,因为改写的内容信息量更大
if (executeContext.getParseInfo().getProperties() != null
&& executeContext.getParseInfo().getProperties().containsKey("CONTEXT")) {
Map<String, Object> context = (Map<String, Object>) executeContext.getParseInfo()
.getProperties().get("CONTEXT");
if (context.get("queryText") != null && "".equals(context.get("queryText"))) {
question = context.get("queryText").toString();
}
}
variable.put("question", question);
variable.put("data", queryResult.getTextResult());
Prompt prompt = PromptTemplate.from(chatApp.getPrompt()).apply(variable);

View File

@@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@@ -39,6 +40,7 @@ public class AgentServiceImpl extends ServiceImpl<AgentDOMapper, AgentDO> implem
private MemoryService memoryService;
@Autowired
@Lazy
private ChatQueryService chatQueryService;
@Autowired

View File

@@ -49,6 +49,7 @@ import net.sf.jsqlparser.schema.Column;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@@ -66,6 +67,7 @@ public class ChatQueryServiceImpl implements ChatQueryService {
@Autowired
private SemanticLayerService semanticLayerService;
@Autowired
@Lazy
private AgentService agentService;
private final List<ChatQueryParser> chatQueryParsers = ComponentFactory.getChatParsers();

View File

@@ -26,7 +26,6 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@@ -110,6 +109,14 @@ public class MemoryServiceImpl implements MemoryService, CommandLineRunner {
@Override
public void batchDelete(List<Long> ids) {
QueryWrapper<ChatMemoryDO> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().in(ChatMemoryDO::getId, ids);
List<ChatMemoryDO> chatMemoryDOS = chatMemoryRepository.getMemories(queryWrapper);
chatMemoryDOS.forEach(chatMemoryDO -> {
if (MemoryStatus.ENABLED.toString().equals(chatMemoryDO.getStatus().trim())) {
disableMemory(chatMemoryDO);
}
});
chatMemoryRepository.batchDelete(ids);
}

View File

@@ -21,7 +21,8 @@ public class LoadRemoveService {
List<String> resultList = new ArrayList<>(value);
if (!CollectionUtils.isEmpty(modelIdOrDataSetIds)) {
resultList.removeIf(nature -> {
if (Objects.isNull(nature)) {
if (Objects.isNull(nature) || !nature.startsWith("_")) { // 系统的字典是以 _ 开头的,
// 过滤因引用外部字典导致的异常
return false;
}
Long id = getId(nature);

View File

@@ -22,4 +22,6 @@ public class Text2SQLExemplar implements Serializable {
private String dbSchema;
private String sql;
protected double similarity; // 传递相似度,可以作为样本筛选的依据
}

View File

@@ -72,7 +72,10 @@ public class ExemplarServiceImpl implements ExemplarService, CommandLineRunner {
embeddingService.retrieveQuery(collection, retrieveQuery, num);
results.forEach(ret -> {
ret.getRetrieval().forEach(r -> {
exemplars.add(JsonUtil.mapToObject(r.getMetadata(), Text2SQLExemplar.class));
Text2SQLExemplar tmp = // 传递相似度,可以作为样本筛选的依据
JsonUtil.mapToObject(r.getMetadata(), Text2SQLExemplar.class);
tmp.setSimilarity(r.getSimilarity());
exemplars.add(tmp);
});
});

View File

@@ -18,6 +18,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static com.tencent.supersonic.common.pojo.Constants.DEFAULT_DETAIL_LIMIT;
import static com.tencent.supersonic.common.pojo.Constants.DEFAULT_METRIC_LIMIT;
@@ -65,12 +66,23 @@ public class SemanticParseInfo implements Serializable {
DataSetMatchResult mr2 = getDataSetMatchResult(o2.getElementMatches());
double difference = mr1.getMaxDatesetSimilarity() - mr2.getMaxDatesetSimilarity();
if (difference == 0) {
if (Math.abs(difference) < 0.0005) { // 看完全匹配的个数,实践证明,可以用户输入规范后,该逻辑具有优势
if (!o1.getDataSetId().equals(o2.getDataSetId())) {
List<SchemaElementMatch> elementMatches1 = o1.getElementMatches().stream()
.filter(e -> e.getSimilarity() == 1).collect(Collectors.toList());
List<SchemaElementMatch> elementMatches2 = o2.getElementMatches().stream()
.filter(e -> e.getSimilarity() == 1).collect(Collectors.toList());
if (elementMatches1.size() > elementMatches2.size()) {
return -1;
} else if (elementMatches1.size() < elementMatches2.size()) {
return 1;
}
}
difference = mr1.getMaxMetricSimilarity() - mr2.getMaxMetricSimilarity();
if (difference == 0) {
if (Math.abs(difference) < 0.0005) {
difference = mr1.getTotalSimilarity() - mr2.getTotalSimilarity();
}
if (difference == 0) {
if (Math.abs(difference) < 0.0005) {
difference = mr1.getMaxMetricUseCnt() - mr2.getMaxMetricUseCnt();
}
}

View File

@@ -16,4 +16,7 @@ public class SqlInfo implements Serializable {
// SQL to be executed finally
private String querySQL;
// Physical SQL corrected by LLM for performance optimization
private String correctedQuerySQL;
}

View File

@@ -8,5 +8,6 @@ public enum ChatWorkflowState {
VALIDATING,
SQL_CORRECTING,
PROCESSING,
PHYSICAL_SQL_CORRECTING,
FINISHED
}

View File

@@ -0,0 +1,98 @@
package com.tencent.supersonic.headless.chat.corrector;
import com.tencent.supersonic.common.pojo.ChatApp;
import com.tencent.supersonic.common.pojo.enums.AppModule;
import com.tencent.supersonic.common.util.ChatAppManager;
import com.tencent.supersonic.headless.api.pojo.SemanticParseInfo;
import com.tencent.supersonic.headless.chat.ChatQueryContext;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.structured.Description;
import dev.langchain4j.provider.ModelProvider;
import dev.langchain4j.service.AiServices;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 物理SQL修正器 - 使用LLM优化物理SQL性能
*/
@Slf4j
public class LLMPhysicalSqlCorrector extends BaseSemanticCorrector {
private static final Logger keyPipelineLog = LoggerFactory.getLogger("keyPipeline");
public static final String APP_KEY = "PHYSICAL_SQL_CORRECTOR";
private static final String INSTRUCTION = ""
+ "#Role: You are a senior database performance optimization expert experienced in SQL tuning."
+ "\n\n#Task: You will be provided with a user question and the corresponding physical SQL query,"
+ " please analyze and optimize this SQL to improve query performance." + "\n\n#Rules:"
+ "\n1. DO NOT add or introduce any new fields, columns, or aliases that are not in the original SQL."
+ "\n2. Push WHERE conditions into JOIN ON clauses when possible to reduce intermediate result sets."
+ "\n3. Optimize JOIN order by placing smaller tables or tables with selective conditions first."
+ "\n4. For date range conditions, ensure they are applied as early as possible in the query execution."
+ "\n5. Remove or comment out database-specific index hints (like USE INDEX) that may cause syntax errors."
+ "\n6. ONLY modify the structure and order of existing elements, do not change field names or add new ones."
+ "\n7. Ensure the optimized SQL is syntactically correct and logically equivalent to the original."
+ "\n\n#Question: {{question}}" + "\n\n#OriginalSQL: {{sql}}";
public LLMPhysicalSqlCorrector() {
ChatAppManager.register(APP_KEY, ChatApp.builder().prompt(INSTRUCTION).name("物理SQL修正")
.appModule(AppModule.CHAT).description("通过大模型对物理SQL做性能优化").enable(false).build());
}
@Data
@ToString
static class PhysicalSql {
@Description("either positive or negative")
private String opinion;
@Description("optimized sql if negative")
private String sql;
}
interface PhysicalSqlExtractor {
PhysicalSql generatePhysicalSql(String text);
}
@Override
public void doCorrect(ChatQueryContext chatQueryContext, SemanticParseInfo semanticParseInfo) {
ChatApp chatApp = chatQueryContext.getRequest().getChatAppConfig().get(APP_KEY);
if (!chatQueryContext.getRequest().getText2SQLType().enableLLM() || Objects.isNull(chatApp)
|| !chatApp.isEnable()) {
return;
}
ChatLanguageModel chatLanguageModel =
ModelProvider.getChatModel(chatApp.getChatModelConfig());
PhysicalSqlExtractor extractor =
AiServices.create(PhysicalSqlExtractor.class, chatLanguageModel);
Prompt prompt = generatePrompt(chatQueryContext.getRequest().getQueryText(),
semanticParseInfo, chatApp.getPrompt());
PhysicalSql physicalSql =
extractor.generatePhysicalSql(prompt.toUserMessage().singleText());
keyPipelineLog.info("LLMPhysicalSqlCorrector modelReq:\n{} \nmodelResp:\n{}", prompt.text(),
physicalSql);
if ("NEGATIVE".equalsIgnoreCase(physicalSql.getOpinion())
&& StringUtils.isNotBlank(physicalSql.getSql())) {
semanticParseInfo.getSqlInfo().setCorrectedQuerySQL(physicalSql.getSql());
}
}
private Prompt generatePrompt(String queryText, SemanticParseInfo semanticParseInfo,
String promptTemplate) {
Map<String, Object> variable = new HashMap<>();
variable.put("question", queryText);
variable.put("sql", semanticParseInfo.getSqlInfo().getQuerySQL());
return PromptTemplate.from(promptTemplate).apply(variable);
}
}

View File

@@ -14,10 +14,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
import static com.tencent.supersonic.headless.chat.parser.ParserConfig.*;
@@ -51,13 +49,33 @@ public class PromptHelper {
// use random collection of exemplars for each self-consistency inference
for (int i = 0; i < selfConsistencyNumber; i++) {
List<Text2SQLExemplar> shuffledList = new ArrayList<>(exemplars);
// only shuffle the exemplars from config
List<Text2SQLExemplar> subList =
shuffledList.subList(llmReq.getDynamicExemplars().size(), shuffledList.size());
Collections.shuffle(subList);
results.add(shuffledList.subList(0, Math.min(shuffledList.size(), fewShotNumber)));
List<Text2SQLExemplar> same = shuffledList.stream() // 相似度极高的话,先找出来
.filter(e -> e.getSimilarity() > 0.989).collect(Collectors.toList());
List<Text2SQLExemplar> noSame = shuffledList.stream()
.filter(e -> e.getSimilarity() <= 0.989).collect(Collectors.toList());
if ((noSame.size() - same.size()) > fewShotNumber) {// 去除部分最低分
noSame.sort(Comparator.comparingDouble(Text2SQLExemplar::getSimilarity));
noSame = noSame.subList((noSame.size() - fewShotNumber) / 2, noSame.size());
}
Text2SQLExemplar mostSimilar = noSame.get(noSame.size() - 1);
Collections.shuffle(noSame);
List<Text2SQLExemplar> ts;
if (same.size() > 0) {// 一样的话,必须作为提示语
ts = new ArrayList<>();
int needSize = Math.min(noSame.size() + same.size(), fewShotNumber);
if (needSize > same.size()) {
ts.addAll(noSame.subList(0, needSize - same.size()));
}
ts.addAll(same);
} else { // 至少要一个最像的
ts = noSame.subList(0, Math.min(noSame.size(), fewShotNumber));
if (!ts.contains(mostSimilar)) {
ts.remove(ts.size() - 1);
ts.add(mostSimilar);
}
}
results.add(ts);
}
return results;
}

View File

@@ -1,5 +1,6 @@
package com.tencent.supersonic.headless.core.pojo;
import com.tencent.supersonic.common.pojo.User;
import com.tencent.supersonic.headless.api.pojo.response.QueryState;
import com.tencent.supersonic.headless.api.pojo.response.SemanticSchemaResp;
import lombok.Data;
@@ -24,6 +25,7 @@ public class QueryStatement {
private SemanticSchemaResp semanticSchema;
private Integer limit = 1000;
private Boolean isTranslated = false;
private User user;
public boolean isOk() {
return StringUtils.isBlank(errMsg) && StringUtils.isNotBlank(sql);

View File

@@ -102,7 +102,7 @@ public class DimValueAspect {
continue;
}
for (DimensionResp dimension : dimensions) {
if (!expression.getFieldName().equals(dimension.getName())
if (!expression.getFieldName().equals(dimension.getBizName())
|| CollectionUtils.isEmpty(dimension.getDimValueMaps())) {
continue;
}
@@ -124,6 +124,7 @@ public class DimValueAspect {
sql = SqlReplaceHelper.replaceValue(sql, filedNameToValueMap);
log.debug("correctorSql after replacing:{}", sql);
querySqlReq.setSql(sql);
querySqlReq.getSqlInfo().setQuerySQL(sql);
Map<String, Map<String, String>> techNameToBizName = getTechNameToBizName(dimensions);
SemanticQueryResp queryResultWithColumns = (SemanticQueryResp) joinPoint.proceed();

View File

@@ -296,6 +296,9 @@ public class S2SemanticLayerService implements SemanticLayerService {
queryStatement.setSql(semanticQueryReq.getSqlInfo().getQuerySQL());
queryStatement.setIsTranslated(true);
}
if (queryStatement != null) {
queryStatement.setUser(user);
}
return queryStatement;
}

View File

@@ -83,13 +83,10 @@ public class DimensionRepositoryImpl implements DimensionRepository {
}
if (StringUtils.isNotBlank(dimensionFilter.getKey())) {
String key = dimensionFilter.getKey();
queryWrapper.lambda()
.and(wrapper -> wrapper
.like(DimensionDO::getName, key).or()
.like(DimensionDO::getBizName, key).or().like(DimensionDO::getDescription, key)
.or().like(DimensionDO::getAlias, key).or()
.like(DimensionDO::getCreatedBy, key)
);
queryWrapper.and(qw->qw.lambda().like(DimensionDO::getName, key).or()
.like(DimensionDO::getBizName, key).or().like(DimensionDO::getDescription, key)
.or().like(DimensionDO::getAlias, key).or()
.like(DimensionDO::getCreatedBy, key));
}
return dimensionDOMapper.selectList(queryWrapper);

View File

@@ -8,6 +8,7 @@ import com.tencent.supersonic.headless.api.pojo.request.SemanticQueryReq;
import com.tencent.supersonic.headless.api.pojo.response.ParseResp;
import com.tencent.supersonic.headless.api.pojo.response.SemanticTranslateResp;
import com.tencent.supersonic.headless.chat.ChatQueryContext;
import com.tencent.supersonic.headless.chat.corrector.LLMPhysicalSqlCorrector;
import com.tencent.supersonic.headless.chat.corrector.SemanticCorrector;
import com.tencent.supersonic.headless.chat.mapper.SchemaMapper;
import com.tencent.supersonic.headless.chat.parser.SemanticParser;
@@ -76,6 +77,10 @@ public class ChatWorkflowEngine {
long start = System.currentTimeMillis();
performTranslating(queryCtx, parseResult);
parseResult.getParseTimeCost().setSqlTime(System.currentTimeMillis() - start);
queryCtx.setChatWorkflowState(ChatWorkflowState.PHYSICAL_SQL_CORRECTING);
break;
case PHYSICAL_SQL_CORRECTING:
performPhysicalSqlCorrecting(queryCtx);
queryCtx.setChatWorkflowState(ChatWorkflowState.FINISHED);
break;
default:
@@ -162,4 +167,26 @@ public class ChatWorkflowEngine {
parseResult.setErrorMsg(String.join("\n", errorMsg));
}
}
private void performPhysicalSqlCorrecting(ChatQueryContext queryCtx) {
List<SemanticQuery> candidateQueries = queryCtx.getCandidateQueries();
if (CollectionUtils.isNotEmpty(candidateQueries)) {
for (SemanticQuery semanticQuery : candidateQueries) {
for (SemanticCorrector corrector : semanticCorrectors) {
if (corrector instanceof LLMPhysicalSqlCorrector) {
corrector.correct(queryCtx, semanticQuery.getParseInfo());
// 如果物理SQL被修正了更新querySQL为修正后的版本
SemanticParseInfo parseInfo = semanticQuery.getParseInfo();
if (StringUtils.isNotBlank(parseInfo.getSqlInfo().getCorrectedQuerySQL())) {
parseInfo.getSqlInfo()
.setQuerySQL(parseInfo.getSqlInfo().getCorrectedQuerySQL());
log.info("Physical SQL corrected and updated querySQL: {}",
parseInfo.getSqlInfo().getQuerySQL());
}
break;
}
}
}
}
}
}

View File

@@ -19,9 +19,10 @@ public class DataSetSchemaBuilder {
public static DataSetSchema build(DataSetSchemaResp resp) {
DataSetSchema dataSetSchema = new DataSetSchema();
dataSetSchema.setQueryConfig(resp.getQueryConfig());
SchemaElement dataSet = SchemaElement.builder().dataSetId(resp.getId())
.dataSetName(resp.getName()).id(resp.getId()).name(resp.getName())
.bizName(resp.getBizName()).type(SchemaElementType.DATASET).build();
SchemaElement dataSet =
SchemaElement.builder().dataSetId(resp.getId()).dataSetName(resp.getName())
.id(resp.getId()).name(resp.getName()).description(resp.getDescription())
.bizName(resp.getBizName()).type(SchemaElementType.DATASET).build();
dataSetSchema.setDataSet(dataSet);
dataSetSchema.setDatabaseType(resp.getDatabaseType());
dataSetSchema.setDatabaseVersion(resp.getDatabaseVersion());

View File

@@ -138,7 +138,8 @@ public class DictUtils {
semanticQueryReq.setNeedAuth(false);
String bizName = dictItemResp.getBizName();
try {
SemanticQueryResp semanticQueryResp = queryService.queryByReq(semanticQueryReq, null);
SemanticQueryResp semanticQueryResp =
queryService.queryByReq(semanticQueryReq, User.getDefaultUser());
if (Objects.isNull(semanticQueryResp)
|| CollectionUtils.isEmpty(semanticQueryResp.getResultList())) {
return lines;
@@ -274,6 +275,9 @@ public class DictUtils {
private QuerySqlReq constructQuerySqlReq(DictItemResp dictItemResp) {
ModelResp model = modelService.getModel(dictItemResp.getModelId());
String tableStr = StringUtils.isNotBlank(model.getModelDetail().getTableQuery())
? model.getModelDetail().getTableQuery()
: "(" + model.getModelDetail().getSqlQuery() + ")";
String sqlPattern =
"select %s,count(1) from %s %s group by %s order by count(1) desc limit %d";
String dimBizName = dictItemResp.getBizName();
@@ -287,8 +291,7 @@ public class DictUtils {
limit = Integer.MAX_VALUE;
}
String sql =
String.format(sqlPattern, dimBizName, model.getBizName(), where, dimBizName, limit);
String sql = String.format(sqlPattern, dimBizName, tableStr, where, dimBizName, limit);
Set<Long> modelIds = new HashSet<>();
modelIds.add(dictItemResp.getModelId());
QuerySqlReq querySqlReq = new QuerySqlReq();

View File

@@ -109,7 +109,8 @@ public class QueryUtils {
column.setModelId(metric.getModelId());
}
// if column nameEn contains metric alias, use metric dataFormatType
if (column.getDataFormatType() == null && metric.getAlias() != null) {
if (column.getDataFormatType() == null
&& StringUtils.isNotEmpty(metric.getAlias())) {
for (String alias : metric.getAlias().split(",")) {
if (nameEn.contains(alias)) {
column.setDataFormatType(metric.getDataFormatType());

View File

@@ -14,7 +14,8 @@ com.tencent.supersonic.headless.chat.parser.SemanticParser=\
com.tencent.supersonic.headless.chat.corrector.SemanticCorrector=\
com.tencent.supersonic.headless.chat.corrector.RuleSqlCorrector,\
com.tencent.supersonic.headless.chat.corrector.LLMSqlCorrector
com.tencent.supersonic.headless.chat.corrector.LLMSqlCorrector,\
com.tencent.supersonic.headless.chat.corrector.LLMPhysicalSqlCorrector
com.tencent.supersonic.headless.chat.knowledge.file.FileHandler=\
com.tencent.supersonic.headless.chat.knowledge.file.FileHandlerImpl

View File

@@ -15,7 +15,8 @@ com.tencent.supersonic.headless.chat.parser.SemanticParser=\
com.tencent.supersonic.headless.chat.corrector.SemanticCorrector=\
com.tencent.supersonic.headless.chat.corrector.RuleSqlCorrector,\
com.tencent.supersonic.headless.chat.corrector.LLMSqlCorrector
com.tencent.supersonic.headless.chat.corrector.LLMSqlCorrector,\
com.tencent.supersonic.headless.chat.corrector.LLMPhysicalSqlCorrector
com.tencent.supersonic.headless.chat.knowledge.file.FileHandler=\
com.tencent.supersonic.headless.chat.knowledge.file.FileHandlerImpl

View File

@@ -420,4 +420,4 @@ ALTER TABLE s2_chat_model add column is_open tinyint DEFAULT NULL COMMENT '是
ALTER TABLE s2_database add column is_open tinyint DEFAULT NULL COMMENT '是否公开';
--20250321
ALTER TABLE s2_user add column last_loin datetime DEFAULT NULL;
ALTER TABLE s2_user add column last_login datetime DEFAULT NULL;

10
pom.xml
View File

@@ -10,9 +10,9 @@
<version>${revision}</version>
<modules>
<module>common</module>
<module>auth</module>
<module>chat</module>
<module>common</module>
<module>launchers</module>
<module>headless</module>
</modules>
@@ -31,6 +31,7 @@
<java.target.version>21</java.target.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<file.encoding>UTF-8</file.encoding>
<jsqlparser.version>4.9</jsqlparser.version>
<pagehelper.version>6.1.0</pagehelper.version>
@@ -254,6 +255,13 @@
<source>${java.source.version}</source>
<target>${java.target.version}</target>
<encoding>${file.encoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>

5
webapp/package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"supersonic-chat-sdk": "link:packages/chat-sdk"
}
}

View File

@@ -79,6 +79,7 @@ export type SqlInfoType = {
parsedS2SQL: string;
correctedS2SQL: string;
querySQL: string;
correctedQuerySQL?: string;
};
export type ChatContextType = {

View File

@@ -58,32 +58,28 @@ const SqlItem: React.FC<Props> = ({
const getSchemaMapText = () => {
return `
Schema映射
${schema?.fieldNameList?.length > 0 ? `名称:${schema.fieldNameList.join('、')}` : ''}${
schema?.values?.length > 0
${schema?.fieldNameList?.length > 0 ? `名称:${schema.fieldNameList.join('、')}` : ''}${schema?.values?.length > 0
? `
取值:${schema.values
.map((item: any) => {
return `${item.fieldName}: ${item.fieldValue}`;
})
.join('、')}`
.map((item: any) => {
return `${item.fieldName}: ${item.fieldValue}`;
})
.join('、')}`
: ''
}${
priorExts
}${priorExts
? `
附加:${priorExts}`
: ''
}${
terms?.length > 0
}${terms?.length > 0
? `
术语:${terms
.map((item: any) => {
return `${item.name}${item.alias?.length > 0 ? `(${item.alias.join(',')})` : ''}: ${
item.description
.map((item: any) => {
return `${item.name}${item.alias?.length > 0 ? `(${item.alias.join(',')})` : ''}: ${item.description
}`;
})
.join('、')}`
})
.join('、')}`
: ''
}
}
`;
};
@@ -91,16 +87,16 @@ ${schema?.fieldNameList?.length > 0 ? `名称:${schema.fieldNameList.join('、
const getFewShotText = () => {
return `
Few-shot示例${fewShots
.map((item: any, index: number) => {
return `
.map((item: any, index: number) => {
return `
示例${index + 1}
问题:${item.question}
SQL
${format(item.sql)}
`;
})
.join('')}
})
.join('')}
`;
};
@@ -120,6 +116,14 @@ ${format(sqlInfo.correctedS2SQL)}
`;
};
const getCorrectedQuerySQLText = () => {
return `
物理SQL修正
${format(sqlInfo.correctedQuerySQL || '')}
`;
};
const getQuerySQLText = () => {
return `
最终执行SQL
@@ -155,6 +159,9 @@ ${executeErrorMsg}
if (sqlInfo.correctedS2SQL) {
text += getCorrectedS2SQLText();
}
if (sqlInfo.correctedQuerySQL) {
text += getCorrectedQuerySQLText();
}
if (sqlInfo.querySQL) {
text += getQuerySQLText();
}
@@ -183,9 +190,8 @@ ${executeErrorMsg}
<div className={`${tipPrefixCls}-content-options`}>
{llmReq && (
<div
className={`${tipPrefixCls}-content-option ${
sqlType === 'schemaMap' ? `${tipPrefixCls}-content-option-active` : ''
}`}
className={`${tipPrefixCls}-content-option ${sqlType === 'schemaMap' ? `${tipPrefixCls}-content-option-active` : ''
}`}
onClick={() => {
setSqlType(sqlType === 'schemaMap' ? '' : 'schemaMap');
}}
@@ -195,9 +201,8 @@ ${executeErrorMsg}
)}
{fewShots.length > 0 && (
<div
className={`${tipPrefixCls}-content-option ${
sqlType === 'fewShots' ? `${tipPrefixCls}-content-option-active` : ''
}`}
className={`${tipPrefixCls}-content-option ${sqlType === 'fewShots' ? `${tipPrefixCls}-content-option-active` : ''
}`}
onClick={() => {
setSqlType(sqlType === 'fewShots' ? '' : 'fewShots');
}}
@@ -207,9 +212,8 @@ ${executeErrorMsg}
)}
{sqlInfo.parsedS2SQL && (
<div
className={`${tipPrefixCls}-content-option ${
sqlType === 'parsedS2SQL' ? `${tipPrefixCls}-content-option-active` : ''
}`}
className={`${tipPrefixCls}-content-option ${sqlType === 'parsedS2SQL' ? `${tipPrefixCls}-content-option-active` : ''
}`}
onClick={() => {
setSqlType(sqlType === 'parsedS2SQL' ? '' : 'parsedS2SQL');
}}
@@ -219,9 +223,8 @@ ${executeErrorMsg}
)}
{sqlInfo.correctedS2SQL && (
<div
className={`${tipPrefixCls}-content-option ${
sqlType === 'correctedS2SQL' ? `${tipPrefixCls}-content-option-active` : ''
}`}
className={`${tipPrefixCls}-content-option ${sqlType === 'correctedS2SQL' ? `${tipPrefixCls}-content-option-active` : ''
}`}
onClick={() => {
setSqlType(sqlType === 'correctedS2SQL' ? '' : 'correctedS2SQL');
}}
@@ -229,16 +232,26 @@ ${executeErrorMsg}
S2SQL
</div>
)}
{sqlInfo.correctedQuerySQL && (
<div
className={`${tipPrefixCls}-content-option ${sqlType === 'correctedQuerySQL' ? `${tipPrefixCls}-content-option-active` : ''
}`}
onClick={() => {
setSqlType(sqlType === 'correctedQuerySQL' ? '' : 'correctedQuerySQL');
}}
>
SQL修正
</div>
)}
{sqlInfo.querySQL && (
<div
className={`${tipPrefixCls}-content-option ${
sqlType === 'querySQL' ? `${tipPrefixCls}-content-option-active` : ''
}`}
className={`${tipPrefixCls}-content-option ${sqlType === 'querySQL' ? `${tipPrefixCls}-content-option-active` : ''
}`}
onClick={() => {
setSqlType(sqlType === 'querySQL' ? '' : 'querySQL');
}}
>
SQL
{sqlInfo.correctedQuerySQL ? '最终执行SQL' : '最终执行SQL'}
</div>
)}
<Button className={`${prefixCls}-export-log`} size="small" onClick={onExportLog}>
@@ -248,13 +261,12 @@ ${executeErrorMsg}
</div>
</div>
<div
className={`${prefixCls} ${
!window.location.pathname.includes('/chat') &&
className={`${prefixCls} ${!window.location.pathname.includes('/chat') &&
integrateSystem &&
integrateSystem !== 'wiki'
? `${prefixCls}-copilot`
: ''
}`}
? `${prefixCls}-copilot`
: ''
}`}
>
{sqlType === 'schemaMap' && (
<div className={`${prefixCls}-code`}>
@@ -290,9 +302,8 @@ ${executeErrorMsg}
<div className={`${prefixCls}-schema-content`}>
{terms
.map((item: any) => {
return `${item.name}${
item.alias?.length > 0 ? `(${item.alias.join(',')})` : ''
}: ${item.description}`;
return `${item.name}${item.alias?.length > 0 ? `(${item.alias.join(',')})` : ''
}: ${item.description}`;
})
.join('、')}
</div>

View File

@@ -416,11 +416,10 @@ const ChatItem: React.FC<Props> = ({
const onExportData = () => {
const { queryColumns, queryResults } = data || {};
if (!!queryResults) {
if (!!queryResults && !!queryColumns) {
const exportData = queryResults.map(item => {
return Object.keys(item).reduce((result, key) => {
const columnName = queryColumns?.find(column => column.nameEn === key)?.name || key;
result[columnName] = item[key];
return queryColumns.reduce((result, column) => {
result[column.name || column.nameEn] = item[column.nameEn];
return result;
}, {});
});

View File

@@ -24,7 +24,6 @@ const Table: React.FC<Props> = ({ data, size, loading, question, onApplyAuth })
dataIndex: bizName,
key: bizName,
title: name || bizName,
defaultSortOrder: 'descend',
sorter:
showType === 'NUMBER'
? (a, b) => {
@@ -73,10 +72,11 @@ const Table: React.FC<Props> = ({ data, size, loading, question, onApplyAuth })
return index % 2 !== 0 ? `${prefixCls}-even-row` : '';
};
const dateColumn = queryColumns.find(column => column.type === 'DATE');
const dateColumn = queryColumns.find(column => column.type === 'DATE' || column.showType === 'DATE');
const dataSource = dateColumn
? queryResults.sort((a, b) => moment(a[dateColumn.bizName]).diff(moment(b[dateColumn.bizName])))
: queryResults;
return (
<div className={prefixCls}>
{question && (