Table of Contents
In the previous episodes...
we created a .Net application powered by Semantic Kernel. We connected it to GitHub Models and added a basic system message asking it to act as a nutrition and fitness assistant.
From the technical standpoint, we also created a basic "architecture", splitting the solution into three projects:
- AI — AI-related logic,
- FatSecretAPI — for integrating FatSecret API,
- Console — a simple basic interface for user interactions.
So far t…
Table of Contents
In the previous episodes...
we created a .Net application powered by Semantic Kernel. We connected it to GitHub Models and added a basic system message asking it to act as a nutrition and fitness assistant.
From the technical standpoint, we also created a basic "architecture", splitting the solution into three projects:
- AI — AI-related logic,
- FatSecretAPI — for integrating FatSecret API,
- Console — a simple basic interface for user interactions.
So far the app runs and answers our questions, but it still doesn’t know anything about us! It’s time to give it some hints, showing who we are and what do we eat.
In this chapter we will create a Plugin for Semantic Kernel. But first, let’s quickly discuss what a Plugin actually is.
Plugins
In Semantic Kernel documentation we can read that Plugins "encapsulate your existing APIs into a collection that can be used by AI". Basically, there are two main types of things we can encapsulate in a Plugin: something to fetch and something to do. For example, AI agent in your IDE can read your codebase ("fetch data") and make changes in the code ("do actions"). This concept is what makes an agent an agent: a language model itself is like a head in a jar (any Futurama fans?), and Plugin is an interface that connects this "head" with the world around it.
In general, there are four ways of providing a plugin to your app:
- adding native code plugin (Microsoft documentation),
- adding OpenAPI as a plugin (Microsoft documentation),
- adding plugin from an MCP server (Microsoft documentation),
- integrating with Azure Logic Apps (Microsoft documentation).
Our application’s main goal is actually to learn, so we will go with the first option - a native code plugin. This means that we will implement a C# class with all the logic we need.
The other three options are more specific. They can be useful, but I don’t want to concentrate on them now, so we don’t get distracted from our basic application.
Just a small remark: the OpenAPI option looks too interesting to completely ignore. If you have created some HTTP APIs you probably know what Swagger/OpenAPI is. In Semantic Kernel you can introduce your OpenAPI specification by providing a link to it. And then, your LLM can send HTTP requests to the endpoints described in your specification. Sounds like a fun thing to try out one day! See the link above to read more about this way of introducing a Plugin.
Anyway, let’s get back to our FatAdvisor and to our decision to implement the Plugin as a native code (a C# class).
We want our LLM to be able to fetch some knowledge from the outside. In this version of the app, we won’t teach our agent to invoke any actions, we will only let it receive the data it needs. We create what is, from the programming language’s point of view, a class with methods. Each of these methods will provide some functionality for the LLM to use. Though "method" is a common concept in OOP and in C# in particular, in AI agent terminology you will find such entry points called "functions", "tools" or "actions". Semantic Kernel allows us to provide descriptions for these methods via attributes. The LLM then reads these descriptions, and it should be enough for it to understand for which purposes which function can be invoked.
We will implement our Plugin as a C# class, so we can inject any services (HTTP clients, DB connections etc.) in it. In our case, the Plugin that we’re going to make will be a wrapper over the FatSecret API. We will add only those functions that our application really needs.
Coding
Interfaces and Stubs
To keep this chapter from becoming too long and to concentrate on the Plugin itself, we will not implement the real integration with third-party services such as the FatSecret API or any others. We will define interfaces and create stub implementation of it imitating the desired behavior. Don’t worry — the real implementation will be described in the next chapter. In addition, with these mocking services I can lie to our FatSecret giving it much better impression of what I’m eating than my real diet.
Let’s define what interfaces we need:
- IFatSecretApiClient - the most important interface for us. It will contain the methods that we can invoke in FatSecret API. The real version sends HTTP requests, while the stub simply returns predefined responses.
- IFatSecretOAuth - the authorization workflow for FatSecret.
- IProfileTokenStorage - not really mandatory, but we want to store our token returned by IFatSecretOAuth, so we can reuse and refresh them properly.
And for each of them let’s create a stub implementation that returns static data.
Now the FatAdvisor.FatSecretApi project structure will look like this:
-
Stubs
-
ApiStubData
-
FoodLog0.json
-
WeightLog0.json
-
FatSecretApiClientStub.cs
-
FatSecretOAuthStub.cs
-
ProfileTokenStorageStub.cs
-
FatSecretApiModule.cs
In the ApiStubData folder there are JSON files that imitate responses from the FatSecret API.
FatAdvisor.FatSecretApi/Stubs/ApiStubData/FoodLog0.json: (full sample JSON file is available in the repository)
{
"food_entries": {
"food_entry": [
{
"calories": "64",
"carbohydrate": "13.32",
"date_int": "20394",
"fat": "0.24",
"food_entry_description": "0.3 serving 30 g Bread slice",
"food_entry_id": "72145983012",
"food_entry_name": "Bread slice",
"food_id": "84213058",
"meal": "Breakfast",
"number_of_units": "0.300",
"protein": "2.19",
"serving_id": "29396720"
},
...
]
}
}
FatAdvisor.FatSecretApi/Stubs/ApiStubData/WeightLog0.json: (full sample JSON file is available in the repository)
{
"month": {
"day": [
{
"date_int": "20362",
"weight_kg": "60.7000"
},
...
],
"from_date_int": "20362",
"to_date_int": "20392"
}
}
Here’s the implementation of FatSecret API client that reads data from these stub files:
FatAdvisor.FatSecretApi/Stubs/FatSecretApiClientStub.cs:
using FatAdvisor.Ai;
using FatAdvisor.Ai.Models;
namespace FatAdvisor.FatSecretApi.Stubs
{
public class FatSecretApiClientStub : IFatSecretApiClient
{
private static Task<string> ReadDataFromFile(string fileName)
{
var filePath = Path.Combine("Stubs", "ApiStubData", fileName);
return File.ReadAllTextAsync(filePath);
}
public Task<string> GetFoodsForDateAsync(DateTime date) => ReadDataFromFile("FoodLog0.json");
public Task<string> GetWeightDiary(DateTime date) => ReadDataFromFile("WeightLog0.json");
public void SetAccessToken(TokenInfo tokenInfo)
{
// Do nothing
}
}
}
We also need to make temporary fake services for authorization. We need just to return something, any string will work.
FatAdvisor.FatSecretApi/Stubs/FatSecretOAuthStub.cs:
using FatAdvisor.Ai;
using FatAdvisor.Ai.Models;
namespace FatAdvisor.FatSecretApi.Stubs
{
public class FatSecretOAuthStub : IFatSecretOAuth
{
public Task<TokenInfo> GetAccessTokenAsync(string requestToken, string requestTokenSecret, string verifier) =>
Task.FromResult(new TokenInfo("access_token_stub", "access_token_secret_stub") );
public string GetAuthorizationUrl(string requestToken, string? callbackUrl) => $"http://localhost?token={requestToken}";
public Task<TokenInfo> GetRequestTokenAsync(string? callbackUrl) =>
Task.FromResult(new TokenInfo("request_token_stub", "request_token_secret_stub") );
}
}
The same with the token storage service. Earlier we pretended to receive a token, now we pretend to store it:
FatAdvisor.FatSecretApi/Stubs/ProfileTokenStorageStub.cs:
using FatAdvisor.Ai;
using FatAdvisor.Ai.Models;
namespace FatAdvisor.FatSecretApi.Stubs
{
public class ProfileTokenStorageStub : IProfileTokenStorage
{
public Task<TokenInfo> FindTokenInStorage() => Task.FromResult(
new TokenInfo("access_token_stub", "access_token_secret_stub"));
public Task SaveTokenToStorage(TokenInfo accessTokenInfo) => Task.CompletedTask;
}
}
Plugin implementation
We have created a fake implementation of external services and now we are ready for the most interesting part of this chapter. We are going to create The Plugin!
There are two main functions we need for our agent to start with:
- get_consumed_food – get all the consumed food (food log) for the selected day.
- get_weight_diary – get the weight diary for the month until the specified date
They will help us find the answer to the main nutrition questions: what did I eat and what are the consequences of it? We will explain to the agent that it can use these functions when it needs them.
So here’s the implementation. Let’s take a look at it, and then we will walk through it. It’s very simple:
FatAdvisor.Ai/FatSecretProfileDataPlugin.cs:
using FatAdvisor.Ai.Models;
using Microsoft.SemanticKernel;
using System.ComponentModel;
namespace FatAdvisor.Ai
{
public class FatSecretProfileDataPlugin
{
private readonly IFatSecretApiClient _fatSecretApiClient;
private readonly IFatSecretOAuth _fatSecretOAuth;
private readonly IProfileTokenStorage _profileTokenStorage;
public FatSecretProfileDataPlugin(
IFatSecretApiClient fatSecretApiClient,
IFatSecretOAuth fatSecretOAuth,
IProfileTokenStorage profileTokenStorage)
{
_fatSecretApiClient = fatSecretApiClient;
_fatSecretOAuth = fatSecretOAuth;
_profileTokenStorage = profileTokenStorage;
}
[KernelFunction("get_consumed_food")]
[Description("Get all the consumed food (food log) for selected day")]
public async Task<string> GetConsumedFood(DateTime date)
{
var accessTokenInfo = await _profileTokenStorage.FindTokenInStorage();
if (accessTokenInfo == null)
{
// If no token is found, we need to authorize the user
accessTokenInfo = await AuthorizeAndSaveToken();
}
_fatSecretApiClient.SetAccessToken(accessTokenInfo);
var todaysFood = await _fatSecretApiClient.GetFoodsForDateAsync(date);
return todaysFood;
}
[KernelFunction("get_weight_diary")]
[Description("Get the weight diary for month due to the specified date")]
public async Task<string> GetWeightDiaryForMonth(DateTime date)
{
var accessTokenInfo = await _profileTokenStorage.FindTokenInStorage();
if (accessTokenInfo == null)
{
// If no token is found, we need to authorize the user
accessTokenInfo = await AuthorizeAndSaveToken();
}
_fatSecretApiClient.SetAccessToken(accessTokenInfo);
var todaysFood = await _fatSecretApiClient.GetWeightDiary(date);
return todaysFood;
}
private async Task<TokenInfo> AuthorizeAndSaveToken()
{
string? callbackUrl = null; //For prototype we could use null.
var requestTokenInfo = await _fatSecretOAuth.GetRequestTokenAsync(callbackUrl);
var authorizationUrl = _fatSecretOAuth.GetAuthorizationUrl(
requestTokenInfo.Token,
callbackUrl);
Console.WriteLine($"Please visit: {authorizationUrl}");
Console.WriteLine("\nPaste the verifier code here:");
string verifier = Console.ReadLine() ?? "";
var accessTokenInfo = await _fatSecretOAuth.GetAccessTokenAsync(
requestTokenInfo.Token,
requestTokenInfo.TokenSecret,
verifier);
await _profileTokenStorage.SaveTokenToStorage(accessTokenInfo);
return accessTokenInfo;
}
}
}
In the constructor there’s nothing extraordinary. We just inject our interfaces (the rules for resolving them will be specified in the Autofac module).
Speaking of Task<TokenInfo> AuthorizeAndSaveToken(), inside this method we call _fatSecretOAuth to execute the 3-legged OAuth logic. We will discuss it in the next chapter where we will talk about real integration with the FatSecret API. But for now it’s enough to understand that this method receives an access token for the FatSecret API and stores it (via _profileTokenStorage abstraction).
But the real shiny stars of our plugin are these two tiny Functions: GetConsumedFood and GetWeightDiaryForMonth. What’s going on inside is very straightforward:
- Try to find an access token for the FatSecret API in the storage.
- If it’s not there, authorize via OAuth dance and store the received token in the storage. Thus, on the upcoming iteration we will save some resources by not requesting OAuth endpoints again and again.
- Invoke the appropriate method of FatSecret API.
A piece of cake, right? What is interesting here, first, the attributes. These two small guys tell the LLM something important:
[KernelFunction("get_consumed_food")]
[Description("Get all the consumed food (food log) for selected day")]
public async Task<string> GetConsumedFood(DateTime date)
Actually, they provide metadata to LLM.
The KernelFunction attribute is needed to mark this C# method as a SemanticKernel’s function. In other words, that’s how we tell the LLM that this method is for it to invoke.
The Description attribute serves to give more context. Let’s admit, we’re living in the future. Through all these years of evolution of programming languages we learned and learned to speak with computers in a formal, deterministic way, through these programming languages. But now we have to decorate our beautiful C# code with wordy, redundant human language. This is the future — but are we happy?
Ok, just kidding.
But I hope you got the idea: with this attribute we can describe the function for the LLM, providing more information — like what this function is supposed to do, or in which situation the LLM might consider running it.
Unless KernelFunction, which is required in the general case for Semantic Kernel functions, the Description attribute is optional. And as the documentations says, we should rely more on concise and understandable function names (and parameter names as well), and use the Description attribute "only when necessary to minimize token consumption."
DI
That’s how we register our Semantic Kernel and plugins in the DI container:
FatAdvisor.Ai/FatSecretAiModule.cs:
using Autofac;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
namespace FatAdvisor.Ai
{
public class FatSecretAiModule : Module
{
private readonly IConfiguration _config;
public FatSecretAiModule(IConfiguration config) => _config = config;
protected override void Load(ContainerBuilder builder)
{
var apiKey = _config["GitHubModels:ApiKey"];
var endpoint = _config["GitHubModels:Endpoint"];
var modelId = "gpt-4o-mini";
builder
.Register(ctx =>
{
var loggerFactory = ctx.Resolve<ILoggerFactory>();
var kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.Services.AddSingleton(loggerFactory);
kernelBuilder.AddOpenAIChatCompletion(
modelId: modelId,
apiKey: apiKey,
endpoint: new Uri(endpoint));
// Register plugins during Kernel construction
var fatSecretPlugin = ctx.Resolve<FatSecretProfileDataPlugin>();
kernelBuilder.Plugins.AddFromObject(fatSecretPlugin);
return kernelBuilder.Build();
})
.As<Kernel>()
.SingleInstance();
builder.RegisterType<FatSecretProfileDataPlugin>().AsSelf().SingleInstance();
}
}
}
And that’s how we register our stubs:
FatAdvisor.FatSecretApi/FatSecretApiModule.cs
using Autofac;
using FatAdvisor.Ai;
using FatAdvisor.FatSecretApi.Stubs;
using Microsoft.Extensions.Configuration;
namespace FatAdvisor.FatSecretApi
{
public class FatSecretApiModule : Module
{
private readonly IConfiguration _config; // Not used right now, but will be useful later.
public FatSecretApiModule(IConfiguration config) => _config = config;
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<FatSecretOAuthStub>().As<IFatSecretOAuth>().SingleInstance();
builder.RegisterType<FatSecretApiClientStub>().As<IFatSecretApiClient>().SingleInstance();
builder.RegisterType<ProfileTokenStorageStub>().As<IProfileTokenStorage>().SingleInstance();
}
}
}
Running!
It seems like we are ready to run the app and ask the Fat Advisor some questions. I’ve modified the ConsoleAppRunner class to formulate both the system and user messages a bit more concretely.
I won’t put the whole code of this class here (you can find it in the repo).
Just to highlight the most important parts of the messages:
var yesterday = DateTime.Now.AddDays(-1);
var chatHistory = new ChatHistory();
chatHistory.AddSystemMessage("You are a nutrition and training assistant. " +
"When answering the user, always consider user's FatSecret profile data including: " +
$"FatSecret food log for the last 2 days (check it for every day in the range of last 2 days until {yesterday}. " +
"If available, consider user's weight diary (for the last month) to understand what " +
"amount of food should be taken. In your response display weight diary and consumed food diary. ");
var userMessage = "Please review my nutrition habits and give me advice on what I should change to gain muscle without gaining fat.";
Let’s run the app, and that’s what we see in the console:
dbug: Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService[0]
Function choice behavior configuration: Choice:auto, AutoInvoke:True, AllowConcurrentInvocation:False, AllowParallelCalls:(null) Functions:FatSecretProfileDataPlugin-get_consumed_food, FatSecretProfileDataPlugin-get_weight_diary
info: Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService[0]
Prompt tokens: 224. Completion tokens: 116. Total tokens: 340.
Ok, this should look familiar and was discussed in the Part 1. There’s a summary of tokens consumption and the basic configuration.
Next, we can see the log of Function calls like that:
info: Microsoft.SemanticKernel.KernelFunction[0]
Function FatSecretProfileDataPlugin-get_consumed_food invoking.
info: Microsoft.SemanticKernel.KernelFunction[0]
Function FatSecretProfileDataPlugin-get_consumed_food succeeded.
info: Microsoft.SemanticKernel.KernelFunction[0]
Function FatSecretProfileDataPlugin-get_consumed_food completed. Duration: 0.0447523s
We see similar logs for every function invocation, which is good and shows that the model correctly selected which functions to call based on the prompt and available plugins.
And finally, after all these log records, we see the model’s response! Here I’ll put just some fragments of it, not the whole text.
### Your Nutrition and Weight Diary Overview
#### Weight Diary (Last Month)
| Date | Weight (kg) |
|------------|-------------|
| 2025-05-03 | 60.7 |
| 2025-05-10 | 60.3 |
| 2025-05-12 | 61.1 |
| 2025-05-22 | 60.8 |
| 2025-05-27 | 61.0 |
#### Consumed Food Diary
**June 3, 2025:**
| Meal | Food Description | Calories | Protein | Carbohydrates | Fat |
|-----------|--------------------------------------|----------|---------|---------------|------|
| Breakfast | Caramel pastry (50g) | 275 | 4.00 | 20.75 | 19.75|
| Breakfast | Bread slice (30g) | 64 | 2.19 | 13.32 | 0.24 |
...
### Nutrition Advice for Muscle Gain without Fat Gain
1. **Increase Protein Intake**: Your protein intake for each day (~49g) is relatively low for muscle gain. Aim for at least **1.6 to 2.2 grams of protein per kilogram of body weight**, which, considering your weight (around 61 kg), should be approximately **100g to 130g** of protein daily.
2. **Quality Carbohydrates**: The carbohydrates in your diet are significant; however, focus on complex carbs (whole grains, oats, sweet potatoes, legumes) rather than sugars and processed foods like the milk chocolate. These will provide sustainable energy for your workouts.
...
Well, that looks pretty fair!
Conclusions
Today we learned how to introduce some Functions to an AI agent. They are pretty simple and only cover the data retrieval part. We know that AI agents can also perform actions, but we haven’t implemented anything like that yet. Maybe later we can add it, at least as an example.
We taught our agent to receive data about our food intake and weight. The data is still fake, so we kind of lie to the agent — but it’s in the name of research!
In the next chapter I’m going to make a couple of steps from AI and Semantic Kernel itself and focus on implementing real interaction with the FatSecret API. This part will be more for those who want to play with the FatSecret API on their own, rather than for those who are learning Semantic Kernel. But I think it should be fun. We want to make things real, right?