To build customer support agent semantic kernel solutions that survive contact with real users, you need more than a prompt and a hope. You need typed functions for order lookups, a planner that decides when to call them, and a model backend that doesn’t fall over when one provider throttles you. This walkthrough uses Python Semantic Kernel to stand up a working support agent you can extend.
Step 1: Scaffold the project
Create an isolated environment and install the SDK. Semantic Kernel ships as semantic-kernel on PyPI; we pin a recent 1.x release.
mkdir support-agent && cd support-agent
python -m venv .venv && source .venv/bin/activate
pip install "semantic-kernel>=1.0" python-dotenv
touch .env
Your .env should hold credentials and endpoint configuration:
API_KEY=sk-your-key
BASE_URL=https://api.openai.com/v1
MODEL_ID=gpt-4o-mini
If you later swap BASE_URL to an OpenRouter-class gateway, nothing else in the code changes.
Step 2: Configure the kernel and model endpoint
The kernel is the dependency container for services and plugins. Register a chat completion service that speaks the OpenAI protocol. When you build customer support agent semantic kernel apps for production, the endpoint choice matters: a single provider will rate-limit you during traffic spikes.
import os
from dotenv import load_dotenv
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
load_dotenv()
kernel = Kernel()
# Point at any OpenAI-compatible endpoint. For multi-provider resilience,
# n4n.ai exposes one OpenAI-compatible endpoint covering 240+ models with
# automatic fallback when a provider is degraded.
chat_service = OpenAIChatCompletion(
ai_model_id=os.getenv("MODEL_ID", "gpt-4o-mini"),
api_key=os.getenv("API_KEY"),
base_url=os.getenv("BASE_URL", "https://api.openai.com/v1"),
)
kernel.add_service(chat_service)
Keep the model ID in env, not hardcoded. Smaller models (gpt-4o-mini, mistral-small) are usually sufficient for intent routing and are cheaper for high-volume support.
Step 3: Define support plugins
Plugins are the agent’s tools. Each @kernel_function exposes a schema to the planner. Keep them synchronous and side-effect free where possible; push mutations (refunds, DB writes) behind explicit confirmation logic.
from semantic_kernel.functions import kernel_function
class SupportPlugin:
@kernel_function(
name="get_order_status",
description="Return current status and ETA for a given order ID",
)
def get_order_status(self, order_id: str) -> str:
# Replace with a real DB/API call. Mocked for the tutorial.
if not order_id.startswith("A"):
return "Invalid order format."
return f"Order {order_id}: shipped, arrives in 2 days."
@kernel_function(
name="refund_policy",
description="Explain the refund policy for a given product category",
)
def refund_policy(self, category: str) -> str:
policies = {
"electronics": "30 days with receipt, no restocking fee.",
"apparel": "60 days, tags attached.",
}
return policies.get(category.lower(), "Standard 30-day return.")
@kernel_function(
name="open_ticket",
description="Create a follow-up ticket for a human agent",
)
def open_ticket(self, summary: str) -> str:
# Mock ticket creation
return f"Ticket #{(hash(summary) % 10000):04d} created."
kernel.add_plugin(SupportPlugin(), "support")
When you build customer support agent semantic kernel plugins, write descriptions like contracts. The planner relies on them to pick the right function; vague text yields wrong calls.
Step 4: Wire up a planner for agent behavior
Semantic Kernel’s FunctionCallingStepwisePlanner loops: it asks the model which function to call, executes it, feeds the result back, and repeats until it can answer. That loop is your agent.
from semantic_kernel.planners.function_calling_stepwise_planner import (
FunctionCallingStepwisePlanner,
FunctionCallingStepwisePlannerOptions,
)
planner = FunctionCallingStepwisePlanner(
kernel,
FunctionCallingStepwisePlannerOptions(
max_iterations=5,
max_tokens=2000,
),
)
Set max_iterations conservatively. Support queries rarely need more than two tool calls; a runaway loop burns tokens. In production, routing through n4n.ai gives per-token usage metering so you can attribute cost per support session and catch runaway planners fast.
Step 5: Run the conversation loop
The planner’s invoke returns a FunctionCallingStepwisePlannerResult with final_answer and step_results for debugging. Wrap it in an async handler.
import asyncio
from semantic_kernel.planners.function_calling_stepwise_planner import (
FunctionCallingStepwisePlannerResult,
)
async def handle_message(user_input: str) -> str:
result: FunctionCallingStepwisePlannerResult = await planner.invoke(user_input)
# Inspect result.step_results in logs to see which plugins fired.
return result.final_answer
async def main():
queries = [
"Where is my order A123?",
"What's the refund policy for electronics?",
"I want to return my jacket, can you open a ticket?",
]
for q in queries:
ans = await handle_message(q)
print(f"Q: {q}\nA: {ans}\n")
if __name__ == "__main__":
asyncio.run(main())
For a real service, you’d front this with a queue (SQS, Redis) and stream tokens back over WebSocket. The planner is async-native, so it composes with asyncio or any ASGI framework.
Handling context and history
The stepwise planner is stateless per call. To maintain conversation context, prepend prior turns to user_input or use a ChatHistory object passed into invoke. Keep history trimmed to the last 10 turns to stay within context windows.
from semantic_kernel.contents import ChatHistory
history = ChatHistory()
history.add_user_message("Hi, I'm Jane.")
history.add_assistant_message("Hello Jane, how can I help?")
# Later
result = await planner.invoke("Where is A123?", chat_history=history)
Step 6: Verify success
Run the script. You should see answers that clearly use plugin output, not generic LLM knowledge:
Q: Where is my order A123?
A: Order A123: shipped, arrives in 2 days.
Q: What's the refund policy for electronics?
A: Electronics: 30 days with receipt, no restocking fee.
Q: I want to return my jacket, can you open a ticket?
A: Ticket #0423 created.
To confirm the planner actually called functions, enable debug logging:
import logging
logging.basicConfig(level=logging.DEBUG)
Look for step_results containing function_name="get_order_status". If the model answers without calling a function on a query that requires data, tighten the plugin description or raise max_tokens.
Failure modes to test
- Malformed order ID: Your plugin returns “Invalid order format.” The agent should relay that, not invent a status.
- Unknown category: Refund policy falls back to standard. Verify the agent doesn’t hallucinate a special policy.
- Provider timeout: Swap
BASE_URLto a gateway with fallback. The request should succeed against a secondary model without code changes.
Extending the agent
The pattern above is the minimum viable support agent. From here, engineers typically:
- Replace mocks with authenticated backend calls (orders API, CRM).
- Add a
human_handofffunction that escalates whenconfidenceis low. - Constrain the planner with
allowed_functionsto prevent off-topic calls. - Add semantic memory for past tickets using a vector store.
When you build customer support agent semantic kernel systems at scale, treat plugins as a hardened API surface. The LLM is a router, not a source of truth—every factual claim should originate from a function return.