Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: Find Associated Tool(s) Called #10654

Closed
aherrick opened this issue Feb 24, 2025 · 6 comments
Closed

.Net: Find Associated Tool(s) Called #10654

aherrick opened this issue Feb 24, 2025 · 6 comments
Assignees
Labels
.NET Issue or Pull requests regarding .NET code

Comments

@aherrick
Copy link

aherrick commented Feb 24, 2025

I'm trying to figure out the cleanest way to figure out what tools were invoked in a call. Here is what I've come up with but there has to be a better way?

using System.ComponentModel;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

var kernel = Kernel
    .CreateBuilder()
    .AddAzureOpenAIChatCompletion(
        deploymentName: "gpt-4o",
        endpoint: "",
        apiKey: ""
    )
    .Build();

kernel.Plugins.AddFromType<WeatherPlugin>();

var chatHistory = new ChatHistory();

chatHistory.AddUserMessage("whats the weather");

var promptExecutionSettings = new PromptExecutionSettings
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
};

var chatService = kernel.GetRequiredService<IChatCompletionService>();
var result = await chatService.GetChatMessageContentAsync(
    chatHistory,
    promptExecutionSettings,
    kernel
);

Console.WriteLine(result.Content);

var plugins = GetPluginsUsed(chatHistory);
foreach (var plugin in plugins)
{
    Console.WriteLine(plugin);
}

Console.ReadLine();

static List<string> GetPluginsUsed(ChatHistory chatHistory)
{
    // Find the index of the most recent user message
    int lastUserMessageIndex = -1;
    for (int i = chatHistory.Count - 1; i >= 0; i--)
    {
        if (chatHistory[i].Role == AuthorRole.User)
        {
            lastUserMessageIndex = i;
            break;
        }
    }

    // Extract only tool messages that appear AFTER the last user message
    var toolMessages = chatHistory
        .Skip(lastUserMessageIndex + 1) // Start from the message after the user's last input
        .Where(msg => msg.Role == AuthorRole.Tool)
        .SelectMany(toolMsg =>
            toolMsg
                .Items.OfType<FunctionResultContent>()
                .Select(x => $"({x.PluginName}: {x.FunctionName})")
        )
        .ToList();

    return toolMessages;
}

public class WeatherPlugin
{
    // Hard coded weather data
    private readonly string[] weatherData =
    [
        "Sunny, 15°C",
        "Cloudy, 12°C",
        "Rainy, 10°C",
        "Snowy, -2°C",
    ];

    private readonly Random random = new();

    [KernelFunction, Description("Get weather information")]
    public string GetWeather()
    {
        // Get random weather data
        int index = random.Next(weatherData.Length);
        return weatherData[index];
    }
}
@moonbox3 moonbox3 changed the title Find Associated Tool(s) Called .Net: Find Associated Tool(s) Called Feb 25, 2025
@moonbox3 moonbox3 added the .NET Issue or Pull requests regarding .NET code label Feb 25, 2025
@moonbox3
Copy link
Contributor

Adding @SergeyMenshykh to help.

@aherrick
Copy link
Author

@moonbox3 @SergeyMenshykh additionally, can data be pulled as to why the LLM picked which plugin(s)?

@SergeyMenshykh
Copy link
Member

SergeyMenshykh commented Feb 27, 2025

Hi @aherrick, I can think of a few ways to get called functions:

  1. Traverse chat history up to the first user message and collect all items of FunctionCallContent type along the way. That's what your code snipper does.
  2. Register either a function invocation filter or an auto function invocation filter, as pointed out by @romainsemur. Both filters will be called per function call.
  3. Enable manual function invocation as described here and here, where the GetChatMessageContentAsync method gives you full access to the content returned by the AI model, including function calls. However, in this case, you will have to invoke the functions yourself.

If you just need to identify called functions, go with option 1 or option 2 because it may be easier from an implementation point of view. If you need a little bit more control than just identifying which function was called, such as terminating function invocation or changing the invocation results, go with option 2. If you need full control over interpreting function calls, such as getting a list of functions called, deciding whether to invoke them or not, changing arguments and results, etc., go with option 3.

@SergeyMenshykh SergeyMenshykh moved this to Sprint: Planned in Semantic Kernel Feb 27, 2025
@SergeyMenshykh SergeyMenshykh moved this from Sprint: Planned to Sprint: In Progress in Semantic Kernel Feb 27, 2025
@aherrick
Copy link
Author

@SergeyMenshykh thanks for the detailed response! my thought was that the "result" object that is returned from the GetChatMessageContentAsync would simply have a list of functions invoked. But I'm ok traversing for now as I need the plugins used inline with the call, so a Function Filter doesn't really help me.

@SergeyMenshykh
Copy link
Member

The issue has been resolved. If you have any other questions or need more help, please reach out.

@SergeyMenshykh SergeyMenshykh moved this from Sprint: In Progress to Sprint: Done in Semantic Kernel Feb 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
.NET Issue or Pull requests regarding .NET code
Projects
Status: Sprint: Done
Development

No branches or pull requests

4 participants