chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,25 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.core_plugins.conversation_summary_plugin import (
ConversationSummaryPlugin,
)
from semantic_kernel.core_plugins.http_plugin import HttpPlugin
from semantic_kernel.core_plugins.math_plugin import MathPlugin
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin import (
SessionsPythonTool,
)
from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin
from semantic_kernel.core_plugins.text_plugin import TextPlugin
from semantic_kernel.core_plugins.time_plugin import TimePlugin
from semantic_kernel.core_plugins.web_search_engine_plugin import WebSearchEnginePlugin
__all__ = [
"ConversationSummaryPlugin",
"HttpPlugin",
"MathPlugin",
"SessionsPythonTool",
"TextMemoryPlugin",
"TextPlugin",
"TimePlugin",
"WebSearchEnginePlugin",
]
@@ -0,0 +1,93 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from typing import TYPE_CHECKING, Annotated, Any
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
if TYPE_CHECKING:
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
logger = logging.getLogger(__name__)
class ConversationSummaryPlugin:
"""Semantic plugin that enables conversations summarization."""
# The max tokens to process in a single semantic function call.
_max_tokens = 1024
_summarize_conversation_prompt_template = (
"BEGIN CONTENT TO SUMMARIZE:\n{{"
"$input"
"}}\nEND CONTENT TO SUMMARIZE.\nSummarize the conversation in 'CONTENT TO"
" SUMMARIZE', identifying main points of discussion and any"
" conclusions that were reached.\nDo not incorporate other general"
" knowledge.\nSummary is in plain text, in complete sentences, with no markup"
" or tags.\n\nBEGIN SUMMARY:\n"
)
def __init__(
self, prompt_template_config: "PromptTemplateConfig", return_key: str = "summary", **kwargs: Any
) -> None:
"""Initializes a new instance of the ConversationSummaryPlugin.
The template for this plugin is built-in, and will overwrite any template passed in the prompt_template_config.
Args:
prompt_template_config (PromptTemplateConfig): The prompt template configuration.
return_key (str): The key to use for the return value.
**kwargs: Additional keyword arguments, not used only for compatibility.
"""
if "kernel" in kwargs:
logger.warning(
"The kernel parameter is not used in the ConversationSummaryPlugin constructor anymore."
"Please make sure to remove and to add the created plugin to the kernel, by using:"
"kernel.add_plugin(conversation_plugin, 'summarizer')"
)
self.return_key = return_key
prompt_template_config.template = ConversationSummaryPlugin._summarize_conversation_prompt_template
prompt_template_config.template_format = "semantic-kernel"
self._summarizeConversationFunction = KernelFunctionFromPrompt(
plugin_name=ConversationSummaryPlugin.__name__,
function_name="SummarizeConversation",
prompt_template_config=prompt_template_config,
)
@kernel_function(
description="Given a long conversation transcript, summarize the conversation.",
name="SummarizeConversation",
)
async def summarize_conversation(
self,
input: Annotated[str, "A long conversation transcript."],
kernel: Annotated["Kernel", "The kernel instance."],
arguments: Annotated["KernelArguments", "Arguments used by the kernel."],
) -> Annotated[
"KernelArguments", "KernelArguments with the summarized conversation result in key self.return_key."
]:
"""Given a long conversation transcript, summarize the conversation.
Args:
input (str): A long conversation transcript.
kernel (Kernel): The kernel for function execution.
arguments (KernelArguments): Arguments used by the kernel.
Returns:
KernelArguments with the summarized conversation result in key self.return_key.
"""
from semantic_kernel.text import text_chunker
from semantic_kernel.text.function_extension import aggregate_chunked_results
lines = text_chunker._split_text_lines(input, ConversationSummaryPlugin._max_tokens, True)
paragraphs = text_chunker._split_text_paragraph(lines, ConversationSummaryPlugin._max_tokens)
arguments[self.return_key] = await aggregate_chunked_results(
self._summarizeConversationFunction, paragraphs, kernel, arguments
)
return arguments
@@ -0,0 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.core_plugins.crew_ai.crew_ai_enterprise import CrewAIEnterprise
from semantic_kernel.core_plugins.crew_ai.crew_ai_models import (
CrewAIStatusResponse,
)
from semantic_kernel.core_plugins.crew_ai.crew_ai_settings import (
CrewAISettings,
)
__all__ = ["CrewAIEnterprise", "CrewAISettings", "CrewAIStatusResponse"]
@@ -0,0 +1,261 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from typing import Any
import aiohttp
from pydantic import Field, ValidationError
from semantic_kernel.core_plugins.crew_ai.crew_ai_enterprise_client import CrewAIEnterpriseClient
from semantic_kernel.core_plugins.crew_ai.crew_ai_models import CrewAIEnterpriseKickoffState, CrewAIStatusResponse
from semantic_kernel.core_plugins.crew_ai.crew_ai_settings import CrewAISettings
from semantic_kernel.exceptions.function_exceptions import (
FunctionExecutionException,
FunctionResultError,
PluginInitializationError,
)
from semantic_kernel.functions import kernel_function
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
from semantic_kernel.functions.kernel_plugin import KernelPlugin
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.feature_stage_decorator import experimental
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class CrewAIEnterprise(KernelBaseModel):
"""Class to interface with Crew.AI Crews from Semantic Kernel.
This object can be used directly or as a plugin in the Kernel.
"""
client: CrewAIEnterpriseClient
polling_interval: float = Field(default=1.0)
polling_timeout: float = Field(default=30.0)
def __init__(
self,
endpoint: str | None = None,
auth_token: str | None = None,
polling_interval: float | None = 1.0,
polling_timeout: float | None = 30.0,
session: aiohttp.ClientSession | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
):
"""Initialize a new instance of the class. This object can be used directly or as a plugin in the Kernel.
Args:
endpoint (str | None, optional): The API endpoint.
auth_token (str | None, optional): The authentication token.
polling_interval (float, optional): The polling interval in seconds. Defaults to 1.0.
polling_timeout (float, optional): The polling timeout in seconds. Defaults to 30.0.
session (aiohttp.ClientSession | None, optional): The HTTP client session. Defaults to None.
env_file_path (str | None): Use the environment settings file as a
fallback to environment variables. (Optional)
env_file_encoding (str | None): The encoding of the environment settings file. (Optional)
"""
try:
settings = CrewAISettings(
endpoint=endpoint,
auth_token=auth_token,
polling_interval=polling_interval,
polling_timeout=polling_timeout,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise PluginInitializationError("Failed to initialize CrewAI settings.") from ex
client = CrewAIEnterpriseClient(
endpoint=settings.endpoint, auth_token=settings.auth_token.get_secret_value(), session=session
)
super().__init__(
client=client,
polling_interval=settings.polling_interval,
polling_timeout=settings.polling_timeout,
)
async def kickoff(
self,
inputs: dict[str, Any] | None = None,
task_webhook_url: str | None = None,
step_webhook_url: str | None = None,
crew_webhook_url: str | None = None,
) -> str:
"""Kickoff a new Crew AI task.
Args:
inputs (dict[str, Any], optional): The inputs for the task. Defaults to None.
task_webhook_url (str | None, optional): The webhook URL for task updates. Defaults to None.
step_webhook_url (str | None, optional): The webhook URL for step updates. Defaults to None.
crew_webhook_url (str | None, optional): The webhook URL for crew updates. Defaults to None.
Returns:
str: The ID of the kickoff response.
"""
try:
kickoff_response = await self.client.kickoff(inputs, task_webhook_url, step_webhook_url, crew_webhook_url)
logger.info(f"CrewAI Crew kicked off with Id: {kickoff_response.kickoff_id}")
return kickoff_response.kickoff_id
except Exception as ex:
raise FunctionExecutionException("Failed to kickoff CrewAI Crew.") from ex
@kernel_function(description="Get the status of a Crew AI kickoff.")
async def get_crew_kickoff_status(self, kickoff_id: str) -> CrewAIStatusResponse:
"""Get the status of a Crew AI task.
Args:
kickoff_id (str): The ID of the kickoff response.
Returns:
CrewAIStatusResponse: The status response of the task.
"""
try:
status_response = await self.client.get_status(kickoff_id)
logger.info(f"CrewAI Crew status for kickoff Id: {kickoff_id} is {status_response.state}")
return status_response
except Exception as ex:
raise FunctionExecutionException(
f"Failed to get status of CrewAI Crew with kickoff Id: {kickoff_id}."
) from ex
@kernel_function(description="Wait for the completion of a Crew AI kickoff.")
async def wait_for_crew_completion(self, kickoff_id: str) -> str:
"""Wait for the completion of a Crew AI task.
Args:
kickoff_id (str): The ID of the kickoff response.
Returns:
str: The result of the task.
Raises:
FunctionExecutionException: If the task fails or an error occurs while waiting for completion.
"""
status_response: CrewAIStatusResponse | None = None
state: str = CrewAIEnterpriseKickoffState.Pending
async def poll_status():
nonlocal state, status_response
while state not in [
CrewAIEnterpriseKickoffState.Failed,
CrewAIEnterpriseKickoffState.Failure,
CrewAIEnterpriseKickoffState.Success,
CrewAIEnterpriseKickoffState.Not_Found,
]:
logger.debug(
f"Waiting for CrewAI Crew with kickoff Id: {kickoff_id} to complete. Current state: {state}"
)
await asyncio.sleep(self.polling_interval)
try:
status_response = await self.client.get_status(kickoff_id)
state = status_response.state
except Exception as ex:
raise FunctionExecutionException(
f"Failed to wait for completion of CrewAI Crew with kickoff Id: {kickoff_id}."
) from ex
await asyncio.wait_for(poll_status(), timeout=self.polling_timeout)
logger.info(f"CrewAI Crew with kickoff Id: {kickoff_id} completed with status: {state}")
result = status_response.result if status_response is not None and status_response.result is not None else ""
if state in ["Failed", "Failure"]:
raise FunctionResultError(f"CrewAI Crew failed with error: {result}")
return result
def create_kernel_plugin(
self,
name: str,
description: str,
parameters: list[KernelParameterMetadata] | None = None,
task_webhook_url: str | None = None,
step_webhook_url: str | None = None,
crew_webhook_url: str | None = None,
) -> KernelPlugin:
"""Creates a kernel plugin that can be used to invoke the CrewAI Crew.
Args:
name (str): The name of the kernel plugin.
description (str): The description of the kernel plugin.
parameters (List[KernelParameterMetadata] | None, optional): The definitions of the Crew's
required inputs. Defaults to None.
task_webhook_url (Optional[str], optional): The task level webhook URL. Defaults to None.
step_webhook_url (Optional[str], optional): The step level webhook URL. Defaults to None.
crew_webhook_url (Optional[str], optional): The crew level webhook URL. Defaults to None.
Returns:
dict[str, Any]: A dictionary representing the kernel plugin.
"""
@kernel_function(description="Kickoff the CrewAI task.")
async def kickoff(**kwargs: Any) -> str:
args = self._build_arguments(parameters, kwargs)
return await self.kickoff(
inputs=args,
task_webhook_url=task_webhook_url,
step_webhook_url=step_webhook_url,
crew_webhook_url=crew_webhook_url,
)
@kernel_function(description="Kickoff the CrewAI task and wait for completion.")
async def kickoff_and_wait(**kwargs: Any) -> str:
args = self._build_arguments(parameters, kwargs)
kickoff_id = await self.kickoff(
inputs=args,
task_webhook_url=task_webhook_url,
step_webhook_url=step_webhook_url,
crew_webhook_url=crew_webhook_url,
)
return await self.wait_for_crew_completion(kickoff_id)
return KernelPlugin(
name,
description,
{
"kickoff": KernelFunctionFromMethod(kickoff, stream_method=None, parameters=parameters),
"kickoff_and_wait": KernelFunctionFromMethod(
kickoff_and_wait, stream_method=None, parameters=parameters
),
"get_status": self.get_crew_kickoff_status,
"wait_for_completion": self.wait_for_crew_completion,
},
)
def _build_arguments(
self, parameters: list[KernelParameterMetadata] | None, arguments: dict[str, Any]
) -> dict[str, Any]:
"""Builds the arguments for the CrewAI task from the provided parameters and arguments.
Args:
parameters (List[KernelParameterMetadata] | None): The metadata for the inputs.
arguments (dict[str, Any]): The provided arguments.
Returns:
dict[str, Any]: The built arguments.
"""
args = {}
if parameters:
for input in parameters:
name = input.name
if name not in arguments:
raise PluginInitializationError(f"Missing required input '{name}' for CrewAI.")
args[name] = arguments[name]
return args
async def __aenter__(self):
"""Enter the session."""
await self.client.__aenter__()
return self
async def __aexit__(self, *args, **kwargs):
"""Close the session."""
await self.client.__aexit__()
@@ -0,0 +1,106 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
import aiohttp
from semantic_kernel.core_plugins.crew_ai.crew_ai_models import (
CrewAIKickoffResponse,
CrewAIRequiredInputs,
CrewAIStatusResponse,
)
from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT
class CrewAIEnterpriseClient:
"""Client to interact with the Crew AI Enterprise API."""
def __init__(
self,
endpoint: str,
auth_token: str,
session: aiohttp.ClientSession | None = None,
):
"""Initializes a new instance of the CrewAIEnterpriseClient class.
Args:
endpoint (str): The API endpoint.
auth_token (str): The authentication token.
session (aiohttp.ClientSession | None, optional): The HTTP client session. Defaults to None.
"""
self.endpoint = endpoint
self.auth_token = auth_token
self.session = session if session is None else aiohttp.ClientSession()
self.request_header = {
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json",
"user_agent": SEMANTIC_KERNEL_USER_AGENT,
}
async def get_inputs(self) -> CrewAIRequiredInputs:
"""Get the required inputs for Crew AI.
Returns:
CrewAIRequiredInputs: The required inputs for Crew AI.
"""
async with (
self.session.get(f"{self.endpoint}/inputs", headers=self.request_header) as response, # type: ignore
):
response.raise_for_status()
return CrewAIRequiredInputs.model_validate_json(await response.text())
async def kickoff(
self,
inputs: dict[str, Any] | None = None,
task_webhook_url: str | None = None,
step_webhook_url: str | None = None,
crew_webhook_url: str | None = None,
) -> CrewAIKickoffResponse:
"""Kickoff a new Crew AI task.
Args:
inputs (Optional[dict[str, Any]], optional): The inputs for the task. Defaults to None.
task_webhook_url (Optional[str], optional): The webhook URL for task updates. Defaults to None.
step_webhook_url (Optional[str], optional): The webhook URL for step updates. Defaults to None.
crew_webhook_url (Optional[str], optional): The webhook URL for crew updates. Defaults to None.
Returns:
CrewAIKickoffResponse: The response from the kickoff request.
"""
content = {
"inputs": inputs,
"taskWebhookUrl": task_webhook_url,
"stepWebhookUrl": step_webhook_url,
"crewWebhookUrl": crew_webhook_url,
}
async with (
self.session.post(f"{self.endpoint}/kickoff", json=content, headers=self.request_header) as response, # type: ignore
):
response.raise_for_status()
body = await response.text()
return CrewAIKickoffResponse.model_validate_json(body)
async def get_status(self, task_id: str) -> CrewAIStatusResponse:
"""Get the status of a Crew AI task.
Args:
task_id (str): The ID of the task.
Returns:
CrewAIStatusResponse: The status response of the task.
"""
async with (
self.session.get(f"{self.endpoint}/status/{task_id}", headers=self.request_header) as response, # type: ignore
):
response.raise_for_status()
body = await response.text()
return CrewAIStatusResponse.model_validate_json(body)
async def __aenter__(self):
"""Enter the session."""
await self.session.__aenter__() # type: ignore
return self
async def __aexit__(self, *args, **kwargs):
"""Close the session."""
await self.session.close() # type: ignore
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from typing import Any
from semantic_kernel.kernel_pydantic import KernelBaseModel
class CrewAIEnterpriseKickoffState(str, Enum):
"""The Crew.AI Enterprise kickoff state."""
Pending = "PENDING"
Started = "STARTED"
Running = "RUNNING"
Success = "SUCCESS"
Failed = "FAILED"
Failure = "FAILURE"
Not_Found = "NOT FOUND"
class CrewAIStatusResponse(KernelBaseModel):
"""Represents the status response from Crew AI."""
state: CrewAIEnterpriseKickoffState
result: str | None = None
last_step: dict[str, Any] | None = None
class CrewAIKickoffResponse(KernelBaseModel):
"""Represents the kickoff response from Crew AI."""
kickoff_id: str
class CrewAIRequiredInputs(KernelBaseModel):
"""Represents the required inputs for Crew AI."""
inputs: dict[str, str]
@@ -0,0 +1,22 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from pydantic import SecretStr
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class CrewAISettings(KernelBaseSettings):
"""The Crew.AI settings.
Required:
- endpoint: str - The API endpoint.
"""
env_prefix: ClassVar[str] = "CREW_AI_"
endpoint: str
auth_token: SecretStr
polling_interval: float = 1.0
polling_timeout: float = 30.0
@@ -0,0 +1,223 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from typing import Annotated, Any, ClassVar
from urllib.parse import urlparse
import aiohttp
from semantic_kernel.exceptions import FunctionExecutionException
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel_pydantic import KernelBaseModel
class HttpPlugin(KernelBaseModel):
"""A plugin that provides HTTP functionality.
Usage:
# With allowed domains (recommended):
kernel.add_plugin(HttpPlugin(allowed_domains=["example.com", "api.example.com"]), "http")
# Explicitly allow all domains (opt-in, less secure):
kernel.add_plugin(HttpPlugin(allow_all_domains=True), "http")
Examples:
{{http.getAsync $url}}
{{http.postAsync $url}}
{{http.putAsync $url}}
{{http.deleteAsync $url}}
Security:
- By default, all requests are blocked unless ``allowed_domains`` is provided
or ``allow_all_domains`` is set to True.
- When ``allowed_domains`` is set and ``allow_all_domains`` is False, HTTP
redirects are disabled to prevent redirect-based domain bypass (SSRF).
- When ``allow_all_domains`` is True, redirects are allowed regardless of
whether ``allowed_domains`` is also set.
- Only ``http`` and ``https`` URL schemes are permitted.
- Only standard ports (80, 443) are permitted by default. Set ``allowed_ports``
to permit additional ports. Port validation is skipped when
``allow_all_domains`` is True.
"""
allowed_domains: set[str] | None = None
"""Set of allowed domains to send requests to."""
allow_all_domains: bool = False
"""When True, requests to any domain are allowed. Must be explicitly set."""
allowed_ports: set[int] | None = None
"""Set of ports permitted for outbound requests. Defaults to ``{80, 443}`` when not set.
Ignored when ``allow_all_domains`` is True. Set explicitly to permit non-standard ports
(e.g. ``allowed_ports={443, 8443}``).
"""
_ALLOWED_SCHEMES: ClassVar[frozenset[str]] = frozenset({"http", "https"})
_DEFAULT_SCHEME_PORTS: ClassVar[dict[str, int]] = {"http": 80, "https": 443}
_DEFAULT_ALLOWED_PORTS: ClassVar[frozenset[int]] = frozenset({80, 443})
@property
def _allow_redirects(self) -> bool:
"""Whether HTTP redirects should be followed.
Redirects are only allowed when ``allow_all_domains`` is True.
When domain restrictions are configured, redirects are disabled
to prevent redirect-based SSRF bypass.
"""
return self.allow_all_domains
def _is_uri_allowed(self, url: str) -> bool:
"""Check if the URL's host and scheme are permitted.
Args:
url: The URL to check.
Returns:
True if the URL is allowed, False otherwise.
"""
parsed = urlparse(url)
# Validate scheme
if parsed.scheme.lower() not in self._ALLOWED_SCHEMES:
return False
host = parsed.hostname
if not host:
return False
# Validate that the port component is syntactically valid, regardless of
# allow_all_domains. Accessing parsed.port raises ValueError for a malformed
# or out-of-range port.
try:
port = parsed.port
except ValueError:
return False
# If allow_all_domains is set, skip the domain and port allow-list checks.
if self.allow_all_domains:
return True
# Enforce the port allow-list (deny-by-default to non-standard ports).
if port is None:
port = self._DEFAULT_SCHEME_PORTS.get(parsed.scheme.lower())
allowed_ports = self.allowed_ports if self.allowed_ports is not None else self._DEFAULT_ALLOWED_PORTS
if port not in allowed_ports:
return False
# If allowed_domains is set, check against it
if self.allowed_domains is not None:
return host.lower() in {domain.lower() for domain in self.allowed_domains}
# Default: deny all
return False
def _validate_url(self, url: str) -> None:
"""Validate the URL before sending a request.
Always checks that the URL is non-empty, uses an allowed scheme, and has a
syntactically valid port. When ``allow_all_domains`` is False, additionally
enforces the port and domain allow-lists.
Args:
url: The URL to validate.
Raises:
FunctionExecutionException: If the URL is empty, uses a disallowed scheme,
has a malformed port, or (unless ``allow_all_domains`` is True) targets
a port or domain that is not allowed.
"""
if not url:
raise FunctionExecutionException("url cannot be `None` or empty")
if not self._is_uri_allowed(url):
raise FunctionExecutionException("Sending requests to the provided location is not allowed.")
@kernel_function(description="Makes a GET request to a url", name="getAsync")
async def get(self, url: Annotated[str, "The URL to send the request to."]) -> str:
"""Sends an HTTP GET request to the specified URI and returns the response body as a string.
Args:
url: The URL to send the request to.
Returns:
The response body as a string.
"""
self._validate_url(url)
async with (
aiohttp.ClientSession() as session,
session.get(url, raise_for_status=True, allow_redirects=self._allow_redirects) as response,
):
return await response.text()
@kernel_function(description="Makes a POST request to a uri", name="postAsync")
async def post(
self,
url: Annotated[str, "The URI to send the request to."],
body: Annotated[dict[str, Any] | None, "The body of the request"] = None,
) -> str:
"""Sends an HTTP POST request to the specified URI and returns the response body as a string.
Args:
url: The URI to send the request to.
body: Contains the body of the request
returns:
The response body as a string.
"""
self._validate_url(url)
headers = {"Content-Type": "application/json"}
data = json.dumps(body) if body is not None else None
async with (
aiohttp.ClientSession() as session,
session.post(
url, headers=headers, data=data, raise_for_status=True, allow_redirects=self._allow_redirects
) as response,
):
return await response.text()
@kernel_function(description="Makes a PUT request to a uri", name="putAsync")
async def put(
self,
url: Annotated[str, "The URI to send the request to."],
body: Annotated[dict[str, Any] | None, "The body of the request"] = None,
) -> str:
"""Sends an HTTP PUT request to the specified URI and returns the response body as a string.
Args:
url: The URI to send the request to.
body: Contains the body of the request
Returns:
The response body as a string.
"""
self._validate_url(url)
headers = {"Content-Type": "application/json"}
data = json.dumps(body) if body is not None else None
async with (
aiohttp.ClientSession() as session,
session.put(
url, headers=headers, data=data, raise_for_status=True, allow_redirects=self._allow_redirects
) as response,
):
return await response.text()
@kernel_function(description="Makes a DELETE request to a uri", name="deleteAsync")
async def delete(self, url: Annotated[str, "The URI to send the request to."]) -> str:
"""Sends an HTTP DELETE request to the specified URI and returns the response body as a string.
Args:
url: The URI to send the request to.
Returns:
The response body as a string.
"""
self._validate_url(url)
async with (
aiohttp.ClientSession() as session,
session.delete(url, raise_for_status=True, allow_redirects=self._allow_redirects) as response,
):
return await response.text()
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class MathPlugin:
"""Description: MathPlugin provides a set of functions to make Math calculations.
Usage:
kernel.add_plugin(MathPlugin(), plugin_name="math")
Examples:
{{math.Add}} => Returns the sum of input and amount (provided in the KernelArguments)
{{math.Subtract}} => Returns the difference of input and amount (provided in the KernelArguments)
"""
def _parse_number(self, val: int | float | str) -> float:
"""Helper to parse a value as a float (supports int, float, str)."""
if isinstance(val, (int, float)):
return float(val)
try:
return float(val)
except Exception as ex:
raise ValueError(f"Cannot convert {val!r} to float for math operation") from ex
@kernel_function(name="Add")
def add(
self,
input: Annotated[int | float | str, "The first number to add"],
amount: Annotated[int | float | str, "The second number to add"],
) -> Annotated[float, "The result"]:
"""Returns the addition result of the values provided (supports float and int)."""
x = self._parse_number(input)
y = self._parse_number(amount)
return x + y
@kernel_function(name="Subtract")
def subtract(
self,
input: Annotated[int | float | str, "The number to subtract from"],
amount: Annotated[int | float | str, "The number to subtract"],
) -> Annotated[float, "The result"]:
"""Returns the difference of numbers provided (supports float and int)."""
x = self._parse_number(input)
y = self._parse_number(amount)
return x - y
@@ -0,0 +1,71 @@
# Getting Started with the Sessions Python Plugin
## Setup
Please follow the [Azure Container Apps Documentation](https://learn.microsoft.com/en-us/azure/container-apps/sessions-code-interpreter) to get started.
## Configuring the Python Plugin
Next, as an environment variable or in the .env file, add the `poolManagementEndpoint` value from above to the variable `ACA_POOL_MANAGEMENT_ENDPOINT`. The `poolManagementEndpoint` should look something like:
```html
https://eastus.acasessions.io/subscriptions/{{subscriptionId}}/resourceGroups/{{resourceGroup}}/sessionPools/{{sessionPool}}/python/execute
```
You can also provide the the `ACA_TOKEN_ENDPOINT` if you want to override the default value of `https://acasessions.io/.default`. If this token endpoint doesn't need to be overridden, then it is
not necessary to include this as an environment variable, in the .env file, or via the plugin's constructor. Please follow the [Azure Container Apps Documentation](https://learn.microsoft.com/en-us/azure/container-apps/sessions-code-interpreter) to review the proper role required to authenticate with the `AsyncTokenCredential`.
Next, let's move on to implementing the plugin in code. It is possible to add the code interpreter plugin as follows:
```python
kernel = Kernel()
service_id = "azure_oai"
chat_service = AzureChatCompletion(
service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True)
)
kernel.add_service(chat_service)
python_code_interpreter = SessionsPythonTool(
auth_callback=auth_callback
)
sessions_tool = kernel.add_plugin(python_code_interpreter, "PythonCodeInterpreter")
code = "import json\n\ndef add_numbers(a, b):\n return a + b\n\nargs = '{\"a\": 1, \"b\": 1}'\nargs_dict = json.loads(args)\nprint(add_numbers(args_dict['a'], args_dict['b']))"
result = await kernel.invoke(sessions_tool["execute_code"], code=code)
print(result)
```
Instead of hard-coding a well-formatted Python code string, you may use automatic function calling inside of SK and allow the model to form the Python and call the plugin.
The authentication callback must return a valid token for the session pool. One possible way of doing this with a `AzureCliCredential` is as follows:
```python
async def auth_callback() -> str:
"""Auth callback for the SessionsPythonTool.
This is a sample auth callback that shows how to use Azure's AzureCliCredential
to get an access token.
"""
global auth_token
current_utc_timestamp = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
if not auth_token or auth_token.expires_on < current_utc_timestamp:
credential = AzureCliCredential()
try:
auth_token = credential.get_token(ACA_TOKEN_ENDPOINT)
except ClientAuthenticationError as cae:
err_messages = getattr(cae, "messages", [])
raise FunctionExecutionException(
f"Failed to retrieve the client auth token with messages: {' '.join(err_messages)}"
) from cae
return auth_token.token
```
Currently, there are two concept examples that show this plugin in more detail:
- [Plugin example](../../../samples/concepts/plugins/azure_python_code_interpreter.py): shows the basic usage of calling the code execute function on the plugin.
- [Function Calling example](../../../samples/concepts/auto_function_calling/azure_python_code_interpreter_function_calling.py): shows a simple chat application that leverages the Python code interpreter plugin for function calling.
@@ -0,0 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin import (
SessionsPythonTool,
)
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_settings import (
SessionsPythonSettings,
)
__all__ = ["SessionsPythonSettings", "SessionsPythonTool"]
@@ -0,0 +1,467 @@
# Copyright (c) Microsoft. All rights reserved.
import inspect
import logging
import os
import re
from collections.abc import Awaitable, Callable
from io import BytesIO
from typing import Annotated, Any
from azure.core.credentials import TokenCredential
from httpx import AsyncClient, HTTPStatusError
from pydantic import ValidationError
from semantic_kernel.const import USER_AGENT
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_settings import (
ACASessionsSettings,
SessionsPythonSettings,
)
from semantic_kernel.core_plugins.sessions_python_tool.sessions_remote_file_metadata import SessionsRemoteFileMetadata
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException, FunctionInitializationError
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseModel
from semantic_kernel.utils.telemetry.user_agent import HTTP_USER_AGENT, version_info
logger = logging.getLogger(__name__)
SESSIONS_USER_AGENT = f"{HTTP_USER_AGENT}/{version_info} (Language=Python)"
SESSIONS_API_VERSION = "2024-02-02-preview"
class SessionsPythonTool(KernelBaseModel):
"""A plugin for running Python code in an Azure Container Apps dynamic sessions code interpreter."""
pool_management_endpoint: HttpsUrl
settings: SessionsPythonSettings
auth_callback: Callable[..., Any | Awaitable[Any]]
http_client: AsyncClient
enable_dangerous_file_uploads: bool = False
"""Flag to enable file upload operations. Must be True along with allowed_upload_directories to enable uploads."""
allowed_upload_directories: set[str] | None = None
"""Allowed local directories for file uploads. If None, upload_file is disabled (deny-by-default)."""
allowed_download_directories: set[str] | None = None
"""Allowed local directories for file downloads. If None, all paths are allowed (permissive-by-default)."""
def __init__(
self,
auth_callback: Callable[..., Any | Awaitable[Any]] | None = None,
pool_management_endpoint: str | None = None,
settings: SessionsPythonSettings | None = None,
http_client: AsyncClient | None = None,
env_file_path: str | None = None,
token_endpoint: str | None = None,
credential: TokenCredential | None = None,
enable_dangerous_file_uploads: bool = False,
allowed_upload_directories: set[str] | list[str] | None = None,
allowed_download_directories: set[str] | list[str] | None = None,
**kwargs,
):
"""Initializes a new instance of the SessionsPythonTool class.
Args:
auth_callback: Callback to retrieve authentication token.
pool_management_endpoint: The ACA pool management endpoint URL.
settings: Python session settings.
http_client: HTTP client for making requests.
env_file_path: Path to .env file.
token_endpoint: Token endpoint for authentication.
credential: Azure credential for authentication.
enable_dangerous_file_uploads: Flag to enable file upload operations.
Must be True along with allowed_upload_directories to enable file uploads.
Default is False (file uploads disabled).
allowed_upload_directories: Set or list of allowed directories for file uploads.
If None, upload_file will be disabled (deny-by-default).
Empty set/list means no directories are allowed (all uploads denied).
allowed_download_directories: Set or list of allowed directories for file downloads.
If None, all paths are allowed (permissive-by-default).
If configured, downloads are restricted to these directories.
kwargs: Additional keyword arguments.
"""
try:
aca_settings = ACASessionsSettings(
env_file_path=env_file_path,
pool_management_endpoint=pool_management_endpoint,
token_endpoint=token_endpoint,
)
except ValidationError as e:
logger.error(f"Failed to load the ACASessionsSettings with message: {e!s}")
raise FunctionInitializationError(f"Failed to load the ACASessionsSettings with message: {e!s}") from e
if not settings:
settings = SessionsPythonSettings()
if not http_client:
http_client = AsyncClient(timeout=5)
if auth_callback is None:
auth_callback = self._default_auth_callback(aca_settings, credential)
# Convert lists to sets and filter out empty strings (which resolve to CWD)
upload_dirs = {d for d in allowed_upload_directories if d} if allowed_upload_directories is not None else None
download_dirs = (
{d for d in allowed_download_directories if d} if allowed_download_directories is not None else None
)
super().__init__(
pool_management_endpoint=aca_settings.pool_management_endpoint,
settings=settings,
auth_callback=auth_callback,
http_client=http_client,
enable_dangerous_file_uploads=enable_dangerous_file_uploads,
allowed_upload_directories=upload_dirs,
allowed_download_directories=download_dirs,
**kwargs,
)
# region Helper Methods
def _default_auth_callback(
self, aca_settings: ACASessionsSettings, credential: TokenCredential | None
) -> Callable[..., Any | Awaitable[Any]]:
"""Generates a default authentication callback using the ACA settings."""
token = aca_settings.get_sessions_auth_token(credential=credential)
if token is None:
raise FunctionInitializationError("Failed to retrieve the client auth token.")
def auth_callback() -> str:
"""Retrieve the client auth token."""
return token
return auth_callback
async def _ensure_auth_token(self) -> str:
"""Ensure the auth token is valid and handle both sync and async callbacks."""
try:
if inspect.iscoroutinefunction(self.auth_callback):
auth_token = await self.auth_callback()
else:
auth_token = self.auth_callback()
except Exception as e:
logger.error(f"Failed to retrieve the client auth token with message: {e!s}")
raise FunctionExecutionException(f"Failed to retrieve the client auth token with message: {e!s}") from e
return auth_token
def _sanitize_input(self, code: str) -> str:
"""Sanitize input to the python REPL.
Remove whitespace, backtick & python (if llm mistakes python console as terminal).
Args:
code (str): The query to sanitize
Returns:
str: The sanitized query
"""
# Removes `, whitespace & python from start
code = re.sub(r"^(\s|`)*(?i:python)?\s*", "", code)
# Removes whitespace & ` from end
return re.sub(r"(\s|`)*$", "", code)
def _construct_remote_file_path(self, remote_file_path: str) -> str:
"""Construct the remote file path.
Args:
remote_file_path (str): The remote file path.
Returns:
str: The remote file path.
"""
if not remote_file_path.startswith("/mnt/data/"):
remote_file_path = f"/mnt/data/{remote_file_path}"
return remote_file_path
def _build_url_with_version(self, base_url, endpoint, params):
"""Builds a URL with the provided base URL, endpoint, and query parameters."""
params["api-version"] = SESSIONS_API_VERSION
query_string = "&".join([f"{key}={value}" for key, value in params.items()])
if not base_url.endswith("/"):
base_url += "/"
if endpoint.endswith("/"):
endpoint = endpoint[:-1]
return f"{base_url}{endpoint}?{query_string}"
def _validate_local_path_for_upload(self, local_file_path: str) -> str:
"""Validate local path is within allowed upload directories.
Args:
local_file_path: The path to validate.
Returns:
str: The canonicalized absolute path.
Raises:
FunctionExecutionException: If file operations are disabled or path is not within allowed directories.
"""
if not self.enable_dangerous_file_uploads:
raise FunctionExecutionException(
"File upload is disabled. Set 'enable_dangerous_file_uploads=True' "
"and configure 'allowed_upload_directories' to enable."
)
if self.allowed_upload_directories is None:
raise FunctionExecutionException("File upload requires 'allowed_upload_directories' to be configured.")
canonical_path = os.path.realpath(local_file_path)
for allowed_dir in self.allowed_upload_directories:
allowed_canonical = os.path.realpath(allowed_dir)
try:
common = os.path.commonpath([allowed_canonical, canonical_path])
if common == allowed_canonical:
return canonical_path
except ValueError:
continue # Different drives on Windows
logger.warning(f"Upload denied for path: {local_file_path} (resolved: {canonical_path})")
raise FunctionExecutionException(
f"Access denied: '{local_file_path}' is not within allowed upload directories."
)
def _validate_local_path_for_download(self, local_file_path: str) -> str:
"""Validate local path is within allowed download directories (optional protection).
Args:
local_file_path: The path to validate.
Returns:
str: The canonicalized absolute path.
Raises:
FunctionExecutionException: If allowed_download_directories is set and path is not within.
"""
# Permissive by default - if no restrictions configured, allow all paths
if self.allowed_download_directories is None:
return os.path.realpath(local_file_path)
parent_dir = os.path.dirname(local_file_path) or "."
canonical_parent = os.path.realpath(parent_dir)
filename = os.path.basename(local_file_path)
canonical_path = os.path.join(canonical_parent, filename)
for allowed_dir in self.allowed_download_directories:
allowed_canonical = os.path.realpath(allowed_dir)
try:
common = os.path.commonpath([allowed_canonical, canonical_parent])
if common == allowed_canonical:
return canonical_path
except ValueError:
continue
logger.warning(f"Download denied for path: {local_file_path}")
raise FunctionExecutionException(
f"Access denied: '{local_file_path}' is not within allowed download directories."
)
# endregion
# region Kernel Functions
@kernel_function(
description="""Executes the provided Python code.
Start and end the code snippet with double quotes to define it as a string.
Insert \\n within the string wherever a new line should appear.
Add spaces directly after \\n sequences to replicate indentation.
Use \" to include double quotes within the code without ending the string.
Keep everything in a single line; the \\n sequences will represent line breaks
when the string is processed or displayed.
""",
name="execute_code",
)
async def execute_code(self, code: Annotated[str, "The valid Python code to execute"]) -> str:
"""Executes the provided Python code.
Args:
code (str): The valid Python code to execute
Returns:
str: The result of the Python code execution in the form of Result, Stdout, and Stderr
Raises:
FunctionExecutionException: If the provided code is empty.
"""
if not code:
raise FunctionExecutionException("The provided code is empty")
if self.settings.sanitize_input:
code = self._sanitize_input(code)
auth_token = await self._ensure_auth_token()
logger.info(f"Executing Python code: {code}")
self.http_client.headers.update({
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json",
USER_AGENT: SESSIONS_USER_AGENT,
})
self.settings.python_code = code
request_body = {
"properties": self.settings.model_dump(exclude_none=True, exclude={"sanitize_input"}, by_alias=True),
}
url = self._build_url_with_version(
base_url=str(self.pool_management_endpoint),
endpoint="code/execute/",
params={"identifier": self.settings.session_id},
)
try:
response = await self.http_client.post(
url=url,
json=request_body,
)
response.raise_for_status()
result = response.json()["properties"]
return (
f"Status:\n{result['status']}\n"
f"Result:\n{result['result']}\n"
f"Stdout:\n{result['stdout']}\n"
f"Stderr:\n{result['stderr']}"
)
except HTTPStatusError as e:
error_message = e.response.text if e.response.text else e.response.reason_phrase
raise FunctionExecutionException(
f"Code execution failed with status code {e.response.status_code} and error: {error_message}"
) from e
@kernel_function(name="upload_file", description="Uploads a file for the current Session ID")
async def upload_file(
self,
*,
local_file_path: Annotated[str, "The path to the local file on the machine"],
remote_file_path: Annotated[
str | None, "The remote path to the file in the session. Defaults to /mnt/data"
] = None,
) -> Annotated[SessionsRemoteFileMetadata, "The metadata of the uploaded file"]:
"""Upload a file to the session pool.
Args:
remote_file_path (str): The path to the file in the session.
local_file_path (str): The path to the file on the local machine.
Must be within allowed_upload_directories.
Returns:
RemoteFileMetadata: The metadata of the uploaded file.
Raises:
FunctionExecutionException: If local_file_path is not provided or not in allowed directories.
"""
if not local_file_path:
raise FunctionExecutionException("Please provide a local file path to upload.")
# Validate path is in allowed directories (deny-by-default)
validated_path = self._validate_local_path_for_upload(local_file_path)
remote_file_path = self._construct_remote_file_path(remote_file_path or os.path.basename(validated_path))
auth_token = await self._ensure_auth_token()
self.http_client.headers.update({
"Authorization": f"Bearer {auth_token}",
USER_AGENT: SESSIONS_USER_AGENT,
})
url = self._build_url_with_version(
base_url=str(self.pool_management_endpoint),
endpoint="files/upload",
params={"identifier": self.settings.session_id},
)
try:
with open(validated_path, "rb") as data:
files = {"file": (remote_file_path, data, "application/octet-stream")}
response = await self.http_client.post(url=url, files=files)
response.raise_for_status()
uploaded_files = await self.list_files()
return next(
file_metadata for file_metadata in uploaded_files if file_metadata.full_path == remote_file_path
)
except HTTPStatusError as e:
error_message = e.response.text if e.response.text else e.response.reason_phrase
raise FunctionExecutionException(
f"Upload failed with status code {e.response.status_code} and error: {error_message}"
) from e
@kernel_function(name="list_files", description="Lists all files in the provided Session ID")
async def list_files(self) -> list[SessionsRemoteFileMetadata]:
"""List the files in the session pool.
Returns:
list[SessionsRemoteFileMetadata]: The metadata for the files in the session pool
"""
auth_token = await self._ensure_auth_token()
self.http_client.headers.update({
"Authorization": f"Bearer {auth_token}",
USER_AGENT: SESSIONS_USER_AGENT,
})
url = self._build_url_with_version(
base_url=str(self.pool_management_endpoint),
endpoint="files",
params={"identifier": self.settings.session_id},
)
try:
response = await self.http_client.get(
url=url,
)
response.raise_for_status()
response_json = response.json()
return [SessionsRemoteFileMetadata.from_dict(entry["properties"]) for entry in response_json["value"]]
except HTTPStatusError as e:
error_message = e.response.text if e.response.text else e.response.reason_phrase
raise FunctionExecutionException(
f"List files failed with status code {e.response.status_code} and error: {error_message}"
) from e
async def download_file(
self,
*,
remote_file_name: Annotated[str, "The name of the file to download, relative to /mnt/data"],
local_file_path: Annotated[str | None, "The local file path to save the file to, optional"] = None,
) -> Annotated[BytesIO | None, "The data of the downloaded file"]:
"""Download a file from the session pool.
Args:
remote_file_name: The name of the file to download, relative to `/mnt/data`.
local_file_path: The path to save the downloaded file to. Should include the extension.
If not provided, the file is returned as a BytesIO object.
Returns:
BytesIO | None: The file content as BytesIO if no local_file_path provided, otherwise None.
Raises:
FunctionExecutionException: If local_file_path is not in allowed directories.
"""
auth_token = await self._ensure_auth_token()
self.http_client.headers.update({
"Authorization": f"Bearer {auth_token}",
USER_AGENT: SESSIONS_USER_AGENT,
})
url = self._build_url_with_version(
base_url=str(self.pool_management_endpoint),
endpoint=f"files/content/{remote_file_name}",
params={"identifier": self.settings.session_id},
)
try:
response = await self.http_client.get(
url=url,
)
response.raise_for_status()
if local_file_path:
# Validate path is in allowed directories (optional, permissive by default)
validated_path = self._validate_local_path_for_download(local_file_path)
with open(validated_path, "wb") as f:
f.write(response.content)
return None
return BytesIO(response.content)
except HTTPStatusError as e:
error_message = e.response.text if e.response.text else e.response.reason_phrase
raise FunctionExecutionException(
f"Download failed with status code {e.response.status_code} and error: {error_message}"
) from e
# endregion
@@ -0,0 +1,93 @@
# Copyright (c) Microsoft. All rights reserved.
import re
import uuid
from enum import Enum
from typing import ClassVar
from urllib.parse import urlsplit, urlunsplit
from azure.core.credentials import TokenCredential
from pydantic import Field, field_validator
from semantic_kernel.exceptions.function_exceptions import PluginInitializationError
from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseModel, KernelBaseSettings
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
class CodeInputType(str, Enum):
"""Code input type."""
Inline = "inline"
class CodeExecutionType(str, Enum):
"""Code execution type."""
Synchronous = "synchronous"
# Asynchronous = "asynchronous" TODO: Enable when available
class SessionsPythonSettings(KernelBaseModel):
"""The Sessions Python code interpreter settings."""
session_id: str | None = Field(default_factory=lambda: str(uuid.uuid4()), alias="identifier", exclude=True)
code_input_type: CodeInputType | None = Field(default=CodeInputType.Inline, alias="codeInputType")
execution_type: CodeExecutionType | None = Field(default=CodeExecutionType.Synchronous, alias="executionType")
python_code: str | None = Field(alias="code", default=None)
timeout_in_sec: int | None = Field(default=100, alias="timeoutInSeconds")
sanitize_input: bool | None = Field(default=True, alias="sanitizeInput")
class ACASessionsSettings(KernelBaseSettings):
"""Azure Container Apps sessions settings.
Required:
- pool_management_endpoint: HttpsUrl - The URL of the Azure Container Apps pool management endpoint.
(Env var ACA_POOL_MANAGEMENT_ENDPOINT)
"""
env_prefix: ClassVar[str] = "ACA_"
pool_management_endpoint: HttpsUrl
token_endpoint: str = "https://acasessions.io/.default"
@field_validator("pool_management_endpoint", mode="before")
@classmethod
def _validate_endpoint(cls, endpoint: str) -> str:
"""Validates the pool management endpoint."""
if "python/execute" in endpoint:
endpoint_parsed = urlsplit(endpoint.replace("python/execute", ""))._asdict()
else:
endpoint_parsed = urlsplit(endpoint)._asdict()
if endpoint_parsed["path"]:
endpoint_parsed["path"] = re.sub(r"/{2,}", "/", endpoint_parsed["path"])
else:
endpoint_parsed["path"] = "/"
return str(urlunsplit(endpoint_parsed.values()))
def get_sessions_auth_token(
self, credential: TokenCredential | None = None, token_endpoint: str | None = None
) -> str | None:
"""Retrieve a Microsoft Entra Auth Token for a given token endpoint for the use with an Azure Container App.
The required role for the token is `Azure ContainerApps Session Executor and Contributor`.
The token endpoint may be specified as an environment variable, via the .env
file or as an argument. If the token endpoint is not provided, the default is None.
The `token_endpoint` argument takes precedence over the `token_endpoint` attribute.
Args:
credential: The credential to use for authentication.
token_endpoint: The token endpoint to use. Defaults to `https://acasessions.io/.default`.
Returns:
The Azure token or None if the token could not be retrieved.
Raises:
ServiceInitializationError: If the token endpoint is not provided.
"""
endpoint_to_use = token_endpoint or self.token_endpoint
if endpoint_to_use is None:
raise PluginInitializationError("Please provide a token endpoint to retrieve the authentication token.")
if credential is None:
raise PluginInitializationError("Please provide a credential to retrieve the authentication token.")
return get_entra_auth_token(credential, endpoint_to_use)
@@ -0,0 +1,34 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.kernel_pydantic import KernelBaseModel
class SessionsRemoteFileMetadata(KernelBaseModel):
"""Metadata for a file in the session."""
"""The filename relative to `/mnt/data/`."""
filename: str
"""The size of the file in bytes."""
size_in_bytes: int
@property
def full_path(self) -> str:
"""Get the full path of the file."""
if not self.filename.startswith("/mnt/data/"):
return f"/mnt/data/{self.filename}"
return self.filename
@staticmethod
def from_dict(data: dict[str, Any]) -> "SessionsRemoteFileMetadata":
"""Create a SessionsRemoteFileMetadata object from a dictionary of file data values.
Args:
data (dict[str, Any]): The file data values.
Returns:
SessionsRemoteFileMetadata: The metadata for the file.
"""
return SessionsRemoteFileMetadata(filename=data["filename"], size_in_bytes=data["size"])
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
import sys
from typing import Annotated, Any, Final
from pydantic import Field
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase
if sys.version_info >= (3, 13):
from warnings import deprecated
else:
from typing_extensions import deprecated
logger: logging.Logger = logging.getLogger(__name__)
DEFAULT_COLLECTION: Final[str] = "generic"
COLLECTION_PARAM: Final[str] = "collection"
DEFAULT_RELEVANCE: Final[float] = 0.75
RELEVANCE_PARAM: Final[str] = "relevance"
DEFAULT_LIMIT: Final[int] = 1
@deprecated(
"This class is deprecated and will be removed in a future version. Use the new `collection.as_text_search` instead."
)
class TextMemoryPlugin(KernelBaseModel):
"""A plugin to interact with a Semantic Text Memory."""
memory: SemanticTextMemoryBase
embeddings_kwargs: dict[str, Any] = Field(default_factory=dict)
def __init__(self, memory: SemanticTextMemoryBase, embeddings_kwargs: dict[str, Any] | None = None) -> None:
"""Initialize a new instance of the TextMemoryPlugin.
Args:
memory (SemanticTextMemoryBase): the underlying Semantic Text Memory to use
embeddings_kwargs (Optional[Dict[str, Any]]): the keyword arguments to pass to the embedding generator
"""
super().__init__(memory=memory, embeddings_kwargs=embeddings_kwargs if embeddings_kwargs is not None else {})
@kernel_function(
description="Recall a fact from the long term memory",
name="recall",
)
async def recall(
self,
ask: Annotated[str, "The information to retrieve"],
collection: Annotated[str, "The collection to search for information."] = DEFAULT_COLLECTION,
relevance: Annotated[
float, "The relevance score, from 0.0 to 1.0; 1.0 means perfect match"
] = DEFAULT_RELEVANCE,
limit: Annotated[int, "The maximum number of relevant memories to recall."] = DEFAULT_LIMIT,
) -> str:
"""Recall a fact from the long term memory.
Example:
{{memory.recall $ask}} => "Paris"
Args:
ask: The question to ask the memory
collection: The collection to search for information
relevance: The relevance score, from 0.0 to 1.0; 1.0 means perfect match
limit: The maximum number of relevant memories to recall
Returns:
The nearest item from the memory store as a string or empty string if not found.
"""
results = await self.memory.search(
collection=collection,
query=ask,
limit=limit,
min_relevance_score=relevance,
)
if results is None or len(results) == 0:
logger.warning(f"Memory not found in collection: {collection}")
return ""
return results[0].text if limit == 1 else json.dumps([r.text for r in results]) # type: ignore
@kernel_function(
description="Save information to semantic memory",
name="save",
)
async def save(
self,
text: Annotated[str, "The information to save."],
key: Annotated[str, "The unique key to associate with the information."],
collection: Annotated[str, "The collection to save the information."] = DEFAULT_COLLECTION,
) -> None:
"""Save a fact to the long term memory.
Args:
text: The text to save to the memory
kernel: The kernel instance, that has a memory store
collection: The collection to save the information
key: The unique key to associate with the information
"""
await self.memory.save_information(
collection=collection, text=text, id=key, embeddings_kwargs=self.embeddings_kwargs
)
@@ -0,0 +1,78 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel_pydantic import KernelBaseModel
class TextPlugin(KernelBaseModel):
"""TextPlugin provides a set of functions to manipulate strings.
Usage:
kernel.add_plugin(TextPlugin(), plugin_name="text")
Examples:
KernelArguments["input"] = " hello world "
{{text.trim $input}} => "hello world"
KernelArguments["input"] = " hello world "
{{text.trimStart $input}} => "hello world "
KernelArguments["input"] = " hello world "
{{text.trimEnd $input}} => " hello world"
KernelArguments["input"] = "hello world"
{{text.uppercase $input}} => "HELLO WORLD"
KernelArguments["input"] = "HELLO WORLD"
{{text.lowercase $input}} => "hello world"
"""
@kernel_function(description="Trim whitespace from the start and end of a string.")
def trim(self, input: str) -> str:
"""Trim whitespace from the start and end of a string.
Example:
KernelArguments["input"] = " hello world "
{{text.trim $input}} => "hello world"
"""
return input.strip()
@kernel_function(description="Trim whitespace from the start of a string.")
def trim_start(self, input: str) -> str:
"""Trim whitespace from the start of a string.
Example:
KernelArguments["input"] = " hello world "
{{input.trim $input}} => "hello world "
"""
return input.lstrip()
@kernel_function(description="Trim whitespace from the end of a string.")
def trim_end(self, input: str) -> str:
"""Trim whitespace from the end of a string.
Example:
KernelArguments["input"] = " hello world "
{{input.trim $input}} => " hello world"
"""
return input.rstrip()
@kernel_function(description="Convert a string to uppercase.")
def uppercase(self, input: str) -> str:
"""Convert a string to uppercase.
Example:
KernelArguments["input"] = "hello world"
{{input.uppercase $input}} => "HELLO WORLD"
"""
return input.upper()
@kernel_function(description="Convert a string to lowercase.")
def lowercase(self, input: str) -> str:
"""Convert a string to lowercase.
Example:
KernelArguments["input"] = "HELLO WORLD"
{{input.lowercase $input}} => "hello world"
"""
return input.lower()
@@ -0,0 +1,242 @@
# Copyright (c) Microsoft. All rights reserved.
import datetime
from semantic_kernel.exceptions import FunctionExecutionException
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel_pydantic import KernelBaseModel
class TimePlugin(KernelBaseModel):
"""TimePlugin provides a set of functions to get the current time and date.
Usage:
kernel.add_plugin(TimePlugin(), plugin_name="time")
Examples:
{{time.date}} => Sunday, 12 January, 2031
{{time.today}} => Sunday, 12 January, 2031
{{time.iso_date}} => 2031-01-12
{{time.now}} => Sunday, January 12, 2031 9:15 PM
{{time.utcNow}} => Sunday, January 13, 2031 5:15 AM
{{time.time}} => 09:15:07 PM
{{time.year}} => 2031
{{time.month}} => January
{{time.monthNumber}} => 01
{{time.day}} => 12
{{time.dayOfWeek}} => Sunday
{{time.hour}} => 9 PM
{{time.hourNumber}} => 21
{{time.days_ago $days}} => Sunday, 7 May, 2023
{{time.last_matching_day $dayName}} => Sunday, 7 May, 2023
{{time.minute}} => 15
{{time.minutes}} => 15
{{time.second}} => 7
{{time.seconds}} => 7
{{time.timeZoneOffset}} => -0800
{{time.timeZoneName}} => PST
"""
@kernel_function(description="Get the current date.")
def date(self) -> str:
"""Get the current date.
Example:
{{time.date}} => Sunday, 12 January, 2031
"""
now = datetime.datetime.now()
return now.strftime("%A, %d %B, %Y")
@kernel_function(description="Get the current date.")
def today(self) -> str:
"""Get the current date.
Example:
{{time.today}} => Sunday, 12 January, 2031
"""
return self.date()
@kernel_function(description="Get the current date in iso format.")
def iso_date(self) -> str:
"""Get the current date in iso format.
Example:
{{time.iso_date}} => 2031-01-12
"""
today = datetime.date.today()
return today.isoformat()
@kernel_function(description="Get the current date and time in the local time zone")
def now(self) -> str:
"""Get the current date and time in the local time zone.
Example:
{{time.now}} => Sunday, January 12, 2031 9:15 PM
"""
now = datetime.datetime.now()
return now.strftime("%A, %B %d, %Y %I:%M %p")
@kernel_function(description="Get the current date and time in UTC", name="utcNow")
def utc_now(self) -> str:
"""Get the current date and time in UTC.
Example:
{{time.utcNow}} => Sunday, January 13, 2031 5:15 AM
"""
now = datetime.datetime.utcnow()
return now.strftime("%A, %B %d, %Y %I:%M %p")
@kernel_function(description="Get the current time in the local time zone")
def time(self) -> str:
"""Get the current time in the local time zone.
Example:
{{time.time}} => 09:15:07 PM
"""
now = datetime.datetime.now()
return now.strftime("%I:%M:%S %p")
@kernel_function(description="Get the current year")
def year(self) -> str:
"""Get the current year.
Example:
{{time.year}} => 2031
"""
now = datetime.datetime.now()
return now.strftime("%Y")
@kernel_function(description="Get the current month")
def month(self) -> str:
"""Get the current month.
Example:
{{time.month}} => January
"""
now = datetime.datetime.now()
return now.strftime("%B")
@kernel_function(description="Get the current month number")
def month_number(self) -> str:
"""Get the current month number.
Example:
{{time.monthNumber}} => 01
"""
now = datetime.datetime.now()
return now.strftime("%m")
@kernel_function(description="Get the current day")
def day(self) -> str:
"""Get the current day of the month.
Example:
{{time.day}} => 12
"""
now = datetime.datetime.now()
return now.strftime("%d")
@kernel_function(description="Get the current day of the week", name="dayOfWeek")
def day_of_week(self) -> str:
"""Get the current day of the week.
Example:
{{time.dayOfWeek}} => Sunday
"""
now = datetime.datetime.now()
return now.strftime("%A")
@kernel_function(description="Get the current hour")
def hour(self) -> str:
"""Get the current hour.
Example:
{{time.hour}} => 9 PM
"""
now = datetime.datetime.now()
return now.strftime("%I %p")
@kernel_function(description="Get the current hour number", name="hourNumber")
def hour_number(self) -> str:
"""Get the current hour number.
Example:
{{time.hourNumber}} => 21
"""
now = datetime.datetime.now()
return now.strftime("%H")
@kernel_function(description="Get the current minute")
def minute(self) -> str:
"""Get the current minute.
Example:
{{time.minute}} => 15
"""
now = datetime.datetime.now()
return now.strftime("%M")
@kernel_function(description="Get the date of offset from today by a provided number of days")
def days_ago(self, days: str) -> str:
"""Get the date a provided number of days in the past.
Args:
days: The number of days to offset from today
Returns:
The date of the offset day.
Example:
{{time.days_ago $input}} => Sunday, 7 May, 2023
"""
d = datetime.date.today() - datetime.timedelta(days=int(days))
return d.strftime("%A, %d %B, %Y")
@kernel_function(description="""Get the date of the last day matching the supplied week day name in English.""")
def date_matching_last_day_name(self, day_name: str) -> str:
"""Get the date of the last day matching the supplied day name.
Args:
day_name: The day name to match with.
Returns:
The date of the matching day.
Example:
{{time.date_matching_last_day_name $input}} => Sunday, 7 May, 2023
"""
d = datetime.date.today()
for i in range(1, 8):
d = d - datetime.timedelta(days=1)
if d.strftime("%A") == day_name:
return d.strftime("%A, %d %B, %Y")
raise FunctionExecutionException("day_name is not recognized")
@kernel_function(description="Get the seconds on the current minute")
def second(self) -> str:
"""Get the seconds on the current minute.
Example:
{{time.second}} => 7
"""
now = datetime.datetime.now()
return now.strftime("%S")
@kernel_function(description="Get the current time zone offset", name="timeZoneOffset")
def time_zone_offset(self) -> str:
"""Get the current time zone offset.
Example:
{{time.timeZoneOffset}} => -08:00
"""
now = datetime.datetime.now()
return now.strftime("%z")
@kernel_function(description="Get the current time zone name", name="timeZoneName")
def time_zone_name(self) -> str:
"""Get the current time zone name.
Example:
{{time.timeZoneName}} => PST
"""
now = datetime.datetime.now()
return now.strftime("%Z")
@@ -0,0 +1,29 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from semantic_kernel.exceptions import FunctionExecutionException
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel_pydantic import KernelBaseModel
class WaitPlugin(KernelBaseModel):
"""WaitPlugin provides a set of functions to wait for a certain amount of time.
Usage:
kernel.add_plugin(WaitPlugin(), plugin_name="wait")
Examples:
{{wait.wait 5}} => Wait for 5 seconds
"""
@kernel_function
async def wait(self, input: Annotated[float | str, "The number of seconds to wait, can be str or float."]) -> None:
"""Wait for a certain number of seconds."""
if isinstance(input, str):
try:
input = float(input)
except ValueError as exc:
raise FunctionExecutionException("seconds text must be a number") from exc
await asyncio.sleep(abs(input))
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import TYPE_CHECKING, Annotated
from semantic_kernel.functions.kernel_function_decorator import kernel_function
if TYPE_CHECKING:
from semantic_kernel.connectors.search_engine.connector import ConnectorBase
class WebSearchEnginePlugin:
"""A plugin that provides web search engine functionality.
Usage:
connector = BingConnector(bing_search_api_key)
kernel.add_plugin(WebSearchEnginePlugin(connector), plugin_name="WebSearch")
Examples:
{{WebSearch.search "What is semantic kernel?"}}
=> Returns the first `num_results` number of results for the given search query
and ignores the first `offset` number of results.
"""
_connector: "ConnectorBase"
def __init__(self, connector: "ConnectorBase") -> None:
"""Initializes a new instance of the WebSearchEnginePlugin class."""
self._connector = connector
@kernel_function(name="search", description="Performs a web search for a given query")
async def search(
self,
query: Annotated[str, "The search query"],
num_results: Annotated[int, "The number of search results to return"] = 1,
offset: Annotated[int, "The number of search results to skip"] = 0,
) -> list[str]:
"""Returns the search results of the query provided."""
return await self._connector.search(query, num_results, offset)