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),
@ -57,4 +67,4 @@ public static class ChatThreadExtensions
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
};
}
}
}

View File

@ -22,7 +22,7 @@
}
<SettingsPanelApp AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)"/>
<SettingsPanelTools />
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.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

@ -769,7 +769,7 @@ public static partial class ManagedConfiguration
var successful = false;
var configuredValue = CloneStringDictionary(configMeta.Default);
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&

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,
};
}
@ -161,7 +162,7 @@ public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalServ
var warningArray = new JsonArray();
foreach (var warning in warnings)
warningArray.Add(warning);
AddIfNotEmpty(metadata, "language", extractedPage.Language);
AddIfNotEmpty(metadata, "published_time", extractedPage.PublishedTime);
AddIfNotEmpty(metadata, "modified_time", extractedPage.ModifiedTime);
@ -172,16 +173,16 @@ public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalServ
metadata["original_content_characters"] = originalContentCharacters;
metadata["returned_content_characters"] = websiteContentAsMarkdown.Length;
}
var content = new JsonObject
{
["text_content"] = websiteContentAsMarkdown,
};
AddIfNotEmpty(content, "title", extractedPage.Title);
AddIfNotEmpty(content, "description", extractedPage.Description);
AddStringArrayIfNotEmpty(content, "authors", extractedPage.Authors);
var result = new JsonObject
{
@ -191,7 +192,7 @@ public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalServ
["content"] = content,
["metadata"] = metadata,
};
return result;
}

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;
@ -66,7 +66,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
ProviderConfidence = providerConfidence,
}, token);
logger.LogInformation("Completed tool execution. ToolName={ToolName}, ToolCallId={ToolCallId}, DurationMs={DurationMs}, Status={Status}", toolName, toolCallId, stopwatch.ElapsedMilliseconds, ToolInvocationTraceStatus.SUCCESS);
var resultModelContent = result.ToModelContent();
var toolInvocationTrace = new ToolInvocationTrace
{
@ -82,8 +82,8 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
Result =
implementation.FormatTraceResult(result.ToModelContent()),
};
return (resultModelContent, toolInvocationTrace);
return (resultModelContent, toolInvocationTrace, result.RequiredProviderConfidence);
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
@ -104,9 +104,9 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
StatusMessage = exception.Message,
Arguments = formattedArguments,
Result = exception.Message,
};
return (exception.Message, toolInvocationTrace);
};
return (exception.Message, toolInvocationTrace, ConfidenceLevel.NONE);
}
catch (Exception exception)
{
@ -125,8 +125,8 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
Arguments = formattedArguments,
Result = error,
};
return (error, toolInvocationTrace);
return (error, toolInvocationTrace, ConfidenceLevel.NONE);
}
}

View File

@ -37,7 +37,7 @@ public static class ToolSelectionRules
var toolPolicyPrompt = $"""
# Tool usage instructions:
You have multiple tools available. Each tool has a different purpose and usage policy. Choose wisely and if you are not sure, always ask the user for clarification. You must follow the usage policy of each tool to ensure accurate and reliable results. Here are the usage policies for each tool:
{string.Join(Environment.NewLine+Environment.NewLine, policySections)}
""";

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.