Python: further improvements to checks and docs in dev_setup (#6445)

### 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.
-->
Added a section on documentation to the DEV_SETUP
Added a check for the copyright row to pre-commit
Closes #5629

### 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
2024-05-31 13:06:41 +00:00
committed by GitHub
parent 377287f4a7
commit fbb26b5cf6
190 changed files with 1884 additions and 1928 deletions
@@ -49,7 +49,6 @@ class ChatCompletionClientBase(AIServiceClientBase, ABC):
settings (PromptExecutionSettings): Settings for the request.
kwargs (Dict[str, Any]): The optional arguments.
Yields:
A stream representing the response(s) from the LLM.
"""
@@ -185,7 +185,6 @@ class RequiredFunction(FunctionCallBehavior):
if not self.enable_kernel_functions:
return
# since using this always calls this single function, we do not want to allow repeated calls
# TODO: reevaluate when other models support function calling then OpenAI.
if self.max_auto_invoke_attempts > 1:
self.max_auto_invoke_attempts = 1
update_settings_callback(
@@ -15,9 +15,9 @@ from semantic_kernel.connectors.ai.google_palm.services.gp_text_embedding import
)
__all__ = [
"GooglePalmTextCompletion",
"GooglePalmChatCompletion",
"GooglePalmTextEmbedding",
"GooglePalmChatPromptExecutionSettings",
"GooglePalmTextCompletion",
"GooglePalmTextEmbedding",
"GooglePalmTextPromptExecutionSettings",
]
@@ -8,7 +8,7 @@ from pydantic import Field, model_validator
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError
# TODO: replace back with google types once pydantic issue is fixed.
# TODO (eavanvalkenburg): replace back with google types once pydantic issue is fixed.
MessagesOptions = list[dict[str, Any]]
MessagePromptOption = Union[str, dict[str, Any]]
@@ -41,8 +41,5 @@ class GooglePalmChatPromptExecutionSettings(GooglePalmPromptExecutionSettings):
@model_validator(mode="after")
def validate_input(self):
"""Validate input."""
if self.prompt is not None:
if self.messages or self.context or self.examples:
raise ServiceInvalidExecutionSettingsError(
"Prompt cannot be used without messages, context or examples"
)
if self.prompt is not None and (self.messages or self.context or self.examples):
raise ServiceInvalidExecutionSettingsError("Prompt cannot be used without messages, context or examples")
@@ -121,7 +121,7 @@ class GooglePalmChatCompletion(ChatCompletionClientBase, TextCompletionClientBas
inner_content=response,
ai_model_id=self.ai_model_id,
metadata=metadata,
role=int_to_role[int(candidate.get("author"))], # TODO: why is author coming back as '1'?
role=int_to_role[int(candidate.get("author"))], # TODO (moonbox3): why is author coming back as '1'?
content=candidate.get("content"),
)
@@ -11,7 +11,7 @@ from semantic_kernel.connectors.ai.hugging_face.services.hf_text_embedding impor
)
__all__ = [
"HuggingFacePromptExecutionSettings",
"HuggingFaceTextCompletion",
"HuggingFaceTextEmbedding",
"HuggingFacePromptExecutionSettings",
]
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from transformers import GenerationConfig
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -6,8 +6,8 @@ from semantic_kernel.connectors.ai.ollama.services.ollama_text_completion import
from semantic_kernel.connectors.ai.ollama.services.ollama_text_embedding import OllamaTextEmbedding
__all__ = [
"OllamaChatCompletion",
"OllamaPromptExecutionSettings",
"OllamaTextCompletion",
"OllamaChatCompletion",
"OllamaTextEmbedding",
]
@@ -56,18 +56,20 @@ class OllamaChatCompletion(TextCompletionClientBase, ChatCompletionClientBase):
settings.ai_model_id = self.ai_model_id
settings.messages = self._prepare_chat_history_for_request(chat_history)
settings.stream = False
async with AsyncSession(self.session) as session:
async with session.post(str(self.url), json=settings.prepare_settings_dict()) as response:
response.raise_for_status()
response_object = await response.json()
return [
ChatMessageContent(
inner_content=response_object,
ai_model_id=self.ai_model_id,
role="assistant",
content=response_object.get("message", {"content": None}).get("content", None),
)
]
async with (
AsyncSession(self.session) as session,
session.post(str(self.url), json=settings.prepare_settings_dict()) as response,
):
response.raise_for_status()
response_object = await response.json()
return [
ChatMessageContent(
inner_content=response_object,
ai_model_id=self.ai_model_id,
role="assistant",
content=response_object.get("message", {"content": None}).get("content", None),
)
]
async def get_streaming_chat_message_contents(
self,
@@ -92,24 +94,26 @@ class OllamaChatCompletion(TextCompletionClientBase, ChatCompletionClientBase):
settings.ai_model_id = self.ai_model_id
settings.messages = self._prepare_chat_history_for_request(chat_history)
settings.stream = True
async with AsyncSession(self.session) as session:
async with session.post(str(self.url), json=settings.prepare_settings_dict()) as response:
response.raise_for_status()
async for line in response.content:
body = json.loads(line)
if body.get("done") and body.get("message", {}).get("content") is None:
break
yield [
StreamingChatMessageContent(
role="assistant",
choice_index=0,
inner_content=body,
ai_model_id=self.ai_model_id,
content=body.get("message", {"content": None}).get("content", None),
)
]
if body.get("done"):
break
async with (
AsyncSession(self.session) as session,
session.post(str(self.url), json=settings.prepare_settings_dict()) as response,
):
response.raise_for_status()
async for line in response.content:
body = json.loads(line)
if body.get("done") and body.get("message", {}).get("content") is None:
break
yield [
StreamingChatMessageContent(
role="assistant",
choice_index=0,
inner_content=body,
ai_model_id=self.ai_model_id,
content=body.get("message", {"content": None}).get("content", None),
)
]
if body.get("done"):
break
async def get_text_contents(
self,
@@ -129,17 +133,19 @@ class OllamaChatCompletion(TextCompletionClientBase, ChatCompletionClientBase):
settings.ai_model_id = self.ai_model_id
settings.messages = [{"role": "user", "content": prompt}]
settings.stream = False
async with AsyncSession(self.session) as session:
async with session.post(str(self.url), json=settings.prepare_settings_dict()) as response:
response.raise_for_status()
response_object = await response.json()
return [
TextContent(
inner_content=response_object,
ai_model_id=self.ai_model_id,
text=response_object.get("message", {"content": None}).get("content", None),
)
]
async with (
AsyncSession(self.session) as session,
session.post(str(self.url), json=settings.prepare_settings_dict()) as response,
):
response.raise_for_status()
response_object = await response.json()
return [
TextContent(
inner_content=response_object,
ai_model_id=self.ai_model_id,
text=response_object.get("message", {"content": None}).get("content", None),
)
]
async def get_streaming_text_contents(
self,
@@ -161,23 +167,25 @@ class OllamaChatCompletion(TextCompletionClientBase, ChatCompletionClientBase):
settings.ai_model_id = self.ai_model_id
settings.messages = [{"role": "user", "content": prompt}]
settings.stream = True
async with AsyncSession(self.session) as session:
async with session.post(str(self.url), json=settings.prepare_settings_dict()) as response:
response.raise_for_status()
async for line in response.content:
body = json.loads(line)
if body.get("done") and body.get("message", {}).get("content") is None:
break
yield [
StreamingTextContent(
choice_index=0,
inner_content=body,
ai_model_id=self.ai_model_id,
text=body.get("message", {"content": None}).get("content", None),
)
]
if body.get("done"):
break
async with (
AsyncSession(self.session) as session,
session.post(str(self.url), json=settings.prepare_settings_dict()) as response,
):
response.raise_for_status()
async for line in response.content:
body = json.loads(line)
if body.get("done") and body.get("message", {}).get("content") is None:
break
yield [
StreamingTextContent(
choice_index=0,
inner_content=body,
ai_model_id=self.ai_model_id,
text=body.get("message", {"content": None}).get("content", None),
)
]
if body.get("done"):
break
def get_prompt_execution_settings_class(self) -> "OllamaChatPromptExecutionSettings":
"""Get the request settings class."""
@@ -48,12 +48,14 @@ class OllamaTextCompletion(TextCompletionClientBase):
settings.ai_model_id = self.ai_model_id
settings.prompt = prompt
settings.stream = False
async with AsyncSession(self.session) as session:
async with session.post(self.url, json=settings.prepare_settings_dict()) as response:
response.raise_for_status()
inner_content = await response.json()
text = inner_content["response"]
return [TextContent(inner_content=inner_content, ai_model_id=self.ai_model_id, text=text)]
async with (
AsyncSession(self.session) as session,
session.post(self.url, json=settings.prepare_settings_dict()) as response,
):
response.raise_for_status()
inner_content = await response.json()
text = inner_content["response"]
return [TextContent(inner_content=inner_content, ai_model_id=self.ai_model_id, text=text)]
async def get_streaming_text_contents(
self,
@@ -76,20 +78,22 @@ class OllamaTextCompletion(TextCompletionClientBase):
settings.ai_model_id = self.ai_model_id
settings.prompt = prompt
settings.stream = True
async with AsyncSession(self.session) as session:
async with session.post(self.url, json=settings.prepare_settings_dict()) as response:
response.raise_for_status()
async for line in response.content:
body = json.loads(line)
if body.get("done") and body.get("response") is None:
break
yield [
StreamingTextContent(
choice_index=0, inner_content=body, ai_model_id=self.ai_model_id, text=body.get("response")
)
]
if body.get("done"):
break
async with (
AsyncSession(self.session) as session,
session.post(self.url, json=settings.prepare_settings_dict()) as response,
):
response.raise_for_status()
async for line in response.content:
body = json.loads(line)
if body.get("done") and body.get("response") is None:
break
yield [
StreamingTextContent(
choice_index=0, inner_content=body, ai_model_id=self.ai_model_id, text=body.get("response")
)
]
if body.get("done"):
break
def get_prompt_execution_settings_class(self) -> "OllamaTextPromptExecutionSettings":
"""Get the request settings class."""
@@ -39,12 +39,11 @@ class OllamaTextEmbedding(EmbeddingGeneratorBase):
async def generate_embeddings(self, texts: list[str], **kwargs: Any) -> ndarray:
result = []
for text in texts:
async with AsyncSession(self.session) as session:
async with session.post(
self.url,
json={"model": self.ai_model_id, "prompt": text, "options": kwargs},
) as response:
response.raise_for_status()
response = await response.json()
result.append(response["embedding"])
async with AsyncSession(self.session) as session, session.post(
self.url,
json={"model": self.ai_model_id, "prompt": text, "options": kwargs},
) as response:
response.raise_for_status()
response = await response.json()
result.append(response["embedding"])
return array(result)
@@ -26,25 +26,25 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion impo
from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding import OpenAITextEmbedding
__all__ = [
"OpenAIPromptExecutionSettings",
"OpenAIChatPromptExecutionSettings",
"OpenAITextPromptExecutionSettings",
"AzureChatPromptExecutionSettings",
"OpenAITextCompletion",
"OpenAIChatCompletion",
"OpenAITextEmbedding",
"AzureTextCompletion",
"AzureChatCompletion",
"AzureTextEmbedding",
"ApiKeyAuthentication",
"AzureAISearchDataSource",
"AzureAISearchDataSourceParameters",
"AzureChatCompletion",
"AzureChatPromptExecutionSettings",
"AzureCosmosDBDataSource",
"AzureCosmosDBDataSourceParameters",
"AzureDataSourceParameters",
"ApiKeyAuthentication",
"AzureEmbeddingDependency",
"AzureTextCompletion",
"AzureTextEmbedding",
"ConnectionStringAuthentication",
"DataSourceFieldsMapping",
"ExtraBody",
"AzureEmbeddingDependency",
"DataSourceFieldsMapping",
"ExtraBody",
"OpenAIChatCompletion",
"OpenAIChatPromptExecutionSettings",
"OpenAIPromptExecutionSettings",
"OpenAITextCompletion",
"OpenAITextEmbedding",
"OpenAITextPromptExecutionSettings",
]
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from typing import Annotated, Any, Literal, Union
@@ -125,15 +125,15 @@ class AzureChatCompletion(AzureOpenAIConfigBase, OpenAIChatCompletionBase, OpenA
"""
return AzureChatCompletion(
service_id=settings.get("service_id"),
api_key=settings.get("api_key", None),
deployment_name=settings.get("deployment_name", None),
endpoint=settings.get("endpoint", None),
base_url=settings.get("base_url", None),
api_version=settings.get("api_version", None),
api_key=settings.get("api_key"),
deployment_name=settings.get("deployment_name"),
endpoint=settings.get("endpoint"),
base_url=settings.get("base_url"),
api_version=settings.get("api_version"),
ad_token=settings.get("ad_token"),
ad_token_provider=settings.get("ad_token_provider"),
default_headers=settings.get("default_headers"),
env_file_path=settings.get("env_file_path", None),
env_file_path=settings.get("env_file_path"),
)
def get_prompt_execution_settings_class(self) -> "PromptExecutionSettings":
@@ -181,10 +181,7 @@ class AzureChatCompletion(AzureOpenAIConfigBase, OpenAIChatCompletionBase, OpenA
def _get_tool_message_from_chat_choice(self, choice: Choice | ChunkChoice) -> str | None:
"""Get the tool message from a choice."""
if isinstance(choice, Choice):
content = choice.message
else:
content = choice.delta
content = choice.message if isinstance(choice, Choice) else choice.delta
if content.model_extra is not None and "context" in content.model_extra:
return json.dumps(content.model_extra["context"])
@@ -100,13 +100,13 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
"""
return AzureTextCompletion(
service_id=settings.get("service_id"),
api_key=settings.get("api_key", None),
deployment_name=settings.get("deployment_name", None),
endpoint=settings.get("endpoint", None),
base_url=settings.get("base_url", None),
api_version=settings.get("api_version", None),
api_key=settings.get("api_key"),
deployment_name=settings.get("deployment_name"),
endpoint=settings.get("endpoint"),
base_url=settings.get("base_url"),
api_version=settings.get("api_version"),
ad_token=settings.get("ad_token"),
ad_token_provider=settings.get("ad_token_provider"),
default_headers=settings.get("default_headers"),
env_file_path=settings.get("env_file_path", None),
env_file_path=settings.get("env_file_path"),
)
@@ -105,13 +105,13 @@ class AzureTextEmbedding(AzureOpenAIConfigBase, OpenAITextEmbeddingBase):
"""
return AzureTextEmbedding(
service_id=settings.get("service_id"),
api_key=settings.get("api_key", None),
deployment_name=settings.get("deployment_name", None),
endpoint=settings.get("endpoint", None),
base_url=settings.get("base_url", None),
api_version=settings.get("api_version", None),
api_key=settings.get("api_key"),
deployment_name=settings.get("deployment_name"),
endpoint=settings.get("endpoint"),
base_url=settings.get("base_url"),
api_version=settings.get("api_version"),
ad_token=settings.get("ad_token"),
ad_token_provider=settings.get("ad_token_provider"),
default_headers=settings.get("default_headers"),
env_file_path=settings.get("env_file_path", None),
env_file_path=settings.get("env_file_path"),
)
@@ -260,10 +260,7 @@ class OpenAIChatCompletionBase(OpenAIHandler, ChatCompletionClientBase):
"""Send the chat request."""
response = await self._send_request(request_settings=settings)
response_metadata = self._get_metadata_from_chat_response(response)
completions = [
self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices
]
return completions
return [self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices]
async def _send_chat_stream_request(
self, settings: OpenAIChatPromptExecutionSettings
@@ -353,10 +350,7 @@ class OpenAIChatCompletionBase(OpenAIHandler, ChatCompletionClientBase):
def _get_tool_calls_from_chat_choice(self, choice: Choice | ChunkChoice) -> list[FunctionCallContent]:
"""Get tool calls from a chat choice."""
if isinstance(choice, Choice):
content = choice.message
else:
content = choice.delta
content = choice.message if isinstance(choice, Choice) else choice.delta
if content.tool_calls is None:
return []
return [
@@ -371,10 +365,7 @@ class OpenAIChatCompletionBase(OpenAIHandler, ChatCompletionClientBase):
def _get_function_call_from_chat_choice(self, choice: Choice | ChunkChoice) -> list[FunctionCallContent]:
"""Get a function call from a chat choice."""
if isinstance(choice, Choice):
content = choice.message
else:
content = choice.delta
content = choice.message if isinstance(choice, Choice) else choice.delta
if content.function_call is None:
return []
return [
@@ -442,18 +433,20 @@ class OpenAIChatCompletionBase(OpenAIHandler, ChatCompletionClientBase):
result="The tool call arguments are malformed, please try again.",
)
chat_history.add_message(message=frc.to_chat_message_content())
return
return None
logger.info(f"Calling {function_call.name} function with args: {function_call.arguments}")
try:
if function_call.name is None:
raise ValueError("The function name is required.")
if isinstance(function_call_behavior, RequiredFunction):
if function_call.name != function_call_behavior.function_fully_qualified_name:
raise ValueError(
f"Only function: {function_call_behavior.function_fully_qualified_name} "
f"is allowed, {function_call.name} is not allowed."
)
if (
isinstance(function_call_behavior, RequiredFunction)
and function_call.name != function_call_behavior.function_fully_qualified_name
):
raise ValueError(
f"Only function: {function_call_behavior.function_fully_qualified_name} "
f"is allowed, {function_call.name} is not allowed."
)
if isinstance(function_call_behavior, EnabledFunctions):
enabled_functions = [
func.fully_qualified_name
@@ -471,7 +464,7 @@ class OpenAIChatCompletionBase(OpenAIHandler, ChatCompletionClientBase):
result="The tool call could not be found, please try again and make sure to validate the name.",
)
chat_history.add_message(message=frc.to_chat_message_content())
return
return None
num_required_func_params = len([param for param in function_to_call.parameters if param.is_required])
if len(parsed_args) < num_required_func_params:
@@ -487,7 +480,7 @@ class OpenAIChatCompletionBase(OpenAIHandler, ChatCompletionClientBase):
result=msg,
)
chat_history.add_message(message=frc.to_chat_message_content())
return
return None
_rebuild_auto_function_invocation_context()
invocation_context = AutoFunctionInvocationContext(
@@ -515,6 +508,7 @@ class OpenAIChatCompletionBase(OpenAIHandler, ChatCompletionClientBase):
function_call_content=function_call, result=invocation_context.function_result
)
chat_history.add_message(message=frc.to_chat_message_content())
return None
async def _inner_auto_function_invoke_handler(self, context: AutoFunctionInvocationContext):
"""Inner auto function invocation handler."""
@@ -74,7 +74,7 @@ class OpenAIHandler(KernelBaseModel, ABC):
response = await self.client.embeddings.create(**settings.prepare_settings_dict())
self.store_usage(response)
# make numpy arrays from the response
# TODO: the openai response is cast to a list[float], could be used instead of ndarray
# TODO (eavanvalkenburg): the openai response is cast to a list[float], could be used instead of ndarray
return [array(x.embedding) for x in response.data]
except Exception as ex:
raise ServiceResponseException(
@@ -79,10 +79,10 @@ class OpenAITextCompletion(OpenAITextCompletionBase, OpenAIConfigBase):
if "default_headers" in settings and isinstance(settings["default_headers"], str):
settings["default_headers"] = json.loads(settings["default_headers"])
return OpenAITextCompletion(
ai_model_id=settings.get("ai_model_id", None),
api_key=settings.get("api_key", None),
org_id=settings.get("org_id", None),
ai_model_id=settings.get("ai_model_id"),
api_key=settings.get("api_key"),
org_id=settings.get("org_id"),
service_id=settings.get("service_id"),
default_headers=settings.get("default_headers"),
env_file_path=settings.get("env_file_path", None),
env_file_path=settings.get("env_file_path"),
)
@@ -79,9 +79,9 @@ class OpenAITextEmbedding(OpenAIConfigBase, OpenAITextEmbeddingBase):
"""
return OpenAITextEmbedding(
ai_model_id=settings["ai_model_id"],
api_key=settings.get("api_key", None),
org_id=settings.get("org_id", None),
api_key=settings.get("api_key"),
org_id=settings.get("org_id"),
service_id=settings.get("service_id"),
default_headers=settings.get("default_headers"),
env_file_path=settings.get("env_file_path", None),
env_file_path=settings.get("env_file_path"),
)
@@ -30,7 +30,7 @@ class OpenAITextEmbeddingBase(OpenAIHandler, EmbeddingGeneratorBase):
raw_embeddings = []
batch_size = batch_size or len(texts)
for i in range(0, len(texts), batch_size):
batch = texts[i : i + batch_size] # noqa: E203
batch = texts[i : i + batch_size]
settings.input = batch
raw_embedding = await self._send_embedding_request(
settings=settings,
@@ -11,10 +11,7 @@ from semantic_kernel.utils.experimental_decorator import experimental_class
ASTRA_CALLER_IDENTITY: str
SEMANTIC_KERNEL_VERSION = APP_INFO.get("Semantic-Kernel-Version")
if SEMANTIC_KERNEL_VERSION:
ASTRA_CALLER_IDENTITY = f"semantic-kernel/{SEMANTIC_KERNEL_VERSION}"
else:
ASTRA_CALLER_IDENTITY = "semantic-kernel"
ASTRA_CALLER_IDENTITY = f"semantic-kernel/{SEMANTIC_KERNEL_VERSION}" if SEMANTIC_KERNEL_VERSION else "semantic-kernel"
@experimental_class
@@ -48,16 +45,16 @@ class AstraClient:
self._session = session
async def _run_query(self, request_url: str, query: dict):
async with AsyncSession(self._session) as session:
async with session.post(request_url, data=json.dumps(query), headers=self.request_header) as response:
if response.status == 200:
response_dict = await response.json()
if "errors" in response_dict:
raise ServiceResponseException(f"Astra DB request error - {response_dict['errors']}")
else:
return response_dict
else:
raise ServiceResponseException(f"Astra DB not available. Status : {response}")
async with (
AsyncSession(self._session) as session,
session.post(request_url, data=json.dumps(query), headers=self.request_header) as response,
):
if response.status == 200:
response_dict = await response.json()
if "errors" in response_dict:
raise ServiceResponseException(f"Astra DB request error - {response_dict['errors']}")
return response_dict
raise ServiceResponseException(f"Astra DB not available. Status : {response}")
async def find_collections(self, include_detail: bool = True):
"""Finds all collections in the keyspace."""
@@ -94,13 +91,13 @@ class AstraClient:
}
}
result = await self._run_query(self.request_base_url, query)
return True if result["status"]["ok"] == 1 else False
return result["status"]["ok"] == 1
async def delete_collection(self, collection_name: str):
"""Deletes a collection from the keyspace."""
query = {"deleteCollection": {"name": collection_name}}
result = await self._run_query(self.request_base_url, query)
return True if result["status"]["ok"] == 1 else False
return result["status"]["ok"] == 1
def _build_request_collection_url(self, collection_name: str):
return f"{self.request_base_url}/{collection_name}"
@@ -74,7 +74,7 @@ class AstraDBMemoryStore(MemoryStoreBase):
if self._embedding_dim > MAX_DIMENSIONALITY:
raise MemoryConnectorInitializationError(
f"Dimensionality of {self._embedding_dim} exceeds "
+ f"the maximum allowed value of {MAX_DIMENSIONALITY}."
f"the maximum allowed value of {MAX_DIMENSIONALITY}."
)
self._client = AstraClient(
@@ -170,7 +170,7 @@ class AstraDBMemoryStore(MemoryStoreBase):
update = {"$set": build_payload(record)}
status = await self._client.update_document(collection_name, filter, update, True)
return status["upsertedId"] if "upsertedId" in status else record._id
return status.get("upsertedId", record._id)
async def upsert_batch(self, collection_name: str, records: list[MemoryRecord]) -> list[str]:
"""Upsert a batch of memory records into the data store.
@@ -36,9 +36,9 @@ def build_payload(record: MemoryRecord) -> dict[str, Any]:
def parse_payload(document: dict[str, Any]) -> MemoryRecord:
"""Parses a record from AstraDb into a MemoryRecord."""
text = document.get("text", None)
description = document["description"] if "description" in document else None
additional_metadata = document["additional_metadata"] if "additional_metadata" in document else None
text = document.get("text")
description = document.get("description")
additional_metadata = document.get("additional_metadata")
return MemoryRecord.local_record(
id=document["_id"],
@@ -5,4 +5,4 @@ from semantic_kernel.connectors.memory.azure_cognitive_search.azure_cognitive_se
AzureCognitiveSearchMemoryStore,
)
__all__ = ["AzureCognitiveSearchMemoryStore", "AzureAISearchSettings"]
__all__ = ["AzureAISearchSettings", "AzureCognitiveSearchMemoryStore"]
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
import contextlib
import logging
import uuid
from inspect import isawaitable
@@ -149,10 +150,8 @@ class AzureCognitiveSearchMemoryStore(MemoryStoreBase):
# Check to see if collection exists
collection_index = None
try:
with contextlib.suppress(ResourceNotFoundError):
collection_index = await self._search_index_client.get_index(collection_name.lower())
except ResourceNotFoundError:
pass
if not collection_index:
# Create the search index with the semantic settings
@@ -204,10 +203,7 @@ class AzureCognitiveSearchMemoryStore(MemoryStoreBase):
try:
collection_result = await self._search_index_client.get_index(name=collection_name.lower())
if collection_result:
return True
else:
return False
return bool(collection_result)
except ResourceNotFoundError:
return False
@@ -259,8 +255,7 @@ class AzureCognitiveSearchMemoryStore(MemoryStoreBase):
if result[0].succeeded:
return search_ids
else:
return None
return None
async def get(self, collection_name: str, key: str, with_embedding: bool = False) -> MemoryRecord:
"""Gets a record.
@@ -372,8 +367,7 @@ class AzureCognitiveSearchMemoryStore(MemoryStoreBase):
if len(memory_records) > 0:
return memory_records[0]
else:
return None
return None
async def get_nearest_matches(
self,
@@ -89,7 +89,7 @@ def get_index_schema(vector_size: int, vector_search_profile_name: str) -> list:
Returns:
list: The Azure Cognitive Search schema as list type.
"""
search_fields = [
return [
SimpleField(
name=SEARCH_FIELD_ID,
type=SearchFieldDataType.String,
@@ -142,8 +142,6 @@ def get_index_schema(vector_size: int, vector_search_profile_name: str) -> list:
),
]
return search_fields
def get_field_selection(with_embeddings: bool) -> list[str]:
"""Get the list of fields to search and load.
@@ -179,7 +177,7 @@ def dict_to_memory_record(data: dict, with_embeddings: bool) -> MemoryRecord:
Returns:
MemoryRecord: The MemoryRecord from Azure Cognitive Search Data Result.
"""
sk_result = MemoryRecord(
return MemoryRecord(
id=decode_id(data[SEARCH_FIELD_ID]),
key=data[SEARCH_FIELD_ID],
text=data[SEARCH_FIELD_TEXT],
@@ -190,7 +188,6 @@ def dict_to_memory_record(data: dict, with_embeddings: bool) -> MemoryRecord:
embedding=data[SEARCH_FIELD_EMBEDDING] if with_embeddings else None,
timestamp=None,
)
return sk_result
def memory_record_to_search_record(record: MemoryRecord) -> dict:
@@ -96,32 +96,34 @@ class MongoStoreApi(AzureCosmosDBStoreApi):
@override
async def create_collection(self, collection_name: str) -> None:
if not await self.does_collection_exist(collection_name):
if self.index_name not in self.database[collection_name].list_indexes():
# check the kind of vector search to be performed
# prepare the command accordingly
create_index_commands = {}
if self.kind == CosmosDBVectorSearchType.VECTOR_IVF:
create_index_commands = self._get_vector_index_ivf(
collection_name, self.kind, self.num_lists, self.similarity, self.vector_dimensions
)
elif self.kind == CosmosDBVectorSearchType.VECTOR_HNSW:
create_index_commands = self._get_vector_index_hnsw(
collection_name,
self.kind,
self.m,
self.ef_construction,
self.similarity,
self.vector_dimensions,
)
# invoke the command from the database object
self.database.command(create_index_commands)
if (
not await self.does_collection_exist(collection_name)
and self.index_name not in self.database[collection_name].list_indexes()
):
# check the kind of vector search to be performed
# prepare the command accordingly
create_index_commands = {}
if self.kind == CosmosDBVectorSearchType.VECTOR_IVF:
create_index_commands = self._get_vector_index_ivf(
collection_name, self.kind, self.num_lists, self.similarity, self.vector_dimensions
)
elif self.kind == CosmosDBVectorSearchType.VECTOR_HNSW:
create_index_commands = self._get_vector_index_hnsw(
collection_name,
self.kind,
self.m,
self.ef_construction,
self.similarity,
self.vector_dimensions,
)
# invoke the command from the database object
self.database.command(create_index_commands)
self.collection = self.database[collection_name]
def _get_vector_index_ivf(
self, collection_name: str, kind: str, num_lists: int, similarity: str, dimensions: int
) -> dict[str, Any]:
command = {
return {
"createIndexes": collection_name,
"indexes": [
{
@@ -136,12 +138,11 @@ class MongoStoreApi(AzureCosmosDBStoreApi):
}
],
}
return command
def _get_vector_index_hnsw(
self, collection_name: str, kind: str, m: int, ef_construction: int, similarity: str, dimensions: int
) -> dict[str, Any]:
command = {
return {
"createIndexes": collection_name,
"indexes": [
{
@@ -157,7 +158,6 @@ class MongoStoreApi(AzureCosmosDBStoreApi):
}
],
}
return command
@override
async def get_collections(self) -> list[str]:
@@ -334,8 +334,7 @@ class MongoStoreApi(AzureCosmosDBStoreApi):
if len(nearest_results) > 0:
return nearest_results[0]
else:
return None
return None
@staticmethod
def __serialize_metadata(record: MemoryRecord) -> str:
@@ -182,8 +182,7 @@ class AzureCosmosDBNoSQLMemoryStore(MemoryStoreBase):
)
if len(nearest_results) > 0:
return nearest_results[0]
else:
return None
return None
@staticmethod
def __serialize_metadata(record: MemoryRecord) -> str:
@@ -65,7 +65,7 @@ class ChromaMemoryStore(MemoryStoreBase):
except ImportError as exc:
raise ServiceInitializationError(
"Could not import chromadb python package. " "Please install it with `pip install chromadb`."
"Could not import chromadb python package. Please install it with `pip install chromadb`."
) from exc
if client_settings:
@@ -131,10 +131,7 @@ class ChromaMemoryStore(MemoryStoreBase):
Returns:
bool: True if the collection exists; otherwise, False.
"""
if await self.get_collection(collection_name) is None:
return False
else:
return True
return await self.get_collection(collection_name) is not None
async def upsert(self, collection_name: str, record: MemoryRecord) -> str:
"""Upsert a single MemoryRecord.
@@ -219,8 +216,7 @@ class ChromaMemoryStore(MemoryStoreBase):
query_includes = ["embeddings", "metadatas", "documents"] if with_embeddings else ["metadatas", "documents"]
value = collection.get(ids=keys, include=query_includes)
record = query_results_to_records(value, with_embeddings)
return record
return query_results_to_records(value, with_embeddings)
async def remove(self, collection_name: str, key: str) -> None:
"""Removes a record.
@@ -312,9 +308,7 @@ class ChromaMemoryStore(MemoryStoreBase):
)
filtered_results = [x for x in sorted_results if x[1] >= min_relevance_score]
top_results = filtered_results[:limit]
return top_results
return filtered_results[:limit]
async def get_nearest_match(
self,
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.memory.milvus.milvus_memory_store import (
MilvusMemoryStore,
)
@@ -80,19 +80,19 @@ def milvus_dict_to_memoryrecord(milvus_dict: dict[str, Any]) -> MemoryRecord:
MemoryRecord
"""
# Embedding needs conversion to numpy array
embedding = milvus_dict.get(SEARCH_FIELD_EMBEDDING, None)
embedding = milvus_dict.get(SEARCH_FIELD_EMBEDDING)
if embedding is not None:
embedding = array(embedding)
return MemoryRecord(
is_reference=milvus_dict.get(SEARCH_FIELD_IS_REF, None),
external_source_name=milvus_dict.get(SEARCH_FIELD_SRC, None),
id=milvus_dict.get(SEARCH_FIELD_ID, None),
description=milvus_dict.get(SEARCH_FIELD_DESC, None),
text=milvus_dict.get(SEARCH_FIELD_TEXT, None),
additional_metadata=milvus_dict.get(SEARCH_FIELD_METADATA, None),
is_reference=milvus_dict.get(SEARCH_FIELD_IS_REF),
external_source_name=milvus_dict.get(SEARCH_FIELD_SRC),
id=milvus_dict.get(SEARCH_FIELD_ID),
description=milvus_dict.get(SEARCH_FIELD_DESC),
text=milvus_dict.get(SEARCH_FIELD_TEXT),
additional_metadata=milvus_dict.get(SEARCH_FIELD_METADATA),
embedding=embedding,
key=milvus_dict.get("key", None),
timestamp=milvus_dict.get(SEARCH_FIELD_TIMESTAMP, None),
key=milvus_dict.get("key"),
timestamp=milvus_dict.get(SEARCH_FIELD_TIMESTAMP),
)
@@ -194,9 +194,8 @@ class MilvusMemoryStore(MemoryStoreBase):
create_fields(dimension_num), "Semantic Kernel Milvus Collection", enable_dynamic_field=True
)
index_param = {"index_type": _INDEX_TYPE, "params": {"nlist": _NLIST}, "metric_type": distance_type}
if utility.has_collection(collection_name):
if overwrite:
utility.drop_collection(collection_name=collection_name)
if utility.has_collection(collection_name) and overwrite:
utility.drop_collection(collection_name=collection_name)
self.collections[collection_name] = Collection(
name=collection_name,
schema=schema,
@@ -453,5 +452,4 @@ class MilvusMemoryStore(MemoryStoreBase):
)
if len(m) > 0:
return m[0]
else:
return None
return None
@@ -61,7 +61,7 @@ class PineconeMemoryStore(MemoryStoreBase):
if default_dimensionality > MAX_DIMENSIONALITY:
raise MemoryConnectorInitializationError(
f"Dimensionality of {default_dimensionality} exceeds "
+ f"the maximum allowed value of {MAX_DIMENSIONALITY}."
f"the maximum allowed value of {MAX_DIMENSIONALITY}."
)
try:
pinecone_settings = PineconeSettings.create(
@@ -217,8 +217,7 @@ class PineconeMemoryStore(MemoryStoreBase):
if upsert_response.upserted_count is None:
raise ServiceResponseException(f"Error upserting record: {upsert_response.message}")
else:
return [record._id for record in records]
return [record._id for record in records]
async def get(self, collection_name: str, key: str, with_embedding: bool = False) -> MemoryRecord:
"""Gets a record.
@@ -259,7 +258,7 @@ class PineconeMemoryStore(MemoryStoreBase):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
fetch_response = await self.__get_batch(collection_name, keys, with_embeddings)
return [parse_payload(fetch_response.vectors[key], with_embeddings) for key in fetch_response.vectors.keys()]
return [parse_payload(fetch_response.vectors[key], with_embeddings) for key in fetch_response.vectors]
async def remove(self, collection_name: str, key: str) -> None:
"""Removes a record.
@@ -351,7 +350,7 @@ class PineconeMemoryStore(MemoryStoreBase):
raise ServiceInvalidRequestError(
"Limit must be less than or equal to " + f"{MAX_QUERY_WITHOUT_METADATA_BATCH_SIZE}"
)
elif limit > MAX_QUERY_WITH_METADATA_BATCH_SIZE:
if limit > MAX_QUERY_WITH_METADATA_BATCH_SIZE:
query_response = collection.query(
vector=embedding.tolist(),
top_k=limit,
@@ -363,7 +362,7 @@ class PineconeMemoryStore(MemoryStoreBase):
vectors = fetch_response.vectors
for match in query_response.matches:
vectors[match.id].update(match)
matches = [vectors[key] for key in vectors.keys()]
matches = [vectors[key] for key in vectors]
else:
query_response = collection.query(
vector=embedding.tolist(),
@@ -98,24 +98,23 @@ class PostgresMemoryStore(MemoryStoreBase):
else:
self._check_dimensionality(dimension_num)
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
cur.execute(
SQL(
"""
with self._connection_pool.connection() as conn, conn.cursor() as cur:
cur.execute(
SQL(
"""
CREATE TABLE IF NOT EXISTS {scm}.{tbl} (
key TEXT PRIMARY KEY,
embedding vector({dim}),
metadata JSONB,
timestamp TIMESTAMP
)"""
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
dim=dimension_num,
),
(),
)
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
dim=dimension_num,
),
(),
)
async def get_collections(self) -> list[str]:
"""Gets the list of collections.
@@ -123,9 +122,8 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
List[str]: The list of collections.
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
return await self.__get_collections(cur)
with self._connection_pool.connection() as conn, conn.cursor() as cur:
return await self.__get_collections(cur)
async def delete_collection(self, collection_name: str) -> None:
"""Deletes a collection.
@@ -136,13 +134,12 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
None
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
cur.execute(
SQL("DROP TABLE IF EXISTS {scm}.{tbl} CASCADE").format(
scm=Identifier(self._schema), tbl=Identifier(collection_name)
),
)
with self._connection_pool.connection() as conn, conn.cursor() as cur:
cur.execute(
SQL("DROP TABLE IF EXISTS {scm}.{tbl} CASCADE").format(
scm=Identifier(self._schema), tbl=Identifier(collection_name)
),
)
async def does_collection_exist(self, collection_name: str) -> bool:
"""Checks if a collection exists.
@@ -153,9 +150,8 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
bool: True if the collection exists; otherwise, False.
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
return await self.__does_collection_exist(cur, collection_name)
with self._connection_pool.connection() as conn, conn.cursor() as cur:
return await self.__does_collection_exist(cur, collection_name)
async def upsert(self, collection_name: str, record: MemoryRecord) -> str:
r"""Upserts a record.
@@ -167,13 +163,12 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
str: The unique database key of the record. In Pinecone, this is the record ID.
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
with self._connection_pool.connection() as conn, conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
INSERT INTO {scm}.{tbl} (key, embedding, metadata, timestamp)
VALUES (%s, %s, %s, %s)
ON CONFLICT (key) DO UPDATE
@@ -182,21 +177,21 @@ class PostgresMemoryStore(MemoryStoreBase):
timestamp = EXCLUDED.timestamp
RETURNING key
"""
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
),
(
record._id,
record.embedding.tolist(),
self.__serialize_metadata(record),
record._timestamp,
),
)
result = cur.fetchone()
if result is None:
raise ServiceResponseException("Upsert failed")
return result[0]
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
),
(
record._id,
record.embedding.tolist(),
self.__serialize_metadata(record),
record._timestamp,
),
)
result = cur.fetchone()
if result is None:
raise ServiceResponseException("Upsert failed")
return result[0]
async def upsert_batch(self, collection_name: str, records: list[MemoryRecord]) -> list[str]:
"""Upserts a batch of records.
@@ -208,14 +203,13 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
List[str]: The unique database keys of the records.
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.nextset()
cur.executemany(
SQL(
"""
with self._connection_pool.connection() as conn, conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.nextset()
cur.executemany(
SQL(
"""
INSERT INTO {scm}.{tbl} (key, embedding, metadata, timestamp)
VALUES (%s, %s, %s, %s)
ON CONFLICT (key) DO UPDATE
@@ -224,29 +218,29 @@ class PostgresMemoryStore(MemoryStoreBase):
timestamp = EXCLUDED.timestamp
RETURNING key
"""
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
),
[
(
record._id,
record.embedding.tolist(),
self.__serialize_metadata(record),
record._timestamp,
)
for record in records
],
returning=True,
)
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
),
[
(
record._id,
record.embedding.tolist(),
self.__serialize_metadata(record),
record._timestamp,
)
for record in records
],
returning=True,
)
# collate results
results = [cur.fetchone()]
while cur.nextset():
results.append(cur.fetchone())
if None in results:
raise ServiceResponseException("Upsert failed")
return [result[0] for result in results if result is not None]
# collate results
results = [cur.fetchone()]
while cur.nextset():
results.append(cur.fetchone())
if None in results:
raise ServiceResponseException("Upsert failed")
return [result[0] for result in results if result is not None]
async def get(self, collection_name: str, key: str, with_embedding: bool = False) -> MemoryRecord:
"""Gets a record.
@@ -259,36 +253,35 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
MemoryRecord: The record.
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
with self._connection_pool.connection() as conn, conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
SELECT key, embedding, metadata, timestamp
FROM {scm}.{tbl}
WHERE key = %s
"""
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
),
(key,),
)
result = cur.fetchone()
if result is None:
raise ServiceResourceNotFoundError("Key not found")
return MemoryRecord.local_record(
id=result[0],
embedding=(
np.fromstring(result[1].strip("[]"), dtype=float, sep=",") if with_embedding else np.array([])
),
text=result[2]["text"],
description=result[2]["description"],
additional_metadata=result[2]["additional_metadata"],
timestamp=result[3],
)
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
),
(key,),
)
result = cur.fetchone()
if result is None:
raise ServiceResourceNotFoundError("Key not found")
return MemoryRecord.local_record(
id=result[0],
embedding=(
np.fromstring(result[1].strip("[]"), dtype=float, sep=",") if with_embedding else np.array([])
),
text=result[2]["text"],
description=result[2]["description"],
additional_metadata=result[2]["additional_metadata"],
timestamp=result[3],
)
async def get_batch(
self, collection_name: str, keys: list[str], with_embeddings: bool = False
@@ -303,39 +296,38 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
List[MemoryRecord]: The records that were found from list of keys, can be empty.
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
with self._connection_pool.connection() as conn, conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
SELECT key, embedding, metadata, timestamp
FROM {scm}.{tbl}
WHERE key = ANY(%s)
"""
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
),
(list(keys),),
)
results = cur.fetchall()
return [
MemoryRecord.local_record(
id=result[0],
embedding=(
np.fromstring(result[1].strip("[]"), dtype=float, sep=",")
if with_embeddings
else np.array([])
),
(list(keys),),
text=result[2]["text"],
description=result[2]["description"],
additional_metadata=result[2]["additional_metadata"],
timestamp=result[3],
)
results = cur.fetchall()
return [
MemoryRecord.local_record(
id=result[0],
embedding=(
np.fromstring(result[1].strip("[]"), dtype=float, sep=",")
if with_embeddings
else np.array([])
),
text=result[2]["text"],
description=result[2]["description"],
additional_metadata=result[2]["additional_metadata"],
timestamp=result[3],
)
for result in results
]
for result in results
]
async def remove(self, collection_name: str, key: str) -> None:
"""Removes a record.
@@ -347,19 +339,18 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
None
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
with self._connection_pool.connection() as conn, conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
DELETE FROM {scm}.{tbl}
WHERE key = %s
"""
).format(scm=Identifier(self._schema), tbl=Identifier(collection_name)),
(key,),
)
).format(scm=Identifier(self._schema), tbl=Identifier(collection_name)),
(key,),
)
async def remove_batch(self, collection_name: str, keys: list[str]) -> None:
"""Removes a batch of records.
@@ -371,19 +362,18 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
None
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
with self._connection_pool.connection() as conn, conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
DELETE FROM {scm}.{tbl}
WHERE key = ANY(%s)
"""
).format(scm=Identifier(self._schema), tbl=Identifier(collection_name)),
(list(keys),),
)
).format(scm=Identifier(self._schema), tbl=Identifier(collection_name)),
(list(keys),),
)
async def get_nearest_matches(
self,
@@ -405,13 +395,12 @@ class PostgresMemoryStore(MemoryStoreBase):
Returns:
List[Tuple[MemoryRecord, float]]: The records and their relevance scores.
"""
with self._connection_pool.connection() as conn:
with conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
with self._connection_pool.connection() as conn, conn.cursor() as cur:
if not await self.__does_collection_exist(cur, collection_name):
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
cur.execute(
SQL(
"""
SELECT key,
embedding,
metadata,
@@ -426,34 +415,34 @@ class PostgresMemoryStore(MemoryStoreBase):
ORDER BY cosine_similarity DESC
LIMIT {limit}
"""
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
mrs=min_relevance_score,
limit=limit,
emb=SQL(",").join(embedding.tolist()),
)
).format(
scm=Identifier(self._schema),
tbl=Identifier(collection_name),
mrs=min_relevance_score,
limit=limit,
emb=SQL(",").join(embedding.tolist()),
)
results = cur.fetchall()
)
results = cur.fetchall()
return [
(
MemoryRecord.local_record(
id=result[0],
embedding=(
np.fromstring(result[1].strip("[]"), dtype=float, sep=",")
if with_embeddings
else np.array([])
),
text=result[2]["text"],
description=result[2]["description"],
additional_metadata=result[2]["additional_metadata"],
timestamp=result[4],
return [
(
MemoryRecord.local_record(
id=result[0],
embedding=(
np.fromstring(result[1].strip("[]"), dtype=float, sep=",")
if with_embeddings
else np.array([])
),
result[3],
)
for result in results
]
text=result[2]["text"],
description=result[2]["description"],
additional_metadata=result[2]["additional_metadata"],
timestamp=result[4],
),
result[3],
)
for result in results
]
async def get_nearest_match(
self,
@@ -67,8 +67,7 @@ class QdrantMemoryStore(MemoryStoreBase):
Returns:
CollectionInfo -- Collection Information from Qdrant about collection.
"""
collection_info = self._qdrantclient.get_collection(collection_name=collection_name)
return collection_info
return self._qdrantclient.get_collection(collection_name=collection_name)
@override
async def delete_collection(self, collection_name: str) -> None:
@@ -96,8 +95,7 @@ class QdrantMemoryStore(MemoryStoreBase):
if result.status == qdrant_models.UpdateStatus.COMPLETED:
return data_to_upsert.id
else:
raise ServiceResponseException("Upsert failed")
raise ServiceResponseException("Upsert failed")
@override
async def upsert_batch(self, collection_name: str, records: list[MemoryRecord]) -> list[str]:
@@ -119,8 +117,7 @@ class QdrantMemoryStore(MemoryStoreBase):
if result.status == qdrant_models.UpdateStatus.COMPLETED:
return [data.id for data in data_to_upsert]
else:
raise ServiceResponseException("Batch upsert failed")
raise ServiceResponseException("Batch upsert failed")
@override
async def get(self, collection_name: str, key: str, with_embedding: bool = False) -> MemoryRecord | None:
@@ -142,8 +139,7 @@ class QdrantMemoryStore(MemoryStoreBase):
key=result.id,
timestamp=result.payload["_timestamp"],
)
else:
return None
return None
@override
async def get_batch(
@@ -287,8 +283,7 @@ class QdrantMemoryStore(MemoryStoreBase):
if existing_record:
return existing_record[0]
else:
return None
return None
async def _convert_from_memory_record(
self, collection_name: str, record: MemoryRecord
@@ -302,10 +297,7 @@ class QdrantMemoryStore(MemoryStoreBase):
payload_id=record._id,
)
if existing_record:
pointId = str(existing_record.id)
else:
pointId = str(uuid.uuid4())
pointId = str(existing_record.id) if existing_record else str(uuid.uuid4())
payload = record.__dict__.copy()
embedding = payload.pop("_embedding")
@@ -48,13 +48,12 @@ def serialize_record_to_redis(record: MemoryRecord, vector_type: np.dtype) -> di
"additional_metadata": record._additional_metadata or "",
}
redis_mapping = {
return {
"key": record._key or "",
"timestamp": record._timestamp.isoformat() if record._timestamp else "",
"metadata": json.dumps(all_metadata),
"embedding": (record._embedding.astype(vector_type).tobytes() if record._embedding is not None else ""),
}
return redis_mapping
def deserialize_redis_to_record(fields: dict[str, Any], vector_type: np.dtype, with_embedding: bool) -> MemoryRecord:
@@ -62,7 +61,7 @@ def deserialize_redis_to_record(fields: dict[str, Any], vector_type: np.dtype, w
metadata = json.loads(fields[b"metadata"])
record = MemoryRecord(
id=metadata["id"],
is_reference=True if metadata["is_reference"] is True else False,
is_reference=metadata["is_reference"] is True,
description=metadata["description"],
external_source_name=metadata["external_source_name"],
text=metadata["text"],
@@ -91,7 +90,7 @@ def deserialize_document_to_record(
metadata = json.loads(doc["metadata"])
record = MemoryRecord(
id=id_str,
is_reference=True if metadata["is_reference"] is True else False,
is_reference=metadata["is_reference"] is True,
description=metadata["description"],
external_source_name=metadata["external_source_name"],
text=metadata["text"],
@@ -108,13 +108,11 @@ def pyarrow_table_to_memoryrecords(table: pa.Table, vectors: ndarray | None = No
Returns:
List[MemoryRecord]: List of MemoryRecords constructed from the table.
"""
result_memory_records = [
return [
MemoryRecord(**row.to_dict(), embedding=vectors[index] if vectors is not None else None)
for index, row in table.to_pandas().iterrows()
]
return result_memory_records
@experimental_class
class USearchMemoryStore(MemoryStoreBase):
@@ -214,7 +212,7 @@ class USearchMemoryStore(MemoryStoreBase):
self._collections[collection_name] = _USearchCollection.create_default(embeddings_index)
return None
return
def _read_embeddings_table(self, path: os.PathLike) -> tuple[pa.Table, dict[str, int]]:
"""Read embeddings from the provided path and generate an ID to label mapping.
@@ -283,7 +281,7 @@ class USearchMemoryStore(MemoryStoreBase):
collection = self._collections.pop(collection_name, None)
if collection:
collection.embeddings_index.reset()
return None
return
async def does_collection_exist(self, collection_name: str) -> bool:
"""Check if collection exists."""
@@ -404,7 +402,7 @@ class USearchMemoryStore(MemoryStoreBase):
"""Remove a single MemoryRecord using its key."""
collection_name = collection_name.lower()
await self.remove_batch(collection_name=collection_name, keys=[key])
return None
return
async def remove_batch(self, collection_name: str, keys: list[str]) -> None:
"""Remove a batch of MemoryRecords using their keys."""
@@ -419,7 +417,7 @@ class USearchMemoryStore(MemoryStoreBase):
for key in keys:
del ucollection.embeddings_id_to_label[key]
return None
return
async def get_nearest_match(
self,
@@ -506,9 +504,6 @@ class USearchMemoryStore(MemoryStoreBase):
exact=exact,
log=log,
)
# assert isinstance(result, Matches) # nosec
relevance_score = 1 / (result.distances + 1)
filtered_labels = result.keys[np.where(relevance_score >= min_relevance_score)[0]]
@@ -566,7 +561,7 @@ class USearchMemoryStore(MemoryStoreBase):
self._get_collection_path(collection_name, file_type=_CollectionFileType.PARQUET),
)
return None
return
async def close(self) -> None:
"""Persist collection, clear.
@@ -1,4 +1,5 @@
# Copyright (c) Microsoft. All rights reserved
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.memory.weaviate.weaviate_memory_store import (
WeaviateMemoryStore,
)
@@ -2,6 +2,7 @@
import asyncio
import logging
from typing import Final
import numpy as np
import weaviate
@@ -63,49 +64,46 @@ SCHEMA = {
ALL_PROPERTIES = [property["name"] for property in SCHEMA["properties"]]
@experimental_class
class WeaviateMemoryStore(MemoryStoreBase):
class FieldMapper:
"""This maps attribute names between the SK's memory record and weaviate's schema.
class FieldMapper:
"""This maps attribute names between the SK's memory record and weaviate's schema.
It provides methods for converting between the two naming conventions.
"""
It provides methods for converting between the two naming conventions.
"""
SK_TO_WEAVIATE_MAPPING = {
"_key": "key",
"_timestamp": "timestamp",
"_is_reference": "isReference",
"_external_source_name": "externalSourceName",
"_id": "skId",
"_description": "description",
"_text": "text",
"_additional_metadata": "additionalMetadata",
"_embedding": "vector",
SK_TO_WEAVIATE_MAPPING: Final[dict[str, str]] = {
"_key": "key",
"_timestamp": "timestamp",
"_is_reference": "isReference",
"_external_source_name": "externalSourceName",
"_id": "skId",
"_description": "description",
"_text": "text",
"_additional_metadata": "additionalMetadata",
"_embedding": "vector",
}
WEAVIATE_TO_SK_MAPPING: Final[dict[str, str]] = {v: k for k, v in SK_TO_WEAVIATE_MAPPING.items()}
@classmethod
def sk_to_weaviate(cls, sk_dict):
"""Used to convert a MemoryRecord to a dict of attribute-values that can be used by Weaviate."""
return {cls.SK_TO_WEAVIATE_MAPPING.get(k, k): v for k, v in sk_dict.items() if k in cls.SK_TO_WEAVIATE_MAPPING}
@classmethod
def weaviate_to_sk(cls, weaviate_dict):
"""Used to convert a Weaviate object to a dict that can be used to initialize a MemoryRecord."""
return {
cls.WEAVIATE_TO_SK_MAPPING.get(k, k): v for k, v in weaviate_dict.items() if k in cls.WEAVIATE_TO_SK_MAPPING
}
WEAVIATE_TO_SK_MAPPING = {v: k for k, v in SK_TO_WEAVIATE_MAPPING.items()}
@classmethod
def remove_underscore_prefix(cls, sk_dict):
"""Used to initialize a MemoryRecord from a SK's dict of private attribute-values."""
return {key.lstrip("_"): value for key, value in sk_dict.items()}
@classmethod
def sk_to_weaviate(cls, sk_dict):
"""Used to convert a MemoryRecord to a dict of attribute-values that can be used by Weaviate."""
return {
cls.SK_TO_WEAVIATE_MAPPING.get(k, k): v for k, v in sk_dict.items() if k in cls.SK_TO_WEAVIATE_MAPPING
}
@classmethod
def weaviate_to_sk(cls, weaviate_dict):
"""Used to convert a Weaviate object to a dict that can be used to initialize a MemoryRecord."""
return {
cls.WEAVIATE_TO_SK_MAPPING.get(k, k): v
for k, v in weaviate_dict.items()
if k in cls.WEAVIATE_TO_SK_MAPPING
}
@classmethod
def remove_underscore_prefix(cls, sk_dict):
"""Used to initialize a MemoryRecord from a SK's dict of private attribute-values."""
return {key.lstrip("_"): value for key, value in sk_dict.items()}
@experimental_class
class WeaviateMemoryStore(MemoryStoreBase):
def __init__(
self,
url: str | None = None,
@@ -166,7 +164,7 @@ class WeaviateMemoryStore(MemoryStoreBase):
async def upsert(self, collection_name: str, record: MemoryRecord) -> str:
"""Upserts a record into Weaviate."""
weaviate_record = self.FieldMapper.sk_to_weaviate(vars(record))
weaviate_record = FieldMapper.sk_to_weaviate(vars(record))
vector = weaviate_record.pop("vector", None)
weaviate_id = weaviate.util.generate_uuid5(weaviate_record, collection_name)
@@ -187,7 +185,7 @@ class WeaviateMemoryStore(MemoryStoreBase):
results = []
with self.client.batch as batch:
for record in records:
weaviate_record = self.FieldMapper.sk_to_weaviate(vars(record))
weaviate_record = FieldMapper.sk_to_weaviate(vars(record))
vector = weaviate_record.pop("vector", None)
weaviate_id = weaviate.util.generate_uuid5(weaviate_record, collection_name)
batch.add_data_object(
@@ -216,11 +214,7 @@ class WeaviateMemoryStore(MemoryStoreBase):
get_dict = results.get("data", {}).get("Get", {})
memory_records = [
self._convert_weaviate_doc_to_memory_record(doc) for docs in get_dict.values() for doc in docs
]
return memory_records
return [self._convert_weaviate_doc_to_memory_record(doc) for docs in get_dict.values() for doc in docs]
def _build_multi_get_query(self, collection_name: str, keys: list[str], with_embedding: bool):
queries = []
@@ -245,8 +239,8 @@ class WeaviateMemoryStore(MemoryStoreBase):
weaviate_doc_copy = weaviate_doc.copy()
vector = weaviate_doc_copy.pop("_additional", {}).get("vector")
weaviate_doc_copy["vector"] = np.array(vector) if vector else None
sk_doc = self.FieldMapper.weaviate_to_sk(weaviate_doc_copy)
mem_vals = self.FieldMapper.remove_underscore_prefix(sk_doc)
sk_doc = FieldMapper.weaviate_to_sk(weaviate_doc_copy)
mem_vals = FieldMapper.remove_underscore_prefix(sk_doc)
return MemoryRecord(**mem_vals)
async def remove(self, collection_name: str, key: str) -> None:
@@ -255,10 +249,6 @@ class WeaviateMemoryStore(MemoryStoreBase):
async def remove_batch(self, collection_name: str, keys: list[str]) -> None:
"""Removes a batch of records from Weaviate by keys."""
# TODO: Use In operator when it's available
# (https://github.com/weaviate/weaviate/issues/2387)
# and handle max delete objects
# (https://weaviate.io/developers/weaviate/api/rest/batch#maximum-number-of-deletes-per-query)
for key in keys:
where = {
"path": ["key"],
@@ -299,7 +289,7 @@ class WeaviateMemoryStore(MemoryStoreBase):
get_dict = results.get("data", {}).get("Get", {})
memory_records_and_scores = [
return [
(
self._convert_weaviate_doc_to_memory_record(doc),
item["_additional"]["certainty"],
@@ -309,8 +299,6 @@ class WeaviateMemoryStore(MemoryStoreBase):
for doc in [item]
]
return memory_records_and_scores
async def get_nearest_match(
self,
collection_name: str,
@@ -10,8 +10,8 @@ from semantic_kernel.connectors.openai_plugin.openai_function_execution_paramete
from semantic_kernel.connectors.openai_plugin.openai_utils import OpenAIUtils
__all__ = [
"OpenAIUtils",
"OpenAIFunctionExecutionParameters",
"OpenAIAuthenticationConfig",
"OpenAIAuthenticationType",
"OpenAIFunctionExecutionParameters",
"OpenAIUtils",
]
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import re
from typing import Any
from typing import Any, Final
from urllib.parse import urlencode, urljoin, urlparse, urlunparse
from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_expected_response import (
@@ -30,7 +30,7 @@ class RestApiOperation:
CONTENT_TYPE_ARGUMENT_NAME = "content-type"
INVALID_SYMBOLS_REGEX = re.compile(r"[^0-9A-Za-z_]+")
_preferred_responses: list[str] = [
_preferred_responses: Final[list[str]] = [
"200",
"201",
"202",
@@ -71,10 +71,7 @@ class RestApiOperation:
def url_join(self, base_url: str, path: str):
"""Join a base URL and a path, correcting for any missing slashes."""
parsed_base = urlparse(base_url)
if not parsed_base.path.endswith("/"):
base_path = parsed_base.path + "/"
else:
base_path = parsed_base.path
base_path = parsed_base.path + "/" if not parsed_base.path.endswith("/") else parsed_base.path
full_path = urljoin(base_path, path.lstrip("/"))
return urlunparse(parsed_base._replace(path=full_path))
@@ -113,7 +110,9 @@ class RestApiOperation:
server_url_string = (
self.server_url.geturl()
if self.server_url
else api_host_url.geturl() if api_host_url else self._raise_invalid_operation_exception()
else api_host_url.geturl()
if api_host_url
else self._raise_invalid_operation_exception()
)
# make sure the base URL ends with a trailing slash
@@ -218,7 +217,7 @@ class RestApiOperation:
self,
properties: list["RestApiOperationPayloadProperty"],
enable_namespacing: bool = False,
root_property_name: bool = None,
root_property_name: bool | None = None,
) -> list["RestApiOperationParameter"]:
parameters: list[RestApiOperationParameter] = []
for property in properties:
@@ -118,8 +118,7 @@ def _create_function_from_operation(
api_host_url=Uri(document_uri).get_left_part() if document_uri is not None else None,
)
response = await runner.run_operation(operation, kernel_arguments, options)
return response
return await runner.run_operation(operation, kernel_arguments, options)
except Exception as e:
logger.error(f"Error running OpenAPI operation: {operation.id}", exc_info=True)
raise FunctionExecutionException(f"Error running OpenAPI operation: {operation.id}") from e
@@ -3,7 +3,7 @@
import logging
from collections import OrderedDict
from collections.abc import Generator
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Final
from urllib.parse import urlparse
from prance import ResolvingParser
@@ -51,8 +51,8 @@ class OpenApiParser:
:return: The parsed OpenAPI file
"""
PAYLOAD_PROPERTIES_HIERARCHY_MAX_DEPTH = 10
supported_media_types = ["application/json", "text/plain"]
PAYLOAD_PROPERTIES_HIERARCHY_MAX_DEPTH: int = 10
SUPPORTED_MEDIA_TYPES: Final[list[str]] = ["application/json", "text/plain"]
def parse(self, openapi_document: str) -> Any | dict[str, Any] | None:
"""Parse the OpenAPI document."""
@@ -121,7 +121,7 @@ class OpenApiParser:
) -> RestApiOperationPayload:
if request_body is None or request_body.get("content") is None:
return None
media_type = next((mt for mt in OpenApiParser.supported_media_types if mt in request_body.get("content")), None)
media_type = next((mt for mt in OpenApiParser.SUPPORTED_MEDIA_TYPES if mt in request_body.get("content")), None)
if media_type is None:
raise Exception(f"Neither of the media types of {operation_id} is supported.")
media_type_metadata = request_body.get("content")[media_type]
@@ -131,7 +131,7 @@ class OpenApiParser:
return RestApiOperationPayload(
media_type,
payload_properties,
request_body.get("description", None),
request_body.get("description"),
schema=media_type_metadata.get("schema", None),
)
@@ -140,7 +140,7 @@ class OpenApiParser:
) -> Generator[tuple[str, RestApiOperationExpectedResponse], None, None]:
for response_key, response_value in responses.items():
media_type = next(
(mt for mt in OpenApiParser.supported_media_types if mt in response_value.get("content", {})), None
(mt for mt in OpenApiParser.SUPPORTED_MEDIA_TYPES if mt in response_value.get("content", {})), None
)
if media_type is not None:
matching_schema = response_value["content"][media_type].get("schema", {})
@@ -163,8 +163,7 @@ class OpenApiRunner:
if hasattr(self, "http_client") and self.http_client is not None:
return await make_request(self.http_client)
else:
async with httpx.AsyncClient() as client:
return await make_request(client)
async with httpx.AsyncClient() as client:
return await make_request(client)
return await fetch()
@@ -32,7 +32,7 @@ class BingConnector(ConnectorBase):
the settings are read from this file path location.
env_file_encoding (str | None): The optional encoding of the .env file.
"""
self.settings = BingSettings.create(
self._settings = BingSettings.create(
api_key=api_key,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
@@ -63,12 +63,14 @@ class BingConnector(ConnectorBase):
headers = {"Ocp-Apim-Subscription-Key": self._settings.api_key.get_secret_value()}
async with aiohttp.ClientSession() as session:
async with session.get(_request_url, headers=headers, raise_for_status=True) as response:
if response.status == 200:
data = await response.json()
pages = data.get("webPages", {}).get("value")
if pages:
return list(map(lambda x: x["snippet"], pages)) or []
else:
return []
async with (
aiohttp.ClientSession() as session,
session.get(_request_url, headers=headers, raise_for_status=True) as response,
):
if response.status == 200:
data = await response.json()
pages = data.get("webPages", {}).get("value")
if pages:
return list(map(lambda x: x["snippet"], pages)) or []
return None
return []
@@ -55,13 +55,11 @@ class GoogleConnector(ConnectorBase):
logger.info("Sending GET request to Google Search API.")
async with aiohttp.ClientSession() as session:
async with session.get(_request_url, raise_for_status=True) as response:
if response.status == 200:
data = await response.json()
logger.info("Request successful.")
logger.info(f"API Response: {data}")
return [x["snippet"] for x in data["items"]]
else:
logger.error(f"Request to Google Search API failed with status code: {response.status}.")
return []
async with aiohttp.ClientSession() as session, session.get(_request_url, raise_for_status=True) as response:
if response.status == 200:
data = await response.json()
logger.info("Request successful.")
logger.info(f"API Response: {data}")
return [x["snippet"] for x in data["items"]]
logger.error(f"Request to Google Search API failed with status code: {response.status}.")
return []
+5 -5
View File
@@ -10,12 +10,12 @@ from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
__all__ = [
"ChatMessageContent",
"ChatHistory",
"AuthorRole",
"TextContent",
"StreamingChatMessageContent",
"StreamingTextContent",
"ChatHistory",
"ChatMessageContent",
"FunctionCallContent",
"FunctionResultContent",
"StreamingChatMessageContent",
"StreamingTextContent",
"TextContent",
]
@@ -14,12 +14,12 @@ 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",
"HttpPlugin",
"ConversationSummaryPlugin",
"MathPlugin",
"SessionsPythonTool",
"WebSearchEnginePlugin",
]
@@ -17,8 +17,8 @@ class ConversationSummaryPlugin:
_summarize_conversation_prompt_template = (
"BEGIN CONTENT TO SUMMARIZE:\n{{"
+ "$input"
+ "}}\nEND CONTENT TO SUMMARIZE.\nSummarize the conversation in 'CONTENT TO"
"$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"
@@ -36,9 +36,8 @@ class HttpPlugin(KernelBaseModel):
if not url:
raise FunctionExecutionException("url cannot be `None` or empty")
async with aiohttp.ClientSession() as session:
async with session.get(url, raise_for_status=True) as response:
return await response.text()
async with aiohttp.ClientSession() as session, session.get(url, raise_for_status=True) as response:
return await response.text()
@kernel_function(description="Makes a POST request to a uri", name="postAsync")
async def post(
@@ -59,9 +58,11 @@ class HttpPlugin(KernelBaseModel):
headers = {"Content-Type": "application/json"}
data = json.dumps(body)
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, data=data, raise_for_status=True) as response:
return await response.text()
async with (
aiohttp.ClientSession() as session,
session.post(url, headers=headers, data=data, raise_for_status=True) as response,
):
return await response.text()
@kernel_function(description="Makes a PUT request to a uri", name="putAsync")
async def put(
@@ -83,9 +84,11 @@ class HttpPlugin(KernelBaseModel):
headers = {"Content-Type": "application/json"}
data = json.dumps(body)
async with aiohttp.ClientSession() as session:
async with session.put(url, headers=headers, data=data, raise_for_status=True) as response:
return await response.text()
async with (
aiohttp.ClientSession() as session,
session.put(url, headers=headers, data=data, raise_for_status=True) 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:
@@ -99,6 +102,5 @@ class HttpPlugin(KernelBaseModel):
"""
if not url:
raise FunctionExecutionException("url cannot be `None` or empty")
async with aiohttp.ClientSession() as session:
async with session.delete(url, raise_for_status=True) as response:
return await response.text()
async with aiohttp.ClientSession() as session, session.delete(url, raise_for_status=True) as response:
return await response.text()
@@ -7,4 +7,4 @@ from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_settings
SessionsPythonSettings,
)
__all__ = ["SessionsPythonTool", "SessionsPythonSettings"]
__all__ = ["SessionsPythonSettings", "SessionsPythonTool"]
@@ -57,8 +57,8 @@ class SessionsPythonTool(KernelBaseModel):
env_file_path=env_file_path, pool_management_endpoint=pool_management_endpoint
)
except ValidationError as e:
logger.error(f"Failed to load the ACASessionsSettings with message: {str(e)}")
raise FunctionExecutionException(f"Failed to load the ACASessionsSettings with message: {str(e)}") from e
logger.error(f"Failed to load the ACASessionsSettings with message: {e!s}")
raise FunctionExecutionException(f"Failed to load the ACASessionsSettings with message: {e!s}") from e
endpoint = pool_management_endpoint or aca_settings.pool_management_endpoint
@@ -89,8 +89,8 @@ class SessionsPythonTool(KernelBaseModel):
try:
auth_token = await self.auth_callback()
except Exception as e:
logger.error(f"Failed to retrieve the client auth token with message: {str(e)}")
raise FunctionExecutionException(f"Failed to retrieve the client auth token with messages: {str(e)}") from 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 messages: {e!s}") from e
return auth_token
@@ -107,8 +107,7 @@ class SessionsPythonTool(KernelBaseModel):
# Removes `, whitespace & python from start
code = re.sub(r"^(\s|`)*(?i:python)?\s*", "", code)
# Removes whitespace & ` from end
code = re.sub(r"(\s|`)*$", "", code)
return code
return re.sub(r"(\s|`)*$", "", code)
@kernel_function(
description="""Executes the provided Python code.
@@ -162,11 +161,15 @@ class SessionsPythonTool(KernelBaseModel):
response.raise_for_status()
result = response.json()
return f"Result:\n{result['result']}Stdout:\n{result['stdout']}Stderr:\n{result['stderr']}" # noqa: E501
return f"Result:\n{result['result']}Stdout:\n{result['stdout']}Stderr:\n{result['stderr']}"
@kernel_function(name="upload_file", description="Uploads a file for the current Session ID")
async def upload_file(
self, *, data: BufferedReader = None, remote_file_path: str = None, local_file_path: str = None
self,
*,
data: BufferedReader | None = None,
remote_file_path: str | None = None,
local_file_path: str | None = None,
) -> SessionsRemoteFileMetadata:
"""Upload a file to the session pool.
@@ -184,7 +187,7 @@ class SessionsPythonTool(KernelBaseModel):
if local_file_path:
if not remote_file_path:
remote_file_path = os.path.basename(local_file_path)
data = open(local_file_path, "rb")
data = open(local_file_path, "rb") # noqa: SIM115
auth_token = await self._ensure_auth_token()
self.http_client.headers.update(
@@ -229,7 +232,9 @@ class SessionsPythonTool(KernelBaseModel):
response_json = response.json()
return [SessionsRemoteFileMetadata.from_dict(entry) for entry in response_json["$values"]]
async def download_file(self, *, remote_file_path: str, local_file_path: str = None) -> BufferedReader | None:
async def download_file(
self, *, remote_file_path: str, local_file_path: str | None = None
) -> BufferedReader | None:
"""Download a file from the session pool.
Args:
@@ -249,7 +254,7 @@ class SessionsPythonTool(KernelBaseModel):
)
response = await self.http_client.get(
url=f"{self.pool_management_endpoint}python/downloadFile?identifier={self.settings.session_id}&filename={remote_file_path}", # noqa: E501
url=f"{self.pool_management_endpoint}python/downloadFile?identifier={self.settings.session_id}&filename={remote_file_path}",
)
response.raise_for_status()
@@ -1,9 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.exceptions.content_exceptions import * # noqa: F401, F403
from semantic_kernel.exceptions.function_exceptions import * # noqa: F401, F403
from semantic_kernel.exceptions.kernel_exceptions import * # noqa: F401, F403
from semantic_kernel.exceptions.memory_connector_exceptions import * # noqa: F401, F403
from semantic_kernel.exceptions.planner_exceptions import * # noqa: F401, F403
from semantic_kernel.exceptions.service_exceptions import * # noqa: F401, F403
from semantic_kernel.exceptions.template_engine_exceptions import * # noqa: F401, F403
from semantic_kernel.exceptions.content_exceptions import * # noqa: F403
from semantic_kernel.exceptions.function_exceptions import * # noqa: F403
from semantic_kernel.exceptions.kernel_exceptions import * # noqa: F403
from semantic_kernel.exceptions.memory_connector_exceptions import * # noqa: F403
from semantic_kernel.exceptions.planner_exceptions import * # noqa: F403
from semantic_kernel.exceptions.service_exceptions import * # noqa: F403
from semantic_kernel.exceptions.template_engine_exceptions import * # noqa: F403
@@ -28,10 +28,10 @@ class FunctionCallInvalidArgumentsException(ContentException):
__all__ = [
"ContentException",
"ContentSerializationError",
"ContentInitializationError",
"ContentAdditionException",
"FunctionCallInvalidNameException",
"ContentException",
"ContentInitializationError",
"ContentSerializationError",
"FunctionCallInvalidArgumentsException",
"FunctionCallInvalidNameException",
]
@@ -50,14 +50,14 @@ class PromptRenderingException(FunctionException):
__all__ = [
"FunctionException",
"FunctionExecutionException",
"FunctionInitializationError",
"FunctionInvalidParamNameError",
"FunctionInvalidNameError",
"FunctionInvalidParamNameError",
"FunctionNameNotUniqueError",
"FunctionResultError",
"FunctionSyntaxError",
"PluginInitializationError",
"PluginInvalidNameError",
"FunctionExecutionException",
"FunctionResultError",
"PromptRenderingException",
]
@@ -47,8 +47,8 @@ __all__ = [
"KernelFunctionAlreadyExistsError",
"KernelFunctionNotFoundError",
"KernelInvokeException",
"KernelPluginInvalidConfigurationError",
"KernelPluginNotFoundError",
"KernelServiceNotFoundError",
"KernelPluginInvalidConfigurationError",
"OperationCancelledException",
]
@@ -29,10 +29,10 @@ class PlannerCreatePlanError(PlannerException):
__all__ = [
"PlannerCreatePlanError",
"PlannerException",
"PlannerExecutionException",
"PlannerInvalidConfigurationError",
"PlannerInvalidGoalError",
"PlannerInvalidPlanError",
"PlannerInvalidConfigurationError",
"PlannerCreatePlanError",
]
@@ -45,14 +45,14 @@ class ServiceResourceNotFoundError(ServiceException):
__all__ = [
"ServiceContentFilterException",
"ServiceException",
"ServiceInvalidAuthError",
"ServiceInitializationError",
"ServiceResponseException",
"ServiceInvalidTypeError",
"ServiceInvalidAuthError",
"ServiceInvalidExecutionSettingsError",
"ServiceInvalidRequestError",
"ServiceInvalidResponseError",
"ServiceInvalidExecutionSettingsError",
"ServiceContentFilterException",
"ServiceInvalidTypeError",
"ServiceResourceNotFoundError",
"ServiceResponseException",
]
@@ -101,20 +101,20 @@ class Jinja2TemplateRenderException(BlockRenderException):
__all__ = [
"BlockException",
"BlockSyntaxError",
"BlockRenderException",
"VarBlockSyntaxError",
"VarBlockRenderError",
"ValBlockSyntaxError",
"NamedArgBlockSyntaxError",
"FunctionIdBlockSyntaxError",
"BlockSyntaxError",
"CodeBlockRenderException",
"CodeBlockSyntaxError",
"CodeBlockTokenError",
"CodeBlockRenderException",
"TemplateSyntaxError",
"TemplateRenderException",
"HandlebarsTemplateSyntaxError",
"FunctionIdBlockSyntaxError",
"HandlebarsTemplateRenderException",
"Jinja2TemplateSyntaxError",
"HandlebarsTemplateSyntaxError",
"Jinja2TemplateRenderException",
"Jinja2TemplateSyntaxError",
"NamedArgBlockSyntaxError",
"TemplateRenderException",
"TemplateSyntaxError",
"ValBlockSyntaxError",
"VarBlockRenderError",
"VarBlockSyntaxError",
]
@@ -121,7 +121,7 @@ def _rebuild_auto_function_invocation_context() -> None:
from semantic_kernel.functions.function_result import FunctionResult # noqa: F401
from semantic_kernel.functions.kernel_arguments import KernelArguments # noqa: F401
from semantic_kernel.functions.kernel_function import KernelFunction # noqa: F401
from semantic_kernel.kernel import Kernel # noqa: F403 F401
from semantic_kernel.kernel import Kernel # noqa: F401
AutoFunctionInvocationContext.model_rebuild()
@@ -141,6 +141,6 @@ def _rebuild_prompt_render_context() -> None:
from semantic_kernel.functions.function_result import FunctionResult # noqa: F401
from semantic_kernel.functions.kernel_arguments import KernelArguments # noqa: F401
from semantic_kernel.functions.kernel_function import KernelFunction # noqa: F401
from semantic_kernel.kernel import Kernel # noqa: F403 F401
from semantic_kernel.kernel import Kernel # noqa: F401
PromptRenderContext.model_rebuild()
+1 -1
View File
@@ -16,8 +16,8 @@ __all__ = [
"KernelFunction",
"KernelFunctionFromMethod",
"KernelFunctionFromPrompt",
"kernel_function",
"KernelFunctionMetadata",
"KernelParameterMetadata",
"KernelPlugin",
"kernel_function",
]
@@ -43,8 +43,8 @@ class FunctionResult(KernelBaseModel):
if isinstance(self.value[0], KernelContent)
else ",".join(map(str, self.value))
)
elif isinstance(self.value, dict):
# TODO: remove this once function result doesn't include input args
if isinstance(self.value, dict):
# TODO (eavanvalkenburg): remove this once function result doesn't include input args
# This is so an integration test can pass.
return str(list(self.value.values())[-1])
return str(self.value)
@@ -59,9 +59,8 @@ class FunctionResult(KernelBaseModel):
Args:
index (int): The index of the inner content if the inner content is a list, default 0.
"""
if isinstance(self.value, list):
if isinstance(self.value[index], KernelContent):
return self.value[index].inner_content
if isinstance(self.value, list) and isinstance(self.value[index], KernelContent):
return self.value[index].inner_content
if isinstance(self.value, KernelContent):
return self.value.inner_content
return None
@@ -388,9 +388,9 @@ class KernelFunctionExtension(KernelBaseModel, ABC):
"""
if not self.plugins:
return []
included_plugins = filters.get("included_plugins", None)
included_plugins = filters.get("included_plugins")
excluded_plugins = filters.get("excluded_plugins", [])
included_functions = filters.get("included_functions", None)
included_functions = filters.get("included_functions")
excluded_functions = filters.get("excluded_functions", [])
if included_plugins and excluded_plugins:
raise ValueError("Cannot use both included_plugins and excluded_plugins at the same time.")
@@ -326,17 +326,17 @@ through prompt_template_config or in the prompt_template."
if not config_exists and not prompt_exists:
raise FunctionInitializationError(
f"{PROMPT_FILE_NAME} and {CONFIG_FILE_NAME} files are required to create a "
f"function from a directory, path: {str(path)}."
f"function from a directory, path: {path!s}."
)
elif not config_exists:
if not config_exists:
raise FunctionInitializationError(
f"{CONFIG_FILE_NAME} files are required to create a function from a directory, "
f"path: {str(path)}, prompt file is there."
f"path: {path!s}, prompt file is there."
)
elif not prompt_exists:
if not prompt_exists:
raise FunctionInitializationError(
f"{PROMPT_FILE_NAME} files are required to create a function from a directory, "
f"path: {str(path)}, config file is there."
f"path: {path!s}, config file is there."
)
function_name = os.path.basename(path)
@@ -22,14 +22,13 @@ class KernelParameterMetadata(KernelBaseModel):
@classmethod
def form_schema(cls, data: Any) -> Any:
"""Create a schema for the parameter metadata."""
if isinstance(data, dict):
if data.get("schema_data") is None:
type_object = data.get("type_object", None)
type_ = data.get("type_", None)
default_value = data.get("default_value", None)
description = data.get("description", None)
inferred_schema = cls.infer_schema(type_object, type_, default_value, description)
data["schema_data"] = inferred_schema
if isinstance(data, dict) and data.get("schema_data") is None:
type_object = data.get("type_object", None)
type_ = data.get("type_", None)
default_value = data.get("default_value", None)
description = data.get("description", None)
inferred_schema = cls.infer_schema(type_object, type_, default_value, description)
data["schema_data"] = inferred_schema
return data
@classmethod
+6 -8
View File
@@ -148,22 +148,20 @@ class Kernel(KernelFilterExtension, KernelFunctionExtension, KernelServicesExten
metadata: dict[str, Any] = {},
**kwargs: Any,
) -> FunctionResult | None:
"""Execute one or more functions.
When multiple functions are passed the FunctionResult of each is put into a list.
"""Execute a function and return the FunctionResult.
Args:
function (KernelFunction): The function or functions to execute,
this value has precedence when supplying both this and using function_name and plugin_name,
if this is none, function_name and plugin_name are used and cannot be None.
this value has precedence when supplying both this and using function_name and plugin_name,
if this is none, function_name and plugin_name are used and cannot be None.
arguments (KernelArguments): The arguments to pass to the function(s), optional
function_name (str | None): The name of the function to execute
plugin_name (str | None): The name of the plugin to execute
metadata (dict[str, Any]): The metadata to pass to the function(s)
kwargs (dict[str, Any]): arguments that can be used instead of supplying KernelArguments
Returns:
FunctionResult | list[FunctionResult] | None: The result of the function(s)
Raises:
KernelInvokeException: If an error occurs during function invocation
"""
if arguments is None:
@@ -183,7 +181,7 @@ class Kernel(KernelFilterExtension, KernelFunctionExtension, KernelServicesExten
except Exception as exc:
logger.error(
"Something went wrong in function invocation. During function invocation:"
f" '{function.fully_qualified_name}'. Error description: '{str(exc)}'"
f" '{function.fully_qualified_name}'. Error description: '{exc!s}'"
)
raise KernelInvokeException(
f"Error occurred while invoking function: '{function.fully_qualified_name}'"
+1 -1
View File
@@ -27,7 +27,7 @@ class KernelBaseSettings(BaseSettings):
In the case where a value is specified for the same Settings field in multiple ways,
the selected value is determined as follows (in descending order of priority):
- Arguments passed to the Settings class initialiser.
- Arguments passed to the Settings class initializer.
- Environment variables, e.g. my_prefix_special_function as described above.
- Variables loaded from a dotenv (.env) file.
- Variables loaded from the secrets directory.
+1 -1
View File
@@ -2,4 +2,4 @@
from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory
from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore
__all__ = ["VolatileMemoryStore", "SemanticTextMemory"]
__all__ = ["SemanticTextMemory", "VolatileMemoryStore"]
+2 -2
View File
@@ -16,7 +16,7 @@ class NullMemory(SemanticTextMemoryBase):
additional_metadata: str | None = None,
) -> None:
"""Nullifies behavior of SemanticTextMemoryBase save_information."""
return None
return
async def save_reference(
self,
@@ -28,7 +28,7 @@ class NullMemory(SemanticTextMemoryBase):
additional_metadata: str | None = None,
) -> None:
"""Nullifies behavior of SemanticTextMemoryBase save_reference."""
return None
return
async def get(self, collection: str, query: str) -> MemoryQueryResult | None:
"""Nullifies behavior of SemanticTextMemoryBase get."""
@@ -15,7 +15,6 @@ from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class SemanticTextMemory(SemanticTextMemoryBase):
_storage: MemoryStoreBase = PrivateAttr()
# TODO: replace with kernel and service_selector pattern
_embeddings_generator: EmbeddingGeneratorBase = PrivateAttr()
def __init__(self, storage: MemoryStoreBase, embeddings_generator: EmbeddingGeneratorBase) -> None:
@@ -49,7 +48,6 @@ class SemanticTextMemory(SemanticTextMemoryBase):
additional_metadata (Optional[str]): Additional metadata of the information.
embeddings_kwargs (Optional[Dict[str, Any]]): The embeddings kwargs of the information.
"""
# TODO: not the best place to create collection, but will address this behavior together with .NET SK
if not await self._storage.does_collection_exist(collection_name=collection):
await self._storage.create_collection(collection_name=collection)
@@ -85,7 +83,6 @@ class SemanticTextMemory(SemanticTextMemoryBase):
additional_metadata (Optional[str]): Additional metadata of the reference.
embeddings_kwargs (Optional[Dict[str, Any]]): The embeddings kwargs of the reference.
"""
# TODO: not the best place to create collection, but will address this behavior together with .NET SK
if not await self._storage.does_collection_exist(collection_name=collection):
await self._storage.create_collection(collection_name=collection)
@@ -65,7 +65,6 @@ class SemanticTextMemoryBase(KernelBaseModel):
self,
collection: str,
key: str,
# TODO: with_embedding: bool,
) -> "MemoryQueryResult | None":
"""Get information from the memory (calls the memory store's get method).
+4 -4
View File
@@ -15,10 +15,10 @@ from semantic_kernel.planners.planner_options import PlannerOptions
from semantic_kernel.planners.sequential_planner import SequentialPlanner
__all__ = [
"Plan",
"SequentialPlanner",
"PlannerOptions",
"FunctionCallingStepwisePlannerOptions",
"FunctionCallingStepwisePlanner",
"FunctionCallingStepwisePlannerOptions",
"FunctionCallingStepwisePlannerResult",
"Plan",
"PlannerOptions",
"SequentialPlanner",
]
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.planners.function_calling_stepwise_planner.function_calling_stepwise_planner import (
FunctionCallingStepwisePlanner,
)
@@ -73,10 +73,19 @@ class FunctionCallingStepwisePlanner(KernelBaseModel):
the function calling stepwise planner. Defaults to None.
"""
options = options or FunctionCallingStepwisePlannerOptions()
generate_plan_yaml = (
options.get_initial_plan() if options.get_initial_plan else open(PLAN_YAML_FILE_PATH).read()
)
step_prompt = options.get_step_prompt() if options.get_step_prompt else open(STEP_PROMPT_FILE_PATH).read()
if options.get_initial_plan:
generate_plan_yaml = options.get_initial_plan()
else:
with open(PLAN_YAML_FILE_PATH) as f:
generate_plan_yaml = f.read()
if options.get_step_prompt:
step_prompt = options.get_step_prompt()
else:
with open(STEP_PROMPT_FILE_PATH) as f:
step_prompt = f.read()
options.excluded_plugins.add(STEPWISE_PLANNER_PLUGIN_NAME)
super().__init__(
@@ -225,7 +234,7 @@ class FunctionCallingStepwisePlanner(KernelBaseModel):
service: OpenAIChatCompletion | AzureChatCompletion,
) -> ChatHistory:
"""Build the chat history for the stepwise planner."""
chat_history = ChatHistory()
ChatHistory()
additional_arguments = KernelArguments(
goal=goal,
initial_plan=initial_plan,
@@ -237,8 +246,7 @@ class FunctionCallingStepwisePlanner(KernelBaseModel):
)
)
prompt = await kernel_prompt_template.render(kernel, arguments)
chat_history = ChatHistory.from_rendered_prompt(prompt)
return chat_history
return ChatHistory.from_rendered_prompt(prompt)
def _create_config_from_yaml(self, kernel: Kernel) -> "KernelFunction":
"""A temporary method to create a function from the yaml file.
@@ -266,7 +274,8 @@ class FunctionCallingStepwisePlanner(KernelBaseModel):
) -> str:
"""Generate the plan for the given question using the kernel."""
generate_plan_function = self._create_config_from_yaml(kernel)
# TODO: revisit when function call behavior is finalized, and other function calling models are added
# TODO (moonbox3): revisit when function call behavior is finalized, and other function calling models are added
# https://github.com/microsoft/semantic-kernel/issues/6458
functions_manual = [
kernel_function_metadata_to_openai_tool_format(f)
for f in kernel.get_list_of_function_metadata(
+23 -28
View File
@@ -81,8 +81,7 @@ class Plan:
"""Check if the plan is native code."""
if self._is_prompt is None:
return None
else:
return not self._is_prompt
return not self._is_prompt
@property
def prompt_execution_settings(self) -> PromptExecutionSettings:
@@ -166,28 +165,27 @@ class Plan:
exc,
) from exc
return result
else:
# loop through steps until completion
partial_results = []
while self.has_next_step:
function_arguments = copy(arguments)
self.add_variables_to_state(self._state, function_arguments)
logger.info(
"Invoking next step: "
+ str(self._steps[self._next_step_index].name)
+ " with arguments: "
+ str(function_arguments)
)
result = await self.invoke_next_step(kernel, function_arguments)
if result:
partial_results.append(result)
self._state[Plan.DEFAULT_RESULT_KEY] = str(result)
arguments = self.update_arguments_with_outputs(arguments)
logger.info(f"updated arguments: {arguments}")
# loop through steps until completion
partial_results = []
while self.has_next_step:
function_arguments = copy(arguments)
self.add_variables_to_state(self._state, function_arguments)
logger.info(
"Invoking next step: "
+ str(self._steps[self._next_step_index].name)
+ " with arguments: "
+ str(function_arguments)
)
result = await self.invoke_next_step(kernel, function_arguments)
if result:
partial_results.append(result)
self._state[Plan.DEFAULT_RESULT_KEY] = str(result)
arguments = self.update_arguments_with_outputs(arguments)
logger.info(f"updated arguments: {arguments}")
result_string = str(partial_results[-1]) if len(partial_results) > 0 else ""
result_string = str(partial_results[-1]) if len(partial_results) > 0 else ""
return FunctionResult(function=self.metadata, value=result_string, metadata={"results": partial_results})
return FunctionResult(function=self.metadata, value=result_string, metadata={"results": partial_results})
def set_ai_configuration(
self,
@@ -296,8 +294,8 @@ class Plan:
def add_variables_to_state(self, state: KernelArguments, variables: KernelArguments) -> None:
"""Add variables to the state."""
for key in variables.keys():
if key not in state.keys():
for key in variables:
if key not in state:
state[key] = variables[key]
def update_arguments_with_outputs(self, arguments: KernelArguments) -> KernelArguments:
@@ -310,10 +308,7 @@ class Plan:
arguments["input"] = result_string
for item in self._steps[self._next_step_index - 1]._outputs:
if item in self._state:
arguments[item] = self._state[item]
else:
arguments[item] = result_string
arguments[item] = self._state.get(item, result_string)
return arguments
def get_next_step_arguments(self, arguments: KernelArguments, step: "Plan") -> KernelArguments:
@@ -24,13 +24,13 @@ class PlannerFunctionExtension:
for parameter in function.parameters
]
inputs = "\n".join(inputs)
return f"{function.fully_qualified_name}:\n description: {function.description}\n inputs:\n " f" {inputs}"
return f"{function.fully_qualified_name}:\n description: {function.description}\n inputs:\n {inputs}"
@staticmethod
def to_embedding_string(function: KernelFunctionMetadata):
"""Convert the function to a string that can be used as an embedding."""
inputs = "\n".join([f" - {parameter.name}: {parameter.description}" for parameter in function.parameters])
return f"{function.name}:\n description: {function.description}\n " f" inputs:\n{inputs}"
return f"{function.name}:\n description: {function.description}\n inputs:\n{inputs}"
class PlannerKernelExtension:
@@ -63,12 +63,8 @@ class PlannerKernelExtension:
excluded_plugins = options.excluded_plugins or []
excluded_functions = options.excluded_functions or []
available_functions = [
return [
func
for func in kernel.get_list_of_function_metadata()
if (func.plugin_name not in excluded_plugins and func.name not in excluded_functions)
]
# TODO support for semantic memory query
return available_functions
@@ -2,6 +2,8 @@
from collections.abc import Callable
from pydantic import Field
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
from semantic_kernel.kernel_pydantic import KernelBaseModel
@@ -9,7 +11,6 @@ from semantic_kernel.kernel_pydantic import KernelBaseModel
class PlannerOptions(KernelBaseModel):
"""The default planner options that planners inherit from."""
excluded_plugins: set[str] = set()
excluded_functions: set[str] = set()
excluded_plugins: set[str] = Field(default_factory=set)
excluded_functions: set[str] = Field(default_factory=set)
get_available_functions: Callable[["PlannerOptions", str | None], list[KernelFunctionMetadata]] | None = None
# TODO semantic_memory_config
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.planners.sequential_planner.sequential_planner import (
SequentialPlanner,
)
@@ -17,7 +17,7 @@ from semantic_kernel.prompt_template.prompt_template_config import PromptTemplat
SEQUENTIAL_PLANNER_DEFAULT_DESCRIPTION = (
"Given a request or command or goal generate a step by step plan to "
+ "fulfill the request using functions. This ability is also known as decision making and function flow"
"fulfill the request using functions. This ability is also known as decision making and function flow"
)
CUR_DIR = os.path.dirname(os.path.realpath(__file__))
@@ -44,7 +44,7 @@ class SequentialPlanner:
kernel: Kernel,
service_id: str,
config: SequentialPlannerConfig = None,
prompt: str = None,
prompt: str | None = None,
) -> None:
"""Initializes a new instance of the SequentialPlanner class.
@@ -76,11 +76,13 @@ class SequentialPlanner:
prompt_config.template = prompt_template
# if a service_id is provided, use it instead of the default
if service_id and service_id not in prompt_config.execution_settings:
# Move 'default' settings to this service_id if 'default' exists
if "default" in prompt_config.execution_settings:
settings = prompt_config.execution_settings.pop("default")
prompt_config.execution_settings[service_id] = settings
if (
service_id
and service_id not in prompt_config.execution_settings
and "default" in prompt_config.execution_settings
):
settings = prompt_config.execution_settings.pop("default")
prompt_config.execution_settings[service_id] = settings
return self._kernel.add_function(
plugin_name=self.RESTRICTED_PLUGIN_NAME,
@@ -8,13 +8,13 @@ class SequentialPlannerConfig:
self,
relevancy_threshold: float | None = None,
max_relevant_functions: int = 100,
excluded_plugins: list[str] = None,
excluded_functions: list[str] = None,
included_functions: list[str] = None,
excluded_plugins: list[str] | None = None,
excluded_functions: list[str] | None = None,
included_functions: list[str] | None = None,
max_tokens: int = 1024,
allow_missing_functions: bool = False,
get_available_functions: Callable = None,
get_plugin_function: Callable = None,
get_available_functions: Callable | None = None,
get_plugin_function: Callable | None = None,
):
"""Initializes a new instance of the SequentialPlannerConfig class."""
self.relevancy_threshold: float = relevancy_threshold
@@ -21,13 +21,13 @@ class SequentialPlannerFunctionExtension:
for parameter in function.parameters
]
inputs = "\n".join(inputs)
return f"{function.fully_qualified_name}:\n description: {function.description}\n inputs:\n " f" {inputs}"
return f"{function.fully_qualified_name}:\n description: {function.description}\n inputs:\n {inputs}"
@staticmethod
def to_embedding_string(function: KernelFunctionMetadata):
"""Convert the function to an embedding string."""
inputs = "\n".join([f" - {parameter.name}: {parameter.description}" for parameter in function.parameters])
return f"{function.name}:\n description: {function.description}\n " f" inputs:\n{inputs}"
return f"{function.name}:\n description: {function.description}\n inputs:\n{inputs}"
class SequentialPlannerKernelExtension:
@@ -38,7 +38,7 @@ class SequentialPlannerKernelExtension:
async def get_functions_manual(
kernel: "Kernel",
arguments: KernelArguments,
semantic_query: str = None,
semantic_query: str | None = None,
config: SequentialPlannerConfig = None,
) -> str:
"""Get the functions manual."""
@@ -99,7 +99,8 @@ class SequentialPlannerKernelExtension:
) -> list[KernelFunctionMetadata]:
"""Get relevant functions from the memories."""
relevant_functions = []
# TODO: cancellation
# TODO (eavanvalkenburg): cancellation
# https://github.com/microsoft/semantic-kernel/issues/5668
if memories is None:
return relevant_functions
for memory_entry in memories:
@@ -69,8 +69,7 @@ class SequentialPlanParser:
if allow_missing_functions:
plan.add_steps([Plan.from_goal(plugin_function_name)])
continue
else:
raise PlannerInvalidPlanError(f"Failed to find function '{plugin_function_name}'.") from exc
raise PlannerInvalidPlanError(f"Failed to find function '{plugin_function_name}'.") from exc
else:
try:
func = kernel.get_function_from_fully_qualified_function_name(plugin_function_name)
@@ -78,10 +77,9 @@ class SequentialPlanParser:
if allow_missing_functions:
plan.add_steps([Plan.from_goal(plugin_function_name)])
continue
else:
raise PlannerInvalidPlanError(
f"Failed to find function '{plugin_function_name}'.",
) from exc
raise PlannerInvalidPlanError(
f"Failed to find function '{plugin_function_name}'.",
) from exc
plan_step = Plan.from_function(func)
@@ -7,9 +7,9 @@ from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptT
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
__all__ = [
"KernelPromptTemplate",
"HandlebarsPromptTemplate",
"Jinja2PromptTemplate",
"InputVariable",
"Jinja2PromptTemplate",
"KernelPromptTemplate",
"PromptTemplateConfig",
]
@@ -76,7 +76,7 @@ class PromptTemplateConfig(KernelBaseModel):
name=variable.name,
description=variable.description,
default_value=variable.default,
type_=variable.json_schema, # TODO: 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
@@ -5,7 +5,7 @@ from semantic_kernel.prompt_template.utils.jinja2_system_helpers import JINJA2_S
from semantic_kernel.prompt_template.utils.template_function_helpers import create_template_helper_from_function
__all__ = [
"create_template_helper_from_function",
"HANDLEBAR_SYSTEM_HELPERS",
"JINJA2_SYSTEM_HELPERS",
"create_template_helper_from_function",
]

Some files were not shown because too many files have changed in this diff Show More