Skip to content

Commit

Permalink
[Feat]: Update suggested tasks to show user's PRs and issues across a…
Browse files Browse the repository at this point in the history
…ll repos
  • Loading branch information
openhands-agent committed Feb 24, 2025
1 parent eb6eb8b commit 22319d0
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 65 deletions.
113 changes: 57 additions & 56 deletions openhands/integrations/github/github_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,27 @@ async def execute_graphql_query(self, query: str, variables: dict[str, Any]) ->
except httpx.HTTPError:
raise GHUnknownException('Unknown error')

async def get_suggested_tasks(self, repo_full_name: str) -> list[SuggestedTask]:
async def get_suggested_tasks(self, repo_full_name: str | None = None) -> list[SuggestedTask]:
"""
Get suggested tasks for a repository, including:
- PRs with merge conflicts
- PRs with failing GitHub Actions
- PRs with unresolved comments
- Issues with specific labels (help wanted, chore, documentation, good first issue)
Get suggested tasks for the authenticated user across all repositories.
Returns:
- PRs authored by the user
- Issues assigned to the user
"""
# Get user info to use in queries
user = await self.get_user()
login = user.login

query = """
query GetSuggestedTasks($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
pullRequests(first: 100, states: [OPEN]) {
query GetUserTasks($login: String!) {
user(login: $login) {
pullRequests(first: 100, states: [OPEN], orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
number
title
repository {
nameWithOwner
}
mergeable
commits(last: 1) {
nodes {
Expand All @@ -197,75 +203,70 @@ async def get_suggested_tasks(self, repo_full_name: str) -> list[SuggestedTask]:
}
}
}
issues(first: 100, states: [OPEN], labels: ["help wanted", "chore", "documentation", "good first issue"]) {
issues(first: 100, states: [OPEN], filterBy: {assignee: $login}, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
number
title
labels(first: 10) {
nodes {
name
}
repository {
nameWithOwner
}
}
}
}
}
"""

owner, name = repo_full_name.split("/")
variables = {
"owner": owner,
"name": name
"login": login
}

response = await self.execute_graphql_query(query, variables)
data = response["data"]["repository"]
tasks: list[SuggestedTask] = []
try:
response = await self.execute_graphql_query(query, variables)
print(f"\nGraphQL Response:")
print(response)

data = response["data"]["user"]
tasks: list[SuggestedTask] = []

# Process pull requests
for pr in data["pullRequests"]["nodes"]:
repo_name = pr["repository"]["nameWithOwner"]

# Always add open PRs
task_type = TaskType.OPEN_PR

# Check for specific states
if pr["mergeable"] == "CONFLICTING":
task_type = TaskType.MERGE_CONFLICTS
elif (pr["commits"]["nodes"] and
pr["commits"]["nodes"][0]["commit"]["statusCheckRollup"] and
pr["commits"]["nodes"][0]["commit"]["statusCheckRollup"]["state"] == "FAILURE"):
task_type = TaskType.FAILING_CHECKS
elif any(review["state"] in ["CHANGES_REQUESTED", "COMMENTED"]
for review in pr["reviews"]["nodes"]):
task_type = TaskType.UNRESOLVED_COMMENTS

# Process pull requests
for pr in data["pullRequests"]["nodes"]:
# Check for merge conflicts
if pr["mergeable"] == "CONFLICTING":
tasks.append(SuggestedTask(
task_type=TaskType.MERGE_CONFLICTS,
repo=repo_full_name,
task_type=task_type,
repo=repo_name,
issue_number=pr["number"],
title=pr["title"]
))

# Check for failing checks
if pr["commits"]["nodes"] and pr["commits"]["nodes"][0]["commit"]["statusCheckRollup"]:
if pr["commits"]["nodes"][0]["commit"]["statusCheckRollup"]["state"] == "FAILURE":
tasks.append(SuggestedTask(
task_type=TaskType.FAILING_CHECKS,
repo=repo_full_name,
issue_number=pr["number"],
title=pr["title"]
))

# Check for unresolved comments
has_unresolved_comments = any(
review["state"] in ["CHANGES_REQUESTED", "COMMENTED"]
for review in pr["reviews"]["nodes"]
)
if has_unresolved_comments:
# Process issues
for issue in data["issues"]["nodes"]:
repo_name = issue["repository"]["nameWithOwner"]
tasks.append(SuggestedTask(
task_type=TaskType.UNRESOLVED_COMMENTS,
repo=repo_full_name,
issue_number=pr["number"],
title=pr["title"]
task_type=TaskType.OPEN_ISSUE,
repo=repo_name,
issue_number=issue["number"],
title=issue["title"]
))

# Process issues
for issue in data["issues"]["nodes"]:
tasks.append(SuggestedTask(
task_type=TaskType.OPEN_ISSUE,
repo=repo_full_name,
issue_number=issue["number"],
title=issue["title"]
))

return tasks
return tasks
except Exception as e:
print(f"Error: {str(e)}")
return []


