Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions samples/Demo/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"NLWebNet": {
"ToolSelectionEnabled": true,
"EnableDetailedLogging": true
},
"Logging": {
"LogLevel": {
"Default": "Information",
"NLWebNet": "Debug",
"NLWebNet.Services.ToolSelector": "Debug",
"NLWebNet.Services.QueryProcessor": "Debug"
}
}
}
3 changes: 3 additions & 0 deletions src/NLWebNet/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static IServiceCollection AddNLWebNet(this IServiceCollection services, A
services.AddScoped<INLWebService, NLWebService>();
services.AddScoped<IQueryProcessor, QueryProcessor>();
services.AddScoped<IResultGenerator, ResultGenerator>();
services.AddScoped<IToolSelector, ToolSelector>();

// Register MCP services
services.AddScoped<IMcpService, McpService>(); // Register default data backend (can be overridden)
Expand Down Expand Up @@ -76,6 +77,7 @@ public static IServiceCollection AddNLWebNet<TDataBackend>(this IServiceCollecti
services.AddScoped<INLWebService, NLWebService>();
services.AddScoped<IQueryProcessor, QueryProcessor>();
services.AddScoped<IResultGenerator, ResultGenerator>();
services.AddScoped<IToolSelector, ToolSelector>();

// Register MCP services
services.AddScoped<IMcpService, McpService>();
Expand Down Expand Up @@ -152,6 +154,7 @@ public static IServiceCollection AddNLWebNetMultiBackend(this IServiceCollection
});

services.AddScoped<IQueryProcessor, QueryProcessor>();
services.AddScoped<IToolSelector, ToolSelector>();
services.AddScoped<IResultGenerator>(provider =>
{
var options = provider.GetRequiredService<IOptions<NLWebOptions>>();
Expand Down
6 changes: 6 additions & 0 deletions src/NLWebNet/Models/NLWebOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,10 @@ public class NLWebOptions
/// Multi-backend configuration options. When enabled, overrides single backend behavior.
/// </summary>
public MultiBackendOptions MultiBackend { get; set; } = new();

/// <summary>
/// Whether to enable tool selection framework for routing queries to appropriate tools.
/// When disabled, maintains existing behavior for backward compatibility.
/// </summary>
public bool ToolSelectionEnabled { get; set; } = false;
}
24 changes: 24 additions & 0 deletions src/NLWebNet/Services/IToolSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using NLWebNet.Models;

namespace NLWebNet.Services;

/// <summary>
/// Interface for selecting appropriate tools based on query intent.
/// </summary>
public interface IToolSelector
{
/// <summary>
/// Selects the appropriate tool based on the query intent and context.
/// </summary>
/// <param name="request">The NLWeb request containing the query and context</param>
/// <param name="cancellationToken">Cancellation token for async operations</param>
/// <returns>The selected tool name, or null if no specific tool is needed</returns>
Task<string?> SelectToolAsync(NLWebRequest request, CancellationToken cancellationToken = default);

/// <summary>
/// Determines if tool selection is needed for the given request.
/// </summary>
/// <param name="request">The NLWeb request to analyze</param>
/// <returns>True if tool selection should be performed, false otherwise</returns>
bool ShouldSelectTool(NLWebRequest request);
}
18 changes: 17 additions & 1 deletion src/NLWebNet/Services/QueryProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NLWebNet.Models;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
Expand All @@ -11,17 +12,32 @@ namespace NLWebNet.Services;
public class QueryProcessor : IQueryProcessor
{
private readonly ILogger<QueryProcessor> _logger;
private readonly IToolSelector? _toolSelector;

public QueryProcessor(ILogger<QueryProcessor> logger)
public QueryProcessor(ILogger<QueryProcessor> logger, IToolSelector? toolSelector = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_toolSelector = toolSelector;
}

/// <inheritdoc />
public async Task<string> ProcessQueryAsync(NLWebRequest request, CancellationToken cancellationToken = default)
{
_logger.LogDebug("Processing query for request {QueryId}", request.QueryId);

// Perform tool selection if available and enabled
if (_toolSelector != null && _toolSelector.ShouldSelectTool(request))
{
var selectedTool = await _toolSelector.SelectToolAsync(request, cancellationToken);
if (!string.IsNullOrEmpty(selectedTool))
{
_logger.LogDebug("Tool selection complete: {Tool} for request {QueryId}", selectedTool, request.QueryId);
// Store the selected tool in the request for downstream processing
// Note: This is a minimal implementation - the tool selection result could be used
// by other components in the pipeline
}
}

// If decontextualized query is already provided, use it
if (!string.IsNullOrEmpty(request.DecontextualizedQuery))
{
Expand Down
107 changes: 107 additions & 0 deletions src/NLWebNet/Services/ToolSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NLWebNet.Models;

namespace NLWebNet.Services;

/// <summary>
/// Implementation of tool selection logic that routes queries to appropriate tools based on intent.
/// </summary>
public class ToolSelector : IToolSelector
{
private readonly ILogger<ToolSelector> _logger;
private readonly NLWebOptions _options;

public ToolSelector(ILogger<ToolSelector> logger, IOptions<NLWebOptions> options)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}

/// <inheritdoc />
public async Task<string?> SelectToolAsync(NLWebRequest request, CancellationToken cancellationToken = default)
{
if (!ShouldSelectTool(request))
{
_logger.LogDebug("Tool selection not needed for request {QueryId}", request.QueryId);
return null;
}

_logger.LogDebug("Selecting tool for query: {Query}", request.Query);

// Simple intent-based tool selection
// In a full implementation, this would use more sophisticated intent analysis
var selectedTool = await AnalyzeQueryIntentAsync(request.Query, cancellationToken);

_logger.LogDebug("Selected tool: {Tool} for query {QueryId}", selectedTool ?? "none", request.QueryId);
return selectedTool;
}

/// <inheritdoc />
public bool ShouldSelectTool(NLWebRequest request)
{
// Don't perform tool selection if:
// 1. Tool selection is disabled in configuration
// 2. Generate mode is used (maintain existing behavior)
// 3. Request already has a decontextualized query (already processed)

if (!_options.ToolSelectionEnabled)
{
return false;
}

if (request.Mode == QueryMode.Generate)
{
_logger.LogDebug("Skipping tool selection for Generate mode to maintain existing behavior");
return false;
}

if (!string.IsNullOrEmpty(request.DecontextualizedQuery))
{
_logger.LogDebug("Skipping tool selection for request with decontextualized query");
return false;
}

return true;
}

/// <summary>
/// Analyzes the query intent to determine the appropriate tool.
/// This is a simplified implementation - production would use more sophisticated NLP.
/// </summary>
private Task<string?> AnalyzeQueryIntentAsync(string query, CancellationToken cancellationToken)
{
var queryLower = query.ToLowerInvariant();

// Basic keyword-based intent detection
// In production, this would use ML models or more sophisticated analysis

if (ContainsKeywords(queryLower, "search", "find", "look for", "locate"))
{
return Task.FromResult<string?>("search");
}

if (ContainsKeywords(queryLower, "compare", "difference", "versus", "vs", "contrast"))
{
return Task.FromResult<string?>("compare");
}

if (ContainsKeywords(queryLower, "details", "information about", "tell me about", "describe"))
{
return Task.FromResult<string?>("details");
}

if (ContainsKeywords(queryLower, "recommend", "suggest", "what should", "ensemble", "set of"))
{
return Task.FromResult<string?>("ensemble");
}

// Default to search tool for general queries
return Task.FromResult<string?>("search");
Copy link

Copilot AI Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider extracting these hard-coded keyword lists and the resulting tool names into named constants or an enum to avoid duplication and reduce typos.

Suggested change
if (ContainsKeywords(queryLower, "search", "find", "look for", "locate"))
{
return Task.FromResult<string?>("search");
}
if (ContainsKeywords(queryLower, "compare", "difference", "versus", "vs", "contrast"))
{
return Task.FromResult<string?>("compare");
}
if (ContainsKeywords(queryLower, "details", "information about", "tell me about", "describe"))
{
return Task.FromResult<string?>("details");
}
if (ContainsKeywords(queryLower, "recommend", "suggest", "what should", "ensemble", "set of"))
{
return Task.FromResult<string?>("ensemble");
}
// Default to search tool for general queries
return Task.FromResult<string?>("search");
if (ContainsKeywords(queryLower, SearchKeywords))
{
return Task.FromResult<string?>(ToolSearch);
}
if (ContainsKeywords(queryLower, CompareKeywords))
{
return Task.FromResult<string?>(ToolCompare);
}
if (ContainsKeywords(queryLower, DetailsKeywords))
{
return Task.FromResult<string?>(ToolDetails);
}
if (ContainsKeywords(queryLower, EnsembleKeywords))
{
return Task.FromResult<string?>(ToolEnsemble);
}
// Default to search tool for general queries
return Task.FromResult<string?>(ToolSearch);

Copilot uses AI. Check for mistakes.
}

private static bool ContainsKeywords(string text, params string[] keywords)
{
return keywords.Any(keyword => text.Contains(keyword));
}
}
76 changes: 76 additions & 0 deletions tests/NLWebNet.Tests/Services/QueryProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,80 @@ public async Task ProcessQueryAsync_ReturnsQuery()
// Assert
Assert.AreEqual("test query", result);
}

[TestMethod]
public async Task ProcessQueryAsync_WithToolSelector_CallsToolSelection()
{
// Arrange
var toolSelectorLogger = new TestLogger<ToolSelector>();
var nlWebOptions = new NLWebOptions { ToolSelectionEnabled = true };
var options = Options.Create(nlWebOptions);
var toolSelector = new ToolSelector(toolSelectorLogger, options);
var queryProcessorWithToolSelector = new QueryProcessor(_logger, toolSelector);

var request = new NLWebRequest
{
Query = "search for something",
Mode = QueryMode.List,
QueryId = "test-query-id"
};

// Act
var result = await queryProcessorWithToolSelector.ProcessQueryAsync(request, CancellationToken.None);

// Assert
Assert.AreEqual("search for something", result);
// The tool selection should have been called but not affect the final result
// since we're not changing the query processing behavior yet
}

[TestMethod]
public async Task ProcessQueryAsync_WithToolSelectorDisabled_DoesNotCallToolSelection()
{
// Arrange
var toolSelectorLogger = new TestLogger<ToolSelector>();
var nlWebOptions = new NLWebOptions { ToolSelectionEnabled = false };
var options = Options.Create(nlWebOptions);
var toolSelector = new ToolSelector(toolSelectorLogger, options);
var queryProcessorWithToolSelector = new QueryProcessor(_logger, toolSelector);

var request = new NLWebRequest
{
Query = "search for something",
Mode = QueryMode.List,
QueryId = "test-query-id"
};

// Act
var result = await queryProcessorWithToolSelector.ProcessQueryAsync(request, CancellationToken.None);

// Assert
Assert.AreEqual("search for something", result);
// Tool selection should not have been called
}

[TestMethod]
public async Task ProcessQueryAsync_WithGenerateMode_SkipsToolSelection()
{
// Arrange
var toolSelectorLogger = new TestLogger<ToolSelector>();
var nlWebOptions = new NLWebOptions { ToolSelectionEnabled = true };
var options = Options.Create(nlWebOptions);
var toolSelector = new ToolSelector(toolSelectorLogger, options);
var queryProcessorWithToolSelector = new QueryProcessor(_logger, toolSelector);

var request = new NLWebRequest
{
Query = "generate something",
Mode = QueryMode.Generate,
QueryId = "test-query-id"
};

// Act
var result = await queryProcessorWithToolSelector.ProcessQueryAsync(request, CancellationToken.None);

// Assert
Assert.AreEqual("generate something", result);
// Tool selection should have been skipped for Generate mode
}
}
Loading
Loading