Compartir a través de


Adición de complementos a partir de especificaciones de OpenAPI

A menudo en una empresa, ya cuenta con un conjunto de APIs que realizan un trabajo real. Estos podrían ser utilizados por otros servicios de automatización o aplicaciones de front-end de energía con las que interactúan los seres humanos. En kernel semántico, puede agregar estas mismas API exactas que los complementos para que los agentes también puedan usarlos.

Ejemplo de especificación de OpenAPI

Por ejemplo, una API que le permite modificar el estado de las bombillas. La especificación OpenAPI, conocida como Especificación de Swagger o simplemente Swagger, para esta API podría tener este aspecto:

{
   "openapi": "3.0.1",
   "info": {
      "title": "Light API",
      "version": "v1"
   },
   "paths": {
      "/Light": {
         "get": {
            "summary": "Retrieves all lights in the system.",
            "operationId": "get_all_lights",
            "responses": {
               "200": {
                  "description": "Returns a list of lights with their current state",
                  "application/json": {
                     "schema": {
                        "type": "array",
                        "items": {
                              "$ref": "#/components/schemas/LightStateModel"
                        }
                     }
                  }
               }
            }
         }
      },
      "/Light/{id}": {
         "post": {
               "summary": "Changes the state of a light.",
               "operationId": "change_light_state",
               "parameters": [
                  {
                     "name": "id",
                     "in": "path",
                     "description": "The ID of the light to change.",
                     "required": true,
                     "style": "simple",
                     "schema": {
                           "type": "string"
                     }
                  }
               ],
               "requestBody": {
                  "description": "The new state of the light and change parameters.",
                  "content": {
                     "application/json": {
                           "schema": {
                              "$ref": "#/components/schemas/ChangeStateRequest"
                           }
                     }
                  }
               },
               "responses": {
                  "200": {
                     "description": "Returns the updated light state",
                     "content": {
                           "application/json": {
                              "schema": {
                                 "$ref": "#/components/schemas/LightStateModel"
                              }
                           }
                     }
                  },
                  "404": {
                     "description": "If the light is not found"
                  }
               }
         }
      }
   },
   "components": {
      "schemas": {
         "ChangeStateRequest": {
               "type": "object",
               "properties": {
                  "isOn": {
                     "type": "boolean",
                     "description": "Specifies whether the light is turned on or off.",
                     "nullable": true
                  },
                  "hexColor": {
                     "type": "string",
                     "description": "The hex color code for the light.",
                     "nullable": true
                  },
                  "brightness": {
                     "type": "integer",
                     "description": "The brightness level of the light.",
                     "format": "int32",
                     "nullable": true
                  },
                  "fadeDurationInMilliseconds": {
                     "type": "integer",
                     "description": "Duration for the light to fade to the new state, in milliseconds.",
                     "format": "int32",
                     "nullable": true
                  },
                  "scheduledTime": {
                     "type": "string",
                     "description": "Use ScheduledTime to synchronize lights. It's recommended that you asynchronously create tasks for each light that's scheduled to avoid blocking the main thread.",
                     "format": "date-time",
                     "nullable": true
                  }
               },
               "additionalProperties": false,
               "description": "Represents a request to change the state of the light."
         },
         "LightStateModel": {
               "type": "object",
               "properties": {
                  "id": {
                     "type": "string",
                     "nullable": true
                  },
                  "name": {
                     "type": "string",
                     "nullable": true
                  },
                  "on": {
                     "type": "boolean",
                     "nullable": true
                  },
                  "brightness": {
                     "type": "integer",
                     "format": "int32",
                     "nullable": true
                  },
                  "hexColor": {
                     "type": "string",
                     "nullable": true
                  }
               },
               "additionalProperties": false
         }
      }
   }
}

Esta especificación proporciona todo lo necesario por la inteligencia artificial para comprender la API y cómo interactuar con ella. La API incluye dos puntos de conexión: uno para obtener todas las luces y otra para cambiar el estado de una luz. También proporciona lo siguiente:

  • Descripciones semánticas de los puntos de conexión y sus parámetros
  • Los tipos de los parámetros
  • Respuestas esperadas

Dado que el agente de IA puede comprender esta especificación, puede agregarla como complemento al agente.

El kernel semántico admite las versiones 2.0 y 3.0 de OpenAPI, y tiene como objetivo dar cabida a las especificaciones de la versión 3.1 al degradarla a la versión 3.0.

