arcade-mcp/toolkits/reddit/arcade_reddit/tools/read.py
Eric Gustin 6af49ef068
Common changes in all toolkits (#345)
Addresses general improvements to all toolkits including changing ruff
from python 3.9 to python 3.10 which is the reason for the removal of
Optional[] among others.

Also, turns out that our `make install` for toolkits wasn't correctly
checking for whether poetry was installed (&> /dev/null syntax isn't
supported by our check-toolkits GitHub action, so we were installing
poetry twice. I replaced with the more portable >/dev/null 2>&1)

Question: Should we also change ruff to py310 for the `arcade/` package
in a later PR?

-------------------

CU-86b4gzyp6
2025-04-04 09:32:37 -07:00

125 lines
4.7 KiB
Python

from typing import Annotated
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Reddit
from arcade_reddit.client import RedditClient
from arcade_reddit.enums import (
RedditTimeFilter,
SubredditListingType,
)
from arcade_reddit.utils import (
create_fullname_for_multiple_posts,
create_path_for_post,
normalize_subreddit_name,
parse_get_content_of_multiple_posts_response,
parse_get_content_of_post_response,
parse_get_posts_in_subreddit_response,
parse_get_top_level_comments_response,
remove_none_values,
)
@tool(requires_auth=Reddit(scopes=["read"]))
async def get_posts_in_subreddit(
context: ToolContext,
subreddit: Annotated[str, "The name of the subreddit to fetch posts from"],
listing: Annotated[
SubredditListingType,
(
"The type of listing to fetch. For simple listings such as 'hot', 'new', or 'rising', "
"the 'time_range' parameter is ignored. For time-based listings such as "
"'top' or 'controversial', the 'time_range' parameter is required."
),
] = SubredditListingType.HOT,
limit: Annotated[int, "The maximum number of posts to fetch. Default is 10, max is 100."] = 10,
cursor: Annotated[str | None, "The pagination token from a previous call"] = None,
time_range: Annotated[
RedditTimeFilter,
"The time range for filtering posts. Must be provided if the listing type is "
f"{SubredditListingType.TOP.value} or {SubredditListingType.CONTROVERSIAL.value}. "
f"Otherwise, it is ignored. Defaults to {RedditTimeFilter.TODAY.value}.",
] = RedditTimeFilter.TODAY,
) -> Annotated[dict, "A dictionary with a cursor for the next page and a list of posts"]:
"""Gets posts titles, links, and other metadata in the specified subreddit
The time_range is required if the listing type is 'top' or 'controversial'.
"""
client = RedditClient(context.get_auth_token_or_empty())
params = {"limit": limit, "after": cursor}
if listing.is_time_based():
params["t"] = time_range.to_api_value()
params = remove_none_values(params)
subreddit = normalize_subreddit_name(subreddit)
data = await client.get(f"r/{subreddit}/{listing.value}", params=params)
result = parse_get_posts_in_subreddit_response(data)
return result
@tool(requires_auth=Reddit(scopes=["read"]))
async def get_content_of_post(
context: ToolContext,
post_identifier: Annotated[
str,
"The identifier of the Reddit post. "
"The identifier may be a reddit URL to the post, a permalink to the post, "
"a fullname for the post, or a post id.",
],
) -> Annotated[dict, "The content (body) of the Reddit post"]:
"""Get the content (body) of a Reddit post by its identifier."""
client = RedditClient(context.get_auth_token_or_empty())
path = create_path_for_post(post_identifier)
data = await client.get(f"{path}.json")
result = parse_get_content_of_post_response(data)
return result
@tool(requires_auth=Reddit(scopes=["read"]))
async def get_content_of_multiple_posts(
context: ToolContext,
post_identifiers: Annotated[
list[str],
"A list of Reddit post identifiers. "
"The identifiers may be reddit URLs to the posts, permalinks to the posts, "
"fullnames for the posts, or post ids. Must be less than or equal to 100 identifiers.",
],
) -> Annotated[dict, "A dictionary containing the content of multiple Reddit posts"]:
"""Get the content (body) of multiple Reddit posts by their identifiers.
Efficiently retrieve the content of multiple posts in a single request.
Always use this tool to retrieve more than one post's content.
"""
client = RedditClient(context.get_auth_token_or_empty())
fullnames, warnings = create_fullname_for_multiple_posts(post_identifiers)
data = await client.get("api/info.json", params={"id": ",".join(fullnames)})
posts = parse_get_content_of_multiple_posts_response(data)
return {"posts": posts, "warnings": warnings}
@tool(requires_auth=Reddit(scopes=["read"]))
async def get_top_level_comments(
context: ToolContext,
post_identifier: Annotated[
str,
"The identifier of the Reddit post to fetch comments from. "
"The identifier may be a reddit URL, a permalink, a fullname, or a post id.",
],
) -> Annotated[dict, "A dictionary with a list of top level comments"]:
"""Get the first page of top-level comments of a Reddit post."""
client = RedditClient(context.get_auth_token_or_empty())
path = create_path_for_post(post_identifier)
data = await client.get(f"{path}.json")
result = parse_get_top_level_comments_response(data)
return result