Skip to content

Commit fca54fb

Browse files
committed
feat(demo): add semantic router with response classifier and UX improvements
Adds semantic router integration and several UX improvements to the RAG demo: - SemanticRouterPanel: New panel for configuring semantic routing with YAML/JSON config loading, enable toggle, sample routes, and testing functionality - Response classifier: Filters out source references when LLM response indicates no relevant information found in context (uses SemanticRouter with permissive threshold for semantic matching) - Settings panel improvements: Document section moved to top for easier PDF upload access, panel now opens by default on startup - VerticalTabPane: Fixed collapsed state initialization so selectTab() properly expands the panel - Updated tests to accommodate RAGService constructor changes
1 parent 48c006e commit fca54fb

File tree

10 files changed

+907
-43
lines changed

10 files changed

+907
-43
lines changed

demos/rag-multimodal/src/main/java/com/redis/vl/demo/rag/service/RAGService.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.redis.vl.extensions.cache.CacheHit;
88
import com.redis.vl.extensions.cache.LangCacheSemanticCache;
99
import com.redis.vl.extensions.cache.SemanticCache;
10+
import com.redis.vl.extensions.router.SemanticRouter;
1011
import com.redis.vl.langchain4j.RedisVLContentRetriever;
1112
import com.redis.vl.langchain4j.RedisVLDocumentStore;
1213
import dev.langchain4j.data.image.Image;
@@ -48,6 +49,7 @@ public class RAGService {
4849
private final LLMConfig config;
4950
private final SemanticCache localCache;
5051
private final LangCacheSemanticCache langCache;
52+
private final SemanticRouter responseClassifier;
5153

5254
private static final String SYSTEM_PROMPT =
5355
"""
@@ -68,6 +70,7 @@ public class RAGService {
6870
* @param config LLM configuration
6971
* @param localCache Local Redis-based semantic cache
7072
* @param langCache LangCache cloud service for semantic caching (can be null)
73+
* @param responseClassifier Semantic router for classifying responses (can be null)
7174
*/
7275
public RAGService(
7376
RedisVLContentRetriever contentRetriever,
@@ -76,14 +79,16 @@ public RAGService(
7679
CostTracker costTracker,
7780
LLMConfig config,
7881
SemanticCache localCache,
79-
LangCacheSemanticCache langCache) {
82+
LangCacheSemanticCache langCache,
83+
SemanticRouter responseClassifier) {
8084
this.contentRetriever = contentRetriever;
8185
this.documentStore = documentStore;
8286
this.chatModel = chatModel;
8387
this.costTracker = costTracker;
8488
this.config = config;
8589
this.localCache = localCache;
8690
this.langCache = langCache;
91+
this.responseClassifier = responseClassifier;
8792
}
8893

8994
/**
@@ -237,7 +242,21 @@ public ChatMessage query(String userQuery, CacheType cacheType) {
237242
Response<AiMessage> response = chatModel.generate(messages);
238243
String responseText = response.content().text();
239244

240-
// 4. Store in cache based on cache type
245+
// 5. Clear references if response indicates no relevant information
246+
if (responseClassifier != null) {
247+
System.out.println("→ Classifying response: " + responseText.substring(0, Math.min(100, responseText.length())) + "...");
248+
var routeMatch = responseClassifier.route(responseText);
249+
System.out.println("→ Route match result: " + (routeMatch != null ? "name=" + routeMatch.getName() + ", distance=" + routeMatch.getDistance() : "null"));
250+
if (routeMatch != null && routeMatch.getName() != null) {
251+
references = List.of();
252+
System.out.println("→ Response classified as '" + routeMatch.getName()
253+
+ "' (distance: " + routeMatch.getDistance() + "), clearing references");
254+
}
255+
} else {
256+
System.out.println("→ Response classifier is NULL - not classifying response");
257+
}
258+
259+
// 6. Store in cache based on cache type
241260
if (cacheType == CacheType.LOCAL && localCache != null) {
242261
System.out.println("→ Storing to Local Cache: " + userQuery);
243262
localCache.store(userQuery, responseText);
@@ -275,4 +294,5 @@ private String buildContext(List<Content> content) {
275294
.map(c -> c.textSegment().text())
276295
.collect(Collectors.joining("\n\n"));
277296
}
297+
278298
}

demos/rag-multimodal/src/main/java/com/redis/vl/demo/rag/service/ServiceFactory.java

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.redis.vl.demo.rag.model.LLMConfig;
55
import com.redis.vl.extensions.cache.LangCacheSemanticCache;
66
import com.redis.vl.extensions.cache.SemanticCache;
7+
import com.redis.vl.extensions.router.Route;
8+
import com.redis.vl.extensions.router.SemanticRouter;
79
import com.redis.vl.index.SearchIndex;
810
import com.redis.vl.langchain4j.RedisVLContentRetriever;
911
import com.redis.vl.langchain4j.RedisVLDocumentStore;
@@ -157,7 +159,10 @@ public RAGService createRAGService(LLMConfig config) {
157159
// Create chat model based on provider
158160
ChatLanguageModel chatModel = createChatModel(config);
159161

160-
return new RAGService(retriever, documentStore, chatModel, costTracker, config, localCache, langCache);
162+
// Create response classifier to detect "no relevant info" responses
163+
SemanticRouter responseClassifier = createResponseClassifier();
164+
165+
return new RAGService(retriever, documentStore, chatModel, costTracker, config, localCache, langCache, responseClassifier);
161166
}
162167

163168
/**
@@ -233,6 +238,80 @@ public CostTracker getCostTracker() {
233238
return costTracker;
234239
}
235240

241+
/**
242+
* Creates a SemanticRouter with the given routes.
243+
*
244+
* @param routes List of routes to configure
245+
* @return SemanticRouter instance
246+
* @throws IllegalStateException if services not initialized
247+
*/
248+
public SemanticRouter createSemanticRouter(List<Route> routes) {
249+
return createSemanticRouter("rag_demo_router", routes);
250+
}
251+
252+
/**
253+
* Creates a SemanticRouter with a custom name and routes.
254+
*
255+
* @param name Name for the router index
256+
* @param routes List of routes to configure
257+
* @return SemanticRouter instance
258+
* @throws IllegalStateException if services not initialized
259+
*/
260+
public SemanticRouter createSemanticRouter(String name, List<Route> routes) {
261+
if (jedis == null || embeddingModel == null) {
262+
throw new IllegalStateException("Services not initialized. Call initialize() first.");
263+
}
264+
265+
LangChain4JVectorizer vectorizer = new LangChain4JVectorizer(
266+
"all-minilm-l6-v2", embeddingModel, VECTOR_DIM);
267+
268+
return SemanticRouter.builder()
269+
.name(name)
270+
.routes(routes)
271+
.vectorizer(vectorizer)
272+
.jedis(jedis)
273+
.overwrite(true) // Recreate index when routes change
274+
.build();
275+
}
276+
277+
/**
278+
* Creates a response classifier router for detecting "no relevant information" responses.
279+
* This router matches LLM responses that indicate the context didn't contain
280+
* relevant information to answer the user's question.
281+
*
282+
* @return SemanticRouter configured for response classification
283+
* @throws IllegalStateException if services not initialized
284+
*/
285+
public SemanticRouter createResponseClassifier() {
286+
// Reference phrases that indicate "no relevant information found"
287+
// These should match typical LLM responses when context doesn't have the answer
288+
List<String> noInfoReferences = List.of(
289+
"The context provided does not contain information",
290+
"The provided context does not contain information about this topic",
291+
"I cannot find relevant information in the given context",
292+
"The context doesn't mention anything about this",
293+
"Based on the provided context, I don't have information on this",
294+
"This topic is not covered in the available context",
295+
"I don't have enough information in the context to answer",
296+
"The documents provided do not address this question",
297+
"There is no relevant information available to answer this",
298+
"I cannot determine this from the given context",
299+
"The context does not include details about this subject",
300+
"does not contain information about",
301+
"I'm unable to find information about this in the context"
302+
);
303+
304+
Route noInfoRoute = Route.builder()
305+
.name("no-relevant-info")
306+
.references(noInfoReferences)
307+
.distanceThreshold(1.5) // Very permissive - cosine distance range is 0-2
308+
.build();
309+
310+
SemanticRouter classifier = createSemanticRouter("response_classifier", List.of(noInfoRoute));
311+
System.out.println("Response classifier initialized with " + noInfoReferences.size() + " reference phrases, threshold: 1.5");
312+
return classifier;
313+
}
314+
236315
/**
237316
* Closes all resources.
238317
*/

demos/rag-multimodal/src/main/java/com/redis/vl/demo/rag/ui/ChatController.java

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
import com.redis.vl.demo.rag.model.LogEntry.Category;
88
import com.redis.vl.demo.rag.service.EventLogger;
99
import com.redis.vl.demo.rag.service.RAGService;
10+
import com.redis.vl.extensions.router.Route;
11+
import com.redis.vl.extensions.router.RouteMatch;
12+
import com.redis.vl.extensions.router.SemanticRouter;
1013
import java.io.File;
14+
import java.util.List;
1115
import java.util.concurrent.ExecutorService;
1216
import java.util.concurrent.Executors;
1317
import javafx.application.Platform;
@@ -27,7 +31,8 @@
2731
public class ChatController extends BorderPane {
2832

2933
private static final int SETTINGS_TAB = 0;
30-
private static final int LOG_TAB = 1;
34+
private static final int ROUTER_TAB = 1;
35+
private static final int LOG_TAB = 2;
3136

3237
private final ObservableList<ChatMessage> messages = FXCollections.observableArrayList();
3338
private final ListView<ChatMessage> messageListView;
@@ -37,12 +42,14 @@ public class ChatController extends BorderPane {
3742
private final VerticalTabPane tabPane;
3843
private final SettingsPanel settingsPanel;
3944
private final LogPanel logPanel;
45+
private final SemanticRouterPanel routerPanel;
4046
private final EventLogger eventLogger;
4147
private File currentPdfFile;
4248

4349
private RAGService ragService;
4450
private com.redis.vl.demo.rag.service.ServiceFactory serviceFactory;
4551
private com.redis.vl.demo.rag.service.PDFIngestionService pdfIngestionService;
52+
private SemanticRouter semanticRouter;
4653
private final ExecutorService executor = Executors.newSingleThreadExecutor();
4754
private final AppConfig config = AppConfig.getInstance();
4855

@@ -65,10 +72,19 @@ public ChatController() {
6572
logPanel = new LogPanel();
6673
logPanel.setEventLogger(eventLogger);
6774

75+
// Semantic router panel content
76+
routerPanel = new SemanticRouterPanel();
77+
routerPanel.setOnEnabledChange(this::onRouterEnabledChange);
78+
6879
// Vertical tab pane (right side)
80+
// Icons: Settings=gear, Router=fork, Log=scroll/document
6981
tabPane = new VerticalTabPane();
70-
tabPane.addTab("⚙", "Settings", settingsPanel);
71-
tabPane.addTab("📋", "Event Log", logPanel);
82+
tabPane.addTab("\u2699", "Settings", settingsPanel); // Tab 0 - Settings (top) - gear
83+
tabPane.addTab("\u2443", "Semantic Router", routerPanel); // Tab 1 - Router (top) - fork symbol
84+
tabPane.addTab("\u2261", "Event Log", logPanel, true); // Tab 2 - Log (bottom) - identical sign (≡)
85+
86+
// Start with settings panel open (for PDF upload)
87+
tabPane.selectTab(SETTINGS_TAB);
7288

7389
// Update log badge when count changes
7490
logPanel.setCountListener(count -> {
@@ -169,6 +185,31 @@ private void sendMessage() {
169185
executor.submit(
170186
() -> {
171187
try {
188+
// Check semantic router BEFORE RAG if enabled
189+
if (routerPanel.isRouterEnabled() && semanticRouter != null) {
190+
RouteMatch match = semanticRouter.route(userInput);
191+
if (match.getName() != null) {
192+
// Query blocked by semantic router
193+
String routeName = match.getName();
194+
double distance = match.getDistance() != null ? match.getDistance() : 0.0;
195+
Platform.runLater(() -> {
196+
long elapsed = System.currentTimeMillis() - startTime;
197+
String blockMessage = String.format(
198+
"This query was blocked by the semantic router.\n\n" +
199+
"Matched route: %s\nDistance: %.4f\n\n" +
200+
"The query appears to be off-topic or outside the scope of the document.",
201+
routeName, distance);
202+
addSystemMessage(blockMessage);
203+
eventLogger.warn(Category.RETRIEVAL, "Query BLOCKED by router: " + routeName + " (distance: " + String.format("%.4f", distance) + ")");
204+
setInputEnabled(true);
205+
settingsPanel.setStatus("Ready");
206+
inputField.requestFocus();
207+
});
208+
return; // Don't proceed to RAG
209+
}
210+
eventLogger.debug(Category.RETRIEVAL, "Router check passed - no route match");
211+
}
212+
172213
ChatMessage response = ragService.query(userInput, cacheType);
173214
long elapsed = System.currentTimeMillis() - startTime;
174215

@@ -355,6 +396,70 @@ public PDFViewerPanel getPdfViewer() {
355396
return pdfViewer;
356397
}
357398

399+
/**
400+
* Gets the semantic router panel for external configuration.
401+
*
402+
* @return SemanticRouterPanel instance
403+
*/
404+
public SemanticRouterPanel getRouterPanel() {
405+
return routerPanel;
406+
}
407+
408+
/**
409+
* Handles router enabled/disabled toggle changes.
410+
*
411+
* @param enabled Whether routing is now enabled
412+
*/
413+
private void onRouterEnabledChange(boolean enabled) {
414+
if (enabled) {
415+
// Initialize the semantic router with sample routes
416+
if (semanticRouter == null && serviceFactory != null) {
417+
eventLogger.info(Category.RETRIEVAL, "Initializing semantic router...");
418+
routerPanel.setStatus("Initializing router...");
419+
420+
executor.submit(() -> {
421+
try {
422+
// Convert sample routes to Route objects
423+
List<Route> routes = routerPanel.getSampleRoutes().stream()
424+
.map(sample -> Route.builder()
425+
.name(sample.name())
426+
.references(sample.references())
427+
.distanceThreshold(sample.threshold())
428+
.build())
429+
.toList();
430+
431+
semanticRouter = serviceFactory.createSemanticRouter(routes);
432+
routerPanel.setRouter(semanticRouter);
433+
434+
Platform.runLater(() -> {
435+
eventLogger.info(Category.RETRIEVAL, "Semantic router initialized with " + routes.size() + " routes");
436+
routerPanel.setStatus("Router ready - " + routes.size() + " routes");
437+
addSystemMessage("Semantic router enabled with " + routes.size() + " routes:\n• " +
438+
String.join("\n• ", routes.stream().map(Route::getName).toList()));
439+
});
440+
} catch (Exception e) {
441+
Platform.runLater(() -> {
442+
eventLogger.error(Category.RETRIEVAL, "Router initialization failed: " + e.getMessage());
443+
routerPanel.setStatus("Router initialization failed");
444+
routerPanel.setRouterEnabled(false); // Disable toggle on failure
445+
addSystemMessage("Failed to initialize semantic router: " + e.getMessage());
446+
});
447+
}
448+
});
449+
} else if (semanticRouter != null) {
450+
eventLogger.info(Category.RETRIEVAL, "Semantic router enabled");
451+
addSystemMessage("Semantic router enabled. Off-topic queries will be blocked.");
452+
} else {
453+
eventLogger.warn(Category.RETRIEVAL, "Cannot enable router - services not initialized");
454+
routerPanel.setStatus("Connect to Redis first");
455+
routerPanel.setRouterEnabled(false);
456+
}
457+
} else {
458+
eventLogger.info(Category.RETRIEVAL, "Semantic router disabled");
459+
addSystemMessage("Semantic router disabled. All queries will be processed.");
460+
}
461+
}
462+
358463
/**
359464
* Truncates a string to the specified length.
360465
*

0 commit comments

Comments
 (0)