Propina

Si tiene especificaciones de OpenAPI existentes, es posible que tenga que realizar modificaciones para que sean más fáciles para que una inteligencia artificial las comprenda. Por ejemplo, puede que tenga que proporcionar instrucciones en las descripciones. Para obtener más consejos sobre cómo hacer que las especificaciones de OpenAPI sean compatibles con IA, consulte consejos y trucos para añadir complementos de OpenAPI.

Adición del complemento OpenAPI

Con algunas líneas de código, puede agregar el complemento OpenAPI al agente. En el fragmento de código siguiente se muestra cómo agregar el complemento light de la especificación openAPI anterior:

await kernel.ImportPluginFromOpenApiAsync(
   pluginName: "lights",
   uri: new Uri("https://example.com/v1/swagger.json"),
   executionParameters: new OpenApiFunctionExecutionParameters()
   {
      // Determines whether payload parameter names are augmented with namespaces.
      // Namespaces prevent naming conflicts by adding the parent parameter name
      // as a prefix, separated by dots
      EnablePayloadNamespacing = true
   }
);

Con el kernel semántico, puede agregar complementos de OpenAPI desde varios orígenes, como una dirección URL, un archivo o una secuencia. Además, los complementos se pueden crear una vez y reutilizarse en varias instancias de kernel o agentes.

// Create the OpenAPI plugin from a local file somewhere at the root of the application
KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync(
    pluginName: "lights",
    filePath: "path/to/lights.json"
);

// Add the plugin to the kernel
Kernel kernel = new Kernel();
kernel.Plugins.Add(plugin);
await kernel.add_plugin_from_openapi(
   plugin_name="lights",
   openapi_document_path="https://example.com/v1/swagger.json",
   execution_settings=OpenAPIFunctionExecutionParameters(
         # Determines whether payload parameter names are augmented with namespaces.
         # Namespaces prevent naming conflicts by adding the parent parameter name
         # as a prefix, separated by dots
         enable_payload_namespacing=True,
   ),
)
String yaml = EmbeddedResourceLoader.readFile("petstore.yaml", ExamplePetstoreImporter.class);

KernelPlugin plugin = SemanticKernelOpenAPIImporter
   .builder()
   .withPluginName("petstore")
   .withSchema(yaml)
   .withServer("http://localhost:8090/api/v3")
   .build();

Kernel kernel = ExampleOpenAPIParent.kernelBuilder()
   .withPlugin(plugin)
   .build();

Después, puede usar el complemento en el agente como si fuera un complemento nativo.

Control de parámetros del complemento OpenAPI

El kernel semántico extrae automáticamente los metadatos, como el nombre, la descripción, el tipo y el esquema de todos los parámetros definidos en los documentos de OpenAPI. Estos metadatos se almacenan en la propiedad KernelFunction.Metadata.Parameters para cada operación de OpenAPI y se proporcionan al LLM junto con el prompt para generar los argumentos correctos para las llamadas de función.

De forma predeterminada, el nombre del parámetro original se proporciona al LLM y lo usa el kernel semántico para buscar el argumento correspondiente en la lista de argumentos proporcionados por el LLM. Sin embargo, puede haber casos en los que el complemento OpenAPI tenga varios parámetros con el mismo nombre. Proporcionar estos metadatos de parámetros al LLM podría crear confusión, lo que podría impedir que LLM genere los argumentos correctos para las llamadas de función.

Además, dado que se crea una función de kernel que no permite nombres de parámetro no únicos para cada operación de OpenAPI, agregar este complemento podría provocar que algunas operaciones no estén disponibles para su uso. En concreto, se omitirán las operaciones con nombres de parámetro no únicos y se registrará una advertencia correspondiente. Incluso si fuera posible incluir varios parámetros con el mismo nombre en la función kernel, esto podría provocar ambigüedad en el proceso de selección de argumentos.

Teniendo en cuenta todo esto, el kernel semántico ofrece una solución para administrar complementos con nombres de parámetro no únicos. Esta solución es especialmente útil al cambiar la propia API no es factible, ya sea debido a que es un servicio de terceros o un sistema heredado.

En el siguiente fragmento de código se muestra cómo controlar nombres de parámetro no únicos en un complemento de OpenAPI. Si la operación de change_light_state tenía un parámetro adicional con el mismo nombre que el parámetro "id" existente, específicamente, para representar un identificador de sesión además del "id" actual que representa el identificador de la luz, podría controlarse como se muestra a continuación:

