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 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")]
[return: Description("An array of lights")]
public async Task<List<LightModel>> GetLightsAsync()
{
return _lights;
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
[return: Description("The updated state of the light; will return null if the light does not exist")]
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) -> Annotated[List[LightModel], "An array of lights"]:
"""Gets a list of lights and their current state."""
return self._lights
@kernel_function
async def change_state(
self,
change_state: LightModel
) -> Annotated[Optional[LightModel], "The updated state of the light; will return null if the light does not exist"]:
"""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.
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 enum? 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")]
[return: Description("An array of lights")]
public async Task<List<LightModel>> GetLightsAsync()
{
_logger.LogInformation("Getting lights");
return lightService.GetLights();
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
[return: Description("The updated state of the light; will return null if the light does not exist")]
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();
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.