Skip to content

.Net: Added BraveConnector in WebSearchPlugin #11308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
957d6ac
.Net: Add Brave API connector and options for web search functionalit…
N-E-W-T-O-N Apr 1, 2025
af06b37
.Net: Add Brave search URL functions for various query types
N-E-W-T-O-N Apr 1, 2025
e5c6c38
.Net: Add BraveTextSearch registration methods to WebKernelBuilder an…
N-E-W-T-O-N Apr 1, 2025
29d4882
.Net: Update BraveConnector to handle video results and fix query par…
N-E-W-T-O-N Apr 1, 2025
45a1a9c
.Net:
N-E-W-T-O-N Apr 1, 2025
644b27c
Add support for additional query types in Brave search functionality
N-E-W-T-O-N Apr 1, 2025
4f6a1a0
Add UseGzip property to BraveTextSearchOptions for response data decr…
N-E-W-T-O-N Apr 1, 2025
e760877
Add BraveWebResult class to represent web search results
N-E-W-T-O-N Apr 1, 2025
2d14978
Merge branch 'microsoft:main' into brave
N-E-W-T-O-N Apr 5, 2025
73cf9a1
.Net: Remove the Unwanted overide method
N-E-W-T-O-N Apr 5, 2025
b916dd3
.NET : Removed property UseGzip from BraveTextSearchOptions & fix mi…
N-E-W-T-O-N Apr 5, 2025
92f0d2f
Refactor BraveSearchResponse: Remove unwanted class & Properties,Clea…
N-E-W-T-O-N Apr 6, 2025
e45164f
Refactor BraveWebResult: Improve property initialization and update X…
N-E-W-T-O-N Apr 6, 2025
ad30003
Refactor BraveSearchResponse: Remove unused properties and clean up c…
N-E-W-T-O-N Apr 6, 2025
54e33e2
.NET:Refactor BraveTextSearch: Improve code readability, add query va…
N-E-W-T-O-N Apr 6, 2025
0d302e1
.NET:Add test data for Brave search results in JSON format.
N-E-W-T-O-N Apr 6, 2025
c945f44
.NET:Add Brave search URL test cases to SearchUrlPluginTests
N-E-W-T-O-N Apr 6, 2025
cc6379a
.NET:Add unit tests for BraveTextSearch functionality and response ha…
N-E-W-T-O-N Apr 6, 2025
416c515
Merge branch 'main' into brave
RogerBarreto Apr 11, 2025
b22e471
Fix warnings
RogerBarreto Apr 11, 2025
c3f56e0
remove comment
RogerBarreto Apr 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dotnet/src/InternalUtilities/src/Http/HttpClientProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
#if NET
using System.Net.Security;
Expand All @@ -26,6 +27,12 @@ internal static class HttpClientProvider
/// <returns>An instance of HttpClient.</returns>
public static HttpClient GetHttpClient() => new(NonDisposableHttpClientHandler.Instance, disposeHandler: false);

/// <summary>
/// Retrieves an instance of HttpClient.
/// </summary>
/// <param name="handler"></param>
/// <returns>An instance of HttpClient.</returns>
public static HttpClient GetHttpClient(HttpMessageHandler handler) => new(handler, disposeHandler: false);
/// <summary>
/// Retrieves an instance of HttpClient.
/// </summary>
Expand Down
160 changes: 160 additions & 0 deletions dotnet/src/Plugins/Plugins.Web/Brave/BraveConnector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Http;

namespace Microsoft.SemanticKernel.Plugins.Web.Brave;

/// <summary>
/// Brave API connector.
/// </summary>
public sealed class BraveConnector : IWebSearchEngineConnector
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
private readonly string? _apiKey;
private readonly Uri? _uri = null;
private const string DefaultUri = "https://api.search.brave.com/res/v1/web/search?q";