OpenApiDocumentParser parser = new();

using FileStream stream = File.OpenRead("path/to/lights.json");

// Parse the OpenAPI document
RestApiSpecification specification = await parser.ParseAsync(stream);

// Get the change_light_state operation
RestApiOperation operation = specification.Operations.Single(o => o.Id == "change_light_state");

// Set the 'lightId' argument name to the 'id' path parameter that represents the ID of the light
RestApiParameter idPathParameter = operation.Parameters.Single(p => p.Location == RestApiParameterLocation.Path && p.Name == "id");
idPathParameter.ArgumentName = "lightId";

// Set the 'sessionId' argument name to the 'id' header parameter that represents the session ID
RestApiParameter idHeaderParameter = operation.Parameters.Single(p => p.Location == RestApiParameterLocation.Header && p.Name == "id");
idHeaderParameter.ArgumentName = "sessionId";

// Import the transformed OpenAPI plugin specification
kernel.ImportPluginFromOpenApi(pluginName: "lights", specification: specification);

Este fragmento de código utiliza la clase OpenApiDocumentParser para analizar el documento openAPI y acceder al objeto de modelo de RestApiSpecification que representa el documento. Asigna nombres de argumento a los parámetros e importa la especificación del complemento OpenAPI transformada en el kernel. El kernel semántico proporciona los nombres de argumento al LLM en lugar de los nombres originales y los usa para buscar los argumentos correspondientes en la lista proporcionada por LLM.

Es importante tener en cuenta que los nombres de argumento no se usan en lugar de los nombres originales al llamar a la operación OpenAPI. En el ejemplo anterior, el parámetro 'id' de la ruta se reemplazará por un valor devuelto por el LLM para el argumento 'lightId'. Lo mismo se aplica al parámetro de encabezado 'id'; el valor devuelto por el LLM para el argumento 'sessionId' se usará como valor para el encabezado denominado 'id'.

Control de la carga de complementos de OpenAPI

Los complementos openAPI pueden modificar el estado del sistema mediante operaciones POST, PUT o PATCH. Estas operaciones suelen requerir que se incluya una carga con la solicitud.

El kernel semántico ofrece algunas opciones para administrar el control de cargas de los complementos de OpenAPI, en función de los requisitos específicos de la API y el escenario.

Construcción de carga dinámica

La construcción de carga dinámica permite crear cargas de operaciones de OpenAPI dinámicamente en función del esquema de carga y los argumentos proporcionados por LLM. Esta característica está habilitada de forma predeterminada, pero se puede deshabilitar estableciendo la propiedad EnableDynamicPayload en false en el objeto OpenApiFunctionExecutionParameters al agregar un complemento OpenAPI.

Por ejemplo, considere la operación change_light_state, que requiere una carga estructurada de la siguiente manera:

{
   "isOn": true,
   "hexColor": "#FF0000",
   "brightness": 100,
   "fadeDurationInMilliseconds": 500,
   "scheduledTime": "2023-07-12T12:00:00Z"
}

Para cambiar el estado de la luz y obtener los valores de las propiedades de carga, el kernel semántico proporciona el LLM con metadatos para la operación para que pueda razonar sobre ella:

{
    "name":"lights-change-light-state",
    "description": "Changes the state of a light.",
    "parameters":[
        { "name": "id", "schema": {"type":"string", "description": "The ID of the light to change.", "format":"uuid"}},
        { "name": "isOn", "schema": { "type": "boolean", "description": "Specifies whether the light is turned on or off."}},
        { "name": "hexColor", "schema": { "type": "string", "description": "Specifies whether the light is turned on or off."}},
        { "name": "brightness", "schema": { "type":"string", "description":"The brightness level of the light.", "enum":["Low","Medium","High"]}},
        { "name": "fadeDurationInMilliseconds", "schema": { "type":"integer", "description":"Duration for the light to fade to the new state, in milliseconds.", "format":"int32"}},
        { "name": "scheduledTime", "schema": {"type":"string", "description":"The time at which the change should occur.", "format":"date-time"}},
    ]
}

Además de proporcionar metadatos de operación a LLM, el kernel semántico realizará los pasos siguientes:

  1. Gestione la llamada LLM a la operación OpenAPI, construyendo la carga en función del esquema y basado en los valores de las propiedades de LLM.
  2. Envíe la solicitud HTTP con la carga útil a la API.

