Codex-based code review

This commit is contained in:
Peer Schütt 2026-07-20 17:46:40 +02:00
parent c827ba2cd8
commit c13255fe77
14 changed files with 156 additions and 93 deletions

View File

@ -2,6 +2,7 @@ using System.Globalization;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AIStudio.Components; using AIStudio.Components;
using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools; using AIStudio.Tools;
@ -79,6 +80,18 @@ public sealed record ChatThread
/// </summary> /// </summary>
public DataSourceSecurity DataSecurity { get; set; } = DataSourceSecurity.NOT_SPECIFIED; public DataSourceSecurity DataSecurity { get; set; } = DataSourceSecurity.NOT_SPECIFIED;
/// <summary>
/// The minimum confidence required for providers that continue this chat after a tool returned sensitive data.
/// </summary>
[JsonInclude]
public ConfidenceLevel RequiredProviderConfidence { get; private set; } = ConfidenceLevel.NONE;
public void RequireProviderConfidence(ConfidenceLevel minimumProviderConfidence)
{
if (minimumProviderConfidence > this.RequiredProviderConfidence)
this.RequiredProviderConfidence = minimumProviderConfidence;
}
/// <summary> /// <summary>
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user. /// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
/// </summary> /// </summary>

View File

@ -27,6 +27,17 @@ public static class ChatThreadExtensions
if (chatThread is null) if (chatThread is null)
return true; return true;
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
var providerConfidence = provider switch
{
IProvider p => p.Provider.GetConfidence(settingsManager).Level,
AIStudio.Settings.Provider p => p.UsedLLMProvider.GetConfidence(settingsManager).Level,
_ => ConfidenceLevel.UNKNOWN,
};
if (providerConfidence < chatThread.RequiredProviderConfidence)
return false;
// The chat thread is available, but the data security is not specified. // The chat thread is available, but the data security is not specified.
// Means, we never used RAG or RAG was enabled, but no data sources were selected. // Means, we never used RAG or RAG was enabled, but no data sources were selected.
// That's fine as well: // That's fine as well:
@ -36,7 +47,6 @@ public static class ChatThreadExtensions
// //
// Is the provider trusted for data-source security checks? // Is the provider trusted for data-source security checks?
// //
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
var isTrustedProvider = provider switch var isTrustedProvider = provider switch
{ {
IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),

View File

@ -1083,48 +1083,54 @@ public abstract class BaseProvider : IProvider, ISecretId
yield break; yield break;
} }
await ShowToolRuntimeStatusAsync(toolCalls try
.Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Function.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Function.Name));
internalMessages.Add(new AssistantToolCallMessage
{ {
Content = responseMessage.Content, await ShowToolRuntimeStatusAsync(toolCalls
ToolCalls = toolCalls, .Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Function.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Function.Name));
});
foreach (var toolCall in toolCalls) internalMessages.Add(new AssistantToolCallMessage
{
if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS)
{ {
var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction(); Content = responseMessage.Content,
ToolCalls = toolCalls,
});
foreach (var toolCall in toolCalls)
{
if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS)
{
var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction();
internalMessages.Add(new ToolResultMessage
{
Content = finalResponseInstruction,
ToolCallId = toolCall.Id,
});
continue;
}
toolCallCount++;
var (toolContent, trace, requiredProviderConfidence) = await toolExecutor.ExecuteAsync(
toolCall.Id,
toolCall.Function.Name,
toolCall.Function.Arguments,
runnableTools,
this.Provider.GetConfidence(settingsManager).Level,
toolCallCount,
token);
chatThread.RequireProviderConfidence(requiredProviderConfidence);
currentAssistantContent?.ToolInvocations.Add(trace);
internalMessages.Add(new ToolResultMessage internalMessages.Add(new ToolResultMessage
{ {
Content = finalResponseInstruction, Content = toolContent,
ToolCallId = toolCall.Id, ToolCallId = toolCall.Id,
}); });
continue;
} }
toolCallCount++;
var (toolContent, trace) = await toolExecutor.ExecuteAsync(
toolCall.Id,
toolCall.Function.Name,
toolCall.Function.Arguments,
runnableTools,
this.Provider.GetConfidence(settingsManager).Level,
toolCallCount,
token);
currentAssistantContent?.ToolInvocations.Add(trace);
internalMessages.Add(new ToolResultMessage
{
Content = toolContent,
ToolCallId = toolCall.Id,
});
} }
finally
if (currentAssistantContent is not null) {
await currentAssistantContent.StreamingEvent(); await ResetToolRuntimeStatusAsync();
}
} }
} }

