Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
72 changes: 62 additions & 10 deletions src/praisonai-agents/praisonaiagents/agent/tool_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -2096,48 +2096,65 @@ def _norm(n):
)

if func:
bind_target = func
bind_arguments = arguments
try:
# BaseTool instances (plugin system) - call run() method
from ..tools.base import BaseTool
if isinstance(func, BaseTool):
bind_target = func.run
casted_arguments = self._cast_arguments(func.run, arguments)
bind_arguments = casted_arguments
return func.run(**casted_arguments)

# Langchain: If it's a class with run but not _run, instantiate and call run
if inspect.isclass(func) and hasattr(func, 'run') and not hasattr(func, '_run'):
instance = func()
bind_target = instance.run
run_params = {k: v for k, v in arguments.items()
if k in inspect.signature(instance.run).parameters
and k != 'self'}
casted_params = self._cast_arguments(instance.run, run_params)
bind_arguments = casted_params
return instance.run(**casted_params)

# CrewAI: If it's a class with an _run method, instantiate and call _run
elif inspect.isclass(func) and hasattr(func, '_run'):
instance = func()
bind_target = instance._run
run_params = {k: v for k, v in arguments.items()
if k in inspect.signature(instance._run).parameters
and k != 'self'}
casted_params = self._cast_arguments(instance._run, run_params)
bind_arguments = casted_params
return instance._run(**casted_params)

# Otherwise treat as regular function
elif callable(func):
bind_target = func
casted_arguments = self._cast_arguments(func, arguments)
bind_arguments = casted_arguments
return func(**casted_arguments)
except Exception as e:
error_msg = str(e)
logging.error(f"Error executing tool {function_name}: {error_msg}")
schema = self._tool_parameter_hint(func)
if schema:
# Fold the parameter names into the error string itself so the
# hint survives conversion to ToolExecutionError (which keeps
# only the message) and actually reaches the model.
error_msg = (
f"{error_msg} Expected parameters for '{function_name}' — "
f"required: {schema['required']}, optional: {schema['optional']}."
)
return {"error": error_msg, "expected_parameters": schema}
# Only echo the parameter schema when the failure is a genuine
# argument-binding error (wrong/missing/extra kwargs). A
# TypeError/ValueError raised *inside* a successfully-bound tool
# (domain validation) must not be mislabelled as a parameter
# problem, or the model would alter valid call arguments instead
# of fixing the offending value.
if self._is_argument_binding_error(bind_target, bind_arguments):
schema = self._tool_parameter_hint(func)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Decorated tools lose parameter hints

When an @tool-decorated FunctionTool receives a missing or unexpected argument, _tool_parameter_hint(func) inspects its generic run(**kwargs) wrapper and finds no named parameters, causing the model to receive only the raw binding error instead of the required and optional parameter names.

Knowledge Base Used: praisonai-agents Core Library

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

if schema:
# Fold the parameter names into the error string itself so
# the hint survives conversion to ToolExecutionError (which
# keeps only the message) and actually reaches the model.
error_msg = (
f"{error_msg} Expected parameters for '{function_name}' — "
f"required: {schema['required']}, optional: {schema['optional']}."
)
return {"error": error_msg, "expected_parameters": schema}
return {"error": error_msg}

# Unresolved: return a corrective, model-readable message so the model can
Expand Down Expand Up @@ -2213,6 +2230,41 @@ def _available_active_tool_names(self):
names = [name for name, _tool in self._iter_active_named_tools()]
return sorted(set(names))

def _resolve_callable_signature_target(self, func):
"""Return the callable whose signature describes ``func``'s arguments."""
target = func
try:
from ..tools.base import BaseTool
if isinstance(func, BaseTool):
return func.run
except ImportError:
pass
if inspect.isclass(func):
run = getattr(func, 'run', None) or getattr(func, '_run', None)
if run is not None:
target = run
return target

def _is_argument_binding_error(self, func, arguments) -> bool:
"""True only when ``arguments`` cannot bind to ``func``'s signature.

Distinguishes a genuine call-boundary failure (wrong/missing/extra
kwargs) from a ``TypeError``/``ValueError`` raised *inside* a
successfully-bound tool during its own domain logic. Only the former
should receive a parameter-schema hint; the latter is a runtime error
the model must fix by changing the value, not the parameter names.
"""
target = self._resolve_callable_signature_target(func)
try:
sig = inspect.signature(target)
except (TypeError, ValueError):
return False
try:
sig.bind(**(arguments or {}))
except TypeError:
return True
return False

def _tool_parameter_hint(self, func):
"""Return {'required': [...], 'optional': [...]} for a callable tool."""
target = func
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ def web_search(query: str, limit: int = 10) -> str:
assert "limit" in result["expected_parameters"]["optional"]


def test_runtime_valueerror_omits_parameter_hint():
# A ValueError raised *inside* a successfully-bound tool (domain validation)
# must not be mislabelled as a parameter-binding problem, so no schema hint
# is echoed — the model should fix the value, not the argument names.
def web_search(query: str) -> str:
"""Search the web."""
raise ValueError("query must not be empty")

agent = _make_agent([web_search])

result = agent._execute_tool_impl("web_search", {"query": ""})
assert isinstance(result, dict)
assert "query must not be empty" in result["error"]
assert "expected_parameters" not in result
assert "Expected parameters" not in result["error"]


def test_unknown_tool_message_reaches_model_via_public_path():
def web_search(query: str) -> str:
"""Search the web."""
Expand Down Expand Up @@ -156,3 +173,19 @@ def test_mcp_tool_name_repairs_case_and_separator():

result = agent._execute_tool_impl("Read-File", {"path": "x"})
assert result == "read_file:{'path': 'x'}"


def test_available_active_tool_names():
def web_search(query: str) -> str:
"""Search the web."""
return query

def calculate(a: int, b: int) -> int:
"""Add numbers."""
return a + b

agent = _make_agent([web_search, calculate])

names = agent._available_active_tool_names()
assert "web_search" in names
assert "calculate" in names
Loading