Skip to content

Commit 86dffa3

Browse files
authored
Merge pull request #65 from springaialibaba/0228-yuluo/add-more-chatclient
[WIP] feat: add web search
2 parents f940100 + 3ea3e83 commit 86dffa3

40 files changed

+2711
-461
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.alibaba.cloud.ai.application.advisor;
2+
3+
import java.util.List;
4+
import java.util.Objects;
5+
6+
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
7+
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
8+
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
9+
import org.springframework.ai.chat.messages.AssistantMessage;
10+
import org.springframework.ai.chat.model.ChatResponse;
11+
import org.springframework.ai.chat.model.Generation;
12+
import org.springframework.util.StringUtils;
13+
14+
/**
15+
* @author yuluo
16+
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
17+
* 将 deepseek-r1 的 reasoning content 整合到输出中
18+
*/
19+
20+
public class ReasoningContentAdvisor implements BaseAdvisor {
21+
22+
private final int order;
23+
24+
public ReasoningContentAdvisor(Integer order) {
25+
this.order = order != null ? order : 0;
26+
}
27+
28+
@Override
29+
public AdvisedRequest before(AdvisedRequest request) {
30+
31+
return request;
32+
}
33+
34+
@Override
35+
public AdvisedResponse after(AdvisedResponse advisedResponse) {
36+
37+
ChatResponse resp = advisedResponse.response();
38+
if (Objects.isNull(resp)) {
39+
40+
return advisedResponse;
41+
}
42+
43+
String reasoningContent = resp.getMetadata().get("reasoning_content");
44+
if (StringUtils.hasText(reasoningContent)) {
45+
List<Generation> thinkGenerations = resp.getResults().stream()
46+
.map(generation -> {
47+
AssistantMessage output = generation.getOutput();
48+
// 将 think 思维链的内容整合到原始的输出中
49+
AssistantMessage thinkAssistantMessage = new AssistantMessage(
50+
String.format("<think>%s</think>", reasoningContent) + output.getContent(),
51+
output.getMetadata(),
52+
output.getToolCalls(),
53+
output.getMedia()
54+
);
55+
return new Generation(thinkAssistantMessage, generation.getMetadata());
56+
}).toList();
57+
58+
ChatResponse thinkChatResp = ChatResponse.builder().from(resp).generations(thinkGenerations).build();
59+
return AdvisedResponse.from(advisedResponse).response(thinkChatResp).build();
60+
61+
}
62+
63+
return advisedResponse;
64+
}
65+
66+
@Override
67+
public int getOrder() {
68+
69+
return this.order;
70+
}
71+
72+
}

spring-ai-alibaba-integration-example/backend/src/main/java/com/alibaba/cloud/ai/application/controller/SAAWebSearchController.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.alibaba.cloud.ai.application.controller;
22

3-
import com.alibaba.cloud.ai.application.entity.result.Result;
4-
import com.alibaba.cloud.ai.application.service.SAAWebSearch;
3+
import com.alibaba.cloud.ai.application.service.SAAWebSearchService;
4+
import com.alibaba.cloud.ai.application.utils.ValidText;
55
import io.swagger.v3.oas.annotations.tags.Tag;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import reactor.core.publisher.Flux;
68

