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
1 change: 1 addition & 0 deletions .github/workflows/check-in-container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
include:
- target: check-agents-in-container
- target: check-mcp-server-in-container
- target: check-jira-issue-fetcher-in-container
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
6 changes: 6 additions & 0 deletions beeai/Containerfile.tests
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ RUN dnf -y install \
python3-pytest \
python3-pytest-asyncio \
python3-specfile \
python3-redis \
python3-requests \
python3-backoff \
&& dnf clean all

RUN git config --global user.email "jotnar-tests@example.com" \
&& git config --global user.name "Jotnar Tests"

# Set PYTHONPATH so agents module can be imported
ENV PYTHONPATH=/src:$PYTHONPATH

# Install BeeAI Framework and FastMCP
RUN pip3 install --no-cache-dir beeai-framework fastmcp redis

Expand Down
2 changes: 2 additions & 0 deletions beeai/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,5 @@ check-agents-in-container:
$(MAKE) -f Makefile.tests check-agents-in-container
check-mcp-server-in-container:
$(MAKE) -f Makefile.tests check-mcp-server-in-container
check-jira-issue-fetcher-in-container:
$(MAKE) -f Makefile.tests check-jira-issue-fetcher-in-container
15 changes: 12 additions & 3 deletions beeai/Makefile.tests
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@ CONTAINER_ENGINE ?= $(shell command -v podman 2>/dev/null || echo "docker")
build-test-image:
$(CONTAINER_ENGINE) build --rm --tag $(TEST_IMAGE) -f Containerfile.tests

.PHONY: check check-agents check-mcp-server check-in-container check-agents-in-container check-mcp-server-in-container
.PHONY: check check-agents check-mcp-server check-in-container \
check-agents-in-container check-mcp-server-in-container check-jira-issue-fetcher-in-container

check-agents:
cd ./agents && \
PYTHONPATH=$(CURDIR) PYTHONDONTWRITEBYTECODE=1 python3 -m pytest --verbose --showlocals $(TEST_TARGET)
check-mcp-server:
cd ./mcp_server && \
PYTHONPATH=$(CURDIR) PYTHONDONTWRITEBYTECODE=1 python3 -m pytest --verbose --showlocals $(TEST_TARGET)
check: check-agents check-mcp-server
check-jira-issue-fetcher:
cd ./jira_issue_fetcher && \
PYTHONPATH=$(CURDIR) PYTHONDONTWRITEBYTECODE=1 python3 -m pytest --verbose --showlocals $(TEST_TARGET)

check: check-agents check-mcp-server check-jira-issue-fetcher

check-agents-in-container:
$(CONTAINER_ENGINE) run --rm -it -v $(CURDIR):/src:z --env TEST_TARGET $(TEST_IMAGE) make -f Makefile.tests check-agents
check-mcp-server-in-container:
$(CONTAINER_ENGINE) run --rm -it -v $(CURDIR):/src:z --env TEST_TARGET $(TEST_IMAGE) make -f Makefile.tests check-mcp-server
check-in-container: check-agents-in-container check-mcp-server-in-container
check-jira-issue-fetcher-in-container:
$(CONTAINER_ENGINE) run --rm -it -v $(CURDIR):/src:z --env TEST_TARGET $(TEST_IMAGE) make -f Makefile.tests check-jira-issue-fetcher

check-in-container: check-agents-in-container check-mcp-server-in-container check-jira-issue-fetcher-in-container
11 changes: 6 additions & 5 deletions beeai/agents/backport_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
from agents.build_agent import create_build_agent, get_prompt as get_build_prompt
from common.models import BackportInputSchema, BackportOutputSchema, BuildInputSchema, BuildOutputSchema, Task
from common.utils import redis_client, fix_await
from constants import I_AM_JOTNAR, CAREFULLY_REVIEW_CHANGES, JiraLabels
from constants import I_AM_JOTNAR, CAREFULLY_REVIEW_CHANGES
from common.constants import JiraLabels, RedisQueues
from observability import setup_observability
from tools.commands import RunShellCommandTool
from tools.specfile import AddChangelogEntryTool, BumpReleaseTool
Expand Down Expand Up @@ -346,7 +347,7 @@ async def comment_in_jira(state):

