|
| 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 |
0 commit comments