Skip to content

Advanced toolsΒ #972

@MervinPraison

Description

@MervinPraison

@claude

Advanced tools
minimal code changes in the main feature and minimal code in the client side code.

1. Pre/Post Execution Hooks πŸͺ

πŸ’‘ Proposed PraisonAI Agents Implementation

from praisonaiagents.tools import tool

Simple pre/post hooks

def log_start(context):
print(f"Starting {context.tool_name} with args: {context.args}")

def log_end(context):
print(f"Finished {context.tool_name}, result: {context.result}")

@tool(
before=log_start,
after=log_end
)
def calculate_metrics(data: list) -> dict:
return {"mean": sum(data)/len(data)}

Multiple hooks with priority

@tool(
before=[
(validate_input, priority=1), # Runs first
(log_start, priority=2) # Runs second
],
after=[log_end, save_to_cache]
)
def process_data(input: str) -> str:
return input.upper()

Hook with error handling

def error_handler(context):
if context.error:
logger.error(f"Tool {context.tool_name} failed: {context.error}")
# Can modify the error or suppress it
context.error = None
context.result = "default_value"

@tool(after=error_handler)
def risky_operation():
# Some code that might fail
pass

Global hooks for all tools

from praisonaiagents import set_global_hooks

set_global_hooks(
before=lambda ctx: logger.info(f"Tool call: {ctx.tool_name}"),
after=lambda ctx: metrics.record(ctx.tool_name, ctx.execution_time)
)
Integration with existing approval system:

Combine with existing @require_approval

@require_approval(risk_level="high")
@tool(
before=validate_permissions,
after=audit_log
)
def delete_user(user_id: str):
# Dangerous operation with hooks and approval
pass

2. Tool-Level Caching with TTL πŸ”„

πŸ’‘ Proposed PraisonAI Agents Implementation

from praisonaiagents.tools import tool, cache

Simple caching decorator

@tool
@cache(ttl=300) # 5 minutes
def fetch_weather(city: str) -> dict:
return requests.get(f"api.weather.com/{city}").json()

Advanced caching with custom key

@tool
@cache(
ttl=3600, # 1 hour
key=lambda city, date: f"{city}:{date}", # Custom cache key
condition=lambda result: result['status'] == 'success' # Only cache successful results
)
def get_historical_weather(city: str, date: str) -> dict:
return fetch_historical_data(city, date)

Cache with different backends

from praisonaiagents.tools import RedisCache, MemoryCache

@tool
@cache(backend=RedisCache(host='localhost'), ttl=1800)
def expensive_calculation(x: int, y: int) -> int:
time.sleep(5)
return x ** y

Cache invalidation

@tool
@cache(ttl=600, tags=['user_data'])
def get_user_profile(user_id: str) -> dict:
return db.fetch_user(user_id)

Invalidate cache by tags

from praisonaiagents.tools import invalidate_cache
invalidate_cache(tags=['user_data'])

Per-agent caching

agent = Agent(
name="DataAgent",
cache_config={
'enabled': True,
'ttl': 300,
'backend': 'redis'
}
)
Built-in cache management:

Cache statistics

from praisonaiagents.tools import get_cache_stats

stats = get_cache_stats()
print(f"Cache hits: {stats.hits}, misses: {stats.misses}")

Clear all caches

from praisonaiagents.tools import clear_all_caches
clear_all_caches()

3. External Execution Markers πŸš€

πŸ’‘ Proposed PraisonAI Agents Implementation

from praisonaiagents.tools import tool, external

Simple external marker

@tool
@external
def run_on_gpu(model_path: str, data: list) -> dict:
# Execution pauses, returns control to handler
pass

External with metadata

@tool
@external(
executor="gpu_cluster",
requirements=["cuda>=11.0", "torch>=2.0"],
estimated_time=300 # 5 minutes
)
def train_model(dataset: str, hyperparams: dict):
pass

Conditional external execution

@tool
@external(when=lambda args: args['size'] > 1000000)
def process_data(data: list, size: int):
# Only external if data is large
pass

Integration with agent

agent = Agent(
name="MLAgent",
tools=[train_model, run_on_gpu],
external_handler=my_external_executor # Custom handler
)

