base.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. from __future__ import annotations
  2. import inspect
  3. import json
  4. from collections.abc import Callable
  5. from typing import Any, get_type_hints
  6. from pydantic import BaseModel
  7. from supply_agent.types import ToolDefinition
  8. def _python_type_to_json_schema(py_type: type) -> dict[str, Any]:
  9. """Map Python types to JSON Schema types."""
  10. origin = getattr(py_type, "__origin__", None)
  11. if py_type is str or py_type is inspect.Parameter.empty:
  12. return {"type": "string"}
  13. if py_type is int:
  14. return {"type": "integer"}
  15. if py_type is float:
  16. return {"type": "number"}
  17. if py_type is bool:
  18. return {"type": "boolean"}
  19. if origin is list:
  20. args = getattr(py_type, "__args__", (Any,))
  21. item_type = args[0] if args else Any
  22. return {"type": "array", "items": _python_type_to_json_schema(item_type)}
  23. if origin is dict:
  24. return {"type": "object"}
  25. return {"type": "string"}
  26. def _build_parameters(func: Callable[..., Any]) -> dict[str, Any]:
  27. """Build JSON Schema parameters from function signature."""
  28. sig = inspect.signature(func)
  29. hints = get_type_hints(func)
  30. properties: dict[str, Any] = {}
  31. required: list[str] = []
  32. for name, param in sig.parameters.items():
  33. if name in ("self", "cls"):
  34. continue
  35. py_type = hints.get(name, str)
  36. prop: dict[str, Any] = _python_type_to_json_schema(py_type)
  37. if param.default is not inspect.Parameter.empty:
  38. if not isinstance(param.default, (BaseModel,)):
  39. prop["default"] = param.default
  40. else:
  41. required.append(name)
  42. properties[name] = prop
  43. return {"type": "object", "properties": properties, "required": required}
  44. def tool(
  45. func: Callable[..., Any] | None = None,
  46. *,
  47. name: str | None = None,
  48. description: str | None = None,
  49. ) -> Callable[..., Any]:
  50. """
  51. Decorator to register a function as an agent tool.
  52. Usage:
  53. @tool
  54. def search(query: str, limit: int = 10) -> str:
  55. '''Search the web for information.'''
  56. ...
  57. """
  58. def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
  59. fn._is_tool = True # type: ignore[attr-defined]
  60. fn._tool_name = name or fn.__name__ # type: ignore[attr-defined]
  61. fn._tool_description = description or (fn.__doc__ or "").strip() # type: ignore[attr-defined]
  62. fn._tool_parameters = _build_parameters(fn) # type: ignore[attr-defined]
  63. return fn
  64. if func is not None:
  65. return decorator(func)
  66. return decorator
  67. class Tool:
  68. """A callable tool with schema metadata."""
  69. def __init__(
  70. self,
  71. func: Callable[..., Any],
  72. name: str | None = None,
  73. description: str | None = None,
  74. ) -> None:
  75. self.func = func
  76. self.name = name or getattr(func, "_tool_name", func.__name__)
  77. self.description = description or getattr(
  78. func, "_tool_description", (func.__doc__ or "").strip()
  79. )
  80. self.parameters = getattr(func, "_tool_parameters", _build_parameters(func))
  81. @property
  82. def definition(self) -> ToolDefinition:
  83. return ToolDefinition(
  84. name=self.name,
  85. description=self.description,
  86. parameters=self.parameters,
  87. )
  88. def __call__(self, **kwargs: Any) -> Any:
  89. return self.func(**kwargs)
  90. async def acall(self, **kwargs: Any) -> Any:
  91. result = self.func(**kwargs)
  92. if inspect.isawaitable(result):
  93. return await result
  94. return result
  95. def execute(self, arguments: str | dict[str, Any]) -> str:
  96. """Execute the tool with JSON or dict arguments, returning a string result."""
  97. if isinstance(arguments, str):
  98. args = json.loads(arguments) if arguments.strip() else {}
  99. else:
  100. args = arguments
  101. try:
  102. result = self(**args)
  103. if inspect.isawaitable(result):
  104. raise RuntimeError(
  105. f"Tool '{self.name}' is async. Use ToolRegistry.aexecute() instead."
  106. )
  107. return str(result) if result is not None else ""
  108. except Exception as e:
  109. return json.dumps({"error": str(e)}, ensure_ascii=False)
  110. async def aexecute(self, arguments: str | dict[str, Any]) -> str:
  111. """Async execute the tool."""
  112. if isinstance(arguments, str):
  113. args = json.loads(arguments) if arguments.strip() else {}
  114. else:
  115. args = arguments
  116. try:
  117. result = await self.acall(**args)
  118. return str(result) if result is not None else ""
  119. except Exception as e:
  120. return json.dumps({"error": str(e)}, ensure_ascii=False)