Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion src/Extensions/Grok/GrokChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ IChatClient GetChatClient(string modelId) => clients.GetOrAdd(modelId, model
FromDate = tool.FromDate,
ToDate = tool.ToDate,
MaxSearchResults = tool.MaxSearchResults,
Sources = tool.Sources
Sources = tool.Sources,
ReturnCitations = tool.ReturnCitations
};
}

Expand Down Expand Up @@ -133,6 +134,7 @@ class GrokChatWebSearchOptions : global::OpenAI.Chat.ChatWebSearchOptions
public DateOnly? ToDate { get; set; }
public int? MaxSearchResults { get; set; }
public IList<GrokSource>? Sources { get; set; }
public bool? ReturnCitations { get; set; }
}

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
Expand Down
87 changes: 23 additions & 64 deletions src/Extensions/Grok/GrokSearchTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,118 +23,77 @@ public enum GrokSearch
Off
}

/// <summary>
/// Configures Grok's live search capabilities.
/// See https://docs.x.ai/docs/guides/live-search.
/// </summary>
/// <summary>Configures Grok's live search capabilities. See https://docs.x.ai/docs/guides/live-search.</summary>
public class GrokSearchTool(GrokSearch mode) : HostedWebSearchTool
{
/// <summary>
/// Sets the search mode for Grok's live search capabilities.
/// </summary>
/// <summary>Sets the search mode for Grok's live search capabilities.</summary>
public GrokSearch Mode { get; } = mode;
/// <inheritdoc/>
public override string Name => "Live Search";
/// <inheritdoc/>
public override string Description => "Performs live search using X.AI";
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#set-date-range-of-the-search-data
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#set-date-range-of-the-search-data</summary>
public DateOnly? FromDate { get; set; }
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#set-date-range-of-the-search-data
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#set-date-range-of-the-search-data</summary>
public DateOnly? ToDate { get; set; }
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#limit-the-maximum-amount-of-data-sources
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#limit-the-maximum-amount-of-data-sources</summary>
public int? MaxSearchResults { get; set; }
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#data-sources-and-parameters
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#data-sources-and-parameters</summary>
public IList<GrokSource>? Sources { get; set; }
/// <summary>See https://docs.x.ai/docs/guides/live-search#returning-citations</summary>
public bool? ReturnCitations { get; set; }
}

/// <summary>
/// Grok Live Search data source base type.
/// </summary>
/// <summary>Grok Live Search data source base type.</summary>
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(GrokWebSource), "web")]
[JsonDerivedType(typeof(GrokNewsSource), "news")]
[JsonDerivedType(typeof(GrokRssSource), "rss")]
[JsonDerivedType(typeof(GrokXSource), "x")]
public abstract class GrokSource { }

/// <summary>
/// Search-based data source base class providing common properties for `web` and `news` sources.
/// </summary>
/// <summary>Search-based data source base class providing common properties for `web` and `news` sources.</summary>
public abstract class GrokSearchSource : GrokSource
{
/// <summary>
/// Include data from a specific country/region by specifying the ISO alpha-2 code of the country.
/// </summary>
/// <summary>Include data from a specific country/region by specifying the ISO alpha-2 code of the country.</summary>
public string? Country { get; set; }
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#parameter-safe_search-supported-by-web-and-news
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#parameter-safe_search-supported-by-web-and-news</summary>
public bool? SafeSearch { get; set; }
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#parameter-excluded_websites-supported-by-web-and-news
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#parameter-excluded_websites-supported-by-web-and-news</summary>
public IList<string>? ExcludedWebsites { get; set; }
}

/// <summary>
/// Web live search source.
/// </summary>
/// <summary>Web live search source.</summary>
public class GrokWebSource : GrokSearchSource
{
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#parameter-allowed_websites-supported-by-web
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#parameter-allowed_websites-supported-by-web</summary>
public IList<string>? AllowedWebsites { get; set; }
}

/// <summary>
/// News live search source.
/// </summary>
/// <summary>News live search source.</summary>
public class GrokNewsSource : GrokSearchSource { }

