Che cos'è un plug-in?

I plug-in sono un componente chiave del kernel semantico. Se hai già usato plug-in dalle estensioni ChatGPT o Copilot in Microsoft 365, hai già familiarità con loro. Con i plug-in, è possibile incapsulare le API esistenti in una raccolta che può essere usata da un'intelligenza artificiale. In questo modo è possibile offrire all'intelligenza artificiale la possibilità di eseguire azioni che non sarebbero in grado di eseguire in altro modo.

Dietro le quinte, il kernel semantico sfrutta le chiamate di funzione, una funzionalità nativa della maggior parte delle più recenti VM per consentire alle macchine virtuali di ottimizzazione, di eseguire la pianificazione e di richiamare le API. Con la chiamata di funzione, i moduli APM possono richiedere (ad esempio, chiamare) una determinata funzione. Il kernel semantico effettua quindi il marshalling della richiesta alla funzione appropriata nella codebase e restituisce i risultati a LLM in modo che LLM possa generare una risposta finale.

Plug-in kernel semantico

Non tutti gli SDK di intelligenza artificiale hanno un concetto analogo ai plug-in (la maggior parte ha solo funzioni o strumenti). Negli scenari aziendali, tuttavia, i plug-in sono utili perché incapsulano un set di funzionalità che rispecchiano il modo in cui gli sviluppatori aziendali sviluppano già servizi e API. I plug-in giocano bene anche con l'inserimento delle dipendenze. All'interno del costruttore di un plug-in, è possibile inserire servizi necessari per eseguire il lavoro del plug-in (ad esempio, connessioni di database, client HTTP e così via). Questa operazione è difficile da eseguire con altri SDK che non dispongono di plug-in.

Anatomia di un plug-in

A livello generale, un plug-in è un gruppo di funzioni che possono essere esposte ad app e servizi di intelligenza artificiale. Le funzioni all'interno dei plug-in possono quindi essere orchestrate da un'applicazione di intelligenza artificiale per eseguire richieste utente. All'interno del kernel semantico, è possibile richiamare queste funzioni automaticamente con chiamate di funzione.

Nota

In altre piattaforme, le funzioni vengono spesso definite "strumenti" o "azioni". Nel kernel semantico si usa il termine "funzioni" perché vengono in genere definite come funzioni native nella codebase.

Solo fornendo funzioni, tuttavia, non è sufficiente per creare un plug-in. Per attivare l'orchestrazione automatica con chiamate di funzione, i plug-in devono anche fornire dettagli che descrivono semanticamente il comportamento. Tutto ciò che deriva dall'input, dall'output e dagli effetti collaterali della funzione deve essere descritto in modo che l'intelligenza artificiale possa comprendere, in caso contrario, l'intelligenza artificiale non chiamerà correttamente la funzione.

Ad esempio, il plug-in di esempio WriterPlugin a destra ha funzioni con descrizioni semantiche che descrivono le funzioni eseguite da ogni funzione. Un LLM può quindi usare queste descrizioni per scegliere le funzioni migliori da chiamare per soddisfare la richiesta di un utente.

Nell'immagine a destra, un LLM chiamerebbe probabilmente le ShortPoem funzioni e StoryGen per soddisfare le richieste degli utenti grazie alle descrizioni semantiche fornite.

Descrizione semantica all'interno del plug-in WriterPlugin

Importazione di diversi tipi di plug-in

Esistono due modi principali per importare i plug-in nel kernel semantico: usando il codice nativo o usando una specifica OpenAPI. Il primo consente di creare plug-in nella codebase esistente in grado di sfruttare le dipendenze e i servizi già presenti. Quest'ultimo consente di importare plug-in da una specifica OpenAPI, che può essere condivisa tra diversi linguaggi di programmazione e piattaforme.

Di seguito viene fornito un semplice esempio di importazione e uso di un plug-in nativo. Per altre informazioni su come importare questi diversi tipi di plug-in, vedere gli articoli seguenti:

Suggerimento

Quando si inizia, è consigliabile usare plug-in di codice nativo. Man mano che l'applicazione matura e mentre si lavora tra team multipiattaforma, è possibile prendere in considerazione l'uso delle specifiche OpenAPI per condividere plug-in in diversi linguaggi di programmazione e piattaforme.

I diversi tipi di funzioni plug-in

All'interno di un plug-in, in genere si avranno due diversi tipi di funzioni, quelli che recuperano i dati per la generazione aumentata di recupero (RAG) e quelli che automatizzano le attività. Anche se ogni tipo è funzionalmente identico, vengono in genere usati in modo diverso all'interno delle applicazioni che usano il kernel semantico.

Ad esempio, con le funzioni di recupero, è possibile usare strategie per migliorare le prestazioni, ad esempio la memorizzazione nella cache e l'uso di modelli intermedi più economici per il riepilogo. Mentre con le funzioni di automazione delle attività, è probabile che si voglia implementare processi di approvazione del ciclo umano per assicurarsi che le attività vengano completate correttamente.

