示例:使用自定义实体搜索 API 创建自定义技能Example: Create a custom skill using the Custom Entity Search API

在此示例中,了解如何创建 Web API 自定义技能。In this example, learn how to create a web API custom skill. 此技能将接受位置、公众人物和组织,并返回其说明。This skill will accept locations, public figures, and organizations, and return descriptions for them. 此示例使用 Azure Function 来包装自定义实体搜索 API,以便实现自定义技能接口。The example uses an Azure Function to wrap the Custom Entity Search API so that it implements the custom skill interface.

先决条件Prerequisites

  • 如果不熟悉自定义技能应实现的输入/输出接口,请阅读自定义技能接口一文。Read about custom skill interface article if you aren't familiar with the input/output interface that a custom skill should implement.

  • 安装 Visual Studio 2019 或更高版本,包括 Azure 开发工作负荷。Install Visual Studio 2019 or later, including the Azure development workload.

创建 Azure 函数Create an Azure Function

尽管此示例使用 Azure Function 来托管 Web API,但并非必须如此。Although this example uses an Azure Function to host a web API, it isn't required. 只要满足认知技能的接口需求,采用的方法并不重要。As long as you meet the interface requirements for a cognitive skill, the approach you take is immaterial. 但是,可通过 Azure Functions 轻松创建自定义技能。Azure Functions, however, make it easy to create a custom skill.

创建函数应用Create a function app

  1. 在 Visual Studio 中,从“文件”菜单中选择“新建” > “项目” 。In Visual Studio, select New > Project from the File menu.

  2. 在“新建项目”对话框中,选择“已安装”,展开“Visual C#” > “云”,选择“Azure Functions”,键入项目的名称,然后选择“确定” 。In the New Project dialog, select Installed, expand Visual C# > Cloud, select Azure Functions, type a Name for your project, and select OK. 函数应用名称必须像 C# 命名空间一样有效,因此请勿使用下划线、连字符或任何其他的非字母数字字符。The function app name must be valid as a C# namespace, so don't use underscores, hyphens, or any other non-alphanumeric characters.

  3. 选择“Azure Functions v2 (.NET Core)” 。Select Azure Functions v2 (.NET Core). 也可以使用版本 1 执行此操作,但下面的代码基于 v2 模板编写。You could also do it with version 1, but the code written below is based on the v2 template.

  4. 选择“HTTP 触发器”作为类型 Select the type to be HTTP Trigger

  5. 对于存储帐户,可选择“无”,因为此函数不需要任何存储 。For Storage Account, you may select None, as you won't need any storage for this function.

  6. 选择“确定”以创建函数项目和 HTTP 触发的函数 。Select OK to create the function project and HTTP triggered function.

修改代码以调用自定义实体搜索服务Modify the code to call the Custom Entity Search Service

Visual Studio 将创建一个项目,并在该项目中创建一个包含所选函数类型的样本代码的类。Visual Studio creates a project and in it a class that contains boilerplate code for the chosen function type. 方法中的 FunctionName 属性设置函数的名称。The FunctionName attribute on the method sets the name of the function. HttpTrigger 属性指定该函数将由某个 HTTP 请求触发。The HttpTrigger attribute specifies that the function is triggered by an HTTP request.

