Pause data generation for human review
This commit is contained in:
@@ -1117,6 +1117,39 @@ class LocalCodingAgent:
|
||||
)
|
||||
if history_entry is not None:
|
||||
file_history.append(history_entry)
|
||||
if tool_result.metadata.get('requires_user_review') is True:
|
||||
stream_events.append(
|
||||
{
|
||||
'type': 'user_review_required',
|
||||
'tool_name': tool_call.name,
|
||||
'tool_call_id': tool_call.id,
|
||||
'message_id': session.messages[tool_message_index].message_id,
|
||||
'metadata': dict(tool_result.metadata),
|
||||
}
|
||||
)
|
||||
result = AgentRunResult(
|
||||
final_output=self._build_user_review_required_output(tool_result),
|
||||
turns=turn_index + 1,
|
||||
tool_calls=tool_calls,
|
||||
transcript=session.transcript(),
|
||||
events=tuple(stream_events),
|
||||
usage=total_usage,
|
||||
total_cost_usd=total_cost_usd,
|
||||
stop_reason='user_review_required',
|
||||
file_history=tuple(file_history),
|
||||
session_id=session_id,
|
||||
scratchpad_directory=(
|
||||
str(scratchpad_directory) if scratchpad_directory is not None else None
|
||||
),
|
||||
)
|
||||
result = self._append_runtime_after_turn_events(
|
||||
result,
|
||||
prompt=effective_prompt,
|
||||
turn_index=turn_index,
|
||||
)
|
||||
result = self._persist_session(session, result)
|
||||
self.last_run_result = result
|
||||
return result
|
||||
|
||||
result = AgentRunResult(
|
||||
final_output=(
|
||||
@@ -1862,6 +1895,49 @@ class LocalCodingAgent:
|
||||
entry['history_kind'] = 'tool'
|
||||
return entry
|
||||
|
||||
def _build_user_review_required_output(self, tool_result: ToolExecutionResult) -> str:
|
||||
fallback = (
|
||||
'已生成待 review 的计划。本轮已暂停,请检查上面的计划;'
|
||||
'如果需要修改,请直接回复修改意见;如果认可,请回复“确认,开始生成”。'
|
||||
)
|
||||
try:
|
||||
payload = json.loads(tool_result.content)
|
||||
except json.JSONDecodeError:
|
||||
return fallback
|
||||
plan = payload.get('plan') if isinstance(payload, dict) else None
|
||||
if not isinstance(plan, dict):
|
||||
return fallback
|
||||
|
||||
lines = [
|
||||
'已生成待 review 的数据生成计划,本轮已暂停。',
|
||||
'',
|
||||
f"- plan_id: `{plan.get('plan_id', '')}`",
|
||||
f"- revision: `{plan.get('revision', '')}`",
|
||||
f"- dataset_label: {plan.get('dataset_label', '')}",
|
||||
]
|
||||
targets = plan.get('target_definitions')
|
||||
if isinstance(targets, list) and targets:
|
||||
lines.append('- target_definitions:')
|
||||
for item in targets:
|
||||
if isinstance(item, dict):
|
||||
lines.append(
|
||||
f" - {item.get('name', '')}: `{item.get('target', '')}`;规则:{item.get('rule', '')}"
|
||||
)
|
||||
elif plan.get('target'):
|
||||
lines.append(f"- target: `{plan.get('target')}`")
|
||||
lines.extend(
|
||||
[
|
||||
f"- 生成数量: {plan.get('total_count', '')}",
|
||||
f"- 单轮/多轮: {plan.get('turn_mix', '')}",
|
||||
f"- 覆盖范围: {plan.get('coverage', '')}",
|
||||
f"- 排除项: {plan.get('exclusions', '')}",
|
||||
f"- 落盘路径: {plan.get('output_path', '')}",
|
||||
'',
|
||||
'请 review 这份计划:需要修改就直接回复修改意见;认可的话回复“确认,开始生成”。',
|
||||
]
|
||||
)
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _compact_prefix_count(self, session: AgentSessionState) -> int:
|
||||
prefix_count = 0
|
||||
for message in session.messages:
|
||||
|
||||
@@ -274,6 +274,59 @@ class AgentRuntimeTests(unittest.TestCase):
|
||||
self.assertGreaterEqual(len(result.transcript), 5)
|
||||
self.assertGreaterEqual(len(result.file_history), 0)
|
||||
|
||||
def test_agent_stops_after_data_agent_plan_requires_review(self) -> None:
|
||||
responses = [
|
||||
{
|
||||
'choices': [
|
||||
{
|
||||
'message': {
|
||||
'role': 'assistant',
|
||||
'content': 'I will prepare a reviewable plan.',
|
||||
'tool_calls': [
|
||||
{
|
||||
'id': 'call_1',
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'data_agent_prepare_generation_plan',
|
||||
'arguments': json.dumps(
|
||||
{
|
||||
'dataset_label': '地图和生活边界数据',
|
||||
'target': 'Agent(tag="life_service")',
|
||||
'total_count': 2,
|
||||
'turn_mix': '1 条单轮,1 条多轮',
|
||||
'coverage': '附近吃喝玩乐',
|
||||
'exclusions': '不要生成导航路线类 query',
|
||||
'output_path': 'tasks/demo/artifacts',
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
'finish_reason': 'tool_calls',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
with patch('src.openai_compat.request.urlopen', side_effect=make_urlopen_side_effect(responses)):
|
||||
agent = LocalCodingAgent(
|
||||
model_config=ModelConfig(
|
||||
model='Qwen/Qwen3-Coder-30B-A3B-Instruct',
|
||||
base_url='http://127.0.0.1:8000/v1',
|
||||
),
|
||||
runtime_config=AgentRuntimeConfig(cwd=workspace),
|
||||
)
|
||||
result = agent.run('Prepare data generation plan')
|
||||
|
||||
self.assertEqual(result.stop_reason, 'user_review_required')
|
||||
self.assertEqual(result.tool_calls, 1)
|
||||
self.assertIn('本轮已暂停', result.final_output)
|
||||
self.assertTrue(
|
||||
any(event.get('type') == 'user_review_required' for event in result.events)
|
||||
)
|
||||
|
||||
def test_write_tool_is_blocked_without_permission(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config = AgentRuntimeConfig(cwd=Path(tmp_dir))
|
||||
|
||||
Reference in New Issue
Block a user