View File

@ -227,8 +227,10 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
{ {
await foreach (var content in this.StreamResponsesWithLocalTools( await foreach (var content in this.StreamResponsesWithLocalTools(
chatModel, chatModel,
chatThread,
baseInput, baseInput,
apiParameters, apiParameters,
providerTools,
runnableTools, runnableTools,
toolExecutor, toolExecutor,
currentAssistantContent, currentAssistantContent,
@ -308,8 +310,10 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
private async IAsyncEnumerable<ContentStreamChunk> StreamResponsesWithLocalTools( private async IAsyncEnumerable<ContentStreamChunk> StreamResponsesWithLocalTools(
Model chatModel, Model chatModel,
ChatThread chatThread,
IList<object> baseInput, IList<object> baseInput,
IDictionary<string, object> apiParameters, IDictionary<string, object> apiParameters,
IList<object> providerTools,
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools, IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
ToolExecutor toolExecutor, ToolExecutor toolExecutor,
ContentText? currentAssistantContent, ContentText? currentAssistantContent,
@ -317,9 +321,16 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
ConfidenceLevel providerConfidence, ConfidenceLevel providerConfidence,
[EnumeratorCancellation] CancellationToken token) [EnumeratorCancellation] CancellationToken token)
{ {
var providerTools = runnableTools var localProviderTools = runnableTools
.Select(x => (object)ProviderToolAdapters.ToResponsesTool(x.Definition)) .Select(x => (object)ProviderToolAdapters.ToResponsesTool(x.Definition))
.ToList(); .ToList();
var localFunctionNames = runnableTools
.Select(x => x.Definition.Function.Name)
.ToHashSet(StringComparer.Ordinal);
var effectiveProviderTools = providerTools
.Where(x => x is not ProviderTool providerTool || !localFunctionNames.Contains(providerTool.Type))
.Concat(localProviderTools)
.ToList();
// Preserve every output item required to continue the response, including // Preserve every output item required to continue the response, including
// reasoning items emitted alongside function calls. // reasoning items emitted alongside function calls.
var internalItems = new List<object>(); var internalItems = new List<object>();
@ -344,7 +355,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
Input = requestInput, Input = requestInput,
Stream = false, Stream = false,
Store = false, Store = false,
Tools = finalResponseRequired ? [] : providerTools, Tools = finalResponseRequired ? [] : effectiveProviderTools,
AdditionalApiParameters = apiParameters, AdditionalApiParameters = apiParameters,
}; };
var response = await this.ExecuteResponsesRequest(requestDto, requestedSecret, token); var response = await this.ExecuteResponsesRequest(requestDto, requestedSecret, token);
@ -381,45 +392,51 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
yield break; yield break;
} }
await ShowToolRuntimeStatusAsync(currentAssistantContent, functionCalls try
.Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Name));
foreach (var outputItem in response.Output)
internalItems.Add(outputItem);
foreach (var functionCall in functionCalls)
{ {
if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS) await ShowToolRuntimeStatusAsync(currentAssistantContent, functionCalls
.Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Name));
foreach (var outputItem in response.Output)
internalItems.Add(outputItem);
foreach (var functionCall in functionCalls)
{ {
var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction(); if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS)
{
var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction();
internalItems.Add(new ResponsesFunctionCallOutputItem
{
CallId = functionCall.CallId,
Output = finalResponseInstruction,
});
continue;
}
toolCallCount++;
var (toolContent, trace, requiredProviderConfidence) = await toolExecutor.ExecuteAsync(
functionCall.CallId,
functionCall.Name,
functionCall.Arguments,
runnableTools,
providerConfidence,
toolCallCount,
token);
chatThread.RequireProviderConfidence(requiredProviderConfidence);
currentAssistantContent?.ToolInvocations.Add(trace);
internalItems.Add(new ResponsesFunctionCallOutputItem internalItems.Add(new ResponsesFunctionCallOutputItem
{ {
CallId = functionCall.CallId, CallId = functionCall.CallId,
Output = finalResponseInstruction, Output = toolContent,
}); });
continue;
} }
toolCallCount++;
var (toolContent, trace) = await toolExecutor.ExecuteAsync(
functionCall.CallId,
functionCall.Name,
functionCall.Arguments,
runnableTools,
providerConfidence,
toolCallCount,
token);
currentAssistantContent?.ToolInvocations.Add(trace);
internalItems.Add(new ResponsesFunctionCallOutputItem
{
CallId = functionCall.CallId,
Output = toolContent,
});
} }
finally
if (currentAssistantContent is not null) {
await currentAssistantContent.StreamingEvent(); await ResetToolRuntimeStatusAsync(currentAssistantContent);
}
} }
} }

