agentr 0.1.7__py3-none-any.whl → 0.1.9__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,329 @@
1
+ import httpx
1
2
  from agentr.application import APIApplication
2
3
  from agentr.integration import Integration
4
+ from agentr.exceptions import NotAuthorizedError
5
+ from loguru import logger
3
6
 
4
7
  class RedditApp(APIApplication):
5
8
  def __init__(self, integration: Integration) -> None:
6
9
  super().__init__(name="reddit", integration=integration)
10
+ self.base_api_url = "https://oauth.reddit.com"
11
+
12
+ def _post(self, url, data):
13
+ try:
14
+ headers = self._get_headers()
15
+ response = httpx.post(url, headers=headers, data=data)
16
+ response.raise_for_status()
17
+ return response
18
+ except NotAuthorizedError as e:
19
+ logger.warning(f"Authorization needed: {e.message}")
20
+ raise e
21
+ except httpx.HTTPStatusError as e:
22
+ if e.response.status_code == 429:
23
+ return e.response.text or "Rate limit exceeded. Please try again later."
24
+ else:
25
+ raise e
26
+ except Exception as e:
27
+ logger.error(f"Error posting {url}: {e}")
28
+ raise e
7
29
 
8
30
  def _get_headers(self):
31
+ if not self.integration:
32
+ raise ValueError("Integration not configured for RedditApp")
9
33
  credentials = self.integration.get_credentials()
10
- if "headers" in credentials:
11
- return credentials["headers"]
34
+ if "access_token" not in credentials:
35
+ logger.error("Reddit credentials found but missing 'access_token'.")
36
+ raise ValueError("Invalid Reddit credentials format.")
37
+
12
38
  return {
13
39
  "Authorization": f"Bearer {credentials['access_token']}",
40
+ "User-Agent": "agentr-reddit-app/0.1 by AgentR"
14
41
  }
