مثال: إنشاء مهارة مخصصة باستخدام Bing Entity Search API

في هذا المثال، تعرف على كيفية إنشاء مهارة مخصصة لواجهة برمجة تطبيقات الويب. ستقبل هذه المهارة المواقع والشخصيات العامة والمنظمات، وتعيد أوصافا لها. يستخدم المثال دالة Azure لتضمين واجهة برمجة تطبيقات Bing Entity Search بحيث تنفذ واجهة المهارة المخصصة.

المتطلبات الأساسية

  • اقرأ عن مقالة واجهة المهارة المخصصة إذا لم تكن على دراية بواجهة الإدخال/الإخراج التي يجب أن تنفذها مهارة مخصصة.

  • إنشاء مورد بحث Bing من خلال مدخل Microsoft Azure. يتوفر مستوى مجاني ويكفي لهذا المثال.

  • تثبيت Visual Studio أو إصدار أحدث.

إنشاء وظيفة Azure

على الرغم من أن هذا المثال يستخدم Azure Function لاستضافة واجهة برمجة تطبيقات ويب، إلا أنه غير مطلوب. طالما أنك تفي بمتطلبات الواجهة للمهارة المعرفية، فإن النهج الذي تتبعه غير المادي. ومع ذلك، تسهل Azure Functions إنشاء مهارة مخصصة.

إنشاء مشروع

  1. في Visual Studio، حدد New>Project من القائمة File.

  2. اختر Azure Functions كقالب وحدد Next. اكتب اسما لمشروعك، وحدد إنشاء. يجب أن يكون اسم تطبيق الدالة صالحا كمساحة اسم C#، لذلك لا تستخدم التسطير السفلي أو الواصلات أو أي أحرف أخرى غير أبجدية رقمية.

  3. حدد إطار عمل يحتوي على دعم طويل الأجل.

  4. اختر مشغل HTTP لنوع الدالة لإضافتها إلى المشروع.

  5. اختر دالة لمستوى التخويل.

  6. حدد Create لإنشاء مشروع الدالة ودالة HTTP المشغلة.

إضافة تعليمة برمجية لاستدعاء واجهة برمجة تطبيقات كيان Bing

ينشئ Visual Studio مشروعا مع التعليمات البرمجية المتداولة لنوع الدالة المختارة. تعين السمة FunctionName على الأسلوب اسم الدالة. تحدد سمة HttpTrigger أن الدالة يتم تشغيلها بواسطة طلب HTTP.

استبدل محتويات Function1.cs بالتعليمات البرمجية التالية:

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 Bing entity search API to connect it with a 
    /// AI enrichment pipeline.
    /// </summary>
    public static class BingEntitySearch
    {
        #region Credentials
        // IMPORTANT: Make sure to enter your credential and to verify the API endpoint matches yours.
        static readonly string bingApiEndpoint = "https://api.bing.microsoft.com/v7.0/entities";
        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 Bing API
        private class BingResponse
        {
            public BingEntities Entities { get; set; }
        }
        private class BingEntities
        {
            public BingEntity[] Value { get; set; }
        }

        private class BingEntity
        {
            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 Bing API
        /// <summary>
        /// Gets metadata for a particular entity based on its name using Bing 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 = bingApiEndpoint + "?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();

                BingResponse bingResult = JsonConvert.DeserializeObject<BingResponse>(responseBody);
                if (bingResult != 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(bingResult.Entities?.Value);
                }
            }

            return result;
        }

        private static OutputRecord.OutputRecordData AddTopEntityMetadata(BingEntity[] entities)
        {
            if (entities != null)
            {
                foreach (BingEntity 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
    }
}

تأكد من إدخال قيمة المفتاح الخاصة بك في key الثابت استنادا إلى المفتاح الذي حصلت عليه عند التسجيل في واجهة برمجة تطبيقات بحث كيان Bing.

اختبار الدالة من Visual Studio

اضغط F5 لتشغيل البرنامج واختبار سلوكيات الدالة. في هذه الحالة، سنستخدم الدالة أدناه للبحث عن كيانين. استخدم عميل REST لإصدار مكالمة مثل المكالمة الموضحة أدناه:

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

نص الطلب

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

الرد

يجب أن تشاهد استجابة مشابهة للمثال التالي:

{
    "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
        },
        "..."
    ]
}

نشر الدالة على Azure

عندما تكون راضيا عن سلوك الدالة، يمكنك نشره.

  1. فيمستكشف الحلول، انقر بزر الماوس الأيمن فوق المشروع وحددنشر. اختر إنشاء نشر جديد>.

  2. إذا لم تكن قد قمت بالفعل بتوصيل Visual Studio بحساب Azure الخاص بك، فحدد إضافة حساب....

  3. اتبع المطالبات التي تظهر على الشاشة. يطلب منك تحديد اسم فريد لخدمة التطبيق واشتراك Azure ومجموعة الموارد وخطة الاستضافة وحساب التخزين الذي تريد استخدامه. يمكنك إنشاء مجموعة موارد جديدة وخطة استضافة جديدة وحساب تخزين إذا لم يكن لديك هذه بالفعل. عند الانتهاء، حدد إنشاء

  4. بعد اكتمال النشر، لاحظ عنوان URL للموقع. إنه عنوان تطبيق الوظائف في Azure.

  5. في مدخل Microsoft Azure، انتقل إلى مجموعة الموارد، وابحث عن الدالة التي EntitySearch نشرتها. ضمن قسم Manage، يجب أن تشاهد Host Keys. حدد أيقونة Copy لمفتاح المضيف الافتراضي.

اختبار الدالة في Azure

الآن بعد أن أصبح لديك مفتاح المضيف الافتراضي، اختبر الدالة كما يلي:

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

نص الطلب

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

يجب أن ينتج هذا المثال نفس النتيجة التي رأيتها سابقا عند تشغيل الدالة في البيئة المحلية.

الاتصال بالبنية الأساسية لبرنامج ربط العمليات التجارية

الآن بعد أن أصبح لديك مهارة مخصصة جديدة، يمكنك إضافتها إلى مجموعة المهارات الخاصة بك. يوضح لك المثال أدناه كيفية استدعاء المهارة لإضافة أوصاف للمؤسسات في المستند (يمكن توسيع هذا العمل أيضا على المواقع والأشخاص). استبدل [your-entity-search-app-name] باسم التطبيق الخاص بك.

{
    "skills": [
      "[... your existing skills remain here]",  
      {
        "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill",
        "description": "Our new Bing 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"
            }
          ]
      }
  ]
}

هنا، نعتمد على مهارة التعرف على الكيان المضمنة لتكون موجودة في مجموعة المهارات وإثراء المستند بقائمة المؤسسات. للرجوع إليها، إليك تكوين مهارة استخراج الكيان الذي سيكون كافيا في إنشاء البيانات التي نحتاجها:

{
    "@odata.type": "#Microsoft.Skills.Text.V3.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"
        }
    ]
},

الخطوات التالية

تهانينا! لقد أنشأت أول مهارتك المخصصة. الآن يمكنك اتباع نفس النمط لإضافة وظائفك المخصصة. انقر فوق الارتباطات التالية لمعرفة المزيد.