Skip to content

Commit bffa347

Browse files
committed
Implement core service interfaces and their implementations for NLWeb processing
- Added INLWebService interface for processing NLWeb requests. - Introduced IQueryProcessor interface for query processing and validation. - Created IResultGenerator interface for generating results based on query modes. - Implemented MockDataBackend for testing with sample data. - Developed NLWebService to orchestrate query processing and result generation. - Implemented QueryProcessor for handling query decontextualization and validation. - Created ResultGenerator for generating AI-powered responses and summaries. - Added unit tests for MockDataBackend and QueryProcessor to ensure functionality. - Introduced TestLogger for capturing log messages during testing.
1 parent 0d78b1a commit bffa347

19 files changed

+1555
-70
lines changed

.github/workflows/build.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,18 @@ jobs:
5050
- name: Build solution
5151
run: dotnet build --configuration ${{ matrix.configuration }} --no-restore --verbosity minimal
5252

53-
- name: Test library (if tests exist)
53+
- name: Test solution
5454
run: |
55-
# Check if there are any test projects in the src/NLWebNet directory
56-
TEST_PROJECTS=$(find src/NLWebNet -name "*.csproj" -exec grep -l "Microsoft.NET.Test.Sdk\|xunit\|NUnit\|MSTest" {} \; 2>/dev/null || true)
55+
# Check if there are any test projects in the solution
56+
TEST_PROJECTS=$(find tests -name "*.csproj" -exec grep -l "Microsoft.NET.Test.Sdk\|xunit\|NUnit\|MSTest" {} \; 2>/dev/null || true)
5757
5858
if [ -n "$TEST_PROJECTS" ]; then
5959
echo "Found test projects, running tests..."
60-
dotnet test src/NLWebNet --configuration ${{ matrix.configuration }} --no-build --verbosity minimal --logger "trx;LogFileName=test-results-${{ matrix.configuration }}.trx" --results-directory TestResults/
60+
dotnet test --configuration ${{ matrix.configuration }} --no-build --verbosity minimal --logger "trx;LogFileName=test-results-${{ matrix.configuration }}.trx" --results-directory TestResults/
6161
else
62-
echo "No test projects found in src/NLWebNet - skipping tests (tests will be implemented in Phase 9)"
62+
echo "No test projects found - skipping tests"
6363
mkdir -p TestResults
64-
echo "##[warning]No test projects found - tests will be implemented in Phase 9"
64+
echo "##[warning]No test projects found"
6565
fi
6666
continue-on-error: false
6767

@@ -140,6 +140,7 @@ jobs:
140140
else
141141
echo "✅ No vulnerable packages found."
142142
fi
143+
143144
package-validation:
144145
runs-on: ubuntu-latest
145146
needs: build
@@ -173,7 +174,8 @@ jobs:
173174
SHORT_SHA=$(git rev-parse --short HEAD)
174175
VERSION="${VERSION}-alpha.${COMMITS_SINCE_TAG}+${SHORT_SHA}"
175176
fi
176-
echo "version=${VERSION}" >> $GITHUB_OUTPUT
177+
178+
echo "version=${VERSION}" >> $GITHUB_OUTPUT
177179
echo "📦 Determined version: ${VERSION}"
178180
179181
- name: Build (Release) for packaging

