YouTube Search tools (#316)
This commit is contained in:
parent
a3371d71ac
commit
2338e823cf
3 changed files with 248 additions and 4 deletions
|
|
@ -2,18 +2,38 @@ import os
|
||||||
|
|
||||||
from arcade_search.enums import GoogleMapsDistanceUnit, GoogleMapsTravelMode
|
from arcade_search.enums import GoogleMapsDistanceUnit, GoogleMapsTravelMode
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# Google default constants
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
DEFAULT_GOOGLE_LANGUAGE = os.getenv("ARCADE_GOOGLE_LANGUAGE", "en")
|
DEFAULT_GOOGLE_LANGUAGE = os.getenv("ARCADE_GOOGLE_LANGUAGE", "en")
|
||||||
|
DEFAULT_GOOGLE_COUNTRY = os.getenv("ARCADE_GOOGLE_COUNTRY")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# Google News default constants
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
DEFAULT_GOOGLE_NEWS_LANGUAGE = os.getenv("ARCADE_GOOGLE_NEWS_LANGUAGE", DEFAULT_GOOGLE_LANGUAGE)
|
DEFAULT_GOOGLE_NEWS_LANGUAGE = os.getenv("ARCADE_GOOGLE_NEWS_LANGUAGE", DEFAULT_GOOGLE_LANGUAGE)
|
||||||
DEFAULT_GOOGLE_NEWS_COUNTRY = os.getenv("ARCADE_GOOGLE_NEWS_COUNTRY", None)
|
DEFAULT_GOOGLE_NEWS_COUNTRY = os.getenv("ARCADE_GOOGLE_NEWS_COUNTRY")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# Google Jobs default constants
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
DEFAULT_GOOGLE_JOBS_LANGUAGE = os.getenv("ARCADE_GOOGLE_JOBS_LANGUAGE", DEFAULT_GOOGLE_LANGUAGE)
|
DEFAULT_GOOGLE_JOBS_LANGUAGE = os.getenv("ARCADE_GOOGLE_JOBS_LANGUAGE", DEFAULT_GOOGLE_LANGUAGE)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# Google Maps default constants
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
DEFAULT_GOOGLE_MAPS_LANGUAGE = os.getenv("ARCADE_GOOGLE_MAPS_LANGUAGE", DEFAULT_GOOGLE_LANGUAGE)
|
DEFAULT_GOOGLE_MAPS_LANGUAGE = os.getenv("ARCADE_GOOGLE_MAPS_LANGUAGE", DEFAULT_GOOGLE_LANGUAGE)
|
||||||
DEFAULT_GOOGLE_MAPS_COUNTRY = os.getenv("ARCADE_GOOGLE_MAPS_COUNTRY", None)
|
DEFAULT_GOOGLE_MAPS_COUNTRY = os.getenv("ARCADE_GOOGLE_MAPS_COUNTRY")
|
||||||
DEFAULT_GOOGLE_MAPS_DISTANCE_UNIT = GoogleMapsDistanceUnit(
|
DEFAULT_GOOGLE_MAPS_DISTANCE_UNIT = GoogleMapsDistanceUnit(
|
||||||
os.getenv("ARCADE_GOOGLE_MAPS_DISTANCE_UNIT", GoogleMapsDistanceUnit.KM.value)
|
os.getenv("ARCADE_GOOGLE_MAPS_DISTANCE_UNIT", GoogleMapsDistanceUnit.KM.value)
|
||||||
)
|
)
|
||||||
DEFAULT_GOOGLE_MAPS_TRAVEL_MODE = GoogleMapsTravelMode(
|
DEFAULT_GOOGLE_MAPS_TRAVEL_MODE = GoogleMapsTravelMode(
|
||||||
os.getenv("ARCADE_GOOGLE_MAPS_TRAVEL_MODE", GoogleMapsTravelMode.BEST.value)
|
os.getenv("ARCADE_GOOGLE_MAPS_TRAVEL_MODE", GoogleMapsTravelMode.BEST.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# YouTube default constants
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
YOUTUBE_MAX_DESCRIPTION_LENGTH = 500
|
||||||
|
DEFAULT_YOUTUBE_SEARCH_LANGUAGE = os.getenv("ARCADE_YOUTUBE_SEARCH_LANGUAGE")
|
||||||
|
DEFAULT_YOUTUBE_SEARCH_COUNTRY = os.getenv("ARCADE_YOUTUBE_SEARCH_COUNTRY")
|
||||||
|
|
|
||||||
101
toolkits/search/arcade_search/tools/youtube.py
Normal file
101
toolkits/search/arcade_search/tools/youtube.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
from typing import Annotated, Any, Optional, cast
|
||||||
|
|
||||||
|
from arcade.sdk import ToolContext, tool
|
||||||
|
from arcade.sdk.errors import ToolExecutionError
|
||||||
|
|
||||||
|
from arcade_search.constants import DEFAULT_YOUTUBE_SEARCH_COUNTRY, DEFAULT_YOUTUBE_SEARCH_LANGUAGE
|
||||||
|
from arcade_search.utils import (
|
||||||
|
call_serpapi,
|
||||||
|
default_country_code,
|
||||||
|
default_language_code,
|
||||||
|
extract_video_details,
|
||||||
|
extract_video_results,
|
||||||
|
prepare_params,
|
||||||
|
resolve_country_code,
|
||||||
|
resolve_language_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@tool(requires_secrets=["SERP_API_KEY"])
|
||||||
|
async def search_youtube_videos(
|
||||||
|
context: ToolContext,
|
||||||
|
keywords: Annotated[
|
||||||
|
str,
|
||||||
|
"The keywords to search for. E.g. 'Python tutorial'.",
|
||||||
|
],
|
||||||
|
language_code: Annotated[
|
||||||
|
Optional[str],
|
||||||
|
"2-character language code to search for. E.g. 'en' for English. "
|
||||||
|
f"Defaults to '{default_language_code(DEFAULT_YOUTUBE_SEARCH_LANGUAGE)}'.",
|
||||||
|
] = None,
|
||||||
|
country_code: Annotated[
|
||||||
|
Optional[str],
|
||||||
|
"2-character country code to search for. E.g. 'us' for United States. "
|
||||||
|
f"Defaults to '{default_country_code(DEFAULT_YOUTUBE_SEARCH_COUNTRY)}'.",
|
||||||
|
] = None,
|
||||||
|
next_page_token: Annotated[
|
||||||
|
Optional[str],
|
||||||
|
"The next page token to use for pagination. "
|
||||||
|
"Defaults to `None` (start from the first page).",
|
||||||
|
] = None,
|
||||||
|
) -> Annotated[dict[str, Any], "List of YouTube videos related to the query."]:
|
||||||
|
"""Search for YouTube videos related to the query."""
|
||||||
|
language_code = resolve_language_code(language_code, DEFAULT_YOUTUBE_SEARCH_LANGUAGE)
|
||||||
|
country_code = resolve_country_code(country_code, DEFAULT_YOUTUBE_SEARCH_COUNTRY)
|
||||||
|
|
||||||
|
params = prepare_params(
|
||||||
|
"youtube",
|
||||||
|
search_query=keywords,
|
||||||
|
hl=language_code,
|
||||||
|
gl=country_code,
|
||||||
|
sp=next_page_token,
|
||||||
|
)
|
||||||
|
results = call_serpapi(context, params)
|
||||||
|
|
||||||
|
if results.get("error"):
|
||||||
|
error_msg = cast(str, results.get("error"))
|
||||||
|
raise ToolExecutionError(error_msg)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"videos": extract_video_results(results),
|
||||||
|
"next_page_token": results.get("serpapi_pagination", {}).get("next_page_token"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@tool(requires_secrets=["SERP_API_KEY"])
|
||||||
|
async def get_youtube_video_details(
|
||||||
|
context: ToolContext,
|
||||||
|
video_id: Annotated[
|
||||||
|
str,
|
||||||
|
"The ID of the YouTube video to get details about. E.g. 'dQw4w9WgXcQ'.",
|
||||||
|
],
|
||||||
|
language_code: Annotated[
|
||||||
|
Optional[str],
|
||||||
|
"2-character language code to search for. E.g. 'en' for English. "
|
||||||
|
f"Defaults to '{default_language_code(DEFAULT_YOUTUBE_SEARCH_LANGUAGE)}'.",
|
||||||
|
] = None,
|
||||||
|
country_code: Annotated[
|
||||||
|
Optional[str],
|
||||||
|
"2-character country code to search for. E.g. 'us' for United States. "
|
||||||
|
f"Defaults to '{default_country_code(DEFAULT_YOUTUBE_SEARCH_COUNTRY)}'.",
|
||||||
|
] = None,
|
||||||
|
) -> Annotated[dict[str, Any], "Details about a YouTube video."]:
|
||||||
|
"""Get details about a YouTube video."""
|
||||||
|
language_code = resolve_language_code(language_code, DEFAULT_YOUTUBE_SEARCH_LANGUAGE)
|
||||||
|
country_code = resolve_country_code(country_code, DEFAULT_YOUTUBE_SEARCH_COUNTRY)
|
||||||
|
|
||||||
|
params = prepare_params(
|
||||||
|
"youtube_video",
|
||||||
|
v=video_id,
|
||||||
|
hl=language_code,
|
||||||
|
gl=country_code,
|
||||||
|
)
|
||||||
|
results = call_serpapi(context, params)
|
||||||
|
|
||||||
|
if results.get("error"):
|
||||||
|
error_msg = cast(str, results.get("error"))
|
||||||
|
raise ToolExecutionError(error_msg)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"video": extract_video_details(results),
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import contextlib
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Optional, cast
|
from typing import Any, Optional, cast
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from arcade.sdk import ToolContext
|
from arcade.sdk import ToolContext
|
||||||
|
|
@ -9,10 +10,13 @@ from arcade.sdk.errors import ToolExecutionError
|
||||||
from serpapi import Client as SerpClient
|
from serpapi import Client as SerpClient
|
||||||
|
|
||||||
from arcade_search.constants import (
|
from arcade_search.constants import (
|
||||||
|
DEFAULT_GOOGLE_COUNTRY,
|
||||||
|
DEFAULT_GOOGLE_LANGUAGE,
|
||||||
DEFAULT_GOOGLE_MAPS_COUNTRY,
|
DEFAULT_GOOGLE_MAPS_COUNTRY,
|
||||||
DEFAULT_GOOGLE_MAPS_DISTANCE_UNIT,
|
DEFAULT_GOOGLE_MAPS_DISTANCE_UNIT,
|
||||||
DEFAULT_GOOGLE_MAPS_LANGUAGE,
|
DEFAULT_GOOGLE_MAPS_LANGUAGE,
|
||||||
DEFAULT_GOOGLE_MAPS_TRAVEL_MODE,
|
DEFAULT_GOOGLE_MAPS_TRAVEL_MODE,
|
||||||
|
YOUTUBE_MAX_DESCRIPTION_LENGTH,
|
||||||
)
|
)
|
||||||
from arcade_search.enums import GoogleMapsDistanceUnit, GoogleMapsTravelMode
|
from arcade_search.enums import GoogleMapsDistanceUnit, GoogleMapsTravelMode
|
||||||
from arcade_search.exceptions import CountryNotFoundError, LanguageNotFoundError
|
from arcade_search.exceptions import CountryNotFoundError, LanguageNotFoundError
|
||||||
|
|
@ -64,6 +68,53 @@ def call_serpapi(context: ToolContext, params: dict) -> dict:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# Google general utils
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
def default_language_code(default_service_language_code: Optional[str] = None) -> Optional[str]:
|
||||||
|
if isinstance(default_service_language_code, str):
|
||||||
|
return default_service_language_code.lower()
|
||||||
|
elif isinstance(DEFAULT_GOOGLE_LANGUAGE, str):
|
||||||
|
return DEFAULT_GOOGLE_LANGUAGE.lower()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def default_country_code(default_service_country_code: Optional[str] = None) -> Optional[str]:
|
||||||
|
if isinstance(default_service_country_code, str):
|
||||||
|
return default_service_country_code.lower()
|
||||||
|
elif isinstance(DEFAULT_GOOGLE_COUNTRY, str):
|
||||||
|
return DEFAULT_GOOGLE_COUNTRY.lower()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_language_code(
|
||||||
|
language_code: Optional[str] = None,
|
||||||
|
default_service_language_code: Optional[str] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
language_code = language_code or default_language_code(default_service_language_code)
|
||||||
|
|
||||||
|
if isinstance(language_code, str):
|
||||||
|
language_code = language_code.lower()
|
||||||
|
if language_code not in LANGUAGE_CODES:
|
||||||
|
raise LanguageNotFoundError(language_code)
|
||||||
|
|
||||||
|
return language_code
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_country_code(
|
||||||
|
country_code: Optional[str] = None,
|
||||||
|
default_service_country_code: Optional[str] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
country_code = country_code or default_country_code(default_service_country_code)
|
||||||
|
|
||||||
|
if isinstance(country_code, str):
|
||||||
|
country_code = country_code.lower()
|
||||||
|
if country_code not in COUNTRY_CODES:
|
||||||
|
raise CountryNotFoundError(country_code)
|
||||||
|
|
||||||
|
return country_code
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------------------------
|
||||||
# Google Maps utils
|
# Google Maps utils
|
||||||
# ------------------------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
|
@ -75,7 +126,7 @@ def get_google_maps_directions(
|
||||||
origin_longitude: Optional[str] = None,
|
origin_longitude: Optional[str] = None,
|
||||||
destination_latitude: Optional[str] = None,
|
destination_latitude: Optional[str] = None,
|
||||||
destination_longitude: Optional[str] = None,
|
destination_longitude: Optional[str] = None,
|
||||||
language: str = DEFAULT_GOOGLE_MAPS_LANGUAGE,
|
language: Optional[str] = DEFAULT_GOOGLE_MAPS_LANGUAGE,
|
||||||
country: Optional[str] = DEFAULT_GOOGLE_MAPS_COUNTRY,
|
country: Optional[str] = DEFAULT_GOOGLE_MAPS_COUNTRY,
|
||||||
distance_unit: GoogleMapsDistanceUnit = DEFAULT_GOOGLE_MAPS_DISTANCE_UNIT,
|
distance_unit: GoogleMapsDistanceUnit = DEFAULT_GOOGLE_MAPS_DISTANCE_UNIT,
|
||||||
travel_mode: GoogleMapsTravelMode = DEFAULT_GOOGLE_MAPS_TRAVEL_MODE,
|
travel_mode: GoogleMapsTravelMode = DEFAULT_GOOGLE_MAPS_TRAVEL_MODE,
|
||||||
|
|
@ -104,7 +155,8 @@ def get_google_maps_directions(
|
||||||
Returns:
|
Returns:
|
||||||
The directions from Google Maps.
|
The directions from Google Maps.
|
||||||
"""
|
"""
|
||||||
language = language.lower()
|
if isinstance(language, str):
|
||||||
|
language = language.lower()
|
||||||
|
|
||||||
if language not in LANGUAGE_CODES:
|
if language not in LANGUAGE_CODES:
|
||||||
raise LanguageNotFoundError(language)
|
raise LanguageNotFoundError(language)
|
||||||
|
|
@ -222,3 +274,74 @@ def extract_news_results(
|
||||||
if limit:
|
if limit:
|
||||||
return news_results[:limit]
|
return news_results[:limit]
|
||||||
return news_results
|
return news_results
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# YouTube utils
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
def extract_video_id_from_link(link: Optional[str]) -> Optional[str]:
|
||||||
|
if not isinstance(link, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
parsed_url = urlparse(link)
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
return query_params.get("v", [""])[0]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_video_description(
|
||||||
|
video: dict[str, Any],
|
||||||
|
max_description_length: int = YOUTUBE_MAX_DESCRIPTION_LENGTH,
|
||||||
|
) -> Optional[str]:
|
||||||
|
description = video.get("description", "")
|
||||||
|
|
||||||
|
if isinstance(description, dict):
|
||||||
|
description = description.get("content", "")
|
||||||
|
|
||||||
|
if isinstance(description, str):
|
||||||
|
too_long = len(description) > max_description_length
|
||||||
|
if too_long:
|
||||||
|
description = description[:max_description_length] + " [truncated]"
|
||||||
|
|
||||||
|
if description is not None:
|
||||||
|
description = str(description).strip()
|
||||||
|
|
||||||
|
return cast(Optional[str], description)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_video_results(
|
||||||
|
results: dict[str, Any],
|
||||||
|
max_description_length: int = YOUTUBE_MAX_DESCRIPTION_LENGTH,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
videos = []
|
||||||
|
|
||||||
|
for video in results.get("video_results", []):
|
||||||
|
videos.append({
|
||||||
|
"id": extract_video_id_from_link(video.get("link")),
|
||||||
|
"title": video.get("title"),
|
||||||
|
"description": extract_video_description(video, max_description_length),
|
||||||
|
"link": video.get("link"),
|
||||||
|
"published_date": video.get("published_date"),
|
||||||
|
"duration": video.get("duration"),
|
||||||
|
"channel": {
|
||||||
|
"name": video.get("channel", {}).get("name"),
|
||||||
|
"link": video.get("channel", {}).get("link"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return videos
|
||||||
|
|
||||||
|
|
||||||
|
def extract_video_details(video: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": extract_video_id_from_link(video.get("link")),
|
||||||
|
"title": video.get("title"),
|
||||||
|
"description": extract_video_description(video, YOUTUBE_MAX_DESCRIPTION_LENGTH),
|
||||||
|
"published_date": video.get("published_date"),
|
||||||
|
"channel": {
|
||||||
|
"name": video.get("channel", {}).get("name"),
|
||||||
|
"link": video.get("channel", {}).get("link"),
|
||||||
|
},
|
||||||
|
"like_count": video.get("extracted_likes"),
|
||||||
|
"view_count": video.get("extracted_views"),
|
||||||
|
"live": video.get("live", False),
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue