Skip to content

Commit 7cd0fea

Browse files
poweihuang0817pohuanmkarledluc
authored
[Python] Port MathSkill from C# to python3 (microsoft#841)
### Motivation and Context Port MathSkill from C# to python3. This is essential for advanced skill like plan ### Description The PR includes two part. The first part is an implementation of MathSkill. The implementation follows C# version. It does several things: 1. Parse initial value 2. Get value from context 3. Sum or subtract The unit test covers these scenarios: 1. Add/subtract value 2. Error should throw if string is not correct. Not able to parse. Last part is the init.py and feature matrix. Unit test pass locally. Co-authored-by: Po-Wei Huang <[email protected]> Co-authored-by: Mark Karle <[email protected]> Co-authored-by: Devis Lucato <[email protected]>
1 parent 0b1e286 commit 7cd0fea

4 files changed

Lines changed: 271 additions & 1 deletion

File tree

FEATURE_MATRIX.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
| ConversationSummarySkill ||| |
3232
| FileIOSkill ||| |
3333
| HttpSkill ||| |
34-
| MathSkill || | |
34+
| MathSkill || | |
3535
| TextSkill ||| |
3636
| TimeSkill ||| |
3737

python/semantic_kernel/core_skills/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from semantic_kernel.core_skills.text_memory_skill import TextMemorySkill
66
from semantic_kernel.core_skills.text_skill import TextSkill
77
from semantic_kernel.core_skills.time_skill import TimeSkill
8+
from semantic_kernel.core_skills.math_skill import MathSkill
89

