Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: Add sequential planner to python #1916

Merged
merged 57 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
03f6e24
beginning POM implementation
awharrison-28 Jun 14, 2023
da99e0a
base object model mostly implemented
awharrison-28 Jun 16, 2023
94afd0b
fully ported, passes intellisense, untested
awharrison-28 Jun 16, 2023
f00e733
added basic action planner, not fully ported and untested
awharrison-28 Jun 16, 2023
d777f62
Merge branch 'microsoft:main' into python/plan_object_model
awharrison-28 Jul 6, 2023
1dcb631
basic tests written for plan creation and executiion
awharrison-28 Jul 8, 2023
6278723
pre-commit checks
awharrison-28 Jul 8, 2023
6f16f4a
added sync tests, added missing copywrite headers, more pre-commit ch…
awharrison-28 Jul 8, 2023
2f03b9a
fixed type
awharrison-28 Jul 8, 2023
54d0d51
remove action planner for this PR
awharrison-28 Jul 8, 2023
0131571
Fix typo
shawncal Jul 8, 2023
37b2977
Merge branch 'main' into python/plan_object_model
shawncal Jul 8, 2023
a49116e
feat: implement sequential planner parser
joowon-dm-snu Jul 9, 2023
bc2e639
feat: update parser typo
joowon-dm-snu Jul 9, 2023
91d4ced
feat: add planning exceptions
joowon-dm-snu Jul 9, 2023
0781a8a
feat: add sequential planner config
joowon-dm-snu Jul 9, 2023
72e39e3
fix: typo
joowon-dm-snu Jul 9, 2023
ac7ca8e
add TODO for better hinting
joowon-dm-snu Jul 10, 2023
373af2c
add planning skill
joowon-dm-snu Jul 10, 2023
27f5aab
implement sequential planner
joowon-dm-snu Jul 10, 2023
98b38e3
add test for planner extension
joowon-dm-snu Jul 10, 2023
256a625
update extensions for tests
joowon-dm-snu Jul 10, 2023
27f439b
implement planner test
joowon-dm-snu Jul 10, 2023
e4f37d4
change logger into Mock
joowon-dm-snu Jul 10, 2023
496f3e1
add fake functions for integration test
joowon-dm-snu Jul 10, 2023
b0a42dd
add tests
joowon-dm-snu Jul 10, 2023
b416bf1
update planner
joowon-dm-snu Jul 10, 2023
e1eb38e
linting
joowon-dm-snu Jul 10, 2023
7bd570e
Merge branch 'main' into python/add_sequential_planner
joowon-dm-snu Jul 10, 2023
f31af74
Merge branch 'main' into python/add_sequential_planner
joowon-dm-snu Jul 14, 2023
c0df7bf
Merge branch 'main' into python/add_sequential_planner
joowon-dm-snu Jul 18, 2023
dc67b84
Merge branch 'main' into python/add_sequential_planner
awharrison-28 Jul 18, 2023
ac2fdd8
Merge branch 'main' into python/add_sequential_planner
awharrison-28 Jul 19, 2023
f972eb2
Merge branch 'main' into python/add_sequential_planner
joowon-dm-snu Jul 20, 2023
7ccf6b0
revert change on launch.json
joowon-dm-snu Jul 20, 2023
f6778e2
add copyright
joowon-dm-snu Jul 20, 2023
c22ccff
add todo, implement skill import helper
joowon-dm-snu Jul 20, 2023
ee0bd9b
solve conversations
joowon-dm-snu Jul 20, 2023
c1b6fce
sync file names with C#
joowon-dm-snu Jul 20, 2023
366fc9b
add init
joowon-dm-snu Jul 20, 2023
6aad764
add sample
joowon-dm-snu Jul 20, 2023
5e098fd
fix errors on plan object
joowon-dm-snu Jul 20, 2023
7a397fe
Merge branch 'main' into python/add_sequential_planner
awharrison-28 Jul 25, 2023
f287ff7
Update python/tests/unit/planning/sequential_planner/test_sequential_…
awharrison-28 Jul 25, 2023
afaf1d4
Update python/tests/unit/planning/sequential_planner/test_sequential_…
awharrison-28 Jul 25, 2023
035a260
Update python/tests/unit/planning/sequential_planner/test_sequential_…
awharrison-28 Jul 25, 2023
1b36a7d
string checking needs to be for None and empty ()
awharrison-28 Jul 25, 2023
934cafc
Merge branch 'python/add_sequential_planner' of https://github.com/jo…
awharrison-28 Jul 25, 2023
d8820da
pre-commit checks
awharrison-28 Jul 25, 2023
09de751
Merge branch 'main' into python/add_sequential_planner
awharrison-28 Jul 25, 2023
1af1edc
sync c# logic
joowon-dm-snu Jul 26, 2023
93fc006
Merge branch 'main' into python/add_sequential_planner
awharrison-28 Jul 26, 2023
b8ffd5d
Merge branch 'main' into python/add_sequential_planner
awharrison-28 Jul 27, 2023
25416cd
fixed input variable checking in plan.py and created a more reliable …
awharrison-28 Jul 28, 2023
bb80a95
Merge branch 'main' into python/add_sequential_planner
awharrison-28 Jul 28, 2023
76abe77
fixed regression
awharrison-28 Jul 28, 2023
3373575
Merge branch 'python/add_sequential_planner' of https://github.com/jo…
awharrison-28 Jul 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
"request": "attach"
},
{
"cwd":"${workspaceFolder}/python",
"cwd": "${workspaceFolder}/python",
"name": "Python: Test Module",
"type": "python",
"request": "launch",
"module": "pytest",
"args": ["${file}"],
"args": ["${file}"]
}
]
}
47 changes: 47 additions & 0 deletions python/samples/kernel-syntax-examples/sequential_planner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright (c) Microsoft. All rights reserved.

