diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/__init__.py b/autogpt_platform/backend/backend/api/features/chat/tools/__init__.py
index dcbc35ef37..350776081a 100644
--- a/autogpt_platform/backend/backend/api/features/chat/tools/__init__.py
+++ b/autogpt_platform/backend/backend/api/features/chat/tools/__init__.py
@@ -12,6 +12,7 @@ from .base import BaseTool
from .create_agent import CreateAgentTool
from .customize_agent import CustomizeAgentTool
from .edit_agent import EditAgentTool
+from .feature_requests import CreateFeatureRequestTool, SearchFeatureRequestsTool
from .find_agent import FindAgentTool
from .find_block import FindBlockTool
from .find_library_agent import FindLibraryAgentTool
@@ -45,6 +46,9 @@ TOOL_REGISTRY: dict[str, BaseTool] = {
"view_agent_output": AgentOutputTool(),
"search_docs": SearchDocsTool(),
"get_doc_page": GetDocPageTool(),
+ # Feature request tools
+ "search_feature_requests": SearchFeatureRequestsTool(),
+ "create_feature_request": CreateFeatureRequestTool(),
# Workspace tools for CoPilot file operations
"list_workspace_files": ListWorkspaceFilesTool(),
"read_workspace_file": ReadWorkspaceFileTool(),
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
new file mode 100644
index 0000000000..5e06d8b4b2
--- /dev/null
+++ b/autogpt_platform/backend/backend/api/features/chat/tools/feature_requests.py
@@ -0,0 +1,369 @@
+"""Feature request tools - search and create feature requests via Linear."""
+
+import logging
+from typing import Any
+
+from pydantic import SecretStr
+
+from backend.api.features.chat.model import ChatSession
+from backend.api.features.chat.tools.base import BaseTool
+from backend.api.features.chat.tools.models import (
+ ErrorResponse,
+ FeatureRequestCreatedResponse,
+ FeatureRequestInfo,
+ FeatureRequestSearchResponse,
+ NoResultsResponse,
+ ToolResponseBase,
+)
+from backend.blocks.linear._api import LinearClient
+from backend.data.model import APIKeyCredentials
+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
+SEARCH_ISSUES_QUERY = """
+query SearchFeatureRequests($term: String!, $filter: IssueFilter, $first: Int) {
+ searchIssues(term: $term, filter: $filter, first: $first) {
+ nodes {
+ id
+ identifier
+ title
+ description
+ }
+ }
+}
+"""
+
+CUSTOMER_UPSERT_MUTATION = """
+mutation CustomerUpsert($input: CustomerUpsertInput!) {
+ customerUpsert(input: $input) {
+ success
+ customer {
+ id
+ name
+ externalIds
+ }
+ }
+}
+"""
+
+ISSUE_CREATE_MUTATION = """
+mutation IssueCreate($input: IssueCreateInput!) {
+ issueCreate(input: $input) {
+ success
+ issue {
+ id
+ identifier
+ title
+ url
+ }
+ }
+}
+"""
+
+CUSTOMER_NEED_CREATE_MUTATION = """
+mutation CustomerNeedCreate($input: CustomerNeedCreateInput!) {
+ customerNeedCreate(input: $input) {
+ success
+ need {
+ id
+ body
+ customer {
+ id
+ name
+ }
+ issue {
+ id
+ identifier
+ title
+ url
+ }
+ }
+ }
+}
+"""
+
+
+_settings: Settings | None = None
+
+
+def _get_settings() -> Settings:
+ global _settings
+ if _settings is None:
+ _settings = Settings()
+ return _settings
+
+
+def _get_linear_client() -> LinearClient:
+ """Create a Linear client using the system API key from settings."""
+ api_key = _get_settings().secrets.linear_api_key
+ if not api_key:
+ raise RuntimeError("LINEAR_API_KEY secret is not configured")
+ credentials = APIKeyCredentials(
+ id="system-linear",
+ provider="linear",
+ api_key=SecretStr(api_key),
+ title="System Linear API Key",
+ )
+ return LinearClient(credentials=credentials)
+
+
+class SearchFeatureRequestsTool(BaseTool):
+ """Tool for searching existing feature requests in Linear."""
+
+ @property
+ def name(self) -> str:
+ return "search_feature_requests"
+
+ @property
+ def description(self) -> str:
+ return (
+ "Search existing feature requests to check if a similar request "
+ "already exists before creating a new one. Returns matching feature "
+ "requests with their ID, title, and description."
+ )
+
+ @property
+ def parameters(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "Search term to find matching feature requests.",
+ },
+ },
+ "required": ["query"],
+ }
+
+ @property
+ def requires_auth(self) -> bool:
+ return True
+
+ async def _execute(
+ self,
+ user_id: str | None,
+ session: ChatSession,
+ **kwargs,
+ ) -> ToolResponseBase:
+ query = kwargs.get("query", "").strip()
+ session_id = session.session_id if session else None
+
+ if not query:
+ return ErrorResponse(
+ message="Please provide a search query.",
+ error="Missing query parameter",
+ session_id=session_id,
+ )
+
+ client = _get_linear_client()
+ data = await client.query(
+ SEARCH_ISSUES_QUERY,
+ {
+ "term": query,
+ "filter": {
+ "project": {"id": {"eq": FEATURE_REQUEST_PROJECT_ID}},
+ },
+ "first": MAX_SEARCH_RESULTS,
+ },
+ )
+
+ nodes = data.get("searchIssues", {}).get("nodes", [])
+
+ if not nodes:
+ return NoResultsResponse(
+ message=f"No feature requests found matching '{query}'.",
+ suggestions=[
+ "Try different keywords",
+ "Use broader search terms",
+ "You can create a new feature request if none exists",
+ ],
+ session_id=session_id,
+ )
+
+ results = [
+ FeatureRequestInfo(
+ id=node["id"],
+ identifier=node["identifier"],
+ title=node["title"],
+ description=node.get("description"),
+ )
+ for node in nodes
+ ]
+
+ return FeatureRequestSearchResponse(
+ message=f"Found {len(results)} feature request(s) matching '{query}'.",
+ results=results,
+ count=len(results),
+ query=query,
+ session_id=session_id,
+ )
+
+
+class CreateFeatureRequestTool(BaseTool):
+ """Tool for creating feature requests (or adding needs to existing ones)."""
+
+ @property
+ def name(self) -> str:
+ return "create_feature_request"
+
+ @property
+ def description(self) -> str:
+ return (
+ "Create a new feature request or add a customer need to an existing one. "
+ "Always search first with search_feature_requests to avoid duplicates. "
+ "If a matching request exists, pass its ID as existing_issue_id to add "
+ "the user's need to it instead of creating a duplicate."
+ )
+
+ @property
+ def parameters(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Title for the feature request.",
+ },
+ "description": {
+ "type": "string",
+ "description": "Detailed description of what the user wants and why.",
+ },
+ "existing_issue_id": {
+ "type": "string",
+ "description": (
+ "If adding a need to an existing feature request, "
+ "provide its Linear issue ID (from search results). "
+ "Omit to create a new feature request."
+ ),
+ },
+ },
+ "required": ["title", "description"],
+ }
+
+ @property
+ def requires_auth(self) -> bool:
+ return True
+
+ async def _find_or_create_customer(
+ self, client: LinearClient, user_id: str
+ ) -> dict:
+ """Find existing customer by user_id or create a new one via upsert."""
+ data = await client.mutate(
+ CUSTOMER_UPSERT_MUTATION,
+ {
+ "input": {
+ "name": user_id,
+ "externalId": user_id,
+ },
+ },
+ )
+ result = data.get("customerUpsert", {})
+ if not result.get("success"):
+ raise RuntimeError(f"Failed to upsert customer: {data}")
+ return result["customer"]
+
+ async def _execute(
+ self,
+ user_id: str | None,
+ session: ChatSession,
+ **kwargs,
+ ) -> ToolResponseBase:
+ title = kwargs.get("title", "").strip()
+ description = kwargs.get("description", "").strip()
+ existing_issue_id = kwargs.get("existing_issue_id")
+ session_id = session.session_id if session else None
+
+ if not title or not description:
+ return ErrorResponse(
+ message="Both title and description are required.",
+ error="Missing required parameters",
+ session_id=session_id,
+ )
+
+ if not user_id:
+ return ErrorResponse(
+ message="Authentication required to create feature requests.",
+ error="Missing user_id",
+ session_id=session_id,
+ )
+
+ client = _get_linear_client()
+
+ # Step 1: Find or create customer for this user
+ customer = await self._find_or_create_customer(client, user_id)
+ customer_id = customer["id"]
+ customer_name = customer["name"]
+
+ # Step 2: Create or reuse issue
+ if existing_issue_id:
+ # Add need to existing issue - we still need the issue details for response
+ is_new_issue = False
+ issue_id = existing_issue_id
+ else:
+ # Create new issue in the feature requests project
+ data = await client.mutate(
+ ISSUE_CREATE_MUTATION,
+ {
+ "input": {
+ "title": title,
+ "description": description,
+ "teamId": TEAM_ID,
+ "projectId": FEATURE_REQUEST_PROJECT_ID,
+ },
+ },
+ )
+ result = data.get("issueCreate", {})
+ if not result.get("success"):
+ return ErrorResponse(
+ message="Failed to create feature request issue.",
+ error=str(data),
+ session_id=session_id,
+ )
+ issue = result["issue"]
+ issue_id = issue["id"]
+ is_new_issue = True
+
+ # Step 3: Create customer need on the issue
+ data = await client.mutate(
+ CUSTOMER_NEED_CREATE_MUTATION,
+ {
+ "input": {
+ "customerId": customer_id,
+ "issueId": issue_id,
+ "body": description,
+ "priority": 0,
+ },
+ },
+ )
+ need_result = data.get("customerNeedCreate", {})
+ if not need_result.get("success"):
+ return ErrorResponse(
+ message="Failed to attach customer need to the feature request.",
+ error=str(data),
+ session_id=session_id,
+ )
+
+ need = need_result["need"]
+ issue_info = need["issue"]
+
+ return FeatureRequestCreatedResponse(
+ message=(
+ f"{'Created new feature request' if is_new_issue else 'Added your request to existing feature request'} "
+ f"[{issue_info['identifier']}] {issue_info['title']}."
+ ),
+ issue_id=issue_info["id"],
+ issue_identifier=issue_info["identifier"],
+ issue_title=issue_info["title"],
+ issue_url=issue_info.get("url", ""),
+ is_new_issue=is_new_issue,
+ customer_name=customer_name,
+ session_id=session_id,
+ )
diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/models.py b/autogpt_platform/backend/backend/api/features/chat/tools/models.py
index 69c8c6c684..d420b289dc 100644
--- a/autogpt_platform/backend/backend/api/features/chat/tools/models.py
+++ b/autogpt_platform/backend/backend/api/features/chat/tools/models.py
@@ -40,6 +40,9 @@ class ResponseType(str, Enum):
OPERATION_IN_PROGRESS = "operation_in_progress"
# Input validation
INPUT_VALIDATION_ERROR = "input_validation_error"
+ # Feature request types
+ FEATURE_REQUEST_SEARCH = "feature_request_search"
+ FEATURE_REQUEST_CREATED = "feature_request_created"
# Base response model
@@ -421,3 +424,34 @@ class AsyncProcessingResponse(ToolResponseBase):
status: str = "accepted" # Must be "accepted" for detection
operation_id: str | None = None
task_id: str | None = None
+
+
+# Feature request models
+class FeatureRequestInfo(BaseModel):
+ """Information about a feature request issue."""
+
+ id: str
+ identifier: str
+ title: str
+ description: str | None = None
+
+
+class FeatureRequestSearchResponse(ToolResponseBase):
+ """Response for search_feature_requests tool."""
+
+ type: ResponseType = ResponseType.FEATURE_REQUEST_SEARCH
+ results: list[FeatureRequestInfo]
+ count: int
+ query: str
+
+
+class FeatureRequestCreatedResponse(ToolResponseBase):
+ """Response for create_feature_request tool."""
+
+ type: ResponseType = ResponseType.FEATURE_REQUEST_CREATED
+ issue_id: str
+ issue_identifier: str
+ issue_title: str
+ issue_url: str
+ is_new_issue: bool # False if added to existing
+ customer_name: str
diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py
index 50b7428160..d539832fb0 100644
--- a/autogpt_platform/backend/backend/util/settings.py
+++ b/autogpt_platform/backend/backend/util/settings.py
@@ -658,6 +658,9 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
mem0_api_key: str = Field(default="", description="Mem0 API key")
elevenlabs_api_key: str = Field(default="", description="ElevenLabs API key")
+ linear_api_key: str = Field(
+ default="", description="Linear API key for system-level operations"
+ )
linear_client_id: str = Field(default="", description="Linear client ID")
linear_client_secret: str = Field(default="", description="Linear client secret")
diff --git a/autogpt_platform/backend/test_linear_customers.py b/autogpt_platform/backend/test_linear_customers.py
new file mode 100644
index 0000000000..6e6f3e48fc
--- /dev/null
+++ b/autogpt_platform/backend/test_linear_customers.py
@@ -0,0 +1,468 @@
+"""
+Test script for Linear GraphQL API - Customer Requests operations.
+
+Tests the exact GraphQL calls needed for:
+1. search_feature_requests - search issues in the Customer Feature Requests project
+2. add_feature_request - upsert customer + create customer need on issue
+
+Requires LINEAR_API_KEY in backend/.env
+Generate one at: https://linear.app/settings/api
+"""
+
+import json
+import os
+import sys
+
+import httpx
+from dotenv import load_dotenv
+
+load_dotenv()
+
+LINEAR_API_URL = "https://api.linear.app/graphql"
+API_KEY = os.getenv("LINEAR_API_KEY")
+
+# Target project for feature requests
+FEATURE_REQUEST_PROJECT_ID = "13f066f3-f639-4a67-aaa3-31483ebdf8cd"
+# Team: Internal
+TEAM_ID = "557fd3d5-087e-43a9-83e3-476c8313ce49"
+
+if not API_KEY:
+ print("ERROR: LINEAR_API_KEY not found in .env")
+ print("Generate a personal API key at: https://linear.app/settings/api")
+ print("Then add LINEAR_API_KEY=lin_api_... to backend/.env")
+ sys.exit(1)
+
+HEADERS = {
+ "Authorization": API_KEY,
+ "Content-Type": "application/json",
+}
+
+
+def graphql(query: str, variables: dict | None = None) -> dict:
+ """Execute a GraphQL query against Linear API."""
+ payload = {"query": query}
+ if variables:
+ payload["variables"] = variables
+
+ resp = httpx.post(LINEAR_API_URL, json=payload, headers=HEADERS, timeout=30)
+ if resp.status_code != 200:
+ print(f"HTTP {resp.status_code}: {resp.text[:500]}")
+ resp.raise_for_status()
+ data = resp.json()
+
+ if "errors" in data:
+ print(f"GraphQL Errors: {json.dumps(data['errors'], indent=2)}")
+
+ return data
+
+
+# ---------------------------------------------------------------------------
+# QUERIES
+# ---------------------------------------------------------------------------
+
+# Search issues within the feature requests project by title/description
+SEARCH_ISSUES_IN_PROJECT = """
+query SearchFeatureRequests($filter: IssueFilter!, $first: Int) {
+ issues(filter: $filter, first: $first) {
+ nodes {
+ id
+ identifier
+ title
+ description
+ url
+ state {
+ name
+ type
+ }
+ project {
+ id
+ name
+ }
+ labels {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+}
+"""
+
+# Get issue with its customer needs
+GET_ISSUE_WITH_NEEDS = """
+query GetIssueWithNeeds($id: String!) {
+ issue(id: $id) {
+ id
+ identifier
+ title
+ url
+ needs {
+ nodes {
+ id
+ body
+ priority
+ customer {
+ id
+ name
+ domains
+ externalIds
+ }
+ }
+ }
+ }
+}
+"""
+
+# Search customers
+SEARCH_CUSTOMERS = """
+query SearchCustomers($filter: CustomerFilter, $first: Int) {
+ customers(filter: $filter, first: $first) {
+ nodes {
+ id
+ name
+ domains
+ externalIds
+ revenue
+ size
+ status {
+ name
+ }
+ tier {
+ name
+ }
+ }
+ }
+}
+"""
+
+# ---------------------------------------------------------------------------
+# MUTATIONS
+# ---------------------------------------------------------------------------
+
+CUSTOMER_UPSERT = """
+mutation CustomerUpsert($input: CustomerUpsertInput!) {
+ customerUpsert(input: $input) {
+ success
+ customer {
+ id
+ name
+ domains
+ externalIds
+ }
+ }
+}
+"""
+
+CUSTOMER_NEED_CREATE = """
+mutation CustomerNeedCreate($input: CustomerNeedCreateInput!) {
+ customerNeedCreate(input: $input) {
+ success
+ need {
+ id
+ body
+ priority
+ customer {
+ id
+ name
+ }
+ issue {
+ id
+ identifier
+ title
+ }
+ }
+ }
+}
+"""
+
+ISSUE_CREATE = """
+mutation IssueCreate($input: IssueCreateInput!) {
+ issueCreate(input: $input) {
+ success
+ issue {
+ id
+ identifier
+ title
+ url
+ }
+ }
+}
+"""
+
+
+# ---------------------------------------------------------------------------
+# TESTS
+# ---------------------------------------------------------------------------
+
+
+def test_1_search_feature_requests():
+ """Search for feature requests in the target project by keyword."""
+ print("\n" + "=" * 60)
+ print("TEST 1: Search feature requests in project by keyword")
+ print("=" * 60)
+
+ search_term = "agent"
+ result = graphql(
+ SEARCH_ISSUES_IN_PROJECT,
+ {
+ "filter": {
+ "project": {"id": {"eq": FEATURE_REQUEST_PROJECT_ID}},
+ "or": [
+ {"title": {"containsIgnoreCase": search_term}},
+ {"description": {"containsIgnoreCase": search_term}},
+ ],
+ },
+ "first": 5,
+ },
+ )
+
+ issues = result.get("data", {}).get("issues", {}).get("nodes", [])
+ for issue in issues:
+ proj = issue.get("project") or {}
+ print(f"\n [{issue['identifier']}] {issue['title']}")
+ print(f" Project: {proj.get('name', 'N/A')}")
+ print(f" State: {issue['state']['name']}")
+ print(f" URL: {issue['url']}")
+
+ print(f"\n Found {len(issues)} issues matching '{search_term}'")
+ return issues
+
+
+def test_2_list_all_in_project():
+ """List all issues in the feature requests project."""
+ print("\n" + "=" * 60)
+ print("TEST 2: List all issues in Customer Feature Requests project")
+ print("=" * 60)
+
+ result = graphql(
+ SEARCH_ISSUES_IN_PROJECT,
+ {
+ "filter": {
+ "project": {"id": {"eq": FEATURE_REQUEST_PROJECT_ID}},
+ },
+ "first": 10,
+ },
+ )
+
+ issues = result.get("data", {}).get("issues", {}).get("nodes", [])
+ if not issues:
+ print(" No issues in project yet (empty project)")
+ for issue in issues:
+ print(f"\n [{issue['identifier']}] {issue['title']}")
+ print(f" State: {issue['state']['name']}")
+
+ print(f"\n Total: {len(issues)} issues")
+ return issues
+
+
+def test_3_search_customers():
+ """List existing customers."""
+ print("\n" + "=" * 60)
+ print("TEST 3: List customers")
+ print("=" * 60)
+
+ result = graphql(SEARCH_CUSTOMERS, {"first": 10})
+ customers = result.get("data", {}).get("customers", {}).get("nodes", [])
+
+ if not customers:
+ print(" No customers exist yet")
+ for c in customers:
+ status = c.get("status") or {}
+ tier = c.get("tier") or {}
+ print(f"\n [{c['id'][:8]}...] {c['name']}")
+ print(f" Domains: {c.get('domains', [])}")
+ print(f" External IDs: {c.get('externalIds', [])}")
+ print(
+ f" Status: {status.get('name', 'N/A')}, Tier: {tier.get('name', 'N/A')}"
+ )
+
+ print(f"\n Total: {len(customers)} customers")
+ return customers
+
+
+def test_4_customer_upsert():
+ """Upsert a test customer."""
+ print("\n" + "=" * 60)
+ print("TEST 4: Customer upsert (find-or-create)")
+ print("=" * 60)
+
+ result = graphql(
+ CUSTOMER_UPSERT,
+ {
+ "input": {
+ "name": "Test Customer (API Test)",
+ "domains": ["test-api-customer.example.com"],
+ "externalId": "test-customer-001",
+ }
+ },
+ )
+
+ upsert = result.get("data", {}).get("customerUpsert", {})
+ if upsert.get("success"):
+ customer = upsert["customer"]
+ print(f" Success! Customer: {customer['name']}")
+ print(f" ID: {customer['id']}")
+ print(f" Domains: {customer['domains']}")
+ print(f" External IDs: {customer['externalIds']}")
+ return customer
+ else:
+ print(f" Failed: {json.dumps(result, indent=2)}")
+ return None
+
+
+def test_5_create_issue_and_need(customer_id: str):
+ """Create a new feature request issue and attach a customer need."""
+ print("\n" + "=" * 60)
+ print("TEST 5: Create issue + customer need")
+ print("=" * 60)
+
+ # Step 1: Create issue in the project
+ result = graphql(
+ ISSUE_CREATE,
+ {
+ "input": {
+ "title": "Test Feature Request (API Test - safe to delete)",
+ "description": "This is a test feature request created via the GraphQL API.",
+ "teamId": TEAM_ID,
+ "projectId": FEATURE_REQUEST_PROJECT_ID,
+ }
+ },
+ )
+
+ data = result.get("data")
+ if not data:
+ print(f" Issue creation failed: {json.dumps(result, indent=2)}")
+ return None
+ issue_data = data.get("issueCreate", {})
+ if not issue_data.get("success"):
+ print(f" Issue creation failed: {json.dumps(result, indent=2)}")
+ return None
+
+ issue = issue_data["issue"]
+ print(f" Created issue: [{issue['identifier']}] {issue['title']}")
+ print(f" URL: {issue['url']}")
+
+ # Step 2: Attach customer need
+ result = graphql(
+ CUSTOMER_NEED_CREATE,
+ {
+ "input": {
+ "customerId": customer_id,
+ "issueId": issue["id"],
+ "body": "Our team really needs this feature for our workflow. High priority for us!",
+ "priority": 0,
+ }
+ },
+ )
+
+ need_data = result.get("data", {}).get("customerNeedCreate", {})
+ if need_data.get("success"):
+ need = need_data["need"]
+ print(f" Attached customer need: {need['id']}")
+ print(f" Customer: {need['customer']['name']}")
+ print(f" Body: {need['body'][:80]}")
+ else:
+ print(f" Customer need creation failed: {json.dumps(result, indent=2)}")
+
+ # Step 3: Verify by fetching the issue with needs
+ print("\n Verifying...")
+ verify = graphql(GET_ISSUE_WITH_NEEDS, {"id": issue["id"]})
+ issue_verify = verify.get("data", {}).get("issue", {})
+ needs = issue_verify.get("needs", {}).get("nodes", [])
+ print(f" Issue now has {len(needs)} customer need(s)")
+ for n in needs:
+ cust = n.get("customer") or {}
+ print(f" - {cust.get('name', 'N/A')}: {n.get('body', '')[:60]}")
+
+ return issue
+
+
+def test_6_add_need_to_existing(customer_id: str, issue_id: str):
+ """Add a customer need to an existing issue (the common case)."""
+ print("\n" + "=" * 60)
+ print("TEST 6: Add customer need to existing issue")
+ print("=" * 60)
+
+ result = graphql(
+ CUSTOMER_NEED_CREATE,
+ {
+ "input": {
+ "customerId": customer_id,
+ "issueId": issue_id,
+ "body": "We also want this! +1 from our organization.",
+ "priority": 0,
+ }
+ },
+ )
+
+ need_data = result.get("data", {}).get("customerNeedCreate", {})
+ if need_data.get("success"):
+ need = need_data["need"]
+ print(f" Success! Need: {need['id']}")
+ print(f" Customer: {need['customer']['name']}")
+ print(f" Issue: [{need['issue']['identifier']}] {need['issue']['title']}")
+ return need
+ else:
+ print(f" Failed: {json.dumps(result, indent=2)}")
+ return None
+
+
+def main():
+ print("Linear GraphQL API - Customer Requests Test Suite")
+ print("=" * 60)
+ print(f"API URL: {LINEAR_API_URL}")
+ print(f"API Key: {API_KEY[:10]}...")
+ print(f"Project: Customer Feature Requests ({FEATURE_REQUEST_PROJECT_ID[:8]}...)")
+
+ # --- Read-only tests ---
+ test_1_search_feature_requests()
+ test_2_list_all_in_project()
+ test_3_search_customers()
+
+ # --- Write tests ---
+ print("\n" + "=" * 60)
+ answer = (
+ input("Run WRITE tests? (creates test customer + issue + need) [y/N]: ")
+ .strip()
+ .lower()
+ )
+ if answer != "y":
+ print("Skipped write tests.")
+ print("\nDone!")
+ return
+
+ customer = test_4_customer_upsert()
+ if not customer:
+ print("Customer upsert failed, stopping.")
+ return
+
+ issue = test_5_create_issue_and_need(customer["id"])
+ if not issue:
+ print("Issue creation failed, stopping.")
+ return
+
+ # Test adding a second need to the same issue (simulates another customer requesting same feature)
+ # First upsert a second customer
+ result = graphql(
+ CUSTOMER_UPSERT,
+ {
+ "input": {
+ "name": "Second Test Customer",
+ "domains": ["second-test.example.com"],
+ "externalId": "test-customer-002",
+ }
+ },
+ )
+ customer2 = result.get("data", {}).get("customerUpsert", {}).get("customer")
+ if customer2:
+ test_6_add_need_to_existing(customer2["id"], issue["id"])
+
+ print("\n" + "=" * 60)
+ print("All tests complete!")
+ print(
+ "Check the project: https://linear.app/autogpt/project/customer-feature-requests-710dcbf8bf4e/issues"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx
index 71ade81a9f..b62e96f58a 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx
@@ -15,6 +15,10 @@ import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai";
import { useEffect, useRef, useState } from "react";
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
+import {
+ CreateFeatureRequestTool,
+ SearchFeatureRequestsTool,
+} from "../../tools/FeatureRequests/FeatureRequests";
import { FindAgentsTool } from "../../tools/FindAgents/FindAgents";
import { FindBlocksTool } from "../../tools/FindBlocks/FindBlocks";
import { RunAgentTool } from "../../tools/RunAgent/RunAgent";
@@ -254,6 +258,20 @@ export const ChatMessagesContainer = ({
part={part as ToolUIPart}
/>
);
+ case "tool-search_feature_requests":
+ return (
+