Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@ process-erratum:
$(COMPOSE_SUPERVISOR) run --rm \
supervisor python -m ymir.supervisor.main $(DEBUG_FLAG) $(IGNORE_NEEDS_ATTENTION_FLAG) $(DRY_RUN_FLAG) process-erratum $(ERRATA_ID)

.PHONY: run-preliminary-testing-agent-standalone
run-preliminary-testing-agent-standalone:
$(COMPOSE_AGENTS) run --rm \
-e JIRA_ISSUE=$(JIRA_ISSUE) \
-e DRY_RUN=$(DRY_RUN) \
-e IGNORE_NEEDS_ATTENTION=$(IGNORE_NEEDS_ATTENTION) \
preliminary-testing-agent


# Common utility targets

Expand Down
7 changes: 7 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ services:
command: ["python", "-m", "ymir.agents.triage_agent"]
profiles: ["agents"]

preliminary-testing-agent:
<<: *beeai-agent-c10s
environment:
<<: *beeai-env
command: ["python", "-m", "ymir.agents.preliminary_testing_agent"]
profiles: ["agents"]

triage-agent-e2e-tests:
<<: *beeai-agent-c10s
environment:
Expand Down
445 changes: 445 additions & 0 deletions ymir/agents/preliminary_testing_agent.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions ymir/common/constants.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from enum import Enum

import aiohttp

BREWHUB_URL = "https://brewhub.engineering.redhat.com/brewhub"

AIOHTTP_TIMEOUT = aiohttp.ClientTimeout(total=30)

JIRA_SEARCH_PATH = "rest/api/3/search/jql"

class RedisQueues(Enum):
Expand Down
3 changes: 3 additions & 0 deletions ymir/tools/privileged/aiohttp_client_session_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ async def _write_jira_mock(issue_key: str, data: dict):


class aiohttpClientSessionMock:
def __init__(self, **kwargs):
pass

# mocking endpoint providing information about issue
issue_get_regex = re.compile(
re.escape(urljoin(os.getenv("JIRA_URL"), f"rest/api/3/issue"))+"/([A-Z0-9-]+)")
Expand Down
4 changes: 2 additions & 2 deletions ymir/tools/privileged/copr_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pydantic import BaseModel, Field

from ymir.common import load_rhel_config
from ymir.common.constants import AIOHTTP_TIMEOUT
from ymir.common.utils import init_kerberos_ticket, KerberosError
from ymir.common.validators import AbsolutePath

Expand Down Expand Up @@ -275,8 +276,7 @@ async def _run(
) -> StringToolOutput:
artifacts_urls = tool_input.artifacts_urls
target_path = tool_input.target_path
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with aiohttp.ClientSession(timeout=AIOHTTP_TIMEOUT) as session:
for url in artifacts_urls:
logger.info(f"Downloading build artifact from: {url}")
try:
Expand Down
6 changes: 6 additions & 0 deletions ymir/tools/privileged/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def _redact(text: str) -> str:
AddMergeRequestLabelsTool,
CloneRepositoryTool,
CreateMergeRequestChecklistTool,
FetchGitlabMrNotesTool,
ForkRepositoryTool,
GetAuthorizedCommentsFromMergeRequestTool,
GetFailedPipelineJobsFromMergeRequestTool,
Expand All @@ -61,8 +62,10 @@ def _redact(text: str) -> str:
EditJiraLabelsTool,
GetJiraDetailsTool,
GetJiraDevStatusTool,
GetJiraPullRequestsTool,
SearchJiraIssuesTool,
SetJiraFieldsTool,
SetPreliminaryTestingTool,
VerifyIssueAuthorTool,
)
from ymir.tools.privileged.lookaside_tools import DownloadSourcesTool, PrepSourcesTool, UploadSourcesTool
Expand Down Expand Up @@ -123,14 +126,17 @@ def main():
OpenMergeRequestTool(),
PushToRemoteRepositoryTool(),
RetryPipelineJobTool(),
FetchGitlabMrNotesTool(),
AddJiraCommentTool(),
ChangeJiraStatusTool(),
CheckCveTriageEligibilityTool(),
EditJiraLabelsTool(),
GetJiraDetailsTool(),
GetJiraDevStatusTool(),
GetJiraPullRequestsTool(),
SearchJiraIssuesTool(),
SetJiraFieldsTool(),
SetPreliminaryTestingTool(),
VerifyIssueAuthorTool(),
DownloadSourcesTool(),
PrepSourcesTool(),
Expand Down
87 changes: 85 additions & 2 deletions ymir/tools/privileged/gitlab_tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import json
import logging
import os
import re
Expand All @@ -15,6 +16,7 @@
from ogr.services.gitlab.pull_request import GitlabPullRequest
from pydantic import BaseModel, Field

