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,271 @@
1
+ """Decorators for creating tool specifications."""
2
+
3
+ import functools
4
+ import inspect
5
+ from collections.abc import Callable
6
+ from typing import Any, Protocol, TypedDict, TypeVar, cast
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from glean.agent_toolkit.registry import get_registry
11
+ from glean.agent_toolkit.spec import ToolSpec
12
+
13
+
14
+ class InputSchema(TypedDict):
15
+ """JSON Schema for tool input."""
16
+
17
+ type: str
18
+ properties: dict[str, Any]
19
+ required: list[str]
20
+
21
+
22
+ CallableT = Callable[..., Any]
23
+
24
+
25
+ class ToolSpecFunction(Protocol):
26
+ """Protocol for functions decorated with tool_spec."""
27
+
28
+ tool_spec: ToolSpec
29
+
30
+ def as_openai_tool(self) -> dict[str, Any] | Any:
31
+ """Convert to OpenAI tool format.
32
+
33
+ Returns:
34
+ OpenAI tool specification
35
+ """
36
+ ...
37
+
38
+ def as_adk_tool(self) -> Any:
39
+ """Convert to Google ADK tool format.
40
+
41
+ Returns:
42
+ Google ADK tool
43
+ """
44
+ ...
45
+
46
+ def as_langchain_tool(self) -> Any:
47
+ """Convert to LangChain tool format.
48
+
49
+ Returns:
50
+ LangChain tool
51
+ """
52
+ ...
53
+
54
+ def as_crewai_tool(self) -> Any:
55
+ """Convert to CrewAI tool format.
56
+
57
+ Returns:
58
+ CrewAI tool
59
+ """
60
+ ...
61
+
62
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
63
+ """Call the function.
64
+
65
+ Args:
66
+ *args: Positional arguments
67
+ **kwargs: Keyword arguments
68
+
69
+ Returns:
70
+ Function result
71
+ """
72
+ ...
73
+
74
+ __name__: str
75
+
76
+
77
+ def tool_spec(
78
+ name: str,
79
+ description: str,
80
+ output_model: type[BaseModel] | None = None,
81
+ version: str | None = None,
82
+ ) -> Callable[[CallableT], ToolSpecFunction]:
83
+ """Decorator for registering a function as a tool.
84
+
85
+ Args:
86
+ name: Name of the tool
87
+ description: Description of the tool
88
+ output_model: Optional Pydantic model for the output
89
+ version: Optional version string
90
+
91
+ Returns:
92
+ Decorated function with tool spec attached
93
+ """
94
+
95
+ def decorator(func: CallableT) -> ToolSpecFunction:
96
+ """Decorator function.
97
+
98
+ Args:
99
+ func: Function to decorate
100
+
101
+ Returns:
102
+ Decorated function
103
+ """
104
+ sig = inspect.signature(func)
105
+ params = {}
106
+ out_type = None
107
+
108
+ for param_name, param in sig.parameters.items():
109
+ if param.annotation != inspect.Parameter.empty:
110
+ params[param_name] = param.annotation
111
+
112
+ if sig.return_annotation != inspect.Signature.empty:
113
+ out_type = sig.return_annotation
114
+
115
+ input_schema: InputSchema = {
116
+ "type": "object",
117
+ "properties": {},
118
+ "required": [],
119
+ }
120
+
121
+ required_fields: list[str] = []
122
+
123
+ for param_name, param in sig.parameters.items():
124
+ if param.default is param.empty:
125
+ required_fields.append(param_name)
126
+
127
+ input_schema["required"] = required_fields
128
+
129
+ if params:
130
+ for param_name, param_type in params.items():
131
+ if isinstance(param_type, type) and issubclass(param_type, str):
132
+ input_schema["properties"][param_name] = {"type": "string"}
133
+ elif isinstance(param_type, type) and issubclass(param_type, int):
134
+ input_schema["properties"][param_name] = {"type": "integer"}
135
+ elif isinstance(param_type, type) and issubclass(param_type, float):
136
+ input_schema["properties"][param_name] = {"type": "number"}
137
+ elif isinstance(param_type, type) and issubclass(param_type, bool):
138
+ input_schema["properties"][param_name] = {"type": "boolean"}
139
+ elif param_type is list or param_type is list[str]:
140
+ input_schema["properties"][param_name] = {
141
+ "type": "array",
142
+ "items": {"type": "string"},
143
+ }
144
+ elif param_type is list[int]:
145
+ input_schema["properties"][param_name] = {
146
+ "type": "array",
147
+ "items": {"type": "integer"},
148
+ }
149
+ else:
150
+ input_schema["properties"][param_name] = {"type": "string"}
151
+
152
+ output_schema: dict[str, Any] = {"type": "object"}
153
+ if out_type is not None and hasattr(out_type, "model_json_schema"):
154
+ output_schema = out_type.model_json_schema()
155
+ elif out_type is int:
156
+ output_schema = {"type": "integer"}
157
+ elif out_type is float:
158
+ output_schema = {"type": "number"}
159
+ elif out_type is bool:
160
+ output_schema = {"type": "boolean"}
161
+ elif out_type is str:
162
+ output_schema = {"type": "string"}
163
+ elif out_type is list or out_type is list[str]:
164
+ output_schema = {
165
+ "type": "array",
166
+ "items": {"type": "string"},
167
+ }
168
+ elif out_type is list[int]:
169
+ output_schema = {
170
+ "type": "array",
171
+ "items": {"type": "integer"},
172
+ }
173
+
174
+ tool_spec_obj = ToolSpec(
175
+ name=name,
176
+ description=description,
177
+ function=func,
178
+ input_schema=cast(dict[str, Any], input_schema),
179
+ output_schema=output_schema,
180
+ version=version,
181
+ output_model=(
182
+ output_model
183
+ if isinstance(output_model, type) and issubclass(output_model, BaseModel)
184
+ else None
185
+ ),
186
+ )
187
+
188
+ get_registry().register(tool_spec_obj)
189
+
190
+ @functools.wraps(func)
191
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
192
+ """Wrapper that preserves the original function's call semantics.
193
+
194
+ Args:
195
+ *args: Positional arguments
196
+ **kwargs: Keyword arguments
197
+
198
+ Returns:
199
+ The result of the function call
200
+ """
201
+ return func(*args, **kwargs)
202
+
203
+ def as_openai_tool() -> dict[str, Any] | Any:
204
+ """Convert to OpenAI tool format.
205
+
206
+ Returns:
207
+ OpenAI tool specification
208
+ """
209
+ from glean.agent_toolkit.adapters.openai import OpenAIAdapter
210
+
211
+ adapter = tool_spec_obj.get_adapter("openai")
212
+ if adapter is None:
213
+ adapter = OpenAIAdapter(tool_spec_obj)
214
+ tool_spec_obj.set_adapter("openai", adapter)
215
+
216
+ return adapter.to_tool()
217
+
218
+ def as_adk_tool() -> Any:
219
+ """Convert to Google ADK tool format.
220
+
221
+ Returns:
222
+ Google ADK tool
223
+ """
224
+ from glean.agent_toolkit.adapters.adk import ADKAdapter
225
+
226
+ adapter = tool_spec_obj.get_adapter("adk")
227
+ if adapter is None:
228
+ adapter = ADKAdapter(tool_spec_obj)
229
+ tool_spec_obj.set_adapter("adk", adapter)
230
+
231
+ return adapter.to_tool()
232
+
233
+ def as_langchain_tool() -> Any:
234
+ """Convert to LangChain tool format.
235
+
236
+ Returns:
237
+ LangChain tool
238
+ """
239
+ from glean.agent_toolkit.adapters.langchain import LangChainAdapter
240
+
241
+ adapter = tool_spec_obj.get_adapter("langchain")
242
+ if adapter is None:
243
+ adapter = LangChainAdapter(tool_spec_obj)
244
+ tool_spec_obj.set_adapter("langchain", adapter)
245
+
246
+ return adapter.to_tool()
247
+
248
+ def as_crewai_tool() -> Any:
249
+ """Convert to CrewAI tool format.
250
+
251
+ Returns:
252
+ CrewAI tool
253
+ """
254
+ from glean.agent_toolkit.adapters.crewai import CrewAIAdapter
255
+
256
+ adapter = tool_spec_obj.get_adapter("crewai")
257
+ if adapter is None:
258
+ adapter = CrewAIAdapter(tool_spec_obj)
259
+ tool_spec_obj.set_adapter("crewai", adapter)
260
+
261
+ return adapter.to_tool()
262
+
263
+ wrapper.as_openai_tool = as_openai_tool # type: ignore
264
+ wrapper.as_adk_tool = as_adk_tool # type: ignore
265
+ wrapper.as_langchain_tool = as_langchain_tool # type: ignore
266
+ wrapper.as_crewai_tool = as_crewai_tool # type: ignore
267
+ wrapper.tool_spec = tool_spec_obj # type: ignore
268
+
269
+ return cast(ToolSpecFunction, wrapper)
270
+
271
+ return decorator
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,50 @@
1
+ """Tool registry for managing and retrieving tool specifications."""
2
+
3
+ from glean.agent_toolkit.spec import ToolSpec
4
+
5
+
6
+ class Registry:
7
+ """Registry for tool specifications."""
8
+
9
+ def __init__(self) -> None:
10
+ """Initialize the registry."""
11
+ self._tools: dict[str, ToolSpec] = {}
12
+
13
+ def register(self, tool_spec: ToolSpec) -> None:
14
+ """Register a tool specification.
15
+
16
+ Args:
17
+ tool_spec: The tool specification to register
18
+ """
19
+ self._tools[tool_spec.name] = tool_spec
20
+
21
+ def get(self, name: str) -> ToolSpec | None:
22
+ """Get a tool specification by name.
23
+
24
+ Args:
25
+ name: The name of the tool
26
+
27
+ Returns:
28
+ The tool specification, or None if not found
29
+ """
30
+ return self._tools.get(name)
31
+
32
+ def list(self) -> list[ToolSpec]:
33
+ """List all registered tool specifications.
34
+
35
+ Returns:
36
+ List of all registered tool specifications
37
+ """
38
+ return list(self._tools.values())
39
+
40
+
41
+ _REGISTRY = Registry()
42
+
43
+
44
+ def get_registry() -> Registry:
45
+ """Get the global registry instance.
46
+
47
+ Returns:
48
+ The global registry instance
49
+ """
50
+ return _REGISTRY
@@ -0,0 +1,51 @@
1
+ """Tool specification dataclass."""
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ @dataclass
11
+ class ToolSpec:
12
+ """Specification for a tool.
13
+
14
+ Attributes:
15
+ name: The name of the tool
16
+ description: A description of what the tool does
17
+ function: The function that implements the tool
18
+ input_schema: JSON schema for the input parameters
19
+ output_schema: JSON schema for the output value
20
+ version: Optional version string
21
+ output_model: Optional pydantic model for the output
22
+ """
23
+
24
+ name: str
25
+ description: str
26
+ function: Callable
27
+ input_schema: dict[str, Any]
28
+ output_schema: dict[str, Any]
29
+ version: str | None = None
30
+ output_model: type[BaseModel] | None = None
31
+ _adapters: dict[str, Any] = field(default_factory=dict)
32
+
33
+ def get_adapter(self, name: str) -> Any | None:
34
+ """Get a cached adapter instance.
35
+
36
+ Args:
37
+ name: The name of the adapter
38
+
39
+ Returns:
40
+ The adapter instance
41
+ """
42
+ return self._adapters.get(name)
43
+
44
+ def set_adapter(self, name: str, adapter: Any) -> None:
45
+ """Set an adapter instance.
46
+
47
+ Args:
48
+ name: The name of the adapter
49
+ adapter: The adapter instance
50
+ """
51
+ self._adapters[name] = adapter
@@ -0,0 +1,43 @@
1
+ """
2
+ Each tool lives in its own module under :pymod:`glean.agent_toolkit.tools`.
3
+
4
+ Importing this package will load all available tools.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib import import_module as _import_module
10
+
11
+ _tool_modules: list[str] = [
12
+ "glean_search",
13
+ "web_search",
14
+ "ai_web_search",
15
+ "calendar_search",
16
+ "employee_search",
17
+ "code_search",
18
+ "gmail_search",
19
+ "outlook_search",
20
+ ]
21
+
22
+ for _mod in _tool_modules:
23
+ _import_module(f"{__name__}.{_mod}")
24
+
25
+ from .ai_web_search import ai_web_search # noqa: E402
26
+ from .calendar_search import calendar_search # noqa: E402
27
+ from .code_search import code_search # noqa: E402
28
+ from .employee_search import employee_search # noqa: E402
29
+ from .glean_search import glean_search # noqa: E402
30
+ from .gmail_search import gmail_search # noqa: E402
31
+ from .outlook_search import outlook_search # noqa: E402
32
+ from .web_search import web_search # noqa: E402
33
+
34
+ __all__: list[str] = [
35
+ "glean_search",
36
+ "web_search",
37
+ "ai_web_search",
38
+ "calendar_search",
39
+ "employee_search",
40
+ "code_search",
41
+ "gmail_search",
42
+ "outlook_search",
43
+ ]
@@ -0,0 +1,36 @@
1
+ """Shared helpers for built-in stub tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+ from glean.api_client import Glean, models
9
+
10
+
11
+ def api_client() -> Glean:
12
+ """Get the Glean API client."""
13
+ instance = os.getenv("GLEAN_INSTANCE")
14
+ api_token = os.getenv("GLEAN_API_TOKEN")
15
+
16
+ if not api_token or not instance:
17
+ raise ValueError("GLEAN_API_TOKEN and GLEAN_INSTANCE environment variables are required")
18
+
19
+ return Glean(api_token=api_token, instance=instance)
20
+
21
+
22
+ def run_tool(
23
+ tool_display_name: str,
24
+ parameters: dict[str, models.ToolsCallParameter],
25
+ ) -> dict[str, Any]:
26
+ """Execute a Glean stub tool and wrap the response."""
27
+ try:
28
+ with api_client() as g_client:
29
+ result = g_client.client.tools.run(
30
+ name=tool_display_name,
31
+ parameters=parameters,
32
+ )
33
+
34
+ return {"result": result}
35
+ except Exception as exc:
36
+ return {"error": str(exc), "result": None}
@@ -0,0 +1,40 @@
1
+ """AI Web Search tool for searching the internet with AI-powered results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from glean.agent_toolkit.decorators import tool_spec
8
+ from glean.agent_toolkit.tools._common import run_tool
9
+ from glean.api_client import models
10
+
11
+
12
+ @tool_spec(
13
+ name="ai_web_search",
14
+ description=(
15
+ "Searches the web for up-to-date external information and generates a well structured "
16
+ "response to the user query grounded in relevant webpages. Closely evaluate the "
17
+ "instructions below for the user query to decide whether to use the web agent. If you "
18
+ "think the scenarios are contradictory, do not use the web agent unless there is clear "
19
+ "user intent. If you think the user query is ambiguous, use the Conversation history to "
20
+ "infer the context.\n"
21
+ "INSTRUCTIONS:\n"
22
+ "Examples of when to use this tool:\n"
23
+ "- User Intent: Use this tool if the user is asking you to search the web, look online, "
24
+ "provide links/sources, or explicitly looking for current external information (outside "
25
+ "of the company) like weather, news, or latest updates/plans.\n"
26
+ "- Freshness: Use this tool if you need up-to-date information on time-dependent topics "
27
+ "or any time you would otherwise refuse to answer a question because your knowledge might "
28
+ "be out of date.\n"
29
+ "- Niche Information: If the answer would likely change based on detailed information not "
30
+ "widely known or understood (which might be found on the internet), use web sources "
31
+ "directly rather than relying on the distilled knowledge from pretraining.\n"
32
+ "- Do NOT use this tool for programming related queries. You already know enough about "
33
+ "those.\n"
34
+ "- Do NOT use this tool for queries seeking general ideas, which may not benefit from "
35
+ "specific information."
36
+ ),
37
+ )
38
+ def ai_web_search(parameters: dict[str, models.ToolsCallParameter]) -> dict[str, Any]:
39
+ """Search the web for up-to-date external information."""
40
+ return run_tool("Gemini Web Search", parameters)
@@ -0,0 +1,18 @@
1
+ """Calendar Search tool for searching calendar events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from glean.agent_toolkit.decorators import tool_spec
8
+ from glean.agent_toolkit.tools._common import run_tool
9
+ from glean.api_client import models
10
+
11
+
12
+ @tool_spec(
13
+ name="calendar_search",
14
+ description="Searches over all the calendar meetings of the company.",
15
+ )
16
+ def calendar_search(parameters: dict[str, models.ToolsCallParameter]) -> dict[str, Any]:
17
+ """Search the calendar for meetings."""
18
+ return run_tool("Meeting Lookup", parameters)
@@ -0,0 +1,29 @@
1
+ """Code Search tool for searching code repositories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from glean.agent_toolkit.decorators import tool_spec
8
+ from glean.agent_toolkit.tools._common import run_tool
9
+ from glean.api_client import models
10
+
11
+
12
+ @tool_spec(
13
+ name="code_search",
14
+ description=(
15
+ "Searches over all code changes made in the company.\n"
16
+ "INSTRUCTIONS:\n"
17
+ "- Use this tool to help users find information in or about code, add new code, etc. "
18
+ "Prefer including code snippets in your response.\n"
19
+ "- This is your primary tool to access knowledge present in the company's code "
20
+ "repositories.\n"
21
+ "- The results returned are not exhaustive; we only return the top few most relevant "
22
+ "results to a query."
23
+ ),
24
+ )
25
+ def code_search(
26
+ parameters: dict[str, models.ToolsCallParameter],
27
+ ) -> dict[str, Any]:
28
+ """Search code repositories based on the query."""
29
+ return run_tool("Code Search", parameters)
@@ -0,0 +1,33 @@
1
+ """Employee Search tool for finding people profiles in the company."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from glean.agent_toolkit.decorators import tool_spec
8
+ from glean.agent_toolkit.tools._common import run_tool
9
+ from glean.api_client import models
10
+
11
+
12
+ @tool_spec(
13
+ name="employee_search",
14
+ description=(
15
+ "Finds people at the company based on their personal information.\n"
16
+ "INSTRUCTIONS:\n"
17
+ "- Only use this when the user explicitly wants to find people in the company (e.g.,"
18
+ ' "who" questions) or for aggregation queries on people.\n'
19
+ "- You can also use this tool to find personal information about employees (e.g., what is "
20
+ "person X's phone number or email address).\n"
21
+ "- Do not use this when the user wants to find people outside of the company, or people "
22
+ "who are no longer at the company.\n"
23
+ "- You can find people based on details such as name, email, title, department, and "
24
+ "location.\n"
25
+ "- The results returned are not exhaustive; we only return the top few most relevant "
26
+ "people to a query.\n"
27
+ '- For analytics questions such as "how many people..." use the "statistics" field '
28
+ "in the output."
29
+ ),
30
+ )
31
+ def employee_search(parameters: dict[str, models.ToolsCallParameter]) -> dict[str, Any]:
32
+ """Search for employees based on the query."""
33
+ return run_tool("Employee Search", parameters)
@@ -0,0 +1,26 @@
1
+ """Glean Search tool for searching company documents and data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from glean.agent_toolkit.decorators import tool_spec
8
+ from glean.agent_toolkit.tools._common import run_tool
9
+ from glean.api_client import models
10
+
11
+
12
+ @tool_spec(
13
+ name="glean_search",
14
+ description=(
15
+ "Finds relevant documents in the company.\n"
16
+ "INSTRUCTIONS:\n"
17
+ "- This is your primary tool to access all knowledge within the company.\n"
18
+ "- The results returned are not exhaustive; we only return the top few most relevant "
19
+ "documents to a query.\n"
20
+ '- For analytics questions such as "how many documents..." use the "statistics" '
21
+ "field in the output."
22
+ ),
23
+ )
24
+ def glean_search(parameters: dict[str, models.ToolsCallParameter]) -> dict[str, Any]:
25
+ """Search Glean for relevant documents using the query."""
26
+ return run_tool("Glean Search", parameters)
@@ -0,0 +1,23 @@
1
+ """Gmail Search tool for searching email messages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from glean.agent_toolkit.decorators import tool_spec
8
+ from glean.agent_toolkit.tools._common import run_tool
9
+ from glean.api_client import models
10
+
11
+
12
+ @tool_spec(
13
+ name="gmail_search",
14
+ description=(
15
+ "Finds relevant emails in the user's mailbox.\n"
16
+ "- Only use this tool if the user asks for email.\n"
17
+ "- Results returned are not exhaustive; we can only return the top 10 emails sorted by "
18
+ "recency (most recent first)."
19
+ ),
20
+ )
21
+ def gmail_search(parameters: dict[str, models.ToolsCallParameter]) -> dict[str, Any]:
22
+ """Search Gmail messages based on the query."""
23
+ return run_tool("Gmail Search", parameters)
@@ -0,0 +1,23 @@
1
+ """Outlook Search tool for searching email messages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from glean.agent_toolkit.decorators import tool_spec
8
+ from glean.agent_toolkit.tools._common import run_tool
9
+ from glean.api_client import models
10
+
11
+
12
+ @tool_spec(
13
+ name="outlook_search",
14
+ description=(
15
+ "Finds relevant emails in the user's mailbox.\n"
16
+ "- Only use this tool if the user asks for email.\n"
17
+ "- Results returned are not exhaustive; we can only return the top 10 emails sorted by "
18
+ "recency (most recent first)."
19
+ ),
20
+ )
21
+ def outlook_search(parameters: dict[str, models.ToolsCallParameter]) -> dict[str, Any]:
22
+ """Search Outlook messages based on the query."""
23
+ return run_tool("Outlook Search", parameters)