79
import org.springframework.web.bind.annotation.GetMapping;
810
import org.springframework.web.bind.annotation.RequestMapping;
@@ -19,18 +21,25 @@
1921
@RequestMapping("/api/v1/")
2022
public class SAAWebSearchController {
2123

22-
private final SAAWebSearch webSearch;
24+
private final SAAWebSearchService webSearch;
2325

24-
public SAAWebSearchController(SAAWebSearch webSearch) {
26+
public SAAWebSearchController(SAAWebSearchService webSearch) {
2527
this.webSearch = webSearch;
2628
}
2729

2830
@GetMapping("/search")
29-
public Result<Object> search(
30-
@RequestParam(value = "query", required = true) String query
31+
public Flux<String> search(
32+
@RequestParam(value = "query") String query,
33+
HttpServletResponse response
3134
) {
3235

33-
return Result.success(webSearch.search(query));
36+
if (!ValidText.isValidate(query)) {
37+
return Flux.just("Invalid query");
38+
}
39+
40+
response.setCharacterEncoding("UTF-8");
41+
42+
return webSearch.chat(query);
3443
}
3544

3645
}

spring-ai-alibaba-integration-example/backend/src/main/java/com/alibaba/cloud/ai/application/service/SAAWebSearch.java

Lines changed: 0 additions & 25 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.alibaba.cloud.ai.application.service;
2+
3+
import com.alibaba.cloud.ai.application.advisor.ReasoningContentAdvisor;
4+
import com.alibaba.cloud.ai.application.websearch.core.IQSSearchEngine;
5+
import com.alibaba.cloud.ai.application.websearch.data.DataClean;
6+
import com.alibaba.cloud.ai.application.websearch.rag.WebSearchRetriever;
7+
import com.alibaba.cloud.ai.application.websearch.rag.join.ConcatenationDocumentJoiner;
8+
import com.alibaba.cloud.ai.application.websearch.rag.prompt.CustomContextQueryAugmenter;
9+
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
10+
import reactor.core.publisher.Flux;
11+
12+
import org.springframework.ai.chat.client.ChatClient;
13+
import org.springframework.ai.chat.client.advisor.RetrievalAugmentationAdvisor;
14+
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
15+
import org.springframework.ai.chat.prompt.PromptTemplate;
16+
import org.springframework.ai.rag.postretrieval.ranking.DocumentRanker;
17+
import org.springframework.ai.rag.preretrieval.query.expansion.QueryExpander;
18+
import org.springframework.ai.rag.preretrieval.query.transformation.QueryTransformer;
19+
import org.springframework.beans.factory.annotation.Qualifier;
20+
import org.springframework.stereotype.Service;
21+
22+
/**
23+
* @author yuluo
24+
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
25+
*/
26+
27+
@Service
28+
public class SAAWebSearchService {
29+
30+
private final ChatClient chatClient;
31+
32+
private final SimpleLoggerAdvisor simpleLoggerAdvisor;
33+
34+
private final ReasoningContentAdvisor reasoningContentAdvisor;
35+
36+
private final QueryTransformer queryTransformer;
37+
38+
private final QueryExpander queryExpander;
39+
40+
private final PromptTemplate queryArgumentPromptTemplate;
41+
42+
private final WebSearchRetriever webSearchRetriever;
43+
44+
private static final String DEFAULT_WEB_SEARCH_MODEL = "deepseek-r1";
45+
46+
public SAAWebSearchService(
47+
ChatClient.Builder chatClientBuilder,
48+
QueryTransformer queryTransformer,
49+
QueryExpander queryExpander,
50+
IQSSearchEngine searchEngine,
51+
DataClean dataCleaner,
52+
DocumentRanker documentRanker,
53+
@Qualifier("queryArgumentPromptTemplate") PromptTemplate queryArgumentPromptTemplate
54+
) {
55+
56+
this.queryTransformer = queryTransformer;
57+
this.queryExpander = queryExpander;
58+
this.queryArgumentPromptTemplate = queryArgumentPromptTemplate;
59+
60+
// 用于 DeepSeek-r1 的 reasoning content 整合到输出中
61+
this.reasoningContentAdvisor = new ReasoningContentAdvisor(1);
62+
63+
// 构建 chatClient
64+
this.chatClient = chatClientBuilder
65+
.defaultOptions(
66+
DashScopeChatOptions.builder()
67+
.withModel(DEFAULT_WEB_SEARCH_MODEL)
68+
// stream 模式下是否开启增量输出
69+
.withIncrementalOutput(true)
70+
.build())
71+
.build();
72+
73+
// 日志
74+
this.simpleLoggerAdvisor = new SimpleLoggerAdvisor(100);
75+
76+
this.webSearchRetriever = WebSearchRetriever.builder()
77+
.searchEngine(searchEngine)
78+
.dataCleaner(dataCleaner)
79+
.maxResults(2)
80+
.enableRanker(true)
81+
.documentRanker(documentRanker)
82+
.build();
83+
}
84+
85+
// 处理用户输入
86+
public Flux<String> chat(String prompt) {
87+
88+
return chatClient.prompt()
89+
.advisors(
90+
createRetrievalAugmentationAdvisor(),
91+
// 不整合到 reasoning content 输出中
92+
// reasoningContentAdvisor,
93+
simpleLoggerAdvisor
94+
).user(prompt)
95+
.stream()
96+
.content();
97+
}
98+
99+
private RetrievalAugmentationAdvisor createRetrievalAugmentationAdvisor() {
100+
101+
return RetrievalAugmentationAdvisor.builder()
102+
.documentRetriever(webSearchRetriever)
103+
.queryTransformers(queryTransformer)
104+
.queryAugmenter(
105+
new CustomContextQueryAugmenter(
106+
queryArgumentPromptTemplate,
107+
null,
108+
true)
109+
).queryExpander(queryExpander)
110+
.documentJoiner(new ConcatenationDocumentJoiner())
111+
.build();
112+
}
113+
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.alibaba.cloud.ai.application.websearch;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
/**
6+
* @author yuluo
7+
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
8+
*/
9+
10+
@ConfigurationProperties("spring.iqs.search")
11+
public class IQSSearchProperties {
12+
13+
private String apiKey;
14+
15+
public String getApiKey() {
16+
return this.apiKey;
17+
}
18+
19+
public void setApiKey(String apiKey) {
20+
this.apiKey = apiKey;
21+
}
22+
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.alibaba.cloud.ai.application.websearch.config;
2+
3+
import com.alibaba.cloud.ai.application.websearch.rag.postretrieval.DashScopeDocumentRanker;
4+
import com.alibaba.cloud.ai.application.websearch.rag.preretrieval.query.expansion.MultiQueryExpander;
5+
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
6+
import com.alibaba.cloud.ai.model.RerankModel;
7+
8+
import org.springframework.ai.chat.client.ChatClient;
9+
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
10+
import org.springframework.ai.chat.prompt.PromptTemplate;
11+
import org.springframework.ai.rag.preretrieval.query.expansion.QueryExpander;
12+
import org.springframework.ai.rag.preretrieval.query.transformation.QueryTransformer;
13+
import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer;
14+
import org.springframework.beans.factory.annotation.Qualifier;
15+
import org.springframework.context.annotation.Bean;
16+
import org.springframework.context.annotation.Configuration;
17+
18+
/**
19+
* @author yuluo
20+
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
21+
*/
22+
23+
@Configuration
24+
public class WeSearchConfiguration {
25+
26+
@Bean
27+
public DashScopeDocumentRanker dashScopeDocumentRanker(
28+
RerankModel rerankModel
29+
) {
30+
return new DashScopeDocumentRanker(rerankModel);
31+
}
32+
33+
@Bean
34+
public QueryTransformer queryTransformer(
35+
ChatClient.Builder chatClientBuilder,
36+
@Qualifier("transformerPromptTemplate") PromptTemplate transformerPromptTemplate
37+
) {
38+
39+
ChatClient chatClient = chatClientBuilder.defaultOptions(
40+
DashScopeChatOptions.builder()
41+
.withModel("qwen-plus")
42+
.build()
43+
).build();
44+
45+
return RewriteQueryTransformer.builder()
46+
.chatClientBuilder(chatClient.mutate())
47+
.promptTemplate(transformerPromptTemplate)
48+
.targetSearchSystem("联网搜索")
49+
.build();
50+
}
51+
52+
@Bean
53+
public QueryExpander queryExpander(
54+
ChatClient.Builder chatClientBuilder
55+
) {
56+
57+
ChatClient chatClient = chatClientBuilder.defaultOptions(
58+
DashScopeChatOptions.builder()
59+
.withModel("qwen-plus")
60+
.build()
61+
).build();
62+
63+
return MultiQueryExpander.builder()
64+
.chatClientBuilder(chatClient.mutate())
65+
.numberOfQueries(2)
66+
.build();
67+
}
68+
69+
@Bean
70+
public SimpleLoggerAdvisor simpleLoggerAdvisor() {
71+
72+
return new SimpleLoggerAdvisor();
73+
}
74+
75+
}

0 commit comments

Comments
 (0)