diff --git a/toolkits/x/arcade_x/tools/tweets.py b/toolkits/x/arcade_x/tools/tweets.py index d9979095..e5cc1cc3 100644 --- a/toolkits/x/arcade_x/tools/tweets.py +++ b/toolkits/x/arcade_x/tools/tweets.py @@ -6,6 +6,7 @@ from arcade.sdk.auth import X from arcade.sdk.errors import RetryableToolError from arcade_x.tools.utils import ( + expand_long_tweet, expand_urls_in_tweets, get_headers_with_token, get_tweet_url, @@ -85,7 +86,7 @@ async def search_recent_tweets_by_username( url = ( "https://api.x.com/2/tweets/search/recent?" - "expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities" + "expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities,note_tweet" ) async with httpx.AsyncClient() as client: @@ -94,6 +95,9 @@ async def search_recent_tweets_by_username( response_data: dict[str, Any] = response.json() + for tweet in response_data["data"]: + expand_long_tweet(tweet) + # Expand the URLs that are in the tweets response_data["data"] = expand_urls_in_tweets( response_data.get("data", []), delete_entities=True @@ -152,7 +156,7 @@ async def search_recent_tweets_by_keywords( url = ( "https://api.x.com/2/tweets/search/recent?" - "expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities" + "expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities,note_tweet" ) async with httpx.AsyncClient() as client: @@ -161,6 +165,9 @@ async def search_recent_tweets_by_keywords( response_data: dict[str, Any] = response.json() + for tweet in response_data["data"]: + expand_long_tweet(tweet) + # Expand the URLs that are in the tweets response_data["data"] = expand_urls_in_tweets( response_data.get("data", []), delete_entities=True @@ -183,7 +190,7 @@ async def lookup_tweet_by_id( params = { "expansions": "author_id", "user.fields": "id,name,username,entities", - "tweet.fields": "entities", + "tweet.fields": "entities,note_tweet", } url = f"{TWEETS_URL}/{tweet_id}" @@ -196,6 +203,8 @@ async def lookup_tweet_by_id( # Get the tweet data tweet_data = response_data.get("data") if tweet_data: + expand_long_tweet(tweet_data) + # Expand the URLs that are in the tweet expanded_tweet_list = expand_urls_in_tweets([tweet_data], delete_entities=True) response_data["data"] = expanded_tweet_list[0] diff --git a/toolkits/x/arcade_x/tools/utils.py b/toolkits/x/arcade_x/tools/utils.py index aa2a69fe..f1e4b48f 100644 --- a/toolkits/x/arcade_x/tools/utils.py +++ b/toolkits/x/arcade_x/tools/utils.py @@ -55,6 +55,17 @@ def sanity_check_tweets_data(tweets_data: dict[str, Any]) -> bool: return True +def expand_long_tweet(tweet_data: dict[str, Any]) -> None: + """Expand a long tweet. + + For tweets exceeding 280 characters, + replace the truncated tweet text with the full tweet text. + """ + if tweet_data.get("note_tweet"): + tweet_data["text"] = tweet_data["note_tweet"]["text"] + del tweet_data["note_tweet"] + + def expand_urls_in_tweets( tweets_data: list[dict[str, Any]], delete_entities: bool = True ) -> list[dict[str, Any]]: diff --git a/toolkits/x/tests/test_tweets.py b/toolkits/x/tests/test_tweets.py index 8db5533c..4ec3a4a8 100644 --- a/toolkits/x/tests/test_tweets.py +++ b/toolkits/x/tests/test_tweets.py @@ -13,6 +13,19 @@ from arcade_x.tools.tweets import ( ) from arcade_x.tools.utils import get_tweet_url +full_tweet_text = ( + "This is a super long tweet that exceeds 280 characters and I want to see if the tool will " + "successfully handle long tweets so I will continue to write this tweet until I have " + "exceeded the 280 character count. So far I have typed 'e' 28 times! Now its 29! Did you " + "know that the oldest tree in the world is... wait I actually don't know this fact." +) +truncated_tweet_text = ( + "This is a super long tweet that exceeds 280 characters and I want to see if the tool will " + "successfully handle long tweets so I will continue to write this tweet until I have " + "exceeded the 280 character count. So far I have typed 'e' 28 times! Now its 29! Did you " + "know that the..." +) + @pytest.mark.asyncio async def test_post_tweet_success(tool_context, mock_httpx_client): @@ -88,7 +101,15 @@ async def test_search_recent_tweets_by_username_success(tool_context, mock_httpx "data": [ { "id": "1234567890", - "text": "Test tweet", + "note_tweet": { + "entities": { + "mentions": [ + {"end": 19, "id": "00000000", "start": 4, "username": "aUsername"} + ] + }, + "text": full_tweet_text, + }, + "text": truncated_tweet_text, "entities": { "urls": [ {"url": "https://t.co/short", "expanded_url": "https://example.com/long"} @@ -105,7 +126,7 @@ async def test_search_recent_tweets_by_username_success(tool_context, mock_httpx assert "data" in result assert len(result["data"]) == 1 - assert result["data"][0]["text"] == "Test tweet" + assert result["data"][0]["text"] == full_tweet_text mock_httpx_client.get.assert_called_once() @@ -132,7 +153,21 @@ async def test_search_recent_tweets_by_keywords_success(tool_context, mock_httpx mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { - "data": [{"id": "1234567890", "text": "Keyword tweet", "entities": {}}], + "data": [ + { + "id": "1234567890", + "note_tweet": { + "entities": { + "mentions": [ + {"end": 19, "id": "00000000", "start": 4, "username": "aUsername"} + ] + }, + "text": full_tweet_text, + }, + "text": truncated_tweet_text, + "entities": {}, + } + ], "includes": {"users": [{"id": "0987654321", "name": "Test User", "username": "testuser"}]}, } mock_httpx_client.get.return_value = mock_response @@ -142,7 +177,7 @@ async def test_search_recent_tweets_by_keywords_success(tool_context, mock_httpx assert "data" in result assert len(result["data"]) == 1 - assert result["data"][0]["text"] == "Keyword tweet" + assert result["data"][0]["text"] == full_tweet_text mock_httpx_client.get.assert_called_once() @@ -162,7 +197,17 @@ async def test_lookup_tweet_by_id_success(tool_context, mock_httpx_client): mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { - "data": {"id": "1234567890", "text": "Lookup tweet", "entities": {}} + "data": { + "id": "1234567890", + "note_tweet": { + "entities": { + "mentions": [{"end": 19, "id": "00000000", "start": 4, "username": "aUsername"}] + }, + "text": full_tweet_text, + }, + "text": truncated_tweet_text, + "entities": {}, + } } mock_httpx_client.get.return_value = mock_response @@ -170,7 +215,7 @@ async def test_lookup_tweet_by_id_success(tool_context, mock_httpx_client): result = await lookup_tweet_by_id(tool_context, tweet_id) assert "data" in result - assert result["data"]["text"] == "Lookup tweet" + assert result["data"]["text"] == full_tweet_text mock_httpx_client.get.assert_called_once()