Skip to content

Commit 1625192

Browse files
markwallace-microsoftdmytrostrukRogerBarreto
authored andcommitted
.Net: Merge Tavily feature branch to main (microsoft#11227)
### Motivation and Context Add a TextSearch implementation using the Tavily Search API - [x] Initial implementation with search - [x] Support for image search - [x] Integration tests Closes microsoft#11060 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄 --------- Co-authored-by: Dmytro Struk <[email protected]> Co-authored-by: Roger Barreto <[email protected]>
1 parent 712f466 commit 1625192

18 files changed

+1623
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json;
4+
using Microsoft.SemanticKernel.Data;
5+
using Microsoft.SemanticKernel.Plugins.Web.Tavily;
6+
7+
namespace Search;
8+
9+
/// <summary>
10+
/// This example shows how to create and use a <see cref="TavilyTextSearch"/>.
11+
/// </summary>
12+
public class Tavily_TextSearch(ITestOutputHelper output) : BaseTest(output)
13+
{
14+
/// <summary>
15+
/// Show how to create a <see cref="TavilyTextSearch"/> and use it to perform a text search.
16+
/// </summary>
17+
[Fact]
18+
public async Task UsingTavilyTextSearch()
19+
{
20+
// Create a logging handler to output HTTP requests and responses
21+
LoggingHandler handler = new(new HttpClientHandler(), this.Output);
22+
using HttpClient httpClient = new(handler);
23+
24+
// Create an ITextSearch instance using Tavily search
25+
var textSearch = new TavilyTextSearch(apiKey: TestConfiguration.Tavily.ApiKey, options: new() { HttpClient = httpClient, IncludeRawContent = true });
26+
27+
var query = "What is the Semantic Kernel?";
28+
29+
// Search and return results as a string items
30+
KernelSearchResults<string> stringResults = await textSearch.SearchAsync(query, new() { Top = 4 });
31+
Console.WriteLine("--- String Results ---\n");
32+
await foreach (string result in stringResults.Results)
33+
{
34+
Console.WriteLine(result);
35+
WriteHorizontalRule();
36+
}
37+
38+
// Search and return results as TextSearchResult items
39+
KernelSearchResults<TextSearchResult> textResults = await textSearch.GetTextSearchResultsAsync(query, new() { Top = 4 });
40+
Console.WriteLine("\n--- Text Search Results ---\n");
41+
await foreach (TextSearchResult result in textResults.Results)
42+
{
43+
Console.WriteLine($"Name: {result.Name}");
44+
Console.WriteLine($"Value: {result.Value}");
45+
Console.WriteLine($"Link: {result.Link}");
46+
WriteHorizontalRule();
47+
}
48+
49+
// Search and return s results as TavilySearchResult items
50+
KernelSearchResults<object> fullResults = await textSearch.GetSearchResultsAsync(query, new() { Top = 4 });
51+
Console.WriteLine("\n--- Tavily Web Page Results ---\n");
52+
await foreach (TavilySearchResult result in fullResults.Results)
53+
{
54+
Console.WriteLine($"Name: {result.Title}");
55+
Console.WriteLine($"Content: {result.Content}");
56+
Console.WriteLine($"Url: {result.Url}");
57+
Console.WriteLine($"RawContent: {result.RawContent}");
58+
Console.WriteLine($"Score: {result.Score}");
59+
WriteHorizontalRule();
60+
}
61+
}
62+
63+
/// <summary>
64+
/// Show how to create a <see cref="TavilyTextSearch"/> and use it to perform a text search which returns an answer.
65+
/// </summary>
66+
[Fact]
67+
public async Task UsingTavilyTextSearchToGetAnAnswer()
68+
{
69+
// Create a logging handler to output HTTP requests and responses
70+
LoggingHandler handler = new(new HttpClientHandler(), this.Output);
71+
using HttpClient httpClient = new(handler);
72+
73+
// Create an ITextSearch instance using Tavily search
74+
var textSearch = new TavilyTextSearch(apiKey: TestConfiguration.Tavily.ApiKey, options: new() { HttpClient = httpClient, IncludeAnswer = true });
75+
76+
var query = "What is the Semantic Kernel?";
77+
78+
// Search and return results as a string items
79+
KernelSearchResults<string> stringResults = await textSearch.SearchAsync(query, new() { Top = 1 });
80+
Console.WriteLine("--- String Results ---\n");
81+
await foreach (string result in stringResults.Results)
82+
{
83+
Console.WriteLine(result);
84+
WriteHorizontalRule();
85+
}
86+
}
87+
88+
/// <summary>
89+
/// Show how to create a <see cref="TavilyTextSearch"/> and use it to perform a text search.
90+
/// </summary>
91+
[Fact]
92+
public async Task UsingTavilyTextSearchAndIncludeEverything()
93+
{
94+
// Create a logging handler to output HTTP requests and responses
95+
LoggingHandler handler = new(new HttpClientHandler(), this.Output);
96+
using HttpClient httpClient = new(handler);
97+
98+
// Create an ITextSearch instance using Tavily search
99+
var textSearch = new TavilyTextSearch(
100+
apiKey: TestConfiguration.Tavily.ApiKey,
101+
options: new()
102+
{
103+
HttpClient = httpClient,
104+
IncludeRawContent = true,
105+
IncludeImages = true,
106+
IncludeImageDescriptions = true,
107+
IncludeAnswer = true,
108+
});
109+
110+
var query = "What is the Semantic Kernel?";
111+
112+
// Search and return s results as TavilySearchResult items
113+
KernelSearchResults<object> fullResults = await textSearch.GetSearchResultsAsync(query, new() { Top = 4, Skip = 0 });
114+
Console.WriteLine("\n--- Tavily Web Page Results ---\n");
115+
await foreach (TavilySearchResult result in fullResults.Results)
116+
{
117+
Console.WriteLine($"Name: {result.Title}");
118+
Console.WriteLine($"Content: {result.Content}");
119+
Console.WriteLine($"Url: {result.Url}");
120+
Console.WriteLine($"RawContent: {result.RawContent}");
121+
Console.WriteLine($"Score: {result.Score}");
122+
WriteHorizontalRule();
123+
}
124+
}
125+
126+
/// <summary>
127+
/// Show how to create a <see cref="TavilyTextSearch"/> with a custom mapper and use it to perform a text search.
128+
/// </summary>
129+
[Fact]
130+
public async Task UsingTavilyTextSearchWithACustomMapperAsync()
131+
{
132+
// Create a logging handler to output HTTP requests and responses
133+
LoggingHandler handler = new(new HttpClientHandler(), this.Output);
134+
using HttpClient httpClient = new(handler);
135+
136+
// Create an ITextSearch instance using Tavily search
137+
var textSearch = new TavilyTextSearch(apiKey: TestConfiguration.Tavily.ApiKey, options: new()
138+
{
139+
HttpClient = httpClient,
140+
StringMapper = new TestTextSearchStringMapper(),
141+
});
142+
143+
var query = "What is the Semantic Kernel?";
144+
145+
// Search with TextSearchResult textResult type
146+
KernelSearchResults<string> stringResults = await textSearch.SearchAsync(query, new() { Top = 2 });
147+
Console.WriteLine("--- Serialized JSON Results ---");
148+
await foreach (string result in stringResults.Results)
149+
{
150+
Console.WriteLine(result);
151+
WriteHorizontalRule();
152+
}
153+
}
154+
155+
/// <summary>
156+
/// Show how to create a <see cref="TavilyTextSearch"/> with a custom mapper and use it to perform a text search.
157+
/// </summary>
158+
[Fact]
159+
public async Task UsingTavilyTextSearchWithAnIncludeDomainFilterAsync()
160+
{
161+
// Create a logging handler to output HTTP requests and responses
162+
LoggingHandler handler = new(new HttpClientHandler(), this.Output);
163+
using HttpClient httpClient = new(handler);
164+
165+
// Create an ITextSearch instance using Tavily search
166+
var textSearch = new TavilyTextSearch(apiKey: TestConfiguration.Tavily.ApiKey, options: new()
167+
{
168+
HttpClient = httpClient,
169+
StringMapper = new TestTextSearchStringMapper(),
170+
});
171+
172+
var query = "What is the Semantic Kernel?";
173+
174+
// Search with TextSearchResult textResult type
175+
TextSearchOptions searchOptions = new() { Top = 4, Filter = new TextSearchFilter().Equality("include_domain", "devblogs.microsoft.com") };
176+
KernelSearchResults<TextSearchResult> textResults = await textSearch.GetTextSearchResultsAsync(query, searchOptions);
177+
Console.WriteLine("--- Microsoft Developer Blogs Results ---");
178+
await foreach (TextSearchResult result in textResults.Results)
179+
{
180+
Console.WriteLine(result.Link);
181+
WriteHorizontalRule();
182+
}
183+
}
184+
185+
#region private
186+
/// <summary>
187+
/// Test mapper which converts an arbitrary search result to a string using JSON serialization.
188+
/// </summary>
189+
private sealed class TestTextSearchStringMapper : ITextSearchStringMapper
190+
{
191+
/// <inheritdoc />
192+
public string MapFromResultToString(object result)
193+
{
194+
return JsonSerializer.Serialize(result);
195+
}
196+
}
197+
#endregion
198+
}

dotnet/src/IntegrationTests/Data/BaseTextSearchTests.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace SemanticKernel.IntegrationTests.Data;
1818
/// </summary>
1919
public abstract class BaseTextSearchTests : BaseIntegrationTest
2020
{
21-
[Fact(Skip = "Failing in integration tests pipeline with - HTTP 429 (insufficient_quota) error.")]
21+
[Fact(Skip = "For manual verification only.")]
2222
public virtual async Task CanSearchAsync()
2323
{
2424
// Arrange
@@ -42,7 +42,7 @@ public virtual async Task CanSearchAsync()
4242
}
4343
}
4444

45-
[Fact(Skip = "Failing in integration tests pipeline with - HTTP 429 (insufficient_quota) error.")]
45+
[Fact(Skip = "For manual verification only.")]
4646
public virtual async Task CanGetTextSearchResultsAsync()
4747
{
4848
// Arrange
@@ -72,7 +72,7 @@ public virtual async Task CanGetTextSearchResultsAsync()
7272
}
7373
}
7474

