Adicionar código nativo como um plugin
A maneira mais fácil de fornecer a um agente de IA recursos que não são suportados nativamente é encapsular código nativo em um plugin. Isso permite que você aproveite suas habilidades existentes como desenvolvedor de aplicativos para estender os recursos de seus agentes de IA.
Nos bastidores, o Semantic Kernel usará as descrições que você fornecer, juntamente com a reflexão, para descrever semanticamente o plug-in para o agente de IA. Isso permite que o agente de IA entenda as capacidades do plugin e como interagir com ele.
Fornecer ao LLM as informações corretas
Ao criar um plugin, você precisa fornecer ao agente de IA as informações certas para entender as capacidades do plug-in e suas funções. O que está incluído:
- O nome do plugin
- Os nomes das funções
- As descrições das funções
- Os parâmetros das funções
- O esquema dos parâmetros
- O esquema do valor de retorno
O valor do Semantic Kernel é que ele pode gerar automaticamente a maioria dessas informações a partir do próprio código. Como desenvolvedor, isso significa apenas que você deve fornecer as descrições semânticas das funções e parâmetros para que o agente de IA possa entendê-los. Se você comentar e anotar seu código corretamente, no entanto, provavelmente já tem essas informações em mãos.
Abaixo, mostraremos as duas maneiras diferentes de fornecer ao seu agente de IA código nativo e como fornecer essas informações semânticas.
Definindo um plug-in usando uma classe
A maneira mais fácil de criar um plug-in nativo é começar com uma classe e, em seguida, adicionar métodos anotados com o KernelFunction
atributo. Também é recomendado usar liberalmente a Description
anotação para fornecer ao agente de IA as informações necessárias para entender a função.
public class LightsPlugin
{
private readonly List<LightModel> _lights;
public LightsPlugin(LoggerFactory loggerFactory, List<LightModel> lights)
{
_lights = lights;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
return _lights;
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
// Find the light to change
var light = _lights.FirstOrDefault(l => l.Id == changeState.Id);
// If the light does not exist, return null
if (light == null)
{
return null;
}
// Update the light state
light.IsOn = changeState.IsOn;
light.Brightness = changeState.Brightness;
light.Color = changeState.Color;
return light;
}
}
from typing import List, Optional, Annotated
class LightsPlugin:
def __init__(self, lights: List[LightModel]):
self._lights = lights
@kernel_function
async def get_lights(self) -> List[LightModel]:
"""Gets a list of lights and their current state."""
return self._lights
@kernel_function
async def change_state(
self,
change_state: LightModel
) -> Optional[LightModel]:
"""Changes the state of the light."""
for light in self._lights:
if light["id"] == change_state["id"]:
light["is_on"] = change_state.get("is_on", light["is_on"])
light["brightness"] = change_state.get("brightness", light["brightness"])
light["hex"] = change_state.get("hex", light["hex"])
return light
return None
public class LightsPlugin {
// Mock data for the lights
private final Map<Integer, LightModel> lights = new HashMap<>();
public LightsPlugin() {
lights.put(1, new LightModel(1, "Table Lamp", false, LightModel.Brightness.MEDIUM, "#FFFFFF"));
lights.put(2, new LightModel(2, "Porch light", false, LightModel.Brightness.HIGH, "#FF0000"));
lights.put(3, new LightModel(3, "Chandelier", true, LightModel.Brightness.LOW, "#FFFF00"));
}
@DefineKernelFunction(name = "get_lights", description = "Gets a list of lights and their current state")
public List<LightModel> getLights() {
System.out.println("Getting lights");
return new ArrayList<>(lights.values());
}
@DefineKernelFunction(name = "change_state", description = "Changes the state of the light")
public LightModel changeState(
@KernelFunctionParameter(
name = "model",
description = "The new state of the model to set. Example model: " +
"{\"id\":99,\"name\":\"Head Lamp\",\"isOn\":false,\"brightness\":\"MEDIUM\",\"color\":\"#FFFFFF\"}",
type = LightModel.class) LightModel model
) {
System.out.println("Changing light " + model.getId() + " " + model.getIsOn());
if (!lights.containsKey(model.getId())) {
throw new IllegalArgumentException("Light not found");
}
lights.put(model.getId(), model);
return lights.get(model.getId());
}
}
Gorjeta
Como os LLMs são predominantemente treinados em código Python, é recomendável usar snake_case para nomes de funções e parâmetros (mesmo se você estiver usando C# ou Java). Isso ajudará o agente de IA a entender melhor a função e seus parâmetros.
Gorjeta
Suas funções podem especificar Kernel
, KernelArguments
, ILoggerFactory
, ILogger
, IAIServiceSelector
, CultureInfo
, IFormatProvider
, CancellationToken
como parâmetros e estes não serão anunciados para o LLM e serão automaticamente definidos quando a função for chamada.
Se você confiar em KernelArguments
em vez de argumentos de entrada explícitos, seu código será responsável por executar conversões de tipo.
Se sua função tiver um objeto complexo como variável de entrada, o Semantic Kernel também gerará um esquema para esse objeto e o passará para o agente de IA. Semelhante às funções, você deve fornecer Description
anotações para propriedades que não são óbvias para a IA. Abaixo está a definição para a LightState
classe e o Brightness
enum.
using System.Text.Json.Serialization;
public class LightModel
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("is_on")]
public bool? IsOn { get; set; }
[JsonPropertyName("brightness")]
public Brightness? Brightness { get; set; }
[JsonPropertyName("color")]
[Description("The color of the light with a hex code (ensure you include the # symbol)")]
public string? Color { get; set; }
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Brightness
{
Low,
Medium,
High
}
from typing import TypedDict
class LightModel(TypedDict):
id: int
name: str
is_on: bool | None
brightness: int | None
hex: str | None
public class LightModel {
private int id;
private String name;
private Boolean isOn;
private Brightness brightness;
private String color;
public enum Brightness {
LOW,
MEDIUM,
HIGH
}
public LightModel(int id, String name, Boolean isOn, Brightness brightness, String color) {
this.id = id;
this.name = name;
this.isOn = isOn;
this.brightness = brightness;
this.color = color;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Boolean getIsOn() {
return isOn;
}
public void setIsOn(Boolean isOn) {
this.isOn = isOn;
}
public Brightness getBrightness() {
return brightness;
}
public void setBrightness(Brightness brightness) {
this.brightness = brightness;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
Nota
Embora este seja um exemplo "divertido", ele faz um bom trabalho mostrando o quão complexos os parâmetros de um plugin podem ser. Neste único caso, temos um objeto complexo com quatro tipos diferentes de propriedades: um inteiro, string, booleano e enum. O valor do Semantic Kernel é que ele pode gerar automaticamente o esquema para este objeto e passá-lo para o agente de IA e organizar os parâmetros gerados pelo agente de IA no objeto correto.
Depois de criar sua classe de plugin, você pode adicioná-la ao kernel usando os AddFromType<>
métodos or AddFromObject
.
Gorjeta
Ao criar uma função, sempre se pergunte "como posso dar à IA ajuda adicional para usar essa função?" Isso pode incluir o uso de tipos de entrada específicos (evite cadeias de caracteres sempre que possível), fornecendo descrições e exemplos.
Adicionando um plugin usando o AddFromObject
método
O AddFromObject
método permite que você adicione uma instância da classe de plug-in diretamente à coleção de plug-ins, caso você queira controlar diretamente como o plug-in é construído.
Por exemplo, o construtor da classe requer a LightsPlugin
lista de luzes. Nesse caso, você pode criar uma instância da classe de plug-in e adicioná-la à coleção de plug-ins.
List<LightModel> lights = new()
{
new LightModel { Id = 1, Name = "Table Lamp", IsOn = false, Brightness = Brightness.Medium, Color = "#FFFFFF" },
new LightModel { Id = 2, Name = "Porch light", IsOn = false, Brightness = Brightness.High, Color = "#FF0000" },
new LightModel { Id = 3, Name = "Chandelier", IsOn = true, Brightness = Brightness.Low, Color = "#FFFF00" }
};
kernel.Plugins.AddFromObject(new LightsPlugin(lights));
Adicionando um plugin usando o AddFromType<>
método
Ao usar o AddFromType<>
método, o kernel usará automaticamente a injeção de dependência para criar uma instância da classe de plug-in e adicioná-la à coleção de plugins.
Isso é útil se o construtor requer serviços ou outras dependências para ser injetado no plugin. Por exemplo, nossa LightsPlugin
classe pode exigir que um registrador e um serviço de luz sejam injetados nele, em vez de uma lista de luzes.
public class LightsPlugin
{
private readonly Logger _logger;
private readonly LightService _lightService;
public LightsPlugin(LoggerFactory loggerFactory, LightService lightService)
{
_logger = loggerFactory.CreateLogger<LightsPlugin>();
_lightService = lightService;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
_logger.LogInformation("Getting lights");
return lightService.GetLights();
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
_logger.LogInformation("Changing light state");
return lightService.ChangeState(changeState);
}
}
Com o Dependency Injection, você pode adicionar os serviços e plugins necessários ao construtor do kernel antes de construir o kernel.
var builder = Kernel.CreateBuilder();
// Add dependencies for the plugin
builder.Services.AddLogging(loggingBuilder => loggingBuilder.AddConsole().SetMinimumLevel(LogLevel.Trace));
builder.Services.AddSingleton<LightService>();
// Add the plugin to the kernel
builder.Plugins.AddFromType<LightsPlugin>("Lights");
// Build the kernel
Kernel kernel = builder.Build();
Definindo um plugin usando uma coleção de funções
Menos comum, mas ainda útil, é definir um plugin usando uma coleção de funções. Isso é particularmente útil se você precisar criar dinamicamente um plugin a partir de um conjunto de funções em tempo de execução.
Usar este processo requer que você use a fábrica de funções para criar funções individuais antes de adicioná-las ao plugin.
kernel.Plugins.AddFromFunctions("time_plugin",
[
KernelFunctionFactory.CreateFromMethod(
method: () => DateTime.Now,
functionName: "get_time",
description: "Get the current time"
),
KernelFunctionFactory.CreateFromMethod(
method: (DateTime start, DateTime end) => (end - start).TotalSeconds,
functionName: "diff_time",
description: "Get the difference between two times in seconds"
)
]);
Estratégias adicionais para adicionar código nativo com injeção de dependência
Se você estiver trabalhando com Injeção de Dependência, há estratégias adicionais que você pode usar para criar e adicionar plug-ins ao kernel. Abaixo estão alguns exemplos de como você pode adicionar um plugin usando a injeção de dependência.
Injetar uma coleção de plug-ins
Gorjeta
Recomendamos que faça da sua coleção de plugins um serviço transitório para que seja eliminado após cada utilização, uma vez que a coleção de plugins é mutável. Criar uma nova coleção de plugins para cada uso é barato, por isso não deve ser uma preocupação de desempenho.
var builder = Host.CreateApplicationBuilder(args);
// Create native plugin collection
builder.Services.AddTransient((serviceProvider)=>{
KernelPluginCollection pluginCollection = [];
pluginCollection.AddFromType<LightsPlugin>("Lights");
return pluginCollection;
});
// Create the kernel service
builder.Services.AddTransient<Kernel>((serviceProvider)=> {
KernelPluginCollection pluginCollection = serviceProvider.GetRequiredService<KernelPluginCollection>();
return new Kernel(serviceProvider, pluginCollection);
});
Gorjeta
Como mencionado no artigo do kernel, o kernel é extremamente leve, portanto, criar um novo kernel para cada uso como transitório não é uma preocupação de desempenho.
Gere seus plugins como singletons
Os plugins não são mutáveis, por isso é normalmente seguro criá-los como singletons. Isso pode ser feito usando a fábrica de plug-ins e adicionando o plug-in resultante à sua coleção de serviços.
var builder = Host.CreateApplicationBuilder(args);
// Create singletons of your plugin
builder.Services.AddKeyedSingleton("LightPlugin", (serviceProvider, key) => {
return KernelPluginFactory.CreateFromType<LightsPlugin>();
});
// Create a kernel service with singleton plugin
builder.Services.AddTransient((serviceProvider)=> {
KernelPluginCollection pluginCollection = [
serviceProvider.GetRequiredKeyedService<KernelPlugin>("LightPlugin")
];
return new Kernel(serviceProvider, pluginCollection);
});
Adicionando um plugin usando o add_plugin
método
O add_plugin
método permite que você adicione uma instância de plug-in ao kernel. Abaixo está um exemplo de como você pode construir a LightsPlugin
classe e adicioná-la ao kernel.
# Create the kernel
kernel = Kernel()
# Create dependencies for the plugin
lights = [
{"id": 1, "name": "Table Lamp", "is_on": False, "brightness": 100, "hex": "FF0000"},
{"id": 2, "name": "Porch light", "is_on": False, "brightness": 50, "hex": "00FF00"},
{"id": 3, "name": "Chandelier", "is_on": True, "brightness": 75, "hex": "0000FF"},
]
# Create the plugin
lights_plugin = LightsPlugin(lights)
# Add the plugin to the kernel
kernel.add_plugin(lights_plugin)
Adicionando um plugin usando o createFromObject
método
O createFromObject
método permite que você construa um plugin do kernel a partir de um objeto com métodos anotados.
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
"LightsPlugin");
Este plugin pode então ser adicionado a um kernel.
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, chatCompletionService)
.withPlugin(lightPlugin)
.build();
Fornecendo esquema de tipo de retorno de funções para LLM
Atualmente, não há um padrão bem definido em todo o setor para fornecer metadados de tipo de retorno de função para modelos de IA. Até que esse padrão seja estabelecido, as técnicas a seguir podem ser consideradas para cenários em que os nomes das propriedades de tipo de retorno são insuficientes para os LLMs raciocinarem sobre seu conteúdo, ou onde contexto adicional ou instruções de manipulação precisam ser associadas ao tipo de retorno para modelar ou aprimorar seus cenários.
Antes de empregar qualquer uma dessas técnicas, é aconselhável fornecer nomes mais descritivos para as propriedades do tipo de retorno, pois essa é a maneira mais direta de melhorar a compreensão do LLM sobre o tipo de retorno e também é econômica em termos de uso de token.
Fornecer informações sobre o tipo de retorno de função na descrição da função
Para aplicar essa técnica, inclua o esquema de tipo de retorno no atributo description da função. O esquema deve detalhar os nomes de propriedade, descrições e tipos, conforme mostrado no exemplo a seguir:
public class LightsPlugin
{
[KernelFunction("change_state")]
[Description("""Changes the state of the light and returns:
{
"type": "object",
"properties": {
"id": { "type": "integer", "description": "Light ID" },
"name": { "type": "string", "description": "Light name" },
"is_on": { "type": "boolean", "description": "Is light on" },
"brightness": { "type": "string", "enum": ["Low", "Medium", "High"], "description": "Brightness level" },
"color": { "type": "string", "description": "Hex color code" }
},
"required": ["id", "name"]
}
""")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
...
}
}
Alguns modelos podem ter limitações no tamanho da descrição da função, por isso é aconselhável manter o esquema conciso e incluir apenas informações essenciais.
Nos casos em que as informações de tipo não são críticas e minimizar o consumo de token é uma prioridade, considere fornecer uma breve descrição do tipo de retorno no atributo description da função em vez do esquema completo.
public class LightsPlugin
{
[KernelFunction("change_state")]
[Description("""Changes the state of the light and returns:
id: light ID,
name: light name,
is_on: is light on,
brightness: brightness level (Low, Medium, High),
color: Hex color code.
""")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
...
}
}
Ambas as abordagens mencionadas acima exigem adicionar manualmente o esquema de tipo de retorno e atualizá-lo sempre que o tipo de retorno for alterado. Para evitar isso, considere a próxima técnica.
Forneça o esquema do tipo de retorno da função como parte do valor de retorno da mesma.
Essa técnica envolve o fornecimento do valor de retorno da função e seu esquema para o LLM, em vez de apenas o valor de retorno. Isso permite que o LLM use o esquema para raciocinar sobre as propriedades do valor de retorno.
Para implementar essa técnica, você precisa criar e registrar um filtro de invocação de função automática. Para obter mais detalhes, consulte o artigo Auto Function Invocation Filter. Esse filtro deve envolver o valor de retorno da função em um objeto personalizado que contenha o valor de retorno original e seu esquema. Segue-se um exemplo:
private sealed class AddReturnTypeSchemaFilter : IAutoFunctionInvocationFilter
{
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
await next(context); // Invoke the original function
// Crete the result with the schema
FunctionResultWithSchema resultWithSchema = new()
{
Value = context.Result.GetValue<object>(), // Get the original result
Schema = context.Function.Metadata.ReturnParameter?.Schema // Get the function return type schema
};
// Return the result with the schema instead of the original one
context.Result = new FunctionResult(context.Result, resultWithSchema);
}
private sealed class FunctionResultWithSchema
{
public object? Value { get; set; }
public KernelJsonSchema? Schema { get; set; }
}
}
// Register the filter
Kernel kernel = new Kernel();
kernel.AutoFunctionInvocationFilters.Add(new AddReturnTypeSchemaFilter());
Com o filtro registrado, agora você pode fornecer descrições para o tipo de retorno e suas propriedades, que serão extraídas automaticamente pelo Kernel Semântico:
[Description("The state of the light")] // Equivalent to annotating the function with the [return: Description("The state of the light")] attribute
public class LightModel
{
[JsonPropertyName("id")]
[Description("The ID of the light")]
public int Id { get; set; }
[JsonPropertyName("name")]
[Description("The name of the light")]
public string? Name { get; set; }
[JsonPropertyName("is_on")]
[Description("Indicates whether the light is on")]
public bool? IsOn { get; set; }
[JsonPropertyName("brightness")]
[Description("The brightness level of the light")]
public Brightness? Brightness { get; set; }
[JsonPropertyName("color")]
[Description("The color of the light with a hex code (ensure you include the # symbol)")]
public string? Color { get; set; }
}
Essa abordagem elimina a necessidade de fornecer e atualizar manualmente o esquema de tipo de retorno cada vez que o tipo de retorno é alterado, pois o esquema é extraído automaticamente pelo Kernel Semântico.
Próximos passos
Agora que você sabe como criar um plugin, agora você pode aprender como usá-los com seu agente de IA. Dependendo do tipo de funções que você adicionou aos seus plugins, existem diferentes padrões que você deve seguir. Para funções de recuperação, consulte o artigo Usando funções de recuperação. Para funções de automação de tarefas, consulte o artigo Usando funções de automação de tarefas.