Skip to content

Commit

Permalink
[Python] Port MathSkill from C# to python3 (microsoft#841)
Browse files Browse the repository at this point in the history
### 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]>
  • Loading branch information
4 people authored May 11, 2023
1 parent 0b1e286 commit 7cd0fea
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 1 deletion.
2 changes: 1 addition & 1 deletion FEATURE_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
| ConversationSummarySkill ||| |
| FileIOSkill ||| |
| HttpSkill ||| |
| MathSkill || | |
| MathSkill || | |
| TextSkill ||| |
| TimeSkill ||| |

Expand Down
2 changes: 2 additions & 0 deletions python/semantic_kernel/core_skills/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from semantic_kernel.core_skills.text_memory_skill import TextMemorySkill
from semantic_kernel.core_skills.text_skill import TextSkill
from semantic_kernel.core_skills.time_skill import TimeSkill
from semantic_kernel.core_skills.math_skill import MathSkill

__all__ = [
"TextMemorySkill",
Expand All @@ -13,4 +14,5 @@
"TimeSkill",
"HttpSkill",
"BasicPlanner",
"MathSkill"
]
88 changes: 88 additions & 0 deletions python/semantic_kernel/core_skills/math_skill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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


class MathSkill:
"""
Description: MathSkill provides a set of functions to make Math calculations.
Usage:
kernel.import_skill("math", new MathSkill())
Examples:
{{math.Add}} => Returns the sum of initial_value_text and Amount (provided in the SKContext)
"""

@sk_function(
description="Adds value to a value",
name="Add",
input_description="The value to add")
@sk_function_context_parameter(
name="Amount",
description="Amount to add",
)
def add(self,
initial_value_text: str,
context: SKContext) -> str:
"""
Returns the Addition result of initial and amount values provided.
:param initial_value_text: Initial value as string to add the specified amount
:param context: Contains the context to get the numbers from
:return: The resulting sum as a string
"""
return MathSkill.add_or_subtract(initial_value_text, context, add=True)

@sk_function(
description="Subtracts value to a value",
name="Subtract",
input_description="The value to subtract")
@sk_function_context_parameter(
name="Amount",
description="Amount to subtract",
)
def subtract(self,
initial_value_text: str,
context: SKContext) -> str:
"""
Returns the difference of numbers provided.
:param initial_value_text: Initial value as string to subtract the specified amount
:param context: Contains the context to get the numbers from
:return: The resulting subtraction as a string
"""
return MathSkill.add_or_subtract(initial_value_text, context, add=False)

@staticmethod
def add_or_subtract(
initial_value_text: str,
context: SKContext,
add: bool) -> str:
"""
Helper function to perform addition or subtraction based on the add flag.
:param initial_value_text: Initial value as string to add or subtract the specified amount
:param context: Contains the context to get the numbers from
:param add: If True, performs addition, otherwise performs subtraction
:return: The resulting sum or subtraction as a string
"""
try:
initial_value = int(initial_value_text)
except ValueError:
raise ValueError(
f"Initial value provided is not in numeric format: {initial_value_text}")

context_amount = context["Amount"]
if context_amount is not None:
try:
amount = int(context_amount)
except ValueError:
raise ValueError(
f"Context amount provided is not in numeric format: {context_amount}")

result = initial_value + amount if add else initial_value - amount
return str(result)
else:
raise ValueError("Context amount should not be None.")
180 changes: 180 additions & 0 deletions python/tests/unit/core_skills/test_math_skill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Copyright (c) Microsoft. All rights reserved.

import pytest

from semantic_kernel import Kernel
from semantic_kernel.core_skills import MathSkill
from semantic_kernel.orchestration.context_variables import ContextVariables


def test_can_be_instantiated():
skill = MathSkill()
assert skill is not None


def test_can_be_imported():
kernel = Kernel()
assert kernel.import_skill(MathSkill(), "math")
assert kernel.skills.has_native_function("math", "add")
assert kernel.skills.has_native_function("math", "subtract")