75-
[Fact(Skip = "Failing in integration tests pipeline with - HTTP 429 (insufficient_quota) error.")]
75+
[Fact(Skip = "For manual verification only.")]
7676
public virtual async Task CanGetSearchResultsAsync()
7777
{
7878
// Arrange
@@ -92,7 +92,7 @@ public virtual async Task CanGetSearchResultsAsync()
9292
Assert.True(this.VerifySearchResults(results, query));
9393
}
9494

95-
[Fact(Skip = "Failing in integration tests pipeline with - HTTP 429 (insufficient_quota) error.")]
95+
[Fact(Skip = "For manual verification only.")]
9696
public virtual async Task UsingTextSearchWithAFilterAsync()
9797
{
9898
// Arrange
@@ -113,7 +113,7 @@ public virtual async Task UsingTextSearchWithAFilterAsync()
113113
Assert.True(this.VerifySearchResults(results, query, filter));
114114
}
115115

116-
[Fact(Skip = "Failing in integration tests pipeline with - HTTP 429 (insufficient_quota) error.")]
116+
[Fact(Skip = "For manual verification only.")]
117117
public virtual async Task FunctionCallingUsingCreateWithSearchAsync()
118118
{
119119
// Arrange
@@ -142,7 +142,7 @@ public virtual async Task FunctionCallingUsingCreateWithSearchAsync()
142142
Assert.NotEmpty(results);
143143
}
144144

