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
+51
View File
@@ -155,6 +155,57 @@ It's important to note that most of this library is written with asynchronous in
developer should always assume everything is asynchronous. One can use the function signature
with either `async def` or `def` to understand if something is asynchronous or not.
### Documentation
Each file should have a single first line containing: # Copyright (c) Microsoft. All rights reserved.
We follow the [Google Docstring](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#383-functions-and-methods) style guide for functions and methods.
They are currently not checked for private functions (functions starting with '_').
They should contain:
- Single line explaining what the function does, ending with a period.
- If necessary to further explain the logic a newline follows the first line and then the explanation is given.
- The following three sections are optional, and if used should be separated by a single empty line.
- Arguments are then specified after a header called `Args:`, with each argument being specified in the following format:
- `arg_name` (`arg_type`): Explanation of the argument, arg_type is optional, as long as you are consistent.
- if a longer explanation is needed for a argument, it should be placed on the next line, indented by 4 spaces.
- Default values do not have to be specified, they will be pulled from the definition.
- Returns are specified after a header called `Returns:` or `Yields:`, with the return type and explanation of the return value.
- Finally, a header for exceptions can be added, called `Raises:`, with each exception being specified in the following format:
- `ExceptionType`: Explanation of the exception.
- if a longer explanation is needed for a exception, it should be placed on the next line, indented by 4 spaces.
Putting them all together, gives you at minimum this:
```python
def equal(arg1: str, arg2: str) -> bool:
"""Compares two strings and returns True if they are the same."""
...
```
Or a complete version of this:
```python
def equal(arg1: str, arg2: str) -> bool:
"""Compares two strings and returns True if they are the same.
Here is extra explanation of the logic involved.
Args:
arg1 (str): The first string to compare.
arg2 (str): The second string to compare.
This string requires extra explanation.
Returns:
bool: True if the strings are the same, False otherwise.
Raises:
ValueError: If one of the strings is empty.
"""
...
```
If in doubt, use the link above to read much more considerations of what to do and when, or use common sense.
## Pydantic and Serialization
[Pydantic Documentation](https://docs.pydantic.dev/1.10/)
+11 -5
View File
@@ -127,18 +127,24 @@ target-version = "py310"
include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"]
[tool.ruff.lint]
select = ["D", "E", "F", "I"]
ignore = ["D100", "D101", "D104"]
preview = true
select = ["D", "E", "F", "I", "CPY", "ISC", "INP", "RSE102", "RET", "SIM", "TD", "FIX", "ERA001", "RUF"]
ignore = ["D100", "D101", "D104", "TD003", "FIX002"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.per-file-ignores]
# Ignore all directories named `tests`.
"tests/**" = ["D"]
"samples/**" = ["D"]
# Ignore all directories named `tests` and `samples`.
"tests/**" = ["D", "INP", "TD", "ERA001", "RUF"]
"samples/**" = ["D", "INP", "ERA001", "RUF"]
# Ignore all files that end in `_test.py`.
"*_test.py" = ["D"]
"*.ipynb" = ["CPY", "E501"]
[tool.ruff.lint.flake8-copyright]
notice-rgx = "^# Copyright \\(c\\) Microsoft\\. All rights reserved\\."
min-file-size = 1
[tool.bandit]
targets = ["python/semantic_kernel"]
@@ -28,7 +28,7 @@ chat_service = AzureChatCompletion(
kernel.add_service(chat_service)
## there are three ways to create the request settings in code: # noqa: E266
## Note: the prompt_execution_settings are a dictionary with the service_id as the key and the request settings as the value. # noqa: E501
# Note: the prompt_execution_settings are a dictionary with the service_id as the key and the request settings as the value. # noqa: E501
## 1. create the request settings from the base class: # noqa: E266
# from semantic_kernel.connectors.ai.chat_completion_client_base import PromptExecutionSettings
@@ -42,7 +42,7 @@ async def input_output_filter(
if context.result:
logger.info(f'Usage: {context.result.metadata.get("usage")}')
context.arguments["chat_history"].add_message(context.result.value[0])
print(f"Mosscap:> {str(context.result)}")
print(f"Mosscap:> {context.result!s}")
async def main() -> None:
+16 -19
View File
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
@@ -78,8 +80,7 @@ def setup(use_azure: bool = False, plugin_name: str = "GroundingPlugin"):
def get_summary_text():
summary_text = """My father, a respected resident of Milan, was a close friend of a merchant named Beaufort who, after a series of misfortunes, moved to Zurich in poverty. My father was upset by his friend's troubles and sought him out, finding him in a mean street. Beaufort had saved a small sum of money, but it was not enough to support him and his daughter, Mary. Mary procured work to eek out a living, but after ten months her father died, leaving her a beggar. My father came to her aid and two years later they married when they visited Rome.""" # noqa: E501
summary_text = summary_text.replace("\n", " ").replace(" ", " ")
return summary_text
return summary_text.replace("\n", " ").replace(" ", " ")
async def run_entity_extraction(kernel: Kernel, plugin_name: str, summary_text: str):
@@ -87,38 +88,34 @@ async def run_entity_extraction(kernel: Kernel, plugin_name: str, summary_text:
topic="people and places", example_entities="John, Jane, mother, brother, Paris, Rome", input=summary_text
)
extraction_result = await kernel.invoke(
return await kernel.invoke(
plugin_name=plugin_name, function_name="ExtractEntities", arguments=arguments
)
return extraction_result
async def run_reference_check(kernel: Kernel, plugin_name: str, extraction_result):
grounding_result = await kernel.invoke(
return await kernel.invoke(
plugin_name=plugin_name,
function_name="ReferenceCheckEntities",
input=str(extraction_result),
reference_context=get_grounding_text(),
)
return grounding_result
async def run_entity_excision(kernel: Kernel, plugin_name: str, summary_text, grounding_result):
excision_result = await kernel.invoke(
return await kernel.invoke(
plugin_name=plugin_name,
function_name="ExciseEntities",
input=summary_text,
ungrounded_entities=grounding_result,
)
return excision_result
async def run_grounding(use_azure: bool = False):
plugin_name = "GroundingPlugin"
kernel = setup(use_azure, plugin_name=plugin_name)
print(f"\n{Colors.CBOLD.value}Groundingsness Checking Plugins\n{Colors.CEND.value}")
print(f"\n{ '-'*80 }\n")
print(f"\n{ '-' * 80 }\n")
print(
f"""{Colors.CGREEN.value}A well-known problem with large language models (LLMs) is that they make things up. These are sometimes called 'hallucinations' but a safer (and less anthropomorphic) term is 'ungrounded addition' - something in the text which cannot be firmly established. When attempting to establish whether or not something in an LLM response is 'true' we can either check for it in the supplied prompt (this is called 'narrow grounding') or use our general knowledge ('broad grounding'). Note that narrow grounding can lead to things being classified as 'true, but ungrounded.' For example "I live in Switzerland" is **not** _narrowly_ grounded in "I live in Geneva" even though it must be true (it **is** _broadly_ grounded).
@@ -133,10 +130,10 @@ What is an 'entity' in this context? In its simplest form, it's a named object s
print(f"\nThe grounding text: \n{Colors.CGREY.value}{get_grounding_text()}{Colors.CEND.value}")
print(f"\n{ '-'*80 }\n")
print(f"\n{ '-' * 80 }\n")
summary_text = get_summary_text()
print(f"Summary text: \n{Colors.CBLUE.value}{summary_text}{Colors.CEND.value}")
print(f"\n{ '-'*80 }\n")
print(f"\n{ '-' * 80 }\n")
print(
f"""{Colors.CGREEN.value}Some things to note:
@@ -154,25 +151,25 @@ The grounding plugin has three stages:
Now, let us start calling individual semantic functions.{Colors.CEND.value}"""
)
print(f"\n{ '-'*80 }\n")
print(f"\n{ '-' * 80 }\n")
print(
f"{Colors.CGREEN.value}First we run the extraction function on the summary, this results in all the extracted entities.{Colors.CEND.value}" # noqa: E501
)
extraction_result = await run_entity_extraction(kernel, plugin_name, summary_text)
print(f"Extraction result: \n{Colors.CBLUE.value}{str(extraction_result)}{Colors.CEND.value}")
print(f"\n{ '-'*80 }\n")
print(f"Extraction result: \n{Colors.CBLUE.value}{extraction_result!s}{Colors.CEND.value}")
print(f"\n{ '-' * 80 }\n")
print(
f"{Colors.CGREEN.value}Next we run the reference check function on the summary, this loads the grounding text as part of it in order to know the 'truth'. This returns a list of ungrounded entities.{Colors.CEND.value}" # noqa: E501
)
grounding_result = await run_reference_check(kernel, plugin_name, extraction_result)
print(f"Grounding result: \n{Colors.CBLUE.value}{str(grounding_result)}{Colors.CEND.value}")
print(f"\n{ '-'*80 }\n")
print(f"Grounding result: \n{Colors.CBLUE.value}{grounding_result!s}{Colors.CEND.value}")
print(f"\n{ '-' * 80 }\n")
print(
f"{Colors.CGREEN.value}Finally we run the excision function on the summary, this removes the ungrounded entities from the summary.{Colors.CEND.value}" # noqa: E501
)
excision_result = await run_entity_excision(kernel, plugin_name, summary_text, grounding_result)
print(f"The final summary text: \n{Colors.CBLUE.value}{str(excision_result)}{Colors.CEND.value}")
print(f"\n{ '-'*80 }\n")
print(f"The final summary text: \n{Colors.CBLUE.value}{excision_result!s}{Colors.CEND.value}")
print(f"\n{ '-' * 80 }\n")
print(f"{Colors.CBOLD.value}Finished!{Colors.CEND.value}")
@@ -50,14 +50,12 @@ async def setup_chat_with_memory(
execution_settings={service_id: kernel.get_prompt_execution_settings_from_service_id(service_id=service_id)},
)
chat_func = kernel.add_function(
return kernel.add_function(
function_name="chat_with_memory",
plugin_name="TextMemoryPlugin",
prompt_template_config=prompt_template_config,
)
return chat_func
async def chat(kernel: Kernel, chat_func: KernelFunction) -> bool:
try:
+1 -3
View File
@@ -50,14 +50,12 @@ async def setup_chat_with_memory(
execution_settings={service_id: kernel.get_prompt_execution_settings_from_service_id(service_id=service_id)},
)
chat_func = kernel.add_function(
return kernel.add_function(
function_name="chat_with_memory",
plugin_name="TextMemoryPlugin",
prompt_template_config=prompt_template_config,
)
return chat_func
async def chat(kernel: Kernel, chat_func: KernelFunction) -> bool:
try:
@@ -23,20 +23,19 @@ class WeatherPlugin:
def get_weather_for_city(self, city: Annotated[str, "The input city"]) -> Annotated[str, "The output is a string"]:
if city == "Boston":
return "61 and rainy"
elif city == "London":
if city == "London":
return "55 and cloudy"
elif city == "Miami":
if city == "Miami":
return "80 and sunny"
elif city == "Paris":
if city == "Paris":
return "60 and rainy"
elif city == "Tokyo":
if city == "Tokyo":
return "50 and sunny"
elif city == "Sydney":
if city == "Sydney":
return "75 and sunny"
elif city == "Tel Aviv":
if city == "Tel Aviv":
return "80 and sunny"
else:
return "31 and snowing"
return "31 and snowing"
async def main():
@@ -59,7 +59,7 @@ class OpenAIAuthenticationProvider:
) -> dict[str, str] | None:
"""An example of how to authenticate a request as part of an auth callback."""
if openai_auth_config.type == OpenAIAuthenticationType.NoneType:
return
return None
scheme = ""
credential = ""
@@ -24,7 +24,7 @@ async def main():
result = await kernel.invoke(plugin["productsUsingGET"], **query_params)
print(f"Function execution result: {str(result)}")
print(f"Function execution result: {result!s}")
if __name__ == "__main__":
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import semantic_kernel as sk
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from aiohttp import web
"""Server"""
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
@@ -11,11 +11,7 @@ from semantic_kernel.prompt_template import PromptTemplateConfig
async def main():
kernel = Kernel()
service_id = "chat-gpt"
kernel.add_service(
AzureChatCompletion(
service_id=service_id,
),
)
kernel.add_service(AzureChatCompletion(service_id=service_id))
connector = BingConnector()
web_plugin = kernel.add_plugin(WebSearchEnginePlugin(connector), "WebSearch")
@@ -10,8 +10,7 @@ async def text_completion_example_complete(kernel: Kernel, user_mssg, settings):
"""Complete a text prompt using the Google PaLM model and print the results."""
palm_text_completion = GooglePalmTextCompletion("models/text-bison-001")
kernel.add_service(palm_text_completion)
answer = await palm_text_completion.get_text_contents(user_mssg, settings)
return answer
return await palm_text_completion.get_text_contents(user_mssg, settings)
async def main() -> None:
@@ -38,7 +38,7 @@ class BookingSampleSettings(BaseSettings):
@classmethod
def create(cls, **kwargs):
if "env_file_path" in kwargs and kwargs["env_file_path"]:
if kwargs.get("env_file_path"):
cls.Config.env_file = kwargs["env_file_path"]
else:
cls.Config.env_file = None
@@ -34,7 +34,7 @@
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel import Kernel, kernel\n",
"from semantic_kernel import Kernel\n",
"\n",
"kernel = Kernel()"
]
@@ -174,7 +174,7 @@
"Epigram\n",
"There are a number of graffiti inscriptions on the Colossus of Memnon. Following three epigrams by Julia Balbilla, a fourth epigram, in elegiac couplets, entitled and presumably authored by \"Demo\" or \"Damo\" (the Greek inscription is difficult to read), is a dedication to the Muses.[2] The poem is traditionally published with the works of Balbilla, though the internal evidence suggests a different author.[1]\n",
"In the poem, Demo explains that Memnon has shown her special respect. In return, Demo offers the gift for poetry, as a gift to the hero. At the end of this epigram, she addresses Memnon, highlighting his divine status by recalling his strength and holiness.[2]\n",
"Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic dialect. The language indicates she was knowledgeable in Homeric poetry—'bearing a pleasant gift', for example, alludes to the use of that phrase throughout the Iliad and Odyssey.[a][2] \n",
"Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic dialect. The language indicates she was knowledgeable in Homeric poetry—'bearing a pleasant gift', for example, alludes to the use of that phrase throughout the Iliad and Odyssey.[a][2]\n",
"\"\"\""
]
},
@@ -194,9 +194,7 @@
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel.functions import KernelArguments\n",
"\n",
"summary = await kernel.invoke(summarize, KernelArguments(input=input_text))\n",
"summary = await kernel.invoke(summarize, input=input_text)\n",
"\n",
"print(summary)"
]
@@ -261,6 +259,10 @@
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (\n",
" OpenAIChatPromptExecutionSettings,\n",
")\n",
"\n",
"prompt = \"\"\"\n",
"{{$input}}\n",
"\n",
@@ -278,10 +280,6 @@
" does not conflict with the First or Second Law.\n",
"\"\"\"\n",
"\n",
"from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (\n",
" OpenAIChatPromptExecutionSettings,\n",
")\n",
"\n",
"if selectedService == Service.OpenAI:\n",
" execution_settings = OpenAIChatPromptExecutionSettings(\n",
" service_id=service_id,\n",
@@ -313,7 +311,7 @@
" prompt_template_config=prompt_template_config,\n",
")\n",
"\n",
"summary = await kernel.invoke(tldr_function, KernelArguments(input=text))\n",
"summary = await kernel.invoke(tldr_function, input=text)\n",
"\n",
"print(f\"Output: {summary}\")"
]
@@ -114,8 +114,8 @@
"from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (\n",
" OpenAIChatPromptExecutionSettings,\n",
")\n",
"from semantic_kernel.prompt_template.input_variable import InputVariable\n",
"from semantic_kernel.prompt_template import PromptTemplateConfig\n",
"from semantic_kernel.prompt_template.input_variable import InputVariable\n",
"\n",
"if selectedService == Service.OpenAI:\n",
" execution_settings = OpenAIChatPromptExecutionSettings(\n",
@@ -92,9 +92,9 @@
"outputs": [],
"source": [
"import semantic_kernel as sk\n",
"import semantic_kernel.connectors.ai.open_ai as sk_oai # noqa: F401\n",
"from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt\n",
"import semantic_kernel.connectors.ai.open_ai as sk_oai\n",
"from semantic_kernel.core_plugins.text_plugin import TextPlugin\n",
"from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt\n",
"\n",
"kernel = sk.Kernel()\n",
"service_id = \"default\"\n",
@@ -271,7 +271,7 @@
"outputs": [],
"source": [
"import semantic_kernel as sk\n",
"import semantic_kernel.connectors.ai.open_ai as sk_oai # noqa: F401\n",
"import semantic_kernel.connectors.ai.open_ai as sk_oai\n",
"\n",
"kernel = sk.Kernel()\n",
"service_id = \"default\"\n",
@@ -303,6 +303,7 @@
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from semantic_kernel.functions.kernel_function_decorator import kernel_function\n",
"\n",
"\n",
File diff suppressed because it is too large Load Diff
@@ -23,18 +23,6 @@
"!python -m pip install semantic-kernel[hugging_face]==1.0.3"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "508ad44f",
"metadata": {},
"outputs": [],
"source": [
"import semantic_kernel as sk\n",
"import semantic_kernel.connectors.ai.hugging_face as sk_hf\n",
"from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -305,7 +305,7 @@
"metadata": {},
"outputs": [],
"source": [
"import random, sys\n",
"import sys\n",
"\n",
"from semantic_kernel.functions import kernel_function\n",
"\n",
@@ -491,7 +491,6 @@
"metadata": {},
"outputs": [],
"source": [
"import random\n",
"\n",
"from semantic_kernel.functions import kernel_function\n",
"\n",
@@ -1,418 +1,418 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "68e1c158",
"metadata": {},
"source": [
"# Multiple Results\n"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "fb81bacd",
"metadata": {},
"source": [
"In this notebook we show how you can in a single request, have the LLM model return multiple results per prompt. This is useful for running experiments where you want to evaluate the robustness of your prompt and the parameters of your config against a particular large language model.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a77bdf89",
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==1.0.3"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3f4bfee4",
"metadata": {},
"outputs": [],
"source": [
"from services import Service\n",
"\n",
"# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n",
"selectedService = Service.OpenAI"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "508ad44f",
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel.contents import ChatHistory # noqa: F401\n",
"\n",
"if selectedService == Service.OpenAI or selectedService == Service.AzureOpenAI:\n",
" from semantic_kernel.connectors.ai.open_ai import ( # noqa: F401\n",
" AzureChatCompletion,\n",
" AzureChatPromptExecutionSettings,\n",
" AzureTextCompletion,\n",
" OpenAIChatCompletion,\n",
" OpenAIChatPromptExecutionSettings,\n",
" OpenAITextCompletion,\n",
" OpenAITextPromptExecutionSettings,\n",
" )\n",
"if selectedService == Service.HuggingFace:\n",
" from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextCompletion # noqa: F401"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "d8ddffc1",
"metadata": {},
"source": [
"First, we will set up the text and chat services we will be submitting prompts to.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8f8dcbc6",
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel import Kernel\n",
"\n",
"kernel = Kernel()\n",
"\n",
"# Configure Azure LLM service\n",
"if selectedService == Service.AzureOpenAI:\n",
" azure_text_service = AzureTextCompletion(\n",
" service_id=\"aoai_text\"\n",
" ) # set the deployment name to the value of your text model (e.g. gpt-35-turbo-instruct)\n",
" azure_chat_service = AzureChatCompletion(\n",
" service_id=\"aoai_chat\"\n",
" ) # set the deployment name to the value of your chat model\n",
"\n",
"# Configure OpenAI service\n",
"if selectedService == Service.OpenAI:\n",
" oai_text_service = OpenAITextCompletion(service_id=\"oai_text\", ai_model_id=\"gpt-3.5-turbo-instruct\")\n",
" oai_chat_service = OpenAIChatCompletion(service_id=\"oai_chat\", ai_model_id=\"gpt-3.5-turbo\")\n",
"\n",
"# Configure Hugging Face service\n",
"if selectedService == Service.HuggingFace:\n",
" hf_text_service = HuggingFaceTextCompletion(service_id=\"hf_text\", ai_model_id=\"distilgpt2\", task=\"text-generation\")"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "50561d82",
"metadata": {},
"source": [
"Next, we'll set up the completion request settings for text completion services.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "628c843e",
"metadata": {},
"outputs": [],
"source": [
"oai_text_prompt_execution_settings = OpenAITextPromptExecutionSettings(\n",
" service=\"oai_text\",\n",
" extension_data={\n",
" \"max_tokens\": 80,\n",
" \"temperature\": 0.7,\n",
" \"top_p\": 1,\n",
" \"frequency_penalty\": 0.5,\n",
" \"presence_penalty\": 0.5,\n",
" \"number_of_responses\": 3,\n",
" },\n",
")"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "857a9c89",
"metadata": {},
"source": [
"## Multiple Open AI Text Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e2979db8",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.OpenAI:\n",
" prompt = \"What is the purpose of a rubber duck?\"\n",
"\n",
" results = await oai_text_service.get_text_contents(prompt=prompt, settings=oai_text_prompt_execution_settings)\n",
" i = 1\n",
" for result in results:\n",
" print(f\"Result {i}: {result}\")\n",
" i += 1"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "4288d09f",
"metadata": {},
"source": [
"## Multiple Azure Open AI Text Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5319f14d",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.AzureOpenAI:\n",
" prompt = \"provide me a list of possible meanings for the acronym 'ORLD'\"\n",
"\n",
" results = await azure_text_service.get_text_contents(prompt=prompt, settings=oai_text_prompt_execution_settings)\n",
" i = 1\n",
" for result in results:\n",
" print(f\"Result {i}: {result}\")\n",
" i += 1"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "eb548f9c",
"metadata": {},
"source": [
"## Multiple Hugging Face Text Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4a148709",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.HuggingFace:\n",
" from semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings import (\n",
" HuggingFacePromptExecutionSettings,\n",
" )\n",
"\n",
" hf_prompt_execution_settings = HuggingFacePromptExecutionSettings(\n",
" service_id=\"hf_text\", extension_data={\"max_new_tokens\": 80, \"temperature\": 0.7, \"top_p\": 1}\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9525e4f3",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.HuggingFace:\n",
" prompt = \"The purpose of a rubber duck is\"\n",
"\n",
" results = await hf_text_service.get_text_contents(\n",
" prompt=prompt, prompt_execution_settings=hf_prompt_execution_settings\n",
" )\n",
" print(\"\".join(results))"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "da632e12",
"metadata": {},
"source": [
"Here, we're setting up the settings for Chat completions.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e5f11e46",
"metadata": {},
"outputs": [],
"source": [
"oai_chat_prompt_execution_settings = OpenAIChatPromptExecutionSettings(\n",
" service_id=\"oai_chat\",\n",
" max_tokens=80,\n",
" temperature=0.7,\n",
" top_p=1,\n",
" frequency_penalty=0.5,\n",
" presence_penalty=0.5,\n",
" number_of_responses=3,\n",
")"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "d6bf238e",
"metadata": {},
"source": [
"## Multiple OpenAI Chat Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dabc6a4c",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.OpenAI:\n",
" chat = ChatHistory()\n",
" chat.add_user_message(\n",
" \"It's a beautiful day outside, birds are singing, flowers are blooming. On days like these, kids like you...\"\n",
" )\n",
" results = await oai_chat_service.get_chat_message_contents(\n",
" chat_history=chat, settings=oai_chat_prompt_execution_settings\n",
" )\n",
" i = 0\n",
" for result in results:\n",
" print(f\"Result {i+1}: {str(result)}\")\n",
" i += 1"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "cdb8f740",
"metadata": {},
"source": [
"## Multiple Azure OpenAI Chat Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "66ba4767",
"metadata": {},
"outputs": [],
"source": [
"az_oai_prompt_execution_settings = AzureChatPromptExecutionSettings(\n",
" service_id=\"aoai_chat\",\n",
" max_tokens=80,\n",
" temperature=0.7,\n",
" top_p=1,\n",
" frequency_penalty=0.5,\n",
" presence_penalty=0.5,\n",
" number_of_responses=3,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b74a64a9",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.AzureOpenAI:\n",
" content = (\n",
" \"Tomorrow is going to be a great day, I can feel it. I'm going to wake up early, go for a run, and then...\"\n",
" )\n",
" chat = ChatHistory()\n",
" chat.add_user_message(content)\n",
" results = await azure_chat_service.get_chat_message_contents(\n",
" chat_history=chat, settings=az_oai_prompt_execution_settings\n",
" )\n",
" i = 0\n",
" for result in results:\n",
" print(f\"Result {i+1}: {str(result)}\")\n",
" i += 1"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "98c8191d",
"metadata": {},
"source": [
"## Streaming Multiple Results\n",
"\n",
"Here is an example pattern if you want to stream your multiple results. Note that this is not supported for Hugging Face text completions at this time.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "26a37702",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.OpenAI:\n",
" import os\n",
" import time\n",
"\n",
" from IPython.display import clear_output\n",
"\n",
" # Determine the clear command based on OS\n",
" clear_command = \"cls\" if os.name == \"nt\" else \"clear\"\n",
"\n",
" chat = ChatHistory()\n",
" chat.add_user_message(\"what is the purpose of a rubber duck?\")\n",
"\n",
" stream = oai_chat_service.get_streaming_chat_message_contents(\n",
" chat_history=chat, settings=oai_chat_prompt_execution_settings\n",
" )\n",
" number_of_responses = oai_chat_prompt_execution_settings.number_of_responses\n",
" texts = [\"\"] * number_of_responses\n",
"\n",
" last_clear_time = time.time()\n",
" clear_interval = 0.5 # seconds\n",
"\n",
" # Note: there are some quirks with displaying the output, which sometimes flashes and disappears.\n",
" # This could be influenced by a few factors specific to Jupyter notebooks and asynchronous processing.\n",
" # The following code attempts to buffer the results to avoid the output flashing on/off the screen.\n",
"\n",
" async for results in stream:\n",
" current_time = time.time()\n",
"\n",
" # Update texts with new results\n",
" for idx, result in enumerate(results):\n",
" if idx < number_of_responses:\n",
" texts[idx] += str(result)\n",
"\n",
" # Clear and display output at intervals\n",
" if current_time - last_clear_time > clear_interval:\n",
" clear_output(wait=True)\n",
" for idx, text in enumerate(texts):\n",
" print(f\"Result {idx + 1}: {text}\")\n",
" last_clear_time = current_time\n",
"\n",
" print(\"----------------------------------------\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "68e1c158",
"metadata": {},
"source": [
"# Multiple Results\n"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "fb81bacd",
"metadata": {},
"source": [
"In this notebook we show how you can in a single request, have the LLM model return multiple results per prompt. This is useful for running experiments where you want to evaluate the robustness of your prompt and the parameters of your config against a particular large language model.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a77bdf89",
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==1.0.3"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3f4bfee4",
"metadata": {},
"outputs": [],
"source": [
"from services import Service\n",
"\n",
"# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n",
"selectedService = Service.OpenAI"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "508ad44f",
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel.contents import ChatHistory\n",
"\n",
"if selectedService == Service.OpenAI or selectedService == Service.AzureOpenAI:\n",
" from semantic_kernel.connectors.ai.open_ai import (\n",
" AzureChatCompletion,\n",
" AzureChatPromptExecutionSettings,\n",
" AzureTextCompletion,\n",
" OpenAIChatCompletion,\n",
" OpenAIChatPromptExecutionSettings,\n",
" OpenAITextCompletion,\n",
" OpenAITextPromptExecutionSettings,\n",
" )\n",
"if selectedService == Service.HuggingFace:\n",
" from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextCompletion"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "d8ddffc1",
"metadata": {},
"source": [
"First, we will set up the text and chat services we will be submitting prompts to.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8f8dcbc6",
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel import Kernel\n",
"\n",
"kernel = Kernel()\n",
"\n",
"# Configure Azure LLM service\n",
"if selectedService == Service.AzureOpenAI:\n",
" azure_text_service = AzureTextCompletion(\n",
" service_id=\"aoai_text\"\n",
" ) # set the deployment name to the value of your text model (e.g. gpt-35-turbo-instruct)\n",
" azure_chat_service = AzureChatCompletion(\n",
" service_id=\"aoai_chat\"\n",
" ) # set the deployment name to the value of your chat model\n",
"\n",
"# Configure OpenAI service\n",
"if selectedService == Service.OpenAI:\n",
" oai_text_service = OpenAITextCompletion(service_id=\"oai_text\", ai_model_id=\"gpt-3.5-turbo-instruct\")\n",
" oai_chat_service = OpenAIChatCompletion(service_id=\"oai_chat\", ai_model_id=\"gpt-3.5-turbo\")\n",
"\n",
"# Configure Hugging Face service\n",
"if selectedService == Service.HuggingFace:\n",
" hf_text_service = HuggingFaceTextCompletion(service_id=\"hf_text\", ai_model_id=\"distilgpt2\", task=\"text-generation\")"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "50561d82",
"metadata": {},
"source": [
"Next, we'll set up the completion request settings for text completion services.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "628c843e",
"metadata": {},
"outputs": [],
"source": [
"oai_text_prompt_execution_settings = OpenAITextPromptExecutionSettings(\n",
" service=\"oai_text\",\n",
" extension_data={\n",
" \"max_tokens\": 80,\n",
" \"temperature\": 0.7,\n",
" \"top_p\": 1,\n",
" \"frequency_penalty\": 0.5,\n",
" \"presence_penalty\": 0.5,\n",
" \"number_of_responses\": 3,\n",
" },\n",
")"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "857a9c89",
"metadata": {},
"source": [
"## Multiple Open AI Text Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e2979db8",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.OpenAI:\n",
" prompt = \"What is the purpose of a rubber duck?\"\n",
"\n",
" results = await oai_text_service.get_text_contents(prompt=prompt, settings=oai_text_prompt_execution_settings)\n",
" i = 1\n",
" for result in results:\n",
" print(f\"Result {i}: {result}\")\n",
" i += 1"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "4288d09f",
"metadata": {},
"source": [
"## Multiple Azure Open AI Text Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5319f14d",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.AzureOpenAI:\n",
" prompt = \"provide me a list of possible meanings for the acronym 'ORLD'\"\n",
"\n",
" results = await azure_text_service.get_text_contents(prompt=prompt, settings=oai_text_prompt_execution_settings)\n",
" i = 1\n",
" for result in results:\n",
" print(f\"Result {i}: {result}\")\n",
" i += 1"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "eb548f9c",
"metadata": {},
"source": [
"## Multiple Hugging Face Text Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4a148709",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.HuggingFace:\n",
" from semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings import (\n",
" HuggingFacePromptExecutionSettings,\n",
" )\n",
"\n",
" hf_prompt_execution_settings = HuggingFacePromptExecutionSettings(\n",
" service_id=\"hf_text\", extension_data={\"max_new_tokens\": 80, \"temperature\": 0.7, \"top_p\": 1}\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9525e4f3",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.HuggingFace:\n",
" prompt = \"The purpose of a rubber duck is\"\n",
"\n",
" results = await hf_text_service.get_text_contents(\n",
" prompt=prompt, prompt_execution_settings=hf_prompt_execution_settings\n",
" )\n",
" print(\"\".join(results))"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "da632e12",
"metadata": {},
"source": [
"Here, we're setting up the settings for Chat completions.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e5f11e46",
"metadata": {},
"outputs": [],
"source": [
"oai_chat_prompt_execution_settings = OpenAIChatPromptExecutionSettings(\n",
" service_id=\"oai_chat\",\n",
" max_tokens=80,\n",
" temperature=0.7,\n",
" top_p=1,\n",
" frequency_penalty=0.5,\n",
" presence_penalty=0.5,\n",
" number_of_responses=3,\n",
")"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "d6bf238e",
"metadata": {},
"source": [
"## Multiple OpenAI Chat Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dabc6a4c",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.OpenAI:\n",
" chat = ChatHistory()\n",
" chat.add_user_message(\n",
" \"It's a beautiful day outside, birds are singing, flowers are blooming. On days like these, kids like you...\"\n",
" )\n",
" results = await oai_chat_service.get_chat_message_contents(\n",
" chat_history=chat, settings=oai_chat_prompt_execution_settings\n",
" )\n",
" i = 0\n",
" for result in results:\n",
" print(f\"Result {i + 1}: {result!s}\")\n",
" i += 1"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "cdb8f740",
"metadata": {},
"source": [
"## Multiple Azure OpenAI Chat Completions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "66ba4767",
"metadata": {},
"outputs": [],
"source": [
"az_oai_prompt_execution_settings = AzureChatPromptExecutionSettings(\n",
" service_id=\"aoai_chat\",\n",
" max_tokens=80,\n",
" temperature=0.7,\n",
" top_p=1,\n",
" frequency_penalty=0.5,\n",
" presence_penalty=0.5,\n",
" number_of_responses=3,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b74a64a9",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.AzureOpenAI:\n",
" content = (\n",
" \"Tomorrow is going to be a great day, I can feel it. I'm going to wake up early, go for a run, and then...\"\n",
" )\n",
" chat = ChatHistory()\n",
" chat.add_user_message(content)\n",
" results = await azure_chat_service.get_chat_message_contents(\n",
" chat_history=chat, settings=az_oai_prompt_execution_settings\n",
" )\n",
" i = 0\n",
" for result in results:\n",
" print(f\"Result {i + 1}: {result!s}\")\n",
" i += 1"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "98c8191d",
"metadata": {},
"source": [
"## Streaming Multiple Results\n",
"\n",
"Here is an example pattern if you want to stream your multiple results. Note that this is not supported for Hugging Face text completions at this time.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "26a37702",
"metadata": {},
"outputs": [],
"source": [
"if selectedService == Service.OpenAI:\n",
" import os\n",
" import time\n",
"\n",
" from IPython.display import clear_output\n",
"\n",
" # Determine the clear command based on OS\n",
" clear_command = \"cls\" if os.name == \"nt\" else \"clear\"\n",
"\n",
" chat = ChatHistory()\n",
" chat.add_user_message(\"what is the purpose of a rubber duck?\")\n",
"\n",
" stream = oai_chat_service.get_streaming_chat_message_contents(\n",
" chat_history=chat, settings=oai_chat_prompt_execution_settings\n",
" )\n",
" number_of_responses = oai_chat_prompt_execution_settings.number_of_responses\n",
" texts = [\"\"] * number_of_responses\n",
"\n",
" last_clear_time = time.time()\n",
" clear_interval = 0.5 # seconds\n",
"\n",
" # Note: there are some quirks with displaying the output, which sometimes flashes and disappears.\n",
" # This could be influenced by a few factors specific to Jupyter notebooks and asynchronous processing.\n",
" # The following code attempts to buffer the results to avoid the output flashing on/off the screen.\n",
"\n",
" async for results in stream:\n",
" current_time = time.time()\n",
"\n",
" # Update texts with new results\n",
" for idx, result in enumerate(results):\n",
" if idx < number_of_responses:\n",
" texts[idx] += str(result)\n",
"\n",
" # Clear and display output at intervals\n",
" if current_time - last_clear_time > clear_interval:\n",
" clear_output(wait=True)\n",
" for idx, text in enumerate(texts):\n",
" print(f\"Result {idx + 1}: {text}\")\n",
" last_clear_time = current_time\n",
"\n",
" print(\"----------------------------------------\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -41,10 +41,10 @@
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel.contents import ChatHistory # noqa: F401\n",
"from semantic_kernel.contents import ChatHistory\n",
"\n",
"if selectedService == Service.OpenAI or selectedService == Service.AzureOpenAI:\n",
" from semantic_kernel.connectors.ai.open_ai import ( # noqa: F401\n",
" from semantic_kernel.connectors.ai.open_ai import (\n",
" AzureChatCompletion,\n",
" AzureChatPromptExecutionSettings,\n",
" AzureTextCompletion,\n",
@@ -54,7 +54,7 @@
" OpenAITextPromptExecutionSettings,\n",
" )\n",
"if selectedService == Service.HuggingFace:\n",
" from semantic_kernel.connectors.ai.hugging_face import ( # noqa: F401\n",
" from semantic_kernel.connectors.ai.hugging_face import (\n",
" HuggingFacePromptExecutionSettings,\n",
" HuggingFaceTextCompletion,\n",
" )"
+3 -2
View File
@@ -1,5 +1,6 @@
"""This module defines an enumeration representing different services.
"""
# Copyright (c) Microsoft. All rights reserved.
"""This module defines an enumeration representing different services."""
from enum import Enum
@@ -114,9 +114,7 @@
"metadata": {},
"outputs": [],
"source": [
"!pip install semantic-kernel==1.0.3\n",
"!pip install weaviate-client\n",
"!pip install python-dotenv"
"!pip install -U semantic-kernel[weaviate]==1.0.3"
]
},
{
@@ -129,13 +127,6 @@
"- on Windows, you may need to run Docker Desktop as administrator\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
@@ -153,7 +144,7 @@
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel.connectors.memory.weaviate import weaviate_memory_store\n",
"from semantic_kernel.connectors.memory.weaviate import WeaviateMemoryStore\n",
"\n",
"# Note the Weaviate Config values need to be either configured as environment variables\n",
"# or in the .env file, as a back up. When creating the instance of the `weaviate_memory_store`\n",
@@ -164,7 +155,7 @@
"# WEAVIATE_API_KEY=\"\"\n",
"# WEAVIATE_USE_EMBED=True|False\n",
"\n",
"store = weaviate_memory_store.WeaviateMemoryStore()\n",
"store = WeaviateMemoryStore()\n",
"store.client.schema.delete_all()"
]
},
@@ -193,11 +184,11 @@
"metadata": {},
"outputs": [],
"source": [
"from semantic_kernel.kernel import Kernel\n",
"from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding\n",
"from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin\n",
"from semantic_kernel.kernel import Kernel\n",
"from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory\n",
"from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore\n",
"from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin\n",
"\n",
"kernel = Kernel()\n",
"\n",
@@ -323,13 +314,11 @@
" },\n",
" )\n",
"\n",
" chat_func = kernel.add_function(\n",
" return kernel.add_function(\n",
" function_name=\"chat_with_memory\",\n",
" plugin_name=\"TextMemoryPlugin\",\n",
" prompt_template_config=prompt_template_config,\n",
" )\n",
"\n",
" return chat_func"
" )"
]
},
{
@@ -438,8 +427,7 @@
"COLLECTION = \"SKGitHub\"\n",
"\n",
"print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n",
"i = 0\n",
"for entry, value in github_files.items():\n",
"for index, (entry, value) in enumerate(github_files.items()):\n",
" await memory.save_reference(\n",
" collection=COLLECTION,\n",
" description=value,\n",
@@ -447,8 +435,7 @@
" external_id=entry,\n",
" external_source_name=\"GitHub\",\n",
" )\n",
" i += 1\n",
" print(\" URL {} saved\".format(i))"
" print(\" URL {} saved\".format(index))"
]
},
{
@@ -469,10 +456,8 @@
"\n",
"memories = await memory.search(COLLECTION, ask, limit=5, min_relevance_score=0.77)\n",
"\n",
"i = 0\n",
"for memory in memories:\n",
" i += 1\n",
" print(f\"Result {i}:\")\n",
"for index, memory in enumerate(memories):\n",
" print(f\"Result {index}:\")\n",
" print(\" URL: : \" + memory.id)\n",
" print(\" Title : \" + memory.description)\n",
" print(\" Relevance: \" + str(memory.relevance))\n",
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from dotenv import dotenv_values
from promptflow import PFClient
from promptflow.entities import AzureOpenAIConnection
@@ -22,7 +22,7 @@ async def main():
service_id = "default"
prompt_template_config = PromptTemplateConfig(
template=ConversationSummaryPlugin._summarize_conversation_prompt_template,
description="Given a section of a conversation transcript, summarize the part of" " the conversation.",
description="Given a section of a conversation transcript, summarize the part of the conversation.",
execution_settings=PromptExecutionSettings(
service_id=service_id, max_tokens=ConversationSummaryPlugin._max_tokens, temperature=0.1, top_p=0.5
),
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from promptflow import PFClient
pf_client = PFClient()
@@ -1,16 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from plugins.MathPlugin.Math import Math as Math # ignore F841
from promptflow import tool
from promptflow.connections import (
AzureOpenAIConnection,
)
from promptflow.connections import AzureOpenAIConnection
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import (
AzureChatCompletion,
AzureTextCompletion,
)
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, AzureTextCompletion
from semantic_kernel.planning.sequential_planner import SequentialPlanner
@@ -1,10 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
import math
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.skill_definition import (
sk_function,
sk_function_context_parameter,
)
from semantic_kernel.skill_definition import sk_function, sk_function_context_parameter
class Math:
@@ -35,7 +35,7 @@ async def main():
prompt_template_config = PromptTemplateConfig(
template=template,
description="Given a section of a conversation transcript, summarize the part of" " the conversation.",
description="Given a section of a conversation transcript, summarize the part of the conversation.",
execution_settings=execution_settings,
InputVariables=[
InputVariable(name="input", description="The user input", is_required=True),
@@ -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",
]

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