现在,将文件 Function1.cs 的所有内容替换为以下代码 :Now, replace all of the content of the file Function1.cs with the following code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace SampleSkills
{
    /// <summary>
    /// Sample custom skill that wraps the Custom Entity Search API to connect it with a 
    /// AI enrichment pipeline.
    /// </summary>
    public static class CustomEntitySearch
    {
        #region Credentials
        // IMPORTANT: Make sure to enter your credential and to verify the API endpoint matches yours.
        static readonly string CustomApiEndpoint = "<enter your custom entity search endpoint here>";
        static readonly string key = "<enter your api key here>";  
        #endregion

        #region Class used to deserialize the request
        private class InputRecord
        {
            public class InputRecordData
            {
                public string Name { get; set; }
            }

            public string RecordId { get; set; }
            public InputRecordData Data { get; set; }
        }

        private class WebApiRequest
        {
            public List<InputRecord> Values { get; set; }
        }
        #endregion

        #region Classes used to serialize the response

        private class OutputRecord
        {
            public class OutputRecordData
            {
                public string Name { get; set; } = "";
                public string Description { get; set; } = "";
                public string Source { get; set; } = "";
                public string SourceUrl { get; set; } = "";
                public string LicenseAttribution { get; set; } = "";
                public string LicenseUrl { get; set; } = "";
            }

            public class OutputRecordMessage
            {
                public string Message { get; set; }
            }

            public string RecordId { get; set; }
            public OutputRecordData Data { get; set; }
            public List<OutputRecordMessage> Errors { get; set; }
            public List<OutputRecordMessage> Warnings { get; set; }
        }

        private class WebApiResponse
        {
            public List<OutputRecord> Values { get; set; }
        }
        #endregion

        #region Classes used to interact with the Custom Entity Search API
        private class CustomResponse
        {
            public CustomEntities Entities { get; set; }
        }
        private class CustomEntities
        {
            public CustomEntity[] Value { get; set; }
        }

        private class CustomEntity
        {
            public class EntityPresentationinfo
            {
                public string[] EntityTypeHints { get; set; }
            }

            public class License
            {
                public string Url { get; set; }
            }

            public class ContractualRule
            {
                public string _type { get; set; }
                public License License { get; set; }
                public string LicenseNotice { get; set; }
                public string Text { get; set; }
                public string Url { get; set; }
            }

            public ContractualRule[] ContractualRules { get; set; }
            public string Description { get; set; }
            public string Name { get; set; }
            public EntityPresentationinfo EntityPresentationInfo { get; set; }
        }
        #endregion

        #region The Azure Function definition

        [FunctionName("EntitySearch")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("Entity Search function: C# HTTP trigger function processed a request.");

            var response = new WebApiResponse
            {
                Values = new List<OutputRecord>()
            };

            string requestBody = new StreamReader(req.Body).ReadToEnd();
            var data = JsonConvert.DeserializeObject<WebApiRequest>(requestBody);

            // Do some schema validation
            if (data == null)
            {
                return new BadRequestObjectResult("The request schema does not match expected schema.");
            }
            if (data.Values == null)
            {
                return new BadRequestObjectResult("The request schema does not match expected schema. Could not find values array.");
            }

            // Calculate the response for each value.
            foreach (var record in data.Values)
            {
                if (record == null || record.RecordId == null) continue;

                OutputRecord responseRecord = new OutputRecord
                {
                    RecordId = record.RecordId
                };

                try
                {
                    responseRecord.Data = GetEntityMetadata(record.Data.Name).Result;
                }
                catch (Exception e)
                {
                    // Something bad happened, log the issue.
                    var error = new OutputRecord.OutputRecordMessage
                    {
                        Message = e.Message
                    };

                    responseRecord.Errors = new List<OutputRecord.OutputRecordMessage>
                    {
                        error
                    };
                }
                finally
                {
                    response.Values.Add(responseRecord);
                }
            }

            return (ActionResult)new OkObjectResult(response);
        }

        #endregion

        #region Methods to call the Custom Entity Search API
        /// <summary>
        /// Gets metadata for a particular entity based on its name using Custom Entity Search
        /// </summary>
        /// <param name="entityName">The name of the entity to extract data for.</param>
        /// <returns>Asynchronous task that returns entity data. </returns>
        private async static Task<OutputRecord.OutputRecordData> GetEntityMetadata(string entityName)
        {
            var uri = CustomApiEndpoint + "?q=" + entityName + "&mkt=en-us&count=10&offset=0&safesearch=Moderate";
            var result = new OutputRecord.OutputRecordData();

            using (var client = new HttpClient())
            using (var request = new HttpRequestMessage {
                Method = HttpMethod.Get,
                RequestUri = new Uri(uri)
            })
            {
                request.Headers.Add("Ocp-Apim-Subscription-Key", key);

                HttpResponseMessage response = await client.SendAsync(request);
                string responseBody = await response?.Content?.ReadAsStringAsync();

                CustomResponse customResult = JsonConvert.DeserializeObject<CustomResponse>(responseBody);
                if (customResult != null)
                {
                    // In addition to the list of entities that could match the name, for simplicity let's return information
                    // for the top match as additional metadata at the root object.
                    return AddTopEntityMetadata(customResult.Entities?.Value);
                }
            }

            return result;
        }

        private static OutputRecord.OutputRecordData AddTopEntityMetadata(CustomEntity[] entities)
        {
            if (entities != null)
            {
                foreach (CustomEntity entity in entities.Where(
                    entity => entity?.EntityPresentationInfo?.EntityTypeHints != null
                        && (entity.EntityPresentationInfo.EntityTypeHints[0] == "Person"
                            || entity.EntityPresentationInfo.EntityTypeHints[0] == "Organization"
                            || entity.EntityPresentationInfo.EntityTypeHints[0] == "Location")
                        && !String.IsNullOrEmpty(entity.Description)))
                {
                    var rootObject = new OutputRecord.OutputRecordData
                    {
                        Description = entity.Description,
                        Name = entity.Name
                    };

                    if (entity.ContractualRules != null)
                    {
                        foreach (var rule in entity.ContractualRules)
                        {
                            switch (rule._type)
                            {
                                case "ContractualRules/LicenseAttribution":
                                    rootObject.LicenseAttribution = rule.LicenseNotice;
                                    rootObject.LicenseUrl = rule.License.Url;
                                    break;
                                case "ContractualRules/LinkAttribution":
                                    rootObject.Source = rule.Text;
                                    rootObject.SourceUrl = rule.Url;
                                    break;
                            }
                        }
                    }

                    return rootObject;
                }
            }

            return new OutputRecord.OutputRecordData();
        }
        #endregion
    }
}

