Files
semantic-kernel/python/semantic_kernel/planning/basic_planner.py
T
Evan MattsonandEvan Mattson 674af5468d Python: Rename skills to plugins. Update prompt template config to use execution settings to align with dotnet. (#4595)
### Motivation and Context

To better align with SK dotnet v1, Python skills have been renamed to
plugins. This removes the root samples skills directory as dotnet
supports the plugins and their configs. This the beginning of more work
to consolidate other files/names, per the backlog. I've run several
greps for files or file names containing "skill" (case insensitive), and
no results appear. Do alert if you find anything else that includes
"skill."

Fixes #3319 

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

This is a large, breaking change PR as the replacement of "skills" to
"plugins" affects many files. It was difficult to break this up into
smaller chunks without leaving the SK Python repo in a bad state for
some time. Other updates include:

- Prompt template has been updated to use `execution_settings`, which
aligns with dotnet, from the previous `completion` that Python used.
- AzureOpenAI/OpenAI function calling has been updated to match the
current versions of tools/tool_choice. Parsing messages in the open_ai
utils respects this as it now looks for tool_calls and it configures the
function_call with the id (new), name, and argument).
- File renames, where the filename used to contain "skill" will look
like new code, but it's the same code, just with "plugin" in the name.
- Kernel examples and notebooks have been tested and are working. Unit
tests and integration tests are passing.

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [X] The code builds clean without any errors or warnings
- [X] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [X] All unit tests pass, and I have added new tests where possible
- [X] I didn't break anyone 😄

---------

Co-authored-by: Evan Mattson <[email protected]>
2024-01-19 16:47:30 +00:00

