From 5bf2555229737495a95c3e24c19baf972ce0d23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Z=2EH?= Date: Tue, 30 Jan 2024 17:26:30 +0100 Subject: [PATCH] Rewrote actions to work around the 5s custom code execution time limit (#3217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated connector. * Updated connector. --------- Co-authored-by: Ståle Zerener Haugnæss --- certified-connectors/Cradl AI/Readme.md | 9 +- .../Cradl AI/apiDefinition.swagger.json | 394 ++++++++++-------- .../Cradl AI/apiProperties.json | 81 +++- certified-connectors/Cradl AI/script.csx | 126 +----- 4 files changed, 327 insertions(+), 283 deletions(-) diff --git a/certified-connectors/Cradl AI/Readme.md b/certified-connectors/Cradl AI/Readme.md index 4fe4386faa..f3b37bf693 100644 --- a/certified-connectors/Cradl AI/Readme.md +++ b/certified-connectors/Cradl AI/Readme.md @@ -16,6 +16,9 @@ A free Cradl AI account. If you don't already have one, you can sign up for free This connector supports the following operations: +### Create document +Create a new document. + ### Parse document with human-in-the-loop Parse a document with *Flows*. This operation runs asynchronous. @@ -26,11 +29,7 @@ Parse a document by calling the model directly. This operation runs synchronousl ## Obtaining Credentials -Log into Cradl AI, then go to [API -> New API key](https://app.cradl.ai/appclients) to create API credentials. Create a new Power Automate connection and copy the `clientId` *and* `clientSecret`to the *Client Credentials* field. The `clientId` and `clientSecret` should be separated by a colon: - -*Client Credentials:* - -> \:\ +Log into Cradl AI, then go to [API -> New API key](https://app.cradl.ai/appclients) to create API credentials. Create a new Power Automate connection and copy `Credentials` to the *Client Credentials* field. ## Getting Started diff --git a/certified-connectors/Cradl AI/apiDefinition.swagger.json b/certified-connectors/Cradl AI/apiDefinition.swagger.json index c4bff8cb9e..b700991fec 100644 --- a/certified-connectors/Cradl AI/apiDefinition.swagger.json +++ b/certified-connectors/Cradl AI/apiDefinition.swagger.json @@ -32,49 +32,28 @@ "consumes": [], "produces": [], "paths": { - "/runs": { + "/documents": { "post": { - "summary": "Parse document with human-in-the-loop", - "operationId": "CreateRun", + "summary": "Create document", + "operationId": "CreateDocument", "description": "Extract data from documents like invoices, receipts, order confirmations, etc.", "consumes": [ "application/json" ], "parameters": [ - { - "name": "WorkflowId", - "x-ms-summary": "Workflow", - "in": "header", - "description": "Choose your workflow", - "required": true, - "type": "string", - "x-ms-dynamic-values": { - "operationId": "GetWorkflows", - "value-path": "workflowId", - "value-title": "name", - "value-collection": "workflows", - "parameters": {} - }, - "x-ms-dynamic-list": { - "operationId": "GetWorkflows", - "itemsPath": "workflows", - "itemValuePath": "workflowId", - "itemTitlePath": "name", - "parameters": {} - } - }, { "name": "Name", "x-ms-summary": "Name", "in": "header", - "description": "The name of your run, for example Invoice.pdf.", + "description": "The name of your document, for example Invoice.pdf.", "required": true, "type": "string" }, { "in": "body", - "name": "file", + "name": "File content.", "description": "The file content.", + "required": true, "schema": { "type": "string", "format": "binary" @@ -87,26 +66,9 @@ "schema": { "type": "object", "properties": { - "executionId": { - "type": "string", - "description": "A unique ID for this execution." - }, - "status": { + "documentId": { "type": "string", - "description": "A status indicator for this execution." - }, - "input": { - "type": "object", - "properties": { - "documentId": { - "type": "string", - "description": "The ID of the document which was submitted." - }, - "title": { - "type": "string", - "description": "A title for this execution." - } - } + "description": "The document ID." } } } @@ -123,44 +85,55 @@ "application/json" ], "parameters": [ - { - "name": "ModelId", - "x-ms-summary": "Model", - "in": "header", - "description": "Choose your model", - "required": true, - "type": "string", - "x-ms-dynamic-values": { - "operationId": "GetModels", - "value-path": "modelId", - "value-title": "name", - "value-collection": "models", - "parameters": {} - }, - "x-ms-dynamic-list": { - "operationId": "GetModels", - "itemsPath": "models", - "itemValuePath": "modelId", - "itemTitlePath": "name", - "parameters": {} - } - }, - { - "name": "Name", - "x-ms-summary": "File name", - "in": "header", - "description": "Name of the file, e.g. Document.pdf.", - "required": true, - "type": "string" - }, { "in": "body", - "name": "file", - "description": "The file content.", + "name": "Request", "schema": { - "type": "string", - "format": "binary" - } + "type": "object", + "properties": { + "modelId": { + "x-ms-summary": "Model", + "description": "Choose your model", + "type": "string", + "x-ms-dynamic-values": { + "operationId": "GetModels", + "value-path": "modelId", + "value-title": "name", + "value-collection": "models", + "parameters": {} + }, + "x-ms-dynamic-list": { + "operationId": "GetModels", + "itemsPath": "models", + "itemValuePath": "modelId", + "itemTitlePath": "name", + "parameters": {} + }, + "title": "Model", + "x-ms-visibility": "important" + }, + "documentId": { + "x-ms-summary": "Document ID", + "description": "Name of the file, e.g. Document.pdf.", + "type": "string", + "title": "Document ID", + "x-ms-visibility": "important" + }, + "postprocessConfig": { + "$ref": "#/definitions/PostprocessConfig" + }, + "preprocessConfig": { + "$ref": "#/definitions/PreprocessConfig" + } + }, + "x-ms-visibility": "important", + "required": [ + "documentId", + "modelId" + ] + }, + "x-ms-visibility": "important", + "required": true } ], "responses": { @@ -183,64 +156,33 @@ "description": "The model which was used." }, "postprocessConfig": { - "type": "object", - "properties": { - "parameters": { - "type": "object", - "properties": { - "n": { - "type": "integer", - "format": "int32", - "description": "n" - } - }, - "description": "parameters" - }, - "strategy": { - "type": "string", - "description": "strategy" - } - }, - "description": "postprocessConfig" + "$ref": "#/definitions/PostprocessConfig" + }, + "preprocessConfig": { + "$ref": "#/definitions/PreprocessConfig" }, "predictions": { "type": "object", "description": "predictions", "additionalProperties": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "value": { - "type": "string" - }, - "confidence": { - "type": "number", - "format": "float" + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "confidence": { + "type": "number", + "format": "float" + } } } } }, - "preprocessConfig": { - "type": "object", - "properties": { - "autoRotate": { - "type": "boolean", - "description": "A flag indicating whether document orientation should be auto detected." - }, - "maxPages": { - "type": "integer", - "format": "int32", - "description": "The maximum number of pages to process in a single request." - }, - "imageQuality": { - "type": "string", - "description": "The image quality used when processing this document." - } - }, - "description": "preprocessConfig" - }, "trainingId": { "type": "string", "description": "The model training." @@ -262,38 +204,88 @@ ], "parameters": [], "responses": { + "200": { + "description": "OK.", + "schema": { + "$ref": "#/definitions/Models" + } + }, "default": { "description": "Default response." + } + } + } + }, + "/workflows": { + "post": { + "summary": "Parse document with human-in-the-loop", + "operationId": "CreateRun", + "description": "Extract data from documents like invoices, receipts, order confirmations, etc.", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "header", + "name": "WorkflowId", + "required": true, + "x-ms-skip-url-encoding": true, + "x-ms-summary": "Workflow", + "description": "Choose your workflow", + "type": "string", + "x-ms-dynamic-values": { + "operationId": "GetWorkflows", + "value-path": "workflowId", + "value-title": "name", + "value-collection": "workflows", + "parameters": {} + }, + "x-ms-dynamic-list": { + "operationId": "GetWorkflows", + "itemsPath": "workflows", + "itemValuePath": "workflowId", + "itemTitlePath": "name", + "parameters": {} + }, + "x-ms-visibility": "important" }, + { + "in": "body", + "name": "Request", + "required": true, + "x-ms-visibility": "important", + "schema": { + "type": "object", + "required": [ + "input" + ], + "properties": { + "input": { + "$ref": "#/definitions/WorkflowInput" + } + } + } + } + ], + "responses": { "200": { - "description": "OK.", + "description": "Default response.", "schema": { "type": "object", "properties": { - "models": { - "type": "array", - "items": { - "type": "object", - "properties": { - "modelId": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } + "executionId": { + "type": "string", + "description": "A unique ID for this execution." }, - "nextToken": { - "type": "string" + "status": { + "type": "string", + "description": "A status indicator for this execution." } } } } } - } - }, - "/workflows": { + }, "get": { "summary": "Get my workflows", "operationId": "GetWorkflows", @@ -304,9 +296,6 @@ ], "parameters": [], "responses": { - "default": { - "description": "Default response." - }, "200": { "description": "OK.", "schema": { @@ -331,38 +320,115 @@ } } } + }, + "default": { + "description": "Default response." } } } } }, + "parameters": {}, + "tags": [], "definitions": { - "GetModelsItem": { + "PostprocessConfig": { "type": "object", + "x-ms-visibility": "advanced", + "x-ms-summary": "Postprocessing.", "properties": { - "modelId": { - "type": "string" + "outputFormat": { + "type": "string", + "description": "Output format.", + "title": "Output format.", + "x-ms-summary": "The output format.", + "default": "v2", + "enum": [ + "v1", + "v2" + ] }, - "name": { - "type": "string" + "strategy": { + "type": "string", + "description": "Prediction aggregation strategy.", + "enum": [ + "BEST_N_PAGES", + "BEST_FIRST" + ], + "x-ms-summary": "The strategy used for aggregating predictions.", + "default": "BEST_FIRST" } } }, - "GetWorkflowsItem": { + "PreprocessConfig": { "type": "object", + "x-ms-visibility": "advanced", + "x-ms-summary": "Preprocessing.", "properties": { - "workflowId": { - "type": "string" + "autoRotate": { + "type": "boolean", + "description": "A flag indicating whether document orientation should be auto detected." + }, + "maxPages": { + "type": "integer", + "format": "int32", + "description": "The maximum number of pages to process in a single request." }, - "name": { + "imageQuality": { + "type": "string", + "description": "The image quality used when processing this document." + } + } + }, + "Models": { + "type": "object", + "properties": { + "models": { + "type": "array", + "items": { + "type": "object", + "properties": { + "modelId": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + }, + "nextToken": { "type": "string" } } + }, + "WorkflowInput": { + "type": "object", + "required": [ + "documentId" + ], + "properties": { + "documentId": { + "x-ms-summary": "Document ID", + "x-ms-visibility": "important", + "title": "Document ID", + "type": "string", + "description": "The ID of the document which was submitted." + }, + "title": { + "type": "string", + "x-ms-summary": "Title", + "x-ms-visibility": "important", + "description": "Title, e.g. Invoice.pdf." + }, + "predictions": { + "type": "object", + "x-ms-summary": "Predictions", + "x-ms-visibility": "advanced", + "description": "Manually override the predictions used in this workflow execution." + } + } } }, - "parameters": {}, - "responses": {}, - "tags": [], "securityDefinitions": { "API Key": { "type": "apiKey", diff --git a/certified-connectors/Cradl AI/apiProperties.json b/certified-connectors/Cradl AI/apiProperties.json index cba80da61f..91b2aa17b2 100644 --- a/certified-connectors/Cradl AI/apiProperties.json +++ b/certified-connectors/Cradl AI/apiProperties.json @@ -1,12 +1,12 @@ { "properties": { "connectionParameters": { - "api_key": { + "ClientCredentials": { "type": "securestring", "uiDefinition": { "displayName": "Client Credentials", - "description": "Client credentials specified as clientId:clientSecret", - "tooltip": "Login to your Cradl AI account to generate client credentials.", + "description": "Your client credentials.", + "tooltip": "Provide your client credentials.", "constraints": { "tabIndex": 2, "clearText": false, @@ -16,10 +16,79 @@ } }, "iconBrandColor": "#121220", - "scriptOperations": [], + "scriptOperations": [ + "CreateDocument", + "CreateRun" + ], "capabilities": [], - "policyTemplateInstances": [], - "publisher": "Cradl AI", + "policyTemplateInstances": [ + { + "templateId": "dynamichosturl", + "title": "Change host to auth", + "parameters": { + "x-ms-apimTemplateParameter.urlTemplate": "https://auth.lucidtech.ai" + } + }, + { + "templateId": "setheader", + "title": "Set basic auth", + "parameters": { + "x-ms-apimTemplateParameter.name": "Authorization", + "x-ms-apimTemplateParameter.value": "Basic @headers('ClientCredentials')", + "x-ms-apimTemplateParameter.existsAction": "override", + "x-ms-apimTemplate-policySection": "Request" + } + }, + { + "templateId": "setheader", + "title": "Set content type", + "parameters": { + "x-ms-apimTemplateParameter.name": "Content-Type", + "x-ms-apimTemplateParameter.value": "application/x-www-form-urlencoded", + "x-ms-apimTemplateParameter.existsAction": "override", + "x-ms-apimTemplate-policySection": "Request" + } + }, + { + "templateId": "setvaluefromurl", + "title": "Authorize", + "parameters": { + "x-ms-apimTemplateParameter.parameterTemplate": "@headers('Authorization')", + "x-ms-apimTemplateParameter.httpMethod": "POST", + "x-ms-apimTemplateParameter.parameterValueUrl": "/oauth2/token?grant_type=client_credentials", + "x-ms-apimTemplateParameter.parameterValuePathTemplate": "@body().access_token", + "x-ms-apimTemplate-policySection": "Request" + } + }, + { + "templateId": "setheader", + "title": "Set auth header", + "parameters": { + "x-ms-apimTemplateParameter.name": "Authorization", + "x-ms-apimTemplateParameter.value": "Bearer @headers('Authorization')", + "x-ms-apimTemplateParameter.existsAction": "override", + "x-ms-apimTemplate-policySection": "Request" + } + }, + { + "templateId": "dynamichosturl", + "title": "Change host back", + "parameters": { + "x-ms-apimTemplateParameter.urlTemplate": "https://api.lucidtech.ai/v1" + } + }, + { + "templateId": "setheader", + "title": "Set JSON content type", + "parameters": { + "x-ms-apimTemplateParameter.name": "Content-Type", + "x-ms-apimTemplateParameter.value": "application/json", + "x-ms-apimTemplateParameter.existsAction": "override", + "x-ms-apimTemplate-policySection": "Request" + } + } + ], + "publisher": "Ståle Zerener Haugnæss", "stackOwner": "Cradl AI" } } \ No newline at end of file diff --git a/certified-connectors/Cradl AI/script.csx b/certified-connectors/Cradl AI/script.csx index 928033c748..08132dfce3 100644 --- a/certified-connectors/Cradl AI/script.csx +++ b/certified-connectors/Cradl AI/script.csx @@ -12,26 +12,21 @@ public class Script : ScriptBase public override async Task ExecuteAsync() { - string[] clientCredentials = this.Context.Request.Headers.GetValues("ClientCredentials").First().Split(':'); - string clientId = clientCredentials[0]; - string clientSecret = clientCredentials[1]; - - var accessToken = await this.GetAccessToken(clientId: clientId, clientSecret: clientSecret); + string clientCredentials = this.Context.Request.Headers.GetValues("ClientCredentials").First(); + + var accessToken = await this.GetAccessToken(clientCredentials); var path = this.Context.Request.RequestUri.AbsolutePath.ToString(); switch (path) { - case "/v1/runs": + case "/v1/workflows": return await CreateRun(accessToken); break; - case "/v1/predictions": - return await CreatePrediction(accessToken); + case "/v1/documents": + return await CreateDocument(accessToken); break; case "/v1/models": return await GetModels(accessToken); break; - case "/v1/workflows": - return await GetWorkflows(accessToken); - break; } return null; @@ -39,86 +34,12 @@ public class Script : ScriptBase private async Task CreateRun(string accessToken) { - string workflowId = this.Context.Request.Headers.GetValues("WorkflowId").First(); - string executionName = this.Context.Request.Headers.GetValues("Name").First(); - - var fileContent = await this.Context.Request.Content.ReadAsByteArrayAsync(); + var request = this.Context.Request; + string workflowId = request.Headers.GetValues("WorkflowId").First(); + request.RequestUri = new Uri($"{Script.API_ENDPOINT}/workflows/{workflowId}/executions"); - var (documentResponse, putResponse) = await this.CreateDocument( - content: fileContent, - name: executionName, - accessToken: accessToken - ); - - var documentId = (string) (await ToJson(documentResponse))["documentId"]; - - var request = CreateAuthorizedRequest( - method: HttpMethod.Post, - path: $"/workflows/{workflowId}/executions", - accessToken: accessToken - ); - - request.Content = CreateJsonContent(new JObject { - ["input"] = new JObject { - ["documentId"] = documentId, - ["title"] = executionName - } - }.ToString()); - return await this.Context.SendAsync(request, this.CancellationToken); } - - private async Task CreatePrediction(string accessToken) - { - string modelId = this.Context.Request.Headers.GetValues("ModelId").First(); - string fileName = this.Context.Request.Headers.GetValues("Name").First(); - - var fileContent = await this.Context.Request.Content.ReadAsByteArrayAsync(); - - var (documentResponse, putResponse) = await this.CreateDocument( - content: fileContent, - name: fileName, - accessToken: accessToken - ); - - var documentId = (string) (await ToJson(documentResponse))["documentId"]; - - var request = CreateAuthorizedRequest( - method: HttpMethod.Post, - path: $"/predictions", - accessToken: accessToken - ); - - request.Content = CreateJsonContent(new JObject { - ["modelId"] = modelId, - ["documentId"] = documentId - }.ToString()); - - var response = await this.Context.SendAsync(request, this.CancellationToken); - - var predictions = new Dictionary(); - foreach (var pred in (await ToJson(response))["predictions"]) { - var label = (string) pred["label"]; - try { - if ((float) predictions[label]["confidence"] < (float) pred["confidence"]) { - predictions[label] = pred; - } - } catch (KeyNotFoundException) { - predictions[label] = pred; - } - } - - var formatted = new JObject(); - foreach (var item in predictions) { - formatted.Add(item.Key, item.Value); - } - - var content = await ToJson(response); - content["predictions"] = formatted; - response.Content = CreateJsonContent(content.ToString()); - - return response; - } private async Task GetModels(string accessToken) { @@ -152,22 +73,9 @@ public class Script : ScriptBase response.Content = CreateJsonContent(content.ToString()); return response; } - - private async Task GetWorkflows(string accessToken) - { - var request = CreateAuthorizedRequest( - method: HttpMethod.Get, - path: $"/workflows", - accessToken: accessToken - ); - - return await this.Context.SendAsync(request, this.CancellationToken); - } - private async Task GetAccessToken(string clientId, string clientSecret) + private async Task GetAccessToken(string authString) { - var authString = Convert.ToBase64String(new UTF8Encoding().GetBytes($"{clientId}:{clientSecret}")); - var tokenRequest = new HttpRequestMessage(HttpMethod.Post, new Uri($"{Script.TOKEN_ENDPOINT}?grant_type=client_credentials")); tokenRequest.Headers.Add("Authorization", $"Basic {authString}"); tokenRequest.Content = new FormUrlEncodedContent(new Dictionary() {}); @@ -176,26 +84,28 @@ public class Script : ScriptBase return (string) (await ToJson(tokenResponse))["access_token"]; } - private async Task<(HttpResponseMessage, HttpResponseMessage)> CreateDocument(byte[] content, string name, string accessToken) + private async Task CreateDocument(string accessToken) { + string fileName = this.Context.Request.Headers.GetValues("Name").First(); + var fileContent = await this.Context.Request.Content.ReadAsByteArrayAsync(); + var request = CreateAuthorizedRequest( method: HttpMethod.Post, path: "/documents", accessToken: accessToken ); - request.Content = CreateJsonContent(new JObject { ["name"] = name }.ToString()); + request.Content = CreateJsonContent(new JObject { ["name"] = fileName }.ToString()); var response = await this.Context.SendAsync(request, this.CancellationToken); var fileUrl = (string) (await ToJson(response))["fileUrl"]; var putRequest = new HttpRequestMessage(HttpMethod.Put, new Uri(fileUrl)); putRequest.Headers.Add("Authorization", $"Bearer {accessToken}"); - - putRequest.Content = new ByteArrayContent(content); - + putRequest.Content = new ByteArrayContent(fileContent); var putResponse = await this.Context.SendAsync(putRequest, this.CancellationToken); - return (response, putResponse); + + return response; } private static HttpRequestMessage CreateAuthorizedRequest(HttpMethod method, string path, string accessToken)