This react-style agent semantic kernel tutorial shows how to assemble a reasoning-and-acting (ReAct) loop using Semantic Kernel’s FunctionCallingStepwisePlanner. If you need an agent that interleaves tool calls with natural language reasoning, this planner is the most direct supported primitive in the SK Python SDK. We’ll build a runnable example from scratch, then cover how to verify it works in a pipeline.
Step 1: Set up the project and understand the SK execution model
Semantic Kernel separates functions (native Python or semantic prompts) from services (chat completions, embeddings). The planner orchestrates functions by asking the chat service to emit tool calls. Before writing code, create an isolated environment and pin a recent version:
python -m venv .venv
source .venv/bin/activate
pip install "semantic-kernel>=1.10.0"
The package pulls in the OpenAI connector by default. SK’s Kernel object is the dependency injection container; you register services and plugins on it. In this react-style agent semantic kernel tutorial we register one chat service and three plugins. The key mental model: the planner does not execute logic itself—it generates a sequence of function invocations based on model output, then runs them.
Step 2: Point the kernel at a function-calling model
The ReAct loop depends on reliable function calling. GPT-4o, GPT-4-turbo, and several open-weight models exposed via OpenAI-compatible servers work. Configure the kernel with an endpoint and key from environment variables:
import os
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
kernel = Kernel()
endpoint = os.getenv("AI_ENDPOINT", "https://api.openai.com/v1")
api_key = os.getenv("OPENAI_API_KEY", "")
kernel.add_chat_service(
"chat",
OpenAIChatCompletion(
ai_model_id="gpt-4o",
api_key=api_key,
ai_endpoint=endpoint,
),
)
If you point ai_endpoint at an OpenAI-compatible gateway such as n4n.ai, a single base URL fronts 240+ models and the gateway handles automatic fallback when a provider is rate-limited or degraded. That removes the need to write your own retry and model-selection logic inside the agent. For local testing, set AI_ENDPOINT to a local vLLM or Ollama proxy that speaks the OpenAI tool-call format.
Step 3: Build native plugins with strict schemas
Tools are just classes with @kernel_function methods. The decorator’s description is sent to the model, so treat it like an API contract. Below we define a weather stub and a calculator wrapper that returns structured strings. Note the async variant—SK supports both sync and async native functions.
from semantic_kernel.functions import kernel_function
from semantic_kernel.core_plugins import MathPlugin
class WeatherPlugin:
@kernel_function(name="get_temperature", description="Return current temp in Celsius for a given city")
def get_temperature(self, city: str) -> str:
# Replace with real HTTP call
return f"22C in {city}"
class CalcPlugin:
@kernel_function(name="multiply", description="Multiply two floats and return the product as string")
async def multiply(self, a: float, b: float) -> str:
# async allows non-blocking I/O in real tools
return str(a * b)
kernel.add_plugin(WeatherPlugin(), plugin_name="weather")
kernel.add_plugin(CalcPlugin(), plugin_name="calc")
kernel.add_plugin(MathPlugin(), plugin_name="math")
Strict type hints (str, float) let SK generate the JSON schema the model sees. Avoid ambiguous parameters; the planner will misuse them. Wrap external calls in try/except and return an error string—the model can often recover if the tool says “error: timeout” rather than raising.
Step 4: Add a semantic function for soft reasoning
Native functions are deterministic. Sometimes you want a prompt-based step, e.g., to reformat observations. Define a semantic function with a prompt template:
from semantic_kernel.prompt_template import PromptTemplate
from semantic_kernel.prompt_template.input_variable import InputVariable
prompt = """
Given the raw tool output: {{$input}}
Summarize it in one short sentence for the final answer.
"""
summary_func = kernel.create_function_from_prompt(
prompt=prompt,
function_name="summarize",
description="Condense tool output into a single sentence",
input_variables=[InputVariable(name="input", description="raw text", is_required=True)],
)
kernel.add_function("utils", summary_func)
This is optional but shows the mix of native and semantic tools in one ReAct graph. Semantic functions count as actions the planner can call, so keep their descriptions as precise as native ones.
Step 5: Configure the FunctionCallingStepwisePlanner
The planner drives the ReAct cycle: reason → act → observe → repeat. Instantiate it with explicit limits:
from semantic_kernel.planning.function_calling_stepwise_planner import (
FunctionCallingStepwisePlanner,
FunctionCallingStepwisePlannerOptions,
)
options = FunctionCallingStepwisePlannerOptions(
max_iterations=8,
max_tokens=4000,
allow_missing_functions=False,
)
planner = FunctionCallingStepwisePlanner(kernel, options)
max_iterations bounds latency and cost. allow_missing_functions should stay False in prod; if the model requests an unregistered tool, the planner raises instead of hallucinating. Under the hood, the planner injects a system prompt that demands the model either call a function or emit a final answer marker. You do not need to write that prompt yourself.
Step 6: Execute the ReAct loop and inspect intermediate steps
Run the planner against a task that needs two tools:
async def run_agent():
task = "Get Tokyo temperature, convert 22C to Fahrenheit using calc.multiply, and summarize."
result = await planner.invoke(task)
print("ANSWER:", result.final_answer)
for i, step in enumerate(result.stepwise_plan, 1):
print(f"Step {i}: {step.description} -> {step.action}")
return result
result = asyncio.run(run_agent())
The stepwise_plan attribute lists each reasoning step and the action taken. In a correct run you’ll see weather.get_temperature, then calc.multiply (or math equivalent), then the summarize function. The loop stops when the model emits a final answer without a tool call. If you need streaming logs, wrap the invoke in a loop that polls planner state—SK does not yet expose step callbacks on this class, so logging inside native functions is the pragmatic approach.
Step 7: Verify and test the agent
Verification is not optional. Wrap the run in assertions or a pytest case:
def test_agent_run():
res = asyncio.run(run_agent())
plan_str = str(res.stepwise_plan)
assert "get_temperature" in plan_str
assert "multiply" in plan_str
assert "71.6" in res.final_answer or "72" in res.final_answer
if __name__ == "__main__":
test_agent_run()
print("PASS")
If the assertions fail, inspect result.stepwise_plan to see where the model diverged. Common fixes: sharpen tool descriptions, reduce max_iterations to force convergence, or split the task. This closing check completes the react-style agent semantic kernel tutorial with a reproducible signal of success. Run python agent.py and confirm the printed steps and PASS.
Production considerations
The stepwise planner issues one completion request per iteration, so token cost scales linearly with steps. Log stepwise_plan for every run; gateways like n4n.ai provide per-token usage metering so you can attribute cost per agent invocation. For long-running agents, persist chat history outside the planner and rebuild the kernel context per call.
Function calling is model-dependent. If you swap models, re-validate the tool schemas. The ReAct pattern is robust, but the planner will not recover from a malformed tool response—wrap native functions in try/except and return error strings the model can reason about. Also, the planner is stateless across invoke calls; for multi-turn conversations, maintain your own message list and feed it via the kernel’s arguments.
You now have a working ReAct agent in Semantic Kernel with verifiable behavior and a clear path to hardening it for production traffic.