42
+
43
+ def get_subreddit_posts(self, subreddit: str, limit: int = 5, timeframe: str = "day") -> str:
44
+ """Get the top posts from a specified subreddit over a given timeframe.
45
+
46
+ Args:
47
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
48
+ limit: The maximum number of posts to return (default: 5, max: 100).
49
+ timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day').
50
+
51
+ Returns:
52
+ A formatted string listing the top posts or an error message.
53
+ """
54
+ valid_timeframes = ['hour', 'day', 'week', 'month', 'year', 'all']
55
+ if timeframe not in valid_timeframes:
56
+ return f"Error: Invalid timeframe '{timeframe}'. Please use one of: {', '.join(valid_timeframes)}"
57
+
58
+ if not 1 <= limit <= 100:
59
+ return f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
60
+
61
+
62
+ url = f"{self.base_api_url}/r/{subreddit}/top"
63
+ params = {
64
+ "limit": limit,
65
+ "t": timeframe
66
+ }
67
+
68
+ logger.info(f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'")
69
+ response = self._get(url, params=params)
15
70
 
16
- def get_subreddit_posts(self, subreddit: str) -> str:
17
- """Get the latest posts from a subreddit
71
+ data = response.json()
72
+
73
+ if "error" in data:
74
+ logger.error(f"Reddit API error: {data['error']} - {data.get('message', '')}")
75
+ return f"Error from Reddit API: {data['error']} - {data.get('message', '')}"
76
+
77
+ posts = data.get("data", {}).get("children", [])
78
+
79
+ if not posts:
80
+ return f"No top posts found in r/{subreddit} for the timeframe '{timeframe}'."
81
+
82
+ result_lines = [f"Top {len(posts)} posts from r/{subreddit} (timeframe: {timeframe}):\n"]
83
+ for i, post_container in enumerate(posts):
84
+ post = post_container.get("data", {})
85
+ title = post.get('title', 'No Title')
86
+ score = post.get('score', 0)
87
+ author = post.get('author', 'Unknown Author')
88
+ permalink = post.get('permalink', '')
89
+ full_url = f"https://www.reddit.com{permalink}" if permalink else "No Link"
90
+
91
+ result_lines.append(f"{i+1}. \"{title}\" by u/{author} (Score: {score})")
92
+ result_lines.append(f" Link: {full_url}")
93
+
94
+ return "\n".join(result_lines)
95
+
96
+
97
+ def search_subreddits(self, query: str, limit: int = 5, sort: str = "relevance") -> str:
98
+ """Search for subreddits matching a query string.
18
99
 
19
100
  Args:
20
- subreddit: The subreddit to get posts from
101
+ query: The text to search for in subreddit names and descriptions.
102
+ limit: The maximum number of subreddits to return (default: 5, max: 100).
103
+ sort: The order of results. Valid options: 'relevance', 'activity' (default: 'relevance').
21
104
 
22
105
  Returns:
23
- A list of posts from the subreddit
106
+ A formatted string listing the found subreddits and their descriptions, or an error message.
24
107
  """
108
+ valid_sorts = ['relevance', 'activity']
109
+ if sort not in valid_sorts:
110
+ return f"Error: Invalid sort option '{sort}'. Please use one of: {', '.join(valid_sorts)}"
25
111
 
112
+ if not 1 <= limit <= 100:
113
+ return f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
26
114
 
27
- def list_tools(self):
28
- return []
29
115
 
116
+ url = f"{self.base_api_url}/subreddits/search"
117
+ params = {
118
+ "q": query,
119
+ "limit": limit,
120
+ "sort": sort,
121
+ # Optionally include NSFW results? Defaulting to false for safety.
122
+ # "include_over_18": "false"
123
+ }
124
+
125
+ logger.info(f"Searching for subreddits matching '{query}' (limit: {limit}, sort: {sort})")
126
+ response = self._get(url, params=params)
127
+
128
+ data = response.json()
129
+
130
+ if "error" in data:
131
+ logger.error(f"Reddit API error during subreddit search: {data['error']} - {data.get('message', '')}")
132
+ return f"Error from Reddit API during search: {data['error']} - {data.get('message', '')}"
133
+
134
+ subreddits = data.get("data", {}).get("children", [])
135
+
136
+ if not subreddits:
137
+ return f"No subreddits found matching the query '{query}'."
138
+
139
+ result_lines = [f"Found {len(subreddits)} subreddits matching '{query}' (sorted by {sort}):\n"]
140
+ for i, sub_container in enumerate(subreddits):
141
+ sub_data = sub_container.get("data", {})
142
+ display_name = sub_data.get('display_name', 'N/A') # e.g., 'python'
143
+ title = sub_data.get('title', 'No Title') # Often the same as display_name or slightly longer
144
+ subscribers = sub_data.get('subscribers', 0)
145
+ # Use public_description if available, fallback to title
146
+ description = sub_data.get('public_description', '').strip() or title
147
+
148
+ # Format subscriber count nicely
149
+ subscriber_str = f"{subscribers:,}" if subscribers else "Unknown"
150
+
151
+ result_lines.append(f"{i+1}. r/{display_name} ({subscriber_str} subscribers)")
152
+ if description:
153
+ result_lines.append(f" Description: {description}")
154
+
155
+ return "\n".join(result_lines)
156
+
157
+ def get_post_flairs(self, subreddit: str):
158
+ """Retrieve the list of available post flairs for a specific subreddit.
159
+
160
+ Args:
161
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
162
+
163
+ Returns:
164
+ A list of dictionaries containing flair details, or an error message.
165
+ """
166
+
167
+ url = f"{self.base_api_url}/r/{subreddit}/api/link_flair_v2"
168
+
169
+ logger.info(f"Fetching post flairs for subreddit: r/{subreddit}")
170
+ response = self._get(url)
171
+
172
+ flairs = response.json()
173
+ if not flairs:
174
+ return f"No post flairs available for r/{subreddit}."
175
+
176
+ return flairs
177
+
178
+ def create_post(self, subreddit: str, title: str, kind: str = "self", text: str = None, url: str = None, flair_id: str = None):
179
+ """Create a new post in a specified subreddit.
180
+
181
+ Args:
182
+ subreddit: The name of the subreddit (e.g., 'python', 'worldnews') without the 'r/'.
183
+ title: The title of the post.
184
+ kind: The type of post; either 'self' (text post) or 'link' (link or image post).
185
+ text: The text content of the post; required if kind is 'self'.
186
+ url: The URL of the link or image; required if kind is 'link'.
187
+ For image posts to be displayed correctly, the URL must directly point to an image file
188
+ and end with a valid image extension (e.g., .jpg, .png, or .gif).
189
+ Note that .gif support can be inconsistent.
190
+ flair_id: The ID of the flair to assign to the post.
191
+
192
+ Returns:
193
+ The JSON response from the Reddit API, or an error message as a string.
194
+ If the reddit api returns an error within the json response, that error will be returned as a string.
195
+ """
196
+
197
+ if kind not in ["self", "link"]:
198
+ raise ValueError("Invalid post kind. Must be one of 'self' or 'link'.")
199
+
200
+ if kind == "self" and not text:
201
+ raise ValueError("Text content is required for text posts.")
202
+ if kind == "link" and not url:
203
+ raise ValueError("URL is required for link posts (including images).")
204
+
205
+ data = {
206
+ "sr": subreddit,
207
+ "title": title,
208
+ "kind": kind,
209
+ "text": text,
210
+ "url": url,
211
+ "flair_id": flair_id,
212
+ }
213
+ data = {k: v for k, v in data.items() if v is not None}
214
+
215
+ url_api = f"{self.base_api_url}/api/submit"
216
+ logger.info(f"Submitting a new post to r/{subreddit}")
217
+ response = self._post(url_api, data=data)
218
+ response_json = response.json()
219
+
220
+ # Check for Reddit API errors in the response
221
+ if response_json and "json" in response_json and "errors" in response_json["json"]:
222
+ errors = response_json["json"]["errors"]
223
+ if errors:
224
+ error_message = ", ".join([f"{code}: {message}" for code, message in errors])
225
+ return f"Reddit API error: {error_message}"
226
+
227
+ return response_json
228
+
229
+ def get_comment_by_id(self, comment_id: str) -> dict:
230
+ """
231
+ Retrieve a specific Reddit comment by its full ID (t1_commentid).
232
+
233
+ Args:
234
+ comment_id: The full unique ID of the comment (e.g., 't1_abcdef').
235
+
236
+ Returns:
237
+ A dictionary containing the comment data, or an error message if retrieval fails.
238
+ """
239
+
240
+ # Define the endpoint URL
241
+ url = f"https://oauth.reddit.com/api/info.json?id={comment_id}"
242
+
243
+ # Make the GET request to the Reddit API
244
+
245
+ response = self._get(url)
246
+
247
+ data = response.json()
248
+ comments = data.get("data", {}).get("children", [])
249
+ if comments:
250
+ return comments[0]["data"]
251
+ else:
252
+ return {"error": "Comment not found."}
253
+
254
+ def post_comment(self, parent_id: str, text: str) -> dict:
255
+ """
256
+ Post a comment to a Reddit post or another comment.
257
+
258
+ Args:
259
+ parent_id: The full ID of the parent comment or post (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
260
+ text: The text content of the comment.
261
+
262
+ Returns:
263
+ A dictionary containing the response from the Reddit API, or an error message if posting fails.
264
+ """
265
+
266
+ url = f"{self.base_api_url}/api/comment"
267
+ data = {
268
+ "parent": parent_id,
269
+ "text": text,
270
+ }
271
+
272
+ logger.info(f"Posting comment to {parent_id}")
273
+ response = self._post(url, data=data)
274
+
275
+ return response.json()
276
+
277
+ def edit_content(self, content_id: str, text: str) -> dict:
278
+ """
279
+ Edit the text content of a Reddit post or comment.
280
+
281
+ Args:
282
+ content_id: The full ID of the content to edit (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
283
+ text: The new text content.
284
+
285
+ Returns:
286
+ A dictionary containing the response from the Reddit API, or an error message if editing fails.
287
+ """
288
+
289
+ url = f"{self.base_api_url}/api/editusertext"
290
+ data = {
291
+ "thing_id": content_id,
292
+ "text": text,
293
+ }
294
+
295
+ logger.info(f"Editing content {content_id}")
296
+ response = self._post(url, data=data)
297
+
298
+ return response.json()
299
+
300
+
301
+ def delete_content(self, content_id: str) -> dict:
302
+ """
303
+ Delete a Reddit post or comment.
304
+
305
+ Args:
306
+ content_id: The full ID of the content to delete (e.g., 't3_abc123' for a post, 't1_def456' for a comment).
307
+
308
+ Returns:
309
+ A dictionary containing the response from the Reddit API, or an error message if deletion fails.
310
+ """
311
+
312
+ url = f"{self.base_api_url}/api/del"
313
+ data = {
314
+ "id": content_id,
315
+ }
316
+
317
+ logger.info(f"Deleting content {content_id}")
318
+ response = self._post(url, data=data)
319
+ response.raise_for_status()
320
+
321
+ # Reddit's delete endpoint returns an empty response on success.
322
+ # We'll just return a success message.
323
+ return {"message": f"Content {content_id} deleted successfully."}
324
+
325
+ def list_tools(self):
326
+ return [
327
+ self.get_subreddit_posts, self.search_subreddits, self.get_post_flairs, self.create_post,
328
+ self.get_comment_by_id, self.post_comment, self.edit_content, self.delete_content
329
+ ]
agentr/cli.py CHANGED
@@ -59,6 +59,8 @@ def install(app_name: str):
59
59
 
60
60
  with open(config_path, 'r') as f:
61
61
  config = json.load(f)
62
+ if 'mcpServers' not in config:
63
+ config['mcpServers'] = {}
62
64
  config['mcpServers']['agentr'] = {
63
65
  "command": "uvx",
64
66
  "args": ["agentr@latest", "run"],
@@ -66,6 +68,35 @@ def install(app_name: str):
66
68
  "AGENTR_API_KEY": api_key
67
69
  }
68
70
  }
71
+ with open(config_path, 'w') as f:
72
+ json.dump(config, f, indent=4)
73
+ typer.echo("App installed successfully")
74
+ elif app_name == "cursor":
75
+ typer.echo(f"Installing mcp server for: {app_name}")
76
+
77
+ # Set up Cursor config path
78
+ config_path = Path.home() / ".cursor/mcp.json"
79
+
80
+ # Create config directory if it doesn't exist
81
+ config_path.parent.mkdir(parents=True, exist_ok=True)
82
+
83
+ # Create or load existing config
84
+ if config_path.exists():
85
+ with open(config_path, 'r') as f:
86
+ config = json.load(f)
87
+ else:
88
+ config = {}
89
+
90
+ if 'mcpServers' not in config:
91
+ config['mcpServers'] = {}
92
+ config['mcpServers']['agentr'] = {
93
+ "command": "uvx",
94
+ "args": ["agentr@latest", "run"],
95
+ "env": {
96
+ "AGENTR_API_KEY": api_key
97
+ }
98
+ }
99
+
69
100
  with open(config_path, 'w') as f:
70
101
  json.dump(config, f, indent=4)
71
102
  typer.echo("App installed successfully")
agentr/integration.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
2
  import os
3
- import sys
4
3
 
5
4
  from loguru import logger
5
+ from agentr.exceptions import NotAuthorizedError
6
6
  from agentr.store import Store
7
7
  import httpx
8
8
 
@@ -14,21 +14,53 @@ Supported integrations:
14
14
  - API Key Integration
15
15
  """
16
16
 
17
+
17
18
  class Integration(ABC):
19
+ """Abstract base class for handling application integrations and authentication.
20
+
21
+ This class defines the interface for different types of integrations that handle
22
+ authentication and authorization with external services.
23
+
24
+ Args:
25
+ name: The name identifier for this integration
26
+ store: Optional Store instance for persisting credentials and other data
27
+
28
+ Attributes:
29
+ name: The name identifier for this integration
30
+ store: Store instance for persisting credentials and other data
31
+ """
18
32
  def __init__(self, name: str, store: Store = None):
19
33
  self.name = name
20
34
  self.store = store
21
35
 
22
36
  @abstractmethod
23
37
  def authorize(self):
38
+ """Authorize the integration.
39
+
40
+ Returns:
41
+ str: Authorization URL.
42
+ """
24
43
  pass
25
44
 
26
45
  @abstractmethod
27
46
  def get_credentials(self):
47
+ """Get credentials for the integration.
48
+
49
+ Returns:
50
+ dict: Credentials for the integration.
51
+
52
+ Raises:
53
+ NotAuthorizedError: If credentials are not found.
54
+ """
28
55
  pass
29
56
 
30
57
  @abstractmethod
31
58
  def set_credentials(self, credentials: dict):
59
+ """Set credentials for the integration.
60
+
61
+ Args:
62
+ credentials: Credentials for the integration.
63
+ """
32
64
  pass
33
65
 
34
66
  class ApiKeyIntegration(Integration):
@@ -46,8 +78,19 @@ class ApiKeyIntegration(Integration):
46
78
  return {"text": "Please configure the environment variable {self.name}_API_KEY"}
47
79
 
48
80
 
49
-
50
81
  class AgentRIntegration(Integration):
82
+ """Integration class for AgentR API authentication and authorization.
83
+
84
+ This class handles API key authentication and OAuth authorization flow for AgentR services.
85
+
86
+ Args:
87
+ name (str): Name of the integration
88
+ api_key (str, optional): AgentR API key. If not provided, will look for AGENTR_API_KEY env var
89
+ **kwargs: Additional keyword arguments passed to parent Integration class
90
+
91
+ Raises:
92
+ ValueError: If no API key is provided or found in environment variables
93
+ """
51
94
  def __init__(self, name: str, api_key: str = None, **kwargs):
52
95
  super().__init__(name, **kwargs)
53
96
  self.api_key = api_key or os.getenv("AGENTR_API_KEY")
@@ -56,12 +99,32 @@ class AgentRIntegration(Integration):
56
99
  raise ValueError("AgentR API key required - get one at https://agentr.dev")
57
100
  self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
58
101
 
59
-
60
102
  def set_credentials(self, credentials: dict| None = None):
103
+ """Set credentials for the integration.
104
+
105
+ This method is not implemented for AgentR integration. Instead it redirects to the authorize flow.
106
+
107
+ Args:
108
+ credentials (dict | None, optional): Credentials dict (not used). Defaults to None.
109
+
110
+ Returns:
111
+ str: Authorization URL from authorize() method
112
+ """
61
113
  return self.authorize()
62
114
  # raise NotImplementedError("AgentR Integration does not support setting credentials. Visit the authorize url to set credentials.")
63
115
 
64
116
  def get_credentials(self):
117
+ """Get credentials for the integration from the AgentR API.
118
+
119
+ Makes API request to retrieve stored credentials for this integration.
120
+
121
+ Returns:
122
+ dict: Credentials data from API response
123
+
124
+ Raises:
125
+ NotAuthorizedError: If credentials are not found (404 response)
126
+ HTTPError: For other API errors
127
+ """
65
128
  response = httpx.get(
66
129
  f"{self.base_url}/api/{self.name}/credentials/",
67
130
  headers={
@@ -69,11 +132,24 @@ class AgentRIntegration(Integration):
69
132
  "X-API-KEY": self.api_key
70
133
  }
71
134
  )
135
+ if response.status_code == 404:
136
+ action = self.authorize()
137
+ raise NotAuthorizedError(action)
72
138
  response.raise_for_status()
73
139
  data = response.json()
74
140
  return data
75
141
 
76
142
  def authorize(self):
143
+ """Get authorization URL for the integration.
144
+
145
+ Makes API request to get OAuth authorization URL.
146
+
147
+ Returns:
148
+ str: Message containing authorization URL
149
+
150
+ Raises:
151
+ HTTPError: If API request fails
152
+ """
77
153
  response = httpx.get(
78
154
  f"{self.base_url}/api/{self.name}/authorize/",
79
155
  headers={
@@ -0,0 +1,25 @@
1
+ # Integrations
2
+
3
+ This package provides integration classes for handling authentication and authorization with external services.
4
+
5
+ ## Overview
6
+
7
+ An Integration defines how an application authenticates and authorizes with a service provider. The base `Integration` class provides an interface that all integrations must implement.
8
+
9
+ ## Supported Integrations
10
+
11
+ ### AgentR Integration
12
+ The `AgentRIntegration` class handles OAuth-based authentication flow with the AgentR API. It requires an API key which can be obtained from [agentr.dev](https://agentr.dev).
13
+
14
+ ### API Key Integration
15
+ The `ApiKeyIntegration` class provides a simple API key based authentication mechanism. API keys are configured via environment variables.
16
+
17
+ ## Usage
18
+
19
+ Each integration implements three key methods:
20
+
21
+ - `authorize()` - Initiates the authorization flow
22
+ - `get_credentials()` - Retrieves stored credentials
23
+ - `set_credentials()` - Stores new credentials
24
+
25
+ See the individual integration classes for specific usage details.
@@ -0,0 +1,5 @@
1
+ from agentr.integrations.agentr import AgentRIntegration
2
+ from agentr.integrations.base import Integration
3
+ from agentr.integrations.api_key import ApiKeyIntegration
4
+
5
+ __all__ = ["AgentRIntegration", "Integration", "ApiKeyIntegration"]
@@ -0,0 +1,87 @@
1
+ from agentr.integrations.base import Integration
2
+ import os
3
+ import httpx
4
+ from loguru import logger
5
+ from agentr.exceptions import NotAuthorizedError
6
+
7
+ class AgentRIntegration(Integration):
8
+ """Integration class for AgentR API authentication and authorization.
9
+
10
+ This class handles API key authentication and OAuth authorization flow for AgentR services.
11
+
12
+ Args:
13
+ name (str): Name of the integration
14
+ api_key (str, optional): AgentR API key. If not provided, will look for AGENTR_API_KEY env var
15
+ **kwargs: Additional keyword arguments passed to parent Integration class
16
+
17
+ Raises:
18
+ ValueError: If no API key is provided or found in environment variables
19
+ """
20
+ def __init__(self, name: str, api_key: str = None, **kwargs):
21
+ super().__init__(name, **kwargs)
22
+ self.api_key = api_key or os.getenv("AGENTR_API_KEY")
23
+ if not self.api_key:
24
+ logger.error("API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable.")
25
+ raise ValueError("AgentR API key required - get one at https://agentr.dev")
26
+ self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
27
+
28
+ def set_credentials(self, credentials: dict| None = None):
29
+ """Set credentials for the integration.
30
+
31
+ This method is not implemented for AgentR integration. Instead it redirects to the authorize flow.
32
+
33
+ Args:
34
+ credentials (dict | None, optional): Credentials dict (not used). Defaults to None.
35
+
36
+ Returns:
37
+ str: Authorization URL from authorize() method
38
+ """
39
+ return self.authorize()
40
+ # raise NotImplementedError("AgentR Integration does not support setting credentials. Visit the authorize url to set credentials.")
41
+
42
+ def get_credentials(self):
43
+ """Get credentials for the integration from the AgentR API.
44
+
45
+ Makes API request to retrieve stored credentials for this integration.
46
+
47
+ Returns:
48
+ dict: Credentials data from API response
49
+
50
+ Raises:
51
+ NotAuthorizedError: If credentials are not found (404 response)
52
+ HTTPError: For other API errors
53
+ """
54
+ response = httpx.get(
55
+ f"{self.base_url}/api/{self.name}/credentials/",
56
+ headers={
57
+ "accept": "application/json",
58
+ "X-API-KEY": self.api_key
59
+ }
60
+ )
61
+ if response.status_code == 404:
62
+ action = self.authorize()
63
+ raise NotAuthorizedError(action)
64
+ response.raise_for_status()
65
+ data = response.json()
66
+ return data
67
+
68
+ def authorize(self):
69
+ """Get authorization URL for the integration.
70
+
71
+ Makes API request to get OAuth authorization URL.
72
+
73
+ Returns:
74
+ str: Message containing authorization URL
75
+
76
+ Raises:
77
+ HTTPError: If API request fails
78
+ """
79
+ response = httpx.get(
80
+ f"{self.base_url}/api/{self.name}/authorize/",
81
+ headers={
82
+ "X-API-KEY": self.api_key
83
+ }
84
+ )
85
+ response.raise_for_status()
86
+ url = response.json()
87
+ return f"Please authorize the application by clicking the link {url}"
@@ -0,0 +1,16 @@
1
+ from agentr.integrations.base import Integration
2
+ from agentr.store import Store
3
+
4
+ class ApiKeyIntegration(Integration):
5
+ def __init__(self, name: str, store: Store = None, **kwargs):
6
+ super().__init__(name, store, **kwargs)
7
+
8
+ def get_credentials(self):
9
+ credentials = self.store.get(self.name)
10
+ return credentials
11
+
12
+ def set_credentials(self, credentials: dict):
13
+ self.store.set(self.name, credentials)
14
+
15
+ def authorize(self):
16
+ return {"text": "Please configure the environment variable {self.name}_API_KEY"}