Skip to content

Commit e23d8ac

Browse files
jayhackrushilpatel0
authored andcommitted
feat: search_linear and create_linear tools (#508)
1 parent c3f7474 commit e23d8ac

File tree

4 files changed

+254
-3
lines changed

4 files changed

+254
-3
lines changed

src/codegen/extensions/langchain/tools.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
from codegen.extensions.linear.linear_client import LinearClient
1111
from codegen.extensions.tools.linear_tools import (
1212
linear_comment_on_issue_tool,
13+
linear_create_issue_tool,
1314
linear_get_issue_comments_tool,
1415
linear_get_issue_tool,
16+
linear_get_teams_tool,
17+
linear_search_issues_tool,
1518
)
1619

1720
from ..tools import (
@@ -532,6 +535,68 @@ def _run(self, issue_id: str, body: str) -> str:
532535
return json.dumps(result, indent=2)
533536

534537

538+
class LinearSearchIssuesInput(BaseModel):
539+
"""Input for searching Linear issues."""
540+
541+
query: str = Field(..., description="Search query string")
542+
limit: int = Field(default=10, description="Maximum number of issues to return")
543+
544+
545+
class LinearSearchIssuesTool(BaseTool):
546+
"""Tool for searching Linear issues."""
547+
548+
name: ClassVar[str] = "linear_search_issues"
549+
description: ClassVar[str] = "Search for Linear issues using a query string"
550+
args_schema: ClassVar[type[BaseModel]] = LinearSearchIssuesInput
551+
client: LinearClient = Field(exclude=True)
552+
553+
def __init__(self, client: LinearClient) -> None:
554+
super().__init__(client=client)
555+
556+
def _run(self, query: str, limit: int = 10) -> str:
557+
result = linear_search_issues_tool(self.client, query, limit)
558+
return json.dumps(result, indent=2)
559+
560+
561+
class LinearCreateIssueInput(BaseModel):
562+
"""Input for creating a Linear issue."""
563+
564+
title: str = Field(..., description="Title of the issue")
565+
description: str | None = Field(None, description="Optional description of the issue")
566+
team_id: str | None = Field(None, description="Optional team ID. If not provided, uses the default team_id (recommended)")
567+
568+
569+
class LinearCreateIssueTool(BaseTool):
570+
"""Tool for creating Linear issues."""
571+
572+
name: ClassVar[str] = "linear_create_issue"
573+
description: ClassVar[str] = "Create a new Linear issue"
574+
args_schema: ClassVar[type[BaseModel]] = LinearCreateIssueInput
575+
client: LinearClient = Field(exclude=True)
576+
577+
def __init__(self, client: LinearClient) -> None:
578+
super().__init__(client=client)
579+
580+
def _run(self, title: str, description: str | None = None, team_id: str | None = None) -> str:
581+
result = linear_create_issue_tool(self.client, title, description, team_id)
582+
return json.dumps(result, indent=2)
583+
584+
585+
class LinearGetTeamsTool(BaseTool):
586+
"""Tool for getting Linear teams."""
587+
588+
name: ClassVar[str] = "linear_get_teams"
589+
description: ClassVar[str] = "Get all Linear teams the authenticated user has access to"
590+
client: LinearClient = Field(exclude=True)
591+
592+
def __init__(self, client: LinearClient) -> None:
593+
super().__init__(client=client)
594+
595+
def _run(self) -> str:
596+
result = linear_get_teams_tool(self.client)
597+
return json.dumps(result, indent=2)
598+
599+
535600
def get_workspace_tools(codebase: Codebase) -> list["BaseTool"]:
536601
"""Get all workspace tools initialized with a codebase.
537602
@@ -561,4 +626,7 @@ def get_workspace_tools(codebase: Codebase) -> list["BaseTool"]:
561626
LinearGetIssueTool(codebase),
562627
LinearGetIssueCommentsTool(codebase),
563628
LinearCommentOnIssueTool(codebase),
629+
LinearSearchIssuesTool(codebase),
630+
LinearCreateIssueTool(codebase),
631+
LinearGetTeamsTool(codebase),
564632
]

src/codegen/extensions/linear/linear_client.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ class LinearUser(BaseModel):
1717
name: str
1818

1919

20+
class LinearTeam(BaseModel):
21+
"""Represents a Linear team."""
22+
23+
id: str
24+
name: str
25+
key: str
26+
27+
2028
class LinearComment(BaseModel):
2129
id: str
2230
body: str
@@ -33,13 +41,18 @@ class LinearClient:
3341
api_headers: dict
3442
api_endpoint = "https://api.linear.app/graphql"
3543

36-
def __init__(self, access_token: Optional[str] = None):
44+
def __init__(self, access_token: Optional[str] = None, team_id: Optional[str] = None):
3745
if not access_token:
3846
access_token = os.getenv("LINEAR_ACCESS_TOKEN")
3947
if not access_token:
4048
msg = "access_token is required"
4149
raise ValueError(msg)
4250
self.access_token = access_token
51+
52+
if not team_id:
53+
team_id = os.getenv("LINEAR_TEAM_ID")
54+
self.team_id = team_id
55+
4356
self.api_headers = {
4457
"Content-Type": "application/json",
4558
"Authorization": self.access_token,
@@ -194,3 +207,101 @@ def search_issues(self, query: str, limit: int = 10) -> list[LinearIssue]:
194207
except Exception as e:
195208
msg = f"Error searching issues\n{data}\n{e}"
196209
raise Exception(msg)
210+
211+
def create_issue(self, title: str, description: str | None = None, team_id: str | None = None) -> LinearIssue:
212+
"""Create a new issue.
213+
214+
Args:
215+
title: Title of the issue
216+
description: Optional description of the issue
217+
team_id: Optional team ID. If not provided, uses the client's configured team_id
218+
219+
Returns:
220+
The created LinearIssue object
221+
222+
Raises:
223+
ValueError: If no team_id is provided or configured
224+
"""
225+
if not team_id:
226+
team_id = self.team_id
227+
if not team_id:
228+
msg = "team_id must be provided either during client initialization or in the create_issue call"
229+
raise ValueError(msg)
230+
231+
mutation = """
232+
mutation createIssue($input: IssueCreateInput!) {
233+
issueCreate(input: $input) {
234+
success
235+
issue {
236+
id
237+
title
238+
description
239+
}
240+
}
241+
}
242+
"""
243+
244+
variables = {
245+
"input": {
246+
"teamId": team_id,
247+
"title": title,
248+
"description": description,
249+
}
250+
}
251+
252+
response = requests.post(
253+
self.api_endpoint,
254+
headers=self.api_headers,
255+
json={"query": mutation, "variables": variables},
256+
)
257+
data = response.json()
258+
259+
try:
260+
issue_data = data["data"]["issueCreate"]["issue"]
261+
return LinearIssue(
262+
id=issue_data["id"],
263+
title=issue_data["title"],
264+
description=issue_data["description"],
265+
)
266+
except Exception as e:
267+
msg = f"Error creating issue\n{data}\n{e}"
268+
raise Exception(msg)
269+
270+
def get_teams(self) -> list[LinearTeam]:
271+
"""Get all teams the authenticated user has access to.
272+
273+
Returns:
274+
List of LinearTeam objects
275+
"""
276+
query = """
277+
query {
278+
teams {
279+
nodes {
280+
id
281+
name
282+
key
283+
}
284+
}
285+
}
286+
"""
287+
288+
response = requests.post(
289+
self.api_endpoint,
290+
headers=self.api_headers,
291+
json={"query": query},
292+
)
293+
data = response.json()
294+
295+
try:
296+
teams_data = data["data"]["teams"]["nodes"]
297+
return [
298+
LinearTeam(
299+
id=team["id"],
300+
name=team["name"],
301+
key=team["key"],
302+
)
303+
for team in teams_data
304+
]
305+
except Exception as e:
306+
msg = f"Error getting teams\n{data}\n{e}"
307+
raise Exception(msg)

src/codegen/extensions/tools/linear_tools.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,30 @@ def linear_register_webhook_tool(client: LinearClient, webhook_url: str, team_id
3737
return {"status": "success", "response": response}
3838
except Exception as e:
3939
return {"error": f"Failed to register webhook: {e!s}"}
40+
41+
42+
def linear_search_issues_tool(client: LinearClient, query: str, limit: int = 10) -> dict[str, Any]:
43+
"""Search for issues using a query string."""
44+
try:
45+
issues = client.search_issues(query, limit)
46+
return {"status": "success", "issues": [issue.dict() for issue in issues]}
47+
except Exception as e:
48+
return {"error": f"Failed to search issues: {e!s}"}
49+
50+
51+
def linear_create_issue_tool(client: LinearClient, title: str, description: str | None = None, team_id: str | None = None) -> dict[str, Any]:
52+
"""Create a new issue."""
53+
try:
54+
issue = client.create_issue(title, description, team_id)
55+
return {"status": "success", "issue": issue.dict()}
56+
except Exception as e:
57+
return {"error": f"Failed to create issue: {e!s}"}
58+
59+
60+
def linear_get_teams_tool(client: LinearClient) -> dict[str, Any]:
61+
"""Get all teams the authenticated user has access to."""
62+
try:
63+
teams = client.get_teams()
64+
return {"status": "success", "teams": [team.dict() for team in teams]}
65+
except Exception as e:
66+
return {"error": f"Failed to get teams: {e!s}"}

tests/integration/extension/test_linear.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
from codegen.extensions.linear.linear_client import LinearClient
88
from codegen.extensions.tools.linear_tools import (
99
linear_comment_on_issue_tool,
10+
linear_create_issue_tool,
1011
linear_get_issue_comments_tool,
1112
linear_get_issue_tool,
13+
linear_get_teams_tool,
14+
linear_search_issues_tool,
1215
)
1316

1417

@@ -18,7 +21,10 @@ def client() -> LinearClient:
1821
token = os.getenv("LINEAR_ACCESS_TOKEN")
1922
if not token:
2023
pytest.skip("LINEAR_ACCESS_TOKEN environment variable not set")
21-
return LinearClient(token)
24+
team_id = os.getenv("LINEAR_TEAM_ID")
25+
if not team_id:
26+
pytest.skip("LINEAR_TEAM_ID environment variable not set")
27+
return LinearClient(token, team_id)
2228

2329

2430
def test_linear_get_issue(client: LinearClient) -> None:
@@ -45,6 +51,45 @@ def test_linear_comment_on_issue(client: LinearClient) -> None:
4551

4652
def test_search_issues(client: LinearClient) -> None:
4753
"""Test searching for issues in Linear."""
48-
issues = client.search_issues("REVEAL_SYMBOL")
54+
issues = linear_search_issues_tool(client, "REVEAL_SYMBOL")
4955
assert issues["status"] == "success"
5056
assert len(issues["issues"]) > 0
57+
58+
59+
def test_create_issue(client: LinearClient) -> None:
60+
"""Test creating an issue in Linear."""
61+
# Test creating an issue with explicit team_id
62+
title = "Test Issue - Automated Testing (Explicit Team)"
63+
description = "This is a test issue created by automated testing with explicit team_id"
64+
65+
issue = client.create_issue(title, description)
66+
assert issue.title == title
67+
assert issue.description == description
68+
69+
# Test creating an issue using default team_id from environment
70+
title2 = "Test Issue - Automated Testing (Default Team)"
71+
description2 = "This is a test issue created by automated testing with default team_id"
72+
73+
issue2 = client.create_issue(title2, description2)
74+
assert issue2.title == title2
75+
assert issue2.description == description2
76+
77+
# Test the tool wrapper with default team_id
78+
result = linear_create_issue_tool(client, "Test Tool Issue", "Test description from tool")
79+
assert result["status"] == "success"
80+
assert result["issue"]["title"] == "Test Tool Issue"
81+
assert result["issue"]["description"] == "Test description from tool"
82+
83+
84+
def test_get_teams(client: LinearClient) -> None:
85+
"""Test getting teams from Linear."""
86+
result = linear_get_teams_tool(client)
87+
assert result["status"] == "success"
88+
assert len(result["teams"]) > 0
89+
90+
# Verify team structure
91+
team = result["teams"][0]
92+
print(result)
93+
assert "id" in team
94+
assert "name" in team
95+
assert "key" in team

0 commit comments

Comments
 (0)