Today we will kick off a short series on building AI agents which have access to OpenAPI tools. In this first part, we will focus on the Semantic Kernel, and in the second part, we will look at Azure AI Foundry.
Series overview π
- Part 1 (this part): Semantic Kernel
- Part 2 (coming soon): Azure AI Foundry
Overview π
Tool calling is an integral part of building AI agents, allowing them to interact with external APIs and services. I covered the basics of tool calling with Azure OpenAI last year, but in the ever changing landscape of AI, there are new tools and frameworks that make this process even more streamlined.
With the rise of AI agents, we can differentiate three primary main approaches to tool calling:
- Dedicated tools: hand-written adapters or functions that are tailored to a specific API or service. These custom integrations sit alongside the AI model, offering precise control, customized error-handling, and a predictable interaction pattern. There is a tight coupling between the code orchestrating the agent and the APIs it uses.
- Schema-Driven tools: generic JSON/HTTP interfaces defined by a formal schema - most commonly OpenAPI specifications or Model Context Protocol (MCP) manifests. By importing a spec or manifest, the AI can call any compliant API through a uniform βfunction-callingβ interface, without needing dedicated code for each endpoint.
- Sandboxed code execution: a live REPL (e.g. Python or JavaScript) environment exposed to the model, where it can generate, execute, and iterate on code on the fly. This on-demand compute layer excels at ad-hoc data manipulation, complex transformations, and mathematical reasoning, without requiring pre-defined API bindings.
In this series, we will focus on the schema-driven approach - more specifically, one based on OpenAPI contracts. And in part 1, we will use the Semantic Kernel to build an AI agent that can call OpenAPI tools.
Setting up the prerequisites π
Note: For this example, we will use Python, but the same principles apply to C# and Java (other languages supported by Semantic Kernel).
Let’s start by installing the necessary packages:
pip install semantic_kernel[azure] fastapi uvicorn requests
We will also need an OpenAPI tool to call. For the purposes of this post, we will use a dummy weather forecast FastAPI application that exposes an OpenAPI spec. You can create a file named fastapi_weather.py with the following content:
import datetime
import random
from typing import List, Optional
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import uvicorn
app = FastAPI(
title="Weather Forecast API",
description="A simple weather forecast API that provides 5-day weather predictions with temperature and weather conditions",
version="1.0.0",
openapi_url="/openapi.json",
servers=[
{
"url": "http://localhost:5270",
"description": "API server"
}
]
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class WeatherForecast(BaseModel):
"""
Weather forecast data model containing temperature and weather conditions for a specific date.
"""
date: str = Field(..., description="The date of the forecast in ISO format (YYYY-MM-DD)", example="2025-06-23")
temperatureC: int = Field(..., description="Temperature in Celsius degrees", example=25, ge=-50, le=60)
summary: str = Field(..., description="Brief description of weather conditions", example="Warm")
@property
def temperatureF(self) -> int:
"""Convert Celsius temperature to Fahrenheit"""
return 32 + int(self.temperatureC / 0.5556)
summaries = [
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
]
@app.get(
"/weatherforecast",
response_model=List[WeatherForecast],
operation_id="GetWeatherForecast",
summary="Get 5-day weather forecast",
description="Retrieves a 5-day weather forecast starting from today. Each forecast includes the date, temperature in Celsius (and Fahrenheit), and a summary of weather conditions.",
response_description="A list of weather forecasts for the next 5 days",
tags=["Weather"]
)
async def get_weather_forecast():
"""
Get a 5-day weather forecast starting from today.
This endpoint generates random weather data for demonstration purposes.
Each forecast includes:
- Date in ISO format (YYYY-MM-DD)
- Temperature in Celsius (automatically converted to Fahrenheit)
- Weather summary description
Returns:
List[WeatherForecast]: A list of 5 weather forecasts starting from today
"""
forecasts = []
for index in range(0, 5):
current_date = datetime.datetime.now() + datetime.timedelta(days=index)
forecast = WeatherForecast(
date=current_date.strftime("%Y-%m-%d"),
temperatureC=random.randint(-20, 55),
summary=random.choice(summaries)
)
forecasts.append(forecast)
return forecasts
if __name__ == "__main__":
uvicorn.run("fastapi_weather:app", host="127.0.0.1", port=5270, reload=True)
The API returns a 5-day weather forecast with random temperatures and summaries. You can run this FastAPI application by executing:
python fastapi_weather.py
The server is then started on localhost:5270. The OpenAPI spec is generated automatically - hence the rich and descriptive comments in the code above. You can access the OpenAPI spec at http://localhost:5270/openapi.json.
The returned document should be as follows:
{
"openapi": "3.1.0",
"info": {
"title": "Weather Forecast API",
"description": "A simple weather forecast API that provides 5-day weather predictions with temperature and weather conditions",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:5270",
"description": "API server"
}
],
"paths": {
"/weatherforecast": {
"get": {
"tags": [
"Weather"
],
"summary": "Get 5-day weather forecast",
"description": "Retrieves a 5-day weather forecast starting from today. Each forecast includes the date, temperature in Celsius (and Fahrenheit), and a summary of weather conditions.",
"operationId": "GetWeatherForecast",
"responses": {
"200": {
"description": "A list of weather forecasts for the next 5 days",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/WeatherForecast"
},
"type": "array",
"title": "Response Getweatherforecast"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"WeatherForecast": {
"properties": {
"date": {
"type": "string",
"title": "Date",
"description": "The date of the forecast in ISO format (YYYY-MM-DD)",
"example": "2025-06-23"
},
"temperatureC": {
"type": "integer",
"maximum": 60.0,
"minimum": -50.0,
"title": "Temperaturec",
"description": "Temperature in Celsius degrees",
"example": 25
},
"summary": {
"type": "string",
"title": "Summary",
"description": "Brief description of weather conditions",
"example": "Warm"
}
},
"type": "object",
"required": [
"date",
"temperatureC",
"summary"
],
"title": "WeatherForecast",
"description": "Weather forecast data model containing temperature and weather conditions for a specific date."
}
}
}
}
Consuming OpenAPI tools with Semantic Kernel π
With this in place, we can now setup a Semantic Kernel AI agent that can call our OpenAPI API as its own tool. There are two primary ways how we can inject an OpenAPI plugin into Semantic Kernel. First option is to have a predefined spec content and add it directly to the kernel. The second option is to provide a URL to the OpenAPI spec, and let the kernel fetch it dynamically. In both cases we will actually use the same add_plugin_from_openapi method, but the first option requires us to fetch the OpenAPI spec ourselves, while the second option allows us to pass a URL directly.
Let’s create a file named sample.py with the following content:
import asyncio
import os
import sys
import requests
from dotenv import load_dotenv
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel import Kernel
load_dotenv()
async def main():
kernel = Kernel()
api_host = os.environ.get("API_HOST", "http://localhost:5270")
openapi_url = f"{api_host}/openapi.json"
# option 1: pre-fetch OpenAPI specification dynamically from the running server
try:
response = requests.get(openapi_url)
if response.status_code == 200:
swagger_dict = response.json()
print(f"β
Successfully fetched OpenAPI spec from {openapi_url}")
else:
raise Exception(f"Failed to fetch OpenAPI spec: HTTP {response.status_code}")
except Exception as e:
print(f"β Error fetching OpenAPI spec from {openapi_url}: {e}")
print(f"Make sure the FastAPI server is running on {api_host}")
return
# add the plugin to the kernel
weather_plugin = kernel.add_plugin_from_openapi(
plugin_name="WeatherPlugin",
openapi_parsed_spec=swagger_dict
)
print("β
Created OpenAPI plugin")
# more code to follow...
This code initializes a Semantic Kernel instance and fetches the OpenAPI spec from the FastAPI server we started earlier. It then adds the OpenAPI plugin to the kernel as a pre-fetched spec. The alternative variant is probably simpler, although you are no longer in control of the OpenAPI spec retrieval.
# option 2: create a plugin from a URL directly
weather_plugin = kernel.add_plugin_from_openapi(
plugin_name="WeatherPlugin",
openapi_document_path=openapi_url,
)
print("β
Created OpenAPI plugin from URL")
The rest of the code is then simply to create an AI agent that can call the OpenAPI tool. We will use Semantic Kernel’s ChatCompletionAgent to create an agent that can interact with the OpenAPI plugin we just created. The necessary environment variables for Azure OpenAI are expected to be set in a .env file or in your environment.
agent = ChatCompletionAgent(
service=AzureChatCompletion(
deployment_name=os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini"),
endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
api_key=os.environ.get("AZURE_OPENAI_API_KEY"),
),
name="WeatherAgent",
instructions="You are a helpful assistant that provides weather forecasts using the OpenAPI plugin.",
plugins=[weather_plugin]
)
print("β
Created weather forecast agent with plugin")
thread = None
user_input = "What's the weather forecast for today?"
print(f"\n㪠User: {user_input}")
response = await agent.get_response(
messages=user_input,
thread=thread,
)
print(f"\nπ€ Agent: {response}")
thread = response.thread
user_input = "Will it be cold tomorrow?"
print(f"\n㪠User: {user_input}")
response = await agent.get_response(
messages=user_input,
thread=thread,
)
print(f"\nπ€ Agent: {response}")
print("\nπ§Ή Cleaning up resources...")
if thread:
try:
await thread.delete()
print("β
Thread deleted")
except Exception as e:
print(f"β οΈ Error deleting thread: {e}")
if __name__ == "__main__":
asyncio.run(main())
And that’s it! You can run this code by executing:
python sample.py
Of course make sure the API server is simultaneously running in the background! This should let a sample conversation play itself out with the AI agent, which will call the OpenAPI tool to get the weather forecast. The agent will respond to user queries about the weather, using the OpenAPI plugin we created.
An example of the output would look like this:
> python sample.py
β
Created OpenAPI plugin from URL
β
Created weather forecast agent with plugin
π¬ User: What's the weather forecast for today?
π€ Agent: The weather forecast for today, June 23, 2025, is as follows:
- **Temperature:** 18Β°C
- **Summary:** Mild
Please let me know if you need the forecast for any other day or location!
π¬ User: Will it be cold tomorrow?
π€ Agent: The weather forecast for tomorrow, June 24, 2025, indicates:
- **Temperature:** 35Β°C
- **Summary:** Hot
So, it will not be cold tomorrow; rather, it will be quite hot. Let me know if you need more information!
π§Ή Cleaning up resources...
β
Thread deleted
And that’s it! It was a simple example of how to use Semantic Kernel to create an AI agent that can call OpenAPI tools. In the next part, we will look at how to do the same with Azure AI Foundry. If you are interested in the demo code from this post, you can find it on GitHub.