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,84 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator, AsyncIterator
from unittest.mock import MagicMock
import pytest
from ollama import AsyncClient
from semantic_kernel.contents.chat_history import ChatHistory
@pytest.fixture()
def model_id() -> str:
return "test_model_id"
@pytest.fixture()
def service_id() -> str:
return "test_service_id"
@pytest.fixture()
def host() -> str:
return "http://localhost:5000"
@pytest.fixture()
def custom_client() -> AsyncClient:
return AsyncClient("http://localhost:5001")
@pytest.fixture()
def chat_history() -> ChatHistory:
chat_history = ChatHistory()
chat_history.add_user_message("test_prompt")
return chat_history
@pytest.fixture()
def prompt() -> str:
return "test_prompt"
@pytest.fixture()
def default_options() -> dict:
return {"test": "test"}
@pytest.fixture()
def ollama_unit_test_env(monkeypatch, host, exclude_list):
"""Fixture to set environment variables for OllamaSettings."""
if exclude_list is None:
exclude_list = []
env_vars = {
"OLLAMA_CHAT_MODEL_ID": "test_chat_model_id",
"OLLAMA_TEXT_MODEL_ID": "test_text_model_id",
"OLLAMA_EMBEDDING_MODEL_ID": "test_embedding_model_id",
"OLLAMA_HOST": host,
}
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def mock_streaming_text_response() -> AsyncIterator:
streaming_text_response = MagicMock(spec=AsyncGenerator)
streaming_text_response.__aiter__.return_value = [{"response": "test_response"}]
return streaming_text_response
@pytest.fixture()
def mock_streaming_chat_response() -> AsyncIterator:
streaming_chat_response = MagicMock(spec=AsyncGenerator)
streaming_chat_response.__aiter__.return_value = [{"message": {"content": "test_response"}}]
return streaming_chat_response
@@ -0,0 +1,453 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from ollama import AsyncClient
import semantic_kernel.connectors.ai.ollama.services.ollama_chat_completion as occ_module
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaChatPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_chat_completion import OllamaChatCompletion
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
ServiceInvalidResponseError,
)
def test_settings(model_id):
"""Test that the settings class is correct."""
ollama = OllamaChatCompletion(ai_model_id=model_id)
settings = ollama.get_prompt_execution_settings_class()
assert settings == OllamaChatPromptExecutionSettings
def test_init_empty_service_id(model_id):
"""Test that the service initializes correctly with an empty service id."""
ollama = OllamaChatCompletion(ai_model_id=model_id)
assert ollama.service_id == model_id
def test_init_empty_string_ai_model_id():
"""Test that the service initializes with a error if there is no ai_model_id."""
with pytest.raises(ServiceInitializationError):
_ = OllamaChatCompletion(ai_model_id="")
def test_custom_client(model_id, custom_client):
"""Test that the service initializes correctly with a custom client."""
ollama = OllamaChatCompletion(ai_model_id=model_id, client=custom_client)
assert ollama.client == custom_client
def test_invalid_ollama_settings():
"""Test that the service initializes incorrectly with invalid settings."""
with pytest.raises(ServiceInitializationError):
_ = OllamaChatCompletion(ai_model_id=123)
@pytest.mark.parametrize("exclude_list", [["OLLAMA_CHAT_MODEL_ID"]], indirect=True)
def test_init_empty_model_id_in_env(ollama_unit_test_env):
"""Test that the service initializes incorrectly with an empty model id."""
with pytest.raises(ServiceInitializationError):
_ = OllamaChatCompletion(env_file_path="fake_env_file_path.env")
def test_function_choice_settings(ollama_unit_test_env):
"""Test that REQUIRED and NONE function choice settings are unsupported."""
ollama = OllamaChatCompletion()
with pytest.raises(ServiceInvalidExecutionSettingsError):
ollama._verify_function_choice_settings(
OllamaChatPromptExecutionSettings(function_choice_behavior=FunctionChoiceBehavior.Required())
)
with pytest.raises(ServiceInvalidExecutionSettingsError):
ollama._verify_function_choice_settings(
OllamaChatPromptExecutionSettings(function_choice_behavior=FunctionChoiceBehavior.NoneInvoke())
)
def test_service_url(ollama_unit_test_env):
"""Test that the service URL is correct."""
ollama = OllamaChatCompletion()
assert ollama.service_url() == ollama_unit_test_env["OLLAMA_HOST"]
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.chat") # mock_chat_client
async def test_custom_host(
mock_chat_client,
mock_client,
model_id,
service_id,
host,
chat_history,
prompt,
default_options,
):
"""Test that the service initializes and generates content correctly with a custom host."""
mock_chat_client.return_value = {"message": {"content": "test_response"}}
ollama = OllamaChatCompletion(ai_model_id=model_id, host=host)
chat_responses = await ollama.get_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
)
# Check that the client was initialized once with the correct host
assert mock_client.call_count == 1
mock_client.assert_called_with(host=host)
# Check that the chat client was called once and the responses are correct
assert mock_chat_client.call_count == 1
assert len(chat_responses) == 1
assert chat_responses[0].content == "test_response"
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.chat") # mock_chat_client
async def test_custom_host_streaming(
mock_chat_client,
mock_client,
mock_streaming_chat_response,
model_id,
service_id,
host,
chat_history,
prompt,
default_options,
):
"""Test that the service initializes and generates streaming content correctly with a custom host."""
mock_chat_client.return_value = mock_streaming_chat_response
ollama = OllamaChatCompletion(ai_model_id=model_id, host=host)
async for messages in ollama.get_streaming_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].content == "test_response"
# Check that the client was initialized once with the correct host
assert mock_client.call_count == 1
mock_client.assert_called_with(host=host)
# Check that the chat client was called once
assert mock_chat_client.call_count == 1
@patch("ollama.AsyncClient.chat")
async def test_chat_completion(mock_chat_client, model_id, service_id, chat_history, default_options):
"""Test that the chat completion service completes correctly."""
mock_chat_client.return_value = {"message": {"content": "test_response"}}
ollama = OllamaChatCompletion(ai_model_id=model_id)
response = await ollama.get_chat_message_contents(
chat_history=chat_history,
settings=OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
)
assert len(response) == 1
assert response[0].content == "test_response"
mock_chat_client.assert_called_once_with(
model=model_id,
messages=ollama._prepare_chat_history_for_request(chat_history),
options=default_options,
stream=False,
)
@patch("ollama.AsyncClient.chat")
async def test_chat_completion_wrong_return_type(
mock_chat_client,
mock_streaming_chat_response,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the chat completion service fails when the return type is incorrect."""
mock_chat_client.return_value = mock_streaming_chat_response # should not be a streaming response
ollama = OllamaChatCompletion(ai_model_id=model_id)
with pytest.raises(ServiceInvalidResponseError):
await ollama.get_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
)
@patch("ollama.AsyncClient.chat")
async def test_streaming_chat_completion(
mock_chat_client,
mock_streaming_chat_response,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the streaming chat completion service completes correctly."""
mock_chat_client.return_value = mock_streaming_chat_response
ollama = OllamaChatCompletion(ai_model_id=model_id)
response = ollama.get_streaming_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
)
responses = []
async for line in response:
if line:
assert line[0].content == "test_response"
responses.append(line[0].content)
assert len(responses) == 1
mock_chat_client.assert_called_once_with(
model=model_id,
messages=ollama._prepare_chat_history_for_request(chat_history),
options=default_options,
stream=True,
)
@patch("ollama.AsyncClient.chat")
async def test_streaming_chat_completion_wrong_return_type(
mock_chat_client,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the chat completion streaming service fails when the return type is incorrect."""
mock_chat_client.return_value = {"message": {"content": "test_response"}} # should not be a non-streaming response
ollama = OllamaChatCompletion(ai_model_id=model_id)
with pytest.raises(ServiceInvalidResponseError):
async for _ in ollama.get_streaming_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
):
pass
@pytest.fixture
async def setup_ollama_chat_completion():
async_client_mock = AsyncMock(spec=AsyncClient)
async_client_mock.chat = AsyncMock()
ollama_chat_completion = OllamaChatCompletion(
service_id="test_service", ai_model_id="test_model_id", client=async_client_mock
)
return ollama_chat_completion, async_client_mock
async def test_service_url_new(setup_ollama_chat_completion):
ollama_chat_completion, async_client_mock = setup_ollama_chat_completion
# Mock the client's internal structure
async_client_mock._client = AsyncMock(spec=httpx.AsyncClient)
async_client_mock._client.base_url = "http://mocked_base_url"
service_url = ollama_chat_completion.service_url()
assert service_url == "http://mocked_base_url"
async def test_prepare_chat_history_for_request(setup_ollama_chat_completion):
ollama_chat_completion, _ = setup_ollama_chat_completion
chat_history = MagicMock(spec=ChatHistory)
chat_history.messages = []
prepared_history = ollama_chat_completion._prepare_chat_history_for_request(chat_history)
assert prepared_history == []
async def test_service_url_with_httpx_client(model_id: str) -> None:
"""
Test that service_url returns the base_url of the underlying httpx.AsyncClient.
"""
# Initialize an AsyncClient and manually set its _client attribute to an httpx.AsyncClient
client = AsyncClient(host="unused")
base = httpx.AsyncClient(base_url="http://example.com:8000")
client._client = base # simulate underlying httpx client
ollama = OllamaChatCompletion(ai_model_id=model_id, client=client)
# service_url should reflect the base_url of the httpx client
assert ollama.service_url() == "http://example.com:8000"
@patch("ollama.AsyncClient.chat", new_callable=AsyncMock)
async def test_chat_response_branch(
mock_chat: AsyncMock,
model_id: str,
service_id: str,
default_options: dict,
chat_history,
monkeypatch,
) -> None:
"""
Test get_chat_message_contents when AsyncClient.chat returns a ChatResponse instance.
"""
class DummyFunction:
def __init__(self, name, arguments):
self.name = name
self.arguments = arguments
class DummyToolCall:
def __init__(self, function):
self.function = function
class DummyMessage:
def __init__(self, content: str, tool_calls=None) -> None:
self.content = content
self.tool_calls = tool_calls or []
class DummyChatResponse:
def __init__(
self,
content: str,
model: str,
prompt_eval_count: int,
eval_count: int,
tool_calls=None,
) -> None:
function_calls = [
DummyToolCall(DummyFunction(tc["function"]["name"], tc["function"]["arguments"])) for tc in tool_calls
]
self.message = DummyMessage(content, function_calls)
self.model = model
self.prompt_eval_count = prompt_eval_count
self.eval_count = eval_count
# Monkeypatch the ChatResponse type in the module so isinstance works
monkeypatch.setattr(occ_module, "ChatResponse", DummyChatResponse)
# Prepare a dummy ChatResponse return value
dummy_resp = DummyChatResponse(
content="resp_text",
model="mdl",
prompt_eval_count=2,
eval_count=3,
tool_calls=[{"function": {"name": "fn", "arguments": {"x": 1}}}],
)
mock_chat.return_value = dummy_resp
ollama = OllamaChatCompletion(ai_model_id=model_id)
settings = OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options)
results = await ollama.get_chat_message_contents(chat_history, settings)
# Only one response expected
assert len(results) == 1
msg = results[0]
# Assert it's a ChatMessageContent
assert isinstance(msg, ChatMessageContent)
# The content property should return the response text
assert msg.content == "resp_text"
# The second item should be a FunctionCallContent
func_item = msg.items[1]
assert isinstance(func_item, FunctionCallContent)
# Validate function call details
assert func_item.name == "fn"
assert func_item.arguments == {"x": 1}
# Check metadata
assert "model" in msg.metadata and msg.metadata["model"] == "mdl"
# Access usage directly, key should exist
usage = msg.metadata["usage"]
assert isinstance(usage, CompletionUsage)
assert usage.prompt_tokens == 2 and usage.completion_tokens == 3
@patch("ollama.AsyncClient.chat", new_callable=AsyncMock)
async def test_streaming_chat_response_branch(
mock_chat: AsyncMock,
model_id: str,
service_id: str,
default_options: dict,
chat_history,
monkeypatch,
) -> None:
"""
Test get_streaming_chat_message_contents when AsyncClient.chat yields ChatResponse instances.
"""
class DummyFunction:
def __init__(self, name, arguments):
self.name = name
self.arguments = arguments
class DummyToolCall:
def __init__(self, function):
self.function = function
class DummyMessage:
def __init__(self, content: str, tool_calls=None) -> None:
self.content = content
self.tool_calls = tool_calls or []
class DummyChatResponse:
def __init__(
self,
content: str,
model: str,
prompt_eval_count: int,
eval_count: int,
tool_calls=None,
) -> None:
function_calls = [
DummyToolCall(DummyFunction(tc["function"]["name"], tc["function"]["arguments"])) for tc in tool_calls
]
self.message = DummyMessage(content, function_calls)
self.model = model
self.prompt_eval_count = prompt_eval_count
self.eval_count = eval_count
# Monkeypatch ChatResponse type
monkeypatch.setattr(occ_module, "ChatResponse", DummyChatResponse)
# Prepare an async generator yielding DummyChatResponse
async def fake_stream() -> AsyncGenerator[DummyChatResponse, None]:
yield DummyChatResponse(
content="stream_text",
model="m2",
prompt_eval_count=1,
eval_count=1,
tool_calls=[{"function": {"name": "f2", "arguments": {}}}],
)
mock_chat.return_value = fake_stream()
ollama = OllamaChatCompletion(ai_model_id=model_id)
settings = OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options)
collected = []
# Iterate over streamed batches
async for batch in ollama.get_streaming_chat_message_contents(chat_history, settings):
# We expect a list with a single StreamingChatMessageContent
assert len(batch) == 1
sc = batch[0]
assert isinstance(sc, StreamingChatMessageContent)
# First item should be text content
text_item = sc.items[0]
assert isinstance(text_item, StreamingTextContent)
assert text_item.text == "stream_text"
# Next item should be a FunctionCallContent
func_item = sc.items[1]
assert isinstance(func_item, FunctionCallContent)
assert func_item.name == "f2"
collected.append(sc)
# Only one batch should be collected
assert len(collected) == 1
@@ -0,0 +1,178 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator
from unittest.mock import MagicMock, patch
import pytest
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaTextPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_text_completion import OllamaTextCompletion
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidResponseError
def test_settings(model_id):
"""Test that the settings class is correct"""
ollama = OllamaTextCompletion(ai_model_id=model_id)
settings = ollama.get_prompt_execution_settings_class()
assert settings == OllamaTextPromptExecutionSettings
def test_init_empty_service_id(model_id):
"""Test that the service initializes correctly with an empty service_id"""
ollama = OllamaTextCompletion(ai_model_id=model_id)
assert ollama.service_id == model_id
def test_custom_client(model_id, custom_client):
"""Test that the service initializes correctly with a custom client."""
ollama = OllamaTextCompletion(ai_model_id=model_id, client=custom_client)
assert ollama.client == custom_client
def test_invalid_ollama_settings():
"""Test that the service initializes incorrectly with invalid settings."""
with pytest.raises(ServiceInitializationError):
_ = OllamaTextCompletion(ai_model_id=123)
def test_service_url(ollama_unit_test_env):
"""Test that the service URL is correct."""
ollama = OllamaTextCompletion()
assert ollama.service_url() == ollama_unit_test_env["OLLAMA_HOST"]
@pytest.mark.parametrize("exclude_list", [["OLLAMA_TEXT_MODEL_ID"]], indirect=True)
def test_init_empty_model_id(ollama_unit_test_env):
"""Test that the service initializes incorrectly with an empty model_id"""
with pytest.raises(ServiceInitializationError):
_ = OllamaTextCompletion(env_file_path="fake_env_file_path.env")
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.generate") # mock_completion_client
async def test_custom_host(
mock_completion_client, mock_client, model_id, service_id, host, chat_history, default_options
):
"""Test that the service initializes and generates content correctly with a custom host."""
mock_completion_client.return_value = {"response": "test_response"}
ollama = OllamaTextCompletion(ai_model_id=model_id, host=host)
_ = await ollama.get_text_contents(
chat_history,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
)
mock_client.assert_called_once_with(host=host)
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.generate") # mock_completion_client
async def test_custom_host_streaming(
mock_completion_client, mock_client, model_id, service_id, host, chat_history, default_options
):
"""Test that the service initializes and generates streaming content correctly with a custom host."""
# Create a proper async iterator mock for streaming
streaming_response = MagicMock(spec=AsyncGenerator)
streaming_response.__aiter__.return_value = [{"response": "test_response"}]
mock_completion_client.return_value = streaming_response
ollama = OllamaTextCompletion(ai_model_id=model_id, host=host)
async for _ in ollama.get_streaming_text_contents(
chat_history,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
):
pass
mock_client.assert_called_once_with(host=host)
@patch("ollama.AsyncClient.generate")
async def test_completion(mock_completion_client, model_id, service_id, prompt, default_options):
"""Test that the service generates content correctly."""
mock_completion_client.return_value = {"response": "test_response"}
ollama = OllamaTextCompletion(ai_model_id=model_id)
response = await ollama.get_text_contents(
prompt=prompt,
settings=OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
)
assert response[0].text == "test_response"
mock_completion_client.assert_called_once_with(
model=model_id,
prompt=prompt,
options=default_options,
stream=False,
)
@patch("ollama.AsyncClient.generate")
async def test_completion_wrong_return_type(
mock_completion_client,
mock_streaming_text_response,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the completion service fails when the return type is incorrect."""
mock_completion_client.return_value = mock_streaming_text_response # should not be a streaming response
ollama = OllamaTextCompletion(ai_model_id=model_id)
with pytest.raises(ServiceInvalidResponseError):
await ollama.get_text_contents(
chat_history,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
)
@patch("ollama.AsyncClient.generate")
async def test_streaming_completion(
mock_completion_client,
mock_streaming_text_response,
model_id,
service_id,
prompt,
default_options,
):
"""Test that the service generates streaming content correctly."""
mock_completion_client.return_value = mock_streaming_text_response
ollama = OllamaTextCompletion(ai_model_id=model_id)
response = ollama.get_streaming_text_contents(
prompt,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
)
responses = []
async for line in response:
assert line[0].text == "test_response"
responses.append(line)
assert len(responses) == 1
mock_completion_client.assert_called_once_with(
model=model_id,
prompt=prompt,
options=default_options,
stream=True,
)
@patch("ollama.AsyncClient.generate")
async def test_streaming_completion_wrong_return_type(
mock_completion_client,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the streaming completion service fails when the return type is incorrect."""
mock_completion_client.return_value = {"response": "test_response"} # should not be a non-streaming response
ollama = OllamaTextCompletion(ai_model_id=model_id)
with pytest.raises(ServiceInvalidResponseError):
async for _ in ollama.get_streaming_text_contents(
chat_history,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
):
pass
@@ -0,0 +1,122 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import patch
import numpy
import pytest
from numpy import array
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaEmbeddingPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_text_embedding import OllamaTextEmbedding
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
def test_init_empty_service_id(model_id):
"""Test that the service initializes correctly with an empty service id."""
ollama = OllamaTextEmbedding(ai_model_id=model_id)
assert ollama.service_id == model_id
def test_custom_client(model_id, custom_client):
"""Test that the service initializes correctly with a custom client."""
ollama = OllamaTextEmbedding(ai_model_id=model_id, client=custom_client)
assert ollama.client == custom_client
def test_invalid_ollama_settings():
"""Test that the service initializes incorrectly with invalid settings."""
with pytest.raises(ServiceInitializationError):
_ = OllamaTextEmbedding(ai_model_id=123)
@pytest.mark.parametrize("exclude_list", [["OLLAMA_EMBEDDING_MODEL_ID"]], indirect=True)
def test_init_empty_model_id(ollama_unit_test_env):
"""Test that the service initializes incorrectly with an empty model id."""
with pytest.raises(ServiceInitializationError):
_ = OllamaTextEmbedding(env_file_path="fake_env_file_path.env")
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.embeddings") # mock_embedding_client
async def test_custom_host(mock_embedding_client, mock_client, model_id, host, prompt):
"""Test that the service initializes and generates embeddings correctly with a custom host."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
ollama = OllamaTextEmbedding(ai_model_id=model_id, host=host)
_ = await ollama.generate_embeddings(
[prompt],
)
mock_client.assert_called_once_with(host=host)
@patch("ollama.AsyncClient.embeddings")
async def test_embedding(mock_embedding_client, model_id, prompt):
"""Test that the service initializes and generates embeddings correctly."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
settings = OllamaEmbeddingPromptExecutionSettings()
settings.options = {"test_key": "test_value"}
ollama = OllamaTextEmbedding(ai_model_id=model_id)
response = await ollama.generate_embeddings(
[prompt],
settings=settings,
)
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_embedding_client.assert_called_once_with(model=model_id, prompt=prompt, options=settings.options)
@patch("ollama.AsyncClient.embeddings")
async def test_embedding_list_input(mock_embedding_client, model_id, prompt):
"""Test that the service initializes and generates embeddings correctly with a list of prompts."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
settings = OllamaEmbeddingPromptExecutionSettings()
settings.options = {"test_key": "test_value"}
ollama = OllamaTextEmbedding(ai_model_id=model_id)
responses = await ollama.generate_embeddings(
[prompt, prompt],
settings=settings,
)
assert len(responses) == 2
assert type(responses) is numpy.ndarray
assert all(type(response) is numpy.ndarray for response in responses)
assert mock_embedding_client.call_count == 2
mock_embedding_client.assert_called_with(model=model_id, prompt=prompt, options=settings.options)
@patch("ollama.AsyncClient.embeddings")
async def test_raw_embedding(mock_embedding_client, model_id, prompt):
"""Test that the service initializes and generates embeddings correctly."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
settings = OllamaEmbeddingPromptExecutionSettings()
settings.options = {"test_key": "test_value"}
ollama = OllamaTextEmbedding(ai_model_id=model_id)
response = await ollama.generate_raw_embeddings(
[prompt],
settings=settings,
)
assert response == [[0.1, 0.2, 0.3]]
mock_embedding_client.assert_called_once_with(model=model_id, prompt=prompt, options=settings.options)
@patch("ollama.AsyncClient.embeddings")
async def test_raw_embedding_list_input(mock_embedding_client, model_id, prompt):
"""Test that the service initializes and generates embeddings correctly with a list of prompts."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
settings = OllamaEmbeddingPromptExecutionSettings()
settings.options = {"test_key": "test_value"}
ollama = OllamaTextEmbedding(ai_model_id=model_id)
responses = await ollama.generate_raw_embeddings(
[prompt, prompt],
settings=settings,
)
assert responses == [[0.1, 0.2, 0.3], [0.1, 0.2, 0.3]]
assert mock_embedding_client.call_count == 2
mock_embedding_client.assert_called_with(model=model_id, prompt=prompt, options=settings.options)
@@ -0,0 +1,240 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock, patch
import pytest
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
# The code under test
from semantic_kernel.connectors.ai.ollama.services.utils import (
MESSAGE_CONVERTERS,
update_settings_from_function_choice_configuration,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
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
@pytest.fixture
def mock_chat_message_content() -> ChatMessageContent:
"""Fixture to create a basic ChatMessageContent object with role=USER and simple text content."""
return ChatMessageContent(
role=AuthorRole.USER,
content="Hello, I am a user message.", # The text content
)
@pytest.fixture
def mock_system_message_content() -> ChatMessageContent:
"""Fixture to create a ChatMessageContent object with role=SYSTEM."""
return ChatMessageContent(role=AuthorRole.SYSTEM, content="This is a system message.")
@pytest.fixture
def mock_assistant_message_content() -> ChatMessageContent:
"""Fixture to create a ChatMessageContent object with role=ASSISTANT."""
return ChatMessageContent(role=AuthorRole.ASSISTANT, content="This is an assistant message.")
@pytest.fixture
def mock_tool_message_content() -> ChatMessageContent:
"""Fixture to create a ChatMessageContent object with role=TOOL."""
return ChatMessageContent(role=AuthorRole.TOOL, content="This is a tool message.")
def test_message_converters_system(mock_system_message_content: ChatMessageContent) -> None:
"""Test that passing a system message returns the correct dictionary structure for 'system' role."""
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.SYSTEM]
result = converter(mock_system_message_content)
# Assert
assert result["role"] == "system", "Expected role to be 'system' on the returned message."
assert result["content"] == mock_system_message_content.content, (
"Expected content to match the system message content."
)
def test_message_converters_user_no_images(mock_chat_message_content: ChatMessageContent) -> None:
"""Test that passing a user message without images returns correct dictionary structure for 'user' role."""
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.USER]
result = converter(mock_chat_message_content)
# Assert
assert result["role"] == "user", "Expected role to be 'user' on the returned message."
assert result["content"] == mock_chat_message_content.content, "Expected content to match the user message content."
# Ensure that no 'images' field is added
assert "images" not in result, "No images should be present if no ImageContent is added."
def test_message_converters_user_with_images() -> None:
"""Test user message with multiple images, verifying the 'images' field is populated."""
# Arrange
img1 = ImageContent(data="some_base64_data")
img2 = ImageContent(data="other_base64_data")
content = ChatMessageContent(role=AuthorRole.USER, items=[img1, img2], content="User with images")
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.USER]
result = converter(content)
# Assert
assert result["role"] == "user"
assert result["content"] == content.content
assert "images" in result, "Images field expected when ImageContent is present."
assert len(result["images"]) == 2, "Two images should be in the 'images' field."
assert result["images"] == [b"some_base64_data", b"other_base64_data"], (
"Image data should match the content from ImageContent."
)
def test_message_converters_user_with_image_missing_data() -> None:
"""Test user message with image content that has missing data, expecting ValueError."""
# Arrange
bad_image = ImageContent(data="") # empty data for image
content = ChatMessageContent(role=AuthorRole.USER, items=[bad_image])
# Act & Assert
converter = MESSAGE_CONVERTERS[AuthorRole.USER]
with pytest.raises(ValueError) as exc_info:
converter(content)
assert "Image item must contain data encoded as base64." in str(exc_info.value), (
"Should raise ValueError for missing base64 data in image."
)
def test_message_converters_assistant_basic(mock_assistant_message_content: ChatMessageContent) -> None:
"""Test assistant message without images or tool calls."""
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT]
result = converter(mock_assistant_message_content)
# Assert
assert result["role"] == "assistant", "Assistant role expected."
assert result["content"] == mock_assistant_message_content.content
assert "images" not in result, "No images included, so should not have an 'images' field."
assert "tool_calls" not in result, "No FunctionCallContent, so 'tool_calls' field shouldn't be present."
def test_message_converters_assistant_with_image() -> None:
"""Test assistant message containing images. Verify 'images' field is added."""
# Arrange
img = ImageContent(data="assistant_base64_data")
content = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[img], content="Assistant image message")
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT]
result = converter(content)
# Assert
assert result["role"] == "assistant"
assert result["content"] == content.content
assert "images" in result, "Images should be included for assistant messages with ImageContent."
assert result["images"] == [b"assistant_base64_data"], "Expected matching base64 data in images."
def test_message_converters_assistant_with_tool_calls() -> None:
"""Test assistant message with FunctionCallContent should populate 'tool_calls'."""
# Arrange
tool_call_1 = FunctionCallContent(function_name="foo", arguments='{"key": "value"}')
tool_call_2 = FunctionCallContent(function_name="bar", arguments='{"another": "123"}')
content = ChatMessageContent(
role=AuthorRole.ASSISTANT, items=[tool_call_1, tool_call_2], content="Assistant with tools"
)
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT]
result = converter(content)
# Assert
assert result["role"] == "assistant"
assert result["content"] == content.content
assert "tool_calls" in result, "tool_calls field should be present for assistant messages with FunctionCallContent."
assert len(result["tool_calls"]) == 2, "Expected two tool calls in the result."
assert result["tool_calls"][0]["function"]["name"] == "foo", "First tool call function name mismatched."
assert result["tool_calls"][0]["function"]["arguments"] == {"key": "value"}, "Expected arguments to be JSON loaded."
assert result["tool_calls"][1]["function"]["name"] == "bar", "Second tool call function name mismatched."
assert result["tool_calls"][1]["function"]["arguments"] == {"another": "123"}, (
"Expected arguments to be JSON loaded."
)
def test_message_converters_tool_with_result() -> None:
"""Test tool message with a FunctionResultContent, verifying the message content is set."""
# Arrange
fr_content = FunctionResultContent(id="some_id", result="some result", function_name="test_func")
tool_message = ChatMessageContent(role=AuthorRole.TOOL, items=[fr_content])
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.TOOL]
result = converter(tool_message)
# Assert
assert result["role"] == "tool", "Expected role to be 'tool' for a tool message."
# The code takes the first FunctionResultContent's result as the content
assert result["content"] == fr_content.result, "Expected content to match the function result."
def test_message_converters_tool_missing_function_result_content(mock_tool_message_content: ChatMessageContent) -> None:
"""Test that if no FunctionResultContent is present, ValueError is raised."""
# Arrange
mock_tool_message_content.items = [] # no FunctionResultContent in items
converter = MESSAGE_CONVERTERS[AuthorRole.TOOL]
# Act & Assert
with pytest.raises(ValueError) as exc_info:
converter(mock_tool_message_content)
assert "Tool message must have a function result content item." in str(exc_info.value)
@pytest.mark.parametrize("choice_type", [FunctionChoiceType.AUTO, FunctionChoiceType.NONE, FunctionChoiceType.REQUIRED])
def test_update_settings_from_function_choice_configuration(choice_type: FunctionChoiceType) -> None:
"""Test that update_settings_from_function_choice_configuration updates the settings with the correct tools."""
# Arrange
# We'll create a mock configuration with some available functions.
mock_config = FunctionCallChoiceConfiguration()
mock_config.available_functions = [MagicMock() for _ in range(2)]
# We also patch the kernel_function_metadata_to_function_call_format function.
# The function returns a dict object describing each function.
mock_tool_description = {"type": "function", "function": {"name": "mocked_function"}}
with patch(
"semantic_kernel.connectors.ai.ollama.services.utils.kernel_function_metadata_to_function_call_format",
return_value=mock_tool_description,
):
settings = PromptExecutionSettings()
# Act
update_settings_from_function_choice_configuration(
function_choice_configuration=mock_config,
settings=settings,
type=choice_type,
)
# Assert
# After the call, either settings.tools or settings.extension_data["tools"] should be set.
# The code tries settings.tools first and if it fails, it sets extension_data["tools"].
# We'll check both possibilities.
possible_tools = getattr(settings, "tools", None)
if possible_tools is not None:
# If settings.tools exists, ensure it got updated
assert len(possible_tools) == 2, "Should have exactly two tools set in the settings.tools attribute."
assert possible_tools[0]["function"]["name"] == "mocked_function", (
"Expected mocked function name in settings.tools."
)
else:
# Otherwise check for extension_data
assert "tools" in settings.extension_data, "Expected 'tools' in extension_data if settings.tools not present."
assert len(settings.extension_data["tools"]) == 2, "Should have exactly two tools in extension_data."
assert settings.extension_data["tools"][0]["function"]["name"] == "mocked_function", (
"Expected mocked function name in extension_data."
)
@@ -0,0 +1,95 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.ollama import OllamaPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import (
OllamaChatPromptExecutionSettings,
OllamaTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_ollama_prompt_execution_settings():
settings = OllamaPromptExecutionSettings()
assert settings.format is None
assert settings.options is None
def test_custom_ollama_prompt_execution_settings():
settings = OllamaPromptExecutionSettings(
format="json",
options={
"key": "value",
},
)
assert settings.format == "json"
assert settings.options == {"key": "value"}
def test_ollama_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = OllamaChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.format is None
assert chat_settings.options is None
def test_ollama_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = OllamaChatPromptExecutionSettings(service_id="test_service", options={"temperature": 0.5})
new_settings = OllamaPromptExecutionSettings(service_id="test_2", options={"temperature": 0.0})
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.options["temperature"] == 0.0
def test_ollama_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"format": "json",
"options": {
"key": "value",
},
},
)
chat_settings = OllamaChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.format == "json"
assert chat_settings.options == {"key": "value"}
def test_ollama_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"tools": [{"function": {}}],
},
)
chat_settings = OllamaChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.tools == [{"function": {}}]
def test_create_options():
settings = OllamaChatPromptExecutionSettings(
service_id="test_service",
extension_data={
"format": "json",
},
)
options = settings.prepare_settings_dict()
assert options["format"] == "json"
def test_default_ollama_text_prompt_execution_settings():
settings = OllamaTextPromptExecutionSettings()
assert settings.system is None
assert settings.template is None
assert settings.context is None
assert settings.raw is None
@@ -0,0 +1,29 @@
# Copyright (c) Microsoft. All rights reserved.
import json
class MockResponse:
def __init__(self, response, status=200):
self._response = response
self.status = status
async def text(self):
return self._response
async def json(self):
return self._response
def raise_for_status(self):
pass
@property
async def content(self):
yield json.dumps(self._response).encode("utf-8")
yield json.dumps({"done": True}).encode("utf-8")
async def __aexit__(self, exc_type, exc, tb):
pass
async def __aenter__(self):
return self