Feat/tool based coordinator routing (#5)
Co-authored-by: Michael <michael@example.com> Reviewed-on: #5
This commit is contained in:
+91
-79
@@ -10,10 +10,11 @@ from gitea.tools.research_tools import ResearchTools
|
||||
from gitea.tools.gitea_tools import GiteaTools
|
||||
from gitea.client import GiteaClient
|
||||
from core.coding_prompt import CODING_AGENT_SYSTEM_PROMPT
|
||||
from core.coordinator_tools import CoordinatorTools
|
||||
from core.prompts import COORDINATOR_SYSTEM_PROMPT
|
||||
from gitea.config import AGENT_MODEL_ID
|
||||
from gitea.workspace import WorkspaceManager
|
||||
from gitea.models import CommentModel, PullRequestFileModel, PullRequestModel, IssueModel
|
||||
|
||||
logger: logging.Logger = logging.getLogger("agent-dispatcher")
|
||||
|
||||
AGENT_USERNAMES: frozenset[str] = frozenset({"meeks-ai", "agent-bot"})
|
||||
@@ -319,44 +320,6 @@ class AgentDispatcher:
|
||||
f"PR Reviews:\n{reviews_str}"
|
||||
)
|
||||
|
||||
STATE_ANALYSIS_SYSTEM_PROMPT = (
|
||||
"You are an AI Coordinator. Your job is to analyze Gitea issues and pull requests, "
|
||||
"read the conversation history, and determine the next action for the agent.\n\n"
|
||||
"You must choose one of the following actions:\n"
|
||||
"1. `PROPOSE_PLAN`: Choose this if code changes are needed to resolve the issue, and either:\n"
|
||||
" - No plan has been proposed yet by the AI agent.\n"
|
||||
" - Or a plan was proposed, but the human replied with feedback, corrections, or requests for changes, so we need to propose a revised plan.\n"
|
||||
" You will write a detailed, step-by-step implementation plan (listing files to modify/create, specific changes to make, verification/test commands).\n"
|
||||
" Your output must include a comment to post, starting with the plan and ending with a question asking if the plan is OK or if they have comments.\n"
|
||||
" CRITICAL: The comment in the `comment_body` field must contain ONLY the implementation plan and the trailing confirmation question. DO NOT include your thought process, reasoning, research notes, or file analysis in the comment. Keep it concise, professional, and limited strictly to the plan itself. Any internal reasoning should be placed in the `reasoning` field of the JSON instead.\n"
|
||||
" CRITICAL: The comment must contain the tags `<!-- agent:plan-proposal -->` and `<!-- agent:awaiting-reply -->` on separate lines at the very end of the comment.\n\n"
|
||||
"2. `ANSWER_QUESTION`: Choose this if the issue is just a question or request for information (no code changes needed), and either:\n"
|
||||
" - No answer has been provided yet by the AI agent.\n"
|
||||
" - Or the agent answered, but the human replied with follow-up questions or clarifications.\n"
|
||||
" You will formulate a clear, helpful answer to the question.\n"
|
||||
" Your output must include a comment to post, starting with the answer and ending with a question asking if this was a good enough answer.\n"
|
||||
" CRITICAL: The comment in the `comment_body` field must contain ONLY the actual answer to the question and the trailing confirmation question. DO NOT include your thought process, reasoning, research notes, or file analysis in the comment. Keep it clean, concise, helpful, and limited strictly to the answer itself. Any internal reasoning should be placed in the `reasoning` field of the JSON instead.\n"
|
||||
" CRITICAL: The comment must contain the tags `<!-- agent:question-response -->` and `<!-- agent:awaiting-reply -->` on separate lines at the very end of the comment.\n\n"
|
||||
"3. `EXECUTE_PLAN`: Choose this if:\n"
|
||||
" - A plan was previously proposed (check the comment history) AND the human has clearly replied with approval/greenlight/go-ahead (e.g. 'yes', 'looks good', 'ok', 'go ahead', etc.).\n"
|
||||
" - OR there is an existing WIP PR or a PR with requested changes, and we need to continue/resume implementing the changes.\n"
|
||||
" You will extract or summarize the approved plan, incorporating any feedback the human gave in their approval/reviews.\n\n"
|
||||
"4. `CLOSE_ISSUE`: Choose this if the AI agent previously answered a question (using `<!-- agent:question-response -->`) and the human has replied confirming they are satisfied or giving approval to close (e.g., 'yes', 'looks good', 'thanks', 'close it', etc.).\n"
|
||||
" You will write a polite final comment to post on the issue.\n\n"
|
||||
"5. `NO_ACTION`: Choose this if the issue/PR is already resolved, or if we cannot proceed for another reason.\n\n"
|
||||
"You MUST respond ONLY with a JSON object inside a ```json markdown code block. Do not include other text.\n"
|
||||
"CRITICAL: The `comment_body` field in the JSON must contain ONLY the implementation plan or the answer itself, and MUST NOT contain any thought process, reasoning, or internal details. Place all thought process and internal reasoning in the `reasoning` field.\n"
|
||||
"Example:\n"
|
||||
"```json\n"
|
||||
"{\n"
|
||||
" \"action\": \"PROPOSE_PLAN\",\n"
|
||||
" \"reasoning\": \"No plan has been proposed yet. We need to implement ...\",\n"
|
||||
" \"comment_body\": \"### Proposed Implementation Plan\\n1. Modify X\\n2. Run Y\\n\\nIs this ok for implementation?\\n<!-- agent:plan-proposal -->\\n<!-- agent:awaiting-reply -->\",\n"
|
||||
" \"approved_plan\": \"\"\n"
|
||||
"}\n"
|
||||
"```"
|
||||
)
|
||||
|
||||
state_analysis_mission = (
|
||||
f"Analyzing issue #{item.task_number} in '{repo}'.\n\n"
|
||||
f"Issue Title: {title}\n"
|
||||
@@ -365,55 +328,103 @@ class AgentDispatcher:
|
||||
f"Existing PR Details:\n{pr_info_str}\n"
|
||||
)
|
||||
|
||||
# Run Coordinator Agent with CoordinatorTools
|
||||
coord_tools = CoordinatorTools()
|
||||
coord_tools_list = [
|
||||
coord_tools.propose_plan,
|
||||
coord_tools.start_implementation,
|
||||
coord_tools.answer_question,
|
||||
coord_tools.close_issue,
|
||||
coord_tools.take_no_action,
|
||||
]
|
||||
combined_tools = planning_tools + coord_tools_list
|
||||
|
||||
logger.info(f"Analyzing conversation state for issue #{item.task_number}...")
|
||||
planning_agent = CodingAgent(self._model_name)
|
||||
planning_agent.system_prompt = STATE_ANALYSIS_SYSTEM_PROMPT
|
||||
plan_response = await planning_agent.run_with_tools(state_analysis_mission, planning_tools)
|
||||
logger.info(f"State analyzer returned: {plan_response}")
|
||||
|
||||
import json
|
||||
decision = {}
|
||||
json_match = re.search(r"```json\s*(.*?)\s*```", plan_response, re.DOTALL)
|
||||
if json_match:
|
||||
json_str = json_match.group(1).strip()
|
||||
else:
|
||||
json_str = plan_response.strip()
|
||||
|
||||
try:
|
||||
decision = json.loads(json_str)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse planning agent decision JSON: {e}. Attempting manual extraction.")
|
||||
planning_agent.system_prompt = COORDINATOR_SYSTEM_PROMPT
|
||||
|
||||
# Let the coordinator analyze and route
|
||||
response_text = await planning_agent.run_with_tools(state_analysis_mission, combined_tools)
|
||||
|
||||
# Fallback if no tool was called
|
||||
if not coord_tools.tool_called:
|
||||
logger.info(f"Coordinator agent did not call any tools. Falling back to JSON text parsing.")
|
||||
import json
|
||||
decision = {}
|
||||
json_match = re.search(r"```json\s*(.*?)\s*```", response_text, re.DOTALL)
|
||||
if json_match:
|
||||
json_str = json_match.group(1).strip()
|
||||
else:
|
||||
json_str = response_text.strip()
|
||||
try:
|
||||
start_idx = json_str.find('{')
|
||||
end_idx = json_str.rfind('}')
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
decision = json.loads(json_str[start_idx:end_idx+1])
|
||||
except Exception:
|
||||
pass
|
||||
decision = json.loads(json_str)
|
||||
except Exception as e:
|
||||
try:
|
||||
start_idx = json_str.find('{')
|
||||
end_idx = json_str.rfind('}')
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
decision = json.loads(json_str[start_idx:end_idx+1])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if decision and "action" in decision:
|
||||
coord_tools.action = decision["action"]
|
||||
if coord_tools.action == "PROPOSE_PLAN":
|
||||
coord_tools.arguments = {
|
||||
"comment_body": decision.get("comment_body", "") or decision.get("reasoning", ""),
|
||||
"issue_number": item.task_number
|
||||
}
|
||||
elif coord_tools.action == "ANSWER_QUESTION":
|
||||
coord_tools.arguments = {
|
||||
"comment_body": decision.get("comment_body", "") or decision.get("reasoning", ""),
|
||||
"issue_number": item.task_number
|
||||
}
|
||||
elif coord_tools.action == "CLOSE_ISSUE":
|
||||
coord_tools.arguments = {
|
||||
"comment": decision.get("comment_body", "Closing the issue as resolved."),
|
||||
"issue_number": item.task_number
|
||||
}
|
||||
elif coord_tools.action == "EXECUTE_PLAN":
|
||||
coord_tools.arguments = {
|
||||
"approved_plan": decision.get("approved_plan", ""),
|
||||
"issue_number": item.task_number
|
||||
}
|
||||
|
||||
if not decision or "action" not in decision:
|
||||
logger.info("Fallback: assuming PROPOSE_PLAN and using raw plan_response")
|
||||
decision = {
|
||||
"action": "PROPOSE_PLAN",
|
||||
"comment_body": f"### Proposed Implementation Plan\n\n{plan_response}\n\nIs this plan ok for implementation or do you have any comments/changes?\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->",
|
||||
"approved_plan": ""
|
||||
}
|
||||
action = coord_tools.action
|
||||
logger.info(f"Coordinator Decided Action: {action} (tool_called={coord_tools.tool_called})")
|
||||
|
||||
action = decision.get("action", "NO_ACTION")
|
||||
reasoning = decision.get("reasoning", "")
|
||||
logger.info(f"Decided Action: {action}. Reasoning: {reasoning}")
|
||||
|
||||
if action in ("PROPOSE_PLAN", "ANSWER_QUESTION"):
|
||||
comment_body = decision.get("comment_body", "")
|
||||
if action == "PROPOSE_PLAN":
|
||||
comment_body = coord_tools.arguments.get("comment_body", "")
|
||||
if not comment_body:
|
||||
comment_body = decision.get("reasoning", "No details provided.")
|
||||
plan = coord_tools.arguments.get("plan", "")
|
||||
comment_body = (
|
||||
f"### Proposed Implementation Plan\n\n"
|
||||
f"{plan}\n\n"
|
||||
f"Is this plan ok for implementation or do you have any comments/changes?\n"
|
||||
f"<!-- agent:plan-proposal -->\n"
|
||||
f"<!-- agent:awaiting-reply -->"
|
||||
)
|
||||
self._client.add_comment(owner, repo_name, item.task_number, comment_body)
|
||||
results.append(f"POSTED_COMMENT: {action} comment posted to issue #{item.task_number}.")
|
||||
results.append(f"POSTED_COMMENT: PROPOSE_PLAN comment posted to issue #{item.task_number}.")
|
||||
break
|
||||
|
||||
elif action == "ANSWER_QUESTION":
|
||||
comment_body = coord_tools.arguments.get("comment_body", "")
|
||||
if not comment_body:
|
||||
answer = coord_tools.arguments.get("answer", "")
|
||||
comment_body = (
|
||||
f"{answer}\n\n"
|
||||
f"Is this answer satisfactory?\n"
|
||||
f"<!-- agent:question-response -->\n"
|
||||
f"<!-- agent:awaiting-reply -->"
|
||||
)
|
||||
self._client.add_comment(owner, repo_name, item.task_number, comment_body)
|
||||
results.append(f"POSTED_COMMENT: ANSWER_QUESTION comment posted to issue #{item.task_number}.")
|
||||
break
|
||||
|
||||
elif action == "CLOSE_ISSUE":
|
||||
comment_body = decision.get("comment_body", "Closing the issue as resolved.")
|
||||
self._client.add_comment(owner, repo_name, item.task_number, comment_body)
|
||||
comment = coord_tools.arguments.get("comment", "Closing the issue as resolved.")
|
||||
self._client.add_comment(owner, repo_name, item.task_number, comment)
|
||||
self._client.close_issue(owner, repo_name, item.task_number)
|
||||
results.append(f"CLOSED_ISSUE: Issue #{item.task_number} closed.")
|
||||
break
|
||||
@@ -423,6 +434,7 @@ class AgentDispatcher:
|
||||
break
|
||||
|
||||
elif action == "EXECUTE_PLAN":
|
||||
approved_plan = coord_tools.arguments.get("approved_plan", "")
|
||||
pr_to_use = existing_pr
|
||||
branch_name = ""
|
||||
|
||||
@@ -472,7 +484,7 @@ class AgentDispatcher:
|
||||
f"PHASE 2: EXECUTION/CODING PHASE\n\n"
|
||||
f"You are implementing changes for issue #{item.task_number} in repository '{repo}'.\n"
|
||||
f"You are working on the existing Pull Request #{pr_to_use.number} on branch '{branch_name}'.\n\n"
|
||||
f"--- APPROVED PLAN ---\n{decision.get('approved_plan', '')}\n--- APPROVED PLAN END ---\n\n"
|
||||
f"--- APPROVED PLAN ---\n{approved_plan}\n--- APPROVED PLAN END ---\n\n"
|
||||
f"Original Mission details:\n{base_mission}\n\n"
|
||||
f"DIRECTIONS:\n"
|
||||
f"1. Checkout the branch '{branch_name}' (it should already be checked out, or run `git checkout {branch_name}`).\n"
|
||||
|
||||
Reference in New Issue
Block a user