Skip to content

Commit 73c1dd2

Browse files
Bing Connector should return more than 1 result
# Motivation and Context fix issue microsoft#508 # Description WebSearch Results using the Bing Connector has a hard code value of count=1. We need to return more than 1 result or should be able to pass count of result requested # Contribution Checklist The code builds clean without any errors or warnings The PR follows SK Contribution Guidelines (https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) The code follows the .NET coding conventions (https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions) verified with dotnet format All unit tests pass, and I have added new tests where possible I didn't break anyone 😄 --------- Co-authored-by: Adrian Bonar <[email protected]>
1 parent d374503 commit 73c1dd2

File tree

5 files changed

+58
-25
lines changed

5 files changed

+58
-25
lines changed

dotnet/src/Skills/Skills.UnitTests/Web/WebSearchEngineSkillTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System;
4+
using System.Collections.Generic;
45
using System.Threading;
56
using System.Threading.Tasks;
67
using Microsoft.SemanticKernel.Memory;
@@ -28,10 +29,10 @@ public WebSearchEngineSkillTests(ITestOutputHelper output)
2829
public async Task SearchAsyncSucceedsAsync()
2930
{
3031
// Arrange
31-
string expected = Guid.NewGuid().ToString();
32+
IEnumerable<string> expected = new[] { Guid.NewGuid().ToString() };
3233

3334
Mock<IWebSearchEngineConnector> connectorMock = new();
34-
connectorMock.Setup(c => c.SearchAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
35+
connectorMock.Setup(c => c.SearchAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
3536
.ReturnsAsync(expected);
3637

3738
WebSearchEngineSkill target = new(connectorMock.Object);

dotnet/src/Skills/Skills.Web/Bing/BingConnector.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System;
4+
using System.Collections.Generic;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Linq;
67
using System.Net.Http;
@@ -16,7 +17,7 @@ namespace Microsoft.SemanticKernel.Skills.Web.Bing;
1617
/// <summary>
1718
/// Bing API connector.
1819
/// </summary>
19-
public class BingConnector : IWebSearchEngineConnector, IDisposable
20+
public sealed class BingConnector : IWebSearchEngineConnector, IDisposable
2021
{
2122
private readonly ILogger _logger;
2223
private readonly HttpClientHandler _httpClientHandler;
@@ -31,9 +32,13 @@ public BingConnector(string apiKey, ILogger<BingConnector>? logger = null)
3132
}
3233

3334
/// <inheritdoc/>
34-
public async Task<string> SearchAsync(string query, CancellationToken cancellationToken = default)
35+
public async Task<IEnumerable<string>> SearchAsync(string query, int count = 1, int offset = 0, CancellationToken cancellationToken = default)
3536
{
36-
Uri uri = new($"https://api.bing.microsoft.com/v7.0/search?q={Uri.EscapeDataString(query)}&count=1");
37+
if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count)); }
38+
if (count >= 50) { throw new ArgumentOutOfRangeException(nameof(count), "{nameof(count)} value must be less than 50."); }
39+
if (offset < 0) { throw new ArgumentOutOfRangeException(nameof(offset)); }
40+
41+
Uri uri = new($"https://api.bing.microsoft.com/v7.0/search?q={Uri.EscapeDataString(query)}&count={count}&offset={offset}");
3742

3843
this._logger.LogDebug("Sending request: {0}", uri);
3944
HttpResponseMessage response = await this._httpClient.GetAsync(uri, cancellationToken).ConfigureAwait(false);
@@ -44,17 +49,12 @@ public async Task<string> SearchAsync(string query, CancellationToken cancellati
4449
this._logger.LogTrace("Response content received: {0}", json);
4550

4651
BingSearchResponse? data = JsonSerializer.Deserialize<BingSearchResponse>(json);
47-
WebPage? firstResult = data?.WebPages?.Value?.FirstOrDefault();
48-
49-
this._logger.LogDebug("Result: {0}, {1}, {2}",
50-
firstResult?.Name,
51-
firstResult?.Url,
52-
firstResult?.Snippet);
52+
WebPage[]? results = data?.WebPages?.Value;
5353

54-
return firstResult?.Snippet ?? string.Empty;
54+
return results == null ? Enumerable.Empty<string>() : results.Select(x => x.Snippet);
5555
}
5656

57-
protected virtual void Dispose(bool disposing)
57+
private void Dispose(bool disposing)
5858
{
5959
if (disposing)
6060
{

dotnet/src/Skills/Skills.Web/Google/GoogleConnector.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System;
4+
using System.Collections.Generic;
45
using System.Linq;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -14,7 +15,7 @@ namespace Microsoft.SemanticKernel.Skills.Web.Google;
1415
/// <summary>
1516
/// Google search connector.
1617
/// </summary>
17-
public class GoogleConnector : IWebSearchEngineConnector, IDisposable
18+
public sealed class GoogleConnector : IWebSearchEngineConnector, IDisposable
1819
{
1920
private readonly ILogger _logger;
2021
private readonly CustomSearchAPIService _search;
@@ -37,21 +38,28 @@ public GoogleConnector(
3738
}
3839

3940
/// <inheritdoc/>
40-
public async Task<string> SearchAsync(string query, CancellationToken cancellationToken = default)
41+
public async Task<IEnumerable<string>> SearchAsync(
42+
string query,
43+
int count,
44+
int offset,
45+
CancellationToken cancellationToken)
4146
{
47+
if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count)); }
48+
if (count > 10) { throw new ArgumentOutOfRangeException(nameof(count), "{nameof(count)} value must be between 0 and 10, inclusive."); }
49+
if (offset < 0) { throw new ArgumentOutOfRangeException(nameof(offset)); }
50+
4251
var search = this._search.Cse.List();
4352
search.Cx = this._searchEngineId;
4453
search.Q = query;
54+
search.Num = count;
55+
search.Start = offset;
4556

4657
var results = await search.ExecuteAsync(cancellationToken).ConfigureAwait(false);
4758

48-
var first = results.Items?.FirstOrDefault();
49-
this._logger.LogDebug("Result: {Title}, {Link}, {Snippet}", first?.Title, first?.Link, first?.Snippet);
50-
51-
return first?.Snippet ?? string.Empty;
59+
return results.Items.Select(item => item.Snippet);
5260
}
5361

54-
protected virtual void Dispose(bool disposing)
62+
private void Dispose(bool disposing)
5563
{
5664
if (disposing)
5765
{

dotnet/src/Skills/Skills.Web/IWebSearchEngineConnector.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System.Collections.Generic;
34
using System.Threading;
45
using System.Threading.Tasks;
56

@@ -14,7 +15,9 @@ public interface IWebSearchEngineConnector
1415
/// Execute a web search engine search.
1516
/// </summary>
1617
/// <param name="query">Query to search.</param>
18+
/// <param name="count">Number of results.</param>
19+
/// <param name="offset ">Number of results to skip.</param>
1720
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
1821
/// <returns>First snippet returned from search.</returns>
19-
Task<string> SearchAsync(string query, CancellationToken cancellationToken = default);
22+
Task<IEnumerable<string>> SearchAsync(string query, int count = 1, int offset = 0, CancellationToken cancellationToken = default);
2023
}

dotnet/src/Skills/Skills.Web/WebSearchEngineSkill.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System.Globalization;
4+
using System.Linq;
5+
using System.Text.Json;
36
using System.Threading.Tasks;
47
using Microsoft.SemanticKernel.Orchestration;
58
using Microsoft.SemanticKernel.SkillDefinition;
@@ -11,6 +14,12 @@ namespace Microsoft.SemanticKernel.Skills.Web;
1114
/// </summary>
1215
public class WebSearchEngineSkill
1316
{
17+
public const string CountParam = "count";
18+
public const string OffsetParam = "offset";
19+
20+
private const string DefaultCount = "1";
21+
private const string DefaultOffset = "0";
22+
1423
private readonly IWebSearchEngineConnector _connector;
1524

1625
public WebSearchEngineSkill(IWebSearchEngineConnector connector)
@@ -19,16 +28,28 @@ public WebSearchEngineSkill(IWebSearchEngineConnector connector)
1928
}
2029

2130
[SKFunction("Perform a web search.")]
31+
[SKFunctionName("Search")]
2232
[SKFunctionInput(Description = "Text to search for")]
23-
[SKFunctionName("search")]
33+
[SKFunctionContextParameter(Name = CountParam, Description = "Number of results", DefaultValue = DefaultCount)]
34+
[SKFunctionContextParameter(Name = OffsetParam, Description = "Number of results to skip", DefaultValue = DefaultOffset)]
2435
public async Task<string> SearchAsync(string query, SKContext context)
2536
{
26-
string result = await this._connector.SearchAsync(query, context.CancellationToken).ConfigureAwait(false);
27-
if (string.IsNullOrWhiteSpace(result))
37+
var count = context.Variables.ContainsKey(CountParam) ? context[CountParam] : DefaultCount;
38+
if (string.IsNullOrWhiteSpace(count)) { count = DefaultCount; }
39+
40+
var offset = context.Variables.ContainsKey(OffsetParam) ? context[OffsetParam] : DefaultOffset;
41+
if (string.IsNullOrWhiteSpace(offset)) { offset = DefaultOffset; }
42+
43+
int countInt = int.Parse(count, CultureInfo.InvariantCulture);
44+
int offsetInt = int.Parse(offset, CultureInfo.InvariantCulture);
45+
var results = await this._connector.SearchAsync(query, countInt, offsetInt, context.CancellationToken).ConfigureAwait(false);
46+
if (!results.Any())
2847
{
2948
context.Fail("Failed to get a response from the web search engine.");
3049
}
3150

32-
return result;
51+
return countInt == 1
52+
? results.FirstOrDefault() ?? string.Empty
53+
: JsonSerializer.Serialize(results);
3354
}
3455
}

0 commit comments

Comments
 (0)