mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 11:19:56 +00:00
### Motivation and Context This replaces passing Logger instances as arguments around with the preferred Python way of having each module define a module-wide `logger` instance at the top of the module. Fixes #3711 ### Description Removes any `log` argument and argument documentation. Replaces usage of those `log` arguments with the module-wide `logger` instance. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Eduard van Valkenburg <[email protected]>
385 lines
12 KiB
Python
385 lines
12 KiB
Python
from unittest.mock import Mock
|
|
|
|
from pytest import mark, raises
|
|
|
|
from semantic_kernel.memory.null_memory import NullMemory
|
|
from semantic_kernel.orchestration.context_variables import ContextVariables
|
|
from semantic_kernel.orchestration.delegate_types import DelegateTypes
|
|
from semantic_kernel.orchestration.sk_context import SKContext
|
|
from semantic_kernel.orchestration.sk_function import SKFunction
|
|
from semantic_kernel.skill_definition.read_only_skill_collection_base import (
|
|
ReadOnlySkillCollectionBase,
|
|
)
|
|
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
|
|
from semantic_kernel.template_engine.blocks.code_block import CodeBlock
|
|
from semantic_kernel.template_engine.blocks.function_id_block import FunctionIdBlock
|
|
from semantic_kernel.template_engine.blocks.val_block import ValBlock
|
|
from semantic_kernel.template_engine.blocks.var_block import VarBlock
|
|
|
|
|
|
class TestCodeBlock:
|
|
def setup_method(self):
|
|
self.skills = Mock(spec=ReadOnlySkillCollectionBase)
|
|
|
|
@mark.asyncio
|
|
async def test_it_throws_if_a_function_doesnt_exist(self):
|
|
context = SKContext.model_construct(
|
|
variables=ContextVariables(),
|
|
memory=NullMemory(),
|
|
skill_collection=self.skills,
|
|
)
|
|
# Make it so our self.skills mock's `has_function` method returns False
|
|
self.skills.has_function.return_value = False
|
|
target = CodeBlock(
|
|
content="functionName",
|
|
)
|
|
|
|
with raises(ValueError):
|
|
await target.render_code_async(context)
|
|
|
|
@mark.asyncio
|
|
async def test_it_throws_if_a_function_call_throws(self):
|
|
context = SKContext.model_construct(
|
|
variables=ContextVariables(),
|
|
memory=NullMemory(),
|
|
skill_collection=self.skills,
|
|
)
|
|
|
|
def invoke(_):
|
|
raise Exception("error")
|
|
|
|
function = SKFunction(
|
|
delegate_type=DelegateTypes.InSKContext,
|
|
delegate_function=invoke,
|
|
skill_name="",
|
|
function_name="funcName",
|
|
description="",
|
|
parameters=[],
|
|
is_semantic=False,
|
|
)
|
|
|
|
self.skills.has_function.return_value = True
|
|
self.skills.get_function.return_value = function
|
|
|
|
target = CodeBlock(
|
|
content="functionName",
|
|
)
|
|
|
|
with raises(ValueError):
|
|
await target.render_code_async(context)
|
|
|
|
def test_it_has_the_correct_type(self):
|
|
assert (
|
|
CodeBlock(
|
|
content="",
|
|
).type
|
|
== BlockTypes.CODE
|
|
)
|
|
|
|
def test_it_trims_spaces(self):
|
|
assert (
|
|
CodeBlock(
|
|
content=" aa ",
|
|
).content
|
|
== "aa"
|
|
)
|
|
|
|
def test_it_checks_validity_of_internal_blocks(self):
|
|
valid_block1 = FunctionIdBlock(content="x")
|
|
|
|
valid_block2 = ValBlock(content="''")
|
|
invalid_block = VarBlock(content="!notvalid")
|
|
|
|
code_block1 = CodeBlock(
|
|
tokens=[valid_block1, valid_block2],
|
|
content="",
|
|
)
|
|
code_block2 = CodeBlock(
|
|
tokens=[valid_block1, invalid_block],
|
|
content="",
|
|
)
|
|
|
|
is_valid1, _ = code_block1.is_valid()
|
|
is_valid2, _ = code_block2.is_valid()
|
|
|
|
assert is_valid1
|
|
assert not is_valid2
|
|
|
|
def test_it_requires_a_valid_function_call(self):
|
|
func_id = FunctionIdBlock(content="funcName")
|
|
|
|
val_block = ValBlock(content="'value'")
|
|
var_block = VarBlock(content="$var")
|
|
|
|
code_block1 = CodeBlock(
|
|
tokens=[func_id, val_block],
|
|
content="",
|
|
)
|
|
code_block2 = CodeBlock(
|
|
tokens=[func_id, var_block],
|
|
content="",
|
|
)
|
|
code_block3 = CodeBlock(
|
|
tokens=[func_id, func_id],
|
|
content="",
|
|
)
|
|
code_block4 = CodeBlock(
|
|
tokens=[func_id, var_block, var_block],
|
|
content="",
|
|
)
|
|
|
|
is_valid1, _ = code_block1.is_valid()
|
|
is_valid2, _ = code_block2.is_valid()
|
|
|
|
is_valid3, _ = code_block3.is_valid()
|
|
is_valid4, _ = code_block4.is_valid()
|
|
|
|
assert is_valid1
|
|
assert is_valid2
|
|
|
|
assert not is_valid3
|
|
assert not is_valid4
|
|
|
|
@mark.asyncio
|
|
async def test_it_renders_code_block_consisting_of_just_a_var_block1(self):
|
|
variables = ContextVariables()
|
|
variables["varName"] = "foo"
|
|
|
|
context = SKContext.model_construct(
|
|
variables=variables,
|
|
memory=NullMemory(),
|
|
skill_collection=None,
|
|
)
|
|
|
|
code_block = CodeBlock(
|
|
content="$varName",
|
|
)
|
|
result = await code_block.render_code_async(context)
|
|
|
|
assert result == "foo"
|
|
|
|
@mark.asyncio
|
|
async def test_it_renders_code_block_consisting_of_just_a_var_block2(self):
|
|
variables = ContextVariables()
|
|
variables["varName"] = "bar"
|
|
|
|
context = SKContext.model_construct(
|
|
variables=variables,
|
|
memory=NullMemory(),
|
|
skill_collection=None,
|
|
)
|
|
|
|
code_block = CodeBlock(
|
|
tokens=[VarBlock(content="$varName")],
|
|
content="",
|
|
)
|
|
result = await code_block.render_code_async(context)
|
|
|
|
assert result == "bar"
|
|
|
|
@mark.asyncio
|
|
async def test_it_renders_code_block_consisting_of_just_a_val_block1(self):
|
|
context = SKContext.model_construct(
|
|
variables=ContextVariables(),
|
|
memory=NullMemory(),
|
|
skill_collection=None,
|
|
)
|
|
|
|
code_block = CodeBlock(
|
|
content="'ciao'",
|
|
)
|
|
result = await code_block.render_code_async(context)
|
|
|
|
assert result == "ciao"
|
|
|
|
@mark.asyncio
|
|
async def test_it_renders_code_block_consisting_of_just_a_val_block2(self):
|
|
context = SKContext.model_construct(
|
|
variables=ContextVariables(),
|
|
memory=NullMemory(),
|
|
skill_collection=None,
|
|
)
|
|
|
|
code_block = CodeBlock(
|
|
tokens=[ValBlock(content="'arrivederci'")],
|
|
content="",
|
|
)
|
|
result = await code_block.render_code_async(context)
|
|
|
|
assert result == "arrivederci"
|
|
|
|
@mark.asyncio
|
|
async def test_it_invokes_function_cloning_all_variables(self):
|
|
# Set up initial context variables
|
|
variables = ContextVariables()
|
|
variables["input"] = "zero"
|
|
variables["var1"] = "uno"
|
|
variables["var2"] = "due"
|
|
|
|
# Create a context with the variables, memory, and skill collection
|
|
context = SKContext.model_construct(
|
|
variables=variables,
|
|
memory=NullMemory(),
|
|
skill_collection=self.skills,
|
|
)
|
|
|
|
# Create a FunctionIdBlock with the function name
|
|
func_id = FunctionIdBlock(content="funcName")
|
|
|
|
# Set up a canary dictionary to track changes in the context variables
|
|
canary = {"input": "", "var1": "", "var2": ""}
|
|
|
|
# Define the function to be invoked, which modifies the canary
|
|
# and context variables
|
|
def invoke(ctx):
|
|
nonlocal canary
|
|
canary["input"] = ctx["input"]
|
|
canary["var1"] = ctx["var1"]
|
|
canary["var2"] = ctx["var2"]
|
|
|
|
ctx["input"] = "overridden"
|
|
ctx["var1"] = "overridden"
|
|
ctx["var2"] = "overridden"
|
|
|
|
# Create an SKFunction with the invoke function as its delegate
|
|
function = SKFunction(
|
|
delegate_type=DelegateTypes.InSKContext,
|
|
delegate_function=invoke,
|
|
skill_name="",
|
|
function_name="funcName",
|
|
description="",
|
|
parameters=[],
|
|
is_semantic=False,
|
|
)
|
|
|
|
# Mock the skill collection's function retrieval
|
|
self.skills.has_function.return_value = True
|
|
self.skills.get_function.return_value = function
|
|
|
|
# Create a CodeBlock with the FunctionIdBlock and render it with the context
|
|
code_block = CodeBlock(
|
|
tokens=[func_id],
|
|
content="",
|
|
)
|
|
await code_block.render_code_async(context)
|
|
|
|
# Check that the canary values match the original context variables
|
|
assert canary["input"] == "zero"
|
|
assert canary["var1"] == "uno"
|
|
assert canary["var2"] == "due"
|
|
|
|
# Check that the original context variables were not modified
|
|
assert variables["input"] == "zero"
|
|
assert variables["var1"] == "uno"
|
|
assert variables["var2"] == "due"
|
|
|
|
@mark.asyncio
|
|
async def test_it_invokes_function_with_custom_variable(self):
|
|
# Define custom variable name and value
|
|
VAR_NAME = "varName"
|
|
VAR_VALUE = "varValue"
|
|
|
|
# Set up initial context variables
|
|
variables = ContextVariables()
|
|
variables[VAR_NAME] = VAR_VALUE
|
|
|
|
# Create a context with the variables, memory, and skill collection
|
|
context = SKContext.model_construct(
|
|
variables=variables,
|
|
memory=NullMemory(),
|
|
skill_collection=self.skills,
|
|
)
|
|
|
|
# Create a FunctionIdBlock with the function name and a
|
|
# VarBlock with the custom variable
|
|
func_id = FunctionIdBlock(content="funcName")
|
|
var_block = VarBlock(content=f"${VAR_NAME}")
|
|
|
|
# Set up a canary variable to track changes in the context input
|
|
canary = ""
|
|
|
|
# Define the function to be invoked, which modifies the canary variable
|
|
def invoke(ctx):
|
|
nonlocal canary
|
|
canary = ctx["input"]
|
|
|
|
# Create an SKFunction with the invoke function as its delegate
|
|
function = SKFunction(
|
|
delegate_type=DelegateTypes.InSKContext,
|
|
delegate_function=invoke,
|
|
skill_name="",
|
|
function_name="funcName",
|
|
description="",
|
|
parameters=[],
|
|
is_semantic=False,
|
|
)
|
|
|
|
# Mock the skill collection's function retrieval
|
|
self.skills.has_function.return_value = True
|
|
self.skills.get_function.return_value = function
|
|
|
|
# Create a CodeBlock with the FunctionIdBlock and VarBlock,
|
|
# and render it with the context
|
|
code_block = CodeBlock(
|
|
tokens=[func_id, var_block],
|
|
content="",
|
|
)
|
|
result = await code_block.render_code_async(context)
|
|
|
|
# Check that the result matches the custom variable value
|
|
assert result == VAR_VALUE
|
|
# Check that the canary value matches the custom variable value
|
|
assert canary == VAR_VALUE
|
|
|
|
@mark.asyncio
|
|
async def test_it_invokes_function_with_custom_value(self):
|
|
# Define a value to be used in the test
|
|
VALUE = "value"
|
|
|
|
# Create a context with empty variables, memory, and skill collection
|
|
context = SKContext.model_construct(
|
|
variables=ContextVariables(),
|
|
memory=NullMemory(),
|
|
skill_collection=self.skills,
|
|
)
|
|
|
|
# Create a FunctionIdBlock with the function name and a ValBlock with the value
|
|
func_id = FunctionIdBlock(content="funcName")
|
|
val_block = ValBlock(content=f"'{VALUE}'")
|
|
|
|
# Set up a canary variable to track changes in the context input
|
|
canary = ""
|
|
|
|
# Define the function to be invoked, which modifies the canary variable
|
|
def invoke(ctx):
|
|
nonlocal canary
|
|
canary = ctx["input"]
|
|
|
|
# Create an SKFunction with the invoke function as its delegate
|
|
function = SKFunction(
|
|
delegate_type=DelegateTypes.InSKContext,
|
|
delegate_function=invoke,
|
|
skill_name="",
|
|
function_name="funcName",
|
|
description="",
|
|
parameters=[],
|
|
is_semantic=False,
|
|
)
|
|
|
|
# Mock the skill collection's function retrieval
|
|
self.skills.has_function.return_value = True
|
|
self.skills.get_function.return_value = function
|
|
|
|
# Create a CodeBlock with the FunctionIdBlock and ValBlock,
|
|
# and render it with the context
|
|
code_block = CodeBlock(
|
|
tokens=[func_id, val_block],
|
|
content="",
|
|
)
|
|
result = await code_block.render_code_async(context)
|
|
|
|
# Check that the result matches the value
|
|
assert result == VALUE
|
|
# Check that the canary value matches the value
|
|
assert canary == VALUE
|