diff --git a/autogpt_platform/backend/.env.default b/autogpt_platform/backend/.env.default index fa52ba812a..2711bd2df9 100644 --- a/autogpt_platform/backend/.env.default +++ b/autogpt_platform/backend/.env.default @@ -104,6 +104,12 @@ TWITTER_CLIENT_SECRET= # 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_API_KEY= +# Linear project and team IDs for the feature request tracker. +# Find these in your Linear workspace URL: linear.app//project/ +# and in team settings. Used by the chat copilot to file and search feature requests. +LINEAR_FEATURE_REQUEST_PROJECT_ID= +LINEAR_FEATURE_REQUEST_TEAM_ID= LINEAR_CLIENT_ID= LINEAR_CLIENT_SECRET= diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests.py b/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests.py index dbae615f6f..fc1ce34d6a 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests.py @@ -17,14 +17,11 @@ from backend.api.features.chat.tools.models import ( ) from backend.blocks.linear._api import LinearClient from backend.data.model import APIKeyCredentials +from backend.data.user import get_user_email_by_id from backend.util.settings import Settings logger = logging.getLogger(__name__) -# Target project and team IDs in our Linear workspace -FEATURE_REQUEST_PROJECT_ID = "13f066f3-f639-4a67-aaa3-31483ebdf8cd" -TEAM_ID = "557fd3d5-087e-43a9-83e3-476c8313ce49" - MAX_SEARCH_RESULTS = 10 # GraphQL queries/mutations @@ -164,13 +161,16 @@ class SearchFeatureRequestsTool(BaseTool): ) try: + secrets = _get_settings().secrets client = _get_linear_client() data = await client.query( SEARCH_ISSUES_QUERY, { "term": query, "filter": { - "project": {"id": {"eq": FEATURE_REQUEST_PROJECT_ID}}, + "project": { + "id": {"eq": secrets.linear_feature_request_project_id} + }, }, "first": MAX_SEARCH_RESULTS, }, @@ -261,14 +261,20 @@ class CreateFeatureRequestTool(BaseTool): return True async def _find_or_create_customer( - self, client: LinearClient, user_id: str + self, client: LinearClient, user_id: str, name: str ) -> dict: - """Find existing customer by user_id or create a new one via upsert.""" + """Find existing customer by user_id or create a new one via upsert. + + Args: + client: Linear API client. + user_id: Stable external ID used to deduplicate customers. + name: Human-readable display name (e.g. the user's email). + """ data = await client.mutate( CUSTOMER_UPSERT_MUTATION, { "input": { - "name": user_id, + "name": name, "externalId": user_id, }, }, @@ -304,6 +310,7 @@ class CreateFeatureRequestTool(BaseTool): ) try: + secrets = _get_settings().secrets client = _get_linear_client() except Exception as e: logger.exception("Failed to create Linear client") @@ -313,9 +320,18 @@ class CreateFeatureRequestTool(BaseTool): session_id=session_id, ) + # Resolve a human-readable name (email) for the Linear customer record. + # Fall back to user_id if the lookup fails or returns None. + try: + customer_display_name = await get_user_email_by_id(user_id) or user_id + except Exception: + customer_display_name = user_id + # Step 1: Find or create customer for this user try: - customer = await self._find_or_create_customer(client, user_id) + customer = await self._find_or_create_customer( + client, user_id, customer_display_name + ) customer_id = customer["id"] customer_name = customer["name"] except Exception as e: @@ -342,8 +358,8 @@ class CreateFeatureRequestTool(BaseTool): "input": { "title": title, "description": description, - "teamId": TEAM_ID, - "projectId": FEATURE_REQUEST_PROJECT_ID, + "teamId": secrets.linear_feature_request_team_id, + "projectId": secrets.linear_feature_request_project_id, }, }, ) diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests_test.py b/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests_test.py index c8b61b9c01..2938c15f96 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests_test.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests_test.py @@ -18,6 +18,7 @@ from backend.api.features.chat.tools.models import ( from ._test_data import make_session _TEST_USER_ID = "test-user-feature-requests" +_TEST_USER_EMAIL = "testuser@example.com" # --------------------------------------------------------------------------- @@ -46,7 +47,7 @@ def _search_response(nodes: list[dict]) -> dict: def _customer_upsert_response( - customer_id: str = "cust-1", name: str = _TEST_USER_ID, success: bool = True + customer_id: str = "cust-1", name: str = _TEST_USER_EMAIL, success: bool = True ) -> dict: return { "customerUpsert": { @@ -88,7 +89,7 @@ def _need_create_response( "need": { "id": need_id, "body": "description", - "customer": {"id": "cust-1", "name": _TEST_USER_ID}, + "customer": {"id": "cust-1", "name": _TEST_USER_EMAIL}, "issue": { "id": issue_id, "identifier": identifier, @@ -224,6 +225,15 @@ class TestSearchFeatureRequestsTool: class TestCreateFeatureRequestTool: """Tests for CreateFeatureRequestTool._execute.""" + @pytest.fixture(autouse=True) + def _patch_email_lookup(self): + with patch( + "backend.api.features.chat.tools.feature_requests.get_user_email_by_id", + new_callable=AsyncMock, + return_value=_TEST_USER_EMAIL, + ): + yield + # ---- Happy paths ------------------------------------------------------- @pytest.mark.asyncio(loop_scope="session") @@ -250,7 +260,7 @@ class TestCreateFeatureRequestTool: assert isinstance(resp, FeatureRequestCreatedResponse) assert resp.is_new_issue is True assert resp.issue_identifier == "FR-1" - assert resp.customer_name == _TEST_USER_ID + assert resp.customer_name == _TEST_USER_EMAIL assert client.mutate.call_count == 3 @pytest.mark.asyncio(loop_scope="session") diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index f35aa8bb3b..c5cca87b6e 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -665,6 +665,14 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): linear_api_key: str = Field( default="", description="Linear API key for system-level operations" ) + linear_feature_request_project_id: str = Field( + default="", + description="Linear project ID where feature requests are tracked", + ) + linear_feature_request_team_id: str = Field( + default="", + description="Linear team ID used when creating feature request issues", + ) linear_client_id: str = Field(default="", description="Linear client ID") linear_client_secret: str = Field(default="", description="Linear client secret")