using AIStudio.Tools.ToolCallingSystem; using AIStudio.Tools.ToolCallingSystem.Harness; namespace AIStudio.Provider.OpenAI; /// /// Speaks the OpenAI Responses wire format for the tool calling loop. /// /// /// Function calls arrive as output items and results go back as function call output items, /// correlated by call ID. Unlike Chat Completions, the whole output of a round has to be sent /// back for the next one, reasoning items included, or the API refuses to continue. /// public sealed class ResponsesToolCallingAdapter(Model chatModel, IList baseInput, IDictionary apiParameters, IList providerTools, IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools, Func> executeRequestAsync) : IToolCallingProviderAdapter { private readonly List internalItems = []; private ResponsesResponse? lastResponse; /// /// The tools offered to the model: the provider-native ones plus our local functions. /// /// /// A provider-native tool whose type collides with one of our function names is dropped /// because the model could not tell the two apart. /// private readonly IList effectiveProviderTools = BuildEffectiveProviderTools(providerTools, runnableTools); /// public async Task ExecuteRoundAsync(string? finalResponseInstruction, bool includeTools, CancellationToken token = default) { var requestInput = new List(baseInput); if (finalResponseInstruction is not null && requestInput.FirstOrDefault() is TextMessage systemPrompt) { requestInput[0] = systemPrompt with { Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{finalResponseInstruction}", }; } requestInput.AddRange(this.internalItems); var response = await executeRequestAsync(new ResponsesAPIRequest { Model = chatModel.Id, Input = requestInput, Stream = false, Store = false, Tools = includeTools ? this.effectiveProviderTools : [], AdditionalApiParameters = apiParameters, }, token); if (response is null) return null; this.lastResponse = response; return new ToolCallingRound( response.GetTextOutput(), response.GetFunctionCalls() .Select(call => new ToolCallingRequestedCall( call.CallId ?? string.Empty, call.Name ?? string.Empty, call.Arguments ?? string.Empty, !string.IsNullOrWhiteSpace(call.Name) && ToolExecutor.IsValidArgumentsJson(call.Arguments))) .ToList(), response.GetSources()); } /// public void RecordAssistantTurn() { if (this.lastResponse is null) return; // Every output item, not just the function calls: the API rejects a continuation whose // reasoning items are missing. foreach (var outputItem in this.lastResponse.Output) this.internalItems.Add(outputItem); } /// /// /// The Responses API has no error flag on a function call output, so a failure travels in the /// output like any other result. /// public void RecordToolResult(string callId, string content, bool isError = false) => this.internalItems.Add(new ResponsesFunctionCallOutputItem { CallId = callId, Output = content, }); private static IList BuildEffectiveProviderTools(IList providerTools, IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools) { var localFunctionNames = runnableTools .Select(x => x.Definition.Function.Name) .ToHashSet(StringComparer.Ordinal); return providerTools .Where(x => x is not ProviderTool providerTool || !localFunctionNames.Contains(providerTool.Type)) .Concat(runnableTools.Select(x => (object)ProviderToolAdapters.ToResponsesTool(x.Definition))) .ToList(); } }