camel-ai 0.2.3a1__py3-none-any.whl → 0.2.3a2__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.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +93 -69
- camel/agents/knowledge_graph_agent.py +4 -6
- camel/bots/__init__.py +16 -2
- camel/bots/discord_app.py +138 -0
- camel/bots/slack/__init__.py +30 -0
- camel/bots/slack/models.py +158 -0
- camel/bots/slack/slack_app.py +255 -0
- camel/configs/__init__.py +1 -2
- camel/configs/anthropic_config.py +2 -5
- camel/configs/base_config.py +6 -6
- camel/configs/groq_config.py +2 -3
- camel/configs/ollama_config.py +1 -2
- camel/configs/openai_config.py +2 -23
- camel/configs/samba_config.py +2 -2
- camel/configs/togetherai_config.py +1 -1
- camel/configs/vllm_config.py +1 -1
- camel/configs/zhipuai_config.py +2 -3
- camel/embeddings/openai_embedding.py +2 -2
- camel/loaders/__init__.py +2 -0
- camel/loaders/chunkr_reader.py +163 -0
- camel/loaders/firecrawl_reader.py +3 -3
- camel/loaders/unstructured_io.py +35 -33
- camel/messages/__init__.py +1 -0
- camel/models/__init__.py +2 -4
- camel/models/anthropic_model.py +32 -26
- camel/models/azure_openai_model.py +39 -36
- camel/models/base_model.py +31 -20
- camel/models/gemini_model.py +37 -29
- camel/models/groq_model.py +29 -23
- camel/models/litellm_model.py +44 -61
- camel/models/mistral_model.py +32 -29
- camel/models/model_factory.py +66 -76
- camel/models/nemotron_model.py +33 -23
- camel/models/ollama_model.py +42 -47
- camel/models/{openai_compatibility_model.py → openai_compatible_model.py} +31 -49
- camel/models/openai_model.py +48 -29
- camel/models/reka_model.py +30 -28
- camel/models/samba_model.py +82 -177
- camel/models/stub_model.py +2 -2
- camel/models/togetherai_model.py +37 -43
- camel/models/vllm_model.py +43 -50
- camel/models/zhipuai_model.py +33 -27
- camel/retrievers/auto_retriever.py +28 -10
- camel/retrievers/vector_retriever.py +58 -47
- camel/societies/babyagi_playing.py +6 -3
- camel/societies/role_playing.py +5 -3
- camel/storages/graph_storages/graph_element.py +3 -5
- camel/storages/key_value_storages/json.py +6 -1
- camel/toolkits/__init__.py +20 -7
- camel/toolkits/arxiv_toolkit.py +155 -0
- camel/toolkits/ask_news_toolkit.py +653 -0
- camel/toolkits/base.py +2 -3
- camel/toolkits/code_execution.py +6 -7
- camel/toolkits/dalle_toolkit.py +6 -6
- camel/toolkits/{openai_function.py → function_tool.py} +34 -11
- camel/toolkits/github_toolkit.py +9 -10
- camel/toolkits/google_maps_toolkit.py +7 -7
- camel/toolkits/google_scholar_toolkit.py +146 -0
- camel/toolkits/linkedin_toolkit.py +7 -7
- camel/toolkits/math_toolkit.py +8 -8
- camel/toolkits/open_api_toolkit.py +5 -5
- camel/toolkits/reddit_toolkit.py +7 -7
- camel/toolkits/retrieval_toolkit.py +5 -5
- camel/toolkits/search_toolkit.py +9 -9
- camel/toolkits/slack_toolkit.py +11 -11
- camel/toolkits/twitter_toolkit.py +378 -452
- camel/toolkits/weather_toolkit.py +6 -6
- camel/toolkits/whatsapp_toolkit.py +177 -0
- camel/types/__init__.py +6 -1
- camel/types/enums.py +40 -85
- camel/types/openai_types.py +3 -0
- camel/types/unified_model_type.py +104 -0
- camel/utils/__init__.py +0 -2
- camel/utils/async_func.py +7 -7
- camel/utils/commons.py +32 -3
- camel/utils/token_counting.py +30 -212
- camel/workforce/role_playing_worker.py +1 -1
- camel/workforce/single_agent_worker.py +1 -1
- camel/workforce/task_channel.py +4 -3
- camel/workforce/workforce.py +4 -4
- camel_ai-0.2.3a2.dist-info/LICENSE +201 -0
- {camel_ai-0.2.3a1.dist-info → camel_ai-0.2.3a2.dist-info}/METADATA +27 -56
- {camel_ai-0.2.3a1.dist-info → camel_ai-0.2.3a2.dist-info}/RECORD +85 -76
- {camel_ai-0.2.3a1.dist-info → camel_ai-0.2.3a2.dist-info}/WHEEL +1 -1
- camel/bots/discord_bot.py +0 -206
- camel/models/open_source_model.py +0 -170
|
@@ -15,505 +15,431 @@ import datetime
|
|
|
15
15
|
import os
|
|
16
16
|
from http import HTTPStatus
|
|
17
17
|
from http.client import responses
|
|
18
|
-
from typing import List, Optional,
|
|
18
|
+
from typing import Any, Dict, List, Optional, Union
|
|
19
19
|
|
|
20
20
|
import requests
|
|
21
|
+
from requests_oauthlib import OAuth1
|
|
21
22
|
|
|
22
|
-
from camel.toolkits import
|
|
23
|
+
from camel.toolkits import FunctionTool
|
|
23
24
|
from camel.toolkits.base import BaseToolkit
|
|
25
|
+
from camel.utils import api_keys_required
|
|
24
26
|
|
|
25
27
|
TWEET_TEXT_LIMIT = 280
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
@api_keys_required(
|
|
31
|
+
"TWITTER_CONSUMER_KEY",
|
|
32
|
+
"TWITTER_CONSUMER_SECRET",
|
|
33
|
+
"TWITTER_ACCESS_TOKEN",
|
|
34
|
+
"TWITTER_ACCESS_TOKEN_SECRET",
|
|
35
|
+
)
|
|
36
|
+
def create_tweet(
|
|
37
|
+
text: str,
|
|
38
|
+
poll_options: Optional[List[str]] = None,
|
|
39
|
+
poll_duration_minutes: Optional[int] = None,
|
|
40
|
+
quote_tweet_id: Optional[Union[int, str]] = None,
|
|
41
|
+
) -> str:
|
|
42
|
+
r"""Creates a new tweet, optionally including a poll or a quote tweet,
|
|
43
|
+
or simply a text-only tweet.
|
|
44
|
+
|
|
45
|
+
This function sends a POST request to the Twitter API to create a new
|
|
46
|
+
tweet. The tweet can be a text-only tweet, or optionally include a poll
|
|
47
|
+
or be a quote tweet. A confirmation prompt is presented to the user
|
|
48
|
+
before the tweet is created.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
text (str): The text of the tweet. The Twitter character limit for
|
|
52
|
+
a single tweet is 280 characters.
|
|
53
|
+
poll_options (Optional[List[str]]): A list of poll options for a
|
|
54
|
+
tweet with a poll.
|
|
55
|
+
poll_duration_minutes (Optional[int]): Duration of the poll in
|
|
56
|
+
minutes for a tweet with a poll. This is only required
|
|
57
|
+
if the request includes poll_options.
|
|
58
|
+
quote_tweet_id (Optional[Union[int, str]]): Link to the tweet being
|
|
59
|
+
quoted.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
str: A message indicating the success of the tweet creation,
|
|
63
|
+
including the tweet ID and text. If the request to the
|
|
64
|
+
Twitter API is not successful, the return is an error message.
|
|
65
|
+
|
|
66
|
+
Note:
|
|
67
|
+
You can only provide either the `quote_tweet_id` parameter or
|
|
68
|
+
the pair of `poll_duration_minutes` and `poll_options` parameters,
|
|
69
|
+
not both.
|
|
70
|
+
|
|
71
|
+
Reference:
|
|
72
|
+
https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference/post-tweets
|
|
33
73
|
"""
|
|
74
|
+
auth = OAuth1(
|
|
75
|
+
os.getenv("TWITTER_CONSUMER_KEY"),
|
|
76
|
+
os.getenv("TWITTER_CONSUMER_SECRET"),
|
|
77
|
+
os.getenv("TWITTER_ACCESS_TOKEN"),
|
|
78
|
+
os.getenv("TWITTER_ACCESS_TOKEN_SECRET"),
|
|
79
|
+
)
|
|
80
|
+
url = "https://api.x.com/2/tweets"
|
|
81
|
+
|
|
82
|
+
# Validate text
|
|
83
|
+
if text is None:
|
|
84
|
+
return "Text cannot be None"
|
|
85
|
+
|
|
86
|
+
if len(text) > TWEET_TEXT_LIMIT:
|
|
87
|
+
return f"Text must not exceed {TWEET_TEXT_LIMIT} characters."
|
|
88
|
+
|
|
89
|
+
# Validate poll options and duration
|
|
90
|
+
if (poll_options is None) != (poll_duration_minutes is None):
|
|
91
|
+
return (
|
|
92
|
+
"Error: Both `poll_options` and `poll_duration_minutes` must "
|
|
93
|
+
"be provided together or not at all."
|
|
94
|
+
)
|
|
34
95
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
poll_duration_minutes: Optional[int] = None,
|
|
41
|
-
quote_tweet_id: Optional[Union[int, str]] = None,
|
|
42
|
-
) -> str:
|
|
43
|
-
r"""Creates a new tweet, optionally including a poll or a quote tweet,
|
|
44
|
-
or simply a text-only tweet.
|
|
45
|
-
|
|
46
|
-
This function sends a POST request to the Twitter API to create a new
|
|
47
|
-
tweet. The tweet can be a text-only tweet, or optionally include a poll
|
|
48
|
-
or be a quote tweet. A confirmation prompt is presented to the user
|
|
49
|
-
before the tweet is created.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
text (str): The text of the tweet. The Twitter character limit for
|
|
53
|
-
a single tweet is 280 characters.
|
|
54
|
-
poll_options (Optional[List[str]]): A list of poll options for a
|
|
55
|
-
tweet with a poll.
|
|
56
|
-
poll_duration_minutes (Optional[int]): Duration of the poll in
|
|
57
|
-
minutes for a tweet with a poll. This is only required
|
|
58
|
-
if the request includes poll_options.
|
|
59
|
-
quote_tweet_id (Optional[Union[int, str]]): Link to the tweet being
|
|
60
|
-
quoted.
|
|
61
|
-
|
|
62
|
-
Note:
|
|
63
|
-
You can only provide either the `quote_tweet_id` parameter or
|
|
64
|
-
the pair of `poll_duration_minutes` and `poll_options` parameters,
|
|
65
|
-
not both.
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
str: A message indicating the success of the tweet creation,
|
|
69
|
-
including the tweet ID and text. If the request to the
|
|
70
|
-
Twitter API is not successful, the return is an error message.
|
|
71
|
-
|
|
72
|
-
Reference:
|
|
73
|
-
https://developer.twitter.com/en/docs/twitter-api/tweets/
|
|
74
|
-
manage-tweets/api-reference/post-tweets
|
|
75
|
-
https://github.com/xdevplatform/Twitter-API-v2-sample-code/blob/
|
|
76
|
-
main/Manage-Tweets/create_tweet.py
|
|
77
|
-
"""
|
|
78
|
-
# validate text
|
|
79
|
-
if text is None:
|
|
80
|
-
return "Text cannot be None"
|
|
81
|
-
elif len(text) > TWEET_TEXT_LIMIT:
|
|
82
|
-
return "Text must not exceed 280 characters."
|
|
83
|
-
|
|
84
|
-
# Validate poll options and duration
|
|
85
|
-
if (poll_options is None) != (poll_duration_minutes is None):
|
|
86
|
-
return (
|
|
87
|
-
"Error: Both `poll_options` and `poll_duration_minutes` must "
|
|
88
|
-
"be provided together or not at all."
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
# Validate exclusive parameters
|
|
92
|
-
if quote_tweet_id is not None and (
|
|
93
|
-
poll_options or poll_duration_minutes
|
|
94
|
-
):
|
|
95
|
-
return (
|
|
96
|
-
"Error: Cannot provide both `quote_tweet_id` and "
|
|
97
|
-
"(`poll_options` or `poll_duration_minutes`)."
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# Print the parameters that are not None
|
|
101
|
-
params = {
|
|
102
|
-
"text": text,
|
|
103
|
-
"poll_options": poll_options,
|
|
104
|
-
"poll_duration_minutes": poll_duration_minutes,
|
|
105
|
-
"quote_tweet_id": quote_tweet_id,
|
|
106
|
-
}
|
|
107
|
-
print("You are going to create a tweet with following parameters:")
|
|
108
|
-
for key, value in params.items():
|
|
109
|
-
if value is not None:
|
|
110
|
-
print(f"{key}: {value}")
|
|
111
|
-
|
|
112
|
-
# Add a confirmation prompt at the beginning of the function
|
|
113
|
-
confirm = input(
|
|
114
|
-
"Are you sure you want to create this tweet? (yes/no): "
|
|
96
|
+
# Validate exclusive parameters
|
|
97
|
+
if quote_tweet_id is not None and (poll_options or poll_duration_minutes):
|
|
98
|
+
return (
|
|
99
|
+
"Error: Cannot provide both `quote_tweet_id` and "
|
|
100
|
+
"(`poll_options` or `poll_duration_minutes`)."
|
|
115
101
|
)
|
|
116
|
-
if confirm.lower() != "yes":
|
|
117
|
-
return "Execution cancelled by the user."
|
|
118
102
|
|
|
119
|
-
|
|
120
|
-
json_data = {}
|
|
103
|
+
payload: Dict[str, Any] = {"text": text}
|
|
121
104
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
105
|
+
if poll_options is not None and poll_duration_minutes is not None:
|
|
106
|
+
payload["poll"] = {
|
|
107
|
+
"options": poll_options,
|
|
108
|
+
"duration_minutes": poll_duration_minutes,
|
|
109
|
+
}
|
|
127
110
|
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
if quote_tweet_id is not None:
|
|
112
|
+
payload["quote_tweet_id"] = str(quote_tweet_id)
|
|
130
113
|
|
|
131
|
-
|
|
114
|
+
# Making the request
|
|
115
|
+
response = requests.post(url, auth=auth, json=payload)
|
|
132
116
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
117
|
+
if response.status_code != HTTPStatus.CREATED:
|
|
118
|
+
error_type = _handle_http_error(response)
|
|
119
|
+
return (
|
|
120
|
+
f"Request returned a(n) {error_type}: "
|
|
121
|
+
f"{response.status_code} {response.text}"
|
|
137
122
|
)
|
|
138
123
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
"Request returned a(n) "
|
|
144
|
-
+ str(error_type)
|
|
145
|
-
+ ": "
|
|
146
|
-
+ str(response.status_code)
|
|
147
|
-
+ " "
|
|
148
|
-
+ response.text
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
# Saving the response as JSON
|
|
152
|
-
json_response = response.json()
|
|
153
|
-
|
|
154
|
-
tweet_id = json_response["data"]["id"]
|
|
155
|
-
tweet_text = json_response["data"]["text"]
|
|
156
|
-
|
|
157
|
-
response_str = (
|
|
158
|
-
f"Create tweet successful. "
|
|
159
|
-
f"The tweet ID is: {tweet_id}. "
|
|
160
|
-
f"The tweet text is: '{tweet_text}'."
|
|
161
|
-
)
|
|
124
|
+
json_response = response.json()
|
|
125
|
+
tweet_id = json_response["data"]["id"]
|
|
126
|
+
tweet_text = json_response["data"]["text"]
|
|
162
127
|
|
|
163
|
-
|
|
128
|
+
return f"Create tweet {tweet_id} successful with content {tweet_text}."
|
|
164
129
|
|
|
165
|
-
def delete_tweet(self, tweet_id: str) -> str:
|
|
166
|
-
r"""Deletes a tweet with the specified ID for an authorized user.
|
|
167
130
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
131
|
+
@api_keys_required(
|
|
132
|
+
"TWITTER_CONSUMER_KEY",
|
|
133
|
+
"TWITTER_CONSUMER_SECRET",
|
|
134
|
+
"TWITTER_ACCESS_TOKEN",
|
|
135
|
+
"TWITTER_ACCESS_TOKEN_SECRET",
|
|
136
|
+
)
|
|
137
|
+
def delete_tweet(tweet_id: str) -> str:
|
|
138
|
+
r"""Deletes a tweet with the specified ID for an authorized user.
|
|
171
139
|
|
|
172
|
-
|
|
173
|
-
|
|
140
|
+
This function sends a DELETE request to the Twitter API to delete
|
|
141
|
+
a tweet with the specified ID. Before sending the request, it
|
|
142
|
+
prompts the user to confirm the deletion.
|
|
174
143
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
deletion was successful, the message includes the ID of the
|
|
178
|
-
deleted tweet. If the deletion was not successful, the message
|
|
179
|
-
includes an error message.
|
|
180
|
-
|
|
181
|
-
Reference:
|
|
182
|
-
https://developer.twitter.com/en/docs/twitter-api/tweets/
|
|
183
|
-
manage-tweets/api-reference/delete-tweets-id
|
|
184
|
-
"""
|
|
185
|
-
# Print the parameters that are not None
|
|
186
|
-
if tweet_id is not None:
|
|
187
|
-
print(
|
|
188
|
-
f"You are going to delete a tweet with the following "
|
|
189
|
-
f"ID: {tweet_id}"
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
# Add a confirmation prompt at the beginning of the function
|
|
193
|
-
confirm = input(
|
|
194
|
-
"Are you sure you want to delete this tweet? (yes/no): "
|
|
195
|
-
)
|
|
196
|
-
if confirm.lower() != "yes":
|
|
197
|
-
return "Execution cancelled by the user."
|
|
144
|
+
Args:
|
|
145
|
+
tweet_id (str): The ID of the tweet to delete.
|
|
198
146
|
|
|
199
|
-
|
|
147
|
+
Returns:
|
|
148
|
+
str: A message indicating the result of the deletion. If the
|
|
149
|
+
deletion was successful, the message includes the ID of the
|
|
150
|
+
deleted tweet. If the deletion was not successful, the message
|
|
151
|
+
includes an error message.
|
|
200
152
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
153
|
+
Reference:
|
|
154
|
+
https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference/delete-tweets-id
|
|
155
|
+
"""
|
|
156
|
+
auth = OAuth1(
|
|
157
|
+
os.getenv("TWITTER_CONSUMER_KEY"),
|
|
158
|
+
os.getenv("TWITTER_CONSUMER_SECRET"),
|
|
159
|
+
os.getenv("TWITTER_ACCESS_TOKEN"),
|
|
160
|
+
os.getenv("TWITTER_ACCESS_TOKEN_SECRET"),
|
|
161
|
+
)
|
|
162
|
+
url = f"https://api.x.com/2/tweets/{tweet_id}"
|
|
163
|
+
response = requests.delete(url, auth=auth)
|
|
164
|
+
|
|
165
|
+
if response.status_code != HTTPStatus.OK:
|
|
166
|
+
error_type = _handle_http_error(response)
|
|
167
|
+
return (
|
|
168
|
+
f"Request returned a(n) {error_type}: "
|
|
169
|
+
f"{response.status_code} {response.text}"
|
|
204
170
|
)
|
|
205
171
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
+ response.text
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
# Saving the response as JSON
|
|
219
|
-
json_response = response.json()
|
|
220
|
-
# `deleted_status` may be True or False.
|
|
221
|
-
# Defaults to False if not found.
|
|
222
|
-
deleted_status = json_response.get("data", {}).get("deleted", False)
|
|
223
|
-
response_str = (
|
|
224
|
-
f"Delete tweet successful: {deleted_status}. "
|
|
225
|
-
f"The tweet ID is: {tweet_id}. "
|
|
172
|
+
json_response = response.json()
|
|
173
|
+
|
|
174
|
+
# `deleted_status` may be True or False.
|
|
175
|
+
# Defaults to False if not found.
|
|
176
|
+
deleted_status = json_response.get("data", {}).get("deleted", False)
|
|
177
|
+
if not deleted_status:
|
|
178
|
+
return (
|
|
179
|
+
f"The tweet with ID {tweet_id} was not deleted. "
|
|
180
|
+
"Please check the tweet ID and try again."
|
|
226
181
|
)
|
|
227
|
-
return response_str
|
|
228
182
|
|
|
229
|
-
|
|
230
|
-
r"""Retrieves and formats the authenticated user's Twitter
|
|
231
|
-
profile info.
|
|
183
|
+
return f"Delete tweet {tweet_id} successful."
|
|
232
184
|
|
|
233
|
-
This function sends a GET request to the Twitter API to retrieve the
|
|
234
|
-
authenticated user's profile information, including their pinned tweet.
|
|
235
|
-
It then formats this information into a readable report.
|
|
236
185
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
Reference:
|
|
247
|
-
https://developer.twitter.com/en/docs/twitter-api/users/lookup/
|
|
248
|
-
api-reference/get-users-me
|
|
249
|
-
"""
|
|
250
|
-
oauth = self._get_oauth_session()
|
|
251
|
-
|
|
252
|
-
tweet_fields = ["created_at", "text"]
|
|
253
|
-
user_fields = [
|
|
254
|
-
"created_at",
|
|
255
|
-
"description",
|
|
256
|
-
"id",
|
|
257
|
-
"location",
|
|
258
|
-
"most_recent_tweet_id",
|
|
259
|
-
"name",
|
|
260
|
-
"pinned_tweet_id",
|
|
261
|
-
"profile_image_url",
|
|
262
|
-
"protected",
|
|
263
|
-
"public_metrics",
|
|
264
|
-
"url",
|
|
265
|
-
"username",
|
|
266
|
-
"verified_type",
|
|
267
|
-
]
|
|
268
|
-
params = {
|
|
269
|
-
"expansions": "pinned_tweet_id",
|
|
270
|
-
"tweet.fields": ",".join(tweet_fields),
|
|
271
|
-
"user.fields": ",".join(user_fields),
|
|
272
|
-
}
|
|
186
|
+
@api_keys_required(
|
|
187
|
+
"TWITTER_CONSUMER_KEY",
|
|
188
|
+
"TWITTER_CONSUMER_SECRET",
|
|
189
|
+
"TWITTER_ACCESS_TOKEN",
|
|
190
|
+
"TWITTER_ACCESS_TOKEN_SECRET",
|
|
191
|
+
)
|
|
192
|
+
def get_my_user_profile() -> str:
|
|
193
|
+
r"""Retrieves the authenticated user's Twitter profile info.
|
|
273
194
|
|
|
274
|
-
|
|
275
|
-
|
|
195
|
+
This function sends a GET request to the Twitter API to retrieve the
|
|
196
|
+
authenticated user's profile information, including their pinned tweet.
|
|
197
|
+
It then formats this information into a readable report.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
str: A formatted report of the authenticated user's Twitter profile
|
|
201
|
+
information. This includes their ID, name, username,
|
|
202
|
+
description, location, most recent tweet ID, profile image URL,
|
|
203
|
+
account creation date, protection status, verification type,
|
|
204
|
+
public metrics, and pinned tweet information. If the request to
|
|
205
|
+
the Twitter API is not successful, the return is an error message.
|
|
206
|
+
|
|
207
|
+
Reference:
|
|
208
|
+
https://developer.x.com/en/docs/x-api/users/lookup/api-reference/get-users-me
|
|
209
|
+
"""
|
|
210
|
+
return _get_user_info()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@api_keys_required(
|
|
214
|
+
"TWITTER_CONSUMER_KEY",
|
|
215
|
+
"TWITTER_CONSUMER_SECRET",
|
|
216
|
+
"TWITTER_ACCESS_TOKEN",
|
|
217
|
+
"TWITTER_ACCESS_TOKEN_SECRET",
|
|
218
|
+
)
|
|
219
|
+
def get_user_by_username(username: str) -> str:
|
|
220
|
+
r"""Retrieves one user's Twitter profile info by username (handle).
|
|
221
|
+
|
|
222
|
+
This function sends a GET request to the Twitter API to retrieve the
|
|
223
|
+
user's profile information, including their pinned tweet.
|
|
224
|
+
It then formats this information into a readable report.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
username (str): The username (handle) of the user to retrieve.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
str: A formatted report of the user's Twitter profile information.
|
|
231
|
+
This includes their ID, name, username, description, location,
|
|
232
|
+
most recent tweet ID, profile image URL, account creation date,
|
|
233
|
+
protection status, verification type, public metrics, and
|
|
234
|
+
pinned tweet information. If the request to the Twitter API is
|
|
235
|
+
not successful, the return is an error message.
|
|
236
|
+
|
|
237
|
+
Reference:
|
|
238
|
+
https://developer.x.com/en/docs/x-api/users/lookup/api-reference/get-users-by-username-username
|
|
239
|
+
"""
|
|
240
|
+
return _get_user_info(username)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _get_user_info(username: Optional[str] = None) -> str:
|
|
244
|
+
r"""Generates a formatted report of the user information from the
|
|
245
|
+
JSON response.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
username (Optional[str], optional): The username of the user to
|
|
249
|
+
retrieve. If None, the function retrieves the authenticated
|
|
250
|
+
user's profile information. (default: :obj:`None`)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
str: A formatted report of the user's Twitter profile information.
|
|
254
|
+
"""
|
|
255
|
+
oauth = OAuth1(
|
|
256
|
+
os.getenv("TWITTER_CONSUMER_KEY"),
|
|
257
|
+
os.getenv("TWITTER_CONSUMER_SECRET"),
|
|
258
|
+
os.getenv("TWITTER_ACCESS_TOKEN"),
|
|
259
|
+
os.getenv("TWITTER_ACCESS_TOKEN_SECRET"),
|
|
260
|
+
)
|
|
261
|
+
url = (
|
|
262
|
+
f"https://api.x.com/2/users/by/username/{username}"
|
|
263
|
+
if username
|
|
264
|
+
else "https://api.x.com/2/users/me"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
tweet_fields = ["created_at", "text"]
|
|
268
|
+
user_fields = [
|
|
269
|
+
"created_at",
|
|
270
|
+
"description",
|
|
271
|
+
"id",
|
|
272
|
+
"location",
|
|
273
|
+
"most_recent_tweet_id",
|
|
274
|
+
"name",
|
|
275
|
+
"pinned_tweet_id",
|
|
276
|
+
"profile_image_url",
|
|
277
|
+
"protected",
|
|
278
|
+
"public_metrics",
|
|
279
|
+
"url",
|
|
280
|
+
"username",
|
|
281
|
+
"verified_type",
|
|
282
|
+
]
|
|
283
|
+
params = {
|
|
284
|
+
"expansions": "pinned_tweet_id",
|
|
285
|
+
"tweet.fields": ",".join(tweet_fields),
|
|
286
|
+
"user.fields": ",".join(user_fields),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
response = requests.get(url, auth=oauth, params=params)
|
|
290
|
+
|
|
291
|
+
if response.status_code != HTTPStatus.OK:
|
|
292
|
+
error_type = _handle_http_error(response)
|
|
293
|
+
return (
|
|
294
|
+
f"Request returned a(n) {error_type}: "
|
|
295
|
+
f"{response.status_code} {response.text}"
|
|
276
296
|
)
|
|
277
297
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
298
|
+
json_response = response.json()
|
|
299
|
+
|
|
300
|
+
user_info = json_response.get("data", {})
|
|
301
|
+
pinned_tweet = json_response.get("includes", {}).get("tweets", [{}])[0]
|
|
302
|
+
|
|
303
|
+
user_report_entries = [
|
|
304
|
+
f"ID: {user_info['id']}",
|
|
305
|
+
f"Name: {user_info['name']}",
|
|
306
|
+
f"Username: {user_info['username']}",
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
# Define the part of keys that need to be repeatedly processed
|
|
310
|
+
user_info_keys = [
|
|
311
|
+
"description",
|
|
312
|
+
"location",
|
|
313
|
+
"most_recent_tweet_id",
|
|
314
|
+
"profile_image_url",
|
|
315
|
+
]
|
|
316
|
+
for key in user_info_keys:
|
|
317
|
+
if not (value := user_info.get(key)):
|
|
318
|
+
continue
|
|
319
|
+
new_key = key.replace('_', ' ').capitalize()
|
|
320
|
+
user_report_entries.append(f"{new_key}: {value}")
|
|
321
|
+
|
|
322
|
+
if "created_at" in user_info:
|
|
323
|
+
created_at = datetime.datetime.strptime(
|
|
324
|
+
user_info["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
325
|
+
)
|
|
326
|
+
date_str = created_at.strftime('%B %d, %Y at %H:%M:%S')
|
|
327
|
+
user_report_entries.append(f"Account created at: {date_str}")
|
|
328
|
+
|
|
329
|
+
protection_status = "private" if user_info["protected"] else "public"
|
|
330
|
+
user_report_entries.append(
|
|
331
|
+
f"Protected: This user's Tweets are {protection_status}"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
verification_messages = {
|
|
335
|
+
"blue": (
|
|
336
|
+
"The user has a blue verification, typically reserved for "
|
|
337
|
+
"public figures, celebrities, or global brands"
|
|
338
|
+
),
|
|
339
|
+
"business": (
|
|
340
|
+
"The user has a business verification, typically "
|
|
341
|
+
"reserved for businesses and corporations"
|
|
342
|
+
),
|
|
343
|
+
"government": (
|
|
344
|
+
"The user has a government verification, typically "
|
|
345
|
+
"reserved for government officials or entities"
|
|
346
|
+
),
|
|
347
|
+
"none": "The user is not verified",
|
|
348
|
+
}
|
|
349
|
+
verification_type = user_info.get("verified_type", "none")
|
|
350
|
+
user_report_entries.append(
|
|
351
|
+
f"Verified type: {verification_messages.get(verification_type)}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if "public_metrics" in user_info:
|
|
355
|
+
metrics = user_info["public_metrics"]
|
|
356
|
+
user_report_entries.append(
|
|
357
|
+
f"Public metrics: "
|
|
358
|
+
f"The user has {metrics.get('followers_count', 0)} followers, "
|
|
359
|
+
f"is following {metrics.get('following_count', 0)} users, "
|
|
360
|
+
f"has made {metrics.get('tweet_count', 0)} tweets, "
|
|
361
|
+
f"is listed in {metrics.get('listed_count', 0)} lists, "
|
|
362
|
+
f"and has received {metrics.get('like_count', 0)} likes"
|
|
363
|
+
)
|
|
284
364
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
tweets = json_response.get('includes', {}).get('tweets', [{}])[0]
|
|
289
|
-
|
|
290
|
-
user_report = ""
|
|
291
|
-
user_report += f"ID: {user_info['id']}. "
|
|
292
|
-
user_report += f"Name: {user_info['name']}. "
|
|
293
|
-
user_report += f"Username: {user_info['username']}. "
|
|
294
|
-
|
|
295
|
-
# Define the part of keys that need to be repeatedly processed
|
|
296
|
-
user_info_keys = [
|
|
297
|
-
'description',
|
|
298
|
-
'location',
|
|
299
|
-
'most_recent_tweet_id',
|
|
300
|
-
'profile_image_url',
|
|
301
|
-
]
|
|
302
|
-
for key in user_info_keys:
|
|
303
|
-
value = user_info.get(key)
|
|
304
|
-
if user_info.get(key):
|
|
305
|
-
user_report += (
|
|
306
|
-
f"{key.replace('_', ' ').capitalize()}: {value}. "
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
if 'created_at' in user_info:
|
|
310
|
-
created_at = datetime.datetime.strptime(
|
|
311
|
-
user_info['created_at'], "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
312
|
-
)
|
|
313
|
-
date_str = created_at.strftime('%B %d, %Y at %H:%M:%S')
|
|
314
|
-
user_report += f"Account created at: {date_str}. "
|
|
315
|
-
|
|
316
|
-
protection_status = "private" if user_info['protected'] else "public"
|
|
317
|
-
user_report += (
|
|
318
|
-
f"Protected: This user's Tweets are {protection_status}. "
|
|
365
|
+
if "pinned_tweet_id" in user_info:
|
|
366
|
+
user_report_entries.append(
|
|
367
|
+
f"Pinned tweet ID: {user_info['pinned_tweet_id']}"
|
|
319
368
|
)
|
|
320
369
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
),
|
|
330
|
-
'government': (
|
|
331
|
-
"The user has a government verification, typically "
|
|
332
|
-
"reserved for government officials or entities. "
|
|
333
|
-
),
|
|
334
|
-
'none': "The user is not verified. ",
|
|
335
|
-
}
|
|
336
|
-
verification_type = user_info.get('verified_type', 'none')
|
|
337
|
-
user_report += (
|
|
338
|
-
f"Verified type: {verification_messages.get(verification_type)}"
|
|
370
|
+
if "created_at" in pinned_tweet and "text" in pinned_tweet:
|
|
371
|
+
tweet_created_at = datetime.datetime.strptime(
|
|
372
|
+
pinned_tweet["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
373
|
+
)
|
|
374
|
+
user_report_entries.append(
|
|
375
|
+
f"Pinned tweet information: Pinned tweet created at "
|
|
376
|
+
f"{tweet_created_at.strftime('%B %d, %Y at %H:%M:%S')} "
|
|
377
|
+
f"with text: '{pinned_tweet['text']}'"
|
|
339
378
|
)
|
|
340
379
|
|
|
341
|
-
|
|
342
|
-
user_report += "Public metrics: "
|
|
343
|
-
metrics = user_info['public_metrics']
|
|
344
|
-
user_report += (
|
|
345
|
-
f"The user has {metrics.get('followers_count', 0)} followers, "
|
|
346
|
-
f"is following {metrics.get('following_count', 0)} users, "
|
|
347
|
-
f"has made {metrics.get('tweet_count', 0)} tweets, "
|
|
348
|
-
f"is listed in {metrics.get('listed_count', 0)} lists, "
|
|
349
|
-
f"and has received {metrics.get('like_count', 0)} likes. "
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
if 'pinned_tweet_id' in user_info:
|
|
353
|
-
user_report += f"Pinned tweet ID: {user_info['pinned_tweet_id']}. "
|
|
354
|
-
|
|
355
|
-
if 'created_at' in tweets and 'text' in tweets:
|
|
356
|
-
user_report += "\nPinned tweet information: "
|
|
357
|
-
tweet_created_at = datetime.datetime.strptime(
|
|
358
|
-
tweets['created_at'], "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
359
|
-
)
|
|
360
|
-
user_report += (
|
|
361
|
-
f"Pinned tweet created at "
|
|
362
|
-
f"{tweet_created_at.strftime('%B %d, %Y at %H:%M:%S')} "
|
|
363
|
-
f"with text: '{tweets['text']}'."
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
return user_report
|
|
367
|
-
|
|
368
|
-
def get_tools(self) -> List[OpenAIFunction]:
|
|
369
|
-
r"""Returns a list of OpenAIFunction objects representing the
|
|
370
|
-
functions in the toolkit.
|
|
380
|
+
return "\n".join(user_report_entries)
|
|
371
381
|
|
|
372
|
-
Returns:
|
|
373
|
-
List[OpenAIFunction]: A list of OpenAIFunction objects
|
|
374
|
-
representing the functions in the toolkit.
|
|
375
|
-
"""
|
|
376
|
-
return [
|
|
377
|
-
OpenAIFunction(self.create_tweet),
|
|
378
|
-
OpenAIFunction(self.delete_tweet),
|
|
379
|
-
OpenAIFunction(self.get_my_user_profile),
|
|
380
|
-
]
|
|
381
382
|
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
def _handle_http_error(response: requests.Response) -> str:
|
|
384
|
+
r"""Handles the HTTP response by checking the status code and
|
|
385
|
+
returning an appropriate message if there is an error.
|
|
384
386
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
+
Args:
|
|
388
|
+
response (requests.Response): The HTTP response to handle.
|
|
387
389
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
"""
|
|
392
|
-
# Get `TWITTER_CONSUMER_KEY` and `TWITTER_CONSUMER_SECRET` here:
|
|
393
|
-
# https://developer.twitter.com/en/portal/products/free
|
|
394
|
-
TWITTER_CONSUMER_KEY = os.environ.get("TWITTER_CONSUMER_KEY")
|
|
395
|
-
TWITTER_CONSUMER_SECRET = os.environ.get("TWITTER_CONSUMER_SECRET")
|
|
396
|
-
|
|
397
|
-
if not TWITTER_CONSUMER_KEY or not TWITTER_CONSUMER_SECRET:
|
|
398
|
-
missing_keys = ", ".join(
|
|
399
|
-
[
|
|
400
|
-
"TWITTER_CONSUMER_KEY" if not TWITTER_CONSUMER_KEY else "",
|
|
401
|
-
"TWITTER_CONSUMER_SECRET"
|
|
402
|
-
if not TWITTER_CONSUMER_SECRET
|
|
403
|
-
else "",
|
|
404
|
-
]
|
|
405
|
-
).strip(", ")
|
|
406
|
-
raise ValueError(
|
|
407
|
-
f"{missing_keys} not found in environment variables. Get them "
|
|
408
|
-
"here: `https://developer.twitter.com/en/portal/products/free`."
|
|
409
|
-
)
|
|
410
|
-
return TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET
|
|
411
|
-
|
|
412
|
-
def _get_oauth_session(self) -> requests.Session:
|
|
413
|
-
r"""Initiates an OAuth1Session with Twitter's API and returns it.
|
|
414
|
-
|
|
415
|
-
The function first fetches a request token, then prompts the user to
|
|
416
|
-
authorize the application. After the user has authorized the
|
|
417
|
-
application and provided a verifier (PIN), the function fetches an
|
|
418
|
-
access token. Finally, a new OAuth1Session is created with the access
|
|
419
|
-
token and returned.
|
|
420
|
-
|
|
421
|
-
Raises:
|
|
422
|
-
RuntimeError: If an error occurs while fetching the OAuth access
|
|
423
|
-
token or the OAuth request token.
|
|
390
|
+
Returns:
|
|
391
|
+
str: A string describing the error, if any. If there is no error,
|
|
392
|
+
the function returns an "Unexpected Exception" message.
|
|
424
393
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
"Please install `requests_oauthlib` first. You can "
|
|
440
|
-
"install it by running `pip install "
|
|
441
|
-
"requests_oauthlib`."
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
consumer_key, consumer_secret = self._get_twitter_api_key()
|
|
445
|
-
|
|
446
|
-
# Get request token
|
|
447
|
-
request_token_url = (
|
|
448
|
-
"https://api.twitter.com/oauth/request_token"
|
|
449
|
-
"?oauth_callback=oob&x_auth_access_type=write"
|
|
450
|
-
)
|
|
451
|
-
oauth = OAuth1Session(consumer_key, client_secret=consumer_secret)
|
|
452
|
-
|
|
453
|
-
try:
|
|
454
|
-
fetch_response = oauth.fetch_request_token(request_token_url)
|
|
455
|
-
except Exception as e:
|
|
456
|
-
raise RuntimeError(
|
|
457
|
-
f"Error occurred while fetching the OAuth access token: {e}"
|
|
458
|
-
)
|
|
459
|
-
|
|
460
|
-
resource_owner_key = fetch_response.get("oauth_token")
|
|
461
|
-
resource_owner_secret = fetch_response.get("oauth_token_secret")
|
|
462
|
-
|
|
463
|
-
# Get authorization
|
|
464
|
-
base_authorization_url = "https://api.twitter.com/oauth/authorize"
|
|
465
|
-
authorization_url = oauth.authorization_url(base_authorization_url)
|
|
466
|
-
print("Please go here and authorize: %s" % authorization_url)
|
|
467
|
-
verifier = input("Paste the PIN here: ")
|
|
468
|
-
|
|
469
|
-
# Get the access token
|
|
470
|
-
access_token_url = "https://api.twitter.com/oauth/access_token"
|
|
471
|
-
oauth = OAuth1Session(
|
|
472
|
-
consumer_key,
|
|
473
|
-
client_secret=consumer_secret,
|
|
474
|
-
resource_owner_key=resource_owner_key,
|
|
475
|
-
resource_owner_secret=resource_owner_secret,
|
|
476
|
-
verifier=verifier,
|
|
477
|
-
)
|
|
394
|
+
Reference:
|
|
395
|
+
https://github.com/tweepy/tweepy/blob/master/tweepy/client.py#L64
|
|
396
|
+
"""
|
|
397
|
+
if response.status_code in responses:
|
|
398
|
+
# For 5xx server errors, return "Twitter Server Error"
|
|
399
|
+
if 500 <= response.status_code < 600:
|
|
400
|
+
return "Twitter Server Error"
|
|
401
|
+
else:
|
|
402
|
+
error_message = responses[response.status_code] + " Error"
|
|
403
|
+
return error_message
|
|
404
|
+
elif not 200 <= response.status_code < 300:
|
|
405
|
+
return "HTTP Exception"
|
|
406
|
+
else:
|
|
407
|
+
return "Unexpected Exception"
|
|
478
408
|
|
|
479
|
-
try:
|
|
480
|
-
oauth_tokens = oauth.fetch_access_token(access_token_url)
|
|
481
|
-
except Exception as e:
|
|
482
|
-
raise RuntimeError(
|
|
483
|
-
f"Error occurred while fetching the OAuth request token: {e}"
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
# Create a new OAuth1Session with the access token
|
|
487
|
-
oauth = OAuth1Session(
|
|
488
|
-
consumer_key,
|
|
489
|
-
client_secret=consumer_secret,
|
|
490
|
-
resource_owner_key=oauth_tokens["oauth_token"],
|
|
491
|
-
resource_owner_secret=oauth_tokens["oauth_token_secret"],
|
|
492
|
-
)
|
|
493
|
-
return oauth
|
|
494
409
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
410
|
+
TWITTER_FUNCS = [
|
|
411
|
+
FunctionTool(create_tweet),
|
|
412
|
+
FunctionTool(delete_tweet),
|
|
413
|
+
FunctionTool(get_my_user_profile),
|
|
414
|
+
FunctionTool(get_user_by_username),
|
|
415
|
+
]
|
|
498
416
|
|
|
499
|
-
Args:
|
|
500
|
-
response (requests.Response): The HTTP response to handle.
|
|
501
417
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
418
|
+
class TwitterToolkit(BaseToolkit):
|
|
419
|
+
r"""A class representing a toolkit for Twitter operations.
|
|
420
|
+
|
|
421
|
+
This class provides methods for creating a tweet, deleting a tweet, and
|
|
422
|
+
getting the authenticated user's profile information.
|
|
505
423
|
|
|
506
|
-
|
|
507
|
-
|
|
424
|
+
References:
|
|
425
|
+
https://developer.x.com/en/portal/dashboard
|
|
426
|
+
|
|
427
|
+
Notes:
|
|
428
|
+
To use this toolkit, you need to set the following environment
|
|
429
|
+
variables:
|
|
430
|
+
- TWITTER_CONSUMER_KEY: The consumer key for the Twitter API.
|
|
431
|
+
- TWITTER_CONSUMER_SECRET: The consumer secret for the Twitter API.
|
|
432
|
+
- TWITTER_ACCESS_TOKEN: The access token for the Twitter API.
|
|
433
|
+
- TWITTER_ACCESS_TOKEN_SECRET: The access token secret for the Twitter
|
|
434
|
+
API.
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
438
|
+
r"""Returns a list of FunctionTool objects representing the
|
|
439
|
+
functions in the toolkit.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
List[FunctionTool]: A list of FunctionTool objects
|
|
443
|
+
representing the functions in the toolkit.
|
|
508
444
|
"""
|
|
509
|
-
|
|
510
|
-
# For 5xx server errors, return "Twitter Server Error"
|
|
511
|
-
if 500 <= response.status_code < 600:
|
|
512
|
-
return "Twitter Server Error"
|
|
513
|
-
else:
|
|
514
|
-
error_message = responses[response.status_code] + " Error"
|
|
515
|
-
return error_message
|
|
516
|
-
elif not 200 <= response.status_code < 300:
|
|
517
|
-
return "HTTP Exception"
|
|
518
|
-
else:
|
|
519
|
-
return "Unexpected Exception"
|
|
445
|
+
return TWITTER_FUNCS
|