while True:
logger.info("Waiting for tasks from backport_queue (timeout: 30s)...")
element = await fix_await(redis.brpop(["backport_queue"], timeout=30))
element = await fix_await(redis.brpop([RedisQueues.BACKPORT_QUEUE.value], timeout=30))
if element is None:
logger.info("No tasks received, continuing to wait...")
continue
Expand All @@ -371,7 +372,7 @@ async def retry(task, error):
f"Task failed (attempt {task.attempts}/{max_retries}), "
f"re-queuing for retry: {backport_data.jira_issue}"
)
await fix_await(redis.lpush("backport_queue", task.model_dump_json()))
await fix_await(redis.lpush(RedisQueues.BACKPORT_QUEUE.value, task.model_dump_json()))
else:
logger.error(
f"Task failed after {max_retries} attempts, "
Expand All @@ -383,7 +384,7 @@ async def retry(task, error):
labels_to_remove=[JiraLabels.BACKPORT_IN_PROGRESS.value],
dry_run=dry_run
)
await fix_await(redis.lpush("error_list", error))
await fix_await(redis.lpush(RedisQueues.ERROR_LIST.value, error))

try:
logger.info(f"Starting backport processing for {backport_data.jira_issue}")
Expand Down Expand Up @@ -411,7 +412,7 @@ async def retry(task, error):
labels_to_remove=[JiraLabels.BACKPORT_IN_PROGRESS.value],
dry_run=dry_run
)
await redis.lpush("completed_backport_list", state.backport_result.model_dump_json())
await redis.lpush(RedisQueues.COMPLETED_BACKPORT_LIST.value, state.backport_result.model_dump_json())
else:
logger.warning(f"Backport failed for {backport_data.jira_issue}: {state.backport_result.error}")
await tasks.set_jira_labels(
Expand Down
24 changes: 0 additions & 24 deletions beeai/agents/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,3 @@

I_AM_JOTNAR = "by Jotnar, a Red Hat Enterprise Linux software maintenance AI agent."
CAREFULLY_REVIEW_CHANGES = "Carefully review the changes and make sure they are correct."

class JiraLabels(Enum):
"""Constants for Jira labels used by Jotnar agents"""
REBASE_IN_PROGRESS = "jotnar_rebase_in_progress"
BACKPORT_IN_PROGRESS = "jotnar_backport_in_progress"
NEEDS_ATTENTION = "jotnar_needs_attention"
NO_ACTION_NEEDED = "jotnar_no_action_needed"

REBASED = "jotnar_rebased"
BACKPORTED = "jotnar_backported"

REBASE_ERRORED = "jotnar_rebase_errored"
BACKPORT_ERRORED = "jotnar_backport_errored"
TRIAGE_ERRORED = "jotnar_triage_errored"

REBASE_FAILED = "jotnar_rebase_failed"
BACKPORT_FAILED = "jotnar_backport_failed"

RETRY_NEEDED = "jotnar_retry_needed"

@classmethod
def all_labels(cls) -> set[str]:
"""Return all Jotnar labels for cleanup operations"""
return {label.value for label in cls}
11 changes: 6 additions & 5 deletions beeai/agents/rebase_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
from common.config import get_package_instructions
from common.models import BuildInputSchema, BuildOutputSchema, RebaseInputSchema, RebaseOutputSchema, Task
from common.utils import redis_client, fix_await
from constants import I_AM_JOTNAR, CAREFULLY_REVIEW_CHANGES, JiraLabels
from constants import I_AM_JOTNAR, CAREFULLY_REVIEW_CHANGES
from common.constants import JiraLabels, RedisQueues
from observability import setup_observability
from tools.commands import RunShellCommandTool
from tools.specfile import AddChangelogEntryTool
Expand Down Expand Up @@ -343,7 +344,7 @@ async def comment_in_jira(state):

while True:
logger.info("Waiting for tasks from rebase_queue (timeout: 30s)...")
element = await fix_await(redis.brpop(["rebase_queue"], timeout=30))
element = await fix_await(redis.brpop([RedisQueues.REBASE_QUEUE.value], timeout=30))
if element is None:
logger.info("No tasks received, continuing to wait...")
continue
Expand All @@ -368,7 +369,7 @@ async def retry(task, error):
f"Task failed (attempt {task.attempts}/{max_retries}), "
f"re-queuing for retry: {rebase_data.jira_issue}"
)
await fix_await(redis.lpush("rebase_queue", task.model_dump_json()))
await fix_await(redis.lpush(RedisQueues.REBASE_QUEUE.value, task.model_dump_json()))
else:
logger.error(
f"Task failed after {max_retries} attempts, "
Expand All @@ -380,7 +381,7 @@ async def retry(task, error):
labels_to_remove=[JiraLabels.REBASE_IN_PROGRESS.value],
dry_run=dry_run
)
await fix_await(redis.lpush("error_list", error))
await fix_await(redis.lpush(RedisQueues.ERROR_LIST.value, error))

