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.
- agentr/__init__.py +1 -1
- agentr/application.py +93 -56
- agentr/applications/__init__.py +27 -0
- agentr/applications/github/app.py +319 -56
- agentr/applications/google_calendar/app.py +489 -74
- agentr/applications/google_mail/app.py +565 -68
- agentr/applications/reddit/app.py +309 -29
- agentr/applications/resend/app.py +43 -43
- agentr/applications/tavily/app.py +57 -57
- agentr/applications/zenquotes/app.py +20 -20
- agentr/cli.py +76 -75
- agentr/config.py +15 -0
- agentr/exceptions.py +6 -5
- agentr/integration.py +161 -98
- agentr/integrations/README.md +25 -0
- agentr/integrations/__init__.py +5 -0
- agentr/integrations/agentr.py +87 -0
- agentr/integrations/api_key.py +16 -0
- agentr/integrations/base.py +60 -0
- agentr/server.py +128 -105
- agentr/store.py +70 -70
- agentr/test.py +14 -37
- agentr/utils/openapi.py +273 -184
- {agentr-0.1.6.dist-info → agentr-0.1.8.dist-info}/METADATA +4 -1
- agentr-0.1.8.dist-info/RECORD +30 -0
- {agentr-0.1.6.dist-info → agentr-0.1.8.dist-info}/licenses/LICENSE +21 -21
- agentr-0.1.6.dist-info/RECORD +0 -23
- {agentr-0.1.6.dist-info → agentr-0.1.8.dist-info}/WHEEL +0 -0
- {agentr-0.1.6.dist-info → agentr-0.1.8.dist-info}/entry_points.txt +0 -0
@@ -1,29 +1,309 @@
|
|
1
|
-
from agentr.application import APIApplication
|
2
|
-
from agentr.integration import Integration
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
"""
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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]
|