otto suggestions

This commit is contained in:
Swifty
2026-02-13 11:58:55 +01:00
parent 0002cf9249
commit 16061aefc6
4 changed files with 54 additions and 14 deletions

View File

@@ -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/<workspace>/project/<project-id>
# 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=

View File

@@ -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,
},
},
)

View File

@@ -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")

View File

@@ -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")