try:
logger.info(f"Starting rebase processing for {rebase_data.jira_issue}")
Expand All @@ -407,7 +408,7 @@ async def retry(task, error):
labels_to_remove=[JiraLabels.REBASE_IN_PROGRESS.value],
dry_run=dry_run
)
await fix_await(redis.lpush("completed_rebase_list", state.rebase_result.model_dump_json()))
await fix_await(redis.lpush(RedisQueues.COMPLETED_REBASE_LIST.value, state.rebase_result.model_dump_json()))
else:
logger.warning(f"Rebase failed for {rebase_data.jira_issue}: {state.rebase_result.error}")
await tasks.set_jira_labels(
Expand Down
2 changes: 1 addition & 1 deletion beeai/agents/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from beeai_framework.tools import Tool

from common.utils import is_cs_branch
from constants import BRANCH_PREFIX, JIRA_COMMENT_TEMPLATE, JiraLabels
from constants import BRANCH_PREFIX, JIRA_COMMENT_TEMPLATE
from utils import check_subprocess, run_subprocess, run_tool, mcp_tools

logger = logging.getLogger(__name__)
Expand Down
16 changes: 8 additions & 8 deletions beeai/agents/triage_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
CVEEligibilityResult,
)
from common.utils import redis_client, fix_await
from constants import JiraLabels
from common.constants import JiraLabels, RedisQueues
from observability import setup_observability
from tools.commands import RunShellCommandTool
from tools.patch_validator import PatchValidatorTool
Expand Down Expand Up @@ -451,7 +451,7 @@ async def comment_in_jira(state):

while True:
logger.info("Waiting for tasks from triage_queue (timeout: 30s)...")
element = await fix_await(redis.brpop(["triage_queue"], timeout=30))
element = await fix_await(redis.brpop([RedisQueues.TRIAGE_QUEUE.value], timeout=30))
if element is None:
logger.info("No tasks received, continuing to wait...")
continue
Expand All @@ -470,7 +470,7 @@ async def retry(task, error):
f"Task failed (attempt {task.attempts}/{max_retries}), "
f"re-queuing for retry: {input.issue}"
)
await fix_await(redis.lpush("triage_queue", task.model_dump_json()))
await fix_await(redis.lpush(RedisQueues.TRIAGE_QUEUE.value, task.model_dump_json()))
else:
logger.error(
f"Task failed after {max_retries} attempts, " f"moving to error list: {input.issue}"
Expand All @@ -480,7 +480,7 @@ async def retry(task, error):
labels_to_add=[JiraLabels.TRIAGE_ERRORED.value],
dry_run=dry_run
)
await fix_await(redis.lpush("error_list", error))
await fix_await(redis.lpush(RedisQueues.ERROR_LIST.value, error))

