serpapi-search-tools 0.1.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.
- serpapi_search_tools/__init__.py +48 -0
- serpapi_search_tools/_adapters.py +540 -0
- serpapi_search_tools/_providers.py +118 -0
- serpapi_search_tools/_query_tools.py +725 -0
- serpapi_search_tools/_shared.py +267 -0
- serpapi_search_tools/_travel_tools.py +584 -0
- serpapi_search_tools/py.typed +1 -0
- serpapi_search_tools-0.1.0.dist-info/METADATA +466 -0
- serpapi_search_tools-0.1.0.dist-info/RECORD +11 -0
- serpapi_search_tools-0.1.0.dist-info/WHEEL +4 -0
- serpapi_search_tools-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Intent-specific SerpApi tools for Python agent SDKs."""
|
|
2
|
+
|
|
3
|
+
from serpapi_search_tools._query_tools import (
|
|
4
|
+
ShoppingSearchEngine,
|
|
5
|
+
WebSearchEngine,
|
|
6
|
+
images_search,
|
|
7
|
+
maps_search,
|
|
8
|
+
news_search,
|
|
9
|
+
shopping_search,
|
|
10
|
+
videos_search,
|
|
11
|
+
web_search,
|
|
12
|
+
)
|
|
13
|
+
from serpapi_search_tools._shared import SearchResultMode, SerpApiSearchError
|
|
14
|
+
from serpapi_search_tools._travel_tools import (
|
|
15
|
+
TravelClass,
|
|
16
|
+
flights_search,
|
|
17
|
+
hotels_search,
|
|
18
|
+
travel_explore_search,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_SUPPORTED_ENGINES_BY_TOOL: dict[str, tuple[str, ...]] = {
|
|
22
|
+
"web_search": tuple(engine.value for engine in WebSearchEngine),
|
|
23
|
+
"news_search": ("google_news",),
|
|
24
|
+
"maps_search": ("google_maps",),
|
|
25
|
+
"images_search": ("google_images",),
|
|
26
|
+
"shopping_search": tuple(engine.value for engine in ShoppingSearchEngine),
|
|
27
|
+
"videos_search": ("youtube",),
|
|
28
|
+
"hotels_search": ("google_hotels",),
|
|
29
|
+
"flights_search": ("google_flights",),
|
|
30
|
+
"travel_explore_search": ("google_travel_explore",),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"SearchResultMode",
|
|
35
|
+
"SerpApiSearchError",
|
|
36
|
+
"ShoppingSearchEngine",
|
|
37
|
+
"TravelClass",
|
|
38
|
+
"WebSearchEngine",
|
|
39
|
+
"flights_search",
|
|
40
|
+
"hotels_search",
|
|
41
|
+
"images_search",
|
|
42
|
+
"maps_search",
|
|
43
|
+
"news_search",
|
|
44
|
+
"shopping_search",
|
|
45
|
+
"travel_explore_search",
|
|
46
|
+
"videos_search",
|
|
47
|
+
"web_search",
|
|
48
|
+
]
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Callable, Mapping
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from types import GenericAlias
|
|
11
|
+
from typing import Annotated, Any, Literal, cast, get_type_hints
|
|
12
|
+
|
|
13
|
+
from serpapi_search_tools._shared import (
|
|
14
|
+
PROVIDER_ALIASES,
|
|
15
|
+
ProviderName,
|
|
16
|
+
ToolDefinition,
|
|
17
|
+
detect_provider,
|
|
18
|
+
normalize_provider,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_TOOL_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
|
22
|
+
_TOOL_INPUT_ERROR_LIMIT = 500
|
|
23
|
+
_PYTHON_VERSION = sys.version_info[:2]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _dependency_error(extra: str, package: str, exc: ImportError) -> ImportError:
|
|
27
|
+
message = (
|
|
28
|
+
f"{package} is required for this adapter. Install it with "
|
|
29
|
+
f"`pip install 'serpapi-search-tools[{extra}]'`."
|
|
30
|
+
)
|
|
31
|
+
error = ImportError(message)
|
|
32
|
+
error.__cause__ = exc
|
|
33
|
+
return error
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _direct_dependency_error(package: str, exc: ImportError) -> ImportError:
|
|
37
|
+
error = ImportError(
|
|
38
|
+
f"{package} is required for this adapter. Install it with `pip install {package}`."
|
|
39
|
+
)
|
|
40
|
+
error.__cause__ = exc
|
|
41
|
+
return error
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _validate_tool_name(name: str) -> None:
|
|
45
|
+
if not _TOOL_NAME_PATTERN.fullmatch(name):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"Tool names must contain 1 to 64 ASCII letters, digits, underscores, or hyphens."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _allows_none(
|
|
52
|
+
name: str,
|
|
53
|
+
schema: Mapping[str, Any],
|
|
54
|
+
required: set[str],
|
|
55
|
+
) -> bool:
|
|
56
|
+
return name not in required and schema.get("default") is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _with_tool_metadata(definition: ToolDefinition) -> Callable[..., str]:
|
|
60
|
+
function = cast(Any, definition.function)
|
|
61
|
+
function.__name__ = definition.name
|
|
62
|
+
function.__qualname__ = definition.name
|
|
63
|
+
function.__doc__ = definition.description
|
|
64
|
+
function.__annotations__ = get_type_hints(function, include_extras=True)
|
|
65
|
+
return cast(Callable[..., str], function)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _pydantic_args_model(
|
|
69
|
+
definition: ToolDefinition,
|
|
70
|
+
*,
|
|
71
|
+
extra: Literal["allow", "ignore", "forbid"] = "forbid",
|
|
72
|
+
) -> Any:
|
|
73
|
+
try:
|
|
74
|
+
from pydantic import ConfigDict, Field, create_model
|
|
75
|
+
except ImportError as exc:
|
|
76
|
+
raise _direct_dependency_error("pydantic", exc) from exc
|
|
77
|
+
|
|
78
|
+
properties = cast(Mapping[str, Mapping[str, Any]], definition.input_schema["properties"])
|
|
79
|
+
required = set(cast(list[str], definition.input_schema.get("required", [])))
|
|
80
|
+
fields: dict[str, tuple[Any, Any]] = {}
|
|
81
|
+
for name, schema in properties.items():
|
|
82
|
+
base_type = _pydantic_type_from_schema(name, schema, Field)
|
|
83
|
+
field_type = base_type
|
|
84
|
+
if _allows_none(name, schema, required):
|
|
85
|
+
field_type = field_type | None
|
|
86
|
+
default = ... if name in required else schema.get("default")
|
|
87
|
+
if default is not None and isinstance(base_type, type) and issubclass(base_type, Enum):
|
|
88
|
+
default = base_type(default)
|
|
89
|
+
constraints = _pydantic_field_constraints(schema)
|
|
90
|
+
schema_extra = {"format": schema["format"]} if "format" in schema else None
|
|
91
|
+
fields[name] = (
|
|
92
|
+
field_type,
|
|
93
|
+
Field(
|
|
94
|
+
default,
|
|
95
|
+
description=schema.get("description"),
|
|
96
|
+
json_schema_extra=schema_extra,
|
|
97
|
+
**constraints,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
model_name = f"{''.join(part.title() for part in definition.name.split('_'))}Arguments"
|
|
101
|
+
model_factory = cast(Callable[..., Any], create_model)
|
|
102
|
+
return model_factory(
|
|
103
|
+
model_name,
|
|
104
|
+
__config__=ConfigDict(
|
|
105
|
+
extra=extra,
|
|
106
|
+
json_schema_extra={"additionalProperties": False},
|
|
107
|
+
),
|
|
108
|
+
**fields,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _pydantic_field_constraints(schema: Mapping[str, Any]) -> dict[str, Any]:
|
|
113
|
+
return {
|
|
114
|
+
key: schema[source]
|
|
115
|
+
for key, source in (
|
|
116
|
+
("ge", "minimum"),
|
|
117
|
+
("le", "maximum"),
|
|
118
|
+
("min_length", "minLength"),
|
|
119
|
+
("max_length", "maxLength"),
|
|
120
|
+
)
|
|
121
|
+
if source in schema
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _pydantic_type_from_schema(
|
|
126
|
+
name: str,
|
|
127
|
+
schema: Mapping[str, Any],
|
|
128
|
+
field_factory: Callable[..., Any],
|
|
129
|
+
) -> Any:
|
|
130
|
+
if schema.get("type") != "array":
|
|
131
|
+
return _python_type_from_schema(name, schema)
|
|
132
|
+
|
|
133
|
+
item_schema = cast(Mapping[str, Any], schema.get("items", {}))
|
|
134
|
+
item_type = _python_type_from_schema(f"{name}_item", item_schema)
|
|
135
|
+
item_constraints = _pydantic_field_constraints(item_schema)
|
|
136
|
+
item_extra = {"format": item_schema["format"]} if "format" in item_schema else None
|
|
137
|
+
if item_constraints or item_extra or item_schema.get("description"):
|
|
138
|
+
item_type = Annotated[
|
|
139
|
+
item_type,
|
|
140
|
+
field_factory(
|
|
141
|
+
description=item_schema.get("description"),
|
|
142
|
+
json_schema_extra=item_extra,
|
|
143
|
+
**item_constraints,
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
return list[item_type]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _with_pydantic_annotations(definition: ToolDefinition) -> Callable[..., str]:
|
|
150
|
+
try:
|
|
151
|
+
from pydantic import Field
|
|
152
|
+
except ImportError as exc:
|
|
153
|
+
raise _direct_dependency_error("pydantic", exc) from exc
|
|
154
|
+
|
|
155
|
+
function = _with_tool_metadata(definition)
|
|
156
|
+
properties = cast(Mapping[str, Mapping[str, Any]], definition.input_schema["properties"])
|
|
157
|
+
required = set(cast(list[str], definition.input_schema.get("required", [])))
|
|
158
|
+
annotations: dict[str, Any] = {}
|
|
159
|
+
for name, schema in properties.items():
|
|
160
|
+
field_type = _pydantic_type_from_schema(name, schema, Field)
|
|
161
|
+
if _allows_none(name, schema, required):
|
|
162
|
+
field_type = field_type | None
|
|
163
|
+
schema_extra = {"format": schema["format"]} if "format" in schema else None
|
|
164
|
+
annotations[name] = Annotated[
|
|
165
|
+
field_type,
|
|
166
|
+
str(schema.get("description") or name),
|
|
167
|
+
Field(
|
|
168
|
+
description=schema.get("description"),
|
|
169
|
+
json_schema_extra=schema_extra,
|
|
170
|
+
**_pydantic_field_constraints(schema),
|
|
171
|
+
),
|
|
172
|
+
]
|
|
173
|
+
annotations["return"] = str
|
|
174
|
+
function.__annotations__ = annotations
|
|
175
|
+
return function
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _python_type_from_schema(name: str, schema: Mapping[str, Any]) -> Any:
|
|
179
|
+
enum_values = schema.get("enum")
|
|
180
|
+
if isinstance(enum_values, list) and enum_values:
|
|
181
|
+
enum_name = f"{''.join(part.title() for part in name.split('_'))}Value"
|
|
182
|
+
return Enum(
|
|
183
|
+
enum_name,
|
|
184
|
+
{str(value).upper(): value for value in enum_values},
|
|
185
|
+
type=str,
|
|
186
|
+
)
|
|
187
|
+
schema_type = schema.get("type")
|
|
188
|
+
if schema_type == "string":
|
|
189
|
+
return str
|
|
190
|
+
if schema_type == "integer":
|
|
191
|
+
return int
|
|
192
|
+
if schema_type == "number":
|
|
193
|
+
return float
|
|
194
|
+
if schema_type == "boolean":
|
|
195
|
+
return bool
|
|
196
|
+
if schema_type == "object":
|
|
197
|
+
return dict[str, Any]
|
|
198
|
+
if schema_type == "array":
|
|
199
|
+
items = cast(Mapping[str, Any], schema.get("items", {}))
|
|
200
|
+
return GenericAlias(list, _python_type_from_schema(f"{name}_item", items))
|
|
201
|
+
return Any
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def as_function_tool(definition: ToolDefinition) -> Callable[..., str]:
|
|
205
|
+
return _with_tool_metadata(definition)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def as_langchain_tool(definition: ToolDefinition) -> Any:
|
|
209
|
+
try:
|
|
210
|
+
from langchain_core.tools import StructuredTool
|
|
211
|
+
except ImportError as exc:
|
|
212
|
+
raise _dependency_error("langchain", "langchain-core", exc) from exc
|
|
213
|
+
|
|
214
|
+
return StructuredTool.from_function(
|
|
215
|
+
func=definition.function,
|
|
216
|
+
name=definition.name,
|
|
217
|
+
description=definition.description,
|
|
218
|
+
args_schema=_pydantic_args_model(definition),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def as_crewai_tool(definition: ToolDefinition) -> Any:
|
|
223
|
+
try:
|
|
224
|
+
from crewai.tools import BaseTool
|
|
225
|
+
except ImportError as exc:
|
|
226
|
+
if _PYTHON_VERSION >= (3, 14):
|
|
227
|
+
raise ImportError(
|
|
228
|
+
"The CrewAI adapter supports Python 3.10 through 3.13. "
|
|
229
|
+
"Use a supported Python version or choose another provider."
|
|
230
|
+
) from exc
|
|
231
|
+
raise _dependency_error("crewai", "crewai", exc) from exc
|
|
232
|
+
|
|
233
|
+
function = _with_tool_metadata(definition)
|
|
234
|
+
|
|
235
|
+
def _run(self: Any, **kwargs: Any) -> str:
|
|
236
|
+
return function(**kwargs)
|
|
237
|
+
|
|
238
|
+
tool_type = type("SerpApiCrewTool", (BaseTool,), {"_run": _run})
|
|
239
|
+
return tool_type(
|
|
240
|
+
name=definition.name,
|
|
241
|
+
description=definition.description,
|
|
242
|
+
args_schema=_pydantic_args_model(definition, extra="ignore"),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def as_llamaindex_tool(definition: ToolDefinition) -> Any:
|
|
247
|
+
try:
|
|
248
|
+
from llama_index.core.tools import FunctionTool
|
|
249
|
+
except ImportError as exc:
|
|
250
|
+
raise _dependency_error("llamaindex", "llama-index-core", exc) from exc
|
|
251
|
+
|
|
252
|
+
return FunctionTool.from_defaults(
|
|
253
|
+
fn=definition.function,
|
|
254
|
+
name=definition.name,
|
|
255
|
+
description=definition.description,
|
|
256
|
+
fn_schema=_pydantic_args_model(definition),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def as_openai_agents_tool(definition: ToolDefinition) -> Any:
|
|
261
|
+
try:
|
|
262
|
+
from agents import FunctionTool
|
|
263
|
+
except ImportError as exc:
|
|
264
|
+
raise _dependency_error("openai-agents", "openai-agents", exc) from exc
|
|
265
|
+
|
|
266
|
+
arguments_model = _pydantic_args_model(definition)
|
|
267
|
+
|
|
268
|
+
async def invoke_tool(context: Any, arguments_json: str) -> str:
|
|
269
|
+
try:
|
|
270
|
+
validated = arguments_model.model_validate_json(arguments_json or "{}")
|
|
271
|
+
except ValueError as exc:
|
|
272
|
+
return _invalid_tool_arguments(exc)
|
|
273
|
+
arguments = validated.model_dump(exclude_unset=True)
|
|
274
|
+
try:
|
|
275
|
+
return await asyncio.to_thread(definition.function, **arguments)
|
|
276
|
+
except ValueError as exc:
|
|
277
|
+
return _invalid_tool_arguments(exc)
|
|
278
|
+
|
|
279
|
+
return FunctionTool(
|
|
280
|
+
name=definition.name,
|
|
281
|
+
description=definition.description,
|
|
282
|
+
params_json_schema=definition.input_schema,
|
|
283
|
+
on_invoke_tool=invoke_tool,
|
|
284
|
+
strict_json_schema=False,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _invalid_tool_arguments(exc: ValueError) -> str:
|
|
289
|
+
details: list[str] = []
|
|
290
|
+
errors_method = getattr(exc, "errors", None)
|
|
291
|
+
if callable(errors_method):
|
|
292
|
+
try:
|
|
293
|
+
errors = errors_method(include_url=False, include_input=False)
|
|
294
|
+
except TypeError:
|
|
295
|
+
errors = errors_method()
|
|
296
|
+
for error in errors[:3]:
|
|
297
|
+
location = ".".join(str(part) for part in error.get("loc", ()))
|
|
298
|
+
message = str(error.get("msg", "invalid value"))
|
|
299
|
+
details.append(f"{location}: {message}" if location else message)
|
|
300
|
+
if not details:
|
|
301
|
+
details.append(str(exc))
|
|
302
|
+
detail = " ".join(" ".join(part.split()) for part in details)
|
|
303
|
+
if len(detail) > _TOOL_INPUT_ERROR_LIMIT:
|
|
304
|
+
detail = f"{detail[: _TOOL_INPUT_ERROR_LIMIT - 3]}..."
|
|
305
|
+
return json.dumps(
|
|
306
|
+
{"error": "Invalid tool arguments.", "details": detail},
|
|
307
|
+
separators=(",", ":"),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def as_claude_agent_sdk_tool(definition: ToolDefinition) -> Any:
|
|
312
|
+
try:
|
|
313
|
+
from claude_agent_sdk import ToolAnnotations, tool
|
|
314
|
+
except ImportError as exc:
|
|
315
|
+
raise _dependency_error("claude-agent-sdk", "claude-agent-sdk", exc) from exc
|
|
316
|
+
|
|
317
|
+
async def run_tool(args: dict[str, Any]) -> dict[str, list[dict[str, str]]]:
|
|
318
|
+
result = await asyncio.to_thread(definition.function, **args)
|
|
319
|
+
return {"content": [{"type": "text", "text": result}]}
|
|
320
|
+
|
|
321
|
+
return tool(
|
|
322
|
+
definition.name,
|
|
323
|
+
definition.description,
|
|
324
|
+
definition.input_schema,
|
|
325
|
+
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
326
|
+
)(run_tool)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def as_autogen_tool(definition: ToolDefinition) -> Any:
|
|
330
|
+
try:
|
|
331
|
+
from autogen_core.tools import FunctionTool
|
|
332
|
+
except ImportError as exc:
|
|
333
|
+
raise _dependency_error("autogen", "autogen-core", exc) from exc
|
|
334
|
+
|
|
335
|
+
native_tool = FunctionTool(
|
|
336
|
+
_with_tool_metadata(definition),
|
|
337
|
+
name=definition.name,
|
|
338
|
+
description=definition.description,
|
|
339
|
+
)
|
|
340
|
+
native_tool._args_type = _pydantic_args_model(definition)
|
|
341
|
+
return native_tool
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def as_microsoft_agent_framework_tool(definition: ToolDefinition) -> Any:
|
|
345
|
+
try:
|
|
346
|
+
from agent_framework import FunctionTool
|
|
347
|
+
except ImportError as exc:
|
|
348
|
+
raise _dependency_error(
|
|
349
|
+
"microsoft-agent-framework",
|
|
350
|
+
"agent-framework-core",
|
|
351
|
+
exc,
|
|
352
|
+
) from exc
|
|
353
|
+
|
|
354
|
+
return FunctionTool(
|
|
355
|
+
name=definition.name,
|
|
356
|
+
description=definition.description,
|
|
357
|
+
func=definition.function,
|
|
358
|
+
input_model=definition.input_schema,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def as_pydantic_ai_tool(definition: ToolDefinition) -> Any:
|
|
363
|
+
try:
|
|
364
|
+
from pydantic_ai.tools import Tool
|
|
365
|
+
except ImportError as exc:
|
|
366
|
+
raise _dependency_error("pydantic-ai", "pydantic-ai", exc) from exc
|
|
367
|
+
|
|
368
|
+
return Tool(
|
|
369
|
+
_with_pydantic_annotations(definition),
|
|
370
|
+
name=definition.name,
|
|
371
|
+
description=definition.description,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def as_haystack_tool(definition: ToolDefinition) -> Any:
|
|
376
|
+
try:
|
|
377
|
+
from haystack.tools import Tool
|
|
378
|
+
except ImportError as exc:
|
|
379
|
+
raise _dependency_error("haystack", "haystack-ai", exc) from exc
|
|
380
|
+
|
|
381
|
+
return Tool(
|
|
382
|
+
name=definition.name,
|
|
383
|
+
description=definition.description,
|
|
384
|
+
parameters=definition.input_schema,
|
|
385
|
+
function=definition.function,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def as_semantic_kernel_tool(definition: ToolDefinition) -> Any:
|
|
390
|
+
try:
|
|
391
|
+
from semantic_kernel.functions import kernel_function
|
|
392
|
+
except ImportError as exc:
|
|
393
|
+
raise _dependency_error("semantic-kernel", "semantic-kernel", exc) from exc
|
|
394
|
+
|
|
395
|
+
function = _with_tool_metadata(definition)
|
|
396
|
+
|
|
397
|
+
def invoke(**kwargs: Any) -> str:
|
|
398
|
+
return function(**kwargs)
|
|
399
|
+
|
|
400
|
+
decorated = kernel_function(name=definition.name, description=definition.description)(invoke)
|
|
401
|
+
properties = cast(Mapping[str, Mapping[str, Any]], definition.input_schema["properties"])
|
|
402
|
+
required = set(cast(list[str], definition.input_schema.get("required", [])))
|
|
403
|
+
parameters: list[dict[str, Any]] = []
|
|
404
|
+
for field_name, property_schema in properties.items():
|
|
405
|
+
type_object = _python_type_from_schema(field_name, property_schema)
|
|
406
|
+
parameter: dict[str, Any] = {
|
|
407
|
+
"name": field_name,
|
|
408
|
+
"description": property_schema.get("description", ""),
|
|
409
|
+
"type_": getattr(type_object, "__name__", str(type_object)),
|
|
410
|
+
"type_object": type_object,
|
|
411
|
+
"is_required": field_name in required,
|
|
412
|
+
"schema_data": dict(property_schema),
|
|
413
|
+
}
|
|
414
|
+
if field_name not in required:
|
|
415
|
+
parameter["default_value"] = property_schema.get("default")
|
|
416
|
+
parameters.append(parameter)
|
|
417
|
+
|
|
418
|
+
decorated.__kernel_function_parameters__ = parameters
|
|
419
|
+
decorated.__signature__ = inspect.signature(function)
|
|
420
|
+
decorated.__annotations__ = function.__annotations__
|
|
421
|
+
return decorated
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def as_agno_tool(definition: ToolDefinition) -> Any:
|
|
425
|
+
try:
|
|
426
|
+
from agno.tools import Function
|
|
427
|
+
except ImportError as exc:
|
|
428
|
+
raise _dependency_error("agno", "agno", exc) from exc
|
|
429
|
+
|
|
430
|
+
function = _with_tool_metadata(definition)
|
|
431
|
+
return Function(
|
|
432
|
+
name=definition.name,
|
|
433
|
+
description=definition.description,
|
|
434
|
+
parameters=definition.input_schema,
|
|
435
|
+
entrypoint=function,
|
|
436
|
+
strict=False,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _smolagents_inputs(schema: Mapping[str, Any]) -> dict[str, dict[str, Any]]:
|
|
441
|
+
properties = cast(Mapping[str, Mapping[str, Any]], schema.get("properties", {}))
|
|
442
|
+
required = set(cast(list[str], schema.get("required", [])))
|
|
443
|
+
inputs: dict[str, dict[str, Any]] = {}
|
|
444
|
+
for field_name, property_schema in properties.items():
|
|
445
|
+
converted = dict(property_schema)
|
|
446
|
+
converted.pop("additionalProperties", None)
|
|
447
|
+
if field_name not in required:
|
|
448
|
+
converted["nullable"] = True
|
|
449
|
+
inputs[field_name] = converted
|
|
450
|
+
return inputs
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def as_smolagents_tool(definition: ToolDefinition) -> Any:
|
|
454
|
+
try:
|
|
455
|
+
from smolagents import Tool
|
|
456
|
+
except ImportError as exc:
|
|
457
|
+
raise _dependency_error("smolagents", "smolagents", exc) from exc
|
|
458
|
+
|
|
459
|
+
def forward(self: Any, **kwargs: Any) -> str:
|
|
460
|
+
return definition.function(**kwargs)
|
|
461
|
+
|
|
462
|
+
original_signature = inspect.signature(definition.function)
|
|
463
|
+
self_parameter = inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
464
|
+
cast(Any, forward).__signature__ = original_signature.replace(
|
|
465
|
+
parameters=[self_parameter, *original_signature.parameters.values()]
|
|
466
|
+
)
|
|
467
|
+
cast(Any, forward).__annotations__ = {
|
|
468
|
+
"self": Any,
|
|
469
|
+
**definition.function.__annotations__,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
tool_type = type(
|
|
473
|
+
"SerpApiSmolagentsTool",
|
|
474
|
+
(Tool,),
|
|
475
|
+
{
|
|
476
|
+
"name": definition.name,
|
|
477
|
+
"description": definition.description,
|
|
478
|
+
"inputs": _smolagents_inputs(definition.input_schema),
|
|
479
|
+
"output_type": "string",
|
|
480
|
+
"forward": forward,
|
|
481
|
+
},
|
|
482
|
+
)
|
|
483
|
+
return tool_type()
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def as_google_adk_tool(definition: ToolDefinition) -> Any:
|
|
487
|
+
try:
|
|
488
|
+
from google.adk.tools import FunctionTool
|
|
489
|
+
except ImportError as exc:
|
|
490
|
+
raise _dependency_error("google-adk", "google-adk", exc) from exc
|
|
491
|
+
|
|
492
|
+
class SerpApiFunctionTool(FunctionTool):
|
|
493
|
+
def _get_declaration(self: Any) -> Any:
|
|
494
|
+
declaration = super()._get_declaration()
|
|
495
|
+
if declaration is not None:
|
|
496
|
+
declaration.parameters_json_schema = definition.input_schema
|
|
497
|
+
return declaration
|
|
498
|
+
|
|
499
|
+
return SerpApiFunctionTool(_with_tool_metadata(definition))
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def as_provider_tool(
|
|
503
|
+
definition: ToolDefinition,
|
|
504
|
+
provider: ProviderName | str = "auto",
|
|
505
|
+
) -> Any:
|
|
506
|
+
"""Adapt a provider-neutral definition into one SDK-native tool."""
|
|
507
|
+
|
|
508
|
+
_validate_tool_name(definition.name)
|
|
509
|
+
definition = ToolDefinition(
|
|
510
|
+
function=_with_tool_metadata(definition),
|
|
511
|
+
name=definition.name,
|
|
512
|
+
description=definition.description,
|
|
513
|
+
input_schema=definition.input_schema,
|
|
514
|
+
)
|
|
515
|
+
normalized = normalize_provider(provider)
|
|
516
|
+
if normalized == "auto":
|
|
517
|
+
normalized = detect_provider()
|
|
518
|
+
|
|
519
|
+
adapters: dict[str, Callable[[ToolDefinition], Any]] = {
|
|
520
|
+
"function": as_function_tool,
|
|
521
|
+
"langchain": as_langchain_tool,
|
|
522
|
+
"langgraph": as_langchain_tool,
|
|
523
|
+
"crewai": as_crewai_tool,
|
|
524
|
+
"llamaindex": as_llamaindex_tool,
|
|
525
|
+
"openai-agents": as_openai_agents_tool,
|
|
526
|
+
"claude-agent-sdk": as_claude_agent_sdk_tool,
|
|
527
|
+
"pydantic-ai": as_pydantic_ai_tool,
|
|
528
|
+
"microsoft-agent-framework": as_microsoft_agent_framework_tool,
|
|
529
|
+
"autogen": as_autogen_tool,
|
|
530
|
+
"haystack": as_haystack_tool,
|
|
531
|
+
"semantic-kernel": as_semantic_kernel_tool,
|
|
532
|
+
"agno": as_agno_tool,
|
|
533
|
+
"smolagents": as_smolagents_tool,
|
|
534
|
+
"google-adk": as_google_adk_tool,
|
|
535
|
+
}
|
|
536
|
+
adapter = adapters.get(normalized)
|
|
537
|
+
if adapter is None:
|
|
538
|
+
supported = ", ".join(PROVIDER_ALIASES)
|
|
539
|
+
raise ValueError(f"Unknown provider '{provider}'. Supported providers: {supported}.")
|
|
540
|
+
return adapter(definition)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal, TypeAlias
|
|
6
|
+
|
|
7
|
+
ProviderName: TypeAlias = Literal[
|
|
8
|
+
"auto",
|
|
9
|
+
"function",
|
|
10
|
+
"langchain",
|
|
11
|
+
"langgraph",
|
|
12
|
+
"crewai",
|
|
13
|
+
"llamaindex",
|
|
14
|
+
"openai-agents",
|
|
15
|
+
"claude-agent-sdk",
|
|
16
|
+
"pydantic-ai",
|
|
17
|
+
"microsoft-agent-framework",
|
|
18
|
+
"autogen",
|
|
19
|
+
"haystack",
|
|
20
|
+
"semantic-kernel",
|
|
21
|
+
"agno",
|
|
22
|
+
"smolagents",
|
|
23
|
+
"google-adk",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ProviderSpec:
|
|
29
|
+
"""Metadata needed to register and auto-detect one agent SDK adapter."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
aliases: tuple[str, ...]
|
|
33
|
+
distribution: str | None
|
|
34
|
+
extra: str | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
PROVIDER_SPECS: tuple[ProviderSpec, ...] = (
|
|
38
|
+
ProviderSpec("langgraph", (), "langgraph", "langgraph"),
|
|
39
|
+
ProviderSpec("langchain", (), "langchain-core", "langchain"),
|
|
40
|
+
ProviderSpec("crewai", (), "crewai", "crewai"),
|
|
41
|
+
ProviderSpec("llamaindex", ("llama_index", "llama-index"), "llama-index-core", "llamaindex"),
|
|
42
|
+
ProviderSpec(
|
|
43
|
+
"openai-agents",
|
|
44
|
+
("openai_agents",),
|
|
45
|
+
"openai-agents",
|
|
46
|
+
"openai-agents",
|
|
47
|
+
),
|
|
48
|
+
ProviderSpec(
|
|
49
|
+
"claude-agent-sdk",
|
|
50
|
+
(
|
|
51
|
+
"claude_agent_sdk",
|
|
52
|
+
"claude-sdk",
|
|
53
|
+
"claude_agent",
|
|
54
|
+
"anthropic-agent-sdk",
|
|
55
|
+
"anthropic_agent_sdk",
|
|
56
|
+
"anthropic-agents",
|
|
57
|
+
"anthropic_agents",
|
|
58
|
+
),
|
|
59
|
+
"claude-agent-sdk",
|
|
60
|
+
"claude-agent-sdk",
|
|
61
|
+
),
|
|
62
|
+
ProviderSpec("pydantic-ai", ("pydantic_ai",), "pydantic-ai", "pydantic-ai"),
|
|
63
|
+
ProviderSpec(
|
|
64
|
+
"microsoft-agent-framework",
|
|
65
|
+
("agent-framework", "agent_framework", "microsoft_agent_framework", "maf"),
|
|
66
|
+
"agent-framework-core",
|
|
67
|
+
"microsoft-agent-framework",
|
|
68
|
+
),
|
|
69
|
+
ProviderSpec("autogen", (), "autogen-core", "autogen"),
|
|
70
|
+
ProviderSpec("haystack", (), "haystack-ai", "haystack"),
|
|
71
|
+
ProviderSpec(
|
|
72
|
+
"semantic-kernel",
|
|
73
|
+
("semantic_kernel",),
|
|
74
|
+
"semantic-kernel",
|
|
75
|
+
"semantic-kernel",
|
|
76
|
+
),
|
|
77
|
+
ProviderSpec("agno", (), "agno", "agno"),
|
|
78
|
+
ProviderSpec("smolagents", ("smol-agents",), "smolagents", "smolagents"),
|
|
79
|
+
ProviderSpec("google-adk", ("google_adk", "adk"), "google-adk", "google-adk"),
|
|
80
|
+
ProviderSpec("function", ("plain", "callable"), None, None),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
PROVIDER_ALIASES: dict[str, str] = {"auto": "auto"}
|
|
84
|
+
for _spec in PROVIDER_SPECS:
|
|
85
|
+
PROVIDER_ALIASES[_spec.name] = _spec.name
|
|
86
|
+
PROVIDER_ALIASES.update(dict.fromkeys(_spec.aliases, _spec.name))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def normalize_provider(provider: str) -> str:
|
|
90
|
+
normalized = provider.strip().lower()
|
|
91
|
+
if normalized in PROVIDER_ALIASES:
|
|
92
|
+
return PROVIDER_ALIASES[normalized]
|
|
93
|
+
return PROVIDER_ALIASES.get(normalized.replace("_", "-"), normalized)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def detect_provider() -> str:
|
|
97
|
+
installed: list[str] = []
|
|
98
|
+
for spec in PROVIDER_SPECS:
|
|
99
|
+
if spec.distribution is None:
|
|
100
|
+
continue
|
|
101
|
+
try:
|
|
102
|
+
importlib.metadata.version(spec.distribution)
|
|
103
|
+
except importlib.metadata.PackageNotFoundError:
|
|
104
|
+
continue
|
|
105
|
+
installed.append(spec.name)
|
|
106
|
+
families = {
|
|
107
|
+
"langchain" if provider in {"langchain", "langgraph"} else provider
|
|
108
|
+
for provider in installed
|
|
109
|
+
}
|
|
110
|
+
if len(families) > 1:
|
|
111
|
+
choices = ", ".join(installed)
|
|
112
|
+
raise RuntimeError(
|
|
113
|
+
f"Multiple supported agent SDKs are installed ({choices}). "
|
|
114
|
+
"Pass provider=... explicitly."
|
|
115
|
+
)
|
|
116
|
+
if installed:
|
|
117
|
+
return installed[0]
|
|
118
|
+
return "function"
|