910
__all__ = [
1011
"TextMemorySkill",
@@ -13,4 +14,5 @@
1314
"TimeSkill",
1415
"HttpSkill",
1516
"BasicPlanner",
17+
"MathSkill"
1618
]
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
from semantic_kernel.orchestration.sk_context import SKContext
4+
from semantic_kernel.skill_definition import sk_function, sk_function_context_parameter
5+
6+
7+
class MathSkill:
8+
"""
9+
Description: MathSkill provides a set of functions to make Math calculations.
10+
11+
Usage:
12+
kernel.import_skill("math", new MathSkill())
13+
14+
Examples:
15+
{{math.Add}} => Returns the sum of initial_value_text and Amount (provided in the SKContext)
16+
"""
17+
18+
@sk_function(
19+
description="Adds value to a value",
20+
name="Add",
21+
input_description="The value to add")
22+
@sk_function_context_parameter(
23+
name="Amount",
24+
description="Amount to add",
25+
)
26+
def add(self,
27+
initial_value_text: str,
28+
context: SKContext) -> str:
29+
"""
30+
Returns the Addition result of initial and amount values provided.
31+
32+
:param initial_value_text: Initial value as string to add the specified amount
33+
:param context: Contains the context to get the numbers from
34+
:return: The resulting sum as a string
35+
"""
36+
return MathSkill.add_or_subtract(initial_value_text, context, add=True)
37+
38+
@sk_function(
39+
description="Subtracts value to a value",
40+
name="Subtract",
41+
input_description="The value to subtract")
42+
@sk_function_context_parameter(
43+
name="Amount",
44+
description="Amount to subtract",
45+
)
46+
def subtract(self,
47+
initial_value_text: str,
48+
context: SKContext) -> str:
49+
"""
50+
Returns the difference of numbers provided.
51+
52+
:param initial_value_text: Initial value as string to subtract the specified amount
53+
:param context: Contains the context to get the numbers from
54+
:return: The resulting subtraction as a string
55+
"""
56+
return MathSkill.add_or_subtract(initial_value_text, context, add=False)
57+
58+
@staticmethod
59+
def add_or_subtract(
60+
initial_value_text: str,
61+
context: SKContext,
62+
add: bool) -> str:
63+
"""
64+
Helper function to perform addition or subtraction based on the add flag.
65+
66+
:param initial_value_text: Initial value as string to add or subtract the specified amount
67+
:param context: Contains the context to get the numbers from
68+
:param add: If True, performs addition, otherwise performs subtraction
69+
:return: The resulting sum or subtraction as a string
70+
"""
71+
try:
72+
initial_value = int(initial_value_text)
73+
except ValueError:
74+
raise ValueError(
75+
f"Initial value provided is not in numeric format: {initial_value_text}")
76+
77+
context_amount = context["Amount"]
78+
if context_amount is not None:
79+
try:
80+
amount = int(context_amount)
81+
except ValueError:
82+
raise ValueError(
83+
f"Context amount provided is not in numeric format: {context_amount}")
84+
85+
result = initial_value + amount if add else initial_value - amount
86+
return str(result)
87+
else:
88+
raise ValueError("Context amount should not be None.")
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import pytest
4+
5+
from semantic_kernel import Kernel
6+
from semantic_kernel.core_skills import MathSkill
7+
from semantic_kernel.orchestration.context_variables import ContextVariables
8+
9+
10+
def test_can_be_instantiated():
11+
skill = MathSkill()
12+
assert skill is not None
13+
14+
15+
def test_can_be_imported():
16+
kernel = Kernel()
17+
assert kernel.import_skill(MathSkill(), "math")
18+
assert kernel.skills.has_native_function("math", "add")
19+
assert kernel.skills.has_native_function("math", "subtract")
20+
21+
22+
@pytest.mark.parametrize("initial_Value, amount, expectedResult", [
23+
("10", "10", "20"),
24+
("0", "10", "10"),
25+
("0", "-10", "-10"),
26+
("10", "0", "10"),
27+
("-1", "10", "9"),
28+
("-10", "10", "0"),
29+
("-192", "13", "-179"),
30+
("-192", "-13", "-205")
31+
])
32+
def test_add_when_valid_parameters_should_succeed(initial_Value, amount, expectedResult):
33+
# Arrange
34+
context = ContextVariables()
35+
context["Amount"] = amount
36+
skill = MathSkill()
37+
38+
# Act
39+
result = skill.add(initial_Value, context)
40+
41+
# Assert
42+
assert result == expectedResult
43+
44+
45+
@pytest.mark.parametrize("initial_Value, amount, expectedResult", [
46+
("10", "10", "0"),
47+
("0", "10", "-10"),
48+
("10", "0", "10"),
49+
("100", "-10", "110"),
50+
("100", "102", "-2"),
51+
("-1", "10", "-11"),
52+
("-10", "10", "-20"),
53+
("-192", "13", "-205")
54+
])
55+
def test_subtract_when_valid_parameters_should_succeed(initial_Value, amount, expectedResult):
56+
# Arrange
57+
context = ContextVariables()
58+
context["Amount"] = amount
59+
skill = MathSkill()
60+
61+
# Act
62+
result = skill.subtract(initial_Value, context)
63+
64+
# Assert
65+
assert result == expectedResult
66+
67+
68+
@pytest.mark.parametrize("initial_Value", [
69+
"$0",
70+
"one hundred",
71+
"20..,,2,1",
72+
".2,2.1",
73+
"0.1.0",
74+
"00-099",
75+
"¹²¹",
76+
"2²",
77+
"zero",
78+
"-100 units",
79+
"1 banana"
80+
])
81+
def test_add_when_invalid_initial_value_should_throw(initial_Value):
82+
# Arrange
83+
context = ContextVariables()
84+
context["Amount"] = "1"
85+
skill = MathSkill()
86+
87+
# Act
88+
with pytest.raises(ValueError) as exception:
89+
result = skill.add(initial_Value, context)
90+
91+
# Assert
92+
assert str(
93+
exception.value) == f"Initial value provided is not in numeric format: {initial_Value}"
94+
assert exception.type == ValueError
95+
96+
97+
@pytest.mark.parametrize('amount', [
98+
"$0",
99+
"one hundred",
100+
"20..,,2,1",
101+
".2,2.1",
102+
"0.1.0",
103+
"00-099",
104+
"¹²¹",
105+
"2²",
106+
"zero",
107+
"-100 units",
108+
"1 banana",
109+
])
110+
def test_add_when_invalid_amount_should_throw(amount):
111+
# Arrange
112+
context = ContextVariables()
113+
context["Amount"] = amount
114+
skill = MathSkill()
115+
116+
# Act / Assert
117+
with pytest.raises(ValueError) as exception:
118+
result = skill.add("1", context)
119+
120+
assert str(
121+
exception.value) == f"Context amount provided is not in numeric format: {amount}"
122+
assert exception.type == ValueError
123+
124+
125+
@pytest.mark.parametrize("initial_value", [
126+
"$0",
127+
"one hundred",
128+
"20..,,2,1",
129+
".2,2.1",
130+
"0.1.0",
131+
"00-099",
132+
"¹²¹",
133+
"2²",
134+
"zero",
135+
"-100 units",
136+
"1 banana",
137+
])
138+
def test_subtract_when_invalid_initial_value_should_throw(initial_value):
139+
# Arrange
140+
context = ContextVariables()
141+
context["Amount"] = "1"
142+
skill = MathSkill()
143+
144+
# Act / Assert
145+
with pytest.raises(ValueError) as exception:
146+
result = skill.subtract(initial_value, context)
147+
148+
# Assert
149+
assert str(
150+
exception.value) == f"Initial value provided is not in numeric format: {initial_value}"
151+
assert exception.type == ValueError
152+
153+
154+
@pytest.mark.parametrize("amount", [
155+
"$0",
156+
"one hundred",
157+
"20..,,2,1",
158+
".2,2.1",
159+
"0.1.0",
160+
"00-099",
161+
"¹²¹",
162+
"2²",
163+
"zero",
164+
"-100 units",
165+
"1 banana",
166+
])
167+
def test_subtract_when_invalid_amount_should_throw(amount):
168+
# Arrange
169+
context = ContextVariables()
170+
context["Amount"] = amount
171+
skill = MathSkill()
172+
173+
# Act / Assert
174+
with pytest.raises(ValueError) as exception:
175+
result = skill.subtract("1", context)
176+
177+
# Assert
178+
assert str(
179+
exception.value) == f"Context amount provided is not in numeric format: {amount}"
180+
assert exception.type == ValueError

0 commit comments

Comments
 (0)