Per altre informazioni sui diversi tipi di funzioni plug-in, vedere gli articoli seguenti:

Introduzione ai plug-in

L'uso dei plug-in all'interno del kernel semantico è sempre un processo in tre passaggi:

  1. Definire il plug-in
  2. Aggiungere il plug-in al kernel
  3. E quindi richiamare le funzioni del plug-in in un prompt con chiamata di funzione

Di seguito verrà fornito un esempio generale di come usare un plug-in all'interno del kernel semantico. Per informazioni più dettagliate su come creare e usare i plug-in, vedere i collegamenti precedenti.

1) Definire il plug-in

Il modo più semplice per creare un plug-in consiste nel definire una classe e annotare i relativi metodi con l'attributo KernelFunction . In questo caso, il kernel semantico sa che si tratta di una funzione che può essere chiamata da un'intelligenza artificiale o a cui viene fatto riferimento in un prompt.

È anche possibile importare plug-in da una specifica OpenAPI.

Di seguito verrà creato un plug-in che può recuperare lo stato delle luci e modificarne lo stato.

Suggerimento

Poiché la maggior parte dell'LLM è stata sottoposta a training con Python per la chiamata a funzioni, è consigliabile usare il case snake per i nomi delle funzioni e i nomi delle proprietà anche se si usa C# o Java SDK.

using System.ComponentModel;
using Microsoft.SemanticKernel;

public class LightsPlugin
{
   // Mock data for the lights
   private readonly List<LightModel> lights = new()
   {
      new LightModel { Id = 1, Name = "Table Lamp", IsOn = false, Brightness = 100, Hex = "FF0000" },
      new LightModel { Id = 2, Name = "Porch light", IsOn = false, Brightness = 50, Hex = "00FF00" },
      new LightModel { Id = 3, Name = "Chandelier", IsOn = true, Brightness = 75, Hex = "0000FF" }
   };

   [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("get_state")]
   [Description("Gets the state of a particular light")]
   [return: Description("The state of the light")]
   public async Task<LightModel?> GetStateAsync([Description("The ID of the light")] int id)
   {
      // Get the state of the light with the specified ID
      return lights.FirstOrDefault(light => light.Id == id);
   }

   [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(int id, LightModel LightModel)
   {
      var light = lights.FirstOrDefault(light => light.Id == id);

      if (light == null)
      {
         return null;
      }

      // Update the light with the new state
      light.IsOn = LightModel.IsOn;
      light.Brightness = LightModel.Brightness;
      light.Hex = LightModel.Hex;

      return light;
   }
}

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 byte? Brightness { get; set; }

   [JsonPropertyName("hex")]
   public string? Hex { get; set; }
}
from typing import TypedDict, Annotated

class LightModel(TypedDict):
   id: int
   name: str
   is_on: bool | None
   brightness: int | None
   hex: str | None

class LightsPlugin:
   lights: list[LightModel] = [
      {"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"},
   ]

   @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 get_state(
      self,
      id: Annotated[int, "The ID of the light"]
   ) -> Annotated[LightModel | None], "The state of the light"]:
      """Gets the state of a particular light."""
      for light in self.lights:
         if light["id"] == id:
               return light
      return None

   @kernel_function
   async def change_state(
      self,
      id: Annotated[int, "The ID of the light"],
      new_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"] == id:
               light["is_on"] = new_state.get("is_on", light["is_on"])
               light["brightness"] = new_state.get("brightness", light["brightness"])
               light["hex"] = new_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));
    lights.put(2, new LightModel(2, "Porch light", false));
    lights.put(3, new LightModel(3, "Chandelier", true));
  }

  @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 = "id", description = "The ID of the light to change") int id,
      @KernelFunctionParameter(name = "isOn", description = "The new state of the light") boolean isOn) {
    System.out.println("Changing light " + id + " " + isOn);
    if (!lights.containsKey(id)) {
      throw new IllegalArgumentException("Light not found");
    }

    lights.get(id).setIsOn(isOn);

    return lights.get(id);
  }
}

Si noti che vengono fornite descrizioni per la funzione, il valore restituito e i parametri. Questo è importante per l'intelligenza artificiale per comprendere cosa fa la funzione e come usarlo.

Suggerimento

Non avere paura di fornire descrizioni dettagliate per le funzioni se un'intelligenza artificiale ha problemi a chiamarli. Esempi di pochi scatti, raccomandazioni per quando usare (e non usare) la funzione e indicazioni su dove ottenere i parametri obbligatori possono essere utili.

2) Aggiungere il plug-in al kernel

Dopo aver definito il plug-in, è possibile aggiungerlo al kernel creando una nuova istanza del plug-in e aggiungendola alla raccolta di plug-in del kernel.

