#!/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-<input>
        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=<name override> 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.
#