try:
await tasks.set_jira_labels(
Expand Down Expand Up @@ -514,7 +514,7 @@ async def retry(task, error):
dry_run=dry_run
)
task = Task(metadata=state.model_dump())
await redis.lpush("rebase_queue", task.model_dump_json())
await redis.lpush(RedisQueues.REBASE_QUEUE.value, task.model_dump_json())
elif output.resolution == Resolution.BACKPORT:
logger.info(f"Triage resolved as BACKPORT for {input.issue}, " f"adding to backport queue")
await tasks.set_jira_labels(
Expand All @@ -523,7 +523,7 @@ async def retry(task, error):
dry_run=dry_run
)
task = Task(metadata=state.model_dump())
await redis.lpush("backport_queue", task.model_dump_json())
await redis.lpush(RedisQueues.BACKPORT_QUEUE.value, task.model_dump_json())
elif output.resolution == Resolution.CLARIFICATION_NEEDED:
logger.info(
f"Triage resolved as CLARIFICATION_NEEDED for {input.issue}, "
Expand All @@ -535,15 +535,15 @@ async def retry(task, error):
dry_run=dry_run
)
task = Task(metadata=state.model_dump())
await redis.lpush("clarification_needed_queue", task.model_dump_json())
await redis.lpush(RedisQueues.CLARIFICATION_NEEDED_QUEUE.value, task.model_dump_json())
elif output.resolution == Resolution.NO_ACTION:
logger.info(f"Triage resolved as NO_ACTION for {input.issue}, " f"adding to no action list")
await tasks.set_jira_labels(
jira_issue=input.issue,
labels_to_add=[JiraLabels.NO_ACTION_NEEDED.value],
dry_run=dry_run
)
await fix_await(redis.lpush("no_action_list", output.data.model_dump_json()))
await fix_await(redis.lpush(RedisQueues.NO_ACTION_LIST.value, output.data.model_dump_json()))
elif output.resolution == Resolution.ERROR:
logger.warning(f"Triage resolved as ERROR for {input.issue}, retrying")
await tasks.set_jira_labels(
Expand Down
54 changes: 54 additions & 0 deletions beeai/common/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from enum import Enum

class RedisQueues(Enum):
"""Constants for Redis queue names used by Jotnar agents"""
TRIAGE_QUEUE = "triage_queue"
REBASE_QUEUE = "rebase_queue"
BACKPORT_QUEUE = "backport_queue"
CLARIFICATION_NEEDED_QUEUE = "clarification_needed_queue"
ERROR_LIST = "error_list"
NO_ACTION_LIST = "no_action_list"
COMPLETED_REBASE_LIST = "completed_rebase_list"
COMPLETED_BACKPORT_LIST = "completed_backport_list"

@classmethod
def all_queues(cls) -> set[str]:
"""Return all Redis queue names for operations that need to check all queues"""
return {queue.value for queue in cls}

@classmethod
def input_queues(cls) -> set[str]:
"""Return input queue names that contain Task objects with metadata"""
return {cls.TRIAGE_QUEUE.value, cls.REBASE_QUEUE.value, cls.BACKPORT_QUEUE.value, cls.CLARIFICATION_NEEDED_QUEUE.value}

@classmethod
def data_queues(cls) -> set[str]:
"""Return data queue names that contain schema objects"""
return {cls.ERROR_LIST.value,
cls.NO_ACTION_LIST.value, cls.COMPLETED_REBASE_LIST.value,
cls.COMPLETED_BACKPORT_LIST.value}


class JiraLabels(Enum):
"""Constants for Jira labels used by Jotnar agents"""
REBASE_IN_PROGRESS = "jotnar_rebase_in_progress"
BACKPORT_IN_PROGRESS = "jotnar_backport_in_progress"
NEEDS_ATTENTION = "jotnar_needs_attention"
NO_ACTION_NEEDED = "jotnar_no_action_needed"

REBASED = "jotnar_rebased"
BACKPORTED = "jotnar_backported"

REBASE_ERRORED = "jotnar_rebase_errored"
BACKPORT_ERRORED = "jotnar_backport_errored"
TRIAGE_ERRORED = "jotnar_triage_errored"

REBASE_FAILED = "jotnar_rebase_failed"
BACKPORT_FAILED = "jotnar_backport_failed"

RETRY_NEEDED = "jotnar_retry_needed"

@classmethod
def all_labels(cls) -> set[str]:
"""Return all Jotnar labels for cleanup operations"""
return {label.value for label in cls}
Loading
Loading