External execution flow

async def my_external_executor(tool_call):
if tool_call.executor == "gpu_cluster":
# Send to GPU cluster
job_id = await gpu_cluster.submit(tool_call)
result = await gpu_cluster.wait_for_result(job_id)
return result
Built-in external patterns:

Human-in-the-loop

@tool
@external(type="human_approval")
def delete_production_data(table: str):
pass

Webhook-based

@tool
@external(
type="webhook",
endpoint="https://api.company.com/execute",
auth_token="${EXTERNAL_API_TOKEN}"
)
def trigger_deployment(version: str):
pass

4. Structured User Input Fields πŸ“

πŸ’‘ Proposed PraisonAI Agents Implementation

from praisonaiagents.tools import tool, user_input, Field

Simple user input

@tool
@user_input(
Field(name="confirm", type=bool, description="Proceed with deletion?"),
Field(name="reason", type=str, description="Reason for deletion", required=False)
)
def delete_records(confirm: bool, reason: str = None):
if confirm:
# Proceed with deletion
pass

Advanced field types

from praisonaiagents.tools import Choice, Range, Pattern

@tool
@user_input(
Field(
name="priority",
type=Choice(["low", "medium", "high"]),
description="Task priority",
default="medium"
),
Field(
name="budget",
type=Range(min=0, max=10000),
description="Budget in USD"
),
Field(
name="email",
type=Pattern(r"^[\w.-]+@[\w.-]+.\w+$"),
description="Contact email"
)
)
def create_project(priority: str, budget: float, email: str):
pass

Dynamic fields based on context

@tool
def process_order(order_type: str):
if order_type == "custom":
# Dynamically request additional fields
details = tool.request_input(
Field(name="specifications", type=str),
Field(name="quantity", type=int),
Field(name="delivery_date", type="date")
)
return handle_custom_order(details)
return handle_standard_order()

Form-like input groups

from praisonaiagents.tools import InputGroup

@tool
@user_input(
InputGroup(
"Personal Information",
Field(name="first_name", type=str),
Field(name="last_name", type=str),
Field(name="age", type=int, optional=True)
),
InputGroup(
"Preferences",
Field(name="newsletter", type=bool, default=True),
Field(name="language", type=Choice(["en", "es", "fr"]))
)
)
def register_user(**kwargs):
pass
Integration with agents:

Agent with input handling

agent = Agent(
name="InteractiveAgent",
on_user_input_required=lambda fields: display_form(fields),
input_timeout=60 # Wait 60 seconds for input
)

Async input handling

async def handle_with_input():
agent = Agent(name="Assistant")

# Will pause and wait for user input
result = await agent.run(
    "Process the user registration",
    on_input=lambda fields: get_user_response(fields)
)

πŸ“‹ Implementation Recommendations

1. Unified Tool Decorator System

Create a comprehensive @tool decorator that supports all features:

@tool(
name="fetch_data",
description="Fetches data from API",
# Hooks
before=validate_input,
after=log_result,
# Caching
cache=True,
cache_ttl=300,
# External execution
external=conditional_external,
# User input
inputs=[Field(name="api_key", type=str, secret=True)],
# Integration with existing
require_approval=True,
risk_level="medium"
)
def fetch_data(api_key: str, endpoint: str):
pass

2. Tool Context Object

Provide rich context to hooks and handlers:

class ToolContext:
tool_name: str
args: dict
kwargs: dict
result: Any
error: Exception | None
agent: Agent | None
execution_time: float
metadata: dict

3. Progressive Enhancement

Start with simple APIs that can be progressively enhanced:

Simple

@tool
@cache(300)
def simple_tool(): pass

Advanced

@tool(
cache={'ttl': 300, 'backend': 'redis', 'key': custom_key},
hooks={'before': [validate, log], 'after': [audit]},
external={'when': is_large, 'executor': 'gpu'}
)
def advanced_tool(): pass

4. Backwards Compatibility

Ensure existing tools continue to work:

Old style still works

def my_tool(x: int) -> int:
return x * 2

agent = Agent(tools=[my_tool]) # Works without @tool decorator

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions