Merge branch 'microsoft:main' into main

This commit is contained in:
Sandra Ahlgrimm
2024-01-26 10:03:52 +01:00
committed by GitHub
9 changed files with 384 additions and 74 deletions
@@ -0,0 +1,156 @@
---
# Strategy for Community Driven Connectors and Features
status: approved
contact: rogerbarreto
date: 2024-01-24
deciders: rogerbarreto, markwallace-microsoft, dmytrostruk, sergeymenshik
consulted:
informed:
---
# Strategy for Community Driven Connectors and Features
## Context and Problem Statement
Normally Connectors are Middle to Complex new Features that can be developed by a single person or a team. In order to avoid conflicts and to have a better control of the development process, we strongly suggest the usage of a Feature Branch Strategy in our repositories.
In our current software development process, managing changes in the main branch has become increasingly complex, leading to potential conflicts and delays in release cycles.
## Standards and Guidelines Principles
- **Pattern**: The Feature Branch Strategy is a well-known pattern for managing changes in a codebase. It is widely used in the industry and is supported by most version control systems, including GitHub, this also gives further clear picture on how the community can meaningfully contribute to the development of connectors or any other bigger feature for SK.
- **Isolated Development Environments**: By using feature branches, each developer can work on different aspects of the project without interfering with others' work. This isolation reduces conflicts and ensures that the main branch remains stable.
- **Streamlined Integration**: Feature branches simplify the process of integrating new code into the main branch. By dealing with smaller, more manageable changes, the risk of major conflicts during integration is minimized.
- **Efficiency in Code Review**: Smaller, more focused changes in feature branches lead to quicker and more efficient code reviews. This efficiency is not just about the ease of reviewing less code at a time but also about the time saved in understanding the context and impact of the changes.
- **Reduced Risk of Bugs**: Isolating development in feature branches reduces the likelihood of introducing bugs into the main branch. It's easier to identify and fix issues within the confined context of a single feature.
- **Timely Feature Integration**: Small, incremental pull requests allow for quicker reviews and faster integration of features into the feature branch and make it easier to merge down into main as the code was already previously reviewed. This timeliness ensures that features are merged and ready for deployment sooner, improving the responsiveness to changes.
- **Code Testing, Coverage and Quality**: To keep a good code quality is imperative that any new code or feature introduced to the codebase is properly tested and validated. Any new feature or code should be covered by unit tests and integration tests. The code should also be validated by our CI/CD pipeline and follow our code quality standards and guidelines.
- **Examples**: Any new feature or code should be accompanied by examples that demonstrate how to use the new feature or code. This is important to ensure that the new feature or code is properly documented and that the community can easily understand and use it.
### Community Feature Branch Strategy
As soon we identify that contributors are willing to take/create a Feature Issue as a potential connector implementation, we will create a new branch for that feature.
Once we have agreed to take a new connector we will work with the contributors to make sure the implementation progresses and is supported if needed.
The contributor(s) will then be one of the responsibles to incrementally add the majority of changes through small Pull Requests to the feature branch under our supervision and review process.
This strategy involves creating a separate branch in the repository for each new big feature, like connectors. This isolation means that changes are made in a controlled environment without affecting the main branch.
We may also engage in the development and changes to the feature branch when needed, the changes and full or co-authorship on the PRs will be tracked and properly referred into the Release Notes.
#### Pros and Cons
- Good, because it allows for focused development on one feature at a time.
- Good, because it promotes smaller, incremental Pull Requests (PRs), simplifying review processes.
- Good, because it reduces the risk of major bugs being merged into the main branch.
- Good, because it makes the process of integrating features into the main branch easier and faster.
- Bad, potentially, if not managed properly, as it can lead to outdated branches if not regularly synchronized with the main branch.
## Local Deployment Platforms / Offline
### LM Studio
LM Studio has a local deployment option, which can be used to deploy models locally. This option is available for Windows, Linux, and MacOS.
Pros:
- API is very similar to OpenAI API
- Many models are already supported
- Easy to use
- Easy to deploy
- GPU support
Cons:
- May require a license to use in a work environment
### Ollama
Ollama has a local deployment option, which can be used to deploy models locally. This option is available for Linux and MacOS only for now.
Pros:
- Easy to use
- Easy to deploy
- Supports Docker deployment
- GPU support
Cons:
- API is not similar to OpenAI API (Needs a dedicated connector)
- Dont have Windows support
### Comparison
| Feature | Ollama | LM Studio |
| --------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Local LLM | Yes | Yes |
| OpenAI API Similarity | Yes | Yes |
| Windows Support | No | Yes |
| Linux Support | Yes | Yes |
| MacOS Support | Yes | Yes |
| Number of Models | [61](https://ollama.ai/library) +Any GGUF converted | [25](https://github.com/lmstudio-ai/model-catalog/tree/main/models) +Any GGUF Converted |
| Model Support | Ollama | LM Studio |
| --------------- | ------ | --------- |
| Phi-2 Support | Yes | Yes |
| Llama-2 Support | Yes | Yes |
| Mistral Support | Yes | Yes |
## Connector/Model Priorities
Currently we are looking for community support on the following models
The support on the below can be either achieved creating a practical example using one of the existing Connectors against one of this models or providing a new Connector that supports a deployment platform that hosts one of the models below:
| Model Name | Local Support | Deployment | Connectors |
| ---------- | ------------- | -------------------------------------- | ------------------------------------------------------ |
| Gpt-4 | No | OpenAI, Azure | Azure+OpenAI |
| Phi-2 | Yes | Azure, Hugging Face, LM Studio, Ollama | OpenAI, HuggingFace, LM Studio\*\*\*, Ollama\*\* |
| Gemini | No | Google AI Platform | GoogleAI\*\* |
| Llama-2 | Yes | Azure, LM Studio, HuggingFace, Ollama | HuggingFace, Azure+OpenAI, LM Studio\*\*\*, Ollama\*\* |
| Mistral | Yes | Azure, LM Studio, HuggingFace, Ollama | HuggingFace, Azure+OpenAI, LM Studio\*\*\*, Ollama\*\* |
| Claude | No | Anthropic, Amazon Bedrock | Anthropic**, Amazon** |
| Titan | No | Amazon Bedrock | Amazon\*\* |
_\*\* Connectors not yet available_
_\*\*\* May not be needed as an OpenAI Connector can be used_
Connectors may be needed not per Model basis but rather per deployment platform.
For example, using OpenAI or HuggingFace connector you may be able to call a Phi-2 Model.
## Expected Connectors to be implemented
The following deployment platforms are not yet supported by any Connectors and we strongly encourage the community to engage and support on those:
Currently the priorities are ordered but not necessarily needs to be implemented sequentially, an
| Deployment Platform | Local Model Support |
| ------------------- | ------------------- |
| Ollama | Yes |
| GoogleAI | No |
| Anthropic | No |
| Amazon | No |
## Decision Outcome
Chosen option: "Feature Branch Strategy", because it allows individual features to be developed in isolation, minimizing conflicts with the main branch and facilitating easier code reviews.
## Fequent Asked Questions
### Is there a migration strategy for initiatives that followed the old contribution way with forks, and now have to switch to branches in microsoft/semantic-kernel?
You proceed normally with the fork and PR targeting `main`, as soon we identify that your contribution PR to main is a big and desirable feature (Look at the ones we described as expected in this ADR) we will create a dedicated feature branch (`feature-yourfeature`) where you can retarget our forks PR to target it.
All further incremental changes and contributions will follow as normal, but instead of `main` you will be targeting the `feature-*` branch.
### How do you want to solve the "up to date with main branch" problem?
This will happen when we all agreed that the current feature implementation is complete and ready to merge in `main`.
As soon the feature is finished, a merge from main will be pushed into the feature branch.
This will normally trigger the conflicts that need to be sorted.
That normally will be the last PR targeting the feature branch which will be followed right away by another PR from the `feature` branch targeting `main` with minimal conflicts if any.
The merging to main might be fast (as all the intermediate feature PRs were all agreed and approved before)
@@ -0,0 +1,140 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Examples;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Xunit;
using Xunit.Abstractions;
public class Example76_Filters : BaseTest
{
/// <summary>
/// Shows how to use function and prompt filters in Kernel.
/// </summary>
[Fact]
public async Task FunctionAndPromptFiltersAsync()
{
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName,
endpoint: TestConfiguration.AzureOpenAI.Endpoint,
apiKey: TestConfiguration.AzureOpenAI.ApiKey);
builder.Services.AddSingleton<ITestOutputHelper>(this.Output);
// Add filters with DI
builder.Services.AddSingleton<IFunctionFilter, FirstFunctionFilter>();
builder.Services.AddSingleton<IFunctionFilter, SecondFunctionFilter>();
var kernel = builder.Build();
// Add filter without DI
kernel.PromptFilters.Add(new FirstPromptFilter(this.Output));
var function = kernel.CreateFunctionFromPrompt("What is Seattle", functionName: "MyFunction");
var result = await kernel.InvokeAsync(function);
WriteLine(result);
}
public Example76_Filters(ITestOutputHelper output) : base(output)
{
}
#region Filters
private sealed class FirstFunctionFilter : IFunctionFilter
{
private readonly ITestOutputHelper _output;
public FirstFunctionFilter(ITestOutputHelper output)
{
this._output = output;
}
public void OnFunctionInvoking(FunctionInvokingContext context) =>
this._output.WriteLine($"{nameof(FirstFunctionFilter)}.{nameof(OnFunctionInvoking)} - {context.Function.Name}");
public void OnFunctionInvoked(FunctionInvokedContext context) =>
this._output.WriteLine($"{nameof(FirstFunctionFilter)}.{nameof(OnFunctionInvoked)} - {context.Function.Name}");
}
private sealed class SecondFunctionFilter : IFunctionFilter
{
private readonly ITestOutputHelper _output;
public SecondFunctionFilter(ITestOutputHelper output)
{
this._output = output;
}
public void OnFunctionInvoking(FunctionInvokingContext context) =>
this._output.WriteLine($"{nameof(SecondFunctionFilter)}.{nameof(OnFunctionInvoking)} - {context.Function.Name}");
public void OnFunctionInvoked(FunctionInvokedContext context) =>
this._output.WriteLine($"{nameof(SecondFunctionFilter)}.{nameof(OnFunctionInvoked)} - {context.Function.Name}");
}
private sealed class FirstPromptFilter : IPromptFilter
{
private readonly ITestOutputHelper _output;
public FirstPromptFilter(ITestOutputHelper output)
{
this._output = output;
}
public void OnPromptRendering(PromptRenderingContext context) =>
this._output.WriteLine($"{nameof(FirstPromptFilter)}.{nameof(OnPromptRendering)} - {context.Function.Name}");
public void OnPromptRendered(PromptRenderedContext context) =>
this._output.WriteLine($"{nameof(FirstPromptFilter)}.{nameof(OnPromptRendered)} - {context.Function.Name}");
}
#endregion
#region Filter capabilities
private sealed class FunctionFilterExample : IFunctionFilter
{
public void OnFunctionInvoked(FunctionInvokedContext context)
{
// Example: get function result value
var value = context.Result.GetValue<object>();
// Example: override function result value
context.SetResultValue("new result value");
// Example: get token usage from metadata
var usage = context.Result.Metadata?["Usage"];
}
public void OnFunctionInvoking(FunctionInvokingContext context)
{
// Example: override kernel arguments
context.Arguments["input"] = "new input";
// Example: cancel function execution
context.Cancel = true;
}
}
private sealed class PromptFilterExample : IPromptFilter
{
public void OnPromptRendered(PromptRenderedContext context)
{
// Example: override rendered prompt before sending it to AI
context.RenderedPrompt = "Safe prompt";
}
public void OnPromptRendering(PromptRenderingContext context)
{
// Example: get function information
var functionName = context.Function.Name;
}
}
#endregion
}
@@ -714,7 +714,7 @@ internal abstract class ClientCore
ChoicesPerPrompt = executionSettings.ResultsPerPrompt,
GenerationSampleCount = executionSettings.ResultsPerPrompt,
LogProbabilityCount = null,
User = null,
User = executionSettings.User,
DeploymentName = deploymentOrModelName
};
@@ -758,6 +758,7 @@ internal abstract class ClientCore
ChoiceCount = executionSettings.ResultsPerPrompt,
DeploymentName = deploymentOrModelName,
Seed = executionSettings.Seed,
User = executionSettings.User
};
switch (executionSettings.ResponseFormat)
@@ -139,6 +139,11 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
/// </remarks>
public ToolCallBehavior? ToolCallBehavior { get; set; }
/// <summary>
/// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse
/// </summary>
public string? User { get; set; }
/// <summary>
/// Default value for chat system property.
/// </summary>
@@ -4,7 +4,6 @@ from typing import Any, Dict, List, Literal, Optional, Union
from pydantic import Field, field_validator, model_validator
from semantic_kernel.connectors.ai.ai_request_settings import AIRequestSettings
from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_CHAT_SYSTEM_PROMPT
logger = logging.getLogger(__name__)
@@ -59,7 +58,7 @@ class OpenAIChatRequestSettings(OpenAIRequestSettings):
tool_choice: Optional[str] = None
function_call: Optional[str] = None
functions: Optional[List[Dict[str, Any]]] = None
messages: Optional[List[Dict[str, Any]]] = [{"role": "system", "content": DEFAULT_CHAT_SYSTEM_PROMPT}]
messages: Optional[List[Dict[str, Any]]] = None
@field_validator("functions", "function_call", mode="after")
@classmethod
@@ -1,19 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from semantic_kernel.connectors.ai.open_ai.models.chat.open_ai_chat_message import (
OpenAIChatMessage,
)
from semantic_kernel.semantic_functions.chat_prompt_template import ChatPromptTemplate
from semantic_kernel.semantic_functions.prompt_template import PromptTemplate
from semantic_kernel.semantic_functions.prompt_template_config import (
PromptTemplateConfig,
)
from semantic_kernel.template_engine.protocols.prompt_templating_engine import (
PromptTemplatingEngine,
)
logger: logging.Logger = logging.getLogger(__name__)
@@ -49,7 +43,7 @@ class OpenAIChatPromptTemplate(ChatPromptTemplate[OpenAIChatMessage]):
)
)
return
self._log.warning("function_call is only used with role: assistant, ignoring")
logger.warning("function_call is only used with role: assistant, ignoring")
function_call = None
self.messages.append(
OpenAIChatMessage(
@@ -59,39 +53,3 @@ class OpenAIChatPromptTemplate(ChatPromptTemplate[OpenAIChatMessage]):
function_call=function_call,
)
)
@classmethod
def restore(
cls,
messages: List[Dict[str, str]],
template: str,
template_engine: PromptTemplatingEngine,
prompt_config: PromptTemplateConfig,
log: Optional[Any] = None,
) -> "OpenAIChatPromptTemplate":
"""Restore a ChatPromptTemplate from a list of role and message pairs.
If there is a chat_system_prompt in the prompt_config.execution_settings settings,
that takes precedence over the first message in the list of messages,
if that is a system message.
"""
if log:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
chat_template = cls(template, template_engine, prompt_config)
if prompt_config.execution_settings.chat_system_prompt and messages[0]["role"] == "system":
existing_system_message = messages.pop(0)
if existing_system_message["message"] != prompt_config.execution_settings.chat_system_prompt:
logger.info(
"Overriding system prompt with chat_system_prompt, old system message: %s, new system message: %s",
existing_system_message["message"],
prompt_config.execution_settings.chat_system_prompt,
)
for message in messages:
chat_template.add_message(
message["role"],
message["message"],
name=message["name"],
function_call=message["function_call"],
)
return chat_template
@@ -31,20 +31,43 @@ class ChatPromptTemplate(PromptTemplate, Generic[ChatMessageT]):
template: str,
template_engine: PromptTemplatingEngine,
prompt_config: PromptTemplateConfig,
log: Optional[Any] = None,
parse_chat_system_prompt: bool = False,
parse_messages: bool = False,
**kwargs: Any,
) -> None:
"""Initialize a chat prompt template.
if there is a field 'chat_system_prompt' in the prompt_config.execution_settings.extension_data,
that value is added to the messages list as a system message,
can be controlled by setting the parse_chat_system_prompt parameter to True.
After that any messages that are in messages in the prompt_config.execution_settings
are added to the messages list.
Can be controlled by setting the parse_messages parameter to True.
Arguments:
template {str} -- The template to use for the chat prompt.
template_engine {PromptTemplatingEngine} -- The templating engine to use.
prompt_config {PromptTemplateConfig} -- The prompt config to use.
parse_chat_system_prompt {bool} -- Whether to parse the chat_system_prompt from
the prompt_config.execution_settings.extension_data.
parse_messages {bool} -- Whether to parse the messages from the prompt_config.execution_settings.
"""
super().__init__(template, template_engine, prompt_config)
if log:
if "log" in kwargs:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
self._messages = []
if self.prompt_config.execution_settings.extension_data.get("chat_system_prompt"):
if parse_chat_system_prompt and "chat_system_prompt" in self.prompt_config.execution_settings.extension_data:
self.add_system_message(self.prompt_config.execution_settings.extension_data["chat_system_prompt"])
if (
hasattr(self.prompt_config.execution_settings, "messages")
parse_messages
and hasattr(self.prompt_config.execution_settings, "messages")
and self.prompt_config.execution_settings.messages
):
for message in self.prompt_config.execution_settings.messages:
self.add_message(message["role"], message["content"])
self.add_message(**message)
async def render_async(self, context: "KernelContext") -> str:
raise NotImplementedError(
@@ -75,6 +98,9 @@ class ChatPromptTemplate(PromptTemplate, Generic[ChatMessageT]):
# When the type is not explicitly set, it is still the typevar, replace with generic ChatMessage
if isinstance(concrete_message, TypeVar):
concrete_message = ChatMessage
assert issubclass(concrete_message, ChatMessage)
if not message and "content" in kwargs:
message = kwargs["content"]
self.messages.append(
concrete_message(
role=role,
@@ -104,26 +130,35 @@ class ChatPromptTemplate(PromptTemplate, Generic[ChatMessageT]):
template: str,
template_engine: PromptTemplatingEngine,
prompt_config: PromptTemplateConfig,
log: Optional[Any] = None,
parse_chat_system_prompt: bool = False,
parse_messages: bool = False,
**kwargs: Any,
) -> "ChatPromptTemplate":
"""Restore a ChatPromptTemplate from a list of role and message pairs.
If there is a chat_system_prompt in the prompt_config.execution_settings settings,
that takes precedence over the first message in the list of messages,
if that is a system message.
"""
if log:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
chat_template = cls(template, template_engine, prompt_config)
if prompt_config.execution_settings.chat_system_prompt and messages[0]["role"] == "system":
existing_system_message = messages.pop(0)
if existing_system_message["message"] != prompt_config.execution_settings.chat_system_prompt:
logger.info(
"Overriding system prompt with chat_system_prompt, old system message: %s, new system message: %s",
existing_system_message["message"],
prompt_config.execution_settings.chat_system_prompt,
)
for message in messages:
chat_template.add_message(message["role"], message["message"])
The parse_messages and parse_chat_system_prompt parameters control whether
the messages and chat_system_prompt from the prompt_config.execution_settings
are parsed and added to the messages list, not whether or not the 'messages'
from the messages parameter are parsed, those are always parsed.
Arguments:
messages {List[Dict[str, str]]} -- The messages to restore,
the default format is [{"role": "user", "message": "Hi there"}].
if the ChatPromptTemplate is created with a different message type,
the messages should contain any fields that are relevant to the message,
for instance: ChatPromptTemplate[OpenAIChatMessage].restore can be used with a format:
[{"role": "assistant", "function_call": FunctionCall()}].
template {str} -- The template to use for the chat prompt.
template_engine {PromptTemplatingEngine} -- The templating engine to use.
prompt_config {PromptTemplateConfig} -- The prompt config to use.
parse_chat_system_prompt {bool} -- Whether to parse the chat_system_prompt from the
prompt_config.execution_settings.extension_data.
parse_messages {bool} -- Whether to parse the messages from the prompt_config.execution_settings.
"""
chat_template = cls(
template, template_engine, prompt_config, parse_chat_system_prompt, parse_messages, **kwargs
)
for message in messages:
chat_template.add_message(**message)
return chat_template
@@ -26,7 +26,7 @@ def test_default_openai_chat_request_settings():
assert settings.stop is None
assert settings.number_of_responses == 1
assert settings.logit_bias == {}
assert settings.messages[0]["content"] == "Assistant is a large language model."
assert settings.messages is None
def test_custom_openai_chat_request_settings():
@@ -151,15 +151,31 @@ def test_chat_prompt_template():
assert chat_prompt_template.messages == []
def test_chat_prompt_template_with_system_prompt():
def test_chat_prompt_template_with_messages():
prompt_template_config = PromptTemplateConfig[OpenAIChatRequestSettings].from_execution_settings(
messages=[{"role": "system", "content": "Custom system prompt."}],
)
chat_prompt_template = ChatPromptTemplate[OpenAIChatMessage](
"{{$user_input}}",
PromptTemplateEngine(),
prompt_config=prompt_template_config,
parse_messages=True,
)
print(chat_prompt_template.messages)
assert len(chat_prompt_template.messages) == 1
assert chat_prompt_template.messages[0].role == "system"
assert chat_prompt_template.messages[0].content_template.template == "Custom system prompt."
def test_chat_prompt_template_with_system_prompt():
prompt_template_config = PromptTemplateConfig[OpenAIChatRequestSettings].from_execution_settings(
chat_system_prompt="Custom system prompt.",
)
chat_prompt_template = ChatPromptTemplate[OpenAIChatMessage](
"{{$user_input}}",
PromptTemplateEngine(),
prompt_config=prompt_template_config,
parse_chat_system_prompt=True,
)
print(chat_prompt_template.messages)
assert len(chat_prompt_template.messages) == 1