Skip to content

Commit 373062f

Browse files
authored
Merge pull request #18 from jlowin/linked
Include linked items
2 parents 1e010a0 + b5dd80c commit 373062f

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

src/ai_labeler/ai.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,20 @@ def validate_labels(result: list[str]):
3232

3333
class Reasoning(BaseModel):
3434
label_name: str
35-
# reasoning: str
3635
should_apply: bool
3736

37+
# Format linked items for context
38+
linked_items_context = ""
39+
if item.linked_items:
40+
linked_items_context = "\nLinked Items:\n"
41+
for linked in item.linked_items:
42+
linked_items_context += f"""
43+
{linked.type.replace('_', ' ').title()} #{linked.number}:
44+
Title: {linked.title}
45+
Labels: {', '.join(linked.labels)}
46+
Body: {linked.body}
47+
"""
48+
3849
reasoning = cf.run(
3950
"""
4051
Consider the provided PR/issue, its context, and any provided
@@ -54,6 +65,11 @@ class Reasoning(BaseModel):
5465
5566
You do not need to return reasoning about labels that are obviously
5667
irrelevant.
68+
69+
When evaluating labels, consider any linked items and their context:
70+
- Look for patterns or relationships between the current item and linked items
71+
- Consider if linked items provide additional context about the scope or impact
72+
- Check if linked items have relevant labels that could inform this decision
5773
""",
5874
instructions=instructions,
5975
result_type=list[Reasoning],
@@ -62,6 +78,7 @@ class Reasoning(BaseModel):
6278
"available_labels": dict(enumerate(labels)),
6379
"additional_context": context_files,
6480
"labeling_instructions": instructions,
81+
"linked_items_context": linked_items_context,
6582
},
6683
agents=[labeler],
6784
completion_tools=["SUCCEED"], # the task can not be marked as failed

src/ai_labeler/github.py

+60-2
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
import os
22
import json
3-
from typing import Optional
3+
import re
4+
from typing import Optional, List
45
from github import Github
5-
from pydantic import BaseModel
6+
from pydantic import BaseModel, Field
67
from .config_parser import Config
78

89

10+
class LinkedItem(BaseModel):
11+
"""Represents a linked GitHub issue or PR"""
12+
13+
number: int
14+
title: str
15+
body: str
16+
labels: List[str]
17+
type: str = Field(..., pattern="^(issue|pull_request)$")
18+
19+
920
class PullRequest(BaseModel):
1021
title: str
1122
body: str
1223
files: dict[str, str | None]
1324
author: str # GitHub username
25+
linked_items: List[LinkedItem] = Field(default_factory=list)
1426

1527

1628
class Issue(BaseModel):
1729
title: str
1830
body: str
1931
author: str # GitHub username
32+
linked_items: List[LinkedItem] = Field(default_factory=list)
2033

2134

2235
class Label(BaseModel):
@@ -135,3 +148,48 @@ def get_available_labels_from_config(
135148
labels.append(label)
136149

137150
return labels
151+
152+
153+
def parse_github_links(text: str) -> List[int]:
154+
"""Extract GitHub issue/PR numbers from text using common formats:
155+
- #123
156+
- repo#123
157+
- org/repo#123
158+
"""
159+
# Match #123 format
160+
numbers = set()
161+
162+
# Basic #123 format
163+
matches = re.finditer(r"(?:^|\s)#(\d+)(?:\s|$)", text)
164+
numbers.update(int(m.group(1)) for m in matches)
165+
166+
# org/repo#123 or repo#123 format (only care about numbers in current repo)
167+
matches = re.finditer(r"(?:[\w-]+/)?[\w-]+#(\d+)", text)
168+
numbers.update(int(m.group(1)) for m in matches)
169+
170+
return sorted(numbers)
171+
172+
173+
def fetch_linked_items(gh_client: Github, numbers: List[int]) -> List[LinkedItem]:
174+
"""Fetch full context of linked issues/PRs"""
175+
repo = gh_client.get_repo(os.getenv("GITHUB_REPOSITORY"))
176+
linked_items = []
177+
178+
for number in numbers:
179+
try:
180+
issue = repo.get_issue(number)
181+
item_type = "pull_request" if issue.pull_request else "issue"
182+
183+
linked_items.append(
184+
LinkedItem(
185+
number=number,
186+
title=issue.title,
187+
body=issue.body or "",
188+
labels=[label.name for label in issue.labels],
189+
type=item_type,
190+
)
191+
)
192+
except Exception as e:
193+
print(f"Warning: Failed to fetch item #{number}: {e}")
194+
195+
return linked_items

src/ai_labeler/label_workflow.py

+13
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
PullRequest,
1010
Issue,
1111
get_event_number,
12+
parse_github_links,
13+
fetch_linked_items,
1214
)
1315
from ai_labeler.ai import labeling_workflow
1416

@@ -67,17 +69,28 @@ def run_label_workflow(
6769
issue = repo.get_issue(number)
6870
if issue.pull_request:
6971
pr = repo.get_pull(number)
72+
73+
# Parse and fetch linked items
74+
linked_numbers = parse_github_links(pr.body or "")
75+
linked_items = fetch_linked_items(gh, linked_numbers)
76+
7077
item = PullRequest(
7178
title=pr.title,
7279
body=pr.body or "",
7380
files={f.filename: f.patch for f in pr.get_files()},
7481
author=pr.user.login,
82+
linked_items=linked_items,
7583
)
7684
else:
85+
# Parse and fetch linked items
86+
linked_numbers = parse_github_links(issue.body or "")
87+
linked_items = fetch_linked_items(gh, linked_numbers)
88+
7789
item = Issue(
7890
title=issue.title,
7991
body=issue.body or "",
8092
author=issue.user.login,
93+
linked_items=linked_items,
8194
)
8295

8396
# Run the labeling workflow

0 commit comments

Comments
 (0)