Skip to content

Commit e8c299a

Browse files
Python: Use overloads for run_async method (microsoft#511)
### Motivation and Context To make the Python SK more pythonic, combined all kernel.run_* methods into a single method with optional parameters. ### Description - Removed `kernel.run_on_vars_async()` - Removed `kernel.run_on_str_async()` - Added `SKContext`, `ContextVariables`, and string params to `kernel.run_async()` - Moved `run_on_vars_async()` logic to `run_async()` method - Arranged method params so that required params come before optional params. - Updated tests and notebooks where applicable.
1 parent fcea07d commit e8c299a

10 files changed

Lines changed: 221 additions & 44 deletions

File tree

python/semantic_kernel/kernel.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,41 @@ def register_semantic_function(
106106

107107
return function
108108

109-
async def run_async(self, *functions: Any) -> SKContext:
110-
return await self.run_on_vars_async(ContextVariables(), *functions)
111-
112-
async def run_on_str_async(self, input_str: str, *functions: Any) -> SKContext:
113-
return await self.run_on_vars_async(ContextVariables(input_str), *functions)
114-
115-
async def run_on_vars_async(
116-
self, input_vars: ContextVariables, *functions: Any
109+
async def run_async(
110+
self,
111+
*functions: Any,
112+
input_context: Optional[SKContext] = None,
113+
input_vars: Optional[ContextVariables] = None,
114+
input_str: Optional[str] = None,
117115
) -> SKContext:
118-
context = SKContext(
119-
input_vars,
120-
self._memory,
121-
self._skill_collection.read_only_skill_collection,
122-
self._log,
123-
)
124-
116+
117+
# if the user passed in a context, prioritize it, but merge with any other inputs
118+
if input_context is not None:
119+
context = input_context
120+
if input_vars is not None:
121+
context._variables = input_vars.merge_or_overwrite(new_vars=context._variables, overwrite=False)
122+
123+
if input_str is not None:
124+
context._variables = ContextVariables(input_str).merge_or_overwrite(new_vars=context._variables, overwrite=False)
125+
126+
# if the user did not pass in a context, prioritize an input string, and merge that with input context variables
127+
else:
128+
if input_str is not None and input_vars is None:
129+
variables = ContextVariables(input_str)
130+
elif input_str is None and input_vars is not None:
131+
variables = input_vars
132+
elif input_str is not None and input_vars is not None:
133+
variables = ContextVariables(input_str)
134+
variables = variables.merge_or_overwrite(new_vars=input_vars, overwrite=False)
135+
else:
136+
variables = ContextVariables()
137+
context = SKContext(
138+
variables,
139+
self._memory,
140+
self._skill_collection.read_only_skill_collection,
141+
self._log,
142+
)
143+
125144
pipeline_step = 0
126145
for func in functions:
127146
assert isinstance(func, SKFunctionBase), (

python/semantic_kernel/kernel_base.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,15 @@ def register_memory(self, memory: SemanticTextMemoryBase) -> None:
6666
pass
6767

6868
@abstractmethod
69-
async def run_on_str_async(self, input_str: str, *args: Any) -> SKContext:
70-
pass
71-
72-
@abstractmethod
73-
async def run_on_vars_async(
74-
self, input_vars: ContextVariables, *args: Any
69+
async def run_async(
70+
self,
71+
*functions: Any,
72+
input_context: Optional[SKContext],
73+
input_vars: Optional[ContextVariables],
74+
input_str: Optional[str]
7575
) -> SKContext:
7676
pass
7777

78-
@abstractmethod
79-
async def run_async(self, *args: Any) -> SKContext:
80-
pass
81-
8278
@abstractmethod
8379
def func(self, skill_name: str, function_name: str) -> SKFunctionBase:
8480
pass

python/semantic_kernel/orchestration/context_variables.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ def update(self, content: str) -> "ContextVariables":
2525
return self
2626

2727
def merge_or_overwrite(
28-
self, new_context: "ContextVariables", merge: bool = True
28+
self, new_vars: "ContextVariables", overwrite: bool = False
2929
) -> "ContextVariables":
30-
if not merge:
31-
self._variables = {}
32-
33-
self._variables.update(new_context._variables)
30+
if overwrite:
31+
self._variables = new_vars._variables
32+
else:
33+
self._variables.update(new_vars._variables)
3434
return self
3535

3636
def set(self, name: str, value: str) -> "ContextVariables":

python/tests/end-to-end/basics.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
# Define semantic function using SK prompt template language
2525
sk_prompt = """
2626
{{$input}}
27+
{{$input2}}
2728
28-
Give me the TLDR in 5 words.
29+
Give me the TLDR in 5 words:
2930
"""
3031

3132
# Create the semantic function
@@ -49,8 +50,49 @@
4950
print(text_to_summarize)
5051
print()
5152

52-
# Summarize and print
53-
summary = asyncio.run(kernel.run_on_str_async(text_to_summarize, tldr_function))
53+
# Summarize input string and print
54+
summary = asyncio.run(kernel.run_async(tldr_function, input_str=text_to_summarize))
5455

5556
output = str(summary).strip()
56-
print(f"Summary is: '{output}'")
57+
print(f"Summary using input string: '{output}'")
58+
59+
# Summarize input as context variable and print
60+
context_vars = sk.ContextVariables(text_to_summarize)
61+
summary = asyncio.run(kernel.run_async(tldr_function, input_vars=context_vars))
62+
63+
output = str(summary).strip()
64+
print(f"Summary using context variables: '{output}'")
65+
66+
# Summarize input context and print
67+
context = kernel.create_new_context()
68+
context["input"] = text_to_summarize
69+
summary = asyncio.run(kernel.run_async(tldr_function, input_context=context))
70+
71+
output = str(summary).strip()
72+
print(f"Summary using input context: '{output}'")
73+
74+
# Summarize input context with additional variables and print
75+
context = kernel.create_new_context()
76+
context["input"] = text_to_summarize
77+
context_vars = sk.ContextVariables("4) All birds are robots.")
78+
summary = asyncio.run(kernel.run_async(tldr_function, input_context=context, input_vars=context_vars))
79+
80+
output = str(summary).strip()
81+
print(f"Summary using context and additional variables: '{output}'")
82+
83+
# Summarize input context with additional input string and print
84+
context = kernel.create_new_context()
85+
context["input"] = text_to_summarize
86+
summary = asyncio.run(kernel.run_async(tldr_function, input_context=context, input_str="4) All birds are robots."))
87+
88+
output = str(summary).strip()
89+
print(f"Summary using context and additional string: '{output}'")
90+
91+
# Summarize input context with additional variables and string and print
92+
context = kernel.create_new_context()
93+
context["input"] = text_to_summarize
94+
context_vars = sk.ContextVariables(variables={"input2":"4) All birds are robots."})
95+
summary = asyncio.run(kernel.run_async(tldr_function, input_context=context, input_vars=context_vars, input_str="new text"))
96+
97+
output = str(summary).strip()
98+
print(f"Summary using context, additional variables, and additional string: '{output}'")

python/tests/end-to-end/chat.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@
3434
chat_function = kernel.register_semantic_function("ChatBot", "Chat", function_config)
3535

3636

37-
async def chat(context: sk.ContextVariables) -> bool:
37+
async def chat(context_vars: sk.ContextVariables) -> bool:
3838
try:
3939
user_input = input("User:> ")
40-
context["user_input"] = user_input
40+
context_vars["user_input"] = user_input
4141
except KeyboardInterrupt:
4242
print("\n\nExiting chat...")
4343
return False
@@ -49,8 +49,8 @@ async def chat(context: sk.ContextVariables) -> bool:
4949
print("\n\nExiting chat...")
5050
return False
5151

52-
answer = await kernel.run_on_vars_async(context, chat_function)
53-
context["chat_history"] += f"\nUser:> {user_input}\nChatBot:> {answer}\n"
52+
answer = await kernel.run_async(chat_function, input_vars=context_vars)
53+
context_vars["chat_history"] += f"\nUser:> {user_input}\nChatBot:> {answer}\n"
5454

5555
print(f"ChatBot:> {answer}")
5656
return True

python/tests/end-to-end/chat_gpt_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@
4040

4141

4242
async def chat() -> bool:
43-
context = sk.ContextVariables()
43+
context_vars = sk.ContextVariables()
4444

4545
try:
4646
user_input = input("User:> ")
47-
context["user_input"] = user_input
47+
context_vars["user_input"] = user_input
4848
except KeyboardInterrupt:
4949
print("\n\nExiting chat...")
5050
return False
@@ -56,7 +56,7 @@ async def chat() -> bool:
5656
print("\n\nExiting chat...")
5757
return False
5858

59-
answer = await kernel.run_on_vars_async(context, chat_function)
59+
answer = await kernel.run_async(chat_function, input_vars=context_vars)
6060
print(f"Mosscap:> {answer}")
6161
return True
6262

python/tests/end-to-end/memory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async def chat(
9797
print("\n\nExiting chat...")
9898
return False
9999

100-
answer = await kernel.run_on_vars_async(context.variables, chat_func)
100+
answer = await kernel.run_async(chat_func, input_vars=context.variables)
101101
context["chat_history"] += f"\nUser:> {user_input}\nChatBot:> {answer}\n"
102102

103103
print(f"ChatBot:> {answer}")

python/tests/end-to-end/skills_from_dir.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
skill = kernel.import_semantic_skill_from_directory(skills_directory, "FunSkill")
3030

3131
result = asyncio.run(
32-
kernel.run_on_str_async("time travel to dinosaur age", skill["Joke"])
32+
kernel.run_async(skill["Joke"], input_str="time travel to dinosaur age")
3333
)
3434
print(result)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from semantic_kernel.orchestration.context_variables import ContextVariables
2+
3+
def test_context_vars_contain_single_var_by_default():
4+
context_vars = ContextVariables()
5+
assert context_vars._variables is not None
6+
assert len(context_vars._variables) == 1
7+
assert context_vars._variables["input"] == ""
8+
9+
def test_context_vars_can_be_constructed_with_string():
10+
content = "Hello, world!"
11+
context_vars = ContextVariables(content)
12+
assert context_vars._variables is not None
13+
assert len(context_vars._variables) == 1
14+
assert context_vars._variables["input"] == content
15+
16+
def test_context_vars_can_be_constructed_with_dict():
17+
variables = {"test_string": "Hello, world!"}
18+
context_vars = ContextVariables(variables=variables)
19+
assert context_vars._variables is not None
20+
assert len(context_vars._variables) == 2
21+
assert context_vars._variables["input"] == ""
22+
assert context_vars._variables["test_string"] == variables["test_string"]
23+
24+
def test_context_vars_can_be_constructed_with_string_and_dict():
25+
content = "I love muffins"
26+
variables = {"test_string": "Hello, world!"}
27+
context_vars = ContextVariables(content=content, variables=variables)
28+
assert context_vars._variables is not None
29+
assert len(context_vars._variables) == 2
30+
assert context_vars._variables["input"] == content
31+
assert context_vars._variables["test_string"] == variables["test_string"]
32+
33+
def test_merged_context_vars_with_empty_input_results_in_empty_input():
34+
content = "I love muffins"
35+
variables = {"test_string": "Hello, world!"}
36+
context_vars1 = ContextVariables(content=content)
37+
context_vars2 = ContextVariables(variables=variables)
38+
context_vars_combined_1with2 = context_vars1.merge_or_overwrite(context_vars2)
39+
context_vars_combined_2with1 = context_vars2.merge_or_overwrite(context_vars1)
40+
41+
assert context_vars_combined_1with2._variables is not None
42+
assert len(context_vars_combined_1with2._variables) == 2
43+
assert context_vars_combined_1with2._variables["input"] == ""
44+
assert context_vars_combined_1with2._variables["test_string"] == variables["test_string"]
45+
46+
assert context_vars_combined_2with1._variables is not None
47+
assert len(context_vars_combined_2with1._variables) == 2
48+
assert context_vars_combined_2with1._variables["input"] == ""
49+
assert context_vars_combined_2with1._variables["test_string"] == variables["test_string"]
50+
51+
def test_merged_context_vars_with_same_input_results_in_unchanged_input():
52+
content = "I love muffins"
53+
variables = {"test_string": "Hello, world!"}
54+
context_vars1 = ContextVariables(content=content)
55+
context_vars2 = ContextVariables(content=content, variables=variables)
56+
context_vars_combined_1with2 = context_vars1.merge_or_overwrite(context_vars2)
57+
context_vars_combined_2with1 = context_vars2.merge_or_overwrite(context_vars1)
58+
59+
assert context_vars_combined_1with2._variables is not None
60+
assert len(context_vars_combined_1with2._variables) == 2
61+
assert context_vars_combined_1with2._variables["input"] == content
62+
assert context_vars_combined_1with2._variables["test_string"] == variables["test_string"]
63+
64+
assert context_vars_combined_2with1._variables is not None
65+
assert len(context_vars_combined_2with1._variables) == 2
66+
assert context_vars_combined_2with1._variables["input"] == content
67+
assert context_vars_combined_2with1._variables["test_string"] == variables["test_string"]
68+
69+
def test_merged_context_vars_with_different_input_results_in_input_overwrite1():
70+
content = "I love muffins"
71+
content2 = "I love cupcakes"
72+
variables = {"test_string": "Hello, world!"}
73+
context_vars1 = ContextVariables(content=content)
74+
context_vars2 = ContextVariables(content=content2, variables=variables)
75+
context_vars_combined_1with2 = context_vars1.merge_or_overwrite(context_vars2, overwrite=False)
76+
77+
assert context_vars_combined_1with2._variables is not None
78+
assert len(context_vars_combined_1with2._variables) == 2
79+
assert context_vars_combined_1with2._variables["input"] == context_vars2._variables["input"]
80+
assert context_vars_combined_1with2._variables["test_string"] == context_vars2._variables["test_string"]
81+
82+
def test_merged_context_vars_with_different_input_results_in_input_overwrite2():
83+
content = "I love muffins"
84+
content2 = "I love cupcakes"
85+
variables = {"test_string": "Hello, world!"}
86+
context_vars1 = ContextVariables(content=content)
87+
context_vars2 = ContextVariables(content=content2, variables=variables)
88+
context_vars_combined_2with1 = context_vars2.merge_or_overwrite(context_vars1, overwrite=False)
89+
90+
assert context_vars_combined_2with1._variables is not None
91+
assert len(context_vars_combined_2with1._variables) == 2
92+
assert context_vars_combined_2with1._variables["input"] == context_vars1["input"]
93+
assert context_vars_combined_2with1._variables["test_string"] == context_vars2._variables["test_string"]
94+
95+
def test_can_overwrite_context_variables1():
96+
content = "I love muffins"
97+
content2 = "I love cupcakes"
98+
variables = {"test_string": "Hello, world!"}
99+
context_vars1 = ContextVariables(content=content)
100+
context_vars2 = ContextVariables(content=content2, variables=variables)
101+
context_vars_overwrite_1with2 = context_vars1.merge_or_overwrite(context_vars2, overwrite=True)
102+
103+
assert context_vars_overwrite_1with2._variables is not None
104+
assert len(context_vars_overwrite_1with2._variables) == len(context_vars2._variables)
105+
assert context_vars_overwrite_1with2._variables["input"] == context_vars2._variables["input"]
106+
assert context_vars_overwrite_1with2._variables["test_string"] == context_vars2["test_string"]
107+
108+
def test_can_overwrite_context_variables2():
109+
content = "I love muffins"
110+
content2 = "I love cupcakes"
111+
variables = {"test_string": "Hello, world!"}
112+
context_vars1 = ContextVariables(content=content)
113+
context_vars2 = ContextVariables(content=content2, variables=variables)
114+
context_vars_overwrite_2with1 = context_vars2.merge_or_overwrite(context_vars1, overwrite=True)
115+
116+
assert context_vars_overwrite_2with1._variables is not None
117+
assert len(context_vars_overwrite_2with1._variables) == len(context_vars1._variables)
118+
assert context_vars_overwrite_2with1._variables["input"] == context_vars1._variables["input"]
119+
120+

samples/notebooks/python/06-memory-and-embeddings.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@
255255
" print(\"\\n\\nExiting chat...\")\n",
256256
" return False\n",
257257
"\n",
258-
" answer = await kernel.run_on_vars_async(context.variables, chat_func)\n",
258+
" answer = await kernel.run_async(chat_func, input_vars=context.variables)\n",
259259
" context[\"chat_history\"] += f\"\\nUser:> {user_input}\\nChatBot:> {answer}\\n\"\n",
260260
"\n",
261261
" print(f\"ChatBot:> {answer}\")\n",

0 commit comments

Comments
 (0)