Skip to content

Commit 3935ff1

Browse files
author
Milder Hernandez Cagua
committed
Add keywords extraction through native function for SK Chaining approach
1 parent 6866ede commit 3935ff1

File tree

6 files changed

+123
-54
lines changed

6 files changed

+123
-54
lines changed

app/backend/src/main/java/com/microsoft/openai/samples/rag/ask/approaches/semantickernel/JavaSemanticKernelChainsApproach.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.microsoft.openai.samples.rag.approaches.RAGApproach;
77
import com.microsoft.openai.samples.rag.approaches.RAGOptions;
88
import com.microsoft.openai.samples.rag.approaches.RAGResponse;
9+
import com.microsoft.openai.samples.rag.retrieval.semantickernel.CognitiveSearchPlugin;
910
import com.microsoft.openai.samples.rag.proxy.CognitiveSearchProxy;
1011
import com.microsoft.openai.samples.rag.proxy.OpenAIProxy;
1112
import com.microsoft.semantickernel.Kernel;
@@ -25,7 +26,7 @@
2526
/**
2627
* Use Java Semantic Kernel framework with semantic and native functions chaining. It uses an
2728
* imperative style for AI orchestration through semantic kernel functions chaining.
28-
* InformationFinder.Search native function and RAG.AnswerQuestion semantic function are called
29+
* InformationFinder.SearchFromQuestion native function and RAG.AnswerQuestion semantic function are called
2930
* sequentially. Several cognitive search retrieval options are available: Text, Vector, Hybrid.
3031
*/
3132
@Component
@@ -74,7 +75,7 @@ public RAGResponse run(String question, RAGOptions options) {
7475
question,
7576
semanticKernel
7677
.getSkill("InformationFinder")
77-
.getFunction("Search", null))
78+
.getFunction("SearchFromQuestion", null))
7879
.block();
7980