Limitaciones de la construcción de carga dinámica

La construcción de carga dinámica es más eficaz para las API con estructuras de carga relativamente simples. Es posible que no funcione de forma confiable o no funcione en absoluto para las cargas de las APIs que muestran las siguientes características:

  • Cargas con nombres de propiedad no únicos independientemente de la ubicación de las propiedades. Por ejemplo, dos propiedades denominadas id, una para el objeto sender y otra para el objeto receptor: json { "sender": { "id": ... }, "receiver": { "id": ... }}
  • Esquemas de carga que usan cualquiera de las palabras clave compuestas oneOf, anyOf, allOf.
  • Esquemas de carga con referencias recursivas. Por ejemplo, json { "parent": { "child": { "$ref": "#parent" } } }

Para controlar las cargas con nombres de propiedad no únicos, tenga en cuenta las siguientes alternativas:

  • Proporcione un nombre de argumento único para cada propiedad no única, utilizando un método similar al descrito en la sección Control de parámetros del complemento OpenAPI.
  • Use espacios de nombres para evitar conflictos de nomenclatura, como se describe en la sección siguiente sobre Espacio de nombres de carga.
  • Deshabilite la construcción de la carga dinámica y permita que LLM cree la carga en función de su esquema, como se explica en la sección El parámetro de carga.

Si los esquemas de cargas usan cualquiera de las oneOf, anyOf, allOf palabras clave compuestas o referencias recursivas, considere la posibilidad de deshabilitar la construcción de carga dinámica y permitir que LLM cree la carga en función de su esquema, como se explica en la sección El parámetro de carga.

Espaciado de nombres de carga

El espaciado de nombres de carga ayuda a evitar conflictos de nomenclatura que pueden producirse debido a nombres de propiedad no únicos en las cargas del complemento OpenAPI.

Cuando se habilita el espacio de nombres, el Kernel Semántico proporciona al LLM metadatos de operación de OpenAPI que incluyen nombres de propiedades aumentadas. Estos nombres aumentados se crean agregando, separados por un punto, el nombre de propiedad principal como prefijo a los nombres de propiedad secundarios.

Por ejemplo, si la operación de change_light_state había incluido un objeto offTimer anidado con una propiedad scheduledTime:

{
  "isOn": true,
  "hexColor": "#FF0000",
  "brightness": 100,
  "fadeDurationInMilliseconds": 500,
  "scheduledTime": "2023-07-12T12:00:00Z",
  "offTimer": {
      "scheduledTime": "2023-07-12T12:00:00Z"
  }
}

Semantic Kernel habría proporcionado al LLM metadatos para la operación que incluyan los siguientes nombres de propiedad.

{
    "name":"lights-change-light-state",
    "description": "Changes the state of a light.",
    "parameters":[
        { "name": "id", "schema": {"type":"string", "description": "The ID of the light to change.", "format":"uuid"}},
        { "name": "isOn", "schema": { "type": "boolean", "description": "Specifies whether the light is turned on or off."}},
        { "name": "hexColor", "schema": { "type": "string", "description": "Specifies whether the light is turned on or off."}},
        { "name": "brightness", "schema": { "type":"string", "description":"The brightness level of the light.", "enum":["Low","Medium","High"]}},
        { "name": "fadeDurationInMilliseconds", "schema": { "type":"integer", "description":"Duration for the light to fade to the new state, in milliseconds.", "format":"int32"}},
        { "name": "scheduledTime", "schema": {"type":"string", "description":"The time at which the change should occur.", "format":"date-time"}},
        { "name": "offTimer.scheduledTime", "schema": {"type":"string", "description":"The time at which the device will be turned off.", "format":"date-time"}},
    ]
}

Además de proporcionar metadatos de operación con nombres de propiedad aumentadas al LLM, el kernel semántico realiza los pasos siguientes:

  1. Gestione la llamada del LLM a la operación OpenAPI y busque los argumentos correspondientes entre los proporcionados por el LLM para todas las propiedades de la carga útil, usando los nombres de propiedades aumentados y recurriendo a los nombres de propiedades originales si es necesario.
  2. Construya la carga con los nombres de propiedad originales como claves y los argumentos resueltos como valores.
  3. Envíe la solicitud HTTP con la carga construida a la API.

