base.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. from typing import List, Optional
  2. import functools
  3. import threading
  4. from toolkit.function_tool import FunctionTool
  5. def with_timeout(timeout=None):
  6. r"""Decorator that adds timeout functionality to functions.
  7. Executes functions with a specified timeout value. Returns a timeout
  8. message if execution time is exceeded.
  9. Args:
  10. timeout (float, optional): The timeout duration in seconds. If None,
  11. will try to get timeout from the instance's timeout attribute.
  12. (default: :obj:`None`)
  13. Example:
  14. >>> @with_timeout(5)
  15. ... def my_function():
  16. ... return "Success"
  17. >>> my_function()
  18. >>> class MyClass:
  19. ... timeout = 5
  20. ... @with_timeout()
  21. ... def my_method(self):
  22. ... return "Success"
  23. """
  24. def decorator(func):
  25. @functools.wraps(func)
  26. def wrapper(*args, **kwargs):
  27. # Determine the effective timeout value
  28. effective_timeout = timeout
  29. if effective_timeout is None and args:
  30. effective_timeout = getattr(args[0], 'timeout', None)
  31. # If no timeout value is provided, execute function normally
  32. if effective_timeout is None:
  33. return func(*args, **kwargs)
  34. # Container to hold the result of the function call
  35. result_container = []
  36. def target():
  37. result_container.append(func(*args, **kwargs))
  38. # Start the function in a new thread
  39. thread = threading.Thread(target=target)
  40. thread.start()
  41. thread.join(effective_timeout)
  42. # Check if the thread is still alive after the timeout
  43. if thread.is_alive():
  44. return (
  45. f"Function `{func.__name__}` execution timed out, "
  46. f"exceeded {effective_timeout} seconds."
  47. )
  48. else:
  49. return result_container[0]
  50. return wrapper
  51. # Handle both @with_timeout and @with_timeout() usage
  52. if callable(timeout):
  53. # If timeout is passed as a function, apply it to the decorator
  54. func, timeout = timeout, None
  55. return decorator(func)
  56. return decorator
  57. class BaseToolkit:
  58. r"""Base class for toolkits.
  59. Args:
  60. timeout (Optional[float]): The timeout for the toolkit.
  61. """
  62. timeout: Optional[float] = None
  63. def __init__(self, timeout: Optional[float] = None):
  64. # check if timeout is a positive number
  65. if timeout is not None and timeout <= 0:
  66. raise ValueError("Timeout must be a positive number.")
  67. self.timeout = timeout
  68. # Add timeout to all callable methods in the toolkit
  69. def __init_subclass__(cls, **kwargs):
  70. super().__init_subclass__(**kwargs)
  71. for attr_name, attr_value in cls.__dict__.items():
  72. if callable(attr_value) and not attr_name.startswith("__"):
  73. setattr(cls, attr_name, with_timeout(attr_value))
  74. def get_tools(self) -> List[FunctionTool]:
  75. r"""Returns a list of FunctionTool objects representing the
  76. functions in the toolkit.
  77. Returns:
  78. List[FunctionTool]: A list of FunctionTool objects
  79. representing the functions in the toolkit.
  80. """
  81. raise NotImplementedError("Subclasses must implement this method.")