NLWebNet.sln

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{A39C23D2-F
1111
EndProject
1212
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLWebNet.Demo", "demo\NLWebNet.Demo.csproj", "{6F25FD99-AF67-4509-A46C-FCD450F6A775}"
1313
EndProject
14+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
15+
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLWebNet.Tests", "tests\NLWebNet.Tests\NLWebNet.Tests.csproj", "{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}"
17+
EndProject
1418
Global
1519
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1620
Debug|Any CPU = Debug|Any CPU
@@ -45,12 +49,25 @@ Global
4549
{6F25FD99-AF67-4509-A46C-FCD450F6A775}.Release|x64.Build.0 = Release|Any CPU
4650
{6F25FD99-AF67-4509-A46C-FCD450F6A775}.Release|x86.ActiveCfg = Release|Any CPU
4751
{6F25FD99-AF67-4509-A46C-FCD450F6A775}.Release|x86.Build.0 = Release|Any CPU
52+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
54+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|x64.ActiveCfg = Debug|Any CPU
55+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|x64.Build.0 = Debug|Any CPU
56+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|x86.ActiveCfg = Debug|Any CPU
57+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Debug|x86.Build.0 = Debug|Any CPU
58+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
59+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Release|Any CPU.Build.0 = Release|Any CPU
60+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Release|x64.ActiveCfg = Release|Any CPU
61+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Release|x64.Build.0 = Release|Any CPU
62+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Release|x86.ActiveCfg = Release|Any CPU
63+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE}.Release|x86.Build.0 = Release|Any CPU
4864
EndGlobalSection
4965
GlobalSection(SolutionProperties) = preSolution
5066
HideSolutionNode = FALSE
5167
EndGlobalSection
5268
GlobalSection(NestedProjects) = preSolution
5369
{1E458E72-D542-44BB-9F84-1EDE008FBB1D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
5470
{6F25FD99-AF67-4509-A46C-FCD450F6A775} = {A39C23D2-F2C0-258D-165A-CF1E7FEE6E7B}
71+
{21F486B2-CB3A-4D61-8C1F-FBCE3CA48CFE} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
5572
EndGlobalSection
5673
EndGlobal

demo/NLWebNet.Demo.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<ItemGroup>
1212
<ProjectReference Include="..\src\NLWebNet\NLWebNet.csproj" />
1313
</ItemGroup> <ItemGroup>
14-
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
14+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
1515
</ItemGroup>
1616

1717
</Project>

doc/todo.md

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ The demo application is now fully functional with a modern .NET 9 Blazor Web App
5151
-**CI/CD Packaging Fix**: Added Release build step to package-validation job to ensure NuGet DLL is available for packaging
5252
-**CI/CD Symbol Generation**: Fixed NuGet package validation by adding proper symbol generation, deterministic builds, and Source Link integration
5353
-**Dynamic Package Versioning**: Implemented Git-based semantic versioning with automatic pre-release numbering for CI builds
54+
-**Business Logic Layer**: Complete implementation of core services (INLWebService, IQueryProcessor, IResultGenerator, IDataBackend) with Microsoft.Extensions.AI integration
55+
-**Comprehensive Testing**: Added MSTest unit tests for QueryProcessor and MockDataBackend (11 tests, 100% pass rate)
56+
-**Testing Framework Migration**: Migrated from xUnit to MSTest 3.2.0 with code coverage support
57+
-**Production Ready**: All builds (Debug/Release) work correctly, demo app runs successfully at <http://localhost:5038>
58+
59+
The project is now ready for Phase 4 (MCP Integration) with a solid foundation of tested, extensible business logic.
5460

5561
The next phase focuses on implementing the core NLWeb library functionality.
5662

@@ -104,19 +110,31 @@ The next phase focuses on implementing the core NLWeb library functionality.
104110
- [x] Symbol generation and deterministic builds for package validation
105111
- [x] Source Link integration for GitHub repository
106112

107-
### Phase 3: Business Logic Layer (Library)
108-
109-
- [ ] Implement core service interfaces and classes:
110-
- [ ] `INLWebService` interface for main business logic
111-
- [ ] `NLWebService` implementation
112-
- [ ] `IQueryProcessor` interface for query processing
113-
- [ ] `QueryProcessor` implementation for decontextualization
114-
- [ ] `IResultGenerator` interface for different modes (list, summarize, generate)
115-
- [ ] `ResultGenerator` implementation using AI services
116-
- [ ] `IDataBackend` interface for pluggable data sources
117-
- [ ] `MockDataBackend` implementation for testing/demo
118-
- [ ] **OPEN QUESTION**: What backend data source will be used for search/retrieval?
119-
- [ ] **OPEN QUESTION**: Which LLM service integration (Azure OpenAI, OpenAI, etc.)?
113+
### Phase 3: Business Logic Layer (Library) ✅
114+
115+
#### Status: Complete
116+
117+
The core business logic layer has been successfully implemented with the following architecture:
118+
119+
- [x] Core service interfaces and implementations:
120+
- [x] `INLWebService` interface and `NLWebService` implementation - Main orchestration service
121+
- [x] `IQueryProcessor` interface and `QueryProcessor` implementation - Query validation and decontextualization
122+
- [x] `IResultGenerator` interface and `ResultGenerator` implementation - AI-powered response generation with streaming support
123+
- [x] `IDataBackend` interface and `MockDataBackend` implementation - Extensible data source abstraction
124+
- [x] `ServiceCollectionExtensions` - Clean dependency injection setup with configurable backends
125+
- [x] **Microsoft.Extensions.AI Integration**: Integrated with the latest Microsoft.Extensions.AI library for AI client abstraction
126+
- [x] **Comprehensive Testing**: Added MSTest unit tests for QueryProcessor and MockDataBackend with 100% test pass rate (migrated from xUnit)
127+
- [x] **Multiple Query Modes**: Full support for List, Summarize, and Generate modes
128+
- [x] **Streaming Support**: Real-time streaming responses using IAsyncEnumerable for better user experience
129+
- [x] **Error Handling**: Robust error handling with graceful fallbacks and proper exception management
130+
- [x] **Modern Architecture**: Clean interfaces, async/await patterns, cancellation token support, and structured logging
131+
132+
#### Technical Notes
133+
134+
- ResultGenerator uses Microsoft.Extensions.AI's IChatClient for AI integration with fallback to template responses
135+
- Streaming implementation avoids yield-return-in-try-catch C# limitation through proper separation of concerns
136+
- MockDataBackend provides realistic sample data with relevance scoring for demo purposes
137+
- **Testing Framework**: Uses MSTest 3.2.0 with Microsoft.Testing.Extensions.CodeCoverage for comprehensive unit testing
120138

121139
### Phase 4: MCP Integration (Library)
122140

src/NLWebNet/Extensions/ServiceCollectionExtensions.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using NLWebNet.Models;
3+
using NLWebNet.Services;
34

45
namespace NLWebNet;
56

@@ -22,10 +23,40 @@ public static IServiceCollection AddNLWebNet(this IServiceCollection services, A
2223
services.Configure(configureOptions);
2324
}
2425

25-
// TODO: Register NLWebNet services here
26-
// services.AddScoped<INLWebService, NLWebService>();
27-
// services.AddScoped<IQueryProcessor, QueryProcessor>();
28-
// services.AddScoped<IResultGenerator, ResultGenerator>();
26+
// Register core NLWebNet services
27+
services.AddScoped<INLWebService, NLWebService>();
28+
services.AddScoped<IQueryProcessor, QueryProcessor>();
29+
services.AddScoped<IResultGenerator, ResultGenerator>();
30+
31+
// Register default data backend (can be overridden)
32+
services.AddScoped<IDataBackend, MockDataBackend>();
33+
34+
return services;
35+
}
36+
37+
/// <summary>
38+
/// Adds NLWebNet services with a custom data backend
39+
/// </summary>
40+
/// <typeparam name="TDataBackend">The custom data backend implementation</typeparam>
41+
/// <param name="services">The service collection</param>
42+
/// <param name="configureOptions">Optional configuration callback</param>
43+
/// <returns>The service collection for chaining</returns>
44+
public static IServiceCollection AddNLWebNet<TDataBackend>(this IServiceCollection services, Action<NLWebOptions>? configureOptions = null)
45+
where TDataBackend : class, IDataBackend
46+
{
47+
// Configure options
48+
if (configureOptions != null)
49+
{
50+
services.Configure(configureOptions);
51+
}
52+
53+
// Register core NLWebNet services
54+
services.AddScoped<INLWebService, NLWebService>();
55+
services.AddScoped<IQueryProcessor, QueryProcessor>();
56+
services.AddScoped<IResultGenerator, ResultGenerator>();
57+
58+
// Register custom data backend
59+
services.AddScoped<IDataBackend, TDataBackend>();
2960

3061
return services;
3162
}

