CLI Engine Env passing and Tool Executor cleanup (#95)
This PR introduces the following changes: - **Engine Environment Configuration**: Adds support for specifying an environment variables file for the engine via the `arcade dev` CLI command. - **Configuration File Handling**: Refactors configuration file handling in the CLI launcher to generalize logic for locating configuration files. - **Tool Execution Logging**: Enhances logging in `BaseActor` to include execution duration and adjusts logging levels for better visibility. - **Enhanced Tool Exception Handling**: Improves exception handling in `ToolExecutor` and updates the `@tool` decorator to ensure proper propagation and handling of exceptions raised during tool execution.
This commit is contained in:
parent
7f13eb5efb
commit
68a4caff98
10 changed files with 314 additions and 68 deletions
|
|
@ -137,22 +137,29 @@ class BaseActor(Actor):
|
|||
**tool_request.inputs or {},
|
||||
)
|
||||
|
||||
end_time = time.time() # End time in seconds
|
||||
duration_ms = (end_time - start_time) * 1000 # Convert to milliseconds
|
||||
|
||||
if output.error:
|
||||
logger.warning(
|
||||
f"{invocation_id} | Tool {tool_fqname} version {tool_request.tool.version} failed"
|
||||
)
|
||||
logger.warning(f"{invocation_id} | Tool error: {output.error.message}")
|
||||
logger.debug(
|
||||
logger.warning(
|
||||
f"{invocation_id} | Tool developer message: {output.error.developer_message}"
|
||||
)
|
||||
logger.debug(
|
||||
f"{invocation_id} | duration: {duration_ms}ms | Tool output: {output.value}"
|
||||
)
|
||||
if output.error.traceback_info:
|
||||
logger.debug(f"{invocation_id} | Tool traceback: {output.error.traceback_info}")
|
||||
else:
|
||||
logger.info(
|
||||
f"{invocation_id} | Tool {tool_fqname} version {tool_request.tool.version} success"
|
||||
)
|
||||
logger.debug(f"{invocation_id} | Tool output: {output}")
|
||||
|
||||
end_time = time.time() # End time in seconds
|
||||
duration_ms = (end_time - start_time) * 1000 # Convert to milliseconds
|
||||
logger.debug(
|
||||
f"{invocation_id} | duration: {duration_ms}ms | Tool output: {output.value}"
|
||||
)
|
||||
|
||||
return ToolCallResponse(
|
||||
invocation_id=invocation_id,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def start_servers(
|
|||
host: str,
|
||||
port: int,
|
||||
engine_config: str | None,
|
||||
engine_env: dict[str, str] | None = None,
|
||||
engine_env: str | None = None,
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
|
|
@ -31,20 +31,27 @@ def start_servers(
|
|||
host: Host for the actor server.
|
||||
port: Port for the actor server.
|
||||
engine_config: Path to the engine configuration file.
|
||||
engine_env: Path to the engine environment file.
|
||||
debug: Whether to run in debug mode.
|
||||
"""
|
||||
# Validate host and port
|
||||
host = _validate_host(host)
|
||||
port = _validate_port(port)
|
||||
|
||||
# Ensure engine_config is provided and validated
|
||||
engine_config = _get_engine_config(engine_config)
|
||||
engine_config = _get_config_file(engine_config, default_filename="engine.yaml")
|
||||
|
||||
# Ensure engine_env is provided or found and either way, validated
|
||||
env_file = _get_config_file(engine_env, default_filename="arcade.env")
|
||||
|
||||
# Prepare command-line arguments for the actor server and engine
|
||||
actor_cmd = _build_actor_command(host, port, debug)
|
||||
engine_cmd = _build_engine_command(engine_config)
|
||||
|
||||
# even if the user didn't pass an env file we may have found it in the default locations
|
||||
engine_cmd = _build_engine_command(engine_config, engine_env=env_file if env_file else None)
|
||||
|
||||
# Start and manage the processes
|
||||
_manage_processes(actor_cmd, engine_cmd, engine_env, debug)
|
||||
_manage_processes(actor_cmd, engine_cmd, debug=debug)
|
||||
|
||||
|
||||
def _validate_host(host: str) -> str:
|
||||
|
|
@ -90,42 +97,44 @@ def _validate_port(port: int) -> int:
|
|||
return port
|
||||
|
||||
|
||||
def _get_engine_config(engine_config: str | None) -> str:
|
||||
def _get_config_file(file_path: str | None, default_filename: str = "engine.yaml") -> str:
|
||||
"""
|
||||
Determines and validates the engine config file path.
|
||||
Determines and validates the config file path.
|
||||
|
||||
Args:
|
||||
engine_config: Optional path provided by the user.
|
||||
file_path: Optional path provided by the user.
|
||||
default_filename: The default filename to look for.
|
||||
|
||||
Returns:
|
||||
The resolved engine config file path.
|
||||
The resolved config file path.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the config file is not found or invalid.
|
||||
RuntimeError: If the config file is not found.
|
||||
"""
|
||||
if engine_config:
|
||||
engine_config_path = Path(os.path.expanduser(engine_config)).resolve()
|
||||
if not engine_config_path.is_file():
|
||||
console.print(
|
||||
f"❌ Engine config file not found at {engine_config_path}", style="bold red"
|
||||
)
|
||||
raise RuntimeError("Engine config file not found.")
|
||||
if file_path:
|
||||
config_path = Path(os.path.expanduser(file_path)).resolve()
|
||||
if not config_path.is_file():
|
||||
console.print(f"❌ Config file not found at {config_path}", style="bold red")
|
||||
raise RuntimeError(f"Config file not found at {config_path}")
|
||||
return str(config_path)
|
||||
|
||||
elif Path(os.path.expanduser("~/.arcade/engine.yaml")).is_file():
|
||||
engine_config_path = Path(os.path.expanduser("~/.arcade/engine.yaml"))
|
||||
console.print(
|
||||
f"Using default engine config file at {engine_config_path}", style="bold green"
|
||||
)
|
||||
else:
|
||||
# Look for engine.yaml in the current directory
|
||||
engine_config_path = Path(os.getcwd()) / "engine.yaml"
|
||||
if not engine_config_path.is_file():
|
||||
console.print(
|
||||
"❌ Engine config file not specified and not found in current directory.",
|
||||
style="bold red",
|
||||
)
|
||||
raise RuntimeError("Engine config file not specified.")
|
||||
return str(engine_config_path)
|
||||
# Look for the file in the current working directory
|
||||
config_path = Path(os.getcwd()) / default_filename
|
||||
if config_path.is_file():
|
||||
console.print(f"Using config file at {config_path}", style="bold green")
|
||||
return str(config_path)
|
||||
|
||||
# Look for the file in the user's home directory under .arcade/
|
||||
home_config_path = Path.home() / ".arcade" / default_filename
|
||||
if home_config_path.is_file():
|
||||
console.print(f"Using config file at {home_config_path}", style="bold green")
|
||||
return str(home_config_path)
|
||||
|
||||
console.print(
|
||||
f"❌ Config file '{default_filename}' not found in any of the default locations.",
|
||||
style="bold red",
|
||||
)
|
||||
raise RuntimeError(f"Config file '{default_filename}' not found.")
|
||||
|
||||
|
||||
def _build_actor_command(host: str, port: int, debug: bool) -> list[str]:
|
||||
|
|
@ -161,12 +170,13 @@ def _build_actor_command(host: str, port: int, debug: bool) -> list[str]:
|
|||
return cmd
|
||||
|
||||
|
||||
def _build_engine_command(engine_config: str) -> list[str]:
|
||||
def _build_engine_command(engine_config: str, engine_env: str | None = None) -> list[str]:
|
||||
"""
|
||||
Builds the command to start the engine.
|
||||
|
||||
Args:
|
||||
engine_config: Path to the engine configuration file.
|
||||
engine_env: Path to the engine environment file.
|
||||
|
||||
Returns:
|
||||
The command as a list.
|
||||
|
|
@ -184,6 +194,10 @@ def _build_engine_command(engine_config: str) -> list[str]:
|
|||
"-c",
|
||||
engine_config,
|
||||
]
|
||||
if engine_env:
|
||||
cmd.append("-e")
|
||||
cmd.append(engine_env)
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
|
|
@ -215,7 +229,7 @@ def _manage_processes(
|
|||
_setup_signal_handlers(terminate_processes)
|
||||
|
||||
retry_count = 0
|
||||
max_retries = 3 # Define the maximum number of retries
|
||||
max_retries = 1 # Define the maximum number of retries
|
||||
|
||||
while retry_count <= max_retries:
|
||||
try:
|
||||
|
|
@ -322,10 +336,15 @@ def _stream_output(process: subprocess.Popen, name: str) -> None:
|
|||
with pipe:
|
||||
for line in iter(pipe.readline, ""):
|
||||
line = line.rstrip()
|
||||
|
||||
if "DEBUG" in line:
|
||||
line = line.replace("DEBUG", "[#87CEFA]DEBUG[/#87CEFA]", 1)
|
||||
if "INFO" in line:
|
||||
line = line.replace("INFO", "[#109a10]INFO[/#109a10]", 1)
|
||||
if "WARNING" in line:
|
||||
line = line.replace("WARNING", "[orange]WARNING[/orange]")
|
||||
line = line.replace("WARNING", "[#FFA500]WARNING[/#FFA500]", 1)
|
||||
if "ERROR" in line:
|
||||
line = line.replace("ERROR", "[red]ERROR[/red]")
|
||||
line = line.replace("ERROR", "[#FF0000]ERROR[/#FF0000]", 1)
|
||||
console.print(f"[{style}]{name}>[/{style}] {line}")
|
||||
|
||||
threading.Thread(target=stream, args=(process.stdout, stdout_style), daemon=True).start()
|
||||
|
|
|
|||
|
|
@ -496,14 +496,16 @@ def dev(
|
|||
engine_config: str = typer.Option(
|
||||
None, "-c", "--config", help="Path to the engine configuration file."
|
||||
),
|
||||
env_file: str = typer.Option(
|
||||
None, "-e", "--env-file", help="Path to the environment variables file."
|
||||
),
|
||||
debug: bool = typer.Option(False, "-d", "--debug", help="Show debug information"),
|
||||
) -> None:
|
||||
"""
|
||||
Start both the actor and engine servers.
|
||||
"""
|
||||
try:
|
||||
# TODO: pass Engine env vars from here
|
||||
start_servers(host, port, engine_config, engine_env=None, debug=debug)
|
||||
start_servers(host, port, engine_config, engine_env=env_file, debug=debug)
|
||||
except Exception as e:
|
||||
error_message = f"❌ Failed to start servers: {escape(str(e))}"
|
||||
console.print(error_message, style="bold red")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
|
||||
|
|
@ -37,19 +38,28 @@ class ToolDefinitionError(ToolError):
|
|||
|
||||
|
||||
class ToolRuntimeError(RuntimeError):
|
||||
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
developer_message: Optional[str] = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.developer_message = developer_message
|
||||
|
||||
def traceback_info(self) -> str | None:
|
||||
# return the traceback information of the parent exception
|
||||
if self.__cause__:
|
||||
return "\n".join(traceback.format_exception(self.__cause__))
|
||||
return None
|
||||
|
||||
|
||||
class ToolExecutionError(ToolRuntimeError):
|
||||
"""
|
||||
Raised when there is an error executing a tool.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||
super().__init__(message, developer_message)
|
||||
pass
|
||||
|
||||
|
||||
class RetryableToolError(ToolExecutionError):
|
||||
|
|
@ -74,8 +84,7 @@ class ToolSerializationError(ToolRuntimeError):
|
|||
Raised when there is an error executing a tool.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||
super().__init__(message, developer_message)
|
||||
pass
|
||||
|
||||
|
||||
class ToolInputError(ToolSerializationError):
|
||||
|
|
@ -83,8 +92,7 @@ class ToolInputError(ToolSerializationError):
|
|||
Raised when there is an error in the input to a tool.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||
super().__init__(message, developer_message)
|
||||
pass
|
||||
|
||||
|
||||
class ToolOutputError(ToolSerializationError):
|
||||
|
|
@ -92,5 +100,4 @@ class ToolOutputError(ToolSerializationError):
|
|||
Raised when there is an error in the output of a tool.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||
super().__init__(message, developer_message)
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import traceback
|
||||
from typing import Any, Callable
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
|
@ -8,6 +9,7 @@ from arcade.core.errors import (
|
|||
ToolInputError,
|
||||
ToolOutputError,
|
||||
ToolRuntimeError,
|
||||
ToolSerializationError,
|
||||
)
|
||||
from arcade.core.output import output_factory
|
||||
from arcade.core.schema import ToolCallOutput, ToolContext, ToolDefinition
|
||||
|
|
@ -58,20 +60,24 @@ class ToolExecutor:
|
|||
retry_after_ms=e.retry_after_ms,
|
||||
)
|
||||
|
||||
except ToolInputError as e:
|
||||
except ToolSerializationError as e:
|
||||
return output_factory.fail(message=e.message, developer_message=e.developer_message)
|
||||
|
||||
except ToolOutputError as e:
|
||||
return output_factory.fail(message=e.message, developer_message=e.developer_message)
|
||||
|
||||
except ToolRuntimeError as e: # Catch any remaining tool-related errors
|
||||
# should catch all tool exceptions due to the try/except in the tool decorator
|
||||
except ToolRuntimeError as e:
|
||||
return output_factory.fail(
|
||||
message=f"Error in execution: {e.message}", developer_message=e.developer_message
|
||||
message=e.message,
|
||||
developer_message=e.developer_message,
|
||||
traceback_info=e.traceback_info(),
|
||||
)
|
||||
|
||||
# if we get here we're in trouble
|
||||
except Exception as e:
|
||||
return output_factory.fail(message="Error in execution", developer_message=str(e))
|
||||
return output_factory.fail(
|
||||
message="Error in execution",
|
||||
developer_message=str(e),
|
||||
traceback_info=traceback.format_exc(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _serialize_input(input_model: type[BaseModel], **kwargs: Any) -> BaseModel:
|
||||
|
|
@ -85,7 +91,9 @@ class ToolExecutor:
|
|||
inputs = input_model(**kwargs)
|
||||
|
||||
except ValidationError as e:
|
||||
raise ToolInputError(message="Error in input", developer_message=str(e)) from e
|
||||
raise ToolInputError(
|
||||
message="Error in tool input deserialization", developer_message=str(e)
|
||||
) from e
|
||||
|
||||
return inputs
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,19 @@ class ToolOutputFactory:
|
|||
value = getattr(data, "result", "") if data else ""
|
||||
return ToolCallOutput(value=value)
|
||||
|
||||
def fail(self, *, message: str, developer_message: str | None = None) -> ToolCallOutput:
|
||||
def fail(
|
||||
self,
|
||||
*,
|
||||
message: str,
|
||||
developer_message: str | None = None,
|
||||
traceback_info: str | None = None,
|
||||
) -> ToolCallOutput:
|
||||
return ToolCallOutput(
|
||||
error=ToolCallError(
|
||||
message=message, developer_message=developer_message, can_retry=False
|
||||
message=message,
|
||||
developer_message=developer_message,
|
||||
can_retry=False,
|
||||
traceback_info=traceback_info,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -32,6 +41,7 @@ class ToolOutputFactory:
|
|||
developer_message: str | None = None,
|
||||
additional_prompt_content: str | None = None,
|
||||
retry_after_ms: int | None = None,
|
||||
traceback_info: str | None = None,
|
||||
) -> ToolCallOutput:
|
||||
return ToolCallOutput(
|
||||
error=ToolCallError(
|
||||
|
|
|
|||
|
|
@ -265,6 +265,8 @@ class ToolCallError(BaseModel):
|
|||
"""Additional content to be included in the retry prompt."""
|
||||
retry_after_ms: int | None = None
|
||||
"""The number of milliseconds (if any) to wait before retrying the tool call."""
|
||||
traceback_info: str | None = None
|
||||
"""The traceback information for the tool call."""
|
||||
|
||||
|
||||
class ToolCallRequiresAuthorization(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
from arcade.core.errors import RetryableToolError, ToolExecutionError
|
||||
|
||||
__all__ = ["SDKError", "WeightError", "ToolExecutionError", "RetryableToolError"]
|
||||
|
||||
|
||||
class SDKError(Exception):
|
||||
"""Base class for all SDK errors."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import functools
|
||||
import inspect
|
||||
from typing import Callable, TypeVar, Union
|
||||
from typing import Any, Callable, TypeVar, Union
|
||||
|
||||
from arcade.core.utils import snake_to_pascal_case
|
||||
from arcade.sdk.auth import ToolAuthorization
|
||||
from arcade.sdk.error import ToolExecutionError
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
# TODO change desc to description
|
||||
def tool(
|
||||
func: Callable | None = None,
|
||||
desc: str | None = None,
|
||||
|
|
@ -18,12 +19,43 @@ def tool(
|
|||
func_name = str(getattr(func, "__name__", None))
|
||||
tool_name = name or snake_to_pascal_case(func_name)
|
||||
|
||||
setattr(func, "__tool_name__", tool_name) # noqa: B010 (Do not call `setattr` with a constant attribute value)
|
||||
setattr(func, "__tool_description__", desc or inspect.cleandoc(func.__doc__ or "")) # noqa: B010
|
||||
setattr(func, "__tool_requires_auth__", requires_auth) # noqa: B010
|
||||
func.__tool_name__ = tool_name # type: ignore[attr-defined]
|
||||
func.__tool_description__ = desc or inspect.cleandoc(func.__doc__ or "") # type: ignore[attr-defined]
|
||||
func.__tool_requires_auth__ = requires_auth # type: ignore[attr-defined]
|
||||
|
||||
return func
|
||||
if inspect.iscoroutinefunction(func):
|
||||
|
||||
if func: # This means the decorator is used without parameters
|
||||
@functools.wraps(func)
|
||||
async def func_with_error_handling(*args: Any, **kwargs: Any) -> Any:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
# make sure developer raised ToolExecutionError is not
|
||||
# reraised incorrectly.
|
||||
except ToolExecutionError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
message=f"Error in execution of {tool_name}",
|
||||
developer_message=f"Error in {func_name}: {e!s}",
|
||||
) from e
|
||||
|
||||
else:
|
||||
|
||||
@functools.wraps(func)
|
||||
def func_with_error_handling(*args: Any, **kwargs: Any) -> Any:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except ToolExecutionError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
message=f"Error in execution of {tool_name}",
|
||||
developer_message=f"Error in {func_name}: {e!s}",
|
||||
) from e
|
||||
|
||||
return func_with_error_handling
|
||||
|
||||
if func:
|
||||
return decorator(func)
|
||||
return decorator
|
||||
|
|
|
|||
154
arcade/tests/core/test_executor.py
Normal file
154
arcade/tests/core/test_executor.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
from typing import Annotated
|
||||
|
||||
import pytest
|
||||
|
||||
from arcade.core.catalog import ToolCatalog
|
||||
from arcade.core.executor import ToolExecutor
|
||||
from arcade.core.schema import ToolCallError, ToolCallOutput, ToolContext
|
||||
from arcade.sdk import tool
|
||||
from arcade.sdk.error import RetryableToolError, ToolExecutionError
|
||||
|
||||
|
||||
@tool
|
||||
def simple_tool(inp: Annotated[str, "input"]) -> Annotated[str, "output"]:
|
||||
"""Simple tool"""
|
||||
return inp
|
||||
|
||||
|
||||
@tool
|
||||
def retryable_error_tool() -> Annotated[str, "output"]:
|
||||
"""Tool that raises a retryable error"""
|
||||
raise RetryableToolError("test", "test", "test", 1000)
|
||||
|
||||
|
||||
@tool
|
||||
def exec_error_tool() -> Annotated[str, "output"]:
|
||||
"""Tool that raises an error"""
|
||||
raise ToolExecutionError("test", "test")
|
||||
|
||||
|
||||
@tool
|
||||
def unexpected_error_tool() -> Annotated[str, "output"]:
|
||||
"""Tool that raises an unexpected error"""
|
||||
raise RuntimeError("test")
|
||||
|
||||
|
||||
@tool
|
||||
def bad_output_error_tool() -> Annotated[str, "output"]:
|
||||
"""tool that returns a bad output type"""
|
||||
return {"output": "test"}
|
||||
|
||||
|
||||
# ---- Test Driver ----
|
||||
|
||||
catalog = ToolCatalog()
|
||||
catalog.add_tool(simple_tool, "simple_toolkit")
|
||||
catalog.add_tool(retryable_error_tool, "simple_toolkit")
|
||||
catalog.add_tool(exec_error_tool, "simple_toolkit")
|
||||
catalog.add_tool(unexpected_error_tool, "simple_toolkit")
|
||||
catalog.add_tool(bad_output_error_tool, "simple_toolkit")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"tool_func, inputs, expected_output",
|
||||
[
|
||||
(simple_tool, {"inp": "test"}, ToolCallOutput(value="test")),
|
||||
(
|
||||
retryable_error_tool,
|
||||
{},
|
||||
ToolCallOutput(
|
||||
error=ToolCallError(
|
||||
message="test",
|
||||
developer_message="test",
|
||||
additional_prompt_content="test",
|
||||
retry_after_ms=1000,
|
||||
can_retry=True,
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
exec_error_tool,
|
||||
{},
|
||||
ToolCallOutput(
|
||||
error=ToolCallError(
|
||||
message="test",
|
||||
developer_message="test",
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
unexpected_error_tool,
|
||||
{},
|
||||
ToolCallOutput(
|
||||
error=ToolCallError(
|
||||
message="Error in execution of UnexpectedErrorTool",
|
||||
developer_message="Error in unexpected_error_tool: test",
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
simple_tool,
|
||||
{"inp": {"test": "test"}}, # takes in a string not a dict
|
||||
ToolCallOutput(
|
||||
error=ToolCallError(
|
||||
message="Error in tool input deserialization",
|
||||
developer_message=None, # can't gaurantee this will be the same
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
bad_output_error_tool,
|
||||
{},
|
||||
ToolCallOutput(
|
||||
error=ToolCallError(
|
||||
message="Failed to serialize tool output",
|
||||
developer_message=None, # can't gaurantee this will be the same
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"simple_tool",
|
||||
"retryable_error_tool",
|
||||
"exec_error_tool",
|
||||
"unexpected_error_tool",
|
||||
"invalid_input_type",
|
||||
"bad_output_type",
|
||||
],
|
||||
)
|
||||
async def test_tool_executor(tool_func, inputs, expected_output):
|
||||
tool_definition = catalog.find_tool_by_func(tool_func)
|
||||
|
||||
dummy_context = ToolContext()
|
||||
full_tool = catalog.get_tool(tool_definition.get_fully_qualified_name())
|
||||
output = await ToolExecutor.run(
|
||||
func=tool_func,
|
||||
definition=tool_definition,
|
||||
input_model=full_tool.input_model,
|
||||
output_model=full_tool.output_model,
|
||||
context=dummy_context,
|
||||
**inputs,
|
||||
)
|
||||
|
||||
check_output(output, expected_output)
|
||||
|
||||
|
||||
def check_output(output: ToolCallOutput, expected_output: ToolCallOutput):
|
||||
# execution error in tool
|
||||
if output.error:
|
||||
assert output.error.message == expected_output.error.message
|
||||
if expected_output.error.developer_message:
|
||||
assert output.error.developer_message == expected_output.error.developer_message
|
||||
if expected_output.error.traceback_info:
|
||||
assert output.error.traceback_info == expected_output.error.traceback_info
|
||||
assert output.error.can_retry == expected_output.error.can_retry
|
||||
assert (
|
||||
output.error.additional_prompt_content
|
||||
== expected_output.error.additional_prompt_content
|
||||
)
|
||||
assert output.error.retry_after_ms == expected_output.error.retry_after_ms
|
||||
|
||||
# normal tool execution
|
||||
else:
|
||||
assert output.value == expected_output.value
|
||||
Loading…
Reference in a new issue