De forma predeterminada, la opción de nombres de carga está deshabilitada. Se puede habilitar estableciendo la propiedad EnablePayloadNamespacing en true en el objeto OpenApiFunctionExecutionParameters al agregar un complemento openAPI:

await kernel.ImportPluginFromOpenApiAsync(
    pluginName: "lights",
    uri: new Uri("https://example.com/v1/swagger.json"),
    executionParameters: new OpenApiFunctionExecutionParameters()
    {
        EnableDynamicPayload = true, // Enable dynamic payload construction. This is enabled by default.
        EnablePayloadNamespacing = true // Enable payload namespacing
    });

Nota

La opción EnablePayloadNamespace solo surte efecto cuando también se habilita la construcción de carga dinámica; de lo contrario, no tiene ningún efecto.

Parámetro de carga

El kernel semántico puede trabajar con cargas creadas por LLM mediante el parámetro de carga. Esto resulta útil cuando el esquema de carga es complejo y contiene nombres de propiedad no únicos, lo que hace que sea inviable para que el kernel semántico construya dinámicamente la carga. En tales casos, confiarás en la capacidad del LLM para comprender el esquema y construir una carga útil válida. Los modelos recientes, como gpt-4o, son eficaces para generar cargas JSON válidas.

Para habilitar el parámetro de carga, establezca la propiedad EnableDynamicPayload en false en el objeto OpenApiFunctionExecutionParameters al agregar un complemento openAPI:

await kernel.ImportPluginFromOpenApiAsync(
    pluginName: "lights",
    uri: new Uri("https://example.com/v1/swagger.json"),
    executionParameters: new OpenApiFunctionExecutionParameters()
    {
        EnableDynamicPayload = false, // Disable dynamic payload construction
    });

Cuando el parámetro de carga está habilitado, el kernel semántico proporciona al LLM metadatos para la operación que incluyen esquemas para los parámetros carga y content_type, lo que permite al LLM comprender la estructura de carga y construirla en consecuencia.

{
    "name": "payload",
    "schema":
    {
        "type": "object",
        "properties": {
            "isOn": {
                "type": "boolean",
                "description": "Specifies whether the light is turned on or off."
            },
            "hexColor": {
                "type": "string",
                "description": "The hex color code for the light.",
            },
            "brightness": {
                "enum": ["Low", "Medium", "High"],
                "type": "string",
                "description": "The brightness level of the light."
            },
            "fadeDurationInMilliseconds": {
                "type": "integer",
                "description": "Duration for the light to fade to the new state, in milliseconds.",
                "format": "int32"
            },
            "scheduledTime": {
                "type": "string",
                "description": "The time at which the change should occur.",
                "format": "date-time"
            }
        },
        "additionalProperties": false,
        "description": "Represents a request to change the state of the light."
    },
    {
        "name": "content_type",
        "schema":
        {
            "type": "string",
            "description": "Content type of REST API request body."
        }
    }
}

Además de proporcionar los metadatos de la operación con el esquema para los parámetros de tipo de contenido y carga al LLM, el kernel semántico realiza los pasos siguientes:

  1. Gestione la llamada LLM a la operación OpenAPI y use los argumentos proporcionados por el LLM para los parámetros de carga y content_type.
  2. Envíe la solicitud HTTP a la API con la carga y el tipo de contenido proporcionados.

Dirección URL base del servidor

Los complementos OpenAPI del kernel semántico requieren una dirección URL base, que se usa para anteponer las rutas de conexión al realizar solicitudes de API. Esta dirección URL base se puede especificar en el documento openAPI, obtenido implícitamente cargando el documento desde una dirección URL o siempre que se agregue el complemento al kernel.

Dirección URL especificada en el documento de OpenAPI

Los documentos de OpenAPI v2 definen la dirección URL del servidor mediante los campos schemes, hosty basePath:

{
   "swagger": "2.0",
   "host": "example.com",
   "basePath": "/v1",
   "schemes": ["https"]
   ...
}

El kernel semántico construirá la dirección URL del servidor como https://example.com/v1.

En cambio, los documentos de OpenAPI v3 definen la dirección URL del servidor mediante el campo servers:

{
   "openapi": "3.0.1",
   "servers": [
      {
         "url": "https://example.com/v1"
      }
   ],
   ...
}

El kernel semántico usará la primera dirección URL del servidor especificada en el documento como dirección URL base: https://example.com/v1.

