chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user