import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.core_skills import FileIOSkill, MathSkill, TextSkill, TimeSkill
from semantic_kernel.planning import SequentialPlanner


async def main():
kernel = sk.Kernel()
api_key, org_id = sk.openai_settings_from_dot_env()

kernel.add_chat_service(
"gpt-3.5", OpenAIChatCompletion("gpt-3.5-turbo", api_key, org_id)
)
kernel.import_skill(MathSkill(), "math")
kernel.import_skill(FileIOSkill(), "fileIO")
kernel.import_skill(TimeSkill(), "time")
kernel.import_skill(TextSkill(), "text")

# create an instance of sequential planner.
planner = SequentialPlanner(kernel)

# the ask for which the sequential planner is going to find a relevant function.
ask = "What day of the week is today, all uppercase?"

# ask the sequential planner to identify a suitable function from the list of functions available.
plan = await planner.create_plan_async(goal=ask)

# ask the sequential planner to execute the identified function.
result = await plan.invoke_async()

for step in plan._steps:
print(step.description, ":", step._state.__dict__)

print("Expected Answer:")
print(result)
"""
Output:
1100
"""


if __name__ == "__main__":
import asyncio

asyncio.run(main())
1 change: 1 addition & 0 deletions python/semantic_kernel/core_skills/math_skill.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.


