Skip to content

Commit 8c2e8fd

Browse files
dlucScottKane
andauthored
Add support for Google searches using Google.Apis.CustomSearchAPI.v1 (#737)
[see #537 - PR rebuilt to fix nuget dependency and merge conflicts] ### Motivation and Context Allow the usage of Google search equivalent to current Bing search implementation. ### Description Uses the Google.Apis.CustomSearchAPI.v1 packages to interface with the Google search API, this requires an API key from GCP and a custom search engine ID --------- Co-authored-by: Scott Kane <[email protected]>
1 parent 3362c20 commit 8c2e8fd

File tree

12 files changed

+302
-97
lines changed

12 files changed

+302
-97
lines changed

dotnet/src/IntegrationTests/WebSkill/WebSkillTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public async Task BingSkillTestAsync(string prompt, string expectedAnswerContain
5555
// Act
5656
SKContext result = await kernel.RunAsync(
5757
prompt,
58-
search["SearchAsync"]
58+
search["Search"]
5959
);
6060

6161
// Assert
@@ -72,7 +72,7 @@ public async Task WebFileDownloadSkillFileTestAsync()
7272
var download = kernel.ImportSkill(skill, "WebFileDownload");
7373
string fileWhereToSaveWebPage = Path.GetTempFileName();
7474
var contextVariables = new ContextVariables("https://www.microsoft.com");
75-
contextVariables.Set(WebFileDownloadSkill.Parameters.FilePath, fileWhereToSaveWebPage);
75+
contextVariables.Set(WebFileDownloadSkill.FilePathParamName, fileWhereToSaveWebPage);
7676

7777
// Act
7878
await kernel.RunAsync(contextVariables, download["DownloadToFile"]);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Google.Apis.CustomSearchAPI.v1;
8+
using Google.Apis.Services;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Extensions.Logging.Abstractions;
11+
12+
namespace Microsoft.SemanticKernel.Skills.Web.Google;
13+
14+
/// <summary>
15+
/// Google search connector.
16+
/// </summary>
17+
public class GoogleConnector : IWebSearchEngineConnector, IDisposable
18+
{
19+
private readonly ILogger _logger;
20+
private readonly CustomSearchAPIService _search;
21+
private readonly string? _searchEngineId;
22+
23+
/// <summary>
24+
/// Google search connector
25+
/// </summary>
26+
/// <param name="apiKey">Google Custom Search API (looks like "ABcdEfG1...")</param>
27+
/// <param name="searchEngineId">Google Search Engine ID (looks like "a12b345...")</param>
28+
/// <param name="logger">Optional logger</param>
29+
public GoogleConnector(
30+
string apiKey,
31+
string searchEngineId,
32+
ILogger<GoogleConnector>? logger = null)
33+
{
34+
this._search = new CustomSearchAPIService(new BaseClientService.Initializer { ApiKey = apiKey });
35+
this._searchEngineId = searchEngineId;
36+
this._logger = logger ?? NullLogger<GoogleConnector>.Instance;
37+
}
38+
39+
/// <inheritdoc/>
40+
public async Task<string> SearchAsync(string query, CancellationToken cancellationToken = default)
41+
{
42+
var search = this._search.Cse.List();
43+
search.Cx = this._searchEngineId;
44+
search.Q = query;
45+
46+
var results = await search.ExecuteAsync(cancellationToken).ConfigureAwait(false);
47+
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;
52+
}
53+
54+
protected virtual void Dispose(bool disposing)
55+
{
56+
if (disposing)
57+
{
58+
this._search.Dispose();
59+
}
60+
}
61+
62+
public void Dispose()
63+
{
64+
this.Dispose(disposing: true);
65+
GC.SuppressFinalize(this);
66+
}
67+
}

dotnet/src/Skills/Skills.Web/Skills.Web.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<ItemGroup>
1919
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="[6.0.0, )" />
2020
<PackageReference Include="System.Text.Json" VersionOverride="[6.0.0, )" />
21+
<PackageReference Include="Google.Apis.CustomSearchAPI.v1" VersionOverride="[1.60.0.3001, )" />
2122
</ItemGroup>
2223

2324
<ItemGroup>

dotnet/src/Skills/Skills.Web/WebFileDownloadSkill.cs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,9 @@ namespace Microsoft.SemanticKernel.Skills.Web;
1818
public class WebFileDownloadSkill : IDisposable
1919
{
2020
/// <summary>
21-
/// Parameter names.
22-
/// <see cref="ContextVariables"/>
21+
/// Skill parameter: where to save file.
2322
/// </summary>
24-
public static class Parameters
25-
{
26-
/// <summary>
27-
/// Where to save file.
28-
/// </summary>
29-
public const string FilePath = "filePath";
30-
}
23+
public const string FilePathParamName = "filePath";
3124

3225
private readonly ILogger _logger;
3326
private readonly HttpClientHandler _httpClientHandler;
@@ -54,15 +47,15 @@ public WebFileDownloadSkill(ILogger<WebFileDownloadSkill>? logger = null)
5447
[SKFunction("Downloads a file to local storage")]
5548
[SKFunctionName("DownloadToFile")]
5649
[SKFunctionInput(Description = "URL of file to download")]
57-
[SKFunctionContextParameter(Name = Parameters.FilePath, Description = "Path where to save file locally")]
50+
[SKFunctionContextParameter(Name = FilePathParamName, Description = "Path where to save file locally")]
5851
public async Task DownloadToFileAsync(string source, SKContext context)
5952
{
6053
this._logger.LogDebug($"{nameof(this.DownloadToFileAsync)} got called");
6154

62-
if (!context.Variables.Get(Parameters.FilePath, out string filePath))
55+
if (!context.Variables.Get(FilePathParamName, out string filePath))
6356
{
6457
this._logger.LogError($"Missing context variable in {nameof(this.DownloadToFileAsync)}");
65-
string errorMessage = $"Missing variable {Parameters.FilePath}";
58+
string errorMessage = $"Missing variable {FilePathParamName}";
6659
context.Fail(errorMessage);
6760

6861
return;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public WebSearchEngineSkill(IWebSearchEngineConnector connector)
2020

2121
[SKFunction("Perform a web search.")]
2222
[SKFunctionInput(Description = "Text to search for")]
23+
[SKFunctionName("search")]
2324
public async Task<string> SearchAsync(string query, SKContext context)
2425
{
2526
string result = await this._connector.SearchAsync(query, context.CancellationToken).ConfigureAwait(false);

samples/dotnet/github-skills/GitHubSkill.cs

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,29 @@ namespace GitHubSkills;
2222
public class GitHubSkill
2323
{
2424
/// <summary>
25-
/// Parameter names.
26-
/// <see cref="ContextVariables"/>
25+
/// Name of the repository repositoryBranch which will be downloaded and summarized.
2726
/// </summary>
28-
public static class Parameters
29-
{
30-
/// <summary>
31-
/// Name of the repository repositoryBranch which will be downloaded and summarized.
32-
/// </summary>
33-
public const string RepositoryBranch = "repositoryBranch";
34-
35-
/// <summary>
36-
/// The search string to match against the names of files in the repository.
37-
/// </summary>
38-
public const string SearchPattern = "searchPattern";
39-
40-
/// <summary>
41-
/// Document file path.
42-
/// </summary>
43-
public const string FilePath = "filePath";
44-
45-
/// <summary>
46-
/// Directory to which to extract compressed file's data.
47-
/// </summary>
48-
public const string DestinationDirectoryPath = "destinationDirectoryPath";
49-
50-
/// <summary>
51-
/// Name of the memory collection used to store the code summaries.
52-
/// </summary>
53-
public const string MemoryCollectionName = "memoryCollectionName";
54-
}
27+
public const string RepositoryBranchParamName = "repositoryBranch";
28+
29+
/// <summary>
30+
/// The search string to match against the names of files in the repository.
31+
/// </summary>
32+
public const string SearchPatternParamName = "searchPattern";
33+
34+
/// <summary>
35+
/// Document file path.
36+
/// </summary>
37+
public const string FilePathParamName = "filePath";
38+
39+
/// <summary>
40+
/// Directory to which to extract compressed file's data.
41+
/// </summary>
42+
public const string DestinationDirectoryPathParamName = "destinationDirectoryPath";
43+
44+
/// <summary>
45+
/// Name of the memory collection used to store the code summaries.
46+
/// </summary>
47+
public const string MemoryCollectionNameParamName = "memoryCollectionName";
5548

5649
/// <summary>
5750
/// The max tokens to process in a single semantic function call.
@@ -110,17 +103,17 @@ public GitHubSkill(IKernel kernel, WebFileDownloadSkill downloadSkill, ILogger<G
110103
[SKFunction("Downloads a repository and summarizes the content")]
111104
[SKFunctionName("SummarizeRepository")]
112105
[SKFunctionInput(Description = "URL of the GitHub repository to summarize")]
113-
[SKFunctionContextParameter(Name = Parameters.RepositoryBranch,
106+
[SKFunctionContextParameter(Name = RepositoryBranchParamName,
114107
Description = "Name of the repository repositoryBranch which will be downloaded and summarized")]
115-
[SKFunctionContextParameter(Name = Parameters.SearchPattern, Description = "The search string to match against the names of files in the repository")]
108+
[SKFunctionContextParameter(Name = SearchPatternParamName, Description = "The search string to match against the names of files in the repository")]
116109
public async Task SummarizeRepositoryAsync(string source, SKContext context)
117110
{
118-
if (!context.Variables.Get(Parameters.RepositoryBranch, out string repositoryBranch) || string.IsNullOrEmpty(repositoryBranch))
111+
if (!context.Variables.Get(RepositoryBranchParamName, out string repositoryBranch) || string.IsNullOrEmpty(repositoryBranch))
119112
{
120113
repositoryBranch = "main";
121114
}
122115

123-
if (!context.Variables.Get(Parameters.SearchPattern, out string searchPattern) || string.IsNullOrEmpty(searchPattern))
116+
if (!context.Variables.Get(SearchPatternParamName, out string searchPattern) || string.IsNullOrEmpty(searchPattern))
124117
{
125118
searchPattern = "*.md";
126119
}
@@ -133,14 +126,14 @@ public async Task SummarizeRepositoryAsync(string source, SKContext context)
133126
{
134127
var repositoryUri = source.Trim(new char[] { ' ', '/' });
135128
var context1 = new SKContext(new ContextVariables(), NullMemory.Instance, null, context.Log);
136-
context1.Variables.Set(Parameters.FilePath, filePath);
129+
context1.Variables.Set(FilePathParamName, filePath);
137130
await this._downloadSkill.DownloadToFileAsync($"{repositoryUri}/archive/refs/heads/{repositoryBranch}.zip", context1);
138131

139132
ZipFile.ExtractToDirectory(filePath, directoryPath);
140133

141134
await this.SummarizeCodeDirectoryAsync(directoryPath, searchPattern, repositoryUri, repositoryBranch, context);
142135

143-
context.Variables.Set(Parameters.MemoryCollectionName, $"{repositoryUri}-{repositoryBranch}");
136+
context.Variables.Set(MemoryCollectionNameParamName, $"{repositoryUri}-{repositoryBranch}");
144137
}
145138
finally
146139
{

samples/dotnet/kernel-syntax-examples/Example04_BingSkillAndConnector.cs

Lines changed: 0 additions & 38 deletions
This file was deleted.

samples/dotnet/kernel-syntax-examples/Example05_CombineLLMPromptsAndNativeCode.cs renamed to samples/dotnet/kernel-syntax-examples/Example04_CombineLLMPromptsAndNativeCode.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
using RepoUtils;
99

1010
// ReSharper disable once InconsistentNaming
11-
public static class Example05_CombineLLMPromptsAndNativeCode
11+
public static class Example04_CombineLLMPromptsAndNativeCode
1212
{
1313
public static async Task RunAsync()
1414
{
@@ -38,18 +38,18 @@ public static async Task RunAsync()
3838

3939
var result1 = await kernel.RunAsync(
4040
ask,
41-
search["SearchAsync"]
41+
search["Search"]
4242
);
4343

4444
var result2 = await kernel.RunAsync(
4545
ask,
46-
search["SearchAsync"],
46+
search["Search"],
4747
sumSkill["Summarize"]
4848
);
4949

5050
var result3 = await kernel.RunAsync(
5151
ask,
52-
search["SearchAsync"],
52+
search["Search"],
5353
sumSkill["Notegen"]
5454
);
5555

samples/dotnet/kernel-syntax-examples/Example06_InlineFunctionDefinition.cs renamed to samples/dotnet/kernel-syntax-examples/Example05_InlineFunctionDefinition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using RepoUtils;
77

88
// ReSharper disable once InconsistentNaming
9-
public static class Example06_InlineFunctionDefinition
9+
public static class Example05_InlineFunctionDefinition
1010
{
1111
public static async Task RunAsync()
1212
{

samples/dotnet/kernel-syntax-examples/Example07_TemplateLanguage.cs renamed to samples/dotnet/kernel-syntax-examples/Example06_TemplateLanguage.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
using System.Threading.Tasks;
55
using Microsoft.SemanticKernel;
66
using Microsoft.SemanticKernel.CoreSkills;
7+
using Microsoft.SemanticKernel.TemplateEngine;
78
using RepoUtils;
89

910
// ReSharper disable once InconsistentNaming
10-
public static class Example07_TemplateLanguage
11+
public static class Example06_TemplateLanguage
1112
{
1213
/// <summary>
1314
/// Show how to invoke a Native Function written in C#
@@ -33,9 +34,40 @@ public static async Task RunAsync()
3334
Is it morning, afternoon, evening, or night (morning/afternoon/evening/night)?
3435
Is it weekend time (weekend/not weekend)?
3536
";
37+
38+
// This allows to see the prompt before it's sent to OpenAI
39+
Console.WriteLine("--- Rendered Prompt");
40+
var promptRenderer = new PromptTemplateEngine();
41+
var renderedPrompt = await promptRenderer.RenderAsync(FUNCTION_DEFINITION, kernel.CreateNewContext());
42+
Console.WriteLine(renderedPrompt);
43+
44+
// Run the prompt / semantic function
3645
var kindOfDay = kernel.CreateSemanticFunction(FUNCTION_DEFINITION, maxTokens: 150);
3746

47+
// Show the result
48+
Console.WriteLine("--- Semantic Function result");
3849
var result = await kindOfDay.InvokeAsync();
3950
Console.WriteLine(result);
51+
52+
/* OUTPUT:
53+
54+
--- Rendered Prompt
55+
56+
Today is: Friday, April 28, 2023
57+
Current time is: 11:04:30 PM
58+
59+
Answer to the following questions using JSON syntax, including the data used.
60+
Is it morning, afternoon, evening, or night (morning/afternoon/evening/night)?
61+
Is it weekend time (weekend/not weekend)?
62+
63+
--- Semantic Function result
64+
65+
{
66+
"date": "Friday, April 28, 2023",
67+
"time": "11:04:30 PM",
68+
"period": "night",
69+
"weekend": "weekend"
70+
}
71+
*/
4072
}
4173
}

0 commit comments

Comments
 (0)