Skip to content

Commit 64493d5

Browse files
committed
Expose OpenAI basic country/region web search parameters
This is generic so that it also works with Grok (for country limiting).
1 parent a1fcd35 commit 64493d5

File tree

4 files changed

+153
-0
lines changed

4 files changed

+153
-0
lines changed

readme.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,25 @@ var options = new ChatOptions
6262
var response = await grok.GetResponseAsync(messages, options);
6363
```
6464

65+
We also provide an OpenAI-compatible `WebSearchTool` that can be used to restrict
66+
the search to a specific country in a way that works with both Grok and OpenAI:
67+
68+
```csharp
69+
var options = new ChatOptions
70+
{
71+
Tools = [new WebSearchTool("AR")] // 👈 search in Argentina
72+
};
73+
```
74+
75+
This is equivalent to the following when used with a Grok client:
76+
```csharp
77+
var options = new ChatOptions
78+
{
79+
// 👇 search in Argentina
80+
Tools = [new GrokSearchTool(GrokSearch.On) { Country = "AR" }]
81+
};
82+
```
83+
6584
### Advanced Live Search
6685

6786
To configure advanced live search options, beyond the `On|Auto|Off` settings
@@ -127,9 +146,22 @@ var options = new ChatOptions
127146
};
128147

129148
var response = await chat.GetResponseAsync(messages, options);
149+
```
150+
151+
Similar to the Grok client, we provide the `WebSearchTool` to enable search customization
152+
in OpenAI too:
130153

154+
```csharp
155+
var options = new ChatOptions
156+
{
157+
// 👇 search in Argentina, Bariloche region
158+
Tools = [new WebSearchTool("AR") { Region = "Bariloche" }]
159+
};
131160
```
132161

162+
If country/region hints to the model are not needed, you can use the built-in M.E.AI
163+
`HostedWebSearchTool` instead, which is a more generic tool.
164+
133165
## Observing Request/Response
134166

135167
The underlying HTTP pipeline provided by the Azure SDK allows setting up

src/AI.Tests/OpenAITests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,51 @@ public async Task OpenAIThinks()
6666
Assert.Equal("medium", search["effort"]?.GetValue<string>());
6767
});
6868
}
69+
70+
[SecretsFact("OPENAI_API_KEY")]
71+
public async Task WebSearchCountry()
72+
{
73+
var messages = new Chat()
74+
{
75+
{ "system", "Sos un asistente del Cerro Catedral, usas la funcionalidad de Live Search en el sitio oficial." },
76+
{ "system", $"Hoy es {DateTime.Now.ToString("o")}." },
77+
{ "system",
78+
"""
79+
Web search sources:
80+
https://catedralaltapatagonia.com/parte-de-nieve/
81+
https://catedralaltapatagonia.com/tarifas/
82+
https://catedralaltapatagonia.com/
83+
84+
DO NOT USE https://partediario.catedralaltapatagonia.com/partediario for web search, it's **OBSOLETE**.
85+
"""},
86+
{ "user", "Cuanto cuesta el pase diario en el Catedral hoy?" },
87+
};
88+
89+
var requests = new List<JsonNode>();
90+
var responses = new List<JsonNode>();
91+
92+
var chat = new OpenAIChatClient(Configuration["OPENAI_API_KEY"]!, "gpt-4.1",
93+
OpenAIClientOptions.Observable(requests.Add, responses.Add).WriteTo(output));
94+
95+
var options = new ChatOptions
96+
{
97+
Tools = [new WebSearchTool("AR") { Region = "Bariloche" }]
98+
};
99+
100+
var response = await chat.GetResponseAsync(messages, options);
101+
var text = response.Text;
102+
103+
// Citations include catedralaltapatagonia.com at least as a web search source
104+
Assert.Single(responses);
105+
var node = responses[0];
106+
Assert.NotNull(node);
107+
var citations = Assert.IsType<JsonArray>(node["citations"], false);
108+
var catedral = citations.Where(x => x != null).Any(x => x!.ToString().Contains("catedralaltapatagonia.com", StringComparison.Ordinal));
109+
110+
Assert.True(catedral, "Expected at least one citation to catedralaltapatagonia.com");
111+
112+
// Uses the default model set by the client when we asked for it
113+
Assert.Equal("grok-3", response.ModelId);
114+
}
115+
69116
}

src/AI/Grok/GrokChatClient.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ IChatClient GetChatClient(string modelId) => clients.GetOrAdd(modelId, model
6464
Mode = search.Value
6565
};
6666
}
67+
else if (tool is null && options.Tools?.OfType<WebSearchTool>().FirstOrDefault() is { } web)
68+
{
69+
searchOptions = new GrokChatWebSearchOptions
70+
{
71+
Mode = GrokSearch.Auto,
72+
Sources = [new GrokWebSource { Country = web.Country }]
73+
};
74+
}
6775
else if (tool is null && options.Tools?.OfType<HostedWebSearchTool>().FirstOrDefault() is not null)
6876
{
6977
searchOptions = new GrokChatWebSearchOptions

src/AI/WebSearchTool.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Microsoft.Extensions.AI;
2+
using OpenAI.Responses;
3+
4+
namespace Devlooped.Extensions.AI;
5+
6+
/// <summary>
7+
/// Basic web search tool that can limit the search to a specific country.
8+
/// </summary>
9+
public class WebSearchTool : HostedWebSearchTool
10+
{
11+
Dictionary<string, object?> additionalProperties;
12+
string? region;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="WebSearchTool"/> class with the specified country.
16+
/// </summary>
17+
/// <param name="country">ISO alpha-2 country code.</param>
18+
public WebSearchTool(string country)
19+
{
20+
Country = country;
21+
additionalProperties = new Dictionary<string, object?>
22+
{
23+
{ nameof(WebSearchToolLocation), WebSearchToolLocation.CreateApproximateLocation(country) }
24+
};
25+
}
26+
27+
/// <summary>
28+
/// Sets the user's country for web search results, using the ISO alpha-2 code.
29+
/// </summary>
30+
public string Country { get; }
31+
32+
/// <summary>
33+
/// Optional free text additional information about the region to be used in the search.
34+
/// Not all providers support this property.
35+
/// </summary>
36+
/// <remarks>
37+
/// <para>Support for the <see cref="Region"/> property:</para>
38+
/// <list type="table">
39+
/// <listheader>
40+
/// <term>Provider</term>
41+
/// <term>Supported</term>
42+
/// </listheader>
43+
/// <item>
44+
/// <term>OpenAI</term>
45+
/// <term>Yes (<see href="https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#user-location">user location</see>)</term>
46+
/// </item>
47+
/// <item>
48+
/// <term>Grok</term>
49+
/// <term>No</term>
50+
/// </item>
51+
/// </list>
52+
/// </remarks>
53+
public string? Region
54+
{
55+
get => region;
56+
set
57+
{
58+
region = value;
59+
additionalProperties[nameof(WebSearchToolLocation)] =
60+
WebSearchToolLocation.CreateApproximateLocation(Country, value);
61+
}
62+
}
63+
64+
/// <inheritdoc/>
65+
public override IReadOnlyDictionary<string, object?> AdditionalProperties => additionalProperties;
66+
}

0 commit comments

Comments
 (0)