View File

@ -136,7 +136,8 @@ public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalServ
return new ToolExecutionResult return new ToolExecutionResult
{ {
JsonContent = BuildModelContent(page, extractedPage, retrievedPage.RetrievedAtUtc, markdown, originalContentCharacters, contentTruncated, warnings) JsonContent = BuildModelContent(page, extractedPage, retrievedPage.RetrievedAtUtc, markdown, originalContentCharacters, contentTruncated, warnings),
RequiredProviderConfidence = retrievedPage.RequiredProviderConfidence,
}; };
} }

View File

@ -24,6 +24,8 @@ public sealed class ToolExecutionResult
public JsonNode? JsonContent { get; init; } public JsonNode? JsonContent { get; init; }
public ConfidenceLevel RequiredProviderConfidence { get; init; } = ConfidenceLevel.NONE;
public string ToModelContent() public string ToModelContent()
{ {
if (this.JsonContent is not null) if (this.JsonContent is not null)

View File

@ -9,7 +9,7 @@ namespace AIStudio.Tools.ToolCallingSystem;
public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogger<ToolExecutor> logger) public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogger<ToolExecutor> logger)
{ {
public async Task<(string Content, ToolInvocationTrace Trace)> ExecuteAsync( public async Task<(string Content, ToolInvocationTrace Trace, ConfidenceLevel RequiredProviderConfidence)> ExecuteAsync(
string toolCallId, string toolCallId,
string toolName, string toolName,
string argumentsJson, string argumentsJson,
@ -49,7 +49,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
StatusMessage = "Tool is not available in the current context.", StatusMessage = "Tool is not available in the current context.",
Arguments = formattedArguments, Arguments = formattedArguments,
Result = error, Result = error,
}); }, ConfidenceLevel.NONE);
} }
var definition = runnableTool.Definition; var definition = runnableTool.Definition;
@ -83,7 +83,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
implementation.FormatTraceResult(result.ToModelContent()), implementation.FormatTraceResult(result.ToModelContent()),
}; };
return (resultModelContent, toolInvocationTrace); return (resultModelContent, toolInvocationTrace, result.RequiredProviderConfidence);
} }
catch (OperationCanceledException) when (token.IsCancellationRequested) catch (OperationCanceledException) when (token.IsCancellationRequested)
{ {
@ -106,7 +106,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
Result = exception.Message, Result = exception.Message,
}; };
return (exception.Message, toolInvocationTrace); return (exception.Message, toolInvocationTrace, ConfidenceLevel.NONE);
} }
catch (Exception exception) catch (Exception exception)
{ {
@ -126,7 +126,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
Result = error, Result = error,
}; };
return (error, toolInvocationTrace); return (error, toolInvocationTrace, ConfidenceLevel.NONE);
} }
} }

View File

@ -14,6 +14,7 @@ public sealed class WebPageRetrievalService(HTMLParser htmlParser)
CancellationToken token = default) CancellationToken token = default)
{ {
var triedOsSso = false; var triedOsSso = false;
var requiredProviderConfidence = ConfidenceLevel.NONE;
HTMLParserWebPage page; HTMLParserWebPage page;
try try
{ {
@ -21,7 +22,14 @@ public sealed class WebPageRetrievalService(HTMLParser htmlParser)
url, url,
token, token,
options.TimeoutSeconds, options.TimeoutSeconds,
async (candidateUrl, validationToken) => await ResolveValidatedUrlAddressesAsync(candidateUrl, options, validationToken), async (candidateUrl, validationToken) =>
{
var addresses = await ResolveValidatedUrlAddressesAsync(candidateUrl, options, validationToken);
if (addresses.Any(IsNonPublicAddress))
requiredProviderConfidence = ConfidenceLevel.HIGH;
return addresses;
},
MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES,
options.UseOsSso ? ExternalWebAuthenticationMode.OS_DEFAULT_CREDENTIALS : ExternalWebAuthenticationMode.NONE, options.UseOsSso ? ExternalWebAuthenticationMode.OS_DEFAULT_CREDENTIALS : ExternalWebAuthenticationMode.NONE,
shouldUseDefaultCredentials: (candidateUrl, addresses) => shouldUseDefaultCredentials: (candidateUrl, addresses) =>
@ -58,6 +66,7 @@ public sealed class WebPageRetrievalService(HTMLParser htmlParser)
Page = page, Page = page,
ExtractedPage = WebPageContentExtractor.Extract(htmlParser, page.Document, page.FinalUrl), ExtractedPage = WebPageContentExtractor.Extract(htmlParser, page.Document, page.FinalUrl),
RetrievedAtUtc = DateTimeOffset.UtcNow, RetrievedAtUtc = DateTimeOffset.UtcNow,
RequiredProviderConfidence = requiredProviderConfidence,
}; };
} }
@ -245,4 +254,6 @@ public sealed class RetrievedWebPage
public required ExtractedWebPage ExtractedPage { get; init; } public required ExtractedWebPage ExtractedPage { get; init; }
public required DateTimeOffset RetrievedAtUtc { get; init; } public required DateTimeOffset RetrievedAtUtc { get; init; }
public ConfidenceLevel RequiredProviderConfidence { get; init; } = ConfidenceLevel.NONE;
} }

