This commit is contained in:
LanWeiFRJ
2025-10-29 14:02:04 +08:00
parent c6b8c4bbd8
commit e334c4f764
52 changed files with 12251 additions and 0 deletions

275
src/managers/loop/base.py Normal file
View File

@@ -0,0 +1,275 @@
from typing import Any, Dict, List
import json
from traceback import format_exc
from src.managers.log.logger import Logger
from src.managers.llm_api.api_manager import LLMAPIManager
from src.managers.prompts.prompts_manager import PromptsManager
from src.tools.base import (
ToolExecutor,
BASH_TOOL_NAME,
STR_REPLACE_BASED_EDIT_TOOL_NAME,
SEARCH_TOOL_NAME,
SUBMIT_RESULT_TOOL_NAME,
)
from src.managers.loop.types import ToolStats, LLMUsage
class BaseLoop:
def __init__(self, instance_id: str, instance_data: Dict[str, Any], logger: Logger, prompts_manager: PromptsManager | None, llm_manager: LLMAPIManager | None, tool_executor: ToolExecutor, config: Dict[str, Any] | None = None):
self.instance_id = instance_id
self.instance_data = instance_data
self.logger = logger
self.prompts_manager = prompts_manager
self.llm_manager = llm_manager
self.tool_executor = tool_executor
self.config = config or {}
self.component_name = self.__class__.__name__
def _make_assistant(
self, content: str | None, tool_calls: Any, messages: List[Dict[str, Any]]
) -> bool:
"""
Construct an assistant message based on the current content and tool calls, and append it to the messages.
"""
safe_content = content or ""
if not safe_content and not tool_calls:
self.logger.warning(
f"[{self.component_name}] Assistant returned an empty message with no tool calls; skipping this message and prompting to continue"
)
messages.append(
{"role": "user", "content": "请继续分析问题并使用工具来解决问题。"}
)
return False
assistant_message: Dict[str, Any] = {"role": "assistant"}
if tool_calls and not safe_content:
assistant_message["content"] = ""
elif safe_content:
assistant_message["content"] = safe_content
if tool_calls:
assistant_message["tool_calls"] = tool_calls
messages.append(assistant_message)
return True
def _make_tool_response(
self, tool_results: List[Any], messages: List[Dict[str, Any]]
) -> None:
"""Convert tool execution results into standard tool messages (role=tool) and append them to the messages.
- Generate content per result: use prompts_manager.tool_response_prompts([{...}]) to produce the content
- Set tool_call_id: prefer ToolResult.id; fallback to ToolResult.call_id
"""
if not tool_results:
return
for result in tool_results:
single_dict = [
{
"name": getattr(result, "name", "unknown"),
"success": getattr(result, "success", False),
"result": getattr(result, "result", None) or "",
"error": getattr(result, "error", None) or "",
}
]
content_text = (
self.prompts_manager.tool_response_prompts(single_dict)
if self.prompts_manager
else ""
)
tool_call_id = getattr(result, "id", None) or getattr(
result, "call_id", None
)
messages.append(
{
"role": "tool",
"content": content_text,
"tool_call_id": tool_call_id,
}
)
def _response_log(
self, response: Any, first_content: str, first_tool_calls: Any, total_turns: int
) -> None:
"""notice log for the current turn's LLM output"""
try:
response_log: Dict[str, Any] = {}
if hasattr(response, "usage") and response.usage:
response_log["usage"] = {
"prompt_tokens": getattr(response.usage, "prompt_tokens", None),
"completion_tokens": getattr(response.usage, "completion_tokens", None),
"total_tokens": getattr(response.usage, "total_tokens", None),
}
if hasattr(response, "choices") and response.choices:
response_log["choice"] = {
"message": {
"content": first_content,
"tool_calls": first_tool_calls,
}
}
if response_log:
self.logger.notice(
f"[{self.component_name}] The {total_turns}th turn output: {json.dumps(response_log, ensure_ascii=False)}"
)
else:
self.logger.notice(
f"[{self.component_name}] The {total_turns}th turn output: {str(response)}"
)
except Exception:
self.logger.notice(
f"[{self.component_name}] 第 {total_turns} 轮: LLM 输出序列化失败,使用字符串表示: {str(response)}, traceback: {format_exc()}."
)
def _debug_messages(
self, turn: int, messages: List[Dict[str, Any]], prefix_len: int = 300
) -> None:
"""debug log for the messages to be sent to the model"""
try:
self.logger.debug(f"[{self.component_name}] msg:")
recent_messages = messages[-2:] if len(messages) > 2 else messages
base_index = len(messages) - len(recent_messages)
for offset, msg in enumerate[Dict[str, Any]](recent_messages):
idx = base_index + offset
role = msg.get("role")
content = msg.get("content")
content_str = content if isinstance(content, str) else ""
preview = content_str[:prefix_len]
content_len = len(content_str)
extra = ""
if role == "assistant":
tool_calls = msg.get("tool_calls")
has_tool = tool_calls is not None and tool_calls != []
try:
tool_calls_json = json.dumps(tool_calls, ensure_ascii=False)
except Exception:
self.logger.warning(
f"[{self.component_name}] In debug_messages function, fail: {format_exc()}, tool calls: {tool_calls}."
)
tool_calls_json = str(tool_calls)
extra = f", has_tool_calls={has_tool}, tool_calls={tool_calls_json}"
elif role == "tool":
tool_call_id = msg.get("tool_call_id")
extra = f", tool_call_id={tool_call_id}"
self.logger.debug(
f"[{self.component_name}] {turn+1}th, msg#{idx}: role={role}, content_len={content_len}, content_preview={json.dumps(preview, ensure_ascii=False)}{extra}"
)
except Exception:
self.logger.warning(
f"[{self.component_name}] In debug_messages function, fail msg: {format_exc()}."
)
def _debug_last_message(
self, turn: int, messages: List[Dict[str, Any]], prefix_len: int = 300
) -> None:
"""debug last turn msg"""
try:
if not messages:
return
last_assistant_idx = None
for i in range(len(messages) - 1, -1, -1):
if messages[i].get("role") == "assistant":
last_assistant_idx = i
break
if last_assistant_idx is None:
return
msg = messages[last_assistant_idx]
content = msg.get("content")
content_str = content if isinstance(content, str) else ""
preview = content_str[:prefix_len]
content_len = len(content_str)
tool_calls = msg.get("tool_calls")
has_tool = tool_calls is not None and tool_calls != []
try:
tool_calls_json = json.dumps(tool_calls, ensure_ascii=False)
except Exception:
self.logger.warning(
f"[{self.component_name}] In debug_last_message function, fail: {format_exc()}, tool calls: {tool_calls}."
)
tool_calls_json = str(tool_calls)
self.logger.debug(
f"[{self.component_name}] {turn+1}th turn, output_preview: role=assistant, content_len={content_len}, content_preview={json.dumps(preview, ensure_ascii=False)}, has_tool_calls={has_tool}, tool_calls={tool_calls_json}"
)
except Exception:
self.logger.warning(
f"[{self.component_name}] In debug_last_message function, last turn fail: {format_exc()}."
)
def _debug_tools(self, tools: List[Dict[str, Any]]) -> None:
"""debug tools msg"""
try:
self.logger.debug(f"[{self.component_name}] tools num: {len(tools)}")
for i, tool in enumerate(tools):
try:
tool_json = json.dumps(tool, ensure_ascii=False)
self.logger.debug(f"[{self.component_name}] tool #{i+1}: {tool_json}")
except Exception:
self.logger.debug(
f"[{self.component_name}] tool #{i+1} fail: {format_exc()}, string: {str(tool)}."
)
except Exception:
try:
self.logger.warning(
f"[{self.component_name}] fail; traceback: {format_exc()}."
)
self.logger.warning(f"[{self.component_name}] tools string: {str(tools)}")
except Exception:
pass
def _get_tools(self) -> List[Dict[str, Any]]:
pass
def _is_bash_tool(self, tool_name: str) -> bool:
return BASH_TOOL_NAME in tool_name
def _is_edit_tool(self, tool_name: str) -> bool:
return "edit" in tool_name or "str_replace" in tool_name or STR_REPLACE_BASED_EDIT_TOOL_NAME in tool_name
def _is_search_tool(self, tool_name: str) -> bool:
return SEARCH_TOOL_NAME in tool_name or "search" in tool_name
def _is_submit_result_tool(self, tool_name: str) -> bool:
return SUBMIT_RESULT_TOOL_NAME in tool_name
def _update_usage(self, response: Any, usage_stats: LLMUsage) -> None:
if hasattr(response, "usage") and response.usage:
usage_stats.prompt_tokens += int(getattr(response.usage, "prompt_tokens", 0) or 0)
usage_stats.completion_tokens += int(
getattr(response.usage, "completion_tokens", 0) or 0
)
usage_stats.total_tokens += int(getattr(response.usage, "total_tokens", 0) or 0)
def _init_usage_stats(self) -> LLMUsage:
return LLMUsage()
def _init_tools_stats(self) -> ToolStats:
return ToolStats()
def _update_tool_call_statistic(
self, tool_results: List[Any], tool_stats: ToolStats
) -> None:
for result in tool_results:
try:
tool_name = getattr(result, "name", "")
tool_name = tool_name.lower() if isinstance(tool_name, str) else ""
success = bool(getattr(result, "success", False))
if self._is_bash_tool(tool_name):
tool_stats.bash["count"] += 1
if not success:
tool_stats.bash["failed"] += 1
elif self._is_edit_tool(tool_name):
tool_stats.edit["count"] += 1
if not success:
tool_stats.edit["failed"] += 1
elif self._is_search_tool(tool_name):
tool_stats.search["count"] += 1
if not success:
tool_stats.search["failed"] += 1
elif self._is_submit_result_tool(tool_name):
tool_stats.submit_result["count"] += 1
if not success:
tool_stats.submit_result["failed"] += 1
except Exception:
continue