OpenAPI v3 también permite parámetros en las URL del servidor mediante variables indicadas por llaves rizadas.

{
   "openapi": "3.0.1",
   "servers": [
      {
         "url": "https://{environment}.example.com/v1",
         "variables": {
            "environment": {
               "default": "prod"
            }
         }
      }
   ],
   ...  
}

En este caso, el kernel semántico reemplazará el marcador de posición de variable por el valor proporcionado como argumento para la variable o el valor predeterminado si no se proporciona ningún argumento, lo que da como resultado la dirección URL: https://prod.example.com/v1.

Si el documento openAPI no especifica ninguna dirección URL del servidor, el kernel semántico usará la dirección URL base del servidor desde el que se cargó el documento de OpenAPI:

await kernel.ImportPluginFromOpenApiAsync(pluginName: "lights", uri: new Uri("https://api-host.com/swagger.json"));

La dirección URL base será en https://api-host.com.

Sobrescritura de la dirección URL del servidor

En algunos casos, es posible que la dirección URL del servidor especificada en el documento openAPI o el servidor desde el que se cargó el documento no sea adecuado para casos de uso que impliquen el complemento OpenAPI.

El kernel semántico permite invalidar la dirección URL del servidor proporcionando una dirección URL base personalizada al agregar el complemento OpenAPI al kernel:

await kernel.ImportPluginFromOpenApiAsync(  
    pluginName: "lights",  
    uri: new Uri("https://example.com/v1/swagger.json"),  
    executionParameters: new OpenApiFunctionExecutionParameters()  
    {  
        ServerUrlOverride = new Uri("https://custom-server.com/v1")  
    });  

En este ejemplo, la dirección URL base se https://custom-server.com/v1, reemplazando la dirección URL del servidor especificada en el documento openAPI y la dirección URL del servidor desde la que se cargó el documento.

Autenticación

La mayoría de las API REST requieren autenticación para acceder a sus recursos. El kernel semántico proporciona un mecanismo que permite integrar una variedad de métodos de autenticación requeridos por los complementos de OpenAPI.

Este mecanismo se basa en una función de autenticación que se invoca antes de cada solicitud de API. Esta función de callback tiene acceso al objeto HttpRequestMessage, que representa la solicitud HTTP que se enviará a la API. Puede usar este objeto para agregar credenciales de autenticación a la solicitud. Las credenciales se pueden agregar como encabezados, parámetros de consulta o en el cuerpo de la solicitud, en función del método de autenticación usado por la API.

Debe registrar esta función de devolución de llamada al añadir el complemento OpenAPI al kernel. El siguiente fragmento de código muestra cómo registrarlo para autenticar solicitudes:

static Task AuthenticateRequestAsyncCallback(HttpRequestMessage request, CancellationToken cancellationToken = default)
{
    // Best Practices:  
    // * Store sensitive information securely, using environment variables or secure configuration management systems.  
    // * Avoid hardcoding sensitive information directly in your source code.  
    // * Regularly rotate tokens and API keys, and revoke any that are no longer in use.  
    // * Use HTTPS to encrypt the transmission of any sensitive information to prevent interception.  

    // Example of Bearer Token Authentication  
    // string token = "your_access_token";  
    // request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);  

    // Example of API Key Authentication  
    // string apiKey = "your_api_key";  
    // request.Headers.Add("X-API-Key", apiKey);    

    return Task.CompletedTask;  
}

await kernel.ImportPluginFromOpenApiAsync(  
    pluginName: "lights",  
    uri: new Uri("https://example.com/v1/swagger.json"),  
    executionParameters: new OpenApiFunctionExecutionParameters()  
    {  
        AuthCallback = AuthenticateRequestAsyncCallback
    });  

Para escenarios de autenticación más complejos que requieren acceso dinámico a los detalles de los esquemas de autenticación admitidos por una API, puede usar metadatos de documentos y operaciones para obtener esta información. Para obtener más información, consulte los metadatos de documentos y operaciones .

Personalización de la lectura del contenido de respuesta

El kernel semántico tiene un mecanismo integrado para leer el contenido de las respuestas HTTP de los complementos de OpenAPI y convertirlos a los tipos de datos de .NET adecuados. Por ejemplo, una respuesta de imagen se puede leer como una matriz de bytes, mientras que una respuesta JSON o XML se puede leer como una cadena.

