refactor: remove GiteaTools facade, use focused tool classes directly
- Removed gitea/tools/gitea_tools.py (useless facade layer) - Removed tests/test_gitea_tools.py (tests for removed facade) - Updated core/dispatcher.py to use IssueTools, PRTools, FileTools, GitTools directly - Updated core/orchestrator.py to use individual tool instances - Updated main.py to create individual tool instances This eliminates the triple layer of indirection (Issue 2.2 from bad_code.md) where GiteaTools just delegated to IssueTools/PRTools/etc with zero added value.
This commit is contained in:
+422
-165
@@ -13,23 +13,33 @@ from core.coordinator_agent import CoordinatorAgent, CoordinatorNoToolCalledErro
|
||||
|
||||
from gitea.tools.coding_tools import CodingTools
|
||||
from gitea.tools.research_tools import ResearchTools
|
||||
from gitea.tools.gitea_tools import GiteaTools
|
||||
from gitea.tools.issue_tools import IssueTools
|
||||
from gitea.tools.pr_tools import PRTools
|
||||
from gitea.tools.file_tools import FileTools
|
||||
from gitea.tools.git_tools import GitTools
|
||||
from gitea.client import GiteaClient
|
||||
from core.coordinator_tools import CoordinatorTools
|
||||
from gitea.workspace import WorkspaceManager
|
||||
from gitea.config import AGENT_MODEL_ID
|
||||
from gitea.models import CommentModel, PullRequestFileModel, PullRequestModel, IssueModel
|
||||
from gitea.models import (
|
||||
CommentModel,
|
||||
PullRequestFileModel,
|
||||
PullRequestModel,
|
||||
IssueModel,
|
||||
)
|
||||
|
||||
logger: logging.Logger = logging.getLogger("agent-dispatcher")
|
||||
|
||||
|
||||
CLOSE_KEYWORDS_PATTERN: re.Pattern[str] = re.compile(
|
||||
rf"\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)\b",
|
||||
re.IGNORECASE
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _find_pr_for_issue_helper(client: GiteaClient, repo_full_name: str, issue_number: int) -> PullRequestModel | None:
|
||||
def _find_pr_for_issue_helper(
|
||||
client: GiteaClient, repo_full_name: str, issue_number: int
|
||||
) -> PullRequestModel | None:
|
||||
"""Find an open pull request that addresses the given issue number."""
|
||||
owner, repo_name = repo_full_name.split("/")
|
||||
try:
|
||||
@@ -40,14 +50,18 @@ def _find_pr_for_issue_helper(client: GiteaClient, repo_full_name: str, issue_nu
|
||||
return pr
|
||||
body = pr.body or ""
|
||||
title = pr.title or ""
|
||||
matches = CLOSE_KEYWORDS_PATTERN.findall(body) + CLOSE_KEYWORDS_PATTERN.findall(title)
|
||||
matches = CLOSE_KEYWORDS_PATTERN.findall(
|
||||
body
|
||||
) + CLOSE_KEYWORDS_PATTERN.findall(title)
|
||||
if any(int(m) == issue_number for m in matches):
|
||||
return pr
|
||||
issue_ref_pattern = re.compile(rf"(?<!\w)#{issue_number}\b")
|
||||
if issue_ref_pattern.search(title) or issue_ref_pattern.search(body):
|
||||
return pr
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking PRs for issue #{issue_number} in {repo_full_name}: {e}")
|
||||
logger.warning(
|
||||
f"Error checking PRs for issue #{issue_number} in {repo_full_name}: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -77,7 +91,7 @@ def _is_awaiting_reply_helper(comments: list[CommentModel], ai_username: str) ->
|
||||
if "<!-- agent:awaiting-reply -->" not in body:
|
||||
return False
|
||||
# Check if any human replied AFTER the last agent comment
|
||||
for c in comments[last_agent_idx + 1:]:
|
||||
for c in comments[last_agent_idx + 1 :]:
|
||||
if c.user and c.user.login not in agent_usernames:
|
||||
return False # Human replied — we can proceed
|
||||
return True # Agent signalled wait, no human replied yet
|
||||
@@ -89,14 +103,20 @@ class TaskProcessor(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
client: GiteaClient,
|
||||
tools: GiteaTools,
|
||||
issue_tools: IssueTools,
|
||||
pr_tools: PRTools,
|
||||
file_tools: FileTools,
|
||||
git_tools: GitTools,
|
||||
model_name: str,
|
||||
repo: str,
|
||||
item: WorkItem,
|
||||
ai_username: str,
|
||||
) -> None:
|
||||
self.client = client
|
||||
self.tools = tools
|
||||
self.issue_tools = issue_tools
|
||||
self.pr_tools = pr_tools
|
||||
self.file_tools = file_tools
|
||||
self.git_tools = git_tools
|
||||
self.model_name = model_name
|
||||
self.repo = repo
|
||||
self.item = item
|
||||
@@ -109,15 +129,15 @@ class TaskProcessor(ABC):
|
||||
self.research_tools = ResearchTools()
|
||||
|
||||
self.planning_tools: list[Callable[..., Any]] = [
|
||||
self.tools.get_issue,
|
||||
self.tools.get_pull_request,
|
||||
self.tools.list_issues,
|
||||
self.tools.list_pull_requests,
|
||||
self.tools.get_file_content,
|
||||
self.tools.get_issue_comments,
|
||||
self.tools.get_pull_request_comments,
|
||||
self.tools.get_pull_request_diff,
|
||||
self.tools.get_pull_request_patch,
|
||||
self.issue_tools.get_issue,
|
||||
self.pr_tools.get_pull_request,
|
||||
self.issue_tools.list_issues,
|
||||
self.pr_tools.list_pull_requests,
|
||||
self.file_tools.get_file_content,
|
||||
self.issue_tools.get_issue_comments,
|
||||
self.pr_tools.get_pull_request_comments,
|
||||
self.pr_tools.get_pull_request_diff,
|
||||
self.pr_tools.get_pull_request_patch,
|
||||
self.coding_tools.list_files,
|
||||
self.coding_tools.read_file,
|
||||
self.coding_tools.grep_search,
|
||||
@@ -128,30 +148,30 @@ class TaskProcessor(ABC):
|
||||
]
|
||||
|
||||
self.coding_tools_list: list[Callable[..., Any]] = [
|
||||
self.tools.get_issue,
|
||||
self.tools.get_pull_request,
|
||||
self.tools.list_issues,
|
||||
self.tools.list_pull_requests,
|
||||
self.tools.get_file_content,
|
||||
self.tools.create_pull_request,
|
||||
self.tools.update_pull_request,
|
||||
self.tools.add_label_to_issue,
|
||||
self.tools.add_label_to_pr,
|
||||
self.tools.create_branch,
|
||||
self.tools.commit_file,
|
||||
self.tools.create_issue,
|
||||
self.tools.add_comment_to_issue,
|
||||
self.tools.close_issue,
|
||||
self.tools.close_pull_request,
|
||||
self.tools.get_issue_comments,
|
||||
self.tools.get_pull_request_comments,
|
||||
self.tools.add_comment,
|
||||
self.tools.add_label,
|
||||
self.tools.update_file,
|
||||
self.tools.get_pull_request_diff,
|
||||
self.tools.get_pull_request_patch,
|
||||
self.tools.approve_pull_request,
|
||||
self.tools.request_changes,
|
||||
self.issue_tools.get_issue,
|
||||
self.pr_tools.get_pull_request,
|
||||
self.issue_tools.list_issues,
|
||||
self.pr_tools.list_pull_requests,
|
||||
self.file_tools.get_file_content,
|
||||
self.pr_tools.create_pull_request,
|
||||
self.pr_tools.update_pull_request,
|
||||
self.issue_tools.add_label_to_issue,
|
||||
self.pr_tools.add_label_to_pr,
|
||||
self.git_tools.create_branch,
|
||||
self.file_tools.commit_file,
|
||||
self.issue_tools.create_issue,
|
||||
self.issue_tools.add_comment_to_issue,
|
||||
self.issue_tools.close_issue,
|
||||
self.pr_tools.close_pull_request,
|
||||
self.issue_tools.get_issue_comments,
|
||||
self.pr_tools.get_pull_request_comments,
|
||||
self.issue_tools.add_comment,
|
||||
self.issue_tools.add_label,
|
||||
self.file_tools.update_file,
|
||||
self.pr_tools.get_pull_request_diff,
|
||||
self.pr_tools.get_pull_request_patch,
|
||||
self.pr_tools.approve_pull_request,
|
||||
self.pr_tools.request_changes,
|
||||
self.coding_tools.list_files,
|
||||
self.coding_tools.read_file,
|
||||
self.coding_tools.write_file,
|
||||
@@ -177,26 +197,40 @@ class PRTaskProcessor(TaskProcessor):
|
||||
pr_details = pr_info.model_dump_json(indent=2)
|
||||
pr_diff = ""
|
||||
try:
|
||||
pr_diff = self.client.get_pull_request_diff(self.owner, self.repo_name, pr_number)
|
||||
pr_diff = self.client.get_pull_request_diff(
|
||||
self.owner, self.repo_name, pr_number
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch PR diff for #{pr_number}: {e}")
|
||||
pr_diff = f"Error fetching diff: {e}"
|
||||
|
||||
pr_files: list[PullRequestFileModel] = []
|
||||
try:
|
||||
pr_files = self.client.get_pull_request_files(self.owner, self.repo_name, pr_number)
|
||||
pr_files = self.client.get_pull_request_files(
|
||||
self.owner, self.repo_name, pr_number
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching files for PR #{pr_number}: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Error fetching files for PR #{pr_number}: {e}", exc_info=True
|
||||
)
|
||||
|
||||
files_summary = "\n".join([f"- {f.filename}" for f in pr_files]) if pr_files else "No files available."
|
||||
files_summary = (
|
||||
"\n".join([f"- {f.filename}" for f in pr_files])
|
||||
if pr_files
|
||||
else "No files available."
|
||||
)
|
||||
|
||||
comments: list[CommentModel] = []
|
||||
try:
|
||||
comments = self.client.get_pull_request_comments(self.owner, self.repo_name, pr_number)
|
||||
comments = self.client.get_pull_request_comments(
|
||||
self.owner, self.repo_name, pr_number
|
||||
)
|
||||
if not isinstance(comments, list):
|
||||
comments = []
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching comments for PR #{pr_number}: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Error fetching comments for PR #{pr_number}: {e}", exc_info=True
|
||||
)
|
||||
|
||||
reviews: list[dict[str, Any]] = []
|
||||
try:
|
||||
@@ -204,28 +238,35 @@ class PRTaskProcessor(TaskProcessor):
|
||||
if not isinstance(reviews, list):
|
||||
reviews = []
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching reviews for PR #{pr_number}: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Error fetching reviews for PR #{pr_number}: {e}", exc_info=True
|
||||
)
|
||||
|
||||
timeline: list[dict[str, Any]] = []
|
||||
for c in comments:
|
||||
timeline.append({
|
||||
"timestamp": c.created_at or "",
|
||||
"user": c.user.login,
|
||||
"type": "comment",
|
||||
"body": c.body,
|
||||
"by_ai": "Reviewed by AI Agent" in c.body or c.user.login == self.ai_username
|
||||
})
|
||||
timeline.append(
|
||||
{
|
||||
"timestamp": c.created_at or "",
|
||||
"user": c.user.login,
|
||||
"type": "comment",
|
||||
"body": c.body,
|
||||
"by_ai": "Reviewed by AI Agent" in c.body
|
||||
or c.user.login == self.ai_username,
|
||||
}
|
||||
)
|
||||
for r in reviews:
|
||||
r_user = (r.get("user") or {}).get("login", "unknown")
|
||||
r_body = r.get("body", "")
|
||||
r_state = r.get("state", "")
|
||||
timeline.append({
|
||||
"timestamp": r.get("submitted_at") or r.get("updated_at") or "",
|
||||
"user": r_user,
|
||||
"type": "review",
|
||||
"body": f"[{r_state}] {r_body}",
|
||||
"by_ai": r_user == self.ai_username
|
||||
})
|
||||
timeline.append(
|
||||
{
|
||||
"timestamp": r.get("submitted_at") or r.get("updated_at") or "",
|
||||
"user": r_user,
|
||||
"type": "review",
|
||||
"body": f"[{r_state}] {r_body}",
|
||||
"by_ai": r_user == self.ai_username,
|
||||
}
|
||||
)
|
||||
|
||||
timeline.sort(key=lambda x: x["timestamp"])
|
||||
|
||||
@@ -237,15 +278,24 @@ class PRTaskProcessor(TaskProcessor):
|
||||
logger.info(f"PR #{pr_number} already addressed by AI. Skipping.")
|
||||
return f"SKIP: PR #{pr_number} has already been addressed by AI. No new action needed."
|
||||
|
||||
comments_str = "\n".join([
|
||||
f"- @{c.user.login} ({c.created_at}): {c.body}"
|
||||
for c in comments
|
||||
]) if comments else "No comments yet."
|
||||
comments_str = (
|
||||
"\n".join(
|
||||
[f"- @{c.user.login} ({c.created_at}): {c.body}" for c in comments]
|
||||
)
|
||||
if comments
|
||||
else "No comments yet."
|
||||
)
|
||||
|
||||
reviews_str = "\n".join([
|
||||
f"- @{(r.get('user') or {}).get('login')} ({r.get('submitted_at')}): [{r.get('state')}] {r.get('body')}"
|
||||
for r in reviews
|
||||
]) if reviews else "No reviews yet."
|
||||
reviews_str = (
|
||||
"\n".join(
|
||||
[
|
||||
f"- @{(r.get('user') or {}).get('login')} ({r.get('submitted_at')}): [{r.get('state')}] {r.get('body')}"
|
||||
for r in reviews
|
||||
]
|
||||
)
|
||||
if reviews
|
||||
else "No reviews yet."
|
||||
)
|
||||
|
||||
connected_issues_ctx = ""
|
||||
pr_body = pr_info.body or ""
|
||||
@@ -255,11 +305,19 @@ class PRTaskProcessor(TaskProcessor):
|
||||
for issue_num in linked_issues:
|
||||
try:
|
||||
issue = self.client.get_issue(self.owner, self.repo_name, issue_num)
|
||||
issue_comments = self.client.get_issue_comments(self.owner, self.repo_name, issue_num)
|
||||
comments_list = "\n".join([
|
||||
f" - @{c.user.login} ({c.created_at}): {c.body}"
|
||||
for c in issue_comments
|
||||
]) if issue_comments else " No comments yet."
|
||||
issue_comments = self.client.get_issue_comments(
|
||||
self.owner, self.repo_name, issue_num
|
||||
)
|
||||
comments_list = (
|
||||
"\n".join(
|
||||
[
|
||||
f" - @{c.user.login} ({c.created_at}): {c.body}"
|
||||
for c in issue_comments
|
||||
]
|
||||
)
|
||||
if issue_comments
|
||||
else " No comments yet."
|
||||
)
|
||||
|
||||
issues_details.append(
|
||||
f"### Connected Issue #{issue_num}: {issue.title}\n"
|
||||
@@ -270,14 +328,23 @@ class PRTaskProcessor(TaskProcessor):
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch connected issue #{issue_num}: {e}")
|
||||
if issues_details:
|
||||
connected_issues_ctx = "\n---\n\n## 📋 CONNECTED ISSUE CONTEXT\n" + "\n\n".join(issues_details)
|
||||
connected_issues_ctx = (
|
||||
"\n---\n\n## 📋 CONNECTED ISSUE CONTEXT\n"
|
||||
+ "\n\n".join(issues_details)
|
||||
)
|
||||
|
||||
pr_head_branch = pr_info.head.get('ref', 'unknown') if pr_info.head else "unknown"
|
||||
pr_base_branch = pr_info.base.get('ref', 'unknown') if pr_info.base else "unknown"
|
||||
pr_head_branch = (
|
||||
pr_info.head.get("ref", "unknown") if pr_info.head else "unknown"
|
||||
)
|
||||
pr_base_branch = (
|
||||
pr_info.base.get("ref", "unknown") if pr_info.base else "unknown"
|
||||
)
|
||||
pr_state = pr_info.state
|
||||
pr_created = pr_info.created_at or "unknown"
|
||||
|
||||
is_fixing_pr = is_own_pr or any(r.get("state") == "REQUEST_CHANGES" for r in reviews)
|
||||
is_fixing_pr = is_own_pr or any(
|
||||
r.get("state") == "REQUEST_CHANGES" for r in reviews
|
||||
)
|
||||
|
||||
if is_fixing_pr:
|
||||
instructions = (
|
||||
@@ -287,7 +354,7 @@ class PRTaskProcessor(TaskProcessor):
|
||||
f" 1. Checkout the PR's head branch: `git checkout {pr_head_branch}`\n"
|
||||
f" 2. Implement the requested fixes or changes on this branch.\n"
|
||||
f" 3. Verify your fixes and run verification/tests.\n"
|
||||
f" 4. Commit and push the changes directly: `git add <files> && git commit -m \"fix: address feedback\" && git push origin {pr_head_branch}`\n"
|
||||
f' 4. Commit and push the changes directly: `git add <files> && git commit -m "fix: address feedback" && git push origin {pr_head_branch}`\n'
|
||||
f" 5. After pushing, comment on the PR (using the `add_comment` tool) with a summary of the fixes implemented."
|
||||
)
|
||||
else:
|
||||
@@ -327,25 +394,38 @@ class PRTaskProcessor(TaskProcessor):
|
||||
|
||||
async def process(self, attempt_limit: int) -> str:
|
||||
try:
|
||||
pr_detail = self.client.get_pull_request(self.owner, self.repo_name, self.item.task_number)
|
||||
pr_detail = self.client.get_pull_request(
|
||||
self.owner, self.repo_name, self.item.task_number
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching PR #{self.item.task_number} detail: {e}")
|
||||
return f"FAILED: Could not fetch details for PR #{self.item.task_number}."
|
||||
|
||||
is_own_pr = (pr_detail.user and pr_detail.user.login == self.ai_username)
|
||||
is_requested_reviewer = any(r.login == self.ai_username for r in pr_detail.requested_reviewers)
|
||||
is_own_pr = pr_detail.user and pr_detail.user.login == self.ai_username
|
||||
is_requested_reviewer = any(
|
||||
r.login == self.ai_username for r in pr_detail.requested_reviewers
|
||||
)
|
||||
|
||||
if not is_own_pr and not is_requested_reviewer:
|
||||
logger.info(f"PR #{self.item.task_number}: Agent is not a requested reviewer. Skipping.")
|
||||
logger.info(
|
||||
f"PR #{self.item.task_number}: Agent is not a requested reviewer. Skipping."
|
||||
)
|
||||
return f"SKIP: Agent is not a requested reviewer on PR #{self.item.task_number}."
|
||||
|
||||
pr_comments = []
|
||||
try:
|
||||
pr_comments = self.client.get_pull_request_comments(self.owner, self.repo_name, self.item.task_number)
|
||||
pr_comments = self.client.get_pull_request_comments(
|
||||
self.owner, self.repo_name, self.item.task_number
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching comments for PR #{self.item.task_number}: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Error fetching comments for PR #{self.item.task_number}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
if _is_awaiting_reply_helper(pr_comments, self.ai_username):
|
||||
logger.info(f"PR #{self.item.task_number}: agent asked a question and is awaiting a human reply. Skipping.")
|
||||
logger.info(
|
||||
f"PR #{self.item.task_number}: agent asked a question and is awaiting a human reply. Skipping."
|
||||
)
|
||||
return f"SKIP: Awaiting human reply on PR #{self.item.task_number}."
|
||||
|
||||
base_mission = self._build_pr_mission(pr_detail, is_own_pr)
|
||||
@@ -355,7 +435,9 @@ class PRTaskProcessor(TaskProcessor):
|
||||
|
||||
for attempt in range(1, attempt_limit + 1):
|
||||
try:
|
||||
logger.info(f"Starting Planning Phase for PR #{self.item.task_number} (attempt {attempt})")
|
||||
logger.info(
|
||||
f"Starting Planning Phase for PR #{self.item.task_number} (attempt {attempt})"
|
||||
)
|
||||
planning_mission = (
|
||||
f"PHASE 1: PLANNING PHASE\n\n"
|
||||
f"Your task is to research the problem, analyse the repository structure, and produce a detailed implementation plan.\n"
|
||||
@@ -369,9 +451,13 @@ class PRTaskProcessor(TaskProcessor):
|
||||
f"4. Output your final plan clearly.\n"
|
||||
)
|
||||
planning_agent = PlanningAgent(self.model_name)
|
||||
plan = await planning_agent.run_with_tools(planning_mission, self.planning_tools)
|
||||
plan = await planning_agent.run_with_tools(
|
||||
planning_mission, self.planning_tools
|
||||
)
|
||||
|
||||
logger.info(f"Starting Coding Phase for PR #{self.item.task_number} (attempt {attempt})")
|
||||
logger.info(
|
||||
f"Starting Coding Phase for PR #{self.item.task_number} (attempt {attempt})"
|
||||
)
|
||||
coding_mission = (
|
||||
f"PHASE 2: EXECUTION/CODING PHASE\n\n"
|
||||
f"You must now implement the changes based on the following plan:\n"
|
||||
@@ -380,10 +466,14 @@ class PRTaskProcessor(TaskProcessor):
|
||||
f"Follow the workflow to implement changes, verify, and complete PR review/updates.\n"
|
||||
)
|
||||
coding_agent = CodingAgent(self.model_name)
|
||||
response = await coding_agent.run_with_tools(coding_mission, self.coding_tools_list)
|
||||
response = await coding_agent.run_with_tools(
|
||||
coding_mission, self.coding_tools_list
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing PR #{self.item.task_number} (attempt {attempt}/{attempt_limit}): {e}")
|
||||
logger.error(
|
||||
f"Error processing PR #{self.item.task_number} (attempt {attempt}/{attempt_limit}): {e}"
|
||||
)
|
||||
if attempt == attempt_limit:
|
||||
return f"FAILED after {attempt_limit} attempts: {str(e)}"
|
||||
return f"FAILED: PR #{self.item.task_number} not processed."
|
||||
@@ -402,15 +492,24 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
|
||||
comments: list[CommentModel] = []
|
||||
try:
|
||||
comments = self.client.get_issue_comments(self.owner, self.repo_name, issue_number)
|
||||
comments = self.client.get_issue_comments(
|
||||
self.owner, self.repo_name, issue_number
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching comments for issue #{issue_number}: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Error fetching comments for issue #{issue_number}: {e}", exc_info=True
|
||||
)
|
||||
|
||||
labels_str = f"Labels: {', '.join(issue_labels)}" if issue_labels else "Labels: none"
|
||||
comments_str = "\n".join([
|
||||
f"- @{c.user.login} ({c.created_at}): {c.body}"
|
||||
for c in comments
|
||||
]) if comments else "No comments yet."
|
||||
labels_str = (
|
||||
f"Labels: {', '.join(issue_labels)}" if issue_labels else "Labels: none"
|
||||
)
|
||||
comments_str = (
|
||||
"\n".join(
|
||||
[f"- @{c.user.login} ({c.created_at}): {c.body}" for c in comments]
|
||||
)
|
||||
if comments
|
||||
else "No comments yet."
|
||||
)
|
||||
|
||||
return (
|
||||
f"Your mission is to resolve issue #{issue_number} in {self.repo}.\n\n"
|
||||
@@ -449,7 +548,9 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
|
||||
async def process(self, attempt_limit: int) -> str:
|
||||
# Check if there is an existing PR for the issue
|
||||
existing_pr = _find_pr_for_issue_helper(self.client, self.repo, self.item.task_number)
|
||||
existing_pr = _find_pr_for_issue_helper(
|
||||
self.client, self.repo, self.item.task_number
|
||||
)
|
||||
is_wip = False
|
||||
has_request_changes = False
|
||||
|
||||
@@ -459,32 +560,56 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
is_wip = title_upper.startswith("WIP:") or "[WIP]" in title_upper
|
||||
|
||||
try:
|
||||
reviews = self.client.get_pr_reviews(self.owner, self.repo_name, existing_pr.number)
|
||||
has_request_changes = any(r.get("state") == "REQUEST_CHANGES" for r in reviews)
|
||||
reviews = self.client.get_pr_reviews(
|
||||
self.owner, self.repo_name, existing_pr.number
|
||||
)
|
||||
has_request_changes = any(
|
||||
r.get("state") == "REQUEST_CHANGES" for r in reviews
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking reviews for PR #{existing_pr.number}: {e}")
|
||||
logger.warning(
|
||||
f"Error checking reviews for PR #{existing_pr.number}: {e}"
|
||||
)
|
||||
|
||||
if not is_wip and not has_request_changes:
|
||||
logger.info(f"Issue #{self.item.task_number} already has open PR #{existing_pr.number}. Skipping.")
|
||||
logger.info(
|
||||
f"Issue #{self.item.task_number} already has open PR #{existing_pr.number}. Skipping."
|
||||
)
|
||||
return f"SKIP: A pull request (PR #{existing_pr.number}) addressing issue #{self.item.task_number} already exists."
|
||||
|
||||
# Check comments on issue and PR
|
||||
issue_comments = []
|
||||
try:
|
||||
issue_comments = self.client.get_issue_comments(self.owner, self.repo_name, self.item.task_number)
|
||||
issue_comments = self.client.get_issue_comments(
|
||||
self.owner, self.repo_name, self.item.task_number
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching comments for issue #{self.item.task_number}: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Error fetching comments for issue #{self.item.task_number}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
pr_comments = []
|
||||
if existing_pr:
|
||||
try:
|
||||
pr_comments = self.client.get_pull_request_comments(self.owner, self.repo_name, existing_pr.number)
|
||||
pr_comments = self.client.get_pull_request_comments(
|
||||
self.owner, self.repo_name, existing_pr.number
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching comments for PR #{existing_pr.number}: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Error fetching comments for PR #{existing_pr.number}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
if _is_awaiting_reply_helper(issue_comments, self.ai_username) or _is_awaiting_reply_helper(pr_comments, self.ai_username):
|
||||
logger.info(f"Issue #{self.item.task_number}: awaiting human reply. Skipping.")
|
||||
return f"SKIP: Awaiting human reply on issue #{self.item.task_number} or PR."
|
||||
if _is_awaiting_reply_helper(
|
||||
issue_comments, self.ai_username
|
||||
) or _is_awaiting_reply_helper(pr_comments, self.ai_username):
|
||||
logger.info(
|
||||
f"Issue #{self.item.task_number}: awaiting human reply. Skipping."
|
||||
)
|
||||
return (
|
||||
f"SKIP: Awaiting human reply on issue #{self.item.task_number} or PR."
|
||||
)
|
||||
|
||||
issue_info = self.item.task_info
|
||||
if not isinstance(issue_info, IssueModel):
|
||||
@@ -492,23 +617,48 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
title = issue_info.title
|
||||
issue_body = issue_info.body or "No description provided."
|
||||
|
||||
issue_comments_str = "\n".join([
|
||||
f"- @{c.user.login} ({c.created_at}): {c.body}" for c in issue_comments
|
||||
]) if issue_comments else "No comments yet."
|
||||
issue_comments_str = (
|
||||
"\n".join(
|
||||
[
|
||||
f"- @{c.user.login} ({c.created_at}): {c.body}"
|
||||
for c in issue_comments
|
||||
]
|
||||
)
|
||||
if issue_comments
|
||||
else "No comments yet."
|
||||
)
|
||||
|
||||
pr_info_str = "No existing PR."
|
||||
if existing_pr:
|
||||
pr_comments_str = "\n".join([
|
||||
f"- @{c.user.login} ({c.created_at}): {c.body}" for c in pr_comments
|
||||
]) if pr_comments else "No PR comments yet."
|
||||
pr_comments_str = (
|
||||
"\n".join(
|
||||
[
|
||||
f"- @{c.user.login} ({c.created_at}): {c.body}"
|
||||
for c in pr_comments
|
||||
]
|
||||
)
|
||||
if pr_comments
|
||||
else "No PR comments yet."
|
||||
)
|
||||
try:
|
||||
reviews = self.client.get_pr_reviews(self.owner, self.repo_name, existing_pr.number)
|
||||
reviews_str = "\n".join([
|
||||
f"- @{r.get('user', {}).get('login')} ({r.get('submitted_at')}): [{r.get('state')}] {r.get('body')}"
|
||||
for r in reviews
|
||||
]) if reviews else "No reviews yet."
|
||||
reviews = self.client.get_pr_reviews(
|
||||
self.owner, self.repo_name, existing_pr.number
|
||||
)
|
||||
reviews_str = (
|
||||
"\n".join(
|
||||
[
|
||||
f"- @{r.get('user', {}).get('login')} ({r.get('submitted_at')}): [{r.get('state')}] {r.get('body')}"
|
||||
for r in reviews
|
||||
]
|
||||
)
|
||||
if reviews
|
||||
else "No reviews yet."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching reviews for PR #{existing_pr.number}: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Error fetching reviews for PR #{existing_pr.number}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
reviews_str = "No reviews available."
|
||||
pr_info_str = (
|
||||
f"PR Number: #{existing_pr.number}\n"
|
||||
@@ -532,11 +682,17 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
coord_tools = CoordinatorTools()
|
||||
coordinator_agent = CoordinatorAgent(self.model_name)
|
||||
|
||||
logger.info(f"Analyzing conversation state for issue #{self.item.task_number}...")
|
||||
await coordinator_agent.decide_action(state_analysis_mission, self.planning_tools, coord_tools)
|
||||
logger.info(
|
||||
f"Analyzing conversation state for issue #{self.item.task_number}..."
|
||||
)
|
||||
await coordinator_agent.decide_action(
|
||||
state_analysis_mission, self.planning_tools, coord_tools
|
||||
)
|
||||
|
||||
action = coord_tools.action
|
||||
logger.info(f"Coordinator Decided Action: {action} (tool_called={coord_tools.tool_called})")
|
||||
logger.info(
|
||||
f"Coordinator Decided Action: {action} (tool_called={coord_tools.tool_called})"
|
||||
)
|
||||
|
||||
if action == "PROPOSE_PLAN":
|
||||
comment_body = coord_tools.arguments.get("comment_body", "")
|
||||
@@ -549,7 +705,9 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
f"<!-- agent:plan-proposal -->\n"
|
||||
f"<!-- agent:awaiting-reply -->"
|
||||
)
|
||||
self.client.add_comment(self.owner, self.repo_name, self.item.task_number, comment_body)
|
||||
self.client.add_comment(
|
||||
self.owner, self.repo_name, self.item.task_number, comment_body
|
||||
)
|
||||
return f"POSTED_COMMENT: PROPOSE_PLAN comment posted to issue #{self.item.task_number}."
|
||||
|
||||
elif action == "ANSWER_QUESTION":
|
||||
@@ -562,17 +720,27 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
f"<!-- agent:question-response -->\n"
|
||||
f"<!-- agent:awaiting-reply -->"
|
||||
)
|
||||
self.client.add_comment(self.owner, self.repo_name, self.item.task_number, comment_body)
|
||||
self.client.add_comment(
|
||||
self.owner, self.repo_name, self.item.task_number, comment_body
|
||||
)
|
||||
return f"POSTED_COMMENT: ANSWER_QUESTION comment posted to issue #{self.item.task_number}."
|
||||
|
||||
elif action == "CLOSE_ISSUE":
|
||||
comment = coord_tools.arguments.get("comment", "Closing the issue as resolved.")
|
||||
self.client.add_comment(self.owner, self.repo_name, self.item.task_number, comment)
|
||||
self.client.close_issue(self.owner, self.repo_name, self.item.task_number)
|
||||
comment = coord_tools.arguments.get(
|
||||
"comment", "Closing the issue as resolved."
|
||||
)
|
||||
self.client.add_comment(
|
||||
self.owner, self.repo_name, self.item.task_number, comment
|
||||
)
|
||||
self.client.close_issue(
|
||||
self.owner, self.repo_name, self.item.task_number
|
||||
)
|
||||
return f"CLOSED_ISSUE: Issue #{self.item.task_number} closed."
|
||||
|
||||
elif action == "NO_ACTION":
|
||||
return f"NO_ACTION: No action taken on issue #{self.item.task_number}."
|
||||
return (
|
||||
f"NO_ACTION: No action taken on issue #{self.item.task_number}."
|
||||
)
|
||||
|
||||
elif action == "EXECUTE_PLAN":
|
||||
approved_plan = coord_tools.arguments.get("approved_plan", "")
|
||||
@@ -581,10 +749,16 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
|
||||
if pr_to_use:
|
||||
branch_name = pr_to_use.head.get("ref", "")
|
||||
logger.info(f"Resuming work on existing PR #{pr_to_use.number} on branch '{branch_name}'")
|
||||
logger.info(
|
||||
f"Resuming work on existing PR #{pr_to_use.number} on branch '{branch_name}'"
|
||||
)
|
||||
else:
|
||||
logger.info(f"Creating new WIP PR for issue #{self.item.task_number}")
|
||||
clean_title = re.sub(r'[^a-zA-Z0-9\s-]', '', title).strip().lower()
|
||||
logger.info(
|
||||
f"Creating new WIP PR for issue #{self.item.task_number}"
|
||||
)
|
||||
clean_title = (
|
||||
re.sub(r"[^a-zA-Z0-9\s-]", "", title).strip().lower()
|
||||
)
|
||||
title_words = clean_title.split()[:5]
|
||||
desc_suffix = "-".join(title_words)
|
||||
if not desc_suffix:
|
||||
@@ -592,26 +766,77 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
branch_name = f"fix/issue-{self.item.task_number}-{desc_suffix}"
|
||||
|
||||
try:
|
||||
subprocess.run(["git", "checkout", "master"], cwd=str(self.repo_path), check=True)
|
||||
subprocess.run(["git", "pull", "origin", "master"], cwd=str(self.repo_path), check=True)
|
||||
subprocess.run(["git", "branch", "-D", branch_name], cwd=str(self.repo_path), stderr=subprocess.DEVNULL)
|
||||
subprocess.run(["git", "checkout", "-b", branch_name], cwd=str(self.repo_path), check=True)
|
||||
subprocess.run(["git", "commit", "--allow-empty", "-m", f"WIP: start implementation for issue #{self.item.task_number}"], cwd=str(self.repo_path), check=True)
|
||||
subprocess.run(["git", "push", "origin", branch_name], cwd=str(self.repo_path), check=True)
|
||||
|
||||
pr_title = f"WIP: {title}"
|
||||
pr_description = f"Work in progress for issue #{self.item.task_number}."
|
||||
pr_to_use = self.client.create_pull_request(
|
||||
self.owner, self.repo_name, head=branch_name, base="master", title=pr_title, description=pr_description
|
||||
subprocess.run(
|
||||
["git", "checkout", "master"],
|
||||
cwd=str(self.repo_path),
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "pull", "origin", "master"],
|
||||
cwd=str(self.repo_path),
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "branch", "-D", branch_name],
|
||||
cwd=str(self.repo_path),
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "checkout", "-b", branch_name],
|
||||
cwd=str(self.repo_path),
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
f"WIP: start implementation for issue #{self.item.task_number}",
|
||||
],
|
||||
cwd=str(self.repo_path),
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "push", "origin", branch_name],
|
||||
cwd=str(self.repo_path),
|
||||
check=True,
|
||||
)
|
||||
|
||||
pr_link = pr_to_use.html_url or f"{self.client.base_url}/{self.repo}/pulls/{pr_to_use.number}"
|
||||
start_comment = f"Started work on PR #{pr_to_use.number} ({pr_link})."
|
||||
self.client.add_comment(self.owner, self.repo_name, self.item.task_number, start_comment)
|
||||
pr_title = f"WIP: {title}"
|
||||
pr_description = (
|
||||
f"Work in progress for issue #{self.item.task_number}."
|
||||
)
|
||||
pr_to_use = self.client.create_pull_request(
|
||||
self.owner,
|
||||
self.repo_name,
|
||||
head=branch_name,
|
||||
base="master",
|
||||
title=pr_title,
|
||||
description=pr_description,
|
||||
)
|
||||
|
||||
logger.info(f"Successfully created WIP PR #{pr_to_use.number} on branch '{branch_name}'")
|
||||
pr_link = (
|
||||
pr_to_use.html_url
|
||||
or f"{self.client.base_url}/{self.repo}/pulls/{pr_to_use.number}"
|
||||
)
|
||||
start_comment = (
|
||||
f"Started work on PR #{pr_to_use.number} ({pr_link})."
|
||||
)
|
||||
self.client.add_comment(
|
||||
self.owner,
|
||||
self.repo_name,
|
||||
self.item.task_number,
|
||||
start_comment,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Successfully created WIP PR #{pr_to_use.number} on branch '{branch_name}'"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create WIP PR for issue #{self.item.task_number}: {e}")
|
||||
logger.error(
|
||||
f"Failed to create WIP PR for issue #{self.item.task_number}: {e}"
|
||||
)
|
||||
return f"FAILED to create WIP PR: {e}"
|
||||
|
||||
base_mission = self._build_issue_mission(issue_info, branch_name)
|
||||
@@ -636,18 +861,28 @@ class IssueTaskProcessor(TaskProcessor):
|
||||
f" - web_search(query, time_range, categories) — search the web via SearXNG/DuckDuckGo\n"
|
||||
f" - fetch_url(url) — read any documentation page in full\n"
|
||||
)
|
||||
logger.info(f"Starting Execution/Coding Phase for issue #{self.item.task_number} on branch '{branch_name}'")
|
||||
logger.info(
|
||||
f"Starting Execution/Coding Phase for issue #{self.item.task_number} on branch '{branch_name}'"
|
||||
)
|
||||
coding_agent = CodingAgent(self.model_name)
|
||||
response = await coding_agent.run_with_tools(coding_mission, self.coding_tools_list)
|
||||
logger.info(f"Agent response for issue #{self.item.task_number}: {response}")
|
||||
response = await coding_agent.run_with_tools(
|
||||
coding_mission, self.coding_tools_list
|
||||
)
|
||||
logger.info(
|
||||
f"Agent response for issue #{self.item.task_number}: {response}"
|
||||
)
|
||||
return response
|
||||
|
||||
except CoordinatorNoToolCalledError as e:
|
||||
logger.error(f"Coordinator error on issue #{self.item.task_number} (attempt {attempt}/{attempt_limit}): {e}")
|
||||
logger.error(
|
||||
f"Coordinator error on issue #{self.item.task_number} (attempt {attempt}/{attempt_limit}): {e}"
|
||||
)
|
||||
if attempt == attempt_limit:
|
||||
return f"FAILED: Coordinator did not call any tools after {attempt_limit} attempts."
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing issue #{self.item.task_number} (attempt {attempt}/{attempt_limit}): {e}")
|
||||
logger.error(
|
||||
f"Error processing issue #{self.item.task_number} (attempt {attempt}/{attempt_limit}): {e}"
|
||||
)
|
||||
if attempt == attempt_limit:
|
||||
return f"FAILED after {attempt_limit} attempts: {str(e)}"
|
||||
return f"FAILED: Issue #{self.item.task_number} not processed."
|
||||
@@ -659,12 +894,18 @@ class AgentDispatcher:
|
||||
def __init__(
|
||||
self,
|
||||
client: GiteaClient,
|
||||
tools: GiteaTools,
|
||||
issue_tools: IssueTools,
|
||||
pr_tools: PRTools,
|
||||
file_tools: FileTools,
|
||||
git_tools: GitTools,
|
||||
model_name: str = AGENT_MODEL_ID,
|
||||
max_retries: int = 2,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._tools = tools
|
||||
self._issue_tools = issue_tools
|
||||
self._pr_tools = pr_tools
|
||||
self._file_tools = file_tools
|
||||
self._git_tools = git_tools
|
||||
self._model_name = model_name
|
||||
self._max_retries = max_retries
|
||||
|
||||
@@ -693,7 +934,10 @@ class AgentDispatcher:
|
||||
if item.task_type == "pr":
|
||||
processor = PRTaskProcessor(
|
||||
client=self._client,
|
||||
tools=self._tools,
|
||||
issue_tools=self._issue_tools,
|
||||
pr_tools=self._pr_tools,
|
||||
file_tools=self._file_tools,
|
||||
git_tools=self._git_tools,
|
||||
model_name=self._model_name,
|
||||
repo=repo,
|
||||
item=item,
|
||||
@@ -702,7 +946,10 @@ class AgentDispatcher:
|
||||
elif item.task_type == "issue":
|
||||
processor = IssueTaskProcessor(
|
||||
client=self._client,
|
||||
tools=self._tools,
|
||||
issue_tools=self._issue_tools,
|
||||
pr_tools=self._pr_tools,
|
||||
file_tools=self._file_tools,
|
||||
git_tools=self._git_tools,
|
||||
model_name=self._model_name,
|
||||
repo=repo,
|
||||
item=item,
|
||||
@@ -713,14 +960,18 @@ class AgentDispatcher:
|
||||
results.append(f"SKIP: Unknown task type {item.task_type}")
|
||||
continue
|
||||
|
||||
logger.info(f"Processing {item.task_type} #{item.task_number} via {processor.__class__.__name__}")
|
||||
logger.info(
|
||||
f"Processing {item.task_type} #{item.task_number} via {processor.__class__.__name__}"
|
||||
)
|
||||
result = await processor.process(attempt_limit=self._max_retries)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
# Backward compatibility helper methods for unit tests
|
||||
def _find_pr_for_issue(self, repo_full_name: str, issue_number: int) -> PullRequestModel | None:
|
||||
def _find_pr_for_issue(
|
||||
self, repo_full_name: str, issue_number: int
|
||||
) -> PullRequestModel | None:
|
||||
return _find_pr_for_issue_helper(self._client, repo_full_name, issue_number)
|
||||
|
||||
def _is_awaiting_reply(self, comments: list[CommentModel]) -> bool:
|
||||
@@ -738,7 +989,10 @@ class AgentDispatcher:
|
||||
raise RuntimeError("No authenticated user found.")
|
||||
processor = PRTaskProcessor(
|
||||
client=self._client,
|
||||
tools=self._tools,
|
||||
issue_tools=self._issue_tools,
|
||||
pr_tools=self._pr_tools,
|
||||
file_tools=self._file_tools,
|
||||
git_tools=self._git_tools,
|
||||
model_name=self._model_name,
|
||||
repo=item.repo_full_name,
|
||||
item=item,
|
||||
@@ -755,7 +1009,10 @@ class AgentDispatcher:
|
||||
raise RuntimeError("No authenticated user found.")
|
||||
processor = IssueTaskProcessor(
|
||||
client=self._client,
|
||||
tools=self._tools,
|
||||
issue_tools=self._issue_tools,
|
||||
pr_tools=self._pr_tools,
|
||||
file_tools=self._file_tools,
|
||||
git_tools=self._git_tools,
|
||||
model_name=self._model_name,
|
||||
repo=item.repo_full_name,
|
||||
item=item,
|
||||
|
||||
+68
-28
@@ -11,10 +11,16 @@ from core.queue import WorkQueue, WorkItem
|
||||
from core.dispatcher import AgentDispatcher
|
||||
from gitea.client import GiteaClient
|
||||
from gitea.models import IssueModel, PullRequestModel, RepositoryModel
|
||||
from gitea.tools.gitea_tools import GiteaTools
|
||||
from gitea.tools.issue_tools import IssueTools
|
||||
from gitea.tools.pr_tools import PRTools
|
||||
from gitea.tools.file_tools import FileTools
|
||||
from gitea.tools.git_tools import GitTools
|
||||
from gitea.config import AGENT_MODEL_ID, AGENT_MAX_RETRIES
|
||||
from gitea.workspace import WorkspaceManager
|
||||
from core.notification_agent import NotificationReaderAgent, NotificationNoToolCalledError
|
||||
from core.notification_agent import (
|
||||
NotificationReaderAgent,
|
||||
NotificationNoToolCalledError,
|
||||
)
|
||||
from core.notification_tools import NotificationTools
|
||||
|
||||
logger: logging.Logger = logging.getLogger("agent-orchestrator")
|
||||
@@ -26,19 +32,32 @@ class AgentOrchestrator:
|
||||
def __init__(
|
||||
self,
|
||||
client: GiteaClient,
|
||||
tools: GiteaTools,
|
||||
issue_tools: IssueTools,
|
||||
pr_tools: PRTools,
|
||||
file_tools: FileTools,
|
||||
git_tools: GitTools,
|
||||
model_name: str = AGENT_MODEL_ID,
|
||||
max_retries: int = AGENT_MAX_RETRIES,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._tools = tools
|
||||
self._issue_tools = issue_tools
|
||||
self._pr_tools = pr_tools
|
||||
self._file_tools = file_tools
|
||||
self._git_tools = git_tools
|
||||
self._model_name = model_name
|
||||
self._work_queue = WorkQueue()
|
||||
self._dispatcher = AgentDispatcher(client, tools, model_name, max_retries)
|
||||
self._dispatcher = AgentDispatcher(
|
||||
client,
|
||||
issue_tools,
|
||||
pr_tools,
|
||||
file_tools,
|
||||
git_tools,
|
||||
model_name,
|
||||
max_retries,
|
||||
)
|
||||
self._notification_reader = NotificationReaderAgent(model_name)
|
||||
self._max_retries = max_retries
|
||||
|
||||
|
||||
def _get_state_file_path(self) -> Path:
|
||||
"""Get the path to the persistent state file."""
|
||||
return Path(__file__).parent.parent / "agent_state.json"
|
||||
@@ -67,7 +86,9 @@ class AgentOrchestrator:
|
||||
async def poll_and_dispatch(self) -> None:
|
||||
"""Poll Gitea unread notifications, enqueue them, and dispatch to agent."""
|
||||
last_checked = self._read_last_checked()
|
||||
logger.info(f"Polling unread notifications since: {last_checked or 'beginning'}")
|
||||
logger.info(
|
||||
f"Polling unread notifications since: {last_checked or 'beginning'}"
|
||||
)
|
||||
|
||||
notifications = self._client.list_unread_notifications(since=last_checked)
|
||||
|
||||
@@ -80,10 +101,10 @@ class AgentOrchestrator:
|
||||
# Filter and enqueue tasks from notifications
|
||||
latest_timestamp = last_checked
|
||||
inspection_tools = [
|
||||
self._tools.get_issue,
|
||||
self._tools.get_pull_request,
|
||||
self._tools.get_issue_comments,
|
||||
self._tools.get_pull_request_comments,
|
||||
self._issue_tools.get_issue,
|
||||
self._pr_tools.get_pull_request,
|
||||
self._issue_tools.get_issue_comments,
|
||||
self._pr_tools.get_pull_request_comments,
|
||||
]
|
||||
|
||||
for n in notifications:
|
||||
@@ -106,7 +127,9 @@ class AgentOrchestrator:
|
||||
try:
|
||||
task_number = int(subj_url.rstrip("/").split("/")[-1])
|
||||
except (ValueError, IndexError):
|
||||
logger.warning(f"Could not parse task number from subject URL: {subj_url}")
|
||||
logger.warning(
|
||||
f"Could not parse task number from subject URL: {subj_url}"
|
||||
)
|
||||
continue
|
||||
|
||||
# Run NotificationReaderAgent to pre-screen the notification
|
||||
@@ -125,21 +148,27 @@ class AgentOrchestrator:
|
||||
attempt += 1
|
||||
try:
|
||||
await self._notification_reader.decide_notification(
|
||||
mission,
|
||||
inspection_tools,
|
||||
notification_tools
|
||||
mission, inspection_tools, notification_tools
|
||||
)
|
||||
success = True
|
||||
break
|
||||
except NotificationNoToolCalledError as e:
|
||||
logger.error(f"Notification reader error on notification {notification_id} (attempt {attempt}/{attempt_limit}): {e}")
|
||||
logger.error(
|
||||
f"Notification reader error on notification {notification_id} (attempt {attempt}/{attempt_limit}): {e}"
|
||||
)
|
||||
|
||||
if not success or notification_tools.action == "SKIP":
|
||||
reason = notification_tools.arguments.get("reason", "Failed to call routing tool / default skip")
|
||||
logger.info(f"Skipping notification {notification_id} for {repo_full_name}#{task_number}. Reason: {reason}")
|
||||
reason = notification_tools.arguments.get(
|
||||
"reason", "Failed to call routing tool / default skip"
|
||||
)
|
||||
logger.info(
|
||||
f"Skipping notification {notification_id} for {repo_full_name}#{task_number}. Reason: {reason}"
|
||||
)
|
||||
if notification_id is not None:
|
||||
self._client.mark_notification_as_read(notification_id)
|
||||
logger.info(f"Marked skipped Gitea notification thread {notification_id} as read.")
|
||||
logger.info(
|
||||
f"Marked skipped Gitea notification thread {notification_id} as read."
|
||||
)
|
||||
continue
|
||||
|
||||
# Route based on decided action
|
||||
@@ -147,7 +176,9 @@ class AgentOrchestrator:
|
||||
try:
|
||||
issue = self._client.get_issue(owner, repo_name, task_number)
|
||||
if issue.repository is None:
|
||||
issue = issue.model_copy(update={"repository": RepositoryModel(**repo_info)})
|
||||
issue = issue.model_copy(
|
||||
update={"repository": RepositoryModel(**repo_info)}
|
||||
)
|
||||
|
||||
item = WorkItem(
|
||||
repo_full_name=repo_full_name,
|
||||
@@ -155,16 +186,20 @@ class AgentOrchestrator:
|
||||
task_number=task_number,
|
||||
task_info=issue,
|
||||
notification_id=notification_id,
|
||||
priority=0
|
||||
priority=0,
|
||||
)
|
||||
self._work_queue.enqueue(item)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch issue #{task_number} for notification: {e}")
|
||||
logger.error(
|
||||
f"Failed to fetch issue #{task_number} for notification: {e}"
|
||||
)
|
||||
elif notification_tools.action == "PROCESS_PR":
|
||||
try:
|
||||
pr = self._client.get_pull_request(owner, repo_name, task_number)
|
||||
if pr.repository is None:
|
||||
pr = pr.model_copy(update={"repository": RepositoryModel(**repo_info)})
|
||||
pr = pr.model_copy(
|
||||
update={"repository": RepositoryModel(**repo_info)}
|
||||
)
|
||||
|
||||
item = WorkItem(
|
||||
repo_full_name=repo_full_name,
|
||||
@@ -172,12 +207,13 @@ class AgentOrchestrator:
|
||||
task_number=task_number,
|
||||
task_info=pr,
|
||||
notification_id=notification_id,
|
||||
priority=0
|
||||
priority=0,
|
||||
)
|
||||
self._work_queue.enqueue(item)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch PR #{task_number} for notification: {e}")
|
||||
|
||||
logger.error(
|
||||
f"Failed to fetch PR #{task_number} for notification: {e}"
|
||||
)
|
||||
|
||||
# Process enqueued work
|
||||
if not self._work_queue.is_empty:
|
||||
@@ -212,7 +248,11 @@ class AgentOrchestrator:
|
||||
|
||||
for i, result in enumerate(results):
|
||||
item = work_items[i]
|
||||
logger.info(f"Completed {item.task_type} #{item.task_number}: {result[:200]}")
|
||||
logger.info(
|
||||
f"Completed {item.task_type} #{item.task_number}: {result[:200]}"
|
||||
)
|
||||
if item.notification_id is not None:
|
||||
self._client.mark_notification_as_read(item.notification_id)
|
||||
logger.info(f"Marked Gitea notification thread {item.notification_id} as read.")
|
||||
logger.info(
|
||||
f"Marked Gitea notification thread {item.notification_id} as read."
|
||||
)
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
from gitea.tools.issue_tools import IssueTools
|
||||
from gitea.tools.pr_tools import PRTools
|
||||
from gitea.tools.file_tools import FileTools
|
||||
from gitea.tools.git_tools import GitTools
|
||||
from gitea.client import GiteaClient
|
||||
|
||||
|
||||
class GiteaTools:
|
||||
"""Facade for Gitea tool operations - delegates to focused tool classes."""
|
||||
|
||||
def __init__(self, client: GiteaClient) -> None:
|
||||
self._client = client
|
||||
self.issue_tools = IssueTools(client)
|
||||
self.pr_tools = PRTools(client)
|
||||
self.file_tools = FileTools(client)
|
||||
self.git_tools = GitTools(client)
|
||||
|
||||
# ---- Issue operations (delegated) ----
|
||||
|
||||
def get_issue(self, owner: str, repo: str, issue_number: int) -> str:
|
||||
"""Get the details of a specific issue. Args: owner (repo owner), repo (repo name), issue_number (issue ID)."""
|
||||
return self.issue_tools.get_issue(owner, repo, issue_number)
|
||||
|
||||
def get_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
|
||||
"""Get the details of a specific pull request. Args: owner (repo owner), repo (repo name), pull_number (PR ID)."""
|
||||
return self.pr_tools.get_pull_request(owner, repo, pull_number)
|
||||
|
||||
def close_issue(self, owner: str, repo: str, issue_number: int) -> str:
|
||||
"""Close an issue. Args: owner (repo owner), repo (repo name), issue_number (issue ID)."""
|
||||
return self.issue_tools.close_issue(owner, repo, issue_number)
|
||||
|
||||
def close_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
|
||||
"""Close a pull request. Args: owner (repo owner), repo (repo name), pull_number (PR ID)."""
|
||||
return self.pr_tools.close_pull_request(owner, repo, pull_number)
|
||||
|
||||
def get_issue_comments(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
issue_number: int,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
) -> str:
|
||||
"""Get all comments on an issue. Args: owner, repo, issue_number, limit (default 20), offset (default 0)."""
|
||||
return self.issue_tools.get_issue_comments(owner, repo, issue_number, limit=limit, offset=offset)
|
||||
|
||||
def get_pull_request_comments(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
pull_number: int,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
) -> str:
|
||||
"""Get all comments on a pull request. Args: owner, repo, pull_number, limit (default 20), offset (default 0)."""
|
||||
return self.pr_tools.get_pull_request_comments(owner, repo, pull_number, limit=limit, offset=offset)
|
||||
|
||||
def list_assigned_issues(self) -> list[dict]:
|
||||
"""List all issues assigned to the authenticated user across all repos."""
|
||||
return self.issue_tools.list_assigned_issues()
|
||||
|
||||
def list_assigned_pull_requests(self) -> list[dict]:
|
||||
"""List all pull requests assigned to the authenticated user across all repos."""
|
||||
return self.pr_tools.list_assigned_pull_requests()
|
||||
|
||||
def list_issues(self, owner: str, repo: str, state: str = "open") -> str:
|
||||
"""List issues in a repository. Args: owner (repo owner), repo (repo name), state (open, closed, or all)."""
|
||||
return self.issue_tools.list_issues(owner, repo, state)
|
||||
|
||||
def list_pull_requests(self, owner: str, repo: str, state: str = "open") -> str:
|
||||
"""List pull requests in a repository. Args: owner (repo owner), repo (repo name), state (open, closed, or all)."""
|
||||
return self.pr_tools.list_pull_requests(owner, repo, state)
|
||||
|
||||
def get_file_content(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
path: str,
|
||||
offset: int = 1,
|
||||
limit: int = 250,
|
||||
) -> str:
|
||||
"""Get the content of a file from a repository. Args: owner, repo, path, offset (line, default 1), limit (default 250)."""
|
||||
return self.file_tools.get_file_content(owner, repo, path, offset=offset, limit=limit)
|
||||
|
||||
def create_pull_request(self, owner: str, repo: str, head: str, base: str, title: str, description: str = "") -> str:
|
||||
"""Create a new pull request. Args: owner, repo, head (source branch), base (target branch), title, description."""
|
||||
return self.pr_tools.create_pull_request(owner, repo, head, base, title, description)
|
||||
|
||||
def update_pull_request(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
pull_number: int,
|
||||
title: str | None = None,
|
||||
body: str | None = None,
|
||||
state: str | None = None,
|
||||
) -> str:
|
||||
"""Update an existing pull request. Args: owner, repo, pull_number, title (optional), body (optional), state (optional)."""
|
||||
return self.pr_tools.update_pull_request(owner, repo, pull_number, title, body, state)
|
||||
|
||||
def create_issue(self, owner: str, repo: str, title: str, body: str, labels: list[str] | None = None, assignees: list[str] | None = None) -> str:
|
||||
"""Create a new issue. Args: owner, repo, title, body, labels (optional), assignees (optional)."""
|
||||
return self.issue_tools.create_issue(owner, repo, title, body, labels, assignees)
|
||||
|
||||
def create_branch(self, owner: str, repo: str, ref: str, sha: str) -> str:
|
||||
"""Create a new branch in a repository. Args: owner, repo, ref (branch name), sha (commit SHA)."""
|
||||
return self.git_tools.create_branch(owner, repo, ref, sha)
|
||||
|
||||
def commit_file(self, owner: str, repo: str, path: str, message: str, content: str, branch: str) -> str:
|
||||
"""Commit a file to a repository. Args: owner, repo, path, message, content, branch."""
|
||||
return self.file_tools.commit_file(owner, repo, path, message, content, branch)
|
||||
|
||||
def add_label_to_issue(self, owner: str, repo: str, issue_number: int, label: str) -> str:
|
||||
"""Add a label to an issue. Args: owner, repo, issue_number, label."""
|
||||
return self.issue_tools.add_label_to_issue(owner, repo, issue_number, label)
|
||||
|
||||
def add_label_to_pr(self, owner: str, repo: str, pr_number: int, label: str) -> str:
|
||||
"""Add a label to a pull request. Args: owner, repo, pr_number, label."""
|
||||
return self.pr_tools.add_label_to_pr(owner, repo, pr_number, label)
|
||||
|
||||
def add_comment_to_issue(self, owner: str, repo: str, issue_number: int, body: str) -> str:
|
||||
"""Add a comment to an issue. Args: owner, repo, issue_number, body."""
|
||||
return self.issue_tools.add_comment_to_issue(owner, repo, issue_number, body)
|
||||
|
||||
def get_pull_request_diff(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
pull_number: int,
|
||||
max_chars: int = 15000,
|
||||
char_offset: int = 0,
|
||||
) -> str:
|
||||
"""Get the diff of a pull request. Args: owner, repo, pull_number, max_chars (default 15000), char_offset (default 0)."""
|
||||
return self.pr_tools.get_pull_request_diff(owner, repo, pull_number, max_chars=max_chars, char_offset=char_offset)
|
||||
|
||||
def get_pull_request_patch(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
pull_number: int,
|
||||
max_chars: int = 15000,
|
||||
char_offset: int = 0,
|
||||
) -> str:
|
||||
"""Get the patch of a pull request. Args: owner, repo, pull_number, max_chars (default 15000), char_offset (default 0)."""
|
||||
return self.pr_tools.get_pull_request_patch(owner, repo, pull_number, max_chars=max_chars, char_offset=char_offset)
|
||||
|
||||
def approve_pull_request(self, owner: str, repo: str, pull_number: int, comment: str) -> str:
|
||||
"""Approve a pull request. Args: owner, repo, pull_number, comment."""
|
||||
return self.pr_tools.approve_pull_request(owner, repo, pull_number, comment)
|
||||
|
||||
def request_changes(self, owner: str, repo: str, pull_number: int, comment: str) -> str:
|
||||
"""Request changes on a pull request. Args: owner, repo, pull_number, comment."""
|
||||
return self.pr_tools.request_changes(owner, repo, pull_number, comment)
|
||||
|
||||
def add_comment(self, owner: str, repo: str, issue_number: int, body: str) -> str:
|
||||
"""Add a comment to an issue. Args: owner, repo, issue_number, body."""
|
||||
return self.issue_tools.add_comment(owner, repo, issue_number, body)
|
||||
|
||||
def add_label(self, owner: str, repo: str, issue_number: int, label: str) -> str:
|
||||
"""Add a label to an issue. Args: owner, repo, issue_number, label."""
|
||||
return self.issue_tools.add_label(owner, repo, issue_number, label)
|
||||
|
||||
def get_file_content_with_ref(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
path: str,
|
||||
ref: str = "master",
|
||||
offset: int = 1,
|
||||
limit: int = 250,
|
||||
) -> str:
|
||||
"""Get file content with a specific git ref. Args: owner, repo, path, ref (branch/tag), offset (line, default 1), limit (default 250)."""
|
||||
return self.file_tools.get_file_content_with_ref(owner, repo, path, ref=ref, offset=offset, limit=limit)
|
||||
|
||||
def update_file(self, owner: str, repo: str, path: str, message: str, content: str, branch: str) -> str:
|
||||
"""Update a file in a repository. Args: owner, repo, path, message, content, branch."""
|
||||
return self.file_tools.update_file(owner, repo, path, message, content, branch)
|
||||
@@ -7,7 +7,10 @@ from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from gitea.client import GiteaClient
|
||||
from gitea.tools.gitea_tools import GiteaTools
|
||||
from gitea.tools.issue_tools import IssueTools
|
||||
from gitea.tools.pr_tools import PRTools
|
||||
from gitea.tools.file_tools import FileTools
|
||||
from gitea.tools.git_tools import GitTools
|
||||
from gitea.config import AGENT_MODEL_ID, AGENT_MAX_RETRIES
|
||||
from core.orchestrator import AgentOrchestrator
|
||||
|
||||
@@ -38,12 +41,11 @@ file_handler = RotatingFileHandler(LOG_FILE, maxBytes=5 * 1024 * 1024, backupCou
|
||||
file_handler.setFormatter(JSONFormatter())
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
handlers=[file_handler, stream_handler]
|
||||
stream_handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO, handlers=[file_handler, stream_handler])
|
||||
logger: logging.Logger = logging.getLogger("coding-agent")
|
||||
|
||||
|
||||
@@ -64,14 +66,27 @@ async def main() -> None:
|
||||
raise RuntimeError("No authenticated user found.")
|
||||
logger.info(f"Authenticated as user: {user.login}")
|
||||
except Exception as e:
|
||||
logger.critical(f"Critical initialization error: No authenticated user found. {e}")
|
||||
logger.critical(
|
||||
f"Critical initialization error: No authenticated user found. {e}"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
tools: GiteaTools = GiteaTools(client)
|
||||
issue_tools: IssueTools = IssueTools(client)
|
||||
pr_tools: PRTools = PRTools(client)
|
||||
file_tools: FileTools = FileTools(client)
|
||||
git_tools: GitTools = GitTools(client)
|
||||
model_name: str = AGENT_MODEL_ID
|
||||
|
||||
# Initialize orchestrator
|
||||
orchestrator: AgentOrchestrator = AgentOrchestrator(client, tools, model_name, AGENT_MAX_RETRIES)
|
||||
orchestrator: AgentOrchestrator = AgentOrchestrator(
|
||||
client,
|
||||
issue_tools,
|
||||
pr_tools,
|
||||
file_tools,
|
||||
git_tools,
|
||||
model_name,
|
||||
AGENT_MAX_RETRIES,
|
||||
)
|
||||
|
||||
logger.info("--- Autonomous Coding Agent Active ---")
|
||||
logger.info(f"Model: {model_name}")
|
||||
@@ -100,11 +115,15 @@ async def main() -> None:
|
||||
break
|
||||
except Exception as e:
|
||||
consecutive_errors += 1
|
||||
logger.error(f"Error in main loop (attempt {consecutive_errors}/{max_consecutive_errors}): {e}")
|
||||
logger.error(
|
||||
f"Error in main loop (attempt {consecutive_errors}/{max_consecutive_errors}): {e}"
|
||||
)
|
||||
|
||||
# If too many consecutive errors, wait longer
|
||||
if consecutive_errors >= max_consecutive_errors:
|
||||
logger.error(f"Too many consecutive errors ({max_consecutive_errors}). Waiting 5 minutes before retry.")
|
||||
logger.error(
|
||||
f"Too many consecutive errors ({max_consecutive_errors}). Waiting 5 minutes before retry."
|
||||
)
|
||||
await asyncio.sleep(300)
|
||||
consecutive_errors = 0
|
||||
else:
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
from unittest.mock import MagicMock
|
||||
from gitea.client import GiteaClient
|
||||
from gitea.tools.gitea_tools import GiteaTools
|
||||
|
||||
|
||||
def test_gitea_tools_delegation() -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
gitea_tools: GiteaTools = GiteaTools(mock_client)
|
||||
|
||||
# 1. get_issue
|
||||
gitea_tools.get_issue("owner", "repo", 1)
|
||||
mock_client.get_issue.assert_called_once_with("owner", "repo", 1)
|
||||
|
||||
# 2. get_pull_request
|
||||
gitea_tools.get_pull_request("owner", "repo", 2)
|
||||
mock_client.get_pull_request.assert_called_once_with("owner", "repo", 2)
|
||||
|
||||
# 3. close_issue
|
||||
gitea_tools.close_issue("owner", "repo", 3)
|
||||
mock_client.close_issue.assert_called_once_with("owner", "repo", 3)
|
||||
|
||||
# 4. close_pull_request
|
||||
gitea_tools.close_pull_request("owner", "repo", 4)
|
||||
mock_client.close_pull_request.assert_called_once_with("owner", "repo", 4)
|
||||
|
||||
# 5. get_issue_comments
|
||||
gitea_tools.get_issue_comments("owner", "repo", 5)
|
||||
mock_client.get_issue_comments.assert_called_once_with("owner", "repo", 5)
|
||||
|
||||
# 6. get_pull_request_comments
|
||||
gitea_tools.get_pull_request_comments("owner", "repo", 6)
|
||||
mock_client.get_pull_request_comments.assert_called_once_with("owner", "repo", 6)
|
||||
|
||||
# 7. list_assigned_issues
|
||||
mock_client.list_all_user_repos.return_value = []
|
||||
gitea_tools.list_assigned_issues()
|
||||
mock_client.list_all_user_repos.assert_called()
|
||||
|
||||
# 8. list_assigned_pull_requests
|
||||
gitea_tools.list_assigned_pull_requests()
|
||||
mock_client.list_all_user_repos.assert_called()
|
||||
|
||||
# 9. list_issues
|
||||
gitea_tools.list_issues("owner", "repo")
|
||||
mock_client.list_repo_issues.assert_called_once_with("owner", "repo", "open")
|
||||
|
||||
# 10. list_pull_requests
|
||||
gitea_tools.list_pull_requests("owner", "repo")
|
||||
mock_client.list_repo_pull_requests.assert_called_once_with("owner", "repo", "open")
|
||||
|
||||
# 11. get_file_content
|
||||
gitea_tools.get_file_content("owner", "repo", "path")
|
||||
mock_client.get_file_content.assert_called_with("owner", "repo", "path")
|
||||
|
||||
# 12. create_pull_request
|
||||
gitea_tools.create_pull_request("owner", "repo", "head", "base", "title", "desc")
|
||||
mock_client.create_pr_via_tea.assert_called_once_with("owner", "repo", "title", "desc", "head", "base")
|
||||
|
||||
# 13. create_issue
|
||||
gitea_tools.create_issue("owner", "repo", "title", "body")
|
||||
mock_client.create_issue.assert_called_once_with("owner", "repo", "title", "body", None, None)
|
||||
|
||||
# 14. create_branch
|
||||
gitea_tools.create_branch("owner", "repo", "branch", "sha")
|
||||
mock_client.create_ref.assert_called_once_with("owner", "repo", "branch", "sha")
|
||||
|
||||
# 15. commit_file
|
||||
gitea_tools.commit_file("owner", "repo", "path", "msg", "content", "branch")
|
||||
mock_client.update_file.assert_called_with("owner", "repo", "path", "msg", "content", "branch")
|
||||
|
||||
# 16. add_label_to_issue
|
||||
gitea_tools.add_label_to_issue("owner", "repo", 1, "bug")
|
||||
mock_client.add_label.assert_called_with("owner", "repo", 1, "bug")
|
||||
|
||||
# 17. add_label_to_pr
|
||||
gitea_tools.add_label_to_pr("owner", "repo", 1, "bug")
|
||||
mock_client.add_label_pr.assert_called_once_with("owner", "repo", 1, "bug")
|
||||
|
||||
# 18. add_comment_to_issue
|
||||
gitea_tools.add_comment_to_issue("owner", "repo", 1, "body")
|
||||
mock_client.add_comment.assert_called_with("owner", "repo", 1, "body")
|
||||
|
||||
# 19. get_pull_request_diff
|
||||
gitea_tools.get_pull_request_diff("owner", "repo", 1)
|
||||
mock_client.get_pull_request_diff.assert_called_once_with("owner", "repo", 1)
|
||||
|
||||
# 20. get_pull_request_patch
|
||||
gitea_tools.get_pull_request_patch("owner", "repo", 1)
|
||||
mock_client.get_pull_request_patch.assert_called_once_with("owner", "repo", 1)
|
||||
|
||||
# 21. approve_pull_request
|
||||
gitea_tools.approve_pull_request("owner", "repo", 1, "good")
|
||||
mock_client.approve_pr.assert_called_once_with("owner", "repo", 1, "good")
|
||||
|
||||
# 22. request_changes
|
||||
gitea_tools.request_changes("owner", "repo", 1, "bad")
|
||||
mock_client.request_changes_pr.assert_called_once_with("owner", "repo", 1, "bad")
|
||||
|
||||
# 23. add_comment
|
||||
gitea_tools.add_comment("owner", "repo", 1, "body")
|
||||
mock_client.add_comment.assert_called_with("owner", "repo", 1, "body")
|
||||
|
||||
# 24. add_label
|
||||
gitea_tools.add_label("owner", "repo", 1, "bug")
|
||||
mock_client.add_label.assert_called_with("owner", "repo", 1, "bug")
|
||||
|
||||
# 25. get_file_content_with_ref
|
||||
gitea_tools.get_file_content_with_ref("owner", "repo", "path", "ref")
|
||||
mock_client.get_file_content.assert_called_with("owner", "repo", "path", "ref")
|
||||
|
||||
# 26. update_file
|
||||
gitea_tools.update_file("owner", "repo", "path", "msg", "content", "branch")
|
||||
mock_client.update_file.assert_called_with("owner", "repo", "path", "msg", "content", "branch")
|
||||
Reference in New Issue
Block a user