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.
@@ -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"