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
57 changes: 52 additions & 5 deletions supervisor/errata_utils.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from datetime import datetime, timezone
from enum import StrEnum
from datetime import datetime
from functools import cache
import logging
import os
from typing import overload
from typing_extensions import Literal

from bs4 import BeautifulSoup, Tag # type: ignore
from pydantic import BaseModel
from requests_gssapi import HTTPSPNEGOAuth

from .http_utils import requests_session
from .supervisor_types import Erratum, ErrataStatus
from .supervisor_types import Erratum, FullErratum, ErrataStatus, Comment

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -57,7 +60,15 @@ def ET_get_html(path: str):
return response.text


def get_erratum(erratum_id: str | int):
@overload
def get_erratum(erratum_id: str | int, full: Literal[False] = False) -> Erratum: ...


@overload
def get_erratum(erratum_id: str | int, full: Literal[True]) -> FullErratum: ...


def get_erratum(erratum_id: str | int, full: bool = False) -> Erratum | FullErratum:
logger.debug("Getting detailed information for erratum %s", erratum_id)
data = ET_api_get(f"erratum/{erratum_id}")
erratum_data = data["errata"]
Expand All @@ -82,7 +93,7 @@ def get_erratum(erratum_id: str | int):
details["status_updated_at"], "%Y-%m-%dT%H:%M:%SZ"
).replace(tzinfo=timezone.utc)

