Skip to content

Commit

Permalink
Python: Adgudime/python/serialization/skill definition (#2329)
Browse files Browse the repository at this point in the history
This is part 4 of the Serialization work for semantic kernel.

Corresponding issue:
#131

### Motivation and Context

While there are no user facing changes, the ReadOnlySkillCollection has
been modified to remove
circular dependencies that were causing issues with serialization.

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [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: Abby Harrison <[email protected]>
Co-authored-by: Abby Harrison <[email protected]>
  • Loading branch information
3 people authored Aug 4, 2023
1 parent e8342c6 commit 77eeff2
Show file tree
Hide file tree
Showing 16 changed files with 357 additions and 276 deletions.
10 changes: 7 additions & 3 deletions python/semantic_kernel/orchestration/sk_function_base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Copyright (c) Microsoft. All rights reserved.

from abc import ABC, abstractmethod
from abc import abstractmethod
from logging import Logger
from typing import TYPE_CHECKING, Callable, Optional
from typing import TYPE_CHECKING, Callable, Optional, TypeVar

from semantic_kernel.connectors.ai.complete_request_settings import (
CompleteRequestSettings,
Expand All @@ -13,6 +13,7 @@
from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.sk_pydantic import PydanticField
from semantic_kernel.skill_definition.function_view import FunctionView

if TYPE_CHECKING:
Expand All @@ -21,7 +22,10 @@
)


class SKFunctionBase(ABC):
SKFunctionT = TypeVar("SKFunctionT", bound="SKFunctionBase")


class SKFunctionBase(PydanticField):
FUNCTION_PARAM_NAME_REGEX = r"^[0-9A-Za-z_]*$"
FUNCTION_NAME_REGEX = r"^[0-9A-Za-z_]*$"
SKILL_NAME_REGEX = r"^[0-9A-Za-z_]*$"
Expand Down
12 changes: 7 additions & 5 deletions python/semantic_kernel/planning/action_planner/action_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,18 @@ async def create_plan_async(self, goal: str) -> Plan:
skill, fun = generated_plan["plan"]["function"].split(".")
function_ref = self._context.skills.get_function(skill, fun)
self._logger.info(
f"ActionPlanner has picked {skill}.{fun}. Reference to this function found in context: {function_ref}"
f"ActionPlanner has picked {skill}.{fun}. Reference to this function"
f" found in context: {function_ref}"
)
plan = Plan(description=goal, function=function_ref)
else:
function_ref = self._context.skills.get_function(
generated_plan["plan"]["function"]
)
self._logger.info(
f"ActionPlanner has picked {generated_plan['plan']['function']}. \
Reference to this function found in context: {function_ref}"
f"ActionPlanner has picked {generated_plan['plan']['function']}. "
" Reference to this function found in context:"
f" {function_ref}"
)
plan = Plan(description=goal, function=function_ref)

Expand Down Expand Up @@ -253,13 +255,13 @@ def list_of_functions(self, goal: str, context: SKContext) -> str:
functions_view = context.skills.get_functions_view()
available_functions = []

for functions in functions_view._native_functions.values():
for functions in functions_view.native_functions.values():
for func in functions:
if func.skill_name.lower() == self._skill_name.lower():
continue
available_functions.append(self._create_function_string(func))

for functions in functions_view._semantic_functions.values():
for functions in functions_view.semantic_functions.values():
for func in functions:
if func.skill_name.lower() == self._skill_name.lower():
continue
Expand Down
4 changes: 2 additions & 2 deletions python/semantic_kernel/planning/basic_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ def _create_available_functions_string(self, kernel: Kernel) -> str:
string for the prompt.
"""
# Get a dictionary of skill names to all native and semantic functions
native_functions = kernel.skills.get_functions_view()._native_functions
semantic_functions = kernel.skills.get_functions_view()._semantic_functions
native_functions = kernel.skills.get_functions_view().native_functions
semantic_functions = kernel.skills.get_functions_view().semantic_functions
native_functions.update(semantic_functions)

# Create a mapping between all function names and their descriptions
Expand Down
2 changes: 1 addition & 1 deletion python/semantic_kernel/planning/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ def get_next_step_variables(
# - 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:
for param in function_params.parameters:
if param.name.lower() == variables._main_key.lower():
continue

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ def to_manual_string(function: FunctionView):
function
)

return f"{qualified_name}:\n description: {function.description}\n inputs:\n {inputs}"
return (
f"{qualified_name}:\n description: {function.description}\n inputs:\n "
f" {inputs}"
)

@staticmethod
def to_fully_qualified_name(function: FunctionView):
Expand All @@ -45,7 +48,10 @@ def to_embedding_string(function: FunctionView):
for parameter in function.parameters
]
)
return f"{function.name}:\n description: {function.description}\n inputs:\n{inputs}"
return (
f"{function.name}:\n description: {function.description}\n "
f" inputs:\n{inputs}"
)


class SequentialPlannerSKContextExtension:
Expand Down Expand Up @@ -97,8 +103,8 @@ async def get_available_functions_async(
functions_view = context.skills.get_functions_view()

available_functions: List[FunctionView] = [
*functions_view._semantic_functions.values(),
*functions_view._native_functions.values(),
*functions_view.semantic_functions.values(),
*functions_view.native_functions.values(),
]
available_functions = itertools.chain.from_iterable(available_functions)

Expand Down
80 changes: 16 additions & 64 deletions python/semantic_kernel/skill_definition/function_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

from typing import List

from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.skill_definition.parameter_view import ParameterView
from semantic_kernel.utils.validation import validate_function_name


class FunctionView:
_name: str
_skill_name: str
_description: str
_is_semantic: bool
_is_asynchronous: bool
_parameters: List[ParameterView]
class FunctionView(SKBaseModel):
name: str
skill_name: str
description: str
is_semantic: bool
parameters: List[ParameterView]
is_asynchronous: bool = True

def __init__(
self,
Expand All @@ -24,60 +25,11 @@ def __init__(
is_asynchronous: bool = True,
) -> None:
validate_function_name(name)

self._name = name
self._skill_name = skill_name
self._description = description
self._parameters = parameters
self._is_semantic = is_semantic
self._is_asynchronous = is_asynchronous

@property
def name(self) -> str:
return self._name

@property
def skill_name(self) -> str:
return self._skill_name

@property
def description(self) -> str:
return self._description

@property
def parameters(self) -> List[ParameterView]:
return self._parameters

@property
def is_semantic(self) -> bool:
return self._is_semantic

@property
def is_asynchronous(self) -> bool:
return self._is_asynchronous

@name.setter
def name(self, value: str) -> None:
validate_function_name(value)

self._name = value

@skill_name.setter
def skill_name(self, value: str) -> None:
self._skill_name = value

@description.setter
def description(self, value: str) -> None:
self._description = value

@parameters.setter
def parameters(self, value: List[ParameterView]) -> None:
self._parameters = value

@is_semantic.setter
def is_semantic(self, value: bool) -> None:
self._is_semantic = value

@is_asynchronous.setter
def is_asynchronous(self, value: bool) -> None:
self._is_asynchronous = value
super().__init__(
name=name,
skill_name=skill_name,
description=description,
parameters=parameters,
is_semantic=is_semantic,
is_asynchronous=is_asynchronous,
)
45 changes: 24 additions & 21 deletions python/semantic_kernel/skill_definition/functions_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,61 @@

from typing import Dict, List

import pydantic as pdt

from semantic_kernel.kernel_exception import KernelException
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.skill_definition.function_view import FunctionView


class FunctionsView:
_semantic_functions: Dict[str, List[FunctionView]]
_native_functions: Dict[str, List[FunctionView]]

def __init__(self) -> None:
self._semantic_functions = {}
self._native_functions = {}
class FunctionsView(SKBaseModel):
semantic_functions: Dict[str, List[FunctionView]] = pdt.Field(default_factory=dict)
native_functions: Dict[str, List[FunctionView]] = pdt.Field(default_factory=dict)

def add_function(self, view: FunctionView) -> "FunctionsView":
if view.is_semantic:
if view.skill_name not in self._semantic_functions:
self._semantic_functions[view.skill_name] = []
self._semantic_functions[view.skill_name].append(view)
if view.skill_name not in self.semantic_functions:
self.semantic_functions[view.skill_name] = []
self.semantic_functions[view.skill_name].append(view)
else:
if view.skill_name not in self._native_functions:
self._native_functions[view.skill_name] = []
self._native_functions[view.skill_name].append(view)
if view.skill_name not in self.native_functions:
self.native_functions[view.skill_name] = []
self.native_functions[view.skill_name].append(view)

return self

def is_semantic(self, skill_name: str, function_name: str) -> bool:
as_sf = self._semantic_functions.get(skill_name, [])
as_sf = self.semantic_functions.get(skill_name, [])
as_sf = any(f.name == function_name for f in as_sf)

as_nf = self._native_functions.get(skill_name, [])
as_nf = self.native_functions.get(skill_name, [])
as_nf = any(f.name == function_name for f in as_nf)

if as_sf and as_nf:
raise KernelException(
KernelException.ErrorCodes.AmbiguousImplementation,
f"There are 2 functions with the same name: {function_name}."
f"One is native and the other semantic.",
(
f"There are 2 functions with the same name: {function_name}."
"One is native and the other semantic."
),
)

return as_sf

def is_native(self, skill_name: str, function_name: str) -> bool:
as_sf = self._semantic_functions.get(skill_name, [])
as_sf = self.semantic_functions.get(skill_name, [])
as_sf = any(f.name == function_name for f in as_sf)

as_nf = self._native_functions.get(skill_name, [])
as_nf = self.native_functions.get(skill_name, [])
as_nf = any(f.name == function_name for f in as_nf)

if as_sf and as_nf:
raise KernelException(
KernelException.ErrorCodes.AmbiguousImplementation,
f"There are 2 functions with the same name: {function_name}."
f"One is native and the other semantic.",
(
f"There are 2 functions with the same name: {function_name}."
"One is native and the other semantic."
),
)

return as_nf
41 changes: 8 additions & 33 deletions python/semantic_kernel/skill_definition/parameter_view.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.

from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.utils.validation import validate_function_param_name


class ParameterView:
_name: str
_description: str
_default_value: str
class ParameterView(SKBaseModel):
name: str
description: str
default_value: str

def __init__(self, name: str, description: str, default_value: str) -> None:
validate_function_param_name(name)

self._name = name
self._description = description
self._default_value = default_value

@property
def name(self) -> str:
return self._name

@property
def description(self) -> str:
return self._description

@property
def default_value(self) -> str:
return self._default_value

@name.setter
def name(self, value: str) -> None:
validate_function_param_name(value)
self._name = value

@description.setter
def description(self, value: str) -> None:
self._description = value

@default_value.setter
def default_value(self, value: str) -> None:
self._default_value = value
super().__init__(
name=name, description=description, default_value=default_value
)
Loading

0 comments on commit 77eeff2

Please sign in to comment.