chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import (
OllamaChatPromptExecutionSettings,
OllamaEmbeddingPromptExecutionSettings,
OllamaPromptExecutionSettings,
OllamaTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.ollama.services.ollama_chat_completion import OllamaChatCompletion
from semantic_kernel.connectors.ai.ollama.services.ollama_text_completion import OllamaTextCompletion
from semantic_kernel.connectors.ai.ollama.services.ollama_text_embedding import OllamaTextEmbedding
__all__ = [
"OllamaChatCompletion",
"OllamaChatPromptExecutionSettings",
"OllamaEmbeddingPromptExecutionSettings",
"OllamaPromptExecutionSettings",
"OllamaTextCompletion",
"OllamaTextEmbedding",
"OllamaTextPromptExecutionSettings",
]
@@ -0,0 +1,39 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated, Any, Literal
from pydantic import Field
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
class OllamaPromptExecutionSettings(PromptExecutionSettings):
"""Settings for Ollama prompt execution."""
format: Literal["json"] | dict[str, Any] | None = None
options: dict[str, Any] | None = None
class OllamaTextPromptExecutionSettings(OllamaPromptExecutionSettings):
"""Settings for Ollama text prompt execution."""
system: str | None = None
template: str | None = None
context: str | None = None
raw: bool | None = None
class OllamaChatPromptExecutionSettings(OllamaPromptExecutionSettings):
"""Settings for Ollama chat prompt execution."""
tools: Annotated[
list[dict[str, Any]] | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
class OllamaEmbeddingPromptExecutionSettings(OllamaPromptExecutionSettings):
"""Settings for Ollama embedding prompt execution."""
@@ -0,0 +1,33 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class OllamaSettings(KernelBaseSettings):
"""Ollama settings.
The settings are first loaded from environment variables with
the prefix 'OLLAMA_'.
If the environment variables are not found, the settings can
be loaded from a .env file with the encoding 'utf-8'.
If the settings are not found in the .env file, the settings
are ignored; however, validation will fail alerting that the
settings are missing.
Required settings for prefix 'OLLAMA' are:
- chat_model_id: str - The chat model ID. (Env var OLLAMA_CHAT_MODEL_ID)
- text_model_id: str - The text model ID. (Env var OLLAMA_TEXT_MODEL_ID)
- embedding_model_id: str - The embedding model ID. (Env var OLLAMA_EMBEDDING_MODEL_ID)
Optional settings for prefix 'OLLAMA' are:
- host: HttpsUrl - The endpoint of the Ollama service. (Env var OLLAMA_HOST)
"""
env_prefix: ClassVar[str] = "OLLAMA_"
chat_model_id: str | None = None
text_model_id: str | None = None
embedding_model_id: str | None = None
host: str | None = None
@@ -0,0 +1,20 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC
from typing import ClassVar
from ollama import AsyncClient
from semantic_kernel.kernel_pydantic import KernelBaseModel
class OllamaBase(KernelBaseModel, ABC):
"""Ollama service base.
Args:
client [AsyncClient]: An Ollama client to use for the service.
"""
MODEL_PROVIDER_NAME: ClassVar[str] = "ollama"
client: AsyncClient
@@ -0,0 +1,367 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Mapping, Sequence
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import httpx
from ollama import AsyncClient
from ollama._types import ChatResponse, Message
from pydantic import ValidationError
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaChatPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.ollama_settings import OllamaSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_base import OllamaBase
from semantic_kernel.connectors.ai.ollama.services.utils import (
MESSAGE_CONVERTERS,
update_settings_from_function_choice_configuration,
)
from semantic_kernel.contents import AuthorRole
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import CMC_ITEM_TYPES, ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.streaming_chat_message_content import STREAMING_CMC_ITEM_TYPES as STREAMING_ITEM_TYPES
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
ServiceInvalidResponseError,
)
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
trace_streaming_chat_completion,
)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
CMC_TYPE = TypeVar("CMC_TYPE", bound=ChatMessageContent)
logger: logging.Logger = logging.getLogger(__name__)
class OllamaChatCompletion(OllamaBase, ChatCompletionClientBase):
"""Initializes a new instance of the OllamaChatCompletion class.
Make sure to have the ollama service running either locally or remotely.
"""
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
def __init__(
self,
service_id: str | None = None,
ai_model_id: str | None = None,
host: str | None = None,
client: AsyncClient | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an OllamaChatCompletion service.
Args:
service_id (Optional[str]): Service ID tied to the execution settings. (Optional)
ai_model_id (Optional[str]): The model name. (Optional)
host (Optional[str]): URL of the Ollama server, defaults to None and
will use the default Ollama service address: http://127.0.0.1:11434. (Optional)
client (Optional[AsyncClient]): A custom Ollama client to use for the service. (Optional)
env_file_path (str | None): Use the environment settings file as a fallback to using env vars.
env_file_encoding (str | None): The encoding of the environment settings file, defaults to 'utf-8'.
"""
try:
ollama_settings = OllamaSettings(
chat_model_id=ai_model_id,
host=host,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Ollama settings.", ex) from ex
if not ollama_settings.chat_model_id:
raise ServiceInitializationError("Ollama chat model ID is required.")
super().__init__(
service_id=service_id or ollama_settings.chat_model_id,
ai_model_id=ollama_settings.chat_model_id,
client=client or AsyncClient(host=ollama_settings.host),
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return OllamaChatPromptExecutionSettings
# Override from AIServiceClientBase
@override
def service_url(self) -> str | None:
if hasattr(self.client, "_client") and isinstance(self.client._client, httpx.AsyncClient):
# Best effort to get the endpoint
return str(self.client._client.base_url)
return None
@override
def _prepare_chat_history_for_request(
self,
chat_history: ChatHistory,
role_key: str = "role",
content_key: str = "content",
) -> list[Message]:
return [MESSAGE_CONVERTERS[message.role](message) for message in chat_history.messages]
@override
def _verify_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if settings.function_choice_behavior and settings.function_choice_behavior.type_ in [
FunctionChoiceType.REQUIRED,
FunctionChoiceType.NONE,
]:
raise ServiceInvalidExecutionSettingsError(
"Ollama does not support function choice behavior of type 'required' or 'none' yet."
)
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return update_settings_from_function_choice_configuration
@override
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if hasattr(settings, "tools"):
settings.tools = None
@override
@trace_chat_completion(OllamaBase.MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, OllamaChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, OllamaChatPromptExecutionSettings) # nosec
prepared_chat_history = self._prepare_chat_history_for_request(chat_history)
response_object = await self.client.chat(
model=self.ai_model_id,
messages=prepared_chat_history,
stream=False,
**settings.prepare_settings_dict(),
)
if isinstance(response_object, ChatResponse):
return [self._create_chat_message_content_from_chat_response(response_object)]
if isinstance(response_object, Mapping):
return [self._create_chat_message_content(response_object)]
raise ServiceInvalidResponseError(
"Invalid response type from Ollama chat completion. "
f"Expected Mapping or ChatResponse but got {type(response_object)}."
)
@override
@trace_streaming_chat_completion(OllamaBase.MODEL_PROVIDER_NAME)
async def _inner_get_streaming_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
function_invoke_attempt: int = 0,
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
if not isinstance(settings, OllamaChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, OllamaChatPromptExecutionSettings) # nosec
prepared_chat_history = self._prepare_chat_history_for_request(chat_history)
response_object = await self.client.chat(
model=self.ai_model_id,
messages=prepared_chat_history,
stream=True,
**settings.prepare_settings_dict(),
)
if not isinstance(response_object, AsyncIterator):
raise ServiceInvalidResponseError(
"Invalid response type from Ollama streaming chat completion. "
f"Expected AsyncIterator but got {type(response_object)}."
)
async for part in response_object:
if isinstance(part, ChatResponse):
yield [self._create_streaming_chat_message_content_from_chat_response(part, function_invoke_attempt)]
continue
if isinstance(part, Mapping):
yield [self._create_streaming_chat_message_content(part, function_invoke_attempt)]
continue
raise ServiceInvalidResponseError(
"Invalid response type from Ollama streaming chat completion. "
f"Expected mapping or ChatResponse but got {type(part)}."
)
# endregion
def _create_streaming_chat_message_content_from_chat_response(
self,
response: ChatResponse,
function_invoke_attempt: int,
) -> StreamingChatMessageContent:
"""Create a chat message content from the response."""
items: list[STREAMING_ITEM_TYPES] = []
if response.message.content:
items.append(
StreamingTextContent(
choice_index=0,
text=response.message.content,
inner_content=response.message,
)
)
self._parse_tool_calls(response.message.tool_calls, items)
return StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.ASSISTANT,
items=items,
inner_content=response,
ai_model_id=self.ai_model_id,
metadata=self._get_metadata_from_chat_response(response),
function_invoke_attempt=function_invoke_attempt,
)
def _parse_tool_calls(self, tool_calls: Sequence[Message.ToolCall] | None, items: list[Any]):
if tool_calls:
for tool_call in tool_calls:
items.append(
FunctionCallContent(
inner_content=tool_call,
ai_model_id=self.ai_model_id,
name=tool_call.function.name,
arguments=tool_call.function.arguments,
)
)
def _create_chat_message_content_from_chat_response(self, response: ChatResponse) -> ChatMessageContent:
"""Create a chat message content from the response."""
items: list[CMC_ITEM_TYPES] = []
if response.message.content:
items.append(
TextContent(
text=response.message.content,
inner_content=response.message,
)
)
self._parse_tool_calls(response.message.tool_calls, items)
return ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=items,
inner_content=response,
ai_model_id=self.ai_model_id,
metadata=self._get_metadata_from_chat_response(response),
)
def _create_chat_message_content(self, response: Mapping[str, Any]) -> ChatMessageContent:
"""Create a chat message content from the response."""
items: list[CMC_ITEM_TYPES] = []
if not (message := response.get("message", None)):
raise ServiceInvalidResponseError("No message content found in response.")
if content := message.get("content", None):
items.append(
TextContent(
text=content,
inner_content=message,
)
)
if tool_calls := message.get("tool_calls", None):
for tool_call in tool_calls:
items.append(
FunctionCallContent(
inner_content=tool_call,
ai_model_id=self.ai_model_id,
name=tool_call.get("function").get("name"),
arguments=tool_call.get("function").get("arguments"),
)
)
return ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=items,
inner_content=response,
metadata=self._get_metadata_from_response(response),
)
def _create_streaming_chat_message_content(
self, part: Mapping[str, Any], function_invoke_attempt: int
) -> StreamingChatMessageContent:
"""Create a streaming chat message content from the response part."""
items: list[STREAMING_ITEM_TYPES] = []
if not (message := part.get("message", None)):
raise ServiceInvalidResponseError("No message content found in response part.")
if content := message.get("content", None):
items.append(
StreamingTextContent(
choice_index=0,
text=content,
inner_content=message,
)
)
if tool_calls := message.get("tool_calls", None):
for tool_call in tool_calls:
items.append(
FunctionCallContent(
inner_content=tool_call,
ai_model_id=self.ai_model_id,
name=tool_call.get("function").get("name"),
arguments=tool_call.get("function").get("arguments"),
)
)
return StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
choice_index=0,
items=items,
inner_content=part,
ai_model_id=self.ai_model_id,
metadata=self._get_metadata_from_response(part),
function_invoke_attempt=function_invoke_attempt,
)
def _get_metadata_from_response(self, response: Mapping[str, Any]) -> dict[str, Any]:
"""Get metadata from the response."""
metadata = {
"model": response.get("model"),
}
if "prompt_eval_count" in response and "eval_count" in response:
metadata["usage"] = CompletionUsage(
prompt_tokens=response.get("prompt_eval_count"),
completion_tokens=response.get("eval_count"),
)
return metadata
def _get_metadata_from_chat_response(self, response: ChatResponse) -> dict[str, Any]:
"""Get metadata from the response."""
metadata: dict[str, Any] = {
"model": response.model,
}
if response.prompt_eval_count and response.eval_count:
metadata["usage"] = CompletionUsage(
prompt_tokens=response.prompt_eval_count,
completion_tokens=response.eval_count,
)
return metadata
@@ -0,0 +1,163 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import AsyncGenerator, AsyncIterator, Mapping
from typing import TYPE_CHECKING, Any
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import httpx
from ollama import AsyncClient
from ollama._types import GenerateResponse
from pydantic import ValidationError
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaTextPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.ollama_settings import OllamaSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_base import OllamaBase
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidResponseError
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_text_completion,
trace_text_completion,
)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
class OllamaTextCompletion(OllamaBase, TextCompletionClientBase):
"""Initializes a new instance of the OllamaTextCompletion class.
Make sure to have the ollama service running either locally or remotely.
"""
def __init__(
self,
service_id: str | None = None,
ai_model_id: str | None = None,
host: str | None = None,
client: AsyncClient | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an OllamaChatCompletion service.
Args:
service_id (Optional[str]): Service ID tied to the execution settings. (Optional)
ai_model_id (Optional[str]): The model name. (Optional)
host (Optional[str]): URL of the Ollama server, defaults to None and
will use the default Ollama service address: http://127.0.0.1:11434. (Optional)
client (Optional[AsyncClient]): A custom Ollama client to use for the service. (Optional)
env_file_path (str | None): Use the environment settings file as a fallback to using env vars.
env_file_encoding (str | None): The encoding of the environment settings file, defaults to 'utf-8'.
"""
try:
ollama_settings = OllamaSettings(
text_model_id=ai_model_id,
host=host,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Ollama settings.", ex) from ex
if not ollama_settings.text_model_id:
raise ServiceInitializationError("Ollama text model ID is required.")
super().__init__(
service_id=service_id or ollama_settings.text_model_id,
ai_model_id=ollama_settings.text_model_id,
client=client or AsyncClient(host=ollama_settings.host),
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return OllamaTextPromptExecutionSettings
# Override from AIServiceClientBase
@override
def service_url(self) -> str | None:
if hasattr(self.client, "_client") and isinstance(self.client._client, httpx.AsyncClient):
# Best effort to get the endpoint
return str(self.client._client.base_url)
return None
@override
@trace_text_completion(OllamaBase.MODEL_PROVIDER_NAME)
async def _inner_get_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> list[TextContent]:
if not isinstance(settings, OllamaTextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, OllamaTextPromptExecutionSettings) # nosec
response_object = await self.client.generate(
model=self.ai_model_id,
prompt=prompt,
stream=False,
**settings.prepare_settings_dict(),
)
if not isinstance(response_object, (Mapping, GenerateResponse)):
raise ServiceInvalidResponseError(
"Invalid response type from Ollama chat completion. "
f"Expected Mapping or GenerateResponse but got {type(response_object)}."
)
return [
TextContent(
inner_content=response_object,
ai_model_id=self.ai_model_id,
text=response_object.response
if isinstance(response_object, GenerateResponse)
else response_object["response"],
)
]
@override
@trace_streaming_text_completion(OllamaBase.MODEL_PROVIDER_NAME)
async def _inner_get_streaming_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> AsyncGenerator[list[StreamingTextContent], Any]:
if not isinstance(settings, OllamaTextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, OllamaTextPromptExecutionSettings) # nosec
response_object = await self.client.generate(
model=self.ai_model_id,
prompt=prompt,
stream=True,
**settings.prepare_settings_dict(),
)
if not isinstance(response_object, AsyncIterator):
raise ServiceInvalidResponseError(
"Invalid response type from Ollama chat completion. "
f"Expected AsyncIterator but got {type(response_object)}."
)
async for part in response_object:
yield [
StreamingTextContent(
choice_index=0,
inner_content=part,
ai_model_id=self.ai_model_id,
text=part.response if isinstance(part, GenerateResponse) else part.get("response"),
)
]
# endregion
@@ -0,0 +1,112 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from typing import TYPE_CHECKING, Any
from ollama import AsyncClient
from pydantic import ValidationError
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaEmbeddingPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.ollama_settings import OllamaSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_base import OllamaBase
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from numpy import array, ndarray
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.utils.feature_stage_decorator import experimental
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class OllamaTextEmbedding(OllamaBase, EmbeddingGeneratorBase):
"""Ollama embeddings client.
Make sure to have the ollama service running either locally or remotely.
"""
def __init__(
self,
service_id: str | None = None,
ai_model_id: str | None = None,
host: str | None = None,
client: AsyncClient | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an OllamaChatCompletion service.
Args:
service_id (Optional[str]): Service ID tied to the execution settings. (Optional)
ai_model_id (Optional[str]): The model name. (Optional)
host (Optional[str]): URL of the Ollama server, defaults to None and
will use the default Ollama service address: http://127.0.0.1:11434. (Optional)
client (Optional[AsyncClient]): A custom Ollama client to use for the service. (Optional)
env_file_path (str | None): Use the environment settings file as a fallback to using env vars.
env_file_encoding (str | None): The encoding of the environment settings file, defaults to 'utf-8'.
"""
try:
ollama_settings = OllamaSettings(
embedding_model_id=ai_model_id,
host=host,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Ollama settings.", ex) from ex
if not ollama_settings.embedding_model_id:
raise ServiceInitializationError("Ollama embedding model ID is not set.")
super().__init__(
service_id=service_id or ollama_settings.embedding_model_id,
ai_model_id=ollama_settings.embedding_model_id,
client=client or AsyncClient(host=ollama_settings.host),
)
@override
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
raw_embeddings = await self.generate_raw_embeddings(texts, settings, **kwargs)
return array(raw_embeddings)
@override
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> Any:
if not settings:
settings = OllamaEmbeddingPromptExecutionSettings()
else:
settings = self.get_prompt_execution_settings_from_settings(settings)
result = []
for text in texts:
response_object = await self.client.embeddings(
model=self.ai_model_id,
prompt=text,
**settings.prepare_settings_dict(),
)
result.append(response_object["embedding"])
return result
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return OllamaEmbeddingPromptExecutionSettings
@@ -0,0 +1,133 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from collections.abc import Callable, Mapping
from typing import TYPE_CHECKING
from ollama._types import Message
from semantic_kernel.connectors.ai.function_calling_utils import kernel_function_metadata_to_function_call_format
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def _format_system_message(message: ChatMessageContent) -> Message:
"""Format a system message to the expected object for the client.
Args:
message: The system message.
Returns:
The formatted system message.
"""
return Message(role="system", content=message.content)
def _format_user_message(message: ChatMessageContent) -> Message:
"""Format a user message to the expected object for the client.
Args:
message: The user message.
Returns:
The formatted user message.
"""
if not any(isinstance(item, (ImageContent)) for item in message.items):
return Message(role="user", content=message.content)
user_message = Message(role="user", content=message.content)
image_items = [item for item in message.items if isinstance(item, ImageContent)]
if image_items:
if any(not image_item.data for image_item in image_items):
raise ValueError("Image item must contain data encoded as base64.")
user_message["images"] = [image_item.data for image_item in image_items]
return user_message
def _format_assistant_message(message: ChatMessageContent) -> Message:
"""Format an assistant message to the expected object for the client.
Args:
message: The assistant message.
Returns:
The formatted assistant message.
"""
assistant_message = Message(role="assistant", content=message.content)
image_items = [item for item in message.items if isinstance(item, ImageContent)]
if image_items:
if any(image_item.data is None for image_item in image_items):
raise ValueError("Image must be encoded as base64.")
assistant_message["images"] = [image_item.data for image_item in image_items]
tool_calls = [item for item in message.items if isinstance(item, FunctionCallContent)]
if tool_calls:
assistant_message["tool_calls"] = [
{
"function": {
"name": tool_call.function_name,
"arguments": tool_call.arguments
if isinstance(tool_call.arguments, Mapping)
else json.loads(tool_call.arguments or "{}"),
}
}
for tool_call in tool_calls
]
return assistant_message
def _format_tool_message(message: ChatMessageContent) -> Message:
"""Format a tool message to the expected object for the client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
function_result_items = [item for item in message.items if isinstance(item, FunctionResultContent)]
if not function_result_items:
raise ValueError("Tool message must have a function result content item.")
return Message(role="tool", content=str(function_result_items[0].result))
MESSAGE_CONVERTERS: dict[AuthorRole, Callable[[ChatMessageContent], Message]] = {
AuthorRole.SYSTEM: _format_system_message,
AuthorRole.USER: _format_user_message,
AuthorRole.ASSISTANT: _format_assistant_message,
AuthorRole.TOOL: _format_tool_message,
}
def update_settings_from_function_choice_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: FunctionChoiceType,
) -> None:
"""Update the settings from a FunctionChoiceConfiguration.
Since this function might be called before the settings are cast to Ollama Settings
We need to try to use the tools attribute or fallback to the extension_data attribute.
"""
if function_choice_configuration.available_functions:
tools = [
kernel_function_metadata_to_function_call_format(f)
for f in function_choice_configuration.available_functions
]
try:
settings.tools = tools # type: ignore
except Exception:
settings.extension_data["tools"] = tools