#!/usr/bin/env python
# coding: utf-8
# # Running Native Functions
#
# Two of the previous notebooks showed how to [execute semantic functions inline](./03-semantic-function-inline.ipynb) and how to [run prompts from a file](./02-running-prompts-from-file.ipynb).
#
# In this notebook, we'll show how to use native functions from a file. We will also show how to call semantic functions from native functions.
#
# This can be useful in a few scenarios:
#
# - Writing logic around how to run a prompt that changes the prompt's outcome.
# - Using external data sources to gather data to concatenate into your prompt.
# - Validating user input data prior to sending it to the LLM prompt.
#
# Native functions are defined using standard Python code. The structure is simple, but not well documented at this point.
#
# The following examples are intended to help guide new users towards successful native & semantic function use with the SK Python framework.
#
# Prepare a semantic kernel instance first, loading also the AI service settings defined in the [Setup notebook](00-getting-started.ipynb):
#
# Import Semantic Kernel SDK from pypi.org
# In[ ]:
# Note: if using a virtual environment, do not run this cell
get_ipython().run_line_magic('pip', 'install -U semantic-kernel')
from semantic_kernel import __version__
__version__
# Initial configuration for the notebook to run properly.
# In[ ]:
# Make sure paths are correct for the imports
import os
import sys
notebook_dir = os.path.abspath("")
parent_dir = os.path.dirname(notebook_dir)
grandparent_dir = os.path.dirname(parent_dir)
sys.path.append(grandparent_dir)
# ### Configuring the Kernel
#
# Let's get started with the necessary configuration to run Semantic Kernel. For Notebooks, we require a `.env` file with the proper settings for the model you use. Create a new file named `.env` and place it in this directory. Copy the contents of the `.env.example` file from this directory and paste it into the `.env` file that you just created.
#
# **NOTE: Please make sure to include `GLOBAL_LLM_SERVICE` set to either OpenAI, AzureOpenAI, or HuggingFace in your .env file. If this setting is not included, the Service will default to AzureOpenAI.**
#
# #### Option 1: using OpenAI
#
# Add your [OpenAI Key](https://openai.com/product/) key to your `.env` file (org Id only if you have multiple orgs):
#
# ```
# GLOBAL_LLM_SERVICE="OpenAI"
# OPENAI_API_KEY="sk-..."
# OPENAI_ORG_ID=""
# OPENAI_CHAT_MODEL_ID=""
# OPENAI_TEXT_MODEL_ID=""
# OPENAI_EMBEDDING_MODEL_ID=""
# ```
# The names should match the names used in the `.env` file, as shown above.
#
# #### Option 2: using Azure OpenAI
#
# Add your [Azure Open AI Service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=programming-language-studio) settings to the `.env` file in the same folder:
#
# ```
# GLOBAL_LLM_SERVICE="AzureOpenAI"
# AZURE_OPENAI_API_KEY="..."
# AZURE_OPENAI_ENDPOINT="https://..."
# AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="..."
# AZURE_OPENAI_TEXT_DEPLOYMENT_NAME="..."
# AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME="..."
# AZURE_OPENAI_API_VERSION="..."
# ```
# The names should match the names used in the `.env` file, as shown above.
#
# For more advanced configuration, please follow the steps outlined in the [setup guide](./CONFIGURING_THE_KERNEL.md).
# We will load our settings and get the LLM service to use for the notebook.
# In[ ]:
from services import Service
from samples.service_settings import ServiceSettings
service_settings = ServiceSettings.create()
# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)
selectedService = (
Service.AzureOpenAI
if service_settings.global_llm_service is None
else Service(service_settings.global_llm_service.lower())
)
print(f"Using service type: {selectedService}")
# We now configure our Chat Completion service on the kernel.
# In[ ]:
from semantic_kernel import Kernel
kernel = Kernel()
service_id = None
if selectedService == Service.OpenAI:
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
service_id = "default"
kernel.add_service(
OpenAIChatCompletion(
service_id=service_id,
),
)
elif selectedService == Service.AzureOpenAI:
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
service_id = "default"
kernel.add_service(
AzureChatCompletion(
service_id=service_id,
),
)
# Let's create a **native** function that gives us a random number between 3 and a user input as the upper limit. We'll use this number to create 3-x paragraphs of text when passed to a semantic function.
#
# First, let's create our native function.
#
# In[ ]:
import random
from semantic_kernel.functions import kernel_function
class GenerateNumberPlugin:
"""
Description: Generate a number between 3-x.
"""
@kernel_function(
description="Generate a random number between 3-x",
name="GenerateNumberThreeOrHigher",
)
def generate_number_three_or_higher(self, input: str) -> str:
"""
Generate a number between 3-
Example:
"8" => rand(3,8)
Args:
input -- The upper limit for the random number generation
Returns:
int value
"""
try:
return str(random.randint(3, int(input)))
except ValueError as e:
print(f"Invalid input {input}")
raise e
# Next, let's create a semantic function that accepts a number as `{{$input}}` and generates that number of paragraphs about two Corgis on an adventure. `$input` is a default variable semantic functions can use.
#
# In[ ]:
from semantic_kernel.connectors.ai.open_ai import AzureChatPromptExecutionSettings, OpenAIChatPromptExecutionSettings
from semantic_kernel.prompt_template import InputVariable, PromptTemplateConfig
prompt = """
Write a short story about two Corgis on an adventure.
The story must be:
- G rated
- Have a positive message
- No sexism, racism or other bias/bigotry
- Be exactly {{$input}} paragraphs long. It must be this length.
"""
if selectedService == Service.OpenAI:
execution_settings = OpenAIChatPromptExecutionSettings(
service_id=service_id,
ai_model_id="gpt-3.5-turbo",
max_tokens=2000,
temperature=0.7,
)
elif selectedService == Service.AzureOpenAI:
execution_settings = AzureChatPromptExecutionSettings(
service_id=service_id,
ai_model_id="gpt-35-turbo",
max_tokens=2000,
temperature=0.7,
)
prompt_template_config = PromptTemplateConfig(
template=prompt,
name="story",
template_format="semantic-kernel",
input_variables=[
InputVariable(name="input", description="The user input", is_required=True),
],
execution_settings=execution_settings,
)
corgi_story = kernel.add_function(
function_name="CorgiStory",
plugin_name="CorgiPlugin",
prompt_template_config=prompt_template_config,
)
generate_number_plugin = kernel.add_plugin(GenerateNumberPlugin(), "GenerateNumberPlugin")
# In[ ]:
# Run the number generator
generate_number_three_or_higher = generate_number_plugin["GenerateNumberThreeOrHigher"]
number_result = await generate_number_three_or_higher(kernel, input=6)
print(number_result)
# In[ ]:
story = await corgi_story.invoke(kernel, input=number_result.value)
# _Note: depending on which model you're using, it may not respond with the proper number of paragraphs._
#
# In[ ]:
print(f"Generating a corgi story exactly {number_result.value} paragraphs long.")
print("=====================================================")
print(story)
# ## Kernel Functions with Annotated Parameters
#
# That works! But let's expand on our example to make it more generic.
#
# For the native function, we'll introduce the lower limit variable. This means that a user will input two numbers and the number generator function will pick a number between the first and second input.
#
# We'll make use of the Python's `Annotated` class to hold these variables.
#
# In[ ]:
kernel.remove_all_services()
service_id = None
if selectedService == Service.OpenAI:
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
service_id = "default"
kernel.add_service(
OpenAIChatCompletion(
service_id=service_id,
),
)
elif selectedService == Service.AzureOpenAI:
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
service_id = "default"
kernel.add_service(
AzureChatCompletion(
service_id=service_id,
),
)
# Let's start with the native function. Notice that we're add the `@kernel_function` decorator that holds the name of the function as well as an optional description. The input parameters are configured as part of the function's signature, and we use the `Annotated` type to specify the required input arguments.
#
# In[ ]:
import sys
from typing import Annotated
from semantic_kernel.functions import kernel_function
class GenerateNumberPlugin:
"""
Description: Generate a number between a min and a max.
"""
@kernel_function(
name="GenerateNumber",
description="Generate a random number between min and max",
)
def generate_number(
self,
min: Annotated[int, "the minimum number of paragraphs"],
max: Annotated[int, "the maximum number of paragraphs"] = 10,
) -> Annotated[int, "the output is a number"]:
"""
Generate a number between min-max
Example:
min="4" max="10" => rand(4,8)
Args:
min -- The lower limit for the random number generation
max -- The upper limit for the random number generation
Returns:
int value
"""
try:
return str(random.randint(min, max))
except ValueError as e:
print(f"Invalid input {min} and {max}")
raise e
# In[ ]:
generate_number_plugin = kernel.add_plugin(GenerateNumberPlugin(), "GenerateNumberPlugin")
generate_number = generate_number_plugin["GenerateNumber"]
# Now let's also allow the semantic function to take in additional arguments. In this case, we're going to allow the our CorgiStory function to be written in a specified language. We'll need to provide a `paragraph_count` and a `language`.
#
# In[ ]:
prompt = """
Write a short story about two Corgis on an adventure.
The story must be:
- G rated
- Have a positive message
- No sexism, racism or other bias/bigotry
- Be exactly {{$paragraph_count}} paragraphs long
- Be written in this language: {{$language}}
"""
if selectedService == Service.OpenAI:
execution_settings = OpenAIChatPromptExecutionSettings(
service_id=service_id,
ai_model_id="gpt-3.5-turbo",
max_tokens=2000,
temperature=0.7,
)
elif selectedService == Service.AzureOpenAI:
execution_settings = AzureChatPromptExecutionSettings(
service_id=service_id,
ai_model_id="gpt-35-turbo",
max_tokens=2000,
temperature=0.7,
)
prompt_template_config = PromptTemplateConfig(
template=prompt,
name="summarize",
template_format="semantic-kernel",
input_variables=[
InputVariable(name="paragraph_count", description="The number of paragraphs", is_required=True),
InputVariable(name="language", description="The language of the story", is_required=True),
],
execution_settings=execution_settings,
)
corgi_story = kernel.add_function(
function_name="CorgiStory",
plugin_name="CorgiPlugin",
prompt_template_config=prompt_template_config,
)
# Let's generate a paragraph count.
#
# In[ ]:
result = await generate_number.invoke(kernel, min=1, max=5)
num_paragraphs = result.value
print(f"Generating a corgi story {num_paragraphs} paragraphs long.")
# We can now invoke our corgi_story function using the `kernel` and the keyword arguments `paragraph_count` and `language`.
#
# In[ ]:
# Pass the output to the semantic story function
desired_language = "Spanish"
story = await corgi_story.invoke(kernel, paragraph_count=num_paragraphs, language=desired_language)
# In[ ]:
print(f"Generating a corgi story {num_paragraphs} paragraphs long in {desired_language}.")
print("=====================================================")
print(story)
# ## Calling Native Functions within a Semantic Function
#
# One neat thing about the Semantic Kernel is that you can also call native functions from within Prompt Functions!
#
# We will make our CorgiStory semantic function call a native function `GenerateNames` which will return names for our Corgi characters.
#
# We do this using the syntax `{{plugin_name.function_name}}`. You can read more about our prompte templating syntax [here](../../../docs/PROMPT_TEMPLATE_LANGUAGE.md).
#
# In[ ]:
from semantic_kernel.functions import kernel_function
class GenerateNamesPlugin:
"""
Description: Generate character names.
"""
# The default function name will be the name of the function itself, however you can override this
# by setting the name= in the @kernel_function decorator. In this case, we're using
# the same name as the function name for simplicity.
@kernel_function(description="Generate character names", name="generate_names")
def generate_names(self) -> str:
"""
Generate two names.
Returns:
str
"""
names = {"Hoagie", "Hamilton", "Bacon", "Pizza", "Boots", "Shorts", "Tuna"}
first_name = random.choice(list(names))
names.remove(first_name)
second_name = random.choice(list(names))
return f"{first_name}, {second_name}"
# In[ ]:
generate_names_plugin = kernel.add_plugin(GenerateNamesPlugin(), plugin_name="GenerateNames")
generate_names = generate_names_plugin["generate_names"]
# In[ ]:
prompt = """
Write a short story about two Corgis on an adventure.
The story must be:
- G rated
- Have a positive message
- No sexism, racism or other bias/bigotry
- Be exactly {{$paragraph_count}} paragraphs long
- Be written in this language: {{$language}}
- The two names of the corgis are {{GenerateNames.generate_names}}
"""
# In[ ]:
if selectedService == Service.OpenAI:
execution_settings = OpenAIChatPromptExecutionSettings(
service_id=service_id,
ai_model_id="gpt-3.5-turbo",
max_tokens=2000,
temperature=0.7,
)
elif selectedService == Service.AzureOpenAI:
execution_settings = AzureChatPromptExecutionSettings(
service_id=service_id,
ai_model_id="gpt-35-turbo",
max_tokens=2000,
temperature=0.7,
)
prompt_template_config = PromptTemplateConfig(
template=prompt,
name="corgi-new",
template_format="semantic-kernel",
input_variables=[
InputVariable(name="paragraph_count", description="The number of paragraphs", is_required=True),
InputVariable(name="language", description="The language of the story", is_required=True),
],
execution_settings=execution_settings,
)
corgi_story = kernel.add_function(
function_name="CorgiStoryUpdated",
plugin_name="CorgiPluginUpdated",
prompt_template_config=prompt_template_config,
)
# In[ ]:
result = await generate_number.invoke(kernel, min=1, max=5)
num_paragraphs = result.value
# In[ ]:
desired_language = "French"
story = await corgi_story.invoke(kernel, paragraph_count=num_paragraphs, language=desired_language)
# In[ ]:
print(f"Generating a corgi story {num_paragraphs} paragraphs long in {desired_language}.")
print("=====================================================")
print(story)
# ### Recap
#
# A quick review of what we've learned here:
#
# - We've learned how to create native and prompt functions and register them to the kernel
# - We've seen how we can use Kernel Arguments to pass in more custom variables into our prompt
# - We've seen how we can call native functions within a prompt.
#