Ajouter du code natif en tant que plug-in
Le moyen le plus simple de fournir à un agent IA des fonctionnalités qui ne sont pas prises en charge en mode natif consiste à encapsuler le code natif dans un plug-in. Cela vous permet de tirer parti de vos compétences existantes en tant que développeur d’applications pour étendre les fonctionnalités de vos agents IA.
En arrière-plan, le noyau sémantique utilisera ensuite les descriptions que vous fournissez, ainsi que la réflexion, pour décrire sémantiquement le plug-in à l’agent IA. Cela permet à l’agent IA de comprendre les fonctionnalités du plug-in et comment interagir avec lui.
Fournir au LLM les informations appropriées
Lors de la création d’un plug-in, vous devez fournir à l’agent IA les informations appropriées pour comprendre les fonctionnalités du plug-in et de ses fonctions. notamment :
- Nom du plug-in
- Noms des fonctions
- Descriptions des fonctions
- Paramètres des fonctions
- Schéma des paramètres
La valeur du noyau sémantique est qu’elle peut générer automatiquement la plupart de ces informations à partir du code lui-même. En tant que développeur, cela signifie simplement que vous devez fournir les descriptions sémantiques des fonctions et des paramètres afin que l’agent IA puisse les comprendre. Si vous commentez et annotez correctement votre code, toutefois, vous disposez probablement déjà de ces informations.
Ci-dessous, nous allons parcourir les deux façons différentes de fournir à votre agent IA du code natif et comment fournir ces informations sémantiques.
Définition d’un plug-in à l’aide d’une classe
Le moyen le plus simple de créer un plug-in natif consiste à commencer par une classe, puis à ajouter des méthodes annotées avec l’attribut KernelFunction
. Il est également recommandé d’utiliser Description
l’annotation de manière libérale pour fournir à l’agent IA les informations nécessaires pour comprendre la fonction.
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());
}
}
Conseil
Étant donné que les modules LLM sont principalement formés sur du code Python, il est recommandé d’utiliser snake_case pour les noms de fonctions et les paramètres (même si vous utilisez C# ou Java). Cela aidera l’agent IA à mieux comprendre la fonction et ses paramètres.
Si votre fonction a un objet complexe en tant que variable d’entrée, le noyau sémantique génère également un schéma pour cet objet et le transmet à l’agent IA. Comme pour les fonctions, vous devez fournir Description
des annotations pour les propriétés qui ne sont pas évidentes pour l’IA. Voici la définition de la LightState
classe et de l’énumération 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;
}
}
Remarque
Bien qu’il s’agit d’un exemple « amusant », il fait un bon travail montrant à quel point les paramètres d’un plug-in peuvent être complexes. Dans ce cas unique, nous avons un objet complexe avec quatre types de propriétés différents : un entier, une chaîne, une valeur booléenne et une énumération. La valeur du noyau sémantique est qu’elle peut générer automatiquement le schéma de cet objet et la transmettre à l’agent IA et marshaler les paramètres générés par l’agent IA dans l’objet correct.
Une fois que vous avez terminé de créer votre classe de plug-in, vous pouvez l’ajouter au noyau à l’aide des méthodes ou AddFromObject
des AddFromType<>
méthodes.
Conseil
Lors de la création d’une fonction, demandez-vous toujours « comment puis-je donner à l’IA une aide supplémentaire pour utiliser cette fonction ? » Cela peut inclure l’utilisation de types d’entrée spécifiques (évitez les chaînes si possible), en fournissant des descriptions et des exemples.
Ajout d’un plug-in à l’aide de la AddFromObject
méthode
La AddFromObject
méthode vous permet d’ajouter une instance de la classe de plug-in directement à la collection de plug-ins au cas où vous souhaitez contrôler directement la façon dont le plug-in est construit.
Par exemple, le constructeur de la LightsPlugin
classe nécessite la liste des lumières. Dans ce cas, vous pouvez créer une instance de la classe de plug-in et l’ajouter à la collection 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));
Ajout d’un plug-in à l’aide de la AddFromType<>
méthode
Lorsque vous utilisez la AddFromType<>
méthode, le noyau utilise automatiquement l’injection de dépendances pour créer une instance de la classe de plug-in et l’ajouter à la collection de plug-ins.
Cela est utile si votre constructeur nécessite des services ou d’autres dépendances à injecter dans le plug-in. Par exemple, notre LightsPlugin
classe peut nécessiter un enregistreur d’événements et un service de lumière à injecter dans celui-ci au lieu d’une liste de lumières.
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);
}
}
Avec l’injection de dépendances, vous pouvez ajouter les services et plug-ins requis au générateur de noyau avant de générer le noyau.
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();
Définition d’un plug-in à l’aide d’une collection de fonctions
Moins courant mais toujours utile, il est utile de définir un plug-in à l’aide d’une collection de fonctions. Cela est particulièrement utile si vous devez créer dynamiquement un plug-in à partir d’un ensemble de fonctions au moment de l’exécution.
L’utilisation de ce processus vous oblige à utiliser la fabrique de fonctions pour créer des fonctions individuelles avant de les ajouter au 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"
)
]);
Stratégies supplémentaires pour l’ajout de code natif avec l’injection de dépendances
Si vous utilisez l’injection de dépendances, vous pouvez créer et ajouter des plug-ins au noyau. Voici quelques exemples de la façon dont vous pouvez ajouter un plug-in à l’aide de l’injection de dépendances.
Injecter une collection de plug-ins
Conseil
Nous vous recommandons de rendre votre collection de plug-ins un service temporaire afin qu’il soit supprimé après chaque utilisation, car la collection de plug-ins est mutable. La création d’une collection de plug-ins pour chaque utilisation est bon marché. Il ne doit donc pas s’agir d’un problème de performances.
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);
});
Conseil
Comme mentionné dans l’article du noyau, le noyau est extrêmement léger, de sorte que la création d’un noyau pour chaque utilisation en tant que temporaire n’est pas un problème de performances.
Générer vos plug-ins en tant que singletons
Les plug-ins ne sont pas mutables. Il est donc généralement sûr de les créer en tant que singletons. Pour ce faire, utilisez la fabrique de plug-ins et ajoutez le plug-in résultant à votre collection de services.
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);
});
Ajout d’un plug-in à l’aide de la add_plugin
méthode
La add_plugin
méthode vous permet d’ajouter une instance de plug-in au noyau. Voici un exemple de la façon dont vous pouvez construire la LightsPlugin
classe et l’ajouter au noyau.
# 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)
Ajout d’un plug-in à l’aide de la createFromObject
méthode
La createFromObject
méthode vous permet de générer un plug-in de noyau à partir d’un objet avec des méthodes annotées.
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
"LightsPlugin");
Ce plug-in peut ensuite être ajouté à un noyau.
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, chatCompletionService)
.withPlugin(lightPlugin)
.build();
Étapes suivantes
Maintenant que vous savez comment créer un plug-in, vous pouvez maintenant apprendre à les utiliser avec votre agent IA. Selon le type de fonctions que vous avez ajoutées à vos plug-ins, vous devez suivre différents modèles. Pour les fonctions de récupération, reportez-vous à l’article sur l’utilisation des fonctions de récupération. Pour les fonctions d’automatisation des tâches, reportez-vous à l’article utilisation des fonctions d’automatisation des tâches .