agentr 0.1.6__py3-none-any.whl → 0.1.8__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.
@@ -1,29 +1,309 @@
1
- from agentr.application import APIApplication
2
- from agentr.integration import Integration
3
-
4
- class RedditApp(APIApplication):
5
- def __init__(self, integration: Integration) -> None:
6
- super().__init__(name="reddit", integration=integration)
7
-
8
- def _get_headers(self):
9
- credentials = self.integration.get_credentials()
10
- if "headers" in credentials:
11
- return credentials["headers"]
12
- return {
13
- "Authorization": f"Bearer {credentials['access_token']}",
14
- }
15
-
16
- def get_subreddit_posts(self, subreddit: str) -> str:
17
- """Get the latest posts from a subreddit
18
-
19
- Args:
20
- subreddit: The subreddit to get posts from
21
-
22
- Returns:
23
- A list of posts from the subreddit
24
- """
25
-
26
-
27
- def list_tools(self):
28
- return []
29
-
1
+ from agentr.application import APIApplication
2
+ from agentr.integration import Integration
3
+ from loguru import logger
4
+
5
+ class RedditApp(APIApplication):
6
+ def __init__(self, integration: Integration) -> None:
7
+ super().__init__(name="reddit", integration=integration)
8
+ self.base_api_url = "https://oauth.reddit.com"
9
+
10
+ def _get_headers(self):
11
+ if not self.integration:
12
+ raise ValueError("Integration not configured for RedditApp")
13
+ credentials = self.integration.get_credentials()
14
+ if "access_token" not in credentials:
15
+ logger.error("Reddit credentials found but missing 'access_token'.")
16
+ raise ValueError("Invalid Reddit credentials format.")
17
+
18
+ return {
19
+ "Authorization": f"Bearer {credentials['access_token']}",
20
+ "User-Agent": "agentr-reddit-app/0.1 by AgentR"
21
+ }
22
+
23
+ def get_subreddit_posts(self, subreddit: str, limit: int = 5, timeframe: str = "day") -> str:
24
+ """Get the top posts from a specified subreddit over a given timeframe.
25
+
26
+ Args:
27
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
28
+ limit: The maximum number of posts to return (default: 5, max: 100).
29
+ timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day').
30
+
31
+ Returns:
32
+ A formatted string listing the top posts or an error message.
33
+ """
34
+ valid_timeframes = ['hour', 'day', 'week', 'month', 'year', 'all']
35
+ if timeframe not in valid_timeframes:
36
+ return f"Error: Invalid timeframe '{timeframe}'. Please use one of: {', '.join(valid_timeframes)}"
37
+
38
+ if not 1 <= limit <= 100:
39
+ return f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
40
+
41
+
42
+ url = f"{self.base_api_url}/r/{subreddit}/top"
43
+ params = {
44
+ "limit": limit,
45
+ "t": timeframe
46
+ }
47
+
48
+ logger.info(f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'")
49
+ response = self._get(url, params=params)
50
+
51
+ data = response.json()
52
+
53
+ if "error" in data:
54
+ logger.error(f"Reddit API error: {data['error']} - {data.get('message', '')}")
55
+ return f"Error from Reddit API: {data['error']} - {data.get('message', '')}"
56
+
57
+ posts = data.get("data", {}).get("children", [])
58
+
59
+ if not posts:
60
+ return f"No top posts found in r/{subreddit} for the timeframe '{timeframe}'."
61
+
62
+ result_lines = [f"Top {len(posts)} posts from r/{subreddit} (timeframe: {timeframe}):\n"]
63
+ for i, post_container in enumerate(posts):
64
+ post = post_container.get("data", {})
65
+ title = post.get('title', 'No Title')
66
+ score = post.get('score', 0)
67
+ author = post.get('author', 'Unknown Author')
68
+ permalink = post.get('permalink', '')
69
+ full_url = f"https://www.reddit.com{permalink}" if permalink else "No Link"
70
+
71
+ result_lines.append(f"{i+1}. \"{title}\" by u/{author} (Score: {score})")
72
+ result_lines.append(f" Link: {full_url}")
73
+
74
+ return "\n".join(result_lines)
75
+
76
+
77
+ def search_subreddits(self, query: str, limit: int = 5, sort: str = "relevance") -> str:
78
+ """Search for subreddits matching a query string.
79
+
80
+ Args:
81
+ query: The text to search for in subreddit names and descriptions.
82
+ limit: The maximum number of subreddits to return (default: 5, max: 100).
83
+ sort: The order of results. Valid options: 'relevance', 'activity' (default: 'relevance').
84
+
85
+ Returns:
86
+ A formatted string listing the found subreddits and their descriptions, or an error message.
87
+ """
88
+ valid_sorts = ['relevance', 'activity']
89
+ if sort not in valid_sorts:
90
+ return f"Error: Invalid sort option '{sort}'. Please use one of: {', '.join(valid_sorts)}"
91
+
92
+ if not 1 <= limit <= 100:
93
+ return f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
94
+
95
+
96
+ url = f"{self.base_api_url}/subreddits/search"
97
+ params = {
98
+ "q": query,
99
+ "limit": limit,
100
+ "sort": sort,
101
+ # Optionally include NSFW results? Defaulting to false for safety.
102
+ # "include_over_18": "false"
103
+ }
104
+
105
+ logger.info(f"Searching for subreddits matching '{query}' (limit: {limit}, sort: {sort})")
106
+ response = self._get(url, params=params)
107
+
108
+ data = response.json()
109
+
110
+ if "error" in data:
111
+ logger.error(f"Reddit API error during subreddit search: {data['error']} - {data.get('message', '')}")
112
+ return f"Error from Reddit API during search: {data['error']} - {data.get('message', '')}"
113
+
114
+ subreddits = data.get("data", {}).get("children", [])
115
+
116
+ if not subreddits:
117
+ return f"No subreddits found matching the query '{query}'."
118
+
119
+ result_lines = [f"Found {len(subreddits)} subreddits matching '{query}' (sorted by {sort}):\n"]
120
+ for i, sub_container in enumerate(subreddits):
121
+ sub_data = sub_container.get("data", {})
122
+ display_name = sub_data.get('display_name', 'N/A') # e.g., 'python'
123
+ title = sub_data.get('title', 'No Title') # Often the same as display_name or slightly longer
124
+ subscribers = sub_data.get('subscribers', 0)
125
+ # Use public_description if available, fallback to title
126
+ description = sub_data.get('public_description', '').strip() or title
127
+
128
+ # Format subscriber count nicely
129
+ subscriber_str = f"{subscribers:,}" if subscribers else "Unknown"
130
+
131
+ result_lines.append(f"{i+1}. r/{display_name} ({subscriber_str} subscribers)")
132
+ if description:
133
+ result_lines.append(f" Description: {description}")
134
+
135
+ return "\n".join(result_lines)
136
+
137
+ def get_post_flairs(self, subreddit: str):
138
+ """Retrieve the list of available post flairs for a specific subreddit.
139
+
140
+ Args:
141
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
142
+
143
+ Returns:
144
+ A list of dictionaries containing flair details, or an error message.
145
+ """
146
+
147
+ url = f"{self.base_api_url}/r/{subreddit}/api/link_flair_v2"
148
+
149
+ logger.info(f"Fetching post flairs for subreddit: r/{subreddit}")
150
+ response = self._get(url)
151
+
152
+ flairs = response.json()
153
+ if not flairs:
154
+ return f"No post flairs available for r/{subreddit}."
155
+
156
+ return flairs
157
+
158
+ def create_post(self, subreddit: str, title: str, kind: str = "self", text: str = None, url: str = None, flair_id: str = None):
159
+ """Create a new post in a specified subreddit.
160
+
161
+ Args:
162
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
163
+ title: The title of the post.
164
+ kind: The type of post; either 'self' (text post) or 'link' (link or image post).
165
+ text: The text content of the post; required if kind is 'self'.
166
+ url: The URL of the link or image; required if kind is 'link'.
167
+ For image posts to be displayed correctly, the URL must directly point to an image file
168
+ and end with a valid image extension (e.g., .jpg, .png, or .gif).
169
+ Note that .gif support can be inconsistent.
170
+ flair_id: The ID of the flair to assign to the post.
171
+
172
+ Returns:
173
+ The JSON response from the Reddit API, or an error message as a string.
174
+ If the reddit api returns an error within the json response, that error will be returned as a string.
175
+ """
176
+
177
+ if kind not in ["self", "link"]:
178
+ raise ValueError("Invalid post kind. Must be one of 'self' or 'link'.")
179
+
180
+ if kind == "self" and not text:
181
+ raise ValueError("Text content is required for text posts.")
182
+ if kind == "link" and not url:
183
+ raise ValueError("URL is required for link posts (including images).")
184
+
185
+ data = {
186
+ "sr": subreddit,
187
+ "title": title,
188
+ "kind": kind,
189
+ "text": text,
190
+ "url": url,
191
+ "flair_id": flair_id,
192
+ }
193
+ data = {k: v for k, v in data.items() if v is not None}
194
+
195
+ url_api = f"{self.base_api_url}/api/submit"
196
+ logger.info(f"Submitting a new post to r/{subreddit}")
197
+ response = self._post(url_api, data=data)
198
+ response_json = response.json()
199
+
200
+ # Check for Reddit API errors in the response
201
+ if response_json and "json" in response_json and "errors" in response_json["json"]:
202
+ errors = response_json["json"]["errors"]
203
+ if errors:
204
+ error_message = ", ".join([f"{code}: {message}" for code, message in errors])
205
+ return f"Reddit API error: {error_message}"
206
+
207
+ return response_json
208
+
209
+ def get_comment_by_id(self, comment_id: str) -> dict:
210
+ """
211
+ Retrieve a specific Reddit comment by its full ID (t1_commentid).
212
+
213
+ Args:
214
+ comment_id: The full unique ID of the comment (e.g., 't1_abcdef').
215
+
216
+ Returns:
217
+ A dictionary containing the comment data, or an error message if retrieval fails.
218
+ """
219
+
220
+ # Define the endpoint URL
221
+ url = f"https://oauth.reddit.com/api/info.json?id={comment_id}"
222
+
223
+ # Make the GET request to the Reddit API
224
+
225
+ response = self._get(url)
226
+
227
+ data = response.json()
228
+ comments = data.get("data", {}).get("children", [])
229
+ if comments:
230
+ return comments[0]["data"]
231
+ else:
232
+ return {"error": "Comment not found."}
233
+
234
+ def post_comment(self, parent_id: str, text: str) -> dict:
235
+ """
236
+ Post a comment to a Reddit post or another comment.
237
+
238
+ Args:
239
+ parent_id: The full ID of the parent comment or post (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
240
+ text: The text content of the comment.
241
+
242
+ Returns:
243
+ A dictionary containing the response from the Reddit API, or an error message if posting fails.
244
+ """
245
+
246
+ url = f"{self.base_api_url}/api/comment"
247
+ data = {
248
+ "parent": parent_id,
249
+ "text": text,
250
+ }
251
+
252
+ logger.info(f"Posting comment to {parent_id}")
253
+ response = self._post(url, data=data)
254
+
255
+ return response.json()
256
+
257
+ def edit_content(self, content_id: str, text: str) -> dict:
258
+ """
259
+ Edit the text content of a Reddit post or comment.
260
+
261
+ Args:
262
+ content_id: The full ID of the content to edit (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
263
+ text: The new text content.
264
+
265
+ Returns:
266
+ A dictionary containing the response from the Reddit API, or an error message if editing fails.
267
+ """
268
+
269
+ url = f"{self.base_api_url}/api/editusertext"
270
+ data = {
271
+ "thing_id": content_id,
272
+ "text": text,
273
+ }
274
+
275
+ logger.info(f"Editing content {content_id}")
276
+ response = self._post(url, data=data)
277
+
278
+ return response.json()
279
+
280
+
281
+ def delete_content(self, content_id: str) -> dict:
282
+ """
283
+ Delete a Reddit post or comment.
284
+
285
+ Args:
286
+ content_id: The full ID of the content to delete (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
287
+
288
+ Returns:
289
+ A dictionary containing the response from the Reddit API, or an error message if deletion fails.
290
+ """
291
+
292
+ url = f"{self.base_api_url}/api/del"
293
+ data = {
294
+ "id": content_id,
295
+ }
296
+
297
+ logger.info(f"Deleting content {content_id}")
298
+ response = self._post(url, data=data)
299
+ response.raise_for_status()
300
+
301
+ # Reddit's delete endpoint returns an empty response on success.
302
+ # We'll just return a success message.
303
+ return {"message": f"Content {content_id} deleted successfully."}
304
+
305
+ def list_tools(self):
306
+ return [
307
+ self.get_subreddit_posts, self.search_subreddits, self.get_post_flairs, self.create_post,
308
+ self.get_comment_by_id, self.post_comment, self.edit_content, self.delete_content
309
+ ]
@@ -1,43 +1,43 @@
1
- from agentr.application import APIApplication
2
- from agentr.integration import Integration
3
-
4
- class ResendApp(APIApplication):
5
- def __init__(self, integration: Integration) -> None:
6
- super().__init__(name="resend", integration=integration)
7
-
8
- def _get_headers(self):
9
- credentials = self.integration.get_credentials()
10
- if not credentials:
11
- raise ValueError("No credentials found")
12
- return {
13
- "Authorization": f"Bearer {credentials['api_key']}",
14
- }
15
-
16
- def send_email(self, to: str, subject: str, content: str) -> str:
17
- """Send an email using the Resend API
18
-
19
- Args:
20
- to: The email address to send the email to
21
- subject: The subject of the email
22
- content: The content of the email
23
-
24
- Returns:
25
- A message indicating that the email was sent successfully
26
- """
27
- credentials = self.integration.get_credentials()
28
- if not credentials:
29
- raise ValueError("No credentials found")
30
- from_email = credentials.get("from_email", "Manoj <manoj@agentr.dev>")
31
- url = "https://api.resend.com/emails"
32
- body = {
33
- "from": from_email,
34
- "to": [to],
35
- "subject": subject,
36
- "text": content
37
- }
38
- self._post(url, body)
39
- return "Sent Successfully"
40
-
41
- def list_tools(self):
42
- return [self.send_email]
43
-
1
+ from agentr.application import APIApplication
2
+ from agentr.integration import Integration
3
+
4
+ class ResendApp(APIApplication):
5
+ def __init__(self, integration: Integration) -> None:
6
+ super().__init__(name="resend", integration=integration)
7
+
8
+ def _get_headers(self):
9
+ credentials = self.integration.get_credentials()
10
+ if not credentials:
11
+ raise ValueError("No credentials found")
12
+ return {
13
+ "Authorization": f"Bearer {credentials['api_key']}",
14
+ }
15
+
16
+ def send_email(self, to: str, subject: str, content: str) -> str:
17
+ """Send an email using the Resend API
18
+
19
+ Args:
20
+ to: The email address to send the email to
21
+ subject: The subject of the email
22
+ content: The content of the email
23
+
24
+ Returns:
25
+ A message indicating that the email was sent successfully
26
+ """
27
+ credentials = self.integration.get_credentials()
28
+ if not credentials:
29
+ raise ValueError("No credentials found")
30
+ from_email = credentials.get("from_email", "Manoj <manoj@agentr.dev>")
31
+ url = "https://api.resend.com/emails"
32
+ body = {
33
+ "from": from_email,
34
+ "to": [to],
35
+ "subject": subject,
36
+ "text": content
37
+ }
38
+ self._post(url, body)
39
+ return "Sent Successfully"
40
+
41
+ def list_tools(self):
42
+ return [self.send_email]
43
+
@@ -1,57 +1,57 @@
1
- from agentr.application import APIApplication
2
- from agentr.integration import Integration
3
-
4
- class TavilyApp(APIApplication):
5
- def __init__(self, integration: Integration) -> None:
6
- name = "tavily"
7
- self.base_url = "https://api.tavily.com"
8
- super().__init__(name=name, integration=integration)
9
-
10
- def _get_headers(self):
11
- credentials = self.integration.get_credentials()
12
- if not credentials:
13
- raise ValueError("No credentials found")
14
- return {
15
- "Authorization": f"Bearer {credentials['api_key']}",
16
- "Content-Type": "application/json"
17
- }
18
-
19
- def search(self, query: str) -> str:
20
- """Search the web using Tavily's search API
21
-
22
- Args:
23
- query: The search query
24
-
25
- Returns:
26
- str: A summary of search results
27
- """
28
- self.validate()
29
- url = f"{self.base_url}/search"
30
- payload = {
31
- "query": query,
32
- "topic": "general",
33
- "search_depth": "basic",
34
- "max_results": 3,
35
- "include_answer": True,
36
- "include_raw_content": False,
37
- "include_images": False,
38
- "include_image_descriptions": False,
39
- "include_domains": [],
40
- "exclude_domains": []
41
- }
42
-
43
- response = self._post(url, payload)
44
- result = response.json()
45
-
46
- if "answer" in result:
47
- return result["answer"]
48
-
49
- # Fallback to combining top results if no direct answer
50
- summaries = []
51
- for item in result.get("results", [])[:3]:
52
- summaries.append(f"• {item['title']}: {item['snippet']}")
53
-
54
- return "\n".join(summaries)
55
-
56
- def list_tools(self):
57
- return [self.search]
1
+ from agentr.application import APIApplication
2
+ from agentr.integration import Integration
3
+
4
+ class TavilyApp(APIApplication):
5
+ def __init__(self, integration: Integration) -> None:
6
+ name = "tavily"
7
+ self.base_url = "https://api.tavily.com"
8
+ super().__init__(name=name, integration=integration)
9
+
10
+ def _get_headers(self):
11
+ credentials = self.integration.get_credentials()
12
+ if not credentials:
13
+ raise ValueError("No credentials found")
14
+ return {
15
+ "Authorization": f"Bearer {credentials['api_key']}",
16
+ "Content-Type": "application/json"
17
+ }
18
+
19
+ def search(self, query: str) -> str:
20
+ """Search the web using Tavily's search API
21
+
22
+ Args:
23
+ query: The search query
24
+
25
+ Returns:
26
+ str: A summary of search results
27
+ """
28
+ self.validate()
29
+ url = f"{self.base_url}/search"
30
+ payload = {
31
+ "query": query,
32
+ "topic": "general",
33
+ "search_depth": "basic",
34
+ "max_results": 3,
35
+ "include_answer": True,
36
+ "include_raw_content": False,
37
+ "include_images": False,
38
+ "include_image_descriptions": False,
39
+ "include_domains": [],
40
+ "exclude_domains": []
41
+ }
42
+
43
+ response = self._post(url, payload)
44
+ result = response.json()
45
+
46
+ if "answer" in result:
47
+ return result["answer"]
48
+
49
+ # Fallback to combining top results if no direct answer
50
+ summaries = []
51
+ for item in result.get("results", [])[:3]:
52
+ summaries.append(f"• {item['title']}: {item['snippet']}")
53
+
54
+ return "\n".join(summaries)
55
+
56
+ def list_tools(self):
57
+ return [self.search]
@@ -1,21 +1,21 @@
1
- from agentr.application import APIApplication
2
-
3
-
4
- class ZenQuoteApp(APIApplication):
5
- def __init__(self, **kwargs) -> None:
6
- super().__init__(name="zenquote", **kwargs)
7
-
8
- def get_quote(self) -> str:
9
- """Get an inspirational quote from the Zen Quotes API
10
-
11
- Returns:
12
- A random inspirational quote
13
- """
14
- url = "https://zenquotes.io/api/random"
15
- response = self._get(url)
16
- data = response.json()
17
- quote_data = data[0]
18
- return f"{quote_data['q']} - {quote_data['a']}"
19
-
20
- def list_tools(self):
1
+ from agentr.application import APIApplication
2
+
3
+
4
+ class ZenQuoteApp(APIApplication):
5
+ def __init__(self, **kwargs) -> None:
6
+ super().__init__(name="zenquote", **kwargs)
7
+
8
+ def get_quote(self) -> str:
9
+ """Get an inspirational quote from the Zen Quotes API
10
+
11
+ Returns:
12
+ A random inspirational quote
13
+ """
14
+ url = "https://zenquotes.io/api/random"
15
+ response = self._get(url)
16
+ data = response.json()
17
+ quote_data = data[0]
18
+ return f"{quote_data['q']} - {quote_data['a']}"
19
+
20
+ def list_tools(self):
21
21
  return [self.get_quote]