base.py 4.3 KB

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