Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
382955c
work in progress
Mar 18, 2025
9ce5b0e
Merge remote-tracking branch 'upstream/main' into new-tests
Mar 18, 2025
8cca51a
new tests for OllamaChatCompletion and OpenAIRealTime classes
Mar 19, 2025
f057ef2
fix
Mar 24, 2025
f1c1a85
updates to tests
Mar 28, 2025
3302266
fixes in tests
Mar 30, 2025
1f32149
fix pre-commit errors
Mar 31, 2025
ef1f0db
fix pre-commit errors
Mar 31, 2025
78f5d6b
fix pre-commit errors
Mar 31, 2025
4dc951b
merge conflict solved
Apr 1, 2025
8c4d3e5
pre-commit
Apr 1, 2025
61e04ed
pre-commit
Apr 1, 2025
e49a62a
pre-commit
Apr 1, 2025
6807474
Merge pull request #1 from gaudyb/new-tests
gaudyb Apr 2, 2025
aa2ac64
revert to original version
Apr 2, 2025
5b0f8c8
Merge remote-tracking branch 'upstream/main'
Apr 2, 2025
fb17b30
feedback implemented
Apr 3, 2025
d61993a
feedback implemented
Apr 3, 2025
47f7586
feedback implemented
Apr 4, 2025
268fb25
Merge remote-tracking branch 'upstream/main'
Apr 8, 2025
0cbde4f
new tests progress
Apr 9, 2025
c5e9eb1
new test
Apr 9, 2025
ac95200
new tests for azure_cosmos_db_mongodb_collection and local_step
Apr 10, 2025
129845a
Merge remote-tracking branch 'upstream/main' into new_tests
Apr 10, 2025
77d1ee6
Merge remote-tracking branch 'upstream/main'
Apr 10, 2025
3cfb325
Merge remote-tracking branch 'origin/main' into new_tests
Apr 10, 2025
bf129df
Merge pull request #2 from gaudyb/new_tests
gaudyb Apr 15, 2025
3df55df
feedback implemented
Apr 24, 2025
4d29984
merge conflict solved
Apr 24, 2025
6fcd98c
uv file updated
Apr 24, 2025
d140381
issues fixed
Apr 24, 2025
d2bbb0c
Merge remote-tracking branch 'upstream/main'
Apr 24, 2025
e9abbbd
Merge remote-tracking branch 'upstream/main'
Apr 25, 2025
d78304b
merge conflict solved
Apr 28, 2025
365691e
merge conflict solved
Apr 29, 2025
b70ef22
Merge remote-tracking branch 'upstream/main'
Apr 29, 2025
566358d
new test class for chroma/utils and new tests for ollama_chat_completion
Apr 29, 2025
d57f7c4
new test class for process_builder
Apr 29, 2025
9422e4a
new tests for step_actor class
Apr 30, 2025
44ce7e6
Merge remote-tracking branch 'upstream/main'
Apr 30, 2025
64a412f
new test classes for azure_cosmos_db_mongodb_store, redis/utils and p…
May 1, 2025
9955a5e
Merge remote-tracking branch 'upstream/main' into more_new_tests
May 1, 2025
df2e9c5
Merge remote-tracking branch 'upstream/main'
May 1, 2025
c18ad27
Merge remote-tracking branch 'upstream/main' into update_test_classes
May 1, 2025
57911d6
Merge remote-tracking branch 'upstream/main' into new_tests_files
May 1, 2025
104c2d2
Merge remote-tracking branch 'origin/main' into new_tests_files
May 1, 2025
b8f3036
Merge remote-tracking branch 'origin/main' into update_test_classes
May 1, 2025
ddf0363
Merge remote-tracking branch 'origin/main' into more_new_tests
May 1, 2025
6a8f0f2
Merge remote-tracking branch 'upstream/main'
May 8, 2025
8c04e16
Merge remote-tracking branch 'origin/main' into update_test_classes
May 8, 2025
623b2be
fix comments from Rodrigo
May 8, 2025
42adf60
Merge remote-tracking branch 'origin/main' into new_tests_files
May 8, 2025
f44d190
PR fixes
May 8, 2025
9122115
Merge remote-tracking branch 'origin/main' into more_new_tests
May 8, 2025
6d86d44
PR fixes
May 8, 2025
2a07cd0
change test file
May 13, 2025
a8af3d7
test fixes
May 15, 2025
bed0e7c
Merge remote-tracking branch 'upstream/main'
May 15, 2025
6191092
Merge remote-tracking branch 'origin/main' into update_test_classes
May 15, 2025
00e4425
Merge remote-tracking branch 'origin/main' into new_tests_files
May 15, 2025
a57c3f3
Merge remote-tracking branch 'origin/main' into more_new_tests
May 15, 2025
7966fc8
fix test
May 15, 2025
f0c2043
Merge pull request #4 from gaudyb/update_test_classes
gaudyb May 15, 2025
0ee07d1
Merge pull request #5 from gaudyb/new_tests_files
gaudyb May 15, 2025
194e191
Merge pull request #6 from gaudyb/more_new_tests
gaudyb May 15, 2025
395026a
Merge remote-tracking branch 'upstream/main'
May 15, 2025
965938e
Merge branch 'main' of https://github.com/gaudyb/semantic-kernel
May 15, 2025
c7b09ba
feedback implemented
May 15, 2025
8b29e7e
Merge branch 'main' into main
moonbox3 May 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
# 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,
Expand Down Expand Up @@ -260,3 +267,187 @@ async def test_prepare_chat_history_for_request(setup_ollama_chat_completion):

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
123 changes: 123 additions & 0 deletions python/tests/unit/processes/dapr_runtime/test_process_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