from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.skill_definition import sk_function, sk_function_context_parameter

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ async def get_async(
self,
collection: str,
query: str,
# TODO: with_embedding: bool,
) -> Optional[MemoryQueryResult]:
"""Get information from the memory (calls the memory store's get method).

Expand Down
2 changes: 2 additions & 0 deletions python/semantic_kernel/planning/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from semantic_kernel.planning.basic_planner import BasicPlanner
from semantic_kernel.planning.plan import Plan
from semantic_kernel.planning.sequential_planner import SequentialPlanner

__all__ = [
"SequentialPlanner",
"BasicPlanner",
"Plan",
]
63 changes: 40 additions & 23 deletions python/semantic_kernel/planning/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
import threading
from logging import Logger
from typing import Any, Callable, List, Optional
from typing import Any, Callable, List, Optional, Union

from semantic_kernel import Kernel
from semantic_kernel.connectors.ai import CompleteRequestSettings
Expand All @@ -20,6 +20,7 @@
from semantic_kernel.skill_definition.read_only_skill_collection_base import (
ReadOnlySkillCollectionBase,
)
from semantic_kernel.utils.null_logger import NullLogger


class Plan(SKFunctionBase):
Expand Down Expand Up @@ -112,6 +113,16 @@ def __init__(
if function is not None:
self.set_function(function)

@classmethod
def from_goal(cls, goal: str) -> "Plan":
return cls(description=goal, skill_name=cls.__name__)

@classmethod
def from_function(cls, function: SKFunctionBase) -> "Plan":
plan = cls()
plan.set_function(function)
return plan

async def invoke_async(
self,
input: Optional[str] = None,
Expand All @@ -121,15 +132,15 @@ async def invoke_async(
logger: Optional[Logger] = None,
# TODO: cancellation_token: CancellationToken,
) -> SKContext:
if input is not None:
if input is not None and input != "":
self._state.update(input)

if context is None:
context = SKContext(
variables=self._state,
skill_collection=None,
memory=memory,
logger=logger,
logger=logger if logger is not None else NullLogger(),
)

if self._function is not None:
Expand All @@ -138,8 +149,8 @@ async def invoke_async(
)
if result.error_occurred:
result.log.error(
msg="Something went wrong in plan step {0}.{1}:'{2}'".format(
self._skill_name, self._name, context.last_error_description
"Something went wrong in plan step {0}.{1}:'{2}'".format(
self._skill_name, self._name, result.last_error_description
)
)
return result
Expand All @@ -162,7 +173,7 @@ def invoke(
memory: Optional[SemanticTextMemoryBase] = None,
logger: Optional[Logger] = None,
) -> SKContext:
if input is not None:
if input is not None and input != "":
self._state.update(input)

if context is None:
Expand Down Expand Up @@ -244,7 +255,7 @@ def set_available_functions(self, plan: "Plan", context: SKContext) -> "Plan":

return plan

def add_steps(self, steps: Optional[List[SKFunctionBase]]) -> None:
def add_steps(self, steps: Union[List["Plan"], List[SKFunctionBase]]) -> None:
for step in steps:
if type(step) is Plan:
self._steps.append(step)
Expand Down Expand Up @@ -344,7 +355,7 @@ def update_context_with_outputs(self, context: SKContext) -> None:
context.variables.update(result_string)

for item in self._steps[self._next_step_index - 1]._outputs:
if item in self._state:
if self._state.contains_key(item):
context.variables.set(item, self._state[item])
else:
context.variables.set(item, result_string)
Expand All @@ -361,33 +372,35 @@ def get_next_step_variables(
# - Empty if sending to another plan
# - Plan.Description
input_string = ""
if step._parameters["input"] is not None:
input_string = self.expand_from_variables(
variables, step._parameters["input"]
)
elif variables["input"] is not None:
input_string = variables["input"]
elif self._state["input"] is not None:
input_string = self._state["input"]
step_input_exists, input_input_value = step._parameters.get("input")
variables_input_exists, variables_input_value = variables.get("input")
state_input_exists, state_input_value = self._state.get("input")
if step_input_exists:
input_string = self.expand_from_variables(variables, input_input_value)
elif variables_input_exists:
input_string = variables_input_value
elif state_input_exists:
input_string = state_input_value
elif len(step._steps) > 0:
input_string = ""
elif self._description is not None:
elif self._description is not None and self._description != "":
input_string = self._description

step_variables = ContextVariables(input_string)

# Priority for remaining stepVariables is:
# - Function Parameters (pull from variables or state by a key value)
# - Step Parameters (pull from variables or state by a key value)
# - All other variables. These are carried over in case the function wants access to the ambient content.
function_params = step.describe()
for param in function_params._parameters:
if param.name.lower == "input":
if param.name.lower() == variables._main_key.lower():
continue
if step_variables.contains_key(param.name):

if variables.contains_key(param.name):
step_variables.set(param.name, variables[param.name])
elif (
self._state.contains_key(param.name)
and self._state[param.name] is not None
elif self._state.contains_key(param.name) and (
self._state[param.name] is not None and self._state[param.name] != ""
):
step_variables.set(param.name, self._state[param.name])

Expand All @@ -405,14 +418,18 @@ def get_next_step_variables(
else:
step_variables.set(param_var, expanded_value)

for item in variables._variables:
if not step_variables.contains_key(item):
step_variables.set(item, variables[item])

return step_variables

def expand_from_variables(
self, variables: ContextVariables, input_string: str
) -> str:
result = input_string
variables_regex = r"\$(?P<var>\w+)"
matches = re.findall(variables_regex, input_string)
matches = [m for m in re.finditer(variables_regex, input_string)]
ordered_matches = sorted(
matches, key=lambda m: len(m.group("var")), reverse=True
)
Expand Down
46 changes: 46 additions & 0 deletions python/semantic_kernel/planning/planning_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft. All rights reserved.

from enum import Enum
from typing import Optional


class PlanningException(Exception):
class ErrorCodes(Enum):
# Unknown error.
UnknownError = -1
# Invalid goal.
InvalidGoal = 0
# Invalid plan.
InvalidPlan = 1
# Invalid configuration.
InvalidConfiguration = 2
# Create plan error.
CreatePlanError = 3

# The error code.
_error_code: ErrorCodes

def __init__(
self,
error_code: ErrorCodes,
message: str,
inner_exception: Optional[Exception] = None,
) -> None:
"""Initializes a new instance of the PlanningError class.

