Chapter 7: Converting Existing APIs into Plugins
Theoretical Foundations
The theoretical foundation of converting existing APIs into plugins rests on a fundamental paradigm shift: transforming rigid, procedural HTTP endpoints into dynamic, semantic capabilities that an AI agent can reason about and invoke autonomously. This process is not merely a syntactic translation; it is an architectural metamorphosis that bridges the deterministic world of RESTful web services with the probabilistic nature of Large Language Models (LLMs).
The Problem of Semantic Dissonance
At its core, an AI agent operates on intent and context. When a user requests, "Check the inventory for the new smartwatch and notify my team if it's below 50 units," the agent must decompose this into actionable steps: query a database, compare a value, and send a message. Traditional REST APIs, however, speak only the language of HTTP verbs, URIs, and structured payloads. They possess no intrinsic understanding of "inventory," "smartwatch," or "notification."
This creates a semantic dissonance. The API knows how to perform the operation (e.g., GET /api/inventory/{id}), but the AI agent knows what the user wants to achieve. Without a bridge, the agent is forced to guess parameter mappings and endpoint structures, leading to brittle, error-prone interactions.
Converting an API into a plugin resolves this dissonance by wrapping the procedural HTTP call in a semantic envelope. This envelope provides the LLM with the necessary context: what the function does, what parameters it requires, and how to map natural language concepts to concrete data types.
The Kitchen Analogy: Recipes vs. Raw Ingredients
Imagine a master chef (the AI Agent) working in a kitchen equipped with a pantry full of raw ingredients (the Legacy REST APIs). The pantry contains flour, eggs, sugar, and spices, but they are unlabeled and stored in random containers. To bake a cake, the chef must first identify the correct ingredients, measure them precisely, and combine them using specific techniques.
If the chef is an expert, they might succeed. However, if the chef is an apprentice (a less capable model), they will struggle. They might grab salt instead of sugar, or use an egg without cracking it.
Plugins are the pre-measured, labeled recipe cards.
Inputs: 2 cups flour, 3 eggs, 1 cup sugar (The Parameters). 2. Process: Mix dry, beat wet, combine, bake at 350°F (The Execution Logic). 3. Output: A cake (The Result).
In the context of Semantic Kernel, the plugin transforms the raw API endpoint into a "recipe" the AI can follow without needing to understand the underlying chemistry (HTTP protocols) or pantry layout (server URLs). The AI simply reads the recipe (the plugin definition) and executes the steps.
The KernelPlugin Architecture: A Unified Interface
In Semantic Kernel, the KernelPlugin class serves as the container for these capabilities. It abstracts away the implementation details, presenting a uniform interface to the AI kernel. Whether a function is a local C# method, a prompt template, or a remote HTTP call, the agent interacts with it through the same KernelFunction abstraction.
This is critical because it allows the Planner (a component discussed in previous chapters) to treat all capabilities as interchangeable nodes in a graph. The Planner doesn't need to know if "GetWeather" is a local calculation or a call to api.weather.com; it only needs to know that invoking GetWeather with a location parameter returns a forecast.
The RestApiOperation Runner: Dynamic HTTP Execution
The theoretical engine driving the conversion of REST APIs is the RestApiOperation runner. Unlike traditional SDK generation, which produces static, hard-coded client classes, the RestApiOperation approach is dynamic and metadata-driven.
Operation ID: The unique identifier for the endpoint. * Path & Method: The URI template and HTTP verb (GET, POST, etc.). * Parameters: Inputs (query, path, header, body) with types and constraints. * Responses: Expected status codes and return schemas.
The RestApiOperation runner uses this metadata at runtime to construct and execute HTTP requests. This is a shift from compile-time binding to runtime orchestration.
Adaptability: If the API changes (e.g., a new optional parameter is added), we only update the metadata definition. The underlying runner logic remains unchanged. This allows the AI to adapt to API drift without requiring a full recompilation of the agent.
2. Context Window Efficiency: Instead of feeding the LLM the raw OpenAPI JSON (which can be massive), we can semantically summarize the operation. The runner handles the translation from the summary back to the precise HTTP call.
3. Complex Parameter Mapping: AI models often output arguments in natural language (e.g., "next Monday"). The runner is responsible for the coercion and mapping phase—converting "next Monday" into the ISO 8601 format required by the API (2023-10-30T00:00:00Z).
Authentication and Security: The Gatekeeper Pattern
A theoretical challenge in exposing APIs to AI is security. An AI agent should not manage raw API keys or OAuth tokens directly; doing so exposes sensitive credentials to the model's context window and potential leakage via logs or prompt injection attacks.
Parameter Extraction: The agent identifies the required auth parameters (e.g., Authorization: Bearer <token>).
2. Credential Injection: Instead of the AI providing the token, a secure HttpHandler injects it. The AI only needs to know that authentication is required, not the secret itself.
3. Policy Enforcement: Handlers can enforce rate limiting, retry policies, and circuit breaking, ensuring the AI agent doesn't overwhelm the legacy system.
This separation of concerns ensures that the Semantic Layer (what the AI knows) is decoupled from the Security Layer (how the system is protected).
Semantic Wrappers: From Intent to Structured Call
The final theoretical pillar is the creation of Semantic Wrappers. While the RestApiOperation runner handles the mechanics of the HTTP call, the Semantic Wrapper handles the translation of intent.
A raw API call is brittle. If an AI tries to construct a JSON body based on a user's vague request, it might omit required fields or use incorrect data types. A Semantic Wrapper acts as a strict contract.
Consider a legacy API endpoint POST /orders. It requires a complex JSON body:
{
"customer": { "id": "string", "email": "string" },
"items": [{ "sku": "string", "qty": "int" }],
"shipping": { "address": "string", "zip": "string" }
}
shipping object.
By creating a Semantic Wrapper, we define a Prompt Template that guides the LLM to extract only the necessary information. The wrapper acts as a filter and a constructor.
User Input: "Order 5 units of the 'X1' laptop to my office."
2. Semantic Wrapper (Plugin): Receives the natural language.
3. LLM Invocation (Internal): The wrapper uses a prompt like: "Given the user request '{{request}}', extract the SKU, quantity, and shipping address. Output valid JSON matching the schema defined in 'order_schema.json'."
4. Structured Output: The LLM outputs a clean, validated JSON object.
5. Execution: The RestApiOperation runner takes this structured JSON and executes the HTTP POST.
This pattern enforces Reliability. The AI is not guessing the API structure; it is filling in a form defined by the plugin.
Visualizing the Architecture
The following diagram illustrates the flow from the User's natural language request, through the Plugin abstraction, down to the HTTP execution, and back.
Theoretical Foundations
Records and Immutability: The metadata describing an API operation (parameters, paths) is effectively immutable data. Using record types ensures that once an operation is parsed from an OpenAPI spec, it cannot be accidentally modified during execution, providing thread safety for concurrent agent invocations.
2. Pattern Matching: When the RestApiOperation runner processes parameters, it uses pattern matching to determine how to serialize data. Is this parameter a query string? A path segment? A JSON body? Pattern matching allows the runner to switch strategies cleanly based on the parameter's metadata type.
3. Source Generators (Theoretical Extension): While the runtime approach is dynamic, source generators could theoretically be used to pre-compile OpenAPI specifications into highly optimized KernelPlugin classes during build time, reducing startup latency for agents requiring strict performance guarantees.
4. Async/Await Streams: API calls are inherently I/O bound. The theoretical model relies on IAsyncEnumerable<T> to stream responses back to the agent, allowing for progressive processing of large datasets without blocking the agent's reasoning cycle.
Converting APIs to plugins is the act of semantic translation. It bridges the gap between the rigid, structured world of HTTP and the fluid, intent-driven world of AI. By utilizing a metadata-driven runner (RestApiOperation) and semantic wrappers, we create a system where the AI doesn't just call an API; it understands the capability the API provides, maps its intent to the required inputs, and executes the call securely and reliably. This transforms the AI from a passive text generator into an active participant in the digital ecosystem.
Basic Code Example
Imagine you have a legacy weather forecasting service, a REST API that your company has used for years. It provides a simple endpoint like GET /forecast?city=London&days=3. You want to build an AI agent that can answer user queries like "What's the weather like in Paris for the next 5 days?". Instead of hardcoding a specific function call, you want the AI to understand the user's intent, map it to your existing API, and execute it dynamically. This is the core problem we solve: converting a standard REST API into a plugin that the AI agent can discover and use.
Basic Code Example: The Weather API Plugin
This example demonstrates how to take a simple OpenAPI specification for a weather service and convert it into a Semantic Kernel plugin. We will simulate the API call using a mock HttpClient for a fully self-contained demonstration.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
using System.Text.Json;
using System.Text.Json.Serialization;
// 1. Define the data model for the API response.
// This helps the AI understand the structure of the data it receives.
public record WeatherForecast(
[property: JsonPropertyName("city")] string City,
[property: JsonPropertyName("date")] DateOnly Date,
[property: JsonPropertyName("temperature_celsius")] double TemperatureCelsius,
[property: JsonPropertyName("description")] string Description
);
// 2. Define the OpenAPI specification for our weather service.
// In a real scenario, this would be a URL or a file path to a .json file.
// For this self-contained example, we define it as a string.
const string OpenApiSpec = """
{
"openapi": "3.0.1",
"info": {
"title": "Weather Forecast API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.weatherexample.com"
}
],
"paths": {
"/forecast": {
"get": {
"summary": "Get weather forecast for a city",
"operationId": "getForecast",
"parameters": [
{
"name": "city",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "days",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 1
}
}
],
"responses": {
"200": {
"description": "A successful response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WeatherForecast"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"WeatherForecast": {
"type": "object",
"properties": {
"city": { "type": "string" },
"date": { "type": "string", "format": "date" },
"temperature_celsius": { "type": "number" },
"description": { "type": "string" }
}
}
}
}
}
""";
// 3. Create a mock HttpClient to simulate the API response.
// This makes the example runnable without an actual external service.
public class MockWeatherHttpClient : HttpClient
{
public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Simulate network delay
await Task.Delay(100, cancellationToken);
// Check if the request is to our specific endpoint
if (request.RequestUri?.AbsolutePath.Contains("/forecast") == true)
{
// Parse query parameters to simulate dynamic response
var query = System.Web.HttpUtility.ParseQueryString(request.RequestUri.Query);
var city = query["city"] ?? "Unknown";
var days = int.Parse(query["days"] ?? "1");
// Generate mock forecast data
var forecasts = Enumerable.Range(0, days)
.Select(i => new WeatherForecast(
City: city,
Date: DateOnly.FromDateTime(DateTime.Now.AddDays(i)),
TemperatureCelsius: 20 + Random.Shared.NextDouble() * 10,
Description: i % 2 == 0 ? "Sunny" : "Cloudy"
))
.ToList();
var jsonResponse = JsonSerializer.Serialize(forecasts);
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(jsonResponse, System.Text.Encoding.UTF8, "application/json")
};
}
return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound);
}
}
// 4. The main execution block
public class Program
{
public static async Task Main(string[] args)
{
// --- A. Kernel Setup ---
// Create a kernel builder to register services and plugins.
var builder = Kernel.CreateBuilder();
// Register our mock HTTP client for dependency injection.
// In production, you would register a real HttpClient with appropriate handlers.
builder.Services.AddSingleton<HttpClient>(new MockWeatherHttpClient());
var kernel = builder.Build();
// --- B. Plugin Creation from OpenAPI ---
// Create a stream from the OpenAPI spec string.
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(OpenApiSpec));
// Import the API as a plugin. Semantic Kernel will parse the spec,
// create a function for the 'getForecast' operation, and map parameters.
// The 'executionParameters' configure how the plugin behaves.
var weatherPlugin = await kernel.ImportPluginFromOpenApiAsync(
"WeatherPlugin",
stream,
new OpenApiFunctionExecutionParameters()
{
// Use our mock HTTP client instead of the default one.
HttpClient = kernel.Services.GetRequiredService<HttpClient>(),
// Enable automatic parameter mapping from function arguments to HTTP request.
EnableDynamicPayload = true,
// Allow the AI to use the plugin without explicit function calling if needed.
EnableFunctions = true
}
);
Console.WriteLine("✅ Weather plugin loaded successfully.");
Console.WriteLine($" Available functions: {string.Join(", ", weatherPlugin.Select(p => p.Name))}");
// --- C. Invoking the Plugin via the AI Agent ---
// Create a prompt that requires the AI to use the weather plugin.
// The AI will see the function definition and decide to call it.
var prompt = "What's the weather forecast for London for the next 3 days?";
Console.WriteLine($"\n🤖 User Query: {prompt}");
// Execute the prompt. The AI (e.g., GPT-4) will analyze the prompt,
// see the 'WeatherPlugin.getForecast' function, and invoke it with the correct arguments.
var result = await kernel.InvokePromptAsync(prompt);
Console.WriteLine("\n✨ AI Agent Response:");
Console.WriteLine(result.ToString());
// --- D. Direct Function Invocation (Alternative Approach) ---
// Sometimes you want to call the plugin directly without AI reasoning.
Console.WriteLine("\n--- Direct Function Call Example ---");
var function = weatherPlugin["getForecast"];
var arguments = new KernelArguments
{
["city"] = "Tokyo",
["days"] = "2"
};
var directResult = await kernel.InvokeAsync(function, arguments);
Console.WriteLine($"Direct API call result for Tokyo:");
Console.WriteLine(directResult.ToString());
}
}
Data Model Definition (WeatherForecast record):
* We define a C# record to represent the JSON structure returned by the API.
* Why? While Semantic Kernel can handle dynamic objects, strongly typing the response helps the AI model understand the data schema better when it's described in the OpenAPI spec or when you want to deserialize the result for further processing in C#.
* The [JsonPropertyName] attribute ensures that the JSON property names (e.g., temperature_celsius) map correctly to the C# property names (e.g., TemperatureCelsius).
-
OpenAPI Specification (
OpenApiSpecstring):- This is a JSON string representing the standard OpenAPI 3.0 definition for our weather endpoint.
- Critical Components:
paths: Defines the available endpoints. Here, we have/forecast.operationId: A unique identifier for the operation. Semantic Kernel uses this to name the plugin function (e.g.,getForecast).parameters: Defines the inputs (city,days). We specifyrequired: trueforcityand a default value fordays.responses: Defines the expected output structure. Semantic Kernel uses this to understand the return type.
-
Mock HTTP Client (
MockWeatherHttpClient):- To make this example self-contained and runnable anywhere, we cannot rely on a real external API.
- We inherit from
HttpClientand overrideSendAsync. - Logic: It intercepts calls to
/forecast, parses the query parameters (city,days), and generates synthetic weather data. - Why is this important? It demonstrates that Semantic Kernel's
RestApiOperationRunnerworks with anyHttpClientimplementation, allowing you to plug in custom handlers for logging, testing, or complex authentication flows.
-
Kernel Setup (
Mainmethod):- We use the modern
Kernel.CreateBuilder()pattern. - Dependency Injection: We register our
MockWeatherHttpClientas a singleton. Semantic Kernel's OpenAPI plugin runner automatically resolvesHttpClientfrom the service provider when executing HTTP requests. - This decouples the plugin logic from the specific HTTP implementation.
- We use the modern
-
Plugin Creation (
ImportPluginFromOpenApiAsync):- This is the core conversion step. We pass the OpenAPI spec stream and a name ("WeatherPlugin").
OpenApiFunctionExecutionParameters: This configuration object is crucial.HttpClient: We explicitly tell the plugin to use our mock client.EnableDynamicPayload: Allows the AI to pass arguments dynamically without strictly matching a predefined payload structure (useful for query parameters).
- Result: The method returns a collection of
KernelPluginobjects. The plugin contains a function namedgetForecastthat wraps the HTTP call.
-
AI-Driven Invocation (
InvokePromptAsync):- We pass a natural language prompt to the kernel.
- How it works: The AI model (e.g., GPT-4) receives the prompt along with the available function definitions (the
getForecasttool). It analyzes the request ("London", "3 days"), matches it to the tool, and generates a JSON request internally. The Semantic Kernel orchestrates the actual HTTP call and returns the formatted result to the user. - This demonstrates the "agentic" capability: the AI decides when and how to use the tool.
-
Direct Function Invocation (
InvokeAsync):- This shows the imperative approach. You retrieve the specific function from the plugin.
- You create a
KernelArgumentsdictionary mapping parameter names to values. - This bypasses the AI's reasoning step and executes the API call immediately. This is useful for deterministic workflows where the logic is handled by your C# code, not the LLM.
Incorrect Parameter Mapping:
* Mistake: Defining a parameter in the OpenAPI spec as in: "query" but trying to pass it as a header or body payload in the KernelArguments.
* Consequence: The API call fails with a 400 Bad Request or missing parameter error.
* Fix: Ensure the KernelArguments keys match the name of the parameters defined in the OpenAPI spec. Check the in field (query, path, header) in your spec.
-
Ignoring Authentication Schemes:
- Mistake: Converting an API that requires Bearer Token authentication but not configuring the
OpenApiFunctionExecutionParameterswith an auth handler. - Consequence: The API returns 401 Unauthorized.
- Fix: You must implement the
IAuthorizationHandlerinterface and assign it toExecutionParameters.AuthorizationHandler. For simple cases, you can also inject anHttpClientthat has a default header set (e.g.,DefaultRequestHeaders.Authorization), though using the handler is the recommended, more flexible pattern.
- Mistake: Converting an API that requires Bearer Token authentication but not configuring the
-
Over-reliance on AI for Parameter Extraction:
- Mistake: Assuming the AI will perfectly extract every parameter from complex, unstructured user input without guidance.
- Consequence: The AI might hallucinate parameters or miss required ones.
- Fix: Use semantic prompts (system instructions) to guide the AI on how to extract parameters. Alternatively, use the "Direct Invocation" pattern (Step D) where your code handles the extraction and validation before calling the plugin.
Visualizing the Flow
The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon
Loading knowledge check...
Code License: All code examples are released under the MIT License. Github repo.
Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.
All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.