Python: experimental py3.13 support (#10276)

### Motivation and Context

<!-- 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.
-->
Some small fixes in uv setup within actions
Added experimental unit tests runs for py3.13
First typing improvement from pyright and pyright config.

### 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 😄
This commit is contained in:
Eduard van Valkenburg
2025-02-12 13:05:39 +00:00
committed by GitHub
parent 9d5ad78fb2
commit 7bdf55028c
54 changed files with 616 additions and 634 deletions
+1
View File
@@ -21,6 +21,7 @@ jobs:
version: "0.5.x"
enable-cache: true
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
cache-dependency-glob: "**/uv.lock"
- name: Check version
run: |
echo "Building and uploading Python package version: ${{ github.event.release.tag_name }}"
@@ -120,6 +120,7 @@ jobs:
version: "0.5.x"
enable-cache: true
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: |
uv sync --all-extras --dev
@@ -257,6 +258,7 @@ jobs:
version: "0.5.x"
enable-cache: true
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: |
uv sync --all-extras --dev
@@ -321,6 +323,7 @@ jobs:
version: "0.5.x"
enable-cache: true
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: |
uv sync --all-extras --dev
@@ -379,6 +382,7 @@ jobs:
version: "0.5.x"
enable-cache: true
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: |
uv sync --all-extras --dev
+1
View File
@@ -30,6 +30,7 @@ jobs:
version: "0.5.x"
enable-cache: true
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
cache-dependency-glob: "**/uv.lock"
- name: Install the project
run: uv sync --all-extras --dev
- uses: pre-commit/[email protected]
@@ -32,6 +32,7 @@ jobs:
version: "0.5.x"
enable-cache: true
cache-suffix: ${{ runner.os }}-${{ env.UV_PYTHON }}
cache-dependency-glob: "**/uv.lock"
- name: Install the project
run: uv sync --all-extras --dev
- name: Test with pytest
+5 -12
View File
@@ -20,18 +20,10 @@ jobs:
python-version: ["3.10", "3.11", "3.12"]
os: [ubuntu-latest, windows-latest, macos-latest]
experimental: [false]
# include:
# - python-version: "3.13"
# os: "ubuntu-latest"
# experimental: true
# - python-version: "3.13t"
# os: "ubuntu-latest"
# experimental: true
# gil: 0
# - python-version: "3.13t"
# os: "ubuntu-latest"
# experimental: true
# gil: 1
include:
- python-version: "3.13"
os: "ubuntu-latest"
experimental: true
env:
UV_PYTHON: ${{ matrix.python-version }}
permissions:
@@ -47,6 +39,7 @@ jobs:
version: "0.5.x"
enable-cache: true
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
cache-dependency-glob: "**/uv.lock"
- name: Install the project
run: uv sync --all-extras --dev -U --prerelease=if-necessary-or-explicit
- name: Test with pytest
+2 -2
View File
@@ -32,14 +32,14 @@ repos:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.4
rev: v0.9.6
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.5.21
rev: 0.5.30
hooks:
# Update the uv lockfile
- id: uv-lock
+4 -4
View File
@@ -32,7 +32,7 @@ help:
echo ""
echo -e "\033[1mVARIABLES:\033[0m"
echo " PYTHON_VERSION - Python version to use. Default is 3.10"
echo " By default, 3.10, 3.11, and 3.12 are installed as well."
echo " By default, 3.10, 3.11, 3.12 and 3.13 are installed as well."
##############################
# INSTALL
@@ -68,8 +68,8 @@ endif
##############################
.ONESHELL:
install-python:
echo "Installing python 3.10, 3.11, 3.12"
uv python install 3.10 3.11 3.12
echo "Installing python versions"
uv python install 3.10 3.11 3.12 3.13
##############################
# INSTALL-PRE-COMMIT
@@ -87,7 +87,7 @@ install-sk:
echo "Creating and activating venv for python $(PYTHON_VERSION)"
uv venv --python $(PYTHON_VERSION)
echo "Installing Semantic Kernel and all dependencies"
uv sync --all-extras --dev
uv sync --all-extras --dev --prerelease=if-necessary-or-explicit
##############################
# CLEAN
+7 -5
View File
@@ -45,6 +45,7 @@ dependencies = [
"pybars4 ~= 0.9",
"jinja2 ~= 3.1",
"nest-asyncio ~= 1.6",
"scipy>=1.15.1",
]
### Optional dependencies
@@ -62,7 +63,7 @@ chroma = [
]
google = [
"google-cloud-aiplatform == 1.79.0",
"google-generativeai == 0.7"
"google-generativeai ~= 0.8"
]
hugging_face = [
"transformers[torch] ~= 4.28",
@@ -87,7 +88,7 @@ ollama = [
"ollama ~= 0.4"
]
onnx = [
"onnxruntime-genai ~= 0.5"
"onnxruntime-genai ~= 0.5; python_version < '3.13'"
]
anthropic = [
"anthropic ~= 0.32"
@@ -107,11 +108,11 @@ redis = [
"redisvl >= 0.3.6",
]
usearch = [
"usearch ~= 2.9",
"usearch ~= 2.16",
"pyarrow >= 12.0,< 20.0"
]
weaviate = [
"weaviate-client>=4.7,<5.0",
"weaviate-client>=4.10,<5.0",
]
pandas = [
"pandas ~= 2.2"
@@ -139,7 +140,8 @@ dev-dependencies = [
"snoop ~= 0.4",
"mypy >= 1.10",
"types-PyYAML ~= 6.0.12.20240311",
"ruff ~= 0.7",
"ruff ~= 0.9",
"pyright>=1.1.392.post0",
]
environments = [
"sys_platform == 'darwin'",
+27
View File
@@ -0,0 +1,27 @@
{
"reportMissingImports": false,
"include": [
"semantic_kernel/**/*.py"
],
"pythonVersion": "3.10",
"exclude": [
"semantic_kernel/memory/*",
"semantic_kernel/planners/*",
"semantic_kernel/connectors/memory/astradb/*",
"semantic_kernel/connectors/memory/azure_ai_search/*",
"semantic_kernel/connectors/memory/azure_cognitive_search/*",
"semantic_kernel/connectors/memory/azure_cosmosdb/*",
"semantic_kernel/connectors/memory/azure_cosmosdb_no_sql/*",
"semantic_kernel/connectors/memory/chroma/*",
"semantic_kernel/connectors/memory/milvus/*",
"semantic_kernel/connectors/memory/mongodb_atlas/*",
"semantic_kernel/connectors/memory/pinecone/*",
"semantic_kernel/connectors/memory/postgres/*",
"semantic_kernel/connectors/memory/qdrant/*",
"semantic_kernel/connectors/memory/redis/*",
"semantic_kernel/connectors/memory/usearch/*",
"semantic_kernel/connectors/memory/weaviate/*"
],
"venvPath": ".",
"venv": ".venv"
}
-6
View File
@@ -102,12 +102,6 @@
- [Azure Chat GPT with Data API Function Calling](./on_your_data/azure_chat_gpt_with_data_api_function_calling.py)
- [Azure Chat GPT with Data API Vector Search](./on_your_data/azure_chat_gpt_with_data_api_vector_search.py)
### Planners - Showing the uses of [`Planners`](https://github.com/microsoft/semantic-kernel/tree/main/python/semantic_kernel/planners)
- [Sequential Planner](./planners/sequential_planner.py)
- [OpenAI Function Calling Stepwise Planner](./planners/openai_function_calling_stepwise_planner.py)
- [Azure OpenAI Function Calling Stepwise Planner](./planners/azure_openai_function_calling_stepwise_planner.py)
### Plugins - Different ways of creating and using [`Plugins`](https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/functions/kernel_plugin.py)
- [Azure Key Vault Settings](./plugins/azure_key_vault_settings.py)
@@ -67,7 +67,7 @@ chat_completion_service, request_settings = get_chat_completion_service_and_requ
# Configure the function choice behavior. Here, we set it to Auto, where auto_invoke=True by default.
# With `auto_invoke=True`, the model will automatically choose and call functions as needed.
request_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
request_settings.function_choice_behavior = FunctionChoiceBehavior.Auto(filters={"excluded_plugins": ["ChatBot"]})
kernel.add_service(chat_completion_service)
@@ -1,53 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.core_plugins import MathPlugin, TimePlugin
from semantic_kernel.planners import FunctionCallingStepwisePlanner, FunctionCallingStepwisePlannerOptions
async def main():
kernel = Kernel()
service_id = "planner"
kernel.add_service(
AzureChatCompletion(
service_id=service_id,
),
)
plugin_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
"resources",
)
kernel.add_plugin(parent_directory=plugin_path, plugin_name="email_plugin")
kernel.add_plugin(MathPlugin(), "MathPlugin")
kernel.add_plugin(TimePlugin(), "TimePlugin")
questions = [
"What is the current hour number, plus 5?",
"What is 387 minus 22? Email the solution to John and Mary.",
"Write a limerick, translate it to Spanish, and send it to Jane",
]
options = FunctionCallingStepwisePlannerOptions(
max_iterations=10,
max_tokens=4000,
)
planner = FunctionCallingStepwisePlanner(service_id=service_id, options=options)
for question in questions:
result = await planner.invoke(kernel, question)
print(f"Q: {question}\nA: {result.final_answer}\n")
# Uncomment the following line to view the planner's process for completing the request
# print(f"Chat history: {result.chat_history}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,51 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.core_plugins import MathPlugin, TimePlugin
from semantic_kernel.planners import FunctionCallingStepwisePlanner, FunctionCallingStepwisePlannerOptions
async def main():
kernel = Kernel()
service_id = "planner"
kernel.add_service(
OpenAIChatCompletion(
service_id=service_id,
),
)
plugin_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
"resources",
)
kernel.add_plugin(parent_directory=plugin_path, plugin_name="email_plugin")
kernel.add_plugins({"MathPlugin": MathPlugin(), "TimePlugin": TimePlugin()})
questions = [
"What is the current hour number, plus 5?",
"What is 387 minus 22? Email the solution to John and Mary.",
"Write a limerick, translate it to Spanish, and send it to Jane",
]
options = FunctionCallingStepwisePlannerOptions(
max_iterations=10,
max_tokens=4000,
)
planner = FunctionCallingStepwisePlanner(service_id=service_id, options=options)
for question in questions:
result = await planner.invoke(kernel, question)
print(f"Q: {question}\nA: {result.final_answer}\n")
# Uncomment the following line to view the planner's process for completing the request
# print(f"\nChat history: {result.chat_history}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,42 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.core_plugins import MathPlugin, TextPlugin, TimePlugin
from semantic_kernel.planners import SequentialPlanner
async def main():
kernel = Kernel()
service_id = "gpt-3.5"
kernel.add_service(OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo"))
kernel.add_plugins({"math": MathPlugin(), "time": TimePlugin(), "text": TextPlugin()})
# create an instance of sequential planner.
planner = SequentialPlanner(service_id=service_id, kernel=kernel)
# the ask for which the sequential planner is going to find a relevant function.
ask = "What day of the week is today, all uppercase?"
# ask the sequential planner to identify a suitable function from the list of functions available.
plan = await planner.create_plan(goal=ask)
# ask the sequential planner to execute the identified function.
result = await plan.invoke(kernel=kernel)
for step in plan._steps:
print(step.description, ":", step._state.__dict__)
print("Expected Answer:")
print(result)
"""
Output:
SUNDAY
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -17,6 +17,8 @@ if TYPE_CHECKING:
from semantic_kernel.agents import Agent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
NEWLINE = "\n"
@experimental_class
class CustomSelectionStrategy(SelectionStrategy):
@@ -80,7 +82,7 @@ Each message in the chat history contains the agent's name and the message conte
Initially, the chat history may be empty.
Here are the agents with their indices, names, and descriptions:
{"\n".join(f"[{index}] {agent.name}:\n{agent.description}" for index, agent in enumerate(agents))}
{NEWLINE.join(f"[{index}] {agent.name}:{NEWLINE}{agent.description}" for index, agent in enumerate(agents))}
Your task is to select the next agent based on the conversation history.
@@ -20,6 +20,8 @@ if TYPE_CHECKING:
TERMINATE_TRUE_KEYWORD = "yes"
TERMINATE_FALSE_KEYWORD = "no"
NEWLINE = "\n"
class CustomTerminationStrategy(TerminationStrategy):
NUM_OF_RETRIES: ClassVar[int] = 3
@@ -82,7 +84,7 @@ Each message in the chat history contains the agent's name and the message conte
The chat history may start empty as no agents have spoken yet.
Here are the agents with their indices, names, and descriptions:
{"\n".join(f"[{index}] {agent.name}:\n{agent.description}" for index, agent in enumerate(self.agents))}
{NEWLINE.join(f"[{index}] {agent.name}:{NEWLINE}{agent.description}" for index, agent in enumerate(self.agents))}
Your task is NOT to continue the conversation. Determine if the latest content is approved by all agents.
If approved, say "{TERMINATE_TRUE_KEYWORD}". Otherwise, say "{TERMINATE_FALSE_KEYWORD}".
@@ -152,7 +152,7 @@ class Agenda:
return "None"
agenda_str = "\n".join(
[
f"{i+1}. [{format_resource(item['resource'], ResourceConstraintUnit.TURNS)}] {item['title']}"
f"{i + 1}. [{format_resource(item['resource'], ResourceConstraintUnit.TURNS)}] {item['title']}"
for i, item in enumerate(agenda_items)
]
)
@@ -205,7 +205,7 @@ class GCResource:
resource_instructions = ""
if self.resource_constraint.mode == ResourceConstraintMode.EXACT:
exact_mode_instructions = f"""There {'are' if is_plural_remaining else 'is'} {formatted_remaining_resource} remaining (including this one) - the conversation will automatically terminate when 0 turns are left. \
exact_mode_instructions = f"""There {"are" if is_plural_remaining else "is"} {formatted_remaining_resource} remaining (including this one) - the conversation will automatically terminate when 0 turns are left. \
You should continue the conversation until it is automatically terminated. This means you should NOT preemptively end the conversation, \
either explicitly (by selecting the "End conversation" action) or implicitly (e.g. by telling the user that you have all required information and they should wait for the next step). \
Your goal is not to maximize efficiency (i.e. complete the artifact as quickly as possible then end the conversation), but rather to make the best use of ALL remaining turns available to you"""
@@ -127,8 +127,10 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
model = GenerativeModel(
self.service_settings.gemini_model_id,
model_name=self.service_settings.gemini_model_id,
system_instruction=filter_system_message(chat_history),
)
@@ -136,7 +138,7 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
contents=self._prepare_chat_history_for_request(chat_history),
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
tools=settings.tools,
tool_config=settings.tool_config,
tool_config=settings.tool_config, # type: ignore
)
return [self._create_chat_message_content(response, candidate) for candidate in response.candidates]
@@ -154,8 +156,10 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
model = GenerativeModel(
self.service_settings.gemini_model_id,
model_name=self.service_settings.gemini_model_id,
system_instruction=filter_system_message(chat_history),
)
@@ -163,7 +167,7 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
contents=self._prepare_chat_history_for_request(chat_history),
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
tools=settings.tools,
tool_config=settings.tool_config,
tool_config=settings.tool_config, # type: ignore
stream=True,
)
@@ -99,8 +99,10 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
assert isinstance(settings, GoogleAITextPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
model = GenerativeModel(
self.service_settings.gemini_model_id,
model_name=self.service_settings.gemini_model_id,
)
response: AsyncGenerateContentResponse = await model.generate_content_async(
@@ -122,8 +124,10 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
assert isinstance(settings, GoogleAITextPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
model = GenerativeModel(
self.service_settings.gemini_model_id,
model_name=self.service_settings.gemini_model_id,
)
response: AsyncGenerateContentResponse = await model.generate_content_async(
@@ -93,7 +93,9 @@ class GoogleAITextEmbedding(GoogleAIBase, EmbeddingGeneratorBase):
assert isinstance(settings, GoogleAIEmbeddingPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
response: BatchEmbeddingDict = await genai.embed_content_async(
if not self.service_settings.embedding_model_id:
raise ServiceInitializationError("The Google AI embedding model ID is required.")
response: BatchEmbeddingDict = await genai.embed_content_async( # type: ignore
model=self.service_settings.embedding_model_id,
content=texts,
**settings.prepare_settings_dict(),
@@ -2,10 +2,16 @@
from typing import Any
from transformers import GenerationConfig
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
try:
from transformers import GenerationConfig
ready = True
except ImportError:
GenerationConfig = Any
ready = False
class HuggingFacePromptExecutionSettings(PromptExecutionSettings):
"""Hugging Face prompt execution settings."""
@@ -19,8 +25,10 @@ class HuggingFacePromptExecutionSettings(PromptExecutionSettings):
temperature: float = 1.0
top_p: float = 1.0
def get_generation_config(self) -> GenerationConfig:
def get_generation_config(self) -> "GenerationConfig":
"""Get the generation config."""
if not ready:
raise ImportError("transformers is not installed.")
return GenerationConfig(
**self.model_dump(
include={"max_new_tokens", "pad_token_id", "eos_token_id", "temperature", "top_p"},
@@ -11,6 +11,7 @@ if sys.version_info >= (3, 12):
else:
from typing_extensions import override # pragma: no cover
import torch
from transformers import AutoTokenizer, TextIteratorStreamer, pipeline
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
@@ -5,13 +5,18 @@ import os
from collections.abc import AsyncGenerator
from typing import Any
import onnxruntime_genai as OnnxRuntimeGenAi
from semantic_kernel.connectors.ai.onnx.onnx_gen_ai_prompt_execution_settings import OnnxGenAIPromptExecutionSettings
from semantic_kernel.contents import ImageContent
from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidResponseError
from semantic_kernel.kernel_pydantic import KernelBaseModel
try:
import onnxruntime_genai as OnnxRuntimeGenAi
ready = True
except ImportError:
ready = False
class OnnxGenAICompletionBase(KernelBaseModel):
"""Base class for OnnxGenAI Completion services."""
@@ -31,6 +36,8 @@ class OnnxGenAICompletionBase(KernelBaseModel):
Raises:
ServiceInitializationError: When model cannot be loaded
"""
if not ready:
raise ImportError("onnxruntime-genai is not installed.")
try:
json_gen_ai_config = os.path.join(ai_model_path + "/genai_config.json")
with open(json_gen_ai_config) as file:
@@ -39,7 +39,7 @@ class ApiKeyAuthentication(AzureChatRequestBase):
"""API key authentication."""
type: Annotated[Literal["APIKey", "api_key"], AfterValidator(to_snake)] = "api_key"
key: str | None = None
key: str
class SystemAssignedManagedIdentityAuthentication(AzureChatRequestBase):
@@ -68,20 +68,20 @@ class BinaryContent(KernelContent):
ai_model_id (str | None): The id of the AI model that generated this response.
metadata (dict[str, Any]): Any metadata that should be attached to the response.
"""
_data_uri = None
temp_data_uri = None
if data_uri:
_data_uri = DataUri.from_data_uri(data_uri, self.default_mime_type)
temp_data_uri = DataUri.from_data_uri(data_uri, self.default_mime_type)
if "metadata" in kwargs:
kwargs["metadata"].update(_data_uri.parameters)
kwargs["metadata"].update(temp_data_uri.parameters)
else:
kwargs["metadata"] = _data_uri.parameters
kwargs["metadata"] = temp_data_uri.parameters
elif data:
if isinstance(data, str):
_data_uri = DataUri(
temp_data_uri = DataUri(
data_str=data, data_format=data_format, mime_type=mime_type or self.default_mime_type
)
else:
_data_uri = DataUri(
temp_data_uri = DataUri(
data_bytes=data, data_format=data_format, mime_type=mime_type or self.default_mime_type
)
@@ -92,7 +92,7 @@ class BinaryContent(KernelContent):
uri = Url(uri)
super().__init__(uri=uri, **kwargs)
self._data_uri = _data_uri
self._data_uri = temp_data_uri
@computed_field # type: ignore
@property
@@ -59,7 +59,7 @@ class ACASessionsSettings(KernelBaseSettings):
else:
endpoint_parsed = urlsplit(endpoint)._asdict()
if endpoint_parsed["path"]:
endpoint_parsed["path"] = re.sub("/{2,}", "/", endpoint_parsed["path"])
endpoint_parsed["path"] = re.sub(r"/{2,}", "/", endpoint_parsed["path"])
else:
endpoint_parsed["path"] = "/"
return str(urlunsplit(endpoint_parsed.values()))
@@ -5,7 +5,7 @@ import contextlib
import json
import logging
import uuid
from collections.abc import Callable
from collections.abc import Callable, MutableSequence, Sequence
from queue import Queue
from typing import Any
@@ -59,8 +59,8 @@ class ProcessActor(StepActor, ProcessInterface):
super().__init__(ctx, actor_id, kernel, factories)
self.kernel = kernel
self.factories = factories
self.steps: list[StepInterface] = []
self.step_infos: list[DaprStepInfo] = []
self.steps: MutableSequence[StepInterface] = []
self.step_infos: MutableSequence[DaprStepInfo] = []
self.initialize_task: bool | None = False
self.external_event_queue: Queue = Queue()
self.process_task: asyncio.Task | None = None
@@ -133,7 +133,7 @@ class ProcessActor(StepActor, ProcessInterface):
if not self.process_task or self.process_task.done():
self.process_task = asyncio.create_task(self.internal_execute(keep_alive=keep_alive))
async def run_once(self, process_event: str) -> None:
async def run_once(self, process_event: KernelProcessEvent | str | None) -> None:
"""Starts the process with an initial event and waits for it to finish.
Args:
@@ -148,7 +148,9 @@ class ProcessActor(StepActor, ProcessInterface):
actor_interface=ExternalEventBufferInterface,
)
try:
await external_event_queue.enqueue(process_event)
await external_event_queue.enqueue(
process_event.model_dump_json() if isinstance(process_event, KernelProcessEvent) else process_event
)
logger.info(f"Run once for process event: {process_event}")
@@ -172,7 +174,7 @@ class ProcessActor(StepActor, ProcessInterface):
with contextlib.suppress(asyncio.CancelledError):
await self.process_task
async def initialize_step(self):
async def initialize_step(self, input: str) -> None:
"""Initializes the step."""
# The process does not need any further initialization
pass
@@ -218,9 +220,13 @@ class ProcessActor(StepActor, ProcessInterface):
process_state = KernelProcessState(self.name, self.id.id)
step_tasks = [step.to_dapr_step_info() for step in self.steps]
steps = await asyncio.gather(*step_tasks)
steps: Sequence[str] = await asyncio.gather(*step_tasks)
return DaprProcessInfo(
inner_step_python_type=self.inner_step_type, edges=self.process.edges, state=process_state, steps=steps
inner_step_python_type=self.inner_step_type,
edges=self.process.edges,
state=process_state,
# steps are model dumps of the classes, which pydantic can parse back.
steps=steps, # type: ignore
)
async def handle_message(self, message: ProcessMessage) -> None:
@@ -254,7 +260,7 @@ class ProcessActor(StepActor, ProcessInterface):
self.output_edges = {kvp[0]: list(kvp[1]) for kvp in self.process.edges.items()}
for step in self.step_infos:
step_actor = None
step_actor: StepInterface | None = None
# The current step should already have a name.
assert step.state and step.state.name is not None # nosec
@@ -286,7 +292,7 @@ class ProcessActor(StepActor, ProcessInterface):
assert step.state and step.state.id is not None # nosec
scoped_step_id = self._scoped_actor_id(ActorId(step.state.id))
step_actor: StepInterface = ActorProxy.create( # type: ignore
step_actor = ActorProxy.create( # type: ignore
actor_type=f"{StepActor.__name__}",
actor_id=scoped_step_id,
actor_interface=StepInterface,
@@ -332,7 +332,7 @@ class StepActor(Actor, StepInterface, KernelProcessMessageChannel):
raise ProcessFunctionNotFoundException(f"Function {target_function} not found in plugin {self.name}")
invoke_result = None
event_name = None
event_name: str = ""
event_value = None
try:
@@ -340,6 +340,8 @@ class StepActor(Actor, StepInterface, KernelProcessMessageChannel):
f"Invoking plugin `{function.plugin_name}` and function `{function.name}` with arguments: {arguments}"
)
invoke_result = await self.invoke_function(function, self.kernel, arguments)
if invoke_result is None:
raise KernelException(f"Function {target_function} returned None.")
event_name = f"{target_function}.OnResult"
event_value = invoke_result.value
@@ -406,7 +408,9 @@ class StepActor(Actor, StepInterface, KernelProcessMessageChannel):
raise ValueError("The inner step type must be initialized before converting to DaprStepInfo.")
step_info = DaprStepInfo(
inner_step_python_type=self.inner_step_type, state=self.step_info.state, edges=self.step_info.edges
inner_step_python_type=self.inner_step_type,
state=self.step_info.state,
edges=self.step_info.edges,
)
return step_info.model_dump_json()
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import MutableSequence
from typing import Literal
from pydantic import Field
@@ -16,8 +17,8 @@ from semantic_kernel.utils.experimental_decorator import experimental_class
class DaprProcessInfo(DaprStepInfo):
"""A Dapr process info."""
type: Literal["DaprProcessInfo"] = Field("DaprProcessInfo") # type: ignore
steps: list["DaprStepInfo | DaprProcessInfo"] = Field(default_factory=list)
type: Literal["DaprProcessInfo"] = Field(default="DaprProcessInfo") # type: ignore
steps: MutableSequence["DaprStepInfo | DaprProcessInfo"] = Field(default_factory=list)
def to_kernel_process(self) -> KernelProcess:
"""Converts the Dapr process info to a kernel process."""
@@ -40,7 +41,7 @@ class DaprProcessInfo(DaprStepInfo):
raise ValueError("Kernel process must be provided")
dapr_step_info = DaprStepInfo.from_kernel_step_info(kernel_process)
dapr_steps: list[DaprProcessInfo | DaprStepInfo] = []
dapr_steps: MutableSequence[DaprProcessInfo | DaprStepInfo] = []
for step in kernel_process.steps:
if isinstance(step, KernelProcess):
@@ -18,7 +18,7 @@ from semantic_kernel.utils.experimental_decorator import experimental_class
class DaprStepInfo(KernelBaseModel):
"""A Dapr step info."""
type: Literal["DaprStepInfo"] = Field("DaprStepInfo")
type: Literal["DaprStepInfo"] = Field(default="DaprStepInfo")
inner_step_python_type: str
state: KernelProcessStepState
edges: dict[str, list[KernelProcessEdge]] = Field(default_factory=dict)
@@ -2,13 +2,16 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from dapr.actor import ActorInterface, actormethod
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
from semantic_kernel.utils.experimental_decorator import experimental_class
if TYPE_CHECKING:
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
@experimental_class
class ProcessInterface(ActorInterface, ABC):
@@ -35,7 +38,7 @@ class ProcessInterface(ActorInterface, ABC):
@abstractmethod
@actormethod(name="run_once")
async def run_once(self, process_event: KernelProcessEvent) -> None:
async def run_once(self, process_event: "KernelProcessEvent | str | None") -> None:
"""Starts the process with an initial event and then waits for the process to finish.
:param process_event: Required. The KernelProcessEvent to start the process with.
@@ -17,9 +17,9 @@ class KernelProcessStepContext(KernelBaseModel):
def __init__(self, channel: KernelProcessMessageChannel):
"""Initialize the step context."""
super().__init__(step_message_channel=channel)
super().__init__(step_message_channel=channel) # type: ignore
async def emit_event(self, process_event: "KernelProcessEvent | str | Enum", **kwargs) -> None:
async def emit_event(self, process_event: "KernelProcessEvent | str | Enum | None", **kwargs) -> None:
"""Emit an event from the current step.
It is possible to either specify a `KernelProcessEvent` object or the ID of the event
@@ -34,10 +34,9 @@ class KernelProcessStepContext(KernelBaseModel):
if process_event is None:
raise ProcessEventUndefinedException("Process event cannot be None")
if isinstance(process_event, Enum):
process_event = process_event.value
if not isinstance(process_event, KernelProcessEvent):
process_event = KernelProcessEvent(id=process_event, **kwargs)
process_event = KernelProcessEvent(
id=process_event.value if isinstance(process_event, Enum) else process_event, **kwargs
)
await self.step_message_channel.emit_event(process_event)
@@ -36,7 +36,7 @@ class LocalKernelProcessContext(KernelBaseModel):
factories=process.factories,
)
super().__init__(local_process=local_process)
super().__init__(local_process=local_process) # type: ignore
async def start_with_event(self, initial_event: "KernelProcessEvent") -> None:
"""Starts the local process with an initial event."""
@@ -145,7 +145,7 @@ class LocalProcess(LocalStep):
assert step.state and step.state.id is not None # nosec
# Create a LocalStep for the step
local_step = LocalStep(
local_step = LocalStep( # type: ignore
step_info=step,
kernel=self.kernel,
factories=self.factories,
@@ -150,7 +150,7 @@ class LocalStep(KernelProcessMessageChannel, KernelBaseModel):
raise ProcessFunctionNotFoundException(f"Function {target_function} not found in plugin {self.name}")
invoke_result = None
event_name = None
event_name: str = ""
event_value = None
try:
@@ -158,6 +158,8 @@ class LocalStep(KernelProcessMessageChannel, KernelBaseModel):
f"Invoking plugin `{function.plugin_name}` and function `{function.name}` with arguments: {arguments}"
)
invoke_result = await self.invoke_function(function, self.kernel, arguments)
if invoke_result is None:
raise KernelException(f"Function {target_function} returned None.")
event_name = f"{target_function}.OnResult"
event_value = invoke_result.value
except Exception as ex:
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC, abstractmethod
from collections.abc import Sequence
from html import escape
from typing import TYPE_CHECKING
@@ -62,7 +63,7 @@ class PromptTemplateBase(KernelBaseModel, ABC):
allow_dangerously_set_content = True
return allow_dangerously_set_content
def _should_escape(self, name: str, input_variables: list["InputVariable"]) -> bool:
def _should_escape(self, name: str, input_variables: Sequence["InputVariable"]) -> bool:
"""Check if the variable should be escaped.
If the PromptTemplate allows dangerously set content, then the variable will not be escaped,
@@ -1,6 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from collections.abc import Mapping, Sequence
from collections.abc import MutableMapping, MutableSequence, Sequence
from typing import TypeVar
from pydantic import Field, field_validator, model_validator
@@ -16,6 +16,8 @@ PromptExecutionSettingsT = TypeVar("PromptExecutionSettingsT", bound=PromptExecu
logger: logging.Logger = logging.getLogger(__name__)
_T = TypeVar("_T", bound="PromptTemplateConfig")
class PromptTemplateConfig(KernelBaseModel):
"""Configuration for a prompt template.
@@ -37,9 +39,9 @@ class PromptTemplateConfig(KernelBaseModel):
description: str | None = ""
template: str | None = None
template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME
input_variables: list[InputVariable] = Field(default_factory=list)
input_variables: MutableSequence[InputVariable] = Field(default_factory=list)
allow_dangerously_set_content: bool = False
execution_settings: dict[str, PromptExecutionSettings] = Field(default_factory=dict)
execution_settings: MutableMapping[str, PromptExecutionSettings] = Field(default_factory=dict)
@model_validator(mode="after")
def check_input_variables(self):
@@ -52,12 +54,12 @@ class PromptTemplateConfig(KernelBaseModel):
@field_validator("execution_settings", mode="before")
@classmethod
def rewrite_execution_settings(
cls,
cls: type[_T],
settings: PromptExecutionSettings
| Sequence[PromptExecutionSettings]
| Mapping[str, PromptExecutionSettings]
| MutableMapping[str, PromptExecutionSettings]
| None,
) -> Mapping[str, PromptExecutionSettings]:
) -> MutableMapping[str, PromptExecutionSettings]:
"""Rewrite execution settings to a dictionary."""
if not settings:
return {}
@@ -81,14 +83,14 @@ class PromptTemplateConfig(KernelBaseModel):
name=variable.name,
description=variable.description,
default_value=variable.default,
type_=variable.json_schema, # TODO (moonbox3): update to handle complex JSON schemas
type=variable.json_schema, # TODO (moonbox3): update to handle complex JSON schemas
is_required=variable.is_required,
)
for variable in self.input_variables
]
@classmethod
def from_json(cls, json_str: str) -> "PromptTemplateConfig":
def from_json(cls: type[_T], json_str: str) -> _T:
"""Create a PromptTemplateConfig instance from a JSON string."""
if not json_str:
raise ValueError("json_str is empty")
@@ -102,15 +104,15 @@ class PromptTemplateConfig(KernelBaseModel):
@classmethod
def restore(
cls,
cls: type[_T],
name: str,
description: str,
template: str,
template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME,
input_variables: Sequence[InputVariable] = [],
execution_settings: Mapping[str, PromptExecutionSettings] = {},
input_variables: MutableSequence[InputVariable] = [],
execution_settings: MutableMapping[str, PromptExecutionSettings] = {},
allow_dangerously_set_content: bool = False,
) -> "PromptTemplateConfig":
) -> _T:
"""Restore a PromptTemplateConfig instance from the specified parameters.
Args:
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import abstractmethod
from typing import TYPE_CHECKING, Protocol, runtime_checkable
if TYPE_CHECKING:
@@ -11,6 +12,7 @@ if TYPE_CHECKING:
class CodeRenderer(Protocol):
"""Protocol for dynamic code blocks that need async IO to be rendered."""
@abstractmethod
async def render_code(self, kernel: "Kernel", arguments: "KernelArguments") -> str:
"""Render the block using the given context.
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import abstractmethod
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
if TYPE_CHECKING:
@@ -11,6 +12,7 @@ if TYPE_CHECKING:
class TextRenderer(Protocol):
"""Protocol for static (text) blocks that don't need async rendering."""
@abstractmethod
def render(self, kernel: "Kernel", arguments: Optional["KernelArguments"] = None) -> str:
"""Render the block using only the given variables.
@@ -13,6 +13,7 @@ from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.utils.experimental_decorator import experimental_function
@@ -434,9 +435,9 @@ def _set_completion_response(
"message": completion.to_dict(),
}
if hasattr(completion, "finish_reason"):
if isinstance(completion, ChatMessageContent):
full_response["finish_reason"] = completion.finish_reason
if hasattr(completion, "choice_index"):
if isinstance(completion, StreamingContentMixin):
full_response["index"] = completion.choice_index
logger.info(
@@ -5,6 +5,11 @@ import os
import sys
from typing import Annotated
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import pytest
from azure.ai.inference.aio import ChatCompletionsClient
from azure.identity import DefaultAzureCredential
@@ -41,11 +46,6 @@ from semantic_kernel.utils.authentication.entra_id_authentication import get_ent
from tests.integration.completions.completion_test_base import CompletionTestBase, ServiceType
from tests.utils import is_service_setup_for_testing
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
# Make sure all services are setup for before running the tests
# The following exceptions apply:
# 1. OpenAI and Azure OpenAI services are always setup for testing.
@@ -12,10 +12,8 @@ else:
from typing_extensions import override # pragma: no cover
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents import ChatMessageContent, TextContent
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.connectors.ai import PromptExecutionSettings
from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent, TextContent
from semantic_kernel.kernel_pydantic import KernelBaseModel
from tests.integration.completions.chat_completion_test_base import (
ChatCompletionTestBase,
@@ -2,6 +2,7 @@
import sys
from functools import partial
from importlib import util
from typing import Any
if sys.version_info >= (3, 12):
@@ -32,6 +33,9 @@ from semantic_kernel.utils.authentication.entra_id_authentication import get_ent
from tests.integration.completions.completion_test_base import CompletionTestBase, ServiceType
from tests.utils import is_service_setup_for_testing, is_test_running_on_supported_platforms, retry
hugging_face_setup = util.find_spec("torch") is not None
azure_openai_setup = True
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_TEXT_MODEL_ID"]) and is_test_running_on_supported_platforms([
"Linux"
@@ -219,7 +223,9 @@ class TestTextCompletion(CompletionTestBase):
service_id="patrickvonplaten/t5-tiny-random",
ai_model_id="patrickvonplaten/t5-tiny-random",
task="text2text-generation",
),
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"hf_summ": (
@@ -227,7 +233,9 @@ class TestTextCompletion(CompletionTestBase):
service_id="jotamunz/billsum_tiny_summarization",
ai_model_id="jotamunz/billsum_tiny_summarization",
task="summarization",
),
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"hf_gen": (
@@ -235,7 +243,9 @@ class TestTextCompletion(CompletionTestBase):
service_id="HuggingFaceM4/tiny-random-LlamaForCausalLM",
ai_model_id="HuggingFaceM4/tiny-random-LlamaForCausalLM",
task="text-generation",
),
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"onnx_gen_ai": (
@@ -733,7 +733,7 @@ async def test_openapi_put_light_by_id(kernel: Kernel):
assert request_content.get("method") == "PUT"
assert request_content.get("url") == "https://127.0.0.1/Lights/1"
assert request_content.get("body") == '{"hexColor": "11EE11"}'
assert request_content.get("body") == '{"hexColor":"11EE11"}'
# endregion
@@ -1,5 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from importlib import util
import pytest
from azure.ai.inference.aio import EmbeddingsClient
from azure.identity import DefaultAzureCredential
@@ -32,6 +34,8 @@ from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecut
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
from tests.utils import is_service_setup_for_testing
hugging_face_setup = util.find_spec("torch") is not None
# Make sure all services are setup for before running the tests
# The following exceptions apply:
# 1. OpenAI and Azure OpenAI services are always setup for testing.
@@ -91,7 +95,9 @@ class EmbeddingServiceTestBase:
PromptExecutionSettings,
),
"hugging_face": (
HuggingFaceTextEmbedding(ai_model_id="sentence-transformers/all-MiniLM-L6-v2"),
HuggingFaceTextEmbedding(ai_model_id="sentence-transformers/all-MiniLM-L6-v2")
if hugging_face_setup
else None,
PromptExecutionSettings,
),
"ollama": (OllamaTextEmbedding() if ollama_setup else None, OllamaEmbeddingPromptExecutionSettings),
-31
View File
@@ -34,13 +34,6 @@ from samples.concepts.local_models.lm_studio_text_embedding import main as lm_st
from samples.concepts.local_models.ollama_chat_completion import main as ollama_chat_completion
from samples.concepts.memory.azure_cognitive_search_memory import main as azure_cognitive_search_memory
from samples.concepts.memory.memory import main as memory
from samples.concepts.planners.azure_openai_function_calling_stepwise_planner import (
main as azure_openai_function_calling_stepwise_planner,
)
from samples.concepts.planners.openai_function_calling_stepwise_planner import (
main as openai_function_calling_stepwise_planner,
)
from samples.concepts.planners.sequential_planner import main as sequential_planner
from samples.concepts.plugins.openai_function_calling_with_custom_plugin import (
main as openai_function_calling_with_custom_plugin,
)
@@ -161,30 +154,6 @@ concepts = [
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
azure_openai_function_calling_stepwise_planner,
[],
id="azure_openai_function_calling_stepwise_planner",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
openai_function_calling_stepwise_planner,
[],
id="openai_function_calling_stepwise_planner",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
sequential_planner,
[],
id="sequential_planner",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
openai_function_calling_with_custom_plugin,
[],
@@ -9,7 +9,6 @@ from samples.learn_resources.ai_services import main as ai_services
from samples.learn_resources.configuring_prompts import main as configuring_prompts
from samples.learn_resources.creating_functions import main as creating_functions
from samples.learn_resources.functions_within_prompts import main as functions_within_prompts
from samples.learn_resources.planner import main as planner
from samples.learn_resources.plugin import main as plugin
from samples.learn_resources.serializing_prompts import main as serializing_prompts
from samples.learn_resources.templates import main as templates
@@ -54,14 +53,6 @@ learn_resources = [
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
planner,
[],
id="planner",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
plugin,
[],
@@ -11,6 +11,15 @@ from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvali
from semantic_kernel.kernel import Kernel
from tests.unit.connectors.ai.onnx.conftest import gen_ai_config, gen_ai_config_vision
try:
import onnxruntime_genai # noqa: F401
ready = True
except ImportError:
ready = False
pytestmark = pytest.mark.skipif(not ready, reason="ONNX Runtime is not installed.")
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@@ -12,6 +12,15 @@ from semantic_kernel.contents import TextContent
from semantic_kernel.exceptions import ServiceInitializationError
from tests.unit.connectors.ai.onnx.conftest import gen_ai_config
try:
import onnxruntime_genai # noqa: F401
ready = True
except ImportError:
ready = False
pytestmark = pytest.mark.skipif(not ready, reason="ONNX Runtime is not installed.")
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@@ -311,6 +311,20 @@ def test_azure_open_ai_chat_prompt_execution_settings_with_aisearch_data_sources
pytest.param({"type": "access_token"}, marks=pytest.mark.xfail),
pytest.param({"type": "invalid"}, marks=pytest.mark.xfail),
],
ids=[
"APIKey",
"api_key",
"api_key_no_key",
"SystemAssignedManagedIdentity",
"system_assigned_managed_identity",
"UserAssignedManagedIdentity",
"user_assigned_managed_identity",
"user_assigned_managed_identity_no_id",
"AccessToken",
"access_token",
"access_token_no_token",
"invalid",
],
)
def test_aisearch_data_source_parameters(authentication) -> None:
AzureAISearchDataSourceParameters(index_name="test_index", authentication=authentication)
+9 -6
View File
@@ -113,14 +113,14 @@ def kernel_factory() -> t.Callable[[t.Type[_Serializable]], _Serializable]:
PROTOCOLS = [
pytest.param(ConversationSummaryPlugin, marks=pytest.mark.xfail(reason="Contains data")),
ConversationSummaryPlugin,
HttpPlugin,
MathPlugin,
TextMemoryPlugin,
TextPlugin,
TimePlugin,
WaitPlugin,
pytest.param(WebSearchEnginePlugin, marks=pytest.mark.xfail(reason="Contains data")),
WebSearchEnginePlugin,
]
BASE_CLASSES = [
@@ -146,17 +146,20 @@ PYDANTIC_MODELS = [
KernelParameterMetadata,
KernelFunctionMetadata,
ChatHistory,
]
KERNEL_FUNCTION_OPTIONAL = [KernelFunction]
KERNEL_FUNCTION_REQUIRED = [
pytest.param(
KernelFunction,
marks=pytest.mark.xfail(reason="Need to implement Pickle serialization."),
),
)
]
class TestUsageInPydanticFields:
@pytest.mark.parametrize(
"kernel_type",
BASE_CLASSES + PROTOCOLS + ENUMS + PYDANTIC_MODELS + STATELESS_CLASSES,
BASE_CLASSES + PROTOCOLS + ENUMS + PYDANTIC_MODELS + STATELESS_CLASSES + KERNEL_FUNCTION_OPTIONAL,
)
def test_usage_as_optional_field(
self,
@@ -170,11 +173,11 @@ class TestUsageInPydanticFields:
class TestModel(KernelBaseModel):
"""A test model."""
field: t.Optional[kernel_type] = None
field: kernel_type | None = None
assert_serializable(TestModel(), TestModel)
@pytest.mark.parametrize("kernel_type", PYDANTIC_MODELS + STATELESS_CLASSES)
@pytest.mark.parametrize("kernel_type", PYDANTIC_MODELS + STATELESS_CLASSES + KERNEL_FUNCTION_REQUIRED)
def test_usage_as_required_field(
self,
kernel_factory: t.Callable[[t.Type[KernelBaseModelFieldT]], KernelBaseModelFieldT],
+372 -330
View File
File diff suppressed because it is too large Load Diff