/// <summary>
/// Initializes a new instance of the <see cref="BraveConnector"/> class.
/// </summary>
/// <param name="apiKey">The API key to authenticate the connector.</param>
/// <param name="uri">The URI of the Bing Search instance. Defaults to "https://api.bing.microsoft.com/v7.0/search?q".</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
public BraveConnector(string apiKey, Uri? uri = null, ILoggerFactory? loggerFactory = null) :
this(apiKey, HttpClientProvider.GetHttpClient(), uri, loggerFactory)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="BraveConnector"/> class.
/// </summary>
/// <param name="apiKey">The API key to authenticate the connector.</param>
/// <param name="httpClient">The HTTP client to use for making requests.</param>
/// <param name="uri">The URI of the Bing Search instance. Defaults to "https://api.bing.microsoft.com/v7.0/search?q".</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
public BraveConnector(string apiKey, HttpClient httpClient, Uri? uri = null, ILoggerFactory? loggerFactory = null)
{
Verify.NotNull(httpClient);

this._apiKey = apiKey;
this._logger = loggerFactory?.CreateLogger(typeof(BraveConnector)) ?? NullLogger.Instance;
this._httpClient = httpClient;
this._httpClient.DefaultRequestHeaders.Add("User-Agent", HttpHeaderConstant.Values.UserAgent);
this._httpClient.DefaultRequestHeaders.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(BraveConnector)));
this._uri = uri ?? new Uri(DefaultUri);
}

/// <inheritdoc/>
public async Task<IEnumerable<T>> SearchAsync<T>(string query, int count = 1, int offset = 0, CancellationToken cancellationToken = default)
{
Verify.NotNull(query) ;

if (count is <= 0 or >= 21)
{
throw new ArgumentOutOfRangeException(nameof(count), count, $"{nameof(count)} value must be greater than 0 and less than 21.");
}

if (offset is < 0 or > 10)
{
throw new ArgumentOutOfRangeException(nameof(offset),offset, $"{nameof(count)} value must be equal or greater than 0 and less than 10.");
}

Uri uri = new($"{this._uri}={Uri.EscapeDataString(query.Trim())}&count={count}&offset={offset}");

this._logger.LogDebug("Sending request: {Uri}", uri);

using HttpResponseMessage response = await this.SendGetRequestAsync(uri, cancellationToken).ConfigureAwait(false);

this._logger.LogDebug("Response received: {StatusCode}", response.StatusCode);

string json = await response.Content.ReadAsStringWithExceptionMappingAsync(cancellationToken).ConfigureAwait(false);

// Sensitive data, logging as trace, disabled by default
this._logger.LogTrace("Response content received: {Data}", json);

var data = JsonSerializer.Deserialize<BraveSearchResponse<BraveWebResult>>(json);

List<T>? returnValues = null;
if (data?.Web?.Results is not null)
{
if (typeof(T) == typeof(string))
{
var results = data?.Web?.Results;
returnValues = results?.Select(x => x.Description).ToList() as List<T>;
}
else if (typeof(T) == typeof(BraveWebResult))
{
var results = data?.Web?.Results!;
returnValues = results.Take(count).ToList() as List<T>;
}
else if (typeof(T) == typeof(WebPage))
{
List<WebPage>? webPages = data.Web?.Results
.Select(x => new WebPage() { Name = x.Title, Snippet = x.Description, Url = x.Url }).ToList();

returnValues = webPages!.Take(count).ToList() as List<T>;
}
else
{
throw new NotSupportedException($"Type {typeof(T)} is not supported.");
}
}

if (data?.Videos?.Results is not null)
{
if (typeof(T) == typeof(string))
{
var results = data?.Videos?.Results;
returnValues = results?.Select(x => x.Description).ToList() as List<T>;
}
else if (typeof(T) == typeof(BraveWebResult))
{
var results = data?.Videos?.Results!;
returnValues = results.Take(count).ToList() as List<T>;
}
else if (typeof(T) == typeof(WebPage))
{
List<WebPage>? webPages = data.Videos?.Results
.Select(x => new WebPage() { Name = x.Title, Snippet = x.Description, Url = x.Url }).ToList();

returnValues = webPages!.Take(count).ToList() as List<T>;
}
else
{
throw new NotSupportedException($"Type {typeof(T)} is not supported.");
}
}
return
returnValues is null ? [] :
returnValues.Count <= count ? returnValues :
returnValues.Take(count);
}

/// <summary>
/// Sends a GET request to the specified URI.
/// </summary>
/// <param name="uri">The URI to send the request to.</param>
/// <param name="cancellationToken">A cancellation token to cancel the request.</param>
/// <returns>A <see cref="HttpResponseMessage"/> representing the response from the request.</returns>
private async Task<HttpResponseMessage> SendGetRequestAsync(Uri uri, CancellationToken cancellationToken = default)
{
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri);

if (!string.IsNullOrEmpty(this._apiKey))
{
httpRequestMessage.Headers.Add("X-Subscription-Token", this._apiKey);
}

return await this._httpClient.SendWithSuccessCheckAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
}
}
Loading