arcade-x 0.1.6__py3-none-any.whl → 0.1.16__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/tools/constants.py +1 -0
- arcade_x/tools/tweets.py +33 -23
- arcade_x/tools/users.py +4 -4
- arcade_x/tools/utils.py +46 -8
- arcade_x-0.1.16.dist-info/METADATA +20 -0
- arcade_x-0.1.16.dist-info/RECORD +10 -0
- {arcade_x-0.1.6.dist-info → arcade_x-0.1.16.dist-info}/WHEEL +1 -1
- {arcade_x-0.1.6.dist-info → arcade_x-0.1.16.dist-info/licenses}/LICENSE +1 -1
- arcade_x-0.1.6.dist-info/METADATA +0 -14
- arcade_x-0.1.6.dist-info/RECORD +0 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TWEETS_URL = "https://api.x.com/2/tweets"
|
arcade_x/tools/tweets.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
from typing import Annotated, Any
|
|
1
|
+
from typing import Annotated, Any
|
|
2
2
|
|
|
3
3
|
import httpx
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from arcade_tdk import ToolContext, tool
|
|
5
|
+
from arcade_tdk.auth import X
|
|
6
|
+
from arcade_tdk.errors import RetryableToolError
|
|
7
7
|
|
|
8
|
+
from arcade_x.tools.constants import TWEETS_URL
|
|
8
9
|
from arcade_x.tools.utils import (
|
|
10
|
+
expand_attached_media,
|
|
11
|
+
expand_long_tweet,
|
|
9
12
|
expand_urls_in_tweets,
|
|
10
13
|
get_headers_with_token,
|
|
11
14
|
get_tweet_url,
|
|
@@ -13,9 +16,6 @@ from arcade_x.tools.utils import (
|
|
|
13
16
|
remove_none_values,
|
|
14
17
|
)
|
|
15
18
|
|
|
16
|
-
TWEETS_URL = "https://api.x.com/2/tweets"
|
|
17
|
-
|
|
18
|
-
|
|
19
19
|
# Manage Tweets Tools. See developer docs for additional available parameters:
|
|
20
20
|
# https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference
|
|
21
21
|
|
|
@@ -67,26 +67,26 @@ async def search_recent_tweets_by_username(
|
|
|
67
67
|
int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
|
|
68
68
|
] = 10,
|
|
69
69
|
next_token: Annotated[
|
|
70
|
-
|
|
70
|
+
str | None, "The pagination token starting from which to return results"
|
|
71
71
|
] = None,
|
|
72
72
|
) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
|
|
73
73
|
"""Search for recent tweets (last 7 days) on X (Twitter) by username.
|
|
74
74
|
Includes replies and reposts."""
|
|
75
75
|
|
|
76
76
|
headers = get_headers_with_token(context)
|
|
77
|
-
params: dict[str,
|
|
77
|
+
params: dict[str, Any] = {
|
|
78
78
|
"query": f"from:{username}",
|
|
79
79
|
"max_results": min(
|
|
80
80
|
max(max_results, 10), 100
|
|
81
81
|
), # X API does not allow 'max_results' less than 10 or greater than 100
|
|
82
82
|
"next_token": next_token,
|
|
83
|
+
"expansions": "author_id",
|
|
84
|
+
"user.fields": "id,name,username,entities",
|
|
85
|
+
"tweet.fields": "entities,note_tweet",
|
|
83
86
|
}
|
|
84
|
-
params = remove_none_values(params)
|
|
87
|
+
params = expand_attached_media(remove_none_values(params))
|
|
85
88
|
|
|
86
|
-
url =
|
|
87
|
-
"https://api.x.com/2/tweets/search/recent?"
|
|
88
|
-
"expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities"
|
|
89
|
-
)
|
|
89
|
+
url = f"{TWEETS_URL}/search/recent"
|
|
90
90
|
|
|
91
91
|
async with httpx.AsyncClient() as client:
|
|
92
92
|
response = await client.get(url, headers=headers, params=params, timeout=10)
|
|
@@ -94,6 +94,9 @@ async def search_recent_tweets_by_username(
|
|
|
94
94
|
|
|
95
95
|
response_data: dict[str, Any] = response.json()
|
|
96
96
|
|
|
97
|
+
for tweet in response_data.get("data", []):
|
|
98
|
+
expand_long_tweet(tweet)
|
|
99
|
+
|
|
97
100
|
# Expand the URLs that are in the tweets
|
|
98
101
|
response_data["data"] = expand_urls_in_tweets(
|
|
99
102
|
response_data.get("data", []), delete_entities=True
|
|
@@ -118,7 +121,7 @@ async def search_recent_tweets_by_keywords(
|
|
|
118
121
|
int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
|
|
119
122
|
] = 10,
|
|
120
123
|
next_token: Annotated[
|
|
121
|
-
|
|
124
|
+
str | None, "The pagination token starting from which to return results"
|
|
122
125
|
] = None,
|
|
123
126
|
) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
|
|
124
127
|
"""
|
|
@@ -128,7 +131,7 @@ async def search_recent_tweets_by_keywords(
|
|
|
128
131
|
"""
|
|
129
132
|
|
|
130
133
|
if not any([keywords, phrases]):
|
|
131
|
-
raise RetryableToolError(
|
|
134
|
+
raise RetryableToolError(
|
|
132
135
|
"No keywords or phrases provided",
|
|
133
136
|
developer_message="Predicted inputs didn't contain any keywords or phrases",
|
|
134
137
|
additional_prompt_content="Please provide at least one keyword or phrase for search",
|
|
@@ -141,19 +144,19 @@ async def search_recent_tweets_by_keywords(
|
|
|
141
144
|
if keywords:
|
|
142
145
|
query += " ".join(keywords or [])
|
|
143
146
|
|
|
144
|
-
params: dict[str,
|
|
147
|
+
params: dict[str, Any] = {
|
|
145
148
|
"query": query.strip(),
|
|
146
149
|
"max_results": min(
|
|
147
150
|
max(max_results, 10), 100
|
|
148
151
|
), # X API does not allow 'max_results' less than 10 or greater than 100
|
|
149
152
|
"next_token": next_token,
|
|
153
|
+
"expansions": "author_id",
|
|
154
|
+
"user.fields": "id,name,username,entities",
|
|
155
|
+
"tweet.fields": "entities,note_tweet",
|
|
150
156
|
}
|
|
151
|
-
params = remove_none_values(params)
|
|
157
|
+
params = expand_attached_media(remove_none_values(params))
|
|
152
158
|
|
|
153
|
-
url =
|
|
154
|
-
"https://api.x.com/2/tweets/search/recent?"
|
|
155
|
-
"expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities"
|
|
156
|
-
)
|
|
159
|
+
url = f"{TWEETS_URL}/search/recent"
|
|
157
160
|
|
|
158
161
|
async with httpx.AsyncClient() as client:
|
|
159
162
|
response = await client.get(url, headers=headers, params=params, timeout=10)
|
|
@@ -161,6 +164,9 @@ async def search_recent_tweets_by_keywords(
|
|
|
161
164
|
|
|
162
165
|
response_data: dict[str, Any] = response.json()
|
|
163
166
|
|
|
167
|
+
for tweet in response_data.get("data", []):
|
|
168
|
+
expand_long_tweet(tweet)
|
|
169
|
+
|
|
164
170
|
# Expand the URLs that are in the tweets
|
|
165
171
|
response_data["data"] = expand_urls_in_tweets(
|
|
166
172
|
response_data.get("data", []), delete_entities=True
|
|
@@ -183,8 +189,10 @@ async def lookup_tweet_by_id(
|
|
|
183
189
|
params = {
|
|
184
190
|
"expansions": "author_id",
|
|
185
191
|
"user.fields": "id,name,username,entities",
|
|
186
|
-
"tweet.fields": "entities",
|
|
192
|
+
"tweet.fields": "entities,note_tweet",
|
|
187
193
|
}
|
|
194
|
+
params = expand_attached_media(params)
|
|
195
|
+
|
|
188
196
|
url = f"{TWEETS_URL}/{tweet_id}"
|
|
189
197
|
|
|
190
198
|
async with httpx.AsyncClient() as client:
|
|
@@ -196,6 +204,8 @@ async def lookup_tweet_by_id(
|
|
|
196
204
|
# Get the tweet data
|
|
197
205
|
tweet_data = response_data.get("data")
|
|
198
206
|
if tweet_data:
|
|
207
|
+
expand_long_tweet(tweet_data)
|
|
208
|
+
|
|
199
209
|
# Expand the URLs that are in the tweet
|
|
200
210
|
expanded_tweet_list = expand_urls_in_tweets([tweet_data], delete_entities=True)
|
|
201
211
|
response_data["data"] = expanded_tweet_list[0]
|
arcade_x/tools/users.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
2
|
|
|
3
3
|
import httpx
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from arcade_tdk import ToolContext, tool
|
|
5
|
+
from arcade_tdk.auth import X
|
|
6
|
+
from arcade_tdk.errors import RetryableToolError
|
|
7
7
|
|
|
8
8
|
from arcade_x.tools.utils import (
|
|
9
9
|
expand_urls_in_user_description,
|
|
@@ -48,7 +48,7 @@ async def lookup_single_user_by_username(
|
|
|
48
48
|
response = await client.get(url, headers=headers, timeout=10)
|
|
49
49
|
if response.status_code == 404:
|
|
50
50
|
# User not found
|
|
51
|
-
raise RetryableToolError(
|
|
51
|
+
raise RetryableToolError(
|
|
52
52
|
"User not found",
|
|
53
53
|
developer_message=f"User with username '{username}' not found.",
|
|
54
54
|
additional_prompt_content="Please check the username and try again.",
|
arcade_x/tools/utils.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from arcade_tdk import ToolContext
|
|
4
|
+
from arcade_tdk.errors import ToolExecutionError
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def get_tweet_url(tweet_id: str) -> str:
|
|
@@ -12,12 +12,15 @@ def get_tweet_url(tweet_id: str) -> str:
|
|
|
12
12
|
def get_headers_with_token(context: ToolContext) -> dict[str, str]:
|
|
13
13
|
"""Get the headers for a request to the X API."""
|
|
14
14
|
if context.authorization is None or context.authorization.token is None:
|
|
15
|
-
raise ToolExecutionError(
|
|
15
|
+
raise ToolExecutionError(
|
|
16
16
|
"Missing Token. Authorization is required to post a tweet.",
|
|
17
17
|
developer_message="Token is not set in the ToolContext.",
|
|
18
18
|
)
|
|
19
|
+
token = (
|
|
20
|
+
context.authorization.token if context.authorization and context.authorization.token else ""
|
|
21
|
+
)
|
|
19
22
|
return {
|
|
20
|
-
"Authorization": f"Bearer {
|
|
23
|
+
"Authorization": f"Bearer {token}",
|
|
21
24
|
"Content-Type": "application/json",
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -35,7 +38,9 @@ def parse_search_recent_tweets_response(response_data: dict[str, Any]) -> dict[s
|
|
|
35
38
|
tweet["tweet_url"] = get_tweet_url(tweet["id"])
|
|
36
39
|
|
|
37
40
|
# Add 'author_username' and 'author_name' to each tweet
|
|
38
|
-
for tweet_data, user_data in zip(
|
|
41
|
+
for tweet_data, user_data in zip(
|
|
42
|
+
response_data["data"], response_data["includes"]["users"], strict=False
|
|
43
|
+
):
|
|
39
44
|
tweet_data["author_username"] = user_data["username"]
|
|
40
45
|
tweet_data["author_name"] = user_data["name"]
|
|
41
46
|
|
|
@@ -55,6 +60,17 @@ def sanity_check_tweets_data(tweets_data: dict[str, Any]) -> bool:
|
|
|
55
60
|
return True
|
|
56
61
|
|
|
57
62
|
|
|
63
|
+
def expand_long_tweet(tweet_data: dict[str, Any]) -> None:
|
|
64
|
+
"""Expand a long tweet.
|
|
65
|
+
|
|
66
|
+
For tweets exceeding 280 characters,
|
|
67
|
+
replace the truncated tweet text with the full tweet text.
|
|
68
|
+
"""
|
|
69
|
+
if tweet_data.get("note_tweet"):
|
|
70
|
+
tweet_data["text"] = tweet_data["note_tweet"]["text"]
|
|
71
|
+
del tweet_data["note_tweet"]
|
|
72
|
+
|
|
73
|
+
|
|
58
74
|
def expand_urls_in_tweets(
|
|
59
75
|
tweets_data: list[dict[str, Any]], delete_entities: bool = True
|
|
60
76
|
) -> list[dict[str, Any]]:
|
|
@@ -66,9 +82,10 @@ def expand_urls_in_tweets(
|
|
|
66
82
|
new_tweet = tweet_data.copy()
|
|
67
83
|
if "entities" in new_tweet and "urls" in new_tweet["entities"]:
|
|
68
84
|
for url_entity in new_tweet["entities"]["urls"]:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
if "url" in url_entity and "expanded_url" in url_entity:
|
|
86
|
+
short_url = url_entity["url"]
|
|
87
|
+
expanded_url = url_entity["expanded_url"]
|
|
88
|
+
new_tweet["text"] = new_tweet["text"].replace(short_url, expanded_url)
|
|
72
89
|
|
|
73
90
|
if delete_entities:
|
|
74
91
|
new_tweet.pop("entities", None)
|
|
@@ -123,3 +140,24 @@ def remove_none_values(params: dict) -> dict:
|
|
|
123
140
|
A new dictionary with None values removed
|
|
124
141
|
"""
|
|
125
142
|
return {k: v for k, v in params.items() if v is not None}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def expand_attached_media(params: dict) -> dict:
|
|
146
|
+
"""
|
|
147
|
+
Include attached media metadata in the request parameters.
|
|
148
|
+
"""
|
|
149
|
+
params["expansions"] += ",attachments.media_keys"
|
|
150
|
+
params["tweet.fields"] += ",attachments"
|
|
151
|
+
params["media.fields"] = ",".join([
|
|
152
|
+
# media_key, url and type are returned by default, added here for clarity
|
|
153
|
+
"media_key",
|
|
154
|
+
"url",
|
|
155
|
+
"type",
|
|
156
|
+
"duration_ms",
|
|
157
|
+
"height",
|
|
158
|
+
"width",
|
|
159
|
+
"preview_image_url",
|
|
160
|
+
"alt_text",
|
|
161
|
+
"public_metrics",
|
|
162
|
+
])
|
|
163
|
+
return params
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arcade_x
|
|
3
|
+
Version: 0.1.16
|
|
4
|
+
Summary: Arcade.dev LLM tools for X (Twitter)
|
|
5
|
+
Author-email: Arcade <dev@arcade.dev>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: arcade-tdk<3.0.0,>=2.0.0
|
|
9
|
+
Requires-Dist: httpx<1.0.0,>=0.27.2
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: arcade-ai[evals]<3.0.0,>=2.0.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: arcade-serve<3.0.0,>=2.0.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: mypy<1.6.0,>=1.5.1; extra == 'dev'
|
|
14
|
+
Requires-Dist: pre-commit<3.5.0,>=3.4.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-asyncio<0.25.0,>=0.24.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-cov<4.1.0,>=4.0.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-mock<3.12.0,>=3.11.1; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest<8.4.0,>=8.3.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff<0.8.0,>=0.7.4; extra == 'dev'
|
|
20
|
+
Requires-Dist: tox<4.12.0,>=4.11.1; extra == 'dev'
|
|
@@ -0,0 +1,10 @@
|
|
|
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/constants.py,sha256=d-OJK5Qx05JRUcpK5G1DcYB91wT37hFSzGmIYfwlEtA,42
|
|
4
|
+
arcade_x/tools/tweets.py,sha256=S1Psv-gFePz-8ibnp7rY-GCdogkAm-hJQZpHf7hUusk,7498
|
|
5
|
+
arcade_x/tools/users.py,sha256=gopYKTBOwbZAeVdL_T3vwkepbpv1bdPbqcU_2oPC3SY,2132
|
|
6
|
+
arcade_x/tools/utils.py,sha256=wxPdp3JDG-gEDM7XUNiZSgL61isVE3SRqaLiwYP-K40,5461
|
|
7
|
+
arcade_x-0.1.16.dist-info/METADATA,sha256=7ox9l5R61ZAaxf46r2Bwib5vxmr1OrWB19LSldl_k2Q,835
|
|
8
|
+
arcade_x-0.1.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
arcade_x-0.1.16.dist-info/licenses/LICENSE,sha256=f4Q0XUZJ2MqZBO1XsqqHhuZfSs2ar1cZEJ45150zERo,1067
|
|
10
|
+
arcade_x-0.1.16.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: arcade_x
|
|
3
|
-
Version: 0.1.6
|
|
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.1.6)
|
|
14
|
-
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
arcade_x-0.1.6.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
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=VchdglDVI15rsEyZuBLIj6qjGql0-UO8TdY6ylGl4yA,7158
|
|
4
|
-
arcade_x/tools/users.py,sha256=zNACU9UhQJkGNnrei-E6GWvFnbDzg0YG9pkdplu8TzM,2148
|
|
5
|
-
arcade_x/tools/utils.py,sha256=YKRGXcUPKcPnIvmiNF2-0jB4edxFmFZLJ90OQNR-DGk,4366
|
|
6
|
-
arcade_x-0.1.6.dist-info/LICENSE,sha256=SphQPbiNmBD1J6yJ7oxG9bIZDbj8lNHKSv5Kl86zA40,1066
|
|
7
|
-
arcade_x-0.1.6.dist-info/METADATA,sha256=5eq4z7KaY2GSDY8J-KLUDRdibTsCsHpt29AgW7qlSac,510
|
|
8
|
-
arcade_x-0.1.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
9
|
-
arcade_x-0.1.6.dist-info/RECORD,,
|