src/NLWebNet/Models/NLWebResponse.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,27 @@ public class NLWebResponse
7878
/// Total number of results found (may be larger than the returned results array).
7979
/// </summary>
8080
[JsonPropertyName("total_results")]
81-
public int? TotalResults { get; set; }
81+
public int? TotalResults { get; set; } /// <summary>
82+
/// Processing time in milliseconds.
83+
/// </summary>
84+
[JsonPropertyName("processing_time_ms")]
85+
public long? ProcessingTimeMs { get; set; }
8286

8387
/// <summary>
84-
/// Processing time in milliseconds.
88+
/// The processed/decontextualized query that was used for search.
8589
/// </summary>
86-
[JsonPropertyName("processing_time_ms")]
87-
public long? ProcessingTimeMs { get; set; }
90+
[JsonPropertyName("processed_query")]
91+
public string? ProcessedQuery { get; set; }
92+
93+
/// <summary>
94+
/// Error message if the request failed.
95+
/// </summary>
96+
[JsonPropertyName("error")]
97+
public string? Error { get; set; }
98+
99+
/// <summary>
100+
/// Indicates whether the streaming response is complete.
101+
/// </summary>
102+
[JsonPropertyName("is_complete")]
103+
public bool IsComplete { get; set; } = true;
88104
}

