In last week blog post, we saw how to extend an AI agents intelligence using function tools.
In this post, we see how you build AI agents and advertise them as function tools to other agents . There are a few reasons why you might want to do this.
In this blog post we cover:
- why implement an agent as function tool
- how to implement an agent as function tool
- creating a nutrition agent that can find protein rich foods
- extending a personal trainer ai agent use the nutritionist agent
- implementing the nutrition agent as a function tool
- testing personal trainer ai agent new capabilities
A video demo and full code examples are included.
~
Why Implement an Agent as Function Tool
You might want to use this pattern to isolate behaviours and responsibilities during your AI agent development.
With this approach, you can build multiple agents, use them individually, or augment an existing AI agent’s capability with behaviours from other agents.
Applying this approach removes the risk of developing monolithic agents.
Creating discrete agents and advertising them as function tools makes it easier to design, build, maintain, and observe your AI agents.
~
How to Implement an Agent as Function Tool
To implement an agent as function tool you use the AsAIFunction method and call this from your parent agent.
You can see an example of this here:
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
.CreateAIAgent(instructions: "You are a personal trainer." +
"You can answer questions related to health, fitness and 5x5 stronglift. " +
"You should only use function tools in the PersonalTrainerAgent or NutritionAgent.",
name: "IronMind AI", null,
tools: [AIFunctionFactory.Create(PersonalTrainerAgent.GetNextAvailableDate),
AIFunctionFactory.Create(PersonalTrainerAgent.CancelAppointment),
AIFunctionFactory.Create(PersonalTrainerAgent.BookAppointment),
AIFunctionFactory.Create(PersonalTrainerAgent.ListBookedAppointments),
YOUR-ADDITIONAL-AGENT.AsAIFunction()]
);
Let’s see how we can apply this pattern to extend our original Iron Mind AI personal trainer agent.
~
An Example – Extending the Iron Mind AI Personal Trainer Agent
In an earlier blog post, we created 4 function tools and made them available to the Iron Mind AI personal trainer agent.
To recap, these were:
- Get next available date for a booking
- Make a booking
- Cancel a booking
- Get list of existing bookings
We want to extend the Iron Mind AI personal trainer agent to handle more than booking sessions. We want to introduce function tools that let people:
- Search and filter for high protein recipes
- Learn how to cook and prepare each recipe
- Examine key nutrients and macros
We can do this by creating a new Nutritionist Agent. Then, by using the .AsAIFunction() method, make this available to our Personal Trainer Agent as an additional function tool.
Before we do that though, a few things must be done:
- Obtain recipe data
- Create a way to search for recipes
- Map to objects for use by our nutrition agent
Lets look at these now.
Obtaining Recipe Data
We need a data source with recipes. There are plenty online and found plenty at: https://www.jamieoliver.com/search/.
We also need objects to represent the data.
- Recipe
- NutritionInfo
The definitions for these are as follows:
public class Recipe
{
public string Title { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public List<string> Ingredients { get; set; } = new();
public List<string> Instructions { get; set; } = new();
public NutritionInfo? Nutrition { get; set; }
public string PrepTime { get; set; } = string.Empty;
public string CookTime { get; set; } = string.Empty;
public int Servings { get; set; }
}
public class NutritionInfo
{
public int Calories { get; set; }
public int ProteinGrams { get; set; }
public int CarbsGrams { get; set; }
public int FatGrams { get; set; }
}
Now we need a way to fetch and serialise data from the source to the above objects.
~
Creating a Nutrition Service to Search for Data
We can create a service class to encapsulate all core search, scraping and parsing logic.
We’ll call this NutritionService.
Rather than manually implement this, I used Claude to create most of this service class then manually removed code that was overkill.
The key methods in the NutritionService are:
- SearchRecipesAsync – searches for recipes that match a search term
- ScrapeRecipeAsync – fetches details for a recipe
Some utility methods are also included to help with scraping and parsing recipe HTML data to the Recipe and NutritionInfo objects we created earlier.
A completed definition for the NutritionService is provided:
public class NutritionService
{
private readonly HttpClient _httpClient;
private readonly HtmlWeb _htmlWeb;
private readonly int _delayMs;
public NutritionService(int delayMs = 1500)
{
_delayMs = delayMs;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) RecipeBot/1.0");
_htmlWeb = new HtmlWeb();
}
/// <summary>
/// Searches Jamie Oliver's site and returns recipe URLs
/// </summary>
public async Task<List<string>> SearchRecipesAsync(string searchTerm)
{
var urls = new List<string>();
var searchUrl = $"https://www.jamieoliver.com/search/{Uri.EscapeDataString(searchTerm)}";
await Task.Delay(_delayMs);
var doc = await _htmlWeb.LoadFromWebAsync(searchUrl);
var linkNodes = doc.DocumentNode.SelectNodes("//a[contains(@href,'/recipes/')]");
if (linkNodes != null)
{
foreach (var linkNode in linkNodes)
{
var href = linkNode.GetAttributeValue("href", "");
if (!string.IsNullOrWhiteSpace(href))
{
if (href.StartsWith("/"))
href = $"https://www.jamieoliver.com{href}";
if (IsRecipeUrl(href) && !urls.Contains(href))
urls.Add(href);
}
}
}
Console.WriteLine($"Found {urls.Count} recipe URLs for search term '{searchTerm}'.");
return urls;
}
/// <summary>
/// Scrapes a single recipe from URL
/// </summary>
public async Task<Recipe?> ScrapeRecipeAsync(string url)
{
await Task.Delay(_delayMs);
var doc = await _htmlWeb.LoadFromWebAsync(url);
// Try JSON-LD first, fallback to HTML
return ParseJsonLdRecipe(doc, url) ?? ParseHtmlRecipe(doc, url);
}
/// <summary>
/// Filters recipes by minimum protein content
/// </summary>
public List<Recipe> FilterByProtein(List<Recipe> recipes, int minProteinGrams)
{
return recipes
.Where(r => r.Nutrition != null && r.Nutrition.ProteinGrams >= minProteinGrams)
.OrderByDescending(r => r.Nutrition!.ProteinGrams)
.ToList();
}
#region Parsing Helpers
private Recipe? ParseJsonLdRecipe(HtmlDocument doc, string url)
{
var scriptNodes = doc.DocumentNode.SelectNodes("//script[@type='application/ld+json']");
if (scriptNodes == null) return null;
foreach (var scriptNode in scriptNodes)
{
var json = scriptNode.InnerText;
if (!json.Contains("\"@type\":\"Recipe\"")) continue;
try
{
using var jsonDoc = JsonDocument.Parse(json);
var root = jsonDoc.RootElement;
var recipeElement = root.ValueKind == JsonValueKind.Array ? root[0] : root;
var recipe = new Recipe
{
Url = url,
Title = GetJsonString(recipeElement, "name"),
PrepTime = GetJsonString(recipeElement, "prepTime"),
CookTime = GetJsonString(recipeElement, "cookTime")
};
// Servings
if (recipeElement.TryGetProperty("recipeYield", out var yieldElement))
{
recipe.Servings = yieldElement.ValueKind == JsonValueKind.Number
? yieldElement.GetInt32()
: ExtractNumber(yieldElement.GetString() ?? "");
}
// Ingredients
if (recipeElement.TryGetProperty("recipeIngredient", out var ingredients))
{
foreach (var ingredient in ingredients.EnumerateArray())
recipe.Ingredients.Add(ingredient.GetString() ?? "");
}
// Instructions
if (recipeElement.TryGetProperty("recipeInstructions", out var instructions))
{
int step = 1;
foreach (var instruction in instructions.EnumerateArray())
{
string text = instruction.ValueKind == JsonValueKind.Object
? GetJsonString(instruction, "text")
: instruction.GetString() ?? "";
if (!string.IsNullOrWhiteSpace(text))
recipe.Instructions.Add($"{step++}. {text}");
}
}
// Nutrition
if (recipeElement.TryGetProperty("nutrition", out var nutrition))
{
recipe.Nutrition = new NutritionInfo
{
Calories = ExtractNumber(GetJsonString(nutrition, "calories")),
ProteinGrams = ExtractNumber(GetJsonString(nutrition, "proteinContent")),
CarbsGrams = ExtractNumber(GetJsonString(nutrition, "carbohydrateContent")),
FatGrams = ExtractNumber(GetJsonString(nutrition, "fatContent"))
};
}
return recipe;
}
catch (JsonException)
{
continue;
}
}
return null;
}
private Recipe? ParseHtmlRecipe(HtmlDocument doc, string url)
{
var recipe = new Recipe { Url = url };
// Title
var titleNode = doc.DocumentNode.SelectSingleNode("//h1");
recipe.Title = CleanText(titleNode?.InnerText ?? "Unknown Recipe");
// Ingredients
var ingredientNodes = doc.DocumentNode.SelectNodes(
"//ul[contains(@class,'ingred')]//li | //div[contains(@class,'ingredient')]//li");
if (ingredientNodes != null)
{
foreach (var node in ingredientNodes)
{
var ingredient = CleanText(node.InnerText);
if (!string.IsNullOrWhiteSpace(ingredient))
recipe.Ingredients.Add(ingredient);
}
}
// Instructions
var instructionNodes = doc.DocumentNode.SelectNodes(
"//ol[contains(@class,'method')]//li | //div[contains(@class,'method')]//p");
if (instructionNodes != null)
{
int step = 1;
foreach (var node in instructionNodes)
{
var instruction = CleanText(node.InnerText);
if (!string.IsNullOrWhiteSpace(instruction) && instruction.Length > 20)
recipe.Instructions.Add($"{step++}. {instruction}");
}
}
return recipe.Ingredients.Any() ? recipe : null;
}
private bool IsRecipeUrl(string url) =>
url.Contains("/recipes/") &&
!url.Contains("/recipes/category") &&
!url.Contains("/recipes/course") &&
!url.EndsWith("/recipes/");
private string CleanText(string text)
{
if (string.IsNullOrWhiteSpace(text)) return string.Empty;
text = HtmlEntity.DeEntitize(text);
text = Regex.Replace(text, @"\s+", " ");
return text.Trim();
}
private string GetJsonString(JsonElement element, string propertyName) =>
element.TryGetProperty(propertyName, out var prop) ? prop.GetString() ?? "" : "";
private int ExtractNumber(string text)
{
if (string.IsNullOrWhiteSpace(text)) return 0;
var match = Regex.Match(text, @"\d+");
return match.Success && int.TryParse(match.Value, out var number) ? number : 0;
}
#endregion
}
With the objects and service class in place, we can expose this functionality through a new agent -the Nutrition Agent.
~
Creating the Nutrition Agent and Design Considerations
This purpose of this agent is to allow people to ask for recipes that support dietary requirements when training with the 5 x 5 weight training programme.
The Nutrition Agent acts as a window into the capabilities of the Nutrition Service and makes these capabilities accessible from a natural language perspective.
Our service class does most of the heavy lifting.
You could argue the agent seems like an extra layer of abstraction over the core functionality in the Nutrition Service class.
This is a valid point. Not everything requires an AI solution.
You might not need to interact with a large language model or reason over code using natural language. In this example, we want to use natural language and assign an agent with task.
By exposing these features as an agent, it can be repurposed, leveraged as a function tool within its own right and easily integrated with large language models or other AI solutions.
The definition of the Nutrition Agent:
public class NutritionAgent
{
[Description("Searches for high protein recipes optimized for strength training. " +
"Returns recipes with detailed nutrition information. " +
"Best for finding recipes for 5x5 training, muscle building, or post-workout meals.")]
public static async Task<List<string>> SearchRecipesAsync([Description("Search term (e.g., 'chicken', 'beef', 'protein', 'breakfast')")] string searchTerm,
[Description("Minimum protein grams per serving (default 20)")] int minProtein = 20,
[Description("Maximum number of recipes to return (default 3)")] int maxResults = 3)
{
var service = new NutritionService();
Console.WriteLine($"Searching for recipes with term '{searchTerm}' and minimum {minProtein}g protein...");
var recipes = await service.SearchRecipesAsync(searchTerm);
var suitableRecipes = new List<Recipe>();
// Scrape each recipe for details
foreach (var recipeUrl in recipes)
{
Console.WriteLine($"Scraping recipe details from {recipeUrl}...");
var recipe = await service.ScrapeRecipeAsync(recipeUrl);
if(suitableRecipes.Count >= maxResults)
{
break;
}
if (recipe != null && recipe.Nutrition.ProteinGrams >= minProtein)
{
Console.WriteLine($"Found suitable recipe: {recipe.Title} with {recipe.Nutrition.ProteinGrams}g protein.");
suitableRecipes.Add(recipe);
}
}
return suitableRecipes.Select(r => JsonSerializer.Serialize(r)).ToList();
}
}
We could create an agent with following the code:
AIAgent nutritionAgent = new OpenAIClient(apiKey)
.GetChatClient(model)
.CreateAIAgent(instructions: "You can fetch recipes and nutrition data.",
name: "IronMind AI", null,
tools [AIFunctionFactory.Create(NutritionAgent.SearchRecipesAsync)]
);
But we want to see how to advertise the Nutrition Agent as an additional function tool from the main personal trainer agent.
~
Implementing the Nutrition Agent as a Function Tool
We can extend our original personal trainer agents intelligence by adding the Nutrition Agent as a function tool.
This is done by using the AsAIFunction method.
We can see this here:
AIAgent nutritionAgent = new OpenAIClient(apiKey)
.GetChatClient(model)
.CreateAIAgent(instructions: "You can fetch recipes and nutrition data.",
name: "IronMind AI", null,
tools: [AIFunctionFactory.Create(NutritionAgent.SearchRecipesAsync)]
);
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
.CreateAIAgent(instructions: "You are a personal trainer." +
"You can answer questions related to health, fitness and 5x5 stronglift. " +
"You shuld only use function tools in the PersonalTrainerAgent or NutritionAgent.",
name: "IronMind AI", null,
tools: [AIFunctionFactory.Create(PersonalTrainerAgent.GetNextAvailableDate),
AIFunctionFactory.Create(PersonalTrainerAgent.CancelAppointment),
AIFunctionFactory.Create(PersonalTrainerAgent.BookAppointment),
AIFunctionFactory.Create(PersonalTrainerAgent.ListBookedAppointments),
nutritionAgent.AsAIFunction()]
);
In the above code, we create the nutrition agent. Next, when the personal trainer agent is created, we pass in the Nutrition Agent but invoke the AsAIFunction method.
This lets you quickly extend the original agents capabilities.
You can use this pattern to build multiple agents, each with different responsibilities and daisy chain capabilities based on your requirements.
~
Testing Iron Mind AI Agent New Capabilities
We can now test the AI agent capabilities in a console application.
static async Task Main(string[] args)
{
AIAgent nutritionAgent = new OpenAIClient(apiKey)
.GetChatClient(model)
.CreateAIAgent(instructions: "You can fetch recipes and nutrition data.",
name: "IronMind AI", null,
tools: [AIFunctionFactory.Create(NutritionAgent.SearchRecipesAsync)]
);
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
.CreateAIAgent(instructions: "You are a personal trainer." +
"You can answer questions related to health, fitness and 5x5 stronglift. " +
"You should only use function tools in the PersonalTrainerAgent or NutritionAgent.",
name: "IronMind AI", null,
tools: [AIFunctionFactory.Create(PersonalTrainerAgent.GetNextAvailableDate),
AIFunctionFactory.Create(PersonalTrainerAgent.CancelAppointment),
AIFunctionFactory.Create(PersonalTrainerAgent.BookAppointment),
AIFunctionFactory.Create(PersonalTrainerAgent.ListBookedAppointments),
nutritionAgent.AsAIFunction()]
);
await RunChatLoopWithThreadAsync(agent);
}
First, we supply a prompt in natural language askin get me 3 recipes with at least 20g of protein in each.
The agent automatically maps the intent to our new nutrition service and attempts search for recipes:
After a few moments, 3 recipes are found:
Next, the agent provides us with the ingredients and cooking instructions:
### 1. Mushroom Stew - **Protein:** 34g - **Calories:** 580 - **Carbs:** 87g - **Fat:** 9g - **Servings:** 2 **Ingredients:** - 250g mixed mushrooms - 4 spring onions - 2 carrots (160g) - 5cm piece of ginger - Olive oil - 150g self-raising flour - 1 x 400g tin of black beans - 1 x 225g tin of sliced water chestnuts - 160g beansprouts - 2 tablespoons gochujang paste - 150g silken tofu - Optional: extra virgin olive oil **Instructions:** 1. Fry mushrooms until nutty, then set aside. 2. Cook spring onions, carrots, and ginger with oil for 5 minutes. 3. Make dumpling dough and form balls. 4. Add beans, water chestnuts, and dumplings to the pan, cover, and cook for 10 minutes. 5. Season, add tofu, and drizzle with extra virgin olive oil before serving. --- ### 2. Toad in the Hole - **Protein:** 27g - **Calories:** 677 - **Carbs:** 44g - **Fat:** 44g - **Servings:** 4 **Ingredients:** - Sunflower oil - 8 higher-welfare sausages - 4 sprigs of fresh rosemary - 2 large red onions - 2 cloves of garlic - 2 knobs of unsalted butter - 6 tablespoons balsamic vinegar - 1 level tablespoon vegetable stock powder or cube - **Batter:** - 285ml milk - 115g plain flour - 3 large free-range eggs **Instructions:** 1. Whisk batter ingredients and set aside. 2. Heat oil in a baking tin in a hot oven. 3. Add sausages and brown, then pour batter over. 4. Bake until golden and crisp. 5. For gravy, fry onions and garlic, add balsamic vinegar and stock, simmer, and serve. --- ### 3. Jamie's Classic Family Lasagne - **Protein:** 28g - **Calories:** 420 - **Carbs:** 35g - **Fat:** 18g - **Servings:** 12 **Ingredients:** - 2 sprigs of fresh rosemary - 100g higher-welfare smoked streaky bacon - Olive oil - 1kg quality minced beef - 1kg higher-welfare minced pork - 4 carrots - 2 onions - 4 sticks of celery - 2 heaped tablespoons tomato purée - 4 x 400g tins plum tomatoes - 350g dried lasagne sheets - **White Sauce:** - 150g mature Cheddar cheese - 2 medium leeks - 2 fresh bay leaves - 4 tablespoons plain flour - 1 litre semi-skimmed milk - 1 whole nutmeg, for grating **Instructions:** 1. Fry rosemary and bacon, then add minced meats. 2. Add chopped vegetables and cook until softened. 3. Mix in tomatoes and simmer until thickened. 4. Make white sauce by cooking leeks and then adding flour and milk. 5. Layer lasagne in a dish, top with cheese, and bake until golden. These recipes provide ample protein and are nutritious options for a healthy diet! Enjoy your cooking!
Perfect.
We have given our original agent a new capability. It can now manage bookings and handle nutrition requests.
~
Demo
In this demo, we can see the above in action. We supply the prompt and the AI agent automatically invokes our new nutrition capabilities.
~
Summary
In this blog post, we’ve seen how agents can be advertised as function tools to another agent.
We saw how to extend or Iron Mind AI personal trainer agent and give it access to capabilities contained within another agent.
You might use this pattern to isolate behaviours and responsibilities during your AI agent development.
In the next blog post, we’ll look at how to add human-in-the-loop approval.
You might want to implement this prior to an agent invoking a specific function tool. Useful for medium to high-risk use cases.
Stay tuned.
~
Like What You’ve Read?
Enjoy what you’ve read, have questions about this content, or would like to see another topic? Drop me a note below.
You can schedule a call using my Calendly link to discuss consulting and development services.
~
Further Reading and Resources
You can find more blogs in this series here and other resources to help you on your agentic AI journey.
More from this series
- Key components in Agent Framework– Microsoft Agent Framework: First Look
- Intro to the Agent Framework–  Introducing Microsoft Agent Framework: An Open-Source Engine for Agentic AI
- Using Conversations and Threads – Microsoft Agent Framework: Conversations and Threads
- Extending Agent Intelligence Using Function Tools – Microsoft Agent Framework: Extending Agent Intelligence Using Function Tools
Microsoft Learn
~
This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the
in C# are 100% mine.