Aggiungere codice nativo come plug-in
Il modo più semplice per fornire a un agente di intelligenza artificiale funzionalità non supportate in modo nativo consiste nel eseguire il wrapping del codice nativo in un plug-in. In questo modo è possibile sfruttare le competenze esistenti come sviluppatore di app per estendere le funzionalità degli agenti di intelligenza artificiale.
Dietro le quinte, Semantic Kernel userà quindi le descrizioni fornite, insieme alla reflection, per descrivere semanticamente il plug-in per l'agente di intelligenza artificiale. In questo modo l'agente di intelligenza artificiale può comprendere le funzionalità del plug-in e come interagire con esso.
Fornire all'LLM le informazioni corrette
Quando si crea un plug-in, è necessario fornire all'agente di intelligenza artificiale le informazioni corrette per comprendere le funzionalità del plug-in e le relative funzioni. Valuta gli ambiti seguenti:
- Nome del plug-in
- Nomi delle funzioni
- Descrizioni delle funzioni
- Parametri delle funzioni
- Schema dei parametri
Il valore del kernel semantico è che può generare automaticamente la maggior parte di queste informazioni dal codice stesso. In qualità di sviluppatore, questo significa semplicemente che è necessario fornire le descrizioni semantiche delle funzioni e dei parametri in modo che l'agente di intelligenza artificiale possa comprenderli. Se tuttavia si commenta correttamente e si annota il codice, è probabile che queste informazioni siano già disponibili.
Di seguito verranno illustrati i due modi diversi per fornire all'agente di intelligenza artificiale codice nativo e come fornire queste informazioni semantiche.
Definizione di un plug-in tramite una classe
Il modo più semplice per creare un plug-in nativo consiste nell'iniziare con una classe e quindi aggiungere metodi annotati con l'attributo KernelFunction
. È anche consigliabile usare liberamente l'annotazione Description
per fornire all'agente di intelligenza artificiale le informazioni necessarie per comprendere la funzione.
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());
}
}
Suggerimento
Poiché i moduli APM vengono prevalentemente sottoposti a training nel codice Python, è consigliabile usare snake_case per i nomi e i parametri delle funzioni (anche se si usa C# o Java). Ciò consentirà all'agente di intelligenza artificiale di comprendere meglio la funzione e i relativi parametri.
Se la funzione ha un oggetto complesso come variabile di input, il kernel semantico genererà anche uno schema per tale oggetto e lo passerà all'agente di intelligenza artificiale. Analogamente alle funzioni, è necessario fornire Description
annotazioni per le proprietà che non sono ovvie per l'intelligenza artificiale. Di seguito è riportata la definizione per la LightState
classe e l'enumerazione Brightness
.
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
Anche se si tratta di un esempio "divertente", è un buon lavoro che mostra quanto sia complesso i parametri di un plug-in. In questo singolo caso, è presente un oggetto complesso con quattro tipi diversi di proprietà: un numero intero, una stringa, un valore booleano e un'enumerazione. Il valore del kernel semantico è che può generare automaticamente lo schema per questo oggetto e passarlo all'agente di intelligenza artificiale e effettuare il marshalling dei parametri generati dall'agente di intelligenza artificiale nell'oggetto corretto.
Dopo aver creato la classe del plug-in, è possibile aggiungerla al kernel usando i AddFromType<>
metodi o AddFromObject
.
Suggerimento
Quando si crea una funzione, chiedersi sempre "come è possibile fornire all'intelligenza artificiale ulteriore assistenza per usare questa funzione?" Ciò può includere l'uso di tipi di input specifici (evitare stringhe laddove possibile), fornire descrizioni ed esempi.
Aggiunta di un plug-in tramite il AddFromObject
metodo
Il AddFromObject
metodo consente di aggiungere un'istanza della classe plug-in direttamente alla raccolta di plug-in nel caso in cui si voglia controllare direttamente come viene costruito il plug-in.
Ad esempio, il costruttore della LightsPlugin
classe richiede l'elenco di luci. In questo caso, è possibile creare un'istanza della classe plugin e aggiungerla alla raccolta di plug-in.
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));
Aggiunta di un plug-in tramite il AddFromType<>
metodo
Quando si usa il AddFromType<>
metodo , il kernel userà automaticamente l'inserimento delle dipendenze per creare un'istanza della classe plugin e aggiungerla alla raccolta di plug-in.
Ciò è utile se il costruttore richiede l'inserimento di servizi o altre dipendenze nel plug-in. Ad esempio, la LightsPlugin
classe potrebbe richiedere l'inserimento di un logger e un servizio di luce invece di un elenco di luci.
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);
}
}
Con l'inserimento delle dipendenze, è possibile aggiungere i servizi e i plug-in necessari al generatore di kernel prima di compilare il 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();
Definizione di un plug-in tramite una raccolta di funzioni
Meno comune ma ancora utile è definire un plug-in usando una raccolta di funzioni. Ciò è particolarmente utile se è necessario creare dinamicamente un plug-in da un set di funzioni in fase di esecuzione.
L'uso di questo processo richiede l'uso della factory di funzioni per creare singole funzioni prima di aggiungerle al plug-in.
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"
)
]);
Strategie aggiuntive per l'aggiunta di codice nativo con inserimento delle dipendenze
Se si lavora con l'inserimento delle dipendenze, è possibile adottare strategie aggiuntive per creare e aggiungere plug-in al kernel. Di seguito sono riportati alcuni esempi di come aggiungere un plug-in usando l'inserimento delle dipendenze.
Inserire una raccolta di plug-in
Suggerimento
È consigliabile rendere la raccolta di plug-in un servizio temporaneo in modo che venga eliminata dopo ogni utilizzo perché la raccolta di plug-in è modificabile. La creazione di una nuova raccolta di plug-in per ogni uso è economica, quindi non dovrebbe essere un problema di prestazioni.
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);
});
Suggerimento
Come accennato nell'articolo del kernel, il kernel è estremamente leggero, quindi la creazione di un nuovo kernel per ogni uso come temporaneo non è un problema di prestazioni.
Generare i plug-in come singleton
I plug-in non sono modificabili, quindi in genere è sicuro crearli come singleton. Questa operazione può essere eseguita usando la factory del plug-in e aggiungendo il plug-in risultante alla raccolta di servizi.
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);
});
Aggiunta di un plug-in tramite il add_plugin
metodo
Il add_plugin
metodo consente di aggiungere un'istanza del plug-in al kernel. Di seguito è riportato un esempio di come costruire la LightsPlugin
classe e aggiungerla al 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)
Aggiunta di un plug-in tramite il createFromObject
metodo
Il createFromObject
metodo consente di compilare un plug-in kernel da un oggetto con metodi annotati.
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
"LightsPlugin");
Questo plug-in può quindi essere aggiunto a un kernel.
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, chatCompletionService)
.withPlugin(lightPlugin)
.build();
Passaggi successivi
Ora che si sa come creare un plug-in, è ora possibile imparare a usarli con l'agente di intelligenza artificiale. A seconda del tipo di funzioni aggiunte ai plug-in, ci sono modelli diversi da seguire. Per le funzioni di recupero, vedere l'articolo uso delle funzioni di recupero. Per le funzioni di automazione delle attività, vedere l'articolo uso delle funzioni di automazione delle attività .