232 lines
7.6 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""A basic JSON-based planner for the Python Semantic Kernel"""
import json
import regex
from semantic_kernel.kernel import Kernel
from semantic_kernel.orchestration.context_variables import ContextVariables
class Plan:
"""A simple plan object for the Semantic Kernel"""
def __init__(self, prompt: str, goal: str, plan: str):
self.prompt = prompt
self.goal = goal
self.generated_plan = plan
def __str__(self):
return f"Prompt: {self.prompt}\nGoal: {self.goal}\nPlan: {self.generated_plan}"
def __repr__(self):
return str(self)
PROMPT = """
You are a planner for the Semantic Kernel.
Your job is to create a properly formatted JSON plan step by step, to satisfy the goal given.
Create a list of subtasks based off the [GOAL] provided.
Each subtask must be from within the [AVAILABLE FUNCTIONS] list. Do not use any functions that are not in the list.
Base your decisions on which functions to use from the description and the name of the function.
Sometimes, a function may take arguments. Provide them if necessary.
The plan should be as short as possible.
For example:
[AVAILABLE FUNCTIONS]
EmailConnector.LookupContactEmail
description: looks up the a contact and retrieves their email address
args:
- name: the name to look up
WriterPlugin.EmailTo
description: email the input text to a recipient
args:
- input: the text to email
- recipient: the recipient's email address. Multiple addresses may be included if separated by ';'.
WriterPlugin.Translate
description: translate the input to another language
args:
- input: the text to translate
- language: the language to translate to
WriterPlugin.Summarize
description: summarize input text
args:
- input: the text to summarize
FunPlugin.Joke
description: Generate a funny joke
args:
- input: the input to generate a joke about
[GOAL]
"Tell a joke about cars. Translate it to Spanish"
[OUTPUT]
{
"input": "cars",
"subtasks": [
{"function": "FunPlugin.Joke"},
{"function": "WriterPlugin.Translate", "args": {"language": "Spanish"}}
]
}
[AVAILABLE FUNCTIONS]
WriterPlugin.Brainstorm
description: Brainstorm ideas
args:
- input: the input to brainstorm about
EdgarAllenPoePlugin.Poe
description: Write in the style of author Edgar Allen Poe
args:
- input: the input to write about
WriterPlugin.EmailTo
description: Write an email to a recipient
args:
- input: the input to write about
- recipient: the recipient's email address.
WriterPlugin.Translate
description: translate the input to another language
args:
- input: the text to translate
- language: the language to translate to
[GOAL]
"Tomorrow is Valentine's day. I need to come up with a few date ideas.
She likes Edgar Allen Poe so write using his style.
E-mail these ideas to my significant other. Translate it to French."
[OUTPUT]
{
"input": "Valentine's Day Date Ideas",
"subtasks": [
{"function": "WriterPlugin.Brainstorm"},
{"function": "EdgarAllenPoePlugin.Poe"},
{"function": "WriterPlugin.EmailTo", "args": {"recipient": "significant_other"}},
{"function": "WriterPlugin.Translate", "args": {"language": "French"}}
]
}
[AVAILABLE FUNCTIONS]
{{$available_functions}}
[GOAL]
{{$goal}}
[OUTPUT]
"""
class BasicPlanner:
"""
Basic JSON-based planner for the Semantic Kernel.
"""
def _create_available_functions_string(self, kernel: Kernel) -> str:
"""
Given an instance of the Kernel, create the [AVAILABLE FUNCTIONS]
string for the prompt.
"""
# Get a dictionary of plugin names to all native and semantic functions
native_functions = kernel.plugins.get_functions_view().native_functions
semantic_functions = kernel.plugins.get_functions_view().semantic_functions
native_functions.update(semantic_functions)
# Create a mapping between all function names and their descriptions
# and also a mapping between function names and their parameters
all_functions = native_functions
plugin_names = list(all_functions.keys())
all_functions_descriptions_dict = {}
all_functions_params_dict = {}
for plugin_name in plugin_names:
for func in all_functions[plugin_name]:
key = plugin_name + "." + func.name
all_functions_descriptions_dict[key] = func.description
all_functions_params_dict[key] = func.parameters
# Create the [AVAILABLE FUNCTIONS] section of the prompt
available_functions_string = ""
for name in list(all_functions_descriptions_dict.keys()):
available_functions_string += name + "\n"
description = all_functions_descriptions_dict[name]
available_functions_string += "description: " + description + "\n"
available_functions_string += "args:\n"
# Add the parameters for each function
parameters = all_functions_params_dict[name]
for param in parameters:
if not param.description:
param_description = ""
else:
param_description = param.description
available_functions_string += "- " + param.name + ": " + param_description + "\n"
available_functions_string += "\n"
return available_functions_string
async def create_plan_async(
self,
goal: str,
kernel: Kernel,
prompt: str = PROMPT,
) -> Plan:
"""
Creates a plan for the given goal based off the functions that
are available in the kernel.
"""
# Create the semantic function for the planner with the given prompt
planner = kernel.create_semantic_function(prompt, max_tokens=1000, temperature=0.8)
available_functions_string = self._create_available_functions_string(kernel)
# Create the context for the planner
context = ContextVariables()
# Add the goal to the context
context["goal"] = goal
context["available_functions"] = available_functions_string
generated_plan = await planner.invoke_async(variables=context)
return Plan(prompt=prompt, goal=goal, plan=generated_plan)
async def execute_plan_async(self, plan: Plan, kernel: Kernel) -> str:
"""
Given a plan, execute each of the functions within the plan
from start to finish and output the result.
"""
# Filter out good JSON from the result in case additional text is present
json_regex = r"\{(?:[^{}]|(?R))*\}"
generated_plan_string = regex.search(json_regex, plan.generated_plan.result).group()
generated_plan = json.loads(generated_plan_string)
context = ContextVariables()
context["input"] = generated_plan["input"]
subtasks = generated_plan["subtasks"]
for subtask in subtasks:
plugin_name, function_name = subtask["function"].split(".")
sk_function = kernel.plugins.get_function(plugin_name, function_name)
# Get the arguments dictionary for the function
args = subtask.get("args", None)
if args:
for key, value in args.items():
context[key] = value
output = await sk_function.invoke_async(variables=context)
else:
output = await sk_function.invoke_async(variables=context)
# Override the input context variable with the output of the function
context["input"] = output.result
# At the very end, return the output of the last function
return output.result