/// <summary>
/// RSS live search source.
/// </summary>
/// <summary>RSS live search source.</summary>
/// <param name="rss">The RSS feed to search.</param>
public class GrokRssSource(string rss) : GrokSource
{
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#parameter-link-supported-by-rss
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#parameter-link-supported-by-rss</summary>
public IList<string>? Links { get; set; } = [rss];
}

/// <summary>
/// X live search source.
/// </summary>
/// <summary>X live search source./summary>
public class GrokXSource : GrokSearchSource
{
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#parameter-excluded_x_handles-supported-by-x
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#parameter-excluded_x_handles-supported-by-x</summary>
[JsonPropertyName("excluded_x_handles")]
public IList<string>? ExcludedHandles { get; set; }
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#parameter-included_x_handles-supported-by-x
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#parameter-included_x_handles-supported-by-x</summary>
[JsonPropertyName("included_x_handles")]
public IList<string>? IncludedHandles { get; set; }
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#parameters-post_favorite_count-and-post_view_count-supported-by-x
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#parameters-post_favorite_count-and-post_view_count-supported-by-x</summary>
[JsonPropertyName("post_favorite_count")]
public int? FavoriteCount { get; set; }
/// <summary>
/// See https://docs.x.ai/docs/guides/live-search#parameters-post_favorite_count-and-post_view_count-supported-by-x
/// </summary>
/// <summary>See https://docs.x.ai/docs/guides/live-search#parameters-post_favorite_count-and-post_view_count-supported-by-x</summary>
[JsonPropertyName("post_view_count")]
public int? ViewCount { get; set; }
}
63 changes: 61 additions & 2 deletions src/Tests/GrokTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public async Task GrokInvokesSpecificSearchUrl()
var requests = new List<JsonNode>();
var responses = new List<JsonNode>();

var grok = new GrokChatClient(Configuration["XAI_API_KEY"]!, "grok-3", OpenAIClientOptions
var grok = new GrokChatClient(Configuration["XAI_API_KEY"]!, "grok-4-fast-non-reasoning", OpenAIClientOptions
.Observable(requests.Add, responses.Add)
.WriteTo(output));

Expand Down Expand Up @@ -236,6 +236,65 @@ public async Task GrokInvokesSpecificSearchUrl()
Assert.True(catedral, "Expected at least one citation to catedralaltapatagonia.com");

// Uses the default model set by the client when we asked for it
Assert.Equal("grok-3", response.ModelId);
Assert.Equal("grok-4-fast-non-reasoning", response.ModelId);
}

[SecretsFact("XAI_API_KEY")]
public async Task CanAvoidCitations()
{
var messages = new Chat()
{
{ "system", "Sos un asistente del Cerro Catedral, usas la funcionalidad de Live Search en el sitio oficial." },
{ "system", $"Hoy es {DateTime.Now.ToString("o")}" },
{ "user", "Que calidad de nieve hay hoy?" },
};

var requests = new List<JsonNode>();
var responses = new List<JsonNode>();

var grok = new GrokChatClient(Configuration["XAI_API_KEY"]!, "grok-4-fast-non-reasoning", OpenAIClientOptions
.Observable(requests.Add, responses.Add)
.WriteTo(output));

var options = new ChatOptions
{
Tools = [new GrokSearchTool(GrokSearch.On)
{
ReturnCitations = false,
Sources =
[
new GrokWebSource
{
AllowedWebsites =
[
"https://catedralaltapatagonia.com",
"https://catedralaltapatagonia.com/parte-de-nieve/",
"https://catedralaltapatagonia.com/tarifas/"
]
},
]
}]
};

var response = await grok.GetResponseAsync(messages, options);
var text = response.Text;

// assert that the request contains the following node
// "search_parameters": {
// "mode": "auto"
// "return_citations": "false"
//}
Assert.All(requests, x =>
{
var search = Assert.IsType<JsonObject>(x["search_parameters"]);
Assert.Equal("on", search["mode"]?.GetValue<string>());
Assert.False(search["return_citations"]?.GetValue<bool>());
});

// Citations are not included
Assert.Single(responses);
var node = responses[0];
Assert.NotNull(node);
Assert.Null(node["citations"]);
}
}