return Erratum(
base_erratum = Erratum(
id=details["id"],
full_advisory=details["fulladvisory"],
url=f"https://errata.engineering.redhat.com/advisory/{erratum_id}",
Expand All @@ -92,10 +103,46 @@ def get_erratum(erratum_id: str | int):
last_status_transition_timestamp=last_status_transition_timestamp,
)

if full:
# fetching comments for the erratum
comments = get_erratum_comments(erratum_id)
return FullErratum(
**base_erratum.__dict__,
comments=comments,
)
else:
return base_erratum


def get_erratum_comments(erratum_id: str | int) -> list[Comment] | None:
"""Get all comments for an erratum with the given erratum_id"""
logger.debug("Getting comments for erratum %s", erratum_id)
data = ET_api_get(f"comments?filter[errata_id]={erratum_id}")

return [
Comment(
authorName=comment_data["attributes"]["who"]["realname"],
authorEmail=comment_data["attributes"]["who"]["login_name"],
created=datetime.fromisoformat(
comment_data["attributes"]["created_at"].replace("Z", "+00:00")
),
body=comment_data["attributes"]["text"],
)
for comment_data in data["data"]
]


@overload
def get_erratum_for_link(link: str, full: Literal[False] = False) -> Erratum: ...


@overload
def get_erratum_for_link(link: str, full: Literal[True]) -> FullErratum: ...


def get_erratum_for_link(link: str):
def get_erratum_for_link(link: str, full: bool = True) -> Erratum | FullErratum:
erratum_id = link.split("/")[-1]
return get_erratum(erratum_id)
return get_erratum(erratum_id, full=full)


class RuleParseError(Exception):
Expand Down
2 changes: 1 addition & 1 deletion supervisor/issue_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ async def run(self) -> WorkflowResult:
)
elif issue.status == IssueStatus.INTEGRATION:
related_erratum = (
get_erratum_for_link(issue.errata_link) if issue.errata_link else None
get_erratum_for_link(issue.errata_link, full=True) if issue.errata_link else None
)
testing_analysis = await analyze_issue(issue, related_erratum)
if testing_analysis.state == TestingState.NOT_RUNNING:
Expand Down
4 changes: 2 additions & 2 deletions supervisor/jira_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .supervisor_types import (
FullIssue,
Issue,
IssueComment,
Comment,
IssueStatus,
JotnarTag,
TestCoverage,
Expand Down Expand Up @@ -151,7 +151,7 @@ def custom_enum_list(enum_class: Type[_E], name) -> list[_E] | None:
**issue.__dict__,
description=issue_data["fields"]["description"],
comments=[
IssueComment(
Comment(
authorName=c["author"]["displayName"],
authorEmail=c["author"]["emailAddress"],
created=datetime.fromisoformat(c["created"]),
Expand Down
18 changes: 10 additions & 8 deletions supervisor/supervisor_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class ErrataStatus(StrEnum):
SHIPPED_LIVE = "SHIPPED_LIVE"


class Comment(BaseModel):
authorName: str
authorEmail: str
created: datetime
body: str

class Erratum(BaseModel):
id: int
full_advisory: str
Expand All @@ -50,6 +56,9 @@ class Erratum(BaseModel):
all_issues_release_pending: bool
last_status_transition_timestamp: datetime

class FullErratum(Erratum):
comments: list[Comment] | None = None


class MergeRequestState(StrEnum):
OPEN = "opened"
Expand Down Expand Up @@ -89,16 +98,9 @@ class Issue(BaseModel):
preliminary_testing: PreliminaryTesting | None = None # RHEL only


class IssueComment(BaseModel):
authorName: str
authorEmail: str
created: datetime
body: str


class FullIssue(Issue):
description: str
comments: list[IssueComment]
comments: list[Comment]


class JotnarTag(BaseModel):
Expand Down
15 changes: 10 additions & 5 deletions supervisor/testing_analyst.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime, timezone
import logging
import os
from datetime import datetime, timezone

from pydantic import BaseModel, Field

Expand All @@ -11,8 +11,9 @@

from agents.utils import get_agent_execution_config
from .qe_data import get_qe_data, TestLocationInfo
from .supervisor_types import Erratum, FullIssue, TestingState
from .supervisor_types import FullErratum, FullIssue, TestingState
from .tools.read_readme import ReadReadmeTool
from .tools.read_issue import ReadIssueTool
from .tools.search_resultsdb import SearchResultsdbTool

logger = logging.getLogger(__name__)
Expand All @@ -23,7 +24,7 @@ class InputSchema(BaseModel):
test_location_info: TestLocationInfo = Field(
description="Information about where to find tests and test results"
)
erratum: Erratum | None = Field(description="Details of the related ERRATUM")
erratum: FullErratum | None = Field(description="Details of the related ERRATUM")
current_time: datetime = Field(description="Current timestamp")


Expand All @@ -42,6 +43,10 @@ def render_prompt(input: InputSchema) -> str:
TEST_LOCATION_INFO: {{ test_location_info }}
CURRENT_TIME: {{ current_time }}

For components handled by the New Errata Workflow Automation(NEWA):
NEWA will post a comment to the erratum when it has started tests and when they finish.
Read the JIRA issue in those comments to find test results.

Call the final_answer tool passing in the state and a comment as follows.
The comment should use JIRA comment syntax.

Expand Down Expand Up @@ -70,14 +75,14 @@ def render_prompt(input: InputSchema) -> str:
).render(input)


async def analyze_issue(jira_issue: FullIssue, erratum: Erratum | None) -> OutputSchema:
async def analyze_issue(jira_issue: FullIssue, erratum: FullErratum | None) -> OutputSchema:
agent = ToolCallingAgent(
llm=ChatModel.from_name(
os.environ["CHAT_MODEL"],
allow_parallel_tool_calls=True,
),
memory=UnconstrainedMemory(),
tools=[ReadReadmeTool(), SearchResultsdbTool()],
tools=[ReadReadmeTool(), ReadIssueTool(), SearchResultsdbTool()],
)

async def run(input: InputSchema):
Expand Down
47 changes: 47 additions & 0 deletions supervisor/tools/read_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import logging
from pydantic import BaseModel, Field

from beeai_framework.context import RunContext
from beeai_framework.emitter import Emitter
from beeai_framework.tools import StringToolOutput, Tool, ToolRunOptions

from ..jira_utils import get_issue

logger = logging.getLogger(__name__)


class ReadIssueInput(BaseModel):
issue_key: str = Field(description="JIRA issue key (e.g.: RHELMISC-12345)")


class ReadIssueTool(Tool[ReadIssueInput, ToolRunOptions, StringToolOutput]):
name = "read_issue" # type: ignore
description = "Read JIRA issue by key to get details, comments, and test results" # type: ignore
input_schema = ReadIssueInput # type: ignore

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

async def _run(
self,
input: ReadIssueInput,
options: ToolRunOptions | None,
context: RunContext,
) -> StringToolOutput:
try:
#fetch the issue using jira utils
issue = get_issue(input.issue_key, full=True)

#return formatted issue data
return StringToolOutput(
result=issue.model_dump_json(indent=2)
)

except Exception as e:
logger.error(f"Failed to read JIRA issue {input.issue_key}: {e}")
return StringToolOutput(
result=f"Error: Failed to read JIRA issue {input.issue_key}: {str(e)}"
)