github_service_cls = os.environ.get(
Expand Down
1 change: 1 addition & 0 deletions openhands/integrations/github/github_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class TaskType(str, Enum):
FAILING_CHECKS = "FAILING_CHECKS"
UNRESOLVED_COMMENTS = "UNRESOLVED_COMMENTS"
OPEN_ISSUE = "OPEN_ISSUE"
OPEN_PR = "OPEN_PR"


class SuggestedTask(BaseModel):
Expand Down
16 changes: 7 additions & 9 deletions openhands/server/routes/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,20 @@ async def search_github_repositories(
)


@app.get('/repository/suggested-tasks')
async def get_repository_suggested_tasks(
repository: str,
@app.get('/suggested-tasks')
async def get_suggested_tasks(
github_user_id: str | None = Depends(get_user_id),
github_user_token: SecretStr | None = Depends(get_github_token),
):
"""
Get suggested tasks for a repository, including:
- PRs with merge conflicts
- PRs with failing GitHub Actions
- PRs with unresolved comments
- Issues with specific labels (help wanted, chore, documentation, good first issue)
Get suggested tasks for the authenticated user across their most recently pushed repositories.
Returns:
- PRs owned by the user
- Issues assigned to the user
"""
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
try:
tasks: list[SuggestedTask] = await client.get_suggested_tasks(repository)
tasks: list[SuggestedTask] = await client.get_suggested_tasks()
return tasks

except GhAuthenticationError as e:
Expand Down
136 changes: 136 additions & 0 deletions tests/unit/test_suggested_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import pytest
from unittest.mock import AsyncMock, patch

from openhands.integrations.github.github_service import GitHubService
from openhands.integrations.github.github_types import GitHubUser, GitHubRepository, TaskType

@pytest.mark.asyncio
async def test_get_suggested_tasks():
# Mock responses
mock_user = GitHubUser(
id=1,
login="test-user",
avatar_url="https://example.com/avatar.jpg",
name="Test User"
)

mock_repos = [
GitHubRepository(
id=1,
full_name="test-org/repo-1",
stargazers_count=10
),
GitHubRepository(
id=2,
full_name="test-user/repo-2",
stargazers_count=5
)
]

# Mock GraphQL response for each repository
mock_graphql_responses = [
{
"data": {
"repository": {
"pullRequests": {
"nodes": [
{
"number": 1,
"title": "PR with conflicts",
"mergeable": "CONFLICTING",
"commits": {
"nodes": [{"commit": {"statusCheckRollup": None}}]
},
"reviews": {"nodes": []}
},
{
"number": 2,
"title": "PR with failing checks",
"mergeable": "MERGEABLE",
"commits": {
"nodes": [{"commit": {"statusCheckRollup": {"state": "FAILURE"}}}]
},
"reviews": {"nodes": []}
}
]
},
"issues": {
"nodes": [
{
"number": 3,
"title": "Assigned issue 1"
}
]
}
}
}
},
{
"data": {
"repository": {
"pullRequests": {
"nodes": [
{
"number": 4,
"title": "PR with comments",
"mergeable": "MERGEABLE",
"commits": {
"nodes": [{"commit": {"statusCheckRollup": {"state": "SUCCESS"}}}]
},
"reviews": {
"nodes": [{"state": "CHANGES_REQUESTED"}]
}
}
]
},
"issues": {
"nodes": [
{
"number": 5,
"title": "Assigned issue 2"
}
]
}
}
}
}
]

# Create service instance with mocked methods
service = GitHubService()
service.get_user = AsyncMock(return_value=mock_user)
service.get_repositories = AsyncMock(return_value=mock_repos)
service.execute_graphql_query = AsyncMock()
service.execute_graphql_query.side_effect = mock_graphql_responses

# Call the function
tasks = await service.get_suggested_tasks()

# Verify the results
assert len(tasks) == 5 # Should have 5 tasks total

# Verify each task type is present
task_types = [task.task_type for task in tasks]
assert TaskType.MERGE_CONFLICTS in task_types
assert TaskType.FAILING_CHECKS in task_types
assert TaskType.UNRESOLVED_COMMENTS in task_types
assert TaskType.OPEN_ISSUE in task_types
assert len([t for t in task_types if t == TaskType.OPEN_ISSUE]) == 2 # Should have 2 open issues

# Verify repositories are correct
repos = {task.repo for task in tasks}
assert "test-org/repo-1" in repos
assert "test-user/repo-2" in repos

# Verify specific tasks
conflict_pr = next(t for t in tasks if t.task_type == TaskType.MERGE_CONFLICTS)
assert conflict_pr.issue_number == 1
assert conflict_pr.title == "PR with conflicts"

failing_pr = next(t for t in tasks if t.task_type == TaskType.FAILING_CHECKS)
assert failing_pr.issue_number == 2
assert failing_pr.title == "PR with failing checks"

commented_pr = next(t for t in tasks if t.task_type == TaskType.UNRESOLVED_COMMENTS)
assert commented_pr.issue_number == 4
assert commented_pr.title == "PR with comments"

0 comments on commit 22319d0

Please sign in to comment.