Skip to content

Commit 5431ef7

Browse files
Copilotjongalloway
andcommitted
Implement Tool Selection Framework with configuration and tests
Co-authored-by: jongalloway <[email protected]>
1 parent 018f493 commit 5431ef7

File tree

7 files changed

+432
-1
lines changed

7 files changed

+432
-1
lines changed

src/NLWebNet/Extensions/ServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public static IServiceCollection AddNLWebNet(this IServiceCollection services, A
3636
services.AddScoped<INLWebService, NLWebService>();
3737
services.AddScoped<IQueryProcessor, QueryProcessor>();
3838
services.AddScoped<IResultGenerator, ResultGenerator>();
39+
services.AddScoped<IToolSelector, ToolSelector>();
3940

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

8082
// Register MCP services
8183
services.AddScoped<IMcpService, McpService>();
@@ -152,6 +154,7 @@ public static IServiceCollection AddNLWebNetMultiBackend(this IServiceCollection
152154
});
153155

154156
services.AddScoped<IQueryProcessor, QueryProcessor>();
157+
services.AddScoped<IToolSelector, ToolSelector>();
155158
services.AddScoped<IResultGenerator>(provider =>
156159
{
157160
var options = provider.GetRequiredService<IOptions<NLWebOptions>>();

src/NLWebNet/Models/NLWebOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,10 @@ public class NLWebOptions
7676
/// Multi-backend configuration options. When enabled, overrides single backend behavior.
7777
/// </summary>
7878
public MultiBackendOptions MultiBackend { get; set; } = new();
79+
80+
/// <summary>
81+
/// Whether to enable tool selection framework for routing queries to appropriate tools.
82+
/// When disabled, maintains existing behavior for backward compatibility.
83+
/// </summary>
84+
public bool ToolSelectionEnabled { get; set; } = false;
7985
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using NLWebNet.Models;
2+
3+
namespace NLWebNet.Services;
4+
5+
/// <summary>
6+
/// Interface for selecting appropriate tools based on query intent.
7+
/// </summary>
8+
public interface IToolSelector
9+
{
10+
/// <summary>
11+
/// Selects the appropriate tool based on the query intent and context.
12+
/// </summary>
13+
/// <param name="request">The NLWeb request containing the query and context</param>
14+
/// <param name="cancellationToken">Cancellation token for async operations</param>
15+
/// <returns>The selected tool name, or null if no specific tool is needed</returns>
16+
Task<string?> SelectToolAsync(NLWebRequest request, CancellationToken cancellationToken = default);
17+
18+
/// <summary>
19+
/// Determines if tool selection is needed for the given request.
20+
/// </summary>
21+
/// <param name="request">The NLWeb request to analyze</param>
22+
/// <returns>True if tool selection should be performed, false otherwise</returns>
23+
bool ShouldSelectTool(NLWebRequest request);
24+
}

src/NLWebNet/Services/QueryProcessor.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.Options;
23
using NLWebNet.Models;
34
using System.ComponentModel.DataAnnotations;
45
using System.Text.Json;
@@ -11,17 +12,32 @@ namespace NLWebNet.Services;
1112
public class QueryProcessor : IQueryProcessor
1213
{
1314
private readonly ILogger<QueryProcessor> _logger;
15+
private readonly IToolSelector? _toolSelector;
1416

15-
public QueryProcessor(ILogger<QueryProcessor> logger)
17+
public QueryProcessor(ILogger<QueryProcessor> logger, IToolSelector? toolSelector = null)
1618
{
1719
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
20+
_toolSelector = toolSelector;
1821
}
1922

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

28+
// Perform tool selection if available and enabled
29+
if (_toolSelector != null && _toolSelector.ShouldSelectTool(request))
30+
{
31+
var selectedTool = await _toolSelector.SelectToolAsync(request, cancellationToken);
32+
if (!string.IsNullOrEmpty(selectedTool))
33+
{
34+
_logger.LogDebug("Tool selection complete: {Tool} for request {QueryId}", selectedTool, request.QueryId);
35+
// Store the selected tool in the request for downstream processing
36+
// Note: This is a minimal implementation - the tool selection result could be used
37+
// by other components in the pipeline
38+
}
39+
}
40+
2541
// If decontextualized query is already provided, use it
2642
if (!string.IsNullOrEmpty(request.DecontextualizedQuery))
2743
{
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.Options;
3+
using NLWebNet.Models;
4+
5+
namespace NLWebNet.Services;
6+
7+
/// <summary>
8+
/// Implementation of tool selection logic that routes queries to appropriate tools based on intent.
9+
/// </summary>
10+
public class ToolSelector : IToolSelector
11+
{
12+
private readonly ILogger<ToolSelector> _logger;
13+
private readonly NLWebOptions _options;
14+
15+
public ToolSelector(ILogger<ToolSelector> logger, IOptions<NLWebOptions> options)
16+
{
17+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
18+
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
19+
}
20+
21+
/// <inheritdoc />
22+
public async Task<string?> SelectToolAsync(NLWebRequest request, CancellationToken cancellationToken = default)
23+
{
24+
if (!ShouldSelectTool(request))
25+
{
26+
_logger.LogDebug("Tool selection not needed for request {QueryId}", request.QueryId);
27+
return null;
28+
}
29+
30+
_logger.LogDebug("Selecting tool for query: {Query}", request.Query);
31+
32+
// Simple intent-based tool selection
33+
// In a full implementation, this would use more sophisticated intent analysis
34+
var selectedTool = await AnalyzeQueryIntentAsync(request.Query, cancellationToken);
35+
36+
_logger.LogDebug("Selected tool: {Tool} for query {QueryId}", selectedTool ?? "none", request.QueryId);
37+
return selectedTool;
38+
}
39+
40+
/// <inheritdoc />
41+
public bool ShouldSelectTool(NLWebRequest request)
42+
{
43+
// Don't perform tool selection if:
44+
// 1. Tool selection is disabled in configuration
45+
// 2. Generate mode is used (maintain existing behavior)
46+
// 3. Request already has a decontextualized query (already processed)
47+
48+
if (!_options.ToolSelectionEnabled)
49+
{
50+
return false;
51+
}
52+
53+
if (request.Mode == QueryMode.Generate)
54+
{
55+
_logger.LogDebug("Skipping tool selection for Generate mode to maintain existing behavior");
56+
return false;
57+
}
58+
59+
if (!string.IsNullOrEmpty(request.DecontextualizedQuery))
60+
{
61+
_logger.LogDebug("Skipping tool selection for request with decontextualized query");
62+
return false;
63+
}
64+
65+
return true;
66+
}
67+
68+
/// <summary>
69+
/// Analyzes the query intent to determine the appropriate tool.
70+
/// This is a simplified implementation - production would use more sophisticated NLP.
71+
/// </summary>
72+
private async Task<string?> AnalyzeQueryIntentAsync(string query, CancellationToken cancellationToken)
73+
{
74+
await Task.Delay(1, cancellationToken); // Simulate async analysis
75+
76+
var queryLower = query.ToLowerInvariant();
77+
78+
// Basic keyword-based intent detection
79+
// In production, this would use ML models or more sophisticated analysis
80+
81+
if (ContainsKeywords(queryLower, "search", "find", "look for", "locate"))
82+
{
83+
return "search";
84+
}
85+
86+
if (ContainsKeywords(queryLower, "compare", "difference", "versus", "vs", "contrast"))
87+
{
88+
return "compare";
89+
}
90+
91+
if (ContainsKeywords(queryLower, "details", "information about", "tell me about", "describe"))
92+
{
93+
return "details";
94+
}
95+
96+
if (ContainsKeywords(queryLower, "recommend", "suggest", "what should", "ensemble", "set of"))
97+
{
98+
return "ensemble";
99+
}
100+
101+
// Default to search tool for general queries
102+
return "search";
103+
}
104+
105+
private static bool ContainsKeywords(string text, params string[] keywords)
106+
{
107+
return keywords.Any(keyword => text.Contains(keyword));
108+
}
109+
}

tests/NLWebNet.Tests/Services/QueryProcessorTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,80 @@ public async Task ProcessQueryAsync_ReturnsQuery()
121121
// Assert
122122
Assert.AreEqual("test query", result);
123123
}
124+
125+
[TestMethod]
126+
public async Task ProcessQueryAsync_WithToolSelector_CallsToolSelection()
127+
{
128+
// Arrange
129+
var toolSelectorLogger = new TestLogger<ToolSelector>();
130+
var nlWebOptions = new NLWebOptions { ToolSelectionEnabled = true };
131+
var options = Options.Create(nlWebOptions);
132+
var toolSelector = new ToolSelector(toolSelectorLogger, options);
133+
var queryProcessorWithToolSelector = new QueryProcessor(_logger, toolSelector);
134+
135+
var request = new NLWebRequest
136+
{
137+
Query = "search for something",
138+
Mode = QueryMode.List,
139+
QueryId = "test-query-id"
140+
};
141+
142+
// Act
143+
var result = await queryProcessorWithToolSelector.ProcessQueryAsync(request, CancellationToken.None);
144+
145+
// Assert
146+
Assert.AreEqual("search for something", result);
147+
// The tool selection should have been called but not affect the final result
148+
// since we're not changing the query processing behavior yet
149+
}
150+
151+
[TestMethod]
152+
public async Task ProcessQueryAsync_WithToolSelectorDisabled_DoesNotCallToolSelection()
153+
{
154+
// Arrange
155+
var toolSelectorLogger = new TestLogger<ToolSelector>();
156+
var nlWebOptions = new NLWebOptions { ToolSelectionEnabled = false };
157+
var options = Options.Create(nlWebOptions);
158+
var toolSelector = new ToolSelector(toolSelectorLogger, options);
159+
var queryProcessorWithToolSelector = new QueryProcessor(_logger, toolSelector);
160+
161+
var request = new NLWebRequest
162+
{
163+
Query = "search for something",
164+
Mode = QueryMode.List,
165+
QueryId = "test-query-id"
166+
};
167+
168+
// Act
169+
var result = await queryProcessorWithToolSelector.ProcessQueryAsync(request, CancellationToken.None);
170+
171+
// Assert
172+
Assert.AreEqual("search for something", result);
173+
// Tool selection should not have been called
174+
}
175+
176+
[TestMethod]
177+
public async Task ProcessQueryAsync_WithGenerateMode_SkipsToolSelection()
178+
{
179+
// Arrange
180+
var toolSelectorLogger = new TestLogger<ToolSelector>();
181+
var nlWebOptions = new NLWebOptions { ToolSelectionEnabled = true };
182+
var options = Options.Create(nlWebOptions);
183+
var toolSelector = new ToolSelector(toolSelectorLogger, options);
184+
var queryProcessorWithToolSelector = new QueryProcessor(_logger, toolSelector);
185+
186+
var request = new NLWebRequest
187+
{
188+
Query = "generate something",
189+
Mode = QueryMode.Generate,
190+
QueryId = "test-query-id"
191+
};
192+
193+
// Act
194+
var result = await queryProcessorWithToolSelector.ProcessQueryAsync(request, CancellationToken.None);
195+
196+
// Assert
197+
Assert.AreEqual("generate something", result);
198+
// Tool selection should have been skipped for Generate mode
199+
}
124200
}

0 commit comments

Comments
 (0)