Skip to content

Commit

Permalink
Try fallback to "value" and "displayName" when building connector sug…
Browse files Browse the repository at this point in the history
…gestions API response (#2635)

Address the schema and actual response misalignment issue especially for
Teams Connector.

**Context**:
In Teams connector, the suggestions API "GetMessageLocations" is
declared as below:

```ts
// dynamic schema
"x-ms-dynamic-values": {
    "operationId": "GetMessageLocations",
    "value-path": "id",
    "value-title": "displayName",
    "value-collection": "value",
    "parameters": {
        "messageType": "ParentMessage",
        "poster": {
            "parameter": "poster"
        }
    }
}

// response type
{
  "type": "object",
  "properties": {
      "locations": {
          "type": "array",
          "items": {
              "type": "object"
          },
          "description": "valid locations to post a message or reply, make verbose"
      }
  }
}
```

It's a historical issue (PAuto supports weak type and consumes this
format) that the actual response format ("value" and "displayName") is
misaligned with the expected format ("id" and "displayName"). Then it
caused the MCS suggestions API returning empty result.

**Proposed solution**:
Fallback to accept "value" and "displayName" when building the
suggestion response if the value accessed by "value-path" or
"value-title" is empty.

(This approached was proved to work in MCS but since some version update
there was a regression.)
  • Loading branch information
yeze322 authored Sep 12, 2024
1 parent cf51f35 commit bbe833d
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 7 deletions.
36 changes: 33 additions & 3 deletions src/libraries/Microsoft.PowerFx.Connectors/ConnectorFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1174,8 +1174,25 @@ private async Task<ConnectorEnhancedSuggestions> GetConnectorSuggestionsFromDyna
foreach (JsonElement jElement in je.EnumerateArray())
{
JsonElement title = ExtractFromJson(jElement, cdv.ValueTitle);
JsonElement value = ExtractFromJson(jElement, cdv.ValuePath);

JsonElement value = ExtractFromJson(jElement, cdv.ValuePath);

// Note: Some connectors have misalignment between the Swagger definition and the actual response, caused suggestion API
// returning empty result. For example, Teams declares the GetMessageLocations response as List<{id, displayName}> but
// in fact returns List<{value, displayName}>.
// Fallback to "displayName" and "value" which are the most commonly used property names in suggestion response.
if (ConnectorSettings?.AllowSuggestionMappingFallback == true)
{
if (title.ValueKind == JsonValueKind.Undefined)
{
title = ExtractFromJson(jElement, "displayName");
}

if (value.ValueKind == JsonValueKind.Undefined)
{
value = ExtractFromJson(jElement, "value");
}
}

if (title.ValueKind == JsonValueKind.Undefined || value.ValueKind == JsonValueKind.Undefined)
{
continue;
Expand Down Expand Up @@ -1213,7 +1230,20 @@ private async Task<ConnectorEnhancedSuggestions> GetConnectorSuggestionsFromDyna
foreach (JsonElement jElement in je.EnumerateArray())
{
JsonElement title = ExtractFromJson(jElement, cdl.ItemTitlePath);
JsonElement value = ExtractFromJson(jElement, cdl.ItemValuePath);
JsonElement value = ExtractFromJson(jElement, cdl.ItemValuePath);

if (ConnectorSettings?.AllowSuggestionMappingFallback == true)
{
if (title.ValueKind == JsonValueKind.Undefined)
{
title = ExtractFromJson(jElement, "displayName");
}

if (value.ValueKind == JsonValueKind.Undefined)
{
value = ExtractFromJson(jElement, "value");
}
}

if (title.ValueKind == JsonValueKind.Undefined || value.ValueKind == JsonValueKind.Undefined)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public ConnectorSettings(string @namespace)
/// </summary>
public bool AllowUnsupportedFunctions { get; init; } = false;

/// <summary>
/// Enables the suggestion mapping logic to use "value" and "displayName" as fallback property names.
/// Serves as a safeguard when the actual response from the suggestion API doesn't align with the Swagger specification.
/// </summary>
public bool AllowSuggestionMappingFallback { get; init; } = false;

/// <summary>
/// Include webhook functions that contain "x-ms-notification-content" in definition.
/// By default these functions won't be accessible by end users.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Responses\SQL Server TestAllFunctions.jsonSet" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Responses\Teams_GetMessageDetails_GetSuggestionsForChannel.json" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Responses\Teams_GetMessageDetails_InputType.json" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Responses\Teams_GetMessageLocations.json" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Responses\TestConnectorDateTimeFormatResponse.json" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Responses\ZD GetDatasetsMetadata.json" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Responses\ZD GetTables.json" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ public async Task ACSL_InvokeFunction_v21_WithInvalidResponse2()
testConnector.SetResponseFromFile(@"Responses\Azure Cognitive Service For Language v2.1_Invalid Response 2.json");

var xx = OpenApiParser.GetFunctions("ACSL", apiDoc, logger).OrderBy(cf => cf.Name).ToList();
ConnectorFunction function = OpenApiParser.GetFunctions("ACSL", apiDoc, logger).OrderBy(cf => cf.Name).ToList()[11];
ConnectorFunction function = OpenApiParser.GetFunctions("ACSL", apiDoc, logger).OrderBy(cf => cf.Name).ToList()[11];
RecalcEngine engine = new RecalcEngine(pfxConfig);

string analysisInput = @"{ conversationItem: { modality: ""text"", language: ""en-us"", text: ""Book me a flight for Munich"" } }";
Expand Down Expand Up @@ -643,7 +643,7 @@ public async Task ACSL_InvokeFunction_v21_WithInvalidResponse3()
FormulaValue httpResult = await function.InvokeAsync(new FormulaValue[] { kind, analysisInputParam, parametersParam }, context, CancellationToken.None);

// valid result
Assert.IsAssignableFrom<RecordValue>(httpResult);
Assert.IsAssignableFrom<RecordValue>(httpResult);
}

[Fact]
Expand Down Expand Up @@ -917,7 +917,7 @@ public void Dataverse_Sample()

// "enum"
Assert.Equal(FormulaType.Decimal, functions[1].OptionalParameters[2].ConnectorType.FormulaType); // "leadsourcecode"
Assert.True(functions[1].OptionalParameters[2].ConnectorType.IsEnum);
Assert.True(functions[1].OptionalParameters[2].ConnectorType.IsEnum);
Assert.Equal(Enumerable.Range(1, 10).Select(i => (decimal)i).ToArray(), functions[1].OptionalParameters[2].ConnectorType.EnumValues.Select(fv => (decimal)fv.ToObject()));
Assert.Equal("Advertisement, Employee Referral, External Referral, Partner, Public Relations, Seminar, Trade Show, Web, Word of Mouth, Other", string.Join(", ", functions[1].OptionalParameters[2].ConnectorType.EnumDisplayNames));
Assert.Equal(4m, functions[1].OptionalParameters[2].ConnectorType.Enum["Partner"].ToObject());
Expand All @@ -931,7 +931,7 @@ public void Dataverse_Sample()
Assert.Equal("2b629105-4a26-4607-97a5-0715059e0a55", functions[1].RequiredParameters[2].ConnectorType.EnumValues[0].ToObject());
Assert.Equal("5cacddd3-d47f-4023-a68e-0ce3e0d401fb", functions[1].RequiredParameters[2].ConnectorType.EnumValues[1].ToObject());
Assert.Equal("INMF", functions[1].RequiredParameters[2].ConnectorType.EnumDisplayNames[0]);
Assert.Equal("MYMF", functions[1].RequiredParameters[2].ConnectorType.EnumDisplayNames[1]);
Assert.Equal("MYMF", functions[1].RequiredParameters[2].ConnectorType.EnumDisplayNames[1]);
}

[Fact]
Expand Down Expand Up @@ -1374,6 +1374,37 @@ public async Task Teams_PostCardAndWaitForResponse()

Assert.NotNull(functionsWithWebhooks.FirstOrDefault(f => f.Name == "PostCardAndWaitForResponse"));
Assert.Null(functionsWithoutWebhooks.FirstOrDefault(f => f.Name == "PostCardAndWaitForResponse"));
}

