153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import asyncio
|
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
from typing import Any, Generic, Protocol, TypeVar
|
|
|
|
from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread
|
|
from semantic_kernel.contents import ChatMessageContent
|
|
|
|
DEFAULT_MAX_ATTEMPTS = 3
|
|
DEFAULT_BACKOFF_SECONDS = 1
|
|
|
|
|
|
class ChatResponseProtocol(Protocol):
|
|
"""Represents a single response item returned by the agent."""
|
|
|
|
@property
|
|
def message(self) -> ChatMessageContent: ...
|
|
|
|
@property
|
|
def thread(self) -> AgentThread | None: ...
|
|
|
|
|
|
class ChatAgentProtocol(Protocol):
|
|
"""Protocol describing the common agent interface used by the tests."""
|
|
|
|
async def get_response(
|
|
self, messages: str | list[str] | None, thread: object | None = None
|
|
) -> ChatResponseProtocol: ...
|
|
|
|
def invoke(
|
|
self, messages: str | list[str] | None, thread: object | None = None
|
|
) -> AsyncIterator[ChatResponseProtocol]: ...
|
|
|
|
def invoke_stream(
|
|
self, messages: str | list[str] | None, thread: object | None = None
|
|
) -> AsyncIterator[ChatResponseProtocol]: ...
|
|
|
|
|
|
TAgent = TypeVar("TAgent", bound=ChatAgentProtocol)
|
|
|
|
|
|
async def run_with_retry(
|
|
coro: Callable[..., Awaitable[Any]],
|
|
*args,
|
|
attempts: int = DEFAULT_MAX_ATTEMPTS,
|
|
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
|
|
**kwargs,
|
|
) -> AgentResponseItem[ChatMessageContent]:
|
|
"""
|
|
Execute an async callable with retry/backoff logic.
|
|
|
|
Args:
|
|
coro: The async function to call
|
|
args: Positional args to pass to the function
|
|
attempts: How many times to attempt before giving up
|
|
backoff_seconds: The initial backoff in seconds, doubled after each failure
|
|
kwargs: Keyword args to pass to the function
|
|
|
|
Returns:
|
|
Whatever the async function returns
|
|
|
|
Raises:
|
|
Exception: If the function fails after the specified number of attempts
|
|
"""
|
|
delay = backoff_seconds
|
|
for attempt in range(1, attempts + 1):
|
|
try:
|
|
return await coro(*args, **kwargs)
|
|
except Exception:
|
|
if attempt == attempts:
|
|
raise
|
|
await asyncio.sleep(delay)
|
|
delay *= 2
|
|
raise RuntimeError("Unexpected error: run_with_retry exit.")
|
|
|
|
|
|
class AgentTestBase(Generic[TAgent]):
|
|
"""Common test base that wraps all agent invocation patterns with retry logic.
|
|
|
|
Each integration test can inherit from this or use its methods directly.
|
|
"""
|
|
|
|
async def get_response_with_retry(
|
|
self,
|
|
agent: Agent,
|
|
messages: str | list[str] | None,
|
|
thread: Any | None = None,
|
|
attempts: int = DEFAULT_MAX_ATTEMPTS,
|
|
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
|
|
) -> AgentResponseItem[ChatMessageContent]:
|
|
"""Wraps agent.get_response(...) in run_with_retry."""
|
|
return await run_with_retry(
|
|
agent.get_response, messages=messages, thread=thread, attempts=attempts, backoff_seconds=backoff_seconds
|
|
)
|
|
|
|
async def get_invoke_with_retry(
|
|
self,
|
|
agent: Any,
|
|
messages: str | list[str] | None,
|
|
thread: Any | None = None,
|
|
attempts: int = DEFAULT_MAX_ATTEMPTS,
|
|
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
|
|
) -> list[AgentResponseItem[ChatMessageContent]]:
|
|
"""Wraps agent.invoke(...) in run_with_retry.
|
|
|
|
Collects generator results in a list before returning them.
|
|
"""
|
|
return await run_with_retry(
|
|
self._collect_from_invoke,
|
|
agent,
|
|
messages,
|
|
thread=thread,
|
|
attempts=attempts,
|
|
backoff_seconds=backoff_seconds,
|
|
)
|
|
|
|
async def get_invoke_stream_with_retry(
|
|
self,
|
|
agent: Any,
|
|
messages: str | list[str] | None,
|
|
thread: Any | None = None,
|
|
attempts: int = DEFAULT_MAX_ATTEMPTS,
|
|
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
|
|
) -> list[AgentResponseItem[ChatMessageContent]]:
|
|
"""Wraps agent.invoke_stream(...) in run_with_retry.
|
|
|
|
Collects streaming results in a list before returning them."""
|
|
return await run_with_retry(
|
|
self._collect_from_invoke_stream,
|
|
agent,
|
|
messages,
|
|
thread=thread,
|
|
attempts=attempts,
|
|
backoff_seconds=backoff_seconds,
|
|
)
|
|
|
|
async def _collect_from_invoke(
|
|
self, agent: Agent, messages: str | list[str] | None, thread: Any | None = None
|
|
) -> list[AgentResponseItem[ChatMessageContent]]:
|
|
results: list[AgentResponseItem[ChatMessageContent]] = []
|
|
async for response in agent.invoke(messages=messages, thread=thread):
|
|
results.append(response)
|
|
return results
|
|
|
|
async def _collect_from_invoke_stream(
|
|
self, agent: Agent, messages: str | list[str] | None, thread: Any | None = None
|
|
) -> list[AgentResponseItem[ChatMessageContent]]:
|
|
results: list[AgentResponseItem[ChatMessageContent]] = []
|
|
async for response in agent.invoke_stream(messages=messages, thread=thread):
|
|
results.append(response)
|
|
return results
|