glean-agent-toolkit 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ """
2
+ Glean Agent Toolkit.
3
+
4
+ Universal Tool/Action Toolkit for Glean agent frameworks.
5
+ """
6
+
7
+ import logging
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ from glean.agent_toolkit.decorators import tool_spec
11
+ from glean.agent_toolkit.registry import get_registry
12
+ from glean.agent_toolkit.spec import ToolSpec
13
+
14
+ from . import adapters
15
+
16
+ __all__ = [
17
+ "tool_spec",
18
+ "get_registry",
19
+ "ToolSpec",
20
+ "adapters",
21
+ "__version__",
22
+ ]
23
+
24
+ try:
25
+ __version__ = version("glean-agent-toolkit")
26
+ except PackageNotFoundError: # pragma: no cover – package not installed
27
+ __version__ = "0.0.0"
28
+
29
+ logger = logging.getLogger(__name__)
30
+ logger.addHandler(logging.NullHandler())
@@ -0,0 +1,24 @@
1
+ """Adapters for converting tool specifications to framework-specific formats."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from glean.agent_toolkit.adapters.adk import ADKAdapter
6
+ from glean.agent_toolkit.adapters.base import BaseAdapter
7
+ from glean.agent_toolkit.adapters.crewai import CrewAIAdapter
8
+ from glean.agent_toolkit.adapters.langchain import LangChainAdapter
9
+ from glean.agent_toolkit.adapters.openai import (
10
+ OpenAIAdapter,
11
+ OpenAIFunctionDef,
12
+ OpenAIToolDef,
13
+ )
14
+
15
+ __all__ = [
16
+ "BaseAdapter",
17
+ "ADKAdapter",
18
+ "CrewAIAdapter",
19
+ "LangChainAdapter",
20
+ "OpenAIAdapter",
21
+ "OpenAIToolDef",
22
+ "OpenAIFunctionDef",
23
+ "BaseModel",
24
+ ]
@@ -0,0 +1,81 @@
1
+ """Google ADK adapter for converting tool specifications."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any, TypeAlias
5
+
6
+ from glean.agent_toolkit.adapters.base import BaseAdapter
7
+ from glean.agent_toolkit.spec import ToolSpec
8
+
9
+ if TYPE_CHECKING:
10
+ from google.adk.tools import FunctionTool as _RealAdkFunctionTool
11
+ else:
12
+ _RealAdkFunctionTool = Any # type: ignore
13
+
14
+ HAS_ADK: bool
15
+
16
+
17
+ class _FallbackAdkFunctionTool:
18
+ """Fallback for google.adk.tools.FunctionTool.
19
+
20
+ This lightweight stand-in mimics the public attributes accessed by tests
21
+ (``name``, ``description``, ``schema`` and ``func``). It purposefully keeps
22
+ the same runtime surface as the real ADK ``FunctionTool`` so that unit
23
+ tests exercising the adapter behave consistently even when the dependency
24
+ is missing.
25
+ """
26
+
27
+ name: str
28
+ description: str | None
29
+ func: Callable[..., Any]
30
+ schema: dict[str, Any] | None
31
+
32
+ def __init__(self, func: Callable[..., Any]) -> None: # noqa: D401 – keep signature minimal
33
+ self.func = func
34
+ self.name = func.__name__
35
+ self.description = func.__doc__
36
+ self.schema = None # Set later by the adapter
37
+
38
+
39
+ try:
40
+ from google.adk.tools import FunctionTool as _RuntimeAdkFunctionTool
41
+
42
+ HAS_ADK = True
43
+ except ImportError: # pragma: no cover
44
+ _RuntimeAdkFunctionTool = _FallbackAdkFunctionTool # type: ignore
45
+ HAS_ADK = False
46
+
47
+ # Single alias used for typing and at runtime
48
+ AdkFunctionTool: TypeAlias = _RealAdkFunctionTool | _FallbackAdkFunctionTool
49
+
50
+
51
+ class ADKAdapter(BaseAdapter["AdkFunctionTool"]):
52
+ """Adapter for Google ADK tools."""
53
+
54
+ def __init__(self, tool_spec: ToolSpec) -> None:
55
+ """Initialize the adapter.
56
+
57
+ Args:
58
+ tool_spec: The tool specification
59
+ """
60
+ super().__init__(tool_spec)
61
+ if not HAS_ADK:
62
+ raise ImportError(
63
+ "Google Agent Development Kit (ADK) is required for ADK adapter. "
64
+ "Install it with `pip install agent_toolkit[adk]` or `pip install google-adk`."
65
+ )
66
+
67
+ def to_tool(self) -> "AdkFunctionTool":
68
+ """Convert to Google ADK FunctionTool format.
69
+
70
+ Returns:
71
+ An ADK FunctionTool instance
72
+ """
73
+ func = self.tool_spec.function
74
+ if not func.__doc__:
75
+ func.__doc__ = self.tool_spec.description
76
+
77
+ tool = _RuntimeAdkFunctionTool(func=func) # type: ignore[arg-type]
78
+
79
+ setattr(tool, "schema", self.tool_spec.input_schema)
80
+
81
+ return tool
@@ -0,0 +1,29 @@
1
+ """Base adapter class for converting tool specifications to framework-specific formats."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Generic, TypeVar
5
+
6
+ from glean.agent_toolkit.spec import ToolSpec
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class BaseAdapter(Generic[T], ABC):
12
+ """Base adapter for converting ToolSpec to framework-specific formats."""
13
+
14
+ def __init__(self, tool_spec: ToolSpec) -> None:
15
+ """Initialize the adapter.
16
+
17
+ Args:
18
+ tool_spec: The tool specification
19
+ """
20
+ self.tool_spec = tool_spec
21
+
22
+ @abstractmethod
23
+ def to_tool(self) -> T:
24
+ """Convert to framework-specific tool format.
25
+
26
+ Returns:
27
+ The framework-specific representation of the tool
28
+ """
29
+ pass
@@ -0,0 +1,208 @@
1
+ """CrewAI adapter for converting tool specifications."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any, Union, cast
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from glean.agent_toolkit.adapters.base import BaseAdapter
9
+ from glean.agent_toolkit.spec import ToolSpec
10
+
11
+ if TYPE_CHECKING:
12
+ from crewai.tools import BaseTool as CrewBaseTool
13
+ else:
14
+ CrewBaseTool = Any # type: ignore # noqa: N816
15
+
16
+ from pydantic import Field as PydanticField # type: ignore
17
+ from pydantic import create_model as pydantic_create_model
18
+
19
+ HAS_CREWAI: bool
20
+
21
+
22
+ class _FallbackCrewBaseTool:
23
+ """Fallback for crewai.tools.BaseTool."""
24
+
25
+ name: str
26
+ description: str
27
+
28
+ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107
29
+ pass
30
+
31
+ def _run(self, *args: Any, **kwargs: Any) -> Any: # noqa: D401
32
+ ...
33
+
34
+
35
+ def _fallback_field(*args: Any, **kwargs: Any) -> Any: # noqa: N802
36
+ """Fallback for pydantic.Field."""
37
+ return None
38
+
39
+
40
+ def _fallback_create_model(*args: Any, **kwargs: Any) -> Any:
41
+ """Fallback for pydantic.create_model."""
42
+ return None
43
+
44
+
45
+ try:
46
+ from crewai.tools import BaseTool as _ActualCrewBaseTool # type: ignore
47
+ from pydantic import Field as _ActualField # type: ignore
48
+ from pydantic import create_model as _actual_create_model
49
+
50
+ BaseTool = _ActualCrewBaseTool
51
+ Field = _ActualField
52
+ create_model = _actual_create_model
53
+ HAS_CREWAI = True
54
+ except ImportError: # pragma: no cover
55
+ BaseTool = _FallbackCrewBaseTool # type: ignore[assignment]
56
+ Field = _fallback_field # type: ignore[assignment]
57
+ create_model = _fallback_create_model # type: ignore[assignment]
58
+ HAS_CREWAI = False
59
+
60
+
61
+ class GleanCrewAITool(BaseTool): # type: ignore[misc]
62
+ """CrewAI tool implementation for Glean tools."""
63
+
64
+ name: str # CrewAI BaseTool requires name and description
65
+ description: str # CrewAI BaseTool requires name and description
66
+ # Reuse BaseTool's built-in placeholder default for ``args_schema`` so that
67
+ # CrewAI can lazily infer a schema when one isn't supplied. We intentionally
68
+ # *do not* override the attribute here to avoid accidentally setting it to
69
+ # ``None`` and breaking CrewAI's internal description generation logic.
70
+
71
+ _function: Callable[..., Any]
72
+
73
+ def __init__(
74
+ self,
75
+ name: str,
76
+ description: str,
77
+ function: Callable[..., Any],
78
+ args_schema: type[BaseModel] | None = None,
79
+ ) -> None:
80
+ """Initialize the tool.
81
+
82
+ Args:
83
+ name: The name of the tool
84
+ description: A description of the tool
85
+ function: The function to call when the tool is invoked
86
+ args_schema: Optional schema for the arguments
87
+ """
88
+ super().__init__(name=name, description=description)
89
+
90
+ self._function = function
91
+
92
+ # Only override ``args_schema`` when we actually created one; otherwise
93
+ # leave the placeholder so CrewAI can lazily generate a schema.
94
+ if args_schema is not None:
95
+ self.args_schema = args_schema
96
+
97
+ object.__setattr__(self, "_tool_spec_ref", None)
98
+
99
+ def _run(self, **kwargs: Any) -> Any:
100
+ """Run the tool with the given arguments.
101
+
102
+ Args:
103
+ **kwargs: The arguments to pass to the function
104
+
105
+ Returns:
106
+ The result of calling the function
107
+ """
108
+ return self._function(**kwargs)
109
+
110
+
111
+ CrewAIToolType = CrewBaseTool | BaseTool # type: ignore[valid-type]
112
+
113
+
114
+ class CrewAIAdapter(BaseAdapter[CrewAIToolType]):
115
+ """Adapter for CrewAI tools."""
116
+
117
+ def __init__(self, tool_spec: ToolSpec) -> None:
118
+ """Initialize the adapter.
119
+
120
+ Args:
121
+ tool_spec: The tool specification
122
+ """
123
+ super().__init__(tool_spec)
124
+ if not HAS_CREWAI:
125
+ raise ImportError(
126
+ "CrewAI package is required for CrewAI adapter. "
127
+ "Install it with `pip install agent_toolkit[crewai]`. "
128
+ "Note: CrewAI requires Python 3.10 or higher."
129
+ )
130
+
131
+ def to_tool(self) -> CrewAIToolType:
132
+ """Convert to CrewAI tool format.
133
+
134
+ Returns:
135
+ A CrewAI BaseTool instance
136
+ """
137
+ # Create and configure the tool
138
+ created_args_schema = self._create_args_schema()
139
+
140
+ tool = GleanCrewAITool(
141
+ name=self.tool_spec.name,
142
+ description=self.tool_spec.description,
143
+ function=self.tool_spec.function,
144
+ args_schema=created_args_schema,
145
+ )
146
+
147
+ # Store the tool_spec reference for testing
148
+ object.__setattr__(tool, "_tool_spec_ref", self.tool_spec)
149
+
150
+ return tool
151
+
152
+ def _create_args_schema(self) -> type[BaseModel] | None:
153
+ """Create a Pydantic model for the arguments schema.
154
+
155
+ Returns:
156
+ A Pydantic model class or None if no properties
157
+ """
158
+ json_schema = self.tool_spec.input_schema
159
+
160
+ props = json_schema.get("properties", {})
161
+ required = json_schema.get("required", [])
162
+
163
+ if not props:
164
+ return None
165
+
166
+ field_defs: dict[str, tuple[type, Any]] = {}
167
+ for name, schema in props.items():
168
+ field_type = self._get_field_type(schema)
169
+ is_required = name in required
170
+ description = schema.get("description", "")
171
+ if is_required:
172
+ field_defs[name] = (field_type, PydanticField(..., description=description))
173
+ else:
174
+ field_defs[name] = (field_type, PydanticField(None, description=description))
175
+
176
+ model_name = f"{self.tool_spec.name}ArgsSchema"
177
+ model = pydantic_create_model(model_name, **field_defs) # type: ignore
178
+
179
+ return cast(type[BaseModel], model)
180
+
181
+ def _get_field_type(self, schema: dict[str, Any]) -> type:
182
+ """Determine the Python type from JSON schema property.
183
+
184
+ Args:
185
+ schema: JSON schema property definition
186
+
187
+ Returns:
188
+ Appropriate Python type
189
+ """
190
+ if "enum" in schema:
191
+ return str
192
+
193
+ schema_type = schema.get("type", "string")
194
+
195
+ if schema_type == "string":
196
+ return str
197
+ elif schema_type == "integer":
198
+ return int
199
+ elif schema_type == "number":
200
+ return float
201
+ elif schema_type == "boolean":
202
+ return bool
203
+ elif schema_type == "array":
204
+ return list
205
+ elif schema_type == "object":
206
+ return dict
207
+ else:
208
+ return str
@@ -0,0 +1,159 @@
1
+ """LangChain adapter for converting tool specifications."""
2
+
3
+ from datetime import date, datetime
4
+ from typing import TYPE_CHECKING, Any, TypeAlias, Union, cast
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from glean.agent_toolkit.adapters.base import BaseAdapter
9
+ from glean.agent_toolkit.spec import ToolSpec
10
+
11
+ if TYPE_CHECKING:
12
+ from langchain.tools import Tool as LangchainTool # pragma: no cover
13
+ else:
14
+ LangchainTool = Any # type: ignore # noqa: N816
15
+
16
+ from pydantic import Field as PydanticField # type: ignore
17
+ from pydantic import create_model as pydantic_create_model
18
+
19
+ ToolClass: Any = object
20
+ Field: Any = object
21
+ create_model = pydantic_create_model
22
+
23
+
24
+ class _FallbackLangchainTool:
25
+ """Fallback for langchain.tools.Tool."""
26
+
27
+ name: str
28
+ description: str
29
+ func: Any
30
+ args_schema: Any
31
+
32
+ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107
33
+ pass
34
+
35
+
36
+ def _fallback_pydantic_field(*args: Any, **kwargs: Any) -> Any: # noqa: N802
37
+ """Fallback for pydantic.Field."""
38
+ return None
39
+
40
+
41
+ def _fallback_pydantic_create_model(*args: Any, **kwargs: Any) -> Any:
42
+ """Fallback for pydantic.create_model."""
43
+ return None
44
+
45
+
46
+ try:
47
+ from langchain.tools import Tool as _ActualLangchainToolImport # type: ignore
48
+ from pydantic import Field as _ActualPydanticFieldImport # type: ignore
49
+ from pydantic import create_model as _actual_pydantic_create_model_import
50
+
51
+ ToolClass = _ActualLangchainToolImport
52
+ Field = _ActualPydanticFieldImport
53
+ create_model = _actual_pydantic_create_model_import
54
+ HAS_LANGCHAIN = True
55
+ except ImportError: # pragma: no cover
56
+ ToolClass = _FallbackLangchainTool # type: ignore[assignment]
57
+ Field = _fallback_pydantic_field
58
+ create_model = _fallback_pydantic_create_model
59
+ HAS_LANGCHAIN = False
60
+
61
+
62
+ if TYPE_CHECKING:
63
+ LangChainToolType: TypeAlias = "LangchainTool"
64
+ else:
65
+ from typing import Any as LangChainToolType # type: ignore
66
+
67
+
68
+ class LangChainAdapter(BaseAdapter[LangChainToolType]):
69
+ """Adapter for LangChain tools."""
70
+
71
+ def __init__(self, tool_spec: ToolSpec) -> None:
72
+ """Initialize the adapter.
73
+
74
+ Args:
75
+ tool_spec: The tool specification
76
+ """
77
+ super().__init__(tool_spec)
78
+ if not HAS_LANGCHAIN:
79
+ raise ImportError(
80
+ "LangChain package is required for LangChain adapter. "
81
+ "Install it with `pip install agent_toolkit[langchain]`."
82
+ )
83
+
84
+ def to_tool(self) -> Any:
85
+ """Convert to LangChain tool format.
86
+
87
+ Returns:
88
+ LangChain Tool instance
89
+ """
90
+ return ToolClass(
91
+ name=self.tool_spec.name,
92
+ description=self.tool_spec.description,
93
+ func=self.tool_spec.function,
94
+ args_schema=self._create_args_schema(),
95
+ )
96
+
97
+ def _create_args_schema(self) -> type[BaseModel] | None:
98
+ """Create a Pydantic model for the arguments schema.
99
+
100
+ Returns:
101
+ A Pydantic model class or None if no properties
102
+ """
103
+ json_schema = self.tool_spec.input_schema
104
+
105
+ props = json_schema.get("properties", {})
106
+ required = json_schema.get("required", [])
107
+
108
+ if not props:
109
+ return None
110
+
111
+ field_defs: dict[str, tuple[type, Any]] = {}
112
+
113
+ for name, schema in props.items():
114
+ field_type = self._get_field_type(schema)
115
+ is_required = name in required
116
+
117
+ description = schema.get("description", "")
118
+
119
+ if is_required:
120
+ field_defs[name] = (field_type, Field(..., description=description))
121
+ else:
122
+ field_defs[name] = (field_type, Field(None, description=description))
123
+
124
+ model = create_model(f"{self.tool_spec.name}Schema", **field_defs) # type: ignore
125
+ return cast(type[BaseModel], model)
126
+
127
+ def _get_field_type(self, schema: dict[str, Any]) -> type:
128
+ """Determine the Python type from JSON schema property.
129
+
130
+ Args:
131
+ schema: JSON schema property definition
132
+
133
+ Returns:
134
+ Appropriate Python type
135
+ """
136
+ if "enum" in schema:
137
+ return str
138
+
139
+ schema_type = schema.get("type", "string")
140
+ schema_format = schema.get("format", "")
141
+
142
+ if schema_type == "string":
143
+ if schema_format == "date-time":
144
+ return datetime
145
+ elif schema_format == "date":
146
+ return date
147
+ return str
148
+ elif schema_type == "integer":
149
+ return int
150
+ elif schema_type == "number":
151
+ return float
152
+ elif schema_type == "boolean":
153
+ return bool
154
+ elif schema_type == "array":
155
+ return list
156
+ elif schema_type == "object":
157
+ return dict
158
+ else:
159
+ return str
@@ -0,0 +1,150 @@
1
+ """OpenAI adapter for converting tool specifications."""
2
+
3
+ import json
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING, Any, TypeAlias, TypedDict, Union
6
+
7
+ from glean.agent_toolkit.adapters.base import BaseAdapter
8
+ from glean.agent_toolkit.spec import ToolSpec
9
+
10
+ if TYPE_CHECKING:
11
+ from agents.tool import FunctionTool as _RealOpenAIFunctionTool
12
+ else:
13
+ _RealOpenAIFunctionTool = Any # type: ignore
14
+
15
+ HAS_OPENAI: bool
16
+
17
+
18
+ class _FallbackOpenAIFunctionTool:
19
+ """Fallback for agents.tool.FunctionTool."""
20
+
21
+ name: str
22
+ description: str
23
+ params_json_schema: Any
24
+ on_invoke_tool: Any
25
+
26
+ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107
27
+ pass
28
+
29
+
30
+ try:
31
+ import openai # noqa: F401 # type: ignore
32
+ from agents.tool import FunctionTool as _RuntimeFunctionTool # type: ignore
33
+
34
+ _RuntimeOpenAIFunctionTool = _RuntimeFunctionTool # type: ignore
35
+ HAS_OPENAI = True
36
+ except ImportError: # pragma: no cover
37
+ _RuntimeOpenAIFunctionTool = _FallbackOpenAIFunctionTool # type: ignore
38
+ HAS_OPENAI = False
39
+
40
+
41
+ OpenAIFunctionTool: TypeAlias = _RealOpenAIFunctionTool | _FallbackOpenAIFunctionTool
42
+
43
+
44
+ OpenAIToolType = dict[str, Any] | OpenAIFunctionTool
45
+
46
+
47
+ class OpenAIFunctionDef(TypedDict):
48
+ """Type definition for OpenAI function definition."""
49
+
50
+ name: str
51
+ description: str
52
+ parameters: dict[str, Any]
53
+
54
+
55
+ class OpenAIToolDef(TypedDict):
56
+ """Type definition for OpenAI tool definition."""
57
+
58
+ type: str
59
+ function: OpenAIFunctionDef
60
+
61
+
62
+ class OpenAIAdapter(BaseAdapter[OpenAIToolType]):
63
+ """Adapter for OpenAI tools."""
64
+
65
+ def __init__(self, tool_spec: ToolSpec) -> None:
66
+ """Initialize the adapter.
67
+
68
+ Args:
69
+ tool_spec: The tool specification
70
+ """
71
+ super().__init__(tool_spec)
72
+ if not HAS_OPENAI:
73
+ raise ImportError(
74
+ "OpenAI package is required for OpenAI adapter. "
75
+ "Install it with `pip install agent_toolkit[openai]`."
76
+ )
77
+
78
+ def to_tool(self) -> Any:
79
+ """Convert to OpenAI tool format.
80
+
81
+ This method tries to use the OpenAI Agents SDK if available,
82
+ falling back to the standard OpenAI function calling format if not.
83
+
84
+ Returns:
85
+ OpenAI tool specification or Agents SDK FunctionTool
86
+ """
87
+ if HAS_OPENAI and _RuntimeOpenAIFunctionTool is not _FallbackOpenAIFunctionTool:
88
+ return self.to_agents_tool()
89
+ else:
90
+ return self.to_standard_tool()
91
+
92
+ def to_standard_tool(self) -> OpenAIToolDef:
93
+ """Convert to standard OpenAI function tool format.
94
+
95
+ Returns:
96
+ OpenAI function calling specification
97
+ """
98
+ params_schema = (
99
+ self.tool_spec.input_schema
100
+ if self.tool_spec.input_schema
101
+ else {"type": "object", "properties": {}}
102
+ )
103
+
104
+ return {
105
+ "type": "function",
106
+ "function": {
107
+ "name": self.tool_spec.name,
108
+ "description": self.tool_spec.description,
109
+ "parameters": params_schema,
110
+ },
111
+ }
112
+
113
+ def to_agents_tool(self) -> OpenAIFunctionTool:
114
+ """Convert to OpenAI Agents SDK FunctionTool.
115
+
116
+ Returns:
117
+ An OpenAI Agents SDK FunctionTool
118
+ """
119
+ original_func = self.tool_spec.function
120
+
121
+ async def on_invoke_tool(ctx: Any, input_str: str) -> Any:
122
+ """Function that invokes the tool with parameters."""
123
+ try:
124
+ params = json.loads(input_str) if input_str else {}
125
+ result = original_func(**params)
126
+ return result
127
+ except Exception as e:
128
+ return f"Error executing tool: {str(e)}"
129
+
130
+ params_json_schema_dict = (
131
+ self.tool_spec.input_schema
132
+ if self.tool_spec.input_schema
133
+ else {"type": "object", "properties": {}}
134
+ )
135
+
136
+ return _RuntimeOpenAIFunctionTool(
137
+ name=self.tool_spec.name,
138
+ description=self.tool_spec.description,
139
+ params_json_schema=params_json_schema_dict,
140
+ on_invoke_tool=on_invoke_tool,
141
+ strict_json_schema=True,
142
+ )
143
+
144
+ def to_callable(self) -> Callable:
145
+ """Get the callable for OpenAI function calling.
146
+
147
+ Returns:
148
+ The callable function
149
+ """
150
+ return self.tool_spec.function