From c61317e448ed1b70856b6c1c942998f5d0e4f6b5 Mon Sep 17 00:00:00 2001 From: Aarushi <50577581+aarushik93@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:44:04 +0000 Subject: [PATCH 01/14] feat(platform): Create external API (#9272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We want to allow external api calls against our platform We also want to keep it sep from internal platform calls for dev ex, security and scale seperation of concerns ### Changes 🏗️ This PR adds the required external routes It mounts the new routes on the same app Infra PR will seprate routing and domains ### Checklist 📋 #### For code changes: - [ ] I have clearly listed my changes in the PR description - [ ] I have made a test plan - [ ] I have tested my changes according to the test plan: - [ ] ...
Example test plan - [ ] Create from scratch and execute an agent with at least 3 blocks - [ ] Import an agent from file upload, and confirm it executes correctly - [ ] Upload agent to marketplace - [ ] Import an agent from marketplace and confirm it executes correctly - [ ] Edit an agent from monitor, and confirm it executes correctly
#### For configuration changes: - [ ] `.env.example` is updated or already compatible with my changes - [ ] `docker-compose.yml` is updated or already compatible with my changes - [ ] I have included a list of my configuration changes in the PR description (under **Changes**)
Examples of configuration changes - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases
--- .../backend/backend/data/execution.py | 27 ++++- .../backend/backend/executor/manager.py | 4 +- .../backend/backend/server/external/api.py | 11 ++ .../backend/server/external/middleware.py | 37 ++++++ .../server/external/routes/__init__.py | 0 .../backend/server/external/routes/v1.py | 111 ++++++++++++++++++ .../backend/backend/server/rest_api.py | 3 + .../backend/backend/server/routers/v1.py | 3 - .../backend/test/executor/test_manager.py | 2 +- 9 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 autogpt_platform/backend/backend/server/external/api.py create mode 100644 autogpt_platform/backend/backend/server/external/middleware.py create mode 100644 autogpt_platform/backend/backend/server/external/routes/__init__.py create mode 100644 autogpt_platform/backend/backend/server/external/routes/v1.py diff --git a/autogpt_platform/backend/backend/data/execution.py b/autogpt_platform/backend/backend/data/execution.py index 8df102fca2..714ea0f51c 100644 --- a/autogpt_platform/backend/backend/data/execution.py +++ b/autogpt_platform/backend/backend/data/execution.py @@ -1,9 +1,10 @@ from collections import defaultdict from datetime import datetime, timezone from multiprocessing import Manager -from typing import Any, AsyncGenerator, Generator, Generic, TypeVar +from typing import Any, AsyncGenerator, Generator, Generic, Optional, TypeVar from prisma.enums import AgentExecutionStatus +from prisma.errors import PrismaError from prisma.models import ( AgentGraphExecution, AgentNodeExecution, @@ -325,6 +326,30 @@ async def update_execution_status( return ExecutionResult.from_db(res) +async def get_execution( + execution_id: str, user_id: str +) -> Optional[AgentNodeExecution]: + """ + Get an execution by ID. Returns None if not found. + + Args: + execution_id: The ID of the execution to retrieve + + Returns: + The execution if found, None otherwise + """ + try: + execution = await AgentNodeExecution.prisma().find_unique( + where={ + "id": execution_id, + "userId": user_id, + } + ) + return execution + except PrismaError: + return None + + async def get_execution_results(graph_exec_id: str) -> list[ExecutionResult]: executions = await AgentNodeExecution.prisma().find_many( where={"agentGraphExecutionId": graph_exec_id}, diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index 50469b50b7..9ea8c9f76d 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -812,8 +812,8 @@ class ExecutionManager(AppService): # Extract request input data, and assign it to the input pin. if block.block_type == BlockType.INPUT: name = node.input_default.get("name") - if name and name in data: - input_data = {"value": data[name]} + if name in data.get("node_input", {}): + input_data = {"value": data["node_input"][name]} # Extract webhook payload, and assign it to the input pin webhook_payload_key = f"webhook_{node.webhook_id}_payload" diff --git a/autogpt_platform/backend/backend/server/external/api.py b/autogpt_platform/backend/backend/server/external/api.py new file mode 100644 index 0000000000..3236766fdd --- /dev/null +++ b/autogpt_platform/backend/backend/server/external/api.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + +from .routes.v1 import v1_router + +external_app = FastAPI( + title="AutoGPT External API", + description="External API for AutoGPT integrations", + docs_url="/docs", + version="1.0", +) +external_app.include_router(v1_router, prefix="/v1") diff --git a/autogpt_platform/backend/backend/server/external/middleware.py b/autogpt_platform/backend/backend/server/external/middleware.py new file mode 100644 index 0000000000..2878e3d310 --- /dev/null +++ b/autogpt_platform/backend/backend/server/external/middleware.py @@ -0,0 +1,37 @@ +from fastapi import Depends, HTTPException, Request +from fastapi.security import APIKeyHeader +from prisma.enums import APIKeyPermission + +from backend.data.api_key import has_permission, validate_api_key + +api_key_header = APIKeyHeader(name="X-API-Key") + + +async def require_api_key(request: Request): + """Base middleware for API key authentication""" + api_key = await api_key_header(request) + + if api_key is None: + raise HTTPException(status_code=401, detail="Missing API key") + + api_key_obj = await validate_api_key(api_key) + + if not api_key_obj: + raise HTTPException(status_code=401, detail="Invalid API key") + + request.state.api_key = api_key_obj + return api_key_obj + + +def require_permission(permission: APIKeyPermission): + """Dependency function for checking specific permissions""" + + async def check_permission(api_key=Depends(require_api_key)): + if not has_permission(api_key, permission): + raise HTTPException( + status_code=403, + detail=f"API key missing required permission: {permission}", + ) + return api_key + + return check_permission diff --git a/autogpt_platform/backend/backend/server/external/routes/__init__.py b/autogpt_platform/backend/backend/server/external/routes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/autogpt_platform/backend/backend/server/external/routes/v1.py b/autogpt_platform/backend/backend/server/external/routes/v1.py new file mode 100644 index 0000000000..ecc64a6037 --- /dev/null +++ b/autogpt_platform/backend/backend/server/external/routes/v1.py @@ -0,0 +1,111 @@ +import logging +from collections import defaultdict +from typing import Any, Sequence + +from autogpt_libs.utils.cache import thread_cached +from fastapi import APIRouter, Depends, HTTPException +from prisma.enums import APIKeyPermission + +import backend.data.block +from backend.data import execution as execution_db +from backend.data import graph as graph_db +from backend.data.api_key import APIKey +from backend.data.block import BlockInput, CompletedBlockOutput +from backend.executor import ExecutionManager +from backend.server.external.middleware import require_permission +from backend.util.service import get_service_client +from backend.util.settings import Settings + + +@thread_cached +def execution_manager_client() -> ExecutionManager: + return get_service_client(ExecutionManager) + + +settings = Settings() +logger = logging.getLogger(__name__) + +v1_router = APIRouter() + + +@v1_router.get( + path="/blocks", + tags=["blocks"], + dependencies=[Depends(require_permission(APIKeyPermission.READ_BLOCK))], +) +def get_graph_blocks() -> Sequence[dict[Any, Any]]: + blocks = [block() for block in backend.data.block.get_blocks().values()] + return [b.to_dict() for b in blocks] + + +@v1_router.post( + path="/blocks/{block_id}/execute", + tags=["blocks"], + dependencies=[Depends(require_permission(APIKeyPermission.EXECUTE_BLOCK))], +) +def execute_graph_block( + block_id: str, + data: BlockInput, + api_key: APIKey = Depends(require_permission(APIKeyPermission.EXECUTE_BLOCK)), +) -> CompletedBlockOutput: + obj = backend.data.block.get_block(block_id) + if not obj: + raise HTTPException(status_code=404, detail=f"Block #{block_id} not found.") + + output = defaultdict(list) + for name, data in obj.execute(data): + output[name].append(data) + return output + + +@v1_router.post( + path="/graphs/{graph_id}/execute", + tags=["graphs"], +) +def execute_graph( + graph_id: str, + node_input: dict[Any, Any], + api_key: APIKey = Depends(require_permission(APIKeyPermission.EXECUTE_GRAPH)), +) -> dict[str, Any]: + try: + graph_exec = execution_manager_client().add_execution( + graph_id, node_input, user_id=api_key.user_id + ) + return {"id": graph_exec.graph_exec_id} + except Exception as e: + msg = e.__str__().encode().decode("unicode_escape") + raise HTTPException(status_code=400, detail=msg) + + +@v1_router.get( + path="/graphs/{graph_id}/executions/{graph_exec_id}/results", + tags=["graphs"], +) +async def get_graph_execution_results( + graph_id: str, + graph_exec_id: str, + api_key: APIKey = Depends(require_permission(APIKeyPermission.READ_GRAPH)), +) -> dict: + graph = await graph_db.get_graph(graph_id, user_id=api_key.user_id) + if not graph: + raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.") + + results = await execution_db.get_execution_results(graph_exec_id) + + return { + "execution_id": graph_exec_id, + "nodes": [ + { + "node_id": result.node_id, + "input": ( + result.input_data.get("value") + if "value" in result.input_data + else result.input_data + ), + "output": result.output_data.get( + "response", result.output_data.get("result", []) + ), + } + for result in results + ], + } diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index c5be1c1792..b5124e0c0a 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -20,6 +20,7 @@ import backend.server.v2.library.routes import backend.server.v2.store.routes import backend.util.service import backend.util.settings +from backend.server.external.api import external_app settings = backend.util.settings.Settings() logger = logging.getLogger(__name__) @@ -94,6 +95,8 @@ app.include_router( backend.server.v2.library.routes.router, tags=["v2"], prefix="/api/library" ) +app.mount("/external-api", external_app) + @app.get(path="/health", tags=["health"], dependencies=[]) async def health(): diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py index f03439a5f8..2b1b76d651 100644 --- a/autogpt_platform/backend/backend/server/routers/v1.py +++ b/autogpt_platform/backend/backend/server/routers/v1.py @@ -613,7 +613,6 @@ def get_execution_schedules( tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) -@feature_flag("api-keys-enabled") async def create_api_key( request: CreateAPIKeyRequest, user_id: Annotated[str, Depends(get_user_id)] ) -> CreateAPIKeyResponse: @@ -637,7 +636,6 @@ async def create_api_key( tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) -@feature_flag("api-keys-enabled") async def get_api_keys( user_id: Annotated[str, Depends(get_user_id)] ) -> list[APIKeyWithoutHash]: @@ -655,7 +653,6 @@ async def get_api_keys( tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) -@feature_flag("api-keys-enabled") async def get_api_key( key_id: str, user_id: Annotated[str, Depends(get_user_id)] ) -> APIKeyWithoutHash: diff --git a/autogpt_platform/backend/test/executor/test_manager.py b/autogpt_platform/backend/test/executor/test_manager.py index 9bcd04a08a..1f69defc84 100644 --- a/autogpt_platform/backend/test/executor/test_manager.py +++ b/autogpt_platform/backend/test/executor/test_manager.py @@ -125,7 +125,7 @@ async def test_agent_execution(server: SpinTestServer): logger.info("Starting test_agent_execution") test_user = await create_test_user() test_graph = await create_graph(server, create_test_graph(), test_user) - data = {"input_1": "Hello", "input_2": "World"} + data = {"node_input": {"input_1": "Hello", "input_2": "World"}} graph_exec_id = await execute_graph( server.agent_server, test_graph, From 0d2bb4678681ac36edd70fe73f8206866fbfa855 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Fri, 17 Jan 2025 14:29:43 +0100 Subject: [PATCH 02/14] fix(frontend): Unbreak save button after save error (#9290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolves #9253 ### Changes 🏗️ - Update state when an error occurs on save, to re-enable the save button ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - Try to save an agent with missing required fields -> should give an error - Fill out the required fields and try saving again -> should work --- autogpt_platform/frontend/src/hooks/useAgentGraph.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/autogpt_platform/frontend/src/hooks/useAgentGraph.ts b/autogpt_platform/frontend/src/hooks/useAgentGraph.ts index a21280f131..c2910f2c51 100644 --- a/autogpt_platform/frontend/src/hooks/useAgentGraph.ts +++ b/autogpt_platform/frontend/src/hooks/useAgentGraph.ts @@ -862,6 +862,7 @@ export default function useAgentGraph( title: "Error saving agent", description: errorMessage, }); + setSaveRunRequest({ request: "save", state: "error" }); } }, [_saveAgent, toast]); From 56612f16cfb28fc2426b84831ba992b19c808476 Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Fri, 17 Jan 2025 07:35:58 -0600 Subject: [PATCH 03/14] feat(platform): Linear integration (#9269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I want to be able to do stuff with linear automatically ### Changes 🏗️ - Adds all the backing details to add linear auth and API access with oauth (and prep for API key) ### Checklist 📋 #### For code changes: - [ ] I have clearly listed my changes in the PR description - [ ] I have made a test plan - [ ] I have tested my changes according to the test plan: - [ ] ...
Example test plan - [ ] Create from scratch and execute an agent with at least 3 blocks - [ ] Import an agent from file upload, and confirm it executes correctly - [ ] Upload agent to marketplace - [ ] Import an agent from marketplace and confirm it executes correctly - [ ] Edit an agent from monitor, and confirm it executes correctly
#### For configuration changes: - [ ] `.env.example` is updated or already compatible with my changes - [ ] `docker-compose.yml` is updated or already compatible with my changes - [ ] I have included a list of my configuration changes in the PR description (under **Changes**)
Examples of configuration changes - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases
--------- Co-authored-by: Aarushi <50577581+aarushik93@users.noreply.github.com> --- autogpt_platform/backend/.env.example | 6 + .../backend/backend/blocks/basic.py | 46 +++ .../backend/backend/blocks/linear/_api.py | 272 ++++++++++++++++++ .../backend/backend/blocks/linear/_auth.py | 101 +++++++ .../backend/backend/blocks/linear/comment.py | 81 ++++++ .../backend/backend/blocks/linear/issues.py | 186 ++++++++++++ .../backend/backend/blocks/linear/models.py | 41 +++ .../backend/backend/blocks/linear/projects.py | 93 ++++++ .../backend/backend/blocks/linear/triggers.py | 0 .../backend/backend/data/block.py | 2 + .../backend/integrations/oauth/__init__.py | 2 + .../backend/integrations/oauth/linear.py | 165 +++++++++++ .../backend/backend/integrations/providers.py | 1 + .../backend/server/integrations/router.py | 5 + .../backend/backend/util/settings.py | 3 + .../integrations/credentials-input.tsx | 4 +- .../integrations/credentials-provider.tsx | 1 + .../src/lib/autogpt-server-api/types.ts | 1 + 18 files changed, 1009 insertions(+), 1 deletion(-) create mode 100644 autogpt_platform/backend/backend/blocks/linear/_api.py create mode 100644 autogpt_platform/backend/backend/blocks/linear/_auth.py create mode 100644 autogpt_platform/backend/backend/blocks/linear/comment.py create mode 100644 autogpt_platform/backend/backend/blocks/linear/issues.py create mode 100644 autogpt_platform/backend/backend/blocks/linear/models.py create mode 100644 autogpt_platform/backend/backend/blocks/linear/projects.py create mode 100644 autogpt_platform/backend/backend/blocks/linear/triggers.py create mode 100644 autogpt_platform/backend/backend/integrations/oauth/linear.py diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.example index ab7b914a73..838cfdb283 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.example @@ -75,6 +75,12 @@ GOOGLE_CLIENT_SECRET= TWITTER_CLIENT_ID= TWITTER_CLIENT_SECRET= +# Linear App +# Make a new workspace for your OAuth APP -- trust me +# https://linear.app/settings/api/applications/new +# Callback URL: http://localhost:3000/auth/integrations/oauth_callback +LINEAR_CLIENT_ID= +LINEAR_CLIENT_SECRET= ## ===== OPTIONAL API KEYS ===== ## diff --git a/autogpt_platform/backend/backend/blocks/basic.py b/autogpt_platform/backend/backend/blocks/basic.py index b68c04bad1..d7dd468c40 100644 --- a/autogpt_platform/backend/backend/blocks/basic.py +++ b/autogpt_platform/backend/backend/blocks/basic.py @@ -1,9 +1,11 @@ +import enum from typing import Any, List from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema, BlockType from backend.data.model import SchemaField from backend.util.mock import MockObject from backend.util.text import TextFormatter +from backend.util.type import convert formatter = TextFormatter() @@ -590,3 +592,47 @@ class CreateListBlock(Block): yield "list", input_data.values except Exception as e: yield "error", f"Failed to create list: {str(e)}" + + +class TypeOptions(enum.Enum): + STRING = "string" + NUMBER = "number" + BOOLEAN = "boolean" + LIST = "list" + DICTIONARY = "dictionary" + + +class UniversalTypeConverterBlock(Block): + class Input(BlockSchema): + value: Any = SchemaField( + description="The value to convert to a universal type." + ) + type: TypeOptions = SchemaField(description="The type to convert the value to.") + + class Output(BlockSchema): + value: Any = SchemaField(description="The converted value.") + + def __init__(self): + super().__init__( + id="95d1b990-ce13-4d88-9737-ba5c2070c97b", + description="This block is used to convert a value to a universal type.", + categories={BlockCategory.BASIC}, + input_schema=UniversalTypeConverterBlock.Input, + output_schema=UniversalTypeConverterBlock.Output, + ) + + def run(self, input_data: Input, **kwargs) -> BlockOutput: + try: + converted_value = convert( + input_data.value, + { + TypeOptions.STRING: str, + TypeOptions.NUMBER: float, + TypeOptions.BOOLEAN: bool, + TypeOptions.LIST: list, + TypeOptions.DICTIONARY: dict, + }[input_data.type], + ) + yield "value", converted_value + except Exception as e: + yield "error", f"Failed to convert value: {str(e)}" diff --git a/autogpt_platform/backend/backend/blocks/linear/_api.py b/autogpt_platform/backend/backend/blocks/linear/_api.py new file mode 100644 index 0000000000..4639975e8e --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/linear/_api.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +import json +from typing import Any, Dict, Optional + +from backend.blocks.linear._auth import LinearCredentials +from backend.blocks.linear.models import ( + CreateCommentResponse, + CreateIssueResponse, + Issue, + Project, +) +from backend.util.request import Requests + + +class LinearAPIException(Exception): + def __init__(self, message: str, status_code: int): + super().__init__(message) + self.status_code = status_code + + +class LinearClient: + """Client for the Linear API + + If you're looking for the schema: https://studio.apollographql.com/public/Linear-API/variant/current/schema + """ + + API_URL = "https://api.linear.app/graphql" + + def __init__( + self, + credentials: LinearCredentials | None = None, + custom_requests: Optional[Requests] = None, + ): + if custom_requests: + self._requests = custom_requests + else: + + headers: Dict[str, str] = { + "Content-Type": "application/json", + } + if credentials: + headers["Authorization"] = credentials.bearer() + + self._requests = Requests( + extra_headers=headers, + trusted_origins=["https://api.linear.app"], + raise_for_status=False, + ) + + def _execute_graphql_request( + self, query: str, variables: dict | None = None + ) -> Any: + """ + Executes a GraphQL request against the Linear API and returns the response data. + + Args: + query: The GraphQL query string. + variables (optional): Any GraphQL query variables + + Returns: + The parsed JSON response data, or raises a LinearAPIException on error. + """ + payload: Dict[str, Any] = {"query": query} + if variables: + payload["variables"] = variables + + response = self._requests.post(self.API_URL, json=payload) + + if not response.ok: + + try: + error_data = response.json() + error_message = error_data.get("errors", [{}])[0].get("message", "") + except json.JSONDecodeError: + error_message = response.text + + raise LinearAPIException( + f"Linear API request failed ({response.status_code}): {error_message}", + response.status_code, + ) + + response_data = response.json() + if "errors" in response_data: + + error_messages = [ + error.get("message", "") for error in response_data["errors"] + ] + raise LinearAPIException( + f"Linear API returned errors: {', '.join(error_messages)}", + response.status_code, + ) + + return response_data["data"] + + def query(self, query: str, variables: Optional[dict] = None) -> dict: + """Executes a GraphQL query. + + Args: + query: The GraphQL query string. + variables: Query variables, if any. + + Returns: + The response data. + """ + return self._execute_graphql_request(query, variables) + + def mutate(self, mutation: str, variables: Optional[dict] = None) -> dict: + """Executes a GraphQL mutation. + + Args: + mutation: The GraphQL mutation string. + variables: Query variables, if any. + + Returns: + The response data. + """ + return self._execute_graphql_request(mutation, variables) + + def try_create_comment(self, issue_id: str, comment: str) -> CreateCommentResponse: + try: + mutation = """ + mutation CommentCreate($input: CommentCreateInput!) { + commentCreate(input: $input) { + success + comment { + id + body + } + } + } + """ + + variables = { + "input": { + "body": comment, + "issueId": issue_id, + } + } + + added_comment = self.mutate(mutation, variables) + # Select the commentCreate field from the mutation response + return CreateCommentResponse(**added_comment["commentCreate"]) + except LinearAPIException as e: + raise e + + def try_get_team_by_name(self, team_name: str) -> str: + try: + query = """ + query GetTeamId($searchTerm: String!) { + teams(filter: { + or: [ + { name: { eqIgnoreCase: $searchTerm } }, + { key: { eqIgnoreCase: $searchTerm } } + ] + }) { + nodes { + id + name + key + } + } + } + """ + + variables: dict[str, Any] = { + "searchTerm": team_name, + } + + team_id = self.query(query, variables) + return team_id["teams"]["nodes"][0]["id"] + except LinearAPIException as e: + raise e + + def try_create_issue( + self, + team_id: str, + title: str, + description: str | None = None, + priority: int | None = None, + project_id: str | None = None, + ) -> CreateIssueResponse: + try: + mutation = """ + mutation IssueCreate($input: IssueCreateInput!) { + issueCreate(input: $input) { + issue { + title + description + id + identifier + priority + } + } + } + """ + + variables: dict[str, Any] = { + "input": { + "teamId": team_id, + "title": title, + } + } + + if project_id: + variables["input"]["projectId"] = project_id + + if description: + variables["input"]["description"] = description + + if priority: + variables["input"]["priority"] = priority + + added_issue = self.mutate(mutation, variables) + return CreateIssueResponse(**added_issue["issueCreate"]) + except LinearAPIException as e: + raise e + + def try_search_projects(self, term: str) -> list[Project]: + try: + query = """ + query SearchProjects($term: String!, $includeComments: Boolean!) { + searchProjects(term: $term, includeComments: $includeComments) { + nodes { + id + name + description + priority + progress + content + } + } + } + """ + + variables: dict[str, Any] = { + "term": term, + "includeComments": True, + } + + projects = self.query(query, variables) + return [ + Project(**project) for project in projects["searchProjects"]["nodes"] + ] + except LinearAPIException as e: + raise e + + def try_search_issues(self, term: str) -> list[Issue]: + try: + query = """ + query SearchIssues($term: String!, $includeComments: Boolean!) { + searchIssues(term: $term, includeComments: $includeComments) { + nodes { + id + identifier + title + description + priority + } + } + } + """ + + variables: dict[str, Any] = { + "term": term, + "includeComments": True, + } + + issues = self.query(query, variables) + return [Issue(**issue) for issue in issues["searchIssues"]["nodes"]] + except LinearAPIException as e: + raise e diff --git a/autogpt_platform/backend/backend/blocks/linear/_auth.py b/autogpt_platform/backend/backend/blocks/linear/_auth.py new file mode 100644 index 0000000000..fb91fbfe7a --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/linear/_auth.py @@ -0,0 +1,101 @@ +from enum import Enum +from typing import Literal + +from pydantic import SecretStr + +from backend.data.model import ( + APIKeyCredentials, + CredentialsField, + CredentialsMetaInput, + OAuth2Credentials, +) +from backend.integrations.providers import ProviderName +from backend.util.settings import Secrets + +secrets = Secrets() +LINEAR_OAUTH_IS_CONFIGURED = bool( + secrets.linear_client_id and secrets.linear_client_secret +) + +LinearCredentials = OAuth2Credentials | APIKeyCredentials +# LinearCredentialsInput = CredentialsMetaInput[ +# Literal[ProviderName.LINEAR], +# Literal["oauth2", "api_key"] if LINEAR_OAUTH_IS_CONFIGURED else Literal["oauth2"], +# ] +LinearCredentialsInput = CredentialsMetaInput[ + Literal[ProviderName.LINEAR], Literal["oauth2"] +] + + +# (required) Comma separated list of scopes: + +# read - (Default) Read access for the user's account. This scope will always be present. + +# write - Write access for the user's account. If your application only needs to create comments, use a more targeted scope + +# issues:create - Allows creating new issues and their attachments + +# comments:create - Allows creating new issue comments + +# timeSchedule:write - Allows creating and modifying time schedules + + +# admin - Full access to admin level endpoints. You should never ask for this permission unless it's absolutely needed +class LinearScope(str, Enum): + READ = "read" + WRITE = "write" + ISSUES_CREATE = "issues:create" + COMMENTS_CREATE = "comments:create" + TIME_SCHEDULE_WRITE = "timeSchedule:write" + ADMIN = "admin" + + +def LinearCredentialsField(scopes: list[LinearScope]) -> LinearCredentialsInput: + """ + Creates a Linear credentials input on a block. + + Params: + scope: The authorization scope needed for the block to work. ([list of available scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes)) + """ # noqa + return CredentialsField( + required_scopes=set([LinearScope.READ.value]).union( + set([scope.value for scope in scopes]) + ), + description="The Linear integration can be used with OAuth, " + "or any API key with sufficient permissions for the blocks it is used on.", + ) + + +TEST_CREDENTIALS_OAUTH = OAuth2Credentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="linear", + title="Mock Linear API key", + username="mock-linear-username", + access_token=SecretStr("mock-linear-access-token"), + access_token_expires_at=None, + refresh_token=SecretStr("mock-linear-refresh-token"), + refresh_token_expires_at=None, + scopes=["mock-linear-scopes"], +) + +TEST_CREDENTIALS_API_KEY = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="linear", + title="Mock Linear API key", + api_key=SecretStr("mock-linear-api-key"), + expires_at=None, +) + +TEST_CREDENTIALS_INPUT_OAUTH = { + "provider": TEST_CREDENTIALS_OAUTH.provider, + "id": TEST_CREDENTIALS_OAUTH.id, + "type": TEST_CREDENTIALS_OAUTH.type, + "title": TEST_CREDENTIALS_OAUTH.type, +} + +TEST_CREDENTIALS_INPUT_API_KEY = { + "provider": TEST_CREDENTIALS_API_KEY.provider, + "id": TEST_CREDENTIALS_API_KEY.id, + "type": TEST_CREDENTIALS_API_KEY.type, + "title": TEST_CREDENTIALS_API_KEY.type, +} diff --git a/autogpt_platform/backend/backend/blocks/linear/comment.py b/autogpt_platform/backend/backend/blocks/linear/comment.py new file mode 100644 index 0000000000..ea2ff1e613 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/linear/comment.py @@ -0,0 +1,81 @@ +from backend.blocks.linear._api import LinearAPIException, LinearClient +from backend.blocks.linear._auth import ( + TEST_CREDENTIALS_INPUT_OAUTH, + TEST_CREDENTIALS_OAUTH, + LinearCredentials, + LinearCredentialsField, + LinearCredentialsInput, + LinearScope, +) +from backend.blocks.linear.models import CreateCommentResponse +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + + +class LinearCreateCommentBlock(Block): + """Block for creating comments on Linear issues""" + + class Input(BlockSchema): + credentials: LinearCredentialsInput = LinearCredentialsField( + scopes=[LinearScope.COMMENTS_CREATE], + ) + issue_id: str = SchemaField(description="ID of the issue to comment on") + comment: str = SchemaField(description="Comment text to add to the issue") + + class Output(BlockSchema): + comment_id: str = SchemaField(description="ID of the created comment") + comment_body: str = SchemaField( + description="Text content of the created comment" + ) + error: str = SchemaField(description="Error message if comment creation failed") + + def __init__(self): + super().__init__( + id="8f7d3a2e-9b5c-4c6a-8f1d-7c8b3e4a5d6c", + description="Creates a new comment on a Linear issue", + input_schema=self.Input, + output_schema=self.Output, + categories={BlockCategory.PRODUCTIVITY, BlockCategory.ISSUE_TRACKING}, + test_input={ + "issue_id": "TEST-123", + "comment": "Test comment", + "credentials": TEST_CREDENTIALS_INPUT_OAUTH, + }, + test_credentials=TEST_CREDENTIALS_OAUTH, + test_output=[("comment_id", "abc123"), ("comment_body", "Test comment")], + test_mock={ + "create_comment": lambda *args, **kwargs: ( + "abc123", + "Test comment", + ) + }, + ) + + @staticmethod + def create_comment( + credentials: LinearCredentials, issue_id: str, comment: str + ) -> tuple[str, str]: + client = LinearClient(credentials=credentials) + response: CreateCommentResponse = client.try_create_comment( + issue_id=issue_id, comment=comment + ) + return response.comment.id, response.comment.body + + def run( + self, input_data: Input, *, credentials: LinearCredentials, **kwargs + ) -> BlockOutput: + """Execute the comment creation""" + try: + comment_id, comment_body = self.create_comment( + credentials=credentials, + issue_id=input_data.issue_id, + comment=input_data.comment, + ) + + yield "comment_id", comment_id + yield "comment_body", comment_body + + except LinearAPIException as e: + yield "error", str(e) + except Exception as e: + yield "error", f"Unexpected error: {str(e)}" diff --git a/autogpt_platform/backend/backend/blocks/linear/issues.py b/autogpt_platform/backend/backend/blocks/linear/issues.py new file mode 100644 index 0000000000..4c2c3ffa09 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/linear/issues.py @@ -0,0 +1,186 @@ +from backend.blocks.linear._api import LinearAPIException, LinearClient +from backend.blocks.linear._auth import ( + TEST_CREDENTIALS_INPUT_OAUTH, + TEST_CREDENTIALS_OAUTH, + LinearCredentials, + LinearCredentialsField, + LinearCredentialsInput, + LinearScope, +) +from backend.blocks.linear.models import CreateIssueResponse, Issue +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + + +class LinearCreateIssueBlock(Block): + """Block for creating issues on Linear""" + + class Input(BlockSchema): + credentials: LinearCredentialsInput = LinearCredentialsField( + scopes=[LinearScope.ISSUES_CREATE], + ) + title: str = SchemaField(description="Title of the issue") + description: str | None = SchemaField(description="Description of the issue") + team_name: str = SchemaField( + description="Name of the team to create the issue on" + ) + priority: int | None = SchemaField( + description="Priority of the issue", + default=None, + minimum=0, + maximum=4, + ) + project_name: str | None = SchemaField( + description="Name of the project to create the issue on", + default=None, + ) + + class Output(BlockSchema): + issue_id: str = SchemaField(description="ID of the created issue") + issue_title: str = SchemaField(description="Title of the created issue") + error: str = SchemaField(description="Error message if issue creation failed") + + def __init__(self): + super().__init__( + id="f9c68f55-dcca-40a8-8771-abf9601680aa", + description="Creates a new issue on Linear", + input_schema=self.Input, + output_schema=self.Output, + categories={BlockCategory.PRODUCTIVITY, BlockCategory.ISSUE_TRACKING}, + test_input={ + "title": "Test issue", + "description": "Test description", + "team_name": "Test team", + "project_name": "Test project", + "credentials": TEST_CREDENTIALS_INPUT_OAUTH, + }, + test_credentials=TEST_CREDENTIALS_OAUTH, + test_output=[("issue_id", "abc123"), ("issue_title", "Test issue")], + test_mock={ + "create_issue": lambda *args, **kwargs: ( + "abc123", + "Test issue", + ) + }, + ) + + @staticmethod + def create_issue( + credentials: LinearCredentials, + team_name: str, + title: str, + description: str | None = None, + priority: int | None = None, + project_name: str | None = None, + ) -> tuple[str, str]: + client = LinearClient(credentials=credentials) + team_id = client.try_get_team_by_name(team_name=team_name) + project_id: str | None = None + if project_name: + projects = client.try_search_projects(term=project_name) + if projects: + project_id = projects[0].id + else: + raise LinearAPIException("Project not found", status_code=404) + response: CreateIssueResponse = client.try_create_issue( + team_id=team_id, + title=title, + description=description, + priority=priority, + project_id=project_id, + ) + return response.issue.identifier, response.issue.title + + def run( + self, input_data: Input, *, credentials: LinearCredentials, **kwargs + ) -> BlockOutput: + """Execute the issue creation""" + try: + issue_id, issue_title = self.create_issue( + credentials=credentials, + team_name=input_data.team_name, + title=input_data.title, + description=input_data.description, + priority=input_data.priority, + project_name=input_data.project_name, + ) + + yield "issue_id", issue_id + yield "issue_title", issue_title + + except LinearAPIException as e: + yield "error", str(e) + except Exception as e: + yield "error", f"Unexpected error: {str(e)}" + + +class LinearSearchIssuesBlock(Block): + """Block for searching issues on Linear""" + + class Input(BlockSchema): + term: str = SchemaField(description="Term to search for issues") + credentials: LinearCredentialsInput = LinearCredentialsField( + scopes=[LinearScope.READ], + ) + + class Output(BlockSchema): + issues: list[Issue] = SchemaField(description="List of issues") + + def __init__(self): + super().__init__( + id="b5a2a0e6-26b4-4c5b-8a42-bc79e9cb65c2", + description="Searches for issues on Linear", + input_schema=self.Input, + output_schema=self.Output, + test_input={ + "term": "Test issue", + "credentials": TEST_CREDENTIALS_INPUT_OAUTH, + }, + test_credentials=TEST_CREDENTIALS_OAUTH, + test_output=[ + ( + "issues", + [ + Issue( + id="abc123", + identifier="abc123", + title="Test issue", + description="Test description", + priority=1, + ) + ], + ) + ], + test_mock={ + "search_issues": lambda *args, **kwargs: [ + Issue( + id="abc123", + identifier="abc123", + title="Test issue", + description="Test description", + priority=1, + ) + ] + }, + ) + + @staticmethod + def search_issues( + credentials: LinearCredentials, + term: str, + ) -> list[Issue]: + client = LinearClient(credentials=credentials) + response: list[Issue] = client.try_search_issues(term=term) + return response + + def run( + self, input_data: Input, *, credentials: LinearCredentials, **kwargs + ) -> BlockOutput: + """Execute the issue search""" + try: + issues = self.search_issues(credentials=credentials, term=input_data.term) + yield "issues", issues + except LinearAPIException as e: + yield "error", str(e) + except Exception as e: + yield "error", f"Unexpected error: {str(e)}" diff --git a/autogpt_platform/backend/backend/blocks/linear/models.py b/autogpt_platform/backend/backend/blocks/linear/models.py new file mode 100644 index 0000000000..a6a2de3cd8 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/linear/models.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel + + +class Comment(BaseModel): + id: str + body: str + + +class CreateCommentInput(BaseModel): + body: str + issueId: str + + +class CreateCommentResponse(BaseModel): + success: bool + comment: Comment + + +class CreateCommentResponseWrapper(BaseModel): + commentCreate: CreateCommentResponse + + +class Issue(BaseModel): + id: str + identifier: str + title: str + description: str | None + priority: int + + +class CreateIssueResponse(BaseModel): + issue: Issue + + +class Project(BaseModel): + id: str + name: str + description: str + priority: int + progress: int + content: str diff --git a/autogpt_platform/backend/backend/blocks/linear/projects.py b/autogpt_platform/backend/backend/blocks/linear/projects.py new file mode 100644 index 0000000000..9d33b3f77b --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/linear/projects.py @@ -0,0 +1,93 @@ +from backend.blocks.linear._api import LinearAPIException, LinearClient +from backend.blocks.linear._auth import ( + TEST_CREDENTIALS_INPUT_OAUTH, + TEST_CREDENTIALS_OAUTH, + LinearCredentials, + LinearCredentialsField, + LinearCredentialsInput, + LinearScope, +) +from backend.blocks.linear.models import Project +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + + +class LinearSearchProjectsBlock(Block): + """Block for searching projects on Linear""" + + class Input(BlockSchema): + credentials: LinearCredentialsInput = LinearCredentialsField( + scopes=[LinearScope.READ], + ) + term: str = SchemaField(description="Term to search for projects") + + class Output(BlockSchema): + projects: list[Project] = SchemaField(description="List of projects") + error: str = SchemaField(description="Error message if issue creation failed") + + def __init__(self): + super().__init__( + id="446a1d35-9d8f-4ac5-83ea-7684ec50e6af", + description="Searches for projects on Linear", + input_schema=self.Input, + output_schema=self.Output, + categories={BlockCategory.PRODUCTIVITY, BlockCategory.ISSUE_TRACKING}, + test_input={ + "term": "Test project", + "credentials": TEST_CREDENTIALS_INPUT_OAUTH, + }, + test_credentials=TEST_CREDENTIALS_OAUTH, + test_output=[ + ( + "projects", + [ + Project( + id="abc123", + name="Test project", + description="Test description", + priority=1, + progress=1, + content="Test content", + ) + ], + ) + ], + test_mock={ + "search_projects": lambda *args, **kwargs: [ + Project( + id="abc123", + name="Test project", + description="Test description", + priority=1, + progress=1, + content="Test content", + ) + ] + }, + ) + + @staticmethod + def search_projects( + credentials: LinearCredentials, + term: str, + ) -> list[Project]: + client = LinearClient(credentials=credentials) + response: list[Project] = client.try_search_projects(term=term) + return response + + def run( + self, input_data: Input, *, credentials: LinearCredentials, **kwargs + ) -> BlockOutput: + """Execute the project search""" + try: + projects = self.search_projects( + credentials=credentials, + term=input_data.term, + ) + + yield "projects", projects + + except LinearAPIException as e: + yield "error", str(e) + except Exception as e: + yield "error", f"Unexpected error: {str(e)}" diff --git a/autogpt_platform/backend/backend/blocks/linear/triggers.py b/autogpt_platform/backend/backend/blocks/linear/triggers.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/autogpt_platform/backend/backend/data/block.py b/autogpt_platform/backend/backend/data/block.py index add06dbb1f..effdb2c5da 100644 --- a/autogpt_platform/backend/backend/data/block.py +++ b/autogpt_platform/backend/backend/data/block.py @@ -64,6 +64,8 @@ class BlockCategory(Enum): SAFETY = ( "Block that provides AI safety mechanisms such as detecting harmful content" ) + PRODUCTIVITY = "Block that helps with productivity" + ISSUE_TRACKING = "Block that helps with issue tracking" def dict(self) -> dict[str, str]: return {"category": self.name, "description": self.value} diff --git a/autogpt_platform/backend/backend/integrations/oauth/__init__.py b/autogpt_platform/backend/backend/integrations/oauth/__init__.py index ec45189c59..50f8a155a6 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/__init__.py +++ b/autogpt_platform/backend/backend/integrations/oauth/__init__.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from .github import GitHubOAuthHandler from .google import GoogleOAuthHandler +from .linear import LinearOAuthHandler from .notion import NotionOAuthHandler from .twitter import TwitterOAuthHandler @@ -17,6 +18,7 @@ HANDLERS_BY_NAME: dict["ProviderName", type["BaseOAuthHandler"]] = { GoogleOAuthHandler, NotionOAuthHandler, TwitterOAuthHandler, + LinearOAuthHandler, ] } # --8<-- [end:HANDLERS_BY_NAMEExample] diff --git a/autogpt_platform/backend/backend/integrations/oauth/linear.py b/autogpt_platform/backend/backend/integrations/oauth/linear.py new file mode 100644 index 0000000000..fd9d379c1e --- /dev/null +++ b/autogpt_platform/backend/backend/integrations/oauth/linear.py @@ -0,0 +1,165 @@ +import json +from typing import Optional +from urllib.parse import urlencode + +from pydantic import SecretStr + +from backend.blocks.linear._api import LinearAPIException +from backend.data.model import APIKeyCredentials, OAuth2Credentials +from backend.integrations.providers import ProviderName +from backend.util.request import requests + +from .base import BaseOAuthHandler + + +class LinearOAuthHandler(BaseOAuthHandler): + """ + OAuth2 handler for Linear. + """ + + PROVIDER_NAME = ProviderName.LINEAR + + def __init__(self, client_id: str, client_secret: str, redirect_uri: str): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + self.auth_base_url = "https://linear.app/oauth/authorize" + self.token_url = "https://api.linear.app/oauth/token" # Correct token URL + self.revoke_url = "https://api.linear.app/oauth/revoke" + + def get_login_url( + self, scopes: list[str], state: str, code_challenge: Optional[str] + ) -> str: + + params = { + "client_id": self.client_id, + "redirect_uri": self.redirect_uri, + "response_type": "code", # Important: include "response_type" + "scope": ",".join(scopes), # Comma-separated, not space-separated + "state": state, + } + return f"{self.auth_base_url}?{urlencode(params)}" + + def exchange_code_for_tokens( + self, code: str, scopes: list[str], code_verifier: Optional[str] + ) -> OAuth2Credentials: + return self._request_tokens({"code": code, "redirect_uri": self.redirect_uri}) + + def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + if not credentials.access_token: + raise ValueError("No access token to revoke") + + headers = { + "Authorization": f"Bearer {credentials.access_token.get_secret_value()}" + } + + response = requests.post(self.revoke_url, headers=headers) + if not response.ok: + try: + error_data = response.json() + error_message = error_data.get("error", "Unknown error") + except json.JSONDecodeError: + error_message = response.text + raise LinearAPIException( + f"Failed to revoke Linear tokens ({response.status_code}): {error_message}", + response.status_code, + ) + + return True # Linear doesn't return JSON on successful revoke + + def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + if not credentials.refresh_token: + raise ValueError( + "No refresh token available." + ) # Linear uses non-expiring tokens + + return self._request_tokens( + { + "refresh_token": credentials.refresh_token.get_secret_value(), + "grant_type": "refresh_token", + } + ) + + def _request_tokens( + self, + params: dict[str, str], + current_credentials: Optional[OAuth2Credentials] = None, + ) -> OAuth2Credentials: + request_body = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "authorization_code", # Ensure grant_type is correct + **params, + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } # Correct header for token request + response = requests.post(self.token_url, data=request_body, headers=headers) + + if not response.ok: + try: + error_data = response.json() + error_message = error_data.get("error", "Unknown error") + + except json.JSONDecodeError: + error_message = response.text + raise LinearAPIException( + f"Failed to fetch Linear tokens ({response.status_code}): {error_message}", + response.status_code, + ) + + token_data = response.json() + + # Note: Linear access tokens do not expire, so we set expires_at to None + new_credentials = OAuth2Credentials( + provider=self.PROVIDER_NAME, + title=current_credentials.title if current_credentials else None, + username=token_data.get("user", {}).get( + "name", "Unknown User" + ), # extract name or set appropriate + access_token=token_data["access_token"], + scopes=token_data["scope"].split( + "," + ), # Linear returns comma-separated scopes + refresh_token=token_data.get( + "refresh_token" + ), # Linear uses non-expiring tokens so this might be null + access_token_expires_at=None, + refresh_token_expires_at=None, + ) + if current_credentials: + new_credentials.id = current_credentials.id + return new_credentials + + def _request_username(self, access_token: str) -> Optional[str]: + + # Use the LinearClient to fetch user details using GraphQL + from backend.blocks.linear._api import LinearClient + + try: + + linear_client = LinearClient( + APIKeyCredentials( + api_key=SecretStr(access_token), + title="temp", + provider=self.PROVIDER_NAME, + expires_at=None, + ) + ) # Temporary credentials for this request + + query = """ + query Viewer { + viewer { + name + } + } + """ + + response = linear_client.query(query) + return response["viewer"]["name"] + + except Exception as e: # Handle any errors + + print(f"Error fetching username: {e}") + return None diff --git a/autogpt_platform/backend/backend/integrations/providers.py b/autogpt_platform/backend/backend/integrations/providers.py index d08d50e021..95751e92df 100644 --- a/autogpt_platform/backend/backend/integrations/providers.py +++ b/autogpt_platform/backend/backend/integrations/providers.py @@ -17,6 +17,7 @@ class ProviderName(str, Enum): HUBSPOT = "hubspot" IDEOGRAM = "ideogram" JINA = "jina" + LINEAR = "linear" MEDIUM = "medium" NOTION = "notion" NVIDIA = "nvidia" diff --git a/autogpt_platform/backend/backend/server/integrations/router.py b/autogpt_platform/backend/backend/server/integrations/router.py index 6a8c274dd7..b85a551375 100644 --- a/autogpt_platform/backend/backend/server/integrations/router.py +++ b/autogpt_platform/backend/backend/server/integrations/router.py @@ -110,6 +110,11 @@ def callback( logger.debug(f"Received credentials with final scopes: {credentials.scopes}") + # Linear returns scopes as a single string with spaces, so we need to split them + # TODO: make a bypass of this part of the OAuth handler + if len(credentials.scopes) == 1 and " " in credentials.scopes[0]: + credentials.scopes = credentials.scopes[0].split(" ") + # Check if the granted scopes are sufficient for the requested scopes if not set(scopes).issubset(set(credentials.scopes)): # For now, we'll just log the warning and continue diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index e08b5ca0bf..8c33c14590 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -313,6 +313,9 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): e2b_api_key: str = Field(default="", description="E2B API key") nvidia_api_key: str = Field(default="", description="Nvidia API key") + linear_client_id: str = Field(default="", description="Linear client ID") + linear_client_secret: str = Field(default="", description="Linear client secret") + stripe_api_key: str = Field(default="", description="Stripe API Key") stripe_webhook_secret: str = Field(default="", description="Stripe Webhook Secret") diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx index 7a32b4378e..93afa31c6e 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx @@ -14,6 +14,7 @@ import { FaGoogle, FaMedium, FaKey, + FaHubspot, } from "react-icons/fa"; import { FC, useMemo, useState } from "react"; import { @@ -66,6 +67,7 @@ export const providerIcons: Record< google_maps: FaGoogle, jina: fallbackIcon, ideogram: fallbackIcon, + linear: fallbackIcon, medium: FaMedium, ollama: fallbackIcon, openai: fallbackIcon, @@ -79,7 +81,7 @@ export const providerIcons: Record< twitter: FaTwitter, unreal_speech: fallbackIcon, exa: fallbackIcon, - hubspot: fallbackIcon, + hubspot: FaHubspot, }; // --8<-- [end:ProviderIconsEmbed] diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index 38c97443ce..23a840e794 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -26,6 +26,7 @@ const providerDisplayNames: Record = { groq: "Groq", ideogram: "Ideogram", jina: "Jina", + linear: "Linear", medium: "Medium", notion: "Notion", nvidia: "Nvidia", diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts index 0eaf4fcbed..cd839d2cd1 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts @@ -111,6 +111,7 @@ export const PROVIDER_NAMES = { GROQ: "groq", IDEOGRAM: "ideogram", JINA: "jina", + LINEAR: "linear", MEDIUM: "medium", NOTION: "notion", NVIDIA: "nvidia", From 800625c95208d609c34f8c71cbde309e151dac26 Mon Sep 17 00:00:00 2001 From: Krzysztof Czerwinski <34861343+kcze@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:49:41 +0100 Subject: [PATCH 04/14] fix(frontend): Change `/store*` url to `/marketplace*` (#9119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have branded it as "Marketplace", so the URL shouldn't be "store". ### Changes 🏗️ - Change frontend URLs from `/store*` to `/marketplace*` - No API route changes to minimize bugs (follow up: https://github.com/Significant-Gravitas/AutoGPT/issues/9118) --- .../backend/backend/data/credit.py | 4 +- .../backend/backend/server/routers/v1.py | 2 +- autogpt_platform/frontend/src/app/layout.tsx | 8 +- .../(user)/api_keys/page.tsx | 0 .../(user)/credits/page.tsx | 0 .../(user)/dashboard/page.tsx | 0 .../(user)/integrations/page.tsx | 0 .../{store => marketplace}/(user)/layout.tsx | 14 +- .../(user)/profile/page.tsx | 0 .../(user)/settings/page.tsx | 0 .../agent/[creator]/[slug]/page.tsx | 4 +- .../creator/[creator]/page.tsx | 2 +- .../frontend/src/app/marketplace/page.tsx | 180 ++++++++++++++++- .../{store => marketplace}/search/page.tsx | 0 autogpt_platform/frontend/src/app/page.tsx | 2 +- .../frontend/src/app/signup/actions.ts | 2 +- .../frontend/src/app/store/page.tsx | 181 ------------------ .../src/components/agptui/AgentInfo.tsx | 2 +- .../src/components/agptui/NavbarLink.tsx | 2 +- .../src/components/agptui/SearchBar.tsx | 2 +- .../src/components/agptui/Sidebar.tsx | 24 +-- .../agptui/composite/AgentsSection.tsx | 2 +- .../agptui/composite/FeaturedCreators.tsx | 2 +- .../agptui/composite/FeaturedSection.tsx | 2 +- .../agptui/composite/HeroSection.tsx | 2 +- .../agptui/composite/PublishAgentPopout.tsx | 2 +- .../src/components/nav/NavBarButtons.tsx | 2 +- .../frontend/src/lib/supabase/middleware.ts | 8 +- .../frontend/src/tests/auth.spec.ts | 8 +- .../frontend/src/tests/profile.spec.ts | 2 +- 30 files changed, 225 insertions(+), 234 deletions(-) rename autogpt_platform/frontend/src/app/{store => marketplace}/(user)/api_keys/page.tsx (100%) rename autogpt_platform/frontend/src/app/{store => marketplace}/(user)/credits/page.tsx (100%) rename autogpt_platform/frontend/src/app/{store => marketplace}/(user)/dashboard/page.tsx (100%) rename autogpt_platform/frontend/src/app/{store => marketplace}/(user)/integrations/page.tsx (100%) rename autogpt_platform/frontend/src/app/{store => marketplace}/(user)/layout.tsx (50%) rename autogpt_platform/frontend/src/app/{store => marketplace}/(user)/profile/page.tsx (100%) rename autogpt_platform/frontend/src/app/{store => marketplace}/(user)/settings/page.tsx (100%) rename autogpt_platform/frontend/src/app/{store => marketplace}/agent/[creator]/[slug]/page.tsx (96%) rename autogpt_platform/frontend/src/app/{store => marketplace}/creator/[creator]/page.tsx (98%) rename autogpt_platform/frontend/src/app/{store => marketplace}/search/page.tsx (100%) delete mode 100644 autogpt_platform/frontend/src/app/store/page.tsx diff --git a/autogpt_platform/backend/backend/data/credit.py b/autogpt_platform/backend/backend/data/credit.py index fe76b9c216..3dbe70cdc3 100644 --- a/autogpt_platform/backend/backend/data/credit.py +++ b/autogpt_platform/backend/backend/data/credit.py @@ -310,9 +310,9 @@ class UserCredit(UserCreditBase): ], mode="payment", success_url=settings.config.platform_base_url - + "/store/credits?topup=success", + + "/marketplace/credits?topup=success", cancel_url=settings.config.platform_base_url - + "/store/credits?topup=cancel", + + "/marketplace/credits?topup=cancel", ) # Create pending transaction diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py index 2b1b76d651..263268e909 100644 --- a/autogpt_platform/backend/backend/server/routers/v1.py +++ b/autogpt_platform/backend/backend/server/routers/v1.py @@ -196,7 +196,7 @@ async def manage_payment_method( ) -> dict[str, str]: session = stripe.billing_portal.Session.create( customer=await get_stripe_customer_id(user_id), - return_url=settings.config.platform_base_url + "/store/credits", + return_url=settings.config.platform_base_url + "/marketplace/credits", ) if not session: raise HTTPException( diff --git a/autogpt_platform/frontend/src/app/layout.tsx b/autogpt_platform/frontend/src/app/layout.tsx index 60efbde136..7dafb49e08 100644 --- a/autogpt_platform/frontend/src/app/layout.tsx +++ b/autogpt_platform/frontend/src/app/layout.tsx @@ -49,7 +49,7 @@ export default async function RootLayout({ links={[ { name: "Marketplace", - href: "/store", + href: "/marketplace", }, { name: "Library", @@ -66,7 +66,7 @@ export default async function RootLayout({ { icon: IconType.Edit, text: "Edit profile", - href: "/store/profile", + href: "/marketplace/profile", }, ], }, @@ -75,7 +75,7 @@ export default async function RootLayout({ { icon: IconType.LayoutDashboard, text: "Creator Dashboard", - href: "/store/dashboard", + href: "/marketplace/dashboard", }, { icon: IconType.UploadCloud, @@ -88,7 +88,7 @@ export default async function RootLayout({ { icon: IconType.Settings, text: "Settings", - href: "/store/settings", + href: "/marketplace/settings", }, ], }, diff --git a/autogpt_platform/frontend/src/app/store/(user)/api_keys/page.tsx b/autogpt_platform/frontend/src/app/marketplace/(user)/api_keys/page.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/store/(user)/api_keys/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/(user)/api_keys/page.tsx diff --git a/autogpt_platform/frontend/src/app/store/(user)/credits/page.tsx b/autogpt_platform/frontend/src/app/marketplace/(user)/credits/page.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/store/(user)/credits/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/(user)/credits/page.tsx diff --git a/autogpt_platform/frontend/src/app/store/(user)/dashboard/page.tsx b/autogpt_platform/frontend/src/app/marketplace/(user)/dashboard/page.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/store/(user)/dashboard/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/(user)/dashboard/page.tsx diff --git a/autogpt_platform/frontend/src/app/store/(user)/integrations/page.tsx b/autogpt_platform/frontend/src/app/marketplace/(user)/integrations/page.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/store/(user)/integrations/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/(user)/integrations/page.tsx diff --git a/autogpt_platform/frontend/src/app/store/(user)/layout.tsx b/autogpt_platform/frontend/src/app/marketplace/(user)/layout.tsx similarity index 50% rename from autogpt_platform/frontend/src/app/store/(user)/layout.tsx rename to autogpt_platform/frontend/src/app/marketplace/(user)/layout.tsx index fa6a66fe95..07b7d3f4b4 100644 --- a/autogpt_platform/frontend/src/app/store/(user)/layout.tsx +++ b/autogpt_platform/frontend/src/app/marketplace/(user)/layout.tsx @@ -5,13 +5,13 @@ export default function Layout({ children }: { children: React.ReactNode }) { const sidebarLinkGroups = [ { links: [ - { text: "Creator Dashboard", href: "/store/dashboard" }, - { text: "Agent dashboard", href: "/store/agent-dashboard" }, - { text: "Credits", href: "/store/credits" }, - { text: "Integrations", href: "/store/integrations" }, - { text: "API Keys", href: "/store/api_keys" }, - { text: "Profile", href: "/store/profile" }, - { text: "Settings", href: "/store/settings" }, + { text: "Creator Dashboard", href: "/marketplace/dashboard" }, + { text: "Agent dashboard", href: "/marketplace/agent-dashboard" }, + { text: "Credits", href: "/marketplace/credits" }, + { text: "Integrations", href: "/marketplace/integrations" }, + { text: "API Keys", href: "/marketplace/api_keys" }, + { text: "Profile", href: "/marketplace/profile" }, + { text: "Settings", href: "/marketplace/settings" }, ], }, ]; diff --git a/autogpt_platform/frontend/src/app/store/(user)/profile/page.tsx b/autogpt_platform/frontend/src/app/marketplace/(user)/profile/page.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/store/(user)/profile/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/(user)/profile/page.tsx diff --git a/autogpt_platform/frontend/src/app/store/(user)/settings/page.tsx b/autogpt_platform/frontend/src/app/marketplace/(user)/settings/page.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/store/(user)/settings/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/(user)/settings/page.tsx diff --git a/autogpt_platform/frontend/src/app/store/agent/[creator]/[slug]/page.tsx b/autogpt_platform/frontend/src/app/marketplace/agent/[creator]/[slug]/page.tsx similarity index 96% rename from autogpt_platform/frontend/src/app/store/agent/[creator]/[slug]/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/agent/[creator]/[slug]/page.tsx index b5d0ebeaa2..cd6b444396 100644 --- a/autogpt_platform/frontend/src/app/store/agent/[creator]/[slug]/page.tsx +++ b/autogpt_platform/frontend/src/app/marketplace/agent/[creator]/[slug]/page.tsx @@ -45,10 +45,10 @@ export default async function Page({ }); const breadcrumbs = [ - { name: "Store", link: "/store" }, + { name: "Store", link: "/marketplace" }, { name: agent.creator, - link: `/store/creator/${encodeURIComponent(agent.creator)}`, + link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`, }, { name: agent.agent_name, link: "#" }, ]; diff --git a/autogpt_platform/frontend/src/app/store/creator/[creator]/page.tsx b/autogpt_platform/frontend/src/app/marketplace/creator/[creator]/page.tsx similarity index 98% rename from autogpt_platform/frontend/src/app/store/creator/[creator]/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/creator/[creator]/page.tsx index 7474aef2e7..904f02b93f 100644 --- a/autogpt_platform/frontend/src/app/store/creator/[creator]/page.tsx +++ b/autogpt_platform/frontend/src/app/marketplace/creator/[creator]/page.tsx @@ -47,7 +47,7 @@ export default async function Page({
diff --git a/autogpt_platform/frontend/src/app/marketplace/page.tsx b/autogpt_platform/frontend/src/app/marketplace/page.tsx index b59d651f0b..297fee0c7f 100644 --- a/autogpt_platform/frontend/src/app/marketplace/page.tsx +++ b/autogpt_platform/frontend/src/app/marketplace/page.tsx @@ -1,7 +1,179 @@ -"use client"; +import * as React from "react"; +import { HeroSection } from "@/components/agptui/composite/HeroSection"; +import { + FeaturedSection, + FeaturedAgent, +} from "@/components/agptui/composite/FeaturedSection"; +import { + AgentsSection, + Agent, +} from "@/components/agptui/composite/AgentsSection"; +import { BecomeACreator } from "@/components/agptui/BecomeACreator"; +import { + FeaturedCreators, + FeaturedCreator, +} from "@/components/agptui/composite/FeaturedCreators"; +import { Separator } from "@/components/ui/separator"; +import { Metadata } from "next"; +import { + StoreAgentsResponse, + CreatorsResponse, +} from "@/lib/autogpt-server-api/types"; +import BackendAPI from "@/lib/autogpt-server-api"; -import { redirect } from "next/navigation"; +async function getStoreData() { + try { + const api = new BackendAPI(); -export default function Page() { - redirect("/store"); + // Add error handling and default values + let featuredAgents: StoreAgentsResponse = { + agents: [], + pagination: { + total_items: 0, + total_pages: 0, + current_page: 0, + page_size: 0, + }, + }; + let topAgents: StoreAgentsResponse = { + agents: [], + pagination: { + total_items: 0, + total_pages: 0, + current_page: 0, + page_size: 0, + }, + }; + let featuredCreators: CreatorsResponse = { + creators: [], + pagination: { + total_items: 0, + total_pages: 0, + current_page: 0, + page_size: 0, + }, + }; + + try { + [featuredAgents, topAgents, featuredCreators] = await Promise.all([ + api.getStoreAgents({ featured: true }), + api.getStoreAgents({ sorted_by: "runs" }), + api.getStoreCreators({ featured: true, sorted_by: "num_agents" }), + ]); + } catch (error) { + console.error("Error fetching store data:", error); + } + + return { + featuredAgents, + topAgents, + featuredCreators, + }; + } catch (error) { + console.error("Error in getStoreData:", error); + return { + featuredAgents: { + agents: [], + pagination: { + total_items: 0, + total_pages: 0, + current_page: 0, + page_size: 0, + }, + }, + topAgents: { + agents: [], + pagination: { + total_items: 0, + total_pages: 0, + current_page: 0, + page_size: 0, + }, + }, + featuredCreators: { + creators: [], + pagination: { + total_items: 0, + total_pages: 0, + current_page: 0, + page_size: 0, + }, + }, + }; + } +} + +// FIX: Correct metadata +export const metadata: Metadata = { + title: "Marketplace - NextGen AutoGPT", + description: "Find and use AI Agents created by our community", + applicationName: "NextGen AutoGPT Store", + authors: [{ name: "AutoGPT Team" }], + keywords: [ + "AI agents", + "automation", + "artificial intelligence", + "AutoGPT", + "marketplace", + ], + robots: { + index: true, + follow: true, + }, + openGraph: { + title: "Marketplace - NextGen AutoGPT", + description: "Find and use AI Agents created by our community", + type: "website", + siteName: "NextGen AutoGPT Store", + images: [ + { + url: "/images/store-og.png", + width: 1200, + height: 630, + alt: "NextGen AutoGPT Store", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "Marketplace - NextGen AutoGPT", + description: "Find and use AI Agents created by our community", + images: ["/images/store-twitter.png"], + }, + icons: { + icon: "/favicon.ico", + shortcut: "/favicon-16x16.png", + apple: "/apple-touch-icon.png", + }, +}; + +export default async function Page({}: {}) { + // Get data server-side + const { featuredAgents, topAgents, featuredCreators } = await getStoreData(); + + return ( +
+
+ + + + + + + + +
+
+ ); } diff --git a/autogpt_platform/frontend/src/app/store/search/page.tsx b/autogpt_platform/frontend/src/app/marketplace/search/page.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/store/search/page.tsx rename to autogpt_platform/frontend/src/app/marketplace/search/page.tsx diff --git a/autogpt_platform/frontend/src/app/page.tsx b/autogpt_platform/frontend/src/app/page.tsx index b59d651f0b..5a079f2629 100644 --- a/autogpt_platform/frontend/src/app/page.tsx +++ b/autogpt_platform/frontend/src/app/page.tsx @@ -3,5 +3,5 @@ import { redirect } from "next/navigation"; export default function Page() { - redirect("/store"); + redirect("/marketplace"); } diff --git a/autogpt_platform/frontend/src/app/signup/actions.ts b/autogpt_platform/frontend/src/app/signup/actions.ts index 0d0c3fb8a4..80fd19aa90 100644 --- a/autogpt_platform/frontend/src/app/signup/actions.ts +++ b/autogpt_platform/frontend/src/app/signup/actions.ts @@ -38,7 +38,7 @@ export async function signup(values: z.infer) { } console.log("Signed up"); revalidatePath("/", "layout"); - redirect("/store/profile"); + redirect("/marketplace/profile"); }, ); } diff --git a/autogpt_platform/frontend/src/app/store/page.tsx b/autogpt_platform/frontend/src/app/store/page.tsx deleted file mode 100644 index c59a124741..0000000000 --- a/autogpt_platform/frontend/src/app/store/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import * as React from "react"; -import { HeroSection } from "@/components/agptui/composite/HeroSection"; -import { - FeaturedSection, - FeaturedAgent, -} from "@/components/agptui/composite/FeaturedSection"; -import { - AgentsSection, - Agent, -} from "@/components/agptui/composite/AgentsSection"; -import { BecomeACreator } from "@/components/agptui/BecomeACreator"; -import { - FeaturedCreators, - FeaturedCreator, -} from "@/components/agptui/composite/FeaturedCreators"; -import { Separator } from "@/components/ui/separator"; -import { Metadata } from "next"; -import { - StoreAgentsResponse, - CreatorsResponse, -} from "@/lib/autogpt-server-api/types"; -import BackendAPI from "@/lib/autogpt-server-api"; - -export const dynamic = "force-dynamic"; - -async function getStoreData() { - try { - const api = new BackendAPI(); - - // Add error handling and default values - let featuredAgents: StoreAgentsResponse = { - agents: [], - pagination: { - total_items: 0, - total_pages: 0, - current_page: 0, - page_size: 0, - }, - }; - let topAgents: StoreAgentsResponse = { - agents: [], - pagination: { - total_items: 0, - total_pages: 0, - current_page: 0, - page_size: 0, - }, - }; - let featuredCreators: CreatorsResponse = { - creators: [], - pagination: { - total_items: 0, - total_pages: 0, - current_page: 0, - page_size: 0, - }, - }; - - try { - [featuredAgents, topAgents, featuredCreators] = await Promise.all([ - api.getStoreAgents({ featured: true }), - api.getStoreAgents({ sorted_by: "runs" }), - api.getStoreCreators({ featured: true, sorted_by: "num_agents" }), - ]); - } catch (error) { - console.error("Error fetching store data:", error); - } - - return { - featuredAgents, - topAgents, - featuredCreators, - }; - } catch (error) { - console.error("Error in getStoreData:", error); - return { - featuredAgents: { - agents: [], - pagination: { - total_items: 0, - total_pages: 0, - current_page: 0, - page_size: 0, - }, - }, - topAgents: { - agents: [], - pagination: { - total_items: 0, - total_pages: 0, - current_page: 0, - page_size: 0, - }, - }, - featuredCreators: { - creators: [], - pagination: { - total_items: 0, - total_pages: 0, - current_page: 0, - page_size: 0, - }, - }, - }; - } -} - -// FIX: Correct metadata -export const metadata: Metadata = { - title: "Marketplace - NextGen AutoGPT", - description: "Find and use AI Agents created by our community", - applicationName: "NextGen AutoGPT Store", - authors: [{ name: "AutoGPT Team" }], - keywords: [ - "AI agents", - "automation", - "artificial intelligence", - "AutoGPT", - "marketplace", - ], - robots: { - index: true, - follow: true, - }, - openGraph: { - title: "Marketplace - NextGen AutoGPT", - description: "Find and use AI Agents created by our community", - type: "website", - siteName: "NextGen AutoGPT Store", - images: [ - { - url: "/images/store-og.png", - width: 1200, - height: 630, - alt: "NextGen AutoGPT Store", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "Marketplace - NextGen AutoGPT", - description: "Find and use AI Agents created by our community", - images: ["/images/store-twitter.png"], - }, - icons: { - icon: "/favicon.ico", - shortcut: "/favicon-16x16.png", - apple: "/apple-touch-icon.png", - }, -}; - -export default async function Page({}: {}) { - // Get data server-side - const { featuredAgents, topAgents, featuredCreators } = await getStoreData(); - - return ( -
-
- - - - - - - - -
-
- ); -} diff --git a/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx b/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx index 03f7d141d7..7291b7923e 100644 --- a/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx +++ b/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx @@ -105,7 +105,7 @@ export const AgentInfo: React.FC = ({ by {creator} diff --git a/autogpt_platform/frontend/src/components/agptui/NavbarLink.tsx b/autogpt_platform/frontend/src/components/agptui/NavbarLink.tsx index 8d4094b40c..5bfe77d86d 100644 --- a/autogpt_platform/frontend/src/components/agptui/NavbarLink.tsx +++ b/autogpt_platform/frontend/src/components/agptui/NavbarLink.tsx @@ -28,7 +28,7 @@ export const NavbarLink = ({ name, href }: NavbarLinkProps) => { : "" } flex items-center justify-start gap-3`} > - {href === "/store" && ( + {href === "/marketplace" && ( diff --git a/autogpt_platform/frontend/src/components/agptui/SearchBar.tsx b/autogpt_platform/frontend/src/components/agptui/SearchBar.tsx index 95e3a3fe87..7f23d87f07 100644 --- a/autogpt_platform/frontend/src/components/agptui/SearchBar.tsx +++ b/autogpt_platform/frontend/src/components/agptui/SearchBar.tsx @@ -36,7 +36,7 @@ export const SearchBar: React.FC = ({ if (searchQuery.trim()) { // Encode the search term and navigate to the desired path const encodedTerm = encodeURIComponent(searchQuery); - router.push(`/store/search?searchTerm=${encodedTerm}`); + router.push(`/marketplace/search?searchTerm=${encodedTerm}`); } }; diff --git a/autogpt_platform/frontend/src/components/agptui/Sidebar.tsx b/autogpt_platform/frontend/src/components/agptui/Sidebar.tsx index 09267bed7f..08f56faa1f 100644 --- a/autogpt_platform/frontend/src/components/agptui/Sidebar.tsx +++ b/autogpt_platform/frontend/src/components/agptui/Sidebar.tsx @@ -46,7 +46,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => {
@@ -56,7 +56,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => { {stripeAvailable && ( @@ -66,7 +66,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => { )} @@ -75,7 +75,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => {
@@ -84,7 +84,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => {
@@ -93,7 +93,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => { @@ -110,7 +110,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => {
@@ -120,7 +120,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => { {stripeAvailable && ( @@ -130,7 +130,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => { )} @@ -139,7 +139,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => {
@@ -148,7 +148,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => {
@@ -157,7 +157,7 @@ export const Sidebar: React.FC = ({ linkGroups }) => { diff --git a/autogpt_platform/frontend/src/components/agptui/composite/AgentsSection.tsx b/autogpt_platform/frontend/src/components/agptui/composite/AgentsSection.tsx index fc4e13af4f..91cbfb1b01 100644 --- a/autogpt_platform/frontend/src/components/agptui/composite/AgentsSection.tsx +++ b/autogpt_platform/frontend/src/components/agptui/composite/AgentsSection.tsx @@ -39,7 +39,7 @@ export const AgentsSection: React.FC = ({ const handleCardClick = (creator: string, slug: string) => { router.push( - `/store/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`, + `/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`, ); }; diff --git a/autogpt_platform/frontend/src/components/agptui/composite/FeaturedCreators.tsx b/autogpt_platform/frontend/src/components/agptui/composite/FeaturedCreators.tsx index bb3ccbec69..bca4c1fc85 100644 --- a/autogpt_platform/frontend/src/components/agptui/composite/FeaturedCreators.tsx +++ b/autogpt_platform/frontend/src/components/agptui/composite/FeaturedCreators.tsx @@ -24,7 +24,7 @@ export const FeaturedCreators: React.FC = ({ const router = useRouter(); const handleCardClick = (creator: string) => { - router.push(`/store/creator/${encodeURIComponent(creator)}`); + router.push(`/marketplace/creator/${encodeURIComponent(creator)}`); }; // Only show first 4 creators diff --git a/autogpt_platform/frontend/src/components/agptui/composite/FeaturedSection.tsx b/autogpt_platform/frontend/src/components/agptui/composite/FeaturedSection.tsx index c8d9155212..5c1d257d92 100644 --- a/autogpt_platform/frontend/src/components/agptui/composite/FeaturedSection.tsx +++ b/autogpt_platform/frontend/src/components/agptui/composite/FeaturedSection.tsx @@ -43,7 +43,7 @@ export const FeaturedSection: React.FC = ({ const handleCardClick = (creator: string, slug: string) => { router.push( - `/store/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`, + `/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`, ); }; diff --git a/autogpt_platform/frontend/src/components/agptui/composite/HeroSection.tsx b/autogpt_platform/frontend/src/components/agptui/composite/HeroSection.tsx index e7b19e1ca7..61d025c351 100644 --- a/autogpt_platform/frontend/src/components/agptui/composite/HeroSection.tsx +++ b/autogpt_platform/frontend/src/components/agptui/composite/HeroSection.tsx @@ -10,7 +10,7 @@ export const HeroSection: React.FC = () => { function onFilterChange(selectedFilters: string[]) { const encodedTerm = encodeURIComponent(selectedFilters.join(", ")); - router.push(`/store/search?searchTerm=${encodedTerm}`); + router.push(`/marketplace/search?searchTerm=${encodedTerm}`); } return ( diff --git a/autogpt_platform/frontend/src/components/agptui/composite/PublishAgentPopout.tsx b/autogpt_platform/frontend/src/components/agptui/composite/PublishAgentPopout.tsx index f2b3c9b79c..56cdac79e9 100644 --- a/autogpt_platform/frontend/src/components/agptui/composite/PublishAgentPopout.tsx +++ b/autogpt_platform/frontend/src/components/agptui/composite/PublishAgentPopout.tsx @@ -260,7 +260,7 @@ export const PublishAgentPopout: React.FC = ({ onClose={handleClose} onDone={handleClose} onViewProgress={() => { - router.push("/store/dashboard"); + router.push("/marketplace/dashboard"); handleClose(); }} /> diff --git a/autogpt_platform/frontend/src/components/nav/NavBarButtons.tsx b/autogpt_platform/frontend/src/components/nav/NavBarButtons.tsx index 5e81f17bd0..7852fb941e 100644 --- a/autogpt_platform/frontend/src/components/nav/NavBarButtons.tsx +++ b/autogpt_platform/frontend/src/components/nav/NavBarButtons.tsx @@ -24,7 +24,7 @@ export function NavBarButtons({ className }: { className?: string }) { icon: , }, { - href: "/store", + href: "/marketplace", text: "Marketplace", icon: , }, diff --git a/autogpt_platform/frontend/src/lib/supabase/middleware.ts b/autogpt_platform/frontend/src/lib/supabase/middleware.ts index e6b4308a2b..363d9da1af 100644 --- a/autogpt_platform/frontend/src/lib/supabase/middleware.ts +++ b/autogpt_platform/frontend/src/lib/supabase/middleware.ts @@ -5,9 +5,9 @@ import { NextResponse, type NextRequest } from "next/server"; const PROTECTED_PAGES = [ "/monitor", "/build", - "/store/profile", - "/store/settings", - "/store/dashboard", + "/marketplace/profile", + "/marketplace/settings", + "/marketplace/dashboard", ]; const ADMIN_PAGES = ["/admin"]; @@ -87,7 +87,7 @@ export async function updateSession(request: NextRequest) { ADMIN_PAGES.some((page) => request.nextUrl.pathname.startsWith(`${page}`)) ) { // no user, potentially respond by redirecting the user to the login page - url.pathname = `/store`; + url.pathname = `/marketplace`; return NextResponse.redirect(url); } diff --git a/autogpt_platform/frontend/src/tests/auth.spec.ts b/autogpt_platform/frontend/src/tests/auth.spec.ts index 8c7ac4ab77..a658dbdb41 100644 --- a/autogpt_platform/frontend/src/tests/auth.spec.ts +++ b/autogpt_platform/frontend/src/tests/auth.spec.ts @@ -5,7 +5,7 @@ test.describe("Authentication", () => { test("user can login successfully", async ({ page, loginPage, testUser }) => { await page.goto("/login"); await loginPage.login(testUser.email, testUser.password); - await test.expect(page).toHaveURL("/store"); + await test.expect(page).toHaveURL("/marketplace"); await test .expect(page.getByTestId("profile-popout-menu-trigger")) .toBeVisible(); @@ -19,7 +19,7 @@ test.describe("Authentication", () => { await page.goto("/login"); await loginPage.login(testUser.email, testUser.password); - await test.expect(page).toHaveURL("/store"); + await test.expect(page).toHaveURL("/marketplace"); // Click on the profile menu trigger to open popout await page.getByTestId("profile-popout-menu-trigger").click(); @@ -43,7 +43,7 @@ test.describe("Authentication", () => { }) => { await page.goto("/login"); await loginPage.login(testUser.email, testUser.password); - await test.expect(page).toHaveURL("/store"); + await test.expect(page).toHaveURL("/marketplace"); // Click on the profile menu trigger to open popout await page.getByTestId("profile-popout-menu-trigger").click(); @@ -52,7 +52,7 @@ test.describe("Authentication", () => { await test.expect(page).toHaveURL("/login"); await loginPage.login(testUser.email, testUser.password); - await test.expect(page).toHaveURL("/store"); + await test.expect(page).toHaveURL("/marketplace"); await test .expect(page.getByTestId("profile-popout-menu-trigger")) .toBeVisible(); diff --git a/autogpt_platform/frontend/src/tests/profile.spec.ts b/autogpt_platform/frontend/src/tests/profile.spec.ts index 03787e2748..22048b0caa 100644 --- a/autogpt_platform/frontend/src/tests/profile.spec.ts +++ b/autogpt_platform/frontend/src/tests/profile.spec.ts @@ -10,7 +10,7 @@ test.describe("Profile", () => { // Start each test with login using worker auth await page.goto("/login"); await loginPage.login(testUser.email, testUser.password); - await test.expect(page).toHaveURL("/store"); + await test.expect(page).toHaveURL("/marketplace"); }); test("user can view their profile information", async ({ From 5383e8ba27e7ed9a1e55cd2023d548bdb96a1700 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 03:17:48 -0600 Subject: [PATCH 05/14] chore(libs/deps-dev): bump ruff from 0.8.6 to 0.9.2 in /autogpt_platform/autogpt_libs in the development-dependencies group across 1 directory (#9299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the development-dependencies group with 1 update in the /autogpt_platform/autogpt_libs directory: [ruff](https://github.com/astral-sh/ruff). Updates `ruff` from 0.8.6 to 0.9.2
Release notes

Sourced from ruff's releases.

0.9.2

Release Notes

Preview features

  • [airflow] Fix typo "security_managr" to "security_manager" (AIR303) (#15463)
  • [airflow] extend and fix AIR302 rules (#15525)
  • [fastapi] Handle parameters with Depends correctly (FAST003) (#15364)
  • [flake8-pytest-style] Implement pytest.warns diagnostics (PT029, PT030, PT031) (#15444)
  • [flake8-pytest-style] Test function parameters with default arguments (PT028) (#15449)
  • [flake8-type-checking] Avoid false positives for | in TC008 (#15201)

Rule changes

  • [flake8-todos] Allow VSCode GitHub PR extension style links in missing-todo-link (TD003) (#15519)
  • [pyflakes] Show syntax error message for F722 (#15523)

Formatter

  • Fix curly bracket spacing around f-string expressions containing curly braces (#15471)
  • Fix joining of f-strings with different quotes when using quote style Preserve (#15524)

Server

  • Avoid indexing the same workspace multiple times (#15495)
  • Display context for ruff.configuration errors (#15452)

Configuration

  • Remove flatten to improve deserialization error messages (#15414)

Bug fixes

  • Parse triple-quoted string annotations as if parenthesized (#15387)
  • [fastapi] Update Annotated fixes (FAST002) (#15462)
  • [flake8-bandit] Check for builtins instead of builtin (S102, PTH123) (#15443)
  • [flake8-pathlib] Fix --select for os-path-dirname (PTH120) (#15446)
  • [ruff] Fix false positive on global keyword (RUF052) (#15235)

Contributors

... (truncated)

Changelog

Sourced from ruff's changelog.

0.9.2

Preview features

  • [airflow] Fix typo "security_managr" to "security_manager" (AIR303) (#15463)
  • [airflow] extend and fix AIR302 rules (#15525)
  • [fastapi] Handle parameters with Depends correctly (FAST003) (#15364)
  • [flake8-pytest-style] Implement pytest.warns diagnostics (PT029, PT030, PT031) (#15444)
  • [flake8-pytest-style] Test function parameters with default arguments (PT028) (#15449)
  • [flake8-type-checking] Avoid false positives for | in TC008 (#15201)

Rule changes

  • [flake8-todos] Allow VSCode GitHub PR extension style links in missing-todo-link (TD003) (#15519)
  • [pyflakes] Show syntax error message for F722 (#15523)

Formatter

  • Fix curly bracket spacing around f-string expressions containing curly braces (#15471)
  • Fix joining of f-strings with different quotes when using quote style Preserve (#15524)

Server

  • Avoid indexing the same workspace multiple times (#15495)
  • Display context for ruff.configuration errors (#15452)

Configuration

  • Remove flatten to improve deserialization error messages (#15414)

Bug fixes

  • Parse triple-quoted string annotations as if parenthesized (#15387)
  • [fastapi] Update Annotated fixes (FAST002) (#15462)
  • [flake8-bandit] Check for builtins instead of builtin (S102, PTH123) (#15443)
  • [flake8-pathlib] Fix --select for os-path-dirname (PTH120) (#15446)
  • [ruff] Fix false positive on global keyword (RUF052) (#15235)

0.9.1

Preview features

  • [pycodestyle] Run too-many-newlines-at-end-of-file on each cell in notebooks (W391) (#15308)
  • [ruff] Omit diagnostic for shadowed private function parameters in used-dummy-variable (RUF052) (#15376)

Rule changes

  • [flake8-bugbear] Improve assert-raises-exception message (B017) (#15389)

Formatter

... (truncated)

Commits
  • 0a39348 Include build binaries
  • 027f800 Comment out non-npm-publish jobs
  • 425870d Upload npm publish logs when failed
  • c20255a Bump version to 0.9.2 (#15529)
  • 4203658 Fix joining of f-strings with different quotes when using quote style `Preser...
  • fc9dd63 [airflow] extend and fix AIR302 rules (#15525)
  • 79e52c7 [pyflakes] Show syntax error message for F722 (#15523)
  • cf4ab7c Parse triple quoted string annotations as if parenthesized (#15387)
  • d2656e8 [flake8-todos] Allow VSCode GitHub PR extension style links in `missing-tod...
  • c53ee60 Typeshed-sync workflow: add appropriate labels, link directly to failing run ...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruff&package-manager=pip&previous-version=0.8.6&new-version=0.9.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- autogpt_platform/autogpt_libs/poetry.lock | 40 ++++++++++---------- autogpt_platform/autogpt_libs/pyproject.toml | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/autogpt_platform/autogpt_libs/poetry.lock b/autogpt_platform/autogpt_libs/poetry.lock index e194da4ca2..7a56c3f59c 100644 --- a/autogpt_platform/autogpt_libs/poetry.lock +++ b/autogpt_platform/autogpt_libs/poetry.lock @@ -1415,29 +1415,29 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.8.6" +version = "0.9.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"}, - {file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"}, - {file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"}, - {file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"}, - {file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"}, - {file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"}, - {file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"}, + {file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"}, + {file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"}, + {file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"}, + {file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"}, + {file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"}, + {file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"}, + {file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"}, + {file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"}, + {file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"}, ] [[package]] @@ -1852,4 +1852,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "bf1b0125759dadb1369fff05ffba64fea3e82b9b7a43d0068e1c80974a4ebc1c" +content-hash = "c4d1013490d59a8953ec233eda4c2f3b1ac458a358e1ff4cb4ff508b1a967018" diff --git a/autogpt_platform/autogpt_libs/pyproject.toml b/autogpt_platform/autogpt_libs/pyproject.toml index 49746c56e4..c7c75cf85e 100644 --- a/autogpt_platform/autogpt_libs/pyproject.toml +++ b/autogpt_platform/autogpt_libs/pyproject.toml @@ -21,7 +21,7 @@ supabase = "^2.10.0" [tool.poetry.group.dev.dependencies] redis = "^5.2.1" -ruff = "^0.8.6" +ruff = "^0.9.2" [build-system] requires = ["poetry-core"] From da7aead3617e82f05fbf3e131f22c26e879a8284 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Fri, 24 Jan 2025 14:17:46 +0100 Subject: [PATCH 06/14] fix(frontend): Fix page layouts (sizing + padding) (#9311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolves #9310 ### Changes 🏗️ - Make base layout full width and fix its sizing behavior - Fix navbar overflowing on the right - Set padding on `/monitoring` - Make `/login` and `/signup` layouts self-center ### Checklist 📋 - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - Check layouts of all pages - `/signup` - `/login` - `/build` - `/monitoring` - `/store` - `/store/profile` - `/store/dashboard` - `/store/settings` --------- Co-authored-by: Zamil Majdy --- autogpt_platform/frontend/src/app/layout.tsx | 4 ++-- autogpt_platform/frontend/src/app/login/page.tsx | 2 +- autogpt_platform/frontend/src/app/monitoring/page.tsx | 2 +- autogpt_platform/frontend/src/app/signup/page.tsx | 2 +- .../frontend/src/components/agptui/Navbar.tsx | 2 +- .../frontend/src/components/auth/AuthCard.tsx | 11 +++++++++-- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/autogpt_platform/frontend/src/app/layout.tsx b/autogpt_platform/frontend/src/app/layout.tsx index 7dafb49e08..9f05958370 100644 --- a/autogpt_platform/frontend/src/app/layout.tsx +++ b/autogpt_platform/frontend/src/app/layout.tsx @@ -44,7 +44,7 @@ export default async function RootLayout({ // enableSystem disableTransitionOnChange > -
+
-
{children}
+
{children}
diff --git a/autogpt_platform/frontend/src/app/login/page.tsx b/autogpt_platform/frontend/src/app/login/page.tsx index 6edaf99006..bb7cf78fe2 100644 --- a/autogpt_platform/frontend/src/app/login/page.tsx +++ b/autogpt_platform/frontend/src/app/login/page.tsx @@ -93,7 +93,7 @@ export default function LoginPage() { } return ( - + Login to your account
diff --git a/autogpt_platform/frontend/src/app/monitoring/page.tsx b/autogpt_platform/frontend/src/app/monitoring/page.tsx index 66a37c96cf..0be12aaee5 100644 --- a/autogpt_platform/frontend/src/app/monitoring/page.tsx +++ b/autogpt_platform/frontend/src/app/monitoring/page.tsx @@ -73,7 +73,7 @@ const Monitor = () => { return (
+ Create a new account diff --git a/autogpt_platform/frontend/src/components/agptui/Navbar.tsx b/autogpt_platform/frontend/src/components/agptui/Navbar.tsx index 33d3fbf42a..9c3c260b94 100644 --- a/autogpt_platform/frontend/src/components/agptui/Navbar.tsx +++ b/autogpt_platform/frontend/src/components/agptui/Navbar.tsx @@ -57,7 +57,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => { return ( <> -
); diff --git a/autogpt_platform/frontend/src/components/agptui/AgentTable.tsx b/autogpt_platform/frontend/src/components/agptui/AgentTable.tsx index e6792c2605..19ee24fbac 100644 --- a/autogpt_platform/frontend/src/components/agptui/AgentTable.tsx +++ b/autogpt_platform/frontend/src/components/agptui/AgentTable.tsx @@ -6,7 +6,13 @@ import { AgentTableCard } from "./AgentTableCard"; import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types"; export interface AgentTableProps { - agents: AgentTableRowProps[]; + agents: Omit< + AgentTableRowProps, + | "setSelectedAgents" + | "selectedAgents" + | "onEditSubmission" + | "onDeleteSubmission" + >[]; onEditSubmission: (submission: StoreSubmissionRequest) => void; onDeleteSubmission: (submission_id: string) => void; } @@ -25,7 +31,7 @@ export const AgentTable: React.FC = ({ const handleSelectAll = React.useCallback( (e: React.ChangeEvent) => { if (e.target.checked) { - setSelectedAgents(new Set(agents.map((agent) => agent.id.toString()))); + setSelectedAgents(new Set(agents.map((agent) => agent.agent_id))); } else { setSelectedAgents(new Set()); } @@ -88,11 +94,16 @@ export const AgentTable: React.FC = ({
- +
))} diff --git a/autogpt_platform/frontend/src/components/agptui/AgentTableRow.tsx b/autogpt_platform/frontend/src/components/agptui/AgentTableRow.tsx index 6b434e2306..9a2b896cc4 100644 --- a/autogpt_platform/frontend/src/components/agptui/AgentTableRow.tsx +++ b/autogpt_platform/frontend/src/components/agptui/AgentTableRow.tsx @@ -21,6 +21,8 @@ export interface AgentTableRowProps { rating: number; dateSubmitted: string; id: number; + selectedAgents: Set; + setSelectedAgents: React.Dispatch>>; onEditSubmission: (submission: StoreSubmissionRequest) => void; onDeleteSubmission: (submission_id: string) => void; } @@ -37,6 +39,8 @@ export const AgentTableRow: React.FC = ({ runs, rating, id, + selectedAgents, + setSelectedAgents, onEditSubmission, onDeleteSubmission, }) => { @@ -53,7 +57,7 @@ export const AgentTableRow: React.FC = ({ description, image_urls: imageSrc, categories: [], - } as StoreSubmissionRequest); + } satisfies StoreSubmissionRequest); }, [ agent_id, agent_version, @@ -68,6 +72,15 @@ export const AgentTableRow: React.FC = ({ onDeleteSubmission(agent_id); }, [agent_id, onDeleteSubmission]); + const handleCheckboxChange = React.useCallback(() => { + if (selectedAgents.has(agent_id)) { + selectedAgents.delete(agent_id); + } else { + selectedAgents.add(agent_id); + } + setSelectedAgents(new Set(selectedAgents)); + }, [agent_id, selectedAgents, setSelectedAgents]); + return (
@@ -77,6 +90,8 @@ export const AgentTableRow: React.FC = ({ id={checkboxId} aria-label={`Select ${agentName}`} className="mr-4 h-5 w-5 rounded border-2 border-neutral-400 dark:border-neutral-600" + checked={selectedAgents.has(agent_id)} + onChange={handleCheckboxChange} /> {/* Single label instead of multiple */}