请确保根据注册自定义实体搜索 API 时获得的密钥,在 key 常量中输入你自己的密钥值 。Make sure to enter your own key value in the key constant based on the key you got when signing up for the Custom Entity Search API.

为方便起见,此示例在单个文件中包含了所有必需的代码。This sample includes all necessary code in a single file for convenience. 可以在 power skills 存储库中找到该同一技能的结构略好的版本。You can find a slightly more structured version of that same skill in the power skills repository.

当然,你可以将文件从 Function1.cs 重命名为 CustomEntitySearch.csOf course, you may rename the file from Function1.cs to CustomEntitySearch.cs.

从 Visual Studio 中测试函数Test the function from Visual Studio

按 F5 运行程序并测试函数行为 。Press F5 to run the program and test function behaviors. 在本例中,我们将使用下面的函数来查找两个实体。In this case, we'll use the function below to look up two entities. 使用 Postman 或 Fiddler 发出如下所示的调用:Use Postman or Fiddler to issue a call like the one shown below:

POST https://localhost:7071/api/EntitySearch

请求正文Request body

{
    "values": [
        {
            "recordId": "e1",
            "data":
            {
                "name":  "Pablo Picasso"
            }
        },
        {
            "recordId": "e2",
            "data":
            {
                "name":  "Microsoft"
            }
        }
    ]
}

响应Response

你应该看到类似于以下示例的响应:You should see a response similar to the following example:

{
    "values": [
        {
            "recordId": "e1",
            "data": {
                "name": "Pablo Picasso",
                "description": "Pablo Ruiz Picasso was a Spanish painter [...]",
                "source": "Wikipedia",
                "sourceUrl": "http://en.wikipedia.org/wiki/Pablo_Picasso",
                "licenseAttribution": "Text under CC-BY-SA license",
                "licenseUrl": "http://creativecommons.org/licenses/by-sa/3.0/"
            },
            "errors": null,
            "warnings": null
        },
        "..."
    ]
}

将函数发布到 AzurePublish the function to Azure