View File

@@ -0,0 +1,339 @@
from typing import Any, Dict, List
import json
from traceback import format_exc
from src.managers.log.logger import Logger
from src.managers.llm_api.api_manager import LLMAPIManager
from src.managers.prompts.prompts_manager import PromptsManager
from src.managers.loop.base import BaseLoop
from src.tools.base import (
ToolExecutor,
ToolResult,
SubmitToolResult,
BASH_TOOL_NAME,
STR_REPLACE_BASED_EDIT_TOOL_NAME,
SEARCH_TOOL_NAME,
SUBMIT_RESULT_TOOL_NAME,
)
class PatchGenerator(BaseLoop):
def __init__(
self,
instance_id: str,
instance_data: Dict[str, Any],
logger: Logger,
prompts_manager: PromptsManager | None,
llm_manager: LLMAPIManager | None,
tool_executor: ToolExecutor,
config: Dict[str, Any] | None = None,
) -> None:
super().__init__(instance_id, instance_data, logger, prompts_manager, llm_manager, tool_executor, config)
async def _submit_all_tool_calls(
self, other_tool_calls: List[Dict[str, Any]]
) -> List[Any]:
"""execute tool calls, return tool execution results list"""
if not other_tool_calls:
return []
from src.tools.base import ToolCall
tool_call_objects = []
for tool_call_dict in other_tool_calls:
raw_args = tool_call_dict.get("function", {}).get("arguments", {})
parsed_args = raw_args
if isinstance(raw_args, str):
try:
parsed_args = json.loads(raw_args)
except Exception as e:
self.logger.warning(f"[{self.component_name}] In _submit_all_tool_calls function, fail: {e}, traceback: {format_exc()}, args: {raw_args}.")
parsed_args = {}
tool_call_obj = ToolCall(
name=tool_call_dict.get("function", {}).get("name", ""),
call_id=tool_call_dict.get("id", ""),
arguments=parsed_args,
id=tool_call_dict.get("id", ""),
)
tool_call_objects.append(tool_call_obj)
return await self.tool_executor.container_sequential_tool_call(
tool_call_objects
)
def _process_submit_result_tool_result(
self,
submit_result: ToolResult,
golden_patch: List[Dict[str, Any]],
) -> None:
"""process submit_result tool call, fill golden_patch and log"""
if not submit_result.success or not submit_result.result:
self.logger.warning(f"[{self.component_name}] submit_result failed and no result.")
return
try:
submit_tool_result = SubmitToolResult.from_string(submit_result.result)
if submit_tool_result.output:
patch_info = {
"patch_content": submit_tool_result.output,
"test_status": submit_tool_result.test_status,
"reasoning": submit_tool_result.reasoning,
}
golden_patch.clear()
golden_patch.append(patch_info)
self.logger.info(
f"[{self.component_name}] patch len: {len(submit_tool_result.output)}."
)
self.logger.info(
f"[{self.component_name}] test status: {submit_tool_result.test_status}."
)
self.logger.info(
f"[{self.component_name}] reasoning: {submit_tool_result.reasoning[:100]}..."
)
else:
self.logger.warning(
f"[{self.component_name}] submit_result success but no patch content."
)
except Exception as e:
self.logger.error(f"[{self.component_name}] parse submit_result result fail: {e}, traceback: {format_exc()}.")
def _get_tools(self) -> List[Dict[str, Any]]:
tools = []
#use_openai_format = self._should_use_openai_format()
use_openai_format = True
for tool in self.tool_executor.tools.values():
if use_openai_format:
tool_def = tool._definition_for_openai_fmt()
else:
tool_def = tool._definition_for_claude_fmt()
tools.append(tool_def)
return tools
def _should_use_openai_format(self) -> bool:
if not self.llm_manager or not hasattr(self.llm_manager, "get_model_name"):
return True # openAI format by default
model_name = self.llm_manager.get_model_name().lower()
return "claude" not in model_name
def _get_issue_prompt(self) -> str:
"""generate issue prompt based on instance data"""
if not self.prompts_manager:
self.logger.warning("PromptsManager not initialized, cannot generate issue prompt.")
return ""
#instance_id = self.instance_data.get("instance_id", "")
#repo = self.instance_data.get("repo", "")
created_at = self.instance_data.get("created_at", "")
base_commit = self.instance_data.get("base_commit", "")
environment_setup_commit = self.instance_data.get(
"environment_setup_commit", ""
)
version = self.instance_data.get("version", "")
problem_statement = self.instance_data.get("problem_statement", "")
difficulty = self.instance_data.get("difficulty", "")
return self.prompts_manager.format_issue_prompt(
created_at=created_at,
base_commit=base_commit,
environment_setup_commit=environment_setup_commit,
version=version,
problem_statement=problem_statement,
difficulty=difficulty,
)
async def _generate_patch(self) -> Dict[str, Any] | None:
"""main loop logic for generating candidate patch"""
usage_stats = self._init_usage_stats()
tool_stats = self._init_tools_stats()
if not self.llm_manager or not self.prompts_manager:
self.logger.error(f"[{self.component_name}] LLM manager or prompts manager not initialized.")
return {
"success": False,
"golden_patch": [],
"llm_usage": usage_stats.to_dict(),
"tool_stats": tool_stats.to_dict(),
"total_turns": 0,
}
tools = self._get_tools()
self._debug_tools(tools)
root_path = self.config.get("builder", {}).get("repo_root_path", "")
max_turn = (
self.config.get("runner", {}).get("generator_loop", {}).get("max_turn", 10)
)
temperature = (
self.config.get("runner", {})
.get("generator_loop", {})
.get("temperature", 0.2)
)
issue_prompt = self._get_issue_prompt()
user_prompt = self.prompts_manager.get_generator_user(root_path, issue_prompt)
system_prompt = self.prompts_manager.get_generator_system(root_path)
total_turns = 0
golden_patch = []
try:
self.logger.info(
f"[{self.component_name}] {self.instance_id}: start generating candidate patch, max turn: {max_turn}"
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
self.logger.notice(
f"[{self.component_name}]: {json.dumps(messages[0], ensure_ascii=False)}"
)
self.logger.notice(
f"[{self.component_name}]: {json.dumps(messages[1], ensure_ascii=False)}"
)
for turn in range(max_turn):
total_turns = turn + 1
self.logger.info(f"[{self.component_name}] The {total_turns}th turn started.")
try:
current_input_msg = messages[-1] if messages else None
if current_input_msg is not None:
self.logger.notice(
f"[{self.component_name}] The {total_turns}th turn input: {json.dumps(current_input_msg, ensure_ascii=False)}"
)
except Exception as e:
self.logger.warning(
f"[{self.component_name}] {total_turns}th turn: LLM input fail: {messages[-1] if messages else None}, error: {e}, traceback: {format_exc()}."
)
self._debug_messages(turn, messages)
response = self.llm_manager.chat(
messages=messages,
tools=tools,
tool_choice="auto",
temperature=temperature,
)
first_content: str = ""
first_tool_calls: Any = None
if hasattr(response, "choices") and response.choices:
ch0 = response.choices[0]
first_content = (
getattr(getattr(ch0, "message", None), "content", None) or ""
)
first_tool_calls = getattr(
getattr(ch0, "message", None), "tool_calls", None
)
self._response_log(
response, first_content, first_tool_calls, total_turns
)
self._update_usage(response, usage_stats)
if hasattr(response, "choices") and response.choices:
content = first_content
tool_calls = first_tool_calls
if not self._make_assistant(content, tool_calls, messages):
continue
if tool_calls:
self.logger.info(
f"[{self.component_name}] {total_turns}th turn: call {len(tool_calls)} tools."
)
tool_results = await self._submit_all_tool_calls(tool_calls)
self._update_tool_call_statistic(tool_results, tool_stats)
if tool_results:
submit_result = None
other_tool_results = []
for tool_result in tool_results:
tool_name = getattr(tool_result, "name", "")
if tool_name == SUBMIT_RESULT_TOOL_NAME:
submit_result = tool_result
else:
other_tool_results.append(tool_result)
if submit_result:
self.logger.debug(
f"[{self.component_name}] {total_turns}th turn: got submit_result tool call."
)
self.logger.debug(f"[{self.component_name}] {total_turns}th turn: submit_result result: {submit_result}")
self._process_submit_result_tool_result(
submit_result, golden_patch
)
self._debug_last_message(turn, messages)
break
if other_tool_results:
self._make_tool_response(other_tool_results, messages)
else:
messages.append(
{
"role": "user",
"content": "请继续分析问题并使用工具来解决问题。",
}
)
self.logger.debug(f"[{self.component_name}] final golden_patch: {golden_patch}")
success = (
len(golden_patch) > 0 and golden_patch[0].get("patch_content", "") != ""
)
self.logger.info(
f"[{self.component_name}] status={success}, total_turns={total_turns}, tools_stats={tool_stats}"
)
result_payload = {
"success": success,
"golden_patch": golden_patch,
"llm_usage": usage_stats.to_dict(),
"tool_stats": tool_stats.to_dict(),
"total_turns": total_turns,
}
try:
self.logger.notice(
f"[{self.component_name}] final output: {json.dumps(result_payload, ensure_ascii=False)}"
)
except Exception as e:
self.logger.warning(
f"[{self.component_name}] output: {str(result_payload)}, error: {e}, traceback: {format_exc()}."
)
return result_payload
except Exception as e:
self.logger.error(f"[{self.component_name}] fail: {e}, traceback: {format_exc()}.")
result_payload = {
"success": False,
"golden_patch": [],
"llm_usage": usage_stats.to_dict(),
"tool_stats": tool_stats.to_dict(),
"total_turns": total_turns,
}
try:
self.logger.notice(
f"[{self.component_name}] 最终返回数据(失败): {json.dumps(result_payload, ensure_ascii=False)}"
)
except Exception as e:
self.logger.notice(
f"[{self.component_name}] 最终返回数据(失败, 字符串回退): {str(result_payload)}, error: {e}, traceback: {format_exc()}."
)
return result_payload

View File

@@ -0,0 +1,338 @@
from typing import Any, Dict, List
import json
from traceback import format_exc
from src.managers.log.logger import Logger
from src.managers.llm_api.api_manager import LLMAPIManager
from src.managers.prompts.prompts_manager import PromptsManager
from src.managers.loop.types import GeneratorResult, SelectorResult, LLMUsage, ToolStats, PatchInfo
from src.tools.base import ToolExecutor, ToolCall, ToolResult
from src.managers.loop.base import BaseLoop
SELECTOR_SUBMIT_TOOL_NAME = "submit_result"
class PatchSelector(BaseLoop):
def __init__(
self,
instance_id: str,
instance_data: Dict[str, Any],
logger: Logger,
prompts_manager: PromptsManager | None,
llm_manager: LLMAPIManager | None,
tool_executor: ToolExecutor,
config: Dict[str, Any] | None = None,
) -> None:
super().__init__(instance_id, instance_data, logger, prompts_manager, llm_manager, tool_executor, config)
def _get_submit_result_tool_name(self):
return SELECTOR_SUBMIT_TOOL_NAME
def _definition_for_submit_tool(self, use_openai_format: bool) -> Dict[str, Any]:
"""submit_result tool"""
if use_openai_format:
return {
"type": "function",
"function": {
"name": self._get_submit_result_tool_name(),
"description": "Submit the final selected patch index and reasoning.",
"parameters": {
"type": "object",
"properties": {
"index": {
"type": "integer",
"description": "The chosen patch index (0-based).",
},
"reason": {
"type": "string",
"description": "Detailed reasoning for the selection.",
},
},
"required": ["index", "reason"],
},
},
}
return {
"type": "function",
"function": {
"name": self._get_submit_result_tool_name(),
"description": "Submit the final selected patch index and reasoning.",
"parameters": {
"type": "object",
"properties": {
"index": {
"type": "integer",
"description": "The chosen patch index (0-based).",
},
"reason": {
"type": "string",
"description": "Detailed reasoning for the selection.",
},
},
"required": ["index", "reason"],
},
},
}
def _build_user_prompt(self, candidates: List[GeneratorResult], root_path: str) -> str:
if not self.prompts_manager:
return ""
return self.prompts_manager.get_selector_user(self.instance_data, candidates, root_path)
def _get_system_prompt(self, patches_count: int, root_path: str) -> str:
if not self.prompts_manager:
return ""
return self.prompts_manager.get_selector_system(patches_count, root_path)
def _get_tools(self) -> List[Dict[str, Any]]:
tool_defs: List[Dict[str, Any]] = []
try:
for tool in self.tool_executor.tools.values():
try:
tool_defs.append(tool._definition_for_openai_fmt())
except Exception:
continue
except Exception:
pass
tool_defs.append(self._definition_for_submit_tool(True))
return tool_defs
def _extract_submit_choice(self, tool_call: Dict[str, Any]) -> Dict[str, Any] | None:
if not tool_call:
return None
fn = tool_call.get("function", {})
if fn.get("name") != self._get_submit_result_tool_name():
return None
raw_args = fn.get("arguments", {})
try:
args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
except Exception:
args = {}
index = args.get("index")
reason = args.get("reason")
if isinstance(index, int) and index >= 0:
return {"index": index, "reason": reason or ""}
return None
async def _submit_other_tool_calls(
self, tool_calls: List[Dict[str, Any]]
) -> List[ToolResult]:
if not tool_calls:
return []
to_run: List[ToolCall] = []
for tool_call_dict in tool_calls:
fn = tool_call_dict.get("function", {})
name = fn.get("name", "")
if name == SELECTOR_SUBMIT_TOOL_NAME:
continue
raw_args = fn.get("arguments", {})
parsed_args = raw_args
if isinstance(raw_args, str):
try:
parsed_args = json.loads(raw_args)
except Exception:
parsed_args = {}
to_run.append(
ToolCall(
name=name,
call_id=tool_call_dict.get("id", ""),
arguments=parsed_args,
id=tool_call_dict.get("id", ""),
)
)
if not to_run:
return []
results: List[ToolResult] = await self.tool_executor.container_sequential_tool_call(to_run)
return results
async def _select_patch(self, candidates: List[GeneratorResult]) -> SelectorResult:
if not candidates:
raise ValueError("No candidates provided")
if not self.llm_manager:
raise ValueError("LLM manager is not initialized")
tools = self._get_tools()
self._debug_tools(tools)
root_path = self.config.get("builder", {}).get("repo_root_path", "")
system_prompt = self._get_system_prompt(len(candidates), root_path)
user_prompt = self._build_user_prompt(candidates, root_path)
messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
try:
self.logger.notice(
f"[{self.component_name}]: {json.dumps(messages[0], ensure_ascii=False)}"
)
self.logger.notice(
f"[{self.component_name}]: {json.dumps(messages[1], ensure_ascii=False)}"
)
except Exception:
self.logger.warning(
f"[{self.component_name}] Initial fail in selector loop: SP={str(messages[0])}, UP={str(messages[1])}, traceback: {format_exc()}."
)
max_turn = int(
self.config.get("runner", {})
.get("selector_loop", {})
.get("max_turn", 200)
)
temperature = (
self.config.get("runner", {})
.get("selector_loop", {})
.get("temperature", 0.2)
)
usage_stats = self._init_usage_stats()
tool_stats = self._init_tools_stats()
total_turns = 0
chosen_index: int | None = None
select_reason: str = ""
for turn in range(max_turn):
try:
try:
current_input_msg = messages[-1] if messages else None
if current_input_msg is not None:
self.logger.notice(
f"[{self.component_name}] The {turn+1}th turn input: {json.dumps(current_input_msg, ensure_ascii=False)}"
)
except Exception:
self.logger.warning(
f"[{self.component_name}] {turn+1}th turn fail: {messages[-1] if messages else None}, traceback: {format_exc()}."
)
self._debug_messages(turn, messages)
response = self.llm_manager.chat(
messages=messages,
tools=tools,
tool_choice="auto",
temperature=temperature,
)
first_tool_calls = None
if hasattr(response, "choices") and response.choices:
ch0 = response.choices[0]
first_tool_calls = getattr(getattr(ch0, "message", None), "tool_calls", None)
first_content = getattr(getattr(ch0, "message", None), "content", None) or ""
else:
first_content = ""
total_turns = turn + 1
self._response_log(response, first_content, first_tool_calls, turn + 1)
self._update_usage(response, usage_stats)
if first_tool_calls:
if not self._make_assistant(first_content, first_tool_calls, messages):
messages.append(
{
"role": "user",
"content": "请完成分析并调用 submit_result 工具给出最终选择与理由。",
}
)
continue
submit_found = False
for tc in first_tool_calls:
choice = self._extract_submit_choice(tc)
if choice is not None:
chosen_index = choice["index"]
reason = choice.get("reason", "")
self.logger.info(
f"[{self.component_name}] choose: index={chosen_index}, reason={reason}"
)
select_reason = reason or ""
submit_found = True
self._debug_last_message(turn, messages)
break
if not submit_found:
results = await self._submit_other_tool_calls(first_tool_calls)
self._make_tool_response(results, messages)
self._update_tool_call_statistic(results, tool_stats)
else:
messages.append(
{
"role": "user",
"content": "请完成分析并调用 submit_result 工具给出最终选择与理由。",
}
)
if chosen_index is not None:
break
except Exception as e:
self.logger.warning(
f"[{self.component_name}] fail: {e}, traceback: {format_exc()}"
)
break
if chosen_index is None:
# If the model provides no choice, fallback: pick the first successful one; otherwise the first
for i, r in enumerate(candidates):
try:
if r.success:
chosen_index = i
break
except Exception:
continue
if chosen_index is None:
chosen_index = 0
if not (0 <= chosen_index < len(candidates)):
chosen_index = 0
selected = candidates[chosen_index]
try:
gp = selected.golden_patch[0] if selected.golden_patch else None
if gp is None:
patch_info = PatchInfo(patch_content="", test_status="", reasoning="")
else:
patch_info = PatchInfo(
patch_content=gp.patch_content,
test_status=gp.test_status,
reasoning=gp.reasoning,
)
except Exception:
patch_info = PatchInfo(patch_content="", test_status="", reasoning="")
selector_result = SelectorResult(
instance_id=selected.instance_id,
generator_id=selected.generator_id,
image=selected.image,
success=True,
golden_patch=patch_info,
llm_usage=usage_stats,
tool_stats=tool_stats,
total_turns=total_turns,
select_reason=select_reason,
error=None,
)
return selector_result

254
src/managers/loop/types.py Normal file
View File

@@ -0,0 +1,254 @@
"""
This module defines the GeneratorResult data structure for patch generation results.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional
from src.tools.base import (
BASH_TOOL_NAME,
STR_REPLACE_BASED_EDIT_TOOL_NAME,
SEARCH_TOOL_NAME,
SUBMIT_RESULT_TOOL_NAME,
)
@dataclass
class LLMUsage:
"""LLM usage statistics."""
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0
def to_dict(self) -> Dict[str, int]:
"""Serialize LLMUsage to a plain dictionary."""
return {
"prompt_tokens": int(self.prompt_tokens),
"completion_tokens": int(self.completion_tokens),
"total_tokens": int(self.total_tokens),
}
@dataclass
class ToolStats:
"""Tool usage statistics per tool.
Each tool is represented by a small map with two fields:
- count: total invocation count
- failed: failed invocation count
"""
bash: Dict[str, int] = field(default_factory=lambda: {"count": 0, "failed": 0})
edit: Dict[str, int] = field(default_factory=lambda: {"count": 0, "failed": 0})
search: Dict[str, int] = field(default_factory=lambda: {"count": 0, "failed": 0})
submit_result: Dict[str, int] = field(default_factory=lambda: {"count": 0, "failed": 0})
def to_dict(self) -> Dict[str, Dict[str, int]]:
"""Serialize ToolStats to a plain dictionary."""
return {
BASH_TOOL_NAME: {"count": int(self.bash.get("count", 0)), "failed": int(self.bash.get("failed", 0))},
STR_REPLACE_BASED_EDIT_TOOL_NAME: {"count": int(self.edit.get("count", 0)), "failed": int(self.edit.get("failed", 0))},
SEARCH_TOOL_NAME: {"count": int(self.search.get("count", 0)), "failed": int(self.search.get("failed", 0))},
SUBMIT_RESULT_TOOL_NAME: {"count": int(self.submit_result.get("count", 0)), "failed": int(self.submit_result.get("failed", 0))},
}
@dataclass
class PatchInfo:
"""Information about a generated patch."""
patch_content: str
test_status: str
reasoning: str
@dataclass
class GeneratorResult:
"""Result from a patch generator."""
instance_id: str
generator_id: int
image: str
success: bool
golden_patch: List[
PatchInfo
]
llm_usage: LLMUsage
tool_stats: ToolStats
total_turns: int
error: Optional[str] = None
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "GeneratorResult":
"""Create GeneratorResult from dictionary."""
# Handle golden_patch conversion
golden_patch = []
if data.get("golden_patch"):
for patch_data in data["golden_patch"]:
if isinstance(patch_data, dict):
golden_patch.append(
PatchInfo(
patch_content=patch_data.get("patch_content", ""),
test_status=patch_data.get("test_status", ""),
reasoning=patch_data.get("reasoning", ""),
)
)
else:
# Legacy format: just patch content string
golden_patch.append(
PatchInfo(
patch_content=str(patch_data), test_status="", reasoning=""
)
)
# Handle LLM usage
llm_usage_data = data.get("llm_usage", {})
llm_usage = LLMUsage(
prompt_tokens=llm_usage_data.get("prompt_tokens", 0),
completion_tokens=llm_usage_data.get("completion_tokens", 0),
total_tokens=llm_usage_data.get("total_tokens", 0),
)
# Handle tool stats
tool_stats_data = data.get("tool_stats", {})
tool_stats = ToolStats(
bash=tool_stats_data.get(BASH_TOOL_NAME, 0),
edit=tool_stats_data.get(STR_REPLACE_BASED_EDIT_TOOL_NAME, 0),
search=tool_stats_data.get(SEARCH_TOOL_NAME, 0),
submit_result=tool_stats_data.get(SUBMIT_RESULT_TOOL_NAME, 0),
)
return cls(
instance_id=data.get("instance_id", ""),
generator_id=data.get("generator_id", 0),
image=data.get("image", ""),
success=data.get("success", False),
golden_patch=golden_patch,
llm_usage=llm_usage,
tool_stats=tool_stats,
total_turns=data.get("total_turns", 0),
error=data.get("error"),
)
def to_dict(self) -> Dict[str, Any]:
"""Convert GeneratorResult to dictionary."""
return {
"instance_id": self.instance_id,
"generator_id": self.generator_id,
"image": self.image,
"success": self.success,
"golden_patch": [
{
"patch_content": patch.patch_content,
"test_status": patch.test_status,
"reasoning": patch.reasoning,
}
for patch in self.golden_patch
],
"llm_usage": {
"prompt_tokens": self.llm_usage.prompt_tokens,
"completion_tokens": self.llm_usage.completion_tokens,
"total_tokens": self.llm_usage.total_tokens,
},
"tool_stats": {
BASH_TOOL_NAME: self.tool_stats.bash,
STR_REPLACE_BASED_EDIT_TOOL_NAME: self.tool_stats.edit,
SEARCH_TOOL_NAME: self.tool_stats.search,
SUBMIT_RESULT_TOOL_NAME: self.tool_stats.submit_result,
},
"total_turns": self.total_turns,
"error": self.error,
}
@dataclass
class SelectorResult:
"""Result from a patch selector.
"""
instance_id: str
generator_id: int
image: str
success: bool
golden_patch: PatchInfo
llm_usage: LLMUsage
tool_stats: ToolStats
total_turns: int
select_reason: str
error: Optional[str] = None
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SelectorResult":
"""Create SelectorResult from dictionary."""
gp_data = data.get("golden_patch", {})
if isinstance(gp_data, dict):
golden_patch = PatchInfo(
patch_content=gp_data.get("patch_content", ""),
test_status=gp_data.get("test_status", ""),
reasoning=gp_data.get("reasoning", ""),
)
else:
golden_patch = PatchInfo(
patch_content=str(gp_data) if gp_data is not None else "",
test_status="",
reasoning="",
)
# LLM usage
llm_usage_data = data.get("llm_usage", {})
llm_usage = LLMUsage(
prompt_tokens=llm_usage_data.get("prompt_tokens", 0),
completion_tokens=llm_usage_data.get("completion_tokens", 0),
total_tokens=llm_usage_data.get("total_tokens", 0),
)
# Tool stats
tool_stats_data = data.get("tool_stats", {})
tool_stats = ToolStats(
bash=tool_stats_data.get(BASH_TOOL_NAME, 0),
edit=tool_stats_data.get(STR_REPLACE_BASED_EDIT_TOOL_NAME, 0),
search=tool_stats_data.get(SEARCH_TOOL_NAME, 0),
submit_result=tool_stats_data.get(SUBMIT_RESULT_TOOL_NAME, 0),
)
return cls(
instance_id=data.get("instance_id", ""),
generator_id=data.get("generator_id", 0),
image=data.get("image", ""),
success=data.get("success", False),
golden_patch=golden_patch,
llm_usage=llm_usage,
tool_stats=tool_stats,
total_turns=data.get("total_turns", 0),
select_reason=data.get("select_reason", ""),
error=data.get("error"),
)
def to_dict(self) -> Dict[str, Any]:
"""Convert SelectorResult to dictionary."""
return {
"instance_id": self.instance_id,
"generator_id": self.generator_id,
"image": self.image,
"success": self.success,
"golden_patch": {
"patch_content": self.golden_patch.patch_content,
"test_status": self.golden_patch.test_status,
"reasoning": self.golden_patch.reasoning,
},
"llm_usage": {
"prompt_tokens": self.llm_usage.prompt_tokens,
"completion_tokens": self.llm_usage.completion_tokens,
"total_tokens": self.llm_usage.total_tokens,
},
"tool_stats": {
BASH_TOOL_NAME: self.tool_stats.bash,
STR_REPLACE_BASED_EDIT_TOOL_NAME: self.tool_stats.edit,
SEARCH_TOOL_NAME: self.tool_stats.search,
SUBMIT_RESULT_TOOL_NAME: self.tool_stats.submit_result,
},
"total_turns": self.total_turns,
"select_reason": self.select_reason,
"error": self.error,
}