import asyncio
import json
from queue import Queue
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext

from semantic_kernel.exceptions.kernel_exceptions import KernelException
from semantic_kernel.exceptions.process_exceptions import ProcessEventUndefinedException
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.process_actor import ProcessActor
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.dapr_runtime.dapr_step_info import DaprStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState


Expand Down Expand Up @@ -158,3 +162,122 @@ def test_handle_message(actor_context):
asyncio.run(actor_context.handle_message(message_mock))

mock_run_once.assert_called_once()


@pytest.fixture
def actor() -> ProcessActor:
"""Create a fresh ProcessActor with mocked dependencies."""
# Arrange: Create a dummy runtime context and ProcessActor instance
actor_id = ActorId("test_actor")
runtime_context = MagicMock()
kernel = MagicMock()
actor_obj = ProcessActor(runtime_context, actor_id, kernel=kernel, factories={})
# Mock internal state manager
actor_obj._state_manager = AsyncMock()
actor_obj._state_manager.try_get_state = AsyncMock(return_value=(False, None))
actor_obj._state_manager.try_add_state = AsyncMock()
actor_obj._state_manager.save_state = AsyncMock()
return actor_obj


def test_name_uninitialized(actor: ProcessActor):
"""Test that accessing name before initialization raises KernelException."""
with pytest.raises(KernelException) as exc_info:
_ = actor.name
assert "must be initialized before accessing the name" in str(exc_info.value)


@pytest.mark.asyncio
async def test_start_not_initialized(actor: ProcessActor):
"""Test that start() without initialization raises ValueError."""
actor.initialize_task = False
with pytest.raises(ValueError):
await actor.start()


@pytest.mark.asyncio
async def test_run_once_none_event(actor: ProcessActor):
"""Test that run_once(None) raises ProcessEventUndefinedException."""
actor.initialize_task = True
with pytest.raises(ProcessEventUndefinedException):
await actor.run_once(None) # type: ignore


@pytest.mark.asyncio
async def test_send_message_none_event(actor: ProcessActor):
"""Test that send_message(None) raises ProcessEventUndefinedException."""
with pytest.raises(ProcessEventUndefinedException):
await actor.send_message(None) # type: ignore


def test_send_message_success(actor: ProcessActor):
"""Test that send_message enqueues the event into external_event_queue."""
event = KernelProcessEvent(id="e1", data="d1")
asyncio.run(actor.send_message(event))
assert isinstance(actor.external_event_queue, Queue)
assert not actor.external_event_queue.empty()
queued = actor.external_event_queue.get()
assert queued is event


@pytest.mark.asyncio
async def test_to_dapr_process_info_uninitialized(actor: ProcessActor):
"""Test to_dapr_process_info raises ValueError if process is None."""
actor.process = None
with pytest.raises(ValueError) as exc:
await actor.to_dapr_process_info()
assert "must be initialized before converting" in str(exc.value)


@pytest.mark.asyncio
async def test_to_dapr_process_info_inner_step_type_none(actor: ProcessActor):
"""Test to_dapr_process_info raises ValueError if inner_step_python_type is None."""
actor.process = MagicMock()
# Simulate a process with missing inner_step_python_type
actor.process.inner_step_python_type = None
actor.process.state = MagicMock(name="Proc", id="pid")
actor.steps = []
with pytest.raises(ValueError) as exc:
await actor.to_dapr_process_info()
assert "inner step type must be defined" in str(exc.value)


@pytest.mark.asyncio
async def test_to_dapr_process_info_success(actor: ProcessActor):
"""Test to_dapr_process_info returns correct dict for initialized process with no steps."""
proc_state = KernelProcessState(name="Proc", version="1.0", id="test_actor")
dapr_proc = DaprProcessInfo(
inner_step_python_type="Type1",
state=proc_state,
edges={},
steps=[],
)
actor.process = dapr_proc
actor.steps = []
result = await actor.to_dapr_process_info()
assert result == dapr_proc.model_dump()


@pytest.mark.asyncio
async def test_stop_no_task(actor: ProcessActor):
"""Test stop() returns normally when no process_task is running."""
actor.process_task = None
await actor.stop()


def test_name_after_manual_set(actor: ProcessActor):
"""Test that name property returns the correct name after manual initialization."""
actor.process = MagicMock()
actor.process.state = MagicMock()
actor.process.state.name = "MyProcess"
actor.process.state.id = "id123"
assert actor.name == "MyProcess"


@pytest.mark.asyncio
async def test_send_outgoing_public_events_no_parent(actor: ProcessActor):
"""Test send_outgoing_public_events does nothing if parent_process_id is None."""
actor.parent_process_id = None
with patch("semantic_kernel.processes.dapr_runtime.actors.process_actor.ActorProxy.create") as mock_proxy:
await actor.send_outgoing_public_events()
mock_proxy.assert_not_called()
Loading
Loading