from ymir.common.constants import AIOHTTP_TIMEOUT
from ymir.common.models import CommentReply, FailedPipelineJob, MergeRequestComment, MergeRequestDetails
from ymir.common.validators import AbsolutePath
from ymir.tools.privileged.utils import clean_stale_repositories
Expand Down Expand Up @@ -923,8 +925,7 @@ async def _run(
headers = _get_auth_headers(request_url)

try:
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with aiohttp.ClientSession(timeout=AIOHTTP_TIMEOUT) as session:
async with session.get(request_url, headers=headers) as response:
if response.status >= 400:
raise ToolError(
Expand All @@ -934,3 +935,85 @@ async def _run(
except aiohttp.ClientError as e:
raise ToolError(f"Failed to fetch patch from {patch_url}: {e}") from e
return StringToolOutput(result=self._truncate(text))


class FetchGitlabMrNotesInput(BaseModel):
project: str = Field(
description="GitLab project path (e.g. 'redhat/centos-stream/rpms/podman')"
)
mr_iid: int = Field(description="Merge request IID within the project")


class FetchGitlabMrNotesTool(
Tool[FetchGitlabMrNotesInput, ToolRunOptions, StringToolOutput]
):
"""
Tool to fetch comments/notes from a GitLab merge request.
This is useful for finding OSCI test results posted as comments
on merge requests with titles like "Results for pipeline ...".
"""

name = "fetch_gitlab_mr_notes" # type: ignore
description = ( # type: ignore
"Fetch comments/notes from a GitLab merge request. "
"Returns JSON with a list of notes including author, body, and creation date. "
"Use this to find OSCI test results posted as comments on merge requests."
)
input_schema = FetchGitlabMrNotesInput # type: ignore

def _create_emitter(self) -> Emitter:
return Emitter.root().child(
namespace=["tool", "gitlab", self.name],
creator=self,
)

async def _run(
self,
input: FetchGitlabMrNotesInput,
options: ToolRunOptions | None,
context: RunContext,
) -> StringToolOutput:
encoded_project = quote(input.project, safe="")
url = (
f"https://gitlab.com/api/v4/projects/{encoded_project}"
f"/merge_requests/{input.mr_iid}/notes"
)
headers = _get_auth_headers(url)
logger.info("Fetching MR notes from %s", url)

try:
async with aiohttp.ClientSession(timeout=AIOHTTP_TIMEOUT) as session:
async with session.get(
url, headers=headers, params={"per_page": "100", "sort": "desc", "order_by": "created_at"}
) as response:
if response.status != 200:
text = await response.text()
logger.error(
"Failed to fetch MR notes (HTTP %d): %s",
response.status,
text,
)
return StringToolOutput(
result=f"Failed to fetch notes for MR !{input.mr_iid} "
f"in {input.project} (HTTP {response.status}): {text}"
)

notes = await response.json()

result = [
{
"author": note.get("author", {}).get("name", "Unknown"),
"body": note["body"],
"created_at": note.get("created_at"),
"system": note.get("system", False),
}
for note in notes
]

return StringToolOutput(result=json.dumps(result, indent=2))

except Exception as e:
logger.error("Error fetching GitLab MR notes: %s", e)
return StringToolOutput(
result=f"Error fetching GitLab MR notes: {e}"
)
Loading
Loading