src/NLWebNet/NLWebNet.csproj

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,46 @@
1-
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup>
2-
<TargetFramework>net9.0</TargetFramework>
3-
<Nullable>enable</Nullable>
4-
<ImplicitUsings>enable</ImplicitUsings>
5-
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
6-
<!-- Package Metadata -->
7-
<PackageId>NLWebNet</PackageId>
8-
<PackageVersion Condition="'$(PackageVersion)' == ''">1.0.0-dev</PackageVersion>
9-
<Authors>NLWebNet Contributors</Authors>
10-
<Description>A .NET library implementing the NLWeb protocol for natural language web interfaces</Description>
11-
<PackageProjectUrl>https://github.com/microsoft/NLWeb</PackageProjectUrl>
12-
<RepositoryUrl>https://github.com/your-org/NLWebNet</RepositoryUrl>
13-
<PackageLicenseExpression>MIT</PackageLicenseExpression>
14-
<PackageReadmeFile>README.md</PackageReadmeFile>
15-
16-
<!-- Symbol and Deterministic Build Configuration -->
17-
<IncludeSymbols>true</IncludeSymbols>
18-
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
19-
<DebugType>portable</DebugType>
20-
<DebugSymbols>true</DebugSymbols>
21-
<Deterministic>true</Deterministic>
22-
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
23-
24-
<!-- Source Link Configuration -->
25-
<PublishRepositoryUrl>true</PublishRepositoryUrl>
26-
<EmbedUntrackedSources>true</EmbedUntrackedSources>
27-
</PropertyGroup> <ItemGroup>
28-
<PackageReference Include="Microsoft.Extensions.AI" Version="9.5.0" />
29-
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
30-
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
31-
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
32-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
33-
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.5" />
34-
<PackageReference Include="ModelContextProtocol" Version="0.2.0-preview.3" />
35-
36-
<!-- Source Link for GitHub -->
37-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
38-
</ItemGroup>
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net9.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
7+
<!-- Package Metadata -->
8+
<PackageId>NLWebNet</PackageId>
9+
<PackageVersion Condition="'$(PackageVersion)' == ''">1.0.0-dev</PackageVersion>
10+
<Authors>NLWebNet Contributors</Authors>
11+
<Description>A .NET library implementing the NLWeb protocol for natural language web interfaces</Description>
12+
<PackageProjectUrl>https://github.com/microsoft/NLWeb</PackageProjectUrl>
13+
<RepositoryUrl>https://github.com/your-org/NLWebNet</RepositoryUrl>
14+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
15+
<PackageReadmeFile>README.md</PackageReadmeFile>
3916

