F# Semantic Kernel

Microsoft’s Semantic Kernel SDK is a set of libraries which allows you to easily build applications which integrate with Large Language Model (LLM) APIs such as OpenAI. It allows you to automatically orchestrate calls between the connected APIs and your code. This is extremely powerful as you can effectively build ‘skills’ for AI assistants to help understand and solve problems in your specific domain.

When building with the Semantic Kernel SDK, a ‘skill’ is effectively a class with a set of methods with annotations on the method itself as well as the input and output values. You can then add this to the kernel and it will be available as a tool when making requests in OpenAI chat completion. The real magic happens when the kernel receives the response as it will then automatically call the method based on the tool call in the OpenAI response.

To get a better understanding of how this works I created a short F# script using the Semantic Kernel SDK. In this script I created a Widget Plugin which can manage widget resources. In this example I am just using an in memory database (a dictionary) but you can imagine that this could be your own application database or API. I am using F#’s built in MailboxProcessor to help manage the console input and output as it helps me reason about the state of the program. The Semantic Kernel documentation has other examples of how to create agents.
#r "nuget: Microsoft.Extensions.Logging"
#r "nuget: Microsoft.Extensions.Logging.Console"
#r "nuget: Microsoft.SemanticKernel"
#r "nuget: FSharp.Control.AsyncSeq"

open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open Microsoft.SemanticKernel
open Microsoft.SemanticKernel.ChatCompletion
open Microsoft.SemanticKernel.Connectors.OpenAI
open FSharp.Control
open System
open System.Text
open System.Threading
open System.Threading.Tasks
open System.ComponentModel
open System.Collections.Generic

type AgentStatus =
    | ReadyForUser
    | ReadyForAssistant

type AgentState =
    { history:ChatMessageContent list
      asisstantBuffer:StringBuilder
      status:AgentStatus }

type AgentAction =
    | GetUserInput
    | GetAssistantResponse of history:ChatMessageContent list

type AgentMessage =
    | UserMessage of content:string
    | StreamingAssistantMessage of content:string
    | StreamingAssistantMessageFinished
    | GetNextAction of channel:AsyncReplyChannel<AgentAction>

type AgentMailbox = MailboxProcessor<AgentMessage>

type Evolve = AgentState -> AgentMessage -> AgentState
type NextAction = AgentState -> AgentAction

let getNextAction : NextAction =
    fun state ->
        match state.status with
        | ReadyForUser -> GetUserInput
        | ReadyForAssistant -> GetAssistantResponse state.history

let evolve : Evolve =
    fun state message ->
        match message with
        | UserMessage content ->
            { state with
                history = ChatMessageContent(AuthorRole.User, content) :: state.history
                status = ReadyForAssistant }
        | StreamingAssistantMessage content ->
            { state with asisstantBuffer = state.asisstantBuffer.Append(content) }
        | StreamingAssistantMessageFinished ->
            let content = state.asisstantBuffer.ToString()
            { state with
                asisstantBuffer = StringBuilder()
                history =  ChatMessageContent(AuthorRole.Assistant, content) :: state.history
                status = ReadyForUser }
        | GetNextAction channel -> channel.Reply(getNextAction state); state

type Agent(kernel:Kernel) =
    let settings = OpenAIPromptExecutionSettings(ToolCallBehavior=ToolCallBehavior.AutoInvokeKernelFunctions)
    let chatService = kernel.GetRequiredService<IChatCompletionService>()

    let startMailbox initialState cancellationToken =
        AgentMailbox.Start((fun inbox ->
            AsyncSeq.initInfiniteAsync (fun _ -> inbox.Receive())
            |> AsyncSeq.fold evolve initialState
            |> Async.Ignore), cancellationToken)

    let rec runAsync (mailbox:AgentMailbox) = async {
        match! mailbox.PostAndAsyncReply(GetNextAction) with
        | GetUserInput ->
            do! Console.Out.WriteAsync("user > ") |> Async.AwaitTask
            let! content = Console.In.ReadLineAsync() |> Async.AwaitTask
            match content with
            | content when String.IsNullOrEmpty(content) ->
                return ()
            | content when content = Environment.NewLine ->
                do! runAsync mailbox
            | content ->
                mailbox.Post(UserMessage content)
        | GetAssistantResponse history ->
            let chunks = chatService.GetStreamingChatMessageContentsAsync(
                ChatHistory(List.rev history),
                executionSettings=settings,
                kernel=kernel)
            for chunk in AsyncSeq.ofAsyncEnum chunks do 
                if chunk.Role.HasValue then
                    do! Console.Out.WriteAsync($"{chunk.Role} > ") |> Async.AwaitTask
                do! Console.Out.WriteAsync(chunk.Content) |> Async.AwaitTask
                mailbox.Post(StreamingAssistantMessage chunk.Content)
            do! Console.Out.WriteLineAsync() |> Async.AwaitTask
            mailbox.Post(StreamingAssistantMessageFinished)
        return! runAsync mailbox
    }

    member _.RunAsync(systemMessage:string) = async {
        let! cancellationToken = Async.CancellationToken
        let initialState =
            { history = List.singleton (ChatMessageContent(AuthorRole.System, systemMessage))
              asisstantBuffer = StringBuilder()
              status = ReadyForAssistant }
        let mailbox = startMailbox initialState cancellationToken
        do! runAsync mailbox
    }

type Widget = { id:Guid; name:string; widgetType:string }