Sin embargo, puede haber casos en los que el mecanismo integrado no sea suficiente para sus necesidades. Por ejemplo, cuando la respuesta es un objeto JSON grande o una imagen que debe leerse como una secuencia para proporcionarse como entrada a otra API. En tales casos, leer el contenido de la respuesta como una cadena o matriz de bytes y, a continuación, convertirlo de nuevo en una secuencia puede ser ineficaz y puede provocar problemas de rendimiento. Para solucionar esto, el kernel semántico permite personalizar la lectura de contenido de respuesta proporcionando un lector de contenido personalizado:

private static async Task<object?> ReadHttpResponseContentAsync(HttpResponseContentReaderContext context, CancellationToken cancellationToken)  
{  
    // Read JSON content as a stream instead of as a string, which is the default behavior.  
    if (context.Response.Content.Headers.ContentType?.MediaType == "application/json")  
    {  
        return await context.Response.Content.ReadAsStreamAsync(cancellationToken);  
    }  

    // HTTP request and response properties can be used to determine how to read the content.  
    if (context.Request.Headers.Contains("x-stream"))  
    {  
        return await context.Response.Content.ReadAsStreamAsync(cancellationToken);  
    }  

    // Return null to indicate that any other HTTP content not handled above should be read by the default reader.  
    return null;  
}  

await kernel.ImportPluginFromOpenApiAsync(  
    pluginName: "lights",  
    uri: new Uri("https://example.com/v1/swagger.json"),  
    executionParameters: new OpenApiFunctionExecutionParameters()  
    {  
        HttpResponseContentReader = ReadHttpResponseContentAsync  
    });  

En este ejemplo, el método ReadHttpResponseContentAsync lee el contenido de la respuesta HTTP como una secuencia cuando el tipo de contenido se application/json o cuando la solicitud contiene un encabezado personalizado x-stream. El método devuelve null para cualquier otro tipo de contenido, lo que indica que se debe usar el lector de contenido predeterminado.

Metadatos de documentos y operaciones

El kernel semántico extrae metadatos de operación y documentos de OpenAPI, como información de API, esquemas de seguridad, identificador de operación, descripción, metadatos de parámetros y muchos más. Proporciona acceso a esta información a través de la propiedad KernelFunction.Metadata.AdditionalParameters. Estos metadatos pueden ser útiles en escenarios en los que se requiere información adicional sobre la API o la operación, como con fines de autenticación:

static async Task AuthenticateRequestAsyncCallbackAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
{
    // Get the function context
    if (request.Options.TryGetValue(OpenApiKernelFunctionContext.KernelFunctionContextKey, out OpenApiKernelFunctionContext? functionContext))
    {
        // Get the operation metadata
        if (functionContext!.Function!.Metadata.AdditionalProperties["operation"] is RestApiOperation operation)
        {
            // Handle API key-based authentication
            IEnumerable<KeyValuePair<RestApiSecurityScheme, IList<string>>> apiKeySchemes = operation.SecurityRequirements.Select(requirement => requirement.FirstOrDefault(schema => schema.Key.SecuritySchemeType == "apiKey"));
            if (apiKeySchemes.Any())
            {
                (RestApiSecurityScheme scheme, IList<string> scopes) = apiKeySchemes.First();

                // Get the API key for the scheme and scopes from your app identity provider
                var apiKey = await this.identityPropvider.GetApiKeyAsync(scheme, scopes);

                // Add the API key to the request headers
                if (scheme.In == RestApiParameterLocation.Header)
                {
                    request.Headers.Add(scheme.Name, apiKey);
                }
                else if (scheme.In == RestApiParameterLocation.Query)
                {
                    request.RequestUri = new Uri($"{request.RequestUri}?{scheme.Name}={apiKey}");
                }
                else
                {
                    throw new NotSupportedException($"API key location '{scheme.In}' is not supported.");
                }
            }

            // Handle other authentication types like Basic, Bearer, OAuth2, etc. For more information, see https://swagger.io/docs/specification/v3_0/authentication/
        }
    }
}

// Import the transformed OpenAPI plugin specification
var plugin = kernel.ImportPluginFromOpenApi(
    pluginName: "lights",
    uri: new Uri("https://example.com/v1/swagger.json"),
    new OpenApiFunctionExecutionParameters()
    {
        AuthCallback = AuthenticateRequestAsyncCallbackAsync
    });