View File

@ -41,9 +41,9 @@
}, },
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"type": "Direct", "type": "Direct",
"requested": "[9.0.12, )", "requested": "[9.0.18, )",
"resolved": "9.0.12", "resolved": "9.0.18",
"contentHash": "StA3kyImQHqDo8A8ZHaSxgASbEuT5UIqgeCvK5SzUPj//xE1QSys421J9pEs4cYuIVwq7CJvWSKxtyH7aPr1LA==" "contentHash": "ztGVXB28bi8SeplFmAx+4MkqP1ieA4UNzj/M3qyyz5tLa37Ln8x8LuaXdxzzoOdaucjQBKXSdCMFSbpQaNGIEg=="
}, },
"MudBlazor": { "MudBlazor": {
"type": "Direct", "type": "Direct",
@ -212,6 +212,6 @@
"type": "Project" "type": "Project"
} }
}, },
"net9.0/win-x64": {} "net9.0/osx-arm64": {}
} }
} }

View File

@ -1,3 +1,4 @@
# v26.7.4, build 249 (2026-07-xx xx:xx UTC) # v26.7.4, build 249 (2026-07-xx xx:xx UTC)
- Added model-driven Web Search and Read Web Page tools for supported AI models.
- Added organization-wide management for tool availability and all Web Search and Read Web Page settings. - Added organization-wide management for tool availability and all Web Search and Read Web Page settings.

View File

@ -78,7 +78,7 @@ Example:
"demoLabel" "demoLabel"
] ]
}, },
"policyInstructions": "Use this tool only when the user asks for current weather conditions.", "systemPromptInstructions": "Use this tool only when the user asks for current weather conditions.",
"function": { "function": {
"name": "get_current_weather", "name": "get_current_weather",
"descriptionForLLM": "Get the current weather in a given location.", "descriptionForLLM": "Get the current weather in a given location.",
@ -116,7 +116,7 @@ Example:
Use stable lower-case IDs with underscores. Keep `id`, `implementationKey`, and `function.name` identical unless there is a clear compatibility reason not to. Use stable lower-case IDs with underscores. Keep `id`, `implementationKey`, and `function.name` identical unless there is a clear compatibility reason not to.
Keep `function.descriptionForLLM` focused on what the tool does. This value is mapped to the provider's function `description` field and is only shown to the LLM. Put sequencing rules, answer-format guidance, or other behavior instructions in `policyInstructions`. When runnable tools are selected, their non-empty policy text is combined centrally and appended to the effective system prompt. Keep `function.descriptionForLLM` focused on what the tool does. This value is mapped to the provider's function `description` field and is only shown to the LLM. Put sequencing rules, answer-format guidance, or other behavior instructions in `systemPromptInstructions`. When runnable tools are selected, their non-empty policy text is combined centrally and appended to the effective system prompt.
## Implementation ## Implementation
@ -189,6 +189,8 @@ Use `ValidateConfigurationAsync` when a setting needs more than "required field
Use `SensitiveTraceArgumentNames` for model-provided arguments that must not be shown in tool traces. Do not return secrets in `TextContent`, `JsonContent`, exception messages, logs, or trace formatting. Use `SensitiveTraceArgumentNames` for model-provided arguments that must not be shown in tool traces. Do not return secrets in `TextContent`, `JsonContent`, exception messages, logs, or trace formatting.
When a tool returns data that future messages must only send to providers at or above a specific confidence level, set `ToolExecutionResult.RequiredProviderConfidence`. AI Studio persists the highest requirement reached by the chat and applies it to later provider checks.
## Security ## Security
Treat model-provided tool arguments as untrusted input. Treat model-provided tool arguments as untrusted input.