type WidgetPlugin(loggerFactory:ILoggerFactory) =
    let logger = loggerFactory.CreateLogger<WidgetPlugin>()
    let widgets = Dictionary<Guid,Widget>()

    [<KernelFunction>]
    [<Description("Create a widget.")>]
    member _.CreateWidget
            ([<Description("The name of the widget.")>] name:string,
             [<Description("The type of widget. One of 'Foo', 'Bar', or 'Baz'.")>] widgetType:string)
            :[<Description("Result of creating widget.")>] Task<string> = task {
        logger.LogInformation("Creating widget: {name}", name)
        let widget = { id = Guid.NewGuid(); name = name; widgetType = widgetType } 
        widgets.Add(widget.id, widget)
        return "Successfully created widget"
    }

    [<KernelFunction>]
    [<Description("Update a widget's name.")>]
    member _.UpdateWidgetName
            ([<Description("Id of the widget to update.")>] id:Guid,
             [<Description("Name of the widget.")>] name:string)
            :[<Description("Result of updating widget name.")>] Task<string> = task {
        logger.LogInformation("Updating widget name: {id} {name}", id, name)
        return
            match widgets.TryGetValue(id) with
            | false, _ ->
                "Widget not found"
            | true, widget ->
                widgets[id] <- { widget with name = name }
                "Successfully updated widget name"
    }

    [<KernelFunction>]
    [<Description("Delete a widget.")>]
    member _.DeleteWidget
            ([<Description("Id of the widget to delete.")>] id:Guid)
            :[<Description("Result of deleting widget.")>] Task<string> = task {
        logger.LogInformation("Deleting widget: {id}", id)
        let removed = widgets.Remove(id)
        return if removed then "Widget deleted" else "Widget not found"
    }

    [<KernelFunction>]
    [<Description("List all the widgets.")>]
    member _.ListWidgets() : [<Description("List of widgets.")>] Task<Widget seq> = task {
        return widgets.Values
    }

let main () =
    let cts = new CancellationTokenSource()
    let cancellationToken = cts.Token
    Console.CancelKeyPress.Add(fun _ -> cts.Cancel())
    let apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
    let loggerFactory = LoggerFactory.Create(fun b -> b.AddConsole().SetMinimumLevel(LogLevel.Information) |> ignore)
    let builder = Kernel.CreateBuilder().AddOpenAIChatCompletion("gpt-4-1106-preview", apiKey)
    builder.Services.AddSingleton(loggerFactory) |> ignore
    builder.Plugins.AddFromType<WidgetPlugin>() |> ignore
    let kernel = builder.Build()

    let systemMessage = """
    You are a friendly assistant. Your job is help client's manage their widgets.
    If you are not given enough information to complete the task then ask for more information.
    Start by asking the client how you can help them with their widgets.
    """
    let agent = Agent(kernel)
    let work = agent.RunAsync(systemMessage)
    try Async.RunSynchronously(work, cancellationToken=cancellationToken)
    with
    | :? OperationCanceledException -> Console.WriteLine("Agent stopped")
    | ex -> raise ex

main()
Here is the output after running the script (you can run with dotnet fsi script.fsx)
assistant > How can I assist you with your widgets today?
user > Could you create a widget named W1
assistant > Of course! To go ahead with creating the widget, I'll just need to know the type of widget you would like me to create. There are three types available: 'Foo', 'Bar', and 'Baz'. Which one would you like for your widget named W1?
user > Foo please
assistant > info: CreateWidget[0]
      Function CreateWidget invoking.
info: FSI_0002.WidgetPlugin[0]
      Creating widget: W1
info: CreateWidget[0]
      Function CreateWidget succeeded.
info: CreateWidget[0]
      Function completed. Duration: 0.0049415s
assistant > The widget named W1 of type 'Foo' has been successfully created. Is there anything else I can help you with regarding your widgets?
user > Could you create another widget named W2 of type Baz
assistant > info: CreateWidget[0]
      Function CreateWidget invoking.
info: FSI_0002.WidgetPlugin[0]
      Creating widget: W2
info: CreateWidget[0]
      Function CreateWidget succeeded.
info: CreateWidget[0]
      Function completed. Duration: 0.0001877s
assistant > The widget named W2 of type 'Baz' has been successfully created. Is there anything else I can assist you with?
user > Could you list my widgets
assistant > info: ListWidgets[0]
      Function ListWidgets invoking.
info: ListWidgets[0]
      Function ListWidgets succeeded.
info: ListWidgets[0]
      Function completed. Duration: 0.002207s
assistant > You currently have the following widgets:

1. Widget Name: W1
   - ID: fc39cda8-835b-4426-8c95-331b911560ed
   - Type: Foo

2. Widget Name: W2
   - ID: 00a79541-09d1-423b-ad9f-6280145fe28b
   - Type: Baz

If you need further assistance with these widgets, feel free to let me know!
user > Thanks. Could you delete widget W1
assistant > info: DeleteWidget[0]
      Function DeleteWidget invoking.
info: FSI_0002.WidgetPlugin[0]
      Deleting widget: fc39cda8-835b-4426-8c95-331b911560ed
info: DeleteWidget[0]
      Function DeleteWidget succeeded.
info: DeleteWidget[0]
      Function completed. Duration: 0.0065881s
assistant > Widget W1 has been successfully deleted. If there's anything else I can do for you, just let me know!
user > List widgets again please
assistant > info: ListWidgets[0]
      Function ListWidgets invoking.
info: ListWidgets[0]
      Function ListWidgets succeeded.
info: ListWidgets[0]
      Function completed. Duration: 0.001004s
assistant > After deleting Widget W1, here's the current list of your widgets:

1. Widget Name: W2
   - ID: 00a79541-09d1-423b-ad9f-6280145fe28b
   - Type: Baz

Please let me know if there's anything more I can help you with your widgets.
user >
Hopefully this gives you a sense of how the Semantic Kernel works. The plugins are extremely powerful as you can take basically any API and connect it with natural language queries.


Andrew Meier

Copyright © 2026 - All right reserved