Questo esempio illustra il modo più semplice per aggiungere una classe come plug-in con il AddFromType metodo . Per altre informazioni su altri modi per aggiungere plug-in, vedere l'articolo relativo all'aggiunta di plug-in nativi.

var builder = new KernelBuilder();
builder.Plugins.AddFromType<LightsPlugin>("Lights")
Kernel kernel = builder.Build();
kernel = Kernel()
kernel.add_plugin(
   LightsPlugin(),
   plugin_name="Lights",
)
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
    "LightsPlugin");
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
    .withAIService(ChatCompletionService.class, chatCompletionService)
    .withPlugin(lightPlugin)
    .build();

3) Richiamare le funzioni del plug-in

Infine, è possibile fare in modo che l'intelligenza artificiale richiami le funzioni del plug-in usando la chiamata di funzione. Di seguito è riportato un esempio che illustra come eseguire il coax dell'intelligenza artificiale per chiamare la get_lights funzione dal Lights plug-in prima di chiamare la change_state funzione per attivare una luce.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

// Create a kernel with Azure OpenAI chat completion
var builder = Kernel.CreateBuilder().AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey);

// Build the kernel
Kernel kernel = builder.Build();
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();

// Add a plugin (the LightsPlugin class is defined below)
kernel.Plugins.AddFromType<LightsPlugin>("Lights");

// Enable planning
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() 
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

// Create a history store the conversation
var history = new ChatHistory();
history.AddUserMessage("Please turn on the lamp");

// Get the response from the AI
var result = await chatCompletionService.GetChatMessageContentAsync(
   history,
   executionSettings: openAIPromptExecutionSettings,
   kernel: kernel);

// Print the results
Console.WriteLine("Assistant > " + result);

// Add the message from the agent to the chat history
history.AddAssistantMessage(result);
import asyncio

from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.functions.kernel_arguments import KernelArguments

from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
    AzureChatPromptExecutionSettings,
)

async def main():
   # Initialize the kernel
   kernel = Kernel()

   # Add Azure OpenAI chat completion
   chat_completion = AzureChatCompletion(
      deployment_name="your_models_deployment_name",
      api_key="your_api_key",
      base_url="your_base_url",
   )
   kernel.add_service(chat_completion)

   # Add a plugin (the LightsPlugin class is defined below)
   kernel.add_plugin(
      LightsPlugin(),
      plugin_name="Lights",
   )

   # Enable planning
   execution_settings = AzureChatPromptExecutionSettings(tool_choice="auto")
   execution_settings.function_call_behavior = FunctionCallBehavior.EnableFunctions(auto_invoke=True, filters={})

   # Create a history of the conversation
   history = ChatHistory()
   history.add_message("Please turn on the lamp")

   # Get the response from the AI
   result = await chat_completion.get_chat_message_content(
      chat_history=history,
      settings=execution_settings,
      kernel=kernel,
   )

   # Print the results
   print("Assistant > " + str(result))

   # Add the message from the agent to the chat history
   history.add_message(result)

# Run the main function
if __name__ == "__main__":
    asyncio.run(main())
// Enable planning
InvocationContext invocationContext = new InvocationContext.Builder()
    .withReturnMode(InvocationReturnMode.LAST_MESSAGE_ONLY)
    .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
    .build();

// Create a history to store the conversation
ChatHistory history = new ChatHistory();
history.addUserMessage("Turn on light 2");

List<ChatMessageContent<?>> results = chatCompletionService
    .getChatMessageContentsAsync(history, kernel, invocationContext)
    .block();

System.out.println("Assistant > " + results.get(0));

Con il codice precedente, si dovrebbe ottenere una risposta simile alla seguente:

Ruolo Message
🔵Utente Accendere la lampada
🔴Assistente (chiamata di funzione) Lights.get_lights()
🟢Strumento [{ "id": 1, "name": "Table Lamp", "isOn": false, "brightness": 100, "hex": "FF0000" }, { "id": 2, "name": "Porch light", "isOn": false, "brightness": 50, "hex": "00FF00" }, { "id": 3, "name": "Chandelier", "isOn": true, "brightness": 75, "hex": "0000FF" }]
🔴Assistente (chiamata di funzione) Lights.change_state(1, { "isOn": true })
🟢Strumento { "id": 1, "name": "Table Lamp", "isOn": true, "brightness": 100, "hex": "FF0000" }
🔴Assistente La lampada è ora attiva

Suggerimento

Anche se è possibile richiamare direttamente una funzione plug-in, questo non è consigliato perché l'intelligenza artificiale deve essere quella che decide quali funzioni chiamare. Se è necessario un controllo esplicito sulle funzioni chiamate, è consigliabile usare i metodi standard nella codebase anziché i plug-in.