Add ability to override output when calling the connector (#2445)

This is useful in a situation when the output type is dynamic and is not
available in the swagger

---------

Co-authored-by: Mike Stall <mikestall@hotmail.com>
This commit is contained in:
Jerry Santana 2024-05-31 13:04:28 -06:00 коммит произвёл GitHub
Родитель caa821f071
Коммит d46881019d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
5 изменённых файлов: 191 добавлений и 17 удалений

Просмотреть файл

@ -795,13 +795,26 @@ namespace Microsoft.PowerFx.Connectors
/// <param name="runtimeContext">RuntimeConnectorContext.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Function result.</returns>
public async Task<FormulaValue> InvokeAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken)
public Task<FormulaValue> InvokeAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken)
{
return InvokeAsync(arguments, runtimeContext, null, cancellationToken);
}
/// <summary>
/// Call connector function.
/// </summary>
/// <param name="arguments">Arguments.</param>
/// <param name="runtimeContext">RuntimeConnectorContext.</param>
/// <param name="outputTypeOverride">The output type that should be used during output parsing.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Function result.</returns>
public async Task<FormulaValue> InvokeAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, FormulaType outputTypeOverride, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
runtimeContext.ExecutionLogger?.LogInformation($"Entering in {this.LogFunction(nameof(InvokeAsync))}, with {LogArguments(arguments)}");
FormulaValue formulaValue = await InvokeInternalAsync(arguments, runtimeContext, cancellationToken).ConfigureAwait(false);
FormulaValue formulaValue = await InvokeInternalAsync(arguments, runtimeContext, outputTypeOverride, cancellationToken).ConfigureAwait(false);
runtimeContext.ExecutionLogger?.LogInformation($"Exiting {this.LogFunction(nameof(InvokeAsync))}, returning from {nameof(InvokeInternalAsync)}, with {LogFormulaValue(formulaValue)}");
return formulaValue;
}
@ -812,7 +825,12 @@ namespace Microsoft.PowerFx.Connectors
}
}
internal async Task<FormulaValue> InvokeInternalAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken)
internal Task<FormulaValue> InvokeInternalAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken)
{
return InvokeInternalAsync(arguments, runtimeContext, null, cancellationToken);
}
internal async Task<FormulaValue> InvokeInternalAsync(FormulaValue[] arguments, BaseRuntimeConnectorContext runtimeContext, FormulaType outputTypeOverride, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@ -832,7 +850,7 @@ namespace Microsoft.PowerFx.Connectors
BaseRuntimeConnectorContext context = ReturnParameterType.Binary ? runtimeContext.WithRawResults() : runtimeContext;
ScopedHttpFunctionInvoker invoker = new ScopedHttpFunctionInvoker(DPath.Root.Append(DName.MakeValid(Namespace, out _)), Name, Namespace, new HttpFunctionInvoker(this, context), context.ThrowOnError);
FormulaValue result = await invoker.InvokeAsync(arguments, context, cancellationToken).ConfigureAwait(false);
FormulaValue result = await invoker.InvokeAsync(arguments, context, outputTypeOverride, cancellationToken).ConfigureAwait(false);
FormulaValue formulaValue = await PostProcessResultAsync(result, runtimeContext, invoker, cancellationToken).ConfigureAwait(false);
runtimeContext.ExecutionLogger?.LogDebug($"Exiting {this.LogFunction(nameof(InvokeInternalAsync))}, returning {LogFormulaValue(formulaValue)}");

Просмотреть файл

@ -376,7 +376,7 @@ namespace Microsoft.PowerFx.Connectors
}
}
public async Task<FormulaValue> DecodeResponseAsync(HttpResponseMessage response, bool throwOnError = false)
public async Task<FormulaValue> DecodeResponseAsync(HttpResponseMessage response, FormulaType returnTypeOverride, bool throwOnError = false)
{
// https://github.com/microsoft/Power-Fx/issues/2119
// https://github.com/microsoft/Power-Fx/issues/1172
@ -429,11 +429,17 @@ namespace Microsoft.PowerFx.Connectors
// We only return UO for unknown fields (not declared in swagger file) if compatibility is SwaggerCompatibility
bool returnUnknownRecordFieldAsUO = _function.ConnectorSettings.Compatibility == ConnectorCompatibility.SwaggerCompatibility && _function.ConnectorSettings.ReturnUnknownRecordFieldsAsUntypedObjects;
var typeToUse = _function.ReturnType;
if (returnTypeOverride != null)
{
typeToUse = returnTypeOverride;
}
return string.IsNullOrWhiteSpace(text)
? FormulaValue.NewBlank(_function.ReturnType)
? FormulaValue.NewBlank(typeToUse)
: _returnRawResults
? FormulaValue.New(text)
: FormulaValueJSON.FromJson(text, new FormulaValueJsonSerializerSettings() { ReturnUnknownRecordFieldsAsUntypedObjects = returnUnknownRecordFieldAsUO }, _function.ReturnType);
: FormulaValueJSON.FromJson(text, new FormulaValueJsonSerializerSettings() { ReturnUnknownRecordFieldsAsUntypedObjects = returnUnknownRecordFieldAsUO }, typeToUse);
}
string reasonPhrase = string.IsNullOrEmpty(response.ReasonPhrase) ? string.Empty : $" ({response.ReasonPhrase})";
@ -453,7 +459,7 @@ namespace Microsoft.PowerFx.Connectors
_function.ReturnType);
}
public async Task<FormulaValue> InvokeAsync(IConvertToUTC utcConverter, string cacheScope, FormulaValue[] args, HttpMessageInvoker localInvoker, CancellationToken cancellationToken, bool throwOnError = false)
public async Task<FormulaValue> InvokeAsync(IConvertToUTC utcConverter, string cacheScope, FormulaValue[] args, HttpMessageInvoker localInvoker, CancellationToken cancellationToken, FormulaType expectedType, bool throwOnError = false)
{
cancellationToken.ThrowIfCancellationRequested();
@ -470,17 +476,17 @@ namespace Microsoft.PowerFx.Connectors
});
}
return await ExecuteHttpRequest(cacheScope, throwOnError, request, localInvoker, cancellationToken).ConfigureAwait(false);
return await ExecuteHttpRequest(cacheScope, throwOnError, request, localInvoker, expectedType, cancellationToken).ConfigureAwait(false);
}
public async Task<FormulaValue> InvokeAsync(string url, string cacheScope, HttpMessageInvoker localInvoker, CancellationToken cancellationToken, bool throwOnError = false)
{
cancellationToken.ThrowIfCancellationRequested();
using HttpRequestMessage request = new HttpRequestMessage(_function.HttpMethod, new Uri(url).PathAndQuery);
return await ExecuteHttpRequest(cacheScope, throwOnError, request, localInvoker, cancellationToken).ConfigureAwait(false);
return await ExecuteHttpRequest(cacheScope, throwOnError, request, localInvoker, null, cancellationToken).ConfigureAwait(false);
}
private async Task<FormulaValue> ExecuteHttpRequest(string cacheScope, bool throwOnError, HttpRequestMessage request, HttpMessageInvoker localInvoker, CancellationToken cancellationToken)
private async Task<FormulaValue> ExecuteHttpRequest(string cacheScope, bool throwOnError, HttpRequestMessage request, HttpMessageInvoker localInvoker, FormulaType returnTypeOverride, CancellationToken cancellationToken)
{
HttpMessageInvoker client = localInvoker ?? _httpClient;
HttpResponseMessage response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
@ -494,7 +500,7 @@ namespace Microsoft.PowerFx.Connectors
_logger?.LogInformation($"In {nameof(HttpFunctionInvoker)}.{nameof(ExecuteHttpRequest)}, response status code: {(int)response.StatusCode} {response.StatusCode}");
}
return await DecodeResponseAsync(response, throwOnError).ConfigureAwait(false);
return await DecodeResponseAsync(response, returnTypeOverride, throwOnError).ConfigureAwait(false);
}
}
@ -521,12 +527,12 @@ namespace Microsoft.PowerFx.Connectors
internal HttpFunctionInvoker Invoker => _invoker;
public async Task<FormulaValue> InvokeAsync(FormulaValue[] args, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken)
public async Task<FormulaValue> InvokeAsync(FormulaValue[] args, BaseRuntimeConnectorContext runtimeContext, FormulaType outputTypeOverride, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var localInvoker = runtimeContext.GetInvoker(this.Namespace.Name);
return await _invoker.InvokeAsync(new ConvertToUTC(runtimeContext.TimeZoneInfo), _cacheScope, args, localInvoker, cancellationToken, _throwOnError).ConfigureAwait(false);
return await _invoker.InvokeAsync(new ConvertToUTC(runtimeContext.TimeZoneInfo), _cacheScope, args, localInvoker, cancellationToken, outputTypeOverride, _throwOnError).ConfigureAwait(false);
}
public async Task<FormulaValue> InvokeAsync(string url, BaseRuntimeConnectorContext runtimeContext, CancellationToken cancellationToken)