当你对函数行为满意时,可将其发布。When you're satisfied with the function behavior, you can publish it.

  1. 在“解决方案资源管理器” 中,右键单击该项目并选择“发布” 。In Solution Explorer, right-click the project and select Publish. 选择“新建” > “发布” 。Choose Create New > Publish.

  2. 如果尚未将 Visual Studio 连接到 Azure 帐户,请选择“添加帐户...” If you haven't already connected Visual Studio to your Azure account, select Add an account....

  3. 请按照屏幕上的提示操作。Follow the on-screen prompts. 系统会要求你为要使用的应用服务、Azure 订阅、资源组、托管计划和存储帐户指定唯一名称。You're asked to specify a unique name for your app service, the Azure subscription, the resource group, the hosting plan, and the storage account you want to use. 如果尚不拥有这些资源,可创建新资源组、新托管计划和存储帐户。You can create a new resource group, a new hosting plan, and a storage account if you don't already have these. 完成后,选择“创建” When finished, select Create

  4. 部署完成后,请记下站点 URL。After the deployment is complete, notice the Site URL. 这是 Azure 中你的函数应用的地址。It is the address of your function app in Azure.

  5. Azure 门户中,导航到资源组,然后查找你发布的 EntitySearch 函数。In the Azure portal, navigate to the Resource Group, and look for the EntitySearch Function you published. 在“管理”部分下,应可看到主机密钥 。Under the Manage section, you should see Host Keys. 对默认主机密钥选择“复制”图标 。Select the Copy icon for the default host key.

在 Azure 中测试函数Test the function in Azure

现在有了默认主机密钥,按以下方式测试函数:Now that you have the default host key, test your function as follows:

POST https://[your-entity-search-app-name].azurewebsites.net/api/EntitySearch?code=[enter default host key here]

请求正文Request Body

{
    "values": [
        {
            "recordId": "e1",
            "data":
            {
                "name":  "Pablo Picasso"
            }
        },
        {
            "recordId": "e2",
            "data":
            {
                "name":  "Microsoft"
            }
        }
    ]
}

此示例生成的结果应与之前在本地环境中运行函数时看到的结果相同。This example should produce the same result you saw previously when running the function in the local environment.

连接到管道Connect to your pipeline

现在有了新的自定义技能,可将其添加到技能组合。Now that you have a new custom skill, you can add it to your skillset. 下面的示例展示了如何调用技能来向文档中添加组织说明(这可以进行扩展以应用于位置和人员)。The example below shows you how to call the skill to add descriptions to organizations in the document (this could be extended to also work on locations and people). [your-entity-search-app-name] 替换为你的应用的名称。Replace [your-entity-search-app-name] with the name of your app.

{
    "skills": [
      "[... your existing skills remain here]",  
      {
        "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill",
        "description": "Our new Custom Entity Search custom skill",
        "uri": "https://[your-entity-search-app-name].azurewebsites.net/api/EntitySearch?code=[enter default host key here]",
          "context": "/document/merged_content/organizations/*",
          "inputs": [
            {
              "name": "name",
              "source": "/document/merged_content/organizations/*"
            }
          ],
          "outputs": [
            {
              "name": "description",
              "targetName": "description"
            }
          ]
      }
  ]
}

此处,我们期望内置的实体识别技能出现在技能集中,并用组织列表扩充了文档。Here, we're counting on the built-in entity recognition skill to be present in the skillset and to have enriched the document with the list of organizations. 下面提供了一个实体提取技能配置作为参考,它足以生成我们需要的数据:For reference, here's an entity extraction skill configuration that would be sufficient in generating the data we need:

{
    "@odata.type": "#Microsoft.Skills.Text.EntityRecognitionSkill",
    "name": "#1",
    "description": "Organization name extraction",
    "context": "/document/merged_content",
    "categories": [ "Organization" ],
    "defaultLanguageCode": "en",
    "inputs": [
        {
            "name": "text",
            "source": "/document/merged_content"
        },
        {
            "name": "languageCode",
            "source": "/document/language"
        }
    ],
    "outputs": [
        {
            "name": "organizations",
            "targetName": "organizations"
        }
    ]
},

后续步骤Next steps

祝贺!Congratulations! 现已创建第一个自定义技能。You've created your first custom skill. 现在,可按照相同的模式添加自己的自定义功能。Now you can follow the same pattern to add your own custom functionality. 单击以下链接了解详细信息。Click the following links to learn more.