mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
feat: add new callbacks to handle tool and model errors
This CL add new callbacks in plugin system: - `on_tool_error_callback` - `on_model_error_callback` This allow the user to create plugins that can handle errors. PiperOrigin-RevId: 786469646
This commit is contained in:
committed by
Copybara-Service
parent
dfc25c17a9
commit
00afaaf2fc
@@ -534,7 +534,13 @@ class BaseLlmFlow(ABC):
|
|||||||
with tracer.start_as_current_span('call_llm'):
|
with tracer.start_as_current_span('call_llm'):
|
||||||
if invocation_context.run_config.support_cfc:
|
if invocation_context.run_config.support_cfc:
|
||||||
invocation_context.live_request_queue = LiveRequestQueue()
|
invocation_context.live_request_queue = LiveRequestQueue()
|
||||||
async for llm_response in self.run_live(invocation_context):
|
responses_generator = self.run_live(invocation_context)
|
||||||
|
async for llm_response in self._run_and_handle_error(
|
||||||
|
responses_generator,
|
||||||
|
invocation_context,
|
||||||
|
llm_request,
|
||||||
|
model_response_event,
|
||||||
|
):
|
||||||
# Runs after_model_callback if it exists.
|
# Runs after_model_callback if it exists.
|
||||||
if altered_llm_response := await self._handle_after_model_callback(
|
if altered_llm_response := await self._handle_after_model_callback(
|
||||||
invocation_context, llm_response, model_response_event
|
invocation_context, llm_response, model_response_event
|
||||||
@@ -553,10 +559,16 @@ class BaseLlmFlow(ABC):
|
|||||||
# the counter beyond the max set value, then the execution is stopped
|
# the counter beyond the max set value, then the execution is stopped
|
||||||
# right here, and exception is thrown.
|
# right here, and exception is thrown.
|
||||||
invocation_context.increment_llm_call_count()
|
invocation_context.increment_llm_call_count()
|
||||||
async for llm_response in llm.generate_content_async(
|
responses_generator = llm.generate_content_async(
|
||||||
llm_request,
|
llm_request,
|
||||||
stream=invocation_context.run_config.streaming_mode
|
stream=invocation_context.run_config.streaming_mode
|
||||||
== StreamingMode.SSE,
|
== StreamingMode.SSE,
|
||||||
|
)
|
||||||
|
async for llm_response in self._run_and_handle_error(
|
||||||
|
responses_generator,
|
||||||
|
invocation_context,
|
||||||
|
llm_request,
|
||||||
|
model_response_event,
|
||||||
):
|
):
|
||||||
trace_call_llm(
|
trace_call_llm(
|
||||||
invocation_context,
|
invocation_context,
|
||||||
@@ -673,6 +685,43 @@ class BaseLlmFlow(ABC):
|
|||||||
|
|
||||||
return model_response_event
|
return model_response_event
|
||||||
|
|
||||||
|
async def _run_and_handle_error(
|
||||||
|
self,
|
||||||
|
response_generator: AsyncGenerator[LlmResponse, None],
|
||||||
|
invocation_context: InvocationContext,
|
||||||
|
llm_request: LlmRequest,
|
||||||
|
model_response_event: Event,
|
||||||
|
) -> AsyncGenerator[LlmResponse, None]:
|
||||||
|
"""Runs the response generator and processes the error with plugins.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response_generator: The response generator to run.
|
||||||
|
invocation_context: The invocation context.
|
||||||
|
llm_request: The LLM request.
|
||||||
|
model_response_event: The model response event.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
A generator of LlmResponse.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async for response in response_generator:
|
||||||
|
yield response
|
||||||
|
except Exception as model_error:
|
||||||
|
callback_context = CallbackContext(
|
||||||
|
invocation_context, event_actions=model_response_event.actions
|
||||||
|
)
|
||||||
|
error_response = (
|
||||||
|
await invocation_context.plugin_manager.run_on_model_error_callback(
|
||||||
|
callback_context=callback_context,
|
||||||
|
llm_request=llm_request,
|
||||||
|
error=model_error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
yield error_response
|
||||||
|
else:
|
||||||
|
raise model_error
|
||||||
|
|
||||||
def __get_llm(self, invocation_context: InvocationContext) -> BaseLlm:
|
def __get_llm(self, invocation_context: InvocationContext) -> BaseLlm:
|
||||||
from ...agents.llm_agent import LlmAgent
|
from ...agents.llm_agent import LlmAgent
|
||||||
|
|
||||||
|
|||||||
@@ -176,9 +176,21 @@ async def handle_function_calls_async(
|
|||||||
|
|
||||||
# Step 3: Otherwise, proceed calling the tool normally.
|
# Step 3: Otherwise, proceed calling the tool normally.
|
||||||
if function_response is None:
|
if function_response is None:
|
||||||
function_response = await __call_tool_async(
|
try:
|
||||||
tool, args=function_args, tool_context=tool_context
|
function_response = await __call_tool_async(
|
||||||
)
|
tool, args=function_args, tool_context=tool_context
|
||||||
|
)
|
||||||
|
except Exception as tool_error:
|
||||||
|
error_response = await invocation_context.plugin_manager.run_on_tool_error_callback(
|
||||||
|
tool=tool,
|
||||||
|
tool_args=function_args,
|
||||||
|
tool_context=tool_context,
|
||||||
|
error=tool_error,
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
function_response = error_response
|
||||||
|
else:
|
||||||
|
raise tool_error
|
||||||
|
|
||||||
# Step 4: Check if plugin after_tool_callback overrides the function
|
# Step 4: Check if plugin after_tool_callback overrides the function
|
||||||
# response.
|
# response.
|
||||||
|
|||||||
@@ -265,6 +265,31 @@ class BasePlugin(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def on_model_error_callback(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
callback_context: CallbackContext,
|
||||||
|
llm_request: LlmRequest,
|
||||||
|
error: Exception,
|
||||||
|
) -> Optional[LlmResponse]:
|
||||||
|
"""Callback executed when a model call encounters an error.
|
||||||
|
|
||||||
|
This callback provides an opportunity to handle model errors gracefully,
|
||||||
|
potentially providing alternative responses or recovery mechanisms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback_context: The context for the current agent call.
|
||||||
|
llm_request: The request that was sent to the model when the error
|
||||||
|
occurred.
|
||||||
|
error: The exception that was raised during model execution.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An optional LlmResponse. If an LlmResponse is returned, it will be used
|
||||||
|
instead of propagating the error. Returning `None` allows the original
|
||||||
|
error to be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
async def before_tool_callback(
|
async def before_tool_callback(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -315,3 +340,29 @@ class BasePlugin(ABC):
|
|||||||
result.
|
result.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def on_tool_error_callback(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tool: BaseTool,
|
||||||
|
tool_args: dict[str, Any],
|
||||||
|
tool_context: ToolContext,
|
||||||
|
error: Exception,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""Callback executed when a tool call encounters an error.
|
||||||
|
|
||||||
|
This callback provides an opportunity to handle tool errors gracefully,
|
||||||
|
potentially providing alternative responses or recovery mechanisms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool: The tool instance that encountered an error.
|
||||||
|
tool_args: The arguments that were passed to the tool.
|
||||||
|
tool_context: The context specific to the tool execution.
|
||||||
|
error: The exception that was raised during tool execution.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An optional dictionary. If a dictionary is returned, it will be used as
|
||||||
|
the tool response instead of propagating the error. Returning `None`
|
||||||
|
allows the original error to be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ PluginCallbackName = Literal[
|
|||||||
"after_tool_callback",
|
"after_tool_callback",
|
||||||
"before_model_callback",
|
"before_model_callback",
|
||||||
"after_model_callback",
|
"after_model_callback",
|
||||||
|
"on_tool_error_callback",
|
||||||
|
"on_model_error_callback",
|
||||||
]
|
]
|
||||||
|
|
||||||
logger = logging.getLogger("google_adk." + __name__)
|
logger = logging.getLogger("google_adk." + __name__)
|
||||||
@@ -195,6 +197,21 @@ class PluginManager:
|
|||||||
result=result,
|
result=result,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def run_on_model_error_callback(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
callback_context: CallbackContext,
|
||||||
|
llm_request: LlmRequest,
|
||||||
|
error: Exception,
|
||||||
|
) -> Optional[LlmResponse]:
|
||||||
|
"""Runs the `on_model_error_callback` for all plugins."""
|
||||||
|
return await self._run_callbacks(
|
||||||
|
"on_model_error_callback",
|
||||||
|
callback_context=callback_context,
|
||||||
|
llm_request=llm_request,
|
||||||
|
error=error,
|
||||||
|
)
|
||||||
|
|
||||||
async def run_before_model_callback(
|
async def run_before_model_callback(
|
||||||
self, *, callback_context: CallbackContext, llm_request: LlmRequest
|
self, *, callback_context: CallbackContext, llm_request: LlmRequest
|
||||||
) -> Optional[LlmResponse]:
|
) -> Optional[LlmResponse]:
|
||||||
@@ -215,6 +232,23 @@ class PluginManager:
|
|||||||
llm_response=llm_response,
|
llm_response=llm_response,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def run_on_tool_error_callback(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tool: BaseTool,
|
||||||
|
tool_args: dict[str, Any],
|
||||||
|
tool_context: ToolContext,
|
||||||
|
error: Exception,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""Runs the `on_tool_error_callback` for all plugins."""
|
||||||
|
return await self._run_callbacks(
|
||||||
|
"on_tool_error_callback",
|
||||||
|
tool=tool,
|
||||||
|
tool_args=tool_args,
|
||||||
|
tool_context=tool_context,
|
||||||
|
error=error,
|
||||||
|
)
|
||||||
|
|
||||||
async def _run_callbacks(
|
async def _run_callbacks(
|
||||||
self, callback_name: PluginCallbackName, **kwargs: Any
|
self, callback_name: PluginCallbackName, **kwargs: Any
|
||||||
) -> Optional[Any]:
|
) -> Optional[Any]:
|
||||||
|
|||||||
@@ -20,19 +20,33 @@ from google.adk.models.llm_request import LlmRequest
|
|||||||
from google.adk.models.llm_response import LlmResponse
|
from google.adk.models.llm_response import LlmResponse
|
||||||
from google.adk.plugins.base_plugin import BasePlugin
|
from google.adk.plugins.base_plugin import BasePlugin
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
from google.genai.errors import ClientError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ... import testing_utils
|
from ... import testing_utils
|
||||||
|
|
||||||
|
mock_error = ClientError(
|
||||||
|
code=429,
|
||||||
|
response_json={
|
||||||
|
'error': {
|
||||||
|
'code': 429,
|
||||||
|
'message': 'Quota exceeded.',
|
||||||
|
'status': 'RESOURCE_EXHAUSTED',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MockPlugin(BasePlugin):
|
class MockPlugin(BasePlugin):
|
||||||
before_model_text = 'before_model_text from MockPlugin'
|
before_model_text = 'before_model_text from MockPlugin'
|
||||||
after_model_text = 'after_model_text from MockPlugin'
|
after_model_text = 'after_model_text from MockPlugin'
|
||||||
|
on_model_error_text = 'on_model_error_text from MockPlugin'
|
||||||
|
|
||||||
def __init__(self, name='mock_plugin'):
|
def __init__(self, name='mock_plugin'):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.enable_before_model_callback = False
|
self.enable_before_model_callback = False
|
||||||
self.enable_after_model_callback = False
|
self.enable_after_model_callback = False
|
||||||
|
self.enable_on_model_error_callback = False
|
||||||
self.before_model_response = LlmResponse(
|
self.before_model_response = LlmResponse(
|
||||||
content=testing_utils.ModelContent(
|
content=testing_utils.ModelContent(
|
||||||
[types.Part.from_text(text=self.before_model_text)]
|
[types.Part.from_text(text=self.before_model_text)]
|
||||||
@@ -43,6 +57,11 @@ class MockPlugin(BasePlugin):
|
|||||||
[types.Part.from_text(text=self.after_model_text)]
|
[types.Part.from_text(text=self.after_model_text)]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.on_model_error_response = LlmResponse(
|
||||||
|
content=testing_utils.ModelContent(
|
||||||
|
[types.Part.from_text(text=self.on_model_error_text)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def before_model_callback(
|
async def before_model_callback(
|
||||||
self, *, callback_context: CallbackContext, llm_request: LlmRequest
|
self, *, callback_context: CallbackContext, llm_request: LlmRequest
|
||||||
@@ -58,6 +77,17 @@ class MockPlugin(BasePlugin):
|
|||||||
return None
|
return None
|
||||||
return self.after_model_response
|
return self.after_model_response
|
||||||
|
|
||||||
|
async def on_model_error_callback(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
callback_context: CallbackContext,
|
||||||
|
llm_request: LlmRequest,
|
||||||
|
error: Exception,
|
||||||
|
) -> Optional[LlmResponse]:
|
||||||
|
if not self.enable_on_model_error_callback:
|
||||||
|
return None
|
||||||
|
return self.on_model_error_response
|
||||||
|
|
||||||
|
|
||||||
CANONICAL_MODEL_CALLBACK_CONTENT = 'canonical_model_callback_content'
|
CANONICAL_MODEL_CALLBACK_CONTENT = 'canonical_model_callback_content'
|
||||||
|
|
||||||
@@ -124,5 +154,36 @@ def test_before_model_callback_fallback_model(mock_plugin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_model_error_callback_with_plugin(mock_plugin):
|
||||||
|
"""Tests that the model error is handled by the plugin."""
|
||||||
|
mock_model = testing_utils.MockModel.create(error=mock_error, responses=[])
|
||||||
|
mock_plugin.enable_on_model_error_callback = True
|
||||||
|
agent = Agent(
|
||||||
|
name='root_agent',
|
||||||
|
model=mock_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = testing_utils.InMemoryRunner(agent, plugins=[mock_plugin])
|
||||||
|
|
||||||
|
assert testing_utils.simplify_events(runner.run('test')) == [
|
||||||
|
('root_agent', mock_plugin.on_model_error_text),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_model_error_callback_fallback_to_runner(mock_plugin):
|
||||||
|
"""Tests that the model error is not handled and falls back to raise from runner."""
|
||||||
|
mock_model = testing_utils.MockModel.create(error=mock_error, responses=[])
|
||||||
|
mock_plugin.enable_on_model_error_callback = False
|
||||||
|
agent = Agent(
|
||||||
|
name='root_agent',
|
||||||
|
model=mock_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
testing_utils.InMemoryRunner(agent, plugins=[mock_plugin])
|
||||||
|
except Exception as e:
|
||||||
|
assert e == mock_error
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
pytest.main([__file__])
|
pytest.main([__file__])
|
||||||
|
|||||||
@@ -24,19 +24,35 @@ from google.adk.tools.base_tool import BaseTool
|
|||||||
from google.adk.tools.function_tool import FunctionTool
|
from google.adk.tools.function_tool import FunctionTool
|
||||||
from google.adk.tools.tool_context import ToolContext
|
from google.adk.tools.tool_context import ToolContext
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
from google.genai.errors import ClientError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ... import testing_utils
|
from ... import testing_utils
|
||||||
|
|
||||||
|
mock_error = ClientError(
|
||||||
|
code=429,
|
||||||
|
response_json={
|
||||||
|
"error": {
|
||||||
|
"code": 429,
|
||||||
|
"message": "Quota exceeded.",
|
||||||
|
"status": "RESOURCE_EXHAUSTED",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MockPlugin(BasePlugin):
|
class MockPlugin(BasePlugin):
|
||||||
before_tool_response = {"MockPlugin": "before_tool_response from MockPlugin"}
|
before_tool_response = {"MockPlugin": "before_tool_response from MockPlugin"}
|
||||||
after_tool_response = {"MockPlugin": "after_tool_response from MockPlugin"}
|
after_tool_response = {"MockPlugin": "after_tool_response from MockPlugin"}
|
||||||
|
on_tool_error_response = {
|
||||||
|
"MockPlugin": "on_tool_error_response from MockPlugin"
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, name="mock_plugin"):
|
def __init__(self, name="mock_plugin"):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.enable_before_tool_callback = False
|
self.enable_before_tool_callback = False
|
||||||
self.enable_after_tool_callback = False
|
self.enable_after_tool_callback = False
|
||||||
|
self.enable_on_tool_error_callback = False
|
||||||
|
|
||||||
async def before_tool_callback(
|
async def before_tool_callback(
|
||||||
self,
|
self,
|
||||||
@@ -61,6 +77,18 @@ class MockPlugin(BasePlugin):
|
|||||||
return None
|
return None
|
||||||
return self.after_tool_response
|
return self.after_tool_response
|
||||||
|
|
||||||
|
async def on_tool_error_callback(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tool: BaseTool,
|
||||||
|
tool_args: dict[str, Any],
|
||||||
|
tool_context: ToolContext,
|
||||||
|
error: Exception,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
if not self.enable_on_tool_error_callback:
|
||||||
|
return None
|
||||||
|
return self.on_tool_error_response
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_tool():
|
def mock_tool():
|
||||||
@@ -70,6 +98,14 @@ def mock_tool():
|
|||||||
return FunctionTool(simple_fn)
|
return FunctionTool(simple_fn)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_error_tool():
|
||||||
|
def raise_error_fn(**kwargs) -> Dict[str, Any]:
|
||||||
|
raise mock_error
|
||||||
|
|
||||||
|
return FunctionTool(raise_error_fn)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_plugin():
|
def mock_plugin():
|
||||||
return MockPlugin()
|
return MockPlugin()
|
||||||
@@ -124,5 +160,30 @@ async def test_async_after_tool_callback(mock_tool, mock_plugin):
|
|||||||
assert part.function_response.response == mock_plugin.after_tool_response
|
assert part.function_response.response == mock_plugin.after_tool_response
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_on_tool_error_use_plugin_response(
|
||||||
|
mock_error_tool, mock_plugin
|
||||||
|
):
|
||||||
|
mock_plugin.enable_on_tool_error_callback = True
|
||||||
|
|
||||||
|
result_event = await invoke_tool_with_plugin(mock_error_tool, mock_plugin)
|
||||||
|
|
||||||
|
assert result_event is not None
|
||||||
|
part = result_event.content.parts[0]
|
||||||
|
assert part.function_response.response == mock_plugin.on_tool_error_response
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_on_tool_error_fallback_to_runner(
|
||||||
|
mock_error_tool, mock_plugin
|
||||||
|
):
|
||||||
|
mock_plugin.enable_on_tool_error_callback = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
await invoke_tool_with_plugin(mock_error_tool, mock_plugin)
|
||||||
|
except Exception as e:
|
||||||
|
assert e == mock_error
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pytest.main([__file__])
|
pytest.main([__file__])
|
||||||
|
|||||||
@@ -67,12 +67,18 @@ class FullOverridePlugin(BasePlugin):
|
|||||||
async def after_tool_callback(self, **kwargs) -> str:
|
async def after_tool_callback(self, **kwargs) -> str:
|
||||||
return "overridden_after_tool"
|
return "overridden_after_tool"
|
||||||
|
|
||||||
|
async def on_tool_error_callback(self, **kwargs) -> str:
|
||||||
|
return "overridden_on_tool_error"
|
||||||
|
|
||||||
async def before_model_callback(self, **kwargs) -> str:
|
async def before_model_callback(self, **kwargs) -> str:
|
||||||
return "overridden_before_model"
|
return "overridden_before_model"
|
||||||
|
|
||||||
async def after_model_callback(self, **kwargs) -> str:
|
async def after_model_callback(self, **kwargs) -> str:
|
||||||
return "overridden_after_model"
|
return "overridden_after_model"
|
||||||
|
|
||||||
|
async def on_model_error_callback(self, **kwargs) -> str:
|
||||||
|
return "overridden_on_model_error"
|
||||||
|
|
||||||
|
|
||||||
def test_base_plugin_initialization():
|
def test_base_plugin_initialization():
|
||||||
"""Tests that a plugin is initialized with the correct name."""
|
"""Tests that a plugin is initialized with the correct name."""
|
||||||
@@ -137,6 +143,15 @@ async def test_base_plugin_default_callbacks_return_none():
|
|||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
|
assert (
|
||||||
|
await plugin.on_tool_error_callback(
|
||||||
|
tool=mock_context,
|
||||||
|
tool_args={},
|
||||||
|
tool_context=mock_context,
|
||||||
|
error=Exception(),
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
await plugin.before_model_callback(
|
await plugin.before_model_callback(
|
||||||
callback_context=mock_context, llm_request=mock_context
|
callback_context=mock_context, llm_request=mock_context
|
||||||
@@ -149,6 +164,14 @@ async def test_base_plugin_default_callbacks_return_none():
|
|||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
|
assert (
|
||||||
|
await plugin.on_model_error_callback(
|
||||||
|
callback_context=mock_context,
|
||||||
|
llm_request=mock_context,
|
||||||
|
error=Exception(),
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -170,6 +193,7 @@ async def test_base_plugin_all_callbacks_can_be_overridden():
|
|||||||
mock_llm_request = Mock(spec=LlmRequest)
|
mock_llm_request = Mock(spec=LlmRequest)
|
||||||
mock_llm_response = Mock(spec=LlmResponse)
|
mock_llm_response = Mock(spec=LlmResponse)
|
||||||
mock_event = Mock(spec=Event)
|
mock_event = Mock(spec=Event)
|
||||||
|
mock_error = Mock(spec=Exception)
|
||||||
|
|
||||||
# Call each method and assert it returns the unique string from the override.
|
# Call each method and assert it returns the unique string from the override.
|
||||||
# This proves that the subclass's method was executed.
|
# This proves that the subclass's method was executed.
|
||||||
@@ -237,3 +261,20 @@ async def test_base_plugin_all_callbacks_can_be_overridden():
|
|||||||
)
|
)
|
||||||
== "overridden_after_tool"
|
== "overridden_after_tool"
|
||||||
)
|
)
|
||||||
|
assert (
|
||||||
|
await plugin.on_tool_error_callback(
|
||||||
|
tool=mock_tool,
|
||||||
|
tool_args={},
|
||||||
|
tool_context=mock_tool_context,
|
||||||
|
error=mock_error,
|
||||||
|
)
|
||||||
|
== "overridden_on_tool_error"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
await plugin.on_model_error_callback(
|
||||||
|
callback_context=mock_callback_context,
|
||||||
|
llm_request=mock_llm_request,
|
||||||
|
error=mock_error,
|
||||||
|
)
|
||||||
|
== "overridden_on_model_error"
|
||||||
|
)
|
||||||
|
|||||||
@@ -77,12 +77,18 @@ class TestPlugin(BasePlugin):
|
|||||||
async def after_tool_callback(self, **kwargs):
|
async def after_tool_callback(self, **kwargs):
|
||||||
return await self._handle_callback("after_tool_callback")
|
return await self._handle_callback("after_tool_callback")
|
||||||
|
|
||||||
|
async def on_tool_error_callback(self, **kwargs):
|
||||||
|
return await self._handle_callback("on_tool_error_callback")
|
||||||
|
|
||||||
async def before_model_callback(self, **kwargs):
|
async def before_model_callback(self, **kwargs):
|
||||||
return await self._handle_callback("before_model_callback")
|
return await self._handle_callback("before_model_callback")
|
||||||
|
|
||||||
async def after_model_callback(self, **kwargs):
|
async def after_model_callback(self, **kwargs):
|
||||||
return await self._handle_callback("after_model_callback")
|
return await self._handle_callback("after_model_callback")
|
||||||
|
|
||||||
|
async def on_model_error_callback(self, **kwargs):
|
||||||
|
return await self._handle_callback("on_model_error_callback")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def service() -> PluginManager:
|
def service() -> PluginManager:
|
||||||
@@ -227,12 +233,23 @@ async def test_all_callbacks_are_supported(
|
|||||||
await service.run_after_tool_callback(
|
await service.run_after_tool_callback(
|
||||||
tool=mock_context, tool_args={}, tool_context=mock_context, result={}
|
tool=mock_context, tool_args={}, tool_context=mock_context, result={}
|
||||||
)
|
)
|
||||||
|
await service.run_on_tool_error_callback(
|
||||||
|
tool=mock_context,
|
||||||
|
tool_args={},
|
||||||
|
tool_context=mock_context,
|
||||||
|
error=mock_context,
|
||||||
|
)
|
||||||
await service.run_before_model_callback(
|
await service.run_before_model_callback(
|
||||||
callback_context=mock_context, llm_request=mock_context
|
callback_context=mock_context, llm_request=mock_context
|
||||||
)
|
)
|
||||||
await service.run_after_model_callback(
|
await service.run_after_model_callback(
|
||||||
callback_context=mock_context, llm_response=mock_context
|
callback_context=mock_context, llm_response=mock_context
|
||||||
)
|
)
|
||||||
|
await service.run_on_model_error_callback(
|
||||||
|
callback_context=mock_context,
|
||||||
|
llm_request=mock_context,
|
||||||
|
error=mock_context,
|
||||||
|
)
|
||||||
|
|
||||||
# Verify all callbacks were logged
|
# Verify all callbacks were logged
|
||||||
expected_callbacks = [
|
expected_callbacks = [
|
||||||
@@ -244,7 +261,9 @@ async def test_all_callbacks_are_supported(
|
|||||||
"after_agent_callback",
|
"after_agent_callback",
|
||||||
"before_tool_callback",
|
"before_tool_callback",
|
||||||
"after_tool_callback",
|
"after_tool_callback",
|
||||||
|
"on_tool_error_callback",
|
||||||
"before_model_callback",
|
"before_model_callback",
|
||||||
"after_model_callback",
|
"after_model_callback",
|
||||||
|
"on_model_error_callback",
|
||||||
]
|
]
|
||||||
assert set(plugin1.call_log) == set(expected_callbacks)
|
assert set(plugin1.call_log) == set(expected_callbacks)
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ class MockModel(BaseLlm):
|
|||||||
|
|
||||||
requests: list[LlmRequest] = []
|
requests: list[LlmRequest] = []
|
||||||
responses: list[LlmResponse]
|
responses: list[LlmResponse]
|
||||||
|
error: Union[Exception, None] = None
|
||||||
response_index: int = -1
|
response_index: int = -1
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -255,7 +256,10 @@ class MockModel(BaseLlm):
|
|||||||
responses: Union[
|
responses: Union[
|
||||||
list[types.Part], list[LlmResponse], list[str], list[list[types.Part]]
|
list[types.Part], list[LlmResponse], list[str], list[list[types.Part]]
|
||||||
],
|
],
|
||||||
|
error: Union[Exception, None] = None,
|
||||||
):
|
):
|
||||||
|
if error and not responses:
|
||||||
|
return cls(responses=[], error=error)
|
||||||
if not responses:
|
if not responses:
|
||||||
return cls(responses=[])
|
return cls(responses=[])
|
||||||
elif isinstance(responses[0], LlmResponse):
|
elif isinstance(responses[0], LlmResponse):
|
||||||
@@ -285,6 +289,8 @@ class MockModel(BaseLlm):
|
|||||||
def generate_content(
|
def generate_content(
|
||||||
self, llm_request: LlmRequest, stream: bool = False
|
self, llm_request: LlmRequest, stream: bool = False
|
||||||
) -> Generator[LlmResponse, None, None]:
|
) -> Generator[LlmResponse, None, None]:
|
||||||
|
if self.error:
|
||||||
|
raise self.error
|
||||||
# Increasement of the index has to happen before the yield.
|
# Increasement of the index has to happen before the yield.
|
||||||
self.response_index += 1
|
self.response_index += 1
|
||||||
self.requests.append(llm_request)
|
self.requests.append(llm_request)
|
||||||
|
|||||||
Reference in New Issue
Block a user