Просмотреть файл

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.OpenApi.Models;
using Microsoft.PowerFx.Core;
using Microsoft.PowerFx.Core.Tests;
using Microsoft.PowerFx.Core.Types;
using Microsoft.PowerFx.Tests;
using Microsoft.PowerFx.Types;
using Newtonsoft.Json;
@ -317,6 +318,46 @@ namespace Microsoft.PowerFx.Connectors.Tests
Assert.Equal("The results of a Conversation task.", connectorReturnType.Description);
}
[Fact]
public async Task ACSL_InvokeFunctionWithOutputOverride()
{
// this test is asserting that we can provide the expected output type that should be used during deserialization
// this is useful in a situation when the output type is dynamic and is not available in the swagger
using var testConnector = new LoggingTestServer(@"Swagger\TestConnectorDateTimeFormat.json", _output);
OpenApiDocument apiDoc = testConnector._apiDocument;
ConsoleLogger logger = new ConsoleLogger(_output);
PowerFxConfig pfxConfig = new PowerFxConfig(Features.PowerFxV1);
ConnectorFunction function = OpenApiParser.GetFunctions(new ConnectorSettings("ACSL") { Compatibility = ConnectorCompatibility.SwaggerCompatibility }, apiDoc).OrderBy(cf => cf.Name).ToList()[0];
Assert.Equal("AnalyzeConversationTextSubmitJob", function.Name);
Assert.Equal("![createdDateTime`'Created Date':d, displayName:s]", function.ReturnType.ToStringWithDisplayNames());
using var testConnector2 = new LoggingTestServer(@"Swagger\TestConnectorDateTimeFormat.json", _output);
using var httpClient2 = new HttpClient(testConnector2);
testConnector2.SetResponseFromFile(@"Responses\TestConnectorDateTimeFormatResponse.json");
using PowerPlatformConnectorClient client2 = new PowerPlatformConnectorClient("https://lucgen-apim.azure-api.net", "aaa373836ffd4915bf6eefd63d164adc" /* environment Id */, "16e7c181-2f8d-4cae-b1f0-179c5c4e4d8b" /* connectionId */, () => "No Auth", httpClient2)
{
SessionId = "a41bd03b-6c3c-4509-a844-e8c51b61f878",
};
BaseRuntimeConnectorContext context2 = new TestConnectorRuntimeContext("ACSL", client2, console: _output);
DType.TryParse("![createdDateTime:d, displayName:d]", out DType dtype);
var expectedFormulaType = FormulaType.Build(dtype);
FormulaValue httpResult = await function.InvokeAsync(new FormulaValue[0], context2, expectedFormulaType, CancellationToken.None).ConfigureAwait(false);
RecordValue httpResultValue = (RecordValue)httpResult;
FormulaValue displayName = httpResultValue.GetField("displayName");
FormulaValue createdDateTime = httpResultValue.GetField("createdDateTime");
Assert.NotNull(httpResult);
Assert.True(httpResult is RecordValue);
Assert.True(displayName is DateTimeValue);
Assert.True(createdDateTime is DateTimeValue);
Assert.True(function.ReturnType != expectedFormulaType);
}
[Fact]
public async Task ACSL_InvokeFunction()
{
@ -871,7 +912,7 @@ namespace Microsoft.PowerFx.Connectors.Tests
Assert.NotNull(returnType);
Assert.True(returnType.FormulaType is RecordType);
string input = testConnector._log.ToString();
string input = testConnector._log.ToString().Replace("\r", string.Empty);
var version = PowerPlatformConnectorClient.Version;
string expected = $@"POST https://tip1002-002.azure-apihub.net/invoke
authority: tip1002-002.azure-apihub.net
@ -951,7 +992,7 @@ POST https://tip1002-002.azure-apihub.net/invoke
Assert.Equal("accountcategorycode", suggestions2.Suggestions[0].DisplayName);
Assert.Equal("Decimal", suggestions2.Suggestions[0].Suggestion.Type.ToString());
string input = testConnector._log.ToString();
string input = testConnector._log.ToString().Replace("\r", string.Empty);
var version = PowerPlatformConnectorClient.Version;
string expected = @$"POST https://tip1-shared.azure-apim.net/invoke
authority: tip1-shared.azure-apim.net
@ -1025,7 +1066,7 @@ POST https://tip1-shared.azure-apim.net/invoke
runtimeContext,
CancellationToken.None).ConfigureAwait(false);
string input = testConnector._log.ToString();
string input = testConnector._log.ToString().Replace("\r", string.Empty);
Assert.Equal("AdaptiveCard", (((RecordValue)result).GetField("type") as UntypedObjectValue).Impl.GetString());
Assert.Equal(
$@"POST https://tip1002-002.azure-apihub.net/invoke

Просмотреть файл

@ -0,0 +1,4 @@
{
"displayName": "2024-05-29T17:57:00.2209666-06:00",
"createdDateTime": "2024-05-29T17:57:00.2209666-06:00"
}

Просмотреть файл

@ -0,0 +1,105 @@
{
"swagger": "2.0",
"info": {
"version": "v2.0",
"title": "Azure Cognitive Service for Language",
"description": "Azure Cognitive Service for Language, previously known as 'Text Analytics' connector detects language, sentiment and more of the text you provide.",
"contact": {
"name": "Microsoft",
"url": "https://gallery.cortanaanalytics.com/MachineLearningAPI/Text-Analytics-2",
"email": "mlapi@microsoft.com"
},
"x-ms-api-annotation": {
"status": "Production"
}
},
"host": "tip1-shared-002.azure-apim.net",
"basePath": "/apim/cognitiveservicestextanalytics",
"schemes": [
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/{connectionId}/language/analyze-conversations/jobs/": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"description": "Submit a collection of conversations for analysis. Specify one or more unique tasks to be executed..",
"operationId": "AnalyzeConversationTextSubmitJob",
"summary": "Async Conversation PII (text) (2022-05-15-preview)",
"parameters": [],
"responses": {
"200": {
"description": "Analysis job status and metadata.",
"schema": {
"$ref": "#/definitions/ConversationalPIITextJobState"
}
},
"202": {
"description": "A successful call results with an Operation-Location header used to check the status of the analysis job.",
"headers": {
"Operation-Location": {
"type": "string"
}
}
},
"default": {
"description": "Error response.",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
},
"externalDocs": {
"url": "https://docs.microsoft.com/connectors/cognitiveservicestextanalytics/#async-conversation-pii-(text)-(2022-05-15-preview)"
}
}
}
},
"definitions": {
"ConversationalPIITextJobState": {
"type": "object",
"description": "Contains the status of the analyze conversations job submitted along with related statistics.",
"properties": {
"displayName": {
"type": "string"
},
"createdDateTime": {
"format": "date-time",
"description": "The date and time in the UTC time zone when the item was created.",
"type": "string",
"x-ms-summary": "Created Date"
}
},
"required": []
},
"ErrorResponse": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {}
}
}
},
"parameters": {},
"x-ms-connector-metadata": [
{
"propertyName": "Website",
"propertyValue": "https://azure.microsoft.com/services/cognitive-services/text-analytics/"
}
],
"externalDocs": {
"url": "https://docs.microsoft.com/connectors/cognitiveservicestextanalytics"
}
}