40-
<ItemGroup>
41-
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
42-
</ItemGroup>
17+
<!-- Symbol and Deterministic Build Configuration -->
18+
<IncludeSymbols>true</IncludeSymbols>
19+
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
20+
<DebugType>portable</DebugType>
21+
<DebugSymbols>true</DebugSymbols>
22+
<Deterministic>true</Deterministic>
23+
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
24+
25+
<!-- Source Link Configuration -->
26+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
27+
<EmbedUntrackedSources>true</EmbedUntrackedSources>
28+
</PropertyGroup>
29+
<ItemGroup>
30+
<PackageReference Include="Microsoft.Extensions.AI" Version="9.6.0" />
31+
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
32+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
33+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
34+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
35+
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.6" />
36+
<PackageReference Include="ModelContextProtocol" Version="0.2.0-preview.3" />
37+
38+
<!-- Source Link for GitHub -->
39+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
40+
</ItemGroup>
41+
42+
<ItemGroup>
43+
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
44+
</ItemGroup>
4345

4446
</Project>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using NLWebNet.Models;
2+
3+
namespace NLWebNet.Services;
4+
5+
/// <summary>
6+
/// Interface for pluggable data backends that provide search and retrieval functionality.
7+
/// </summary>
8+
public interface IDataBackend
9+
{
10+
/// <summary>
11+
/// Searches the backend data store for relevant results.
12+
/// </summary>
13+
/// <param name="query">The search query</param>
14+
/// <param name="site">Optional site filter to restrict search scope</param>
15+
/// <param name="maxResults">Maximum number of results to return</param>
16+
/// <param name="cancellationToken">Cancellation token for async operations</param>
17+
/// <returns>A collection of search results with relevance scores</returns>
18+
Task<IEnumerable<NLWebResult>> SearchAsync(string query, string? site = null, int maxResults = 10, CancellationToken cancellationToken = default);
19+
20+
/// <summary>
21+
/// Gets available sites/scopes in the backend.
22+
/// </summary>
23+
/// <param name="cancellationToken">Cancellation token for async operations</param>
24+
/// <returns>A collection of available site identifiers</returns>
25+
Task<IEnumerable<string>> GetAvailableSitesAsync(CancellationToken cancellationToken = default);
26+
27+
/// <summary>
28+
/// Gets detailed information about a specific item by its URL or ID.
29+
/// </summary>
30+
/// <param name="url">The URL or identifier of the item</param>
31+
/// <param name="cancellationToken">Cancellation token for async operations</param>
32+
/// <returns>The detailed item information, or null if not found</returns>
33+
Task<NLWebResult?> GetItemByUrlAsync(string url, CancellationToken cancellationToken = default);
34+
35+
/// <summary>
36+
/// Gets the backend's capabilities and configuration.
37+
/// </summary>
38+
/// <returns>Information about what the backend supports</returns>
39+
BackendCapabilities GetCapabilities();
40+
}
41+
42+
/// <summary>
43+
/// Describes the capabilities of a data backend.
44+
/// </summary>
45+
public record BackendCapabilities
46+
{
47+
/// <summary>
48+
/// Whether the backend supports site-based filtering.
49+
/// </summary>
50+
public bool SupportsSiteFiltering { get; init; } = false;
51+
52+
/// <summary>
53+
/// Whether the backend supports full-text search.
54+
/// </summary>
55+
public bool SupportsFullTextSearch { get; init; } = true;
56+
57+
/// <summary>
58+
/// Whether the backend supports semantic/vector search.
59+
/// </summary>
60+
public bool SupportsSemanticSearch { get; init; } = false;
61+
62+
/// <summary>
63+
/// Maximum number of results the backend can return.
64+
/// </summary>
65+
public int MaxResults { get; init; } = 100;
66+
67+
/// <summary>
68+
/// Description of the backend implementation.
69+
/// </summary>
70+
public string Description { get; init; } = string.Empty;
71+
}

0 commit comments

Comments
 (0)