await kernel.InvokePromptAsync("Test");

En este ejemplo, el método AuthenticateRequestAsyncCallbackAsync lee los metadatos de la operación del contexto de la función y extrae los requisitos de seguridad de la operación para determinar el esquema de autenticación. A continuación, recupera la clave API, para el esquema y los ámbitos, desde el proveedor de identidades de la aplicación y la agrega a los encabezados de solicitud o parámetros de consulta.

En la tabla siguiente se enumeran los metadatos disponibles en el diccionario de KernelFunction.Metadata.AdditionalParameters:

Clave Tipo Descripción
información RestApiInfo Información de API, incluido el título, la descripción y la versión.
operación RestApiOperation Detalles de la operación de API, como id, description, path, method, etc.
seguridad IList<RestApiSecurityRequirement> Requisitos de seguridad de API: tipo, nombre, etc.

Sugerencias y trucos para agregar complementos de OpenAPI

Dado que las especificaciones de OpenAPI están diseñadas normalmente para los seres humanos, es posible que tenga que realizar algunas modificaciones para que sean más fáciles de entender para una inteligencia artificial. Estos son algunos consejos y trucos para ayudarle a hacerlo:

Recomendación Descripción
Control de versión de las especificaciones de API En lugar de apuntar a una especificación de API activa, considere la posibilidad de proteger y versionar el archivo Swagger. Esto permitirá a los investigadores de inteligencia artificial probar (y modificar) la especificación de API que usa el agente de IA sin afectar a la API activa y viceversa.
Limitar el número de puntos de conexión Intente limitar el número de puntos de conexión de la API. Consolide funcionalidades similares en puntos de conexión únicos con parámetros opcionales para reducir la complejidad.
Usar nombres descriptivos para puntos de conexión y parámetros Asegúrese de que los nombres de los puntos de conexión y los parámetros son descriptivos y autoexplicativos. Esto ayuda a la inteligencia artificial a comprender su propósito sin necesidad de explicaciones exhaustivas.
Usar convenciones de nomenclatura coherentes Mantenga convenciones de nomenclatura coherentes en toda la API. Esto reduce la confusión y ayuda a la inteligencia artificial a aprender y predecir la estructura de la API más fácilmente.
Simplificar las especificaciones de la API A menudo, las especificaciones de OpenAPI son muy detalladas e incluyen una gran cantidad de información que no es necesaria para que el agente de IA ayude a un usuario. Cuanto más sencillo sea la API, menos tokens debe gastar para describirlo y cuantos menos tokens necesita enviar solicitudes a ella.
Evitar parámetros de cadena Siempre que sea posible, evite el uso de parámetros de cadena en la API. En su lugar, use tipos más específicos, como enteros, booleanos o enumeraciones. Esto ayudará a la inteligencia artificial a comprender mejor la API.
Proporcionar ejemplos en descripciones Cuando los humanos usan archivos de Swagger, normalmente pueden probar la API mediante la interfaz de usuario de Swagger, que incluye solicitudes y respuestas de ejemplo. Dado que el agente de IA no puede hacerlo, considere la posibilidad de proporcionar ejemplos en las descripciones de los parámetros.
Hacer referencia a otros puntos de conexión en descripciones A menudo, las IA confunden puntos de conexión similares. Para ayudar a la inteligencia artificial a diferenciar entre puntos de conexión, considere la posibilidad de hacer referencia a otros puntos de conexión en las descripciones. Por ejemplo, podría decir "Este punto de conexión es similar al punto de conexión de get_all_lights, pero solo devuelve una sola luz".
Proporcionar mensajes de error útiles Aunque no está dentro de la especificación de OpenAPI, considere la posibilidad de proporcionar mensajes de error que ayuden a la inteligencia artificial a corregirse automáticamente. Por ejemplo, si un usuario proporciona un identificador no válido, considere la posibilidad de proporcionar un mensaje de error que sugiere que el agente de IA obtenga el identificador correcto del punto de conexión de get_all_lights.

Pasos siguientes

Ahora que sabe cómo crear un complemento, ahora puede aprender a usarlos con el agente de IA. Dependiendo del tipo de funciones que haya agregado a los complementos, hay diferentes patrones que debe seguir. Para conocer las funciones de recuperación, consulte el artículo uso de funciones de recuperación. Para las funciones de automatización de tareas, consulte el artículo sobre el uso de funciones de automatización de tareas.