[Fact]
public async Task Teams_GetMessageLocation_WithSuggestions()
{
using var testConnector = new LoggingTestServer(@"Swagger\Teams.json", _output);
using var httpClient = new HttpClient(testConnector);
using PowerPlatformConnectorClient client = new PowerPlatformConnectorClient("https://tip1002-002.azure-apihub.net", "7592282b-e371-e3f6-8e04-e8f23e64227c" /* environment Id */, "shared-cardsforpower-eafc4fa0-c560-4eba-a5b2-3e1ebc63193a" /* connectionId */, () => "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dC...", httpClient) { SessionId = "a41bd03b-6c3c-4509-a844-e8c51b61f878" };

BaseRuntimeConnectorContext runtimeContext = new TestConnectorRuntimeContext("DV", client, console: _output);

ConnectorFunction[] functions = OpenApiParser.GetFunctions(new ConnectorSettings("DV") { Compatibility = ConnectorCompatibility.SwaggerCompatibility, AllowSuggestionMappingFallback = true }, testConnector._apiDocument).ToArray();
ConnectorFunction createRecord = functions.First(f => f.Name == "PostCardToConversation");

// This example response returns with "value" and "displayName", only when AllowSuggestionMappingFallback = true it will be recognized.
testConnector.SetResponseFromFile(@"Responses\Teams_GetMessageLocations.json");

ConnectorParameters parameters1 = await createRecord.GetParameterSuggestionsAsync(
new NamedValue[]
{
new NamedValue("poster", FormulaValue.New("Flow bot"))
},
createRecord.RequiredParameters[1], // actionName
runtimeContext,
CancellationToken.None);

ConnectorParameterWithSuggestions suggestions1 = parameters1.ParametersWithSuggestions[1];
Assert.Equal(3, suggestions1.Suggestions.Count);
Assert.Equal("Channel", suggestions1.Suggestions[0].DisplayName);
Assert.Equal("Group chat", suggestions1.Suggestions[1].DisplayName);
Assert.Equal("Chat with Flow bot", suggestions1.Suggestions[2].DisplayName);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"value": [
{
"value": "Channel",
"displayName": "Channel"
},
{
"value": "Group chat",
"displayName": "Group chat"
},
{
"value": "Chat with Flow bot",
"displayName": "Chat with Flow bot"
}
]
}

0 comments on commit bbe833d

Please sign in to comment.