This tutorial walks through semantic kernel plugins function calling n4n.ai to connect native Python functions to an LLM orchestration loop. You’ll define a weather plugin, point Semantic Kernel at an OpenAI-compatible gateway, and let the model invoke your code automatically.
Prerequisites
- Python 3.10 or newer
semantic-kernelPython package (1.3.0+)- An API key for the n4n.ai OpenAI-compatible endpoint (or any similar gateway)
- Comfort with async/await and type hints
pip install semantic-kernel==1.3.0
Step 1: Define a native plugin
Semantic Kernel exposes Python methods to the model through the @kernel_function decorator. The LLM sees the function name, description, and typed parameters—not your source code.
from semantic_kernel.functions import kernel_function
class WeatherPlugin:
@kernel_function(
name="get_current_weather",
description="Get the current weather for a city. Returns temperature in Celsius.",
)
def get_current_weather(self, city: str) -> str:
# Mock implementation; swap for a real HTTP call.
return f"It is 22°C and sunny in {city}."
The description is mandatory. The model uses it to decide whether to call the function. A missing or vague description silently drops the tool from consideration.
Step 2: Configure the kernel with the gateway
Semantic Kernel’s OpenAI connector accepts a base_url. Point it at the n4n.ai OpenAI-compatible endpoint to route 240+ models through one URL. The gateway forwards tool schemas unchanged and handles provider fallback when a backend is degraded.
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
async def main():
kernel = Kernel()
chat_service = OpenAIChatCompletion(
ai_model_id="gpt-4o-mini",
api_key="YOUR_N4N_API_KEY",
base_url="https://api.n4n.ai/v1",
)
kernel.add_service(chat_service)
kernel.add_plugin(WeatherPlugin(), plugin_name="weather")
settings = OpenAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto()
)
result = await kernel.invoke_prompt(
"What's the weather in Berlin?", settings=settings
)
print(result)
if __name__ == "__main__":
asyncio.run(main())
Run the script. Expected output:
It is 22°C and sunny in Berlin.
The model emitted a tool call, the kernel executed get_current_weather("Berlin"), and the returned string was folded back into the final answer.
Step 3: Inspect the invocation and token usage
The semantic kernel plugins function calling n4n.ai flow gives you full visibility into what the model did. After the call, the kernel history contains the assistant’s tool-call message and the tool’s response.
for msg in kernel.history:
if msg.role == "tool":
print("Tool payload:", msg.content)
usage = chat_service.get_last_usage()
if usage:
print(f"Prompt tokens: {usage.prompt_tokens}")
print(f"Completion tokens: {usage.completion_tokens}")
Sample output:
Tool payload: It is 22°C and sunny in Berlin.
Prompt tokens: 84
Completion tokens: 11
Per-token metering arrives straight from the gateway, so you can attribute cost without custom instrumentation.
Step 4: Add a second plugin to show composition
Real agents rarely have one tool. Add a time plugin and ask a compound question.
from datetime import datetime, timezone
class TimePlugin:
@kernel_function(
name="get_utc_time",
description="Return current UTC time as HH:MM string.",
)
def get_utc_time(self) -> str:
return datetime.now(timezone.utc).strftime("%H:%M")
# inside main(), after weather plugin:
kernel.add_plugin(TimePlugin(), plugin_name="clock")
result = await kernel.invoke_prompt(
"What time is it in UTC, and is it hot in Paris?",
settings=settings,
)
print(result)
The model may call both get_utc_time and get_current_weather in a single turn. The kernel runs them, collects results, and returns a merged answer.
Step 5: Return structured data instead of strings
String returns are fine for prototypes. For production, return typed objects so the model gets clean JSON.
from dataclasses import dataclass
@dataclass
class WeatherReport:
city: str
temp_c: float
conditions: str
class WeatherPlugin:
@kernel_function(name="get_weather", description="Get structured weather report.")
def get_weather(self, city: str) -> WeatherReport:
return WeatherReport(city=city, temp_c=22.0, conditions="sunny")
Semantic Kernel serializes the dataclass to JSON when sending the tool result back to the model. You can verify by printing the tool message content—it will be a JSON object, not a formatted sentence.
Step 6: Test plugins without the model
Before wiring the LLM, call the function directly to validate logic.
plugin = WeatherPlugin()
report = plugin.get_weather("London")
print(report.temp_c, report.conditions)
This isolates bugs in your native code from prompt or routing issues.
Routing directives and cache control
The gateway honors client routing directives and forwards provider cache-control hints. If you need to pin a provider, set a header on the underlying client:
chat_service.client.default_headers["x-n4n-provider"] = "openai"
Automatic fallback already covers rate limits, so only set this when you have a specific compliance or latency requirement.
Common pitfalls
- Trailing slash on base_url: The OpenAI SDK appends
/chat/completions. Usehttps://api.n4n.ai/v1with no slash. - Sync functions doing I/O: Mark them
asyncif they call external services, or they’ll block the event loop. - Undocumented parameters: Every argument the model can pass must be a typed parameter on the decorated method. Python
*argswon’t appear in the schema. - Overlapping descriptions: If two plugins sound identical, the model guesses. Make descriptions distinct and action-oriented.
Wrapping up
You now have a working native function loop using semantic kernel plugins function calling n4n.ai. The plugin code is identical regardless of which backend model the gateway selects—change ai_model_id and the same weather tool runs against a different provider with zero refactoring.