Skip to content

Commit 224857e

Browse files
alexchaomanderachao-microsoftAlex Chaoawharrison-28mkarle
authored
Python: Basic JSON-based Planner implementation (microsoft#597)
### Motivation and Context Many users and customers have expressed interest in having planning capabilities in Python. This PR seeks to provide a basic version of the planner so that people can learn and play around with the concepts and can be comfortable with implementing their own versions. A Jupyter notebook is included. ### Description Most of the logic of the plan can be found under the `PROMPT` in `basic_planner.py`. There is a known limitation that this currently doesn't explicit parse out arguments that are defined in the semantic function prompts, so when creating the [AVAILABLE FUNCTIONS] string, it just uses the name of the function and the description. The execution of the plan also happens from start to finish. There is currently no support for piecewise execution. --------- Co-authored-by: Alex Chao <[email protected]> Co-authored-by: Alex Chao <[email protected]> Co-authored-by: Abby Harrison <[email protected]> Co-authored-by: Mark Karle <[email protected]>
1 parent d55519d commit 224857e

7 files changed

Lines changed: 538 additions & 198 deletions

File tree

python/semantic_kernel/core_skills/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,11 @@
66
from semantic_kernel.core_skills.text_skill import TextSkill
77
from semantic_kernel.core_skills.time_skill import TimeSkill
88

9-
__all__ = ["TextMemorySkill", "TextSkill", "FileIOSkill", "TimeSkill", "HttpSkill"]
9+
__all__ = [
10+
"TextMemorySkill",
11+
"TextSkill",
12+
"FileIOSkill",
13+
"TimeSkill",
14+
"HttpSkill",
15+
"BasicPlanner",
16+
]
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
"""A basic JSON-based planner for the Python Semantic Kernel"""
4+
import json
5+
6+
from semantic_kernel.kernel import Kernel
7+
from semantic_kernel.orchestration.context_variables import ContextVariables
8+
from semantic_kernel.planning.plan import Plan
9+
10+
PROMPT = """
11+
You are a planner for the Semantic Kernel.
12+
Your job is to create a properly formatted JSON plan step by step, to satisfy the goal given.
13+
Create a list of subtasks based off the [GOAL] provided.
14+
Each subtask must be from within the [AVAILABLE FUNCTIONS] list. Do not use any functions that are not in the list.
15+
Base your decisions on which functions to use from the description and the name of the function.
16+
Sometimes, a function may take arguments. Provide them if necessary.
17+
The plan should be as short as possible.
18+
For example:
19+
20+
[AVAILABLE FUNCTIONS]
21+
EmailConnector.LookupContactEmail
22+
description: looks up the a contact and retrieves their email address
23+
args:
24+
- name: the name to look up
25+
26+
WriterSkill.EmailTo
27+
description: email the input text to a recipient
28+
args:
29+
- input: the text to email
30+
- recipient: the recipient's email address. Multiple addresses may be included if separated by ';'.
31+
32+
WriterSkill.Translate
33+
description: translate the input to another language
34+
args:
35+
- input: the text to translate
36+
- language: the language to translate to
37+
38+
WriterSkill.Summarize
39+
description: summarize input text
40+
args:
41+
- input: the text to summarize
42+
43+
FunSkill.Joke
44+
description: Generate a funny joke
45+
args:
46+
- input: the input to generate a joke about
47+
48+
[GOAL]
49+
"Tell a joke about cars. Translate it to Spanish"
50+
51+
[OUTPUT]
52+
{
53+
"input": "cars",
54+
"subtasks": [
55+
{"function": "FunSkill.Joke"},
56+
{"function": "WriterSkill.Translate", "args": {"language": "Spanish"}}
57+
]
58+
}
59+
60+
[AVAILABLE FUNCTIONS]
61+
WriterSkill.Brainstorm
62+
description: Brainstorm ideas
63+
args:
64+
- input: the input to brainstorm about
65+
66+
EdgarAllenPoeSkill.Poe
67+
description: Write in the style of author Edgar Allen Poe
68+
args:
69+
- input: the input to write about
70+
71+
WriterSkill.EmailTo
72+
description: Write an email to a recipient
73+
args:
74+
- input: the input to write about
75+
- recipient: the recipient's email address.
76+
77+
WriterSkill.Translate
78+
description: translate the input to another language
79+
args:
80+
- input: the text to translate
81+
- language: the language to translate to
82+
83+
[GOAL]
84+
"Tomorrow is Valentine's day. I need to come up with a few date ideas.
85+
She likes Edgar Allen Poe so write using his style.
86+
E-mail these ideas to my significant other. Translate it to French."
87+
88+
[OUTPUT]
89+
{
90+
"input": "Valentine's Day Date Ideas",
91+
"subtasks": [
92+
{"function": "WriterSkill.Brainstorm"},
93+
{"function": "EdgarAllenPoeSkill.Poe"},
94+
{"function": "WriterSkill.EmailTo", "args": {"recipient": "significant_other"}},
95+
{"function": "translate", "args": {"language": "French"}}
96+
]
97+
}
98+
99+
[AVAILABLE FUNCTIONS]
100+
{{$available_functions}}
101+
102+
[GOAL]
103+
{{$goal}}
104+
105+
[OUTPUT]
106+
"""
107+
108+
109+
class BasicPlanner:
110+
"""
111+
Basic JSON-based planner for the Semantic Kernel.
112+
"""
113+
114+
def _create_available_functions_string(self, kernel: Kernel) -> str:
115+
"""
116+
Given an instance of the Kernel, create the [AVAILABLE FUNCTIONS]
117+
string for the prompt.
118+
"""
119+
# Get a dictionary of skill names to all native and semantic functions
120+
native_functions = kernel.skills.get_functions_view()._native_functions
121+
semantic_functions = kernel.skills.get_functions_view()._semantic_functions
122+
native_functions.update(semantic_functions)
123+
124+
# Create a mapping between all function names and their descriptions
125+
# and also a mapping between function names and their parameters
126+
all_functions = native_functions
127+
skill_names = list(all_functions.keys())
128+
all_functions_descriptions_dict = {}
129+
all_functions_params_dict = {}
130+
131+
for skill_name in skill_names:
132+
for func in all_functions[skill_name]:
133+
key = skill_name + "." + func.name
134+
all_functions_descriptions_dict[key] = func.description
135+
all_functions_params_dict[key] = func.parameters
136+
137+
# Create the [AVAILABLE FUNCTIONS] section of the prompt
138+
available_functions_string = ""
139+
for name in list(all_functions_descriptions_dict.keys()):
140+
available_functions_string += name + "\n"
141+
description = all_functions_descriptions_dict[name]
142+
available_functions_string += "description: " + description + "\n"
143+
available_functions_string += "args:\n"
144+
145+
# Add the parameters for each function
146+
parameters = all_functions_params_dict[name]
147+
for param in parameters:
148+
if not param.description:
149+
param_description = ""
150+
else:
151+
param_description = param.description
152+
available_functions_string += (
153+
"- " + param.name + ": " + param_description + "\n"
154+
)
155+
available_functions_string += "\n"
156+
157+
return available_functions_string
158+
159+
async def create_plan_async(
160+
self,
161+
goal: str,
162+
kernel: Kernel,
163+
prompt: str = PROMPT,
164+
) -> Plan:
165+
"""
166+
Creates a plan for the given goal based off the functions that
167+
are available in the kernel.
168+
"""
169+
170+
# Create the semantic function for the planner with the given prompt
171+
planner = kernel.create_semantic_function(
172+
prompt, max_tokens=1000, temperature=0.8
173+
)
174+
175+
available_functions_string = self._create_available_functions_string(kernel)
176+
177+
# Create the context for the planner
178+
context = ContextVariables()
179+
# Add the goal to the context
180+
context["goal"] = goal
181+
context["available_functions"] = available_functions_string
182+
generated_plan = await planner.invoke_async(variables=context)
183+
return Plan(prompt=prompt, goal=goal, plan=generated_plan)
184+
185+
async def execute_plan_async(self, plan: Plan, kernel: Kernel) -> str:
186+
"""
187+
Given a plan, execute each of the functions within the plan
188+
from start to finish and output the result.
189+
"""
190+
generated_plan = json.loads(plan.generated_plan.result)
191+
192+
context = ContextVariables()
193+
context["input"] = generated_plan["input"]
194+
subtasks = generated_plan["subtasks"]
195+
196+
for subtask in subtasks:
197+
skill_name, function_name = subtask["function"].split(".")
198+
sk_function = kernel.skills.get_function(skill_name, function_name)
199+
200+
# Get the arguments dictionary for the function
201+
args = subtask.get("args", None)
202+
if args:
203+
for key, value in args.items():
204+
context[key] = value
205+
output = await sk_function.invoke_async(variables=context)
206+
207+
else:
208+
output = await sk_function.invoke_async(variables=context)
209+
210+
# Override the input context variable with the output of the function
211+
context["input"] = output.result
212+
213+
# At the very end, return the output of the last function
214+
return output.result
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Any
2+
3+
4+
class Plan:
5+
# The goal that wants to be achieved
6+
goal: str
7+
8+
# The prompt to be used to generate the plan
9+
prompt: str
10+
11+
# The generated plan that consists of a list of steps to complete the goal
12+
generated_plan: Any
13+
14+
def __init__(self, goal, prompt, plan):
15+
self.goal = goal
16+
self.prompt = prompt
17+
self.generated_plan = plan

0 commit comments

Comments
 (0)