arcade-x 0.0.13__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.
- arcade_x/__init__.py +0 -0
- arcade_x/tools/__init__.py +0 -0
- arcade_x/tools/tweets.py +159 -0
- arcade_x/tools/users.py +65 -0
- arcade_x/tools/utils.py +107 -0
- arcade_x-0.0.13.dist-info/METADATA +14 -0
- arcade_x-0.0.13.dist-info/RECORD +8 -0
- arcade_x-0.0.13.dist-info/WHEEL +4 -0
arcade_x/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
arcade_x/tools/tweets.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from arcade.core.errors import ToolExecutionError
|
|
6
|
+
from arcade.core.schema import ToolContext
|
|
7
|
+
from arcade.sdk import tool
|
|
8
|
+
from arcade.sdk.auth import X
|
|
9
|
+
from arcade_x.tools.utils import (
|
|
10
|
+
expand_urls_in_tweets,
|
|
11
|
+
get_tweet_url,
|
|
12
|
+
parse_search_recent_tweets_response,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
TWEETS_URL = "https://api.x.com/2/tweets"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Manage Tweets Tools. See developer docs for additional available parameters: https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference
|
|
19
|
+
@tool(
|
|
20
|
+
requires_auth=X(
|
|
21
|
+
scopes=["tweet.read", "tweet.write", "users.read"],
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
async def post_tweet(
|
|
25
|
+
context: ToolContext,
|
|
26
|
+
tweet_text: Annotated[str, "The text content of the tweet you want to post"],
|
|
27
|
+
) -> Annotated[str, "Success string and the URL of the tweet"]:
|
|
28
|
+
"""Post a tweet to X (Twitter)."""
|
|
29
|
+
|
|
30
|
+
headers = {
|
|
31
|
+
"Authorization": f"Bearer {context.authorization.token}",
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
}
|
|
34
|
+
payload = {"text": tweet_text}
|
|
35
|
+
|
|
36
|
+
async with httpx.AsyncClient() as client:
|
|
37
|
+
response = await client.post(TWEETS_URL, headers=headers, json=payload, timeout=10)
|
|
38
|
+
|
|
39
|
+
if response.status_code != 201:
|
|
40
|
+
raise ToolExecutionError(
|
|
41
|
+
f"Failed to post a tweet during execution of '{post_tweet.__name__}' tool. Request returned an error: {response.status_code} {response.text}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
tweet_id = response.json()["data"]["id"]
|
|
45
|
+
return f"Tweet with id {tweet_id} posted successfully. URL: {get_tweet_url(tweet_id)}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@tool(requires_auth=X(scopes=["tweet.read", "tweet.write", "users.read"]))
|
|
49
|
+
async def delete_tweet_by_id(
|
|
50
|
+
context: ToolContext,
|
|
51
|
+
tweet_id: Annotated[str, "The ID of the tweet you want to delete"],
|
|
52
|
+
) -> Annotated[str, "Success string confirming the tweet deletion"]:
|
|
53
|
+
"""Delete a tweet on X (Twitter)."""
|
|
54
|
+
|
|
55
|
+
headers = {"Authorization": f"Bearer {context.authorization.token}"}
|
|
56
|
+
url = f"{TWEETS_URL}/{tweet_id}"
|
|
57
|
+
|
|
58
|
+
async with httpx.AsyncClient() as client:
|
|
59
|
+
response = await client.delete(url, headers=headers, timeout=10)
|
|
60
|
+
|
|
61
|
+
if response.status_code != 200:
|
|
62
|
+
raise ToolExecutionError(
|
|
63
|
+
f"Failed to delete the tweet during execution of '{delete_tweet_by_id.__name__}' tool. Request returned an error: {response.status_code} {response.text}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return f"Tweet with id {tweet_id} deleted successfully."
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@tool(requires_auth=X(scopes=["tweet.read", "users.read"]))
|
|
70
|
+
async def search_recent_tweets_by_username(
|
|
71
|
+
context: ToolContext,
|
|
72
|
+
username: Annotated[str, "The username of the X (Twitter) user to look up"],
|
|
73
|
+
max_results: Annotated[
|
|
74
|
+
int, "The maximum number of results to return. Cannot be less than 10"
|
|
75
|
+
] = 10,
|
|
76
|
+
) -> Annotated[dict, "Dictionary containing the search results"]:
|
|
77
|
+
"""Search for recent tweets (last 7 days) on X (Twitter) by username. Includes replies and reposts."""
|
|
78
|
+
|
|
79
|
+
headers = {
|
|
80
|
+
"Authorization": f"Bearer {context.authorization.token}",
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
}
|
|
83
|
+
params = {
|
|
84
|
+
"query": f"from:{username}",
|
|
85
|
+
"max_results": max(max_results, 10), # X API does not allow 'max_results' less than 10
|
|
86
|
+
}
|
|
87
|
+
url = "https://api.x.com/2/tweets/search/recent?expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities"
|
|
88
|
+
|
|
89
|
+
async with httpx.AsyncClient() as client:
|
|
90
|
+
response = await client.get(url, headers=headers, params=params, timeout=10)
|
|
91
|
+
|
|
92
|
+
if response.status_code != 200:
|
|
93
|
+
raise ToolExecutionError(
|
|
94
|
+
f"Failed to search recent tweets during execution of '{search_recent_tweets_by_username.__name__}' tool. Request returned an error: {response.status_code} {response.text}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
response_data = response.json()
|
|
98
|
+
|
|
99
|
+
# Expand the urls that are in the tweets
|
|
100
|
+
expand_urls_in_tweets(response_data.get("data", []), delete_entities=True)
|
|
101
|
+
|
|
102
|
+
parse_search_recent_tweets_response(response_data)
|
|
103
|
+
|
|
104
|
+
return response_data
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@tool(requires_auth=X(scopes=["tweet.read", "users.read"]))
|
|
108
|
+
async def search_recent_tweets_by_keywords(
|
|
109
|
+
context: ToolContext,
|
|
110
|
+
keywords: Annotated[
|
|
111
|
+
list[str] | None, "List of keywords that must be present in the tweet"
|
|
112
|
+
] = None,
|
|
113
|
+
phrases: Annotated[
|
|
114
|
+
list[str] | None, "List of phrases that must be present in the tweet"
|
|
115
|
+
] = None,
|
|
116
|
+
max_results: Annotated[
|
|
117
|
+
int, "The maximum number of results to return. Cannot be less than 10"
|
|
118
|
+
] = 10,
|
|
119
|
+
) -> Annotated[dict, "Dictionary containing the search results"]:
|
|
120
|
+
"""
|
|
121
|
+
Search for recent tweets (last 7 days) on X (Twitter) by required keywords and phrases. Includes replies and reposts
|
|
122
|
+
One of the following input parametersMUST be provided: keywords, phrases
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
if not any([keywords, phrases]):
|
|
126
|
+
raise ValueError(
|
|
127
|
+
"At least one of keywords or phrases must be provided to the '{search_recent_tweets_by_keywords.__name__}' tool."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
headers = {
|
|
131
|
+
"Authorization": f"Bearer {context.authorization.token}",
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
}
|
|
134
|
+
query = "".join([f'"{phrase}" ' for phrase in (phrases or [])])
|
|
135
|
+
if keywords:
|
|
136
|
+
query += " ".join(keywords or [])
|
|
137
|
+
|
|
138
|
+
params = {
|
|
139
|
+
"query": query,
|
|
140
|
+
"max_results": max(max_results, 10), # X API does not allow 'max_results' less than 10
|
|
141
|
+
}
|
|
142
|
+
url = "https://api.x.com/2/tweets/search/recent?expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities"
|
|
143
|
+
|
|
144
|
+
async with httpx.AsyncClient() as client:
|
|
145
|
+
response = await client.get(url, headers=headers, params=params, timeout=10)
|
|
146
|
+
|
|
147
|
+
if response.status_code != 200:
|
|
148
|
+
raise ToolExecutionError(
|
|
149
|
+
f"Failed to search recent tweets during execution of '{search_recent_tweets_by_keywords.__name__}' tool. Request returned an error: {response.status_code} {response.text}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
response_data = response.json()
|
|
153
|
+
|
|
154
|
+
# Expand the urls that are in the tweets
|
|
155
|
+
expand_urls_in_tweets(response_data.get("data", []), delete_entities=True)
|
|
156
|
+
|
|
157
|
+
parse_search_recent_tweets_response(response_data)
|
|
158
|
+
|
|
159
|
+
return response_data
|
arcade_x/tools/users.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from arcade.core.errors import ToolExecutionError
|
|
6
|
+
from arcade.core.schema import ToolContext
|
|
7
|
+
from arcade.sdk import tool
|
|
8
|
+
from arcade.sdk.auth import X
|
|
9
|
+
from arcade_x.tools.utils import expand_urls_in_user_description, expand_urls_in_user_url
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Users Lookup Tools. See developer docs for additional available query parameters: https://developer.x.com/en/docs/x-api/users/lookup/api-reference
|
|
13
|
+
@tool(requires_auth=X(scopes=["users.read", "tweet.read"]))
|
|
14
|
+
async def lookup_single_user_by_username(
|
|
15
|
+
context: ToolContext,
|
|
16
|
+
username: Annotated[str, "The username of the X (Twitter) user to look up"],
|
|
17
|
+
) -> Annotated[dict, "User information including id, name, username, and description"]:
|
|
18
|
+
"""Look up a user on X (Twitter) by their username."""
|
|
19
|
+
|
|
20
|
+
headers = {
|
|
21
|
+
"Authorization": f"Bearer {context.authorization.token}",
|
|
22
|
+
}
|
|
23
|
+
url = f"https://api.x.com/2/users/by/username/{username}?user.fields=created_at,description,id,location,most_recent_tweet_id,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,verified_type,withheld,entities"
|
|
24
|
+
|
|
25
|
+
async with httpx.AsyncClient() as client:
|
|
26
|
+
response = await client.get(url, headers=headers, timeout=10)
|
|
27
|
+
|
|
28
|
+
if response.status_code != 200:
|
|
29
|
+
raise ToolExecutionError(
|
|
30
|
+
f"Failed to look up user during execution of '{lookup_single_user_by_username.__name__}' tool. Request returned an error: {response.status_code} {response.text}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Parse the response JSON
|
|
34
|
+
user_data = response.json()["data"]
|
|
35
|
+
|
|
36
|
+
expand_urls_in_user_description(user_data, delete_entities=False)
|
|
37
|
+
expand_urls_in_user_url(user_data, delete_entities=True)
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
Example response["data"] structure:
|
|
41
|
+
{
|
|
42
|
+
"data": {
|
|
43
|
+
"verified_type": str,
|
|
44
|
+
"public_metrics": {
|
|
45
|
+
"followers_count": int,
|
|
46
|
+
"following_count": int,
|
|
47
|
+
"tweet_count": int,
|
|
48
|
+
"listed_count": int,
|
|
49
|
+
"like_count": int
|
|
50
|
+
},
|
|
51
|
+
"id": str,
|
|
52
|
+
"most_recent_tweet_id": str,
|
|
53
|
+
"url": str,
|
|
54
|
+
"verified": bool,
|
|
55
|
+
"location": str,
|
|
56
|
+
"description": str,
|
|
57
|
+
"name": str,
|
|
58
|
+
"username": str,
|
|
59
|
+
"profile_image_url": str,
|
|
60
|
+
"created_at": str,
|
|
61
|
+
"protected": bool
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
"""
|
|
65
|
+
return {"data": user_data}
|
arcade_x/tools/utils.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_tweet_url(tweet_id: str) -> str:
|
|
5
|
+
"""Get the URL of a tweet given its ID."""
|
|
6
|
+
return f"https://x.com/x/status/{tweet_id}"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_search_recent_tweets_response(response_data: Any) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
Parses response from the X API search recent tweets endpoint.
|
|
12
|
+
Returns a JSON string with the tweets data.
|
|
13
|
+
|
|
14
|
+
Example parsed response:
|
|
15
|
+
"tweets": [
|
|
16
|
+
{
|
|
17
|
+
"author_id": "558248927",
|
|
18
|
+
"id": "1838272933141319832",
|
|
19
|
+
"edit_history_tweet_ids": [
|
|
20
|
+
"1838272933141319832"
|
|
21
|
+
],
|
|
22
|
+
"text": "PR pending on @LangChainAI, will be integrated there soon! https://t.co/DPWd4lccQo",
|
|
23
|
+
"tweet_url": "https://x.com/x/status/1838272933141319832",
|
|
24
|
+
"author_username": "tomas_hk",
|
|
25
|
+
"author_name": "Tomas Hernando Kofman"
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
if not sanity_check_tweets_data(response_data):
|
|
31
|
+
return {"data": []}
|
|
32
|
+
|
|
33
|
+
for tweet in response_data["data"]:
|
|
34
|
+
tweet["tweet_url"] = get_tweet_url(tweet["id"])
|
|
35
|
+
|
|
36
|
+
for tweet_data, user_data in zip(response_data["data"], response_data["includes"]["users"]):
|
|
37
|
+
tweet_data["author_username"] = user_data["username"]
|
|
38
|
+
tweet_data["author_name"] = user_data["name"]
|
|
39
|
+
|
|
40
|
+
return response_data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def sanity_check_tweets_data(tweets_data: dict) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Sanity check the tweets data.
|
|
46
|
+
Returns True if the tweets data is valid and contains tweets, False otherwise.
|
|
47
|
+
"""
|
|
48
|
+
if not tweets_data.get("data", []):
|
|
49
|
+
return False
|
|
50
|
+
return tweets_data.get("includes", {}).get("users", [])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def expand_urls_in_tweets(tweets_data: list[dict], delete_entities: bool = True) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Expands the urls in the test of the provided tweets.
|
|
56
|
+
X shortens urls, and consequently, this can cause language models to hallucinate.
|
|
57
|
+
See more about X's link shortner at https://help.x.com/en/using-x/url-shortener
|
|
58
|
+
"""
|
|
59
|
+
for tweet_data in tweets_data:
|
|
60
|
+
if "entities" in tweet_data and "urls" in tweet_data["entities"]:
|
|
61
|
+
for url_entity in tweet_data["entities"]["urls"]:
|
|
62
|
+
short_url = url_entity["url"]
|
|
63
|
+
expanded_url = url_entity["expanded_url"]
|
|
64
|
+
tweet_data["text"] = tweet_data["text"].replace(short_url, expanded_url)
|
|
65
|
+
|
|
66
|
+
if delete_entities:
|
|
67
|
+
tweet_data.pop(
|
|
68
|
+
"entities", None
|
|
69
|
+
) # Now that we've expanded the urls in the tweet, we no longer need the entities
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def expand_urls_in_user_description(user_data: dict, delete_entities: bool = True) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Expands the urls in the description of the provided user.
|
|
75
|
+
X shortens urls, and consequently, this can cause language models to hallucinate.
|
|
76
|
+
See more about X's link shortner at https://help.x.com/en/using-x/url-shortener
|
|
77
|
+
"""
|
|
78
|
+
description_urls = user_data.get("entities", {}).get("description", {}).get("urls", [])
|
|
79
|
+
description = user_data.get("description", "")
|
|
80
|
+
for url_info in description_urls:
|
|
81
|
+
t_co_link = url_info["url"]
|
|
82
|
+
expanded_url = url_info["expanded_url"]
|
|
83
|
+
description = description.replace(t_co_link, expanded_url)
|
|
84
|
+
user_data["description"] = description
|
|
85
|
+
|
|
86
|
+
if delete_entities:
|
|
87
|
+
# Entities is no longer needed now that we have expanded the t.co links
|
|
88
|
+
user_data.pop("entities", None)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def expand_urls_in_user_url(user_data: dict, delete_entities: bool = True) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Expands the urls in the url section of the provided user.
|
|
94
|
+
X shortens urls, and consequently, this can cause language models to hallucinate.
|
|
95
|
+
See more about X's link shortner at https://help.x.com/en/using-x/url-shortener
|
|
96
|
+
"""
|
|
97
|
+
url_urls = user_data.get("entities", {}).get("url", {}).get("urls", [])
|
|
98
|
+
url = user_data.get("url", "")
|
|
99
|
+
for url_info in url_urls:
|
|
100
|
+
t_co_link = url_info["url"]
|
|
101
|
+
expanded_url = url_info["expanded_url"]
|
|
102
|
+
url = url.replace(t_co_link, expanded_url)
|
|
103
|
+
user_data["url"] = url
|
|
104
|
+
|
|
105
|
+
if delete_entities:
|
|
106
|
+
# Entities is no longer needed now that we have expanded the t.co links
|
|
107
|
+
user_data.pop("entities", None)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: arcade_x
|
|
3
|
+
Version: 0.0.13
|
|
4
|
+
Summary: LLM tools for interacting with X (Twitter)
|
|
5
|
+
Author: Arcade AI
|
|
6
|
+
Author-email: dev@arcade-ai.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: arcade-ai (==0.0.13)
|
|
14
|
+
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
arcade_x/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
arcade_x/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
arcade_x/tools/tweets.py,sha256=SZht8WD03Kxmgxx9GSYTSC9BuXCkJp6NTUo5-VpiL1w,6088
|
|
4
|
+
arcade_x/tools/users.py,sha256=tGI2KXF_S4YaABKihLS0B7VgG8A5XOfynA-uYXKyxlU,2432
|
|
5
|
+
arcade_x/tools/utils.py,sha256=VwssFULxj0zysJdZHnbs9NE4h3JLGSm6SCVhfu-lx-Q,4137
|
|
6
|
+
arcade_x-0.0.13.dist-info/METADATA,sha256=32mRyE0YEhHzCXdvE0Kp395JV05gpFaanqyZfyKkKPY,512
|
|
7
|
+
arcade_x-0.0.13.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
8
|
+
arcade_x-0.0.13.dist-info/RECORD,,
|