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 AIStudio.Components;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
@ -79,6 +80,18 @@ public sealed record ChatThread
/// </summary>
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>
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
/// </summary>

View File

@ -27,6 +27,17 @@ public static class ChatThreadExtensions
if (chatThread is null)
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.
// Means, we never used RAG or RAG was enabled, but no data sources were selected.
// That's fine as well:
@ -36,7 +47,6 @@ public static class ChatThreadExtensions
//
// Is the provider trusted for data-source security checks?
//
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
var isTrustedProvider = provider switch
{
IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),

View File

@ -1083,48 +1083,54 @@ public abstract class BaseProvider : IProvider, ISecretId
yield break;
}
await ShowToolRuntimeStatusAsync(toolCalls
.Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Function.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Function.Name));
internalMessages.Add(new AssistantToolCallMessage
try
{
Content = responseMessage.Content,
ToolCalls = toolCalls,
});
await ShowToolRuntimeStatusAsync(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)
{
if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS)
internalMessages.Add(new AssistantToolCallMessage
{
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
{
Content = finalResponseInstruction,
Content = toolContent,
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,
});
}
if (currentAssistantContent is not null)
await currentAssistantContent.StreamingEvent();
finally
{
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(
chatModel,
chatThread,
baseInput,
apiParameters,
providerTools,
runnableTools,
toolExecutor,
currentAssistantContent,
@ -308,8 +310,10 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
private async IAsyncEnumerable<ContentStreamChunk> StreamResponsesWithLocalTools(
Model chatModel,
ChatThread chatThread,
IList<object> baseInput,
IDictionary<string, object> apiParameters,
IList<object> providerTools,
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
ToolExecutor toolExecutor,
ContentText? currentAssistantContent,
@ -317,9 +321,16 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
ConfidenceLevel providerConfidence,
[EnumeratorCancellation] CancellationToken token)
{
var providerTools = runnableTools
var localProviderTools = runnableTools
.Select(x => (object)ProviderToolAdapters.ToResponsesTool(x.Definition))
.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
// reasoning items emitted alongside function calls.
var internalItems = new List<object>();
@ -344,7 +355,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
Input = requestInput,
Stream = false,
Store = false,
Tools = finalResponseRequired ? [] : providerTools,
Tools = finalResponseRequired ? [] : effectiveProviderTools,
AdditionalApiParameters = apiParameters,
};
var response = await this.ExecuteResponsesRequest(requestDto, requestedSecret, token);
@ -381,45 +392,51 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
yield break;
}
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)
try
{
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
{
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,
});
}
if (currentAssistantContent is not null)
await currentAssistantContent.StreamingEvent();
finally
{
await ResetToolRuntimeStatusAsync(currentAssistantContent);
}
}
}

View File

@ -136,7 +136,8 @@ public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalServ
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 ConfidenceLevel RequiredProviderConfidence { get; init; } = ConfidenceLevel.NONE;
public string ToModelContent()
{
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 async Task<(string Content, ToolInvocationTrace Trace)> ExecuteAsync(
public async Task<(string Content, ToolInvocationTrace Trace, ConfidenceLevel RequiredProviderConfidence)> ExecuteAsync(
string toolCallId,
string toolName,
string argumentsJson,
@ -49,7 +49,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
StatusMessage = "Tool is not available in the current context.",
Arguments = formattedArguments,
Result = error,
});
}, ConfidenceLevel.NONE);
}
var definition = runnableTool.Definition;
@ -83,7 +83,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
implementation.FormatTraceResult(result.ToModelContent()),
};
return (resultModelContent, toolInvocationTrace);
return (resultModelContent, toolInvocationTrace, result.RequiredProviderConfidence);
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
@ -106,7 +106,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
Result = exception.Message,
};
return (exception.Message, toolInvocationTrace);
return (exception.Message, toolInvocationTrace, ConfidenceLevel.NONE);
}
catch (Exception exception)
{
@ -126,7 +126,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
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)
{
var triedOsSso = false;
var requiredProviderConfidence = ConfidenceLevel.NONE;
HTMLParserWebPage page;
try
{
@ -21,7 +22,14 @@ public sealed class WebPageRetrievalService(HTMLParser htmlParser)
url,
token,
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,
options.UseOsSso ? ExternalWebAuthenticationMode.OS_DEFAULT_CREDENTIALS : ExternalWebAuthenticationMode.NONE,
shouldUseDefaultCredentials: (candidateUrl, addresses) =>
@ -58,6 +66,7 @@ public sealed class WebPageRetrievalService(HTMLParser htmlParser)
Page = page,
ExtractedPage = WebPageContentExtractor.Extract(htmlParser, page.Document, page.FinalUrl),
RetrievedAtUtc = DateTimeOffset.UtcNow,
RequiredProviderConfidence = requiredProviderConfidence,
};
}
@ -245,4 +254,6 @@ public sealed class RetrievedWebPage
public required ExtractedWebPage ExtractedPage { 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": {
"type": "Direct",
"requested": "[9.0.12, )",
"resolved": "9.0.12",
"contentHash": "StA3kyImQHqDo8A8ZHaSxgASbEuT5UIqgeCvK5SzUPj//xE1QSys421J9pEs4cYuIVwq7CJvWSKxtyH7aPr1LA=="
"requested": "[9.0.18, )",
"resolved": "9.0.18",
"contentHash": "ztGVXB28bi8SeplFmAx+4MkqP1ieA4UNzj/M3qyyz5tLa37Ln8x8LuaXdxzzoOdaucjQBKXSdCMFSbpQaNGIEg=="
},
"MudBlazor": {
"type": "Direct",
@ -212,6 +212,6 @@
"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)
- 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.

View File

@ -78,7 +78,7 @@ Example:
"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": {
"name": "get_current_weather",
"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.
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
@ -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.
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
Treat model-provided tool arguments as untrusted input.