145-
[Fact(Skip = "Failing in integration tests pipeline with - HTTP 429 (insufficient_quota) error.")]
145+
[Fact(Skip = "For manual verification only.")]
146146
public virtual async Task FunctionCallingUsingCreateWithGetSearchResultsAsync()
147147
{
148148
// Arrange
@@ -171,7 +171,7 @@ public virtual async Task FunctionCallingUsingCreateWithGetSearchResultsAsync()
171171
Assert.NotEmpty(results);
172172
}
173173

174-
[Fact(Skip = "Failing in integration tests pipeline with - HTTP 429 (insufficient_quota) error.")]
174+
[Fact(Skip = "For manual verification only.")]
175175
public virtual async Task FunctionCallingUsingGetTextSearchResultsAsync()
176176
{
177177
// Arrange
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Threading.Tasks;
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.SemanticKernel.Data;
6+
using Microsoft.SemanticKernel.Plugins.Web.Tavily;
7+
using SemanticKernel.IntegrationTests.Data;
8+
using SemanticKernel.IntegrationTests.TestSettings;
9+
using Xunit;
10+
11+
namespace SemanticKernel.IntegrationTests.Plugins.Web.Tavily;
12+
13+
/// <summary>
14+
/// Integration tests for <see cref="TavilyTextSearch"/>.
15+
/// </summary>
16+
public class TavilyTextSearchTests : BaseTextSearchTests
17+
{
18+
/// <inheritdoc/>
19+
public override Task<ITextSearch> CreateTextSearchAsync()
20+
{
21+
var configuration = this.Configuration.GetSection("Tavily").Get<TavilyConfiguration>();
22+
Assert.NotNull(configuration);
23+
Assert.NotNull(configuration.ApiKey);
24+
25+
return Task.FromResult<ITextSearch>(new TavilyTextSearch(apiKey: configuration.ApiKey));
26+
}
27+
28+
/// <inheritdoc/>
29+
public override string GetQuery() => "What is the Semantic Kernel?";
30+
31+
/// <inheritdoc/>
32+
public override TextSearchFilter GetTextSearchFilter() => new TextSearchFilter().Equality("include_domain", "devblogs.microsoft.com");
33+
34+
/// <inheritdoc/>
35+
public override bool VerifySearchResults(object[] results, string query, TextSearchFilter? filter = null)
36+
{
37+
Assert.NotNull(results);
38+
Assert.NotEmpty(results);
39+
Assert.Equal(4, results.Length);
40+
foreach (var result in results)
41+
{
42+
Assert.NotNull(result);
43+
Assert.IsType<TavilySearchResult>(result);
44+
}
45+
46+
return true;
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
namespace SemanticKernel.IntegrationTests.TestSettings;
3+
4+
#pragma warning disable CA1812 // Configuration classes are instantiated through IConfiguration.
5+
internal sealed class TavilyConfiguration(string apiKey)
6+
{
7+
public string ApiKey { get; init; } = apiKey;
8+
}

dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs

+7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public static void Initialize(IConfigurationRoot configRoot)
3535
public static PineconeConfig Pinecone => LoadSection<PineconeConfig>();
3636
public static BingConfig Bing => LoadSection<BingConfig>();
3737
public static GoogleConfig Google => LoadSection<GoogleConfig>();
38+
public static TavilyConfig Tavily => LoadSection<TavilyConfig>();
3839
public static GithubConfig Github => LoadSection<GithubConfig>();
3940
public static PostgresConfig Postgres => LoadSection<PostgresConfig>();
4041
public static RedisConfig Redis => LoadSection<RedisConfig>();
@@ -176,6 +177,12 @@ public class GoogleConfig
176177
public string SearchEngineId { get; set; }
177178
}
178179

180+
public class TavilyConfig
181+
{
182+
public string Endpoint { get; set; } = "https://api.tavily.com/search";
183+
public string ApiKey { get; set; }
184+
}
185+
179186
public class GithubConfig
180187
{
181188
public string PAT { get; set; }

dotnet/src/Plugins/Plugins.UnitTests/Plugins.UnitTests.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,9 @@
4444
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
4545
</None>
4646
</ItemGroup>
47+
48+
<ItemGroup>
49+
<Folder Include="Web\Tavily\" />
50+
</ItemGroup>
4751

4852
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"query": "What is the Semantic Kernel?",
3+
"follow_up_questions": null,
4+
"answer": null,
5+
"images": [],
6+
"results": [
7+
{
8+
"title": "Hello, Semantic Kernel! | Semantic Kernel - devblogs.microsoft.com",
9+
"url": "https://devblogs.microsoft.com/semantic-kernel/hello-world/",
10+
"content": "Semantic Kernel (SK) is a lightweight SDK that lets you mix conventional programming languages, like C# and Python, with the latest in Large Language Model (LLM) AI \"prompts\" with prompt templating, chaining, and planning capabilities. This enables you to build new experiences into your apps to bring unparalleled productivity for your users",
11+
"score": 0.944878,
12+
"raw_content": null
13+
},
14+
{
15+
"title": "Building Generative AI apps with .NET 8 and Semantic Kernel",
16+
"url": "https://devblogs.microsoft.com/semantic-kernel/building-generative-ai-apps-with-net-8-and-semantic-kernel/",
17+
"content": "What is Semantic Kernel and why should I use it? In many of our samples, you'll see us using Semantic Kernel (SK) - SK is an open-source library that lets you easily build AI solutions that can call your existing code. As a highly extensible SDK, you can use Semantic Kernel to work with models from OpenAI, Azure OpenAI, Hugging Face, and",
18+
"score": 0.8412138,
19+
"raw_content": null
20+
},
21+
{
22+
"title": "Semantic Kernel and AutoGen Part 2 - devblogs.microsoft.com",
23+
"url": "https://devblogs.microsoft.com/semantic-kernel/semantic-kernel-and-autogen-part-2/",
24+
"content": "Following on from our blog post a couple months ago: Microsoft's Agentic AI Frameworks: AutoGen and Semantic Kernel, Microsoft's agentic AI story is evolving at a steady pace.Both Azure AI Foundry's Semantic Kernel and AI Frontier's AutoGen are designed to empower developers to build advanced multi-agent systems. The AI Frontier's team is charging ahead pushing the boundaries of",
25+
"score": 0.78631157,
26+
"raw_content": null
27+
},
28+
{
29+
"title": "Introducing Semantic Kernel for Java | Semantic Kernel",
30+
"url": "https://devblogs.microsoft.com/semantic-kernel/introducing-semantic-kernel-for-java/",
31+
"content": "Semantic Kernel for Java is an open source library that empowers developers to harness the power of AI while coding in Java. It is compatible with Java 8 and above, ensuring flexibility and accessibility to a wide range of Java developers. By integrating AI services into your Java applications, you can unlock the full potential of artificial",
32+
"score": 0.7703443,
33+
"raw_content": null
34+
}
35+
],
36+
"response_time": 3.16
37+
}

0 commit comments

Comments
 (0)