Arguments:
error_code {ErrorCodes} -- The error code.
message {str} -- The error message.
inner_exception {Exception} -- The inner exception.
"""
super().__init__(error_code, message, inner_exception)
self._error_code = error_code

@property
def error_code(self) -> ErrorCodes:
"""Gets the error code.

Returns:
ErrorCodes -- The error code.
"""
return self._error_code
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"schema": 1,
"description": "Given a request or command or goal generate a step by step plan to fulfill the request using functions. This ability is also known as decision making and function flow",
"type": "completion",
"completion": {
"max_tokens": 1024,
"temperature": 0,
"top_p": 0,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop_sequences": ["<!-- END -->"]
},
"input": {
"parameters": [
{
"name": "input",
"description": "The question to answer",
"defaultValue": ""
},
{
"name": "available_functions",
"description": "The list of the agent's available_functions",
"defaultValue": ""
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
Create an XML plan step by step, to satisfy the goal given, with the available functions.

[AVAILABLE FUNCTIONS]

{{$available_functions}}

[END AVAILABLE FUNCTIONS]

To create a plan, follow these steps:
0. The plan should be as short as possible.
1. From a <goal> create a <plan> as a series of <functions>.
2. A plan has 'INPUT' available in context variables by default.
3. Before using any function in a plan, check that it is present in the [AVAILABLE FUNCTIONS] list. If it is not, do not use it.
4. Only use functions that are required for the given goal.
5. Append an "END" XML comment at the end of the plan after the final closing </plan> tag.
6. Always output valid XML that can be parsed by an XML parser.
7. If a plan cannot be created with the [AVAILABLE FUNCTIONS], return <plan />.

All plans take the form of:
<plan>
<!-- ... reason for taking step ... -->
<function.{FullyQualifiedFunctionName} ... />
<!-- ... reason for taking step ... -->
<function.{FullyQualifiedFunctionName} ... />
<!-- ... reason for taking step ... -->
<function.{FullyQualifiedFunctionName} ... />
(... etc ...)
</plan>
<!-- END -->

To call a function, follow these steps:
1. A function has one or more named parameters and a single 'output' which are all strings. Parameter values should be xml escaped.
2. To save an 'output' from a <function>, to pass into a future <function>, use <function.{FullyQualifiedFunctionName} ... setContextVariable="<UNIQUE_VARIABLE_KEY>"/>
3. To save an 'output' from a <function>, to return as part of a plan result, use <function.{FullyQualifiedFunctionName} ... appendToResult="RESULT__<UNIQUE_RESULT_KEY>"/>
4. Use a '$' to reference a context variable in a parameter, e.g. when `INPUT='world'` the parameter 'Hello $INPUT' will evaluate to `Hello world`.
5. Functions do not have access to the context variables of other functions. Do not attempt to use context variables as arrays or objects. Instead, use available functions to extract specific elements or properties from context variables.

DO NOT DO THIS, THE PARAMETER VALUE IS NOT XML ESCAPED:
<function.Name4 input="$SOME_PREVIOUS_OUTPUT" parameter_name="some value with a <!-- comment in it-->"/>

DO NOT DO THIS, THE PARAMETER VALUE IS ATTEMPTING TO USE A CONTEXT VARIABLE AS AN ARRAY/OBJECT:
<function.CallFunction input="$OTHER_OUTPUT[1]"/>

Here is a valid example of how to call a function "_Function_.Name" with a single input and save its output:
<function._Function_.Name input="this is my input" setContextVariable="SOME_KEY"/>

Here is a valid example of how to call a function "FunctionName2" with a single input and return its output as part of the plan result:
<function.FunctionName2 input="Hello $INPUT" appendToResult="RESULT__FINAL_ANSWER"/>

Here is a valid example of how to call a function "Name3" with multiple inputs:
<function.Name3 input="$SOME_PREVIOUS_OUTPUT" parameter_name="some value with a &lt;!-- comment in it--&gt;"/>

Begin!

<goal>{{$input}}</goal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from semantic_kernel.planning.sequential_planner.sequential_planner import (
SequentialPlanner,
)

__all__ = [
"SequentialPlanner",
]
Loading
Loading