208 lines
7.3 KiB
Python
208 lines
7.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Unit tests for AgentScopeLLM with Ollama using asyncio.gather() for
|
|
parallel calls."""
|
|
import asyncio
|
|
from typing import Any
|
|
from unittest.async_case import IsolatedAsyncioTestCase
|
|
from unittest.mock import patch, AsyncMock, MagicMock
|
|
|
|
from agentscope.memory._long_term_memory._mem0._mem0_utils import AgentScopeLLM
|
|
from agentscope.model import OllamaChatModel
|
|
|
|
# Try to import BaseLlmConfig, but handle ImportError gracefully
|
|
try:
|
|
from mem0.configs.llms.base import BaseLlmConfig
|
|
except ImportError:
|
|
# If mem0 is not installed, create a mock class
|
|
BaseLlmConfig = MagicMock
|
|
|
|
|
|
class OllamaMessageMock:
|
|
"""Mock class for Ollama message objects."""
|
|
|
|
def __init__(
|
|
self,
|
|
content: str = "",
|
|
thinking: str = "",
|
|
tool_calls: list = None,
|
|
) -> None:
|
|
self.content = content
|
|
self.thinking = thinking
|
|
self.tool_calls = tool_calls or []
|
|
|
|
|
|
class OllamaResponseMock:
|
|
"""Mock class for Ollama response objects."""
|
|
|
|
def __init__(
|
|
self,
|
|
content: str = "",
|
|
thinking: str = "",
|
|
tool_calls: list = None,
|
|
prompt_eval_count: int = 10,
|
|
eval_count: int = 20,
|
|
) -> None:
|
|
self.message = OllamaMessageMock(
|
|
content=content,
|
|
thinking=thinking,
|
|
tool_calls=tool_calls or [],
|
|
)
|
|
self.prompt_eval_count = prompt_eval_count
|
|
self.eval_count = eval_count
|
|
|
|
def get(self, key: str, default: Any | None = None) -> Any:
|
|
"""Mock dict-like get method."""
|
|
return getattr(self, key, default)
|
|
|
|
def __contains__(self, key: str) -> bool:
|
|
"""Mock dict-like contains method to support 'in' operator."""
|
|
return hasattr(self, key)
|
|
|
|
|
|
class TestAgentScopeLLMWithOllama(IsolatedAsyncioTestCase):
|
|
"""Test cases for AgentScopeLLM with
|
|
OllamaChatModel using asyncio.gather()."""
|
|
|
|
def test_agentscope_llm_parallel_calls_with_asyncio_gather(self) -> None:
|
|
"""Test parallel calls using asyncio.gather() - original bug scenario.
|
|
|
|
This test reproduces the original bug where parallel calls using
|
|
asyncio.gather() would cause "Event loop is closed" errors.
|
|
The persistent event loop management fix should resolve this issue.
|
|
"""
|
|
with patch("ollama.AsyncClient") as mock_client_class:
|
|
mock_client = AsyncMock()
|
|
mock_client_class.return_value = mock_client
|
|
|
|
# Create OllamaChatModel instance
|
|
ollama_model = OllamaChatModel(
|
|
model_name="qwen3:14b",
|
|
stream=False,
|
|
enable_thinking=False,
|
|
)
|
|
ollama_model.client = mock_client
|
|
|
|
# Mock Ollama chat response
|
|
mock_ollama_response = OllamaResponseMock(
|
|
content="Test response",
|
|
)
|
|
mock_client.chat = AsyncMock(
|
|
return_value=mock_ollama_response,
|
|
)
|
|
|
|
# Create AgentScopeLLM config
|
|
# Directly set model attribute, simpler than using
|
|
# LlmConfig constructor
|
|
llm_config = BaseLlmConfig()
|
|
llm_config.model = ollama_model
|
|
|
|
llm = AgentScopeLLM(config=llm_config)
|
|
|
|
# Create multiple different messages for parallel calls
|
|
messages_list = [
|
|
[{"role": "user", "content": "I like staying in homestays"}],
|
|
[{"role": "user", "content": "I prefer coffee over tea"}],
|
|
[{"role": "user", "content": "My favorite color is blue"}],
|
|
[{"role": "user", "content": "I work as a software engineer"}],
|
|
[
|
|
{
|
|
"role": "user",
|
|
"content": "I enjoy reading science fiction",
|
|
},
|
|
],
|
|
]
|
|
|
|
# Define async function to call generate_response
|
|
async def call_llm(messages: list[dict[str, str]]) -> str | dict:
|
|
"""Call LLM generate_response in async context."""
|
|
return llm.generate_response(messages)
|
|
|
|
# Use asyncio.gather() to make parallel calls
|
|
# Without the fix, this would fail with
|
|
# "Event loop is closed" error
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
try:
|
|
results = loop.run_until_complete(
|
|
asyncio.gather(
|
|
*[call_llm(msgs) for msgs in messages_list],
|
|
),
|
|
)
|
|
finally:
|
|
loop.close()
|
|
|
|
# Verify all parallel calls completed successfully
|
|
self.assertEqual(len(results), len(messages_list))
|
|
for result in results:
|
|
self.assertIsInstance(result, str)
|
|
self.assertGreater(len(result), 0)
|
|
|
|
# Verify Ollama client was called for each parallel request
|
|
self.assertEqual(
|
|
mock_client.chat.call_count,
|
|
len(messages_list),
|
|
)
|
|
|
|
async def test_agentscope_llm_async_gather_in_async_context(self) -> None:
|
|
"""Test asyncio.gather() in an async test context.
|
|
|
|
This test uses the async test framework to properly test
|
|
parallel calls using asyncio.gather().
|
|
"""
|
|
with patch("ollama.AsyncClient") as mock_client_class:
|
|
mock_client = AsyncMock()
|
|
mock_client_class.return_value = mock_client
|
|
|
|
# Create OllamaChatModel instance
|
|
ollama_model = OllamaChatModel(
|
|
model_name="qwen3:14b",
|
|
stream=False,
|
|
enable_thinking=False,
|
|
)
|
|
ollama_model.client = mock_client
|
|
|
|
# Mock Ollama chat response
|
|
mock_ollama_response = OllamaResponseMock(
|
|
content="Test response",
|
|
)
|
|
mock_client.chat = AsyncMock(
|
|
return_value=mock_ollama_response,
|
|
)
|
|
|
|
# Create AgentScopeLLM config
|
|
# Directly set model attribute, simpler than using
|
|
# LlmConfig constructor
|
|
llm_config = BaseLlmConfig()
|
|
llm_config.model = ollama_model
|
|
|
|
llm = AgentScopeLLM(config=llm_config)
|
|
|
|
messages_list = [
|
|
[{"role": "user", "content": "First message"}],
|
|
[{"role": "user", "content": "Second message"}],
|
|
[{"role": "user", "content": "Third message"}],
|
|
]
|
|
|
|
# Define async function to call generate_response
|
|
async def call_llm(messages: list[dict[str, str]]) -> str | dict:
|
|
"""Call LLM generate_response."""
|
|
return llm.generate_response(messages)
|
|
|
|
# Use asyncio.gather() to make parallel calls
|
|
# This is the exact scenario that was causing the bug
|
|
results = await asyncio.gather(
|
|
*[call_llm(msgs) for msgs in messages_list],
|
|
)
|
|
|
|
# Verify all parallel calls completed successfully
|
|
self.assertEqual(len(results), len(messages_list))
|
|
for result in results:
|
|
self.assertIsInstance(result, str)
|
|
self.assertGreater(len(result), 0)
|
|
|
|
# Verify Ollama client was called for each parallel request
|
|
self.assertEqual(
|
|
mock_client.chat.call_count,
|
|
len(messages_list),
|
|
)
|