@pytest.mark.parametrize("initial_Value, amount, expectedResult", [
("10", "10", "20"),
("0", "10", "10"),
("0", "-10", "-10"),
("10", "0", "10"),
("-1", "10", "9"),
("-10", "10", "0"),
("-192", "13", "-179"),
("-192", "-13", "-205")
])
def test_add_when_valid_parameters_should_succeed(initial_Value, amount, expectedResult):
# Arrange
context = ContextVariables()
context["Amount"] = amount
skill = MathSkill()

# Act
result = skill.add(initial_Value, context)

# Assert
assert result == expectedResult


@pytest.mark.parametrize("initial_Value, amount, expectedResult", [
("10", "10", "0"),
("0", "10", "-10"),
("10", "0", "10"),
("100", "-10", "110"),
("100", "102", "-2"),
("-1", "10", "-11"),
("-10", "10", "-20"),
("-192", "13", "-205")
])
def test_subtract_when_valid_parameters_should_succeed(initial_Value, amount, expectedResult):
# Arrange
context = ContextVariables()
context["Amount"] = amount
skill = MathSkill()

# Act
result = skill.subtract(initial_Value, context)

# Assert
assert result == expectedResult


@pytest.mark.parametrize("initial_Value", [
"$0",
"one hundred",
"20..,,2,1",
".2,2.1",
"0.1.0",
"00-099",
"¹²¹",
"2²",
"zero",
"-100 units",
"1 banana"
])
def test_add_when_invalid_initial_value_should_throw(initial_Value):
# Arrange
context = ContextVariables()
context["Amount"] = "1"
skill = MathSkill()

# Act
with pytest.raises(ValueError) as exception:
result = skill.add(initial_Value, context)

# Assert
assert str(
exception.value) == f"Initial value provided is not in numeric format: {initial_Value}"
assert exception.type == ValueError


@pytest.mark.parametrize('amount', [
"$0",
"one hundred",
"20..,,2,1",
".2,2.1",
"0.1.0",
"00-099",
"¹²¹",
"2²",
"zero",
"-100 units",
"1 banana",
])
def test_add_when_invalid_amount_should_throw(amount):
# Arrange
context = ContextVariables()
context["Amount"] = amount
skill = MathSkill()

# Act / Assert
with pytest.raises(ValueError) as exception:
result = skill.add("1", context)

assert str(
exception.value) == f"Context amount provided is not in numeric format: {amount}"
assert exception.type == ValueError


@pytest.mark.parametrize("initial_value", [
"$0",
"one hundred",
"20..,,2,1",
".2,2.1",
"0.1.0",
"00-099",
"¹²¹",
"2²",
"zero",
"-100 units",
"1 banana",
])
def test_subtract_when_invalid_initial_value_should_throw(initial_value):
# Arrange
context = ContextVariables()
context["Amount"] = "1"
skill = MathSkill()

# Act / Assert
with pytest.raises(ValueError) as exception:
result = skill.subtract(initial_value, context)

# Assert
assert str(
exception.value) == f"Initial value provided is not in numeric format: {initial_value}"
assert exception.type == ValueError


@pytest.mark.parametrize("amount", [
"$0",
"one hundred",
"20..,,2,1",
".2,2.1",
"0.1.0",
"00-099",
"¹²¹",
"2²",
"zero",
"-100 units",
"1 banana",
])
def test_subtract_when_invalid_amount_should_throw(amount):
# Arrange
context = ContextVariables()
context["Amount"] = amount
skill = MathSkill()

# Act / Assert
with pytest.raises(ValueError) as exception:
result = skill.subtract("1", context)

# Assert
assert str(
exception.value) == f"Context amount provided is not in numeric format: {amount}"
assert exception.type == ValueError

0 comments on commit 7cd0fea

Please sign in to comment.