8081
var sources = formSourcesList(searchContext.getResult());
@@ -135,9 +136,9 @@ private List<ContentSource> formSourcesList(String result) {
135136

136137
/**
137138
* Build semantic kernel context with AnswerQuestion semantic function and
138-
* InformationFinder.Search native function. AnswerQuestion is imported from
139-
* src/main/resources/semantickernel/Plugins. InformationFinder.Search is implemented in a
140-
* traditional Java class method: CognitiveSearchPlugin.search
139+
* InformationFinder.SearchFromQuestion native function. AnswerQuestion is imported from
140+
* src/main/resources/semantickernel/Plugins. InformationFinder.SearchFromQuestion is implemented in a
141+
* traditional Java class method: CognitiveSearchPlugin.searchFromConversation
141142
*
142143
* @param options
143144
* @return
@@ -155,7 +156,6 @@ private Kernel buildSemanticKernel(RAGOptions options) {
155156
kernel.importSkill(
156157
new CognitiveSearchPlugin(this.cognitiveSearchProxy, this.openAIProxy, options),
157158
"InformationFinder");
158-
159159
kernel.importSkillFromResources("semantickernel/Plugins", "RAG", "AnswerQuestion", null);
160160

161161
return kernel;

app/backend/src/main/java/com/microsoft/openai/samples/rag/ask/approaches/semantickernel/JavaSemanticKernelPlannerApproach.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.microsoft.openai.samples.rag.approaches.RAGApproach;
66
import com.microsoft.openai.samples.rag.approaches.RAGOptions;
77
import com.microsoft.openai.samples.rag.approaches.RAGResponse;
8+
import com.microsoft.openai.samples.rag.retrieval.semantickernel.CognitiveSearchPlugin;
89
import com.microsoft.openai.samples.rag.proxy.CognitiveSearchProxy;
910
import com.microsoft.openai.samples.rag.proxy.OpenAIProxy;
1011
import com.microsoft.semantickernel.Kernel;
@@ -97,8 +98,8 @@ public void runStreaming(
9798
/**
9899
* Build semantic kernel context with AnswerQuestion semantic function and
99100
* InformationFinder.Search native function. AnswerQuestion is imported from
100-
* src/main/resources/semantickernel/Plugins. InformationFinder.Search is implemented in a
101-
* traditional Java class method: CognitiveSearchPlugin.search
101+
* src/main/resources/semantickernel/Plugins. InformationFinder.SearchFromQuestion is implemented in a
102+
* traditional Java class method: CognitiveSearchPlugin.searchFromQuestion
102103
*
103104
* @param options
104105
* @return

app/backend/src/main/java/com/microsoft/openai/samples/rag/chat/approaches/semantickernel/JavaSemanticKernelChainsChatApproach.java

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@
55
import com.microsoft.openai.samples.rag.approaches.RAGApproach;
66
import com.microsoft.openai.samples.rag.approaches.RAGOptions;
77
import com.microsoft.openai.samples.rag.approaches.RAGResponse;
8-
import com.microsoft.openai.samples.rag.ask.approaches.semantickernel.CognitiveSearchPlugin;
8+
import com.microsoft.openai.samples.rag.retrieval.semantickernel.CognitiveSearchPlugin;
99
import com.microsoft.openai.samples.rag.common.ChatGPTConversation;
1010
import com.microsoft.openai.samples.rag.common.ChatGPTUtils;
1111
import com.microsoft.openai.samples.rag.proxy.CognitiveSearchProxy;
1212
import com.microsoft.openai.samples.rag.proxy.OpenAIProxy;
1313
import com.microsoft.semantickernel.Kernel;
1414
import com.microsoft.semantickernel.SKBuilders;
15-
import com.microsoft.semantickernel.chatcompletion.ChatCompletion;
15+
import com.microsoft.semantickernel.orchestration.ContextVariables;
1616
import com.microsoft.semantickernel.orchestration.SKContext;
17-
import org.slf4j.Logger;
18-
import org.slf4j.LoggerFactory;
1917
import org.springframework.beans.factory.annotation.Value;
2018
import org.springframework.stereotype.Component;
2119

@@ -27,18 +25,13 @@
2725
import java.util.stream.Collectors;
2826

2927
/**
30-
* Simple chat-read-retrieve-read java implementation, using the Cognitive Search and OpenAI APIs directly.
31-
* It uses the ChatGPT API to turn the user question into a good search query.
32-
* It queries Azure Cognitive Search for search results for that query (optionally using the vector embeddings for that query).
33-
* It then combines the search results and original user question, and asks ChatGPT API to answer the question based on the sources. It includes the last 4K of message history as well (or however many tokens are allowed by the deployed model).
28+
* Use Java Semantic Kernel framework with semantic and native functions chaining. It uses an
29+
* imperative style for AI orchestration through semantic kernel functions chaining.
30+
* InformationFinder.SearchFromConversation native function and RAG.AnswerConversation semantic function are called
31+
* sequentially. Several cognitive search retrieval options are available: Text, Vector, Hybrid.
3432
*/
3533
@Component
3634
public class JavaSemanticKernelChainsChatApproach implements RAGApproach<ChatGPTConversation, RAGResponse> {
37-
38-
private static final Logger LOGGER = LoggerFactory.getLogger(JavaSemanticKernelChainsChatApproach.class);
39-
private static final String PLAN_PROMPT = """
40-
Take the input as a question and answer it finding any information needed
41-
""";
4235
private final CognitiveSearchProxy cognitiveSearchProxy;
4336

4437
private final OpenAIProxy openAIProxy;
@@ -61,30 +54,38 @@ public JavaSemanticKernelChainsChatApproach(CognitiveSearchProxy cognitiveSearch
6154
*/
6255
@Override
6356
public RAGResponse run(ChatGPTConversation questionOrConversation, RAGOptions options) {
64-
6557
String question = ChatGPTUtils.getLastUserQuestion(questionOrConversation.getMessages());
58+
String conversation = ChatGPTUtils.formatAsChatML(questionOrConversation.toOpenAIChatMessages());
6659

6760
Kernel semanticKernel = buildSemanticKernel(options);
6861

62+
// STEP 1: Retrieve relevant documents using the current conversation. It reuses the
63+
// CognitiveSearchRetriever appraoch through the CognitiveSearchPlugin native function.
6964
SKContext searchContext =
7065
semanticKernel.runAsync(
71-
question,
72-
semanticKernel.getSkill("InformationFinder").getFunction("Search", null)).block();
73-
74-
var sources = formSourcesList(searchContext.getResult());
66+
conversation,
67+
semanticKernel.getSkill("InformationFinder").getFunction("SearchFromConversation", null)).block();
7568

76-
var answerVariables = SKBuilders.variables()
69+
// STEP 2: Build a SK context with the sources retrieved from the memory store and conversation
70+
ContextVariables variables = SKBuilders.variables()
7771
.withVariable("sources", searchContext.getResult())
78-
.withVariable("input", question)
72+
.withVariable("conversation", conversation)
73+
.withVariable("suggestions", String.valueOf(options.isSuggestFollowupQuestions()))
74+
.withVariable("input", question)
7975
.build();
8076

81-
SKContext answerExecutionContext =
82-
semanticKernel.runAsync(answerVariables,
83-
semanticKernel.getSkill("RAG").getFunction("AnswerQuestion", null)).block();
77+
/**
78+
* STEP 3: Get a reference of the semantic function [AnswerConversation] of the [RAG] plugin
79+
* (a.k.a. skill) from the SK skills registry and provide it with the pre-built context.
80+
* Triggering Open AI to get a reply.
81+
*/
82+
SKContext reply = semanticKernel.runAsync(variables,
83+
semanticKernel.getSkill("RAG").getFunction("AnswerConversation", null)).block();
84+
8485
return new RAGResponse.Builder()
8586
.prompt("Prompt is managed by Semantic Kernel")
86-
.answer(answerExecutionContext.getResult())
87-
.sources(sources)
87+
.answer(reply.getResult())
88+
.sources(formSourcesList(searchContext.getResult()))
8889
.sourcesAsText(searchContext.getResult())
8990
.question(question)
9091
.build();
@@ -118,6 +119,15 @@ private List<ContentSource> formSourcesList(String result) {
118119
.collect(Collectors.toList());
119120
}
120121

122+
/**
123+
* Build semantic kernel context with AnswerConversation semantic function and
124+
* InformationFinder.SearchFromConversation native function. AnswerConversation is imported from
125+
* src/main/resources/semantickernel/Plugins. InformationFinder.SearchFromConversation is implemented in a
126+
* traditional Java class method: CognitiveSearchPlugin.searchFromConversation
127+
*
128+
* @param options
129+
* @return
130+
*/
121131
private Kernel buildSemanticKernel(RAGOptions options) {
122132
Kernel kernel = SKBuilders.kernel()
123133
.withDefaultAIService(SKBuilders.chatCompletion()
@@ -126,14 +136,10 @@ private Kernel buildSemanticKernel(RAGOptions options) {
126136
.build())
127137
.build();
128138

129-
kernel.importSkill(new CognitiveSearchPlugin(this.cognitiveSearchProxy, this.openAIProxy, options), "InformationFinder");
130-
131-
kernel.importSkillFromResources(
132-
"semantickernel/Plugins",
133-
"RAG",
134-
"AnswerQuestion",
135-
null
136-
);
139+
kernel.importSkill(
140+
new CognitiveSearchPlugin(this.cognitiveSearchProxy, this.openAIProxy, options),
141+
"InformationFinder");
142+
kernel.importSkillFromResources("semantickernel/Plugins", "RAG", "AnswerConversation", null);
137143

138144
return kernel;
139145
}

app/backend/src/main/java/com/microsoft/openai/samples/rag/chat/approaches/semantickernel/JavaSemanticKernelWithMemoryChatApproach.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
/**
3636
* Accomplish the same task as in the PlainJavaAskApproach approach but using Semantic Kernel framework:
3737
* 1. Memory abstraction is used for vector search capability. It uses Azure Cognitive Search as memory store.
38-
* 2. Semantic function has been defined to ask question using sources from memory search results
38+
* 2. Semantic functions have been defined to ask question using sources from memory search results
3939
*/
4040
@Component
4141
public class JavaSemanticKernelWithMemoryChatApproach implements RAGApproach<ChatGPTConversation, RAGResponse> {
@@ -89,11 +89,11 @@ public RAGResponse run(ChatGPTConversation questionOrConversation, RAGOptions op
8989
.setVariable("suggestions", String.valueOf(options.isSuggestFollowupQuestions()))
9090
.setVariable("input", question);
9191

92-
Mono<SKContext> reply = answerConversation.invokeAsync(skcontext);
92+
SKContext reply = (SKContext) answerConversation.invokeAsync(skcontext).block();
9393

9494
return new RAGResponse.Builder()
95-
.prompt("placeholders for prompt")
96-
.answer(reply.block().getResult())
95+
.prompt("Prompt is managed by Semantic Kernel")
96+
.answer(reply.getResult())
9797
.sources(sourcesList)
9898
.sourcesAsText(sources)
9999
.question(question)

app/backend/src/main/java/com/microsoft/openai/samples/rag/common/ChatGPTUtils.java

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.azure.ai.openai.models.ChatMessage;
55
import com.azure.ai.openai.models.ChatRole;
66

7+
import java.util.ArrayList;
78
import java.util.HashMap;
89
import java.util.List;
910

@@ -26,21 +27,55 @@ public static ChatCompletionsOptions buildDefaultChatCompletionsOptions(List<Cha
2627
return completionsOptions;
2728
}
2829

30+
private static final String IM_START_USER = "<|im_start|>user";
31+
private static final String IM_START_ASSISTANT = "<|im_start|>assistant";
32+
private static final String IM_START_SYSTEM = "<|im_start|>system";
33+
2934
public static String formatAsChatML(List<ChatMessage> messages) {
3035
StringBuilder sb = new StringBuilder();
3136
messages.forEach(message -> {
3237
if (message.getRole() == ChatRole.USER) {
33-
sb.append("<|im_start|>user\n");
38+
sb.append(IM_START_USER).append("\n");
3439
} else if (message.getRole() == ChatRole.ASSISTANT) {
35-
sb.append("<|im_start|>assistant\n");
40+
sb.append(IM_START_ASSISTANT).append("\n");
3641
} else {
37-
sb.append("<|im_start|>system\n");
42+
sb.append(IM_START_SYSTEM).append("\n");
3843
}
3944
sb.append(message.getContent()).append("\n").append("|im_end|").append("\n");
4045
});
4146
return sb.toString();
4247
}
4348

49+
public static List<ChatMessage> parseChatML(String chatML) {
50+
List<ChatMessage> messages = new ArrayList<>();
51+
String[] messageTokens = chatML.split("\\|im_end\\|\\n");
52+
53+
for (String messageToken : messageTokens) {
54+
String[] lines = messageToken.trim().split("\n");
55+
56+
if (lines.length >= 2) {
57+
ChatRole role = ChatRole.SYSTEM;
58+
if (IM_START_USER.equals(lines[0])) {
59+
role = ChatRole.USER;
60+
} else if (IM_START_ASSISTANT.equals(lines[0])) {
61+
role = ChatRole.ASSISTANT;
62+
}
63+
64+
StringBuilder content = new StringBuilder();
65+
for (int i = 1; i < lines.length; ++i) {
66+
content.append(lines[i]);
67+
if (i < lines.length - 1) {
68+
content.append("\n");
69+
}
70+
}
71+
72+
messages.add(new ChatMessage(role).setContent(content.toString()));
73+
}
74+
}
75+
76+
return messages;
77+
}
78+
4479
public static String getLastUserQuestion(List<ChatGPTMessage> messages) {
4580
List<ChatGPTMessage> userMessages = messages
4681
.stream()
Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright (c) Microsoft. All rights reserved.
2-
package com.microsoft.openai.samples.rag.ask.approaches.semantickernel;
2+
package com.microsoft.openai.samples.rag.retrieval.semantickernel;
33

4-
import com.azure.search.documents.models.*;
54
import com.microsoft.openai.samples.rag.approaches.ContentSource;
65
import com.microsoft.openai.samples.rag.approaches.RAGOptions;
6+
import com.microsoft.openai.samples.rag.common.ChatGPTConversation;
7+
import com.microsoft.openai.samples.rag.common.ChatGPTMessage;
8+
import com.microsoft.openai.samples.rag.common.ChatGPTUtils;
79
import com.microsoft.openai.samples.rag.proxy.CognitiveSearchProxy;
810
import com.microsoft.openai.samples.rag.proxy.OpenAIProxy;
911
import com.microsoft.openai.samples.rag.retrieval.CognitiveSearchRetriever;
@@ -31,9 +33,9 @@ public CognitiveSearchPlugin(
3133
}
3234

3335
@DefineSKFunction(
34-
name = "Search",
36+
name = "SearchFromQuestion",
3537
description = "Search information relevant to answering a given query")
36-
public Mono<String> search(
38+
public Mono<String> searchFromQuestion(
3739
@SKFunctionInputAttribute(description = "the query to answer") String query) {
3840

3941
CognitiveSearchRetriever retriever =
@@ -45,8 +47,33 @@ public Mono<String> search(
4547
sources.size(),
4648
query);
4749

50+
return Mono.just(buildSources(sources));
51+
}
52+
53+
@DefineSKFunction(
54+
name = "SearchFromConversation",
55+
description = "Search information relevant to a conversation")
56+
public Mono<String> searchFromConversation(
57+
@SKFunctionInputAttribute(description = "the conversation to search the information from") String conversation) {
58+
// Parse conversation
59+
List<ChatGPTMessage> chatMessages = ChatGPTUtils.parseChatML(conversation).stream().map(message ->
60+
new ChatGPTMessage(ChatGPTMessage.ChatRole.fromString(message.getRole().toString()), message.getContent())
61+
).toList();
62+
63+
CognitiveSearchRetriever retriever =
64+
new CognitiveSearchRetriever(this.cognitiveSearchProxy, this.openAIProxy);
65+
List<ContentSource> sources = retriever.retrieveFromConversation(new ChatGPTConversation(chatMessages), this.options);
66+
67+
LOGGER.info(
68+
"Total {} sources found in cognitive search",
69+
sources.size());
70+
71+
return Mono.just(buildSources(sources));
72+
}
73+
74+
private String buildSources (List<ContentSource> sources) {
4875
StringBuilder sourcesStringBuilder = new StringBuilder();
49-
// Build sources section
76+
5077
sources.iterator()
5178
.forEachRemaining(
5279
source ->
@@ -55,6 +82,6 @@ public Mono<String> search(
5582
.append(": ")
5683
.append(source.getSourceContent().replace("\n", ""))
5784
.append("\n"));
58-
return Mono.just(sourcesStringBuilder.toString());
85+
return sourcesStringBuilder.toString();
5986
}
6087
}

0 commit comments

Comments
 (0)