arcade-x 0.1.5__py3-none-any.whl → 0.1.15__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 +49 -22
- arcade_x/tools/users.py +4 -4
- arcade_x/tools/utils.py +56 -6
- arcade_x-0.1.15.dist-info/METADATA +20 -0
- arcade_x-0.1.15.dist-info/RECORD +10 -0
- {arcade_x-0.1.5.dist-info → arcade_x-0.1.15.dist-info}/WHEEL +1 -1
- {arcade_x-0.1.5.dist-info → arcade_x-0.1.15.dist-info/licenses}/LICENSE +1 -1
- arcade_x-0.1.5.dist-info/METADATA +0 -14
- arcade_x-0.1.5.dist-info/RECORD +0 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TWEETS_URL = "https://api.x.com/2/tweets"
|
arcade_x/tools/tweets.py
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
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,
|
|
12
15
|
parse_search_recent_tweets_response,
|
|
16
|
+
remove_none_values,
|
|
13
17
|
)
|
|
14
18
|
|
|
15
|
-
TWEETS_URL = "https://api.x.com/2/tweets"
|
|
16
|
-
|
|
17
|
-
|
|
18
19
|
# Manage Tweets Tools. See developer docs for additional available parameters:
|
|
19
20
|
# https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference
|
|
20
21
|
|
|
@@ -63,21 +64,29 @@ async def search_recent_tweets_by_username(
|
|
|
63
64
|
context: ToolContext,
|
|
64
65
|
username: Annotated[str, "The username of the X (Twitter) user to look up"],
|
|
65
66
|
max_results: Annotated[
|
|
66
|
-
int, "The maximum number of results to return.
|
|
67
|
+
int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
|
|
67
68
|
] = 10,
|
|
69
|
+
next_token: Annotated[
|
|
70
|
+
str | None, "The pagination token starting from which to return results"
|
|
71
|
+
] = None,
|
|
68
72
|
) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
|
|
69
73
|
"""Search for recent tweets (last 7 days) on X (Twitter) by username.
|
|
70
74
|
Includes replies and reposts."""
|
|
71
75
|
|
|
72
76
|
headers = get_headers_with_token(context)
|
|
73
|
-
params: dict[str,
|
|
77
|
+
params: dict[str, Any] = {
|
|
74
78
|
"query": f"from:{username}",
|
|
75
|
-
"max_results":
|
|
79
|
+
"max_results": min(
|
|
80
|
+
max(max_results, 10), 100
|
|
81
|
+
), # X API does not allow 'max_results' less than 10 or greater than 100
|
|
82
|
+
"next_token": next_token,
|
|
83
|
+
"expansions": "author_id",
|
|
84
|
+
"user.fields": "id,name,username,entities",
|
|
85
|
+
"tweet.fields": "entities,note_tweet",
|
|
76
86
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
87
|
+
params = expand_attached_media(remove_none_values(params))
|
|
88
|
+
|
|
89
|
+
url = f"{TWEETS_URL}/search/recent"
|
|
81
90
|
|
|
82
91
|
async with httpx.AsyncClient() as client:
|
|
83
92
|
response = await client.get(url, headers=headers, params=params, timeout=10)
|
|
@@ -85,6 +94,9 @@ async def search_recent_tweets_by_username(
|
|
|
85
94
|
|
|
86
95
|
response_data: dict[str, Any] = response.json()
|
|
87
96
|
|
|
97
|
+
for tweet in response_data.get("data", []):
|
|
98
|
+
expand_long_tweet(tweet)
|
|
99
|
+
|
|
88
100
|
# Expand the URLs that are in the tweets
|
|
89
101
|
response_data["data"] = expand_urls_in_tweets(
|
|
90
102
|
response_data.get("data", []), delete_entities=True
|
|
@@ -106,8 +118,11 @@ async def search_recent_tweets_by_keywords(
|
|
|
106
118
|
list[str] | None, "List of phrases that must be present in the tweet"
|
|
107
119
|
] = None,
|
|
108
120
|
max_results: Annotated[
|
|
109
|
-
int, "The maximum number of results to return.
|
|
121
|
+
int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
|
|
110
122
|
] = 10,
|
|
123
|
+
next_token: Annotated[
|
|
124
|
+
str | None, "The pagination token starting from which to return results"
|
|
125
|
+
] = None,
|
|
111
126
|
) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
|
|
112
127
|
"""
|
|
113
128
|
Search for recent tweets (last 7 days) on X (Twitter) by required keywords and phrases.
|
|
@@ -116,7 +131,7 @@ async def search_recent_tweets_by_keywords(
|
|
|
116
131
|
"""
|
|
117
132
|
|
|
118
133
|
if not any([keywords, phrases]):
|
|
119
|
-
raise RetryableToolError(
|
|
134
|
+
raise RetryableToolError(
|
|
120
135
|
"No keywords or phrases provided",
|
|
121
136
|
developer_message="Predicted inputs didn't contain any keywords or phrases",
|
|
122
137
|
additional_prompt_content="Please provide at least one keyword or phrase for search",
|
|
@@ -129,14 +144,19 @@ async def search_recent_tweets_by_keywords(
|
|
|
129
144
|
if keywords:
|
|
130
145
|
query += " ".join(keywords or [])
|
|
131
146
|
|
|
132
|
-
params: dict[str,
|
|
147
|
+
params: dict[str, Any] = {
|
|
133
148
|
"query": query.strip(),
|
|
134
|
-
"max_results":
|
|
149
|
+
"max_results": min(
|
|
150
|
+
max(max_results, 10), 100
|
|
151
|
+
), # X API does not allow 'max_results' less than 10 or greater than 100
|
|
152
|
+
"next_token": next_token,
|
|
153
|
+
"expansions": "author_id",
|
|
154
|
+
"user.fields": "id,name,username,entities",
|
|
155
|
+
"tweet.fields": "entities,note_tweet",
|
|
135
156
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
157
|
+
params = expand_attached_media(remove_none_values(params))
|
|
158
|
+
|
|
159
|
+
url = f"{TWEETS_URL}/search/recent"
|
|
140
160
|
|
|
141
161
|
async with httpx.AsyncClient() as client:
|
|
142
162
|
response = await client.get(url, headers=headers, params=params, timeout=10)
|
|
@@ -144,6 +164,9 @@ async def search_recent_tweets_by_keywords(
|
|
|
144
164
|
|
|
145
165
|
response_data: dict[str, Any] = response.json()
|
|
146
166
|
|
|
167
|
+
for tweet in response_data.get("data", []):
|
|
168
|
+
expand_long_tweet(tweet)
|
|
169
|
+
|
|
147
170
|
# Expand the URLs that are in the tweets
|
|
148
171
|
response_data["data"] = expand_urls_in_tweets(
|
|
149
172
|
response_data.get("data", []), delete_entities=True
|
|
@@ -166,8 +189,10 @@ async def lookup_tweet_by_id(
|
|
|
166
189
|
params = {
|
|
167
190
|
"expansions": "author_id",
|
|
168
191
|
"user.fields": "id,name,username,entities",
|
|
169
|
-
"tweet.fields": "entities",
|
|
192
|
+
"tweet.fields": "entities,note_tweet",
|
|
170
193
|
}
|
|
194
|
+
params = expand_attached_media(params)
|
|
195
|
+
|
|
171
196
|
url = f"{TWEETS_URL}/{tweet_id}"
|
|
172
197
|
|
|
173
198
|
async with httpx.AsyncClient() as client:
|
|
@@ -179,6 +204,8 @@ async def lookup_tweet_by_id(
|
|
|
179
204
|
# Get the tweet data
|
|
180
205
|
tweet_data = response_data.get("data")
|
|
181
206
|
if tweet_data:
|
|
207
|
+
expand_long_tweet(tweet_data)
|
|
208
|
+
|
|
182
209
|
# Expand the URLs that are in the tweet
|
|
183
210
|
expanded_tweet_list = expand_urls_in_tweets([tweet_data], delete_entities=True)
|
|
184
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
|
|
|
@@ -28,14 +31,16 @@ def parse_search_recent_tweets_response(response_data: dict[str, Any]) -> dict[s
|
|
|
28
31
|
Returns the modified response data with added 'tweet_url', 'author_username', and 'author_name'.
|
|
29
32
|
"""
|
|
30
33
|
if not sanity_check_tweets_data(response_data):
|
|
31
|
-
return {"data": []}
|
|
34
|
+
return {"data": [], "next_token": ""}
|
|
32
35
|
|
|
33
36
|
# Add 'tweet_url' to each tweet
|
|
34
37
|
for tweet in response_data["data"]:
|
|
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]]:
|
|
@@ -110,3 +126,37 @@ def expand_urls_in_user_url(user_data: dict, delete_entities: bool = True) -> di
|
|
|
110
126
|
if delete_entities:
|
|
111
127
|
new_user_data.pop("entities", None)
|
|
112
128
|
return new_user_data
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def remove_none_values(params: dict) -> dict:
|
|
132
|
+
"""
|
|
133
|
+
Remove key/value pairs with None values from a dictionary.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
params: The dictionary to clean
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A new dictionary with None values removed
|
|
140
|
+
"""
|
|
141
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def expand_attached_media(params: dict) -> dict:
|
|
145
|
+
"""
|
|
146
|
+
Include attached media metadata in the request parameters.
|
|
147
|
+
"""
|
|
148
|
+
params["expansions"] += ",attachments.media_keys"
|
|
149
|
+
params["tweet.fields"] += ",attachments"
|
|
150
|
+
params["media.fields"] = ",".join([
|
|
151
|
+
# media_key, url and type are returned by default, added here for clarity
|
|
152
|
+
"media_key",
|
|
153
|
+
"url",
|
|
154
|
+
"type",
|
|
155
|
+
"duration_ms",
|
|
156
|
+
"height",
|
|
157
|
+
"width",
|
|
158
|
+
"preview_image_url",
|
|
159
|
+
"alt_text",
|
|
160
|
+
"public_metrics",
|
|
161
|
+
])
|
|
162
|
+
return params
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arcade_x
|
|
3
|
+
Version: 0.1.15
|
|
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=Xx4S5bq0udUwSiodf5cs19ftgWHROO-scpY3RY-Fam8,5376
|
|
7
|
+
arcade_x-0.1.15.dist-info/METADATA,sha256=4Y_NCBB8UOHDcKEdJMGGWVV1FfsEMkluR3ebobt0OwE,835
|
|
8
|
+
arcade_x-0.1.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
arcade_x-0.1.15.dist-info/licenses/LICENSE,sha256=f4Q0XUZJ2MqZBO1XsqqHhuZfSs2ar1cZEJ45150zERo,1067
|
|
10
|
+
arcade_x-0.1.15.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: arcade_x
|
|
3
|
-
Version: 0.1.5
|
|
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.5)
|
|
14
|
-
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
arcade_x-0.1.5.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=IDAjx1j_XLoc7ILyuRKVyjTPwQeNtD47lnyKUMO-WpU,6594
|
|
4
|
-
arcade_x/tools/users.py,sha256=zNACU9UhQJkGNnrei-E6GWvFnbDzg0YG9pkdplu8TzM,2148
|
|
5
|
-
arcade_x/tools/utils.py,sha256=VEeUo715m6l6X9Syszy2E72rdvv4h_tc-P7wMaQEbGo,4044
|
|
6
|
-
arcade_x-0.1.5.dist-info/LICENSE,sha256=SphQPbiNmBD1J6yJ7oxG9bIZDbj8lNHKSv5Kl86zA40,1066
|
|
7
|
-
arcade_x-0.1.5.dist-info/METADATA,sha256=pa4GiYU352AShVYdd2rDjVa3AP5b1sFtJpi1gUihQmM,510
|
|
8
|
-
arcade_x-0.1.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
9
|
-
arcade_x-0.1.5.dist-info/RECORD,,
|