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(
        name="get_lights",
        description="Gets a list of lights and their current state",
    )
    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(
        name="change_state",
        description="Changes the state of the light",
    )
    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

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, Optional

class LightModel(TypedDict):
    id: int
    name: str
    is_on: Optional[bool]
    brightness: Optional[int]
    hex: Optional[str]

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)

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à .